LoginSignup
0
1

Custom Elementsでslotを1回だけ書いて何度も利用する

Posted at

はじめに

以前slotを何度も利用したいという記事を書きました

ここではslotの名前を変えて対処を行いましたが、1つのslotを使い回す方法がわかったのでまとめます

やり方

まずはコードをすべて載せます

index.tsx
import { FC, useEffect, useRef, useState } from "react";
import { createRoot, Root } from "react-dom/client";
import { CacheProvider, css, EmotionCache } from "@emotion/react";
import createCache from "@emotion/cache";
import resetCss from "./reset.css?inline";

const useSlot = (ref: React.RefObject<HTMLSlotElement>) => {
  const [childNodes, setChildNodes] = useState<ChildNode[]>();
  useEffect(() => {
    const { current } = ref;
    if (!current) return;
    setChildNodes(current.assignedNodes() as ChildNode[]);
  }, []);
  return childNodes;
};


const SampleElement: FC = () => {
  const slot = useRef<HTMLSlotElement>(null);
  const result = useSlot(slot);

  if (result && result.length > 0) {
    const [children] = result;
    return (
      <>
        {[...Array(5)].map((_, n) => (
          <div key={n}>
            <span ref={(ref) => ref?.appendChild(children.cloneNode(true))}>
              {n}
            </span>
          </div>
        ))}
      </>
    );
  }

  return (
    <>
      <slot name="test" ref={slot} style={{ display: "none" }} />
      <div>no children</div>
    </>
  );
};

class SampleElementComponent extends HTMLElement {
  root: Root;
  cache: EmotionCache;
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.root = createRoot(this.shadowRoot!);
    this.cache = createCache({
      key: "sample-element",
      container: this.shadowRoot!,
    });
    this.setResetCss();
  }

  setResetCss() {
    const style = document.createElement("style");
    style.innerHTML = resetCss;
    this.shadowRoot!.appendChild(style);
  }

  connectedCallback() {
    this.root.render(
      <CacheProvider value={this.cache}>
        <SampleElement />
      </CacheProvider>
    );
  }
}

customElements.define("sample-element", SampleElementComponent);
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./src/index.tsx" type="module"></script>
    <div>
      <sample-element>
        <span slot="test">slot show</span>
      </sample-element>
    </div>
  </body>
</html>

解説

  const slot = useRef<HTMLSlotElement>(null);
  const result = useSlot(slot);

useRefを利用してスロットを直接WebComponentsから参照できるようにします
はじめはnullの状態でuseSlotが呼び出されます

const useSlot = (ref: React.RefObject<HTMLSlotElement>) => {
  const [childNodes, setChildNodes] = useState<ChildNode[]>();
  useEffect(() => {
    const { current } = ref;
    if (!current) return;
    setChildNodes(current.assignedNodes() as ChildNode[]);
  }, []);
  return childNodes;
};

useSlotが実行されます。childNodesというステートでスロットに割り当てられたノードを管理しています
コンポーネントのマウント時に引数で渡ってきたrefから要素(current)を取得します

しかしこの段階ではnullなので、ifでreturnされてuseSlotは終わります

その後以下が描画されます

  return (
    <>
      <slot name="test" ref={slot} style={{ display: "none" }} />
      <div>no children</div>
    </>
  );

ref={slot}と指定しているので、この要素がDOMにマウントされたタイミングで、slot.currentはこの要素を指すようになります。

すると、useSlotが再度実行されます

const useSlot = (ref: React.RefObject<HTMLSlotElement>) => {
  const [childNodes, setChildNodes] = useState<ChildNode[]>();
  useEffect(() => {
    const { current } = ref;
    if (!current) return;
    setChildNodes(current.assignedNodes() as ChildNode[]);
  }, []);
  return childNodes;
};

今回はcurrentにノードがあるので、setChildNodesにノードがセットされて返却されます
するとresultが存在するため以下が描画されます

  if (result && result.length > 0) {
    const [children] = result;
    return (
      <>
        {[...Array(5)].map((_, n) => (
          <div key={n}>
            <span ref={(ref) => ref?.appendChild(children.cloneNode(true))}>
              {n}
            </span>
          </div>
        ))}
      </>
    );
  }

ここで、<span ref={(ref) => ref?.appendChild(children.cloneNode(true))}>とすることで先程要素に対してrefコールバックを設定しています。
このコールバックは、要素がDOMにマウントされるタイミングで呼ばれ、その要素への参照(この場合はref)が引数として渡されます。

appendChildでノードをクローンして追加しています
これを行うことで先程のuseSlotの返却した<slot name="test" ref={slot} style={{ display: "none" }} />の要素が使えるようになります

slotを埋め込んだ状態のものをそのままコピーすることができるので何度も同じslotが利用できます

おわりに

教わったときにすごい仕組みだなと思ったので処理を追ってまとめてみました

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1