Electron＋Webix を使う

# はじめに

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

[Electron を始める #JavaScript - Qiita](https://qiita.com/tinymouse/items/6d111f714db061af119c)
[Electron を始めるの続き #JavaScript - Qiita](https://qiita.com/tinymouse/items/23b54cc0fe552be05b70)

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

この二つを一緒に使ってみたいと思います。

# Electron＋Webix を使う

## Electron のプロジェクトを新規作成する

まず、Electron のプロジェクトを新規作成します。

ワークスペースのフォルダおよびファイルは以下のようにしました。

```
package.json
src
    main.js
    preload.js
    index.html
    style.css
    renderer.js
node_modules
```



## Webix のライブラリを組込する

作成したプロジェクトに Webix のライブラリを組込します。

以下のページからダウンロードします。
[Download a JavaScript UI Widgets Library Webix Based on HTML5 and CSS3 Standards](http://webix.com/download/)

以下のファイルをワークスペースにコピーします。

```
codebase
    webix.min.css
    webix.min.js
```

配置するのはプロジェクトの以下のフォルダにしました。

```
src
    lib
        webix
            webix.min.css
            webix.min.js
```

`index.html` に以下のコードを追記します。

```html:index.html
    <link rel="stylesheet" href="./lib/webix/webix.css" type="text/css"> 
    <script src="./lib/webix/webix.js" type="text/javascript"></script>  
```

Electron 公式サイトにある `index.html` の `Content-Security-Policy` を変更しておかないと Webix のスクリプトが実行されませんでした。↓

```html:index.html
        <meta
        http-equiv="Content-Security-Policy"
        content="default-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'"
        />
        <meta
        http-equiv="X-Content-Security-Policy"
        content="default-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'"
        />
```

併せて、`style.css` の設定は以下のようにしておきます。

```html:index.html
    <link rel="stylesheet" href="./style.css" type="text/css">
```
```css:style.css
html {
    background-color: white;
}

body {
    margin: 0; padding: 0;
}

body {
    height: 100vh; width: 100%;
}
```

## アプリの画面を記述する

Webix ライブラリを使ってアプリの画面を記述します。

[Getting Started - Webix Docs](https://docs.webix.com/desktop__getting_started.html)

Webix の UI コンポーネントは JavaScript コードで生成します。`renderer.js` に記述します。

```js:renderer.js
    webix.ui({
        rows:[
            {
                type:'header', view:'template', template:"Try Webix",
            },
            {
                view:'form',
                elements:[
                    {
                        view:'text', id:"name", label:"Name", name:"name"
                    },
                    {
                        view:'button', id:"clickme", value:"Click me.",
                    },
                    {
                        view:'text', id:"message", label:"Message", name:"message"
                    }
                ],
            },
        ],
    });
```

Webix のコンポーネントは基本的に、親要素の領域一杯に表示されます。`container` プロパティに HTML タグを指定して、その領域一杯に表示させることもできます。
`<body></body>` に何も置かないで `container` の指定しないと、コンポーネントはブラウザのウィンドウ一杯に表示されます。↑

上記のコードは、通常は以下の初期化ブロックに記述します。

```js:rebderer.js
webix.ready(function(){
    webix.ui({
    (以下略)
```

ビルドして実行してみます。

![サンプル①.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114460/4cd4564d-0f8d-46c4-93bc-a60638c86e57.png)

## イベントハンドラを記述する

ボタンをクリックすると実行するイベントハンドラを記述します。↓

```js:renderer.js
    $$('clickme').attachEvent('onItemClick', function(){
        var name = $$('name').getValue();
        if (name == "") {
            webix.message("Please enter your name.");
            return;
        }
        $$('message').setValue("Hello, " + name);
    });
```

[Event Handling of Guides, Interacting with Users Webix Docs](https://docs.webix.com/desktop__event_handling.html)

## バックエンドのプログラムを呼出する

バックエンドの `main.js` に以下の関数を用意します。

```js:main.js
// Greet returns a greeting for the given name
function greet(name) {
    if (name == "") {
        throw new Error("argument 'name' is empty");
    }
    return `Hello, ${name}!`;
}
```

以下の設定を用意します。

```js:main.js
ipcMain.handle('greet', (e, arg) => {
    try {
        const result = greet(arg);
        return [result, null];
    }
    catch (err) {
        return [null, err.message];
    }
});
```
```js:preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('main', {
    greet: (arg) => {
        return ipcRenderer.invoke('greet', arg);
    }
}
```

この関数をフロントエンドのコードで呼出します。

```js:renderer.js
    $$('clickme').attachEvent('onItemClick', function(){
        (中略)
     /* $$('message').setValue("Hello, " + name);
        ↓ */
        var [result, err] = await main.greet(name);
        if (!err) {
            $$('message').setValue(result);
        }
        else {
            webix.message("Function failed: " + err);
        }
    });
```

# Electron＋NeDB を使ってみる

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

## Node.js の NeDb パッケージを組込する            

Electron の Node.js プログラムで NeDB データベースを利用できるよう用意します。

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

Node.js アプリのワークスペースにインストールします。

```shell
npm install @seald-io/nedb --save 
```

Electron の Node.js プログラムに以下のコードを追加します。

```js:main.js
const Datastore = require('@seald-io/nedb');
```

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

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

```js:main.js
const dbfile = path.join(path.resolve("."), "data.db");

const list = new Datastore({ filename: dbfile });

async function initDb() {
    await list.loadDatabase();

    var doc = { id:1, title:"The Shawshank Redemption", year:1994, votes:678790, rating:9.2, rank:1 };
    var docs = await list.findAsync({ id: doc.id });
    if (docs.length == 0) {
        await list.insertAsync(doc);
    }
    doc = { id:2, title:"The Godfather", year:1972, votes:511495, rating:9.2, rank:2 };
    docs = await list.findAsync({ id: doc.id });
    if (docs.length == 0) {
        await list.insertAsync(doc);
    }
}
```

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

```js:main.js
ipcMain.handle('initDb', async (e, arg) => {
    try {
        await initDb();
        return [null];
    }
    catch (err) {
        return [err.message];
    }
});
```
```js:preload.js
contextBridge.exposeInMainWorld('main', {
    initDb: () => {
        return ipcRenderer.invoke('initDb');
    },
```
```js:rederer.js
    var [err] = await main.initDb();
    if (!err) {
        webix.message("Database initialized.");
    }
    else {
        webix.message("Database init failed: " + err);
    } 
```

# Webix の datatable を使ってみる①

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

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

```js:renderer.js
webix.ui({
    rows:[
        {   
            view:'datatable',
            id:"list",
            columns:[
                { id:'no', 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/19d2326e-ea73-4765-a095-caea5f54cd3d.png)

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

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

```js:main.js
async function getItems() {
    const docs = await list.findAsync({});
    return docs;
}

ipcMain.handle('getItems', async (e, arg) => {
    try {
        const docs = await getItems();
        return [docs, null];
    }
    catch (err) {
        return [null, err.message];
    }
})
```
```js:renderer.js
contextBridge.exposeInMainWorld('main', {
    getItems: () => {
        return ipcRenderer.invoke('getItems');
    },
```

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

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

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

```js:renderer.js
    var [docs, err] = await main.getItems();
    if (!err) {
        $$('list').parse(docs);
    }
    else {
        webix.message(err);
    }
```

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

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

`initDb()` と `getItems()` は `async` 関数なので、`await` で呼出します。そのため、`webix.ready(function(){`

```js:renderer.js
webix.ready(async function(){

    (中略)

    var [err] = await main.initDb();
    (中略)

    var [docs, err] = await main.getItems();
    (中略)
})
```

# Webix の datatable を使ってみる②

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

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

```js:renderer.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/082c610a-6b68-4a03-97d4-252efa7a410d.png)

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

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

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

```js:renderer.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'],
            votes: values['votes']
        });
    }
```

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

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

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

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

```js:renderer.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` に追加します。

```js:main.js
async function insertItem(item) {
    await list.insertAsync(item);
}

ipcMain.handle('insertItem', async (e, arg) => {
    try {
        await insertItem(arg);
        return [null];
    }
    catch (err) {
        return [err.message];
    }
})
```
```js:preload.js
contextBridge.exposeInMainWorld('main', {
    insertItem: (item) => {
        return ipcRenderer.invoke('insertItem', item);
    },
```

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

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

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

```js:renderer.js
    $$('list').attachEvent('onBeforeAdd', function(id, data, index){
       var [err] = await main.insertItem(data);
        if (!err) {
            webix.message("Inserted.")
        }
        else {
            webix.message(err)
        }
    });
```

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

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

```js:main.js
async function updateItem(item) {
    await list.updateAsync({ 'id':item.id }, item);
}

ipcMain.handle('updateItem', async (e, arg) => {
    try {
        await updateItem(arg);
        return [null];
    }
    catch (err) {
        return [err.message];
    }
})
```
```js:preload.js
contextBridge.exposeInMainWorld('main', {
    updateItem: (item) => {
        return ipcRenderer.invoke('updateItem', item);
    },
```
                
## データを変更するメソッドを呼出する②

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

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

```js:main.js
    $$('list').attachEvent('onDataUpdate', function(id, data, old){
        var [err] = await main.updateItem(data);
        if (!err) {
            webix.message("Updated.")
        }
        else {
            webix.message(err)
        }
    });
```

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

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

```js:main.js
async function deleteItem(item) {
    await list.removeAsync({ 'id':item.id });
}

ipcMain.handle('deleteItem', async (e, arg) => {
    try {
        await deleteItem(arg);
        return [null];
    }
    catch (err) {
        return [err.message];
    }
})
```
```js:preload.js
contextBridge.exposeInMainWorld('main', {
    deleteItem: (item) => {
        return ipcRenderer.invoke('deleteItem', item);
    },
```

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

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

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

```js:renderer.js
    $$('list').attachEvent('onBeforeDelete', async function(id){
        var dt = this;
        var [err] = await main.deleteItem(dt.getItem(id));
        if (!err) {
            webix.message("Deleted.");
        }
        else {
            dt.undo();
            webix.message(err);
        }
    });
```

`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/40e30859-e8d0-410e-9148-04cc1a7f3268.png)

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

```js:renderer.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
    var [docs, err] = await main.getItems();
    if (!err) {
        $$('list').parse(docs);
        $$('list').render();  // 強制的に描画し直す
    (以下略)
```

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

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

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

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

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

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

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

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

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

```js:main.js
async function upsertItem(item) {
    await list.updateAsync({ 'id':item.id }, item, { upsert: true });
}

ipcMain.handle('upsertItem', async (e, arg) => {
    try {
        await upsertItem(arg);
        return [null];
    }
    catch (err) {
        return [err.message];
    }
})
```
```js:preload.js
contextBridge.exposeInMainWorld('main', {
    upsertItem: (item) => {
        return ipcRenderer.invoke('upsertItem', item);
    }
```

いわゆる「UPSERT」処理ですね。NeDB では `updateAsync({ upsert: true })` で書けます。↑

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

```js:renderer.js
    $$('list').attachEvent('onDataUpdate', async function(id, data){
        if (!canAdd(id)) {
            return;
        }
        var [err] = await main.upsertItem(data);
        if (!err) {
            webix.message("Upserted.")
        }
        else {
            webix.message(err)
        }
    });
```