Riot.js Redux Rails 7 / 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
参考
checkbox
チェックボックスを作成。
// ./client/src/tags/task-list.tag <task-list> <ul> <li each={task in this.opts.tasks}> // 追加 <label class={completed: task.isComplete}> <input type="checkbox" id={task.id} checked={task.isComplete} onchange={handleCheck} > {task.name} </label> </li> </ul> <script> handleCheck(e) { this.opts.handlecheck(e.target.id, e.target.checked) } </script> </task-list>
todo-app.tagにマウントされているtask-listタグに処理を追加。
// ./client/src/tags/todo-app.tag <task-list tasks={this.state.tasks} handlecheck={handleTaskCompletionChange} > </task-list> handleTaskCompletionChange(id, isComplete) { store.dispatch(actions.toggleComplete(id, isComplete)) }
サーバーにチェックの状態を送るメソッドなどをactions.jsに追加。
// ./client/src/actions.js module.exports = { loadTasks, addTask, textExists, // 追加 toggleComplete, }; function toggleComplete(id, isComplete) { return (dispatch) => { $.ajax({ url: `/api/tasks/${id}`, type: 'PATCH', dataType: 'json', data: { isComplete }, success: (res) => { dispatch(completeChanged(res.id, res.isComplete)); }, error: (xhr, status, err) => { console.log(`/api/tasks/${id}`, status, err.toString()); }, }); }; } function completeChanged(id, isComplete) { return { type: 'TASK_COMPLETION_CHANGED', data: { id, isComplete } }; }
アクションを送られたときの処理をindex.jsに追加。
// ./client/src/index.js case 'TASK_COMPLETION_CHANGED': { const taskIndex = state.tasks.findIndex((task) => { return task.id == action.data.id; }); const newTasks = [ ...state.tasks.slice(0, taskIndex), Object.assign({}, state.tasks[taskIndex], { isComplete: action.data.isComplete }), ...state.tasks.slice(taskIndex + 1), ]; return Object.assign({}, state, { tasks: newTasks }); }
チェックした時のスタイルを追加。
/* ./app/assets/stylesheets/application.css */ .completed { text-decoration: line-through; color: #ccc; } label { cursor: pointer; }
ここまででviewは完成。
Rails
Migration
サーバー側でisCompleteを扱えるようにする。 ターミナルでctrl-cでforemanを終了させてから
$ rails g migration addIsCompleteToTasks isComplete:boolean
できたマイグレーションファイルに , default: false を追加し
# ./db/migrate/日付_add_is_complete_to_tasks.rb class AddIsCompleteToTasks < ActiveRecord::Migration[5.0] def change add_column :tasks, :isComplete, :boolean, default: false end end
マイグレーションを反映させる。
$ rails db:migrate
これでデータベースのテーブルにisCompleteカラムが追加された。
Controller
次にcontrollerでupdateについての処理を書く。
# ./app/controllers/api/tasks_controller.rb def update @task = Task.find(params[:id]) @task.assign_attributes(task_params) if @task.save render :show, status: :ok else head :unprocessable_entity end end private # isCompleteの追加 def task_params params.permit(:name, :isComplete) end
View(JSON)
JSONがisCompleteも返すようにindex.json.jbuilder、show.json.jbuilderにisCompleteを追加。
# ./app/views/api/tasks/index.json.jbuilder json.tasks(@tasks) { |t| json.extract! t, :id, :name, :isComplete }
# ./app/views/api/tasks/show.json.jbuilder json.extract! @task, :id, :name, :isComplete
再びターミナルで
$ foreman start
でサーバーを開始。
ブラウザでlocalhost:5000を更新してチェックしてもエラーにならないことを確認。
また更新後チェックの状態が維持されることも確認。
サーバーエラー時の処理
チェックの状態が変更された時にサーバー側でエラーが起きると、サーバー側とブラウザ側で矛盾が生じる(checkされているのにisCompleteがfalseまたはその逆)。それを防ぐために、エラー時にチェックを反転させる処理をactions.jsに書く。
// ./client/src/actions.js function toggleComplete(id, isComplete) { return (dispatch) => { $.ajax({ url: `/api/tasks/${id}`, type: 'PATCH', dataType: 'json', data: { isComplete }, success: (res) => { dispatch(completeChanged(res.id, res.isComplete)); }, error: (xhr, status, err) => { // 追加 dispatch(completeChanged(id, !isComplete)); console.log(`/api/tasks/${id}`, status, err.toString()); }, }); }; }
またこの時エラーが起こったことがわかりやすいように、エラーメッセージが表示されるようにする。
まずerror-messageタグを作成。
// ./client/src/tags/error-message.tag <error-message> <div show={this.opts.iserror}> {this.opts.message} </div> <style scoped> :scope div { margin-top: 5px; padding-left: 10px; color: white; background-color: red; } </style> </error-message>
todo-app.tagでerror-messageタグをマウントさせる。
// ./client/src/tags/todo-app.tag </task-form> <error-message message={"Text Message"} iserror={true} > </error-message> <loading-indicator ...>
タグを使うためにindex.jsで読み込む。
// ./client/src/index.js import './tags/error-message.tag';
ブラウザでlocalhost:5000を更新して Text Message というメッセージが表示されていることを確認。
エラーメッセージが表示できるようになったので、エラーが起こった時にだけ表示されるようにする。
showErrorメソッドを作成し、サーバーのエラー時の処理に追加。
// ./client/src/actions.js error: (xhr, status, err) => { // 追加 dispatch(showError('API Error')); dispatch(completeChanged(id, !isComplete)); console.log(`/api/tasks/${id}`, status, err.toString()); }, // 追加 function showError(message) { return { type: 'SHOW_ERROR', data: message }; }
dispatchメソッドでアクションが送られたときの処理を追加。
// ./client/src/index.js case 'SHOW_ERROR': return Object.assign({}, state, { isError: true, errorMessage: action.data });
先程のように固定ではなく、stateの変化に対応できるよう書き換え。
// ./client/src/tags/todo-app.tag <error-message message={this.state.errorMessage} iserror={this.state.isError} > </error-message>
ここでわざとサーバーのurlを間違えたものにしておき、エラーが起こるようにしておく。
ブラウザでlocalhost:5000を更新する。チェックの状態を変更するとエラーが起こりエラーメッセージが表示されること、チェックが反転されることを確認。
次にエラーメッセージが2秒後消えるようにする。
// ./client/src/actions.js error: (xhr, status, err) => { // 書き換え dispatch(tempErrorMessage('API Error')); dispatch(completeChanged(id, !isComplete)); console.log(`/api/tasks/${id}`, status, err.toString()); }, // 追加 function hideError() { return { type: 'HIDE_ERROR' }; } function tempErrorMessage(message) { return (dispatch) => { dispatch(showError(message)); setTimeout(() => { dispatch(hideError()); }, 2000); }; }
新たなアクションの処理を追加。
// ./client/src/index.js case 'HIDE_ERROR': return Object.assign({}, state, { isError: false, errorMessage: '' });
ブラウザでlocalhost:5000を更新する。チェックの状態を変更するとエラーが起こりエラーメッセージが表示され2秒後に消えること、チェックが反転されることを確認。
一応loadTasks()やaddTask()などサーバーと通信している他のメソッドにも
error: (xhr, status, err) => { dispatch(toggleLoading(false)); // 追加 dispatch(tempErrorMessage('API Error'));
このように追加して、エラーメッセージが表示されるようにしておく。
次回はTodoを削除できるようにする。
Riot.js Redux Rails 6 / 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
参考
Rails
新たなTodoを追加する際のサーバー側の処理を作成する。
Controller
tasks_controller.rbに
# ./app/controllers/api/tasks_controller.rb def create @task = Task.new(task_params) if @task.save render :show, status: :created else head :unprocessable_entity end end private def task_params params.permit(:name) end
を追加。
View(JSON)
保存に成功したときにJSON返すshow.json.jbuilderを作成。
# ./app/views/api/tasks/show.json.jbuilder json.extract! @task, :id, :name
これでサーバー側は完成。
Todoを追加するためのtask-formタグ
UIをRiot公式のTodoのように作成する。 + フォームに入力があった時にボタンが有効になるようにする。 + 何個目のTodoを登録しようとしているかわかるようにする。
// ./client/src/tags/task-form.tag <task-form> <form onsubmit={handleSubmit}> <input type="text" name="newTask" onkeyup={handleKeyup} placeholder="new task" > <button type="submit" disabled={!this.opts.istext} > Add Task # {this.opts.objects.length + 1} </button> </form> <script> handleSubmit() { if (!this.newTask.value) { return } this.opts.addtask(this.newTask.value) this.newTask.value = '' } handleKeyup() { this.opts.handlekeyup(this.newTask.value) } </script> </task-form>
サーバーに新しいタスクを送るメソッドなどをactions.jsに追加。
// ./client/src/actions.js module.exports = { loadTasks, // 追加 addTask, textExists, }; // 追加 function addTask(newTask) { return (dispatch) => { dispatch(toggleLoading(true)); $.ajax({ url: '/api/tasks.json', type: 'POST', dataType: 'json', data: { name: newTask }, success: (res) => { dispatch(newTaskAdded(res.id, res.name)); dispatch(toggleLoading(false)); }, error: (xhr, status, err) => { dispatch(toggleLoading(false)); console.log('/api/tasks.json', status, err.toString()); }, }); }; } function newTaskAdded(id, name) { return { type: 'TASK_ADDED', data: { id, name } }; } function textExists(value) { return { type: 'TEXT_EXISTS', data: value }; }
タグをマウントするための処理と、アクションを送られたときの処理をindex.jsに追加。
// ./client/src/index.js import './tags/task-form.tag'; // reducerに追加 case 'TASK_ADDED': return Object.assign({}, state, { tasks: state.tasks.concat(action.data) }); case 'TEXT_EXISTS': return Object.assign({}, state, { isText: action.data });
todo-appタグにtask-formタグ、h3タグ、メソッドを追加
<!-- ./client/src/tags/todo-app.tag --> <todo-app> <h3>Todo List</h3> <task-form addtask={this.handleNewTask} handlekeyup={handleInputForm} objects={this.state.tasks} istext={this.state.isText}> </task-form> <loading- ...> // scriptに追加 handleNewTask(task) { store.dispatch(actions.addTask(task)) } handleInputForm(value) { store.dispatch(actions.textExists(value)) }
CSSの追加
cssを追加しUIをRiot公式のTodoに近づける。
/* ./app/assets/stylesheets/application.css */ body { font-family: 'myriad pro', sans-serif; font-size: 20px; border: 0; } todo-app { display: block; max-width: 600px; margin: 5% auto; } form input { font-size: 85%; padding: .4em; border: 1px solid #ccc; border-radius: 2px; } button { background-color: #1FADC5; border: 1px solid rgba(0,0,0,.2); font-size: 75%; color: #fff; padding: .4em 1.2em; border-radius: 2em; cursor: pointer; margin: 0 .23em; outline: none; } button[disabled] { background-color: #ddd; color: #aaa; } ul { padding: 0; } li { list-style-type: none; padding: .2em 0; }
ブラウザでlocalhost:5000を更新して確認。
フォームにタスクの名前を入力しボタンを押して登録できること、更新後も登録したTodoが維持されていることを確認。
次回は完了したTodoをチェック出来るようにする。
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が消える。
サーバーからデータを読み込んで表示することが出来た。
次回はフォームを作成し、新しくタスクを追加できるようにする。