AngularElementsでAMP htmlを動的にロードする

本記事ではAngularElementsを使ってAMP htmlをロードするという試みをします。
AMPは、ページを高速に表示してSEOがよくなるやつですね。[^1]


# Angular Elementsについて

ブートストラップ以下のツリーを通さず**直接DOMにAngularComponentを追加できるようになる**です。

ユースケースは[1年前の記事](https://qiita.com/studioTeaTwo/items/13d3eed751781bf039fc)でごちゃごちゃ書きましたが、相変わらず2つパラレルに走っています。

## Angularアプリ内でElementsを使う方法

公式ドキュメントのサンプルが現在このユースケースです。Angularアプリ内で動的コンポーネントをどうやって実現するかという方向です。サンプルでは、AngularComponentでそのままやる方法と、customElementを用いる方法の2つが併記されています。

[「サンプル: ポップアップサービス」](https://angular.jp/guide/elements#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB-%E3%83%9D%E3%83%83%E3%83%97%E3%82%A2%E3%83%83%E3%83%97%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9)

## 単発のcustomElementsとしてビルドして配布し、他のアプリケーションから使用する方法

この記事が非常にクオリティ高いです。
customElementのブートストラップや他のelementとの外部インターフェースについて理解が深まります。結論としてはivy待ちですね。

[「Angular Elementsことはじめ – Custom Elementsを実装する方法」](https://mae.chab.in/archives/60134)

また、公式ドキュメントですが、[配布用のユースケースを載せてよ](https://github.com/angular/angular/issues/26981)というイシューが上がっているため、そのうち出てきそうです。

# AMP htmlをAngularElementsでロードする

- AngularElementsを使います。
- サーバからAMP htmlを読み込み、elementsのiframeにセットします。

## 実装

公式ドキュメントのサンプルをベースに進めていきます。

### AMP表示用のcomponentを用意する

いつものようにcomponentを作ります。iframeタグを用意して`srcdoc`属性にXHRで取得したAMPhtmlソースをセットしようとしています。このcomponentは後にcusutomElement化します。そうすると`@Input`がHTMLElementのproperty属性になります。

```ts:amp-display.component.ts
@Component({
  selector: 'app-amp-display',
  template: `
    <iframe [srcdoc]="safeMsg"></iframe>
    <button (click)="closed.next()">&#x2716;</button>
  `
})
export class AmpDisplayComponent {
  safeMsg: SafeHtml;

  // customElement化した時にproperty属性になります。
  @Input()
  set message(message: string) {
    this.safeMsg = this.sanitizer.bypassSecurityTrustHtml(message);
  }

  @Output()
  closed = new EventEmitter();

  constructor(private sanitizer: DomSanitizer) {}
}
```

### customElement化する

ここは公式ドキュメントのサンプルそのままです。ソースコメントにいたっては完全にコピペです。
AppComponentのコンストラクタ内で先ほどのcomponentをNgElementという変換器にかけた上で`window.customElements`に登録します。これでもう標準のカスタム要素になりました。DOM APIから`<app-amp-display>`で操作可能です。逆にAngular内で扱おうとすると、例えばtemplateに`<app-amp-display>`をセットしても様々な困難がやってきます。

```ts:app.component.ts
@Component({
  selector: 'app-root',
  template: `
    <button (click)="ampDisplay.showAsElement()">Show as element</button>
  `
})
export class AppComponent {
  constructor(injector: Injector, public ampDisplay: AmpDisplayService) {
    // Convert `PopupComponent` to a custom element.
    const AmpDisplayElement = createCustomElement(AmpDisplayComponent, {
      injector
    });
    // Register the custom element with the browser.
    customElements.define('amp-display-element', AmpDisplayElement);
  }
}
```

### AMPを読み込む

serviceを起こして、`responseType: 'text'`でサーバからAMPソースを読み込むようにします。AMPソースは、URLを叩けば普通に表示できる`<html>`タグ以下のフルhtmlドキュメントです。

```ts
@Injectable()
export class AmpDisplayService {
  private ampUrl = 'http://localhost:8080/amp';

  constructor(
    private http: HttpClient
  ) {}

  private getContent() {
    return this.http.get(this.ampUrl, { responseType: 'text' });
  }
}
```

### Angularアプリ内からcustomElementを呼び出す

serviceにcustomElementをDOMに付与するメソッドを追加します。すでにwindowには登録済みなので、`document.createElement()`で作成できるようになっています。`@Input() message`がHTMLElementのproperty属性として変換されているため、取得したAMPソースを渡してやります。

```ts
@Injectable()
export class AmpDisplayService {
  private ampUrl = 'http://localhost:8080/amp';

  constructor(
    private injector: Injector,
    private http: HttpClient
  ) {}

  // This uses the new custom-element method to add the popup to the DOM.
  showAsElement() {
    // Create element
    const ampDisplayEl: NgElement &
      WithProperties<AmpDisplayComponent> = document.createElement(
      'amp-display-element'
    ) as any;

    // Listen to the close event
    ampDisplayEl.addEventListener('closed', () =>
      document.body.removeChild(ampDisplayEl)
    );

    this.getContent().subscribe(text => {
      // Set the message
      ampDisplayEl.message = text;
      // Add to the DOM
      document.body.appendChild(ampDisplayEl);
    });
  }
}
```

### APMコンテンツを用意する

以下のAMPチュートリアルを参考にHTMLドキュメントを用意しました。expressサーバから供給します。

https://www.ampproject.org/ja/docs/getting_started/create/basic_markup

## 結果

ボタンを押すと、AMPソースをサーバーから取得して、トーストに表示しています。グレーの背景がAMPソースです。

![トリム.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/194428/545eca78-4d09-97f1-0d4f-9a7263c2312d.gif)

## リポジトリ

今回のコードはこちらにアップしています。

https://github.com/studioTeaTwo/elements-amp

# ほんとうにやりたかったこと

正直、iframeは逃げました。ほんとうにやりたかったことは、iframeの代わりにshadowDOMを使ってAMPを表示する[^2]という話がありました。そこまで来るとAngularElementsを使ってcustomElementにする意味もあるというもので。しかし、shadowRootに`<html>`タグ以下をそのまま追加すればいいってものでもなく、AMP projectで用意されている`window.AMP.attachShadowDoc()`という謎モジュールの解明で時間切れとなりました。reactのサンプル実装があるので、ご興味ある方はトライしてみてください。

[「AMP を埋め込んでデータソースとして使用する」](https://www.ampproject.org/ja/docs/integration/pwa-amp/amp-in-pwa)

デモサイト[^3]を見るとおおよそやってることはわかるかと思います。今回やろうとしていた特定領域に出力先をコントールするのではなくドキュメント全体に適用するような手法で、実はそれならAngularでも`window.AMP.attachShadowDoc()`を使ってできました。機会を見てまた記事にするかもしれません。


明日は@Czernyさんです。

[^1]: https://www.ampproject.org/ja/learn/overview/
[^2]: https://html5experts.jp/shumpei-shiraishi/24795/
[^3]: https://choumx.github.io/amp-pwa AMPhtmlから`<style>`と`<body>`タグだけを取り出してshadowRootに適用している感じです。`<meta>`や`<script>`はホストのreactドキュメントに適用しているような動きに見えます。`shadow-v0.js`というのがOSS化されてないと思うので出力結果からの憶測です。