WebComponents: ReactNative.View のような CSS の既定値を持つだけの x-view を作ってみる

ReactNative 触った事ある人なら、ReactNative.View の iOS の UIView や Android View みたいな一貫性のある基底クラスが羨ましい人も多いと思う。 今の Webでは実質 div がそれを担っているわけだが、div になんでも押し付けるのはよくないという意見もわかるところがあり、それ専用の基底となる x-view 要素を作るとどうなるだろうか。

ほしいもの

  • デフォルトが display: flex;
  • デフォルトが box-sizing: border-box;

flex は便利だが Web のプリミティブ要素として既定値でflex を持つものはないので、毎度手数が多くなって嫌だなぁぐらいの気持ちがあった。

実装

とりあえず ReactNativeWeb がどう実装してるか真似しつつ実装した

const defaultViewStyle = document.createElement("style");
defaultViewStyle.textContent = `
  :host {
    border-width: 0;
    border-style: solid;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    margin: 0;
    padding: 0;
    position: relative;
    z-index: 0;
    min-height: 0;
    min-width: 0;
  }
`;

customElements.define(
  "x-view",
  class extends HTMLElement {
    constructor() {
      super();
      const shadow = this.attachShadow({ mode: "open" });
      shadow.appendChild(document.importNode(defaultViewStyle, true));
      shadow.appendChild(document.createElement("slot"));
    }
  }
);

children を展開する slot を持ち、自分自身(:host)にのみ適用するstyleを持つ。div を継承せず直接 HTMLElement の子要素になってる

style は毎度インラインのHTMLから生成するより、style を clone したほうが速いかも?と思ってこうしてるが、特に根拠はない。どうするのがいいんですかね。

これでこういうHTMLが動くようになる

<x-view style='height: 200px;'>
  <x-view style='flex: 1'>a</x-view>
  <x-view style='flex: 2'>b</x-view>
</x-view>

デフォルト display: flex なので flex 値の付与だけでいい。(実際こんなstyleを直接書く書き方はしないが、簡単のため)

TypeScript

この要素を使って、React で

ReactDOM.render(<x-view>a</x-view>, element); 

しようとすると、未定義要素を生成しようとしてるので怒られる。

React の型定義から、これを通す型宣言を追加する。

declare namespace JSX {
  interface IntrinsicElements {
    "x-view": React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement>,
      HTMLElement
    >;
  }
}

たぶんこんんあ感じ。

x-pane

x-view は ReactNative 互換を意識したのであんまり攻めた感じではなかったのだが、実際自分がCSS書く際は width: 100%; height: 100%; justify-content: center; align-items: center; を規定値として持つように作ることが多い。一般的なCSS仕草かは知らない。

名前思いつかないが、とりあえず x-pane とした。

const defaultPaneStyle = document.createElement("style");
defaultPaneStyle.textContent = `
  :host {
    align-items: center;
    border-width: 0;
    border-style: solid;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    height: 100%;
    justify-content: center;
    margin: 0;
    min-height: 0;
    min-width: 0;
    padding: 0;
    position: relative;
    width: 100%;
    z-index: 0;
  }
`;

customElements.define(
  "x-pane",
  class extends HTMLElement {
    constructor() {
      super();
      const shadow = this.attachShadow({ mode: "open" });
      shadow.appendChild(document.importNode(defaultPaneStyle, true));
      shadow.appendChild(document.createElement("slot"));
    }
  }
);

declare namespace JSX {
  interface IntrinsicElements {
    "x-pane": React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement>,
      HTMLElement
    >;
  }
}
<x-view style="height: 300px;">
  <x-view style="flex: 2; background: gray;">
    <x-pane style="color: green;">Pane0</x-pane>
  </x-view>
  <x-view style="flex: 1;">
    <x-view style="flex-direction: row; height: 100%;">
      <x-view style="flex: 3; background: #a88;">
        <x-pane>Pane1</x-pane>
      </x-view>
      <x-view style="flex: 1; background: #8a8;">
        <x-pane>Pane2</x-pane>
      </x-view>
    </x-view>
  </x-view>
</x-view>

https://camo.githubusercontent.com/259981a9bbd9f14b8d8429af76514ad480c2b7e0/68747470733a2f2f6779617a6f2e636f6d2f65653934353133346366373934313561343337326535623230366666356331632e706e67

みたいな感じになる。

感想

IE webcomponents ポリフィルフレンドリーな書き方するとまた別の書き方になると思う。さして手が増えるわけではないが。 正直、この程度だったら .view {} でよい。webcomponents があふれかえる時代になったら、基底要素としてこういう書き方しても怒られないかもしれない、ぐらいの気持ち。