Onsen UI＋Alpine.js を使ってみた

# はじめに

いわゆるウェブアプリを作るのに自分は Vue.js を使っています。

[Onsen UI＋Vue.js を使ってみた - Qiita](https://qiita.com/tinymouse/items/82cce234e4b6fc0f17c7)

友人に Alpine.js を紹介されました。よさそうなので使ってみることにします。

# Onsen UI とは

Onsen UI は、HTML5 モバイルアプリ開発用のオープンソースの UI フレームワークおよびコンポーネントです。独自の HTML タグを記述してモバイルアプリの画面を作ることができます。

[Onsen UI 2: HTML5モバイルアプリを速く、美しく - Onsen UI](https://ja.onsen.io/)

# Alpine.js とは

Alpine.js は、新しい、軽量の JavaScript フレームワークです。Vue.js と同様のディレクティブが用意されています。コンポーネントは作れませんが、そのために学習しやすくなっています。

[Alpine.js](https://alpinejs.dev/)

# Onsen UI＋Alpine.js を使ってみた

* Onsen UI 2.12
* Alpine.js 3.13

## Onsen UI を使う

CDN を使うことにします。HTML ファイルに以下を追記します。

```html:app.html
    <link rel="stylesheet" href="https://unpkg.com/onsenui@2.12.8/css/onsenui.css">
    <link rel="stylesheet" href="https://unpkg.com/onsenui@2.12.8/css/onsen-css-components.css">
    <script src="https://unpkg.com/onsenui@2.12.8/js/onsenui.min.js"></script>  
```

Onsen UI は独自タグを使います。

```html:app.html
    <ons-navigator id="main-navi" page="list-page">
    </ons-navigator>    

    <template id="list-page">
        <ons-page id="list-page">
            <ons-toolbar id="list-toolber">
                <div class="center">Try Onsen + Alpine.js</div>
            </ons-toolbar>
            <ons-list id="contact-list">
            </ons-list>
        </ons-page>
    </template>
```

ツールバーがついたリストビューが本体のページを準備します。

JavaScript のコードは ```ons.ready()``` 関数に記述します。

```js:app.js
    ons.ready(function(){
        ....
    });
```

ドキュメント：[Onsen UI 2 Docs - Onsen UI Framework](https://onsen.io/v2/docs/js.html)

## Alpine.js を使う

CDN を使うことにします。HTML ファイルに以下を追記します。

```html:app.html
    <script defer src="https://unpkg.com/alpinejs@3.13.2/dist/cdn.min.js"></script>
```

ドキュメント：[Docs - Alpine.js](https://alpinejs.dev/start-here)

## データを準備する

まず、データを準備します。

```js:app.js
    const _contacts = [
        { 
            firstName: "Alice", lastName: "Arten", hasPhone: true, phoneNumber: "555-0184" 
        },{ 
            firstName: "Bob", lastName: "Brigham", hasPhone: true, phoneNumber: "555-0163" 
        },{ 
            firstName: "Charlie", lastName: "Campbell", hasPhone: true, phoneNumber: "555-0129" 
        }
    ];
```

## データをレンダリングする

```<ons-page id="list-page">``` の内容が Alpine.js で操作されるよう ```x-data``` ディレクティブで指定します。
さらに、変数 ```_contacts``` を別名 ```list``` で扱えるよう指定します。

```html:app.html
      <ons-page id="list-page" x-data="{ list: _contacts }">
          以下略
```

```<ons-list id="content-list">``` にデータがリスト表示されるようテンプレートを追記します。
リストを表示するので ```x-for``` ディレクティブを使います。

```html:app.html
        <ons-list id="contact-list">
            <template x-for='item in list'>
                <ons-list-item>
                    <div class="center"><span x-text='item.firstName'></span>&nbsp;<span x-text='item.lastName'></span></div>
                </ons-list-item>
            </template>
        </ons-list>
```

## ユーザ入力をハンドリングする

リストビューに「編集」と「削除」ボタンを追加します。
```x-on``` ディレクティブで実行したいメソッドを指定します。

```html:app.html
        <template x-for='item in list'>
            <ons-list-item>
                中略
                <div class="right">
                    <ons-button modifier="quiet" x-on:click="showDetail(item)"><ons-icon icon="pencil"></ons-icon></ons-button>
                    <ons-button modifier="quiet" x-on:click="deleteItem(item)"><ons-icon icon="trash-o"></ons-icon></ons-button>              
                </div>
```

実行するメソッドを ```x-data``` に設定します。

```html:app.atml
        <ons-page id="list-page" x-data="{ 
            list: _contacts,

            showDetail(item) {
                console.log("showDetail:", JSON.stringify(item));
            },
            deleteItem(item) {
                console.log("deleteItem:", JSON.stringify(item));
            }
        }">
```

このコードはエラーになります。↑
```x-data``` に指定した ```"``` で始めた文字列の中で ```"``` を使っているからです。
エラーにならないよう ```"``` と ```'``` を注意して使います。↓

```html:app.atml
        <ons-page id="list-page" x-data='{ 
            list: _contacts,
    
            showDetail(item) {
                console.log("showDetail:", JSON.stringify(item));
            },
            deleteItem(item) {
                console.log("deleteItem:", JSON.stringify(item));
            }
        }'>
```

## x-data に指定する内容を関数にする

HTML タグに JavaScript コードを書けるのが Alpine.js のいいところですが、```"``` と ```'``` を注意して書かないといけないのは不便です。
```x-data``` に指定する内容を関数にして ```<script>``` のエリアに書くことができます。

```html:app.html
        <ons-page id="list-page" x-data="list_data()">
```

```js:app.js
    const list_data = () => ({
        list: _contacts,
  
        showDetail(item) {
            console.log("showDetail:", JSON.stringify(item));
        },
        deleteItem(item) {
            console.log("deleteItem:", JSON.stringify(item));
        }
    });
```

## データを削除できるようにする

```js:app.js
    const list_data = () => ({
        中略
        deleteItem(item) {
            中略
            const index = this.list.indexOf(item);
            this.list.splice(index, 1);
        }
    });
```

データを操作しただけだが、画面も変わります。

## 詳細ページを表示する

```html:app.html
        <template id="detail-page">
            <ons-page id="detail-page">
                <ons-toolbar>
                    <div class="left"><ons-back-button>Back</ons-back-button></div>
                    <div class="center">Detail</div>
                </ons-toolbar>
                <ons-list id="detail">
                    <ons-list-item>
                        <ons-input placeholder="First Name"></ons-input>
                    </ons-list-item>
                    <ons-list-item>
                        <ons-input placeholder="Last Name"></ons-input>
                    </ons-list-item>
                </ons-list>
            </ons-page>
        </template>  
```

```js:app.js
    const list_data = () => ({
        中略
        showDetail(item) {
            中略
            document.querySelector('#main-navi').pushPage('detail-page');
        },
```

Onsen UI の機能でページ遷移します。

## 詳細ページに内容を表示する

ポインタ ```_selected``` を用意して、一覧ページで選択した内容をセットします。

```js:app.js
    let _selected = null;

    const list_data = () => ({
        中略
        showDetail(item) {
            中略
            _selected = item;
            document.querySelector('#main-navi').pushPage('detail-page');
        },
```

これを詳細ページで参照できるようにします。

```js:app.js
    const detail_data = () => ({
        item: _selected
    });
```
```html:app.html
        <ons-page id="detail-page" x-data="detail_data()">
            中略
            <ons-list id="detail">
                <ons-list-item>
                    <ons-input x-model="item.firstName" placeholder="First Name"></ons-input>
                </ons-list-item>
                <ons-list-item>
                    <ons-input x-model="item.lastName" placeholder="Last Name"></ons-input>
                </ons-list-item>
```

データバインディングのため ```x-model``` ディレクティブを追記します。↑

## 双方向データバインディングする

```_selected``` の内容が画面に表示されるだけでなく、
画面の要素の内容で ```_selected``` の内容が更新されるようになります。

```_selected``` はポインタです。値がコピーされているわけではありません。
詳細画面の要素の内容を変えると、```_contents``` の内容も変わっています。

さらに、一覧ページの画面の内容も変わっています。

Alpine.js のおかげで、双方向データバインディングが簡単にできます。

## データを追加できるようにする

ツールバーに「＋」ボタンを追加して、データを追加できるようにします。

```html:app.html
      <ons-page id="list-page" x-data="list_data()">
          <ons-toolbar id="list-toolber">
              中略
              <div class="right">
                  <ons-toolbar-button x-on:click="addItem()"><ons-icon icon="fa-plus"></ons-icon></ons-toolbar-button>
              </div>
```

```js:app.js    
    const list_data = () => ({
        中略
        addItem() {
            console.log("addItem:");
            const contact = {
              firstName: "", lastName: "", hasPhone: false, phoneNumber: "" 
            };
            this.list.push(contact);
            _selected = contact;
            document.querySelector('#main-navi').pushPage('detail-page');
        }                
```

## 詳細ページで保存するまでデータを変更しないようにする

詳細ページで内容を変更すると否応なしに一覧ページの内容も変わります。また、一覧ページで「＋」実行した時点で一覧に追加されます。
詳細ページに「保存」ボタンを用意して、そこで初めて内容を変更するようにしたいものです。

詳細ページのデータを扱う変数 ```_current``` を、```_selected``` と別に用意して使用します。詳細ページを開く前に、この変数に内容をセットします。↓

```js:app.js
    let _current = null;
```
```js:app.js
        showDetail: function(item){
            中略
            _selected = item;
            _current = { ...item };
            document.querySelector('#main-navi').pushPage('detail-page');
        },
        中略
        addItem() {
            中略
            _selected = null;
            _current = {
              firstName: "", lastName: "", hasPhone: false, phoneNumber: "" 
            };
            document.querySelector('#main-navi').pushPage('detail-page');
        }
  
    中略
    const detail_data = () => ({
        中略
        item: _current,
```

詳細ページに「保存」ボタンを用意して、内容を保存するようにします。↓

```html:app.html
        <ons-page id="detail-page" x-data="detail_data()">
        <ons-toolbar>
            中略
            <div class="right">
                <ons-toolbar-button x-on:click="saveItem()">Save</ons-toolbar-button>
            </div>
```
```js:app.js
        const detail_data = () => ({
            中略
            list: _contacts,
            中略
            saveItem: function(){
                console.log("saveItem:");
        
                if (_selected) {
                    Object.assign(_selected, this.item);
                }
                else {
                    this.list.push({ ...this.item });
                }
        
                document.querySelector('#main-navi').popPage();
            }
        });    
```

内容を保存した時点で、詳細ページから一覧ページに戻るようにします。↑