Riot.js Observable Rails 2 / 5 「Todoリストを表示する」
はじめに
Riot.js Redux Railsシリーズで作ったTodoアプリのRedux部分をObservableを使って書いた。 (Observerパターンとか全然わかってない。ReduxをObservableで置き換える意味があるかもわからない。なんとなく似ていると思ったのでやってみた。)
Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Redux部分をObservableにしたのと、それに伴う変更以外全く同じ)が、今回の一連の記事だけ読んでも出来るように省略はしない。
環境構築
Todo Appの作成
- Riot.js Observable Rails 2 / 5 「Todoリストを表示する」
- Riot.js Observable Rails 3 / 5 「新たなTodoを追加出来るようにする」
- Riot.js Observable Rails 4 / 5 「完了したTodoをチェック出来るようにする」
- Riot.js Observable Rails 5 / 5 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/Riot_Observable_Rails
参考
Rails
todo-appタグをマウントするトップページを作成
$ rails g controller Top index
routes.rbを書き換える
# .config/routes.rb Rails.application.routes.draw do root 'top#index' end
次にindex.html.erbにタグを書く
# ./app/views/top/index.html.erb <todo-app></todo-app>
index.js
まず以下のようにしてみた。
// ./client/src/index.js import riot from 'riot'; class Store { constructor() { this.state = { tasks: [] }; riot.observable(this); } getState() { return this.state; } setState(value) { this.state = Object.assign({}, this.state, value); } } const store = new Store(); document.addEventListener('DOMContentLoaded', () => { riot.mount('todo-app', { store }); });
stateを持つStoreクラスを作成し、インスタンスをobservableで監視出来るようにし、storeインスタンスを作成。作成されたobservableインスタンス(storeインスタンス)をtodo-appタグに渡す。
タグの作成
todo-appタグ
Todo Appのメインとなるtodo-appタグを作成。
<!-- ./client/src/tags/todo-app.tag --> <todo-app> <task-list tasks={this.state.tasks}></task-list> <script> const actions = require('../actions.js') const store = this.opts.store // Reduxと違う部分 store.on('*', () => { this.state = store.getState() this.update() }) </script> </todo-app>
Reduxのsubscribeではなくel.on(events, callback)を使い '*' で全てのイベントを監視するようにしてある。 todoのリストはtask-listタグで表示させる。
task-listタグ
<!-- ./client/src/tags/task-list.tag --> <task-list> <ul> <li each={task in this.opts.tasks}> {task.name} </li> </ul> </task-list>
RailsでAPIサーバーを作成
Controller
まずターミナルで
$ rails g controller api/tasks --no-assets
作成したtasks_controller.rbを
# ./app/controllers/api/tasks_controller.rb class Api::TasksController < ApplicationController def index @tasks = Task.all end end
このようにし、次にroutes.rbに
# ./config.routes.rb namespace :api, format: 'json' do resources :tasks end
を追加。
Model
次にまたターミナルで
$ rails g model task name:string
日時_create_tasks.rbに
# ./db/migrate/日時_create_tasks.rb class CreateTasks < ActiveRecord::Migration[5.0] def change create_table :tasks do |t| t.string :name, null: false t.timestamps end end end
のように , null: false (nameが空だと登録できないようにしている)を追加しターミナルで
$ rails db:migrate
する。また、task.rbにvalidationを追加(同じくnameが空にならないように。しかし、apiとして使う場合モデルのバリデーションは必要ないかもしれない。わからない。)。
# ./app/models/task.rb class Task < ApplicationRecord validates :name, presence: true end
次にseeds.rbファイルに
# ./db/seeds.rb Task.create(name: 'Lesson outline') Task.create(name: 'Record video')
を追加し、ターミナルで
$ rails db:seed
を実行しデータを登録しておく。
View(JSON)
次にJSONを返すindex.json.jbuilderを作成。
# ./app/views/api/tasks/index.json.jbuilder json.tasks(@tasks) { |t| json.extract! t, :id, :name }
ターミナルで
$ foreman start
し、localhost:5000/api/tasksにアクセスすると {"tasks":[{"id":1,"name":"Lesson outline"},{"id":2,"name":"Record video"}]}が表示されている。
サーバーからの読み込み
actions.jsにサーバーからデータを読み込むためのメソッドを作成する。
// ./client/src/actions.js module.exports = { loadTasks, }; function loadTasks(store) { // redux-thunkのようなmiddlewareを使わなくても良い $.ajax({ url: '/api/tasks.json', dataType: 'json', success: (res) => { // dispatchではなくtriggerでイベントをトリガー tasksloaded(store, res.tasks) }, error: (xhr, status, err) => { console.log('/api/tasks.json', status, err.toString()); }, }); } function tasksloaded(store, tasks) { store.trigger('TASKS_LOADED', { data: tasks }); }
index.jsにマウントするタグの読み込みを追加。
イベントがトリガーされるのを監視して処理する部分を追加。
// ./client/src/index.js import './tags/todo-app.tag'; import './tags/task-list.tag'; // reducerが担っていた部分 store.on('TASKS_LOADED', (action) => { store.setState({ tasks: action.data }); });
todo-app.tagにtodo-appタグがマウントされた時にする処理を追加する。
// ./client/src/tags/todo-app.tag this.on('mount', () => { actions.loadTasks(store) })
ここで一度、ブラウザでlocalhost:5000を更新して確認。先程登録したデータが表示される。
ロード時のアニメーションをつける
参考動画に沿って擬似的にロード時間を設け、ロード時のアニメーションを表示出来るようにする。アニメーションはデータ読み込み後消えるようにする。
まず2秒間の擬似的なロード時間を設けるために、actions.jsを書き換える。
// ./client/src/actions.js function loadTasks(store) { $.ajax({ url: '/api/tasks.json', dataType: 'json', success: (res) => { // 書き換え setTimeout(() => { tasksloaded(store, res.tasks); }, 2000) }, error: (xhr, status, err) => { console.log('/api/tasks.json', status, err.toString()); }, }); }; }
参考動画のソースのloading.gifをapp/assets/imagesにおく。
次にloading-indicatorタグを作成。
<!-- ./client/src/tags/loading-indicator.tag --> <loading-indicator> <img src="/assets/loading.gif" show={this.opts.loading}> </loading-indicator>
loading-indicatorタグをtodo-appタグにマウント。
<!-- ./client/src/tags/todo-app.tag --> <loading-indicator loading={this.state.isLoading}></loading-indicator>
loading-indicatorをマウントできるようにindex.jsで読み込む。
// ./client/src/index.js import './tags/loading-indicator.tag';
状態の変化のためのメソッド(イベントのトリガー)を作成。
// ./client/src/actions.js function toggleLoading(store, isLoading) { store.trigger('TOGGLE_LOADING', { data: isLoading }); }
イベントがトリガーされた時の処理をindex.jsに追加。
// ./client/src/index.js store.on('TOGGLE_LOADING', (action) => { store.setState({ isLoading: action.data }); });
ここまででloading-indicatorの用意は出来たので、loadTasks()にイベントをトリガーする処理を追加。
// ./client/src/actions.js function loadTasks(store) { // 追加 toggleLoading(store, true); $.ajax({ url: '/api/tasks.json', dataType: 'json', success: (res) => { setTimeout(() => { tasksloaded(store, res.tasks); // 追加 toggleLoading(store, false); }, 2000) }, error: (xhr, status, err) => { // 追加 toggleLoading(store, false); console.log('/api/tasks.json', status, err.toString()); }, }); }; }
ブラウザでlocalhost:5000を更新して確認。
まずloading-indicatorが表示され、2秒後にデータの表示とともにloading-indicatorが消える。
Reduxのdispatch(action)をel.trigger(event)、reducerの処理をel.on(event)にすることで、同じようなものができた。