MDC-112 Web: Integrating MDC with Web Frameworks

1. Introduction

logo_components_color_2x_web_96dp.png

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

  1. Implement the Adapter, using non-framework JavaScript, and
  2. 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?

Novice Intermediate Proficient

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:

22a33efc2a687408.png

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.

b55c66dd400cf34f.png

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:

4ca3cf6d216f9290.png

Take a look at the code and project

If you open your code editor, the project directory should look something like:

e9a3270d6a67c589.png

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:

  1. Navigation icon and title section
  2. 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

  1. Implement the Adapter methods.
  2. Initialize the Foundation in the componentDidMount.
  3. Call the Foundation.destroy method in the componentWillUnmount.
  4. 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:

  1. <TopAppBar /> component via the className prop.
  2. The Adapter method via addClass or removeClass.
  3. 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:

3d983b98c2092e7a.png

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!

I was able to complete this codelab with a reasonable amount of time and effort

Strongly agree Agree Neutral Disagree Strongly disagree

I would like to continue using Material Components in the future

Strongly agree Agree Neutral Disagree Strongly disagree