3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-12

solidjsとは何か

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

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

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

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

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

コード

成果物

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

solidjs playground

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

// 垂直方向で分割するコンポーネント
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イベントを監視します
  2. mouse downすると同時にmouse moveイベントを監視します
  3. mouse moveするたびにheightをsetします
  4. heightを基準に要素の高さのパーセントを計算して、styleを変更(solidjsが直接文字列のstyleを受け付けるおかげで簡単にできた)
  5. mouse onとする同時にmouse moveとmouse onの監視を閉じます、するとheightが最後のmouse moveの位置になります
// 水平方向で分割するコンポーネント
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に変えただけです。

そして呼び出し側は

// 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にコピぺするためのコード:

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")!);
3
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?