Laravel + Vue Js admin using Vuetify
Laravel can be set with and without Vue js. However, my personal experience says that using Vue, the admin can become flexible and easy to maintain when it comes to doing some complex operations like generating Product Attributes.
Plugins and Components needed in the Vue Laravel Admin Application
Step 1: Create a View File - dashboard.blade.php in resources/views/admin folder
Add following code in it
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Dashboard</title>
<link href="https://fonts.googleapis.com/css?family=Muli:300,400,600,700,800,900&display=swap" rel="stylesheet">
<script>
const ADMIN_ROUTE = "admin";
const ADMIN_APP_ROUTE = "admin/app";
const ADMIN_API_ROUTE = "admin/api";
</script>
</head>
<body>
<div id="dApp"> </div>
<script src="{{ url('js/vApp.js') }}"></script>
</body>
</html>
The above code will ensure a few things such as it provides root div dApp where the application will be rendered and declare a few constant such as ADMIN_APP_ROUTE, ADMIN_API_ROUTE and ADMIN_ROUTE which can be used later in the application
Step 2: Configure webpack.mix.js (located at the root folder of a Laravel Application)
Change mix.js('resources/js/app.js', 'public/js') to mix.js('_vApp/vApp.js', 'public/js')
So now it is necessary to create _vApp folder in the root directory of the application. This is the directory where all the files, components and plugins folders will be created.
Step 3: Create vApp.js file the _vApp directory and start configuring the Vue Admin Application
import Vue from 'vue'
import axios from './plugins/Axios'
import vuetify from './plugins/Vuetify'
import routes from './routes'
import vuex from './store/index.js'
//Default page or layout
import layout from './pages/layout'
const app = new Vue({
vuex,
router: routes,
axios,
vuetify,
el: '#dApp',
render: h => h(layout)
});
So considering above code, the next directories to be created under _vApp directory are plugins and store.
Step 4: Create Axios.js file in Plugins directory and add following content in it
import Vue from 'vue'
import axios from 'axios'
var axiosx = axios.create({
baseURL: 'http://127.0.0.1:8000/' + ADMIN_API_ROUTE
});
Vue.prototype.$axiosx = axiosx;
Now, the above code will enable the use of this.$axiosx throughout the Vue application. Moreover, we have declared baseURL so that we won't need to add endpoint every time while making an API call. We will just need to provide relative path such as '/get/users' instead of http://127.0.0.1:8000/admin/api/get/users and this makes maintenance of the application much easier.
Step 5: Create Vuetify.js file in Plugins directory and add following content in it
import Vue from 'vue'
import '@mdi/font/css/materialdesignicons.min.css'
import Vuetify, {
VApp,
VCard,
VCardText,
VCardTitle,
VCardActions,
VForm,
VTextField,
VTextarea,
VBtn,
VAppBar,
VAppBarNavIcon,
VContainer,
VNavigationDrawer,
VDivider,
VList,
VListItem,
VListItemIcon,
VListItemTitle,
VListItemGroup,
VIcon,
VListItemAvatar,
VImg,
VListItemContent,
VListItemSubtitle,
VListItemAction,
VToolbar,
VToolbarTitle,
VMenu,
VSimpleTable,
VPagination,
VSelect,
VSwitch,
VProgressLinear,
VFlex,
VLayout,
VCheckbox,
VExpansionPanels,
VExpansionPanel,
VExpansionPanelHeader,
VExpansionPanelContent,
VDialog,
VAlert,
VRadio,
VRadioGroup,
VCombobox,
VAutocomplete,
VTabs,
VTab,
VTabItem,
VTabsItems,
VTabsSlider,
VCol,
VRow,
VListGroup,
VSnackbar
} from "vuetify/lib"
Vue.use(Vuetify, {
components: {
VApp,
VDialog,
VAlert,
VListGroup,
VCol,
VRow,
VTabs,
VTab,
VTabItem,
VTabsItems,
VTabsSlider,
VRadio,
VAutocomplete,
VCombobox,
VRadioGroup,
VExpansionPanels,
VExpansionPanel,
VExpansionPanelHeader,
VExpansionPanelContent,
VLayout,
VFlex,
VCheckbox,
VCard,
VCardText,
VCardTitle,
VCardActions,
VForm,
VTextField,
VTextarea,
VBtn,
VAppBar,
VAppBarNavIcon,
VContainer,
VNavigationDrawer,
VDivider,
VList,
VListItem,
VListItemIcon,
VListItemTitle,
VListItemGroup,
VIcon,
VListItemAvatar,
VImg,
VListItemContent,
VListItemSubtitle,
VListItemAction,
VToolbar,
VToolbarTitle,
VMenu,
VSimpleTable,
VPagination,
VSelect,
VSwitch,
VProgressLinear,
VSnackbar
}
})
export default new Vuetify({
icons: {
iconfont: 'mdi'
},
theme: {
dark: false,
},
themes: {
light: {
primary: "#ed4b3f",
secondary: "#b0bec5",
accent: "#8c9eff",
error: "#b71c1c",
},
},
})
Step 6: Create routes file (routes.js) under _vApp directory, this file handles all the routes of the application. Following content is just a base
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
//Components for routes
import Form from './pages/Form'
const routes = [
{
path: '/form',
component: Form
}
];
const router = new VueRouter({
mode: 'history',
routes: routes,
base: ADMIN_APP_ROUTE
})
export default router;
In above code, ADMIN_APP_ROUTE is declared in the dashboard.blade.php file (See Step 1)
Step 7: Initialise Vuex in the application
Create directory "store" under _vApp folder and create file "index.js" in it with following content
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
loading: false,
test: 'jigesh',
snackbar: {
status: false,
text: '',
color: 'error'
},
api: 'http://127.0.0.1:8000/console/api',
media_dialog: false,
selectedMedia: [],
selectedMediaIds: [],
selectMultipleMedia: true,
mediaSelectionFinished: false,
mediaType: 'Image'
},
mutations: {
setMediaType (state, type) {
state.mediaType = type;
},
snackbar (state, data) {
state.snackbar.status = data.status;
if (!data.text) {
data.text = '';
}
state.snackbar.text = data.text;
if (data && data.status && data.status == 'success') {
state.snackbar.color="success";
}
},
loading (state, status) {
state.loading = status;
},
setSelectMultipleMedia (state, data) {
state.selectMultipleMedia = data;
},
mediaSelectionFinished (state, data) {
state.mediaSelectionFinished = data;
},
setSelectedMedia (state, array) {
state.selectedMedia = array;
},
setSelectedMediaIds (state, array) {
state.selectedMediaIds = array;
},
media_dialog (state, status) {
state.media_dialog = status;
},
}
});
Vue.prototype.$store = store;
export default store;
In above code, you can see state and mutations object is defined
Step 8: Create pages directory and define layout component
In this step create "pages" folder in _vApp directory. In this folder, all new pages will be added. For now, add layout.vue file, this is the base file where all the router-view will be rendered along with assets (style.css) will be defined and imported. As well as that, we will also manage a global SnackBar
<template>
<v-app class="main-bg">
<div class="flex">
<Media></Media>
<Drawer></Drawer>
<div class="flex column">
<div id="_wrapper" class="pa-5">
<router-view></router-view>
</div>
</div>
<div class="UppyProgressBar"></div>
<v-snackbar
v-model="snackbar.status"
:timeout="timeout"
:top="false"
:bottom="true"
>
{{ snackbar.text }}
<v-btn
color="blue"
text
@click="snackbar.status = false"
>
Close
</v-btn>
</v-snackbar>
</div>
</v-app>
</template>
<script>
import Vue from 'vue'
import Header from '../components/Header.vue'
import Drawer from '../components/Drawer.vue'
import Media from '../components/MediaDialog.vue'
import File from '../components/File.vue'
import Textarea from '../components/RichTextarea.vue'
import Geo from '../components/GeoComplete.vue'
Vue.component('Drawer', Drawer)
Vue.component('Header', Header)
Vue.component('Media', Media)
Vue.component('File', File)
Vue.component('Textarea', Textarea)
Vue.component('Geo', Geo)
export default {
data () {
return {
loading: true,
timeout: 6000,
media: Media,
snackbar: {
text: this.$store.state.snackbar.text,
status: this.$store.state.snackbar.status
},
media_dialog: this.$store.state.media_dialog
}
},
watch: {
media_dialog (val) {
if (!val) {
this.$store.commit('media_dialog', false);
}
},
'$store.state.media_dialog' (val) {
if (val) {
this.media_dialog = val;
}
},
'$store.state.snackbar.text' (val) {
this.snackbar.text = val;
},
'$store.state.snackbar.status' (val) {
if (val) {
this.snackbar.status = true;
}
},
'snackbar.status' (val) {
if (!val) {
this.$store.commit('snackbar', { status: false });
}
}
}
}
</script>
<style lang="scss">
@import url('../assets/style.scss')
</style>
The other custom components can be created under components directory (_vApp/components) as I did Header, Drawer, Media, File, TextArea, and Geo.
This keeps flow of this Vue application really organised
Since now, in 8 steps process, we have configured Vue application with Vue Router, Vuex, Vuetify, Axios which is a core part of any Vue application.
In upcoming process, we will render Header, Drawer Menu etc.
Step 9: Create Header.vue file under components directory, this will be the header part of the Laravel Vue JS admin dashboard
<template>
<v-app-bar
color="#fff"
flat
light
height="64px"
max-height="64px"
id="_mainAppBar"
>
<div class="flex align-center">
<v-toolbar-title v-text="heading"></v-toolbar-title>
<div class="ml-5" v-if="caption">
<small class="caption">(Showing {{ caption }})</small>
</div>
</div>
<div class="flex-grow-1"></div>
<div id="_metas_other" class="flex align-center flex-end">
<slot>
<v-btn icon>
<v-icon>mdi-heart</v-icon>
</v-btn>
<v-btn icon>
<v-icon>mdi-magnify</v-icon>
</v-btn>
</slot>
</div>
<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
v-for="n in 5"
:key="n"
@click="() => {}"
>
<v-list-item-title>Option {{ n }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-progress-linear
:active="$store.state.loading"
:indeterminate="$store.state.loading"
absolute
bottom
color="info"
></v-progress-linear>
</v-app-bar>
</template>
<script>
export default {
props: {
heading: {},
caption: {}
},
data () {
return {
}
}
}
</script>
Step 9: Create Drawer.vue file under components directory
<template>
<v-navigation-drawer
expand-on-hover
height="auto"
permanent
id="_mainDrawer"
>
<template v-slot:prepend>
<v-list class="py-0" id="_logo">
<v-list-item
link
two-line
>
<v-list-item-content>
<v-list-item-title class="title _900">AdLara</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</template>
<v-divider></v-divider>
<v-list
nav
dense
id="_sideList"
>
<v-list-item-group color="primary">
<v-list-item link color="red" to="/dashboard">
<v-list-item-icon>
<v-icon>mdi-view-dashboard</v-icon>
</v-list-item-icon>
<v-list-item-title>Dashboard</v-list-item-title>
</v-list-item>
<v-list-item link to="/table">
<v-list-item-icon>
<v-icon>mdi-table-large</v-icon>
</v-list-item-icon>
<v-list-item-title>Table</v-list-item-title>
</v-list-item>
<v-list-item link to="/form">
<v-list-item-icon>
<v-icon>mdi-star</v-icon>
</v-list-item-icon>
<v-list-item-title>Starred</v-list-item-title>
</v-list-item>
<v-list-item link to="/component/list">
<v-list-item-icon>
<v-icon>mdi-iframe-braces-outline</v-icon>
</v-list-item-icon>
<v-list-item-title>Component</v-list-item-title>
</v-list-item>
</v-list-item-group>
</v-list>
</v-navigation-drawer>
</template>
Step 10: Create MediaDialog.vue file in components directory
<template>
<div>
<v-dialog
id="_media"
width="100%"
max-width="1300px"
v-model="media_dialog"
>
<v-card
max-height="800"
min-height="700"
color="#f9f9f9"
tile
id="_mediaCard"
:loading="loading"
>
<v-tabs
v-model="active"
color="info"
left
id="_mediaTabs"
>
<v-tab>Media</v-tab>
<v-tab>Upload</v-tab>
<v-tab-item>
<v-container fluid class="pa-0" id="_mainContainer">
<div id="innerWrap">
<div
:class="'_image ' + getClass(m)"
v-for="(m, i) in media"
:key="i"
@click="chosen(m)"
>
<img
v-if="m.url && m.type == 'image'"
:src="m.url"
/>
<span
v-if="m.url && (m.format == 'application/pdf' || m.format == 'pdf')"
class="_pdf"
>
<v-icon>mdi-file-pdf-box</v-icon>
{{ m.name }}
</span>
<video
v-if="m.url && m.type == 'video' && m.format != 'embeded'"
>
<source :src="m.url" />
</video>
<div v-html="m.name" v-if="m.url && m.type == 'video' && m.format == 'embeded'"></div>
<div class="_layer"></div>
</div>
</div>
</v-container>
</v-tab-item>
<v-tab-item
eager
>
<v-container fluid>
<div id="imageUploader"></div>
</v-container>
</v-tab-item>
</v-tabs>
<div id="_mediaFooter"
v-if="active == 0"
>
<div class="flex space-between">
<div class="flex">
<div class="_paginate">
<v-select
dense
solo
v-model="paginate"
v-if="mediaData &&
mediaData.last_page &&
mediaData.last_page > 1"
:items="getPaginate(mediaData.last_page)"
hide-details
>
</v-select>
</div>
<div class="_type ml-5">
<v-select
solo
v-model="type"
:items="types"
hide-details
>
</v-select>
</div>
</div>
<v-btn
color="info"
@click="finishMediaSelection"
>
Finish
</v-btn>
</div>
</div>
</v-card>
</v-dialog>
</div>
</template>
<script>
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import XHRUpload from '@uppy/xhr-upload'
import axios from 'axios'
import '@uppy/core/dist/style.min.css'
import '@uppy/dashboard/dist/style.min.css'
export default {
data () {
return {
paginate: 1,
url: null,
loading: true,
mediaData: [],
media: [],
active: 0,
selected: this.$store.state.selectedMedia,
selectedIds: this.$store.state.selectedMediaIds,
media_dialog: this.$store.state.media_dialog,
dashboard_initialised: false,
types: ['Image', 'Video', 'PDF'],
type: this.$store.state.mediaType,
media_fetched: false,
selectionFinished: false
}
},
methods: {
getPaginate (end) {
var start = 1;
return new Array(end - start).fill().map((d, i) => i + start);
},
finishMediaSelection() {
this.selectionFinished = true;
this.$store.commit('mediaSelectionFinished', true);
this.selected = [];
this.getClass();
this.media_dialog = false;
setTimeout(() => {
/*
| Keep selection open for the next time
| Fixed bug for not able to select media after the first selection
*/
this.selectionFinished = false;
}, 100);
},
chosen (media) {
var selected = this.selected;
var index = this.selectedIds.indexOf(media.id);
var multiple = this.$store.state.selectMultipleMedia;
// If image exists in the array -> Remove it
if (index > -1) {
//this removes matched index
this.selected.splice(index, 1);
this.getClass(media);
return;
}
// If the selection is not multiple (Means single) then it resets this.selected array so that previously selected object (media) will be flushed
if (!multiple && selected && selected.length) {
this.selected = [];
}
//Finally adding media to the array and adding class selected
this.selected.push(media);
this.getClass(media);
},
getClass (media) {
if (media) {
var selected = this.selected;
for (var key in selected) {
if (selected && selected[key]) {
if (media.id == selected[key]['id']) {
return 'selected';
}
}
}
}
return '';
},
getAllMedia (page = 1) {
this.loading = true;
this.$axiosx.get('/media?page=' + page + '&type=' + this.type.toLowerCase())
.then((res) => {
this.loading = false;
this.media = res.data.data;
this.mediaData = res.data;
});
},
initUppy () {
var url = this.$store.state.api + '/media/upload';
Uppy().use(Dashboard, {
target: "#imageUploader",
id: 'Dashboard',
trigger: '#uppy-select-files',
inline: true,
width: '100%',
height: '100vh'
}).use(XHRUpload, {
endpoint: url,
getResponseData: ((responseText, response) => {
var res = JSON.parse(responseText);
if (res.status == 'error') {
this.$store.commit('snackbar', { status: true, message: res.message });
}
this.getAllMedia();
this.active = 0;
this.paginate = 1;
return {
url: res.url
}
})
})
}
},
watch: {
selected (list) {
// If this.selected has a length then it will update this.selectedIds
if (list && list.length) {
//Before pushing arrays into ids, resetting this.selectedIds to remove any duplication
this.selectedIds = [];
//Finally mapping list to this.selectedIds
list.map((l) => {
this.selectedIds.push(l.id);
});
} else {
//If list has no length then resetting this.selectedIds array
this.selectedIds = [];
}
//If selection finshed button is triggered then it should not update the change since this.selected & this.selectedIds would be resetted
if (!this.selectionFinished) {
this.$store.commit('setSelectedMedia', this.selected);
this.$store.commit('setSelectedMediaIds', this.selectedIds);
}
},
type (val) {
this.getAllMedia();
},
'$store.state.mediaType' (type) {
this.type = type;
},
paginate (number) {
this.getAllMedia(number);
},
media_dialog (val) {
if (val) {
// If media is not already fetched
if (!this.media_fetched) {
this.getAllMedia();
}
this.selected = this.$store.state.selectedMedia;
this.selectedIds = this.$store.state.selectedMediaIds;
//if media dialog is initialised and dashboard (uppy) is not initialised then initialise it
var t = this;
if (!this.dashboard_initialised) {
//Setting timeout as the function need to be awaited for the element to be rendered
setTimeout(function () {
t.initUppy();
}, 100);
this.dashboard_initialised = true;
}
}
if (!val) {
this.$store.commit('media_dialog', false);
}
},
'$store.state.media_dialog' (val) {
if (val) {
this.media_dialog = val;
}
},
}
}
</script>
<style>
a.uppy-Dashboard-poweredBy {
display: none;
}
#myUploader {
position: relative;
background-color: #fafafa;
max-width: 100%;
max-height: 100%;
min-height: 450px;
outline: none;
border: 1px solid #eaeaea;
border-radius: 5px;
}
div#innerWrap {
display: flex;
flex-flow: row wrap;
background: transparent;
padding: 0;
min-height: 560px;
overflow: auto;
}
._image {
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
margin: 15px;
cursor: pointer;
position: relative;
border: 1px solid #ccc;
width: calc(12.5% - 30px);
overflow: hidden;
height: 100px;
}
._addTrigger._image {
border: 1px solid #ccc;
}
._image img {
height: 100%;
width: 100%;
object-fit: contain;
object-position: center;
}
._addTrigger > ._inner {
width: 48px;
}
._addTrigger i {
font-size: 48px;
color: #999;
}
._cover {
position: absolute !important;
bottom: 10px;
}
.bg-gray {
background: #fafafa;
}
body .uppy-Dashboard-inner {
border-radius: 0;
max-height: 550px;
}
button._delete.v-btn.v-btn--floating.v-btn--small {
position: absolute;
top: -15px;
right: -15px;
visibility: hidden;
opacity: 0;
transition: .3s;
}
._image:hover button._delete.v-btn.v-btn--floating.v-btn--small {
visibility: visible;
opacity: 1;
}
@media(max-width: 768px) {
._image {
width: calc(25% - 30px);
margin: 15px 0;
}
}
@media (max-width: 640px) {
._image {
width: 100%;
}
}
#_media .v-dialog {
border-radius: 0;
}
#_mainContainer {
background: #f9f9f9;
border-top: 1px solid #f0f0f0;
}
#_mediaCard.v-card--loading {
overflow: visible;
}
._image.selected {
outline: 2px solid #3398f3;
border: 1px solid transparent;
}
._pdf {
text-align: center;
text-decoration: none;
display: flex;
flex-flow: column;
height: 100%;
font-size: 13px;
}
._pdf .v-icon.v-icon {
font-size: 80px;
color: #f64545;
background: #fff;
height: 80%;
}
._image video {
max-width: 100%;
}
._paginate {
max-width: 80px;
}
._layer {
position: absolute;
z-index: 0;
background: #000;
opacity: 0.2;
height: 100%;
width: 100%;
}
</style>
Step 11: Create File.vue file in components directory
<template>
<div class="mb-5 mt-5">
<v-btn
large
color="#444"
tile
outlined
@click="mediaDialog"
:block="block"
>
<v-icon left>mdi-cloud-upload-outline</v-icon>
{{ text }}
</v-btn>
<div
class="_fileZone flex row-wrap"
v-if="mediaList && mediaList.length"
>
<div
:class="'_zone ' + cls"
v-for="(m, i) in mediaList"
:key="i"
>
<input v-if="name" type="hidden" :name="getName()" :value="m.id">
<a
v-if="m.type == 'application' && (m.format == 'application/pdf' || m.format == 'pdf')"
:href="m.url"
class="_pdf"
target="_blank"
>
<v-icon>mdi-file-pdf-box</v-icon>
{{ m.name }}
</a>
<img
v-if="m.type == 'image'"
:src="m.url"
:alt="m.name"
>
<video
controls
v-if="m.url && m.type == 'video' && m.format != 'embeded'"
>
<source :src="m.url" />
</video>
<div v-html="m.name" v-if="m.url && m.type == 'video' && m.format == 'embeded'">
</div>
</div>
</div>
</div>
</template>
<script>
import MediaDialog from './MediaDialog'
export default {
props: {
multiple: {
default: true
},
text: {
default: 'Select Media'
},
name: {
},
block: {
default: false
},
cls: {
default: ''
},
type: {
default: 'Image'
},
value: {
default: function () {
return '';
}
}
},
data () {
return {
active: false,
mediaList: [],
mediaListIds: []
}
},
mounted () {
this.iterateMediaList();
},
methods: {
iterateMediaList (list = null) {
if (!list) {
list = this.value;
}
if (list) {
if (!this.multiple) {
var mediaList = new Array();
mediaList.push(list);
} else {
var mediaList = list;
}
this.mediaList = mediaList;
}
},
getName () {
if (this.multiple) {
return this.name + '[]';
}
return this.name;
},
mediaDialog () {
this.active = true;
this.$store.commit('setSelectMultipleMedia', this.multiple);
this.$store.commit('setSelectedMedia', this.mediaList);
this.$store.commit('setSelectedMediaIds', this.mediaListIds);
this.$store.commit('setMediaType', this.type);
this.$store.commit('media_dialog', true);
}
},
watch: {
'$store.state.mediaSelectionFinished' (state) {
if (state && this.active) {
this.mediaList = this.$store.state.selectedMedia;
this.mediaListIds = this.$store.state.selectedMediaIds;
this.active = false;
this.$store.commit('mediaSelectionFinished', false);
}
},
value (list) {
this.iterateMediaList(list);
},
mediaList (list) {
}
}
}
</script>
<style lang="scss">
._fileZone ._zone {
padding: 10px;
border: 1px solid #ccc;
margin: 7.5px;
display: flex;
height: 200px;
align-items: center;
overflow: hidden;
width: calc(20% - 15px);
}
._fileZone ._zone img {
max-width: 100%;
margin: auto;
display: block;
height: 100%;
}
._fileZone {
margin: 10px -7.5px;
overflow: auto;
display: flex;
align-items: center;
max-width: 200%;
}
a._pdf {
width: 100%;
color: #444 !important;
}
._zone video, ._zone iframe {
max-width: 100%;
}
._fileZone ._zone._block {
width: 100%;
}
</style>
Step 12: Create RichTextarea.vue file in components directory
<template>
<div class="mb-5 mt-5">
<div class="v-label theme--light mb-3" v-text="label"></div>
<textarea
id="editor"
>
</textarea>
</div>
</template>
<script>
export default {
props: {
label: {
default: 'Content'
}
},
mounted () {
}
}
</script>
Step 13: Create GeoComplete.vue file in components directory
<template>
<div>
<vuetify-google-autocomplete
:id="id"
append-icon="mdi-map-marker"
:disabled="false"
:placeholder="placeholder"
:label="label"
v-model="location"
v-on:placechanged="getAddressData"
:country="country"
>
</vuetify-google-autocomplete>
<input type="hidden" name="street1" v-if="street1" :value="street1">
<input type="hidden" name="city" v-if="city" :value="city">
<input type="hidden" name="state" v-if="state" :value="state">
<input type="hidden" name="zip" v-if="zip" :value="zip">
<input type="hidden" name="lat" v-if="lat" :value="lat">
<input type="hidden" name="lng" v-if="lng" :value="lng">
</div>
</template>
<script>
import Vue from 'vue'
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete'
Vue.use(VuetifyGoogleAutocomplete, {
apiKey: 'YOUR_API_KEY'
});
export default {
props: {
obj: {},
id: {
default: 'map'
},
placeholder: {
default: 'Start typing'
},
label: {
default: 'Location'
},
name: {
default: 'location'
},
value: {
default: function () {
return [];
}
}
},
data () {
return {
street1: null,
city: null,
state: null,
zip: null,
lat: null,
lng: null,
lng: null,
country: ['us'],
location: ''
}
},
watch: {
'value' : function (val) {
if (val) {
console.log(val);
this.location = val.location;
this.street1 = val.street;
this.city = val.city;
this.state = val.state;
this.zip = val.zip;
this.lat = val.lat;
this.lng = val.lng;
}
}
},
methods: {
getAddressData (addressData, placeResultData, id) {
if (addressData) {
// this.location = placeResultData.formatted_address;
this.street1 = addressData.name;
this.city = addressData.locality;
this.state = addressData.administrative_area_level_1;
this.zip = addressData.postal_code;
this.lat = addressData.latitude;
this.lng = addressData.longitude;
addressData.location = this.location;
this.$emit('geo', addressData);
}
}
}
}
</script>
Make sure you get your Google Autocomplete API Key following this instructions https://developers.google.com/places/web-service/get-api-key
In the next tutorial, I will explain more about Laravel routes to have this Laravel Vue Admin working 100%. Stay Tuned