Aurelia + GraphQL でチュートリアルのTodoアプリを作成する

# はじめに

Aureliaの公式サイトには、Todoアプリを作成するチュートリアル([https://aurelia.io/docs/tutorials/creating-a-todo-app/](https://aurelia.io/docs/tutorials/creating-a-todo-app/))があります。
今回は、GraphQL + ApolloでTodoをDBで管理できるようにカスタマイズしたいと思います。
基本的な構造は、公式のチュートリアルと同じになります。

完成系のソースコードは[Github](https://github.com/dkawabata/aurelia-apollo-todo)に置いています。

# 環境

|                | バージョン |
| :-----------   | :-------- |
| aurelia-cli    | v1.2.2 |
| hasura         | v1.0.0-beta.10 |

# 環境構築

## Aureliaプロジェクト

`aurelia-cli`を使って、Todoアプリ用のプロジェクトを作成します。
`aurelia-cli`がインストールされていない場合は、インストールします。

```bash
npm install -g aurelia-cli
```

`au new`コマンドでプロジェクトを作成します。
今回は、プロジェクト名を`aurelia-apollo-todo`、それ以外はデフォルトで作成しています。

```bash
au new

? Please enter a name for your new project: aurelia-apollo-todo
? Would you like to use the default setup or customize your choices? …
▸ Default ESNext App
? Would you like to install all the npm dependencies? …
▸ Yes, use Yarn
```

プロジェクトの作成が完了したら、起動してみましょう。

```bash
cd aurelia-apollo-todo
au run
```

`http://localhost:8080`にアクセスすると、『Hello World!』が表示されます。

## GraphQLサーバー
GraphQLサーバーとして、Hasuraを使用します。
Hasuraはローカル環境で動かすために、`docker-compose.yml`が提供されています。

```bash
wget https://raw.githubusercontent.com/hasura/graphql-engine/master/install-manifests/docker-compose/docker-compose.yaml
```

ついでに、先ほど作成したAureliaのプロジェクトも、Docker Composeで動かすようにします。

```Dockerfile:Dockerfile
FROM node:10.17.0-alpine

WORKDIR /app

COPY ./package.json .
COPY ./yarn.lock .

RUN apk update && \
  yarn global add aurelia-cli && \
  yarn
```

```yml:docker-compose.yml
version: '3.6'
services:
  web:
    build: .
    ports:
      - 8000:8000
    volumes:
      - .:/app
    stdin_open: true
    tty: true
    command: yarn start --host 0.0.0.0
  postgres:
    image: postgres
    volumes:
      - db_data:/var/lib/postgresql/data
  graphql-engine:
    image: hasura/graphql-engine:v1.0.0-beta.10
    ports:
      - "8080:8080"
    depends_on:
      - "postgres"
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:@postgres:5432/postgres
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
      HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
      ## uncomment next line to set an admin secret
      # HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
volumes:
  db_data:
```

```diff:aurelia_project/aurelia.json
"platform": {
  "hmr": false,
  "open": false,
-  "port": 8000,
+  "port": 8000,
  "host": "localhost",
  "output": "dist"
},
```

AureliaのプロジェクトとHasuraでPortが同じになってしまうので、Aureliaのほうを`8000`に変更しています。
`Dockerfile`と`docker-compose.yml`が作成できたら、Docker Composeを起動します。

```bash
docker-compose up -d --build
```

`http://localhost:8000`にアクセスすると、先ほどと同じく『Hello World!』が表示されます。
`http://localhost:8080`にアクセスすると、Hasuraの画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161900/f957e916-b920-5788-0d6d-2beab19ee7d7.png)


## todosテーブルの作成

Todoを登録するために使用するテーブルを作成します。

- uuid ・・・一意になるID
- description ・・・Todoの内容
- done・・・完了したかどうか

内容を登録するための`description`と、完了したかどうかを登録する`done`というカラムを用意します。
`DATA` - `Create Table`と選択して、実際に`todos`テーブルを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161900/5ff8babe-7068-3dcd-38cf-656888dd8557.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161900/3ed4dc1f-23a1-872f-63c0-5108663d339c.png)

必要な項目を入力して、`Add Table`をクリックすると、`todos`テーブルが作成されます。

Todoアプリに表示するためのサンプルデータを投入しておきましょう。
Insert Rowタブを選択すると、データの登録が出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161900/580540e1-4c84-cc8c-7c5d-b9aac7a5f988.png)

内容は何でも構いませんが、`description`には「Sample Todo」と入力し、`done`はFalseを選択しておきます。
入力出来たら、`Save`をクリックするとデータが登録されます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161900/2fd36dd0-0aaf-928b-b9cc-f73a8d274ffb.png)

# Todoアプリ作成

本題のTodoアプリを作成していきます。
まずは必要なライブラリをインストールします。

```bash
docker-compose exec web yarn add apollo-client apollo-cache-inmemory apollo-link-http graphql-tag graphql
```

webpack.config.jsにloaderの設定を追加しておきます。

```diff:webpack.config.js
devServer: {
  contentBase: outDir,
  // serve index.html for all 404 (required for push-state)
  historyApiFallback: true,
  hot: hmr || project.platform.hmr,
  port: port || project.platform.port,
  host: host,
+  watchOptions: {
+    aggregateTimeout: 300,
+    poll: 1000
+  }
},
devtool: production ? 'nosources-source-map' : 'cheap-module-eval-source-map',
module: {
  rules: [
(略)

+    {
+      test: /\.(graphql|gql)$/,
+      exclude: /node_modules/,
+      loader: 'graphql-tag/loader'
+    }
  ]
}
```

設定を変更したので、webコンテナを再起動します。

```bash
docker-compose restart web
```

## Todoの一覧を表示する

プロジェクト作成時から存在している、`src/app.js`と`src/app.html`を以下のように書き換えます。

```js:src/app.js
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import TODOS from './graphql/Todos.gql';

const cache = new InMemoryCache();
const link = new HttpLink({
  uri: 'http://localhost:8080/v1/graphql'
});

const client = new ApolloClient({
  cache,
  link
});

export class App {
  heading = 'Todos';
  todos = [];

  constructor() {
    client.query({
      query: TODOS
    }).then(data => this.todos = data.data.todos);
  }
}
```

```src/app.html
<template>
  <h1>${heading}</h1>

  <ul>
    <li repeat.for="todo of todos">
      <input type="checkbox" checked.bind="todo.done">
      <span>
        ${todo.description}
      </span>
    </li>
  </ul>
</template>
```

`import TODOS from './graphql/Todos.gql';`でインポートしているファイルは、GraphQLのQueryを定義しているファイルです。
todosテーブルに登録されているTodoを全て取得するクエリです。

```graphql:src/graphql/Todos.gql
query Todos{
  todos {
    uuid
    description
    done
  }
}
```

`http://localhost:8000`にアクセスすると、先ほど登録したデータが表示されているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161900/bddd8f37-054c-f4a7-57ec-1b67743945a6.png)

`src/app.js`の20～22行目で、クエリを実行してデータを取得しています。
todosには配列でデータが格納されていて、`src/app.html`の5行目の`repeat.for="todo of todos"`で1つずつ取り出し`li`を生成しています。

## 新しいTodoを追加する

新しいTodoを追加することができるように、テキストボックスとボタンを追加します。

```diff:src/app.html

<template>
  <h1>${heading}</h1>

+  <form submit.trigger="addTodo()">
+    <input type="text" value.bind="todoDescription">
+    <button type="submit" disabled.bind="!todoDescription">Add Todo</button>
+  </form>

  <ul>
    <li repeat.for="todo of todos">
      <input type="checkbox" checked.bind="todo.done">
      <span>
        ${todo.description}
      </span>
    </li>
  </ul>

</template>
```

```graphql:src/graphql/InsertTodo.gql
mutation ($description: String) {
  insert_todos(objects: {description: $description, done: false}) {
    returning {
      uuid
      description
      done
    }
  }
}
```

```diff:src/app.js
+ import INSERT_TODO from './graphql/InsertTodo.gql';
(略)

export class App {
  heading = 'Todos';
  todos = [];
+  todoDescription = ''
(略)

+  addTodo() {
+    if (this.todoDescription) {
+      client.mutate({
+        mutation: INSERT_TODO,
+        variables: { description: this.todoDescription },
+        update: (store, { data }) => {
+          let { todos } = store.readQuery({ query: TODOS });
+          todos.push(data.insert_todos.returning[0]);
+          store.writeQuery({
+            query: TODOS,
+            data: { 'Todos': todos }
+          });
+        }
+      });
+      this.todoDescription = '';
+    }
+  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161900/56be04dc-7df1-5880-080b-c40b51fcc6f6.png)

`input type="text" value.bind="todoDescription"`でテキストボックスのvalueと、`src/app.js`の`todoDescription`が双方向にバインドされます。
そのため、テキストボックスに入力した内容が、`todoDescription`に反映されます。

`bind`以外にも、`to-view`や`from-view`などありますが、`bind`を使うとバインディングモードを自動的に判断してくれます。

`src/app.html`で`submit.trigger="addTodo()"`と書いているため、ボタンをクリック(submit)すると`addTodo`メソッドが実行されます。
`addTodo`メソッドの中では、`todoDescription`の内容を登録するMutationが実行されます。

![insertTodo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161900/9401b52d-ed71-a6cc-4890-912030516d0a.gif)

## Todoをdoneにする

完了したTodoにチェックすると、`todos`テーブルの`done`カラムを`true`で更新するようにします。
また、チェックすると、取り消し線が表示されるようにします。

```diff:src/app.html
  <ul>
    <li repeat.for="todo of todos">
-      <input type="checkbox" checked.bind="todo.done">
-      <span>
+      <input type="checkbox" checked.bind="todo.done" change.delegate="updateTodo(todo)">
+      <span css="text-decoration: ${todo.done ? 'line-through' : 'none'}">
        ${todo.description}
      </span>
    </li>
  </ul>
```

```graphql:src/graphql/UpdateTodo.gql
mutation ($uuid: uuid, $done: Boolean) {
  update_todos(where: {uuid: {_eq: $uuid}}, _set: {done: $done}) {
    returning {
      uuid
      done
      description
    }
  }
}
```

```diff:src/app.js
+import UPDATE_TODO from './graphql/UpdateTodo.gql';

export class App {
(略)

+  updateTodo(todo) {
+    client.mutate({
+      mutation: UPDATE_TODO,
+      variables: { uuid: todo.uuid, done: todo.done }
+    });
+  }
}
```

`todo.done`が`true`の場合（チェックボックスにチェックが付いている）、`text-decoration: line-through`が適用され、取り消し線が表示されます。
また、チェックボックスの状態が変更した際に`updateTodo`メソッドが実行されるようになっています。
`updateTodo`で更新用のmutationが実行され、`todos`テーブルの`done`カラムが更新されます。
（チェックを外すと、falseで更新されます）

これで完了したタスクをdoneにすることが出来るようになりました。

## Todoを削除する

```diff:src/app.html

  <input type="checkbox" checked.bind="todo.done" change.delegate="updateTodo(todo)">
  <span css="text-decoration: ${todo.done ? 'line-through' : 'none'}">
    ${todo.description}
  </span>
+  <button click.trigger="removeTodo(todo)">Remove</button>
```

```graphql:src/graphql/removeTodo.gql
mutation ($uuid: uuid) {
  delete_todos(where: {uuid: {_eq: $uuid}}) {
    returning {
      description
      done
      uuid
    }
  }
}
```
```diff:src/app.js
+ import REMOVE_TODO from './graphql/RemoveTodo.gql';

export class App {
(略)

+  removeTodo(todo) {
+    client.mutate({
+     mutation: REMOVE_TODO,
+      variables: { uuid: todo.uuid },
+      update: (store, { data }) => {
+        let { todos } = store.readQuery({ query: TODOS });
+        const index = todos.findIndex(t => t.uuid === data.delete_todos.returning[0].uuid);
+        if (index > -1) {
+          todos.splice(index, 1);
+        }
+        store.writeQuery({
+          query: TODOS,
+          data: { 'Todos': todos }
+        });
+      }
+    });
+  }
}
```

削除ボタンを追加し、`click.trigger`でクリックされた際に、`removeTodo`メソッドが実行されるようにしています。
`removeTodo`では、削除用の`mutation`を実行しています。

これで、間違って登録しても削除出来ますね！

# まとめ

半分ぐらい、Aureliaに関係のない内容になってしまった気がします :sweat_smile:
普段はVue.jsを触る機会が多いですが、雰囲気が似ているので、分かりやすかったです。
折角なので、もう少し色々触ってみようかと思いました。

# 参考

https://github.com/hasura/graphql-engine
https://aurelia.io/