(以下、Directive ComparatorのREADMEの日本語訳です。著者は同じであるため、一部日本語ではニュアンスを変えていますが、基本的には同じ内容です。最新情報は原文の方を読んでください)
LWCでのコンポーネント記述にまつわる問題
コンポーネント内にrank
とfullName
のプロパティがあり、rank
の値が特別なものである場合に特別なメッセージを表示したい場合、Auraコンポーネントの場合は以下のように書きますね。
<aura:component>
<aura:attribute name="rank" type="String" />
<div>
<aura:if isTrue="{!v.rank == 'gold'}">
<span>Hi, {!v.fullName} - special offer to you, click <a href="">here</a>.</span>
</aura:if>
<aura:if isTrue="{!v.rank == 'silver'}">
<span>Hi, {!v.fullName}, thanks for visiting again !</span>
</aura:if>
<aura:if isTrue="{!v.rank == 'bronze'}">
<span>Welcome, {!v.fullName}</span>
</aura:if>
</div>
</aura:component>
Lightning Web Components (LWC)では、Auraコンポーネントと異なり、テンプレートでのインライン式ができないため、比較はスクリプトファイルに記述する必要があります。具体的には以下のようにプロパティ比較の式をコンポーネントクラスのgetter関数に移動する必要があります。
import { LightningElement } from 'lwc';
export default class MyComponent1 extends LightningElement {
rank;
fullName;
get isGoldRank() {
return this.rank === 'gold';
}
get isSilverRank() {
return this.rank === 'gold';
}
get isBronzeRank() {
return this.rank === 'bronze';
}
}
<template>
<div>
<template lwc:if={isGoldRank}>
<span>Hi, {fullName} - special offer to you, click <a href="">here</a>.</span>
</template>
<template lwc:if={isSilverRank}>
<span>Hi, {fullName}, thanks for visiting again !</span>
</template>
<template lwc:if={isBronzeRank}>
<span>Welcome, {fullName}</span>
</template>
</div>
</template>
配列のループ内で比較するとなると、気が遠くなる作業です。各項目の比較結果を含むように配列を前もって変換しておく必要があります。
import { LightningElement } from 'lwc';
export default class MyComponent2 extends LightningElement {
customerId = 1;
customers_ = [
{ id: 1, fullName: 'John Doe', rank: 'gold' },
{ id: 2, fullName: 'Amy Taylor', rank: 'silver' },
{ id: 3, fullName: 'Michael Jones', rank: 'bronze' },
{ id: 4, fullName: 'Jane Doe', rank: 'silver' },
];
get customers() {
return this.customers_.map((customer) => ({
...customer,
isSelected: customer.id === this.customerId,
isGoldRank: customer.rank === 'gold',
isSilverRank: customer.rank === 'silver',
isBronzeRank: customer.rank === 'bronze',
});
}
}
<template>
<div>
<template for:each={customers} for:item="customer">
<div class="customer-info" key={customer.id}>
<span class="icon">
<template lwc:if={customer.isGoldRank}>
<lightning-icon icon-name="standard:reward" size="medium"></lightning-icon>
</template>
<template lwc:if={customer.isSilverRank}>
<lightning-icon icon-name="standard:promotions" size="small"></lightning-icon>
</template>
<template lwc:if={customer.isBronzeRank}>
<lightning-icon icon-name="standard:customer" size="x-small"></lightning-icon>
</template>
</span>
<span class="name">
<template lwc:if={customer.isSelected}>
<strong>** {customer.fullName} **</strong>
</template>
<template lwc:else>
<span>{customer.fullName}</span>
</template>
</span>
</div>
</template>
</div>
</template>
この制約は、ロジックをテンプレートから切り離すというLightning Web Componentsの思想によるものですが、残念ながらスクリプトファイル内に過剰にgetterが定義されることになり、コンポーネントの全体での見通しが悪くなる傾向があります。
解決法: Directive Comparator
Directive Comparator for Lightning Web Componentsは、上記の懸念を解決します。クラスからgetterを削除し、コンパレータ生成関数で生成された初期値(不変の値)を持つプロパティをクラスに追加するだけです。
import { LightningElement } from "lwc";
import { comparator } from "c/directiveComparator";
export default class DirectiveComparatorSimpleExample extends LightningElement {
rank;
fullName;
$ = comparator(this, {
rank: ["gold", "silver", "bronze"]
});
}
テンプレートのマークアップはこのようになります。クラスには比較のためのgetter関数はどこにもありません。
<template>
<div>
<template lwc:if={$.rank.is.gold}>
<span>Hi, {fullName} - special offer to you, click <a href="">here</a>.</span>
</template>
<template lwc:if={$.rank.is.silver}>
<span>Hi, {fullName}, thanks for visiting again !</span>
</template>
<template lwc:if={$.rank.is.bronze}>
<span>Welcome, {fullName}</span>
</template>
</div>
</template>
比較はイテレーション内で行うこともできます。各イテレーション要素には、比較式を形成するためのコンパレータ・プロパティが設定されています。
import { LightningElement } from "lwc";
import { comparator, NUMBER_VALUE } from "c/directiveComparator";
export default class DirectiveComparatorIterationExample extends LightningElement {
customerId = 1;
customers = [
{ id: 1, fullName: "John Doe", rank: "gold" },
{ id: 2, fullName: "Amy Taylor", rank: "silver" },
{ id: 3, fullName: "Michael Jones", rank: "bronze" },
{ id: 4, fullName: "Jane Doe", rank: "silver" }
];
$ = comparator(this, {
customerId: NUMBER_VALUE,
customers: [
{
id: NUMBER_VALUE,
rank: ["gold", "silver", "bronze"]
}
]
});
}
<template>
<div>
<template for:each={$.customers} for:item="customer">
<div class="customer-info" key={customer.id}>
<span class="icon">
<template lwc:if={customer.$.rank.equals.gold}>
<lightning-icon
icon-name="standard:reward"
size="medium"
></lightning-icon>
</template>
<template lwc:if={customer.$.rank.equals.silver}>
<lightning-icon
icon-name="standard:promotions"
size="small"
></lightning-icon>
</template>
<template lwc:if={customer.$.rank.equals.bronze}>
<lightning-icon
icon-name="standard:customer"
size="x-small"
></lightning-icon>
</template>
</span>
<span class="name">
<template lwc:if={customer.$.id.equals.$customerId}>
<strong>** {customer.fullName} **</strong>
</template>
<template lwc:else>
<span>{customer.fullName}</span>
</template>
</span>
</div>
</template>
</div>
</template>
利用方法
コンパレータの宣言
Directive Comparatorを使用するには、Githubレポジトリからc/directiveComparator
コンポーネントを自分のプロジェクト内に移植した後、コンパレータを利用するコンポーネントにcomparator
関数をインポートします。この関数は、クラスのフィールド宣言で使用することを想定しています。
import { LightningElement } from "lwc";
import { comparator } from "c/directiveComparator";
export default class MyComponent extends LightningElement {
prop1;
prop2 = 123;
// ... other field declarations ...
// use in field declaration
$ = comparator(this, {
/* ... */
});
// ... method declarations ...
}
comparator
関数は、context
、contextType
、options
の3つのパラメータを受け取ります。
context
は、比較するプロパティのルートオブジェクトです。これは通常コンポーネントのインスタンスを参照することを想定しているので、第1引数にthis
が渡されます。
contextType
は、テンプレート内で比較したいプロパティの型定義の情報です。プリミティブなプロパティは、STRING_VALUE
、NUMBER_VALUE
、BOOLEAN_VALUE
で表すことができます。プロパティがオブジェクトや配列の場合、型定義もサブオブジェクト/配列にネストします。
import { LightningElement } from "lwc";
import {
comparator,
NUMBER_VALUE,
STRING_VALUE,
BOOLEAN_VALUE,
ANY_VALUE
} from "c/directiveComparator";
export default class MyComponent extends LightningElement {
prop1 = 1;
prop2 = "abc";
prop3 = null;
object1 = {
foo: "FOO",
bar: "BAR"
};
array1 = [];
$ = comparator(this, {
prop1: NUMBER_VALUE,
prop2: STRING_VALUE,
prop3: ANY_VALUE,
object: {
foo: STRING_VALUE,
bar: STRING_VALUE
},
array: [
{
id: STRING_VALUE,
active: BOOLEAN_VALUE
}
]
});
}
引数のcontextType
は省略することができます。contextType
を省略した場合、クラスで定義されているすべてのプロパティをスキャンし、その型情報を推定します。
ただし、省略できたとしても、推定は初期化段階でのみ実行されるため、完全な推定にはなりません。安定した利用のためには、できるだけcontextType
を引数に渡すことをお勧めします。
export default class MyComponent extends LightningElement {
prop1 = 1;
prop2 = "abc";
// ...
// the comarator field declaration should come to the last in the field declarations.
$ = comparator(this);
}
テンプレート内でのディレクティブの記述
テンプレートに比較結果を結びつける属性(例えばlwc:if
など)がある場合、クラスのプロパティを直接参照する代わりに、前のステップで宣言したコンパレータを使用することができます.
例えば、prop1
プロパティの値が1より大きいかどうかをチェックしたい場合、テンプレートをこのように書くことができます。
<template>
<div>
<template lwc:if={$.prop1.gt.one}>
<span>prop1 is greater than 1</span>
</template>
</div>
</template>
上記のテンプレートでは、$.prop1.gt.one
が比較演算のディレクティブとなっています。$.prop1
の部分がコンポーネントのprop1
プロパティの値を参照するコンパレータプロパティ、gt
が比較演算子(>)、one
がオペランドとして使用するあらかじめ定義された定数値(1)を表しています。
もし、あなたがテンプレートでイテレーションを使用していたとしても、心配する必要はありません。Directive Comparatorはそのような使い方をサポートします。
次のようなクラスが定義されているとします:
export default class MyComponent extends LightningElement {
contactId = "c01";
contacts = [
{ Id: "c01", Name: "John Doe" },
{ Id: "c02", Name: "Amy Taylor" }
//...
];
$ = comparator(this, {
contactId: STRING_VALUE,
contacts: [
{
Id: STRING_VALUE,
Name: STRING_VALUE
}
]
});
}
contactsリストを反復処理するテンプレートは、次のようになります:
<template>
<ul>
<template for:each={$.contacts} for:item="contact">
<li key={contact.Id}>
{contact.Name}
<template lwc:if={contact.$.Id.equals.$contactId}>
<strong>(*)</strong>
</template>
</li>
</template>
</ul>
</template>
上記のテンプレートでは、for:each
属性でcontacts
の代わりに$.contacts
ディレクティブを使用し、コンタクトリストを反復しています。このイテレータは、各イテレーション要素に追加のプロパティである$
を与えています。これは、イテレーション要素のプロパティに対するコンパレータオブジェクトです。
反復ループの中で、テンプレートはlwc:if
を使って条件付きで情報を表示し、その条件はcontact.$.Id.equals.$contactId
と記述されています。このcontact.$.Id
の部分は、イテレーション要素であるcontact
のId
プロパティを参照するためのコンパレータプロパティです。equals
は等号演算子を表します。$contactId
は、ルートコンテキストのプロパティ、つまりコンポーネント内のcontactId
フィールドの値を指します。
比較演算子
比較演算のディレクティブを構成するために、あらかじめ比較演算子が定義されています。以下は、利用可能な演算子です。
- is / equals - 2つの値が正確に等しい値かどうかをチェックします
- isNot / notEquals - 2つの値が正確に等しい値でないかどうかをチェックします
- gt / greaterThan - プロパティの値が比較する値にくらべて大きい値かどうかをチェックします
- gte / greaterThanOrEquals - プロパティの値が比較する値にくらべて大きいあるいは等しい値かどうかをチェックします
- lt / lessThan - プロパティの値が比較する値にくらべて小さい値かどうかをチェックします
- lte / lessThanOrEquals - プロパティの値が比較する値にくらべて小さいあるいは等しい値かどうかをチェックします
- startsWith - プロパティの値(文字列型)が比較する文字列で開始しているかどうかをチェックします
- endsWith - プロパティの値(文字列型)が比較する文字列で終了しているかどうかをチェックします
- includes - プロパティの値(文字列型)が比較する文字列を含んでいるかどうかをチェックします
- isTruthy / isFalsy - プロパティの値がtruthy/falsyかどうかをチェックします
- isNull / isNotNull - プロパティの値がnullかそうでないかをチェックします
- isUndefined / isNotUndefined - プロパティの値がJavaScriptのundefinedかそうでないかをチェックします
- isNullish / isNotNullish - プロパティの値がJavaScriptのnullあるいはundefinedかどうかをチェックします
- isEmpty / isNotEmpty - プロパティの値がJavaScriptのnullあるいはundefinedか、また文字列の場合は空文字列か、配列の場合は空配列どうかをチェックします
-
not - 後続の比較結果を否定する演算子です。例えば、
$.prop1.not.startWith.foo
は、演算子startsWith
を用いて、prop1
のプロパティ値と定数foo
との比較した結果を否定した結果となります
定数
比較演算の際には、たとえば、0、"foo"、true、nullなどの定数値とプロパティを比較する必要がある場合があります。
このような定数値は、型定義で宣言することができます。
プロパティの型定義では、比較演算に利用可能な定数値のリストを配列で渡すことができます。
export default class MyComponent extends LightningElement {
type = "customer";
$ = comparator(this, {
type: ["customer", "partner", "competitor"],
});
}
<template>
<div>
<template lwc:if={$.type.equals.competitor}>
<span>You are not allowed to submit the inquiry form, sorry.</span>
</template>
</div>
</template>
LWCのディレクティブで禁止されている文字を含むテキストや数値を渡したい場合は、名前と値のペア(tupple)で渡すことができます。
export default class MyComponent extends LightningElement {
type = "01. Customer";
limit = 10;
$ = comparator(this, {
type: [
["customer", "01. Customer"],
["partner", "02. Partner"],
["competitor", "03. Competitor"]
],
limit: [
["ten", 10],
["twenty", 20]
]
});
}
<template>
<div>
<template lwc:if={$.type.equals.competitor}>
<span>You are not allowed to submit the inquiry form, sorry.</span>
</template>
<template lwc:if={$.limit.gt.ten}>
<span>The specified limit exceeds the hard limit value (10).</span>
</template>
</div>
</template>
グローバルに利用する定数の宣言
コンポーネントのプロパティで広く使われている定数がある場合は、comparator
関数のoptions
引数のconstants
に渡します。
export default class PersonComponent extends LightningElement {
name = "Michael Johnson";
title = "CEO";
$ = comparator(
this,
{
name: STRING_VALUE,
title: STRING_VALUE
},
{
constants: {
min: 1,
max: 255
}
}
);
}
<template>
<div class="person">
<div class="name">
{name}
<template lwc:if={$.name.length.lt.min}>
<span>Name is less than minimum length</span>
</template>
<template lwc:elseif={$.name.length.gt.max}>
<span>Name exceeds maximum length</span>
</template>
</div>
<div class="title">
{title}
<template lwc:if={$.title.length.lt.min}>
<span>Title is less than minimum length</span>
</template>
<template lwc:elseif={$.title.length.gt.max}>
<span>Title exceeds maximum length</span>
</template>
</div>
</div>
</template>
事前定義済みの定数
以下の定数はあらかじめ用意されており、宣言しなくても使えます:
- zero
- one
- true
- false
- null
- undefined
コンテキストプロパティの参照
比較される値となるオペランドでは、ルートコンテキストのプロパティ、つまり、コンポーネントのフィールド値を参照することができます。オペランドにおいては$を先頭に付けた名前で参照されます。
export default class MyComponent extends LightningElement {
selected = 2;
fruits = [{
id: 1,
name: "apple"
}, {
id: 2,
name: "orange"
}, {
id: 3,
name: "melon"
}, {
id: 4,
name: "banana"
}];
$ = comparator(this, {
selected: NUMBER_VALUE,
fruits: [{
id: NUMBER_VALUE,
name: STRING_VALUE
}],
});
}
<template>
<ul>
<template for:each={$.fruits} for:item="fruit">
<li key={fruit.id}>
<template lwc:if={fruit.$.id.is.$selected}>
<b>{fruit.name}</b>
</template>
<template lwc:else>{fruit.name} </template>
</li>
</template>
</ul>
</template>
上記のテンプレートでは、lwc:if
の値にfruit.$.id.is.$selected
という比較演算ディレクティブが与えられており、そのオペランド部分に$selected
の表記が使われていますが、これはコンポーネント内のselected
フィールド値を参照していることを意味します。
まとめ
LWCの開発ってたいへんですね