君が代

基本的にワイの忘備録

【Vue忘備録】タスクを分類するの巻

前回の続きです。

kimigayo.hatenablog.com

概要

タスクにステータスを追加し、ステータス毎にタスクを分類します。

  • タスクのステータス編集
  • ステータス毎の分類

タスクのステータス編集

まず下記をタスクに追加します。

enum status: { todo: 1, doing: 2, done: 3 }
#初期値は1

TaskCreateModal.vue

<template>
  <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="form-group">
              <label for="title">タイトル</label>
              <input
                type="text"
                class="form-control"
                id="title"
                v-model="task.title"
              >
            </div>
            <div class="form-group">
              <label for="description">説明文</label>
              <textarea
                class="form-control"
                id="description"
                rows="5"
                v-model="task.description"
              ></textarea>
            </div>
            <div class="form-group">
              <label for="status">ステータス</label>
              <select id="status" v-model="task.status" class="form-control">
                <option value="todo">TODO</option>
                <option value="doing">DOING</option>
                <option value="done">DONE</option>
              </select>
            </div>
            <div class="d-flex justify-content-between">
              <button class="btn btn-success" @click="handleCreateTask">追加</button>
              <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: "TaskCreateModal",
  data() {
    return {
      task: {
        title: '',
        description: '',
        status: ''
      }
    }
  },
  methods: {
    handleCloseModal() {
      this.$emit('close-modal')
    },
    handleCreateTask() {
      this.$emit('create-task', this.task)
    }
  }
}
</script>

<style scoped>
.modal {
  display: block;
}
</style>

TaskEditModal.vue

<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="form-group">
              <label for="title">タイトル</label>
              <input
                type="text"
                class="form-control"
                id="title"
                v-model="task.title"
              >
            </div>
            <div class="form-group">
              <label for="description">説明文</label>
              <textarea
                class="form-control"
                id="description"
                rows="5"
                v-model="task.description"
              ></textarea>
            </div>
            <div class="form-group">
              <label for="status">ステータス</label>
              <select id="status" v-model="task.status" class="form-control">
                <option value="todo">TODO</option>
                <option value="doing">DOING</option>
                <option value="done">DONE</option>
              </select>
            </div>
            <div class="d-flex justify-content-between">
              <button class="btn btn-success" @click="handleUpdateTask">更新</button>
              <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: {
    task: {
      id: {
        type: Number,
        required: true
      },
      title: {
        type: String,
        required: true
      },
      description: {
        type: String,
        required: true
      },
      status: {
        type: Number,
        required: true
      }
    }
  },
  methods: {
    handleCloseModal() {
      this.$emit('close-modal')
    },
    handleUpdateTask() {
      this.$emit('update-task', this.task)
    }
  }
}
</script>

<style scoped>
.modal {
  display: block;
}
</style>

これでタスクへステータスを付与することができました。

ステータス毎の分類

次にタスク1つ分のコンポーネントを作成します。 これをステータス毎で繰り返し表示させる形で分類を行なっていきます。 より深ぼって説明するとこの後にステータス毎のタスクのリストを作成するのですが、そのリスト内で下記で作成するTaskItemステータス毎に呼び出すというイメージです。

app/javascript/pages/task/components/TaskItem.vue

<template>
  <div //task/index.vueのv-for〜以下を記載
    :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
    }
  },
  methods: {
    handleShowTaskDetailModal(task) {
      this.$emit('handleShowTaskDetailModal', task)
    }
  }
}
</script>

app/javascript/pages/task/components/TaskList.vue

<template>
  <div class="col-12 col-lg-4">
    <div :id="taskListId" class="bg-light rounded shadow m-3 p-3">
      <slot name="header">タスク区分</slot> //slotを活用してタスク区分のタイトルを表示
      <template v-for="task in tasks">
        <TaskItem :key="task.id" :task="task" @handleShowTaskDetailModal="$listeners['handleShowTaskDetailModal']" />
      </template>
    </div>
  </div>
</template>
<script>
import TaskItem from "./TaskItem" //TaskItemを読み込み
export default {
  name: "TaskList",
  components: {
    TaskItem
  },
  props: {
    tasks: {
      type: Array,
      required: true
    },
    taskListId: {
      type: String,
      required: true
    }
  },
}
</script>

最後に上記のリストをindexの中で呼び出します。 index.vue

<template>
  <div>
    <div class="container-fluid">
      <div class="row">
        <TaskList
          :tasks="todoTasks"
          taskListId="todo-list"
          @handleShowTaskDetailModal="handleShowTaskDetailModal"
        >
          <template v-slot:header>
            <div class="h4">TODO</div>
          </template>
        </TaskList>
        <TaskList
          :tasks="doingTasks"
          taskListId="doing-list"
          @handleShowTaskDetailModal="handleShowTaskDetailModal"
        >
          <template v-slot:header>
            <div class="h4">DOING</div>
          </template>
        </TaskList>
        <TaskList
          :tasks="doneTasks"
          taskListId="done-list"
          @handleShowTaskDetailModal="handleShowTaskDetailModal"
        >
          <template v-slot:header>
            <div class="h4">DONE</div>
          </template>
        </TaskList>
      </div>
    </div>
    <div class="text-center">
      <button class="btn btn-secondary" @click="handleShowTaskCreateModal">タスクを追加</button>
    </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"
                      :task="taskDetail"
                      @close-modal="handleCloseTaskDetailModal"
                      @show-edit-modal="handleShowTaskEditModal"
                      @delete-task="handleDeleteTask"
                        />
    </transition>
    <transition  name="fade">                   
      <TaskCreateModal v-if="isVisibleTaskCreateModal"
                       @close-modal="handleCloseTaskCreateModal"
                       @create-task="handleCreateTask"
                        />
    </transition>
     <transition name="fade">
      <TaskEditModal v-if="isVisibleTaskEditModal"
                     :task="taskEdit" 
                     @close-modal="handleCloseTaskEditModal"
                     @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';
import TaskList from './components/TaskList';

export default {
  components: {
    TaskDetailModal,
    TaskCreateModal,
    TaskEditModal,
    TaskList,
  },
  name: 'TaskIndex',
  data() {
    return {
      taskDetail: {},
      isVisibleTaskDetailModal: false,
      isVisibleTaskCreateModal: false,
      isVisibleTaskEditModal: false,
      taskEdit: {},
      task: ''
    };
  },
  computed: {
    ...mapGetters([ 'tasks' ]),
    todoTasks() { //ステータス毎にタスクを分類
      return this.tasks.filter(task => {
        return task.status == "todo"
      })
    },
    doingTasks() {
      return this.tasks.filter(task => {
        return task.status == "doing"
      })
    },
    doneTasks() {
      return this.tasks.filter(task => {
        return task.status == "done"
      })
    }
  },
  created() {
    this.getTasks();
  },
  methods: {
    ...mapActions([
      'getTasks',
      'createTask',
      'updateTask',
      'deleteTask'
    ]),
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task;
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDeatil = {};
    },
    handleShowTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = true;
    },
    handleCloseTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = false;
    },
    handleShowTaskEditModal(task) {
      this.taskEdit = Object.assign({}, task);
      this.handleCloseTaskDetailModal();
      this.isVisibleTaskEditModal = true;
    },
    handleCloseTaskEditModal() {
      this.isVisibleTaskEditModal = false;
    },
    parentMethod(task) {
      this.task = task;
    },
    async handleCreateTask(task) {
      try {
        await this.createTask(task);
        this.handleCloseTaskCreateModal();
      } catch (error) {
        console.log(error);
      }
    },
    async handleUpdateTask(task) {
      try {
        await this.updateTask(task);
        this.handleCloseTaskEditModal();
      } catch (error) {
        console.log(error);
      }
    },
    async handleDeleteTask(task) {
      try {
        await this.deleteTask(task);
        this.handleCloseTaskDetailModal();
      } catch(error) {
        console.log(error);
      }
    }
  }
}
</script>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

【Vue忘備録】更新と削除機能を実装するの巻

前回の続きです。

kimigayo.hatenablog.com

タスクを作成できるようになったので今度はそれを更新、削除できるようにしていきます。

概要

  • 編集機能
  • 削除機能

## 編集機能

モーダルの追加、モーダルの表示、編集という順で書きます。

まずはタスクの編集用のモーダルを追加。 pages/task/components/TaskEditModal.vue

<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="form-group">
              <label for="title">タイトル</label>
              <input
                type="text"
                class="form-control"
                id="title"
              >
            </div>
            <div class="form-group">
              <label for="description">説明文</label>
              <textarea
                class="form-control"
                id="description"
                rows="5"
              ></textarea>
            </div>
            <div class="d-flex justify-content-between">
              <button class="btn btn-success">更新</button>
              <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",
  methods: {
    handleCloseModal() {
      this.$emit('close-modal')
    }
}
</script>

<style scoped>
.modal {
  display: block;
}
</style>

コンポーネントで非表示の処理をします。 pages/task/index.vue

<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 class="btn btn-secondary" @click="handleShowTaskCreateModal">タスクを追加</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"
                      :task="taskDetail"
                      @close-modal="handleCloseTaskDetailModal"
                        />
    </transition>
    <transition  name="fade">                   
      <TaskCreateModal v-if="isVisibleTaskCreateModal"
                       @close-modal="handleCloseTaskCreateModal"
                       @create-task="handleCreateTask"
                        />
    </transition>
     <transition name="fade">
      <TaskEditModal v-if="isVisibleTaskEditModal" //モーダルの表示、非表示
                     @close-modal="handleCloseTaskEditModal" //モーダル非表示の処理
                        />
    </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, //初期値は非表示
    };
  },
  computed: {
    ...mapGetters([ 'tasks' ])
  },
  created() {
    this.getTasks();
  },
  methods: {
    ...mapActions([
      'getTasks',
      'createTask'
    ]),
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task;
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDeatil = {};
    },
    handleShowTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = true;
    },
    handleCloseTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = false;
    },
    handleCloseTaskEditModal() {
      this.isVisibleTaskEditModal = false; //モーダルの非表示
    },
    async handleCreateTask(task) {
      try {
        await this.createTask(task);
        this.handleCloseTaskCreateModal();
      } catch (error) {
        console.log(error);
      }
    }
  }
}
</script>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

次にタスク詳細のモーダルから編集モーダルを呼び出す実装をします。 pages/task/components/TaskDetailModal.vue

<template>
  <div :id="'task-detail-modal-' + task.id">
  <transition name="fade">
      <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>&times;</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">削除</button>
            <button type="button" class="btn btn-secondary" @click="handleCloseModal">閉じる</button>
          </div>
          </div>
        </div>
      </div>
      <div class="modal-backdrop show"></div>
  </transition>
  </div>
</template>

<script>
export default {
  name: 'TaskDetailModal',
  methods: {
    handleCloseModal(){
      this.$emit('close-modal')
    },
    handleShowTaskEditModal(){
      this.$emit('show-edit-modal')
    }
  }
}
</script>

<style scoped>
/* 表示/非表示はvueで制御するので最初から表示状態にする */
 .modal {
  display: block;
}
</style>

削除ボタンも合わせて実装。

pages/task/index.vue

<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 class="btn btn-secondary" @click="handleShowTaskCreateModal">タスクを追加</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"
                      :task="taskDetail"
                      @close-modal="handleCloseTaskDetailModal"
                      @show-edit-modal="handleShowTaskEditModal" //呼び出し
                        />
    </transition>
    <transition  name="fade">                   
      <TaskCreateModal v-if="isVisibleTaskCreateModal"
                       @close-modal="handleCloseTaskCreateModal"
                       @create-task="handleCreateTask"
                        />
    </transition>
     <transition name="fade">
      <TaskEditModal v-if="isVisibleTaskEditModal" //モーダルの表示、非表示
                     @close-modal="handleCloseTaskEditModal" //モーダル非表示の処理
                        />
    </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, //初期値は非表示
    };
  },
  computed: {
    ...mapGetters([ 'tasks' ])
  },
  created() {
    this.getTasks();
  },
  methods: {
    ...mapActions([
      'getTasks',
      'createTask'
    ]),
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task;
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDeatil = {};
    },
    handleShowTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = true;
    },
    handleCloseTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = false;
    },
 handleShowTaskEditModal(task) {
      this.handleCloseTaskDetailModal();
      this.isVisibleTaskEditModal = true; //モーダルの表示
    },
    handleCloseTaskEditModal() {
      this.isVisibleTaskEditModal = false; //モーダルの非表示
    },
    async handleCreateTask(task) {
      try {
        await this.createTask(task);
        this.handleCloseTaskCreateModal();
      } catch (error) {
        console.log(error);
      }
    }
  }
}
</script>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

次は詳細から編集モーダルへ移った際のデータの受け渡しを親コンポーネント経由で行う実装。 pages/task/index.vue

<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 class="btn btn-secondary" @click="handleShowTaskCreateModal">タスクを追加</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"
                      :task="taskDetail"
                      @close-modal="handleCloseTaskDetailModal"
                      @show-edit-modal="handleShowTaskEditModal" //呼び出し
                        />
    </transition>
    <transition  name="fade">                   
      <TaskCreateModal v-if="isVisibleTaskCreateModal"
                       @close-modal="handleCloseTaskCreateModal"
                       @create-task="handleCreateTask"
                        />
    </transition>
     <transition name="fade">
      <TaskEditModal v-if="isVisibleTaskEditModal" //モーダルの表示、非表示
        :task="taskEdit"  //代入しておくことで子コンポーネントでpropsを使ってデータを読み込める
                     @close-modal="handleCloseTaskEditModal" //モーダル非表示の処理
                        />
    </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.getTasks();
  },
  methods: {
    ...mapActions([
      'getTasks',
      'createTask'
    ]),
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task;
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDeatil = {};
    },
    handleShowTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = true;
    },
    handleCloseTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = false;
    },
 handleShowTaskEditModal(task) {
      this.taskEdit = Object.assign({}, task); //要注意
      this.handleCloseTaskDetailModal();
      this.isVisibleTaskEditModal = true; //モーダルの表示
    },
    handleCloseTaskEditModal() {
      this.isVisibleTaskEditModal = false; //モーダルの非表示
    },
    async handleCreateTask(task) {
      try {
        await this.createTask(task);
        this.handleCloseTaskCreateModal();
      } catch (error) {
        console.log(error);
      }
    }
  }
}
</script>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>
this.taskEdit = Object.assign({}, task);

this.taskEdit = taskとtaskEditとtaskが同じ参照を持ちテキストエリアを変更した後キャンセルで戻った場合も既存データに影響が出るのでObject.assignを使用してオブジェクトをコピーする必要がある。

pages/task/components/TaskDetailModal.vue

<template>
  <div :id="'task-detail-modal-' + task.id">
  <transition name="fade">
      <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>&times;</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">削除</button>
            <button type="button" class="btn btn-secondary" @click="handleCloseModal">閉じる</button>
          </div>
          </div>
        </div>
      </div>
      <div class="modal-backdrop show"></div>
  </transition>
  </div>
</template>

<script>
export default {
  name: 'TaskDetailModal',
  props: { //追加
    task: {
      id: {
        type: Number,
        required: true
      },
      title: {
        type: String,
        required: true
      },
      description: {
        type: String,
        required: true
      }
    }
  },
  methods: {
    handleCloseModal(){
      this.$emit('close-modal')
    },
    handleShowTaskEditModal(){
      this.$emit('show-edit-modal', this.task) //追加
    }
  }
}
</script>

pages/task/components/TaskEditModal.vue

<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="form-group">
              <label for="title">タイトル</label>
              <input
                type="text"
                class="form-control"
                id="title"
                v-model="task.title" //追加
              >
            </div>
            <div class="form-group">
              <label for="description">説明文</label>
              <textarea
                class="form-control"
                id="description"
                rows="5"
                v-model="task.description" //追加
              ></textarea>
            </div>
            <div class="d-flex justify-content-between">
              <button class="btn btn-success">更新</button>
              <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: { //追加
    task: {
      id: {
        type: Number,
        required: true
      },
      title: {
        type: String,
        required: true
      },
      description: {
        type: String,
        required: true
      }
    }
  },
  methods: {
    handleCloseModal() {
      this.$emit('close-modal')
    }
  }
}
</script>

<style scoped>
.modal {
  display: block;
}
</style>

受け渡しができるようになったので最後にaxiosとstoreを使って更新の処理を実装します。 pages/task/components/TaskEditModal.vue

<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="form-group">
              <label for="title">タイトル</label>
              <input
                type="text"
                class="form-control"
                id="title"
                v-model="task.title"
              >
            </div>
            <div class="form-group">
              <label for="description">説明文</label>
              <textarea
                class="form-control"
                id="description"
                rows="5"
                v-model="task.description"
              ></textarea>
            </div>
            <div class="d-flex justify-content-between">
              <button class="btn btn-success" @click="handleUpdateTask">更新</button> //追加
              <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: {
    task: {
      id: {
        type: Number,
        required: true
      },
      title: {
        type: String,
        required: true
      },
      description: {
        type: String,
        required: true
      }
    }
  },
  methods: {
    handleCloseModal() {
      this.$emit('close-modal')
    },
    handleUpdateTask() {
      this.$emit('update-task', this.task) //追加
    }
  }
}
</script>

<style scoped>
.modal {
  display: block;
}
</style>

emitメソッドを使って親コンポーネントでタスク更新を行います。

pages/task/index.vue

<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 class="btn btn-secondary" @click="handleShowTaskCreateModal">タスクを追加</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"
                      :task="taskDetail"
                      @close-modal="handleCloseTaskDetailModal"
                      @show-edit-modal="handleShowTaskEditModal"
                        />
    </transition>
    <transition  name="fade">                   
      <TaskCreateModal v-if="isVisibleTaskCreateModal"
                       @close-modal="handleCloseTaskCreateModal"
                       @create-task="handleCreateTask"
                        />
    </transition>
     <transition name="fade">
      <TaskEditModal v-if="isVisibleTaskEditModal"
                     :task="taskEdit" 
                     @close-modal="handleCloseTaskEditModal"
                     @update-task="handleUpdateTask" //前述のemitでの呼び出し
                        />
    </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.getTasks();
  },
  methods: {
    ...mapActions([
      'getTasks',
      'createTask',
      'updateTask',
    ]),
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task;
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDeatil = {};
    },
    handleShowTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = true;
    },
    handleCloseTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = false;
    },
    handleShowTaskEditModal(task) {
      this.taskEdit = Object.assign({}, task);
      this.handleCloseTaskDetailModal();
      this.isVisibleTaskEditModal = true;
    },
    handleCloseTaskEditModal() {
      this.isVisibleTaskEditModal = false;
    },
    async handleCreateTask(task) {
      try {
        await this.createTask(task);
        this.handleCloseTaskCreateModal();
      } catch (error) {
        console.log(error);
      }
    },
    async handleUpdateTask(task) { //更新処理
      try {
        await this.updateTask(task);
        this.handleCloseTaskEditModal();
      } catch (error) {
        console.log(error);
      }
    }
  }
}
</script>

store/index.js

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) {
      return state.tasks
    }
  },
  mutations: {
    setTasks(state, tasks) {
      state.tasks = tasks
    },
    addTask(state, task) {
      state.tasks.push(task)
    },
    updateTask: (state, updateTask) => {
      const index = state.tasks.findIndex(task => {
        return task.id == updateTask.id
      })
      state.tasks.splice(index, 1, updateTask)
    }
  },
  actions: {
    getTasks({ 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)
      })
    },
    updateTask({commit}, task) {
      return axios.patch(`tasks/${task.id}`, task)
        .then(res => {
          commit('updateTask', res.data)
        })
    }
  }
})

以上で更新は完了。

削除

削除については同じような実装なので、サクッといきます。

pages/task/components/TaskDeatilModal.vue

<template>
  <div :id="'task-detail-modal-' + task.id">
  <transition name="fade">
      <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>&times;</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>
  </transition>
  </div>
</template>

<script>
export default {
  name: 'TaskDetailModal',
  props: {
    task: {
      id: {
        type: Number,
        required: true
      },
      title: {
        type: String,
        required: true
      },
      description: {
        type: String,
        required: true
      }
    }
  },
  methods: {
    handleCloseModal(){
      this.$emit('close-modal')
    },
    handleShowTaskEditModal(){
      this.$emit('show-edit-modal', this.task)
    },
    handleDeleteTask(){
      this.$emit('delete-task', this.task) //emitで親コンポーネントへ
    },
  }
}
</script>

<style scoped>
/* 表示/非表示はvueで制御するので最初から表示状態にする */
 .modal {
  display: block;
}
</style>

pages/task/index.vue

<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 class="btn btn-secondary" @click="handleShowTaskCreateModal">タスクを追加</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"
                      :task="taskDetail"
                      @close-modal="handleCloseTaskDetailModal"
                      @show-edit-modal="handleShowTaskEditModal"
                      @delete-task="handleDeleteTask" //子コンポーネントから呼び出し
                        />
    </transition>
    <transition  name="fade">                   
      <TaskCreateModal v-if="isVisibleTaskCreateModal"
                       @close-modal="handleCloseTaskCreateModal"
                       @create-task="handleCreateTask"
                        />
    </transition>
     <transition name="fade">
      <TaskEditModal v-if="isVisibleTaskEditModal"
                     :task="taskEdit" 
                     @close-modal="handleCloseTaskEditModal"
                     @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.getTasks();
  },
  methods: {
    ...mapActions([
      'getTasks',
      'createTask',
      'updateTask',
      'deleteTask'
    ]),
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task;
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDeatil = {};
    },
    handleShowTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = true;
    },
    handleCloseTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = false;
    },
    handleShowTaskEditModal(task) {
      this.taskEdit = Object.assign({}, task);
      this.handleCloseTaskDetailModal();
      this.isVisibleTaskEditModal = true;
    },
    handleCloseTaskEditModal() {
      this.isVisibleTaskEditModal = false;
    },
    async handleCreateTask(task) {
      try {
        await this.createTask(task);
        this.handleCloseTaskCreateModal();
      } catch (error) {
        console.log(error);
      }
    },
    async handleUpdateTask(task) {
      try {
        await this.updateTask(task);
        this.handleCloseTaskEditModal();
      } catch (error) {
        console.log(error);
      }
    },
    async handleDeleteTask(task) { //削除
      try {
        await this.deleteTask(task);
        this.handleCloseTaskDetailModal();
      } catch(error) {
        console.log(error);
      }
    }
  }
}
</script>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

store/index.js

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) {
      return state.tasks
    }
  },
  mutations: {
    setTasks(state, tasks) {
      state.tasks = tasks
    },
    addTask(state, task) {
      state.tasks.push(task)
    },
    updateTask: (state, updateTask) => {
      const index = state.tasks.findIndex(task => {
        return task.id == updateTask.id
      })
      state.tasks.splice(index, 1, updateTask)
    },
    deleteTask: (state, deleteTask) => {
      state.tasks = state.tasks.filter(task => {
        return task.id != deleteTask.id
      })
    },
  },
  actions: {
    getTasks({ 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)
      })
    },
    updateTask({commit}, task) {
      return axios.patch(`tasks/${task.id}`, task)
        .then(res => {
          commit('updateTask', res.data)
        })
    },
    deleteTask({commit}, task) {
      return axios.delete(`tasks/${task.id}`)
        .then(res => {
          commit('deleteTask', res.data)
        })
    }
  }
})

【Vue忘備録】Vuexを使ってタスクを追加するの巻

前回の続きです。

kimigayo.hatenablog.com

概要

  • タスク追加
  • Vuexの導入
  • Vuexを使ってタスク一覧と追加

タスク追加

まずタスク追加用のモーダルの作成から入ります。

app/javascript/pages/task/components/TaskCreateModal.vue

<template>
  <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="form-group">
              <label for="title">タイトル</label>
              <input
                type="text"
                class="form-control"
                id="title"
                v-model="task.title"
              >
            </div>
            <div class="form-group">
              <label for="description">説明文</label>
              <textarea
                class="form-control"
                id="description"
                rows="5"
                v-model="task.description"
              ></textarea>
            </div>
            <div class="d-flex justify-content-between">
              <button class="btn btn-success" @click="handleCreateTask">追加</button>
              <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: "TaskCreateModal",
  data() {
    return {
      task: {
        title: '',
        description: ''
      }
    }
  },
  methods: {
    handleCloseModal() {
      this.$emit('close-modal')
    },
    handleCreateTask() {
      this.$emit('create-task', this.task)
    }
  }
}
</script>

<style scoped>
.modal {
  display: block;
}
</style>

前回と同様にemitメソッドを使って子コンポーネントで発生したイベントを親コンポーネントで捉えられるようにします。 同じくモーダルは固定表示したままで親コンポーネントで表示、非表示の切り替えを行っていきます。

app/javascript/pages/task/index.vue

<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="{ 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>                   
      <TaskCreateModal v-if="isVisibleTaskCreateModal" //on/offの切り替え
                       @close-modal="handleCloseTaskCreateModal" /////emitで発火
                       @create-task="handleCreateTask" /////emitで発火
                        />
      </transition>
  </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, //初期値はfalseで表示
    }
  },
  created() {
    this.getTasks();
  },
  methods: {
    getTasks() {
      this.$axios.get('tasks')
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
    async handleCreateTask(task) {   //←←←
      try {
        this.$axios.post('tasks', task) //axiosでhandleCreateTask(task)にて子から受け取ったデータ(引数)をapi/tasksへPOST
        .then(res => {
          this.$router.go({path: this.$router.currentRoute.path, force: true})
        });
        this.handleCloseTaskCreateModal();
      } catch(error) {
        console.log(error)
      }
    },
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task;
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDeatil = {};
    },
    handleShowTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = true;
    },
    handleCloseTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = false;
    }
  }
}
</script>

<style scoped>
</style>

以上でタスクの追加が行えるようになりました。 次からVuexを導入して今までと同じことができるようにしていきます。

Vuexの導入

yarn add vuex

packs/hello_vue.js

import Vue from 'vue'
import App from '../app.vue'
import router from '../router'
import axios from '../plugins/axios'
import store from '../store'
import 'bootstrap/dist/css/bootstrap.css'

Vue.config.productionTip = false
Vue.prototype.$axios = axios

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)
})

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import axios from '../plugins/axios'

Vue.use(Vuex)

export default new Vuex.Store({
  state: { //アプリケーション全体で使用されるデータ/コンポーネントでいうdata
    tasks: []
  },
  getters: { //stateから別の値を算出するために使う
      return state.tasks
    }
  },
  mutations: { //stateを更新するために使う
    setTasks(state, tasks) {
      state.tasks = tasks
    },
    addTask(state, task) {
      state.tasks.push(task)
    }
  },
  actions: { //非同期処理やAPIとの通信を行う
// actionでAPIからtask一覧取得
    getTasks({ commit }) {
      axios.get('tasks')
        .then(res => {
          commit('setTasks', res.data)
        })
        .catch(err => console.log(err.response));
    },
// コンポーネントから受け取ったデータを引数で受け取り、APIに送信してタスクPOSTする
    createTask({commit}, task) {
      return axios.post('tasks', task)
        .then(res => {
        commit('addTask', res.data)
      })
    }
  }
})

vuex.vuejs.org

Vuexを使ってタスク一覧と追加

pages/index.vue

<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 class="btn btn-secondary" @click="handleShowTaskCreateModal">タスクを追加</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>                   
      <TaskCreateModal v-if="isVisibleTaskCreateModal"
                       @close-modal="handleCloseTaskCreateModal"
                       @create-task="handleCreateTask"
                        />
      </transition>
  </div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex' 
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' ])
  },
  created() {
    this.getTasks();
  },
  methods: {
    ...mapActions([
      'getTasks',
      'createTask'
    ]),
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task;
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDeatil = {};
    },
    handleShowTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = true;
    },
    handleCloseTaskCreateModal(task) {
      this.isVisibleTaskCreateModal = false;
    },
    async handleCreateTask(task) {
      try {
        await this.createTask(task)
        this.handleCloseTaskCreateModal()
      } catch (error) {
        console.log(error)
      }
    }
  }
}
</script>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

以上で完了。

【Vue忘備録】モーダルを作ってタスクの詳細画面を作るの巻

前回の続きです。axiosを使ってタスク一覧を表示させるところまでやりました。

kimigayo.hatenablog.com

概要

以下の手順で行います。

  • モーダルを表示
  • モーダルにタスクの説明文を追加

モーダル作成

$ touch app/javascript/pages/task/components/TaskDetailModal.vue

TaskDetailModal.vue

<template>
  <transition name="fade">
      <div class="modal">
        <div class="modal-dialog">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title">タスクのタイトル</h5>
              <button type="button" class="close">
                <span>&times;</span>
              </button>
            </div>
            <div class="modal-body">
              <p>タスクの説明文</p>
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-secondary">
                閉じる
              </button>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-backdrop show"></div>
  </transition>
</template>

<script>
export default {
  name: 'TaskDetailModal'
}
</script>

<style scoped>
 .modal {
  display: block;
}
</style>

ひとまず、モーダルを用意。

 .modal {
  display: block;
}

これでモーダルは固定表示しております。 表示、非表示の切り替えはVueで行う。

雛形の作成は完了したので次にモーダルの表示を行えるようにします。 app/javascript/pages/task/index.vue

<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"
             class="bg-white border shadow-sm rounded my-2 p-4">
          <span>{{ task.title }}</span>
        </div>
      </div>
    </div>
    <div class="text-center">
      <router-link to="/" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    <TaskDetailModal />  追記
  </div>
</template>

<script>
import TaskDetailModal from './components/TaskDetailModal'  追記

export default {
  components: {  
    TaskDetailModal 追記
  },
  name: 'TaskIndex',
  data() {
    return {
      tasks: []
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
  }
}
</script>

<style scoped>
</style>

現状こんな感じ。 Image from Gyazo

これは前述した通り固定表示なので、これから非表示の切り替えを行います。

モーダルの表示、非表示の切り替え

app/javascript/pages/task/index.vue

<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"
             class="bg-white border shadow-sm rounded my-2 p-4">
          <span>{{ task.title }}</span>
        </div>
      </div>
    </div>
    <div class="text-center">
      <router-link to="/" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    <TaskDetailModal v-if="isVisibleTaskDetailModal" />  追記
  </div>
</template>

<script>
import TaskDetailModal from './components/TaskDetailModal'

export default {
  components: {
    TaskDetailModal
  },
  name: 'TaskIndex',
  data() {
    return {
      tasks: [],
      isVisibleTaskDetailModal: false  追記
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
  }
}
</script>

<style scoped>
</style>

v-ifに指定されたデータがtrueなら対象のHTMLタグは出力され、falseなら出力されませんのでモーダルはtrueなら表示、falseなら非表示。 isVisibleTaskDetailModal: false ここのfalseをtrueに変えれば先ほどと同じようにモーダルが固定表示の状態になります。

なので、次はタスクをクリックすると isVisibleTaskDetailModal: true モーダルが表示されるようにしていきます。

app/javascript/pages/task/index.vue

<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>
        <div class="btn btn-dark">タスクを追加</div>
      </div>
    </div>
    <div class="text-center">
      <router-link :to="{ name: 'TopIndex' }" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    <TaskDetailModal v-if="isVisibleTaskDetailModal" />
  </div>
</template>
<script>
import TaskDetailModal from './components/TaskDetailModal'
export default {
  components: {
    TaskDetailModal
  },
  name: 'TaskIndex',
  data() {
    return {
      tasks: [],
      isVisibleTaskDetailModal: false
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
    handleShowTaskDetailModal(task) { 追記
      this.isVisibleTaskDetailModal = true;
    },
  }
}
</script>

<style scoped>
</style>

これでクリックすることで isVisibleTaskDetailModalをtrueにしモーダルを表示。

次は表示したモーダルを非表示にしていきます。

TaskDetailModal.vue

<template>
  <transition name="fade">
      <div class="modal" @click.self="handleCloseModal"> 追記
        <div class="modal-dialog">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title">タスクのタイトル</h5>
              <button type="button" class="close" @click="handleCloseModal"> 追記
                <span>&times;</span>
              </button>
            </div>
            <div class="modal-body">
              <p>タスクの説明文</p>
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-secondary" @click="handleCloseModal"> 追記
                閉じる
              </button>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-backdrop show"></div>
  </transition>
</template>

<script>
export default {
  name: 'TaskDetailModal',
  methods: { 追記
    handleCloseModal(){
      this.$emit('close-modal')
    }
  }
}
</script>

<style scoped>
 .modal {
  display: block;
}
</style>

モーダルを制御しているのは親コンポーネントなので子コンポーネントに存在する「閉じる」を押した時にそのイベントを親コンポーネントに伝達する必要があります。 このような子コンポーネントで発生したイベントを親コンポーネントで捉えるためにはemitメソッドを使います。

methods: { 追記
    handleCloseModal(){
      this.$emit('close-modal')
    }

上記は$emitを使って親コンポーネントの@close-modalにセットされてるメソッドを呼び出している。

app/javascript/pages/task/index.vue

<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>
        <div class="btn btn-dark">タスクを追加</div>
      </div>
    </div>
    <div class="text-center">
      <router-link :to="{ name: 'TopIndex' }" class="btn btn-dark mt-5">戻る</router-link>
    </div>
    <TaskDetailModal v-if="isVisibleTaskDetailModal"
                     @close-modal="handleCloseTaskDetailModal" 追記
                      />
  </div>
</template>
<script>
import TaskDetailModal from './components/TaskDetailModal'

export default {
  components: {
    TaskDetailModal
  },
  name: 'TaskIndex',
  data() {
    return {
      tasks: [],
      isVisibleTaskDetailModal: false
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
    },
    handleCloseTaskDetailModal() { 追記
      this.isVisibleTaskDetailModal = false;
    }
  }
}
</script>

@close-modalにセットされたhandleCloseTaskDetailModalメソッドを使ってisVisibleTaskDetailModalをfalseにすることでモーダルを非表示にする処理がこれにて完了。

モーダルにデータを表示

まずは親コンポーネントから子コンポーネントにデータを渡します。

app/javascript/pages/task/index.vue

<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>
      </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>
  </div>
</template>
<script>
import TaskDetailModal from './components/TaskDetailModal'

export default {
  components: {
    TaskDetailModal
  },
  name: 'TaskIndex',
  data() {
    return {
      tasks: [],
      taskDetail: {}, 追記
      isVisibleTaskDetailModal: false
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    },
    handleShowTaskDetailModal(task) {
      this.isVisibleTaskDetailModal = true;
      this.taskDetail = task; 追記
    },
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false;
      this.taskDeatil = {}; 追記
    }
  }
}
</script>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

:〇〇に変数を代入しておくことで子コンポーネントでpropsを使ってデータを読み込める。

 <TaskDetailModal v-if="isVisibleTaskDetailModal"
                      @close-modal="handleCloseTaskDetailModal"
                      :task="taskDetail" 追記
                        />

そしてtaskDetailの初期値は空のハッシュにしておいて表示されたらタスクを代入して、非表示の際はハッシュをまた空にします。

TaskDetailModal.vue

<template>
  <div :id="'task-detail-modal-' + task.id">
  <transition name="fade">
      <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>&times;</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-secondary" @click="handleCloseModal">
                閉じる
              </button>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-backdrop show"></div>
  </transition>
  </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')
    }
  }
}
</script>

最後にモーダルコンポーネントでpropsを受け取ってデータを表示させます。

これにて完了。

propsについてはこちら。

jp.vuejs.org

【Vue忘備録】axiosを使ってAPIからデータを取得するの巻

前回の記事の続きです。

kimigayo.hatenablog.com

概要

今回作成しているのはタスク管理のサービスなので、タスクのモデルとコントローラーを作成して、タスクのデータをaxiosを使ってVueからAPIを呼び出す実装を行います。

Taskモデルとコントローラー

class Task < ApplicationRecord
  validates :title, presence: true
end
class Api::TasksController < ApplicationController
  before_action :set_task, only: %i[show update destroy]

  def index
    @tasks = Task.all
    render json: @tasks
  end

  def show
    render json: @task
  end

  def create
    @task = Task.new(task_params)

    if @task.save
      render json: @task
    else
      render json: @task.errors, status: :bad_request
    end
  end

  def update
    if @task.update(task_params)
      render json: @task
    else
      render json: @task.errors, status: :bad_request
    end
  end

  def destroy
    @task.destroy!
    render json: @task
  end

  private

  def set_task
    @task = Task.find(params[:id])
  end

  def task_params
    params.require(:task).permit(:title)
  end
end
Rails.application.routes.draw do
  root 'home#index'
  namespace :api do
    resources :tasks
  end
  get '*path', to: 'home#index'
end

タスクのデータはcurlコマンドを使って作成。 CSRFの対応を行わないとPOSTの処理ができないので下記も追加してください。

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end

CSRFクロスサイトリクエストフォージェリ)とは

この攻撃方法は、ユーザーによる認証が完了したと考えられるWebアプリケーションのページに、悪意のあるコードやリンクを仕込むというものです。そのWebアプリケーションへのセッションがタイムアウトしていなければ、攻撃者は本来認証されていないはずのコマンドを実行できてしまいます。

railsguides.jp

RailsにはCSRFに対する機能が標準で用意されていますが、APIで開発する際にはバックエンドとフロントエンドが明確に分離しているのでクロスサイトの概念がなくなります。そのため protect_from_forgery with: :null_session を追加してCSRFトークンの検証を無効化します。

axiosの導入

yarnを使ってaxiosをインストールします。

yarn add axios

axiosはAPI通信をするためのHTTPクライアントです。 github.com

app/javascript/plugins/axios.js

import axios from 'axios'

const axiosInstance = axios.create({
  baseURL: 'api'
})

export default axiosInstance

baseURLについて

axiosを使うと通常は下記のような形式で記載します。

axios.get('https://localhost:3000/api/tasks')

ただ上記のものだと開発環境でしか使えないので本番環境に対応するには条件分岐する必要があります。

if(本番環境) {
  axios.get('https://kimigayo.com/api/tasks')
} else {
  axios.get('https://localhost:3000/api/tasks')
}

しかしこの書き方だとaxiosを使っている全ての場所で条件分岐を書く必要があるので、ここでbaseURLオプションを使ってaxiosに指定するURLからドメイン名を省略しています。

github.com

app/javascripts/packs/hello_vue.js

import Vue from 'vue'
import App from '../app.vue'
import router from '../router'
import axios from '../plugins/axios'
import 'bootstrap/dist/css/bootstrap.css'

Vue.config.productionTip = false
Vue.prototype.$axios = axios

import axios from '../plugins/axios' 先ほど作成したaxiosインスタンを読み込み

Vue.prototype.$axios = axios prototypeに入れる

prototypeに関して

ES6ではこのようにモジュールをインポートしてそれを使うという書き方をします。

import axios from 'axios' // インポートして
// 省略...
axios.get('/tasks') // それを使う

でも各コンポーネントでこのインポートの処理を書くのはアレなのでプロトタイプに入れることによって全てのVueインスタンスで使えるようにします。

$axiosのように$を入れてるのは定義したデータ、算出プロパティ、またはメソッドとの衝突を回避するための命名規則です。

このようにすることで全てのVueインスタンスthis.$axios.get〜 のような形でaxiosを使えるようになります。

jp.vuejs.org

APIの呼び出し

ここからは先ほど導入したaxiosを使ってAPIを呼び出していきます。

app/javascript/pages/task/index.vue

<script>
export default {
  name: "TaskIndex",
  data() {
    return {
      tasks: []
    }
  },
  created() {
    this.fetchTasks();
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks")
        .then(res => this.tasks = res.data)
        .catch(err => console.log(err.status));
    }
  }
}
</script>

<style scoped>
</style>

まず初期表示でタスクを表示できないように、dataオブジェクトのtasksに空配列を定義します。

createdフック

次にライフサイクルフックのcreatedを使って、タスクデータの取得ロジックを書きます。 createdフックにはVueインスタンスが生成された後に実行したい処理を記述 します。 一般的にはこのcreatedフック内でtemplateで表示するためのデータを取得する処理を書きます。

jp.vuejs.org

メソッドの定義についてはmethodsに対して記述を行っています。

jp.vuejs.org

個人的な忘備録

リファクタリングする前は下記のような形で書いてた。

<script>
import axios from 'axios';

export default {
  name: "TaskIndex",
  data() {
    return {
      tasks: []
    }
  },
  mounted() {
    axios
      .get('/api/tasks.json')
      .then(response => (this.tasks = response.data))
  }
}
</script>

以上で終了!

【Vue忘備録】ヘッダーとフッターをコンポーネントに分けるの巻

概要

表題の通り、ヘッダーとフッターのコンポーネントを分割するの巻。

todo

  • ヘッダーとフッター用のコンポーネントを作成する。
  • app/javascript/pages/top/index.vue 内のヘッダーとフッターをそれぞれ上記のコンポーネントへ移動する。
  • それぞれをテンプレート内で呼び出す。

app/javascript/pages/top/index.vue

<template>
  <div class="d-flex flex-column min-vh-100">
    <header class="mb-auto">
      <nav class="navbar navbar-dark bg-dark">
        <span class="navbar-brand mb-0 h1">{{ title }}</span>
      </nav>
    </header>
    <div class="text-center">
      <h3>タスクを管理するやで!</h3>
      <div class="mt-4">タスク管理に自信ニキになるやで。</div>
      <router-link :to="{ name: 'TaskIndex' }" class="btn btn-dark mt-5">はじめる</router-link>
    </div>
    <footer class="mt-auto text-center">
      <small>Copyright &copy; 2021. Kimigayo</small>
    </footer>
  </div>
</template>

<script>
export default {
  name: "TopIndex",
  data() {
    return {
      title: "タスク管理アプリ"
    }
  }
}
</script>

<style scoped>
</style>

コンポーネントの作成

前述の通りヘッダーとフッターのコンポーネントを作成し、上記のファイルの内容を移します。

javascript/components/TheHeader.vue

<template>
  <header>
    <nav class="navbar navbar-dark bg-dark">
      <span class="navbar-brand mb-0 h1">タスク管理アプリ</span>
    </nav>
  </header>
</template>

<script>
export default {
  name: "TheHeader"
}
</script>

<style scoped>
</style>

javascript/components/TheFooter.vue

<template>
  <footer class="text-center">
    <small>Copyright &copy; 2021. Kimigayo</small>
  </footer>
</template>

<script>
export default {
  name: "TheFooter"
}
</script>

<style scoped>
</style>

あとはテンプレート内でコンポーネントを呼び出す処理を書きます。

app/javascript/app.vue

<template>
  <div class="d-flex flex-column min-vh-100">
    <TheHeader class="mb-auto" />
    <router-view />
    <TheFooter class="mt-auto" />
  </div>
</template>

<script>
import TheHeader from './components/TheHeader.vue';
import TheFooter from './components/TheFooter.vue';

export default {
  components: {
    TheHeader,
    TheFooter
  }
}
</script>

参考にした記事一覧

jp.vuejs.org

jp.vuejs.org

uncle-javascript.com

【Rails忘備録】Rspecでreloadを使ってenumのデータを更新するの巻

先ほど記事のspec作成編です。

kimigayo.hatenablog.com

 describe 'PATCH cancel' do
    it '単発レッスン/attendanceキャンセル' do
      post cancel_admin_attendance_path(attendance1), params: {
        attendance: attributes_for(:attendance)
      }
      expect(status).to eq 302
      expect(attendance1.reload.status).to eq 'canceled'
      expect(flash[:notice]).to eq '参加予定を更新しました。'
    end
end

reloadを間に入れてインスタンスを変更する。