[solidjs]ドラッグで画面分割するコンポーネントを作ってみた

## solidjsとは何か

solidjsはreact likeで書ける今注目を浴びているフロントエンドのライブラリーの１つです。
その設計哲学はreactに深く影響されており、コードもreactによく似ています。しかし最大の違いはsolidjsではバーチャルDOMを使っていない点です。そしてこのおかげでreactとvueなどのライブラリーより早いパフォーマンスを得ることができた。

reactとの違いはこちらの記事で上手くまとめられていますので、興味のある方はぜひ：

https://qiita.com/shadowTanaka/items/b6d00863a8d6bff37de6

現有のライブラリーとの違いは詳しく公式ページをご覧ください：
https://www.solidjs.com/guides/comparison

## この記事を書こうとしたきっかけ

しかしその若さ故に、特定の問題を解決してくれるライブラリーなどはまだまだ少ない。例えばvueのsplitpaneとreactのreact-split-paneなど簡単に画面分割してくれるものはない。なので試しに作ってみた。

## コード
[成果物](https://playground.solidjs.com/?version=1.4.1#NobwRAdghgtgpmAXGGUCWEwBowBcCeADgsrgM4Ae2YZA9gK4BOAxiWGjIbY7gAQi9GcCABM4jXgF9eAM0a0YvADo1aAGzQiAtACsyAegDucAEYqA3EogcuPfr2ZCouOAGU0Ac2hqsvWhABZBgg+aTkFZVUNbT0LKysbbj4CYnsABSghEIBhBS4IYVxfAClXAA0AUTU4eBCpWXlFAHI6aN0yJssIK2Z-Mj4MgoBNRF4MrNxczn9CgB4QK15eXFpCKprR0srq2qLF3hNaXBWYdZhN8rPCq0kAPl4AXl4ACkJ5QjIASkf7hYgl3oQfq8YAACzgnlBRV4ZDguAAEhCPFCALqPBxOFzuLxQNTPAAMn321T4hCgBVyIXQBUYACU4DJRvCACoBAAyABE0AA3K51AA+vHoogZGDgIi6xLhfkCDFhHNohgg8PJImqEiezzgoyC9FhFW5hW+D1++yW-hgcpqtEN6K1OqtBqNP34ZqWMLhiMhuC1ADpmBpCkMif93ZI3RarfRCHbtbxdfrDSFjabQ+6ZZa9dbbZqU7wIPQ1Go3eaIJnYdG7XmC0W3eHQ-X9v5dSFns883902gZC8yRT-LhqeJ6TJvp30x6EUioa9yXBKYOxXSGf7AyEvci+PpeAAmEPp+tLSQh-ZCXBMf7PN2zW4l3izEQ8u9LIQyB4gPvzgdD5cyQ8TpZ+nwap33HACliaR8yEINQoHwJpRiaGRqgoJosGfd0kJQrRkMVBDeCaXo1HoGAIDQjCIPBb18KaABGfF8QAUnItMJ0kf901vVj0wfHkYQIECQDA8Cmiozd8IAAwAEg-OcFx-EdeAAfheMSZ2+bdP3kpcR1XNBCg3KFPgAKno-FeFGABWfFJEYiT0O4icmhMKBmAAaw8eRhW0IjuBoxgPBc55aJ3fFfBCsLdwAZki-FfT3JoMPYrjwPdD93jIX0VjWHYOInWZ9EfbkUvA3juQomUEzgBUlXfZsrRq5VVXVPKAKAkDEscgCYAwLRDE0XBQSswgKC6VL0x6iAtDU3BhtGiqlhc9zPOCHz1G4UYAGJmHxHadrG8aASYOhGFGeRDC0IQyDQAAvOADtSzrUtuAqipKgCyoq9q4FAhaCJmySZK078dIZZSXjM3gtFU6cfQ03hgapUGZCUvSDNhkyzO+KybLshzDogpaPK80QtF8xh-MCqBgtC3wdxi8Lad4OKEoq9iMPe8D0tWTLDmOBQzla91Xp5TmlhF4rr30EqQ0bCBAWBQY4DKUZxkKKZ8jmcdqhkXAzgubYakKfHBG9fXeC2Pk9ggO47TeHmO32BW+GAfqREG3xYVwAB1AbQTRJ5HDgZw3E8bwCX3XgSQRuSQZpEcmVZTkeSt3hBW80UCgleJQ2j+qs0alVRHVWMHSzJ1kxdYTIyzS0cxeOMqor3BHccr3ffd0E-QDfSQjKSOjwjMsoxjTVG8dJMW6ru8a9hOu4CrF0a2LRzZ7gStcyXwsV4PfY5dLFsfXbafWO7XtY6R+OGTHO92792d+0v4cVx7woO8G3htz3Os95PUMzwvC8a8Yt7xFQwq+d8iNFxXz-Bhb6v0uqYSgjBOCNFkJwFQibR66CKA4TUHhRC50WKHVErDGiZlmJYJEm7Qa5CGKUKSkLEB4sir8WAj9ISFUmg0NBIDWSj9oHPx7CpZ4PDj6aQvoI38aMQjv1BJjBiFleDWVsvZLhRMVreTJutCmhCqY00ihFOmDNmbxU+E9cCyUKrcw+L6HWetcoYQlsw0BT5EEHwaoqCAdVZQFy8UXNU4ghbpngRYw6k0+p+zmg9caESZrRL+hokma18GnV4NtXamSYmpWYMdDaDh1CXTgNdO62SnIc2cU4sB7i2GCWEo9HhfCoEKTBiIyG0NRH33hs05GqNX6yL9gowkSiVF4z+s5VyxNVraNSZTIKRjoqGKZizcxbNgm8BcUsGxmVGBm0cYgypjlDk8WlvsWWOcZDCmYLgNA-gxhQA8HAY+44AGMEvNeVh8CVDxN4GZbkoIVAgNmErIYGFspnHfBVYFc4yh-XsRC+p40yq3HscsDBuBjkE3WemXZm4EV-RYaLXFUI0UUAxYVUWf1sUbIKkrMoLj1l8xOPimpyKjjggkC4MlmKAJMNpXOIYnNMXnO6BALIYhGBtjzNCx5n9bi+BELQXJuxfSPIcUbEIAAhfAABJEQV4wBQEIIQFQnwACEnwuhgEkCiIAA)

依頼関係は一切ない。solidjs単体で実現できます。
リンクは効かない、もしくは挙動がおかしい場合はご自身でsolidjs playgroundを開いて、ページ最下部のコードを貼り付けて、ご確認してください。

[solidjs playground](https://playground.solidjs.com/?version=1.3.13#NobwRAdghgtgpmAXGGUCWEwBowBcCeADgsrgM4Ae2YZA9gK4BOAxiWGjIbY7gAQi9GcCABM4jXgF9eAM0a0YvADo1aAGzQiAtACsyAegDucAEYqA3EogcuPfr2ZCouOAGU0Ac2hqps+YpU6DW09CysrGXoIZlw0WgheAGEGCBdGAAoASn4rXgd4sj5gZhTcLF4yOFxkqNwAXV4AXgcnF3cvKDV0gAZMywT8iELeDEc4eFSm3iymgD4KqprU9JLamYBqXgBGPvCBoVwmBPTcvN4AHhN6XFx43gJiRpUrm-iVXnjEjWYAa0aQUZCCa4SSzU5nfirZaZSTgi76F63CBgga7CCwiBWISicTpGaNebnJZpXj6WblES0Zj0YEAOg8VQAompxsJcAAhfAASREJzAUEIhBUmTRYEkdSAA)

では詳しくコードを見てみましょう

```tsx
// 垂直方向で分割するコンポーネント
import { createSignal, onMount } from 'solid-js';

import type { ParentComponent, JSXElement } from 'solid-js';

const PaneY: ParentComponent<{
  topElem: JSXElement,
  bottomElem: JSXElement
}> = (props) => {
  const [height, setHeight] = createSignal(0)
  let paneContainerRef: HTMLDivElement | undefined;

  let onMouseDownHandler = (e: MouseEvent) => {
    onmousemove = (e: MouseEvent) => {
      console.log('on mouse move', e.clientY);
      setHeight(e.clientY)
    }
    onmouseup = (e: MouseEvent) => {
      console.log('mouse up', e.clientY);
      onmousemove = () => null
      onmouseup = () => null
    }
    console.log('on mouse down', e.clientY);
  }

  onMount(() => {
    if (paneContainerRef) {
      setHeight(paneContainerRef.clientHeight / 2)
    }
  })

  return (
    <>
      <div
        ref={paneContainerRef}
        style={{
          'display': 'flex',
          'flex-flow': 'column',
          'height': '100%',
        }}
      >
        <div style={{
          'height': `${paneContainerRef ? (height() / paneContainerRef.clientHeight)*100 : 50}%`,
          'background-color': 'rgba(120, 120, 230, 0.2)'
        }}>
          {props.topElem}
        </div>
        <div
          onMouseDown={onMouseDownHandler}
          style='
            min-width: 5px;
            min-height: 5px;
            background-color: #c0c0c0;
            cursor: row-resize;
          '
        ></div>
        <div
          style={{
            'height': `${paneContainerRef ? (100 - (height() / paneContainerRef?.clientHeight)*100) : 50}%`,
            'background-color': 'rgba(120, 230, 120, 0.2)'
          }}
        >
          {props.bottomElem}
        </div>
      </div>
    </>
  )
}

export default PaneY
```
簡単にコードを解説します。
drag eventをではなく、mouse eventを使って制御します。（dragイベントはdefaultで要素を複製してしまうので、見栄えはよくない）
1. divタグにevent listenerを追加し、mouse downイベントを監視します
1. mouse　downすると同時にmouse moveイベントを監視します
1. mouse moveするたびにheightをsetします
1. heightを基準に要素の高さのパーセントを計算して、styleを変更（solidjsが直接文字列のstyleを受け付けるおかげで簡単にできた）
1. mouse onとする同時にmouse moveとmouse onの監視を閉じます、するとheightが最後のmouse moveの位置になります

```tsx
// 水平方向で分割するコンポーネント
import { createSignal, onMount } from 'solid-js';

import type { ParentComponent, JSXElement } from 'solid-js';

const PaneX: ParentComponent<{
  leftElem: JSXElement,
  rightElem: JSXElement,
}> = (props) => {
  const [width, setWidth] = createSignal(0)
  let paneContainerRef: HTMLDivElement | undefined;

  let onMouseDownHandler = (e: MouseEvent) => {
    onmousemove = (e: MouseEvent) => {
      console.log('on mouse move', e.clientX);
      setWidth(e.clientX)
    }
    onmouseup = (e: MouseEvent) => {
      console.log('mouse up', e.clientX)
      onmousemove = () => null
      onmouseup = () => null
    }
    console.log('on mouse down', e.clientX);
  }

  onMount(() => {
    if (paneContainerRef) {
      setWidth(paneContainerRef.clientWidth / 2)
    }
  })

  return (
    <>
      <div
        ref={paneContainerRef}
        style={{
          'display': 'flex',
          'flex-flow': 'row',
          'height': '100%',
          'width': '100%',
        }}
      >
        <div style={{
          'width': `${paneContainerRef ? (width() / paneContainerRef.clientWidth)*100 : 50}%`,
          'background-color': 'rgba(120, 120, 230, 0.2)'
        }}>
          {props.leftElem}
        </div>
        <div
          onMouseDown={onMouseDownHandler}
          style='
            min-width: 5px;
            min-height: 5px;
            background-color: #c0c0c0;
            cursor: col-resize;
          '
        ></div>
        <div
          style={{
            'width': `${paneContainerRef ? (100 - (width() / paneContainerRef?.clientWidth)*100) : 50}%`,
            'background-color': 'rgba(120, 230, 120, 0.2)'
          }}
        >
          {props.rightElem}
        </div>
      </div>
    </>
  )
}

export default PaneX
```

水平方向も同じで、計算に使うheightをwidthに変えただけです。

そして呼び出し側は
```tsx
// import pathはご自身のpathに変えてください
import { PaneY, PaneX } from "../components"

const Page = () => {
  return (
    <>
      <PaneY
        topElem={
          <PaneX
            leftElem={
              <div>left text</div>
            }
            rightElem={
              <div>right text</div>
            }
          ></PaneX>
        }
        bottomElem={
          <div>other text</div>
        }
      ></PaneY>
    </>
  )
}

export default Page
```

簡単さを追求して複数のchildrenをpropsとして渡して処理しているので、呼び出しはちょっと見苦しいです。分割の中に再分割する場合は、そのelemの中に`Pane`を渡してあげればいい。


## solidjs playgroundにコピぺするためのコード：
```tsx
import { render } from "solid-js/web";
import { createSignal, onMount } from "solid-js";

import type { ParentComponent, JSXElement } from 'solid-js';

const PaneY: ParentComponent<{
  topElem: JSXElement,
  bottomElem: JSXElement
}> = (props) => {
  const [height, setHeight] = createSignal(0)
  let paneContainerRef: HTMLDivElement | undefined;

  let onMouseDownHandler = (e: MouseEvent) => {
    onmousemove = (e: MouseEvent) => {
      setHeight(e.clientY)
    }
    onmouseup = (e: MouseEvent) => {
      onmousemove = () => null
      onmouseup = () => null
    }
  }

  onMount(() => {
    if (paneContainerRef) {
      setHeight(paneContainerRef.clientHeight / 2)
    }
  })

  return (
    <>
      <div
        ref={paneContainerRef}
        style={{
          'display': 'flex',
          'flex-flow': 'column',
          'height': '100%',
        }}
      >
        <div style={{
          'height': `${paneContainerRef ? (height() / paneContainerRef.clientHeight)*100 : 50}%`,
          'background-color': 'rgba(120, 120, 230, 0.2)'
        }}>
          {props.topElem}
        </div>
        <div
          onMouseDown={onMouseDownHandler}
          style='
            min-width: 5px;
            min-height: 5px;
            background-color: #c0c0c0;
            cursor: row-resize;
          '
        ></div>
        <div
          style={{
            'height': `${paneContainerRef ? (100 - (height() / paneContainerRef?.clientHeight)*100) : 50}%`,
            'background-color': 'rgba(120, 230, 120, 0.2)'
          }}
        >
          {props.bottomElem}
        </div>
      </div>
    </>
  )
}

const PaneX: ParentComponent<{
  leftElem: JSXElement,
  rightElem: JSXElement,
}> = (props) => {
  const [width, setWidth] = createSignal(0)
  let paneContainerRef: HTMLDivElement | undefined;

  let onMouseDownHandler = (e: MouseEvent) => {
    onmousemove = (e: MouseEvent) => {
      setWidth(e.clientX)
    }
    onmouseup = (e: MouseEvent) => {
      onmousemove = () => null
      onmouseup = () => null
    }
  }

  onMount(() => {
    if (paneContainerRef) {
      setWidth(paneContainerRef.clientWidth / 2)
    }
  })

  return (
    <>
      <div
        ref={paneContainerRef}
        style={{
          'display': 'flex',
          'flex-flow': 'row',
          'height': '100%',
          'width': '100%',
        }}
      >
        <div style={{
          'width': `${paneContainerRef ? (width() / paneContainerRef.clientWidth)*100 : 50}%`,
          'background-color': 'rgba(120, 120, 230, 0.2)'
        }}>
          {props.leftElem}
        </div>
        <div
          onMouseDown={onMouseDownHandler}
          style='
            min-width: 5px;
            min-height: 5px;
            background-color: #c0c0c0;
            cursor: col-resize;
          '
        ></div>
        <div
          style={{
            'width': `${paneContainerRef ? (100 - (width() / paneContainerRef?.clientWidth)*100) : 50}%`,
            'background-color': 'rgba(120, 230, 120, 0.2)'
          }}
        >
          {props.rightElem}
        </div>
      </div>
    </>
  )
}

function Page() {
  return (
    <div style="height: 100vh">
      <PaneY
        topElem={
          <PaneX
            leftElem={
              <div>left text</div>
            }
            rightElem={
              <div>right text</div>
            }
          ></PaneX>
        }
        bottomElem={
          <div>other text</div>
        }
      ></PaneY>
    </div>
  )
}

render(() => <Page />, document.getElementById("app")!);
```