Embedding React components in Elm with custom elements
Tags: elm, customelements, webcomponents, react
Published: 2018-08-28 12:53:13

---
title: Embedding React components in Elm with custom elements
published: true
description: We show how to embed a React component in Elm, binding properties and event handlers in a straightforward way.
tags: #elm #custom-elements #webcomponents #react
---
Elm is great, but sometimes there are things that exist outside of the Elm ecosystem that you want to use. In this case we wanted a dynamic form builder based on [JSON Schema](https://json-schema.org). While it's certainly possible to write such a thing in Elm, we just wanted something that worked to start with.

There are a number of dynamic form builders out there, but [react-jsonschema-form](https://github.com/mozilla-services/react-jsonschema-form) has a simple API and works well for us right now.

I've not included any screenshot, because it really works just like the React version, so see screenshots there.

# Basic principles
We have experimented with different ways to embed custom elements in Elm, but have found that the following principles work well and make the code easy to read and write:
* Make one javascript file and one corresponding Elm module.
* Only use properties, not attributes, to communicate with the component from Elm.
* Keep internal copies of all properties in the component.
* Do not keep any state in the component, if possible, keep all state in Elm.
* Render component on property change, provided that all mandatory properties are set.
* Only use custom events to communicate with Elm from the component, and bind these to the relevant React events.

# In practice
Now, how does this work in practice? See the code listings below. Here are some implementation notes:
* As you can see, we set internal properties in the custom element, for instance `this._schema`.
* As you also can see, we use `this.` even when you don't need to. I find this helps knowing what thing I'm actually using.
* Each time a property is set, we call `renderForm()` which checks to see if the relevant properties are set, and then in turn calls `ReactDOM.render()` to create the React component.
* We bind the custom element internal properties to the React properties.
* We bind our custom event dispatchers to the React event handlers. Note that you need to use the `detail:` field when creating a custom event, any other field will just be dropped.
* In Elm, we use the generated `<react-jsonschema-element>` just as we would use any other HTML element.
* You can see that we pass everything as `Json.Decode.Value`. In this case this is what we want, because we keep this in Elm as `Value` due to its dynamic nature, but you can of course put any JSON decoder in the `Html.Events.on` handler, as well as any JSON encoder in the `Html.Attributes.property` call.
* In your `Main`, we let Elm be the keeper of the state, so any time we get an `onChange` we change the data in our model, passing it to the component again. This lets us keep in sync with the React component's internal state.

# Note
* Custom elements do not work in all browsers yet. However, we have found that including a polyfill from https://github.com/webcomponents/webcomponentsjs in your `index.html` works well.

# Code
The javascript file:
```javascript
import React from "react";
import ReactDOM from "react-dom";
import Form from "react-jsonschema-form";

// React jsonschema form custom element
class ReactJsonschemaForm extends HTMLElement {
  set schema(value) {
    this._schema = value;
    this.renderForm();
  }

  set uiSchema(value) {
    this._uiSchema = value;
    this.renderForm();
  }

  set data(value) {
    this._data = value;
    this.renderForm();
  }

  renderForm() {
    // Only render if schema property has been set
    if (this._schema) {
      ReactDOM.render(
        React.createElement(Form,
          {
            schema: this._schema,
            formData: this._data ? this._data : undefined,
            uiSchema: this._uiSchema ? this._uiSchema : undefined,
            onChange: this.sendChange.bind(this),
            onSubmit: this.sendSubmit.bind(this)
          },
          React.createElement('div', null, [
            React.createElement('button', { key: "submit", type: "submit" }, "Submit"),
            React.createElement('button', { key: "cancel", type: "button", onClick: this.sendCancel.bind(this) }, "Cancel")
          ])
        ),
        this
      );
    }
  }

  sendChange(change) {
    this.dispatchEvent(new CustomEvent('form-change', { detail: change.formData }));
  }

  sendSubmit(change) {
    this.dispatchEvent(new CustomEvent('form-submit', { detail: change.formData }));
  }

  sendCancel() {
    this.dispatchEvent(new CustomEvent('form-cancel'));
  }
}
customElements.define('react-jsonschema-form', ReactJsonschemaForm);
```

The Elm module:
```elm
module ReactJsonschemaForm exposing (view)

import Html
import Html.Attributes
import Html.Events
import Json.Decode

view :
    { schema : Json.Decode.Value
    , uiSchema : Json.Decode.Value
    , data : Json.Decode.Value
    , onChange : Json.Decode.Value -> msg
    , onSubmit : Json.Decode.Value -> msg
    , onCancel : msg
    }
    -> Html.Html msg
view { onChange, onSubmit, onCancel, schema, uiSchema, data } =
    Html.node "react-jsonschema-form"
        [ Html.Attributes.property "uiSchema" uiSchema
        , Html.Attributes.property "data" data
        , Html.Attributes.property "schema" schema
        , Html.Events.on "form-change" (Json.Decode.field "detail" Json.Decode.value |> Json.Decode.map onChange)
        , Html.Events.on "form-submit" (Json.Decode.field "detail" Json.Decode.value |> Json.Decode.map onSubmit)
        , Html.Events.on "form-cancel" (Json.Decode.succeed onCancel)
        ]
        []
```
Interesting parts of the Main file:
```elm
module Main exposing (main)

import Json.Decode
import ReactJsonschemaForm

type Model =
    { data: Json.Decode.Value
    , schema: Json.Decode.Value
    , uiSchema: Json.Decode.Value
    , ...
    }

type Msg
    = FormChanged Json.Decode.Value
    | FormSubmitted Json.Decode.Value
    | FormCancelled

update msg model =
    case msg of
        FormChanged data ->
            ( { model | data = data }, Cmd.none )
        FormSubmitted data ->
            ( model, sendDataCmd data )
        ...

view model =
    ReactJsonschemaForm.view
        { schema = model.schema
        , uiSchema = model.uiSchema
        , data = model.data
        , onChange = FormChanged
        , onSubmit = FormSubmitted
        , onCancel = FormCancelled
        }
```