はじめに
Aureliaの公式サイトには、Todoアプリを作成するチュートリアル(https://aurelia.io/docs/tutorials/creating-a-todo-app/)があります。
今回は、GraphQL + ApolloでTodoをDBで管理できるようにカスタマイズしたいと思います。
基本的な構造は、公式のチュートリアルと同じになります。
完成系のソースコードはGithubに置いています。
環境
バージョン | |
---|---|
aurelia-cli | v1.2.2 |
hasura | v1.0.0-beta.10 |
環境構築
Aureliaプロジェクト
aurelia-cli
を使って、Todoアプリ用のプロジェクトを作成します。
aurelia-cli
がインストールされていない場合は、インストールします。
npm install -g aurelia-cli
au new
コマンドでプロジェクトを作成します。
今回は、プロジェクト名をaurelia-apollo-todo
、それ以外はデフォルトで作成しています。
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
プロジェクトの作成が完了したら、起動してみましょう。
cd aurelia-apollo-todo
au run
http://localhost:8080
にアクセスすると、『Hello World!』が表示されます。
GraphQLサーバー
GraphQLサーバーとして、Hasuraを使用します。
Hasuraはローカル環境で動かすために、docker-compose.yml
が提供されています。
wget https://raw.githubusercontent.com/hasura/graphql-engine/master/install-manifests/docker-compose/docker-compose.yaml
ついでに、先ほど作成したAureliaのプロジェクトも、Docker Composeで動かすようにします。
FROM node:10.17.0-alpine
WORKDIR /app
COPY ./package.json .
COPY ./yarn.lock .
RUN apk update && \
yarn global add aurelia-cli && \
yarn
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:
"platform": {
"hmr": false,
"open": false,
- "port": 8000,
+ "port": 8000,
"host": "localhost",
"output": "dist"
},
AureliaのプロジェクトとHasuraでPortが同じになってしまうので、Aureliaのほうを8000
に変更しています。
Dockerfile
とdocker-compose.yml
が作成できたら、Docker Composeを起動します。
docker-compose up -d --build
http://localhost:8000
にアクセスすると、先ほどと同じく『Hello World!』が表示されます。
http://localhost:8080
にアクセスすると、Hasuraの画面が表示されます。
todosテーブルの作成
Todoを登録するために使用するテーブルを作成します。
- uuid ・・・一意になるID
- description ・・・Todoの内容
- done・・・完了したかどうか
内容を登録するためのdescription
と、完了したかどうかを登録するdone
というカラムを用意します。
DATA
- Create Table
と選択して、実際にtodos
テーブルを作成します。
必要な項目を入力して、Add Table
をクリックすると、todos
テーブルが作成されます。
Todoアプリに表示するためのサンプルデータを投入しておきましょう。
Insert Rowタブを選択すると、データの登録が出来ます。
内容は何でも構いませんが、description
には「Sample Todo」と入力し、done
はFalseを選択しておきます。
入力出来たら、Save
をクリックするとデータが登録されます。
Todoアプリ作成
本題のTodoアプリを作成していきます。
まずは必要なライブラリをインストールします。
docker-compose exec web yarn add apollo-client apollo-cache-inmemory apollo-link-http graphql-tag graphql
webpack.config.jsにloaderの設定を追加しておきます。
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コンテナを再起動します。
docker-compose restart web
Todoの一覧を表示する
プロジェクト作成時から存在している、src/app.js
とsrc/app.html
を以下のように書き換えます。
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);
}
}
<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を全て取得するクエリです。
query Todos{
todos {
uuid
description
done
}
}
http://localhost:8000
にアクセスすると、先ほど登録したデータが表示されているはずです。
src/app.js
の20~22行目で、クエリを実行してデータを取得しています。
todosには配列でデータが格納されていて、src/app.html
の5行目のrepeat.for="todo of todos"
で1つずつ取り出しli
を生成しています。
新しいTodoを追加する
新しいTodoを追加することができるように、テキストボックスとボタンを追加します。
<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>
mutation ($description: String) {
insert_todos(objects: {description: $description, done: false}) {
returning {
uuid
description
done
}
}
}
+ 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 = '';
+ }
+ }
}
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が実行されます。
Todoをdoneにする
完了したTodoにチェックすると、todos
テーブルのdone
カラムをtrue
で更新するようにします。
また、チェックすると、取り消し線が表示されるようにします。
<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>
mutation ($uuid: uuid, $done: Boolean) {
update_todos(where: {uuid: {_eq: $uuid}}, _set: {done: $done}) {
returning {
uuid
done
description
}
}
}
+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を削除する
<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>
mutation ($uuid: uuid) {
delete_todos(where: {uuid: {_eq: $uuid}}) {
returning {
description
done
uuid
}
}
}
+ 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に関係のない内容になってしまった気がします
普段はVue.jsを触る機会が多いですが、雰囲気が似ているので、分かりやすかったです。
折角なので、もう少し色々触ってみようかと思いました。
参考
https://github.com/hasura/graphql-engine
https://aurelia.io/