[030.2] Web Components Introduction

A quick primer on using Web Components with Elm

Subscribe now

Web Components Introduction [11.30.2016]

If you're not familiar with Web Components, you can think of them as a way to provide custom elements in HTML. Because they work just like regular elements, they provide a very nice way to have stateful UI components without having to wire up a lot of state management just to get them. You can think of elm-mdl here - I love it, but it is a bit verbose. Let's have a look at how Web Components can cut down on typing.

Project

I'm starting out with the project knewter/elm_web_components_playground tagged before this episode. This is a basic Elm application with an initial web components setup. I've included steps showing how I started it at the end of the episode's script.

Let's look at the index.html file and talk about a few things.

vim src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <meta content="width=device-width, initial-scale=1.0" name="viewport">
    <!-- here we pull in the webcomponents polyfill -->
    <script src="/bower_components/webcomponentsjs/webcomponents.js"></script>
    <!-- This part took me a while to figure out, found it on a ML thread with
         the help of Richard Feldman ultimately - basically, you need to use
         shadow DOM or else nested webcomponents don't work right in strange but
         awful ways. -->
    <script>
      window.Polymer = {
        dom: 'shadow'
      };
    </script>
    <!-- Then we pull in polymer with an import link -->
    <link rel="import" href="/bower_components/polymer/polymer.html">
    <title>Elm App</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

So that last thing - the import link tag - is the core of Web Components. I won't dig in too terribly deeply since I hope you did some reading up on them yesterday, but you can think of this sort of like a require statement in some other programming language. It pulls in this code and executes it, which ultimately gives us a web component.

Our app doesn't look terribly exciting yet. We can make some nice things happen pretty quickly though. Let's bring in the paper-input component - this is a Material Design component for input tags.

First, we'll install it from bower:

bower install paper-input --save

Next, we'll import it into our HTML file:

vim src/index.html
    <link rel="import" href="/bower_components/paper-input/paper-input.html">

From here, we can just use it. We'll open up our Elm app and add a node of type paper-input:

vim src/App.elm
module App exposing (..)

import Html exposing (Html, text, div, node)
-- ...
view : Model -> Html Msg
view model =
    div
        []
        [ text model.message
        , node "paper-input" [] []
        ]

If you go back to the browser, you can see we have an input field taking up the full width of its container. It's got some funky underline stuff happening, but otherwise it's kind of unexciting. Let's add a label to it, with an attribute:

module App exposing (..)
-- ...
import Html.Attributes exposing (attribute)
-- ...
view : Model -> Html Msg
view model =
    div
        []
        [ text model.message
        , node "paper-input"
            [ attribute "label" "Username" ]
            []
        ]

Now when we type into the label, we can see that it has the nice animated Material Design label movement that you'd expect. There are a lot more attributes that you can use from this component, but that's enough for now.

Next, let's add a nice button with a ripple. We'll bring in the component:

bower install paper-button --save
vim src/index.html
    <link rel="import" href="/bower_components/paper-button/paper-button.html">

And we'll add a button - we'll wrap it in a div just so it forces it to go to the next line:

vim src/App.elm
-- ...
view : Model -> Html Msg
view model =
    div
        []
        [ text model.message
        , node "paper-input"
            [ attribute "label" "Username" ]
            []
        -- add this
        , div
            []
            [ node "paper-button"
                []
                [ text "Clicky" ]
            ]
        ]

If you click the button, it has the nice ripple effect. We can set it to raised; this means we set an attribute 'raised' with no value - I always do this by setting an attribute with the same value as the attribute name itself, though there might be a better way I'm not yet privy to:

view : Model -> Html Msg
view model =
    div
        []
        [ text model.message
        , node "paper-input"
            [ attribute "label" "Username" ]
            []
        , div
            []
            [ node "paper-button"
                -- Add this attribute
                [ attribute "raised" "raised" ]
                [ text "Clicky" ]
            ]
        ]

That looks a little nicer. Finally, we can make it colored:

module App exposing (..)
-- ...
import Html.Attributes exposing (attribute, style)
-- ...
view : Model -> Html Msg
view model =
    div
        []
        [ text model.message
        , node "paper-input"
            [ attribute "label" "Username" ]
            []
        , div
            []
            [ node "paper-button"
                [ attribute "raised" "raised"
                -- We're just using normal CSS here
                , style
                    [ ( "background", "#1E88E5" )
                    , ( "color", "white" )
                    ]
                ]
                [ text "Clicky" ]
            ]
        ]

With that, we got some pretty good material design styles in just a few lines of code. We didn't have to add any new Msgs to our app and we didn't have to remember to setup any initialization or subscriptions. More importantly, we get to use the normal Elm Html functions, and everything just works like we'd expect.

Summary

In today's episode we saw a very rapid introduction to using Web Components in Elm. We get the niceties of Material Design with less hassle than using elm-mdl provides. Also, we now have access to a whole range of useful components out of the box that we can interact with easily - we've connected Elm to the entire Web Components ecosystem, which means we have more than just Elm developers helping build the components we use. This is a good thing!

What we don't have is type safety in terms of what options we pass to these components - in elm-mdl, we're safe from using components in a way they're not meant to be used, because the types save us. I think the best world might be using Web Components but wrapping them in some types gradually to help us use them well - that way we get the best of the outside world, but in an Elm-y embrace.

I hope you enjoyed it. See you soon!

Resources

Project Setup

create-elm-app elm_web_components_playground
cd elm_web_components_playground
elm-app eject
npm install --save-dev html-webpack-plugin
npm install
bower init .
bower install webcomponentsjs --save
bower install polymer --save
vim src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <meta content="width=device-width, initial-scale=1.0" name="viewport">
    <script src="/bower_components/webcomponentsjs/webcomponents.js"></script>
    <script>
      window.Polymer = {
        dom: 'shadow'
      };
    </script>
    <link rel="import" href="/bower_components/polymer/polymer.html">
    <title>Elm App</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
elm-app package install
elm-app start
# for some reason here I got this error:
#
#   Map.!: given key is not an element in the map
#
# This fixed it:
rm -fr elm-stuff
elm-app package install
elm-app start