├── .gitignore ├── LICENSE ├── README.md ├── internationalization ├── README.md ├── demo.gif ├── elm.json ├── index.html └── src │ └── Main.elm ├── localStorage ├── README.md ├── elm.json ├── index.html └── src │ └── Main.elm ├── more ├── README.md └── webcomponents │ ├── README.md │ └── minimal-es5-setup.html └── websockets ├── README.md ├── demo.gif ├── elm.json ├── index.html └── src └── Main.elm /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | elm-stuff 4 | elm.js 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Elm Community 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Using JS within Elm 2 | 3 | Elm can interact with JavaScript in three ways: 4 | 5 | - [flags](https://guide.elm-lang.org/interop/flags.html) 6 | - [ports](https://guide.elm-lang.org/interop/ports.html) 7 | - [custom elements](https://guide.elm-lang.org/interop/custom_elements.html) 8 | 9 | Not all browser APIs are covered by an official package yet, so if you are evaluating using Elm in your company, definitely browse through the examples here to get familiar with flags, ports, and custom elements to make sure these interop mechanisms will fully meet your needs. It may be safest to circle back to Elm later if not! 10 | 11 | 12 | ## Ports 13 | 14 | - [localStorage](/localStorage) — [demo](https://ellie-app.com/8yYddD6HRYJa1) 15 | - [WebSockets](/websockets) — [demo](https://ellie-app.com/8yYgw7y7sM2a1) 16 | 17 | 18 | ## Custom Elements 19 | 20 | - [Internationalization](/internationalization) — [demo](https://ellie-app.com/8yYbRQ3Hzrta1) 21 | - [Pie Chart Widget](https://ellie-app.com/8B2B8fWbvZwa1) 22 | - [Calendar Widget](https://ellie-app.com/8B8D2Q3WLh7a1) 23 | - [Project Fluent](https://github.com/wolfadex/fluent-web/) 24 | 25 | 26 | ## Do you want to know more? 27 | 28 | The top-level examples presented here are intentionally boiled down to a minimal setup for you to understand the basic ideas and get started quickly. As the web platform is a place with a lot of history and odd API corners there are more involved examples and tutorials to be explored in the [more](/more) section. 29 | 30 | * [Everything you need to know to use WebComponents in your Elm app](/more/webcomponents) 31 | -------------------------------------------------------------------------------- /internationalization/README.md: -------------------------------------------------------------------------------- 1 | # Internationalization - [Live Demo](https://ellie-app.com/8yYbRQ3Hzrta1) 2 | 3 | This is a minimal example of how to use the `Intl` library with a custom element. 4 | 5 | ![Demo](demo.gif) 6 | 7 | The important code lives in `src/Main.elm` and in `index.html` with comments! 8 | 9 | Check out [`wolfadex/fluent-web`](https://github.com/wolfadex/fluent-web/) for a more complete approach, making [Project Fluent](https://projectfluent.org/) available through custom elements. 10 | 11 | 12 | ## Building Locally 13 | 14 | Run the following commands: 15 | 16 | ```bash 17 | git clone https://github.com/elm-community/js-integration-examples.git 18 | cd js-integration-examples/internationalization 19 | 20 | elm make src/Main.elm --output=elm.js 21 | open index.html 22 | ``` 23 | 24 | Some terminals may not have an `open` command, in which case you should open the index.html file in your browser another way. 25 | -------------------------------------------------------------------------------- /internationalization/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-community/js-integration-examples/3952f9440d3f144b99b2d9bc6b39938d1517c976/internationalization/demo.gif -------------------------------------------------------------------------------- /internationalization/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0", 12 | "elm/json": "1.1.3" 13 | }, 14 | "indirect": { 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.2" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": {}, 22 | "indirect": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internationalization/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elm + Intl 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /internationalization/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | import Browser 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Json.Decode as D 8 | 9 | 10 | 11 | -- MAIN 12 | 13 | 14 | main : Program () Model Msg 15 | main = 16 | Browser.element 17 | { init = init 18 | , view = view 19 | , update = update 20 | , subscriptions = \_ -> Sub.none 21 | } 22 | 23 | 24 | 25 | -- MODEL 26 | 27 | 28 | type alias Model = 29 | { language : String 30 | } 31 | 32 | 33 | init : () -> ( Model, Cmd Msg ) 34 | init _ = 35 | ( { language = "sr-RS" } 36 | , Cmd.none 37 | ) 38 | 39 | 40 | 41 | -- UPDATE 42 | 43 | 44 | type Msg 45 | = LanguageChanged String 46 | 47 | 48 | update : Msg -> Model -> ( Model, Cmd Msg ) 49 | update msg model = 50 | case msg of 51 | LanguageChanged language -> 52 | ( { model | language = language } 53 | , Cmd.none 54 | ) 55 | 56 | 57 | 58 | -- VIEW 59 | 60 | 61 | view : Model -> Html Msg 62 | view model = 63 | div [] 64 | [ p [] [ viewDate model.language 2012 5 ] 65 | , select 66 | [ on "change" (D.map LanguageChanged valueDecoder) 67 | ] 68 | [ option [ value "sr-RS" ] [ text "sr-RS" ] 69 | , option [ value "en-GB" ] [ text "en-GB" ] 70 | , option [ value "en-US" ] [ text "en-US" ] 71 | ] 72 | ] 73 | 74 | 75 | -- Use the Custom Element defined in index.html 76 | -- 77 | viewDate : String -> Int -> Int -> Html msg 78 | viewDate lang year month = 79 | node "intl-date" 80 | [ attribute "lang" lang 81 | , attribute "year" (String.fromInt year) 82 | , attribute "month" (String.fromInt month) 83 | ] 84 | [] 85 | 86 | 87 | valueDecoder : D.Decoder String 88 | valueDecoder = 89 | D.field "currentTarget" (D.field "value" D.string) 90 | -------------------------------------------------------------------------------- /localStorage/README.md: -------------------------------------------------------------------------------- 1 | # Local Storage - [Live Demo](https://ellie-app.com/8yYddD6HRYJa1) 2 | 3 | This is a minimal example of how to use `localStorage` through ports. 4 | 5 | It remembers user data across sessions. This data may be lost if the user clears their cookies, so it is safest to think of this as a **cache** rather than normal storage. 6 | 7 | Anyway, the important code lives in `src/Main.elm` and in `index.html` with comments! 8 | 9 | 10 | ## Building Locally 11 | 12 | Run the following commands: 13 | 14 | ```bash 15 | git clone https://github.com/elm-community/js-integration-examples.git 16 | cd js-integration-examples/localStorage 17 | 18 | elm make src/Main.elm --output=elm.js 19 | open index.html 20 | ``` 21 | 22 | Some terminals may not have an `open` command, in which case you should open the index.html file in your browser another way. 23 | -------------------------------------------------------------------------------- /localStorage/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0", 12 | "elm/json": "1.1.3" 13 | }, 14 | "indirect": { 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.2" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": {}, 22 | "indirect": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /localStorage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elm + localStorage 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /localStorage/src/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (..) 2 | 3 | import Browser 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Json.Decode as D 8 | import Json.Encode as E 9 | 10 | 11 | 12 | -- MAIN 13 | 14 | 15 | main : Program E.Value Model Msg 16 | main = 17 | Browser.element 18 | { init = init 19 | , view = view 20 | , update = updateWithStorage 21 | , subscriptions = \_ -> Sub.none 22 | } 23 | 24 | 25 | 26 | -- MODEL 27 | 28 | 29 | type alias Model = 30 | { name : String 31 | , email : String 32 | } 33 | 34 | 35 | -- Here we use "flags" to load information in from localStorage. The 36 | -- data comes in as a JS value, so we define a `decoder` at the bottom 37 | -- of this file to turn it into an Elm value. 38 | -- 39 | -- Check out index.html to see the corresponding code on the JS side. 40 | -- 41 | init : E.Value -> ( Model, Cmd Msg ) 42 | init flags = 43 | ( 44 | case D.decodeValue decoder flags of 45 | Ok model -> model 46 | Err _ -> { name = "", email = "" } 47 | , 48 | Cmd.none 49 | ) 50 | 51 | 52 | 53 | -- UPDATE 54 | 55 | 56 | type Msg 57 | = NameChanged String 58 | | EmailChanged String 59 | 60 | 61 | update : Msg -> Model -> ( Model, Cmd Msg ) 62 | update msg model = 63 | case msg of 64 | NameChanged name -> 65 | ( { model | name = name } 66 | , Cmd.none 67 | ) 68 | 69 | EmailChanged email -> 70 | ( { model | email = email } 71 | , Cmd.none 72 | ) 73 | 74 | 75 | 76 | -- VIEW 77 | 78 | 79 | view : Model -> Html Msg 80 | view model = 81 | div [] 82 | [ input 83 | [ type_ "text" 84 | , placeholder "Name" 85 | , onInput NameChanged 86 | , value model.name 87 | ] 88 | [] 89 | , input 90 | [ type_ "text" 91 | , placeholder "Email" 92 | , onInput EmailChanged 93 | , value model.email 94 | ] 95 | [] 96 | ] 97 | 98 | 99 | 100 | -- PORTS 101 | 102 | 103 | port setStorage : E.Value -> Cmd msg 104 | 105 | 106 | -- We want to `setStorage` on every update, so this function adds 107 | -- the setStorage command on each step of the update function. 108 | -- 109 | -- Check out index.html to see how this is handled on the JS side. 110 | -- 111 | updateWithStorage : Msg -> Model -> ( Model, Cmd Msg ) 112 | updateWithStorage msg oldModel = 113 | let 114 | ( newModel, cmds ) = update msg oldModel 115 | in 116 | ( newModel 117 | , Cmd.batch [ setStorage (encode newModel), cmds ] 118 | ) 119 | 120 | 121 | 122 | -- JSON ENCODE/DECODE 123 | 124 | 125 | encode : Model -> E.Value 126 | encode model = 127 | E.object 128 | [ ("name", E.string model.name) 129 | , ("email", E.string model.email) 130 | ] 131 | 132 | 133 | decoder : D.Decoder Model 134 | decoder = 135 | D.map2 Model 136 | (D.field "name" D.string) 137 | (D.field "email" D.string) 138 | -------------------------------------------------------------------------------- /more/README.md: -------------------------------------------------------------------------------- 1 | # More JavaScript Integration Examples 2 | 3 | This section provides more involved interop examples as well as tutorials around JavaScript usage in conjunction with Elm. 4 | 5 | * [Everything you need to know to use WebComponents in your Elm app](webcomponents/README.md) 6 | -------------------------------------------------------------------------------- /more/webcomponents/README.md: -------------------------------------------------------------------------------- 1 | # A Guide to Using Elm With Webcomponents 2 | 3 | This document is meant to be a practical guide to the somewhat confusing [web components specs][wc-specs]. 4 | 5 | Most documentation on web components show the spec and basic usage examples, assuming that you already know the intricate details of why specific parts of the spec exist in the first place and what use case they try to solve. This situation can be daunting even for experienced web developers and even more so for newcomers. 6 | 7 | We try to be as concise as possible, while also providing enough information to get you up and running. 8 | 9 | ## TL;DR 10 | If you are looking for a quick start with Elm, Webcomponents and the setup of choice have a look at 11 | * [web components setup](#web-components-setup) 12 | * [the browser support section](#Browser-Support) 13 | * don't forget to check [the gotchas section](#Gotchas) to learn how to build Webcomponents that play nice with Elm. 14 | 15 | ## Prerequisites 16 | 17 | First-off: if you haven't read [the official Elm guide][guide] you should do so before reading on, of particular note is [the interop section][guide-interop] as this is where the usage of [ports][guide-ports] and [custom elements][guide-custom-elements] is motivated. 18 | 19 | Now that you're up to date and positive that using web components is the way to solve the problem at hand we'll start off with a quick summary of what web components are, followed by a rundown of the parts of [the spec][wc-specs] you'll most likely interact with when using Elm. 20 | 21 | The remainder of the guide is dedicated to getting your app ready for web components in all the browsers you want/need to support. 22 | 23 | ## What are web components and what can I do with them? 24 | 25 | You might have heard or read "just use web components" as an answer to the question how to integrate a particular JavaScript API or external library with Elm. 26 | 27 | To quote the first paragraph on the [web components specs page][wc-specs] 28 | 29 | > These four specifications can __be used on their own__ but combined allow developers to define their own tags (custom element), whose styles are encapsulated and isolated (shadow dom), that can be restamped many times (template), and have a consistent way of being integrated into applications (es module). 30 | 31 | I've emphasized the most important information: in order to "use web components" you can and should actually pick and choose what part of the spec you need in order to solve your problem. 32 | 33 | * You don't need to use Shadow DOM, if you don't need style encapsulation. 34 | * You don't need to use HTML templates, if you already have a templating solution. 35 | * You don't need ES Modules, if you have a compile-to-js language like, say, *drumroll* Elm. 36 | 37 | You may find yourself using all of these specs but they aren't usually necessary in conjuction with Elm. If you want to dive deeper into Webcomponents in plain JavaScript the [MDN page about Webcomponents][mdn-wc] is a good place to start. 38 | 39 | 40 | ## Web Components Setup 41 | 42 | At the time of writing this article you're probably using some form of build system for your JavaScript assets, by choice, custom or force. A detailed assessment of bundling modern web apps is outside the scope of this guide but you can check out [our minimal ES5 compatible setup](minimal-es5-setup.html) that provides the polyfills necessary to use web components with older browsers like Internet Explorer 11. 43 | 44 | There is more information available in the [browser support section](#browser-support). 45 | 46 | 47 | ## Custom Elements And Elm 48 | 49 | Note that if at this point you skipped the earlier requests to have a look at the [interop section of the Elm guide][guide-interop] it's a good idea to do so now. 50 | 51 | So we've looked at [the specs][wc-specs] and the most relevant one to Elm is arguably [Custom Elements][wc-custom-elements]. Looking at the examples on that page, defining a custom element is easy enough. 52 | 53 | ```javascript 54 | class MyElement extends HTMLElement {} 55 | customElements.define("my-element", MyElement); 56 | ``` 57 | 58 | This defines a new HTML element ``. Note that 59 | * the name *has* to include a hyphen and 60 | * the class *needs* to extend `HTMLElement`. 61 | 62 | This is what custom elements are about: they let you build your own HTML elements with behavior tailored to your application that are indistinguishable from built-in elements like `` or `
`. Which in turn means we can create these kind of elements within Elm without problems. 63 | 64 | ```elm 65 | import Html 66 | 67 | element = 68 | Html.node "my-element" [] [ Html.text "Awesome!" ] 69 | ``` 70 | 71 | Let's have a look at the anatomy of a custom element. Note that this only covers the part of the API that is most relevant to Elm, we provide links to associated concepts where appropriate. 72 | 73 | ### Construction ([demo](https://ellie-app.com/8Vw6BbYYpc4a1)) 74 | 75 | A custom element, just like any other built-in element, can be created declaratively using HTML or imperatively using JavaScript. 76 | 77 | ```javascript 78 | customElements.define("my-element", class extends HTMLElement {}); 79 | 80 | const element = document.createElement("my-element"); 81 | ``` 82 | ```html 83 | 84 | 85 | ``` 86 | ```elm 87 | import Html 88 | 89 | myElement = 90 | Html.node "my-element" [] [] 91 | ``` 92 | 93 | ### Lifecycles ([demo](https://ellie-app.com/8Vw7J3nFNNma1)) 94 | 95 | There are lifecycles you can attach clunkily-named callbacks to. 96 | 97 | ```javascript 98 | customElements.define("i-support-lifecycles", class extends HTMLElement { 99 | constructor() { 100 | super(); 101 | // This is being initialized, it's not been 102 | // added to any document yet but you can initialize your fields but 103 | // don't temper with the DOM just yet, do that in `connectedCallback` 104 | } 105 | adoptedCallback() { 106 | // This has been moved to a different document 107 | } 108 | connectedCallback() { 109 | // This has been added to the DOM 110 | } 111 | disconnectedCallback() { 112 | // This has been removed from the DOM 113 | } 114 | }); 115 | ``` 116 | 117 | ### Attributes ([demo](https://ellie-app.com/8Vwfz6c5v2wa1)) 118 | 119 | Custom elements may declare supported attributes via `observedAttributes` - only attribute names returned from this trigger the `attributeChangedCallback` when changed. Note that attributes can only carry `string` values. 120 | 121 | There's also a [discussion on whether to use an attribute or a property](#attributes-vs-properties), if you're not sure which to use. 122 | 123 | ```javascript 124 | customElements.define("twbs-alert", class extends HTMLElement { 125 | static get observedAttributes() { 126 | // We need to declare which attributes should be observed, 127 | // only these trigger the `attributeChangedCallback` 128 | return ['type']; 129 | } 130 | connectedCallback() { 131 | this.classList.add('alert'); 132 | } 133 | attributeChangedCallback(name, oldValue, newValue) { 134 | switch (name) { 135 | case 'type': 136 | this.classList.remove(`alert-${oldValue}`); 137 | this.classList.add(`alert-${newValue}`); 138 | break; 139 | } 140 | } 141 | }); 142 | 143 | const element = document.createElement("twbs-alert"); 144 | element.setAttribute("type", "info"); 145 | ``` 146 | ```html 147 | 148 | ``` 149 | ```elm 150 | import Html 151 | import Html.Attributes 152 | 153 | alert = 154 | Html.node "twbs-alert" 155 | [ Html.Attributes.attribute "type" "info" 156 | -- or alternatively Html.Attributes.type_ "info" 157 | ] 158 | [ Html.text "This is a Twitter Bootstrap info box" 159 | ] 160 | ``` 161 | 162 | If you need to transfer object data you can use a [property](#Properties). 163 | 164 | ### Properties ([demo](https://ellie-app.com/8VwjNrnhyKKa1)) 165 | 166 | Custom elements can declare properties via `get` and `set`, most kinds of JavaScript objects are supported. 167 | 168 | There's also a [discussion on whether to use an attribute or a property](#attributes-vs-properties), if you're not sure which to use. 169 | 170 | ```javascript 171 | customElements.define("atla-trivia", class extends HTMLElement { 172 | constructor() { 173 | super(); 174 | this._meta = null; 175 | } 176 | set meta(value) { 177 | this._meta = value; 178 | } 179 | get meta() { 180 | return this._meta; 181 | } 182 | }); 183 | 184 | const element = document.createElement("atla-trivia"); 185 | element.meta = { 186 | teamAvatar: ["Aang", "Katara", "Soka"], 187 | seasons: 3, 188 | }; 189 | ``` 190 | ```html 191 | 192 | ``` 193 | 194 | With Elm you need to use a JSON encoder provided by the [`elm/json`][elmpkg-elm-json] package. 195 | 196 | ```elm 197 | import Html 198 | import Html.Attributes 199 | import Json.Encode -- elm install elm/json 200 | 201 | trivia = 202 | Html.node "atla-trivia" 203 | [ Html.Attributes.property "meta" 204 | (Json.Encode.object 205 | [ ( "teamAvatar" 206 | , Json.Encode.list Json.Encode.string 207 | [ "Aang" 208 | , "Katara" 209 | , "Soka" 210 | ] 211 | ) 212 | , ( "seasons", Json.Encode.int 3 ) 213 | ] 214 | ) 215 | ] 216 | [] 217 | ``` 218 | 219 | ### Attributes vs Properties 220 | For Elm projects a good rule of thumb is 221 | 222 | > Use properties unless you want your custom elements to be used from hand-written or server-rendered HTML. 223 | 224 | The reasoning being 225 | * You're interacting with your custom element via JavaScript anyways, so the fact that properties can not be set from raw HTML is usually not an issue 226 | * You can transfer structured data via properties, not just strings 227 | * It's easier to use a consistent interaction method with custom elements from Elm - just use `Html.Attributes.property` everywhere 228 | 229 | On the other hand writing custom elements using only attributes might be more suitable for your use case as they can easily be included in static HTML, hand-written or produced by server-side-rendering. 230 | 231 | 232 | ### Children ([demo](https://ellie-app.com/8VwmHKFMYCqa1)) 233 | As we've noted a number of times: custom elements are just like regular HTML elements, this includes the ability to be a root node for a sub-tree, your custom element can have child nodes. 234 | 235 | ```javascript 236 | customElements.define("tree-root", class extends HTMLElement {}); 237 | 238 | const root = document.createElement("tree-root"); 239 | const span = document.createElement("span"); 240 | span.innerText = "A span"; 241 | const div = document.createElement("div"); 242 | div.innerText = "A div"; 243 | const plainText = document.createTextNode("Plain text"); 244 | 245 | root.appendChild(span); 246 | root.appendChild(div); 247 | root.appendChild(plainText); 248 | ``` 249 | ```html 250 | 251 | A span 252 |
A div
253 | Plain text 254 |
255 | ``` 256 | 257 | This is equivalent to the following Elm code. Be sure to read up on [the gotchas](#Gotchas) due to Elm's virtual DOM, though. 258 | 259 | ```elm 260 | import Html 261 | 262 | subTree = 263 | Html.node "tree-root" [] 264 | [ Html.span [] [ Html.text "A span" ] 265 | , Html.div [] [ Html.text "A div" ] 266 | , Html.text "Plain Text" 267 | ] 268 | ``` 269 | 270 | ### Listening to Events ([demo](https://ellie-app.com/8Vwpg8T5GDQa1)) 271 | Custom elements support listening to events; this is usually not that useful in conjunction with Elm since you can't imperatively trigger events with it. However, it allows you to employ some nifty tricks like [event delegation][jq-event-delegation] where you use the [DOM's event bubbling phase][mdn-event-bubbling] to listen for events that "bubble up" from your custom element's children. 272 | 273 | ```javascript 274 | customElements.define("event-delegator", class extends HTMLElement { 275 | _handleInnerClick(evt) { 276 | evt.preventDefault(); 277 | evt.stopPropagation(); 278 | alert(`You clicked inside of me`); 279 | } 280 | connectedCallback() { 281 | this.addEventListener("click", this._handleInnerClick) 282 | } 283 | disconnectedCallback() { 284 | this.removeEventListener("click", this._handleInnerClick) 285 | } 286 | }); 287 | 288 | const element = document.createElement("event-delegator"); 289 | const button = document.createElement("button"); 290 | button.innerHTML = "Click Me!"; 291 | 292 | element.appendChild(button); 293 | document.body.appendChild(element); 294 | ``` 295 | ```html 296 | 297 | 298 | 299 | ``` 300 | 301 | As we've seen in [the Children section](#Children) building DOM trees with Elm is a breeze. In this example we see both the power and the potential problems with using custom elements in Elm, they allow you to execute arbitrary JavaScript inside your declarative views. So be aware of the fact that a rogue custom element can compromise Elm's runtime guarantees, have a look at [the Gotchas section](#Gotchas) to learn more. 302 | 303 | ```elm 304 | import Html 305 | 306 | root = 307 | Html.node "event-delegator" [] 308 | [ Html.button [ {- no `onClick` here -} ] 309 | [ Html.text "Click Me!" 310 | ] 311 | ] 312 | ``` 313 | 314 | 315 | ### Triggering Events ([demo](https://ellie-app.com/8VvL6ggT5qJa1)) 316 | 317 | Custom elements [can listen to events](#Listening-to-Events) but they become really useful as soon as they're triggering events themselves. You mainly want to use this as an adapter to give Elm access to [Web APIs][html5-apis] it does not yet support in form of a core package or to embed functionality from external JavaScript libraries. 318 | 319 | To demonstrate this we build a slightly more involved custom element `` that lets the user copy text from an Elm app via button click using the [Document.execCommand API][doc-exec-command]. This is a fairly old non-standard API that's widely supported, nonetheless. The [Clipboard API][mdn-clipboard] is the modern successor, in case you don't need support for older browsers. 320 | 321 | The gist is that our element listens for `click` events from its children, copies the value of its `text` attribute to the clipboard and triggers a [`CustomEvent`][mdn-customevent] notifying Elm that the operation has been successful, Elm can also decode event data being passed. 322 | 323 | ```javascript 324 | customElements.define("copy-to-clipboard", class extends HTMLElement { 325 | static get observedAttributes() { 326 | return ["text"]; 327 | } 328 | _handleClick(evt) { 329 | evt.preventDefault(); 330 | evt.stopPropagation(); 331 | const text = this.getAttribute("text"); 332 | this._copy(text); 333 | this.dispatchEvent(new CustomEvent("clipboard", { 334 | bubbles: true, 335 | cancelable: true, 336 | detail: { 337 | copiedText: text, 338 | }, 339 | })); 340 | } 341 | _copy(value) { 342 | const preSelected = 343 | document.getSelection().rangeCount > 0 344 | ? document.getSelection().getRangeAt(0) 345 | : false; 346 | 347 | const textarea = document.createElement('textarea'); 348 | textarea.setAttribute('readonly', ''); 349 | textarea.style.position = 'absolute'; 350 | textarea.style.left = '-9999px'; 351 | textarea.value = value; 352 | document.body.appendChild(textarea); 353 | 354 | textarea.select(); 355 | document.execCommand('copy'); 356 | document.body.removeChild(textarea); 357 | if (preSelected) { 358 | document.getSelection().removeAllRanges(); 359 | document.getSelection().addRange(preSelected); 360 | } 361 | } 362 | connectedCallback() { 363 | this.addEventListener("click", this._handleClick); 364 | } 365 | disconnectedCallback() { 366 | this.removeEventListener("click", this._handleClick); 367 | } 368 | }); 369 | ``` 370 | ```elm 371 | module Main exposing (main) 372 | 373 | import Browser 374 | import Html exposing (Html) 375 | import Html.Attributes 376 | import Html.Events 377 | import Json.Decode exposing (Decoder) 378 | 379 | 380 | type Msg 381 | = CopiedToClipboard String 382 | 383 | 384 | type alias Model = 385 | { copied : Maybe String 386 | } 387 | 388 | 389 | clipboardEventDecoder : (String -> msg) -> Decoder msg 390 | clipboardEventDecoder toMsg = 391 | Json.Decode.map (\copiedTextFromDetail -> toMsg copiedTextFromDetail) 392 | (Json.Decode.at [ "detail", "copiedText" ] Json.Decode.string) 393 | 394 | 395 | view : Model -> Html Msg 396 | view { copied } = 397 | let 398 | textToCopy = 399 | "Text from Elm" 400 | in 401 | Html.div [] 402 | [ Html.node "copy-to-clipboard" 403 | [ Html.Attributes.attribute "text" textToCopy 404 | , Html.Events.on "clipboard" (clipboardEventDecoder CopiedToClipboard) 405 | ] 406 | [ Html.button [] 407 | [ Html.text "Copy " 408 | , Html.text ("\"" ++ textToCopy ++ "\"") 409 | , Html.text " to clipboard" 410 | ] 411 | ] 412 | , case copied of 413 | Just _ -> 414 | Html.div [] 415 | [ Html.div [] [ Html.text "Copied!" ] 416 | , Html.textarea 417 | [ Html.Attributes.placeholder "Try pasting it in here" 418 | ] 419 | [] 420 | ] 421 | 422 | Nothing -> 423 | Html.text "" 424 | ] 425 | 426 | 427 | update : Msg -> Model -> Model 428 | update (CopiedToClipboard text) model = 429 | { model | copied = Just text } 430 | 431 | 432 | main : Program () Model Msg 433 | main = 434 | Browser.sandbox 435 | { init = { copied = Nothing } 436 | , update = update 437 | , view = view 438 | } 439 | 440 | ``` 441 | 442 | _Note that Internet Explorer needs [a polyfill for CustomEvent][mdn-customevent-polyfill]._ 443 | 444 | Many Elm apps use this technique to embed libraries like [CodeMirror](https://github.com/ellie-app/ellie/blob/a45637b81e2495ffada12f9a75dd6bb547a69226/assets/src/Ellie/Ui/CodeEditor.js) or [Google Maps](https://package.elm-lang.org/packages/PaackEng/elm-google-maps/latest/). 445 | 446 | Until now all seems hunky-dory in the world of custom elements being embedded with Elm but there are some [gotchas](#Gotchas) you need to be aware of. We'll take a look at these in the next section. 447 | 448 | 449 | ## Gotchas 450 | 451 | There are some things to keep in mind when employing custom elements in your Elm app. 452 | 453 | ### Web Components And Virtual DOM 454 | 455 | Elm takes full control of the part of the DOM it manages. Like other virtual-dom based libraries it keeps track of the current state of the DOM in the form of an in-memory representation of the tree and assumes that what is currently rendered in the real DOM is a pure derivative from this in-memory representation. 456 | 457 | Some libraries are more forgiving than others with unexpected mutations but if you mess with those nodes too much you risk breaking their invariants, which in turn will cause runtime exceptions, even in Elm. What that means in practice is that you should adhere to the following rules for your custom elements to play nice with virtual-dom libraries in general. 458 | 459 | * 1) Make sure your custom element cleans up after itself via `disconnectedCallback` as Elm may decide to re-create any part of the DOM without notice. 460 | * 2) This also means that you should not rely on Elm creating your custom element node exactly x amount of times. 461 | * 3) If your custom element is supposed to receive child nodes from the outside like [in our little event delegation example](#Listening-to-Events) make sure not to add or remove any children as this may confuse Elm's virtual-dom. 462 | * 4) If your custom element doesn't expect children from the outside you are free to manage the element's child nodes. 463 | * 5) If you need both external and self-managed children you can "hide" them inside a [Shadow Root][mdn-shadow-dom], Elm won't inspect sub-trees of shadow roots. Note that there are polyfills for the Shadow DOM spec out there that work in older browsers but this API is farely involved so these might slow down the browser significantly and/or have unexpected behavior. 464 | 465 | ### Customized Built-ins 466 | 467 | The [spec][wc-specs] mentions that you can [extend built-in elements][mdn-customized-builtins], e.g. to make your own ` 556 | ``` 557 | 558 | If we open this in Chrome our button is indeed a button with an orange background. Safari does not concur, neither does pre-Chromium Microsoft Edge nor [any Webkit based browser possibly forever][webkit-nope] which includes the iOS browser. `Customized built-in` s as they're called are part of the spec but a non-significant amount of browsers doesn't and probably won't support them anytime soon. 559 | 560 | Although there is [a polyfill][customized-polyfill] it's probably best to ignore that part of the spec, it's not safe to use as [is documented in this w3c issue][w3c-nope] and layering polyfills upon polyfills onto each other might have consequences. 561 | 562 | Also note that Elm's virtual-dom does not support creating these customized built-ins, see the [gotchas section](#gotchas) for more information. 563 | 564 | [customized-polyfill]: https://github.com/ungap/custom-elements-builtin 565 | [webkit-nope]: https://bugs.webkit.org/show_bug.cgi?id=182671 566 | [w3c-nope]: https://github.com/w3c/webcomponents/issues/509 567 | 568 | 569 | ## Conclusion 570 | 571 | In this guide we've discussed the impetus of why webcomponents exist in the first place and what parts of the spec are particularly relevant when working with Elm. 572 | 573 | We've investigated a number usecases with demos where custom elements come in handy to enhance Elm's vocabulary in terms of not yet natively supported web APIs and interop scenarios with external JavaScript libraries. 574 | 575 | I hope you now have a fair grasp on these topics so you can confidently incorporate custom elements in your Elm workflow when needed. 576 | 577 | 578 | ## Further Reading 579 | 580 | * [Mozilla Developer Network Article on Webcomponents][mdn-wc] 581 | * [https://webcomponents.org][wc-home] 582 | * [Alex Korban's A Straight Forwared Introduction to Custom Elements](https://korban.net/posts/elm/2018-09-17-introduction-custom-elements-shadow-dom/) 583 | * [All the Ways To Make a Web Component](https://webcomponents.dev/blog/all-the-ways-to-make-a-web-component/) 584 | 585 | 586 | [doc-exec-command]: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand 587 | [elmpkg-elm-json]: https://package.elm-lang.org/packages/elm/json/latest 588 | [jq-event-delegation]: https://learn.jquery.com/events/event-delegation/ 589 | [guide]: https://guide.elm-lang.org 590 | [guide-interop]: https://guide.elm-lang.org/interop/ 591 | [guide-ports]: https://guide.elm-lang.org/interop/ports.html 592 | [guide-custom-elements]:https://guide.elm-lang.org/interop/custom_elements.html 593 | [html5-apis]: https://developer.mozilla.org/en-US/docs/Web/API 594 | [mdn-clipboard]: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API 595 | [mdn-customevent]: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent 596 | [mdn-customevent-polyfill]: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill 597 | [mdn-customized-builtins]:https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Customized_built-in_elements 598 | [mdn-event-bubbling]: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture 599 | [mdn-shadow-dom]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM 600 | [mdn-slot]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot 601 | [mdn-wc]: https://developer.mozilla.org/en-US/docs/Web/Web_Components 602 | [w3c]: https://www.w3.org/ 603 | [wc-custom-elements]:https://www.webcomponents.org/specs#the-custom-elements-specification 604 | [wc-home]: https://www.webcomponents.org/ 605 | [wc-polyfills]: https://www.webcomponents.org/polyfills 606 | [wc-polyfill-custom-elements]: https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements 607 | [wc-polyfill-custom-elements-es5]: https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements#es5-vs-es2015 608 | [wc-specs]: https://www.webcomponents.org/specs 609 | -------------------------------------------------------------------------------- /more/webcomponents/minimal-es5-setup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Web components example 7 | 8 | 9 | 10 | 11 | 12 | 28 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /websockets/README.md: -------------------------------------------------------------------------------- 1 | # WebSockets - [Live Demo](https://ellie-app.com/8yYgw7y7sM2a1) 2 | 3 | This is a minimal example of how to connect to a WebSocket. It connects to `wss://echo.websocket.org` which just repeats whatever you say. 4 | 5 | ![Demo](demo.gif) 6 | 7 | The important code lives in `src/Main.elm` and in `index.html` with comments! 8 | 9 | 10 | ## Building Locally 11 | 12 | Run the following commands: 13 | 14 | ```bash 15 | git clone https://github.com/elm-community/js-integration-examples.git 16 | cd js-integration-examples/websockets 17 | 18 | elm make src/Main.elm --output=elm.js 19 | open index.html 20 | ``` 21 | 22 | Some terminals may not have an `open` command, in which case you should open the index.html file in your browser another way. 23 | -------------------------------------------------------------------------------- /websockets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-community/js-integration-examples/3952f9440d3f144b99b2d9bc6b39938d1517c976/websockets/demo.gif -------------------------------------------------------------------------------- /websockets/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0", 12 | "elm/json": "1.1.3" 13 | }, 14 | "indirect": { 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.2" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": {}, 22 | "indirect": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /websockets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elm + Websockets 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /websockets/src/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (..) 2 | 3 | import Browser 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Json.Decode as D 8 | 9 | 10 | 11 | -- MAIN 12 | 13 | 14 | main : Program () Model Msg 15 | main = 16 | Browser.element 17 | { init = init 18 | , view = view 19 | , update = update 20 | , subscriptions = subscriptions 21 | } 22 | 23 | 24 | 25 | 26 | -- PORTS 27 | 28 | 29 | port sendMessage : String -> Cmd msg 30 | port messageReceiver : (String -> msg) -> Sub msg 31 | 32 | 33 | 34 | -- MODEL 35 | 36 | 37 | type alias Model = 38 | { draft : String 39 | , messages : List String 40 | } 41 | 42 | 43 | init : () -> ( Model, Cmd Msg ) 44 | init flags = 45 | ( { draft = "", messages = [] } 46 | , Cmd.none 47 | ) 48 | 49 | 50 | 51 | -- UPDATE 52 | 53 | 54 | type Msg 55 | = DraftChanged String 56 | | Send 57 | | Recv String 58 | 59 | 60 | -- Use the `sendMessage` port when someone presses ENTER or clicks 61 | -- the "Send" button. Check out index.html to see the corresponding 62 | -- JS where this is piped into a WebSocket. 63 | -- 64 | update : Msg -> Model -> ( Model, Cmd Msg ) 65 | update msg model = 66 | case msg of 67 | DraftChanged draft -> 68 | ( { model | draft = draft } 69 | , Cmd.none 70 | ) 71 | 72 | Send -> 73 | ( { model | draft = "" } 74 | , sendMessage model.draft 75 | ) 76 | 77 | Recv message -> 78 | ( { model | messages = model.messages ++ [message] } 79 | , Cmd.none 80 | ) 81 | 82 | 83 | 84 | -- SUBSCRIPTIONS 85 | 86 | 87 | -- Subscribe to the `messageReceiver` port to hear about messages coming in 88 | -- from JS. Check out the index.html file to see how this is hooked up to a 89 | -- WebSocket. 90 | -- 91 | subscriptions : Model -> Sub Msg 92 | subscriptions _ = 93 | messageReceiver Recv 94 | 95 | 96 | 97 | -- VIEW 98 | 99 | 100 | view : Model -> Html Msg 101 | view model = 102 | div [] 103 | [ h1 [] [ text "Echo Chat" ] 104 | , ul [] 105 | (List.map (\msg -> li [] [ text msg ]) model.messages) 106 | , input 107 | [ type_ "text" 108 | , placeholder "Draft" 109 | , onInput DraftChanged 110 | , on "keydown" (ifIsEnter Send) 111 | , value model.draft 112 | ] 113 | [] 114 | , button [ onClick Send ] [ text "Send" ] 115 | ] 116 | 117 | 118 | 119 | -- DETECT ENTER 120 | 121 | 122 | ifIsEnter : msg -> D.Decoder msg 123 | ifIsEnter msg = 124 | D.field "key" D.string 125 | |> D.andThen (\key -> if key == "Enter" then D.succeed msg else D.fail "some other key") 126 | --------------------------------------------------------------------------------