LWCでデータテーブルを作る場合のコンポーネント分割論(コードあり)
こんにちは、きゃらめるです。
今回はSalesforce Platform アドベントカレンダーの15日目の記事です!
qiita.com
初めてSalesforce Platformのアドベントカレンダーに参加いたします!お邪魔します!
今年は初めてお仕事としてLWCを触ることができ、ブログのネタもLWCに偏っているな…と感じました。
そんなわけで、今回もLWCです。わーい!
今回は、データテーブルをLWCで実装する際にコンポーネントはどんな風に分割しようかな?というお話です。
お仕事でLWCを触った際に後悔していることのひとつが、このデータテーブル的な動きをする要素のコンポーネントの分割方法でした。
実装自体はできているのですが、あまり思っていた形にならなかったな…と感じていて、じゃあどんな風に実装すればよかったのかな?というところを改めて考え直してみました。
ちなみに、データテーブルの要件としてはこんな感じでした。
- データの編集ができる(※テーブル上で編集した内容はApex側で処理して保存する想定)
- 最終行の項目を編集すると、後ろに新しい行が自動的に追加される
- 削除ボタンを押すと、対象行をが削除される
実際の動きとしてはこんな感じです ↓↓
今回のブログでは、
- お仕事で実装した時のコンポーネントの分割について
- 分割方法を変えた方がよかったと感じた理由
- 分割方法を変えて再実装してみた&所感
の順で書いていきます。
※再実装後のソースコード(上記の画像のようなテーブルのコンポーネント)はGitHub上に公開しています!
github.com
お仕事で実装した時のコンポーネントの分割について
今回作成したページでは、データテーブルはあくまでページの要素の一部分で、他にも様々な情報表示していました。
(Salesforceの標準の詳細ページの一部に、データテーブルが埋め込まれている、みたいなイメージをしていただけるとありがたいです!)
なので、大元のページ(=詳細ページ的な部分)が1つのLWCになっていて、その中の一部の要素が別のLWCに切り出されたりしながら配置されていました。
その中のデータテーブル部分をどのように分割したのかというと…こんな感じです↓
画像のように、テーブルの各行を1コンポーネントとして分割していました。
もう一つポイントとしては、ヘッダー部分だけは大元のコンポーネント上に記載していて、特にコンポーネントを分けていませんでした。
そして、画面上で変更されたデータ等は、すべて大元のページのLWCに集まる仕組みになっていました。
分割方法を変えた方がよかったと感じた理由
なぜこの部分について公開しているか…理由は大きく二つあります。
- テーブルの中身をコンポーネントとして切り出すと列がキレイに揃わなくなる
- テーブルのデータを子側にオブジェクトとして渡していた場合、子側でデータを変更できない
ひとつずつ説明していきますね。
列がキレイに揃わなくなる
実際の画像がこちらです。
実装としては、tbody内のtr以下を子コンポーネントにした形なのですが…キュッと左に寄ってしまっています。
では、どのように解決したのか。
下記のリンクで、まさに私と同じようなコンポーネント分割をおこなった方が質問投稿しておられたので助かりました;;
salesforce.stackexchange.com
CSSで表示の仕方を指定してあげることで解決しています。
:host { display: table-row; }
ただ、上記のリンクにこんな回答がありました。
Ideally, you shouldn't use table; lightning-layout and lightning-layout-item work as tables for all practical purposes.
(理想としては、テーブルを使わないでください。lightnin-layoutとlightning-layout-itemが、実用上のテーブルのように動作します。)
こちらの方法は修正に時間かかりそうだったので軽く試してやめてしまったのですが、こういう分割の仕方をする必要があるのであれば、tableは使わないのが得策なのかもしれません。
子側でデータを変更できない
これは、親側から子側に「オブジェクトで」データを渡したときの話です。
通常の文字列や数値を渡している場合、子側でそのデータを書き換えて保持しておくことが可能です。
一方、オブジェクトで渡した場合は、中身の値を書き換えようとすると怒られます。
画面からの入力を受け付けるデータテーブルでは、親側から渡された初期データを表示させると同時に、更新があった場合にはそのデータを保持し、更新後のデータを画面に表示させる必要があります。
そのためには、親から受け取ったデータを、ユーザが更新した内容でそのまま変更できる方が便利なのですが…それができず途中で詰まりました;
そんなときに以下の記事を見つけて、オブジェクトで渡した場合は値を変更できない、という仕様を知りました。
note.com
解決策としては、結局親コンポーネントにすべての情報を集約させる、という形を取りました。
今回の仕様には、データの削除が含まれています。これを今回の行毎のコンポーネント分割で実装しようとすると、以下のようになります。
つまり、更新された内容を親側が認知できていない状態だと、削除を行ったときに古いデータの内容で表示されてしまいます。
「削除時や保存時等、親側で最新のデータが必要になったときに子から貰う」、という手法を考えていましたが、結局子側ではオブジェクトのデータ更新ができないため、親側に集約させておくことで完結させました。
まぁ、これはこれで実装はできましたし、親に集約させておくことで便利な点もありました。
分割方法を変えて再実装してみた&所感
実装後、上記に書いたような内容を振り返ったとき、「結局、行毎のコンポーネントに分けた意味はあったのだろうか?」という気持ちになりました。
今の私の考えとしては、
のいずれかで、よかったのではないかと思っています。
もちろん、要件によっては何が「よい」なのかは変わってくると思います。
ちなみに、私のお仕事の要件上では「コンポーネント分割しない」は悪手だった(大元のページに機能が多すぎて、ただでさえJSが肥大化している…)と思っていますので、今回は「データテーブル全体を1コンポーネントとして扱っていたら…」ということで再実装をしてみました。
再掲になりますが、実装後のソースコードはこちらで公開しています。
中身は至ってシンプル。datatableとexampleというLWCがあり、exampleはあくまでデータテーブルのデータを渡したり、表示したりするための親コンポーネントとなっています。
本体のdatatableで行っている内容は
- exampleから受け取ったデータ(A)を、表示に適した形式のデータ(B)に整形する
- 画面上でデータが書き換わった際、整形後のデータ(B)を書き換える ※(B)はdatatable内で定義しているため、書き換え可能
- 行追加・削除時も、整形後のデータ(B)に対して行追加・削除を行う
- 画面上の更新を反映しているデータ(B)を、exampleからもらったデータの形式に直すgetterメソッドを用意
といった内容です。
個人的に、保存時に扱いやすいデータ構造と、テーブルでの表示時に扱いやすい構造は違うように感じていたので、example側では保存時に扱いやすい形でデータを保持してdatatable側に渡す→datatable側で表示しやすい形に変えて扱う、という形に落ち着きました。
最後のgetterメソッドは、datatable側でのデータを親のexample側に返す時用にあったら便利かと思って作りました。
これによって、画面上で変更される度にexample側にデータを返す必要がなくなります。
その分、親のexample側で表示データをいじりたいとなるとちょっと厄介かもしれませんが…テーブルの行追加・削除もdatatableコンポーネント内で完結しているので、「親側でデータを変えたいとき」というのは発生しにくいと思っています。
なによりこの分割方法の方が、親が子の実装を知らなくてもいい(決められた形式で値を渡せばいい)、疎結合な関係になっているように感じます。
おわりに
ということで、今年実装したものについて振り返りを兼ねて書いてみました('ω')
実際にこの部分の実装を修正すべきかというと、そこは優先度やリファクタリング工数との兼ね合いだったりするので難しいですが…
ただ、自分の書いたコードについて「こうすればよかったのかも」と思うことはあっても、今まではなかなか振り返れないことが多かったので、こうやってたまに棚卸するのはいいですね。ちょっとすっきりしました。
来年はJavaScriptデベロッパー試験を受験から始まり、アプリケーションアーキテクトに向けた資格に挑戦していきます。頑張るぞ!
また来年もよろしくお願いします!