1. Introduction
Material Components (MDC) help developers implement Material Design. Created by a team of engineers and UX designers at Google, MDC features dozens of beautiful and functional UI components and is available for Android, iOS, web and Flutter.material.io/develop |
MDC Web is engineered to integrate into any front end framework while upholding the principles of Material Design. The following codelab guides you through building a React Component, which uses MDC Web as a foundation. The principles learned in this codelab can be applied to any JavaScript framework.
How MDC Web is built
MDC Web's JavaScript layer is comprised of three classes per component: the Component, Foundation, and Adapter. This pattern gives MDC Web the flexibility to integrate with frontend frameworks.
The Foundation contains the business logic that implements Material Design. The Foundation does not reference any HTML elements. This lets us abstract HTML interaction logic into the Adapter. Foundation has an Adapter.
The Adapter is an interface. The Adapter interface is referenced by the Foundation to implement Material Design business logic. You can implement the Adapter in different frameworks such as Angular or React. An implementation of an Adapter interacts with the DOM structure.
The Component has a Foundation, and its role is to
- Implement the Adapter, using non-framework JavaScript, and
- Provide public methods that proxy to methods in the Foundation.
What MDC Web provides
Every package in MDC Web comes with a Component, Foundation, and Adapter. To instantiate a Component you must pass the root element to the Component's constructor method. The Component implements an Adapter, which interacts with the DOM and HTML elements. The Component then instantiates the Foundation, which calls the Adapter methods.
To integrate MDC Web into a framework you need to create your own Component in that framework's language/syntax. The framework Component implements MDC Web's Adapter and uses MDC Web's Foundation.
What you'll build
This codelab demonstrates how to build a custom Adapter to use the Foundation logic to achieve a Material Design React Component. It covers the advanced topics found at Integrating into Frameworks. React is used in this codelab as an example framework, but this approach can be applied to any other framework.
In this codelab, you'll build the Top App Bar and recreate the top app bar demo page. The demo page layout is already setup so you can start working on the Top App Bar. The Top App Bar will include:
- Navigation icon
- Action items
- There are 4 variants available: Short, always collapsed, fixed, and prominent variants
What you'll need:
- A recent version of Node.js (which comes bundled with npm, a JavaScript package manager)
- The sample code (to be downloaded in the next step)
- Basic knowledge of HTML, CSS, JavaScript, and React
How would you rate your level of experience with web development?
2. Set up dev environment
Download the starter codelab app
The starter app is located within the material-components-web-codelabs-master/mdc-112/starter
directory.
...or clone it from GitHub
To clone this codelab from GitHub, run the following commands:
git clone https://github.com/material-components/material-components-web-codelabs
cd material-components-web-codelabs/mdc-112/starter
Install project dependencies
From the starter directory material-components-web-codelabs/mdc-112/starter
, run:
npm install
You will see a lot of activity and at the end, your terminal should show a successful install:
Run the starter app
In the same directory, run:
npm start
The webpack-dev-server
will start. Point your browser to http://localhost:8080/ to see the page.
Success! The starter code for the Top App Bar React Demo page should be running in your browser. You should see a wall of lorem ipsum text, a Controls box (bottom right), and an unfinished Top App Bar:
Take a look at the code and project
If you open your code editor, the project directory should look something like:
Open the file App.js
and look at the render
method, which includes the <TopAppBar>
Component:
App.js
render() {
const {isFixed, isShort, isRtl, isProminent, isAlwaysCollapsed, shouldReinit} = this.state;
return (
<section
dir={isRtl ? 'rtl' : 'ltr'}
className='mdc-typography'>
{
shouldReinit ? null :
<TopAppBar
navIcon={this.renderNavIcon()}
short={isShort}
prominent={isProminent}
fixed={isFixed}
alwaysCollapsed={isAlwaysCollapsed}
title='Mountain View, CA'
actionItems={this.actionItems}
/>
}
<div className={classnames('mdc-top-app-bar--fixed-adjust', {
'mdc-top-app-bar--short-fixed-adjust': isShort || isAlwaysCollapsed,
'mdc-top-app-bar--prominent-fixed-adjust': isProminent,
})}>
{this.renderDemoParagraphs()}
</div>
{this.renderControls()}
</section>
);
}
This is the entry point for the TopAppBar
in the application.
Open the file TopAppBar.js
which is a bare React Component
class with a render
method:
TopAppBar.js
import React from 'react';
export default class TopAppBar extends React.Component {
render() {
return (
<header>
TOP APP BAR
</header>
);
}
}
3. Composition of component
In React, the render
method outputs the Component's HTML. The Top App Bar Component will render a <header />
tag, and will be composed of 2 main sections:
- Navigation icon and title section
- Action icons section
If you have questions about the elements that comprise the Top App Bar, visit the documentation on GitHub.
Modify the render()
method in TopAppBar.js
to look like this:
render() {
const {
title,
navIcon,
} = this.props;
return (
<header
className={this.classes}
style={this.getMergedStyles()}
ref={this.topAppBarElement}
>
<div className='mdc-top-app-bar__row'>
<section className='mdc-top-app-bar__section mdc-top-app-bar__section--align-start'>
{navIcon ? navIcon : null}
<span className="mdc-top-app-bar__title">
{title}
</span>
</section>
{this.renderActionItems()}
</div>
</header>
);
}
There are two section elements in this HTML. The first contains a navigation icon and title. The second contains action icons.
Next, add the renderActionItems
method:
renderActionItems() {
const {actionItems} = this.props;
if (!actionItems) {
return;
}
return (
<section className='mdc-top-app-bar__section mdc-top-app-bar__section--align-end' role='toolbar'>
{/* need to clone element to set key */}
{actionItems.map((item, key) => React.cloneElement(item, {key}))}
</section>
);
}
A developer will import TopAppBar
into their React application and pass action icons to the TopAppBar
element. You can see example code initializing a TopAppBar
in App.js
.
The getMergedStyles
method is missing, which is used in the render
method. Please add the following JavaScript method to the TopAppBar
class:
getMergedStyles = () => {
const {style} = this.props;
const {style: internalStyle} = this.state;
return Object.assign({}, internalStyle, style);
}
this.classes
is also missing from the render
method, but will be covered in a later section. Besides the missing getter method, this.classes
, there are still pieces of the TopAppBar
you need to implement before Top App Bar can render correctly.
The pieces of the React Component that are still missing from Top App Bar are:
- An initialized foundation
- Adapter methods to pass into the foundation
- JSX markup
- Variant management (fixed, short, always collapsed, prominent)
The approach
- Implement the Adapter methods.
- Initialize the Foundation in the
componentDidMount
. - Call the Foundation.destroy method in the
componentWillUnmount
. - Establish variant management via a getter method that combines appropriate class names.
4. Implement Adapter methods
The non-framework JS TopAppBar
Component implements the following Adapter methods (listed in detail here):
hasClass()
addClass()
removeClass()
registerNavigationIconInteractionHandler()
deregisterNavigationIconInteractionHandler()
notifyNavigationIconClicked()
setStyle()
getTopAppBarHeight()
registerScrollHandler()
deregisterScrollHandler()
registerResizeHandler()
deregisterResizeHandler()
getViewportScrollY()
getTotalActionItems()
Because React has synthetic events and different best coding practices and patterns, the Adapter methods need to be reimplemented.
Adapter Getter Method
In the TopAppBar.js
file add the following JavaScript method to TopAppBar
:
get adapter() {
const {actionItems} = this.props;
return {
hasClass: (className) => this.classes.split(' ').includes(className),
addClass: (className) => this.setState({classList: this.state.classList.add(className)}),
removeClass: (className) => {
const {classList} = this.state;
classList.delete(className);
this.setState({classList});
},
setStyle: this.setStyle,
getTopAppBarHeight: () => this.topAppBarElement.current.clientHeight,
registerScrollHandler: (handler) => window.addEventListener('scroll', handler),
deregisterScrollHandler: (handler) => window.removeEventListener('scroll', handler),
registerResizeHandler: (handler) => window.addEventListener('resize', handler),
deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler),
getViewportScrollY: () => window.pageYOffset,
getTotalActionItems: () => actionItems && actionItems.length,
};
}
The adapter APIs for scroll and resize event registration are implemented identically to the non-framework JS version, because React doesn't have any synthetic event for scrolling or resizing and defers to the native DOM event system. getViewPortScrollY
also needs to defer to the native DOM since it is a function on the window
object, which is not in React's API. Adapter implementations will be different for each framework.
You may notice this.setStyle
is missing, which is called by the get adapter
method. In the TopAppBar.js
file, add the missing JavaScript method to the TopAppBar
class:
setStyle = (varName, value) => {
const updatedStyle = Object.assign({}, this.state.style);
updatedStyle[varName] = value;
this.setState({style: updatedStyle});
}
You've just implemented the Adapter! Note that you may see errors in your console at this point because the full implementation is not yet complete. The next section will guide you through how to add and remove CSS classes.
5. Implement Component methods
Managing Variants and Classes
React doesn't have an API to manage classes. To mimic native JavaScript's add/remove CSS class methods, add the classList
state variable. There are three pieces of code in TopAppBar
that interact with CSS classes:
<TopAppBar />
component via theclassName
prop.- The Adapter method via
addClass
orremoveClass
. - Hard coded within the
<TopAppBar />
React Component.
First, add the following import at the top of TopAppBar.js
, below the existing imports:
import classnames from 'classnames';
Then add the following code inside the class declaration of the TopAppBar
Component:
export default class TopAppBar extends React.Component {
constructor(props) {
super(props);
this.topAppBarElement = React.createRef();
}
state = {
classList: new Set(),
style: {},
};
get classes() {
const {classList} = this.state;
const {
alwaysCollapsed,
className,
short,
fixed,
prominent,
} = this.props;
return classnames('mdc-top-app-bar', Array.from(classList), className, {
'mdc-top-app-bar--fixed': fixed,
'mdc-top-app-bar--short': short,
'mdc-top-app-bar--short-collapsed': alwaysCollapsed,
'mdc-top-app-bar--prominent': prominent,
});
}
...
}
If you head to http://localhost:8080 the Controls checkboxes should now toggle on/off the class names from the DOM.
This code makes TopAppBar
usable by many developers. Developers can interact with the TopAppBar
API, without worrying about the implementation details of CSS classes.
You've now successfully implemented the Adapter. The next section will guide you through instantiating a Foundation.
Mounting and Unmounting the Component
Foundation instantiation happens in the componentDidMount
method.
First, import the MDC Top App Bar foundations by adding the following import after the existing imports in TopAppBar.js
:
import {MDCTopAppBarFoundation, MDCFixedTopAppBarFoundation, MDCShortTopAppBarFoundation} from '@material/top-app-bar';
Next, add the following JavaScript code into the TopAppBar
class:
export default class TopAppBar extends React.Component {
...
foundation_ = null;
componentDidMount() {
this.initializeFoundation();
}
componentWillUnmount() {
this.foundation_.destroy();
}
initializeFoundation = () => {
if (this.props.short) {
this.foundation_ = new MDCShortTopAppBarFoundation(this.adapter);
} else if (this.props.fixed) {
this.foundation_ = new MDCFixedTopAppBarFoundation(this.adapter);
} else {
this.foundation_ = new MDCTopAppBarFoundation(this.adapter);
}
this.foundation_.init();
}
...
}
One good React coding practice is to define propTypes and defaultProps. Add the following import after the existing imports in TopAppBar.js:
import PropTypes from 'prop-types';
Then add the following code to the bottom of TopAppBar.js
(after the Component class):
import PropTypes from 'prop-types';
TopAppBar.propTypes = {
alwaysCollapsed: PropTypes.bool,
short: PropTypes.bool,
fixed: PropTypes.bool,
prominent: PropTypes.bool,
title: PropTypes.string,
actionItems: PropTypes.arrayOf(PropTypes.element),
navIcon: PropTypes.element,
};
TopAppBar.defaultProps = {
alwaysCollapsed: false,
short: false,
fixed: false,
prominent: false,
title: '',
actionItems: null,
navIcon: null,
};
You have now successfully implemented the Top App Bar React Component. If you navigate to http://localhost:8080 you can play with the demo page. The demo page will work the same as MDC Web's demo page. The demo page should look like this:
6. Wrap up
In this tutorial we covered how to wrap MDC Web's Foundation for use in a React application. There are a few libraries on Github and npm that wrap MDC Web Components as described in Integrating into Frameworks. We recommend you use the list found here. This list also includes other frameworks besides React such as Angular and Vue.
This tutorial highlights our decision to split MDC Web code into 3 parts, the Foundation, Adapter, and Component. This architecture allows components to share common code while working with all frameworks. Thanks for trying Material Components React and please check out our new library MDC React. We hope you enjoyed this codelab!