React Redux Rails 3 / 5 「新たなTodoを追加出来るようにする」
はじめに
Riot.js Redux Railsシリーズで作ったTodoアプリのRiot.js部分をReactを使って書いた。
Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Riot.js部分をReactにしただけ)が、今回の一連の記事だけ読んでも出来るように省略はしない。
環境構築
- React Redux Rails 1 / 5 「環境構築」
Todo Appの作成
- React Redux Rails 2 / 5 「Todoリストを表示する」
- React Redux Rails 3 / 5 「新たなTodoを追加出来るようにする」
- React Redux Rails 4 / 5 「完了したTodoをチェック出来るようにする」
- React Redux Rails 5 / 5 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/React_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/components/task-form.jsx import React from 'react'; export default class TaskForm extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); this.handleKeyup = this.handleKeyup.bind(this); } handleSubmit(e) { e.preventDefault(); this.props.addtask(this.newTask.value); this.newTask.value = ''; } handleKeyup(e) { this.props.handlekeyup(e.target.value); } render() { return ( <form onSubmit={this.handleSubmit}> <input type="text" ref={(ref) => this.newTask = ref} onKeyUp={this.handleKeyup} placeholder="new task" /> <button type="submit" disabled={!this.props.istext} > Add Task # {this.props.objects.length + 1} </button> </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.jsxのreducerに追加。
// ./client/src/index.jsx 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/components/todo-app.jsx import TaskForm from './task-form.jsx'; class TodoApp extends React.Component { constructor(props) { super(props); // 追加 this.handleNewTask = this.handleNewTask.bind(this); this.handleInputForm = this.handleInputForm.bind(this); } handleNewTask(task) { this.props.dispatch(actions.addTask(task)); } handleInputForm(value) { this.props.dispatch(actions.textExists(value)); } render() { return ( <div> <h3>Todo List</h3> <TaskForm addtask={this.handleNewTask} handlekeyup={this.handleInputForm} objects={this.props.tasks} istext={this.props.isText} /> {this.props.isLoading ... function mapStateToProps(state) { const { tasks, isLoading, isText } = state; return { tasks, isLoading, isText, }; }
cssを追加しUIをRiot公式のTodoに近づける。
/* ./app/assets/stylesheets/application.css */ body { font-family: 'myriad pro', sans-serif; font-size: 20px; border: 0; } div#content { 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をチェック出来るようにする。
React Redux Rails 2 / 5 「Todoリストを表示する」
はじめに
Riot.js Redux Railsシリーズで作ったTodoアプリのRiot.js部分をReactを使って書いた。
Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Riot.js部分をReactにしただけ)が、今回の一連の記事だけ読んでも出来るように省略はしない(ほぼコピペだし)。
環境構築
- React Redux Rails 1 / 5 「環境構築」
Todo Appの作成
- React Redux Rails 2 / 5 「Todoリストを表示する」
- React Redux Rails 3 / 5 「新たなTodoを追加出来るようにする」
- React Redux Rails 4 / 5 「完了したTodoをチェック出来るようにする」
- React Redux Rails 5 / 5 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/React_Redux_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にreactの導入部を書く
# ./app/views/top/index.html.erb <div id="content"></div>
index.jsx
以下のようになった。
// ./client/src/index.jsx import React from 'react'; import { render } from 'react-dom'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; function reducer(state = { tasks: [] }, action) { return state; } const reduxStore = createStore( reducer, applyMiddleware(thunk) ); document.addEventListener('DOMContentLoaded', () => { render( <Provider store={reduxStore}> <TodoApp /> </Provider>, document.getElementById('content') ); });
reducerを作成し、そのreducerや非同期処理に必要なredux-thunkを持ったStoreを作成。作成されたStoreをreact-reduxのProviderを使ってtodo-appコンポーネントに渡す。
コンポーネントの作成
todo-appコンポーネント
Todo Appのメインとなるtodo-appコンポーネントを作成。
// ./client/src/components/todo-app.jsx import React from 'react'; import { connect } from 'react-redux'; import actions from '../actions.js'; import TaskList from './task-list.jsx'; class TodoApp extends React.Component { constructor(props) { super(props); } render() { return ( <div> <TaskList tasks={this.props.tasks} /> </div> ); } } function mapStateToProps(state) { const { tasks } = state; return { tasks, }; } export default connect(mapStateToProps)(TodoApp);
react-reduxのconnectを使うとReduxのsubscribeが必要ないらしい(react-reduxがよくわかっていない)。 todoのリストはtask-listコンポーネントで表示させる。
task-listコンポーネント
// ./client/src/components/task-list.jsx import React from 'react'; export default class TaskList extends React.Component { constructor(props) { super(props); } render() { return ( <ul> {this.props.tasks.map(task => ( <li key={task.id}> {task.name} </li> ))} </ul> ); } }
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() { 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 }; }
index.jsxのreducerにアクションが渡されたときの処理を追加。
// ./client/src/index.jsx export default function reducer(state = { tasks: [] }, action) { switch (action.type) { case 'TASKS_LOADED': return Object.assign({}, state, { tasks: action.data }); default: return state; } }
todo-app.jsxにtodo-appコンポーネントがマウントされた時にする処理を追加する。
// ./client/src/components/todo-app.jsx componentDidMount() { this.props.dispatch(actions.loadTasks()) }
ここで一度、ブラウザでlocalhost:5000を更新して確認。先程登録したデータが表示される。
ロード時のアニメーションをつける
参考動画に沿って擬似的にロード時間を設け、ロード時のアニメーションを表示出来るようにする。アニメーションはデータ読み込み後消えるようにする。
まず2秒間の擬似的なロード時間を設けるために、actions.jsを書き換える。
// ./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)); }, 2000) }, error: (xhr, status, err) => { console.log('/api/tasks.json', status, err.toString()); }, }); }; }
参考動画のソースのloading.gifをapp/assets/imagesにおく。
次にloading-indicatorコンポーネントを作成。
// ./client/src/components/loading-indicator.jsx import React from 'react'; export default class LoadingIndicator extends React.Component { render() { return ( <div> <img src="/assets/loading.gif" alt="loading-indicator" /> </div> ); } }
loading-indicatorコンポーネントをtodo-appコンポーネントで読み込んでマウント。
// ./client/src/components/todo-app.jsx import LoadingIndicator from './loading-indicator.jsx'; {this.props.isLoading ? <LoadingIndicator /> : null} <TaskList tasks={this.props.tasks} /> function mapStateToProps(state) { const { tasks, isLoading } = state; return { tasks, isLoading, }; }
状態の変化のためのメソッド(アクション)を作成。
// ./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 }); default:
ここまでで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が消える。
React Redux Rails 1 / 5 「環境構築」
はじめに
Riot.js Redux Railsシリーズで作ったTodoアプリのRiot.js部分をReactを使って書いた。
Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Riot.js部分をReactにしただけ)が、今回の一連の記事だけ読んでも出来るように省略はしない(ほぼコピペだし)。
環境構築
- React Redux Rails 1 / 5 「環境構築」
Todo Appの作成
- React Redux Rails 2 / 5 「Todoリストを表示する」
- React Redux Rails 3 / 5 「新たなTodoを追加出来るようにする」
- React Redux Rails 4 / 5 「完了したTodoをチェック出来るようにする」
- React Redux Rails 5 / 5 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/React_Redux_Rails
参考
環境構築
ターミナル
$ rails new react_redux_rails $ cd react_redux_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 babel-core babel-loader babel-preset-react babel-preset-es2015
同じく
$ npm install --save react react-dom redux react-redux redux-thunk
webpackの設定
webpack.config.jsを作成し
// ./client/webpack.config.js module.exports = { entry: { app: './src/index.jsx', }, output: { path: '../app/assets/javascripts/webpack', filename: 'bundle.js', }, module: { loaders: [ { test: /\.(js|jsx)$/, loader: "babel", exclude: /node_modules/, query: { presets: ["es2015", "react"], } }, ] }, };
そして
$ cd ..
で、アプリケーションのルートに戻っておく
Gemfile
Gemfileに
# Gemfile gem 'foreman'
を追加し
$ bundle install
Procfile
アプリケーションのルートディレクトリにProcfileを作成
rails: rails s webpack: npm --prefix client run webpack-watch
環境構築終わり。
次回からTodoアプリの作成。