LoginSignup
2
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-10

はじめに

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で動かすようにします。

Dockerfile
FROM node:10.17.0-alpine

WORKDIR /app

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

RUN apk update && \
  yarn global add aurelia-cli && \
  yarn
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:
aurelia_project/aurelia.json
"platform": {
  "hmr": false,
  "open": false,
-  "port": 8000,
+  "port": 8000,
  "host": "localhost",
  "output": "dist"
},

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

docker-compose up -d --build

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

image.png

todosテーブルの作成

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

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

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

image.png

image.png

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

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

image.png

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

image.png

Todoアプリ作成

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

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

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

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コンテナを再起動します。

docker-compose restart web

Todoの一覧を表示する

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

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を全て取得するクエリです。

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

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

image.png

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

新しいTodoを追加する

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

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>
src/graphql/InsertTodo.gql
mutation ($description: String) {
  insert_todos(objects: {description: $description, done: false}) {
    returning {
      uuid
      description
      done
    }
  }
}
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

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

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

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

insertTodo.gif

Todoをdoneにする

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

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>
src/graphql/UpdateTodo.gql
mutation ($uuid: uuid, $done: Boolean) {
  update_todos(where: {uuid: {_eq: $uuid}}, _set: {done: $done}) {
    returning {
      uuid
      done
      description
    }
  }
}
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.donetrueの場合(チェックボックスにチェックが付いている)、text-decoration: line-throughが適用され、取り消し線が表示されます。
また、チェックボックスの状態が変更した際にupdateTodoメソッドが実行されるようになっています。
updateTodoで更新用のmutationが実行され、todosテーブルのdoneカラムが更新されます。
(チェックを外すと、falseで更新されます)

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

Todoを削除する

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>
src/graphql/removeTodo.gql
mutation ($uuid: uuid) {
  delete_todos(where: {uuid: {_eq: $uuid}}) {
    returning {
      description
      done
      uuid
    }
  }
}
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/

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1