LWCのテンプレート内で比較演算ディレクティブを実現する方法

（以下、[Directive Comparator](https://github.com/stomita/lwc-directive-comparator)のREADMEの日本語訳です。著者は同じであるため、一部日本語ではニュアンスを変えていますが、基本的には同じ内容です。最新情報は原文の方を読んでください）

---

## LWCでのコンポーネント記述にまつわる問題

コンポーネント内に`rank`と`fullName`のプロパティがあり、`rank`の値が特別なものである場合に特別なメッセージを表示したい場合、Auraコンポーネントの場合は以下のように書きますね。

```html
<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関数に移動する必要があります。

```javascript:myComponent1.js
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';
  }
}
```

```html:myComponent1.html
<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>
```

配列のループ内で比較するとなると、気が遠くなる作業です。各項目の比較結果を含むように配列を前もって変換しておく必要があります。

```javascript:myComponent2.js
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',
    });
  }
}
```

```html:myComponent.html
<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](https://github.com/stomita/lwc-directive-comparator)は、上記の懸念を解決します。クラスからgetterを削除し、コンパレータ生成関数で生成された初期値（不変の値）を持つプロパティをクラスに追加するだけです。

```javascript:directiveComparatorSimpleExample.js
import { LightningElement } from "lwc";
import { comparator } from "c/directiveComparator";

export default class DirectiveComparatorSimpleExample extends LightningElement {
  rank;

  fullName;

  $ = comparator(this, {
    rank: ["gold", "silver", "bronze"]
  });
}
```

テンプレートのマークアップはこのようになります。クラスには比較のためのgetter関数はどこにもありません。

```html:directiveComparatorSimpleExample.html
<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>
```

比較はイテレーション内で行うこともできます。各イテレーション要素には、比較式を形成するためのコンパレータ・プロパティが設定されています。

```javascript:directiveComparatorIterationExample.js
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"]
      }
    ]
  });
}
```

```html:directiveComparatorIterationExample.html
<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レポジトリ](https://github.com/stomita/lwc-directive-comparator)から`c/directiveComparator`コンポーネントを自分のプロジェクト内に移植した後、コンパレータを利用するコンポーネントに`comparator`関数をインポートします。この関数は、クラスのフィールド宣言で使用することを想定しています。

```javascript
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`で表すことができます。プロパティがオブジェクトや配列の場合、型定義もサブオブジェクト/配列にネストします。

```javascript
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`を引数に渡すことをお勧めします。

```javascript
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より大きいかどうかをチェックしたい場合、テンプレートをこのように書くことができます。

```html
<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はそのような使い方をサポートします。

次のようなクラスが定義されているとします：

```javascript
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リストを反復処理するテンプレートは、次のようになります：

```html
<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などの定数値とプロパティを比較する必要がある場合があります。
このような定数値は、型定義で宣言することができます。
プロパティの型定義では、比較演算に利用可能な定数値のリストを配列で渡すことができます。

```javascript
export default class MyComponent extends LightningElement {
  type = "customer";

  $ = comparator(this, {
    type: ["customer", "partner", "competitor"],
  });
}
```

```html
<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）で渡すことができます。

```javascript
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]
    ]
  });
}
```

```html
<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`に渡します。

```javascript
export default class PersonComponent extends LightningElement {
  name = "Michael Johnson";
  title = "CEO";

  $ = comparator(
    this,
    {
      name: STRING_VALUE,
      title: STRING_VALUE
    },
    {
      constants: {
        min: 1,
        max: 255
      }
    }
  );
}
```

```html
<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

### コンテキストプロパティの参照

比較される値となるオペランドでは、ルートコンテキストのプロパティ、つまり、コンポーネントのフィールド値を参照することができます。オペランドにおいては$を先頭に付けた名前で参照されます。

```javascript
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
    }],
  });
}
```

```html
<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の開発ってたいへんですね