MDC-112 Web: MDC とウェブ フレームワークの統合

1. はじめに

logo_components_color_2x_web_96dp.png

マテリアル コンポーネント(MDC)は、デベロッパーがマテリアル デザインを実装する際に役立ちます。Google のエンジニアと UX デザイナーのチームが作成した MDC には、美しく機能的な UI コンポーネントが多数含まれており、Android、iOS、ウェブ、Flutter.material.io/develop に利用可能です。

MDC Web は、マテリアル デザインの原則を維持しながら、あらゆるフロントエンド フレームワークに統合できるように設計されています。次の Codelab では、MDC Web を基盤として使用する React コンポーネントを構築する手順を説明します。この Codelab で学んだ原則は、あらゆる JavaScript フレームワークに適用できます。

MDC Web の構築方法

MDC Web の JavaScript レイヤは、コンポーネントごとに 3 つのクラス(ComponentFoundationAdapter)で構成されています。このパターンにより、MDC Web はフロントエンド フレームワークと柔軟に統合できます。

Foundation には、マテリアル デザインを実装するビジネス ロジックが含まれています。Foundation は HTML 要素を参照しません。これにより、HTML 操作ロジックを Adapter に抽象化できます。Foundation には Adapter があります。

Adapter はインターフェースです。Adapter インターフェースは、マテリアル デザインのビジネス ロジックを実装するために Foundation によって参照されます。アダプターは、Angular や React などのさまざまなフレームワークで実装できます。Adapter の実装は DOM 構造とやり取りします。

コンポーネントには基盤があり、その役割は

  1. フレームワーク以外の JavaScript を使用して Adapter を実装します。
  2. Foundation のメソッドにプロキシするパブリック メソッドを提供します。

MDC Web が提供するもの

MDC Web のすべてのパッケージには、コンポーネント基盤アダプターが付属しています。Component をインスタンス化するには、ルート element を Component のコンストラクタ メソッドに渡す必要があります。ComponentAdapter を実装し、DOM と HTML 要素を操作します。ComponentFoundation をインスタンス化し、Adapter メソッドを呼び出します。

MDC Web をフレームワークに統合するには、そのフレームワークの言語/構文で独自のコンポーネントを作成する必要があります。フレームワークのコンポーネントは、MDC Web のアダプタを実装し、MDC Web の基盤を使用します。

作成するアプリの概要

この Codelab では、Foundation ロジックを使用してマテリアル デザインの React コンポーネントを実現するカスタム Adapter をビルドする方法について説明します。フレームワークへの統合にある高度なトピックを扱います。この Codelab ではフレームワークの例として React を使用していますが、このアプローチは他のフレームワークにも適用できます。

この Codelab では、上部アプリバーを作成し、上部アプリバーのデモページを再現します。デモページのレイアウトはすでに設定されているため、上部のアプリバーの作業を開始できます。トップ アプリバーには、次のものが含まれます。

  • ナビゲーション アイコン
  • アクション アイテム
  • 短い常に折りたたまれている固定目立つの 4 つのバリエーションがあります。

必要なもの:

  • Node.js の最新バージョン(JavaScript パッケージ マネージャーである npm がバンドルされています)
  • サンプルコード(次の手順でダウンロード)
  • HTML、CSS、JavaScript、React に関する基礎的な知識

ウェブ開発の経験についてお答えください。

初心者 中級者 習熟者

2. 開発環境をセットアップする

Codelab のスターター アプリをダウンロードする

スターター アプリは material-components-web-codelabs-master/mdc-112/starter ディレクトリ内にあります。

GitHub からクローンを作成する

GitHub からこの Codelab のクローンを作成するには、次のコマンドを実行します。

git clone https://github.com/material-components/material-components-web-codelabs
cd material-components-web-codelabs/mdc-112/starter

プロジェクトの依存関係をインストールする

スターター ディレクトリ material-components-web-codelabs/mdc-112/starter から、次のコマンドを実行します。

npm install

アクティビティが多数表示され、最後にターミナルにインストールが成功したことが表示されます。

22a33efc2a687408.png

スターター アプリを実行する

同じディレクトリで、次のコマンドを実行します。

npm start

webpack-dev-server が開始されます。ブラウザで http://localhost:8080/ にアクセスして、ページを表示します。

b55c66dd400cf34f.png

完了しました。Top App Bar React Demo ページのスターター コードがブラウザで動作しているはずです。lorem ipsum のテキストの壁、[Controls] ボックス(右下)、未完成のトップアプリバーが表示されます。

4ca3cf6d216f9290.png

コードとプロジェクトを確認する

コードエディタを開くと、プロジェクト ディレクトリは次のようになります。

e9a3270d6a67c589.png

App.js ファイルを開き、<TopAppBar> コンポーネントを含む render メソッドを確認します。

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>
    );
  }

これは、アプリケーションの TopAppBar のエントリ ポイントです。

render メソッドを含む React の Component クラスである TopAppBar.js ファイルを開きます。

TopAppBar.js

import React from 'react';

export default class TopAppBar extends React.Component {
  render() {
    return (
      <header>
        TOP APP BAR
      </header>
    );
  }
}

3. コンポーネントの構成

React では、render メソッドはコンポーネントの HTML を出力します。上部のアプリバー コンポーネントは <header /> タグをレンダリングし、次の 2 つの主要なセクションで構成されます。

  1. ナビゲーション アイコンとタイトルのセクション
  2. アクション アイコン セクション

トップアプリバーを構成する要素についてご不明な点がある場合は、GitHub のドキュメントをご覧ください。

TopAppBar.jsrender() メソッドを次のように変更します。

  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>
    );
  }

この HTML には 2 つのセクション要素があります。1 つ目はナビゲーション アイコンとタイトルを含みます。2 つ目はアクション アイコンを含みます。

次に、renderActionItems メソッドを追加します。

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>
  );
}

デベロッパーは TopAppBar を React アプリケーションにインポートし、アクション アイコンを TopAppBar 要素に渡します。App.jsTopAppBar を初期化するサンプルコードをご覧ください。

render メソッドで使用される getMergedStyles メソッドがありません。次の JavaScript メソッドを TopAppBar クラスに追加してください。

getMergedStyles = () => {
  const {style} = this.props;
  const {style: internalStyle} = this.state;
  return Object.assign({}, internalStyle, style);
}

this.classesrender メソッドにもありませんが、後のセクションで説明します。欠落しているゲッター メソッド this.classes の他に、トップ アプリバーが正しくレンダリングされる前に実装する必要がある TopAppBar の部分がまだあります。

トップ アプリバーにまだ含まれていない React コンポーネントは次のとおりです。

  • 初期化された基盤
  • 基盤に渡すアダプタ メソッド
  • JSX マークアップ
  • バリエーション管理(固定、短縮、常に折りたたまれている、目立つ)

アプローチ

  1. Adapter メソッドを実装します。
  2. componentDidMountFoundation を初期化します。
  3. componentWillUnmountFoundation.destroy メソッドを呼び出します。
  4. 適切なクラス名を組み合わせたゲッター メソッドを使用して、バリアント管理を確立します。

4. Adapter メソッドを実装する

フレームワーク以外の JS TopAppBar コンポーネントは、次のアダプタ メソッドを実装します(詳細についてはこちらを参照)。

  • hasClass()
  • addClass()
  • removeClass()
  • registerNavigationIconInteractionHandler()
  • deregisterNavigationIconInteractionHandler()
  • notifyNavigationIconClicked()
  • setStyle()
  • getTopAppBarHeight()
  • registerScrollHandler()
  • deregisterScrollHandler()
  • registerResizeHandler()
  • deregisterResizeHandler()
  • getViewportScrollY()
  • getTotalActionItems()

React には合成イベントがあり、コーディングのベスト プラクティスとパターンが異なるため、Adapter メソッドを再実装する必要があります。

アダプタのゲッター メソッド

TopAppBar.js ファイルで、次の JavaScript メソッドを 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,
  };
}

React にはスクロールやサイズ変更の合成イベントがなく、ネイティブ DOM イベント システムに委任されるため、スクロール イベントとサイズ変更イベントの登録用アダプタ API は、フレームワーク以外の JS バージョンと同じように実装されます。getViewPortScrollY は、React の API にない window オブジェクトの関数であるため、ネイティブ DOM にも委任する必要があります。アダプタの実装はフレームワークごとに異なります。

this.setStyle が欠落していることに気づくかもしれません。これは get adapter メソッドによって呼び出されます。TopAppBar.js ファイルで、不足している JavaScript メソッドを TopAppBar クラスに追加します。

setStyle = (varName, value) => {
  const updatedStyle = Object.assign({}, this.state.style);
  updatedStyle[varName] = value;
  this.setState({style: updatedStyle});
}

これで Adapter を実装しました。この時点では、実装がまだ完了していないため、コンソールにエラーが表示されることがあります。次のセクションでは、CSS クラスを追加および削除する方法について説明します。

5. Component メソッドを実装する

バリアントとクラスの管理

React にはクラスを管理する API がありません。ネイティブ JavaScript の CSS クラスの追加/削除メソッドを模倣するには、classList 状態変数を追加します。TopAppBar には、CSS クラスとやり取りする 3 つのコードがあります。

  1. className プロパティを介して <TopAppBar /> コンポーネントを渡します。
  2. addClass または removeClass を介した Adapter メソッド。
  3. <TopAppBar /> React コンポーネント内でハードコードされています。

まず、TopAppBar.js の先頭で、既存のインポートの下に次のインポートを追加します。

import classnames from 'classnames';

次に、TopAppBar コンポーネントのクラス宣言内に次のコードを追加します。

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,
    });
  }

  ... 
}

http://localhost:8080 にアクセスすると、[Controls] チェックボックスで DOM のクラス名をオン/オフできるようになっています。

このコードにより、多くのデベロッパーが TopAppBar を使用できるようになります。デベロッパーは、CSS クラスの実装の詳細を気にすることなく、TopAppBar API を操作できます。

これで、アダプタの実装が完了しました。次のセクションでは、Foundation をインスタンス化する手順について説明します。

コンポーネントのマウントとアンマウント

Foundation のインスタンス化は componentDidMount メソッドで行われます。

まず、TopAppBar.js の既存のインポートの後に次のインポートを追加して、MDC トップアプリバーの基盤をインポートします。

import {MDCTopAppBarFoundation, MDCFixedTopAppBarFoundation, MDCShortTopAppBarFoundation} from '@material/top-app-bar';

次に、次の JavaScript コードを TopAppBar クラスに追加します。

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();
  }
 
  ... 

}

React の適切なコーディング方法の 1 つは、propTypes と defaultProps を定義することです。TopAppBar.js の既存のインポートの後に、次のインポートを追加します。

import PropTypes from 'prop-types';

次に、TopAppBar.js の一番下(Component クラスの後)に次のコードを追加します。

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,
};

これで、Top App Bar React コンポーネントの実装が完了しました。http://localhost:8080 に移動すると、デモページを操作できます。デモページは MDC Web のデモページと同じように動作します。デモページは次のようになります。

3d983b98c2092e7a.png

6. まとめ

このチュートリアルでは、React アプリケーションで使用するために MDC Web の Foundation をラップする方法について説明しました。フレームワークへの統合で説明されているように、Github と npm には MDC Web コンポーネントをラップするライブラリがいくつかあります。こちらにあるリストを使用することをおすすめします。このリストには、React 以外のフレームワーク(Angular や Vue など)も含まれています。

このチュートリアルでは、MDC Web コードを 3 つの部分(FoundationAdapterComponent)に分割するという決定について説明します。このアーキテクチャにより、コンポーネントはすべてのフレームワークで動作しながら、共通のコードを共有できます。Material Components React をお試しいただきありがとうございます。新しいライブラリ MDC React をぜひお試しください。この Codelab がお役に立ちましたら幸いです。

この Codelab を完了するためにそれなりの時間と労力を必要とした

非常にそう思う そう思う どちらとも言えない そう思わない まったくそう思わない

今後もマテリアル コンポーネントを使用したい

非常にそう思う そう思う どちらとも言えない そう思わない まったくそう思わない