Riot.js Redux Rails 5 / 8 「Todoリストを表示する」
はじめに
フロントにRiot.jsとReduxを使いサーバーサイドにRailsを使い、初めに環境構築し、次にReduxの使い方の流れをみて、最終的に簡単なTodoAppを作成する。
環境構築
- Riot.js Redux Rails 1 / 8 「環境構築」
簡単な例でReduxの流れをみる
- Riot.js Redux Rails 2 / 8 「Reduxを使ってtitleを表示させる」
- Riot.js Redux Rails 3 / 8 「formを追加しtitleの変更を可能にする」
- Riot.js Redux Rails 4 / 8 「3で作成したformを、title-formタグとして独立させる」
Todo Appの作成
- Riot.js Redux Rails 5 / 8 「Todoリストを表示する」
- Riot.js Redux Rails 6 / 8 「新たなTodoを追加出来るようにする」
- Riot.js Redux Rails 7 / 8 「完了したTodoをチェック出来るようにする」
- Riot.js Redux Rails 8 / 8 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/Riot_Redux_Rails
参考
- RiotJS and Redux - Part 4
- reduxのcomposeとapplyMiddlewareとenhancer
- react-railsを使ってReactのTutorialをやってみる
準備
今まで作ってきたsample-outputタグ、title-formタグは使わないのでindex.html.erbから
# ./app/views/top/index.html.erb <sample-output></sample-output> <title-form></title-form>
これらを削除し空にする。またindex.jsから
// ./client/src/index.js import './tags/sample-output.tag'; import './tags/title-form.tag'; title: 'Default title' 'CHANGE_TITLE': return Object.assign({}, state, { title: action.data });
も削除。するとこうなる
// ./client/src/index.js import riot from 'riot'; import { createStore } from 'redux'; function reducer(state = { }, action) { switch (action.type) { case default: return state; } } const reduxStore = createStore(reducer); document.addEventListener('DOMContentLoaded', () => { riot.mount('*', { store: reduxStore }); });
またactions.jsから
// ./client/src/actions.js changeTitle, function changeTitle(newTitle) { return { type: 'CHANGE_TITLE', data: newTitle }; }
も削除。するとこうなる
// ./client/src/actions.js module.exports = { };
これで準備完了。
RailsでAPIサーバーを作成
ターミナルでctrl-cを入力しforemanを一時終了。
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
indexアクションを追加し、次に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"}]}が表示されている。
redux-thunk
(redux-thunkについては「なんか非同期処理に必要なんだな」ぐらいの理解しかない。)
redux-thunkを使うための準備としてindex.jsを以下のようにする。
// ./client/src/index.js // 追加 import thunk from 'redux-thunk'; // 書き換え import { createStore, applyMiddleware } from 'redux'; const reduxStore = createStore( reducer, applyMiddleware(thunk) );
todo-appタグ
Todo Appのメインとなるtodo-appタグを作成。
<!-- ./client/src/tags/todo-app.tag --> <todo-app> <script> const actions = require('../actions.js') const store = this.opts.store store.subscribe(() => { this.state = store.getState() this.update() }) </script> </todo-app>
todoのリストはtask-listタグで表示させる。
// ./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 store.subscribe(() => { this.state = store.getState() this.update() }) </script> </todo-app>
task-listタグ
// ./client/src/tags/task-list.tag <task-list> <ul> <li each={task in this.opts.tasks}> {task.name} </li> </ul> </task-list>
サーバーからの読み込み
サーバーからデータを読み込むためのメソッドを作成する。
// ./client/src/action.js module.exports = { loadTasks, }; function loadTasks() { return (dispatch) => { $.ajax({ url: '/api/tasks.json', dataType: 'json', success: (res) => { dispatch(tasksloaded(res.tasks)); }, error: (xhr, status, err) => { console.log('/api/tasks.json', status, err.toString()); }, }); }; } function tasksloaded(tasks) { return { type: 'TASKS_LOADED', data: tasks }; }
(npmでjQueryをインストールしていないがRailsに入っているので良きに計らってくれると思い、ajaxをそのまま書いた)
タグをマウントするための処理と、アクションを送られたときの処理をindex.jsに書く。
// ./client/src/index.js // 追加 import './tags/todo-app.tag'; import './tags/task-list.tag'; // 書き換え function reducer(state = { tasks: [] }, action) { switch (action.type) { case 'TASKS_LOADED': return Object.assign({}, state, { tasks: action.data }); default: return state; } } document.addEventListener('DOMContentLoaded', () => { riot.mount('todo-app', { store: reduxStore }); });
index.html.erbにタグを書く。
# ./app/views/top/index.html.erb <todo-app></todo-app>
タグがマウントされた時にloadTasks()が実行されるようにtodo-appタグのscriptに以下を追加。
// ./client/tags/todo-app.tag this.on('mount', ()=> { store.dispatch(actions.loadTasks()) })
ブラウザでlocalhost:5000を更新して確認。先程登録したデータが表示される。
ロード時のアニメーションをつける
参考動画に沿って擬似的にロード時間を設け、ロード時のアニメーションを表示出来るようにする。アニメーションはデータ読み込み後消えるようにする。
まず2秒間の擬似的なロード時間を設けるために、actions.jsを書き換える。
// ./client/src/actions.js function loadTasks() { return (dispatch) => { $.ajax({ url: '/api/tasks.json', dataType: 'json', success: (res) => { // 書き換え setTimeout(() => { dispatch(tasksloaded(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> <task-list
loading-indicatorをマウントできるようにindex.jsで読み込む。
// ./client/src/index.js import './tags/loading-indicator.tag';
状態の変化のためのメソッド(アクション)を作成。
// ./client/src/actions.js function toggleLoading(isLoading) { return { type: 'TOGGLE_LOADING', data: isLoading }; }
アクションを送られた時の処理をreducerに追加。
// ./client/src/index.js case 'TOGGLE_LOADING': return Object.assign({}, state, { isLoading: action.data });
ここまででloading-indicatorの用意は出来たので、loadTasks()にdispatchメソッドでアクションを送る処理を追加。
// ./client/src/actions.js function loadTasks() { return (dispatch) => { // 追加 dispatch(toggleLoading(true)); $.ajax({ url: '/api/tasks.json', dataType: 'json', success: (res) => { setTimeout(() => { dispatch(tasksloaded(res.tasks)); // 追加 dispatch(toggleLoading(false)); }, 2000) }, error: (xhr, status, err) => { // 追加 dispatch(toggleLoading(false)); console.log('/api/tasks.json', status, err.toString()); }, }); }; }
ブラウザでlocalhost:5000を更新して確認。
まずloading-indicatorが表示され、2秒後にデータの表示とともにloading-indicatorが消える。
サーバーからデータを読み込んで表示することが出来た。
次回はフォームを作成し、新しくタスクを追加できるようにする。