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)にすることで、同じようなものができた。
Riot.js Observable Rails 1 / 5 「環境構築」
はじめに
Riot.js Redux Railsシリーズで作ったTodoアプリのRedux部分をObservableを使って書いた。 (Observerパターンとか全然わかってない。ReduxをObservableで置き換える意味があるかもわからない。なんとなく似ていると思ったのでやってみた。)
Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Redux部分をObservableにしたのと、それに伴う変更以外全く同じ)が、今回の一連の記事だけ読んでも出来るように省略はしない。
環境構築
- Riot.js Observable Rails 1 / 5 「環境構築」
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
参考
環境構築
Mac
Rails 5.0.0.1
ruby 2.3.1
riot 2.6.7
ターミナル
$ rails new riot_observable_rails $ cd riot_observable_rails $ mkdir client $ cd client
clientディレクトリ
Package.json
$ npm init -y
これによりpackage.jsonが作成されるので
{ "private": "true", "scripts": { "webpack-watch": "webpack -w" } }
のように書き換える。 再びターミナルで
$ npm install --save-dev webpack tag-loader babel-core babel-loader babel-preset-es2015
同じく
$ npm install --save riot
webpackの設定
webpack.config.jsを作成し
// ./client/webpack.config.js module.exports = { entry: './src/index.js', output: { path: '../app/assets/javascripts/webpack', filename: 'bundle.js', }, module: { loaders: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, query: { presets: ['es2015'], }, }, { test: /\.tag$/, loader: 'tag', exclude: /node_modules/, }, ], }, };
そして
$ cd ..
で、アプリケーションのルートに戻っておく
Gemfile
Gemfileに
# Gemfile gem 'foreman'
を追加し
$ bundle install
Procfile
アプリケーションのルートディレクトリにProcfileを作成
rails: rails s webpack: npm --prefix client run webpack-watch
環境構築終わり。
次回からTodoアプリの作成。
Riot.js Redux Rails 8 / 8 「チェックしてあるタスクを削除出来るようにする」
はじめに
フロントに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
参考
Riot公式のTodoのようにチェックしてあるタスクを全て削除するボタンを作成する。
Rails
routes
まず一度に複数削除する場合、サーバー側のurlはどうすれば良いのかわからなかった。 試しに
# ./config/routes.rb namespace :api, format: 'json' do resources :tasks do delete :del_tasks end end
としたところ
$ rails routes
でルーティングを確認するとurlが
/api/tasks/:task_id/del_tasks(.:format)の様になった。
複数削除なので:task_idがいらないと思い、
# ./config/routes.rb namespace :api, format: 'json' do resources :tasks do delete :del_tasks, on: :collection end end
としたところ
/api/tasks/del_tasks(.:format)の様になったのでこれを使う。
controller
一度に複数削除する方法がわからないのでidを配列で受け取り一つずつ処理するようにした。
# ./app/controllers/api/tasks_controller.rb def del_tasks ids = params[:ids] ids.each do |id| if Task.find(id).destroy head :no_content else head :unprocessable_entity end end end private def task_params # idsを追加 params.permit(:name, :isComplete, :ids) end
サーバー側はこれで終了。
Del Tasksボタン
todo-app.tagにDel Tasksのbuttonタグをマウントさせ、処理のためのメソッドも追加。
// ./client/src/tags/todo-app.tag </task-form> // 追加 <button class="del" onclick={handleDeleteTasks} disabled={this.state.tasks.filter(complete).length == 0} > Del Tasks X {this.state.tasks.filter(complete).length} </button> <div style="clear: both"></div> // scriptに追加 complete(task) { return task.isComplete == true } handleDeleteTasks() { // 大変だった箇所。チェックしてあるタスクのidをどのように取得するか迷った。 let ids = [] // まずチェックされているタスクだけの配列を作成 const comp = this.state.tasks.filter((task) => this.complete(task)) // チェックされているタスクの数だけfor文で処理 for (let i = 0; i < comp.length; i += 1) { // チェックされているタスクのidをidsに格納 ids[i] = comp[i].id } store.dispatch(actions.deleteTasks(ids)) }
Del Tasksボタンをformの隣に配置するためにtask-formタグにstyleを加える。
// ./client/src/tags/task-form.tag <style scoped> :scope form { float: left; } </style>
先程決めたサーバーのURLに配列idsを送るメソッドなどをactions.jsに作成。
// ./client/src/actions.js module.exports = { loadTasks, addTask, textExists, toggleComplete, // 追加 deleteTasks, }; function deleteTasks(ids) { return (dispatch) => { $.ajax({ url: '/api/tasks/del_tasks', type: 'DELETE', dataType: 'json', data: { ids }, success: () => { dispatch(deletedTasks(ids)); }, error: (xhr, status, err) => { dispatch(toggleLoading(false)); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); }; } function deletedTasks(ids) { return { type: 'DELETED_TASKS', data: ids }; }
一番大変だった箇所。どうやって削除後の新しい配列を作るかかなり迷った。
// ./client/src/index.js case 'DELETED_TASKS': { // stateを直接いじってはいけないらしいのでコピー let newTasks = state.tasks.slice(); // 配列で渡されたidの数だけfor文で繰り返す。 for (let i = 0; i < action.data.length; i += 1) { // 配列で渡されたidから、配列中のインデックスを取得 const taskIndex = newTasks.findIndex((task) => { return task.id == action.data[i]; }); // 取得したインデックスを利用しコピーした配列から直接、破壊的に削除。 newTasks.splice(taskIndex, 1); } return Object.assign({}, state, { tasks: newTasks }); }
cssに以下を追加しDel Tasksボタンの色を変える。
/* ./app/assets/stylesheets/application.css */ .del { background-color: #d21073; border: 1px solid rgba(0,0,0,.2); }
ブラウザでlocalhost:5000を更新する。チェックの数によってDel Tasksボタンの数値が変わること、ボタンを押して削除出来ることを確認。
ここまででほぼ完成。ついでに一つずつ削除する処理も追加しておく。
Rails
Controller
# ./app/controllers/api/tasks_controller.rb def destroy if Task.find(params[:id]).destroy head :no_content else head :unprocessable_entity end end
Delボタンの追加
いつもの流れ。まずタグをtask-list.tagに追加。
// ./client/src/tags/task-list.tag </label> // 追加 <button class="del" id={task.id} show={task.isComplete} onclick={handleClick}> Del </button> // scriptに追加 handleClick(e) { this.opts.handledeletetask(e.target.id) } // styleを追加 <style scoped> :scope .del { float: right; } </style>
todo-app.tagにもろもろ追加。
// ./client/src/tags/todo-app.tag <task-list tasks={this.state.tasks} handlecheck={handleTaskCompletionChange} // 追加 handledeletetask={handleDeleteTask}> </task-list> // scriptに追加 handleDeleteTask(id) { store.dispatch(actions.deleteTask(id)) }
サーバーと通信するメソッドなどをactions.jsに追加。
// ./client/src/actions.js module.exports = { loadTasks, addTask, textExists, toggleComplete, deleteTasks, // 追加 deleteTask, }; function deleteTask(id) { return (dispatch) => { dispatch(toggleLoading(true)); $.ajax({ url: `/api/tasks/${id}`, type: 'DELETE', dataType: 'json', success: () => { dispatch(deletedTask(id)); dispatch(toggleLoading(false)); }, error: (xhr, status, err) => { dispatch(toggleLoading(false)); dispatch(tempErrorMessage('API Error')); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); }; } function deletedTask(id) { return { type: 'DELETED_TASK', data: id }; }
アクションを送られたときの処理をindex.jsに追加。
// ./client/src/index.js case 'DELETED_TASK': { const newTasks = state.tasks.filter((task) => { return task.id != action.data; }); return Object.assign({}, state, { tasks: newTasks }); }
ブラウザでlocalhost:5000を更新する。チェックしてあるタスクの右にDelボタンが表示されること、Delボタンを押して削除出来ることを確認。
Todoアプリ完成。