2019-08-20に投稿

HTMLのノードを直接生成するJSXファクトリを簡単に書いてみた

JSXって知っていますか? JSXとは、Reactなどに使われているXML(HTML)とjavascriptの混在記法です。
そして、JSXを内部的に処理する関数をJSXファクトリといいます。
これは、BabelやTypeScriptコンパイラの設定で自由に変更可能です。
ところでWeb Componentsという、新しいHTMLエレメントを自由に作れるjavascriptのAPIが存在するのはご存じでしょうか。

これを使えば、Reactのようなことがネイティブのjavascriptでできるのではないだろうか?そう考えたのでやってみることにしました。

まずはJSXファクトリの仕様から説明します。
JSXファクトリは以下のようなシグネチャを持つ関数であれば何でも構いません。

/**
 * @param element 文字列または関数またはクラス。文字列の場合はタグの名前、クラス・関数の場合はタグが表すコンポーネント。
 * @param props 要素の属性。オブジェクトで渡す。存在しない場合はnull。
 * @param children 可変長引数で、現在の要素の子要素を渡す。
 */
function JSXFactory(element, props, ...children) {}

そしてこれは重要なことなのですが、この関数が返す値や動作は何でもよいのです。
JSXとは、単なる糖衣構文でしかありません。つまり、その動作は実装した人にゆだねられます。
以下に、JSXの変換例を示します。

import React from 'react';

let node = <div className="hoge">This is child text.</div>;

これは以下のようなjavascriptに変換されます。

import React from 'react';

let node = React.createElement('div', {'className': 'hoge'}, 'This is child text');

そしてこの、React.createElementにあたる関数はtsconfig.json内で以下のように設定できます。

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "myCreateElement"
  }
}

この場合はjsxFactoryにmyCreateElementを設定しているため、React.createElement関数の代わりにmyCreateElementが使われます。
さて、これらの仕様を使って、普通のHTMLノードを生成するJSXファクトリを作ってみたいと思います。
まずは型の宣言から。

declare namespace JSX {
  export interface JSXElement<P = {}> {}
  export interface IntrinsicElements {
    [elemName: string]: any;
  }
  export type GlobalAttributes = EventAttributes | ElementAttributes;
  export type ElementAttributes = /* HTMLノードのグローバル属性(略) */
  export type EventAttributes = /* イベント属性(略) */
}

シンプルに行きましょう。
組み込みの要素の型をいちいち定義していたら日が暮れます。
なのでanyで誤魔化しました。

続いては本題のJSXファクトリの実装です。

import './types';

type PropType<K> = { [P in JSX.GlobalAttributes | keyof K]: any };

export function createElement<K, E extends JSX.JSXElement<K>>(
  element: E | string,
  props: PropType<K>,
  ...children: any[]
): Element {
  let elem: Element;
  if (typeof element === 'string') {
    elem = document.createElement(element);
  } else {
    elem = document.createElement(
      (element as any).name.replace(/(?!^)([A-Z0-9])/g, '-$1').toLowerCase()
    );
  }
  Object.keys(props || {})
    .filter(it => props.hasOwnProperty(it))
    .forEach(it =>
      elem.setAttribute(convertPropName(it), props[it as keyof PropType<K>])
    );
  children.forEach(it =>
    !(it instanceof Element)
      ? it.hasOwnProperty('render') && typeof it.render === 'function'
        ? elem.appendChild(it.render())
        : elem.append(it.toString())
      : elem.appendChild(it)
  );
  return elem;
}

function convertPropName(name: string): string {
  switch (name) {
    case 'className':
      return 'class';
    case 'htmlFor':
      return 'for';
    default:
      return name;
  }
}

やってること自体はきわめて単純。
document.createElement関数でノードを作り、引数に従って属性や子要素を追加しているだけです。
JSXではclassとforがそれぞれclassNameとhtmlForになるため、convertPropName関数はそれを変換します。

にしてもJSXファクトリ、かなり面白いですね。
これ使えばいろいろできそうな予感。
React以外でも活用の目はありそうです。

Originally published at www.tech-frodo.xyz

frodo821

Pythonをこよなく愛するプログラマ。広く浅くアンテナを張っている(つもり)。 出来ることと言えばフロントエンド全般やJava、Kotlinを使ったアプリ開発、PHP/Pythonでバックエンドが少々にC#、Python、Java、Kotlin、Scalaなどを使ったデスクトップ開発くらいのもの。PyPIで自作ライブラリ公開したりもしている。https://pypi.org/project/rattlepy/ 好きなフロントエンドフレームワークはReact、バックエンドフレームワークはCodeIgniterとMasonite。自分のサイトもReactを使って構築した。

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!

ボードとは?

関連記事

コメント