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を開いて、ページ最下部のコードを貼り付けて、ご確認してください。
では詳しくコードを見てみましょう
// 垂直方向で分割するコンポーネント
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で要素を複製してしまうので、見栄えはよくない)
- divタグにevent listenerを追加し、mouse downイベントを監視します
- mouse downすると同時にmouse moveイベントを監視します
- mouse moveするたびにheightをsetします
- heightを基準に要素の高さのパーセントを計算して、styleを変更(solidjsが直接文字列のstyleを受け付けるおかげで簡単にできた)
- 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")!);