Minihack 2023 LWC をやってみる(脳内ダンプ)
使っている組織は Trailhead Playground (楽だから)
とりあえず、https://salesforce-minihack-swtt2023.herokuapp.com/ の setup.sh を実行するところまでは完了した。
SFDX cli とか VSCodeのSFDCプラグインの類は導入済み。
とりあえずソースを見る
/force-app/main/default
わー、メタデータがいっぱい。オブジェクトのメタデータってフィールドごとに2ファイルあるんだー
…あれ、お仕事で見てるソースだと2ファイル(本体とメタ)だった気がする…
とりあえず、TrailheadでLWC入門はやったので、classesにコントローラ的なのが入る、あとフロントはWebコンポーネント的な感じになってるのはわかる。
SpeciesService.cls を見ていく。 わからん。全然わからん。
なにこれなもの
- @AuraEnabled
- SOQLの WITH USER_MODE
- public with sharing class
@AuraEnabled
クライアント側およびサーバ側から Apex コントローラメソッドへのアクセスが可能になります。
このアノテーションを指定することで、メソッドを Lightning コンポーネント
(Lightning Web コンポーネントと Aura コンポーネントの両方) で使用できるようになります。
このアノテーションが付加されたメソッドのみが公開されます。
つけないとフロントエンドから呼べないよ。ってことっぽい。
SOQL の WITH USER MODE
Apex code runs in system mode by default,
Salesforce recommends that you enforce Field Level Security (FLS) by using WITH USER_MODE rather than WITH SECURITY-ENFORCED because of these additional advantages.
言われてみれば、SECURITY-ENFORCEDは業務のコードで見たことあるかも。 WITH USER MODE はそれをもっと良い感じに効かせてくれるからおすすめと。
これ、Field Level Securityで読めないことになっているフィールドを読もうとしたら後で怒られるやつじゃないかという気がする。(SOQLレベルで怒られるようだとだいぶ使いにくい)
public with sharing class
現在のユーザの共有ルールを強制実行するには、クラスの宣言時に with sharing キーワードを使用します。このキーワードを明示的に設定すると、現在のユーザコンテキストで Apex コードが実行されます。
あれ、じゃあ、SOQLに指定している WITH USER MODEしなくても with sharing で宣言されたクラスならユーザーコンテキストで実行されてるんだから大丈夫なのでは?という気もするけど・・・?
わざわざSFDC公式のコードで指定しているということは、SOQLは別のコンテキストなんだろう
さてこまった。 LWCの画面の定義がどれかわからない。コンポーネントが/lwc 以下にあるのはわかる。
・・・あー。わかった。そうかこれLightningなんだからページがあるのか。ホーム画面をカスタムするのと同じように、 /flexipages 以下にページの定義があって、それをタブにして見せてるだけだこれ。
設定からタブを見てみると Species Explorer タブは Lightning ページタブという分類になっているので間違ってなさそう。実際、Speciesタブを表示した状態で歯車→編集ページを選択するといじれそうな感じになっているので多分よさげ。
ページを編集してみると、SpeciesList って右上に表示されるのでまぁ、それがコンポーネント名だろう。
speciesList.htmlを見てみると、まぁ普通にテンプレートっぽい。
Thymeleafとかを見たことがアレばまったく違和感がないレベルだと思う。
と思ったけど、 species ってどこから湧いてきた?!
speciesList.js を見てみると、なんか Javascriptモジュールっぽい書き方をしたなにかが出てきた。
というか、これは普通にJSモジュールな気がする。 多少独特なのをimportしているけども。
jsのspeciesをspecie に書き換えてデプロイしたらちゃんと壊れたのでjsとhtmlはセットで考えてよさげ。
どう組み合わさっているのかは謎だけれども、ファイル名が一緒なら可なのか、ディレクトリ名と合わせる必要があるのか、要確認事項。
で、speciesがどこから来たのかの調査に来たんだった。
@wire(getSpecies)
species;
@wireしてるのでこれはテンプレートに渡される。 で、値は getSpecies だよ。ということと推測
getSpeciesは、import文で定義していて、 /lwc/SpeciesService#getSpecies のことだよね。
ぐぐってみると、@wireの他に@trackというのもある模様。 Knockout.js を思い出す
@wireには第二引数もあって、それを使うとパラメタを渡せるっぽい。
しかし、SFの組織ってApexのコードは持つわ(向こうでコンパイルするから)JSもHTMLも持つわで
ソースだけでも保持するデータ量がすごいことになってそう。
あ。学習の餌にしたらすごいことになる…? Einstain copilot とかやるのかなぁ
テンプレート中の species がどこから来たのかなんとなくわかったのでhtmlに帰ってきた。
template っていう見慣れないタグがある。 と思ったらなんとHTML標準
for:each とかはさすがにLWC方言だろうと思われる。
よく見ると、 species.data を for:each しているが、 SpeciesService#getSpecies の返り値は、ただのList<Species__c> なのでなんかよしなにラップされてJSONになるっぽい。
やっとここまで来て、お題を見ようね。という感じになる。
お題は、
- フィルタ条件欄を追加しろ。
- フィルタは3文字以上入力するまで稼働するな(3文字入れたあとに消して2文字にしたらどうするんだろ)
- 結果はキャッシュせよ (最初から cache = true って書いてるしそういうことだよね)
API バージョン 55.0 以降では、アノテーション @AuraEnabled(cacheable=true scope='global') を使用して、Apex メソッドをグローバルキャッシュにキャッシュできます。
今回なら全体に向けてキャッシュしちゃっていいのでこれを使ったほうがモアベターっぽいが…?
キャッシュって条件がどれでとか設定しないと誤爆しそうだけどどうなるんだろ。
フィルタ条件は、lightning-input ベースコンポーネントを利用して…とちょっとSalesforce Japaneseが炸裂しているので、誘導に従ってコンポーネント一覧を見てみる…と、めちゃくちゃ多い。
で、結局 lightning-input っていうコンポーネントがあるからそれを使えばちゃんとSalesforceっぽい見た目のテキストボックスができる。
さて、テキストボックスができたのはいいけどこれをどうやってイベント掴んで内容を更新するか。
お題のサンプルを見ると、検索ボタンはない。
ということはリアルタイムにフィルタリングしてね。という雰囲気を感じる。
ligntning-input の解説を見るとご丁寧にイベントハンドラのサンプルがあるのでそれに乗っかってみる。
SpeciesList.jsが検索機能を持つのは大変気持ち悪いんだけども、多分ここに持たせないとイベントを伝搬させてフィルタ→表示更新みたいな話になり大変めんどくさそう。というかそこまでやるとMinihackではなくほどほどHackになってしまうのでスコープ外にしたい気持ちになる。
が、私は完璧主義者なので、あえてフィルタするところは別コンポーネントにしてみる。
とりあえず、speciesFilterコンポーネントを作る。
speciesListをコピペしてファイル名を全部speciesFilterにするだけ。
とりあえずデプロイ。OK。 JSのClass名も変だけど通る。
さすがに直しておこう。
speciesList.html の検索欄を書いてたところを <c-species-filter></c-species-filter>
にしてデプロイ、再表示確認。OK。
c- はまぁカスタムコンポーネントだからだろう
species-filter のところは speciesFilterをケバブケースにすれば自動的に認識してくれる。
テキストが変わったときのイベントがほしい件、サンプル通りに書けばOK
<lightning-input type="text" name="filterText"
label="Filter species by Name or Description:"
onchange={handleInputChange}
>
</lightning-input>
handleInputChange(event) {
this.textValue = event.detail.value;
console.log(this.textValue);
alert(this.textValue);
}
Javascript部分はブラウザ側で動いている。Salesforceだから〜みたいなのはなくて割とクリーンなJSな感じ。イベントハンドラのバインドはよしなにしてくれてはいるけど。
alert(); と書くとエラーっぽい表示はしてくるが、ちゃんと動いてくれるのでデバッグも安心
これで、とりあえず入力された中身は取れるようになった。
あとは、このフィルタ文字列をうまく上位に渡して検索をかけてもらう感じにすればよさげ。
抽象的にいえば、 子要素から親要素にイベントを伝えるにはどうすれば良い?
ということでググってみると LWC eventというのがあるらしいのでこれで行く。
data = {detail: { nicedata: 'its nice', baddata: 'bad' }};
this.dispatchEvent(new CustomEvent('filterchanged', data));
これだけ。簡単。 dataの第一段にある detail は必須。つけないとデータが飛ばない。
イベントを送っても、受信するヒトがいないとなんの意味もないので、受信をやっていく。
これも話は単純で、 onイベント名={イベントハンドラのメソッド名} をカスタムコンポーネントに指定するだけ。
イベント名は、今やっている例であれば、 filterStringChanged なので、onfilterStringChanged になる。 ダサい。カスタムイベントのイベント名は最初を大文字にしておいた方がいいかもしれない。もしくは、全部小文字か。
とりあえず、大文字始まりに直しておく。
イベント名は小文字のみ使用可能。なのでイベント名の方を治す。長いと読みにくいので filterchanged としておく。
だんだん終わりが見えてきた。あとは、speciesList.js のspecies(変数名)をフィルタ条件に応じて書き換えればOK。
とりあえず、 @wire を外して… とやると、ページをロードしたときにspeciesがなくてエラーになってしまうので、そこはそのままにしておく。多分後で考える必要がありそう。
まずは、フィルタ条件が変更されたイベントが飛んできたら再検索する部分を作っていく。
最初にまぁ、すでに動いている getSpecies() を呼び出して再描画っぽいことをすることを考える。
species(変数名)を書き換えれば、バインドされているので自動的に書き換わりそうな感じがする。
…と、ここで罠にハマる。 getSpecies() はPromiseなのでawaitしないといけない(別にPromise.thenしてもいいけどめんどくさい)ので、イベントハンドラのメソッドを async にする。
で、データを取ってきてspeciesに入れてみると… 見事にからっぽになる。
これは、単純に @wire(getSpecies) したときは {data: [species...]} になっているので、html側がそれに合わせた書き方をしているので、単純に入れるのではなく形を合わせてあげないといけない。
これだけのことに1時間悩んだ。
さて、あとは… getFilteredSpeciesを呼び出せば終わり。
import文を追加して getFilteredSpecies('filtertext'); ...
はい、内部エラー!!
正解は、 await getFilteredSpecies({searchText: 'filterText'});
である。
…まじかー
残りその1は、3文字以上に限りフィルタかける部分。
これはフィルタ検索窓の方で3文字未満の場合イベントを発生させないのが正解なのか、考えどころさんではある気がするが、いい加減ダレてきたのでイベント受け側で処理してしまうことにした。
残りその2は、フィルタ文字列が1文字〜2文字の間は再検索しても意味がない(画面がちらつくだけ)ので検索しない…つもりだったが別に毎度検索してもちらつかなかったのでやらなくてもOK。(反映のところが上手いのか、はたまた気が付かないだけなのか)
残りその3、応答はキャッシュすること。は @AuraEnabled(Cache=true) なのでキャッシュされてるでしょということでOK!
以上完成!!ざっくり4時間くらいかかった。
調べまくったのと、変なところでドハマリした(speciesが妙なproxyオブジェクトになっているのでもしかして中身を入れ替えたらだめ?とか無駄な疑いをしていた。Knockout.jsの経験が邪魔をした感じ)
(フィルタ側で3文字未満の場合、カラ文字列のイベントを発生させるあたりが落とし所かなぁ… 3文字。はパラメタとって欲しいけど)