Closed21

Minihack 2023 LWC をやってみる(脳内ダンプ)

Yakumo SakiYakumo Saki

とりあえずソースを見る
/force-app/main/default
わー、メタデータがいっぱい。オブジェクトのメタデータってフィールドごとに2ファイルあるんだー
…あれ、お仕事で見てるソースだと2ファイル(本体とメタ)だった気がする…

Yakumo SakiYakumo Saki

とりあえず、TrailheadでLWC入門はやったので、classesにコントローラ的なのが入る、あとフロントはWebコンポーネント的な感じになってるのはわかる。

Yakumo SakiYakumo Saki

SpeciesService.cls を見ていく。 わからん。全然わからん。
なにこれなもの

  • @AuraEnabled
  • SOQLの WITH USER_MODE
  • public with sharing class
Yakumo SakiYakumo Saki

@AuraEnabled

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_classes_annotation_AuraEnabled.htm

クライアント側およびサーバ側から Apex コントローラメソッドへのアクセスが可能になります。
このアノテーションを指定することで、メソッドを Lightning コンポーネント 
(Lightning Web コンポーネントと Aura コンポーネントの両方) で使用できるようになります。
このアノテーションが付加されたメソッドのみが公開されます。 

つけないとフロントエンドから呼べないよ。ってことっぽい。

Yakumo SakiYakumo Saki

SOQL の WITH USER MODE

https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_enforce_usermode.htm

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レベルで怒られるようだとだいぶ使いにくい)

Yakumo SakiYakumo Saki

public with sharing class

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_classes_keywords_sharing.htm

現在のユーザの共有ルールを強制実行するには、クラスの宣言時に with sharing キーワードを使用します。このキーワードを明示的に設定すると、現在のユーザコンテキストで Apex コードが実行されます。

あれ、じゃあ、SOQLに指定している WITH USER MODEしなくても with sharing で宣言されたクラスならユーザーコンテキストで実行されてるんだから大丈夫なのでは?という気もするけど・・・?
わざわざSFDC公式のコードで指定しているということは、SOQLは別のコンテキストなんだろう

Yakumo SakiYakumo Saki

さてこまった。 LWCの画面の定義がどれかわからない。コンポーネントが/lwc 以下にあるのはわかる。

・・・あー。わかった。そうかこれLightningなんだからページがあるのか。ホーム画面をカスタムするのと同じように、 /flexipages 以下にページの定義があって、それをタブにして見せてるだけだこれ。

設定からタブを見てみると Species Explorer タブは Lightning ページタブという分類になっているので間違ってなさそう。実際、Speciesタブを表示した状態で歯車→編集ページを選択するといじれそうな感じになっているので多分よさげ。

Yakumo SakiYakumo Saki

ページを編集してみると、SpeciesList って右上に表示されるのでまぁ、それがコンポーネント名だろう。
speciesList.htmlを見てみると、まぁ普通にテンプレートっぽい。
Thymeleafとかを見たことがアレばまったく違和感がないレベルだと思う。

と思ったけど、 species ってどこから湧いてきた?!

Yakumo SakiYakumo Saki

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 とかやるのかなぁ

Yakumo SakiYakumo Saki

テンプレート中の species がどこから来たのかなんとなくわかったのでhtmlに帰ってきた。
template っていう見慣れないタグがある。 と思ったらなんとHTML標準
https://developer.mozilla.org/ja/docs/Web/HTML/Element/template

for:each とかはさすがにLWC方言だろうと思われる。
よく見ると、 species.data を for:each しているが、 SpeciesService#getSpecies の返り値は、ただのList<Species__c> なのでなんかよしなにラップされてJSONになるっぽい。

Yakumo SakiYakumo Saki

やっとここまで来て、お題を見ようね。という感じになる。
お題は、

  • フィルタ条件欄を追加しろ。
  • フィルタは3文字以上入力するまで稼働するな(3文字入れたあとに消して2文字にしたらどうするんだろ)
  • 結果はキャッシュせよ (最初から cache = true って書いてるしそういうことだよね)

API バージョン 55.0 以降では、アノテーション @AuraEnabled(cacheable=true scope='global') を使用して、Apex メソッドをグローバルキャッシュにキャッシュできます。

今回なら全体に向けてキャッシュしちゃっていいのでこれを使ったほうがモアベターっぽいが…?
キャッシュって条件がどれでとか設定しないと誤爆しそうだけどどうなるんだろ。

Yakumo SakiYakumo Saki

フィルタ条件は、lightning-input ベースコンポーネントを利用して…とちょっとSalesforce Japaneseが炸裂しているので、誘導に従ってコンポーネント一覧を見てみる…と、めちゃくちゃ多い。

で、結局 lightning-input っていうコンポーネントがあるからそれを使えばちゃんとSalesforceっぽい見た目のテキストボックスができる。

Yakumo SakiYakumo Saki

さて、テキストボックスができたのはいいけどこれをどうやってイベント掴んで内容を更新するか。
お題のサンプルを見ると、検索ボタンはない。
ということはリアルタイムにフィルタリングしてね。という雰囲気を感じる。

ligntning-input の解説を見るとご丁寧にイベントハンドラのサンプルがあるのでそれに乗っかってみる。
https://developer.salesforce.com/docs/component-library/bundle/lightning-input/example

Yakumo SakiYakumo Saki

SpeciesList.jsが検索機能を持つのは大変気持ち悪いんだけども、多分ここに持たせないとイベントを伝搬させてフィルタ→表示更新みたいな話になり大変めんどくさそう。というかそこまでやるとMinihackではなくほどほどHackになってしまうのでスコープ外にしたい気持ちになる。
が、私は完璧主義者なので、あえてフィルタするところは別コンポーネントにしてみる。

とりあえず、speciesFilterコンポーネントを作る。
speciesListをコピペしてファイル名を全部speciesFilterにするだけ。
とりあえずデプロイ。OK。 JSのClass名も変だけど通る。
さすがに直しておこう。

speciesList.html の検索欄を書いてたところを <c-species-filter></c-species-filter>
にしてデプロイ、再表示確認。OK。
c- はまぁカスタムコンポーネントだからだろう
species-filter のところは speciesFilterをケバブケースにすれば自動的に認識してくれる。

Yakumo SakiYakumo Saki

テキストが変わったときのイベントがほしい件、サンプル通りに書けば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(); と書くとエラーっぽい表示はしてくるが、ちゃんと動いてくれるのでデバッグも安心

Yakumo SakiYakumo Saki

これで、とりあえず入力された中身は取れるようになった。
あとは、このフィルタ文字列をうまく上位に渡して検索をかけてもらう感じにすればよさげ。

抽象的にいえば、 子要素から親要素にイベントを伝えるにはどうすれば良い?
ということでググってみると LWC eventというのがあるらしいのでこれで行く。

data = {detail: { nicedata: 'its nice', baddata: 'bad' }};
this.dispatchEvent(new CustomEvent('filterchanged', data));

これだけ。簡単。 dataの第一段にある detail は必須。つけないとデータが飛ばない。

Yakumo SakiYakumo Saki

イベントを送っても、受信するヒトがいないとなんの意味もないので、受信をやっていく。
これも話は単純で、 onイベント名={イベントハンドラのメソッド名} をカスタムコンポーネントに指定するだけ。
イベント名は、今やっている例であれば、 filterStringChanged なので、onfilterStringChanged になる。 ダサい。カスタムイベントのイベント名は最初を大文字にしておいた方がいいかもしれない。もしくは、全部小文字か。
とりあえず、大文字始まりに直しておく。

イベント名は小文字のみ使用可能。なのでイベント名の方を治す。長いと読みにくいので filterchanged としておく。

Yakumo SakiYakumo Saki

だんだん終わりが見えてきた。あとは、speciesList.js のspecies(変数名)をフィルタ条件に応じて書き換えればOK。

とりあえず、 @wire を外して… とやると、ページをロードしたときにspeciesがなくてエラーになってしまうので、そこはそのままにしておく。多分後で考える必要がありそう。

まずは、フィルタ条件が変更されたイベントが飛んできたら再検索する部分を作っていく。
最初にまぁ、すでに動いている getSpecies() を呼び出して再描画っぽいことをすることを考える。
species(変数名)を書き換えれば、バインドされているので自動的に書き換わりそうな感じがする。
…と、ここで罠にハマる。 getSpecies() はPromiseなのでawaitしないといけない(別にPromise.thenしてもいいけどめんどくさい)ので、イベントハンドラのメソッドを async にする。

で、データを取ってきてspeciesに入れてみると… 見事にからっぽになる。
これは、単純に @wire(getSpecies) したときは {data: [species...]} になっているので、html側がそれに合わせた書き方をしているので、単純に入れるのではなく形を合わせてあげないといけない。

これだけのことに1時間悩んだ。

Yakumo SakiYakumo Saki

さて、あとは… getFilteredSpeciesを呼び出せば終わり。
import文を追加して getFilteredSpecies('filtertext'); ...
はい、内部エラー!!

正解は、 await getFilteredSpecies({searchText: 'filterText'}); である。
…まじかー

Yakumo SakiYakumo Saki

残りその1は、3文字以上に限りフィルタかける部分。
これはフィルタ検索窓の方で3文字未満の場合イベントを発生させないのが正解なのか、考えどころさんではある気がするが、いい加減ダレてきたのでイベント受け側で処理してしまうことにした。

残りその2は、フィルタ文字列が1文字〜2文字の間は再検索しても意味がない(画面がちらつくだけ)ので検索しない…つもりだったが別に毎度検索してもちらつかなかったのでやらなくてもOK。(反映のところが上手いのか、はたまた気が付かないだけなのか)

残りその3、応答はキャッシュすること。は @AuraEnabled(Cache=true) なのでキャッシュされてるでしょということでOK!

以上完成!!ざっくり4時間くらいかかった。
調べまくったのと、変なところでドハマリした(speciesが妙なproxyオブジェクトになっているのでもしかして中身を入れ替えたらだめ?とか無駄な疑いをしていた。Knockout.jsの経験が邪魔をした感じ)

(フィルタ側で3文字未満の場合、カラ文字列のイベントを発生させるあたりが落とし所かなぁ… 3文字。はパラメタとって欲しいけど)

このスクラップは2023/07/17にクローズされました