1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

CakePHP4 で作ったAPIを laravel-mix の Vue.jsからよぶ(少し Atomic Design)

Last updated at Posted at 2020-04-08

CakePHP4 で「独立したコアレイヤパターン」を適用する で API を作ったので、画面から呼び出せるようにしようと思います。

ただ、、、僕は Vue.js 初心者っというより、フロントエンドの開発はほとんどやっていないです。
Vuex は、実プロジェクトだと優秀なフロントエンジニアが活用してるんですが、自分は使い方がわかってないので使っていません。

「Atomic Design」の考えを少し取り入れて作ります。

今回は、Javascript で書いていますが、できれば Typescript にしたいなっと思っています。

この記事でわかること

事前準備

docker-compose up -d
バージョン
docker Docker version 19.03.8, build afacb8b
docker-compose docker-compose version 1.25.4, build 8d51620a
CakePHP4 4.0.4
Vue.js 2.6.11
vue-router 3.1.6

まずは共通的なところ

js ファイルは ./assets/js 配下に格納します。
この格納位置は、webpack.mix.js で指定しています。

webpack.mix.js
const mix = require('laravel-mix');

mix.setPublicPath('webroot')
    .js('assets/js/app.js', 'assets/js')
    .sass('assets/sass/app.scss', 'assets/css')
    .sourceMaps().webpackConfig({devtool: 'source-map'});

bootstrap.js は laravel-mix を導入したときから変えていません。

assets/js/bootstrap.js
window._ = require('lodash');

/**
 * We'll load jQuery and the Bootstrap jQuery plugin which provides support
 * for JavaScript based Bootstrap features such as modals and tabs. This
 * code may be modified to fit the specific needs of your application.
 */

try {
    window.Popper = require('popper.js').default;
    window.$ = window.jQuery = require('jquery');

    require('bootstrap');
} catch (e) {}

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

window.axios = require('axios');

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */

// import Echo from 'laravel-echo';

// window.Pusher = require('pusher-js');

// window.Echo = new Echo({
//     broadcaster: 'pusher',
//     key: process.env.MIX_PUSHER_APP_KEY,
//     cluster: process.env.MIX_PUSHER_APP_CLUSTER,
//     encrypted: true
// });

vue-router でルーティングをします。各コンポーネントは後ほど作ります。

assets/js/router.js
import Vue from 'vue'
import VueRouter from 'vue-router'

import Home from './components/HomeComponent.vue'
import TaskList from './components/pages/TaskListPage.vue'
import TaskAdd from './components/pages/TaskAddPage.vue'
import TaskEdit from './components/pages/TaskEditPage.vue'

Vue.use(VueRouter)

// パスとコンポーネントのマッピング
const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/task/list',
    component: TaskList
  },
  {
    path: '/task/add',
    component: TaskAdd
  },
  {
    path: '/task/edit/:id',
    component: TaskEdit
  }
]

// VueRouterインスタンスを作成する
const router = new VueRouter({
  mode: 'history',
  routes
})

// VueRouterインスタンスをエクスポートする
// app.jsでインポートするため
export default router

起点となるルートコンポーネント App.vue を作ります。

assets/js/App.vue
<script>
</script>

<template>
  <div>
    <div class="card-body">
      <RouterLink class="navbar" to="/">ホーム</RouterLink>
      |
      <RouterLink class="navbar" to="/task/list">タスク一覧</RouterLink>
      |
      <RouterLink class="navbar" to="/task/add">タスク追加</RouterLink>
    </div>
    <RouterView />
  </div>
</template>

これでやっと、app.js を作ります。

assets/js/app.js
require('./bootstrap');

window.Vue = require('vue');

import router from './router'
import App from './App.vue'

const app = new Vue({
    el: '#app',
    router, // ルーティングの定義を読み込む
    components: { App }, // ルートコンポーネントの使用を宣言する
    template: '<App />' // ルートコンポーネントを描画する
});

コンポーネントの作成

コンポーネントを作成します。
「Atomic Design」の template と page をわけるところのみの考えを取り入れます。

タスク一覧

TaskListTemplate.vue を作ります。ここにはロジックは書かないようにします。
props を使って、親コンポーネントとなる page コンポーネントから描画する情報を受け取るようにします。

assets/js/components/templates/TaskListTemplate.vue
<script>
export default {
  props: {
    tasks: {
      type: Array
    }
  }
};
</script>

<template>
  <div class="task-list">
    <table>
      <thead>
        <tr>
          <th>タスク概要</th>
          <th>アクション</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="task in tasks" :key="task.id">
          <td>{{ task.description }}</td>
          <td>
            <router-link :to="{ path: `/task/edit/${task.id}` }">編集</router-link>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style scoped>
</style>

TaskListPage.vue を作ります。こちらには表示するための情報を取得するロジックを書きます。

assets/js/components/pages/TaskListPage.vue
<script>
import TaskListTemplate from "../templates/TaskListTemplate.vue";

export default {
  components: {
    TaskListTemplate
  },
  data() {
    return {
      tasks: []
    };
  },
  methods: {
    async fetchTasks() {
      const response = await axios.get(`/api/ca-task/search.json`);
      if (response.status !== 200) {
        return false;
      }
      this.tasks = response.data.data;
    }
  },
  watch: {
    $route: {
      async handler() {
        await this.fetchTasks();
      },
      immediate: true
    }
  }
};
</script>

<template>
  <task-list-template :tasks="tasks"></task-list-template>
</template>

タスク追加

TaskAddTemplate.vue を作ります。ここにはロジックは書かないようにします。
emit を使って、親コンポーネントとなる page コンポーネントへ保存されたときの動きを任せます。

assets/js/components/templates/TaskAddTemplate.vue
<script>
export default {
  data() {
    return {
      task: {
        description: ''
      }
    };
  },
  methods: {
    onSave() {
      this.$emit("save", { task: this.task });
    }
  }
};
</script>

<template>
  <div class="task-add">
    <form @submit.prevent="onSave">

      <div>タスク概要</div>
      <div><textarea v-model="task.description"></textarea></div>

      <div class="form__button">
        <button type="submit">保存</button>
      </div>
    </form>
  </div>
</template>

<style scoped>
</style>

TaskAddPage.vue を作ります。こちらには保存処理を書きます。

assets/js/components/pages/TaskAddPage.vue
<script>
import TaskAddTemplate from "../templates/TaskAddTemplate.vue";

export default {
  components: {
    TaskAddTemplate
  },
  methods: {
    async onSave(event) {
      const response = await axios.post(`/api/ca-task/create.json`, event.task);
      if (response.status !== 200) {
        alert('保存に失敗しました。');
        return;
      }

      alert('保存しました。');
      this.$router.push({ path: '/task/list' });
    }
  }
};
</script>

<template>
  <task-add-template @save="onSave($event)"></task-add-template>
</template>

タスク編集・削除

TaskEditTemplate.vue を作ります。ここにはロジックは書かないようにします。
props を使って、編集する前のタスク情報(現在のタスク情報)を取得して、
emit を使って、親コンポーネントとなる page コンポーネントへ保存・削除・キャンセルの動きを任せます。

削除確認のアラートは、画面デザインに関するところかなっと思い、こちらに記載することにしました。実際は、Molecules として作成されたモーダルになるかもしれません。

assets/js/components/templates/TaskEditTemplate.vue
<script>
export default {
  props: {
    task: {
      type: Object
    }
  },
  methods: {
    onSave() {
      this.$emit("save", { task: this.task });
    },
    onDelete() {
      if (!confirm('削除します。')) {
        return;
      }

      this.$emit("delete", { task: this.task });
    },
    onCancel() {
      this.$emit("cancel", { task: this.task });
    }
  }
};
</script>

<template>
  <div class="task-add">
    <form @submit.prevent="onSave">

      <div>タスク概要</div>
      <div><textarea v-model="task.description"></textarea></div>

      <div class="form__button">
        <button type="submit">保存</button>
        <button type="button" @click.prevent="onDelete">削除</button>
        <button type="button" @click.prevent="onCancel">キャンセル</button>
      </div>
    </form>
  </div>
</template>

<style scoped>
</style>

TaskEditPage.vue を作ります。こちらにはタスク一覧・タスク追加で行ったことの集大成のような感じの作りになります。

assets/js/components/pages/TaskEditPage.vue
<script>
import TaskEditTemplate from "../templates/TaskEditTemplate.vue";

export default {
  components: {
    TaskEditTemplate
  },
  data() {
    return {
      task: {}
    }
  },
  async mounted() {
    const response = await axios.get(`/api/ca-task/view/${this.$route.params.id}.json`);
    if (response.status !== 200) {
      alert('取得に失敗しました。');
       return;
     }

    this.task = response.data.data;
  },
  methods: {
    async onSave(event) {
      const response = await axios.put(`/api/ca-task/update/${this.$route.params.id}.json`, event.task);
      if (response.status !== 200) {
        alert('保存に失敗しました。');
        return;
      }

      alert('保存しました。');
      this.$router.push({ path: '/task/list' });
    },
    async onDelete(event) {
      const response = await axios.delete(`/api/ca-task/delete/${this.$route.params.id}.json`);
      if (response.status !== 204) {
        alert('削除に失敗しました。');
        return;
      }

      alert('削除しました。');
      this.$router.push({ path: '/task/list' });
    },
    onCancel(event) {
      this.$router.push({ path: '/task/list' });
    }
  }
};
</script>

<template>
  <task-edit-template :task="task" @save="onSave($event)" @delete="onDelete($event)" @cancel="onCancel($event)"></task-edit-template>
</template>

さいごに

Validation も考慮していない作りですが、「Atomic Design」の Templates と Pages の考えは上手に取り込めたと思います。
propsemit を使うことで、画面デザインとロジックをわけることができたと思います。

補足)「Atomic Design」の感想

Atoms、Molecules、Organisms っと作っていくと、HTML 以外の別言語を見ている気がしてきます。
でも、実際にコーディングしている人に聞くと、作りやすいみたいですね。
サーバーサイドだと、フレームワークをラップしまくって、独自フレームワークっぽくなってるのはすごく扱いづらいんですが、フロントエンドだとそうでもないみたいなのが面白いです。

どこに差があるんでしょうかね。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?