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

【B2B不動産ポータル 制作日誌 Vol.1】検索UI構築:SLDSグリッドとコンポーネント分割の実践

今回から、「LWR (Build Your Own) × Agentforce」 を用いたB2B不動産ポータルサイト 「Real Estate Search」 の構築連載をスタートします。

本連載では、実際に動作しているコードをベースに、LWRサイト構築の勘所を解説していきます。 Vol.1のテーマは、サイトのメイン機能である「物件検索画面のUI構築」です。

Vol.1の今回は、サイトの顔となる「トップページ(検索結果一覧)」のUI実装について解説します。標準コンポーネントがほとんど存在しないLWRでは、検索画面全体をLWCで設計する必要があります。今回は保守性を高めるための「コンポーネント分割」と、SLDS (Salesforce Lightning Design System) を活用した「レスポンシブ・グリッド」の実装について紹介します。

DXforce Point for Developers

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

データ構造のポイント(物件と住戸)

UIを作る前に、表示するデータの構造を少しだけ整理しておきます。今回は「建物」と「部屋」を分けて管理しています。

  • 物件 (Building__c): 建物情報(住所、最寄駅、築年数など)
  • 募集住戸 (Listing__c): 部屋情報(賃料、間取り、平米数など)

今回の検索結果コンポーネントで表示するのは、従オブジェクトである 「募集住戸 (Listing__c)」 です。

カスタム住所項目の有効化

GitHubに共有したオブジェクトをそのままデプロイする前に、カスタムオブジェクトで住所型項目を使えるようにしましょう。以下の設定作業が必要です。

  • カスタム住所項目を有効化
    • [設定] > [ユーザーインターフェース] を開き、[カスタム住所項目を使用] にチェックを入れる。
  • 州/国/テリトリー選択リストを設定
    • [設定] > [州/国/テリトリー選択リスト] を開き、国や都道府県を日本語化する。
  • カスタム住所項目を有効化
  • 州/国/テリトリー選択リストを設定

検索結果一覧の全体像

完成した画面がこちらです。ホーム画面に検索機能を集めています。
※ヘッダーのヒーロー検索領域はVol.4で紹介します。
検索条件にヒットした物件がカード形式で並び、以下のように表示がレスポンシブに切り替わるUIになっています。

  • PC:条件検索欄1列+検索結果3列
  • タブレット:条件検索欄1列+検索結果1列
  • スマホ:条件検索欄と検索結果が縦に1列
  • PC
  • タブレット&スマホ

画面全体のアーキテクチャ

検索画面は1つの巨大なLWCで作るのではなく、役割ごとに以下の3つに分割しました。

  1. Container (propertySearchContainer): 親。データの取得、レイアウト枠の定義、状態管理。
  2. Filter (propertySearchFilter): 子(左側)。検索条件の入力フォーム。
  3. Result (propertySearchResult): 子(右側)。検索結果のカード表示。

これにより、「検索ロジック」と「表示デザイン」が分離され、コードの見通しが劇的に良くなります。

親コンポーネントでのレイアウト定義

まずは親となる propertySearchContainer です。 ここでは SLDS Grid System を使い、PC画面では「検索条件:結果一覧 = 3 : 9」の比率で表示し、スマホでは縦に積まれるレスポンシブ設定を記述します。

propertySearchContainer.html

<template>
    <div class="slds-grid slds-wrap slds-gutters_x-small">
        
        <div class="slds-col slds-size_1-of-1 slds-medium-size_6-of-12 slds-large-size_3-of-12">
            <div class="slds-box slds-m-bottom_medium form-container">
                <c-property-search-filter onsearch={handleSearch}></c-property-search-filter>
            </div>
        </div>

        <div class="slds-col slds-size_1-of-1 slds-medium-size_6-of-12 slds-large-size_9-of-12">
            <template if:true={isLoading}>
                <div class="slds-is-relative slds-p-around_large">
                    <lightning-spinner alternative-text="Loading" size="medium"></lightning-spinner>
                </div>
            </template>

            <template if:false={isLoading}>
                <c-property-search-result listings={processedListings}></c-property-search-result>
                
                <template if:true={isNoResults}>
                    <div class="slds-text-align_center slds-p-around_large">
                        <p>条件に一致する物件が見つかりませんでした。</p>
                    </div>
                </template>
            </template>
        </div>
    </div>
</template>
DXforce Point for Developers

実装のポイント:
slds-large-size_3-of-12 と 9-of-12 を組み合わせることで、LWRサイト特有の幅広なキャンバスを有効活用しています。
データ取得(Apex呼び出し)や、CMS画像URLの生成ロジックはこの親コンポーネントのJSに集約させています。

検索結果カードのデザイン

次に、右側に表示される検索結果 propertySearchResult です。 不動産サイトにおいて写真は命です。ここでは「建物外観」と「部屋の内装」を2枚並べて表示するデザインを採用しました。

propertySearchResult.html

<template>
    <div class="slds-grid slds-wrap slds-gutters_x-small">
        <template for:each={listings} for:item="item">
            <div key={item.Id} class={cardWrapperClass}>

                <article class="slds-card slds-card_boundary h-100">
                    <div class="slds-card__body slds-card__body_inner slds-p-top_small">
                        
                        <div class="slds-grid slds-gutters_xx-small slds-m-bottom_small gallery-container">
                            <div class="slds-col slds-size_1-of-2 image-wrapper">
                                <template if:true={item.buildingPhotoUrl}>
                                    <img src={item.buildingPhotoUrl} alt="Exterior" class="property-photo">
                                </template>
                                <template if:false={item.buildingPhotoUrl}>
                                    <div class="slds-align_absolute-center h-100 slds-text-color_weak">No Photo</div>
                                </template>
                            </div>
                            <div class="slds-col slds-size_1-of-2 image-wrapper">
                                <template if:true={item.listingPhotoUrl}>
                                    <img src={item.listingPhotoUrl} alt="Interior" class="property-photo">
                                </template>
                                <template if:false={item.listingPhotoUrl}>
                                    <div class="slds-align_absolute-center h-100 slds-text-color_weak">No Photo</div>
                                </template>
                            </div>
                        </div>

                        <h3 class="slds-text-heading_small slds-truncate" title={item.buildingName}>
                            <a href="#" onclick={handleNavigate} data-record-id={item.Id}>{item.buildingName} {item.Name}</a>
                        </h3>
                        <div class="slds-m-top_x-small">
                            <p class="slds-text-title_caps slds-text-color_weak">{item.FloorPlan__c} / {item.AreaSize__c}㎡</p>
                            <p class="slds-text-heading_medium slds-m-vertical_xx-small">
                                <lightning-formatted-number value={item.Rent__c} format-style="currency" currency-code="JPY"></lightning-formatted-number>
                            </p>
                            <p class="slds-text-body_small">
                                {item.nearestStation} 徒歩{item.walkMinutes}分
                            </p>
                        </div>
                    </div>
                    
                    <footer class="slds-card__footer">
                        <lightning-button 
                            label="詳細・空室確認" 
                            variant="neutral" 
                            class="slds-m-right_x-small"
                            onclick={handleNavigate}
                            data-record-id={item.Id}>
                        </lightning-button>
                    </footer>
                </article>

            </div>
        </template>
    </div>
</template>

propertySearchResult.css

画像の縦横比崩れを防ぐため、CSSで object-fit: cover を指定しています。これにより、アップロードされた写真のサイズがバラバラでも、カードの高さは綺麗に揃います。

.gallery-container {
    height: 150px;
}

.image-wrapper {
    height: 100%;
    overflow: hidden;
    background-color: var(--dxp-g-root, #f3f2f2); /* サイトの背景色変数を使用 */
}

.property-photo {
    width: 100%;
    height: 100%;
    object-fit: cover; /* 画像のトリミング指定 */
}

LWRにおける画面遷移の実装

LWRサイトでレコード詳細画面へ遷移する場合、NavigationMixinstandard__recordPage を使用します。

propertySearchResult.js

標準のオブジェクトページ設定がLWRでも有効に機能するため、standard__recordPage を使うのが最も標準的かつ安全な実装です。

handleNavigate(event) {
        event.preventDefault();
        const recordId = event.target.dataset.recordId;

        if (recordId) {
            this[NavigationMixin.Navigate]({
                type: 'standard__recordPage',
                attributes: {
                    recordId: recordId,
                    objectApiName: 'Listing__c',
                    actionName: 'view'
                }
            });
        }
    }

まとめ

Vol.1では、実用的な不動産検索画面のUI実装を行いました。

  1. コンポーネント分割: 親(ロジック)と子(表示)を分離し、データフローを整理。
  2. SLDSグリッド: クラス指定のみで、複雑なレスポンシブ・レイアウトを実現。
  3. CSS: object-fit で画像の見栄えを制御。

次回 Vol.2 では、クリックした先の「物件詳細画面」の構築に進みます。 標準コンポーネントが少ないLWR環境において、「物件画像スライダー(カルーセル)」をCSSアニメーションを使って自作する方法を紹介します。

DXforceの管理人

福島 瑛二

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

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

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

Trailblazer: efukushima

福島 瑛二をフォローする

読者の声

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