- Web Components
Let’s build an app that shows the highest mountains in the world on a map to learn how to interact with custom elements from Elm. Here’s how the final app will look:
Installing Custom Elements
beginning-elm directory in terminal.
We’ll be using the
google-map custom element in our app, so let’s install it by running the following command from the
beginning-elm directory in terminal.
Once the installation is complete, you should see a new directory called
beginning-elm. That directory contains all the code necessary for the
google-map custom element to work.
Importing Custom Elements
We can load custom elements in our app by using the
<link> tag inside the
beginning-elm/index.html file. Replace the contents of that file with the following.
The first line inside
The next line imports the
google-map custom element.
The third line imports the
google-map-marker custom element. We’ll be using markers to show where the mountains are located on a map.
The code for
google-map-marker is located inside
Finally, we need to declare the height of the map inside the
By default the map’s height is set to zero pixels, so if you don’t specify a height you won’t see a map on the page.
Create a new file called
CustomElements.elm in the
beginning-elm/elm-examples directory and add the code below to it.
We imported a bunch of modules needed by our app. We then defined a model which contains the coordinates to center the maps on and a list of markers.
Defining Custom Element Nodes
The beautiful thing about custom elements is that someone else writes the necessary code and all we have to do to take advantage of their hard work is include the tag, such as
google-map, in our app. Just like that our app acquires all the super powers possessed by those custom elements.
In Elm a tag is nothing but a simple wrapper to the
Html.node function. If you peruse the
Html module’s source code, you’ll notice that all of the functions we have used so far from that module do nothing more than apply the
node function. For example, here is what the
input function’s implementation looks like:
Actually, the real code for those functions looks something like this:
Elm doesn’t require us to specify parameters when defining a function. That’s because Elm functions can be partially applied. Although the
node function actually takes three arguments, we don’t have to provide all of them when we apply it.
node "button" returns a partially applied function whose type is:
That’s why the
input functions and all other tag functions in
Html module have that type.
To use the custom elements inside an Elm app we need to convert them into DOM nodes using the
Html.node function. Add the following code to the bottom of
As you can see the implementation for
googleMapMarker look very similar to that of
Let’s use those custom tags to create our app’s view. Add the following code to the bottom of
view function displays a header and a map. The
googleMap node accepts a list of attributes and a list of children nodes. To specify a custom attribute we need to use the
attribute function defined in the
Html.Attributes module. Here’s how that function’s type looks:
The first argument represents the name of an attribute and the second argument represents the value for that attribute.
longitude attributes represent the coordinates to center the map on. The
drag-events attribute tells the map to notify us whenever the map is moved around. Finally
zoom sets the zoom level for the map. Many more attributes are available for customizing the map. You can find them here.
Markers are displayed as the map’s children nodes. The
googleMapMarker node used inside
viewMarker also takes a list of attributes and a list of children nodes. When a marker is clicked, its children nodes are displayed on a popup view like this:
There are no messages flowing through the app yet, so our
update function will look very simple. Add the following code to the bottom of
NoOp means “no operation.” It’s just a placeholder message for now. We’ll replace it with a real one later.
Next we need to create an initial model. Add the following code to the bottom of
initialModel uses various constructor functions generated by the
type alias definitions we added earlier.
Putting Everything Together
Let’s wire everything up by adding
main to the bottom of
With that, we’re ready to test. Run
elm-live from the
beginning-elm directory in terminal using the following command.
http://localhost:8000/ in a browser and you should see three markers plotted on a map.
When you click a marker, a popup view will display the image and title associated with that marker.
When a marker is clicked, the
google-map custom element automatically opens the popup view with children nodes. We don’t need to handle any events for that to happen.
Handling Events Generated by Custom Elements
We can ask a custom element to notify us when something interesting happens by handling one of the published events. For example the
google-map element generates more than a dozen events listed here. Let’s handle the event called
google-map-drag to understand how an Elm app receives event notifications from a custom element.
As the name suggests, the
google-map-drag event is generated whenever we drag the map around. This is a custom event, so the
Html.Events module doesn’t know anything about it. In the previous chapters, we used functions such as
onInput — defined in
Html.Events — to handle events generated by the
input elements respectively.
Html.Events doesn’t know about
google-map-drag, it lets us handle that event through the use of
on function which has the following type.
Add the following code right above the
view function in
onGoogleMapDrag uses the
on function to create a custom event handler. The first argument is the name of the event:
google-map-drag. The second argument is a JSON decoder. Whenever the map is dragged around,
google-map sends an event object to the Elm app. The structure of the event object looks something like this:
Unfortunately, as of this writing the official documentation for
google-map or any other custom element for that matter doesn’t specify the structure of the data included in an event object.
To figure out what data was contained inside the
google-map. I then used the code inside the last
<script> tag shown below to print the event object in the browser console. Hopefully, the documentation will include this detail in the future.
The code for decoding coordinates out of the event object JSON is quite simple.
We used the
requiredAt function from the
Json.Decode.Pipeline module to pull the coordinates out of a nested JSON object. The code in
onGoogleMapDrag, however, looks a bit strange.
Why are we using the
Json.Decode.map function instead of passing
coordinatesDecoder directly to the
on function like this:
The answer lies in the
on function’s type signature.
Our decoder returns the
Coordinates type, but the
on function expects a decoder that returns a message. We need a function that can convert
Coordinates into a message type.
Json.Decode.map is that function. Here’s how its type signature looks:
Json.Decode.map translates the
Coordinates type into
UpdateCenter which is a message we need to define. Replace
UpdateCenter in the
Now do the same in the
Next we need to add the
onGoogleMapDrag custom event handler to the
googleMap node in
The only thing remaining is to display the center coordinates when the map is being dragged. Add the following code right below the
viewCoordinates below the
googleMap node in
elm-live window in terminal to make sure there are no errors and go back to the page at
http://localhost:8000/. The coordinates displayed at the bottom of the page should change when you drag the map around.
Here is the entire code from
CustomElements.elm for your reference: