Angular Elements でWeb Components化した Dialog をHugo に埋め込む

はじめに

Hugo のような静的なコンテンツを扱うフレームワークを利用している際に部分的に動的なガジェットが欲しくなることがあります。
Hugo が読み込める形で javascript のみでなんとかする方法はガジェットの規模によっては少ししんどく、外部ライブラリーを探して読み込む方法は、完全に要件を満たしていなかったり、依存関係やライセンスの問題など本質的ではない問題に当たってしまうことが多い印象です。

やはり慣れたフレームワークでシュッと作ってしまいたくなるわけです。
なのでAngular Elements で作ったアプリケーションを Hugo に埋め込む方法をまとめます。
今回は画像をクリックすると Modal としてクリックされた画像が浮かぶ簡単なアプリケーションを作成します。

実物が動いているページはこちら
また、Angularのソースコードは記事の最後にGithubのリンクを載せておきます。

f:id:nananao_dev:20191016093940p:plain
Angular Elements Modal

Angular Elements

Angular Elements とはなんでしょう。
Angular のドキュメントには以下のようにあります。

Angular Elements は、 Custom Elements (Web Componentsとも呼ばれます)としてパッケージ化される Angular コンポーネントです。Custom Elements は、フレームワークに依存しない形で新たな HTML 要素を定義するウェブ標準技術です。

angular.jp

難しいですね。
いい感じに埋め込めるガジェットが Angular で作れるようになったんだなぁくらいでいい気がします。

Angular CLI で 環境構築

現時点で Angular8 で環境構築を行います。
フットプリントの縮小が見込まれるため ivy を enable にしています。

$ ng new img-modal --enable-ivy --routing  --style=scss
$ cd img-modal
$ ng add @angular/material
? Choose a prebuilt theme name, or "custom" for a custom theme: Custom
? Set up HammerJS for gesture recognition? Yes
? Set up browser animations for Angular Material? Yes
$ ng add @angular/elements
$ ng serve

localhost:4200 にアクセスすると以下のような初期画面が表示されます。
最近とてもかっこよくなりました。

f:id:nananao_dev:20191016102856p:plain
angular start image

Angular Elements の作成

まずはコンポーネントを作成します。
私の構成だと作ったコンポーネント名が Web Components として利用する HTML タグの一部として利用することになります。

$ ng g component img-modal

app.component の設定

app.component.html は router-outlet のみ残します。

<router-outlet></router-outlet>

app.component.ts は以下のように編集します。
注意点としては selectorapp-root から先ほど作成したimg-modal Component の selector である app-img-modal を設定します。
あとは createCustomElement として先ほど作成したimg-modal Component を登録します。

import { Component, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { ImgModalComponent } from './img-modal/img-modal.component';

@Component({
  selector: 'app-img-modal',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  constructor(
    private injector: Injector,
  ) {
    const AppImgModalElement = createCustomElement(
      ImgModalComponent,
      { injector: this.injector }
    );
    customElements.define('app-img-modal', AppImgModalElement);
  }
}

app.module.ts は以下のように設定します。
entryComponents として ImgModalComponent を登録しています。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';

// material
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatDialogModule } from '@angular/material/dialog';

// component
import { AppComponent } from './app.component';
import { ImgModalComponent } from './img-modal/img-modal.component';

@NgModule({
  declarations: [
    AppComponent,
    ImgModalComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MatDialogModule
  ],
  providers: [],
  bootstrap: [AppComponent],
  entryComponents: [
    ImgModalComponent
  ]
})
export class AppModule { }

img-modal.component の設定

img-modal.component.ts を設定します。
今回は Hugo に埋め込んだ際の HTML から src として画像の URL と alt として alt をもらう想定です。
DialogComponent は Angular Material のDialog の使い方そのものですが @Inject(MAT_DIALOG_DATA) public data: ImageData としてデータの受け渡しをしてます。

import { Component, Inject, Input } from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';

@Component({
  selector: 'app-img-modal',
  templateUrl: './img-modal.component.html',
  styleUrls: ['./img-modal.component.scss']
})
export class ImgModalComponent {
  @Input() src: string;
  @Input() alt?: string;

  constructor(
    public dialog: MatDialog
  ) { }

  openDialog(): void {
    const dialogRef = this.dialog.open(DialogComponent, {
      width: '90%',
      data: {src: this.src, alt: this.alt}
    });
  }
}

@Component({
  selector: 'app-dialog',
  templateUrl: 'dialog.html',
})
export class DialogComponent {

  constructor(
    public dialogRef: MatDialogRef<DialogComponent>,
    @Inject(MAT_DIALOG_DATA) public data: ImageData,
  ) {}

}

img-modal.component.html は以下の通りです。
ただの img タグです。クリックすると Dialog が開きます。

<img [src]="src" [alt]="alt" (click)="openDialog()">

dialog.html として Dialog として浮かんでくる HTML を書きます。
今回は雑にもらったデータをそのまま表示します。

<img [src]="data.src" [alt]="data.alt" height="100%" width="100%">

Angular Elements の表示

さて、このままでは ng serve の結果は何も表示されていません。
それは index.html として <app-root></app-root> を表示しているためです。
ng serve でも Angular Elements をデバッグできるように index.html を以下のように編集します。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>ImgModal</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
  <app-img-modal src="https://placehold.it/900x600" alt="This is test image"></app-img-modal>
</body>
</html>

これで以下のように表示されたはずです。

f:id:nananao_dev:20191017075618p:plain
angular modal example

Angular Elements の build

Angular は単一の jsファイルとして build されません。
また、Angular8 から es5 と es2015 で別ファイルになっています。
なので以下のように package.jsonスクリプトで es5 と es2015 それぞれ単一の jsファイルにまとめています。

"scripts": {
    "build:elements": "ng build --prod --output-hashing=none && cat dist/img-modal/{runtime-es5,polyfills-es5,scripts,main-es5}.js > dist/img-modal/app-img-modal-es5.js && cat dist/img-modal/{runtime-es2015,polyfills-es2015,scripts,main-es2015}.js > dist/img-modal/app-img-modal-es2015.js",
  },

上記 scripts を実行します。

$ npm run build:elements

すると dist/img-modal/ 配下に es5 と es2015 それぞれのファイルが build されています。
enable ivy してこの結果です。 もう少し小さくなってくれると嬉しいですね。

$ ls -lh dist/img-modal/
total 4208
-rw-r--r--  1 nao  staff   433K 10 17 08:08 app-img-modal-es2015.js
-rw-r--r--  1 nao  staff   569K 10 17 08:08 app-img-modal-es5.js
-rw-r--r--  1 nao  staff    61K 10 17 08:08 styles.css

Hugoに埋め込み

build した jsファイルと css ファイルを Hugo に埋め込みます。
今回はただファイルを読み込むのみなので static フォルダ配下に cssフォルダを作成し styles.css をコピーします。また、jsフォルダを作成し app-img-modal-es2015.jsapp-img-modal-es5.js 両方をコピーして置いておきます。

partials/head.html から読み込みます。

<script type="module" src="{{ .Site.BaseURL }}/js/app-img-modal-es2015.js"></script>
<script src="{{ .Site.BaseURL }}/js/app-img-modal-es5.js" nomodule defer></script>

<link rel="stylesheet" href="{{ .Site.BaseURL }}/css/styles.css">

あとは template から以下のように利用します。

<app-img-modal src="https://source.unsplash.com/random/512x512">

まとめ

Angular Elements でWeb Components化した Dialog をHugo に埋め込む方法をまとめました。
簡単なガジェットが Angular でかけるのはありがたいですね。色々触っていこうと思います。

github.com