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. 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 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 callsReactDOM.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 asValue
due to its dynamic nature, but you can of course put any JSON decoder in theHtml.Events.on
handler, as well as any JSON encoder in theHtml.Attributes.property
call. - In your
Main
, we let Elm be the keeper of the state, so any time we get anonChange
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:
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:
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:
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
}
Top comments (3)
I know React and some very basic bits of Elm, but I didn't understand how it works. I mean, you gave the same name to both the components, and I'm finding it hard to get how both are interacting.
Can you explain/disambiguate?
Nice topic anyway, I had a similar task some year ago: dinamically render a form from an XML doc (along with XML Schema). The very first PoC was to use JSX to render XML tags like React components.
Hi!
Yes, actually there are three components involved. The first is the React component, the second is the web component, and the third is the Elm module. They exist in different layers, the web component wraps the React component and the Elm module wraps the web component.
Hopefully this clears it up. Feel free to ask for additional clarification.
Yes, this clears it up a lot. Thank you :-)