Motivation
Since the very first draft of Material Design released, it has gotten more and more attention by the day! The main focus of Material Design was on mobile (as delivered with Android 5.0 Lolipop) and the only official web elements were provided by Polymer paper elements, which only supported the so-called evergreen browsers. So web designers and developers started writing their own material components for different frameworks (materialup is a good place to search for various material design projects) according to the official material design specs.
Hopefully the whole chaos of different implementations comes to an end with the introduction of Material Design Lite:
Material Design Lite lets you add a Material Design look and feel to your websites. It doesn’t rely on any JavaScript frameworks and aims to optimize for cross-device use, gracefully degrade in older browsers, and offer an experience that is immediately accessible.
This article investigates the latest release of MDL and its integration with React.
MDL: How it works?
MDL combines JS and CSS to realize a number of material components. Normal DOM components (such as <button>
) can be unobtrusively upgraded to material components by adding specific classes to them while a central component called componentHandler
keeps track of all upgraded components. A deeper description of the componentHandler
is given in MDL component design pattern:
[component handler] handles the registration of new components such that DOM upgrades are automatically performed on document load, as well as making it super easy to handle upgrades of elements that may be added after initial page load.
So as long as all DOM elements which are to be upgraded to material components are already available on window.load
, we can be sure that componentHandler
automatically upgrades them all. However, if you happen to dynamically add DOM elements after the page load, you need to register the elements manually. So if your React elements are rendered after window.load
, you might as well either use upgradeElement(element, jsClass)
or upgradeDom(jsClass, cssClass)
. According to the documentation, these two differ in following terms:
-
upgradeDom(jsClass, cssClass)
: Searches existing DOM for elements of our component type and upgrades them if they have not already been upgraded. -
upgradeElement(element, jsClass)
: Upgrades a specific element rather than all in the DOM.
in both cases jsClass
is the programatic name of the material component (see below). Upon load, each component registers it self with the componentHandler
using its constructor, programatic name, and corresponding css class. The material button for example does the following:
componentHandler.register({
constructor: MaterialButton,
classAsString: 'MaterialButton',
cssClass: 'mdl-js-button'
});
In case of upgradeDom
, cssClass
is the class of DOM element which is to be upgraded to the desired material component defined by jsClass
(i.e. its programatic name). If both parameters are absent, the whole DOM is searched for upgradable components and matching elements (i.e. those having mdl-js-*
class) are upgraded. If cssClass
is missing, only those upgradable components having the CSS class corresponding to jsClass
(e.g. the cssClass
matching MaterialButton
is mdl-js-button
) are upgraded. If both are provided, it is easy to figure out what happens!
In case of upgradeElement
, element
is the actual DOM element (e.g. var element = document.getElementById('myElement');
). This interface requires both parameters to be present. This, however, is a restriction which is to be removed in the future (the release candidate already has solved this problem).
The list of all programatic names and corresponding CSS classes is as follows:
Element Name | jsClass |
cssClass |
---|---|---|
Button | MaterialButton |
mdl-js-button |
Checkbox | MaterialCheckbox |
mdl-js-checkbox |
Icon Toggle | MaterialIconToggle |
mdl-js-icon-toggle |
Menu | MaterialMenu |
mdl-js-menu |
Progress | MaterialProgress |
mdl-js-progress |
Radio | MaterialRadio |
mdl-js-radio |
Slider | MaterialSlider |
mdl-js-slider |
Spinner | MaterialSpinner |
mdl-js-spinner |
Switch | MaterialSwitch |
mdl-js-switch |
Tabs | MaterialTabs |
mdl-js-tabs |
Text field | MaterialTextfield |
mdl-js-textfield |
Tooltip | MaterialTooltip |
mdl-tooltip |
Layout | MaterialLayout |
mdl-js-layout |
Data table | MaterialDataTable |
mdl-js-data-table |
Ripple | MaterialRipple |
mdl-js-ripple-effect |
React + MDL
Lets say that we want to have a simple login form, which is only rendered if the session expires. Our component would look something like this:
var AuthDialog = React.createClass({
getInitialState: function() {
return {
loginDialogVisible: false
};
},
render: function() {
var cx = React.addons.classSet,
classes = cx({
"active": this.state.loginDialogVisible
});
var dialog = (
<div className="login-dialog">
<form ref="form" className="mdl-card mdl-shadow--2dp" onSubmit={this.formSubmit}>
<div className="mdl-card__title mdl-card--expand">
<h2 className="mdl-card__title-text">
Please sign in to continue
</h2>
</div>
<div className="mdl-card__supporting-text">
<div className="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input ref="user" className="mdl-textfield__input" type="text" id="username" />
<label className="mdl-textfield__label" htmlFor="username">Username</label>
</div>
<br/>
<div className="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input ref="pass" className="mdl-textfield__input" type="password" id="password" />
<label className="mdl-textfield__label" htmlFor="password">Password</label>
</div>
<br/>
<div className="mdl-card__actions mdl-card--border">
<button ref="submit" className="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">
Submit
</button>
</div>
</div>
</form>
</div>
);
return (
<div id="auth" className={classes}>
{ this.state.loginDialogVisible ? dialog : '' }
</div>
);
},
componentDidUpdate: function() {
// This upgrades all upgradable components (i.e. with 'mdl-js-*' class)
componentHandler.upgradeDom();
// We could have done this manually for each component
/*
* var submitButton = this.refs.submit.getDOMNode();
* componentHandler.upgradeElement(submitButton, "MaterialButton");
* componentHandler.upgradeElement(submitButton, "MaterialRipple");
*/
},
formSubmit: function(e) {
e.preventDefault();
var user = this.refs.user.getDOMNode().value,
pass = this.refs.pass.getDOMNode().value;
// Authenticate with user/pass
// [...]
if (authenticated) {
// Hide the dialog
this.setState({loginDialogVisible: false});
}
},
updateOnExpiry: function() {
// Register timer or event to detect session expiry
// [...]
// Check if session is expired
if (expired) {
this.setState({loginDialogVisible: true});
}
}
});
Since there is no guarantee (and most probably it can be assumed) that this component is rendered after window
has already been loaded, we need to upgrade our DOM elements to material components using the componentHandler
. The best lifecycle of a React component to trigger the componentHandler
is in my opinion componentDidUpdate
, since it is:
Invoked immediately after the component’s updates are flushed to the DOM.
As it can be seen from the code above, DOM elements can be upgraded either using the upgradeDom
or upgradeElement
. My favorite option is to use the former (i.e. upgradeDom
) without any parameters just to make sure every candidate is actually upgraded and to avoid upgrading each component.
Conclusion
Integrating MDL with React is as easy as calling a single method: componentHandler.upgradeDom();
. In my opinion, the best place to put this code is in the componentDidUpdate
callback, which is invoked just after and element has been updated and successfully rendered. This way we can make sure that our React component already exists in the DOM and can be upgraded by the componentHanlder
.