連動選択リストの UI の作成
学習の目的
- ユーザインターフェース API に対する要求を実行し、1 つのレコードタイプのすべての選択リスト値を取得する。
- オブジェクトの連動項目の対応付けが含まれるプロパティを挙げる。
- 項目の制御項目のリストが含まれるプロパティを挙げる。
連動選択リストの特殊な点
連動選択リスト項目のユーザインターフェースを作成するには注意が必要ですが、ユーザインターフェース API によって容易になりました。連動選択リスト項目の値は、別の選択リストやチェックボックス (制御項目) の選択内容に基づいて絞り込まれます。Record Viewer アプリケーションの連動選択リストエディタのスクリーンショットを見てみましょう。[Country (国)] 選択リストが [State/Province (州)] 選択リスト ([Country (国)] 選択リストと連動) を制御します。ユーザが国を選択すると、[State/Province (州)] の値が一致する値に絞り込まれます。ユーザが州を選択すると、[City (市区郡)] の値が絞り込まれます。
このように、連動選択リストによって値が絞り込まれるため、スクロールする時間が短くなり、フォームへの記入が早くなります。さらに、連動選択リストエディタによって関連項目の編集が容易になります (特にモバイルデバイスの場合)。連動値の変更に合わせて項目から項目へとスクロールするのがどれだけ難しいか想像してみてください。1 つのダイアログボックスですべての関連選択リスト値を変更できればはるかに簡単です。
一方で、ユーザインターフェースの作成を難しくしているのが、こうした制御項目と連動項目のリレーションです。選択リスト (またはチェックボックス) は複数の連動選択リストを制御できます。さらに、連動選択リストは制御されますが、制御することもできます。実際に、連動選択リストは他の複数の連動選択リストを制御できます。連動選択リスト項目階層はツリーです。また、オブジェクトに多くのツリーがある場合もあります。
- objectInfo.dependentFields には、オブジェクトの連動項目の対応付けが含まれます。
- objectInfo.fields[fieldName].controllingFields には、項目の制御項目のリストが含まれます。
GET /ui-api/object-info/{objectApiName}/picklist-values/{recordTypeId}
Record Viewer アプリケーションでは、連動選択リストエディタの構築にこれらのプロパティとリソースを使用しています。
連動選択リストエディタの使用
Record Viewer アプリケーションで連動選択リストエディタを表示するには、オブジェクトの少なくとも 2 つの項目間に連動関係を作成します。
Trailhead Playground で、取引先オブジェクトの項目の連動関係を作成します。次に、Record Viewer アプリケーションで取引先を編集して、連動選択リストエディタの動作を実際に確認します。
- [Setup (設定)] から、「object (オブジェクト)」と入力し、[Object Manager (オブジェクトマネージャ)] を選択します。
- を選択します。
- [新規] をクリックします。
- 2 つの既存の項目間にリレーションを作成しましょう。制御項目で、[Rating (評価)] を選択します。連動項目で、[Customer Priority (顧客の優先度)] を選択します。[続行] をクリックします。
- [Hot (見込み有り)] に [High (高)]、[Warm (将来見込み有り)] に [Low (低)] と [Medium (中)]、[Cold (見込み無し] に [Low (低)] が含まれるように項目の連動関係を編集します。[保存] をクリックします。
- Record Viewer アプリケーションを再読み込みし、[Recent Items List (最近使ったデータリスト)] で取引先を選択します。
- [編集] をクリックします。
- [Account Rating (取引先 評価)] または [Customer Priority (顧客の優先度)] 項目の横で鉛筆アイコンをクリックします。
連動選択リストエディタのダイアログボックスが開きます。エディタを使用して両方の項目の値を選択します。一方の項目の値を選択すると、もう一方の項目の値が [Setup (設定)] の選択に基づいて絞り込まれます。
連動選択リストがユーザインターフェース API で公開される方法
連動選択リスト項目は、制御項目と連動項目の階層である連動関係ツリーに含まれます。ツリーの各ノードには、任意の数の子ノードを設定できます。子のないノードはリーフと呼ばれます。
ここで使用するサンプルの連動関係ツリーは、1 つのルート項目 Rating (評価) が 1 つの項目 CustomerPriority__c (顧客の優先度) を制御するという単純なものです。Rating が別の項目を制御する場合、この連動関係階層はツリーのようになります。項目が 2 つしかない連動関係ツリーにブランチはなく、トランク (幹) だけです。
オブジェクトには、こうした連動関係ツリーを必要なだけ設定できます。連動選択リストをユーザが編集できる UI を作成するには、各ツリーの完全な階層を把握する必要があります。
たとえば、取引先オブジェクトには 2 つの連動関係ツリーがあり、一方のルートは Country (国) で、もう一方のルートは Rating だとします。連動関係ツリーの例は、各制御項目が 1 つの連動項目だけを制御するという単純なものです。ただし、Country 項目が State/Province (州) 項目を制御するだけでなく、Language (言語) 項目と Time Zone (タイムゾーン) 項目も制御する可能性もあります。
すでに学習したように、ユーザインターフェース API には、項目の連動関係構造を提供する 2 つの応答プロパティがあります。どちらのプロパティも objectInfo ペイロードに含まれており、このペイロードは /ui-api/record-ui/{recordIds} からの応答に含まれています。JSON 応答を表示するには、Record Viewer アプリケーションで取引先を開き、[Show JSON (JSON を表示)] をクリックします。
- objectInfo.dependentFields
- このプロパティは、オブジェクトのすべての項目について連動項目ツリーを提供します。このサンプル JSON には 1 つのルート項目 Rating がありますが、オブジェクトには複数のルート項目がある場合があります。
"objectInfos" : { "Account" : { ... "dependentFields" : { "Rating" : { "CustomerPriority__c" : { } } }, ...
- Country、State/Province、City の例のように、オブジェクトに連動項目の拡張階層がある場合、dependentFields プロパティは、City のようなリーフノードに到達するまで連動項目をネストします。
"objectInfos" : { "Account" : { ... "dependentFields": { "Country__c": { "StateProvince__c": { "City__c": {} } } }, ...
- objectInfo.fields[fieldName].controllingFields
- このプロパティは、この特定の項目を制御する項目のリストを提供します。リストは、この項目を直接制御する項目から開始します。この単純な例では、CustomerPriority__c 項目には 1 つの制御項目 Rating があります。
"objectInfos" : { "Account" : { ... "fields" : { ... "CustomerPriority__c" : { "apiName" : "CustomerPriority__c", ... "controllingFields" : [ "Rating" ], ...
- Country、State/Province、City の例のように、オブジェクトに制御項目の拡張階層がある場合、controllingFields プロパティには、直属の制御項目から開始する制御項目のリストが含まれます。
"objectInfos" : { "Account" : { ... "fields" : { ... "City__c" : { "apiName" : "City__c", ... "controllingFields" : [ "StateProvince__c", "Country__c"], ...
GET /ui-api/object-info/{objectApiName}/picklist-values/{recordTypeId}
"CustomerPriority__c" : { "controllerValues" : { "Hot" : 0, "Warm" : 1, "Cold" : 2 }, "defaultValue" : null, "url" : "/services/data/v48.0/ui-api/object-info/Account/picklist-values/012000000000000AAA/CustomerPriority__c", "values" : [ { "attributes" : null, "label" : "High", "validFor" : [ 0 ], "value" : "High" }, { "attributes" : null, "label" : "Low", "validFor" : [ 1, 2 ], "value" : "Low" }, { "attributes" : null, "label" : "Medium", "validFor" : [ 1 ], "value" : "Medium" } ] },
controllerValues プロパティは、直属の制御項目の選択リスト値とインデックスを提供します。この例では、項目は Customer Priority で、直属の制御項目は Rating です。Rating の値は、Hot (見込み有り)、Warm (将来見込み有り)、Cold (見込みなし) で、インデックスは 0、1、2 です。
Customer Priority 値 (High (高)、Low (低)、Medium (中)) ごとに、validFor プロパティには Rating 値が含まれます。この値は、選択リストで選択されると、Customer Priority 値を除外するのではなく含めます。
たとえば、Rating が Hot (0) の場合、Customer Priority 選択リストは、High のみが含まれるように絞り込まれます。Rating が Warm (1) の場合、Customer Priority 選択リストは、Low と Medium が含まれるように絞り込まれます。Rating が Cold (2) の場合、Customer Priority 選択リストは、Low のみが含まれるように絞り込まれます。
これで、必要な情報とそれをユーザインターフェース API から取得する方法を理解できました。次は Record Viewer アプリケーションをその連動選択リストエディタを作成する方法を見てみましょう。
選択リスト値の取得
ユーザがレコードに対して [Create (作成)]、[Clone (コピー)]、または [Edit (編集)] をクリックすると、Record Viewer アプリケーションは指定されたレコードタイプのオブジェクトの全項目について選択リスト値を事前に取得します。
コードは、RecordViewerWrapper.js を参照してください。各シナリオ (onNewRecordClick、onCloneClick、onEditClick) で dispatch(actions.fetchPicklists(creds, apiName, recordType)) へのコールを探します。
/* /sagas/picklistFetcher.js */ let url = action.creds.instanceUrl + '/services/data/v48.0/ui-api/object-info/' + action.apiName + '/picklist-values/' + action.recordType + '/';
/* /reducers/picklists.js */ const picklists = (state = {values: undefined}, action) => { switch (action.type) { case 'RECEIVE_PICKLISTS': return { fieldValues : action.result.picklistFieldValues } case 'FETCH_PICKLISTS': // Clear values when new collection is requested. return { fieldValues: undefined } default: return state } } export default picklists
連動選択リストエディタの表示
レコードが編集モードのとき、アプリケーションでは連動項目の横に鉛筆アイコンが表示され、ユーザが連動選択リストエディタを開けることを示します。
ユーザが鉛筆アイコンをクリックすると、アプリケーションで連動選択リストエディタがモーダルダイアログボックスとして開きます。コードでは、連動選択リストエディタは、DepGraphEditor コンポーネントと呼ばれます。
/* /components/RecordRow.js */ if (objectInfo && ((component.field && component.field in objectInfo.dependentFields) || (component.fieldInfo && component.fieldInfo.controllingFields.length > 0))) { // Last field in controlling fields is the root field let lastControllingIndex = component.fieldInfo.controllingFields.length - 1 let rootField = null if (component.field in objectInfo.dependentFields){ rootField = component.field } else { rootField = component.fieldInfo.controllingFields[lastControllingIndex] } // Retrieve the picklist fields that need to show up. const subfieldTree = objectInfo.dependentFields[rootField] const modalFields = getFlattenedTree(objectInfo, subfieldTree, rootField) // Open a modal on click of the input field. let fieldTree = {} fieldTree[rootField] = subfieldTree return ( <div> <label key={'componentInput' + itemLabel + ',' + i} onClick={(event) => onEditDepGraph(picklists, modalFields, editValues, fieldTree, uiMode.toString())} value={currPicklistValue}> {editValues[component.field].current} </label> <button className="fa fa-pencil" key={'componentInput' + itemLabel + 'button,' + i} onClick={(event) => onEditDepGraph(picklists, modalFields, editValues, fieldTree, uiMode.toString())}> </button> </div> ); }
モーダルダイアログボックスを開くために、onClick() メソッドが onEditDepGraph() メソッドを呼び出し、このメソッドがアクション EDIT_DEP_GRAPH をディスパッチし、このアクションがモードを EditDepGraph に変更します。モードが変わるとモーダルダイアログボックスが開きます。
/* /reducers/record.js */ case 'EDIT_DEP_GRAPH': return { ...state, prevMode: action.prevMode, // save previous mode to return to on close mode: 'EditDepGraph' }
/* /components/Record.js */ <div> {uiMode === 'EditDepGraph' && <DepGraphEditor depGraph={depGraph} picklists={picklists} editValues={recordView.editValues} onFieldValueUpdate={onDepGraphFieldValueUpdate} onClose={() => onDepGraphClose(prevMode)}/> }
選択リスト値の更新
ユーザが選択リスト項目で新しい値を選択したら、アプリケーションはその子選択リスト項目の値を更新する必要があります。さらに、それらの子に子がある場合、アプリケーションは、子のないリーフ選択リスト項目に到達するまでそれらの子もすべて更新する必要があります。こうした更新を行うには、値が変更された項目の下位にある連動関係サブツリーを把握することが重要です。
また、項目の事前選択された値が引き続き有効である場合、アプリケーションはその値を保持する必要があります。
/* /sagas/depGraphValueUpdater.js */ // Traverse down to the target field in the tree. const field = modalFieldsMap[action.field]; var treeNode = action.fieldTree; for (var i = field.controllingFields.length -1; i>=0; i--) { var controllingField = field.controllingFields[i] treeNode = treeNode[controllingField]; } treeNode = treeNode[action.field]; // Now treeNode is the subtree rooted at the target field.
この抽出されたサブツリー構造と、ユーザがレコードに対して [Create (作成)]、[Clone (コピー)]、[Edit (編集)] をクリックして実行したときに取得された選択リスト値を使用して、サブツリーの各選択リスト項目に有効な選択リスト値が作成されます。
アプリケーションが特定の項目に対して有効な選択リスト値を取得する方法については、/helpers/depGraphHelper.js の getLegalValues() を参照してください。コードがサブツリーをトラバースし、幅優先探索を使用して子選択リスト項目ごとに有効な値を繰り返し入力する方法については、depGraphValueUpdater.js saga を参照してください。
これで完了です! 連動選択リストの UI を作成するのは、パンをスライスするほど簡単ではありませんが、チョコレートスフレを焼くよりは簡単で同じくらいおいしくなるでしょう。
まとめましょう。連動選択リストエディタの UI を作成するために、ユーザインターフェース API では、必要な情報すべてを 3 つの場所、objectInfo.dependentFields プロパティ、objectInfo.fields[fieldName].controllingFields プロパティ、および /ui-api/object-info/{objectApiName}/picklist-values/{recordTypeId} エンドポイントからの応答で提供します。
ユーザインターフェース API には、顧客を満足させる Salesforce カスタムユーザインターフェースを作成するための機能と柔軟性を備えています。Record Viewer アプリケーションをきっかけに皆さんがよいスタートを切ることを期待しています。自由に GitHub リポジトリのソースコードを活用し、プル要求を送信してください。皆さんのアプリケーションについて話を聞くのを楽しみにしています!