Knockout.js(+Electron)でTODOアプリ作成してみた

お仕事で Knockout.js + Electron を触る可能性があるので
まずはチュートリアルってことで TODOアプリ を作成してみました

# Knockout.jsとは

[Githubリポジトリ](https://github.com/knockout/knockout)
[日本語ドキュメント](http://kojs.sukobuto.com/docs/introduction)

Knockout.jsは以下の特徴を持ったMVVMライブラリです

- エレガントな依存トラッキング
- 宣言型 バインディング
- 拡張が容易

他のMVVMに比べて非常にシンプルでわかりやすい、というのが第一印象です

## インストール

公式サイトでは `<script>` タグでの読み込みが紹介されていますが
今回は Electron で動かすので npm モジュールとしてインストールします

```
$ npm install --save knockout
```

ついでに `electron` `webpack `webpack-dev-server` をインストールします

```
$ npm install --save electron webpack webpack-dev-server
```

## webpackの設定

webpackの設定を以下のようにします

```js:webpack.config.js
const path = require('path');

module.exports = {
  context: path.resolve(__dirname, './src'),
  entry: './index.js',
  output: {
    filename: 'bundle.js',
    // webとelectron両方で動かすため path と publicPath を同じ値にしておく
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'dist'
  },

  devServer: {
    contentBase: path.resolve(__dirname, './src'),
    port: 3000,
  },

  // electron用の設定??
  target: 'electron-renderer'
};⏎
```

今回はwebとelectron両方で動かしたかったので
pathとpublicPathの値を同じパスにしました。

`target` の値を `electron-renderer` にすると
electron用にビルドしてくれる？みたいですが正直よくわかりませんでした。。。

またpackage.jsonにもコマンドを追加しておきます

```js:package.json
  "scripts": {
    "webpack": "webpack-dev-server --hot --inline --target web",
    "electron": "webpack && electron ."
  },
```

## UI

TODOアプリの見た目は[こちらのサイト](https://www.w3schools.com/howto/howto_js_todolist.asp)からお借りしました

```html:src/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <link rel="stylesheet" href="style.css">
    <script src="../dist/bundle.js"></script>
  </head>
  <body>
    <div class="header">
        <h2>My To Do List</h2>
        <input type="text" placeholder="Title..." data-bind="value: taskSummary">
        <span class="addBtn" data-bind="click: addTask">Add</span>
      </div>
      
      <ul data-bind="foreach: todoList">
        <li data-bind="text: summary, css: {'checked': isDone}, click: $root.toggleDone"></li>
      </ul>
  </body>
</html>

```

`data-bind`属性の部分が knockout.jsのデータバインドを宣言している部分となります
シンプルでわかりやすいですね:sparkles:

## ロジック

```js:src/index.js
const ko = require('knockout')

function Viewmodel() {
    let self = this

    self.todoList = ko.observableArray([
        {summary: "hogehoge", isDone: true},
        {summary: "hogehoge", isDone: true},
        {summary: "hogehoge3", isDone: true}
    ])

    self.toggleDone = (task) => {
        let updateTask = {
            summary: task.summary,
            isDone: !task.isDone
        }

        self.todoList.replace(task, updateTask)
    }

    self.taskSummary = ko.observable('')
    self.addTask = _ => {
        let newTask = {
            summary: self.taskSummary(),
            isDone: false
        }
        
        self.todoList.push(newTask)
        self.taskSummary('')
    }
}

window.onload = _ => {
    ko.applyBindings(new Viewmodel())
}
```

Viewmodelを作成し、そちらを `ko.applyBindings` に渡しています。
ES2015で書くんだったら class で作成したほうがよかったかもしれないです

ko.applyBindings 関数は DOMが準備された後に呼び出さないとエラーになるため
windows.onloadの中で読んでいます

Viewmodelではプロパティの値を `ovservable` `observableArray ` でラッピングしています
こうすることでプロパティの更新をknocokout.jsが検知し、UIを変更します

observavleArrayの特定の要素のみ更新する方法でちょっとハマりましたが
observableArray#replace 関数を使うことで特定の要素のみ更新できるようです


参考: https://stackoverflow.com/questions/8774943/knockoutjs-observablearray-to-update-when-inner-observable-is-changed

## electron

Electronで動かすための main.js を書きます

```js:src/main.js
// electronモジュールを読み込み
const electron = require('electron');
const {app} = electron;
const {BrowserWindow} = electron; //ウィンドウを表す[BrowserWindow]はelectronモジュールに含まれている

// 新しいウィンドウ(Webページ)を生成
let win;
function createWindow() {
  // BrowserWindowインスタンスを生成
  win = new BrowserWindow({width: 800, height: 600});
  // index.htmlを表示
  win.loadURL(`file://${__dirname}/src/index.html`);
  // デバッグするためのDevToolsを表示
  win.webContents.openDevTools();
  // ウィンドウを閉じたら参照を破棄
  win.on('closed', () => {   // ()は　function ()と書いていい
    win = null;
  });
}
// アプリの準備が整ったらウィンドウを表示
app.on('ready', createWindow);
// 全てのウィンドウを閉じたらアプリを終了
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});
app.on('activate', () => {
  if (win === null) {
    createWindow();
  }
});
```

ネットに落ちていたサンプルコードをそのままコピペしただけになります。。。


## 作成したもの

デモ: https://tamanugi.github.io/knockout_electron/src/index.html
リポジトリ: https://github.com/tamanugi/knockout_electron

#　感想とか

とてもシンプルでとっつきやすいライブラリだと感じました。
他のMVVMとくらべても覚えることが少なく、非常にわかりやすいです

どちらかというとWebpackとelctronについて勉強しないとだめですね。。。(webpackﾑｽﾞｶｼｲ