This project aims to bridge the connection between a React Component and a CustomElement. The strategy the library takes is to create a model that defines how the DOM is translated into React props.
Since the goal is support React components as CustomElements we are not supporting extending from builting elements such as Paragraph, Select etc.
It supports:
- Updating the React component automatically when attributes get changed
- Supports automatic registration and triggering of events
- Supports multiple render targets, the custom element itself, a container div or shadow root.
- Allows parsing and updating of nested structures of DOM, for example:
<select><optionvalue="something">Else</option><optionvalue="certain">Value</option></select>This DOM structure can be transformed into a model and automatically injected into a React Component. You can transform any DOM structure into a model that will be passed to the React Component. This allows you to make use of most of the DOM api and encapsulate React as an implementation detail.
npm install @adobe/react-webcomponent
If you are using Babel 6: Because we are targeting CustomElements V1 and we are using Babel to transpile our code there will be a problem with instantiating the CustomElement. See this issue for the discussion. Include the custom-elements-es5-adapter before you load this librabry to fix this issue.
Is you are using Babel 7: the issues is fixed so you shouldn't need anything else.
We are using class properties and decorators so make sure you include the appropiat babel plugins to use this.
The first thing which is need is a React Component to expose as a Custom Element
importReact,{Component}from'react';import{createCustomElement,DOMModel,byContentVal,byAttrVal,registerEvent}from"@adobe/react-webcomponent";classReactButtonextendsComponent{constructor(props){super(props);}render(){return(<div><buttonweight={this.props.weight}>{this.props.label}</button><p>Text</p></div>)}}Then you need to create a model which defines how the DOM is parsed into React properties.
classButtonModelextendsDOMModel{
@byContentValtext="something";
@byAttrValweight;
@registerEvent("change")change;}You create the custom element
constButtonCustomElement=createCustomElement(ReactButton,ButtonModel,"container");And then register it
window.customElements.define("test-button",ButtonCustomElement);When defining the CustomElement you have the posibility to specify where the React component will be rendered by specifying the renderRoot property.
The possible values are:
- container
This will generate an extra div inside the custom element and the React Component will be rendered there. This is useful because React will remove all the children of the container element it renders in.
So if you would like to parse values from the provided markup of the custom element and modify them, the elements will be lost after the initial rendering.
For example:
<my-button><my-button-label>My Button</my-button-label></my-button>If we wouldn't render in a container the
my-button-labelelement would be removed by React when rendering.
shadowRoot
This will determine the creation of the custom element shadowRoot and the React component will be rendered in itelement
The React component will be rendered directly in the custom element.
By default we provide the utility to create a custom element createCustomElement. This encapsulates the default behaviour but doesn't allow extension of the element.
This can be bypassed and the customElement can be extended with new capabilities.
import{CustomElement}from"@adobe/react-webcomponent";classButtonCustomElementextendsCustomElement{constructor(){super();this._custom=3;}getcustom(){returnthis._custom;}setcustom(value){this._custom=value;}};ButtonCustomElement.observedAttributes=Model.prototype.attributes;ButtonCustomElement.domModel=Model;ButtonCustomElement.ReactComponent=ReactComponent;ButtonCustomElement.renderRoot="container";// optional, defaults to "element"window.customElements.define("test-button",ButtonCustomElement);This utility is reponsible from converting a DOM node to a model. The model is decorated with a series of specialize decorators. Each decorator will parse the dom and construct the model:
- byAttrVal
- byBooleanAttrVal
- byJsonAttrVal
- byContentVal
- byChildContentVal
- byChildRef
- byModel
- byChildModelVal
- byChildrenRefArray
- byChildrenTypeArray
- registerEvent
Parses the element and sets the value corresponding to the attribute value of element
@byAttrVal(attrName:string)-defaultstothenameofthepropertythatitdecorates.classModelextendsDOMModel{
@byAttrVal()weight;
@byAttrVal("custom-attribute-name")reactPropName;}Usage:
<divid="elem"weight="3"custom-attribute-name="some value"/>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{weight: "3",reactPropName: "some value"}Parses the element and sets the value corresponding to the presence of the attribute on the element.
The value of the attribute is ignored, only the presence of the attribute determines the value
@byBooleanAttrVal(attrName:string)-defaultstothenameofthepropertyitdecoratesclassModelextendsDOMModel{
@byBooleanAttrVal()checked;
@byBooleanAttrVal("is-required")required;}Usage:
<divid="elem"checked/>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{checked: true,required: undefined}<divid="elem"checkedis-required/>model.fromDOM(document.getElementById("elem"));model~{checked: true,required: true}Parses the element and sets the value by parsing the value using JSON.parse.
classModelextendsDOMModel{
@byJsonAttrVal()obj;
@byJsonAttrVal("alias-attr")anotherObj;}<divid="elem"obj='[{"example":1},{"test":2}]'alias-attr='[{"other":"example},{"test":3}]'/>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{obj: [{"example":1},{"test":2}],anotherObj: [{"other":"example},{"test":3}]}Parse the element and sets the value to the innerText of the element.
classModelextendsDOMModel{
@byContentVal()label;}<divid="elem">My Label</div>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{label: "My Label"}Parse the element looking for an element that matches the given selector and sets value to the innerText of that element
classModelextendsDOMModel{
@byChildContentVal("custom-label")label;}<divid="elem"><custom-label>My Label</custom-label></div>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{label: "My Label"}Parses the element and looks for an child element that matches the given selector and sets the value to result of parsing the child element with the given model
classCustomLabelModelextendsDOMModel{
@byContentVal()value;
@byAttrVal()required;}classModelextendsDOMModel{
@byChildContentVal("custom-label",CustomLabelModel)label;}<divid="elem"><custom-labelrequired>My Label</custom-label></div>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{label: {value: "My Label",required: true}}Assigns the value of running a given model over the element. This allows the element model to be saved on a different property than directly on the model.
classCustomModelextendsDOMModel{
@byContentVal()value;
@byAttrVal()required;}classModelextendsDOMModel{
@byModelVal()item;}<divid="elem"required>Content</div>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{item: {value: "Content",required: true}}Parse the element and sets the value by getting the model value from the custom elem. This attribute only returns something if there is a custom element parsed.
Using the Button defined at the beginning:
window.customElements.define("test-button",ButtonCustomElement);We define a model
classModelextendsDOMModel{
@byChildModelVal("test-button")button;}<div><test-buttonweight="3">Click me</test-button></div>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{button: {weight: 3,text: "Click me"}}The fundamental difference here is that the model is defined in another custom element and it is reused in this model. So it doesn't get redefined.
Parse the element children and selects all the elements that match the provided selector.
For each element it uses the referenced model to parse the value of the element.
All the resulting array of values is stored as the value on the decorated property.
classOptionModelextendsDOMModel{
@byContentVal()content;
@byAttrVal()value;
@byBooleanAttrVal()selected;}classSelectModelextendsDOMModel{
@byChildrenRefArray("option",OptionModel)options;}<selectid="elem"><optionvalue="1">Amsterdam</option><optionvalue="2">Berlin</option><optionvalue="3">London</option></select>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{options: [{value: 1,content: "Amsterdam"},{value: 2,content: "Berlin"},{value: 3,content: "London"}]}Parses the element children and for each child if the nodeName matches one from the provided map it will parse that child with the corresponding model.
classChild1ModelextendsDOMModel{
@byAttrVal()checked;}classChild2ModelextendsDOMModel{
@byAttrVal()selected;}classModelextendsDOMModel{
@byChildrenTypeArray({"child-one": Child1Model,"child-two": Child2Model})items;}<divid="elem"><child-onechecked/><child-twoselected/></div>constmodel=newModel().fromDOM(document.getElementById("elem"));model~{items: [{checked: true},{selected: true}]}Registers an event to be registered on the React component and when it is called it a CustomEvent will be triggered on the custom element.
The event name is automatically transformed into camelCase and prefixed with onThis behaviours happens on the CustomElement not the DOMModel, the DOMModel only registers the event
classModelextendsDOMModel{
@registerEvent("change")change;}Eventually this will be converted the CustomElement in a onChange property on the React component.