Buildless Angular app in single HTML file

概要

Skypack で提供されている ESM 形式の npm パッケージを、ブラウザの JavaScript から直接 import することによって Anguar を動作させることができた。

www.skypack.dev

実際のコード

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Buildless Angular app</title>
    <script type="module">
      import "https://cdn.skypack.dev/zone.js";
      import { Subject } from "https://cdn.skypack.dev/rxjs@7";
      import {
        filter,
        share,
        switchMap,
      } from "https://cdn.skypack.dev/rxjs@7/operators";
      import {
        Component,
        Inject,
        Injectable,
        NgModule
      } from "https://cdn.skypack.dev/@angular/core@12";
      import {
        BrowserModule
      } from "https://cdn.skypack.dev/@angular/platform-browser@12";
      import {
        platformBrowserDynamic
      } from "https://cdn.skypack.dev/@angular/platform-browser-dynamic@12";
      import {
        HttpClient,
        HttpClientModule,
      } from "https://cdn.skypack.dev/@angular/common@12/http";

      class AppService {
        static annotations = [new Injectable()];

        static parameters = [[new Inject(HttpClient)]];
        constructor(httpClient) {
          this.httpClient = httpClient;
        }

        static base = "https://jsonplaceholder.typicode.com";

        getUsers() {
          return this.httpClient.get(`${AppService.base}/users`);
        }

        getPosts(userId) {
          return this.httpClient.get(`${AppService.base}/posts`, {
            params: { userId },
          });
        }
      }

      class AppComponent {
        static annotations = [
          new Component({
            selector: "app-root",
            template: `
              <h4>Show Posts</h4>
              <label>
                Select User: 
                <select (change)="selectedUserId$.next($event.target.value)">
                  <option></option>
                  <option *ngFor="let user of users$ | async" [value]="user.id">
                    @{{user.username}}: {{user.name}}
                  </option>
                </select>
              </label>
              <ul>
                <li *ngFor="let post of posts$ | async">
                  {{post.title}}
                </li>
              </ul>
              <p>
                Powered by
                <a href="https://jsonplaceholder.typicode.com/" target="_blank">
                  JSONPlaceholder
                </a>
              </p>
            `,
          }),
        ];

        static parameters = [[new Inject(AppService)]];
        constructor(appService) {
          this.appService = appService;
          this.users$ = appService.getUsers().pipe(share());
        }

        selectedUserId$ = new Subject();

        posts$ = this.selectedUserId$.pipe(
          filter((userId) => userId > 0),
          switchMap((userId) => this.appService.getPosts(userId)),
          share()
        );
      }

      class AppModule {
        static annotations = [
          new NgModule({
            declarations: [AppComponent],
            imports: [BrowserModule, HttpClientModule],
            providers: [AppService],
            bootstrap: [AppComponent],
          }),
        ];
      }

      await platformBrowserDynamic().bootstrapModule(AppModule);
    </script>
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>