Lightning Web コンポーネントで動的入力フォームを生成する

# 概要
LightningWebコンポーネントで入力フォームを動的に追加・削除する機能を簡易的にはでありますが作成したいと思います。
LWCではどうやらcreateElementのような動的にDOMを作成する機能を意図的に制限しているようです。
そのため、HTMLテンプレートとして用意されている`for:each`ディレクティブを使用し、Listを反復処理させる事で入力フォームの追加・削除を実現していきます。

# 実装イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/803801/727db3dd-ab83-9617-f679-491fe663075e.png)


# 全体コード
```html:form.html
<template>
    <lightning-card title="顧客登録">
        <lightning-button label="Add" slot="actions" onclick={addRow}></lightning-button>
        <div class="slds-p-horizontal_small">        
            <table class="slds-table slds-table_bordered slds-table_cell-buffer"> 
                <thead>
                    <tr class="slds-text-title_caps">
                        <th scope="col">
                            <div class="slds-truncate">姓</div>
                        </th>
                        <th scope="col" class="table-body">
                            <div class="slds-truncate">名</div>
                        </th>
                        <th scope="col" class="table-body">
                            <div class="slds-truncate">メールアドレス</div>
                        </th>
                        <th scope="col" class="table-body">
                            <div class="slds-truncate"></div>
                        </th>
                    </tr>
                </thead>
                <tbody>
                    <template for:each={list} for:item="row" for:index="index">
                        <tr key={row.key} id={row.key}> 
                            <td class="table-body" style="padding-bottom:20px;">
                                <lightning-input data-id={index} type="text" onchange={handleChangeLastName}></lightning-input>
                            </td>
                            <td class="table-body" style="padding-bottom:20px;">
                                <lightning-input data-id={index} type="text" onchange={handleChangeFirstName}></lightning-input>
                            </td>
                            <td class="table-body" style="padding-bottom:20px;">
                                <lightning-input data-id={index} type="email" onchange={handleChangeEmail}></lightning-input>
                            </td>
                            <td>
                                <lightning-button-icon 
                                    icon-name="utility:delete"
                                    data-id={index}       
                                    alternative-text="Delete"     
                                    class="slds-m-left_xx-small"
                                    onclick={removeRow} 
                                    title="Delete">
                                </lightning-button-icon>
                            </td>
                        </tr>
                    </template>
                </tbody>
            </table>
        </div>
        <p slot="footer">
            <lightning-button name="Save" label="登録" onclick={handleClickSubmit} disabled={disabled}></lightning-button>
        </p>
    </lightning-card>
</template>
```

```js:form.js
import { LightningElement, track } from 'lwc';

export default class DynamicForm extends LightningElement {
    @track list = [];
    index = 0;
    item = {};

    addRow(){
        this.index++;
        var i = this.index;
        var item = {};
        item.key = i;
        this.list.push(item);
    }
    removeRow(e){
        var selectedRow = e.currentTarget;
        var key = selectedRow.dataset.id;
        if(this.list.length>1){
            this.list.splice(key, 1);
            this.index--;
        }else if(this.list.length == 1){
            this.list = [];
            this.index = 0;
        }
    }
    handleChangeLastName(e){
        var selectedRow = e.currentTarget;
        var key = selectedRow.dataset.id;
        this.list[key].lastname = e.detail.value;
    }
    handleChangeFirstName(e){
        var selectedRow = e.currentTarget;
        var key = selectedRow.dataset.id;
        this.list[key].firstname = e.detail.value;
    }
    handleChangeEmail(e){
        var selectedRow = e.currentTarget;
        var key = selectedRow.dataset.id;
        this.list[key].email = e.detail.value;
    }
    handleClickSubmit(){
        // 登録処理
    }
}
```

# 解説
js側で宣言したlist変数をHTML側で反復処理していきます。

```html
<tbody>
    <template for:each={list} for:item="row" for:index="index">
        <tr key={row.key} id={row.key}> 
            <td class="table-body" style="padding-bottom:20px;">
                <lightning-input data-id={index} type="text" onchange={handleChangeLastName}></lightning-input>
            </td>
            <td class="table-body" style="padding-bottom:20px;">
                <lightning-input data-id={index} type="text" onchange={handleChangeFirstName}></lightning-input>
            </td>
            <td class="table-body" style="padding-bottom:20px;">
                <lightning-input data-id={index} type="email" onchange={handleChangeEmail}></lightning-input>
            </td>
            <td>
                <lightning-button-icon 
                    icon-name="utility:delete"
                    data-id={index}       
                    alternative-text="Delete"     
                    class="slds-m-left_xx-small"
                    onclick={removeRow} 
                    title="Delete">
                </lightning-button-icon>
            </td>
        </tr>
    </template>
</tbody>
```

Addボタンがクリックされた際にこのlist変数に対してindexを値として持つキーを設定します。

```js
addRow(){
    this.index++;
    var i = this.index;
    var item = {};
    item.key = i;
    this.list.push(item);
}
```

また、HTMLで反復処理している各アイテムの要素に対して、indexをdata-idとして与える事でlist変数と入力フォームの各行を連動させていきます。
フォームに文字列が入力された際、`currentTarget`でクリックされた要素を取得。`dataset.id`でdata-idの値を取得します。

```js
handleChangeLastName(e){
    var selectedRow = e.currentTarget;
    var key = selectedRow.dataset.id;
    this.list[key].lastname = e.detail.value;
}
handleChangeFirstName(e){
    var selectedRow = e.currentTarget;
    var key = selectedRow.dataset.id;
    this.list[key].firstname = e.detail.value;
}
handleChangeEmail(e){
    var selectedRow = e.currentTarget;
    var key = selectedRow.dataset.id;
    this.list[key].email = e.detail.value;
}
```

ここで取得したdata-idのindex値に対応するlist変数の要素に対してキーバリューをセットする事で入力値を各要素に格納します。

また、行を削除する時は入力を格納する時と同様にクリックされた要素のdata-idを取得し、list変数から該当のindex要素を削除します。

```js
removeRow(e){
    var selectedRow = e.currentTarget;
    var key = selectedRow.dataset.id;
    if(this.list.length>1){
        this.list.splice(key, 1);
        this.index--;
    }else if(this.list.length == 1){
        this.list = [];
        this.index = 0;
    }
}
```

# 最後に
後はlistをapex側に送り、自由に作成処理を行ってください。
私はjs側でjson文字列に変換した後、apex側で再度プリミティブ型に変換して処理しました。
もっと効率的な手法があればご指摘頂きたいです。
追加するフォームを別コンポーネントとして作成し連携させれば、より柔軟にDOM操作が行えそうですね。