loading...

Talk: When and how to use Web Components with Elm

lukewestby profile image Luke Westby ・16 min read

I gave this talk at Elm Europe this year (2018) in order to solidify the existing knowledge about using Custom Elements as an acceptable JavaScript interop technique in Elm. (I also fulfilled what I know realize was a life-long dream of making a joke about Jeb Bush in a tech conference talk 🤪).

You can find the video below as well as a cleaned-up transcript of the talk with descriptions of slides included. All code and slide content can be found at https://github.com/lukewestby/elm-europe-2018.

If you are looking for speakers, and you like this talk and enjoy my speaking style feel free to reach out at lukewestby1@gmail.com!

Transcript

Slide 1: "Hello!"

I love these headset mics. I feel like I should be standing here with my
fingers steepled saying stuff like, you know, "We have machine learning... but do we have machine understanding?". Anyway, hello! Everybody's really excited for custom elements.

Slide 2: "I'm Luke"

I'm Luke.

Slide 3: Photograph of the pier in Manhattan Beach, CA

I came here from Manhattan Beach, California which is right near LeBron – excuse me – Los Angeles.

Slide 4: NoRedInk logo

I work at NoRedInk on the internal team. Right now I'm converting all of our Elm code to Elm 0.19. Very fun.

Slide 5: Ellie logo, NoRedInk logo, Elm logo

I also work on Ellie and contribute to Elm in other ways.

Slide 6: The sidebar of the Elm Slack, with the #beginners channel selected

One of the things that I do among those three three things is hang out in Slack and watch the #beginners channel.

Slide 7: The JavaScript logo and the Elm logo side-by-side

Something that frequently comes up as a question is: "I have this JavaScript library that I have to use but I want to build an Elm app. How do I do these things together?". There's a lot of reasons to need to do that. You can't do everything with Elm.

Slide 8: A screenshot of the list of all Web APIs on MDN

For instance, this is how many web APIs there are. That's not how many Elm packages there are. So can anybody in the audience who maybe has been in the Elm community for a while tell me how you might go about using one of these if it's not in the
Elm platform?

[Audience members respond "ports"]

Slide 9: Search bar in the Elm Slack, searching for "use a port"

Yeah. Use a port. It's kind of become almost a meme at this point in the community that the response to "How do I use JavaScript" is to use a port. It's kind of a
closed question almost.

Slide 10: A drawing of Pandora's Box being opened

So what I want to do in this in this talk is reopen it just a little bit. Probably the world won't end, but we'll see.

Slide 11: Ancient Aliens meme, with the text "Blockchain"

So...

This has nothing to do with anything. (Audience laughter). I forgot to take this slide out. I don't remember what it was for. There's a there's more... uh... let's see if I can work it in.

Slide 12: A Google Map showing a pin for the location of the conference

There are more reasons to to want to use JavaScript in the browser than making making calls to your growth-hacking AI blockchain client. Equally as often, or possibly more, you might want to render a map in case you forget where the conference is and you want to build an Elm program to remind you.

Slide 13: A screenshot of the text editor from https://ellie-app.com

Or maybe you want to build an Elm compiler into the into a browser platform and you need a text editor for people to share code and tell them where they can check out you mixtape.

Slide 14: "port module Main"

(Speaking quickly)

You can do this with ports. You can do anything with ports. It'd be very hard to get me on record saying that you can't do something with ports. If you've interacted with me online at all you know that to be true.

So the way that we would go about building, maybe, this...

Slide 15: A screen shot of Ellie's text editor again

... with ports is we would ...

Slide 16: A call to Html.div with an id attribute

... first make a div and we give it an ID like "text_editor"

Slide 17: Two ports for communicating between Elm and JS

And then make some ports. And we tag it with with some string that we can read in JavaScript and we'd send some data as a Value sometimes. We'd have a port coming in where we can tag some information with a string and pass it as a Value and deal with it in Elm in a subscription.

Slide 18: An init value that uses a port to send an "Init" message to JS

And then when we initialize our application we have to set up the initial model and also send out an "Init" command.

Slide 19: A port subscription in JS that sets up a CodeMirror instance when it receives a message tagged as "Init"

(Speaking even faster, for dramatic effect)

And then handle that in JavaScript, so we subscribe and we get the type and we do requestAnimationFrame so we can wait till the virtual DOM is finish rendering and then we init and then we look out the element and hopefully it's there. Who knows what we do if it's not. I certainly don't. Then we make the editor assuming the element is there and then we attach the editor to the element so we don't have to remake it every time.

Slide 20: Focus on CodeMirror creation code, with the addition of a listener to CodeMirror's changes event to forward changes back over a port to Elm

And then we listen for changes on the editor. we send those back in through the
port. We call it "ValueUpdated" as a string tag get the value from the editor.

Slide 21: A Msg type in Elm with a CodeChanged constructor to tag incoming strings from CodeMirror

Make a CodeChanged message so that we can capture that into our update function

Slide 22: A case in an update function that assigns the string from CodeChanged to a field on the model

Handle that message. Set the code in the model.

Slide 23: A subscription to textEditorIn port that decodes messages from CodeMirror

And then subscribe to textEditorIn. Lookup the type. See if it's "ValueUpdated". Decode the data. If it fails (shrugs shoulders). If it's something else (shrugs shoulders again). That works. That's fine.

Slide 24: The Msg type with a new constructor ResetCode for blanking out the editor

And then maybe also we want to do things like reset the code from within the Elm application if the user clicks a button. Ao we make a ResetCode message and then in our update function we reset the code in the model we send a message out through
textEditorOut.

Slide 25: Handling ResetCode in update to send an empty string over the textEditorOut port.

Give it the name "ValueUpdated". Pass it as a string.

Slide 26: Handling "ValueUpdated" in JavaScript by calling setValue on the CodeMirror instance

And in our subscription in JavaScript we look for the element again. Hopefully it's there. (Audience laughter) Look for the editor. Hopefully that's there. And then we set the data. And then we're ...

Slide 26: "DONE"

... done. (Audience laughter and applause).

Slide 27: Several copies of all of the code from the previous slides overlayed on top of one another like scattered sheets of paper.

What if we have more than one text editor in our application?

Slide 28: An example of using Html.textarea

If we're not writing code this is how we get text input from the user. It's just a text area and we use an HTML attribute to say that this is what the code is and we use onInput to handle the event from HTML. This is, like, a pretty good way to represent some visual compon– excuse me. Some special elements of our user interface that could be interacted with by the user.

Slide 29: A hypothetical code snippet where Html.codeEditor exists and is used similarly to Html.textarea

So wouldn't it be cool if we also had these. Like we could just say, like, give me a codeEditor element and then put these attributes on there let me listen for editor changes. Obviously these functions don't exist in the html package, but ...

Slide 30: "Folks"

... folks ...

Slide 31: "Turs out"

... turns out ...

Slide 32: "You can do this"

... you can do this. (Pretending as if an audience member is saying it:) WHAAAT?

Slide 33: "You can do this with just today's Elm and the Web Platform"

You can do it with just today's Elm and the web platform as it currently exists.

Slide 34: "RIGHT NOW"

Right now.

Slide 35: Classic "mind blown" GIF from Tim and Eric

So let's let's do it.

Slide 36: Hypothetical Html.codeEditor code again

Like I said, these functions don't exist in the html package

Slide 37: Replacing the hypothetical functions with their real analogues in Html.node and Html.Attributes.property

But these functions do. With Html.node, which lets us pick any tag name we want, and we have Html.Attributes.property which lets us assign any Json.Value to any JavaScript property of that element. Similarly, Html.Events.on lets us listen to any event.

Live Demo. All demo code is available at https://github.com/lukewestby/elm-europe-2018

So here's that same code in a text editor and here is that element rendered. So we can see we're assigning editorValue here. The value of that should be "hello world", according to our Elm program ... and it is. And then if we go back to the elements tab of Chrome's dev tools and take a look at our event listeners here we see that there's an editorChanged event. You can just do this. It's pretty cool.

So right now we have a nice grey rectangle which, as much as I'd love for that to be the way that my text editor looks some days, it's not super useful right now. So we actually have to explain to the browser how to deal with this. And the
way we do that is with the custom elements API. So we do customElements.define. So this was 'code-editor'. And we're gonna write a class that extends HTMLElement. Oh my gosh, VSCode, we get it.

So the one thing that we absolutely have to do to get started is write this connectedCallback method. This method is called by the custom elements API in the
browser whenever the element has been inserted into the DOM or it's been discovered in the DOM before this definition was defined. So it guarantees us that all the the DOM APIs are going to be ready for us by the time this code is executed. So we'll say this._editor = CodeMirror(this, ...) and we'll do some some options.

We'll do indentUnit: 4. We'll do mode and we'll do lineNumbers: true and we'll do value, hard-coded to "module Main exposing (..)" and we'll save it.
And WebPack will surely be very fast and we compile this for us (laughing). There it is. So there's ... there's ... (audience applause). Yeah! That's pretty cool. Thank you for applauding, but unfortunately that's not the text that we said we should have in our in our Elm program so it's not super useful yet.

So what we need to do is set up a property getter and setter to intercept
the virtual DOM's behavior of getting and setting this property. So the first thing that we will do is set up a constructor, and we have to call super because JavaScript says so. We make an _editorValue backing store. This should be familiar to anybody who's done a lot of C# programming. We put that
default value up in the constructor and we will set this to _editorValue and now
when we get, editoValue will return this._editorValue and when we set it
the real magic happens. So not only can we assign here, we can also check that
our editor exists and this time it's totally cool if it doesn't because we're
guaranteed for this method to run and we always know that the latest value that
we've set is going to be used. So that's fine.

So we can just return here and if it does exist we setValue. And then the last thing that I like to make sure to do is avoid doing any unnecessary work. So if the value is already equal to the thing that was set we're not going to do anything.
So now I'll save that and we'll stand here awkwardly with our arms folded while
that recompiles. (Brief silence). So France is cool. (Audience laughter).

There it is. "Hello world". You know what, you don't have to clap every time. I'll let you know when to clap after I'm finished writing. I'm a regular Jeb Bush. (Laughter from American audience members, I'm assuming).

Okay so we can type in here and CodeMirror is fine with that but as you can see no messages are being triggered. We have this whole elaborate listener set up but but
nothing coming in. So the way that we deal with this is to go down to where we
defined out editor, and if you'll remember from before when I was talking really
fast about ports we listen to on "changes" to find out when CodeMirror has changed.

And this time we will first sync our backing store with the CodeMirror instance and then we'll dispatch an event – this is a built-in function – with a new CustomEvent – this is a built-in constructor. And we said it was 'editorChanged'. I'll save it. I hope the syntax is right ... looks like it is. I think this is the last time we're gonna do this so that's fine. And now when I expose everything from Main we can see there's some messages there and it's filling in character by character.

(Audience applause).

That's how you use a custom element with Elm! When I figured out how to do this I was pretty psyched. I was like "hell yeah, dude"

Slide 38: "'Hell yeah, dude' – Me, to myself after figuring out how to do this"

Remember that. (Audience laughter).

Slide 39: "⚠️Some warnings ⚠️"

It's not all fun and games though. It's not all jokes. There's some things you need to be aware of when you're doing this, even though I totally recommend that you do.

Slide 40: "Warning #1 The Elm Virtual DOM is always right"

The first thing, warning number one, is that the Elm virtual DOM is always right. It's always going to have the last say about what's actually in the DOM. So, to illustrate my point,

Slide 41: A Maps.map view function with Maps.marker children

I when I teach this to people sometimes they ask, maybe they're coming from React, and they ask "If I have a Google Maps element can I put markers in the map as nested elements? That kind of looks the way that I think that should look." And I agree that it looks nice, like this is pretty cool.

Slide 42: What Elm needs the Maps.map rendered output to be vs. what Leaflet or Google Maps would need the DOM to be, displayed top vs. bottom.

The problem is the top code is what Elm thinks you're asking for when you when you do this and these markers represent elements this is what Elm thinks that you want it to be and when you render your map into the custom element this is what it's actually going to be. So you have this conflict here and the virtual DOM is always going to overwrite what's in there, that you've put in there kind of outside of its
knowledge.

Slide 43: "Shadow DOM"

So what you should do instead when you have situations like this is
either use the shadow DOM if your browser space supports it.

Slide 44: Caniuse stats for shadow DOM v1.

Unfortunately it probably doesn't and unfortunately this is not really possible to polyfill completely, so we have some waiting to do there.

Slide 45: Elm code to writing markers into the map with Html.Attriutes.property

Or what I recommend that you do is represent your markers, represent your
your nested data as like some other kind of structured data and encode it to JSON
and pass it as a property and interpret it in JavaScript. That works just as well.

Slide 46: "Warning #2 The Elm Virtual DOM reuses nodes"

Warning number two is that virtual DOM reuses nodes. And this is kind of weird
and it's hard to explain why it happens, but you'll know it when you see it and I'll give you an example so maybe if you go down this path and you encounter
something that's really weird you'll remember this and kind of have an intuitive understanding of what's going on.

Slide 47: Two dark blue rectangles, with one light blue rectangle inside of one of the dark blue ones

So let's say we have a custom element for rendering blue
rectangles. It's like really just incredibly useful custom element. It's very important. They can be light blue or dark blue, and the custom element decides. One
is inside of the other and what we want to do is remove the outer dark blue one
and have the light blue one fill the space.

Slide 48: The light blue rectangle has replaced the dark blue rectangle that contained it

So what we expect it to look like is this.

Slide 49: The light blue rectangle has vanished and the dark blue rectangles remain.

What's actually going to happen is this. And that's because the virtual DOM saw the outer rectangle and it's like "oh this is a rectangle I'm supposed
to put a rectangle here so I'm done." And it's wrong but it's not Elm's fault, it's mine for making a bad rectangle. So there's those tricks you can do and I really encourage you to avoid getting into the situation but it is good to be
aware of just in case.

Slide 50: "Warning #3 Don't use value"

And then warning number three,

Slide 51: Same as slide 50, but with small text

(Whispering) and this is this a small warning

Slide 52: Back to normal sized text

It's not very big deal. Warning number three is don't use value because the
Elm virtual DOM special cases this particular property name to deal with
built-in inputs.

Slide 53: "Recommendations: 1. Always use leaf nodes 2. Don't use value"

So here's what I recommend you do when you're building your custom elements. The first one is the most important and it's to always use leaf nodes whether your
custom element itself is at the very end of your tree branch or whether you've
got some stuff inside of it but that stuff is fixed. I'll give some examples of that. And the second one is to not use value for the reason I just mentioned.

Slide 54: A simplified version of the Html.node call to create the editor custom element in Ellie

So here is almost verbatim the the code editor view from from Ellie. It takes a
bunch of attributes and it renders them all and most importantly that last list
there is empty. Elm, when it sees an empty list from diff to diff, it doesn't clear anything out. It just skips it and says "I get it there's nothing here," even though we're lying. You can see the actual implementation of this in the
Ellie repo and I know this is a long URL and I'm gonna click pass it but I'll
post it in slack or something. (It's here: https://github.com/ellie-app/ellie/blob/master/assets/src/Ellie/Ui/CodeEditor.elm#L112-L114)

Slide 55: code from the NoRedInk codebase for using a custom element that grows and shrinks a textarea when the user types

And here is another example from the NoRedInk code base. This is in our UI library. We have this textarea that grows and shrinks as the user types and delete stuff and that's not what textareas usually do or are even capable of doing so you need JavaScript to do that. And so what we do is we wrap it in a custom element. The custom element goes and finds that textarea and modifies it in a way that the
Elm virtual DOM is like totally okay with. So since everything within the custom element is fixed and not really changing around this will survive the
diff. So you can you can do it that way too. You can't do dynamic stuff inside of, a dynamic list of elements inside of a custom element and feel totally secure about doing that so I recommend that you don't.

Slide 56: The JavaScript and Elm logos side-by-side

So that's how you do custom elements. That's how you avoid getting in trouble. And so this question of "how do I interoperate with JavaScript" I think has a more nuanced, slightly more nuanced answer now.

Slide 57: "UI widget? Custom Element!"

First ask, "Is the thing you want to do a UI widget? Is it a map or a date picker or a fancy input?" In that case use a custom element.

Slide 58: "Growth Hacking AI Blockchain client, etc? Use a port"

Otherwise if you have a growth hacking AI blockchain client or something similar to that, you use a port. And so with that I'll just leave you with something that's kind of a tradition at Elm conferences: to put a slide with Evan's face superimposed on an actor.

Slide 59: A diagram of the Actor Model, with Evan's face on top of elements of the diagram representing actors in the system.

So here's mine. (Audience laughter).

Slide 60: "lukewestby1@gmail.com", followed by links

Yeah you can email me about this or you can find me in slack and those are some some links that you should check out to learn about how to do this.

Thanks everybody (Audience applause).

Oh yeah, questions.

So I've actually done this with CodeMirror as well I came up with a very similar solution with custom elements but I had this problem that if you typed really fast in the CodeMirror editor like you do kind of day-to-day, often the letters kind of ended up in a different order or you dropped some characters. Kind of they got
overwritten by the Elm state management – overwrote the internal CodeMirror state management. Did you manage to work out how to how to stop that from happening?

Yes, so in the example that I gave I'm just dispatching events every time
the change event happens and in real life I debounce the code that dispatches the event on requestIdleCallback. So as long as you're typing or have typed recently it won't bother to trigger any changes. So by the time it actually sends the event into Elm the editor is like guaranteed to be settled.

Well that's awesome, thanks.

Yeah so you can find that in the Ellie codebase.

So what's the main advantage of using custom elements over mutation observers?

I think the the the instance management – I guess is what I would call
that. Like you don't have to manually watch the DOM and wait for those
elements to be discovered. You don't have to have a mutation observer, you don't have to write the mutation observer that then queries the DOM that's changed and
find all elements and then iterate over them, it just kind of does that for you. I
think most of the polyfills that exist are based on mutation observer so nothing
you can do with a custom element can't be done with a mutation observer. It's
just kind of nicer it's a nicer API.

How do you manage Firefox> do you use a polyfill?

Actually the second link here is a repo for like the official polyfill. It works very well in Firefox. We use it in production at NoRedInk and I use it in production on Ellie and we haven't had any problems with Firefox at all. And I think actually they're like about to release support like in the next version so that's very cool.

All right I think that's it I think and you can come find me wherever I'll be happy to help.

Posted on Aug 23 '18 by:

lukewestby profile

Luke Westby

@lukewestby

Sunrise Movement website team. Elm core contributor. elm-conf organizing team alumnus.

Discussion

markdown guide
 

I really enjoyed watching this. Never used Elm, and it's great to see how much utility you're getting out of custom elements.