【必読】なぜ今、Aura ではなく LWR を選ぶべきなのか?

【決定版】LWC設定画面の究極形!60以上の設定項目をJSON管理と部品化で完全制覇する


LWRサイトを細部までカスタマイズしようとすると、必然的に設定項目は増えていきます。 過去の記事で紹介した「Dark Mode Toggle」は、テキストや背景だけでなく、リストビューのヘッダー、ボタンの全ステート(Hover/Focus)、フォームの枠線など、60以上のCSS変数を制御する高機能コンポーネントです。

これを標準の js-meta.xml で定義すると、設定画面は無限スクロールとなり、運用者は「どこを変更したかわからない」「元に戻せない」というストレスを抱えます。

今回は、以下の技術を組み合わせて、この問題を根本から解決する「究極の管理画面」を構築します。

  1. JSON一元管理: 60個の値を1つのプロパティに集約する。
  2. UIの部品化: カラーピッカーとリセットボタンをセットにした「設定部品」を作る。
  3. 個別リセット: 項目ごとに「未設定(デフォルト)」に戻せる機能を提供する。

これらをCustom Property Editor (CPE) の技術をベースに実装しています。
CPEについて詳しくは以下をご覧ください。

DXforce Point for Developers

今回の記事で作成した「ダークモード切り替えコンポーネント」の全ソースコードをGitHubで公開しています。 ぜひ Clone して、あなたの組織で動かしてみてください。

ステップ1: 設定部品コンポーネント (c-removable-color-picker)

60回も同じHTMLを書くのは非効率です。まずは、「カラーピッカー」と「状態表示バッジ」、そして「リセットボタン」がセットになった子コンポーネントを作成します。

HTML (removableColorPicker.html)

<template>
    <div class="slds-form-element slds-m-bottom_x-small">
        <label class="slds-form-element__label">{label}</label>
        <div class="slds-form-element__control">
            <div class="slds-grid slds-gutters_x-small slds-grid_vertical-align-center">
                <div class="slds-col slds-grow-none">
                    <input 
                        type="color" 
                        class="slds-input slds-p-around_none"
                        style="height: 32px; width: 50px;"
                        value={internalValue} 
                        onchange={handleChange}>
                </div>
                <div class="slds-col slds-grow">
                    <lightning-input 
                        type="text" 
                        variant="label-hidden"
                        value={internalValue}
                        pattern="^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
                        onchange={handleChange}
                        placeholder="#RRGGBB">
                    </lightning-input>
                </div>
            </div>

            <div class="slds-grid slds-gutters_x-small slds-grid_vertical-align-center">
                <div class="slds-m-around_x-small">
                    <template if:true={hasValue}>
                        <span class="slds-badge slds-theme_success">設定中: {value}</span>
                    </template>
                    <template if:false={hasValue}>
                        <span class="slds-badge">デフォルト</span>
                    </template>
                </div>

                <template if:true={hasValue}>
                    <div class="slds-m-around_x-small">
                        <lightning-button-icon 
                            icon-name="utility:undo" 
                            variant="border-filled" 
                            alternative-text="リセット" 
                            onclick={handleClear}>
                        </lightning-button-icon>
                    </div>
                </template>
            </div>
        </div>
    </div>
</template>

JavaScript (removableColorPicker.js)

import { LightningElement, api } from 'lwc';

export default class RemovableColorPicker extends LightningElement {
    @api label; 
    @api name; 
    @api value;

    /**
     * 値が設定されているか(バッジ表示用)
     * null, undefined, 空文字以外なら true
     */
    get hasValue() {
        return this.value != null && this.value !== '';
    }

    /**
     * カラーピッカーに渡す値
     * 余計なデフォルト値を設定せず、nullならnullを渡します。
     * (lightning-input type="color" は null を受け取るとブラウザ標準の色を表示しますが、
     * hasValueがfalseなので「デフォルト」バッジが表示され、区別がつきます)
     */
    get internalValue() {
        return this.value;
    }

    handleChange(event) {
        // 値が空文字の場合は null として扱う
        const val = event.target.value;
        this.dispatchChange(val === '' ? null : val);
    }

    handleClear() {
        this.dispatchChange(null);
    }

    dispatchChange(val) {
        this.dispatchEvent(new CustomEvent('colorchange', {
            detail: {
                name: this.name,
                value: val
            }
        }));
    }
}

ステップ2: 親エディタコンポーネント (c-dark-mode-configurator)

作成した部品を使い、アコーディオンの中に並べていきます。 部品化したおかげで、HTMLが劇的に読みやすくなります。

HTML (darkModeConfigurator.html)

<template>
    <div class="slds-p-around_x-small">
        <div class="slds-grid slds-grid_align-spread slds-m-bottom_small" style="align-items: center;">
            <span class="slds-text-heading_small">ダークモード設定</span>
            <template if:true={isModified}>
                <lightning-button-icon icon-name="utility:delete" variant="border-filled" 
                    alternative-text="全リセット" onclick={handleResetAll} title="全項目を未設定に戻す">
                </lightning-button-icon>
            </template>
        </div>
        <div class="slds-text-body_small slds-text-color_weak slds-m-bottom_medium">
            ※ 未設定の項目は、ライトモード(標準)の色がそのまま使用されます。
        </div>

        <lightning-accordion allow-multiple-sections-open active-section-name="basic">
            // ここに各種設定を記述します(詳細はGitHubをご覧ください)
        </lightning-accordion>
    </div>
</template>

JavaScript (darkModeConfigurator.js)

import { LightningElement, api, track } from 'lwc';

export default class DarkModeConfigurator extends LightningElement {
    // 内部保持用の変数
    _value;
    
    // 設定オブジェクト(画面表示用)
    @track settings = {};

    /**
     * @api value の Getter
     * Builderが値を取得する際に呼ばれます
     */
    @api
    get value() {
        return this._value;
    }

    /**
     * @api value の Setter
     * Builderから保存されたデータが渡された瞬間に実行されます
     * connectedCallbackよりも確実です
     */
    set value(v) {
        this._value = v;
        if (v) {
            try {
                // 保存されたJSON文字列をオブジェクトに戻す
                this.settings = JSON.parse(v);
            } catch (e) {
                console.error('JSON Parse Error', e);
                this.settings = {};
            }
        } else {
            // 値がない場合は初期化
            this.settings = {};
        }
    }

    /**
     * 何か一つでも設定されているか判定
     */
    get isModified() {
        return Object.keys(this.settings).length > 0;
    }

    /**
     * 子コンポーネントからの変更通知を一括処理
     */
    handleColorChange(event) {
        const { name, value } = event.detail;

        // null または 空文字なら設定から削除(リセット)
        if (value === null || value === '') {
            // リアクティブ性を保つため、オブジェクトを複製して削除
            const newSettings = { ...this.settings };
            delete newSettings[name];
            this.settings = newSettings;
        } else {
            // 値があれば更新
            this.settings = { ...this.settings, [name]: value };
        }
        
        this.notifyChange();
    }

    /**
     * 全リセットボタン
     */
    handleResetAll() {
        this.settings = {};
        this.notifyChange();
    }

    /**
     * Builderに変更を通知
     */
    notifyChange() {
        // 設定オブジェクトをJSON文字列に変換して渡す
        const newValue = JSON.stringify(this.settings);
        
        // 自身の_valueも更新しておく(ループ防止のため本来はチェックが必要だが、今回は簡易実装)
        this._value = newValue;

        this.dispatchEvent(new CustomEvent('valuechange', {
            detail: { value: newValue }
        }));
    }
}

ステップ3: メタデータ (ultimateDarkModeSwitcher.js-meta.xml)

60行あったプロパティ定義は、この1つの定義で完結します。

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>65.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Dark Mode Switcher Ultimate</masterLabel>
    <targets>
        <target>lightningCommunity__Page</target>
        <target>lightningCommunity__Default</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightningCommunity__Default">
            <property 
                name="themeSettings" 
                type="String" 
                label="テーマ設定 (JSON)" 
                editor="c/darkModeConfigurator" 
            />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

ステップ4: メインロジック (ultimateDarkModeSwitcher.js)

最後に、受け取ったJSONをパースして適用します。 以前は60個の @api 変数がありましたが、それらはすべてJSONのキーとして扱います。

import { LightningElement, api } from 'lwc';

export default class DarkModeSwitcherUltimate extends LightningElement {
    @api themeSettings;

    isDark = false;

    // ...

    getSettings() {
        try {
            return this.themeSettings ? JSON.parse(this.themeSettings) : {};
        } catch (e) {
            return {};
        }
    }

    applyTheme() {
        const style = document.documentElement.style;
        const settings = this.getSettings(); // JSONから設定を取得
        const darkVars = this.getCssVariableMap(settings); // 設定を渡してマップ生成
        const darkKeys = Object.keys(darkVars);

        if (this.isDark) {
            // ダークモード適用: 設定値があるものだけ書き換える
            darkKeys.forEach(key => {
                if (darkVars[key]) style.setProperty(key, darkVars[key]);
            });
            // 強制スタイル注入
            this.injectOverrideStyles(settings);
        } else {
            // ダークモード解除
            darkKeys.forEach(key => {
                style.removeProperty(key);
            });
            // ライトモード設定など(必要に応じて)
            // ...
        }
    }

    // ... injectOverrideStyles も同様に settings引数 を使うように修正 ...

    /**
     * settingsオブジェクトを受け取ってマップを生成
     * ※ これまでは this.colorRoot でしたが s.colorRoot に変わります
     */
    getCssVariableMap(s) {
        return {
            // --- 基本 ---
            '--dxp-g-root': s.colorRoot,
            '--dxp-g-root-contrast': s.colorText,
            '--dxp-g-brand': s.colorBrand,
            '--dxp-g-brand-contrast': s.colorBrandContrast,
            // ... 他60項目すべて同様にマッピング ...
        };
    }
}

まとめ

このアーキテクチャを採用することで、以下の「UX革命」が起きました。

  1. 個別リセットが可能に: 「あ、ボタンの色だけデフォルトに戻したい」という時も、横の×ボタンを押すだけです。
  2. 管理画面がスッキリ: 60個の項目が初期状態ではコンパクトに収まっています。
  3. 安全な運用: 設定していない項目(null)は、サイト本来のデザイン(ライトモード)を維持します。

「多機能」と「使いやすさ」はトレードオフではありません。Custom Property Editor の力を引き出せば、その両方を満たす素晴らしいコンポーネントが作成可能です。

DXforceの管理人

福島 瑛二

2013年にJavaエンジニアとしてのキャリアをスタート。2019年にSalesforceと出会い、Salesforceエンジニアの道へ。

デザインや UI/UX の観点からもシステムを捉え、ユーザーにとって心地よい体験を実装することにやりがいを感じています。

CRM(顧客データ)や Data Cloud と連携した高度なサイトを目に見える形で表現できる Experience Cloud に大きな可能性を見出しており、バックエンドのデータ構造とフロントエンドの表現力を極めることがこれからの Salesforce エンジニアに求められるスキルだと確信しています。

Trailblazer: efukushima

福島 瑛二をフォローする

読者の声

タイトルとURLをコピーしました