はじめに
デスクトップアプリを作るための開発環境 Electron と、ウェブアプリを作るためのライブラリ Webix を使っています。
Electron を始める #JavaScript - Qiita
Electron を始めるの続き #JavaScript - Qiita
Webix を使ってみた #JavaScript - Qiita
Webix を使ってみたの続き #JavaScript - Qiita
この二つを一緒に使ってみたいと思います。
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
以下のファイルをワークスペースにコピーします。
codebase
webix.min.css
webix.min.js
配置するのはプロジェクトの以下のフォルダにしました。
src
lib
webix
webix.min.css
webix.min.js
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 のスクリプトが実行されませんでした。↓
<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
の設定は以下のようにしておきます。
<link rel="stylesheet" href="./style.css" type="text/css">
html {
background-color: white;
}
body {
margin: 0; padding: 0;
}
body {
height: 100vh; width: 100%;
}
アプリの画面を記述する
Webix ライブラリを使ってアプリの画面を記述します。
Webix の UI コンポーネントは JavaScript コードで生成します。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
の指定しないと、コンポーネントはブラウザのウィンドウ一杯に表示されます。↑
上記のコードは、通常は以下の初期化ブロックに記述します。
webix.ready(function(){
webix.ui({
(以下略)
ビルドして実行してみます。
イベントハンドラを記述する
ボタンをクリックすると実行するイベントハンドラを記述します。↓
$$('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
バックエンドのプログラムを呼出する
バックエンドの 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}!`;
}
以下の設定を用意します。
ipcMain.handle('greet', (e, arg) => {
try {
const result = greet(arg);
return [result, null];
}
catch (err) {
return [null, err.message];
}
});
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('main', {
greet: (arg) => {
return ipcRenderer.invoke('greet', arg);
}
}
この関数をフロントエンドのコードで呼出します。
$$('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
Node.js アプリのワークスペースにインストールします。
npm install @seald-io/nedb --save
Electron の Node.js プログラムに以下のコードを追加します。
const Datastore = require('@seald-io/nedb');
アプリの起動時にデータベースを作成する
実行ファイルと同じフォルダにデータファイル data.db
を作ることにします。さらに、テーブル list
を作成します。それにデータを追加しておきます。↓
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()
をフロントエンドのプログラムで呼出します。↓
ipcMain.handle('initDb', async (e, arg) => {
try {
await initDb();
return [null];
}
catch (err) {
return [err.message];
}
});
contextBridge.exposeInMainWorld('main', {
initDb: () => {
return ipcRenderer.invoke('initDb');
},
var [err] = await main.initDb();
if (!err) {
webix.message("Database initialized.");
}
else {
webix.message("Database init failed: " + err);
}
Webix の datatable を使ってみる①
datatable コンポーネントを記述する
まず、datatable
コンポーネントを画面に表示します。
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
コンポーネントにデータをセットして表示します。
データを取得するメソッドを用意する
NeDB からデータを取得するメソッドを用意します。
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];
}
})
contextBridge.exposeInMainWorld('main', {
getItems: () => {
return ipcRenderer.invoke('getItems');
},
テーブル list
に記録されている全件を取得します。↑
datatable にデータをセットする
これをフロントエンドのプログラムで呼出します。
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(){
webix.ready(async function(){
(中略)
var [err] = await main.initDb();
(中略)
var [docs, err] = await main.getItems();
(中略)
})
Webix の datatable を使ってみる②
datatable を編集可能にする
editable
プロパティを指定すると、セル単位でインライン編集できるようになります。
{
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
さらに、データの追加と削除できるように入力フォームを用意しましょう。
datatable にデータを追加する
add
メソッドを使って datatable
に追加できます。
まず、入力フォームを追加します。「追加」ボタンを用意しています。
{
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
に行を追加します。↓
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
から行を削除できます。
まず、入力フォームに「削除」ボタンを追加します。
{
view:'datatable',
id:"list",
(中略)
select:'row',
},
{
view:'form',
(中略)
{ view:"button", width:160, value:"Remove selected", click:removeData },
] }
]
}
(中略)
「追加」ボタンをクリックすると関数 removeData
を実行するようにしています。ここで datatable
から行を削除します。
function removeData(){
var id = $$('list').getSelectedId()
if (!id) {
return;
}
$$('list').remove(id);
}
選択した行を削除するので、datatable
に select:'row'
を指定して行単位で選択できるようにしておきます。↓
{
view:'datatable',
id:"list",
(中略)
select:`row`,
},
参考 https://snippet.webix.com/247a9541
データの変更を受けるメソッドを用意する①
データの追加を受けるメソッドを用意します。指定されたデータ item
をテーブル list
に追加します。
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];
}
})
contextBridge.exposeInMainWorld('main', {
insertItem: (item) => {
return ipcRenderer.invoke('insertItem', item);
},
データを変更するメソッドを呼出する①
データが変更されたイベントを受けて、上記のメソッドをフロントエンドのプログラムで呼出します。
datatable
に行が追加されるタイミングで onBeforeAdd
イベントが発生します。
$$('list').attachEvent('onBeforeAdd', function(id, data, index){
var [err] = await main.insertItem(data);
if (!err) {
webix.message("Inserted.")
}
else {
webix.message(err)
}
});
データの変更を受けるメソッドを用意する②
データの変更を受けるメソッドを用意します。指定されたデータ item
をテーブル list
に更新します。
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];
}
})
contextBridge.exposeInMainWorld('main', {
updateItem: (item) => {
return ipcRenderer.invoke('updateItem', item);
},
データを変更するメソッドを呼出する②
データが変更されたイベントを受けて、上記のメソッドをフロントエンドのプログラムで呼出します。
datatable
で表示の内容が変更されると onDataUpdate
イベントが発生します。
$$('list').attachEvent('onDataUpdate', function(id, data, old){
var [err] = await main.updateItem(data);
if (!err) {
webix.message("Updated.")
}
else {
webix.message(err)
}
});
データの変更を受けるメソッドを用意する③
データの変更を受けるメソッドを用意します。指定されたデータ item
をテーブル list
から削除します。
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];
}
})
contextBridge.exposeInMainWorld('main', {
deleteItem: (item) => {
return ipcRenderer.invoke('deleteItem', item);
},
データを変更するメソッドを呼出する③
データが変更されたイベントを受けて、上記のメソッドをフロントエンドのプログラムで呼出します。
datatable
で行が削除されるタイミングで onBeforeDelete
イベントが発生します。
$$('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
コンポーネントに指定しておきます。↓
{
view:'datatable',
id:"list",
(中略)
undo:true,
},
Webix の datatable を使ってみる③
新規追加の空行を追加する
一覧表示の最終に空行を表示してデータを入力して追加できると便利ですね。
onBeforeRender
イベントで空行を追加します。
{
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」にして、連番を表示するようにしています。↑
一覧にデータを追加したとき上記のハンドラが呼出されるよう、以下のコードを加えておきます。
var [docs, err] = await main.getItems();
if (!err) {
$$('list').parse(docs);
$$('list').render(); // 強制的に描画し直す
(以下略)
追加していいか判定する
上記のコードで canAdd
関数を用意して使っています。指定された行の、全てのセルが空でないか確認して、空がなければ行の追加を可能と判断します。↓
function canAdd(id) {
return $$('list').getColumns(true).every(function(obj){
return ($$('list').getItem(id)[obj.id])
});
}
データを変更するメソッドを呼出する②
onBeforeAdd
イベントが発生する空行を追加した時点では全ての項目は空なので、ここで記録する処理しても意味ありません。
onDataUpdate
イベントでも全ての項目がセットされていないと、処理してはだめでしょう。canAdd
を使って判定します。
$$('list').attachEvent('onBeforeAdd', function(id, data){
// 何もしない
});
$$('list').attachEvent('onDataUpdate', function(id, data){
if (!canAdd(id)) {
return;
}
// ここでデータ更新するメソッドを呼出
});
データ更新するメソッドを呼出しますが、この時点で追加か更新か判別して処理できません。↑
データの変更を受けるメソッドを用意する②
id
を使ってサーバ側で登録済か確認して、未登録なら追加、そうでなければ更新の処理するのがいいでしょう。
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];
}
})
contextBridge.exposeInMainWorld('main', {
upsertItem: (item) => {
return ipcRenderer.invoke('upsertItem', item);
}
いわゆる「UPSERT」処理ですね。NeDB では updateAsync({ upsert: true })
で書けます。↑
これをフロントエンドのプログラムで呼出します。↓
$$('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)
}
});