LoginSignup
0
2

More than 1 year has passed since last update.

[solidjs]solid一本で作る実用的なコンポーネントサンプル(code example)

Last updated at Posted at 2022-09-13

コードを試す

興味のあるコードを以下にコピぺして試すことができる
SoildJS Playground

split panel(画面分割)

ドラッグで画面分割サイズを変えることのできるコンポーネント

custom drag & drop(ドラッグ&ドロップ)

自前のロジックを使った拡張性の高いドラッグ&ドロップコンポーネント

import { createSignal } from "solid-js";

const Draggable = () => {
  const [mouseCoordinates, setMouseCoordinates] = createSignal({
    x: 0,
    y: 0,
  })
  const [dragElem, setDragElem] = createSignal<{
    zIndex: number | "auto",
    position: "static" | "relative" | "absolute" | "sticky" | "fixed",
    hidden: boolean,
  }>({
    zIndex: "auto",
    position: "static",
    hidden: false,
  })

  let dragElemRef: HTMLDivElement | undefined;
  let dropElemRef: HTMLDivElement | undefined;

  let startDragHandler = (e: MouseEvent) => {
    let belowElem: Element | null

    onmousemove = (e: MouseEvent) => {
      e.preventDefault()
      setDragElem({
        zIndex: 1000,
        position: "absolute",
        hidden: true,
      })
      belowElem = document.elementFromPoint(mouseCoordinates().x, mouseCoordinates().y)
      setDragElem({
        zIndex: 1000,
        position: "absolute",
        hidden: false,
      })
      setMouseCoordinates({
        x: e.clientX,
        y: e.clientY,
      })
      if (belowElem) {
        if (belowElem === dropElemRef) {
          console.log("drag enter");
        }
      }
    }

    onmouseup = (e: MouseEvent) => {
      setDragElem({
        zIndex: "auto",
        position: "static",
        hidden: false,
      })
      if (belowElem) {
        if (belowElem === dropElemRef) {
          console.log("drop in");
        }
      }
      onmousemove = () => null
      onmouseup = () => null
    }
  }

  return (
    <>
      <div
        ref={dragElemRef}
        style={{
          "z-index": `${dragElem().zIndex}`,
          "position": `${dragElem().position}`,
          "left": `${dragElemRef ? mouseCoordinates().x - dragElemRef.offsetWidth/2 : ""}px`,
          "top": `${dragElemRef ? mouseCoordinates().y - dragElemRef.offsetHeight/2 : ""}px`
        }}
        onMouseDown={startDragHandler}
        hidden={dragElem().hidden}
      >
        Drag me
      </div>
      <div
        ref={dropElemRef}
        style={{
          "height": "300px",
          "background-color": "rgba(120, 230, 60, 0.2)",
        }}
      >
        drop here
      </div>
    </>
  )
}

export default Draggable

risizeable floating window(可変フローティングウィンドウ)

コンポーネント側

// コンポーネント側
import { createSignal } from "solid-js";

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

const FloatingWindow: ParentComponent<{
  controllerWrapperClass?: string,
  floatingControlClass?: string,
  floatingControlContent: JSXElement,
  floatingContent?: JSXElement,
  cancelControlContent: JSXElement,
  cancelControlClass?: string,
  contentsWrapperClass?: string,
  wrapperClass?: string,
  defaultWindowSize: {
    width: number | "",
    height: number | "",
  }
}> = (props) => {
  const [mouseCoordinates, setMouseCoordinates] = createSignal({
    x: 0,
    y: 0,
  })
  const [floatingElem, setFloatingElem] = createSignal<{
    zIndex: number | "auto" | "",
    position: "static" | "relative" | "absolute" | "sticky" | "fixed",
    isFloating: boolean,
  }>({
    zIndex: "auto",
    position: "static",
    isFloating: false,
  })
  const [windowSize, setWindowSize] = createSignal({
    width: props.defaultWindowSize.width,
    height: props.defaultWindowSize.height,
  })

  let floatingElemRef: HTMLDivElement | undefined;
  let wrapperElemRef: HTMLDivElement | undefined;
  let contentsWrapperElemRef: HTMLDivElement | undefined;
  let resizeElemRef: HTMLDivElement | undefined;;

  let startDragHandler = (e: MouseEvent) => {
    onmousemove = (e: MouseEvent) => {
      e.preventDefault()

      setFloatingElem({
        zIndex: 1000,
        position: "fixed",
        isFloating: true,
      })
      setMouseCoordinates({
        x: e.clientX,
        y: e.clientY,
      })
    }

    onmouseup = () => {
      setFloatingElem({
        zIndex: 1000,
        position: "absolute",
        isFloating: true,
      })
      onmousemove = () => null
      onmouseup = () => null
    }
  }

  const startResize = (e: MouseEvent) => {
    console.log("wrapper offset height", wrapperElemRef?.offsetHeight)
    console.log("mouse client y height", e.clientY)

    onmousemove = (e: MouseEvent) => {
      e.preventDefault()
      if (wrapperElemRef && contentsWrapperElemRef && floatingElemRef && resizeElemRef) {
        console.log(e.clientY);
        setWindowSize({
          width: e.clientX - (wrapperElemRef.offsetLeft - resizeElemRef.offsetWidth),
          height: e.clientY - (wrapperElemRef.offsetTop + floatingElemRef.offsetHeight),
        })
      }
    }
    onmouseup = () => {
      console.log("stop resize");
      onmousemove = () => null
      onmouseup = () => null
    }
  }

  const cancelFloating = () => {
    setFloatingElem({
      zIndex: "auto",
      position: "static",
      isFloating: false,
    })
    if (floatingElemRef) {
      setMouseCoordinates({
        x: 0,
        y: 0,
      })
    }
  }

  return (
    <>
      <div
        ref={wrapperElemRef}
        style={{
          "z-index": floatingElem().zIndex,
          "position": `${floatingElem().position}`,
          "left": `${floatingElemRef ? mouseCoordinates().x - floatingElemRef.offsetWidth/2 : ""}px`,
          "top": `${floatingElemRef ? mouseCoordinates().y - floatingElemRef.offsetHeight/2 : ""}px`,
        }}
        class={props.wrapperClass}
      >
        <div
          style={{
            "width": floatingElem().isFloating ? windowSize().width +"px" : "",
          }}
          class={props.controllerWrapperClass}
        >
          <div
            ref={floatingElemRef}
            style={{
              "cursor": "move",
            }}
            onMouseDown={startDragHandler}
            class={props.floatingControlClass}
          >
            {props.floatingControlContent}
          </div>
          {props.floatingContent}
          <div
            onClick={cancelFloating}
            class={props.cancelControlClass}
          >
            {props.cancelControlContent}
          </div>
        </div>
        <div
          ref={contentsWrapperElemRef}
          style={{
            "position": "relative",
            "width": floatingElem().isFloating ? windowSize().width+"px" : "",
            "height": floatingElem().isFloating ? windowSize().height+"px" : "",  
          }}
          class={props.contentsWrapperClass}
        >
          {props.children}
          {floatingElem().isFloating
            && <div
            ref={resizeElemRef}
            style={{
            "position": "absolute",
            "right": "0",
            "bottom": "0",
            "z-index": floatingElem().zIndex,
            "background-color": "#e6e6fa",
            "cursor": "nwse-resize",
            }}
            onMouseDown={startResize}
          >
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
              <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 4.5l15 15m0 0V8.25m0 11.25H8.25" />
            </svg>
          </div>}
        </div>
      </div>
    </>
  )
}

export default FloatingWindow

呼び出し側
コンポーネント側にsolidjs単体で作ったが、呼び出し側は便利のためにtailwindcssとheroiconsを利用しています
このままコピぺすると見た目がややおかしくなるかもしれない

// 呼び出し側
import { FloatingWindow } from "@/components";

const FloatingPage = () => {
  return (
    <>
      <div
        style={{
          "height": "300px",
          "background-color": "rgba(120, 230, 60, 0.2)",
        }}
      >
        some contents
      </div>
      <FloatingWindow
        defaultWindowSize={{
          height: 200,
          width: 200,
        }}
        wrapperClass=""
        controllerWrapperClass="flex justify-between items-center border-2 rounded-lg mb-1 bg-red-200"
        floatingControlClass="w-fit"
        floatingControlContent={
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
            <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
          </svg>
        }
        floatingContent={
          <div>VideoSource</div>
        }
        cancelControlContent={
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
        }
        contentsWrapperClass="border-2 rounded-lg bg-red-200 h-full"
      >
        <div>
          Floating Contents 02
        </div>
      </FloatingWindow>
      <FloatingWindow
        defaultWindowSize={{
          height: 200,
          width: 200,
        }}
        wrapperClass=""
        controllerWrapperClass="flex justify-between items-center border-2 rounded-lg mb-1 bg-sky-200"
        floatingControlClass="w-fit"
        floatingControlContent={
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
            <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
          </svg>
        }
        floatingContent={
          <div>VideoSource</div>
        }
        cancelControlContent={
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
        }
        contentsWrapperClass="border-2 rounded-lg bg-sky-200 w-full"
      >
        <div>
          Floating Contents 01
        </div>
      </FloatingWindow>
    </>
  )
}

export default FloatingPage

全部propsに打ち込んで使っているので、呼び出し側の記述がやや苦しいです。さらにコンポーネントを細かく抽出して、コードの見栄えを良くしたほうがいいですね。
package用に汎用性を持たせていないため、実際に使うときは色々な場所を変更することになると思います。

props解説:

// componentに渡すprops
ParentComponent<{
  controllerWarpperClass?: string, // controllerWrapperのclass HTML要素を制御する
  floatingControlClass?: string, // dragして位置を移動させるところのclass
  floatingControlContent: JSXElement, // dragして移動させるところの見た目
  floatingContent?: JSXElement, // dragControlに位置しているが、dragやclickなどの操作に反応しない部分のcontent、説明とかを入れるところ
  cancelControlContent: JSXElement, // clickしたらfloatingしたコンテンツを元の位置に戻す
  cancelControlClass?: string, // cancelControlの見た目
  contentsWrapperClass?: string, // mainコンテンツ(つまりprops.children)を包むwrapperのclass
  wrapperClass?: string, // floatingWindowまるごと包んだ最上層divのclass
  defaultWindowSize: { // floatingした後のdefaultWindowSize
    width: number | "",
    height: number | "",
  }
}>

随時加筆

  • latest -> 20220914-1233JST[make floating window resizeable]
  • 20220913-1605JST[add floating window & dnd]
  • 20220913[init]
0
2
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
2