Wails＋Webix を使ってみたの続き

# はじめに

デスクトップアプリを作るための開発環境 Wails と、ウェブアプリを作るためのライブラリ Webix を使ってみました。

[Wails＋Webix を使ってみた #Wails - Qiita](https://qiita.com/tinymouse/items/9107e434c40612ffd8ac)

Webix のコンポーネントは多くの種類が用意されていますが特に、データを一覧表示する `datatable` コンポーネントは便利です。

[Webix を使ってみたの続き #JavaScript - Qiita](https://qiita.com/tinymouse/items/bf8ce1b37bac8df98c7c)

これを Wails アプリで使ってみたいと思います。

# Wails＋SQLite を使ってみる

アプリのデータは SQLite データベースに記録するようにしたいと思います。

## Go の SQLite パッケージを組込する            

Wails の Go プログラムで SQLite データベースを利用できるよう用意します。

[Go 言語で SQLite を使う（Windows 向けの紹介） | text.Baldanders.info](https://text.baldanders.info/golang/sqlite-with-golang-in-windows/)
[Go 言語で SQLite3 #Mac - Qiita](https://qiita.com/KoyoTakenoshita/items/522a4ff1a81b51e8d5b3)

Wails の Go プログラムに以下のコードを追加します。

```go:app.go
import (
    "database/sql"

    _ "github.com/mattn/go-sqlite3"
)
```

`wails build` ないし `wails dev` すると、`go mod tidy` されて、プロジェクトの `go.mod` ファイルが更新され、開発環境の `go` ディレクトリに必要なパッケージがインストールされます。

## アプリの起動時にデータベースを作成する            

実行ファイルと同じフォルダにデータファイル `data.db` を作ることにします。さらに、テーブル `list` を作成します。それにデータを追加しておきます。↓

```go:app.go
var dbfile string

func init() {
    path, _ := os.Executable()
    dbfile = filepath.Join(filepath.Dir(path), "data.db")
}

func (a *App) InitDb() error {
    db, _ := sql.Open("sqlite3", dbfile)
    defer db.Close()

    _, err := db.Exec(`CREATE TABLE IF NOT EXISTS list (
        id STRING, title STRING, year INT, votes INT, rank INT,
        PRIMARY KEY(id)
    )`)
    if err != nil {
        return err
    }
    _, err = db.Exec(`INSERT INTO list (id, title, year, votes) VALUES (
        1, "The Shawshank Redemption", 1994, 678790
    ) 
    ON CONFLICT (id) DO NOTHING
    ;
    INSERT INTO list (id, title, year, votes) VALUES (
        2, "The Godfather", 1972, 511495
    )
    ON CONFLICT (id) DO NOTHING`)
    if err != nil {
        return err
    }
    return nil
}
```

全て `init()` に書いてもよさそうですが、処理でエラー発生したときにフロントエンドのプログラムでメッセージ表示したいでしょう。`InitDb()` をフロントエンドのプログラムで呼出します。↓

```js:main.js
import {InitDb} from '../wailsjs/go/main/App';

webix.ready(function(){

    InitDb()
    .then(function(){
        webix.message("Database initialized.");
    })
    .catch(function(reason){
        webix.message("Database init failed: " + reason);
    })
```

# Webix の datatable を使ってみる①

## datatable コンポーネントを記述する

まず、`datatable` コンポーネントを画面に表示します。

```js:main.js
webix.ui({
    rows:[
        {   
            view:'datatable',
            id:"list",
            columns:[
                { id:'rank', header:"", width:30 },
                { id:'title', header:"Title", width:300 },
                { id:'year', header:"Released", width:80 },
                { id:'votes', header:"Votes", width:100    }
            ],
        },
    ]
});
```

この `datatable` コンポーネントにデータをセットして表示します。

![サンプル①.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114460/b79f8fff-3383-0f5b-9ee6-a2bc426d2cf4.png)

## データを取得するメソッドを用意する

SQLite からデータを取得するメソッドを用意します。

```go:app.go
type Item struct {
    Id    string `json:"id"`
    Title string `json:"title"`
    Year  int    `json:"year"`
    Votes int    `json:"votes"`
}

func (a *App) GetItems() ([]Item, error) {
    db, _ := sql.Open("sqlite3", dbfile)
    defer db.Close()

    rows, _ := db.Query(`SELECT id, title, year, votes FROM list`)
    defer rows.Close()

    var items []Item
    for rows.Next() {
        var item Item
        err := rows.Scan(&item.Id, &item.Title, &item.Year, &item.Votes)
        if err != nil {
            return []Item{}, err
        }
        items = append(items, item)
    }
    return items, nil
}
```

テーブル `list` に記録されている全件を取得します。↑

## datatable にデータをセットする

これをフロントエンドのプログラムで呼出します。

```js:main.js
import {InitDb, GetItems} from '../wailsjs/go/main/App';

webix.ready(function(){
    (中略)

    GetItems()
    .then(function(result){
        $$('list').parse(result);
    })
    .catch(function(reason){
        webix.message(reason);
    });
```

`parse` メソッドを使って `datatable` にデータをセットします。↑

参考 https://snippet.webix.com/598f77de

データベースの準備する `InitDb()` が完了する前に、データを取得する処理 `GetItems()` を呼出してはまずいでしょう。

```js:main.js
    (async function(){
        await InitDb()
        .then(function(){
            webix.message("Database initialized.");
        })
        .catch(function(reason){
            webix.message("Database init failed: " + reason);
        });
    
        await GetItems()
        .then(function(result){
            $$('list').parse(result);
        })
        .catch(function(reason){
            webix.message("Load failed: " + reason);
        });
    })();
```

# Webix の datatable を使ってみる②

### datatable を編集可能にする

`editable` プロパティを指定すると、セル単位でインライン編集できるようになります。

```js:main.js
        {   
            view:'datatable',
            id:"list",
            (中略)
            columns:[
                { id:'rank', header:"", width:30 },
                { id:'title', header:"Title", width:300, editor:'text' },
                { id:'year', header:"Released", width:80, editor:'text' },
                { id:'votes', header:"Votes", width:100, editor:'text' }
            ],
            editable:true,
        },
```

参考 https://snippet.webix.com/f1edcadc

さらに、データの追加と削除できるように入力フォームを用意しましょう。

![サンプル②.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114460/330cd8b0-de5c-555a-db08-531a65b0a9b8.png)

### datatable にデータを追加する

`add` メソッドを使って `datatable` に追加できます。

まず、入力フォームを追加します。「追加」ボタンを用意しています。

```js:main.js
        { 
            view:'datatable', 
            id:"list",
            (中略)
        },
        { 
            view:'form',
            id:"input",
            elements:[
                { view:'text', name:"title", value:"New Movie",inputWidth:200 },
                { view:'text', name:"year", value:"2025", inputWidth:200 },
                { view:"text", name:"votes", value:"999999", inputWidth:200 },
                { cols:[
                    { view:'button', width:100, value:"Add", click:addData }, 
                ] }
            ]
        }
```

「追加」ボタンをクリックすると関数 `addData` を実行するようにしています。ここで `datatable` に行を追加します。↓

```js
    function addData(){
        var values = $$('input').getValues();
        $$('list').add({
            title: values['title'],
            year: values['year']
        });
    }
```

参考 https://snippet.webix.com/247a9541

### datatable にデータを削除する            

`remove` メソッドを使って `datatable` から行を削除できます。

まず、入力フォームに「削除」ボタンを追加します。

```js:main.js
        { 
            view:'datatable', 
            id:"list",
            (中略)
            select:'row',
        },
        { 
            view:'form',
            (中略)
                    { view:"button", width:160, value:"Remove selected", click:removeData }, 
                ] }
            ]
        }
(中略)
```

「追加」ボタンをクリックすると関数 `removeData` を実行するようにしています。ここで `datatable` から行を削除します。

```js
function removeData(){
    var id = $$('list').getSelectedId()
    if (!id) {
        return;
    }
    $$('list').remove(id);
}
```

選択した行を削除するので、`datatable` に `select:'row'` を指定して行単位で選択できるようにしておきます。↓

```js
        {   
            view:'datatable',
            id:"list",
            (中略)
            select:`row`,
        },
```

参考 https://snippet.webix.com/247a9541
    
## データの変更を受けるメソッドを用意する①

データの追加を受けるメソッドを用意します。指定されたデータ `item` をテーブル `list` に追加します。

```go:app.go
func (a *App) InsertItem(item map[string]interface{}) error {
    db, _ := sql.Open("sqlite3", dbfile)
    defer db.Close()

    _, err := db.Exec(`INSERT INTO list (id, title, year, votes) VALUES (?, ?, ?, ?)`,
        item["id"], item["title"], item["year"], item["votes"])
    if err != nil {
        return err
    }
    return nil
}
```

## データを変更するメソッドを呼出する①

データが変更されたイベントを受けて、上記のメソッドをフロントエンドのプログラムで呼出します。

`datatable` に行が追加されるタイミングで `onBeforeAdd` イベントが発生します。

```js:main.js
import {InitDb, GetItems, InsertItem} from '../wailsjs/go/main/App';
(中略)
    $$('list').attachEvent('onBeforeAdd', function(id, data, index){
        InsertItem(data)
        .then(function(){
            webix.message("Inserted.")
        })
        .catch(function(reason){
            webix.message(reason)
        });
    });
```

## データの変更を受けるメソッドを用意する②

データの変更を受けるメソッドを用意します。指定されたデータ `item` をテーブル `list` に更新します。

```go:app.go
func (a *App) UpdateItem(item map[string]interface{}) error {
    db, _ := sql.Open("sqlite3", dbfile)
    defer db.Close()

    _, err := db.Exec(`UPDATE list SET title = ?, year = ?, votes = ? WHERE id = ?`,
        item["title"], item["year"], item["votes"], item["id"])
    if err != nil {
        return err
    }
    return nil
}
```
                
## データを変更するメソッドを呼出する②

データが変更されたイベントを受けて、上記のメソッドをフロントエンドのプログラムで呼出します。

`datatable` で表示の内容が変更されると `onDataUpdate` イベントが発生します。

```js:main.js
import {InitDb, GetItems, InsertItem, UpdateItem} from '../wailsjs/go/main/App';
(中略)
    $$('list').attachEvent('onDataUpdate', function(id, data, old){
        UpdateItem(data)
        .then(function(){
            webix.message("Updated.")
        })
        .catch(function(reason){
            webix.message(reason)
        });
    });
```

## データの変更を受けるメソッドを用意する③

データの変更を受けるメソッドを用意します。指定されたデータ `item` をテーブル `list` から削除します。

```go:app.go
func (a *App) DeleteItem(item map[string]interface{}) error {
    db, _ := sql.Open("sqlite3", dbfile)
    defer db.Close()

    _, err := db.Exec(`DELETE FROM list WHERE id = ?`, item["id"])
    if err != nil {
        return err
    }
    return nil
}
```

## データを変更するメソッドを呼出する③

データが変更されたイベントを受けて、上記のメソッドをフロントエンドのプログラムで呼出します。

`datatable` で行が削除されるタイミングで `onBeforeDelete` イベントが発生します。

```js:main.js
import {InitDb, GetItems, InsertItem, UpdateItem, DeleteItem} from '../wailsjs/go/main/App';
(中略)
    $$('list').attachEvent('onBeforeDelete', function(id){
        var dt = this;
        DeleteItem(dt.getItem(id))
        .then(function(){
            webix.message("Deleted.");
        })
        .catch(function(reason){
            dt.undo();
            webix.message(reason);
        });
    });
```

`DaleteItem` メソッドが失敗したときは `datatable` コンポーネントで行の削除をアンドゥします。↑
アンドゥできるように `datatable` コンポーネントに指定しておきます。↓

```js
        {   
            view:'datatable',
            id:"list",
            (中略)
            undo:true,
        },
```

# Webix の datatable を使ってみる③

### 新規追加の空行を追加する

一覧表示の最終に空行を表示してデータを入力して追加できると便利ですね。

![サンプル③.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114460/a2bd1751-a85d-f1da-be7d-0098b579a567.png)


`onBeforeRender` イベントで空行を追加します。

```js:main.js
        {   
            view:'datatable',
            id:"list",
            columns:[
                { id:'no', header:"", width:30 },
                { id:'title', header:"Title", width:300, editor:'text' },
                { id:'year', header:"Released", width:80, editor:'text' },
                { id:'votes', header:"Votes", width:100, editor:'text' }
            ],
            editable:true,
        },
    (中略)

    $$('list').attachEvent('onBeforeRender', function(){
        var dt = this;
        if (dt.count() == 0){
            dt.add({});
        }
        if (canAdd(dt.getLastId())) {
            dt.add({});
        }
        dt.data.each(function(obj, i){
            obj['no'] = i + 1;
        });
    });
```

先頭の列を「no」にして、連番を表示するようにしています。↑

一覧にデータを追加したとき上記のハンドラが呼出されるよう、以下のコードを加えておきます。

```js
    GetItems()
    .then(function(result){
        $$('list').parse(result);
        $$('list').render();  // 強制的に描画し直す
    (以下略)
```

### 追加していいか判定する

上記のコードで `canAdd` 関数を用意して使っています。指定された行の、全てのセルが空でないか確認して、空がなければ行の追加を可能と判断します。↓

```js
    function canAdd(id) {
        return $$('list').getColumns(true).every(function(obj){
            return ($$('list').getItem(id)[obj.id])
        });
    }
```

## データを変更するメソッドを呼出する②

`onBeforeAdd` イベントが発生する空行を追加した時点では全ての項目は空なので、ここで記録する処理しても意味ありません。
`onDataUpdate` イベントでも全ての項目がセットされていないと、処理してはだめでしょう。`canAdd` を使って判定します。

```js:main.js
    $$('list').attachEvent('onBeforeAdd', function(id, data){
        // 何もしない
    });
    
    $$('list').attachEvent('onDataUpdate', function(id, data){
        if (!canAdd(id)) {
            return;
        }
        // ここでデータ更新するメソッドを呼出
    });
```

データ更新するメソッドを呼出しますが、この時点で追加か更新か判別して処理できません。↑

## データの変更を受けるメソッドを用意する②

`id` を使ってサーバ側で登録済か確認して、未登録なら追加、そうでなければ更新の処理するのがいいでしょう。

```go:app.go
func (a *App) UpsertItem(item map[string]interface{}) error {
    db, _ := sql.Open("sqlite3", dbfile)
    defer db.Close()

    _, err := db.Exec(`INSERT INTO list (id, title, year, votes) VALUES ($1, $2, $3, $4)
        ON CONFLICT(id)
        DO UPDATE SET title = $2, year = $3, votes = $4`,
        item["id"], item["title"], item["year"], item["votes"])
    if err != nil {
        return err
    }
    return nil
}
```

いわゆる「UPSERT」処理ですが、SQLite なら一回のクエリで書くことができます。↑

これをフロントエンドのプログラムで呼出します。↓

```js:main.js
import {InitDb, GetItems, UpsertItem, DeleteItem} from '../wailsjs/go/main/App';
(中略)
    $$('list').attachEvent('onDataUpdate', function(id, data){
        if (!canAdd(id)) {
            return;
        }
        UpsertItem(data)
        .then(function(){
            webix.message("Upserted.")
        })
        .catch(function(reason){
            webix.message(reason)
        });
    });
```