Belajar Vuex: CRUD Laravel + Vue + Vue Router dan Vuex

whynwd

whynwd Senin, 08 November 2021

Vuex merupakan fitur atau tool dari vue.js untuk menyimpan dan manajemen data terpusat untuk semua komponen vue. Data dari permintaan api dapat disimpan secara global pada 'store' dengan sekali permintaan dan dapat digunakan pada semua komponen yang membutuhkan data dan atau dengan state yang sama.

Selain itu, tentu kita juga dapat mengubah atau memanipulasi state dan melakukan sinkronisasi global disemua komponen, baik dari komponen parent/utama dan anak.

Belajar Vuex: CRUD Laravel + Vue + Vue Router dan Vuex

Untuk lebih detail, melihat contoh dan seperti apa penggunaannya, mari kita buat aplikasi CRUD Vuex sederhana dengan Laravel + Vue + Vue Router.

Belajar Vuex: CRUD Laravel + Vue + Vue Router dan Vuex

Instalasi

Kita lakukan instalasi laravel, vue dan vuex terlebih dahulu. Untuk databasenya sendiri kita gunakan MySQL.

Laravel new lavuexcrud
cd lavuexcrud
npm install
npm install vue vuex vue-router
npm install vue-loader vue-template-compiler --save-dev

Konfigurasi Vue, Vuex, dan Vue Router

Setelah semua instalasi selesai, selanjutnya kita buat Vue Instance, Router dan Vuex Store. Langkah pembuatanya silahkan ikuti langkah-langkah berikut:

1. Pada file resources/js/app.js, tambahkan di bawah ini.

import Vue from 'vue';
import App from './layouts/App'
import store from './store/index'
import router from './routes/index';

new Vue({
  store,
  router,
  render: h => h(App)
}).$mount('#app')

2. Buat folder baru pada direktori resources/js dengan nama layouts dan tambahkan file App.vue didalamnya.

//resources/js/layouts/App.vue
<template>
  <div class="layout">
    <div class="menu">
      <ul> 
        <li><router-link to="/">Home</router-link></li>
        <li><router-link to="/posts">Posts</router-link></li>
      </ul>
    </div>
    <div class="container">
      <router-view></router-view>
    </div>
  </div>
</template>

<style>
body{
  background: #fafafa;
}
.layout{
  max-width: 800px;
  margin: 0 auto;
} 
.menu ul{
  padding: 0;
}
.menu ul li{
  display: inline-block;
  padding: 8px 16px;
  margin-right: 12px;
  background: #bbbbbb;
  border-radius: 2px;
}
.menu ul li a{
  color: #222;
  font-size: 16px;
  text-decoration: none;
}
.menu ul li a:hover{
  color: #fff;
}
</style>

3. Buat folder baru pada direktori resources/js dengan nama store dan tambahkan file index.js didalamnya.

//resources/js/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({    
  //
})

4. Buat folder baru pada direktori resources/js dengan nama routes dan tambahkan file index.js didalamnya.

//resources/js/routes/index.js
import Vue from 'vue';
import VueRouter from 'vue-router'

Vue.use(VueRouter)

import Home from '../pages/homepage'
import Index from '../pages/posts/index'
import Posts from '../pages/posts/Posts'
import Create from '../pages/posts/create' 
 
const routes = [
  {
    path: '/',
    name: 'homepage',
    component: Home
  },
  {
    path: '/posts', 
    component: Index, 
    children: [
      { 
        path: '',
        name: 'posts',
        component: Posts 
      },
      { 
        path: 'create', 
        component: Create 
      },
    ]
  }, 
   
]

const router = new VueRouter({
  mode: 'history', 
  routes
}) 

export default router

5. Buat folder baru pada direktori resources/js dengan nama pages dan tambahkan file homepage.vue didalamnya.

//resources/js/pages/homepage.vue
<template>
  <div>
    <p>Homepage</p>
  </div>
</template>

6. Pada folder pages, tambahkan folder baru dengan nama posts lalu isi dengan 3 file didalamnya: index.vue, create.vue, dan posts.vue.

//resources/js/pages/posts/index.vue
<template>
  <div class="posts">
    <div class="wrapper">
     <div class="menu">
      <ul> 
        <li><router-link to="/posts">All Posts</router-link></li> 
        <li><router-link to="/posts/create">+ New Post</router-link></li> 
      </ul>
    </div>
      <router-view></router-view>
    </div>
  </div>
</template> 

<style scoped>
.wrapper{ 
  background: #eee;
  border-radius: 2px;
  padding: 22px;
}
.menu{
  display: inline-block;
  width: 100%;
}
.menu ul{
  float: right;
  margin-top: 0;
}
.menu ul li{
  background: #b78686;
}
</style>
//resources/js/pages/posts/create.vue
<template>
  <div>
    Form
  </div>
</template>
//resources/js/pages/posts/posts.vue
<template>
  <div>
    Posts
  </div>
</template>

7. Buka file welcome.blade.php, ubah html yang ada dengan dibawah ini.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{csrf_token()}}">
    <title>LaVuexCrud</title> 
  </head>
  <body>  
      <div id="app">
          <app></app>   
      </div>

    <script src="{{ mix('js/app.js') }}"></script>
  </body>
</html>

8. Buka routes/web.php, ubah route yang ada dengan dibawah ini.

Route::get('/{any}', function(){
        return view('welcome');
    })->where('any', '.*');

9. Terakhir buka webpack.mix.js, tambahkan metode vue() seperti dibawah ini.

mix.js('resources/js/app.js', 'public/js')
   .vue();

Setelah tahapan diatas selesai, selanjutnya kita lakukan compiling script. Pada terminal, silahkan buat dua tab dan jalankan perintah dibawah ini.

//tab 1
npm run dev

//tab 2
php artisan serve

Setelah compiling selesai, kita buka aplikasi pada browser dan pastikan semua telah bekerja, termasuk Vuex Store yang dapat dilihat pada vue devtools browser telah aktif.

Belajar Vuex: CRUD Laravel + Vue + Vue Router dan Vuex

State Aplikasi

Vuex menyediakan cara atau metode dalam mengelola store untuk kita terapkan dengan melihat skala aplikasi kita.

Jika aplikasi kita tidak terlalu besar dan hanya bergantung pada satu objek state, kita cukup membuat single state yang digunakan ke semua komponen aplikasi.

const store = new Vuex.Store({    
  state: {
    user: null,
    posts: []
  },
  mutations: {},
  getters: {},
  actions: {} 
})

//store.state.user
//store.state.posts

Namun jika kita membutuhkan lebih dari satu state dengan mutations, actions, getters yang mandiri, kita dapat membagi store dengan cara membuat modules.

const auth = {
  state: () => ({ 
    user: null,
  }), 
  mutations: {},
  getters: {},
  actions: {}
}

const blogs = {
  state: () => ({ 
    posts: [],
    categories: [],
  }), 
  mutations: {},
  getters: {},
  actions: {}
}

const store = new Vuex.Store({    
    modules: {
      auth,
      blogs
    }
})

//store.state.auth.user
//store.state.blogs.posts
//store.state.blogs.categories

Dalam mengelola store ini dapat kita sesuaikan tergantung bagaimana kita ingin mengaturnya. -- Selanjutnya mari kita buat state.

Menambahkan State.

Untuk store yang kita miliki, kita akan tambahkan state dan properti yang kita butuhkan. Dan untuk mempermudah kita menulis fungsi dan mengorganisirnya, kita akan buat file modul untuk fungsi yang akan kita buat.

Pada direktori store, silahkan tambahkan beberapa file dengan nama state.js, mutations.js, getters.js, dan actions.js.

- store 
  + state.js 
  + mutations.js
  + getters.js 
  + actions.js
  - index.js

Kemudian import pada store/index.js.

...
import actions from './actions'
import mutations from './mutations'
import getters from './getters'
import state from './state'

export default new Vuex.Store({    
    state,
    mutations,
    getters,
    actions,
})

Database, Model, Controller

Selanjutnya kita bekerja dengan database, eloquent model, dan controller. Pertama, silahkan buat database MySQL baru dan hubungkan dengan aplikasi.

//.env
DB_DATABASE=lavuexcrud
DB_USERNAME=root
DB_PASSWORD=

Setelah itu kita tambahkan tabel ke database. Kita akan tambahkan sebuah tabel dengan nama posts. Silahkan sesuaikan jika ingin nama lain.

php artisan make:migration create_posts_table

Buka file migrasi yang telah dibuat, tambahkan beberapa kolom seperti dibawah ini.

public function up()
{
  Schema::create('posts', function (Blueprint $table) {
     $table->id();
     $table->string('title');
     $table->text('body'); 
     $table->string('slug');
     $table->timestamps();
  });
}

Kemudian lakukan migrasi.

 php artisan migrate

Terakhir, kita buat model Post sekaligus controller.

php artisan make:model Post -c

Request Model

Selanjutnya kita buat metode permintaan eloquent model pada controller untuk menyimpan, mengambil, dan hapus data pada database. Buka PostController.php tambahkan dibawah ini.

<?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\Models\Post;

class PostController extends Controller
{
   public function posts(){
         
      $post = Post::orderBy('id', 'desc')->get();
      return response()->json(['posts' => $post], 200);  
   }

   public function store(Request $request)
   {
      $validated = $request->validate([
         'title' => 'required|unique:posts|max:255',
         'body' => 'required',
      ]);

      $post = new Post;  
      $post->title = $request->title;
      $post->body = $request->body; 
      $post->slug = Str::slug($post->title, '-'); 

      $post->save();

      return response()->json(['post' => $post], 201); 

   }

   public function update(Request $request, $slug)
   {
      $validated = $request->validate([
         'title' => 'required|max:255',
         'body' => 'required',
      ]);

      $post = Post::where('slug', $slug)->firstOrFail();
      $post->title = $request->title;
      $post->body = $request->body;  

      $post->save();

      return response()->json(['post' => $post], 201); 

   }

   public function destroy($slug){

      $post = Post::where('slug', $slug)->delete();
      return response()->json(['post' => 'deleted.'], 202); 
   }

}

Route Api

Kemudian tambahkan route berikut pada routes/api.php.

...
use App\Http\Controllers\PostController;

Route::get('/posts', [PostController::class, 'posts']);
Route::post('/post/store', [PostController::class, 'store']);
Route::post('/post/{slug}/edit',[PostController::class, 'update']); 
Route::delete('/post/{post:slug}', [PostController::class, 'destroy']);

Store: Action

Setelah api dibuat, kita lanjutkan membuat action store. Pada file actions.js, silahkan buat seperti dibawah ini.

import router from '../routes';
import store from '../store';

const actions = { 
  addPost({ commit }, value) {  
    axios.post('/api/post/store', value)
      .then(response => { 
        commit('SET_POST', response.data.post)    
        
        if(store.state.errors !== null){
          commit('SET_ERRORS', null)  
        } 
      
        router.replace({ name: 'posts' });

      }).catch(error => { 
        commit('SET_ERRORS', error.response.data.errors) 
      })
  },
  async getPosts({ commit }) { 
    const res = await axios.get('/api/posts') 
    commit('SET_POSTS', res.data.posts)
  }

}

export default actions

Kita buat permintaan POST ke api menggunakan axios. Axios ini tidak kita install manual karena sudah tersedia. lihat bootstrap.js.

Pada action diatas, kita buat 2 metode addPost dan getPosts. Pada metode addPost, kita meneruskan argumen yang diterima dan melakukan commit. Ada dua mutasi yang kita lakukan setelah menerima callback. Jika permintaan berhasil, kita commit mutasi tipe SET_POST dengan payload respon data yang diterima, sebaliknya jika terjadi error, kita lakukan mutasi dengan tipe SET_ERRORS.

Lalu pada metode getPosts, kita membuat permintaan GET dengan mutasi tipe SET_POSTS untuk semua data 'posts' yang akan ditampilkan.

Store: Mutations

Kita sudah menetapkan mutasi yang akan dilakukan. Untuk meneruskan argumen atau value ke state, kita perlu menetapkan handler. Pada mutations.js tambahkan di bawah ini.

const mutations = {
  SET_POSTS(state, value) {
    state.posts = value
  },
  SET_ERRORS (state, value) {
    state.errors = value
  },
  SET_POST(state, value) {
    state.posts.unshift(value)  
  },
}
export default mutations

Pada tipe SET_POST diatas, kita gunakan metode unshift untuk menambahkan atau memasukan data objek dari post baru yang di buat ke array pada state posts tanpa memembuat permintaan ulang ke endpoint.

Selain cara diatas, sebenarnya kita dapat melakukan dispatch, mamasukan metode atau action lain dalam satu action dan dijalankan dalam satu tindakan.

Perbedaannya adalah melakukan permintaan ulang ke api untuk data saat ini.

addPost({ commit, dispatch }, value) {  
 axios.post('/api/post/store', value)
  .then(response => { 
    dispatch('getPosts') 
   ...
}

Store: State

Kita lanjutkan untuk menambahkan properti state. Pada state.js buat seperti dibawah ini.

const state = {
  posts: [],
  errors: null
}

export default state

Html Form

Selanjutnya kita beralih ke komponen untuk membuat html form. Pada file create.vue, tambahkan html dengan data properti dan methods berikut.

<template>
  <div class="AddPost"> 
    <div class="form"> 
      <ul v-if="errors" class="err-msg">
          <li v-for="(error, index) in errors" :key="index">{{ error[0] }}</li>
      </ul>  
      <form @submit.prevent="submit">
        <div class="form-group"> 
          <label class="title" id="title" for="title">Title</label>
          <div>
            <input name="title" v-model="form.title" type="text" id="title" placeholder="Title..."> 
          </div>
        </div>
        <div class="form-group"> 
          <label class="body" id="body" for="body">Body</label>
            <div>
            <textarea name="body" v-model="form.body" id="body" placeholder="Body...">
            </textarea>
          </div>
        </div>  
        <button class="btn" type="submit">Submit</button> 
     </form>
    </div>
  </div>
</template>

<script>

import { mapActions, mapState } from "vuex";  

export default {   
  data() {
    return {
      form: {
        title: '',
        body: '', 
      }
    }
  },
  computed: { 
    /** helper */
  ...mapState({
        errors: 'errors',
    }),

    // errors (){ ;
    //    return this.$store.state.errors;
    // },
  },
  beforeRouteLeave (to, from , next) { 
    if(this.$store.state.errors !== null){  
      const answer = window.confirm('Yakin meninggalkan halaman?') 
      if (answer) {
        this.$store.commit('SET_ERRORS', null);
      } else{
          return
      }
    }  
      next() 
  }, 
  methods: { 
    async submit() {   
      await this.$store.dispatch('addPost', this.form)
    } 

    // ...mapActions(["addPost"]),
    //     async submit(){ 
    //       await this.addPost(this.form);  
    //    }, 
  }
    
}
</script>

<style scoped>
.form{
  border: 1px solid #ddd;
  padding: 22px;
  border-radius: 2px;
  background: #fff;
}
.form-group{
  background: #eff4f4;
  padding: 18px;
  border-radius: 2px;
  margin-bottom: 16px;
}
input[type=text], textarea {
  width: 100%;
  padding: 12px 20px;
  margin: 8px 0;
  display: inline-block;
  border: 1px solid #ccc;
  border-radius: 2px;
  box-sizing: border-box;
}

input[type=submit] {
  width: 100%;
  background-color: #4CAF50;
  color: white;
  padding: 14px 20px;
  margin: 8px 0;
  border: none;
  border-radius: 2px;
  cursor: pointer;
}

button{
  background: #8daaaa;
  padding: 10px 24px;
  border: none;
  border-radius: 2px;
  color: #fff;
  cursor: pointer;
}
.err-msg li{
  color: #ea6464;
  font-size: 18px;
}
</style>

Mari kita lihat komponen create.vue diatas. Terdapat dua cara berbeda dengan fungsi yang sama yang dapat kita gunakan membuat tindakan bekerja dengan store. Pertama kita dapat langsung mengakses state menggunkan this.$store, dan yang kedua menggunakan helper.

Properti this.$store ini dapat kita gunakan setelah kita lakukan injeksi Vuex, menambahkannya ke Vue Instance, yang mempermudah kita dalam mengakses state pada semua komponen tanpa melakukan import modul di setiap komponen.

import store from './store/index'

new Vue({
  store,
  ...
})

Sedangkan untuk helper sendiri dapat kita gunakan untuk mempermudah kita dalam menjalankan fungsi atau mengakses berapa properti state sekaligus dalam satu tindakan.

Terdapat beberapa helper yang bisa kita gunakan, diantaranya: mapState, mapGetters, mapMutations, dan mapActions.

Menampilkan Data

Selanjutnya kita tampilkan data pada komponen. Pada posts/index.vue, tambahkan dibawah ini (metode dan props).

<template>
  <div class="posts">
    ...

      <router-view :posts="posts"></router-view>
     
    ...
  </div>
</template> 

<script>
export default {  
  computed: {
    posts(){
      return this.$store.state.posts
    },
  },
  created() {
    this.getPosts();
  },
  methods: {
    getPosts() {
      this.$store.dispatch('getPosts');
    }
  },
}
</script>

<style scoped> ... </style>

Karena kita membangun nested component dengan vue-router, kita gunakan props untuk meneruskan data ke komponen anak. Pada posts/posts.vue, buat seperti dibawah ini.

<template> 
  <div class="posts">
    <div class="item" v-for="(post, index) in posts" :key="index">     
      <router-link class="title" :to="'/posts/' + post.slug">
        <h2>{{post.title}}</h2>
      </router-link>
      <div class="action"></div>
    </div> 
  </div>  
</template>

<script>
export default {
  props: ['posts'],  
}
</script>

<style scoped>
.item{
    padding: 10px 16px;
    border: 1px solid #dcdcdc;
    margin-bottom: 16px;
    background: #e4e3e6;
    border-radius: 2px;
}  
.item h2{
  color: #49545f;
  text-decoration: none; 
  margin: 0;
  line-height: 2;
}
.item h2:hover{ 
  color: #4a6ce8;
}
a {
  text-decoration: none;
}
.action a{
  margin-right: 10px;
  border: 1px solid #c7c7c7;
  padding: 2px 4px;
  border-radius: 2px;
  font-size: 14px;
  line-height: 2;
}
.action .edit{
  color: #2f8d4c;
}
.action .delete{
  color: #c55555;
}
</style>

Sampai disini tinggal mencobanya. Silahkan lakukan compiling script terlebih dahulu kemudian coba buat data baru.

//tab1
//kita gunakan 'watch' agar mix otomatis setiap ada perubahan
//dan tetap aktif

npm run watch
Belajar Vuex: CRUD Laravel + Vue + Vue Router dan Vuex

Store: Getters

Selanjutnya kita akan menggunakan getters, membuat fungsi untuk melakukan kueri array pada state, melakukan pencarian single objek untuk ditampilkan pada komponen dinamis.

Pada getters.js, tambahkan dibawah ini.

const getters = { 
  getPostBySlug: (state) => (slug) => {
    return state.posts.find(post => post.slug === slug)
  }
}

export default getters

Kemudian buat file baru pada folder posts dengan nama view.vue lalu copas template dibawah ini.

<template>
  <div class="post" v-if="post">
    <h1>{{post.title}}</h1>
    <hr>
    <p>{{post.body}}</p>
  </div>
</template>

<script>
export default { 
  computed: {
    post(){ 
      return this.$store.getters.getPostBySlug(this.$route.params.slug);
    }
  }, 
}
</script>

Terakhir, kita tambahkan dinamis route pada routes/index.js.

import View from '../pages/posts/view' 

children: [
  ...
  { 
    path: ':slug',   
    component: View 
  },
]

Simpan dan silahkan mencobanya.

Belajar Vuex: CRUD Laravel + Vue + Vue Router dan Vuex

Edit & Delete

Kita lanjutnya mengerjakan fungsi edit dan hapus data. Kita kerjakan dari membuat action. Pada actions.js, tambahkan dibawah ini.

//actions.js
editPost({ commit }, value) {   
  axios.post('/api/post/' + value.slug + '/edit', value.formData)
    .then(response => { 
      console.log(response);
      commit('SET_POST_UPDATED', response.data.post)    
      
      if(store.state.errors !== null){
        commit('SET_ERRORS', null)  
      } 
    
      router.replace({ name: 'posts' });

    }).catch(error => { 
      commit('SET_ERRORS', error.response.data.errors) 
    })
},

deletePost({ commit }, postSlug) { 
  axios.delete('/api/post/' + postSlug) 
    .then(res => { 
      commit('SET_POST_DELETED', postSlug) 
    }).catch(error => { 
      console.log(error)
    })
}

Kemudian pada mutations.js tambahkan dibawah ini.

import Vue from 'vue';

const mutations = {
  ...

  SET_POST_UPDATED(state, value) {
    const index = state.posts.findIndex(post => post.slug === value.slug);
    Vue.set(state.posts, index, value);    
  },
  SET_POST_DELETED(state, value) {
    const index = state.posts.findIndex(post => post.slug === value); 
    state.posts.splice(index, 1); 
  },

}

Kita melakukan pencarian elemen array pada state sesuai index berdasarkan nilai argumen. Pada SET_POST_UPDATED, kita melakukan pembaharuan atau mengatur ulang objek elemen array, danSET_POST_DELETED menghapus elemen dari daftar.

Selanjutnya kita buat komponen untuk halaman edit. Pada folder posts silahkan buat file baru dengan nama Edit.vue, kemudian copas template dari create.vue lalu buat seperti dibawah ini.

<template>
  <!-- copas form dari create.vue disini -->
</template>

<script>  
import { mapActions } from "vuex"; 

export default {  
  data() {
    return {
      form: {
        title: '',
        body: '', 
      }
    }
  },
  computed: { 
    errors (){
       return this.$store.state.errors;
    }, 
    post(){  
       return this.$store.getters.getPostBySlug(this.$route.params.slug); 
    }
  },   
  watch: {
   post: {
     immediate: true,
     handler(val) {  
       this.form =  {
         title: val.title,
         body: val.body
       }
     }
   }
  },
  beforeRouteLeave (to, from , next) { 
    if(this.$store.state.errors !== null){  
      const answer = window.confirm('Yakin meninggalkan halaman?') 
      if (answer) {
        this.$store.commit('SET_ERRORS', null);
      } else{
        return
      }
    }  
      next() 
  }, 
  methods: { 
   ...mapActions(["editPost"]),
      submit(){
        this.editPost({
          formData: this.form,
          slug: this.$route.params.slug, 
       });  
     } 
  }
    
}
</script>

<style scoped>
/* copas css dari create.vue  disini*/
</style>

Setelah itu tambahkan route untuk komponen edit pada routes/index.js.

...
import Edit from '../pages/posts/edit' 

children: [
 ...
  { 
    path: ':slug/edit',   
    component: Edit 
  },
]

Terakhir, tambahkan router link dan metode deletePost() dibawah ini pada posts.vue.

...
<div class="action">
  <router-link class="edit" :to="'/posts/' + post.slug + '/edit'">
    Edit
  </router-link>
  <a href="#" class="delete" @click="deletePost(post)">Delete</a> 
</div>
...
<script>
export default {
  props: ['posts'], 
  methods:{   
    deletePost(post){
      this.$store.dispatch('deletePost', post.slug); 
    }
  }
}
</script>

Simpan dan silahkan coba melakukan pengeditan dan panghapusan.

Selesai

Kita selesai sampai disini. CRUD sederhana menggunakan Vuex telah selesai dikerjakan dengan fungsi atau fitur yang ada. Kita telah mempelajari bagaimana kita dapat menggunakan penyimpanan 'store' terpusat untuk semua komponen dan melihat bagimana kita dapat memanipulasi state dan membuat tindakan yang bekerja dengan backend api.

Metode atau fitur-fitur yang ada pada Vuex mungkin ada yang tidak digunakan pada aplikasi CRUD sederhana ini, untuk melihat dan mempelajari fitur vuex lainnya, silahkan lihat pada situs Vuex: vuex.vuejs.org.

Semoga bermanfaat, silahkan dicoba dan dikembangkan.

Source Code

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel