VueJS-#15 PWA Vue Firebase

Persiapan

Buat Proyek vue-vuetify-pwa

vue create vue-vuetify-pwa
Pilih Manually
Pilih PWA

Install Vuetify Theme

vue add vuetify

Install Firebase into Vue Project

npm install firebase --save

Buat Project di FireBase

Sebelum memulai membuat Program, kita akan menyiapkan Proyek di Firebase, masuk ke Firebase Console buat Proyek baru, setelah selesai masuk ke Menu Project Overview => Setelan proyek

Setelan Proyek

Menambahkan Firebase ke Aplikasi Web

Parameter Pengaturan Firebase

Sekarang kita akan membuat setingan untuk firebase di Program Vue kita sesuaikan dengan gambar di atas. Buka main.js tambahkan baris berikut diantara import dan Vue.config.productionTip = false;

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "./registerServiceWorker";
import vuetify from './plugins/vuetify';
import firebase from 'firebase/app';
import '@firebase/messaging';

const firebaseConfig = {
    apiKey: "AIzaxxxxxxxxxxxxxxxxxxxxx_xxxxxM3tRDNPc",
    authDomain: "vue-firebase-xxxxx.firebaseapp.com",
    databaseURL: "https://vue-firebase-xxxxx.firebaseio.com",
    projectId: "vue-firebase-xxxxx",
    storageBucket: "vue-firebase-xxxxx.appspot.com",
    messagingSenderId: "8142xxxxx626",
    appId: "1:8142xxxxx626:web:a313b9d16f14abb0fdb383"
};
firebase.initializeApp(firebaseConfig);

const messaging = firebase.messaging();

messaging.usePublicVapidKey("BASZxxxxxxxxxxxxxxx2tlVbCLpQ-F1nlfv9J0nOLF6457jgltDDLt-xxxxxxxxxxxxxx-K7NaBcCJqHbuPwf-o"); // 1. Generate a new key pair

// Request Permission of Notifications
messaging.requestPermission().then(() => {
  console.log('Notification permission granted.');

  // Get Token
  messaging.getToken().then((token) => {
    console.log(token)
  })
}).catch((err) => {
  console.log('Unable to get permission to notify.', err);
});

messaging.onMessage((payload) => {
    console.log('Message received. ', payload);
    // ...
});

Vue.config.productionTip = false;

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

npm install firebase --save

Menyiapkan Route

router.js

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Login from "./views/Login.vue";
import Profile from "./views/Profile.vue";
import Todos from "@/views/Todos";
import store from "./store.js";

Vue.use(Router);

let router =  new Router({
    mode: "history",
    base: process.env.BASE_URL,
    routes: [
        {
            path: "/home",
            name: "Home",
            component: Home,
            meta: {
                auth: true
            }
        },
        {
            path: '/about',
            name: 'about',
            component: () => import('./views/About.vue'),
            meta: {
                auth: true
            }
        },
        {
            path: "/",
            name: "Login",
            component: Login,
            meta: {
                guest: true
            }
        },
        {
            path: "/profile",
            name: "Profile",
            component: Profile,
            meta: {
                //auth: true
            }
        },
        {
            path: '/todo',
            name: 'Todos',
            component: Todos,
            meta: {
                auth: true
            }
        }
    ]
});
router.beforeEach((to, from, next) => {
    if (to.matched.some(record => record.meta.auth)) {
        if (store.state.isLogin) {
            next();
        } else {
            next({
                path: "/",
            });
        }
    } else if (to.matched.some(record => record.meta.guest)) {
        if (store.state.isLogin) {
            next({
                path: "/profile",
            });
        } else {
            next();
        }
    } else {
        next();
    }
});
export default router;

Menyiapkan Store

Kita perlu menyiapkan store untuk menyimpan status login & data user yang nantinya akan ditampilkan pada component Profile.vue

import Vue from "vue";
import Vuex from "vuex";
import todo from './modules/todo';

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        isLogin: false,
        user:null,
    },
    mutations: {
        resetState (state) {
            state.isLogin=false;
            state.user=null;
        }
    },
    actions: {
        clearState ({ commit }) {
            commit('resetState');
        },
    },
    modules: {
        todo,
    }
});

Login component menggunakan FirebaseUI

FirebaseUI adalah library yang dibuat sebagai tambahan dari Firebase Authentication SDK yang menyediakan alur UI drop-in untuk digunakan dalam aplikasi Anda. Baca penjelasannya disini.

Sekarang kita akan menambahkan FirebaseUI di program Vue kita

npm install firebaseui --save

Karena kita akan login menggunakan gmail atau email, maka kita harus mengaktifkan options Authentication pada Firebase Console. Masuk ke Menu Authentication=>Pilih Tab Metode Login lalu aktifkan pilihan Email & Google

Metode Login

Setelah itu kita buat component Login.vue yang nantinya akan menampilkan halaman login.

<template>
    <div>
        <v-container>
                <v-row>
                    <v-col cols="2" md="4"></v-col>
                    <v-col cols="8" md="4">
                        <br/><br/>
                        <v-sheet class="text-center"
                            elevation="3"
                        >
                            <br/><br/>
                            <section id="firebaseui-auth-container"></section>
                            <br/><br/>
                        </v-sheet>
                    </v-col>
                    <v-col cols="2" md="4"></v-col>
                </v-row>
        </v-container>
    </div>
</template>

<script>
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/messaging';
import * as firebaseui from "firebaseui";
import "firebaseui/dist/firebaseui.css";
export default {
    name: "Login",
    data() {
        return {};
    },
    created() {
        firebase.auth().onAuthStateChanged(user => {
            if (user) {
                this.$store.state.isLogin=true;
                this.$store.state.user=user;
                this.user=this.$store.state.user;
                this.$router.push('/profile');
            }
        });
    },
    mounted() {
        let ui = firebaseui.auth.AuthUI.getInstance();
        if (!ui) {
            ui = new firebaseui.auth.AuthUI(firebase.auth());
        }
        var uiConfig = {
            signInSuccessUrl: "/profile",
            signInFlow: "popup",
            signInOptions: [
                //firebase.auth.FacebookAuthProvider.PROVIDER_ID,
                firebase.auth.GoogleAuthProvider.PROVIDER_ID,
                firebase.auth.EmailAuthProvider.PROVIDER_ID
            ]
        };
        ui.start("#firebaseui-auth-container", uiConfig);
    }
};
</script>

<style>
</style>

Perhatikan baris signInSuccessUrl: “/profile”, Jika login berhasil route akan diarahkan ke route /profile.

Buat NavBar

Selanjutnya kita akan membuat sebuah component NavBar.vue yang nantinya bisa diakses di setiap component jika dalam posisi login

components/NavBar.vue

<template>
    <section>
        <v-navigation-drawer v-model="drawer" app>
        <v-list-item>
            <v-list-item-content>
            <v-list-item-title class="title">
                Application Name
            </v-list-item-title>
            <v-list-item-subtitle>
                subtext
            </v-list-item-subtitle>
            </v-list-item-content>
        </v-list-item>
        <v-divider></v-divider>
        <v-list dense nav>
            <v-list-item to="/Home">
                <v-list-item-action>
                    <v-icon>mdi-home</v-icon>
                </v-list-item-action>
                <v-list-item-content>
                    <v-list-item-title>Home</v-list-item-title>
                </v-list-item-content>
            </v-list-item>
            <v-list-item  to="/About">
                <v-list-item-action>
                    <v-icon>mdi-help-circle</v-icon>
                </v-list-item-action>
                <v-list-item-content>
                    <v-list-item-title>About</v-list-item-title>
                </v-list-item-content>
            </v-list-item>
            <v-list-item  to="/ToDo">
                <v-list-item-action>
                    <v-icon>mdi-clipboard-list-outline</v-icon>
                </v-list-item-action>
                <v-list-item-content>
                    <v-list-item-title>ToDo List</v-list-item-title>
                </v-list-item-content>
            </v-list-item>
        </v-list>
        <v-divider></v-divider>
        <v-list dense nav>
            <v-list-item  to="/Messages">
                <v-list-item-action>
                    <v-badge>
                        <template v-slot:badge>0</template>
                        <v-icon>mdi-email</v-icon>
                    </v-badge>
                </v-list-item-action>
                <v-list-item-content>
                    <v-list-item-title>Messages</v-list-item-title>
                </v-list-item-content>
            </v-list-item>
        </v-list>
        <template v-slot:append>
            <div class="pa-2">
                <v-btn @click="signoutButtonPressed" color="red darken-4 white--text" block>Logout</v-btn>
            </div>
        </template>
        </v-navigation-drawer>

        <v-app-bar app color="blue darken-4" dark>
            <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
            <v-toolbar-title>Application</v-toolbar-title>
            <div class="flex-grow-1"></div>

            <v-btn icon>
                <v-icon>mdi-heart</v-icon>
            </v-btn>
            <v-menu left bottom>
                <template v-slot:activator="{ on }">
                <v-btn icon v-on="on">
                    <v-icon>mdi-dots-vertical</v-icon>
                </v-btn>
                </template>

                <v-list>
                    <v-list-item  to="/Profile">
                        <v-list-item-action>
                            <v-icon>mdi-account-circle</v-icon>
                        </v-list-item-action>
                        <v-list-item-content>
                            <v-list-item-title>Profile</v-list-item-title>
                        </v-list-item-content>
                    </v-list-item>
                    <v-list-item  to="/Setting">
                        <v-list-item-action>
                            <v-icon>mdi-settings</v-icon>
                        </v-list-item-action>
                        <v-list-item-content>
                            <v-list-item-title>Setting</v-list-item-title>
                        </v-list-item-content>
                    </v-list-item>
                </v-list>
            </v-menu>
        </v-app-bar>
    </section>
</template>
<script>
import firebase from 'firebase/app';
import 'firebase/auth';
export default {
    props: {
      source: String,
    },
    data: () => ({
      drawer: null,
    }),
    created() {
        firebase.auth().onAuthStateChanged(user => {
            if (!user&amp;&amp;this.$route.name!='Login') {
                this.$router.push('/');
            }
        });
    },
    methods: {
        signoutButtonPressed(e) {
            e.stopPropagation();
            firebase.auth().signOut();
            this.$store.dispatch('clearState');
        }
    },
};
</script>

Buat Layout

Buat layout pada App.vue

<template>
  <v-app teal>
    <NavBar v-if="getStatusLogin"/>
    <v-content>
      <router-view />
    </v-content>
    <v-footer color="blue darken-4" app>
      <span class="white--text">© 2019</span>
    </v-footer>
  </v-app>
</template>

<script>
import NavBar from "./components/NavBar";
import firebase from 'firebase/app';
import 'firebase/auth';
export default {
    name: "App",
    components: {
        NavBar
    },
    data: () => ({
        //
    }),
    computed: {
        getStatusLogin: function () {
            console.log(this.$store.state.isLogin);
            return this.$store.state.isLogin;
        }
    }
};
</script>

perhatikan baris <NavBar v-if=”getStatusLogin”/> component ini hanya akan tampil jika posisi user login dimana getStatusLogin diambil dari store.

Buat View

Views/Profile.vue

<template>
    <section>
        <v-container class="pa-2" fluid>
            <v-row>
                <v-col>
                    <v-card color="#385F73" dark>
                        <v-card-text v-if="user" class="white--text">
                            <v-avatar><img :src="user.photoURL" alt="avatar"></v-avatar>
                            <div class="headline mb-2" v-if="user">{{user.displayName}}</div>
                            <p>
                                name:<strong>{{user.displayName}}</strong>
                                <br />email:
                                <strong>{{user.email}}</strong>
                                <br />uid:
                                <strong>{{user.uid}}</strong>
                                <br />provider:
                                <strong class="teal-text">{{user.providerData[0].providerId}}</strong>
                            </p>
                        </v-card-text>

                        <v-card-actions>
                            <v-btn text>Listen Now</v-btn>
                        </v-card-actions>
                    </v-card>
                </v-col>
            </v-row>
        </v-container>
    </section>
</template>

<script>
import firebase from 'firebase/app';
import 'firebase/auth';
export default {
    data: () => ({
        user:null,
        result:null,
        errors:null
    }),
    created() {
        firebase.auth().onAuthStateChanged(user => {
            if (user) {
                this.$store.state.isLogin=true;
                this.$store.state.user=user;
                this.user=this.$store.state.user;
            }
        });
    },
}
</script>

Initialize a Firebase project

Untuk menginisialisasi bahwa proyek ini menggunakan firebase

firebase init

Sehingga struktur direktori akan menjadi sbb:

Proyek akan di build di direktori dist

Setting Firebase Message

Tambahkan file firebase-messaging-sw.js pada folder public, isi program seperti di bawah ini:

// [START initialize_firebase_in_sw]
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/6.3.4/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/6.3.4/firebase-messaging.js');
// Initialize the Firebase app in the service worker by passing in the messagingSenderId.
firebase.initializeApp({
  'messagingSenderId': '81422xxxxx26' // 4. Get Firebase Configuration
});

// Retrieve an instance of Firebase Messaging so that it can handle background messages.
const messaging = firebase.messaging();
// [END initialize_firebase_in_sw]
messaging.setBackgroundMessageHandler(function(payload) {
    console.log('[firebase-messaging-sw.js] Received background message ', payload);
    // Customize notification here
    const notificationTitle = 'Background Message Title';
    const notificationOptions = {
      body: 'Background Message body.',
      icon: ''
    };
  
    return self.registration.showNotification(notificationTitle,
      notificationOptions);
});

Sesuaikan messagingSenderId dengan Firebase anda.

Build Proyek

PWA hanya akan berjalan di server distribution, build proyek anda:

npm run build

Tambahkan redirect

Tambahkan baris berikut pada router.js

{
    path: "/index.html",
    redirect: "/"
}

Deploy

firebase deploy

Dapatkan Token Firebase

Test Send Notification

sumber:

  1. https://medium.com/the-web-tub/creating-your-first-vue-js-pwa-project-22f7c552fb34
  2. https://medium.com/@eder.ramirez87/modern-pwa-with-vue-cli-3-vuetify-firestore-workbox-part-1-974383be5540

VueJS-#14 Publish to Webserver

Buat Config

Buat file vue.config.js di Path Proyek “/”, dengan isi seperti di bawah ini :

// vue.config.js
module.exports = {
    publicPath: process.env.NODE_ENV === 'production'
    ? '/dist/'
    : '/'
}

Artinya jika di build maka path = /dist/, akan berpengaruh saat diupload ke htdocs Apache

npm run build
After Build

Copy folder dist ke htdocs/dist, jalankan apache lalu buka page di browser.

VueJs-#13 Vue Cloud FireStore

Pendahuluan

Pada tutorial sebelumnya kita membuat Vue Login menggunakan FirebaseUI, sekarang kita akan membuat ToDoList Page dan menyimpannya ke dalam FireStore.

Kita akan menggunakan proyek tutorial sebelumnya dan menambahkan ToDoList Page.

Persiapan

  1. Buat Proyek Vue Login menggunakan FirebaseUI

Membuat Page ToDoList

Siapkan Route, edit router.js, tambahkan baris berikut :

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Login from "./components/Login.vue";
import Profile from "./components/Profile.vue";
import Todos from '@/components/Todos'

import firebase from 'firebase/app';
import 'firebase/auth';

Vue.use(Router);

let router =  new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
        path: "/home",
        name: "Home",
        component: Home,
        meta: {
            auth: true
        }
    },
    {
        path: '/about',
        name: 'about',
        component: () => import('./views/About.vue'),
        meta: {
            auth: true
        }
    },
    {
        path: "/",
        name: "Login",
        component: Login,
        meta: {
            guest: true
        }
    },
    {
        path: "/profile",
        name: "Profile",
        component: Profile,
        meta: {
            auth: true
        }
    },
    {
        path: '/todo',
        name: 'Todos',
        component: Todos,
        meta: {
            auth: true
        }
    }
  ]
});
router.beforeEach((to, from, next) => {
    if (to.matched.some(record => record.meta.auth)) {
      firebase.auth().onAuthStateChanged(user => {
        if (user) {
          next()
        } else {
          next({
            path: "/",
          })
        }
      })
    } else if (to.matched.some(record => record.meta.guest)) {
      firebase.auth().onAuthStateChanged(user => {
        if (user) {
          next({
            path: "/profile",
          })
        } else {
          next()
        }
      })
    } else {
      next()
    }
  })
  export default router

Buat Component, components/Todos.vue

<template>
    <div>
        <b-card no-body style="max-width: 20rem;margin:auto;" class="mt-5 text-center">
            <template v-slot:header>
                <h4 class="mb-0">To-Dos</h4>
            </template>
            <b-card-body>
                 <b-input-group prepend="To Do" class="mt-3">
                    <b-form-input id="new_todo" type="text" class="validate" v-model="todo.title"></b-form-input>
                    <b-input-group-append>
                        <b-button variant="outline-success" @click="addTodo">Add</b-button>
                    </b-input-group-append>
                </b-input-group>
            </b-card-body>
            <b-list-group flush>
                <b-list-group-item class="d-flex justify-content-between align-items-center"
                    v-for="todo in todos"
                    :key="todo.id"
                    :class="{ fade: todo.isCompleted }"
                >
                    <b-button variant="danger" size="sm" class="deleteIcon" @click="deleteToDo(todo.id)">X</b-button>
                    {{todo.title}}
                    <b-form-checkbox
                        type="checkbox"
                        :checked="todo.isCompleted"
                        @change="updateTodoItem(todo.id, $event)"
                        switch
                    >
                    </b-form-checkbox>
                </b-list-group-item>
            </b-list-group>
        </b-card>
    </div>
</template>
<script>
import firebase from 'firebase/app';
import 'firebase/firestore';
export default {
    data() {
        return {
            todos: [],
            todo: {
                title: ""
            }
        };
    },
    created() {
        this.getTodos();
    },
    methods: {
        addTodo() {
            firebase
                .firestore()
                .collection("users")
                .doc(firebase.auth().currentUser.uid)
                .collection("todos")
                .add({
                    title: this.todo.title,
                    createdAt: new Date(),
                    isCompleted: false
                });
        },
        async getTodos() {
            var todosRef = await firebase
                .firestore()
                .collection("users")
                .doc(firebase.auth().currentUser.uid)
                .collection("todos");
            todosRef.onSnapshot(snap => {
                this.todos = [];
                snap.forEach(doc => {
                    var todo = doc.data();
                    todo.id = doc.id;
                    this.todos.push(todo);
                });
            });
        },
        updateTodoItem(docId, e) {
            var isChecked = e;
            firebase
                .firestore()
                .collection("users")
                .doc(firebase.auth().currentUser.uid)
                .collection("todos")
                .doc(docId)
                .update({
                    isCompleted: isChecked
                });
        },
        deleteToDo(docId) {
            firebase
                .firestore()
                .collection("users")
                .doc(firebase.auth().currentUser.uid)
                .collection("todos")
                .doc(docId)
                .delete();
        }
    }
};
</script>
<style>
.fade {
    opacity: 0.4 !important;Jala
}
.collection.with-header {
    max-width: 500px;
    margin: 0 auto;
}
.deleteIcon {
    margin-right: 10px;
    cursor: pointer;
}
.deleteIcon:hover {
    opacity: 0.5;
}
</style>

Jalankan program, masuk menu “ToDo” :

ToDos
Struktur Cloud Firestore

ECMA Script

ECMA : European Computer Manufacturers Association yaitu asosiasi pabrik industri di Eropa, kini dikenal dengan nama ECMA Internasional, merupakan asosiasi industri untuk standardisasi sistem informasi dan komunikasi.

ECMA Script adalah sebuah standart bahasa, gampangnya javascript mengikuti standart ini

JavaScript = ECMAScript + DOM API;

ECMAScript® Language Specification membrikan semua logika untuk membuat dan mengedit objects, arrays, numbers, dan sebagainya.

DOM API membuat bisa berkomunikasi dengan  HTML/XML documents (contoh document.getElementById('id');).

berikut ini link ECMA terbaru http://www.ecma-international.org/publications/standards/Ecma-262.htm

kembali kebelakang.

Sejarah Java script yang begitu panjang tidak bisa dilepaskan oleh netscape. Tahun 1994 website pada saat itu umumnya dibuat menggunakan bahasa pemograman PERL yang pemrosesannya hanya bisa dilakukan di sisi web server sehingga tidak efisien.

Tahun 1995, Brendan Eich dari Netscape mengembangkan bahasa pemograman script yang dinamakan Mocha. Mocha diubah menjadi LiveScript untuk versi client-side, dan LiveWire untuk versi server-side.

Munculnya JavaScript saat Netscape mengadakan kerjasama dengan Sun Microsystems untuk mengembangkan LiveScript,  Versi JavaScript ini dinamakan dengan JavaScript 1.0.kemudian microsoft menyaninginya dengan JScript pada browser Internet Explorer mereka

Implementasi JScript di dalam Internet Explorer membuat kalangan programmer bingung, karena terdapat 2 versi JavaScript: JavaScript di Netscape Navigator andJScript pada Internet Explorer. Versi JavaScript juga memiliki 2 versi, yakni versi 1.0 dan 1.1. Hal ini semakin menambah kerumitan dalam pembuatan program. Permasalahan terjadi karena ketiga versi JavaScript tersebut memiliki perbedaan fitur.

Pada pertengahan tahun 1997, JavaScript 1.1 diajukan ke badan standarisasi Eropa: European Computer Manufacturers Association (ECMA) untuk membuat sebuah standar bahasa pemograman script web browser. Atas dasar ini, dibentuklah sebuah komite dengan anggota yang terdiri dari programmer dari berbagai perusahaan internet pada saat itu, seperti Netscape, Sun, Microsoft, Borland, NOMBAS serta beberapa perusahaan lain yang tertarik dengan perkembangan JavaScript.

standardisasi dimulai sejak November 1996, dan diadopsi mulai Juni 1997 oleh ECMA, dan ISO ECMAScript keluar pada April 1998.

Komite standarisasi ini menghasilkan bahasa pemograman yang disebutECMAScript, atau secara formal disebut ECMAScript -262. 1 tahun berikutnya, badan standarisasi ISO (International Organization for Standardization) juga mengadopsi ECMAScript sebagai standar. Sejak saat itu, semua web browser menjadikan ECMAScript sebagai standar acuan untuk JavaScript.

ECMAScript engine adalah program untuk mengeksekusi program yang ditulis dalam ECMAScript contohnya JavaScript.

ada banyak Engine ECMA  seperti v8 untuk chrome, chakra untuk IE, Spider Monkey untuk Firefox

VueJs-#08c Vuex Module & Helper

Pendahuluan

Membuat sebuah aplikasi tidak hanya sebatas dapat membuatnya bekerja sesuai yang kita inginkan, melainkan juga memikirkan kemungkinan perubahan dan penambahan fitur dari aplikasi tersebut. Nah, apa jadinya, jika aplikasi kamu sudah cukup kompleks dan beberapa bulan kemudian kamu ingin melakukan perubahan, tapi justru membuatmu kebingungan?

Salah satu fitur lainnya dari Vuex, selain memudahkan kamu dalam manajemen state agar dapat digunakan oleh semua component yang berperan, juga memungkinkan kamu untuk memecah block code yang telah dibuat kedalam Module sesuai dengan peruntukannya masing-masing. Misalnya, kita memiliki 3 buah fitur, yakni: Donatur, Bantuan dan Transaksi. Jika ketiga code untuk meng-handle fitur tersebut disatukan dalam satu buah file, maka mengelolanya akan sangat membingungkan dikemudian hari. Bagaimana tidak? Jika code dari masing-masing fitur saling tercampur baur satu sama lain.

Tahap Persiapan

Meskipun artikel ini merupakan lanjutan dari seri sebelumnya, tapi kita akan memulainya dengan case yang berbeda, yakni: membuat aplikasi donasi. Buat project fresh install terlebih dahulu:

npm create donasi

Pilih Manually select feature, lalu ceklis pilihan berikut :

Masuk ke direktori donasi, install bootstrapVue

cd donasi
vue add bootstrap-vue

Menyiapkan Component Helper

Setelah selesai buka editor, kita akan membuat 2 buah component helper, Donatur & Bantuan.

components/Donatur.vue

<template>
    <div>
        <div class="form-group">
            <label for="">Donatur</label>
            <select @change="$emit('selectedDonatur', $event.target.value)" class="form-control">
                <option value="">Pilih Donatur</option>
                <option v-for="donatur in listDonatur" :key="donatur.id" :value="donatur.id">
                    {{ donatur.name }}
                </option>
            </select>
        </div>
    </div>
</template>
<script>
    export default {
        computed: {
            //MEMBUAT COMPUTED PROPERTY DENGAN NAMA listDonatur()
            listDonatur() {
                //DATA DIAMBIL DARI STATE MODULE donatur
                return this.$store.state.donatur.listDonatur
            }
        }
    }
</script>

components/Bantuan.vue

<template>
    <div>
        <div class="form-group">
            <label for="">Jenis Bantuan</label>
            <select @change="$emit('selectedBantuan', $event.target.value)" class="form-control">
                <option value="">Pilih Bantuan</option>
                <option v-for="row in listBantuan" :key="row.id" :value="row.id">
                    {{ row.name }}
                </option>
            </select>
        </div>
    </div>
</template>
<script>
export default {
    computed: {
        //MEMBUAT COMPUTED PROPERTY DENGAN NAMA listBantuan()
        listBantuan() {
            //DATA DIAMBIL DARI STATE MODULE clients
            return this.$store.state.bantuan.listBantuan
        }
    }
}
</script>

Menyiapkan State Module untuk Component Helper

Selanjutnya kita akan membuat state module untuk kedua component di atas.

modules/donatur.js

const donatur = {
    namespaced: true,
    state: {
        listDonatur: [
            { id:'1', name: 'Riski Amelia' },
            { id:'2', name: 'Ima Sumadir' },
            { id:'3', name: 'Apri Yunita' },
            { id:'4', name: 'Tuti Winarti' }
        ]
    },
    getters: {
        getNameById: (state) => (id) => {
            return state.listDonatur.find((obj) => {
                return obj.id===id
            })
        }
    },
    mutations: {
        // NONE
    },
    actions: {
        // NONE
    }
}

export default donatur

modules/bantuan.js

const bantuan = {
    namespaced: true,
    state: {
        listBantuan: [
            { id:"1", name: 'Gempa Lombok', },
            { id:"2", name: 'Beasiswa Pendidikan' },
            { id:"3", name: 'Banjir Bandang' },
            { id:"4", name: 'Tanggungan Kesehatan' }
        ]
    },
    getters: {
        getBantuanById: (state) => (id) => {
            return state.listBantuan.find((obj) => {
                return obj.id===id
            })
        }
    },
    mutations: {
        // NONE
    },
    actions: {
        // NONE
    }
}

export default bantuan

Sebelum ke tahap selanjutnya, saya akan mengajak anda memahami kedua component helper di atas yaitu Donatur.vue dan Bantuan.vue dimana keduanya hanya berisi template select form yang berisi data master dari dua state module yaitu donatur.js dan bantuan.js.

Menyiapkan Component View

Selanjutnya kita akan membuat View Donasi.vue yang berisi component FormDonasi.vue untuk manambahkan transaksi & component Transaksi untuk menampilkan data transaksi.

views/Donasi.vue

<template>
  <div class="home">
    <div class="container" style="padding-top: 20px">
        <div class="row">
          <div class="col-md-6">
            <FormDonasi/>
          </div>
          <div class="col-md-6">
            <Transaksi/>
          </div>
        </div>
    </div>
  </div>
</template>

<script>
import FormDonasi from "@/components/FormDonasi.vue";
import Transaksi from "@/components/Transaksi.vue";

export default {
  name: "home",
  components: {
    FormDonasi,
    Transaksi
  }
};
</script>

component/FormDonasi.vue

<template>      
    <div class="card">
        <div class="card-header">
            <h3 class="card-title">Form Donasi</h3>
            <!--<h4 v-if="typeof donatur_row === 'object'">{{donatur_row.name}}</h4>
            <h4 v-if="typeof bantuan_row === 'object'">{{bantuan_row.name}}</h4>-->
        </div>
        <div class="card-body">
            <form @submit.prevent="formSubmit">
                <list-donatur @selectedDonatur="selectedDonatur" />
                <lokasi-bantuan @selectedBantuan="selectedBantuan" />
                <div class="form-group">
                    <label for="">Jumlah Donasi (Rp)</label>
                    <input type="number" v-model="jumlah" class="form-control">
                </div>
                <div class="form-group">
                    <button class="btn btn-primary btn-sm" :disabled="isLoading">
                        {{ isLoading ? 'Loading...':'Donasi!' }}
                    </button>
                </div>
            </form>
        </div>
    </div>
</template>

<script>
import Donatur from "@/components/Donatur.vue";
import Clients from "@/components/Bantuan.vue";

export default {
    components: {
        'list-donatur': Donatur,
        'lokasi-bantuan': Clients,
    },
    data() {
        return {
            donatur: '',
            bantuan: '',
            jumlah: 0
        }
    },
    computed: {
        donatur_row: function () {
            //MENGAMBIL NAMA DONATUR BERDASARKAN ID MENGGUNAKAN GETTERS
            return this.$store.getters['donatur/getNameById'](this.donatur);
        },
        bantuan_row: function () {
            //MENGAMBIL NAMA BANTUAN BERDASARKAN ID MENGGUNAKAN GETTERS
            return this.$store.getters['bantuan/getBantuanById'](this.bantuan);
        },
        isLoading() {
            //MENGAMBIL STATE DARI ROOT VUEX (store.js)
            //JADI, SETELAH .state. LANGSUNG MENYEBUTKAN NAMA STATENYA
            //KARENA TIDAK MASUK KEDALAM MODULE
            return this.$store.state.isLoading
        }
    },
    methods: {
        //HANDLE EMIT DARI COMPONENT DONATUR
        selectedDonatur(val) {
            this.donatur = val;
        },
        //HANDLE EMIT DARI COMPONENT CLIENTS
        selectedBantuan(val) {
            this.bantuan = val
        },
        formSubmit(event) {
            //MENGUBAH ID MENJADI NAMA
            this.donatur= typeof this.donatur_row === 'object'?this.donatur_row.name:'0';
            this.bantuan= typeof this.bantuan_row === 'object'?this.bantuan_row.name:'0';
            this.$store.dispatch('transaksi/save_donasi', {
                //MENGIRIM 4 BUAH PARAMETER YANG AKAN DIPUSH KEDALAM LISTS ARRAY listTransaksi
                id: Math.random().toString(36).substring(7),
                donatur: this.donatur,
                bantuan: this.bantuan,
                jumlah: this.jumlah
            });
            this.jumlah="0";
            event.target.reset();
        }
    }
}
</script>

Penjelasan mengenai

component/Transaksi.vue

<template>
    <div class="card">
        <div class="card-header">
            <h3 class="card-title">Donasi Terkumpul</h3>
        </div>
        <div class="card-body">         
            <div class="table-responsive">
                <table class="table table-hover table-bordered">
                    <thead>
                        <tr>
                            <th>#</th>
                            <th>Donatur</th>
                            <th>Jenis Bantuan</th>
                            <th>Jumlah</th>
                        </tr>
                    </thead>
                    <tbody>
                    <!-- LOOPING LIST ARRAY DARI listTransaksi -->
                    <tr v-for="(row, index) in listTransaksi" :key="index">
                        <td>{{ row.id }}</td>
                        <td>{{ row.donatur }}</td>
                        <td>{{ row.bantuan }}</td>
                        <td>{{ row.jumlah }}</td>
                    </tr>
                    <tr>
                        <td>&nbsp;</td>
                        <td>&nbsp;</td>
                        <td>&nbsp;</td>
                        <td>{{ total }}</td>
                    </tr>
                </tbody>
                </table>
            </div>
        </div>
    </div>
</template>
<script>
import { mapState } from 'vuex' //IMPORT mapState
export default {
    computed: {
        //MENGGUNAKAN HELPER mapState UNTUK MEMANGGIL MODULE transaksi
        ...mapState('transaksi', {
            // DIMANA DATA YANG AKAN DIAMBIL ADALAH STATE listTransaksi
            listTransaksi: state => state.listTransaksi,
        }),
        total() {
            return this.listTransaksi.map(item => item.jumlah).reduce((total, amount) => total + parseInt(amount));
        }
    }
}
</script>

Menyiapkan State Module untuk Transaksi

Karena Transaksi.vue merupakan sebuat data maka kita juga akan state module untuk component transaksi yaitu modules/transaksi.js

modules/transaksi.js

const transaksi = {
    namespaced: true,
    state: {
        //DEFAULT DATA TRANSAKSI YANG AKAN DITAMPILKAN
        //PADA COMPONENT TRANSAKSI.VUE
        listTransaksi: [
            { id: 'TRX1P1', donatur: 'Anugrah Sandi', bantuan: 'Gempa Lombok', jumlah: 100000 },
            { id: 'TRX1P2', donatur: 'Dharma', bantuan: 'Banjir Bandang', jumlah: 250000 },
            { id: 'TRX1P3', donatur: 'Asis Ramadhan', bantuan: 'Beasiswa Pendidikan', jumlah: 3000000 }
        ]
    },
    mutations: {
        //MENGUBAH STATE DENGAN
        ADD_DONASI: (state, payload) => {
            //MENAMBAHKAN DATA BARU KEDALAM ARRAY MENGGUNAKAN PUSH()
            state.listTransaksi.push(payload);
        }
    },
    actions: {
        save_donasi({ commit, rootState }, payload) {
            // rootState BERARTI MENGAKSES STATE YANG TIDAK BERADA DALAM MODULES
            // DALAM HAL INI STATE isLoading YANG ADA DI DALAM FILE store.js
            rootState.isLoading = true //SET TRUE UNTUK MEMBERIKAN EFEK LOADING
            setTimeout(() => {
                //MENGINSTRUKSIKAN PADA MUTATIONS TERKAIT UNTUK MENJALANKAN INSTRUKSINYA
                commit('ADD_DONASI', payload)
                // STATE isLoading DI MATIKAN KEMBALI
                rootState.isLoading = false
            }, 1000)
        }
    }
}

export default transaksi

Menambahkan Menu/Link menuju Donasi.vue

Setelah semuanya siap kita akan membuat Link Menu untuk menuju View Donasi dengan cara menambahkan link pada App.vue

src/App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/donasi">Donasi</router-link>
    </div>
    <router-view />
  </div>
</template>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

Menambahkan Router

Buka file src/router.js lalu tambahkan route berikut :

{
    path: "/donasi",
    name: "Donasi",
    component: () =>
    import("./views/Donasi.vue")
}

Implementasi

Jalankan program dengan perintah berikut :

npm run serve

Hasilnya sbb :

Klik Link Donasi

Donasi

Isi Form lalu ketik tombol Donasi

After Insert

Struktur Direktori

Struktur Direktori

VueJs-#08b Vuex Mutations & Actions

Pendahuluan

Melanjutkan seri Tutorial Vuex, pada artikel sebelumnya telah dibahas bagaimana menggunakan state agar dapat digunakan oleh seluruh component yang ada. Maka pada artikel kali ini kita akan membahas bagaimana menggunakan Mutations dan Actions untuk mengelola dan melakukan perubahan data pada state.

Mutations bertugas untuk mengkonfirmasi setiap perubahan state yang akan dilakukan

Actions bertugas untuk memberikan perintah terhadap apa yang akan dilakukan. Misalnya saja, untuk melakukan perubahan data listAgenda, maka kita akan menyusun data yang akan di instruksikan ke Mutations agar melakukan perubahan pada state menggunakan perintah commit, meskipun kita dapat melakukan perubahan state secara langsung pada actions yang kita buat.

Persiapan

Pada tutorial VueJs State sebelumnya kita sudah membuat page dan component untuk menampilkan Agenda yang bernama AgendaShow. Sekarang kita buat sebuah component untuk menambahkan data bernama AgendaInput.vue berikut isi filenya:

<template>
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title">{{postTitle}}</h3>
            </div>
            <div class="card-body">
                <div class="form-group">
                    <label for="">Hari</label>
                    <input type="text" v-model="agenda.hari" class="form-control" required>
                </div>
                <div class="form-group">
                    <label for="">Kegiatan</label>
                    <input type="text" v-model="agenda.kegiatan" class="form-control" required>
                </div>
                <div class="form-group">
                    <button class="btn btn-danger btn-sm" @click.prevent="simpan">Tambahkan</button>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    export default {
        name: 'AgendaInput',
        props: ['postTitle'],
        data() {
            return {
                agenda: {
                    hari: '',
                    kegiatan: ''
                }
            }
        },
        methods: {
            simpan() {
                
            }
        }
    }
</script>

Saya ingin menampilkan component AgendaInput.vue sebelum component AgendaShow.vue. Pada View views/LearningVuex.vue tambahkan component AgendaInput.vue seperti di bawah ini :

<template>
	<div id="app" class="container">
		<div class="row py-4">
			<agenda-input post-title="Input Agenda"/>
			<agenda-show post-title="Daftar Agenda"/>
		</div>
	</div>
</template>

<script>
	import AgendaInput from '../components/AgendaInput.vue'
	import AgendaShow from '../components/AgendaShow.vue'

	export default {
		name: 'app',
		components: {
			AgendaInput,
			AgendaShow
		}
	}
</script>

Hasilnya sebagai berikut:

+ Component AgendaInput

Actions dan Mutations

Hal pertama yang akan kita lakukan adalah memodifikasi bagian methods tambahkan simpan() pada component AgendaInput.vue, tambahkan code berikut:

methods: {
    simpan() {
        this.$store.dispatch('simpanAgenda', this.agenda)
        this.agenda = {
            hari: '',
            kegiatan: ''
        }
    }
}

Penjelasan

  • this.$store.dispatch(‘simpanAgenda’, this.agenda) akan memberikan instruksi pada actions dengan mengirimkan isi dari this.agenda yang di dapatkan dari data() yang telah di-input
  • this.agenda  berfungsi untuk mengosongkan form inputan setelah di submit.

Setelah itu buka file store.js dan tambahkan code berikut pada bagian actions :

actions: {
    simpanAgenda({ commit, state }, agenda) {
        state.isLoading = true
        setTimeout(() => {
            commit('KONFIRMASI_AGENDA', agenda)
            state.isLoading = false
        }, 1500)
    }
}

Penjelasan: fungsi simpanAgenda() memiliki dua parameter, parameter pertama yang diapit oleh kurung kerawal adalah fungsi yang dimiliki oleh Vuex, diantaranya: commit, state, rootState, dispatch, dan lain sebagainya.

Namun kami ini kita hanya akan menggunakan commit dan state saja. Sedangkan untuk parameter kedua akan menampung data yang dikirimkan dari yang meng-instruksikan, dalam hal ini adalah method dari component AgendaInput.vue. 

Line-2 mengakses state isLoading dan mengubah value-nya menjadi true (Note: Inilah cara mengubah state tanpa melalui mutations). 

Line-3 membuat fungsi setTimeout untuk mengatur setelah beberapa saat barulah fungsi selanjutnya dijalankan. 

Line-4 meng-instruksikan kepada mutations dengan fungsi commit(), dimana instruksi tersebut ditujukan pada KONFIRMASI_AGENDA yang terdapat pada mutations, parameter kedua dari commit berisi data yang akan dikirimkan. 

Line selanjutnya mengubah kembali nilai dari state isLoading menjadi false.

Ada dua bagian yang perlu diperhatikan, yakni adanya state isLoading dan mutations KONFIRMASI_AGENDA, baik, mari kita lengkapi satu persatu secara bertahap. Masih di dalam file store.js, pada bagian state, tambahkan code berikut:

state: {
    isLoading: false, //<=Kode yang ditambahkan
    agenda: [
        { hari: 'Senin', kegiatan: 'Belajar Vuejs' },
        { hari: 'Selasa', kegiatan: 'Belajar Laravel' },
        { hari: 'Rabu', kegiatan: 'Belajar Mysql' }
    ],
},

Kemudian pada bagian mutations, tambahkan code berikut:

mutations: {
    KONFIRMASI_AGENDA: (state, agenda) => {
        state.agenda.push(agenda)
    }
},

Penjelasan

KONFIRMASI_AGENDA memiliki dua parameter, parameter pertama adalah state untuk mengakses state yang ada. Sedangkan parameter kedua adalah value yang diterima dari yang meng-instruksikan. state.agenda berarti kita mengakses state listAgenda untuk kemudian ditambahkan data baru dengan fungsi push().

Sampai pada tahap ini, sudah dapat berfungsi sebagaimana mestinya. Tapi, kita menambahkan isLoading yang bertujuan untuk memberikan efek loading ketika tombol ditekan, maka buka file AgendaInput.vue, kemudian modifikasi pada tag button menjadi:

<button class="btn btn-danger btn-sm" :disabled="isLoading" @click.prevent="simpan">{{ isLoading ? 'Loading...':'Tambahkan' }}</button>

Masih di dalam file yang sama, tambahkan computed property berikut:

computed: {
    isLoading() {
        return this.$store.state.isLoading
    }
},

Jalankan

Masukan Hari: Minggu, Kegiatan: Libur, lalu klik tombol Tambahkan maka hasil yang akan diperoleh akan tampak seperti berikut :

Tambahkan Agenda

File lengkap :

file components/AgendaInput.vue

<template>
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title">{{postTitle}}</h3>
            </div>
            <div class="card-body">
                <div class="form-group">
                    <label for="">Hari</label>
                    <input type="text" v-model="agenda.hari" class="form-control" required>
                </div>
                <div class="form-group">
                    <label for="">Kegiatan</label>
                    <input type="text" v-model="agenda.kegiatan" class="form-control" required>
                </div>
                <div class="form-group">
                    <button class="btn btn-danger btn-sm"
                    :disabled="isLoading"
                    @click.prevent="simpan">{{ isLoading ? 'Loading...':'Tambahkan' }}</button>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    export default {
        name: 'AgendaInput',
        props: ['postTitle'],
        data() {
            return {
                agenda: {
                    hari: '',
                    kegiatan: ''
                }
            }
        },
        computed: {
            isLoading() {
                return this.$store.state.isLoading
            }
        },
        methods: {
            simpan() {
                this.$store.dispatch('simpanAgenda', this.agenda)
                this.agenda = {
                    hari: '',
                    kegiatan: ''
                }
            }
        }
    }
</script>

file src/store.js

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
      isLoading: false,
      agenda: [
        { hari: 'Senin', kegiatan: 'Belajar Vuejs' },
        { hari: 'Selasa', kegiatan: 'Belajar Laravel' },
        { hari: 'Rabu', kegiatan: 'Belajar Mysql' }
      ],
      listAgenda2: [
        { hari: 'Kamis', kegiatan: 'Belajar Nuxt' },
        { hari: "Jum'at", kegiatan: 'Belajar Flutter' },
      ]
  },
  mutations: {
    KONFIRMASI_AGENDA: (state, agenda) => {
        state.agenda.push(agenda)
    }
  },
  actions: {
    simpanAgenda({ commit, state }, agenda) {
      state.isLoading = true
      setTimeout(() => {
          commit('KONFIRMASI_AGENDA', agenda)
          state.isLoading = false
      }, 1500)
    }
  }
});

Kesimpulan

Actions dan Mutations dapat di-ibaratkan sebagai instruktur dan eksekutor, meskipun sang instruktur juga dapat mengeksekusi instruksinya sendiri. 3 Bagian penting (baca: statemutations dan actions) inilah yang memiliki banyak peranan dalam mengelola data menggunakan Vuex. Lalu bagaimana misalnya jika aplikasi yang sedang kita bangun sudah sangat kompleks? Jika code tersebut di gabungkan di dalam satu file yang sama, sebut saja store.js untuk mengelola datanya, maka akan sangat membingungkan jika sudah memiliki banyak code.


Sumber :

VueJs-#08a Vuex State

Pengenalan

Vuex adalah library Vue.js yang dapat digunakan untuk meng-handle state (state manajemen), dimana data yang ada dapat didefinisikan dan dipusatkan pada sebuah file sehingga bisa digunakan oleh semua component yang ada.

Dengan menggunakan Vuex, komunikasi antar component menjadi lebih mudah. Sebagaimana yang kita ketahui bahwasanya dalam interaksi antar component agar dapat saling bertukar data adalah dengan menggunakan props, maka dengan Vuex kamu cukup mengakses state yang telah didefinisikan pada Vuex Store.

Persiapan

Artikel ini merupakan bagian pertama, maka kita memulainya dengan project yang masih fresh installInstall Vue.js dengan menggunakan vue cli:

vue create vue-bootstrap-vuex

Pilih enable options router & vuex

Sehingga Struktur direktorinya menjadi seperti gambar di bawah ini :

vue with router, vuex, & bootstrap

Perhatikan gambar di atas pada folder src terdapat file-file bawaan options penginstallan router & vuex :

  • router.js, file ini berfunsi untuk mendefinisikan router2 berdasarkan komponen, dibuat otomatis karena kita menyertakan options vue-router
  • store.js, file ini berisi state vuex dibuat otomatis karena kita menyertakan options vuex
  • views, folder ini berisi template views untuk komponen dibuat otomatis karena kita menyertakan options vue-router

Setelah selesai untuk mempercantik tampilan kita menggunakan theme bootstrap Install bootstrapVue

vue add bootstrap-vue

Sekarang kita ubah src/App.vue

<template>
  <div id="app">
    <div>
      <b-navbar toggleable="lg" type="dark" variant="info">
        <b-navbar-brand href="#">NavBar</b-navbar-brand>
     
        <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
     
        <b-collapse id="nav-collapse" is-nav>
          <b-navbar-nav>
            <b-nav-item to="/">Home</b-nav-item>
            <b-nav-item to="About">About</b-nav-item>
            <b-nav-item to="#" disabled>Disabled</b-nav-item>
          </b-navbar-nav>
     
          <!-- Right aligned nav items -->
          <b-navbar-nav class="ml-auto">
            <b-nav-form>
              <b-form-input size="sm" class="mr-sm-2" placeholder="Search"></b-form-input>
              <b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button>
            </b-nav-form>
     
            <b-nav-item-dropdown text="Lang" right>
              <b-dropdown-item href="#">EN</b-dropdown-item>
              <b-dropdown-item href="#">ES</b-dropdown-item>
              <b-dropdown-item href="#">RU</b-dropdown-item>
              <b-dropdown-item href="#">FA</b-dropdown-item>
            </b-nav-item-dropdown>
     
            <b-nav-item-dropdown right>
              <!-- Using 'button-content' slot -->
              <template v-slot:button-content>
                <em>User</em>
              </template>
              <b-dropdown-item href="#">Profile</b-dropdown-item>
              <b-dropdown-item href="#">Sign Out</b-dropdown-item>
            </b-nav-item-dropdown>
          </b-navbar-nav>
        </b-collapse>
      </b-navbar>
    </div>
    <div class="container mt-2">
      <router-view></router-view>
    </div>
  </div>
  <!--<div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view />
  </div>-->
</template>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

Jalankan service vue:

npm run serve
Tampilan dengan Bootstrap

Manajemen State Dengan Vuex

Tambahkan Menu baru pada src/App.vue

<template>
  <div id="app">
    <div>
      <b-navbar toggleable="lg" type="dark" variant="info">
        <b-navbar-brand href="#">NavBar</b-navbar-brand>
        <b-navbar-toggle target="nav-collapse"></b-navbar-toggle> 
        <b-collapse id="nav-collapse" is-nav>
          <b-navbar-nav>
            <b-nav-item to="/">Home</b-nav-item>
            <b-nav-item to="About">About</b-nav-item>
            <b-nav-item to="LearningVuex">Learning Vuex</b-nav-item>
            <b-nav-item to="#" disabled>Disabled</b-nav-item>
          </b-navbar-nav>

          <!-- Right aligned nav items -->
          <b-navbar-nav class="ml-auto">
            <b-nav-form>
              <b-form-input size="sm" class="mr-sm-2" placeholder="Search"></b-form-input>
              <b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button>
            </b-nav-form> 
            <b-nav-item-dropdown text="Lang" right>
              <b-dropdown-item href="#">EN</b-dropdown-item>
              <b-dropdown-item href="#">ES</b-dropdown-item>
              <b-dropdown-item href="#">RU</b-dropdown-item>
              <b-dropdown-item href="#">FA</b-dropdown-item>
            </b-nav-item-dropdown>
     
            <b-nav-item-dropdown right>
              <!-- Using 'button-content' slot -->
              <template v-slot:button-content>
                <em>User</em>
              </template>
              <b-dropdown-item href="#">Profile</b-dropdown-item>
              <b-dropdown-item href="#">Sign Out</b-dropdown-item>
            </b-nav-item-dropdown>
          </b-navbar-nav>
        </b-collapse>
      </b-navbar>
    </div>
    <div class="container mt-2">
      <router-view/>
    </div>
  </div>
</template>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>

Siapkan route “Learning Vuex” di src/router.js

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      path: "/about",
      name: "about",
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () =>
        import(/* webpackChunkName: "about" */ "./views/About.vue")
    },
    {
      path: "/learningvuex",
      name: "LearningVuex",
      component: () =>
        import("./views/LearningVuex.vue")
    }
  ]
});

Buka file src/store.js

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
      agenda: [
        { hari: 'Senin', kegiatan: 'Belajar Vuejs' },
        { hari: 'Selasa', kegiatan: 'Belajar Laravel' },
        { hari: 'Rabu', kegiatan: 'Belajar Mysql' }
      ],
  },
  mutations: {},
  actions: {}
});

Sekarang kita akan membuat Component memanggil state di atas:

Membuat Page Views LearningVuex.vue untuk memanggil kedua Component

<template>
	<div id="app" class="container">
		<div class="row py-4">
			<agenda-show post-title="Daftar Agenda"/>
		</div>
	</div>
</template>

<script>
	import AgendaShow from '../components/AgendaShow.vue'

	export default {
		name: 'app',
		components: {
			AgendaShow
		}
	}
</script>

Membuat Component :

src/components/AgendaShow.vue

<template>
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title">{{postTitle}}</h3>
            </div>
            <div class="card-body">
                <div class="table-responsive">
                    <table class="table table-hover table-bordered">
                        <thead>
                            <tr>
                                <th>#</th>
                                <th>Hari</th>
                                <th>Agenda</th>
                                <th></th>
                            </tr>
                        </thead>
                        <tbody>
                            <!-- Memanggil state agenda dari STORE -->
                            <tr v-for="(row, index) in this.$store.state.agenda" :key="index">
                                <td>{{ index+1 }}</td>
                                <td>{{ row.hari }}</td>
                                <td>{{ row.kegiatan }}</td>
                                <td></td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
    export default {
        name: 'AgendaShow',
        props: ['postTitle'],
    }
</script>

Buka kembali browser http://localhost:8080

Table Agenda

VueJs-#12 Vue Login FirebaseUI

Firebase adalah infrastruktur cloud real-time untuk aplikasi sisi klien, yang memungkinkan pengembang mengubah aplikasi kerangka Front-end apa pun seperti Vue, React, Angular, iOS atau Android menjadi produk tumpukan penuh yang mampu melakukan penskalaan tanpa batas di cloud.

Persiapan

Install Project menggunakan Vue Cli

vue create vue-firebase

Pilih Manually select feature, lalu ceklis pilihan berikut :

Select Options

Install Theme BootstrapVue

vue add bootstrap-vue

Install Firebase into Vue Project

npm install firebase --save

Buat Project di FireBase

Sebelum memulai membuat Program, kita akan menyiapkan Proyek di Firebase, masuk ke Firebase Console buat Proyek baru, setelah selesai masuk ke Menu Project Overview => Setelan proyek

Setelan Proyek

Menambahkan Firebase ke Aplikasi Web

Parameter Pengaturan Firebase

Sekarang kita akan membuat setingan untuk firebase di Program Vue kita sesuaikan dengan gambar di atas. Buka main.js tambahkan baris berikut diantara import dan Vue.config.productionTip = false;

import firebase from 'firebase'

const firebaseConfig = {
  apiKey: "*****",
  authDomain: "firebaseUIAuth-*****.firebaseapp.com",
  databaseURL: "https://****-709a3.firebaseio.com",
  projectId: "firebaseUIAuth-709a3",
  storageBucket: "",
  messagingSenderId: "2547636***397",
  appId: "1:254763***397:web:***c15c671b5c"
};
firebase.initializeApp(firebaseConfig);

Menyiapkan Route

router.js

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Login from "@/components/Login.vue";
import Profile from "@/components/Profile.vue";
import Todos from "@/components/Todos";

import firebase from 'firebase/app';
import 'firebase/auth';

Vue.use(Router);

let router =  new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
        path: "/home",
        name: "Home",
        component: Home,
        meta: {
            auth: true
        }
    },
    {
        path: '/about',
        name: 'about',
        component: () => import('./views/About.vue')
    },
    {
        path: "/",
        name: "Login",
        component: Login,
        meta: {
            guest: true
        }
    },
    {
        path: "/profile",
        name: "Profile",
        component: Profile,
        meta: {
            auth: true
        }
    },
    {
        path: '/todo',
        name: 'Todos',
        component: Todos,
        meta: {
            auth: true
        }
    }
  ]
});
router.beforeEach((to, from, next) => {
    if (to.matched.some(record => record.meta.auth)) {
      firebase.auth().onAuthStateChanged(user => {
        if (user) {
          next()
        } else {
          next({
            path: "/",
          })
        }
      })
    } else if (to.matched.some(record => record.meta.guest)) {
      firebase.auth().onAuthStateChanged(user => {
        if (user) {
          next({
            path: "/profile",
          })
        } else {
          next()
        }
      })
    } else {
      next()
    }
});
export default router

router.beforeEach berfungsi untuk meredirect page yang memiliki property :

meta: {
   auth: true
}

Singkatnya jika belum login atau user kosong route akan diredirect kembali ke route Login.

Menyiapkan Store

Kita perlu menyiapkan store untuk menyimpan status login & data user yang nantinya akan ditampilkan pada component Profile.vue

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    isLogin:false,
    user:null
  },
  mutations: {},
  actions: {}
});

Login component menggunakan FirebaseUI

FirebaseUI adalah library yang dibuat sebagai tambahan dari Firebase Authentication SDK yang menyediakan alur UI drop-in untuk digunakan dalam aplikasi Anda. Baca penjelasannya disini.

Sekarang kita akan menambahkan FirebaseUI di program Vue kita

npm install firebaseui --save

Karena kita akan login menggunakan gmail atau email, maka kita harus mengaktifkan options Authentication pada Firebase Console. Masuk ke Menu Authentication=>Pilih Tab Metode Login lalu aktifkan pilihan Email & Google

Metode Login

Setelah itu kita buat component Login.vue yang nantinya akan menampilkan halaman login.

<template>
    <div>
        <br/><br/><br/><br/>
        <h5 class="center-align">Login</h5>
        <section id="firebaseui-auth-container"></section>
    </div>
</template>

<script>
import firebase from "firebase";
import * as firebaseui from "firebaseui";
import "firebaseui/dist/firebaseui.css";
export default {
    name: "Login",
    data() {
        return {};
    },
    mounted() {
        let ui = firebaseui.auth.AuthUI.getInstance();
        if (!ui) {
            ui = new firebaseui.auth.AuthUI(firebase.auth());
        }
        var uiConfig = {
            signInSuccessUrl: "/profile", // This redirect can be achived by route using callback.
            signInFlow: "popup",
            signInOptions: [
                //firebase.auth.FacebookAuthProvider.PROVIDER_ID,
                firebase.auth.GoogleAuthProvider.PROVIDER_ID,
                firebase.auth.EmailAuthProvider.PROVIDER_ID
            ]
        };
        ui.start("#firebaseui-auth-container", uiConfig);
    }
};
</script>

<style>
</style>

Perhatikan baris signInSuccessUrl: “/profile”, Jika login berhasil route akan diarahkan ke route /profile.

Buat Program

Selanjutnya kita akan membuat sebuah component NavBar.vue yang nantinya bisa diakses di setiap component jika dalam posisi login, untuk mempercantik tampilannya kita akan menambahkan BootstrapVue proyek kita.

vue add bootstrap-vue

components/NavBar.vue

<template>
    <section>
        <b-navbar toggleable="lg" type="dark" variant="info">
            <b-navbar-brand href="#">ombagoes.com</b-navbar-brand>
            <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
            <b-collapse id="nav-collapse" is-nav>
                <b-navbar-nav>
                    <b-nav-item to="/Home">Home</b-nav-item>
                    <b-nav-item to="About">About</b-nav-item>
                    <b-nav-item to="#" disabled>Disabled</b-nav-item>
                </b-navbar-nav>
                <b-navbar-nav class="ml-auto">
                    <b-nav-item v-show="!user" href="/">Login</b-nav-item>
                    <b-nav-item-dropdown v-if="user" right>
                        <template v-slot:button-content>
                            <em>{{user.displayName}}</em>
                        </template>
                        <b-dropdown-item to="/profile">Profile</b-dropdown-item>
                        <b-dropdown-item @click="signoutButtonPressed">
                            Sign Out
                        </b-dropdown-item>
                    </b-nav-item-dropdown>
                </b-navbar-nav>
            </b-collapse>
        </b-navbar>
    </section>
</template>

<script>
import firebase from "firebase";
export default {
    data() {
        return {
            user: null
        };
    },
    created() {
        firebase.auth().onAuthStateChanged(user => {
            if (user) {
                this.user = user;
                this.$store.state.isLogin=true;
                this.$store.state.user=user;
            }
        });
    },
    methods: {
        signoutButtonPressed(e) {
            e.stopPropagation();
            firebase.auth().signOut();
            this.$router.push({ name: "Login" });
        }
    }
};
</script>

NavBar.vue mengatur penyimpanan status login jika ada perubahan status jika login akan menyimpan data user di store dan menghapus data user jika sebaliknya.

Lalu buat layout pada App.vue

<template>
    <div id="app">
        <navigation v-show="getAuth"/>
        <router-view/>
    </div>
</template>

<script>
import navigation from "@/components/NavBar.vue";
export default {
    name: "App",
    components: {
        navigation
    },
    computed: {
        getAuth: function (){
            return this.$store.state.isLogin;
        }
    }
};
</script>

<style>
#app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
}
</style>

perhatikan baris <navigation v-show=”getAuth” /> component ini hanya akan tampil jika posisi user login dimana getAuth diambil dari store.

Menyiapkan component Profile

components/Profile.vue akan menampilkan data user yang sebelumnya disimpan pada store.

<template>
    <section>
        <div v-if="getUser">
            <b-card v-bind:img-src="getUser.photoURL" img-alt="Card image" img-left class="mb-3">
                <b-card-text>
                    <p>
                        name:<strong>{{getUser.displayName}}</strong>
                        <br />email:
                        <strong>{{getUser.email}}</strong>
                        <br />uid:
                        <strong>{{getUser.uid}}</strong>
                        <br />provider:
                        <strong class="teal-text">{{getUser.providerData[0].providerId}}</strong>
                    </p>
                </b-card-text>
            </b-card>
        </div>
    </section>
</template>

<script>
import firebase from "firebase";
export default {
    data() {
        return {};
    },
    computed:{
        getUser:function(){
            return this.$store.state.user;
        }
    },
};
</script>

Jalankan hasilnya sbb:

Login
Pilih Email
Profile

Source

  1. https://softauthor.com/firebaseui-vue-login-with-facebook-google-and-email-pasword/

VueJs-#11 Vue Vuetify Login & Crud Auth JWT

Apa itu Vuetify?

Vuetify merupakan sebuah framework desain komponen material untuk UI/UX yang berasosiasi langsung dengan Vue.js. Mirip seperti BootstrapVue, kira-kira begitulah.

Persiapan

vue create vue-vuetify
Options

Setelah selesai kita masuk direktori vue-vuetify, lalu tambahkan vuetify pada proyek kita.

cd vue-vuetify
vue add vuetify

Lalu jalankan service…

npm run serve
Vuetify Home

Halaman utamanya akan berubah seperti gambar di atas.

Karena nantinya kita akan membutuhkan icon :

npm install --save material-design-icons-iconfont

Edit src/plugins/vuetify.js

import 'material-design-icons-iconfont/dist/material-design-icons.css'
import Vue from 'vue';
import Vuetify from 'vuetify/lib';

Vue.use(Vuetify);

export default new Vuetify({
  icons: {
    iconfont: 'mdi',
  },
});

Planing saya akan membuat 5 Page sbb:

  1. Login integration to Laravel Login JWT API
  2. Home
  3. About
  4. ToDos Crud integration to Laravel ToDos API
  5. Profile

Karena kita akan mengakses API, kita memerlukan axios

npm install --save axios

Router

Kita akan menggunakan Auth akses permission, sehingga router.js menjadi seperti berikut :

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Login from "./components/Login.vue";
import Register from "./components/Register.vue";
import Profile from "./components/Profile.vue";
import Todos from "@/components/Todos";
import store from "./store.js";

Vue.use(Router);

let router =  new Router({
    mode: "history",
    base: process.env.BASE_URL,
    routes: [
        {
            path: "/home",
            name: "Home",
            component: Home,
            meta: {
                auth: true
            }
        },
        {
            path: '/about',
            name: 'about',
            component: () => import('./views/About.vue'),
            meta: {
                auth: true
            }
        },
        {
            path: "/",
            name: "Login",
            component: Login,
            meta: {
                guest: true
            }
        },
        {
            path: "/register",
            name: "Register",
            component: Register,
            meta: {
                guest: true
            }
        },
        {
            path: "/profile",
            name: "Profile",
            component: Profile,
            meta: {
                auth: true
            }
        },
        {
            path: '/todo',
            name: 'Todos',
            component: Todos,
            meta: {
                auth: true
            }
        }
    ]
});
router.beforeEach((to, from, next) => {
    if (to.matched.some(record => record.meta.auth)) {    
        if (store.state.isLogin) {
            next();
        } else {
            next({
                path: "/",
            });
        }
    } else if (to.matched.some(record => record.meta.guest)) {
        if (store.state.isLogin) {
            next({
                path: "/profile",
            });
        } else {
            next();
        }
    } else {
        next();
    }
});
export default router;

Membuat Store

store.js

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        isLogin: false,
        token:null,
        user:null,
    },
    mutations: {
        resetState (state) {
            state.isLogin=false;
            state.token=null;
            state.user=null;
        }
    },
    actions: {
        clearState ({ commit }) {
            commit('resetState');
        },
    }
});

Membuat Component NavBar

src/component/NavBar.vue

<template>
    <section>
        <v-navigation-drawer v-model="drawer" app>
        <v-list-item>
            <v-list-item-content>
            <v-list-item-title class="title">
                Application Name
            </v-list-item-title>
            <v-list-item-subtitle>
                subtext
            </v-list-item-subtitle>
            </v-list-item-content>
        </v-list-item>
        <v-divider></v-divider>
        <v-list dense nav>
            <v-list-item to="/Home">
                <v-list-item-action>
                    <v-icon>mdi-home</v-icon>
                </v-list-item-action>
                <v-list-item-content>
                    <v-list-item-title>Home</v-list-item-title>
                </v-list-item-content>
            </v-list-item>
            <v-list-item  to="/About">
                <v-list-item-action>
                    <v-icon>mdi-help-circle</v-icon>
                </v-list-item-action>
                <v-list-item-content>
                    <v-list-item-title>About</v-list-item-title>
                </v-list-item-content>
            </v-list-item>
        </v-list>
        <v-divider></v-divider>
        <v-list dense nav>
            <v-list-item  to="/Messages">
                <v-list-item-action>
                    <v-badge>
                        <template v-slot:badge>0</template>
                        <v-icon>mdi-email</v-icon>
                    </v-badge>
                </v-list-item-action>
                <v-list-item-content>
                    <v-list-item-title>Messages</v-list-item-title>
                </v-list-item-content>
            </v-list-item>
        </v-list>
        <template v-slot:append>
            <div class="pa-2">
                <v-btn @click="signoutButtonPressed" color="red darken-4 white--text" block>Logout</v-btn>
            </div>
        </template>
        </v-navigation-drawer>

        <v-app-bar app color="blue darken-4" dark>
            <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
            <v-toolbar-title>Application</v-toolbar-title>
            <div class="flex-grow-1"></div>

            <v-btn icon>
                <v-icon>mdi-heart</v-icon>
            </v-btn>
            <v-menu left bottom>
                <template v-slot:activator="{ on }">
                <v-btn icon v-on="on">
                    <v-icon>mdi-dots-vertical</v-icon>
                </v-btn>
                </template>

                <v-list>
                    <v-list-item  to="/Profile">
                        <v-list-item-action>
                            <v-icon>mdi-account-circle</v-icon>
                        </v-list-item-action>
                        <v-list-item-content>
                            <v-list-item-title>Profile</v-list-item-title>
                        </v-list-item-content>
                    </v-list-item>
                    <v-list-item  to="/Setting">
                        <v-list-item-action>
                            <v-icon>mdi-settings</v-icon>
                        </v-list-item-action>
                        <v-list-item-content>
                            <v-list-item-title>Setting</v-list-item-title>
                        </v-list-item-content>
                    </v-list-item>
                </v-list>
            </v-menu>
        </v-app-bar>
    </section>
</template>
<script>
export default {
    props: {
      source: String,
    },
    data: () => ({
      drawer: null,
    }),
    methods: {
        signoutButtonPressed() {
            this.$store.dispatch('clearState');
            this.$router.push({ name: "Login" });
        }
    }
};
</script>

Membuat Layout

src/App.vue

<template>
  <v-app teal>
    <NavBar v-show="getStatusLogin"/>
    <v-content>
      <router-view />
    </v-content>
    <v-footer color="blue darken-4" app>
      <span class="white--text">© 2019</span>
    </v-footer>
  </v-app>
</template>

<script>
import NavBar from "./components/NavBar";

export default {
    name: "App",
    components: {
        NavBar
    },
    data: () => ({
        //
    }),
    computed: {
        getStatusLogin: function () {
            return this.$store.state.isLogin;
        }
    }
};
</script>

Membuat Halaman Login

src/component/Login.vue

<template>
    <section>
        <v-container class="fill-height" fluid>
            <v-row align="center" justify="center">
                <v-col cols="12" sm="8" md="4">
                    <v-card class="mx-auto">
                        <v-toolbar color="primary" dark flat>
                            <v-toolbar-title>Login form</v-toolbar-title>
                        </v-toolbar>
                        <v-card-text>
                            <v-form ref="form" v-model="valid">
                                <v-text-field
                                    label="Email"
                                    v-model="email"
                                    prepend-icon="mail"
                                    type="text"
                                    :rules="emailRules"
                                    required
                                ></v-text-field>

                                <v-text-field
                                    label="Password"
                                    v-model="password"
                                    prepend-icon="lock"
                                    type="password"
                                    :rules="passwordRules"
                                    required
                                ></v-text-field>
                            </v-form>
                        </v-card-text>
                        <v-card-actions>
                            <div class="flex-grow-1"></div>
                            <v-btn color="primary" :disabled="!valid" @click="doLogin">Login</v-btn>
                            <v-btn color="primary" link to="/Register">Register</v-btn>
                        </v-card-actions>
                    </v-card>
                    <v-snackbar v-model="snackbar" bottom>
                        {{error_message}}
                    </v-snackbar>
                </v-col>
            </v-row>
        </v-container>
    </section>
</template>

<script>
export default {
    props: {
        source: String
    },
    data: () => ({
        drawer: null,
        snackbar:false,
        valid: true,
        email: "",
        emailRules: [
            v => !!v || "E-mail is required",
            v => /.+@.+\..+/.test(v) || "E-mail must be valid"
        ],
        password: "",
        passwordRules: [
            v => !!v || "Password is required",
            v => (v &amp;&amp; v.length >= 6) || "Password must be larger than 6 characters"
        ],
        result:null,
        error_message:""
    }),
    methods: {
        reset() {
            this.$refs.form.reset();
        },
        doLogin() {
            const axios = require('axios');
            axios.post("http://localhost:8000/api/login", {
                email:this.email,
                password:this.password,
            })
            .then(response => {
                this.result = response.data;
                if(this.result.token){
                    axios.get("http://localhost:8000/api/user", {
                        headers: {
                            Authorization: 'Bearer ' + this.result.token,
                        }
                    })
                    .then(response => {
                        this.result = response.data;
                        if(this.result.user){
                            this.$store.state.user=this.result.user;
                            this.$store.state.isLogin=true;
                            this.$store.state.jwt_token=this.result.token;
                            this.$router.push({ name: "Profile" });
                        }else{
                            this.error_message=response.data.status;
                            this.snackbar=true;
                        }
                    })
                }
            })
            .catch(e => {   
                if (e.response &amp;&amp; e.response.status === 400) {
                    this.error_message=e.response.data.error;
                    this.snackbar=true;
                }
            });
        }
    },
};
</script>

src/components/Profile.vue

<template>
    <section>
        <v-container class="pa-2" fluid>
            <v-row>
                <v-col>
                    <v-alert v-if="errors" type="error" class="mt-4">
                        {{errors}}
                    </v-alert>
                    <v-card color="#385F73" dark>
                        <v-card-text class="white--text">
                        <div class="headline mb-2" v-if="user">{{user.name}}</div>
                            Listen to your favorite artists and albums whenever and wherever, online and offline.
                        </v-card-text>

                        <v-card-actions>
                            <v-btn text>Listen Now</v-btn>
                        </v-card-actions>
                    </v-card>
                </v-col>
            </v-row>
        </v-container>
    </section>
</template>

<script>
export default {
    data: () => ({
        user:null,
        result:null,
        errors:null
    }),
    mounted: function () {
        this.user=this.$store.state.user;
    }
}
</script>

Todo Crud

Membuat Module

src/modules/todo.js

const todo = {
    namespaced: true,
    state: {
        listData: [
            { id:1, user_id:1, title: 'Example', },
        ]
    },
    getters: {
        getRowById: (state) => (id) => {
            return state.listData.find((obj) => {
                return obj.id===id
            })
        }
    },
    mutations: {
        ADD_ROW: (state, payload) => {
            state.listData.push(payload.row);
        },
        EDIT_ROW(state, payload){
            var listData = state.listData
            listData.forEach((element, index) => {
                if(element.id === payload.id) {
                    listData[index] = payload;
                }
            });
            state.listData = listData;
        },
        REMOVE_ROW(state, payload){
            state.listData.splice(state.listData.indexOf(payload.row), 1);
        },
    },
    actions: {
        save_row({ commit, rootState }, payload) {
            rootState.isLoading = true;
            setTimeout(() => {
                commit('ADD_ROW', payload)
                rootState.isLoading = false
            }, 1000)
        },
        edit_row({commit}, payload){
            commit('EDIT_ROW', payload)
        },
        remove_row({commit}, payload){
            commit('REMOVE_ROW', payload)
        },
    }
}

export default todo;

Setelah itu tambahkan modules todo pada store.js

import Vue from "vue";
import Vuex from "vuex";
import todo from './modules/todo';

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        isLoading:false,
        isLogin: false,
        token:null,
        user:null,
    },
    mutations: {
        resetState (state) {
            state.isLoading=false;
            state.isLogin=false;
            state.token=null;
            state.user=null;
        }
    },
    actions: {
        clearState ({ commit }) {
            commit('resetState');
        },
    },
    modules: {
        todo,
    }
});

Membuat Page

src/components/Todos.vue

<template>
    <section>
        <v-form v-model="valid">
            <v-container>
                <v-row>
                    <v-col cols="8" md="8">
                    <v-text-field
                        v-model="title"
                        :counter="10"
                        label="Enter what todo"
                        required
                    ></v-text-field>
                    </v-col>

                    <v-col cols="4" md="4">
                        <v-btn class="ma-2 red white--text" @click="addItem">
                            Add
                            <v-icon right dark>mdi-cloud-upload</v-icon>
                        </v-btn>
                    </v-col>
                </v-row>
                <v-row>
                    <v-col cols="12" md="12">
                        <v-list dense>
                            <v-list-item-group color="primary">
                                <v-list-item v-for="todo in todos" :key="todo.id">
                                    <v-list-item-content>
                                        <v-list-item-subtitle>
                                            <v-row>
                                                <v-col cols="2" md="2">
                                                    <v-btn @click="removeItem(todo)">X</v-btn>
                                                </v-col>
                                                <v-col cols="8" md="8">
                                                    <v-chip color="red" text-color="white">
                                                    {{todo.title}}
                                                    </v-chip>
                                                </v-col>
                                                <v-col cols="2" md="2">
                                                    <v-switch value :input-value="todo.isCompleted==1?true:false" 
                                                        @change="updateItem(todo,$event)"></v-switch>
                                                </v-col>
                                            </v-row>
                                        </v-list-item-subtitle>
                                    </v-list-item-content>
                                </v-list-item>
                            </v-list-item-group>
                        </v-list>
                    </v-col>
                </v-row>
            </v-container>
        </v-form>
    </section>
</template>

<script>
import axios from 'axios';

export default {
    data: () => ({
        drawer: null,
        snackbar:false,
        valid: true,
        title: "",
        message: "",
        checkboxes:[]
    }),
    created: function () {
        axios.get("http://localhost:8000/api/todo", {
            headers: {
                Authorization: 'Bearer ' + this.$store.state.token
            }
        })
        .then(response => {
            this.$store.state.todo.listData=response.data;
        });
    },
    computed: {
        todos(){
            return this.$store.state.todo.listData;
        }
    },
    methods: {
        addItem() {
            axios.post("http://localhost:8000/api/todo", {
                title:this.title,
                user_id:this.$store.state.user.id
            },
            {
                headers: {
                    Authorization: 'Bearer ' + this.$store.state.token
                }
            })
            .then(response => {
                this.message = response.data.status;
                this.$store.dispatch('todo/save_row', {
                    row:response.data.row,
                });
            });
        },
        updateItem(row, value) {
            axios.put("http://localhost:8000/api/todo/"+row.id, {
                isCompleted:value?1:0,
            },
            {
                headers: {
                    Authorization: 'Bearer ' + this.$store.state.token
                }
            })
            .then(response => {
                this.message = response.data.message;
                this.$store.dispatch('todo/edit_row', {
                    row: response.data.row
                });
            });
        },
        removeItem(row) {
            axios.delete("http://localhost:8000/api/todo/"+row.id,
            {
                headers: {
                    Authorization: 'Bearer ' + this.$store.state.token
                }
            })
            .then(response => {
                this.message = response.data.message;
                this.$store.dispatch('todo/remove_row', {
                    row: row,
                });
            });
        },
    }
}
</script>

Sumber

  1. https://github.com/sudheerj/vuejs-todolist/blob/master/src/store.js

VueJs-#10 Bootstrap-Vue

BootstrapVue dibuat berdasarkan template yang paling populer di dunia yaitu Bootstrap v4, untuk membangun situs yang responsif menggunakan Vue.js.

Persiapan:

  1. Buat Project Menggunakan Vue CLI

Kita angap nama Project ini adalah learning-vue-bootstrap. Masuk ke direktori project:

Install BootstrapVue.

cd learning-vue-bootstrap
vue add bootstrap-vue

Install Vue Router

npm install vue-router

Buat file src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from "@/components/HelloWorld"
import Home from "@/components/Home"
import About from "@/components/About";

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Welcome',
      component: HelloWorld
    },
    {
      path: '/Home',
      name: 'Home',
      component: Home
    },
    {
      path: "/about",
      name: "About",
      component: About
    }
  ]
})

Karena kita menggunakan router, maka kita harus mengimport router pada src/main.js

import '@babel/polyfill'
import 'mutationobserver-shim'
import Vue from 'vue'
import './plugins/bootstrap-vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

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

Kita lihat di index.js ada dua komponen tambahan yaitu Home & About. Kita buat kedua komponen ini

src/component/Home.vue

<template>
    <div class="home">
        <b-jumbotron header="HOME" lead="https://ombagoes.com">
            <p><strong>{{title}}</strong> Lorem Ipsum is simply dummy text of the printing and typesetting industry.
            Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
            when an unknown printer took a galley of type and scrambled it to make a type specimen book.
            It has survived not only five centuries, but also the leap into electronic typesetting,
            remaining essentially unchanged.
            It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
            and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>
        </b-jumbotron>
    </div>
</template>
<script>
export default {
  data() {
      return {
          title: 'Halaman Utama'
      }
  },
}
</script>

src/component/About.vue

<template>
    <div class="about">
        <b-jumbotron header="ABOUT US" lead="https://ombagoes.com">
            <p><strong>{{title}}</strong> It is a long established fact that a reader will be distracted by the readable content of a page
            when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal
            distribution of letters, as opposed to using 'Content here, content here',
            making it look like readable English. Many desktop publishing packages and
            web page editors now use Lorem Ipsum as their default model text, and a search
            for 'lorem ipsum' will uncover many web sites still in their infancy.
            Various versions have evolved over the years,
            sometimes by accident, sometimes on purpose (injected humour and the like).</p>
        </b-jumbotron>
    </div>
</template>
<script>
export default {
  data() {
      return {
          title: 'Tentang Kami'
      }
  },
}
</script>

Sekarang kita tambahkan navigasi pada src/App.vue

<template>
  <div id="app">
    <div>
      <b-navbar toggleable="lg" type="dark" variant="info">
        <b-navbar-brand href="#">NavBar</b-navbar-brand>
    
        <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
    
        <b-collapse id="nav-collapse" is-nav>
          <b-navbar-nav>
            <b-nav-item to="/Home">Home</b-nav-item>
            <b-nav-item to="About">About</b-nav-item>
            <b-nav-item to="#" disabled>Disabled</b-nav-item>
          </b-navbar-nav>
    
          <!-- Right aligned nav items -->
          <b-navbar-nav class="ml-auto">
            <b-nav-form>
              <b-form-input size="sm" class="mr-sm-2" placeholder="Search"></b-form-input>
              <b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button>
            </b-nav-form>
    
            <b-nav-item-dropdown text="Lang" right>
              <b-dropdown-item href="#">EN</b-dropdown-item>
              <b-dropdown-item href="#">ES</b-dropdown-item>
              <b-dropdown-item href="#">RU</b-dropdown-item>
              <b-dropdown-item href="#">FA</b-dropdown-item>
            </b-nav-item-dropdown>
    
            <b-nav-item-dropdown right>
              <!-- Using 'button-content' slot -->
              <template v-slot:button-content>
                <em>User</em>
              </template>
              <b-dropdown-item href="#">Profile</b-dropdown-item>
              <b-dropdown-item href="#">Sign Out</b-dropdown-item>
            </b-nav-item-dropdown>
          </b-navbar-nav>
        </b-collapse>
      </b-navbar>
    </div>
    <div class="container mt-2">
      <router-view></router-view>
    </div>
  </div>
</template>

<script>

export default {
  
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  /*margin-top: 60px;*/
}
</style>

Jalankan Program

Menu Home
npm run serve

Lalu buka browser http://localhost:8080/#/Home

Cantik bukan tampilannya. Untuk mempelajari komponen BootstrapVue silakan kunjungi halamannya di https://bootstrap-vue.js.org