16
9

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.

Svelteを使う上での小技集

Posted at

はじめに

皆さんSvelte使ってますか。
https://svelte.dev/
Svelteは少ないコード量でハイパフォーマンスが出せるReact、Vueに次ぐ新しいフレームワークです。
非常に便利で重宝しているのですが、実務で使うにあたりちょっと足りない機能があったりします。
Reactだと標準機能になっている部分をちょっとした小技で補っているのでいくつかご紹介します。

小技集

オブジェクトでのstyle指定

Reactだと出来たオブジェクトでのstyle指定がなぜかSvelteだとできないのです。

Reactの場合
<div style={{ width: 100, display:'flex' }} />

Svelteの場合は以下のように通常とHTMLと同じように文字列で指定することしかできません。
他にもクラス名+CSSでのスタイルを記述することも可能ですが、インラインで書きたいときには不便です。

Svelteの場合
<div style="width:100px;display:flex" />

そんな私が使っているのが、オブジェクトStyleを文字列に変換してくれる関数です。

style.ts
import type { CSSObject } from '@emotion/css';

const keyWithPx: (keyof CSSObject)[] = [
    'width',
    'maxWidth',
    'minWidth',
    'height',
    'minHeight',
    'maxHeight',
    'top',
    'bottom',
    'left',
    'right',
    'fontSize',
    'padding',
    'margin',
    'marginTop',
    'marginRight',
    'marginBottom',
    'marginLeft',
    'paddingTop',
    'paddingRight',
    'paddingBottom',
    'paddingLeft',
    'borderRadius',
    'outlineOffset'
];

export const styleToString = (style: CSSObject) => {
    return Object.keys(style).reduce((acc, key) => {
        const value = style[key];
        const styleKey = key
            .split(/(?=[A-Z])/)
            .join('-')
            .toLowerCase();

        return (
            acc +
            (value != null
                ? `${styleKey}:${
                      typeof value === 'number' && keyWithPx.includes(key)
                          ? `${value}px`
                          : value
                  };`
                : '')
        );
    }, '');
};

export class Style {
    cssObject: CSSObject;

    constructor(cssObject?: CSSObject) {
        this.cssObject = cssObject ?? {};
    }

    toString() {
        return styleToString(this.cssObject);
    }
}

これによって以下のような記述が可能になります。
Reactより多少長いですが、型補完も効くので便利です。
widthdisplayなどが詰まった連想配列の型は@emotion/cssからお借りしています。
Reactと同じように、widthheightなどは数値でも指定できるようにしています。

<div style={new Style({ width: 100, display: 'flex' }).toString()} />

ReactPortal

Reactには親コンポーネントのDOM階層外にあるDOMノードに対して子コンポーネントをレンダーするための仕組みを提供してくれています。
https://ja.reactjs.org/docs/portals.html
UIコンポーネントでいうところのモーダルなどを作る際に便利だったりしますが、Svelteの公式からはこのような機能は提供されていません。

私はこのような独自のPortalコンポーネントで補っています。

Portal.svelte
<script context="module" lang="ts">
    import { tick } from 'svelte';

    type Target = HTMLElement | string;

    export function portal(el: HTMLElement, target: Target = 'body') {
        let targetEl;
        async function update(newTarget: Target) {
            target = newTarget;

            if (typeof target === 'string') {
                targetEl = document.querySelector(target);
                if (targetEl === null) {
                    await tick();
                    targetEl = document.querySelector(target);
                }
                if (targetEl === null) {
                    throw new Error('no element found');
                }
            } else if (target instanceof HTMLElement) {
                targetEl = target;
            } else {
                throw new TypeError();
            }

            targetEl.appendChild(el);
            el.hidden = false;
        }

        function destroy() {
            if (el.parentNode) {
                el.parentNode.removeChild(el);
            }
        }

        update(target);

        return {
            update,
            destroy
        };
    }
</script>

<script lang="ts">
    export let target: HTMLElement | string = 'body';
</script>

<div use:portal={target} hidden>
    <slot />
</div>

Svelteのディレクティブ機能を使って実装をしています。
コンポーネントのtargetプロパティに渡したDOMのに、子コンポーネントがレンダリングされます。

以下のように使うことができます。
この場合はTooltipコンポーネントがdivの直下ではなく、bodyの直下に描画されます。

portalSample.svelte
<div>
    <Portal target={document.body}>
        <Tooltip />
    </Portal>
</div>

その他

SvelteにはErrorBoundaryの機能がありません。(ReactでいうところのSuspenseコンポーネント)
そのため、一部でエラーが出た際にはそのエラーがルートのコンポーネントまで伝わり、画面全体が固まります。
これについては、様々な解決策を出しているサイトを見ましたがどれも上手くいかないので、現状改善は難しいと思われます。
本格的な対策は公式の対応を待つとしても、なるべくエラーを起こさないことがとりあえずの対策になるかと思います。

私のお勧めはTypeScriptを使うのは大前提ですが、中でもtsconfigのnoUncheckedIndexedAccesstrueにすると良いと思います。
これは連想配列などでプロパティが存在しない可能性も考慮して、プロパティの中身をxxx | undefinedという型に推論してくれるというものになります。
個人的な感覚ですが、TypeScriptで書いても発生するJavaScriptエラーの大半がこの型の不一致によるものだと思うので、この設定を追加することで多くのエラーをコンパイル時に見つけることができます。

const obj: Record<string, string> = {};
obj1.foo;
// on : string | undefined
// off: string

おわりに

いかがでしたでしょうか。
他にも小技を見つけ次第追記していこうと思います。

16
9
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
16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?