maesblog

Angular Elementsことはじめ – Custom Elementsを実装する方法

2018年5月4日にAngular 6がリリースされました。Angular 6は、Angular自体の機能というよりは、Angularをいかに使ってもらえるかというところに重点が置かれたリリース内容でした。その中の1つに、Angular Elementsという、Angularの利用を促進しそうな機能が新たに追加されました。詳細は記事内に書きますが、Angularで構築したコンポーネントを手軽に別のWebサイトで通常のHTMLのように扱えるCustom Elementsに変換する機能です。似たようなことがReactやVueでは既にできていて、Angularはずっと苦手としていた部分です。今回自分の勉強がてら、Angular Elementsの使い方をまとめてみました。参考にしてもらえればと思います。

はじめに – Angular Elements とは

Angular Elementsは、AngularコンポーネントをCustom Elements(Web Componentsの機能の一部)として出力する機能です。このAngular Elementsを使用すると、静的なHTMLのコンテンツ内に、Angularで構築したコンポーネントをCustom Elementsとして手軽に取り込むことができるようになります。取り込んだCustom Elementsは、通常のHTMLの要素と同じように振る舞い、style要素やscript要素で操作できるようになります。

詳細は以下をご確認ください。

Angular Elementsのリリースにより、WordPressのような別のサイトで、ReactやVueでは可能であった再利用可能なウィジェット的な使い方がAngularでも手軽にできるようになりました。しかもWeb標準技術のCustom Elementsとしてです。今後Angularの用途はこれまで以上に増えていくことが予想されています。

今回は、このAngular Elementsを使ったCustom Elementsの実装方法について紹介していきます。まずは、以下でAngular Elementsを使って実装したCustom Elementsのサンプルをご確認ください。

Angular Elements サンプル

以下は、Angular Elementsのサンプルです。Angular Elementsとして作ったCustom Elementsをこちらのブログに(jsファイルとして)読み込んで表示させています。 

loading
↑はAngular Elementsで実装したタグを読み込んだだけで実現しています

機能的には、テキストフィールドに入力した文字が、ボタン押下時に、Helloの後に反映されるというものです。それからマウスオーバーすると、Custom Elements内の背景色が変わります。こうした機能は、Angularとして実装している部分もありますが、一部はCustom Elementsとして読み込んだ後に、こちらのページ上で、style要素やscript要素を使ってスタイルや処理を書いています

以下のように表示させたい箇所にCustom Elementsのタグを記述するだけで使えるようになります。

<app-hello name="Angular Elements!"></app-hello>

このようにCustom Elementsとしてページに読み込んだ要素は、普通のHTML要素を扱う感覚で扱えるようになるところが、なかなか面白いんじゃないかと思います。そしてAngularで実装したものを、こうしたブログなど静的なページにも簡単に取り込めるようになったことが何より嬉しいですね。

これから、このサンプルを通してAngular Elementsの実装方法について紹介していきます。なお、ソースコードについては、こちらのブログでも説明とともにこの後紹介していきますが、GitHub上にもアップしておきました。こちらも併せてご確認いただければと思います。

Angular Elements の開発環境を構築する

Angular Elementsの機能を利用するには、Angularのバージョンが6以上である必要があります。ほとんどの場合、Angular CLIを使って開発することが多いかと思いますので、Angular CLI前提で話を進めます。

Angular CLIのアップグレード

まずは、Angular CLIのバージョンを確認します。

$ ng --version

もし、Angular CLIのバージョンが6以下であれば、以下のコマンドでAngular CLIを最新のバージョンにアップグレードします(インストールされていなかった場合でも、以下のコマンドでインストールされます)。

$ npm install -g @angular/cli

再度、ng --versionでAngular CLIのバージョンを確認します。

angular cli: ng --version

Angule Elements用のプロジェクトを作成

Angular CLIを最新のバージョンにアップグレードしたら、次はAngule Elements用のプロジェクトを作ります。以下のAngular CLIのコマンドでAngularプロジェクトを作成します。プロジェクト名(ディレクトリ名)は任意です。ここでは、「angular-elements-sample」としました。

$ ng new angular-elements-sample

ちなみに、Node.jsのバージョンによっては怒られてプロジェクトが作られない場合があるので、状況に応じてNode.jsのバージョンアップも行なってください。

プロジェクトを作成したら、プロジェクトディレクトリに移動して、ng add @angular/elementsコマンドを実行します。document-register-element.jsなどAngular Elementsに必要なポリフィルや依存関係がプロジェクトに追加されます。

$ cd angular-elements-sample
$ ng add @angular/elements

ちなみに、ng addもAngular 6(Angular CLI 6)で追加された機能で、Angularプロジェクトに新しい機能を追加します。

ローカルサーバを起動する

実装中は、ローカルサーバを立ち上げて実装内容を確認していくこととなるかと思います。以下のコマンドで、ローカルサーバを起動します。

$ npm start

> angular-elements-sample@0.0.0 start /Users/maechabin/Sites/practice/angular-elements-sample
> ng serve

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **

ブラウザでlocalhost:4200にアクセスします(とりあえず、最初はAngular CLIのデフォルトのアプリが表示されます)。

これでAngular Elementsの開発ができるようになりました。次にコードを書いていきます。

Angular Elements を実装する

Angular Elementsの実装について説明していきます。実装の際のポイントは以下の通りです。

  • ブートストラップ処理の変更
  • Custom ElementsとしてホストされるAngularコンポーネントの作成
  • Custom Elementsの持っている機能にアクセスする

フォルダ構成

今回の実装では、ルートコンポーネントはapp.componentとして、実際の機能は新たにhello.componentを作成して、こちらで実装していくことにします。

app/
├─ hello/
│    └─ hello.component.ts
├─ app.component.ts
├─ app.module.ts
└─ index.html

ブートストラップ処理の変更

Angular Elementsは、自分自身でブートストラップを行います。Custom ElementsとしてDOMに追加されると自動的にブートストラップし、取り除かれると自分自身で破棄します。つまり、ここでは通常のAngularで行うブートストラップの処理は必要ありません。

ここでは、@angular/elementsモジュールのcreateCustomElementメソッドと、Custom Elementsで用意されているcustomElements.defineメソッドを使って、ブートストラップの処理を書きます。

まず、app.component.tsを開いて、以下のように修正します。 

import { Component, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { HelloComponent } from './hello/hello.component';

@Component({
  selector: 'app-hello',
  template: ``,
})
export class AppComponent  {
  constructor(
    private injector: Injector,
  ) {
    const AppHelloElement = createCustomElement(
      HelloComponent,
      { injector: this.injector }
    );
    customElements.define('app-hello', AppHelloElement);
  }
}
app.component.ts

createCustomElementメソッドで、HelloComponentをCustom Elementsに変換して、customElements.defineで、app-hello要素として扱えるように定義するといった処理の内容となります。この時、Custom Elementsとして定義する要素名とAppComponentのselector名は同じものとする必要があります

次にapp.module.tsを開いて、以下のように変更します。

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { HelloComponent } from './hello/hello.component';

@NgModule({
  imports: [BrowserModule, FormsModule],
  declarations: [AppComponent, HelloComponent],
  bootstrap: [AppComponent],
  entryComponents: [HelloComponent]
})
export class AppModule { }
app.module.ts

@NgModuleデコレータのbootstrapAppComponentを指定し、entryComponentsHelloComponentを指定します。これは、ブートストラップの対象をAppComponentとすると同時に、実際にCustom ComponentsとなるHelloComponentは、Angularアプリとは関係なくなるので、コンパイルの対象から外すということを意味しています。

FormsModuleは、ブートストラップには直接関係ありませんが、HelloComponentで実装する機能のために読み込んでいます。

ブートストラップに関する実装は以上となります。

Custom ElementsとしてホストされるAngularコンポーネントの作成

ブートストラップ周りの処理を実装したので、次に実際にCustom ElementsとしてホストされるAngularコンポーネントの作成について説明していきます。ここでは、HelloComponentとして新たに実装していきます。

新たに作成したhello.component.tsを開いて、以下のように記述します(templatestylesの部分は別ファイルとしても良いです)。

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-hello',
  template: `
    <div>
      <h1>Hello {{ name }}!!</h1>
      <input [(ngModel)]="value">
      <button (click)="handleClick()">Click!</button>
    </div>
  `,
  styles: [`
    :host {
      display: block;
      border: 1px solid black;
    }

    div {
      padding: 16px;
      text-align: center;
      font-family: Lato;
    }
  `]
})
export class HelloComponent  {
  value: string;

  @Input() name: string;
  @Output() clickButton: EventEmitter = new EventEmitter();

  handleClick() {
    this.clickButton.emit(this.value);
    this.value = '';
  }
}
hello.component.ts

ここは、普段通りAngularのコンポーネントを実装します。上記のサンプルのコードも特に難しいことはしていません。

なお、サンプルで@Input@Outputの2つのデコレータを利用していますが、@InputはCustom Elementsのプロパティとなり、@OutputはCustom Elementsのイベントとなるという点について覚えておくとよいでしょう。

つまり、以下のように実際にCustom Elementsとしてページに読み込んだ際に、name属性に指定した値は、HelloComponent内で@Inputを通して取得できますし、HelloComponent内で@Outputとして定義したイベントは、読み込んだページにてaddEventListenerを通して、イベントをハンドリングすることができます。

<app-hello name="Angular Elements!">loading</app-hello>

<script>
const appHello = document.querySelector('app-root');
appHello.addEventListener('clickButton', (e) => {
  // イベント発生時の処理
});
</script>
読み込み先のファイル

Custom Elementsの持っている機能にアクセスする

Angular Elementsは、さらにCustom Elementsの持っているHTML要素としての機能にアクセスする機能も持ち合わせています。@HostBinding@HostListenerの2つのデコレータを使います。この2つのデコレータは説明が難しいですが、@Input@Outputの逆の役割を持つものだと思ってください。

@HostBindingは、Custom Elementsの属性に値をバインドするためのデコレータです。@HostListenerは、Custom Elementsのイベントを受け取るためのデコレータです。

実際にサンプルのコードに落とし込んだものが以下となります。

import { Component, Input, Output, EventEmitter, HostListener, HostBinding } from '@angular/core';

@Component({
  selector: 'app-hello',
  template: `
    <div>
      <h1>Hello {{ name }}!!</h1>
      <input [(ngModel)]="value">
      <button (click)="handleClick()">Click!</button>
    </div>
  `,
  styles: [`
    :host {
      display: block;
      border: 1px solid black;
    }

    div {
      padding: 16px;
      text-align: center;
      font-family: Lato;
    }
  `]
})
export class HelloComponent  {
  value: string;

  @Input() name: string;
  @Output() clickButton: EventEmitter = new EventEmitter();
  @HostBinding('style.background-color') color = 'white';
  @HostListener('mouseover') onclick() {
    this.color = '#eee';
  }
  @HostListener('mouseout') onmouseout() {
    this.color = 'white';
  }

  handleClick() {
    this.clickButton.emit(this.value);
    this.value = '';
  }
}
hello.component.ts

上記のコードでは、@HostBindingを使って、Custom Elementsのstyle要素のbackground-colorにアクセスし、背景色をAngular Elements側から指定するようにしています。また@HostListenerを使って、Custom Elementsのmouseovermouseoutのイベントの発火を取得するようにしています。

ここまで紹介してきた機能で、ほぼほぼAngular Elementsの実装が行えるようになるかと思います。

プロダクションビルド

続いて、Angular Elementsとして実装したものを、他のサイトで読み込むためのファイルにビルドする方法を紹介します。

通常のビルド

まず通常のビルドを行うには、package.jsonに登録されている以下のビルド用のコマンドを実行します。このコマンドの実態は、ng buildコマンドです。

$ npm run build

プロダクションビルド

プロダクションビルドを行うには、package.jsonscriptフィールドに以下のコマンドを登録し、ターミナルでnpm run build:prodコマンドを実行します。UglifyJSによってファイルの圧縮や制限付きのデッドコードなどが削除されます。

"build:prod": "ng build --prod"

distディレクトリが新たに作成されて、その中にビルドされたファイルが出力されます。ただし、現状では生成されるファイルが、runtime.jspolyfills.jsscripts.jsmain.jsと4つもあります。この4つのファイルを全て読み込まないと、Custom Elementsが正常に動かないので、これだとせっかく作ったCustom Elementsも配布がしにくいですね。

出力されるファイルを1つにする

おそらく今後のバージョンアップでシングルファイルとして出力されるようになるかと思いますが、現状では自分でシングルファイルを吐き出すようにコマンドを作る必要がありますpackage.jsonscriptフィールドに以下のコマンドを登録します。

"build:elements": "ng build --prod --output-hashing=none && cat dist/angular-elements-sample/{runtime,polyfills,scripts,main}.js > dist/angular-elements-sample/app-hello.js",

ターミナルで上記で登録したコマンドを実行します。

$ npm run build:elements

このコマンドを実行すると、runtime.jspolyfills.jsscripts.jsmain.jsの4つのファイルが一つにまとめられて、app-hello.jsというファイル名でdistディレクトリ内のangular-elements-sampleディレクトリに出力されるようになります

ちなみに、--output-hashing=noneは、ファイル名にハッシュがつかないようにするためのオプションです。--prodを指定した場合、一緒にこのオプションを指定しなかったら、runtime.a66f828dca56eeb90e02.jsといったファイル名で出力されます。 

これでシングルファイルとして出力されるようになりますので、別のページで読み込む場合も、このファイルを読み込むだけで済むようになります。配布する場合もだいぶ楽になりますね。

Custom Elements として別の Web サイトで使用する

最後に出力したCustom Elementsファイルを、別のWebサイトで読み込んで使う方法を紹介します。

使い方は簡単で、上記で出力したファイルをscript要素で読み込み、Custom Elementsの要素を使用したい場所に設置します。設置したCustom Elementsの要素は、通常のHTMLの要素と同じように、style要素やscript要素を使って操作することが可能です。

実際のサンプルコードは以下のようになっています。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>AngularElementsSample</title>

    <!-- style要素内でCustom Elementに対する処理も書ける -->
    <style>
      .angular-elemtents-sample input {
        border: 2px solid #333;
        font-size: 16px;
        padding: 2px;
      }
      .angular-elemtents-sample button {
        background-color: #616161;
        color: #fff;
        padding: 4px 8px;
        font-size: 16px;
        border: none;
        cursor: pointer;
      }
    </style>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
    <!-- Custom Elementを表示させる -->
    <app-hello
      name="Angular Elements!"
      class="angular-elemtents-sample"
    >loading</app-hello>

    <!-- 出力されたファイルを読み込む -->
    <script src="app-hello.js"></script>
    <!-- script要素内でCustom Elementに対する処理も書ける -->
    <script>
      const appHello = document.querySelector('app-hello');
      appHello.addEventListener('clickButton', (e) => {
        console.log(e.detail);
        appHello.setAttribute('name', e.detail);
      });
    </script>
  </body>
</html>
読み込み先のファイル

まとめ

以上、Angular Elementsの使い方の紹介となります。おそらく記事を読むだけだと、わかりにくい部分があるかと思いますので、実際に実装されることをお勧めします。特に@Input@Outputなどのデコレータを使ったデータの入出力の部分など、実際に実装することで、使い方のポイントが見えてくるかと思います。

今回自分でAngular ElementsでCustom Elementsを作ってみて、やはり通常のHTMLと同様に扱えるようになるという点が面白いなと思いました。Custom Elements自体も正直よくわかっていなかった部分があるので、そういう意味でも今回Angular Elementsを通して、Custom Elementsの使い方がわかったのも収穫かなと思います。Angular Elementsが浸透したら、確実に世の中的にAngularの使用頻はこれまで以上に増えていくんじゃないかなと思います。ウィジェットやプラグインのようなものをAngularで作る文化が定着してくれると嬉しいですね。普及のなかなか進まないCustom Elements(Web Components)の普及にも一役買ってくれるとよいですね。

最後にAngularの公式ドキュメント以外に参考にしたサイトを紹介しておきます。

また、今回のサンプルのソースコードについては、GitHub上にもアップしています。ユニットテストなどもちょこっと書いたりしているので、こちらも併せて見てみてください。

関連記事

コメント

  • 必須

コメント