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

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>