Open10

Web Components用フレームワークhybrids

北市真北市真

https://hybrids.js.org/
Web Componentsは、コンポーネントを作るライブラリーはたくさんあるんだけど、Reactとかぐらいのレベルの「フレームワーク」はないなあと思って探していたら見付けた。
結構面白そうで、ドキュメントを読んだら触ってみたい。

北市真北市真

JavaScriptオブジェクトの「モデル」があり、コンポーネントはそれを表現したり、ユーザーインタラクションによってモデルに変更を加えたりする。

北市真北市真

そのためにstoreという物が使われている。これが中々面白い。と同時に独特だから可搬性は低そう。

北市真北市真

でも今は、可搬性とかいいからいい感じに使えるフレームワークがあった方がいい時代だと思う。

北市真北市真

モデルはオブジェクトだが、なるべく関数型で扱おうとしている。
例えばこんなコードがある:

import { User } from "./models.js";

function submit(host, event) {
  event.preventDefault();

  // Creates a real `User` model instance
  store.submit(host.user).then(() => {
    // Clears values in the form
    host.user = null;
  });
}

const CreateUserForm = {
  user: store(User, { draft: true }),
  render: ({ user }) => html`
    <form onsubmit="${submit}">
      <div class="${ error: store.error(user, "firstName") }">
        <input defaultValue="${user.firstName}" oninput="${html.set(user, "firstName")}">
      </div>

      <div class="${ error: store.error(user, "lastName") }">
        <input defaultValue="${user.lastName}" oninput="${html.set(user, "lastName")}">
      </div>
    </form>
  `,
}

store.error(user, "firstName")userオブジェクトのfirstNameプロパティがエラーかどうか、エラーならそのエラーを返す関数。エラーかどうかを調べるのにuserにエラーオブジェクトなどを持たせるのではなく、関数で取得しているのが特徴的というか、オブジェクト指向脳のうちは慣れない。

北市真北市真

こんなふうにモデル定義をする:

const User = {
  id: true,
  firstName: "",
  lastName: "",
  ...,
};

const Profile = {
  user: User,
  isAdmin: false,
  ...,
};

TypeScriptの型やインターフェイスを使うのではなくJavaScriptで定義している。

北市真北市真

TypeScriptでモデルのインターフェイスを定義することもできるけど、結局JavaScript(ランタイム)でも定義はしないといけない:

import { store, Model } from "hybrids";

interface User {
  id: string;
  name: string;
};

const User: Model<User> = {
  id: true,
  name: "",
  [store.connect]: (id) => fetch(`/users/${id}`).then(res => res.json()),
};

export default User;

https://hybrids.js.org/#/store/typescript

北市真北市真

独特すぎてこれ、他のウェブコンポーネントと相互作用できるんだろうか。我々は普段HTMLXxxElementインターフェイス相手にプログラミングするわけだけど、そういうふうになるの?

北市真北市真

こういうコンポーネントを作ったとする。

export default define<AppLayout>({
  tag: 'app-layout',
  sidebar: store(Sidebar),
  render: ({sidebar}) => html`
    <div class="header">
      <slot name="header"></slot>
    </div>
    <div class="body">
      <div class="main">
        <slot name="main"></slot>
      </div>
      <div class="sidebar" aria-hidden="${sidebar.open ? 'false' : 'true'}">
        <slot name="sidebar"></slot>
      </div>
    </div>
  `
});

hybridsだとこのグローバルステートであるsidebar: store(Sidebar)を使って.sidebarの開閉を制御するしかないように思うのだけど、本当は<app-layout aria-expanded="true">とかやって開閉したい。JavaScriptのレベルでは

appLayout.ariaExpanded = true;

とか。

つまり

  • 親コンポーネントから<app-layout>の属性を設定することで開閉させたい
  • その属性の値を元にSidebarステートも更新したい
    ということなのだけど、レンダリングが純粋関数でしてしまっているので属性の変更 -> ステートの更新という順番で処理できない気がする。