LWC

LWCのレコードアクションで画面を順に切り替える実装

1つの画面に項目を詰め込むと、入力の負担が増えます。ステップに分けて、入力済みの値を保ったまま前後できるレコードアクションの作り方を書きます。

2024.04.30

なぜ画面を分けるのか

取引先責任者を作るのに、日付・数値・テキストをまとめて1画面に並べると、必須項目の見落としが増えます。ステップに分けて1画面1テーマにすると、入力の負担が下がります。

ここで作るのは、取引先のレコードページに置くクイックアクションです。ボタンを押すと、日付選択・数値入力・テキスト入力の3画面が順に開き、前の画面へ戻っても入力した値は消えません。

ページそのものを移動するNavigationMixinとは違います。ここでの切り替えは、1つのコンポーネントの中でテンプレートの表示・非表示を切り替えるだけで、URLは変わりません。同じ複数画面の入力を画面フロー(Screen Flow)で作ることもできますが、既存のクイックアクションの見た目に合わせたいときはLWCを選びます。

画面構成

lightning-progress-indicatorで現在位置を示し、template if:trueでステップごとの中身を出し分けます。

stepsAccountScreenActionSample.html

<template>
    <div style="margin-top: 20px">
        <lightning-progress-indicator current-step={currentStep}>
            <lightning-progress-step label="日付を選択する" value="step1" onclick={handleClickStep}></lightning-progress-step>
            <lightning-progress-step label="数値項目を入力する" value="step2" onclick={handleClickStep}></lightning-progress-step>
            <lightning-progress-step label="テキスト項目を入力する" value="step3" onclick={handleClickStep}></lightning-progress-step>
        </lightning-progress-indicator>
    </div>
    <template if:true={isStep1}>
        <lightning-quick-action-panel header="日付を選択する">
            <lightning-layout multiple-rows="true">
                <lightning-combobox name="selectMonth"
                    label="月"
                    options={monthOptions}
                    dropdown-alignment="left"
                    value={month}
                    required
                    onchange={month_handleOnChange}>
                </lightning-combobox>
            </lightning-layout>
            <div slot="footer">
                <lightning-button variant="neutral" label="キャンセル" onclick={handleClickCancel} class="slds-m-left_x-small"></lightning-button>
                <lightning-button variant="brand" label="次へ" onclick={handleClickNext} disabled={isNotSelected} class="slds-m-left_x-small"></lightning-button>
            </div>
        </lightning-quick-action-panel>
    </template>
    <template if:true={isStep2}>
        <lightning-quick-action-panel header="数値項目を入力する">
            <lightning-input type="number" label="数値" value={num} required onchange={num_handleOnChange}></lightning-input>
            <div slot="footer">
                <lightning-button variant="neutral" label="戻る" onclick={handleClickPrev} class="slds-m-left_x-small"></lightning-button>
                <lightning-button variant="brand" label="次へ" onclick={handleClickNext} disabled={isNotSelected} class="slds-m-left_x-small"></lightning-button>
            </div>
        </lightning-quick-action-panel>
    </template>
    <template if:true={isStep3}>
        <lightning-quick-action-panel header="テキスト項目を入力する">
            <lightning-input type="text" label="テキスト" value={text} required onchange={text_handleOnChange}></lightning-input>
            <div slot="footer">
                <lightning-button variant="neutral" label="戻る" onclick={handleClickPrev} class="slds-m-left_x-small"></lightning-button>
                <lightning-button variant="brand" label="作成" onclick={handleClickNext} class="slds-m-left_x-small"></lightning-button>
            </div>
            <template if:true={executing}>
                <lightning-spinner alternative-text="実行中です" variant="brand"></lightning-spinner>
            </template>
        </lightning-quick-action-panel>
    </template>
</template>

進捗表示と3つのステップ画面のマークアップです。テンプレートの出し分けだけで、URL遷移はありません。

lightning-progress-indicatorのcurrent-stepは、lightning-progress-stepのvalueと一致させます。値が食い違うと、進捗の見た目と表示中のステップがずれます。

画面の切り替えとレコード作成

stepsAccountScreenActionSample.js

import { api, wire, LightningElement } from "lwc";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import { CloseActionScreenEvent } from "lightning/actions";
import { CurrentPageReference } from "lightning/navigation";
import { RefreshEvent } from "lightning/refresh";
import getAccountId from "@salesforce/apex/stepsAccountScreenActionSampleController.getAccountId";
import createContact from "@salesforce/apex/stepsAccountScreenActionSampleController.createContact";

export default class StepsAccountScreenActionSample extends LightningElement {
    month;
    num;
    text;
    executing = false;
    currentStep = "step1";
    @api accountId;
    @api recordId;

    @wire(CurrentPageReference)
    async getStateParameters(currentPageReference) {
        this.recordId = currentPageReference.state.recordId || null;
        try {
            this.accountId = await getAccountId({ recordId: this.recordId });
        } catch (error) {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: "エラーが発生しました",
                    message: error.body.message,
                    variant: "error"
                })
            );
        }
    }

    async handleClickCreate() {
        this.executing = true;
        try {
            const result = await createContact({
                accountId: this.accountId,
                month: Number(this.month),
                num: this.num,
                text: this.text
            });
            if (result === "success") {
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: "作成が完了しました",
                        variant: "success"
                    })
                );
                this.dispatchEvent(new RefreshEvent());
            } else {
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: "エラーが発生しました",
                        variant: "error"
                    })
                );
            }
        } catch (error) {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: "エラーが発生しました",
                    message: error.body.message,
                    variant: "error"
                })
            );
        } finally {
            this.executing = false;
            this.dispatchEvent(new CloseActionScreenEvent());
        }
    }

    month_handleOnChange(event) {
        this.month = event.target.value;
    }

    num_handleOnChange(event) {
        this.num = event.target.value;
    }

    text_handleOnChange(event) {
        this.text = event.target.value;
    }

    handleClickStep(event) {
        const currentStep = Number(this.currentStep.replace("step", ""));
        const nextStep = Number(event.target.value.replace("step", ""));
        if (nextStep - currentStep === 1) {
            this.handleClickNext();
        } else if (nextStep - currentStep === -1) {
            this.handleClickPrev();
        }
    }

    handleClickCancel() {
        this.dispatchEvent(new CloseActionScreenEvent());
    }

    handleClickNext() {
        if (this.currentStep === "step1") {
            if (!this.month) {
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: "月を選択してください",
                        variant: "error"
                    })
                );
                return;
            }
            this.currentStep = "step2";
        } else if (this.currentStep === "step2") {
            this.currentStep = "step3";
        } else if (this.currentStep === "step3") {
            this.handleClickCreate();
        }
    }

    handleClickPrev() {
        if (this.currentStep === "step2") {
            this.currentStep = "step1";
        } else if (this.currentStep === "step3") {
            this.currentStep = "step2";
        }
    }

    get isStep1() {
        return this.currentStep === "step1";
    }

    get isStep2() {
        return this.currentStep === "step2";
    }

    get isStep3() {
        return this.currentStep === "step3";
    }

    get monthOptions() {
        const options = [];
        for (let i = 1; i <= 12; i++) {
            options.push({ label: `${i}月`, value: String(i) });
        }
        return options;
    }
}

画面の切り替えとレコード作成を行うコントローラです。DMLはApex側でロールバック付きにしてあります。

当時の記事の書き方から、3つ直しています。

  1. @trackを外しました。各フィールドはどれも単純な値の再代入で、配列やオブジェクトの内部を書き換えているわけではありません。テンプレートで使うフィールドは既定でリアクティブなので不要です。
  2. 内部のイベントハンドラから@apiを外しました。@apiは外部から直接呼べる公開APIを作るための修飾子です。ボタンからしか呼ばない処理に付けると、意図せず外部へ公開してしまいます。
  3. eval("$A.get('e.force:refreshView').fire()")をlightning/refreshのRefreshEventに差し替えました。Auraのイベントをevalで無理に呼ぶ書き方で、標準APIではありません。公式ガイドは、RefreshEventをforce:refreshViewの後継として明記しています。このコンポーネントもLightning Web Securityの仮想サンドボックス内で動くので、標準化されていない書き方には頼らないほうが安全です。

Apexコントローラ

stepsAccountScreenActionSampleController.cls

public with sharing class stepsAccountScreenActionSampleController {
    @AuraEnabled(cacheable=false)
    public static String getAccountId(Id recordId) {
        List<Account> accounts = [
            SELECT Id
            FROM Account
            WHERE Id = :recordId
            LIMIT 1
        ];
        String ret = '';
        if (accounts.size() > 0) {
            ret = accounts.get(0).Id;
        }
        return ret;
    }

    @AuraEnabled(cacheable=false)
    public static String createContact(
        Id accountId,
        Integer month,
        Integer num,
        String text
    ) {
        Savepoint sp = Database.setSavepoint();
        Contact con = new Contact();
        con.AccountId = accountId;
        con.LastName = text;
        con.Birthdate = Date.newInstance(Date.today().year(), month, 1);
        con.number__c = num;
        try {
            insert con;
        } catch (Exception e) {
            Database.rollback(sp);
            return 'error';
        }
        return 'success';
    }
}

Savepointで保護したうえで取引先責任者を1件作成します。失敗時はロールバックします。

レコードを作る処理なのでcacheable=falseのままにします。失敗したらDatabase.rollbackで戻す構成です。

メタデータとクイックアクションの配置

stepsAccountScreenActionSample.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>67.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordAction</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordAction">
            <actionType>ScreenAction</actionType>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

レコードページのアクションとして公開する設定です。apiVersionを67.0へ上げています。

targetをlightning__RecordActionにし、actionTypeをScreenActionにすると、レコードページのアクションから画面付きで呼び出せるコンポーネントとして公開されます。apiVersionは当時の59.0からSummer '26の67.0へ上げました。

配置と確認の手順です。

  1. 取引先オブジェクトにアクションを追加する

    取引先オブジェクトのボタン・リンク・アクションで新規アクションを作成し、アクションタイプを「Lightning Web コンポーネント」、コンポーネントに本コンポーネントを指定します。

  2. ページレイアウトへアクションを配置する

    取引先のページレイアウト編集で、モバイルおよびLightning Experienceのアクション欄へ、作成したアクションをドラッグして保存します。

  3. 取引先レコードから動作を確かめる

    対象の取引先レコードを開き、追加したアクションを押します。Step1で日付を選び「次へ」、Step2で数値を入力して「次へ」、Step3でテキストを入力して「作成」を押します。

  4. 作成結果を確認する

    成功のトーストが出て画面が閉じたあと、取引先責任者の関連リストに新しいレコードが増えていることを確かめます。

ここで間違えやすい

間違い何が起きるか
currentStepの値とlightning-progress-stepのvalueを食い違わせる進捗表示と実際に出ている画面がずれる
内部専用のメソッドに@apiを付ける外部から呼べる公開APIとして意図せず公開される
単純な値の再代入にまで@trackを付ける動作はするが不要。配列・オブジェクトの内部を書き換えるとき専用
evalでAuraのイベントを呼ぼうとする標準APIではない。lightning/refreshのRefreshEventに直す
ステップ番号の文字列比較だけで分岐を作り込むステップが増えるほど分岐が複雑になり、抜けが起きやすい

確認した環境

  • 2026年9月/Salesforce Summer '26(APIバージョン67.0)時点のlightning-progress-indicatorコンポーネントリファレンス、LWC開発者ガイドの設定タグ・RefreshView APIのページで、進捗表示と画面更新の仕様を確認しています
  • ご自身の組織のオブジェクト・項目名に合わせて確かめてください

まとめ

  • クイックアクションを複数ステップに分けると、1画面あたりの入力負担が下がります
  • lightning-progress-indicatorのcurrent-stepは、lightning-progress-stepのvalueと必ず一致させます
  • 単純な値の再代入には@trackは不要です。テンプレートで使うフィールドは既定でリアクティブです
  • @apiは外部へ公開する意図があるものだけに付けます。内部専用のハンドラには付けません
  • evalでAuraのforce:refreshViewを呼ぶ書き方は、lightning/refreshのRefreshEventに直します
  • apiVersionはSummer '26の67.0へ上げました

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。