Render a React component with attributes defined in the DOM

Using the JavaScript UI component library React is, among other benefits, a good way to disseminate small pieces of SPA intelligence into a more classical website, although it can sometimes be tricky to conciliate both worlds.

For this example, we will be building a form, one field of which holds a color value. We already set up a classic <input> to allow our users to type in the hexadecimal code they desire ; however, we would like to improve their experience by adding a colorpicker control that will change the field's value for them in a more pleasant way.

Having already been using React here and there throughout the project, we chose a library that makes use of it, and provides what we need as a React component. Since we want it to control the field, the latter needs to be rendered by React as well, which means we will roll out a component of our own that includes both.

We quickly face a dilemma, though : we still want the <input>'s value to be POSTed to the server under the right name, and we don't want to delegate to React the handling of CSS classes and such. All of the element's attributes shall keep being defined in our HTML template, for the sake of separation of concerns. Unfortunately, React doesn't allow us to fetch such data from the DOM when rendering its components out of the box : this is the issue we are going to address here.

First, let's define a target element for our component to render into (nothing new here) :

<div class="color-picker"></div>

We will now append every attribute we want applied to our <input> to this <div>, prepending the keys with data- to keep everything safe and valid :

<div class="color-picker" data-class="some-class" data-name="color" data-value="#BADA55"></div>

Now, we want the <input> rendered by React to get all these attributes applied to it (without the data- prefix, obviously), so everything not related to React keeps working just like without it. Here is our component's code :

var React = require('react'),
ReactColorPicker = require('react-color-picker');

module.exports = React.createClass({
getInitialState: function() {
return {
color: '#FFFFFF',
attrs: {}
};
},

updateColor: function(color) {
this.setState({ color: color.toUpperCase() });
},

// Called by React right after mounting the component in the DOM
componentDidMount: function() {
var parentNode = this.getDOMNode().parentNode,
attrs = this.state.attrs;

// We iterate over the target node (our div)'s attributes
[].slice.call(parentNode.attributes).forEach(function (attr) {
if (attr.name.match(/^data-/)) {
var realName = attr.name.substr(5);

if ('value' == realName) {
// The value will be handled by React directly, as a separate state variable
this.updateColor(attr.value);
} else {
// This follows React's convention, class being a JS keyword
if ('class' == realName) {
realName = 'className';
}

// Other attributes will be grouped in an object
attrs[realName] = attr.value;
}

// Clean up the target node
parentNode.removeAttribute(attr.name);
}
}, this);

// We trigger our component's update to make it rerender with its new attributes
this.setState({ attrs: attrs });
},

render: function() {
// The {...var} notation allows us to apply a key-value JS object
// as a collection of HTML attributes
return (
<div>
<ReactColorPicker value={this.state.color} onDrag={this.updateColor} />
<input value={this.state.color} {...this.state.attrs} onChange={this.updateColor} />
</div>
);
}
});

Hopefully, future versions of the tool may allow us to render components dynamically based on the DOM's initial state more easily. A cleaner solution could have been to handle our field with a subcomponent and make these attributes part of its props, to enhance its reusability ; it would also be nice to display the <input> natively inside the <div>, so it remains available without JavaScript, but this might lead to some markup duplication - as for the attributes themselves, it should not be a problem if your form is part of a server-side template.

I will be glad to hear what you have to say about this, if you ever encountered similar situations and if you have a better solution to handle such cases : don't be shy !