【Vue.js】eslintを使って構文チェックを自動化する
はじめに
eslint
を使ってvue.jsの構文チェックを自動化する方法について
eslintの設定
まずは下記のコマンドでeslint
をインストールします。
$ yarn add --dev eslint eslint-plugin-vue
続いてアプリケーションのルートディレクトリに.eslintrc.js
ファイルを作成します。
$ touch .eslintrc.js
作成したファイルに下記のコードを書きます。
module.exports = { "extends": [ "plugin:vue/recommended", ] };
plugin:vue/recommended
の記述をすることで、vue.jsスタイルガイドの優先度A〜Cのルールに対してチェックを行ってくれます。
続いて、package.json
にeslint
のコマンドを登録します。
"scripts": { "lint": "eslint --ext .js,.vue app/javascript", "lint-fix": "eslint --fix --ext .js,.vue app/javascript" },
構文チェック
eslint
の設定が終わったら、下記コマンドで構文チェックをします。
$ yarn run lint
上記のコマンドを実行すると、たくさんのwarning
が表示されるかと思います。
それを全て手動で修正するのは大変なので、下記コマンドを打って自動で修正します。
$ yarn run lint-fix
自動修正できない箇所に関しては、手動で修正します。
【Vue.js】slotを使って親コンポーネントからテンプレートを差し込む
はじめに
v-slot
を使って親コンポーネントから子コンポーネントにテンプレートを差し込む方法について
親コンポーネント
<template> <div class="row"> <childComponent :tasks="todoTasks" taskListId="todo-list" @handleShowTaskDetailModal="handleShowTaskDetailModal"> <template v-slot:header> // v-slotで任意の名前を指定 <div class="h4">TODO</div> // この記述が差し込まれる </template> </childComponent> <childComponent :tasks="doingTasks" taskListId="doing-list" @handleShowTaskDetailModal="handleShowTaskDetailModal"> <template v-slot:header> // v-slotで任意の名前を指定 <div class="h4">DOING</div> // この記述が差し込まれる </template> </childComponent> <childComponent:tasks="doneTasks" taskListId="done-list" @handleShowTaskDetailModal="handleShowTaskDetailModal"> <template v-slot:header> // v-slotで任意の名前を指定 <div class="h4">DONE</div> // この記述が差し込まれる </template> </childComponent> </div> </template>
子コンポーネント
<template> <div class="col-12 col-lg-4"> <div :id="taskListId" class="bg-light rounded shadow m-3 p-3"> <slot name="header"></slot> // ここに差し込まれる // 親コンポーネントで指定した任意の名前を指定する <template v-for="task in tasks"> <TaskItem :key="task.id" :task="task" @handleShowTaskDetailModal="$listeners['handleShowTaskDetailModal']" /> </template> </div> </div> </template>
上記のようにv-slot
を使うことで、同じコンポーネントを呼びだしながら、部分的にテンプレートを変えることができる。
【Vue.js】孫コンポーネントから親コンポーネントにデータを渡す
はじめに
孫コンポーネントから親コンポーネントにデータを渡す方法について
データを渡す方法
孫コンポーネントから親コンポーネントにデータを渡すには、孫コンポーネントと子コンポーネントで$emit
メソッドを使う方法でもできますが、
子コンポーネントで$listeners
メソッドを使うことでもっとわかりやすくなります。
孫コンポーネント
<template> <div :id="'task-' + task.id" class="bg-white border shadow-sm rounded my-2 p-4 d-flex align-items-center" @click="handleShowTaskDetailModal(task)"> <span>{{ task.title }}</span> </div> </template> <script> export default { name: 'TaskItem', props: { task: { type: Object, required: true } }, // $emit を使って子コンポーネントのメソッドを呼びデータを渡す methods: { handleShowTaskDetailModal(task) { this.$emit('handleShowTaskDetailModal', task) } } } </script>
子コンポーネント
<template> <div class="col-12 col-lg-4"> <div :id="taskListId" class="bg-light rounded shadow m-3 p-3"> <slot name="header">タスク区分</slot> <template v-for="task in tasks"> <TaskItem :key="task.id" :task="task" @handleShowTaskDetailModal="$listeners['handleShowTaskDetailModal']" /> // $listenersを使って孫コンポーネントのデータを親コンポーネントに渡す </template> </div> </div> </template> <script> import TaskItem from './TaskItem' export default { components: { TaskItem }, name: "TaskList"
親コンポーネント
<TaskList :tasks="todoTasks" taskListId="todo-list" @handleShowTaskDetailModal="handleShowTaskDetailModal"> // このイベントが発火 <template> ...... ...... <template v-slot:header> <div class="h4">TODO</div> </template> </TaskList> ....... ....... </template> <script> ....... import TaskList from './components/TaskList' export default { components: { TaskList }, // 上記のイベントが発火して下記メソッドが実行される methods: { handleShowTaskDetailModal(task) { this.isVisibleTaskDetailModal = true; this.taskDetail = task; }, ......... ......... </script>
【Vue.js】vuexを使ってタスクの更新と削除機能を実装
はじめに
vuexを使ってタスクの更新と削除機能を実装します。
親コンポーネントから詳細ページモーダルを呼び出して、
その詳細ページに編集ボタンと削除ボタンがあり、
編集ボタンを押すと、編集ページのモーダルが表示される。
タスク編集ページを作成
<template> <div :id="'task-edit-modal-' + task.id"> <div class="modal" @click.self="handleCloseModal"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-body"> <div class="from-group mb-3"> <label for="title">タイトル</label> <input type="text" class="form-control" id="title" v-model="task.title"> // v-modelを使って編集内容をtask.titleに代入 </div> <div class="from-group mb-3"> <label for="description">説明文</label> <textarea class="form-control" id="description" rows="5" v-model="task.description"></textarea> // v-modelを使って編集内容をtask.titleに代入 </div> <div class="d-flex justify-content-between"> <button type="button" class="btn btn-success" @click="hadleUpdateTask">更新</button> <button type="button" class="btn btn-secondary" @click="handleCloseModal">閉じる</button> </div> </div> </div> </div> </div> <div class="modal-backdrop show"></div> </div> </template> <script> export default { name: 'TaskEditModal', props: { // propsを使って親コンポーネントからデータを受け取る task: { id: { type: Number, required: true }, title: { type: String, required: true }, description: { type: String, required: true } } }, methods: { handleCloseModal() { this.$emit('close-modal') // $emitメソッドを使って親コンポーネントのイベントを発火 }, hadleUpdateTask() { this.$emit('update-task', this.task) } } } </script> <style scoped> .modal { display: block; } </style>
詳細ページを修正
<template> <div :id="'task-detail-modal-' + task.id"> <div class="modal" @click.self="handleCloseModal"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">{{ task.title }}</h5> <button type="button" class="close" @click="handleCloseModal"> <span>×</span> </button> </div> <div class="modal-body" v-if="task.description"> <p>{{ task.description }}</p> </div> <div class="modal-footer"> <button type="button" class="btn btn-success" @click="handleShowTaskEditModal">編集</button> // 編集ボタンと削除ボタンを追加 <button type="button" class="btn btn-danger" @click="handleDeleteTask">削除</button> <button type="button" class="btn btn-secondary" @click="handleCloseModal">閉じる</button> </div> </div> </div> </div> <div class="modal-backdrop show"></div> </div> </template> <script> export default { name: "TaskDetailModal", props: { task: { title: { type: String, required: true }, description: { type: String, required: true } } }, methods: { handleCloseModal() { this.$emit('close-modal') }, // 編集ボタンを押したときに親コンポーネントの'show-edit-modal'イベントを発火させる。 // 第二引数にはtaskのデータを渡す。 handleShowTaskEditModal() { this.$emit('show-edit-modal', this.task) }, // 削除ボタンを押したときに親コンポーネントの'delete-task'イベントを発火させる。 // 第二引数にはtaskのデータを渡す。 handleDeleteTask() { this.$emit('delete-task', this.task) } } } </script> <style scoped> .modal { display: block; } </style>
親コンポーネントを修正
<template> <div> <div class="d-flex"> <div class="col-4 bg-light rounded shadow m-3 p-3"> <div class="h4">TODO</div> <div v-for="task in tasks" :key="task.id" :id="'task-' + task.id" class="bg-white border shadow-sm rounded my-2 p-4 d-flex align-items-center" @click="handleShowTaskDetailModal(task)"> <span>{{ task.title }}</span> </div> <button @click="handleShowTaskCreateModal" type="button" class="btn btn-secondary"> タスクを追加 </button> </div> </div> <div class="text-center"> <router-link :to="{ name: 'TopIndex' }" class="btn btn-dark mt-5">戻る</router-link> </div> <transition name="fade"> <TaskDetailModal v-if="isVisibleTaskDetailModal" @close-modal="handleCloseTaskDetailModal" :task="taskDetail" @show-edit-modal="handleShowTaskEditModal" // 詳細ページで編集ボタンを押した時のイベント @delete-task="handleDeleteTask" /> // 詳細ページで削除ボタンを押した時のイベント </transition> <transition name="fade"> <TaskCreateModal v-if="isVisibleTaskCreateModal" @close-modal="handleCloseTaskCreateModal" @create-task="handleCreateTask" /> </transition> // 編集ページモーダルのコンポーネントを呼び出す。 // boolean形式で表示非表示を設定 <transition name="fade"> <TaskEditModal v-if="isVisibleTaskEditModal" @close-modal="handleCloseTaskEditModal" :task="taskEdit" // taskのデータを渡す @update-task="handleUpdateTask"/> // 編集ページで更新ボタンを押した時のイベント </transition> </div> </template> <script> import { mapGetters, mapActions } from 'vuex' import TaskDetailModal from './components/TaskDetailModal' import TaskCreateModal from './components/TaskCreateModal' import TaskEditModal from './components/TaskEditModal' // それぞれのコンポーネントをインポート export default { components: { TaskDetailModal, TaskCreateModal, TaskEditModal }, name: "TaskIndex", data() { return { taskDetail: {}, isVisibleTaskDetailModal: false, isVisibleTaskCreateModal: false, isVisibleTaskEditModal: false, taskEdit: {} // 編集ページにデータを渡すためのプロパティを定義 } }, computed: { ...mapGetters([ 'tasks' ]) }, created() { this.fetchTasks(); }, methods: { ...mapActions([ 'fetchTasks', 'createTask', 'updateTask', 'deleteTask' ]), // vuexのactionsのメソッドを直接呼び出せるようにするためにマッピングする async handleCreateTask(task) { try { await this.createTask(task); this.handleCloseTaskCreateModal(); } catch(error) { console.log(error) } }, // 編集ページで更新ボタンが押された時に発火するメソッド async handleUpdateTask(task) { try { await this.updateTask(task); // vuexのactionsで定義しているメソッド this.handleCloseTaskEditModal(); } catch(error) { console.log(error) } }, async handleDeleteTask(task) { try { await this.deleteTask(task); // vuexのactionsで定義しているメソッド this.handleCloseTaskDetailModal(); } catch(error) { console.log(error) } }, handleCloseTaskDetailModal() { this.isVisibleTaskDetailModal = false; this.taskDetail = {}; }, handleShowTaskDetailModal(task) { this.isVisibleTaskDetailModal = true; this.taskDetail = task; }, handleCloseTaskCreateModal() { this.isVisibleTaskCreateModal = false; }, handleShowTaskCreateModal() { this.isVisibleTaskCreateModal = true; }, handleCloseTaskEditModal() { this.isVisibleTaskEditModal = false; }, handleShowTaskEditModal(task) { this.taskEdit = Object.assign({}, task); // taskのデータを空のオブジェクトにマージしてから値渡しする。 // 参照渡しをすると更新ボタンを押さずに編集モーダルを閉じても値が更新されてしまう。v-modelを使っているため this.isVisibleTaskEditModal = true; this.isVisibleTaskDetailModal = false; }, } } </script> <style scoped> .fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
vuexの設定
import Vue from 'vue' import Vuex from 'vuex' import axios from '../plugins/axios' Vue.use(Vuex) export default new Vuex.Store({ state: { tasks: [] }, getters: { tasks: state => state.tasks }, mutations: { setTasks: (state, tasks) => { state.tasks = tasks }, addTask: (state, task) => { state.tasks.push(task) }, // 更新の処理 updateTask: (state, editTask) => { const index = state.tasks.findIndex(task => { return task.id == editTask.id // findIndexを使って編集するタスクのインデックス番号を取得 }) state.tasks.splice(index, 1, editTask) // spliceメソッドをを使ってタスクを更新する。(正確には交換) }, // 削除の処理 deleteTask: (state, deleteTask) => { state.tasks = state.tasks.filter(task => { return task.id != deleteTask.id // filterメソッドを使って削除するタスク以外の要素の配列を代入する }) } }, actions: { fetchTasks({ commit }) { axios.get('tasks') .then(res => { commit('setTasks', res.data) }) .catch(err => console.log(err.response)); }, createTask({ commit }, task) { return axios.post('tasks', task) .then(res => { commit('addTask', res.data) }) }, // axiosを使って該当のURLにpatchリクエストを送る。 updateTask({ commit }, task) { return axios.patch(`tasks/${task.id}`, task) .then(res => { commit('updateTask', res.data) // 取得したtaskのデータを引数にしてmutationsの'updateTask'を実行する }) }, // axiosを使って該当のURLにdeleteリクエストを送る。 deleteTask({ commit }, task) { return axios.delete(`tasks/${task.id}`, task) .then(res => { commit('deleteTask', res.data) // 取得したtaskのデータを引数にしてmutationsの'deleteTask'を実行する }) } }, })
完成画面
編集します。
更新します。
更新できました!
続いて削除します。
削除できました!
メモ【javascript】
Object.assign
Object.assign
はオブジェクトをマージすることができます。
const object_1 = { a: 1, b: 2 } const object_2 = { c: 3, d: 4 } const united_object = Object.assign(object_1, object_2) console.log(object_1) => { a: 1, b: 2, c: 3, d: 4 } console.log(object_2) => { c: 3, d: 4 } console.log(united_object) => { a: 1, b: 2, c: 3, d: 4 }
上記のように第一引数に第二引数をマージすることができます。
同じプロパティを持っている場合は上書きされます。
const object_1 = { a: 1, b: 2 } const object_2 = { b: 3, c: 4 } const united_object = Object.assign(object_1, object_2) console.log(object_1) => { a: 1, b: 3, c:4 } console.log(object_2) => { b: 3, c: 4 } console.log(united_object) => { a: 1, b: 3, c:4 }
spliceメソッド
spliceメソッドは配列の要素を削除したり追加したりできます。
また削除と追加を同時にして要素を交換できます。
第一引数だけ指定した場合は、指定したインデックス番号以降の要素を削除します。
var array = ['A', 'B', 'C', 'D', 'E'] array.splice(2) => ['A', 'B']
インデックス番号が2の要素であるC
以降の要素が削除される。
第二引数を指定すると、第一引数で指定したインデックス番号から第二引数で指定した数の分だけ要素を削除します。
var array = ['A', 'B', 'C', 'D', 'E'] array.splice(2, 2) => ['A', 'B', 'E']
インデックス番号が2の要素C
と要素D
の二つ分が削除されます。
第三引数を指定すると、第一引数で指定したインデックス番号から第二引数で指定した数の分だけ要素を削除し、
第三引数で指定した要素を追加します。
第二引数を0
にした場合は、第三引数の要素を追加するだけになります。
var array = ['A', 'B', 'C', 'D', 'E'] array.splice(1, 0, '追加します' ) => ['A', '追加します', 'B', 'C', 'D', 'E']
第二引数を1
にすると、要素の交換になります。
var array = ['A', 'B', 'C', 'D', 'E'] array.splice(1, 1, '追加します' ) => ['A', '追加します', 'C', 'D', 'E']
findIndex
findIndex
は配列の要素から特定のインデックス番号を返します。
const b = 'B' const array = ['A', 'B', 'C', 'D', 'E'] const index = array.findIndex(item => item === b) console.log(index) => 1
【Vue.js】vuexを使ってデータを管理する
はじめに
状態管理ライブラリのvuexを使ってデータを管理します。
vuexの設定
まずはvuexをインストールします。
$ yarn add vuex
vuexの設定ファイルを作成します。
$ touch app/javascript/store/index.js
import Vue from 'vue' import Vuex from 'vuex' import axios from '../plugins/axios' // axiosを使ってapi通信でデータを取ってくるのでaxiosをインポートします Vue.use(Vuex) // モジュールシステムを使う際に必須 export default new Vuex.Store({ state: { // stateで空のtasksを定義 tasks: [] }, getters: { // getterでstateのtasksにアクセスできる tasks: state => state.tasks }, mutations: { // mutationsでstateの状態を変更する setTasks: (state, tasks) => { state.tasks = tasks }, addTask: (state, task) => { state.tasks.push(task) } }, actions: { // actionsでapi通信でのデータ取得などの処理を書く fetchTasks({ commit }) { axios.get('tasks') // axiosを使って'/tasks'にgetリクエストを送る .then(res => { commit('setTasks', res.data) // mutationsの'setTasks'を呼び出して、第二引数に取得したタスク一覧データを渡す }) .catch(err => console.log(err.response)); }, createTask({ commit }, task) { return axios.post('tasks', task) // axiosで'/tasks'にpostリクエストを送る .then(res => { commit('addTask', res.data) // mutationsの'addTask'を呼び出して、第二引数で取得したtaskのデータを渡す }) } }, })
コンポーネントを修正
<template> <div> <div class="d-flex"> <div class="col-4 bg-light rounded shadow m-3 p-3"> <div class="h4">TODO</div> <div v-for="task in tasks" :key="task.id" :id="'task-' + task.id" class="bg-white border shadow-sm rounded my-2 p-4" @click="handleShowTaskDetailModal(task)"> <span>{{ task.title }}</span> </div> <button @click="handleShowTaskCreateModal" type="button" class="btn btn-secondary"> タスクを追加 </button> </div> </div> <div class="text-center"> <router-link :to="{ name: 'TopIndex' }" class="btn btn-dark mt-5">戻る</router-link> </div> <transition name="fade"> <TaskDetailModal v-if="isVisibleTaskDetailModal" @close-modal="handleCloseTaskDetailModal" :task="taskDetail" /> </transition> <transition name="fade"> <TaskCreateModal v-if="isVisibleTaskCreateModal" @close-modal="handleCloseTaskCreateModal" @create-task="handleCreateTask" /> </transition> </div> </template> <script> import { mapGetters, mapActions } from 'vuex' // vuexで定義したgetterとacitonsをインポート import TaskDetailModal from './components/TaskDetailModal' import TaskCreateModal from './components/TaskCreateModal' export default { components: { TaskDetailModal, TaskCreateModal }, name: "TaskIndex", data() { return { taskDetail: {}, isVisibleTaskDetailModal: false, isVisibleTaskCreateModal: false } }, computed: { ...mapGetters([ 'tasks' ]) // mapGettersヘルパーを使ってcomputed(算出プロパティ)とマッピングさせる // stateのtasksの値が変わると更新される }, created() { this.fetchTasks(); // actionsで定義した'fetchTasks'メソッドを呼び出す }, methods: { ...mapActions([ 'fetchTasks', 'createTask' ]), // マッピングさせて直接storeのメソッドを呼べるようにする async handleCreateTask(task) { try { this.createTask(task); // actionsで定義した'createTask'メソッドを呼び出す this.handleCloseTaskCreateModal(); } catch(error) { console.log(error) } }, handleCloseTaskDetailModal() { this.isVisibleTaskDetailModal = false; this.taskDetail = {}; }, handleShowTaskDetailModal(task) { this.isVisibleTaskDetailModal = true; this.taskDetail = task; }, handleCloseTaskCreateModal() { this.isVisibleTaskCreateModal = false; }, handleShowTaskCreateModal() { this.isVisibleTaskCreateModal = true; }, } } </script> <style scoped> .fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
【Vue.js】タスク追加ページのモーダルをつくる
はじめに
タスクを追加するページのモーダルをコンポーネントとして作成します。
タスク追加ページを作成
$ touch app/javascript/pages/task/components/TaskCreateModal.vue
<template> <transition name="fade"> <div id="task-create-modal"> <div class="modal" @click.self="handleCloseModal"> // モーダル以外の部分をクリックした時にモーダルを閉じる <div class="modal-dialog"> <div class="modal-content"> <div class="modal-body"> <div class="from-group mb-3"> <label for="title">タイトル</label> <input type="text" class="form-control" id="title" v-model="task.title"> // v-modelを使って入力された内容をそのままtitleに代入 </div> <div class="from-group mb-3"> <label for="description">説明文</label> <textarea class="form-control" id="description" rows="5" v-model="task.description"></textarea> // v-modelを使って入力された内容をそのままdescriptionに代入 </div> <div class="d-flex justify-content-between"> <button type="button" class="btn btn-success" @click="handleCreateTask">追加</button> <button type="button" class="btn btn-secondary" @click="handleCloseModal">閉じる</button> </div> </div> </div> </div> </div> <div class="modal-backdrop show"></div> </div> </transition> </template> <script> export default { name: 'TaskCreateModal', data() { return { task: { title: '', description: '' } } }, methods :{ handleCloseModal() { // 閉じるボタンを押した時に親コンポーネントの'close-modal'イベントを発火 this.$emit('close-modal') }, // 第二引数で入力されたデータを渡す。 handleCreateTask() { // 追加ボタンを押したときに親コンポーネントの'create-task'イベントを発火 this.$emit('create-task', this.task) } } } </script>
<template> <div> <div class="d-flex"> <div class="col-4 bg-light rounded shadow m-3 p-3"> <div class="h4">TODO</div> <div v-for="task in tasks" :key="task.id" :id="'task-' + task.id" @click="handleShowTaskDetailModal(task)" class="bg-white border shadow-sm rounded my-2 p-4"> <span>{{ task.title }}</span> </div> <button @click="handleShowTaskCreateModal" type="button" class="btn btn-secondary"> タスクを追加 </button> </div> </div> <div class="text-center"> <router-link to="/" class="btn btn-dark mt-5">戻る</router-link> </div> <TaskDetailModal v-if="isVisibleTaskDetailModal" :task="taskDetail" @close-modal="handleCloseTaskDetailModal" /> <TaskCreateModal v-if="isVisibleTaskCreateModal" @close-modal="handleCloseTaskCreateModal" // 子コンポーネントで閉じるボタンを押した時のイベント @create-task="handleCreateTask"/> // 子コンポーネントで追加ボタンを押した時のイベント </div> </template><script> import TaskDetailModal from './components/TaskDetailModal' import TaskCreateModal from './components/TaskCreateModal' // 追加ページのコンポーネントをインポート
export default { components: { TaskDetailModal TaskCreateModal }, name: 'TaskIndex', data() { return { tasks: [], taskDetail: {}, isVisibleTaskDetailModal: false, isVisibleTaskCreateModal: false, // デフォルトでは非表示 } }, created() { this.fetchTasks(); }, methods: { fetchTasks() { this.$axios.get('tasks') .then(res => this.tasks = res.data) .catch(err => console.log(err.status)); }, handleCreateTask(task) {
try { this.$axios.post('tasks', task) // axiosを使って/tasksにpostリクエストを送る .then(res => { // ↓ postリクエストが成功した場合に擬似リロード処理を実行 this.$router.go({path: this.$router.currentRoute.path, force: true}) }); this.handleCloseTaskCreateModal(); } catch(error) { // postリクエストが失敗した場合のエラーをコンソールに表示 console.log(error) } }, handleShowTaskDetailModal { this.isVisibleTaskDetailModal = true; this.taskDetail = task; }, handleCloseTaskDetailModal() { this.isVisibleTaskDetailModal = false; this.taskDetail = {}; }, // ↓ 追加ページのモーダルの表示非表示の処理 handleShowTaskCreateModal() { this.isVisibleTaskCreateModal = true; }, handleCloseTaskCreateModal() { this.isVisibleTaskCreateModal = false; } } } </script><style scoped> </style>
完成画面