<!DOCTYPE html>
<html lang="en" style="overflow: hidden;">
<head>
<meta charset="utf-8">
<title>Vue Vuetify Dialog Dynamic</title>
<link href="https://cdn.jsdelivr.net/npm/@mdi/font/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.5.10/dist/vuetify.min.css" rel="stylesheet">
</head>
<body>
<div id="app">
<v-app>
<!-- Global Snackbar -->
<v-snackbar v-model="snackbar.show" :timeout="snackbar.timeout" :color="snackbar.color">
{{ snackbar.text }}
<template v-slot:action="{ attrs }">
<v-btn text v-bind="attrs" @click="snackbar.show = false">Tutup</v-btn>
</template>
</v-snackbar>
<!-- Dialog Stack -->
<template v-for="(dialog, index) in dialogs">
<v-dialog
:key="dialog.id"
v-model="dialog.show"
:max-width="dialog.width"
:persistent="dialog.persistent"
:z-index="500 + index * 10"
>
<v-card v-if="dialog.type !== 'loading'">
<v-toolbar dark class="mb-4" color="primary">
<v-toolbar-title>{{ dialog.title }}</v-toolbar-title>
</v-toolbar>
<v-card-text>
<div v-if="dialog.type === 'info'">{{ dialog.content }}</div>
<component v-else :is="dialog.template" v-model="dialog.data"></component>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
@click="() => {
dialog.actions.ok.action(dialog.data);
if (dialog.actions.ok.close !== false) {
closeDialog(dialog.id);
}
}"
>
{{ dialog.actions.ok.title || 'OK' }}
</v-btn>
<v-btn
v-if="dialog.actions.cancel"
text
@click="() => {
if (dialog.actions.cancel.action) {
dialog.actions.cancel.action(dialog.data);
}
closeDialog(dialog.id);
}"
>
{{ dialog.actions.cancel.title || 'Cancel' }}
</v-btn>
</v-card-actions>
</v-card>
<v-card v-else flat tile>
<v-card-text>
<div style="display: flex; justify-content: center; align-items: center; height: 100px;">
<v-progress-circular indeterminate color="primary" size="40" />
</div>
</v-card-text>
</v-card>
</v-dialog>
</template>
<router-view />
</v-app>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.5.10/dist/vuetify.js"></script>
<script>
const crud = (op, sheetName, params) => {
const db = JSON.parse(localStorage.getItem('localDB')) || {};
if (!db[sheetName]) db[sheetName] = { headers: [], rows: [] };
const sheet = db[sheetName];
const res = { status: '', data: [], message: '' };
if (op === 'create') {
JSON.parse(params).forEach(d => {
if (!sheet.headers.length) sheet.headers = Object.keys(d);
sheet.rows.push(sheet.headers.map(h => d[h] || ''));
});
localStorage.setItem('localDB', JSON.stringify(db));
res.status = 'success';
} else if (op === 'read') {
res.data = sheet.rows.map(row => sheet.headers.reduce((o, h, i) => (o[h] = row[i], o), {}));
res.status = 'success';
}
return JSON.stringify(res);
};
const Dashboard = {
template: `<v-container>
<h2>Dashboard</h2>
<p>Selamat datang di dashboard.</p>
</v-container>`
};
const ListView = {
data: () => ({ items: [], username: '' }),
created() {
const user = localStorage.getItem('aktif');
this.username = user;
const key = `items-${user}`;
if (!JSON.parse(crud("read", key)).data.length)
crud("create", key, JSON.stringify([{ name: "Item A", email: "" }]));
this.items = JSON.parse(crud("read", key)).data;
},
methods: {
async editItem(item) {
const editDialogId = this.$root.showDialog({
type: 'form',
title: 'Edit Data',
content: `
<v-text-field label="Nama" v-model="value.name"></v-text-field>
<v-text-field label="Email" v-model="value.email"></v-text-field>
`,
data: { name: item.name, email: item.email },
actions: {
ok: {
title: 'Simpan',
close: false,
action: async (data) => {
const confirm = await new Promise(resolve => {
const confirmDialogId = this.$root.showDialog({
type: 'info',
title:'Simpan',
content: 'Yakin ingin menyimpan perubahan?',width: '300px',
actions: {
ok: {
title: 'Ya',
action: () => {
this.$root.closeDialog(confirmDialogId);
resolve(true);
}
},
cancel: {
title: 'Tidak',
action: () => {
this.$root.closeDialog(confirmDialogId);
resolve(false);
}
}
}
});
});
if (confirm) {
const loadingDialogId = this.$root.showDialog({
type: 'loading',
content: '',
width: '100px',
persistent: true,
actions: { ok: { show: false, action: () => {} } }
});
try {
await new Promise(resolve => setTimeout(resolve, 1000));
const key = `items-${this.username}`;
const all = JSON.parse(crud("read", key)).data;
const index = all.findIndex(x => x.name === item.name);
if (index !== -1) {
all[index] = data;
localStorage.setItem('localDB', JSON.stringify({
...JSON.parse(localStorage.getItem('localDB')),
[key]: {
headers: ['name', 'email'],
rows: all.map(d => [d.name, d.email])
}
}));
this.items = all;
this.$root.showSnackbar("Data berhasil disimpan!", "success");
}
} finally {
this.$root.closeDialog(loadingDialogId);
this.$root.closeDialog(editDialogId);
}
}
}
},
cancel: {
title: 'Batal',
action: () => {
this.$root.showSnackbar("Edit dibatalkan", "info");
}
}
}
});
}
},
template: `<v-container>
<v-simple-table dense>
<thead><tr><th>Nama</th></tr></thead>
<tbody>
<tr v-for="(item, i) in items" :key="i" @click="editItem(item)">
<td>{{ item.name }}</td>
</tr>
</tbody>
</v-simple-table>
</v-container>`
};
const Login = {
data: () => ({ isRegister: false, user: { username: '', password: '' } }),
methods: {
toggleMode() { this.isRegister = !this.isRegister },
submit() {
const { username, password } = this.user;
if (!username || !password) {
this.$root.showSnackbar('Username dan password harus diisi','warning');
return;
}
const users = JSON.parse(crud("read", "users")).data;
if (this.isRegister) {
if (users.find(u => u.username === username)) {
this.$root.showSnackbar('Username sudah terdaftar!','warning');
return;
}
crud("create", "users", JSON.stringify([this.user]));
this.$root.showSnackbar('Registrasi berhasil', 'success');
this.toggleMode();
} else {
const match = users.find(u => u.username === username && u.password === password);
if (match) {
localStorage.setItem('aktif', username);
this.$root.showSnackbar('Login berhasil', 'success');
this.$router.push("/app/dashboard");
} else {
this.$root.showSnackbar('Login Gagal, username/password salah!', 'error');
}
}
}
},
template: `<v-container fill-height>
<v-row align="center" justify="center">
<v-col cols="12" md="10">
<v-card class="mx-auto" :style="{ maxWidth: '290px' }">
<v-toolbar dark color="primary">
<v-toolbar-title>{{ isRegister ? 'Register' : 'Login' }}</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-text-field v-model="user.username" label="Username" />
<v-text-field v-model="user.password" label="Password" type="password" />
</v-card-text>
<v-card-actions>
<v-btn color="primary" @click="submit">{{ isRegister ? 'Register' : 'Login' }}</v-btn>
<v-spacer></v-spacer>
<v-btn text @click="toggleMode">
{{ isRegister ? 'Sudah punya akun?' : 'Belum punya akun?' }}
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>`
};
const appChildRoutes = [
{ path: 'dashboard', title: 'Dashboard', icon: 'mdi-view-dashboard', component: Dashboard },
{ path: 'list', title: 'List View', icon: 'mdi-format-list-bulleted', component: ListView }
];
const AppLayout = {
data: () => ({ drawer: false, links: appChildRoutes }),
computed: {
currentTitle() {
return (this.links.find(r => '/app/' + r.path === this.$route.path) || {}).title || "My App";
}
},
methods: {
go(path) {
const full = '/app/' + path;
if (this.$route.path !== full) this.$router.push(full);
this.drawer = false;
},
logout() {
localStorage.removeItem('aktif');
this.$root.showSnackbar("Berhasil logout", "info");
this.$router.push('/');
}
},
template: `<v-app>
<v-navigation-drawer app v-model="drawer">
<v-list dense>
<v-list-item v-for="r in links" :key="r.path" link @click="go(r.path)">
<v-list-item-icon><v-icon>{{ r.icon }}</v-icon></v-list-item-icon>
<v-list-item-title>{{ r.title }}</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item link @click="logout">
<v-list-item-icon><v-icon>mdi-logout</v-icon></v-list-item-icon>
<v-list-item-title>Logout</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar app color="primary" dark>
<v-app-bar-nav-icon @click="drawer = !drawer" />
<v-toolbar-title>{{ currentTitle }}</v-toolbar-title>
</v-app-bar>
<v-main><router-view /></v-main>
</v-app>`
};
const router = new VueRouter({
routes: [
{ path: '/', component: Login },
{ path: '/app', component: AppLayout, children: appChildRoutes }
]
});
router.beforeEach((to, from, next) => {
const loggedIn = localStorage.getItem('aktif');
if (to.path.startsWith('/app') && !loggedIn) {
next('/');
} else {
next();
}
});
new Vue({
el: '#app',
vuetify: new Vuetify(),
router,
data: () => ({
dialogs: [],
snackbar: {
show: false,
text: '',
color: 'success',
timeout: 3000
},
defaultDialog: {
type: '',
title: '',
template: null,
content: null,
data: null,
width: '500px',
persistent: true,
actions: {
ok: { title: 'OK', action: () => {} },
cancel: null
}
}
}),
created() {
localStorage.removeItem('aktif');
},
methods: {
showDialog(typeOrOptions, title = '', content = '') {
let options = {};
if (typeof typeOrOptions === 'string') {
options = { type: typeOrOptions, title, content };
} else {
options = typeOrOptions;
}
const dialog = {
...this.defaultDialog,
...options,
id: 'dialog-' + Date.now() + Math.random().toString(36).substr(2, 5),
show: true
};
if (options.type === 'form') {
const componentId = 'dynamic-form-' + dialog.id;
Vue.component(componentId, {
props: ['value'],
template: `<div>${options.content}</div>`
});
dialog.template = componentId;
} else if (options.type === 'loading') {
const componentId = 'loading-dialog-' + dialog.id;
Vue.component(componentId, {
template: `
<div style="display: flex; justify-content: center; align-items: center; height: 100px;">
<v-progress-circular indeterminate color="primary" size="40" />
</div>
`
});
dialog.template = componentId;
}
this.dialogs.push(dialog);
return dialog.id;
},
closeDialog(id) {
this.dialogs = this.dialogs.filter(dialog => dialog.id !== id);
},
showSnackbar(text, color = 'success') {
this.snackbar.text = text;
this.snackbar.color = color;
this.snackbar.show = true;
}
}
});
</script>
</body>
</html>