jQueryで書いたAjaxを、Fetch APIで書く
はじめに
今まで作ってきたTodoApp(3つのシリーズ)のサーバーとの通信部分を、RailsをAPIサーバーとして完全に切り離した時を想定して(Riot.jsやReactを使ったときはjQueryは使わないだろうから)、jQueryからFetch APIを使ったものにしてみた。
参考
注意
csrfTokenはAPIサーバーとして切り離した時は関係ない。 そのかわり、corsに対応するため
fetch(url, { mode: 'cors', credentials: 'include' })
が必要。credentialsはCookieやBASIC 認証などの認証情報。
loadTasks()
jQuery
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)); dispatch(tempErrorMessage('API Error')); console.log('/api/tasks.json', status, err.toString()); }, }); }; }
Fetch API
まずサーバー側のエラーを直接はキャッチ出来ないらしいので、下のようにチェックしてエラーを投げる。 今後全てのメソッドに使う。
function checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response; } else { var error = new Error(response.statusText); error.response = response; throw error; } }
function loadTasks() { return (dispatch) => { const csrfToken = document.getElementsByName('csrf-token').item(0).content; dispatch(toggleLoading(true)); fetch('/api/tasks.json', { credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, }).then((response) => { // ここでエラーチェック checkStatus(response); return response.json(); }).then((json) => { setTimeout(() => { dispatch(tasksloaded(json.tasks)); dispatch(toggleLoading(false)); }, 2000) }).catch((err) => { dispatch(toggleLoading(false)); dispatch(tempErrorMessage('API Error')); console.error(err); }); }; }
addTask()
jQuery
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)); dispatch(tempErrorMessage('API Error')); console.log('/api/tasks.json', status, err.toString()); }, }); }; }
Fetch API
function addTask(newTask) { return (dispatch) => { const csrfToken = document.getElementsByName('csrf-token').item(0).content; dispatch(toggleLoading(true)); fetch('/api/tasks.json', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, body: JSON.stringify({ name: newTask }), }).then((response) => { // ここでエラーチェック checkStatus(response) return response.json(); }).then((json) => { dispatch(newTaskAdded(json.id, json.name)); dispatch(toggleLoading(false)); }).catch((err) => { dispatch(toggleLoading(false)); dispatch(tempErrorMessage('API Error')); console.error(err); }); }; }
toggleLoading()
jQuery
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(tempErrorMessage('API Error')); dispatch(completeChanged(id, !isComplete)); console.log(`/api/tasks/${id}`, status, err.toString()); }, }); }; }
Fetch API
function toggleComplete(id, isComplete) { return (dispatch) => { const csrfToken = document.getElementsByName('csrf-token').item(0).content; fetch(`/api/tasks/${id}`, { method: 'PATCH', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, body: JSON.stringify({ isComplete }), }).then((response) => { // ここでエラーチェック checkStatus(response) return response.json(); }).then((json) => { console.log(json); dispatch(completeChanged(json.id, json.isComplete)); }).catch((err) => { dispatch(toggleLoading(false)); dispatch(tempErrorMessage('API Error')); console.error(err); }); }; }
deleteTasks()
jQuery
function deleteTasks(ids) { return (dispatch) => { dispatch(toggleLoading(true)); $.ajax({ url: '/api/tasks/del_tasks', type: 'DELETE', dataType: 'json', data: { ids }, success: () => { dispatch(deletedTasks(ids)); dispatch(toggleLoading(false)); }, error: (xhr, status, err) => { dispatch(tempErrorMessage('API Error')); dispatch(toggleLoading(false)); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); }; }
Fetch API
function deleteTasks(ids) { return (dispatch) => { const csrfToken = document.getElementsByName('csrf-token').item(0).content; dispatch(toggleLoading(true)); fetch('/api/tasks/del_tasks', { method: 'DELETE', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, body: JSON.stringify({ ids }), }).then((response) => { // ここでエラーチェック checkStatus(response) }).then(() => { dispatch(deletedTasks(ids)); dispatch(toggleLoading(false)); }).catch((err) => { dispatch(toggleLoading(false)); dispatch(tempErrorMessage('API Error')); console.error(err); }); }; }
deleteTask()
jQuery
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(tempErrorMessage('API Error')); dispatch(toggleLoading(false)); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); }; }
Fetch API
function deleteTask(id) { return (dispatch) => { const csrfToken = document.getElementsByName('csrf-token').item(0).content; dispatch(toggleLoading(true)); fetch(`/api/tasks/${id}`, { method: 'DELETE', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, }).then((response) => { // ここでエラーチェック checkStatus(response) }).then(() => { dispatch(deletedTask(id)); dispatch(toggleLoading(false)); }).catch((err) => { dispatch(toggleLoading(false)); dispatch(tempErrorMessage('API Error')); console.error(err); }); }; }
React Redux Rails 5 / 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
参考
今回はRiot公式のTodoのようにチェックしてあるタスクを全て削除するボタンを作成する。
Rails
routes
まず複数削除のためのルートを追加
# ./config/routes.rb namespace :api, format: 'json' do resources :tasks do delete :del_tasks, on: :collection end end
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.jsxにDel Tasksのbuttonコンポーネントをマウントさせ、処理のためのメソッドも追加。
// ./client/src/components/todo-app.jsx class TodoApp extends React.Component { constructor(props) { super(props); this.handleNewTask = this.handleNewTask.bind(this); this.handleInputForm = this.handleInputForm.bind(this); this.handleTaskCompletionChange = this.handleTaskCompletionChange.bind(this); // 追加 this.handleDeleteTasks = this.handleDeleteTasks.bind(this); } complete(task) { return task.isComplete == true; } handleDeleteTasks() { let ids = []; const comp = this.props.tasks.filter((task) => this.complete(task)); for (let i = 0; i < comp.length; i += 1) { ids[i] = comp[i].id; } this.props.dispatch(actions.deleteTasks(ids)); } <button className="del" onClick={this.handleDeleteTasks} disabled={this.props.tasks.filter(this.complete).length == 0} > Del Tasks X {this.props.tasks.filter(this.complete).length} </button> <div style={{ clear: "both" }} /> {this.props.isError ...
Del Tasksボタンをformの隣に配置するためにtask-formコンポーネントにstyleを加える。
// ./client/src/components/task-form.jsx render() { return ( <form onSubmit={this.handleSubmit} // 追加 style={{ float: "left" }} >
先程決めたサーバーの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(tempErrorMessage('API Error')); dispatch(toggleLoading(false)); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); }; } function deletedTasks(ids) { return { type: 'DELETED_TASKS', data: ids }; }
アクションが渡された時の処理をindex.jsxのreducerに追加。
// ./client/src/index.jsx case 'DELETED_TASKS': { let newTasks = state.tasks.slice(); for (let i = 0; i < action.data.length; i += 1) { 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ボタンの追加
まずDelボタンをtask-list.jsxに追加。
// ./client/src/components/task-list.jsx export default class TaskList extends React.Component { constructor(props) { super(props); this.handleCheck = this.handleCheck.bind(this); // 追加 this.handleClick = this.handleClick.bind(this); } handleClick(e) { this.props.handledeletetask(e.target.id); } </label> // 追加 {task.isComplete ? <button className="del" id={task.id} onClick={this.handleClick} style={{ float: "right" }} > Del </button> : null}
todo-app.jsxにもろもろ追加。
// ./client/src/components/todo-app.jsx this.handleDeleteTasks = this.handleDeleteTasks.bind(this); // 追加 this.handleDeleteTask = this.handleDeleteTask.bind(this); } handleDeleteTask(id) { this.props.dispatch(actions.deleteTask(id)); } <TaskList tasks={this.props.tasks} handlecheck={this.handleTaskCompletionChange} // 追加 handledeletetask={this.handleDeleteTask} />
サーバーと通信するメソッドなどを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(tempErrorMessage('API Error')); dispatch(toggleLoading(false)); console.log('/api/tasks/del_tasks', status, err.toString()); }, }); }; } function deletedTask(id) { return { type: 'DELETED_TASK', data: id }; }
アクションが渡された時の処理をindex.jsxのreducerに追加。
// ./client/src/index.jsx case 'DELETED_TASK': { const newTasks = state.tasks.filter((task) => { return task.id != action.data; }); return Object.assign({}, state, { tasks: newTasks }); } default:
ブラウザでlocalhost:5000を更新する。チェックしてあるタスクの右にDelボタンが表示されること、Delボタンを押して削除出来ることを確認。
React版Todoアプリの完成。
React Redux Rails 4 / 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
参考
checkbox
チェックボックスを作成。
// ./client/src/components/task-list.jsx export default class TaskList extends React.Component { constructor(props) { super(props); // 追加 this.handleCheck = this.handleCheck.bind(this); } handleCheck(e) { this.props.handlecheck(e.target.id, e.target.checked); } render() { return ( <ul> {this.props.tasks.map(task => ( <li key={task.id}> <label className={task.isComplete ? "completed" : null}> <input type="checkbox" id={task.id} checked={task.isComplete} onChange={this.handleCheck} /> {task.name} </label> </li> ))} </ul> ); } }
todo-app.jsxにマウントされているtask-listコンポーネントに
// ./client/src/components/todo-app.jsx class TodoApp extends React.Component { constructor(props) { super(props); this.handleNewTask = this.handleNewTask.bind(this); this.handleInputForm = this.handleInputForm.bind(this); // 追加 this.handleTaskCompletionChange = this.handleTaskCompletionChange.bind(this); } // 追加 handleTaskCompletionChange(id, isComplete) { this.props.dispatch(actions.toggleComplete(id, isComplete)); } <TaskList tasks={this.props.tasks} // 追加 handlecheck={this.handleTaskCompletionChange} />
サーバーにチェックの状態を送るメソッドなどを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.jsxのreducerに追加。
// ./client/src/index.jsx 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にisCompleteを追加。
# ./app/views/api/tasks/index.json.jbuilder json.tasks(@tasks) { |t| json.extract! t, :id, :name, :isComplete }
同じく、JSONがisCompleteを返すようにshow.json.jbuilderにisCompleteを追加。
# ./app/views/api/tasks/show.json.jbuilder json.extract! @task, :id, :name, :isComplete
再びターミナルで
$ foreman start
でサーバーを開始。
ブラウザでlocalhost:5000を更新してチェックしてもエラーにならないことを確認。
また更新後チェックの状態が維持されることも確認。
Warning
タスクを新しく追加し、チェックの状態を変えると以下のような警告がでるが、解消できなかった。(実力不足!!!)
動作には問題なかったので諦めた。
Warning: TaskList is changing an uncontrolled input of type checkbox to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://fb.me/react-controlled-components
サーバーエラー時の処理
チェックの状態が変更された時にサーバー側でエラーが起きると、サーバー側とブラウザ側で矛盾が生じる(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/components/error-message.jsx import React from 'react'; export default class ErrorMessage extends React.Component { render() { const style = { marginTop: "5px", paddingLeft: "10px", color: "white", backgroundColor: "red", }; return ( <div style={style}> {this.props.message} </div> ); } }
todo-app.jsxでerror-messageコンポーネントを読み込みマウントさせる。
// ./client/src/components/todo-app.jsx import ErrorMessage from './error-message.jsx'; <ErrorMessage message={"Text Message"} /> {this.props.isLoading
ブラウザでlocalhost:5000を更新して Text Message というメッセージが表示されていることを確認。
エラーメッセージが表示できるようになったので、エラーが起こった時にだけ表示されるようにする。
showErrorメソッドを作成し、サーバーのエラー時の処理に追加。
// ./client/src/action.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メソッドでアクションが送られたときの処理をreducerに追加。
// ./client/src/index.js case 'SHOW_ERROR': return Object.assign({}, state, { isError: true, errorMessage: action.data });
先程のように固定ではなく、stateの変化に対応できるよう書き換え。
// ./client/src/components/todo-app.jsx // 書き換え {this.props.isError ? <ErrorMessage message={this.props.errorMessage} /> : null} {this.props.isLoading function mapStateToProps(state) { const { tasks, isLoading, isText, isError, errorMessage } = state; return { tasks, isLoading, isText, isError, errorMessage, }; }
ここでわざとサーバーのurlを間違えたものにしておき、エラーが起こるようにしておく。
ブラウザでlocalhost:5000を更新する。チェックの状態を変更するとエラーが起こりエラーメッセージが表示されること、チェックが反転されることを確認。
次にエラーメッセージが2秒後消えるようにする。
// ./client/src/action.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.jsx 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を削除できるようにする。