Learning Quasar V1: Cross-Platform Apps (with Vue 2, Vuex & Firebase) Notes
In my opinion, the best thing I learned in here was how to manage state with vuex. Finally understood how this state management works.
Intro Vue - check here if needed to refresh your vue knowledge
Routes and Layouts
Example
router/routes.js
const routes = [
{
path: '/',
component: () => import('layouts/Layout.vue'),
children: [
{
path: '',
component: () => import('pages/PageTodo.vue')
},
{
path: '/settings',
component: () => import('pages/PageSettings.vue')
}
]
},
// Always leave this as last one,
// but you can also remove it
{
path: '*',
component: () => import('pages/Error404.vue')
}
]
export default routes
layouts/Layout.vue
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated>
<q-toolbar>
<q-toolbar-title class="absolute-center">
Awesome App
</q-toolbar-title>
</q-toolbar>
</q-header>
<q-footer>
<q-tabs>
<q-route-tab
v-for="(nav, index) in navs"
:to="nav.to"
:icon="nav.icon"
:label="nav.label"
:key="index" />
</q-tabs>
</q-footer>
<q-drawer
v-model="leftDrawerOpen"
:width="250"
show-if-above
bordered
:breakpoint="767"
content-class="bg-primary"
>
<q-list dark>
<q-item-label
header
class="text-white"
>
Navigation
</q-item-label>
<q-item
v-for="(nav, index) in navs"
:to="nav.to"
exact
clickable
:key=index
class="text-white">
<q-item-section avatar>
<q-icon :name="nav.icon" />
</q-item-section>
<q-item-section>
{{nav.label}}
</q-item-section>
</q-item>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script>
export default {
name: 'MainLayout',
data () {
return {
leftDrawerOpen: false,
navs: [
{
label: 'Todo',
icon: 'list',
to: '/'
},
{
label: 'Settings',
icon: 'settings',
to: '/settings'
}
]
}
}
}
</script>
<style scoped lang="scss">
@media screen and (min-width:768px) {
.q-footer {
display:none;
}
}
.q-drawer {
.q-router-link--exact-active {
font-weight:bold;
}
}
</style>
Vuex Set state
pages/PageTodo.vue
<template>
<q-page class="q-pa-md">
<div class="q-pa-md" style="max-width: 350px">
<q-list
separator
bordered >
<task v-for="(task, key) in tasks"
:key="key"
:task="task"
:id="key"
></task>
</q-list>
</div>
</q-page>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters('tasks', ['tasks'])
},
components: {
'task' : require('components/Tasks/Task.vue').default
}
}
</script>
<style scoped>
</style>
Create components/Tasks/Task.vue
<template>
<q-item
@click="task.completed = !task.completed"
:class="!task.completed ? 'bg-orange-1' : 'bg-green-1'"
clickable
v-ripple>
<q-item-section side top>
<q-checkbox v-model="task.completed" />
</q-item-section>
<q-item-section>
<q-item
:class="{ 'text-strike' : task.completed }"
>{{ task.name }}</q-item>
</q-item-section>
<q-item-section side >
<div class="row">
<div class="column justify-center">
<q-icon
size="18px"
name="event"
class="q-mr-xs" />
</div>
<div class="column">
<q-item-label
class="row justify-end"
caption>{{ task.dueDate }}</q-item-label>
<q-item-label caption>
<small>{{ task.dueTime }}</small>
</q-item-label>
</div>
</div>
</q-item-section>
</q-item>
</template>
<script>
export default {
props: ['task', 'id']
}
</script>
Create store/store-tasks.js
store/store-tasks.js
const state = {
tasks: {
'ID1' : {
name:'Go to shop',
completed: false,
dueDate: '2019/05/12',
dueTime: '18:30'
},
'ID2' : {
name:'Get bananas',
completed: true,
dueDate: '2019/05/13',
dueTime: '5:30'
},
'ID3' : {
name:'Get apples',
completed: false,
dueDate: '2019/05/14',
dueTime: '3:30'
}
}
}
const mutations = {
}store/store-tasks.js
const state = {
tasks: {
'ID1' : {
name:'Go to shop',
completed: false,
dueDate: '2019/05/12',
dueTime: '18:30'
},
'ID2' : {
name:'Get bananas',
completed: true,
dueDate: '2019/05/13',
dueTime: '5:30'
},
'ID3' : {
name:'Get apples',
completed: false,
dueDate: '2019/05/14',
dueTime: '3:30'
}
}
}
const mutations = {
}
const actions = {
}
const getters = {
tasks: (state) => {
return state.tasks
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
const actions = {
}
const getters = {
tasks: (state) => {
return state.tasks
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
in store/index.js add these
import tasks from './store-tasks'
modules: {
tasks
},
Vuex Action and Mutation
Dialog
// Quasar plugins
plugins: [
'Notify',
'Dialog'
]
promptToDelete(id) {
this.$q.dialog({
title: 'Confirm',
message: 'Really delete?',
ok: {
push: true
},
cancel: {
color: 'negative'
},
persistent: true
}).onOk(() => {
this.deleteTask(id)
})
}
Mostly in Delete and Update
store/store-tasks.js
import Vue from 'vue'
const state = {
tasks: {
'ID1' : {
name:'Go to shop',
completed: false,
dueDate: '2019/05/12',
dueTime: '18:30'
},
'ID2' : {
name:'Get bananas',
completed: false,
dueDate: '2019/05/13',
dueTime: '5:30'
},
'ID3' : {
name:'Get apples',
completed: false,
dueDate: '2019/05/14',
dueTime: '3:30'
}
}
}
const mutations = {
updateTask(state, payload) {
Object.assign(state.tasks[payload.id], payload.updates)
},
deleteTask(state,id) {
Vue.delete(state.tasks, id)
}
}
const actions = {
updateTask({ commit }, payload) {
commit('updateTask', payload)
},
deleteTask({ commit }, id) {
commit('deleteTask', id)
delete state.tasks['id']
}
}
const getters = {
tasks: (state) => {
return state.tasks
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
components/Tasks/Task.vue
<template>
<q-item
@click="updateTask({id: id, updates: {completed: !task.completed}})"
:class="!task.completed ? 'bg-orange-1' : 'bg-green-1'"
clickable
v-ripple>
<q-item-section side top>
<q-checkbox
:value="task.completed"
class="no-pointer-events" />
</q-item-section>
<q-item-section>
<q-item
:class="{ 'text-strike' : task.completed }"
>{{ task.name }}</q-item>
</q-item-section>
<q-item-section side >
<div class="row">
<div class="column justify-center">
<q-icon
size="18px"
name="event"
class="q-mr-xs" />
</div>
<div class="column">
<q-item-label
class="row justify-end"
caption>{{ task.dueDate }}</q-item-label>
<q-item-label caption>
<small>{{ task.dueTime }}</small>
</q-item-label>
</div>
</div>
</q-item-section>
<q-item-section side >
<q-btn
@click.stop="promptToDelete(id)"
flat
round
dense
color="red"
icon="delete" />
</q-item-section>
</q-item>
</template>
<script>
import { mapActions } from 'vuex'
export default {
props: ['task', 'id'],
methods: {
...mapActions('tasks', ['updateTask', 'deleteTask']),
promptToDelete(id) {
this.$q.dialog({
title: 'Confirm',
message: 'Really delete?',
ok: {
push: true
},
cancel: {
color: 'negative'
},
persistent: true
}).onOk(() => {
this.deleteTask(id)
})
}
}
}
</script>
Form, Fields, validation
components/Tasks/Modals/AddTask
<template>
<q-card>
<q-card-section class="row">
<div class="text-h6">Add Task</div>
<q-space />
<q-btn
v-close-popup
dense
flat
round
icon="close" />
</q-card-section>
<form @submit.prevent="submitForm">
<q-card-section>
<div class="row q-mb-sm">
<q-input outlined
v-model="taskToSubmit.name" label="Task Name"
:rules="[val => !!val || 'Field is required']"
ref="name"
autofocus
class="col">
<template v-slot:append>
<q-icon
v-if="taskToSubmit.name"
@click="taskToSubmit.name = ''"
name="close"
class="cursor-pointer" />
</template>
</q-input>
</div>
<div class="row q-mb-sm">
<q-input
outlined
v-model="taskToSubmit.dueDate"
label="Due Date">
<template v-slot:append>
<q-icon
v-if="taskToSubmit.dueDate"
@click="clearDueDate"
name="close"
class="cursor-pointer" />
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy>
<q-date v-model="taskToSubmit.dueDate" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div
v-if="taskToSubmit.dueDate"
class="row q-mb-sm">
<q-input
outlined
label="Due time"
v-model="taskToSubmit.dueTime"
class="col">
<template v-slot:append>
<q-icon
v-if="taskToSubmit.dueTime"
@click="taskToSubmit.dueTime = ''"
name="close"
class="cursor-pointer" />
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy>
<q-time v-model="taskToSubmit.dueTime" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
type="submit"
label="Save"
color="primary" />
</q-card-actions>
</form>
</q-card>
</template>
<script>
import { mapActions } from 'vuex'
export default {
data() {
return {
taskToSubmit: {
name:'',
completed: false,
dueDate: '',
dueTime: ''
}
}
},
methods: {
...mapActions('tasks', ['addTask']),
submitForm() {
this.$refs.name.validate()
if (!this.$refs.name.hasError) {
this.submitTask()
}
},
submitTask() {
this.addTask(this.taskToSubmit)
this.$emit('close')
},
clearDueDate(){
this.taskToSubmit.dueDate = ''
this.taskToSubmit.dueTime = ''
}
}
}
</script>
adding button and modal add task
pages/PageTodo
<template>
<q-page class="q-pa-md">
<div class="q-pa-md">
<q-list
v-if=Object.keys(tasks).length
separator
bordered >
<task v-for="(task, key) in tasks"
:key="key"
:task="task"
:id="key"
></task>
</q-list>
<div class="absolute-bottom text-center q-mb-lg">
<q-btn
@click="showAddTask=true"
round
color="primary"
size="24px"
icon="add" />
</div>
<q-dialog
v-model="showAddTask">
<add-task @close="showAddTask = false" />
</q-dialog>
</div>
</q-page>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data() {
return {
showAddTask: false
}
},
computed: {
...mapGetters('tasks', ['tasks'])
},
components: {
'task' : require('components/Tasks/Task.vue').default,
'add-task' : require('components/Tasks/Modals/AddTask.vue').default
}
}
</script>
<style scoped>
</style>
components/AddTask.vue
<template>
<q-card>
<q-card-section class="row">
<div class="text-h6">Add Task</div>
<q-space />
<q-btn
v-close-popup
dense
flat
round
icon="close" />
</q-card-section>
<form @submit.prevent="submitForm">
<q-card-section>
<div class="row q-mb-sm">
<q-input outlined
v-model="taskToSubmit.name" label="Task Name"
:rules="[val => !!val || 'Field is required']"
ref="name"
autofocus
class="col">
<template v-slot:append>
<q-icon
v-if="taskToSubmit.name"
@click="taskToSubmit.name = ''"
name="close"
class="cursor-pointer" />
</template>
</q-input>
</div>
<div class="row q-mb-sm">
<q-input
outlined
v-model="taskToSubmit.dueDate"
label="Due Date">
<template v-slot:append>
<q-icon
v-if="taskToSubmit.dueDate"
@click="clearDueDate"
name="close"
class="cursor-pointer" />
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy>
<q-date v-model="taskToSubmit.dueDate" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div
v-if="taskToSubmit.dueDate"
class="row q-mb-sm">
<q-input
outlined
label="Due time"
v-model="taskToSubmit.dueTime"
class="col">
<template v-slot:append>
<q-icon
v-if="taskToSubmit.dueTime"
@click="taskToSubmit.dueTime = ''"
name="close"
class="cursor-pointer" />
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy>
<q-time v-model="taskToSubmit.dueTime" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
type="submit"
label="Save"
color="primary" />
</q-card-actions>
</form>
</q-card>
</template>
<script>
import { mapActions } from 'vuex'
export default {
data() {
return {
taskToSubmit: {
name:'',
completed: false,
dueDate: '',
dueTime: ''
}
}
},
methods: {
...mapActions('tasks', ['addTask']),
submitForm() {
this.$refs.name.validate()
if (!this.$refs.name.hasError) {
this.submitTask()
}
},
submitTask() {
this.addTask(this.taskToSubmit)
this.$emit('close')
},
clearDueDate(){
this.taskToSubmit.dueDate = ''
this.taskToSubmit.dueTime = ''
}
}
}
</script>
store/store-tasks.vue
import Vue from 'vue'
import { uid } from 'quasar'
const state = {
tasks: {
// 'ID1' : {
// name:'Go to shop',
// completed: false,
// dueDate: '2019/05/12',
// dueTime: '18:30'
// },
// 'ID2' : {
// name:'Get bananas',
// completed: false,
// dueDate: '2019/05/13',
// dueTime: '5:30'
// },
// 'ID3' : {
// name:'Get apples',
// completed: false,
// dueDate: '2019/05/14',
// dueTime: '3:30'
// }
}
}
const mutations = {
updateTask(state, payload) {
Object.assign(state.tasks[payload.id], payload.updates)
},
deleteTask(state,id) {
Vue.delete(state.tasks, id)
},
addTask(state, payload) {
Vue.set(state.tasks, payload.id, payload.task)
}
}
const actions = {
updateTask({ commit }, payload) {
commit('updateTask', payload)
},
deleteTask({ commit }, id) {
commit('deleteTask', id)
delete state.tasks['id']
},
addTask({ commit }, task) {
let taskId = uid()
let payload = {
id: taskId,
task: task
}
commit('addTask', payload)
}
}
const getters = {
tasks: (state) => {
return state.tasks
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
Child Component in-depth
components/Tasks/Modal/AddTask.vue
<template>
<q-card>
<modal-header>Add Task</modal-header>
<form @submit.prevent="submitForm">
<q-card-section>
<modal-task-name :name.sync="taskToSubmit.name"
ref="modalTaskName" />
<modal-due-date
:dueDate.sync="taskToSubmit.dueDate"
@clear="clearDueDate" />
<modal-due-time
v-if="taskToSubmit.dueDate"
:dueTime.sync="taskToSubmit.dueTime" />
</q-card-section>
<modal-buttons></modal-buttons>
</form>
</q-card>
</template>
<script>
import { mapActions } from 'vuex'
export default {
data() {
return {
taskToSubmit: {
name:'',
completed: false,
dueDate: '',
dueTime: ''
}
}
},
methods: {
...mapActions('tasks', ['addTask']),
submitForm() {
this.$refs.modalTaskName.$refs.name.validate()
if (!this.$refs.modalTaskName.$refs.name.hasError) {
this.submitTask()
}
},
submitTask() {
this.addTask(this.taskToSubmit)
this.$emit('close')
},
clearDueDate(){
this.taskToSubmit.dueDate = ''
this.taskToSubmit.dueTime = ''
}
},
components: {
'modal-header': require('components/Tasks/Modals/Shared/ModalHeader.vue').default,
'modal-task-name': require('components/Tasks/Modals/Shared/ModalTaskName.vue').default,
'modal-due-date': require('components/Tasks/Modals/Shared/ModalDueDate.vue').default,
'modal-due-time': require('components/Tasks/Modals/Shared/ModalDueTime.vue').default,
'modal-buttons': require('components/Tasks/Modals/Shared/ModalButtons.vue').default,
}
}
</script>
<modal-header>Add Task</modal-header>
components/Tasks/Modals/Shared/ModalHeader.vue
<template>
<q-card-section class="row">
<div class="text-h6"><slot></slot></div>
<q-space />
<q-btn
v-close-popup
dense
flat
round
icon="close" />
</q-card-section>
</template>
<script>
export default {
}
</script>
<modal-task-name :name.sync="taskToSubmit.name" ref="modalTaskName" />
components/Tasks/Modals/Shared/ModalTaskName.vue
<template>
<div class="row q-mb-sm">
<q-input outlined
:value="name"
@input="$emit('update:name', $event)"
:rules="[val => !!val || 'Field is required']"
ref="name"
autofocus
class="col">
<template v-slot:append>
<q-icon
v-if="name"
@click="$emit('update:name', '')"
name="close"
class="cursor-pointer" />
</template>
</q-input>
</div>
</template>
<script>
export default {
props: ['name']
}
</script>
<modal-due-date :dueDate.sync="taskToSubmit.dueDate" @clear="clearDueDate" />
components/Tasks/Modals/Shared/ModalDueDate.vue
<template>
<div class="row q-mb-sm">
<q-input
outlined
:value="dueDate"
@input="$emit('update:dueDate', $event)"
label="Due Date">
<template v-slot:append>
<q-icon
v-if="dueDate"
@click="$emit('clear')"
name="close"
class="cursor-pointer" />
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy>
<q-date :value="dueDate" @input="$emit('update:dueDate', $event)"/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</template>
<script>
export default {
props: ['dueDate']
}
</script>
<modal-due-time v-if="taskToSubmit.dueDate" :dueTime.sync="taskToSubmit.dueTime" />
components/Tasks/Modals/Shared/ModalDueTime.vue
<template>
<div
class="row q-mb-sm">
<q-input
outlined
label="Due time"
:value="dueTime"
@input="$emit('update:dueTime', $event)"
class="col">
<template v-slot:append>
<q-icon
v-if="dueTime"
@click="$emit('update:dueTime', '')"
name="close"
class="cursor-pointer" />
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy>
<q-time
:value="dueTime"
@input="$emit('update:dueTime', $event)" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</template>
<script>
export default {
props: ['dueTime']
}
</script>
<modal-buttons></modal-buttons>
components/Tasks/Modals/Shared/ModalDueTime.vue
<template>
<q-card-actions align="right">
<q-btn
type="submit"
label="Save"
color="primary" />
</q-card-actions>
</template>
Split tasks, event global listener
store/store-tasks.js
const getters = {
tasksTodo: (state) => {
let tasks = {}
Object.keys(state.tasks).forEach(function(key) {
let task = state.tasks[key]
if (!task.completed) {
tasks[key] = task
}
})
return tasks
},
tasksCompleted: (state) => {
let tasks = {}
Object.keys(state.tasks).forEach(function(key) {
let task = state.tasks[key]
if (task.completed) {
tasks[key] = task
}
})
return tasks
}
}
on mounted this.$root.$on('showAddTask', () => { this.showAddTask = true }) this listen from the event from NoTasks.vue ← global event listener
<template>
<q-page class="q-pa-md">
<div class="q-pa-md">
<no-tasks
v-if=!Object.keys(tasksTodo).length
class="q-mb-lg"></no-tasks>
<tasks-todo
v-else
:tasksTodo="tasksTodo"
/>
<tasks-completed
v-if=Object.keys(tasksCompleted).length
:tasksCompleted="tasksCompleted"
/>
<div class="absolute-bottom text-center q-mb-lg">
<q-btn
@click="showAddTask=true"
round
color="primary"
size="24px"
icon="add" />
</div>
<q-dialog
v-model="showAddTask">
<add-task @close="showAddTask = false" />
</q-dialog>
</div>
</q-page>
</template>
<script>
import { mapGetters } from 'vuex'
import TasksTodo from '../components/Tasks/TasksTodo.vue'
export default {
data() {
return {
showAddTask: false
}
},
computed: {
...mapGetters('tasks', ['tasksTodo', 'tasksCompleted'])
},
mounted() {
this.$root.$on('showAddTask', () => {
this.showAddTask = true
})
},
components: {
'add-task' : require('components/Tasks/Modals/AddTask.vue').default,
'tasks-todo' : require('components/Tasks/TasksTodo.vue').default,
'tasks-completed' : require('components/Tasks/TasksCompleted.vue').default,
'no-tasks' : require('components/Tasks/NoTasks.vue').default
}
}
</script>
<style scoped>
</style>
components/Tasks/TasksTodo.vue
<template>
<div>
<list-header
bgcolor="bg-orange-4"
>Todo</list-header>
<q-list
separator
bordered >
<task v-for="(task, key) in tasksTodo"
:key="key"
:task="task"
:id="key"
></task>
</q-list>
</div>
</template>
<script>
export default {
props: ['tasksTodo'],
components: {
'task' : require('components/Tasks/Task.vue').default,
'list-header' : require('components/Shared/ListHeader.vue').default,
}
}
</script>
components/Tasks/TasksCompleted.vue
<template>
<div class="q-mt-lg">
<list-header bgcolor="bg-green-4">Completed</list-header>
<q-list
separator
bordered >
<task v-for="(task, key) in tasksCompleted"
:key="key"
:task="task"
:id="key"
></task>
</q-list>
</div>
</template>
<script>
export default {
props: ['tasksCompleted'],
components: {
'task' : require('components/Tasks/Task.vue').default,
'list-header' : require('components/Shared/ListHeader.vue').default,
}
}
</script>
@click="$root.$emit('showAddTask')" this will fire event to PageTodo.vue
components/Tasks/NoTasks.vue
<template>
<q-banner class="bg-grey-3">
<template v-slot:avatar>
<q-icon name="check" color="primary" />
</template>
No tasks to do today!
<template v-slot:action>
<q-btn
@click="$root.$emit('showAddTask')"
flat
color="primary"
label="Add Task" />
</template>
</q-banner>
</template>
<script>
export default {
}
</script>
had trouble in here, because the the parent page uses just bgcolor where before it always uses :bgcolor, i think the difference is in, if the value is static just normal string then in the parent use bgcolor instead of :bgcolor where you need to have value in data
<template>
<q-banner
dense
inline-actions
:class="bgcolor"
class="list-header text-white text-center">
<span class="text-bold text-subtitle1"><slot></slot></span>
</q-banner>
</template>
<script>
export default {
props: ['bgcolor']
}
</script>
<style>
.list-header {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
</style>
Add search bar
Note : Why are using mapState instead of creating a Getter? Because we don't need to manipulate this property before we get it (as we did with task). So we might as well map directly from the state.
components/Tasks/Tools/Search.vue
<template>
<q-input
outlined
class="col"
v-model="searchField"
label="Search">
<template v-slot:append>
<q-icon v-if="searchField !== ''" name="close" @click="searchField = ''" class="cursor-pointer" />
<q-icon name="search" />
</template>
</q-input>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
computed: {
...mapState('tasks', ['search']),
searchField: {
get() {
return this.search
},
set(value) {
this.setSearch(value)
}
}
},
methods: {
...mapActions('tasks', ['setSearch'])
}
}
</script>
Updated PageTodo.vue
<template>
<q-page class="q-pa-md">
<div class="row q-mb-lg">
<search />
</div>
<p v-if="search && !Object.keys(tasksTodo).length && !Object.keys(tasksCompleted).length">No Search results.</p>
<div>
<no-tasks
v-if="!Object.keys(tasksTodo).length && !search"
class="q-mb-lg"></no-tasks>
<tasks-todo
v-if="Object.keys(tasksTodo).length"
:tasksTodo="tasksTodo"
/>
<tasks-completed
v-if=Object.keys(tasksCompleted).length
:tasksCompleted="tasksCompleted"
/>
<div class="absolute-bottom text-center q-mb-lg">
<q-btn
@click="showAddTask=true"
round
color="primary"
size="24px"
icon="add" />
</div>
<q-dialog
v-model="showAddTask">
<add-task @close="showAddTask = false" />
</q-dialog>
</div>
</q-page>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
export default {
data() {
return {
showAddTask: false
}
},
computed: {
...mapGetters('tasks', ['tasksTodo', 'tasksCompleted']),
...mapState('tasks', ['search'])
},
mounted() {
this.$root.$on('showAddTask', () => {
this.showAddTask = true
})
},
components: {
'add-task' : require('components/Tasks/Modals/AddTask.vue').default,
'tasks-todo' : require('components/Tasks/TasksTodo.vue').default,
'tasks-completed' : require('components/Tasks/TasksCompleted.vue').default,
'no-tasks' : require('components/Tasks/NoTasks.vue').default,
'search' : require('components/Tasks/Tools/Search.vue').default
}
}
</script>
<style scoped>
</style>
Updated store-tasks.vue
import Vue from 'vue'
import { uid } from 'quasar'
const state = {
tasks: {
'ID1' : {
name:'Go to shop',
completed: false,
dueDate: '2019/05/12',
dueTime: '18:30'
},
'ID2' : {
name:'Get bananas',
completed: false,
dueDate: '2019/05/13',
dueTime: '5:30'
},
'ID3' : {
name:'Get apples',
completed: false,
dueDate: '2019/05/14',
dueTime: '3:30'
}
},
search: ''
}
const mutations = {
updateTask(state, payload) {
Object.assign(state.tasks[payload.id], payload.updates)
},
deleteTask(state,id) {
Vue.delete(state.tasks, id)
},
addTask(state, payload) {
Vue.set(state.tasks, payload.id, payload.task)
},
setSearch(state, value) {
state.search = value
}
}
const actions = {
updateTask({ commit }, payload) {
commit('updateTask', payload)
},
deleteTask({ commit }, id) {
commit('deleteTask', id)
delete state.tasks['id']
},
addTask({ commit }, task) {
let taskId = uid()
let payload = {
id: taskId,
task: task
}
commit('addTask', payload)
},
setSearch({ commit}, value) {
commit('setSearch', value)
}
}
const getters = {
tasksFiltered: (state) => {
let tasksFiltered = {}
if (state.search) {
Object.keys(state.tasks).forEach(function(key) {
let task = state.tasks[key],
taskNameLowerCase = task.name.toLowerCase(),
searchLowerCase = state.search.toLowerCase()
if (taskNameLowerCase.includes(searchLowerCase)) {
tasksFiltered[key] = task
}
})
return tasksFiltered
}
return state.tasks
},
tasksTodo: (state, getters) => {
let tasksFiltered = getters.tasksFiltered
let tasks = {}
Object.keys(tasksFiltered).forEach(function(key) {
let task = tasksFiltered[key]
if (!task.completed) {
tasks[key] = task
}
})
return tasks
},
tasksCompleted: (state, getters) => {
let tasksFiltered = getters.tasksFiltered
let tasks = {}
Object.keys(tasksFiltered).forEach(function(key) {
let task = tasksFiltered[key]
if (task.completed) {
tasks[key] = task
}
})
return tasks
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
Add a sort Dropdown
store/store-tasks.js
import Vue from 'vue'
import { uid } from 'quasar'
const state = {
tasks: {
'ID1' : {
name:'Go to shop',
completed: false,
dueDate: '2019/05/12',
dueTime: '18:30'
},
'ID2' : {
name:'Get bananas',
completed: false,
dueDate: '2019/05/13',
dueTime: '5:30'
},
'ID3' : {
name:'Get apples',
completed: false,
dueDate: '2019/05/14',
dueTime: '3:30'
}
},
search: '',
sort: 'dueDate',
}
const mutations = {
updateTask(state, payload) {
Object.assign(state.tasks[payload.id], payload.updates)
},
deleteTask(state,id) {
Vue.delete(state.tasks, id)
},
addTask(state, payload) {
Vue.set(state.tasks, payload.id, payload.task)
},
setSearch(state, value) {
state.search = value
},
setSort(state, value) {
state.sort = value
}
}
const actions = {
updateTask({ commit }, payload) {
commit('updateTask', payload)
},
deleteTask({ commit }, id) {
commit('deleteTask', id)
delete state.tasks['id']
},
addTask({ commit }, task) {
let taskId = uid()
let payload = {
id: taskId,
task: task
}
commit('addTask', payload)
},
setSearch({ commit}, value) {
commit('setSearch', value)
},
setSort({ commit}, value) {
commit('setSort', value)
}
}
const getters = {
tasksSorted: (state) => {
let tasksSorted = {},
keysOrdered = Object.keys(state.tasks)
keysOrdered.sort((a,b) => {
sort: 'dueDate',let taskAProp = state.tasks[a][state.sort].toLowerCase(),
taskBProp = state.tasks[b][state.sort].toLowerCase()
if (taskAProp > taskBProp) return 1
else if (taskAProp < taskBProp)
return -1
else return 0
})
keysOrdered.forEach((key) => {
tasksSorted[key] = state.tasks[key]
})
return tasksSorted
},
tasksFiltered: (state, getters) => {
let tasksSorted = getters.tasksSorted,
tasksFiltered = {}
if (state.search) {
Object.keys(tasksSorted).forEach(function(key) {
let task = tasksSorted[key],
taskNameLowerCase = task.name.toLowerCase(),
searchLowerCase = state.search.toLowerCase()
if (taskNameLowerCase.includes(searchLowerCase)) {
tasksFiltered[key] = task
}
})
return tasksFiltered
}
return tasksSorted
},
tasksTodo: (state, getters) => {
let tasksFiltered = getters.tasksFiltered
let tasks = {}
Object.keys(tasksFiltered).forEach(function(key) {
let task = tasksFiltered[key]
if (!task.completed) {
tasks[key] = task
}
})
return tasks
},
tasksCompleted: (state, getters) => {
let tasksFiltered = getters.tasksFiltered
let tasks = {}
Object.keys(tasksFiltered).forEach(function(key) {
let task = tasksFiltered[key]
if (task.completed) {
tasks[key] = task
}
})
return tasks
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
pages/PageTodo.vue
<template>
<q-page class="q-pa-md">
<div class="row q-mb-lg">
<search />
<sort />
</div>
<p v-if="search && !Object.keys(tasksTodo).length && !Object.keys(tasksCompleted).length">No Search results.</p>
<div>
<no-tasks
v-if="!Object.keys(tasksTodo).length && !search"
class="q-mb-lg"></no-tasks>
<tasks-todo
v-if="Object.keys(tasksTodo).length"
:tasksTodo="tasksTodo"
/>
<tasks-completed
v-if=Object.keys(tasksCompleted).length
:tasksCompleted="tasksCompleted"
/>
<div class="absolute-bottom text-center q-mb-lg">
<q-btn
@click="showAddTask=true"
round
color="primary"
size="24px"
icon="add" />
</div>
<q-dialog
v-model="showAddTask">
<add-task @close="showAddTask = false" />
</q-dialog>
</div>
</q-page>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
export default {
data() {
return {
showAddTask: false
}
},
computed: {
...mapGetters('tasks', ['tasksTodo', 'tasksCompleted']),
...mapState('tasks', ['search'])
},
mounted() {
this.$root.$on('showAddTask', () => {
this.showAddTask = true
})
},
components: {
'add-task' : require('components/Tasks/Modals/AddTask.vue').default,
'tasks-todo' : require('components/Tasks/TasksTodo.vue').default,
'tasks-completed' : require('components/Tasks/TasksCompleted.vue').default,
'no-tasks' : require('components/Tasks/NoTasks.vue').default,
'search' : require('components/Tasks/Tools/Search.vue').default,
'sort' : require('components/Tasks/Tools/Sort.vue').default
}
}
</script>
<style scoped>
</style>
components/Tasks/Tools/Sort.vue
<template>
<q-select
filled
v-model="sortBy"
:options="options"
emit-value
map-options
class="col q-ml-sm"
label="Sort By"
stack-label/>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
data() {
return {
options: [
{
label: 'Name',
value: 'name'
},
{
label: 'Date',
value: 'dueDate'
}
],
}
},
computed: {
...mapState('tasks', ['sort']),
sortBy: {
get() {
return this.sort
},
set(value) {
this.setSort(value)
}
}
},
methods: {
...mapActions('tasks', ['setSort'])
}
}
</script>
<style scoped>
.q-select {
flex: 0 0 112px;
}
</style>
Transitions, Directives, Filters, mixins & Scroll area
Directive scoped select all
components/Tasks/Modals/Shared/ModalTaskName.vue
<template>
<div class="row q-mb-sm">
<q-input outlined
:value="name"
@input="$emit('update:name', $event)"
:rules="[val => !!val || 'Field is required']"
ref="name"
autofocus
v-select-all
class="col">
<template v-slot:append>
<q-icon
v-if="name"
@click="$emit('update:name', '')"
name="close"
class="cursor-pointer" />
</template>
</q-input>
</div>
</template>
<script>
export default {
props: ['name'],
directives: {
selectAll: {
inserted(el) {
let input = el.querySelector('.q-field__native')
input.addEventListener('focus', () => {
if (input.value.length) {
input.select()
}
})
}
}
}
}
</script>
Change to Global Directive
components/Tasks/Modals/Shared/ModalTaskName.vue
<template>
<div class="row q-mb-sm">
<q-input outlined
:value="name"
@input="$emit('update:name', $event)"
:rules="[val => !!val || 'Field is required']"
ref="name"
autofocus
v-select-all
class="col">
<template v-slot:append>
<q-icon
v-if="name"
@click="$emit('update:name', '')"
name="close"
class="cursor-pointer" />
</template>
</q-input>
</div>
</template>
<script>
import { selectAll } from 'src/directives/directive-select-all'
export default {
props: ['name'],
directives: {
selectAll
}
}
</script>
move it to another file
src/directives/directive-select-all
export const selectAll = {
inserted(el) {
let input = el.querySelector('.q-field__native')
input.addEventListener('focus', () => {
if (input.value.length) {
input.select()
}
})
}
}
components/Tasks/Tools/Search.vue
<template>
<q-input
outlined
class="col"
v-model="searchField"
v-select-all
label="Search">
<template v-slot:append>
<q-icon v-if="searchField !== ''" name="close" @click="searchField = ''" class="cursor-pointer" />
<q-icon name="search" />
</template>
</q-input>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { selectAll } from 'src/directives/directive-select-all'
export default {
computed: {
...mapState('tasks', ['search']),
searchField: {
get() {
return this.search
},
set(value) {
this.setSearch(value)
}
}
},
methods: {
...mapActions('tasks', ['setSearch'])
},
directives: {
selectAll
}
}
</script>
Hold for 1 second to open a modal & nicer date with filter
components/Tasks/Task.vue
<template>
<q-item
@click="updateTask({id: id, updates: {completed: !task.completed}})"
:class="!task.completed ? 'bg-orange-1' : 'bg-green-1'"
v-touch-hold:1000.mouse="showEditTaskModal"
clickable
v-ripple>
<q-item-section side top>
<q-checkbox
:value="task.completed"
class="no-pointer-events" />
</q-item-section>
<q-item-section>
<q-item
:class="{ 'text-strike' : task.completed }"
v-html="$options.filters.searchHighlight(task.name, search)"
></q-item>
</q-item-section>
<q-item-section
v-if="task.dueDate"
side>
<div class="row">
<div class="column justify-center">
<q-icon
size="18px"
name="event"
class="q-mr-xs" />
</div>
<div class="column">
<q-item-label
class="row justify-end"
caption>{{ task.dueDate | niceDate }}</q-item-label>
<q-item-label caption>
<small>{{ task.dueTime }}</small>
</q-item-label>
</div>
</div>
</q-item-section>
<q-item-section side >
<div class="row">
<q-btn
@click.stop="showEditTaskModal"
flat
round
dense
color="primary"
icon="edit" />
<q-btn
@click.stop="promptToDelete(id)"
flat
round
dense
color="red"
icon="delete" />
</div>
</q-item-section>
<q-dialog
v-model="showEditTask">
<edit-task
@close="showEditTask = false"
:task="task"
:id="id" />
</q-dialog>
</q-item>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { date } from 'quasar'
export default {
props: ['task', 'id'],
data() {
return {
showEditTask: false
}
},
computed: {
...mapState('tasks', ['search'])
},
methods: {
...mapActions('tasks', ['updateTask', 'deleteTask']),
showEditTaskModal() {
this.showEditTask = true
},
promptToDelete(id) {
this.$q.dialog({
title: 'Confirm',
message: 'Really delete?',
ok: {
push: true
},
cancel: {
color: 'negative'
},
persistent: true
}).onOk(() => {
this.deleteTask(id)
})
}
},
filters: {
niceDate(value) {
return date.formatDate(value, 'MMM D')
},
searchHighlight(value, search) {
if (search) {
let searchRegExp = new RegExp(search, 'ig')
return value.replace(searchRegExp, (match) => {
return '<span class="bg-yellow-6">'+ match+'</span>'
})
}
return value
}
},
components: {
'edit-task': require('components/Tasks/Modals/EditTask.vue').default
}
}
</script>
Mixin, reformat files
make it partials
components/Tasks/Modals/AddTask.vue
<template>
<q-card>
<modal-header>Add Task</modal-header>
<form @submit.prevent="submitForm">
<q-card-section>
<modal-task-name :name.sync="taskToSubmit.name"
ref="modalTaskName" />
<modal-due-date
:dueDate.sync="taskToSubmit.dueDate"
@clear="clearDueDate" />
<modal-due-time
v-if="taskToSubmit.dueDate"
:dueTime.sync="taskToSubmit.dueTime" />
</q-card-section>
<modal-buttons></modal-buttons>
</form>
</q-card>
</template>
<script>
import { mapActions } from 'vuex'
import mixinAddEditTask from 'src/mixins/mixin-add-edit-task'
export default {
mixins: [mixinAddEditTask],
data() {
return {
taskToSubmit: {
name:'',
completed: false,
dueDate: '',
dueTime: ''
}
}
},
methods: {
...mapActions('tasks', ['addTask']),
submitTask() {
this.addTask(this.taskToSubmit)
this.$emit('close')
},
},
}
</script>
components/Tasks/Modals/EditTask.vue
<template>
<q-card>
<modal-header>Edit Task</modal-header>
<form @submit.prevent="submitForm">
<q-card-section>
<modal-task-name :name.sync="taskToSubmit.name"
ref="modalTaskName" />
<modal-due-date
:dueDate.sync="taskToSubmit.dueDate"
@clear="clearDueDate" />
<modal-due-time
v-if="taskToSubmit.dueDate"
:dueTime.sync="taskToSubmit.dueTime" />
</q-card-section>
<modal-buttons></modal-buttons>
</form>
</q-card>
</template>
<script>
import { mapActions } from 'vuex'
import mixinAddEditTask from 'src/mixins/mixin-add-edit-task'
export default {
mixins: [mixinAddEditTask],
props: ['task', 'id'],
data() {
return {
taskToSubmit: {}
}
},
methods: {
...mapActions('tasks', ['updateTask']),
submitTask() {
this.updateTask({
id: this.id,
updates: this.taskToSubmit
})
this.$emit('close')
},
},
mounted() {
this.taskToSubmit = Object.assign({}, this.task)
}
}
</script>
mixins/mixin-add-edit-task.js
export default {
methods: {
submitForm() {
this.$refs.modalTaskName.$refs.name.validate()
if (!this.$refs.modalTaskName.$refs.name.hasError) {
this.submitTask()
}
},
clearDueDate(){
this.taskToSubmit.dueDate = ''
this.taskToSubmit.dueTime = ''
}
},
components: {
'modal-header': require('components/Tasks/Modals/Shared/ModalHeader.vue').default,
'modal-task-name': require('components/Tasks/Modals/Shared/ModalTaskName.vue').default,
'modal-due-date': require('components/Tasks/Modals/Shared/ModalDueDate.vue').default,
'modal-due-time': require('components/Tasks/Modals/Shared/ModalDueTime.vue').default,
'modal-buttons': require('components/Tasks/Modals/Shared/ModalButtons.vue').default,
}
}
To add transition (animation)
this is like a div just wrap it up
<transition
appear
enter-active-class="animated zoomIn"
leave-active-class="animated zoomOut"
absolute-top>
</transition>
Add Scrollable Area
<template>
<q-page>
<div class="q-pa-md absolute full-width full-height column">
<div class="row q-mb-lg">
<search />
<sort />
</div>
<p v-if="search && !Object.keys(tasksTodo).length && !Object.keys(tasksCompleted).length">No Search results.</p>
<q-scroll-area class="q-scroll-area-tasks">
<no-tasks
v-if="!Object.keys(tasksTodo).length && !search"
class="q-mb-lg"></no-tasks>
<tasks-todo
v-if="Object.keys(tasksTodo).length"
:tasksTodo="tasksTodo"
/>
<tasks-completed
v-if=Object.keys(tasksCompleted).length
:tasksCompleted="tasksCompleted"
class="q-mb-xl"
/>
</q-scroll-area>
<div class="absolute-bottom text-center q-mb-lg">
<q-btn
@click="showAddTask=true"
round
color="primary"
size="24px"
icon="add" />
</div>
<q-dialog
v-model="showAddTask">
<add-task @close="showAddTask = false" />
</q-dialog>
</div>
</q-page>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
export default {
data() {
return {
showAddTask: false
}
},
computed: {
...mapGetters('tasks', ['tasksTodo', 'tasksCompleted']),
...mapState('tasks', ['search'])
},
mounted() {
this.$root.$on('showAddTask', () => {
this.showAddTask = true
})
},
components: {
'add-task' : require('components/Tasks/Modals/AddTask.vue').default,
'tasks-todo' : require('components/Tasks/TasksTodo.vue').default,
'tasks-completed' : require('components/Tasks/TasksCompleted.vue').default,
'no-tasks' : require('components/Tasks/NoTasks.vue').default,
'search' : require('components/Tasks/Tools/Search.vue').default,
'sort' : require('components/Tasks/Tools/Sort.vue').default
}
}
</script>
<style>
.q-scroll-area-tasks {
display:flex;
flex-grow:1;
}
</style>
Page Settings
make a different store for settings data
import { LocalStorage } from 'quasar'
const state = {
settings: {
show12HourTimeFormat: false,
showTasksInOneList: false,
}
}
const mutations = {
setShow12HourTimeFormat(state, value) {
state.settings.show12HourTimeFormat = value
},
setShowTasksInOneList(state, value) {
state.settings.showTasksInOneList = value
},
setSettings(state, settings) {
Object.assign(state.settings, settings)
}
}
const actions = {
setShow12HourTimeFormat({ commit, dispatch }, value) {
commit('setShow12HourTimeFormat', value)
dispatch('saveSettings')
},
setShowTasksInOneList({ commit, dispatch }, value) {
commit('setShowTasksInOneList', value)
dispatch('saveSettings')
},
saveSettings({state}) {
LocalStorage.set('settings', state.settings)
},
getSettings({commit}) {
let settings = LocalStorage.getItem('settings')
if (settings) {
commit('setSettings', settings)
}
}
}
const getters = {
settings: state => {
return state.settings
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
update store/index.js
import settings from './store-settings'
modules: {
tasks, settings
},
pages/PageSettings.vue
<template>
<q-page padding>
<q-list bordered padding>
<q-item-label header>Settings</q-item-label>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Show 12 hour time format</q-item-label>
</q-item-section>
<q-item-section side >
<q-toggle color="blue" v-model="show12HourTimeFormat" />
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Show tasks in one list</q-item-label>
</q-item-section>
<q-item-section side >
<q-toggle color="blue" v-model="showTasksInOneList" />
</q-item-section>
</q-item>
</q-list>
</q-page>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters('settings', ['settings']),
show12HourTimeFormat: {
get() {
return this.settings.show12HourTimeFormat
},
set(value) {
this.setShow12HourTimeFormat(value)
}
},
showTasksInOneList: {
get() {
return this.settings.showTasksInOneList
},
set(value) {
this.setShowTasksInOneList(value)
}
}
},
methods: {
...mapActions('settings', ['setShow12HourTimeFormat', 'setShowTasksInOneList' ])
}
}
</script>
<style scoped>
</style>
components/Tasks/TasksTodo.vue
<template>
<transition
appear
enter-active-class="animated zoomIn"
leave-active-class="animated zoomOut"
absolute-top>
<div>
<list-header
v-if="!settings.showTasksInOneList"
bgcolor="bg-orange-4"
>Todo</list-header>
<q-list
separator
bordered >
<task v-for="(task, key) in tasksTodo"
:key="key"
:task="task"
:id="key"
></task>
</q-list>
</div>
</transition>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
props: ['tasksTodo'],
computed: {
...mapGetters('settings', ['settings'])
},
components: {
'task' : require('components/Tasks/Task.vue').default,
'list-header' : require('components/Shared/ListHeader.vue').default,
}
}
</script>
TasksCompleted.vue
<template>
<transition
appear
enter-active-class="animated zoomIn"
leave-active-class="animated zoomOut">
<div
:class="{ 'q-mt-lg' : !settings.showTasksInOneList }">
<list-header
v-if="!settings.showTasksInOneList" bgcolor="bg-green-4">Completed</list-header>
<q-list
separator
bordered >
<task v-for="(task, key) in tasksCompleted"
:key="key"
:task="task"
:id="key"
></task>
</q-list>
</div>
</transition>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
props: ['tasksCompleted'],
computed: {
...mapGetters('settings', ['settings'])
},
components: {
'task' : require('components/Tasks/Task.vue').default,
'list-header' : require('components/Shared/ListHeader.vue').default,
}
}
</script>
Task.vue
<template>
<q-item
@click="updateTask({id: id, updates: {completed: !task.completed}})"
:class="!task.completed ? 'bg-orange-1' : 'bg-green-1'"
v-touch-hold:1000.mouse="showEditTaskModal"
clickable
v-ripple>
<q-item-section side top>
<q-checkbox
:value="task.completed"
class="no-pointer-events" />
</q-item-section>
<q-item-section>
<q-item
:class="{ 'text-strike' : task.completed }"
v-html="$options.filters.searchHighlight(task.name, search)"
></q-item>
</q-item-section>
<q-item-section
v-if="task.dueDate"
side>
<div class="row">
<div class="column justify-center">
<q-icon
size="18px"
name="event"
class="q-mr-xs" />
</div>
<div class="column">
<q-item-label
class="row justify-end"
caption>{{ task.dueDate | niceDate }}</q-item-label>
<q-item-label caption>
<small>{{ taskDueTime }}</small>
</q-item-label>
</div>
</div>
</q-item-section>
<q-item-section side >
<div class="row">
<q-btn
@click.stop="showEditTaskModal"
flat
round
dense
color="primary"
icon="edit" />
<q-btn
@click.stop="promptToDelete(id)"
flat
round
dense
color="red"
icon="delete" />
</div>
</q-item-section>
<q-dialog
v-model="showEditTask">
<edit-task
@close="showEditTask = false"
:task="task"
:id="id" />
</q-dialog>
</q-item>
</template>
<script>
import { mapState, mapActions, mapGetters } from 'vuex'
import { date } from 'quasar'
export default {
props: ['task', 'id'],
data() {
return {
showEditTask: false
}
},
computed: {
...mapState('tasks', ['search']),
...mapGetters('settings', ['settings']),
taskDueTime() {
if (this.settings.show12HourTimeFormat) {
return date.formatDate(this.task.dueDate + ' ' + this.task.dueTime, 'h:mm A')
}
return this.task.dueTime
}
},
methods: {
...mapActions('tasks', ['updateTask', 'deleteTask']),
showEditTaskModal() {
this.showEditTask = true
},
promptToDelete(id) {
this.$q.dialog({
title: 'Confirm',
message: 'Really delete?',
ok: {
push: true
},
cancel: {
color: 'negative'
},
persistent: true
}).onOk(() => {
this.deleteTask(id)
})
}
},
filters: {
niceDate(value) {
return date.formatDate(value, 'MMM D')
},
searchHighlight(value, search) {
if (search) {
let searchRegExp = new RegExp(search, 'ig')
return value.replace(searchRegExp, (match) => {
return '<span class="bg-yellow-6">'+ match+'</span>'
})
}
return value
}
},
components: {
'edit-task': require('components/Tasks/Modals/EditTask.vue').default
}
}
</script>
Localstorage
you can use javascript
localStorage.setItem('show12HourTimeFormat', value)
save to localstorage quasar localstorage
store/store-settings.js
import { LocalStorage } from 'quasar'
const actions = {
setShow12HourTimeFormat({ commit, dispatch }, value) {
commit('setShow12HourTimeFormat', value)
dispatch('saveSettings')
},
setShowTasksInOneList({ commit, dispatch }, value) {
commit('setShowTasksInOneList', value)
dispatch('saveSettings')
},
saveSettings({state}) {
LocalStorage.set('settings', state.settings)
}
}
To retrive data from local storage
App.vue
<template>
<div id="q-app">
<router-view />
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
methods: {
...mapActions('settings', ['getSettings'])
},
mounted() {
this.getSettings()
}
}
</script>
const actions = {
setShow12HourTimeFormat({ commit, dispatch }, value) {
commit('setShow12HourTimeFormat', value)
dispatch('saveSettings')
},
setShowTasksInOneList({ commit, dispatch }, value) {
commit('setShowTasksInOneList', value)
dispatch('saveSettings')
},
saveSettings({state}) {
LocalStorage.set('settings', state.settings)
},
getSettings({commit}) {
let settings = LocalStorage.getItem('settings')
if (settings) {
commit('setSettings', settings)
}
}
}
const mutations = {
setShow12HourTimeFormat(state, value) {
state.settings.show12HourTimeFormat = value
},
setShowTasksInOneList(state, value) {
state.settings.showTasksInOneList = value
},
setSettings(state, settings) {
Object.assign(state.settings, settings)
}
}
New help page
PageSetting.vue
<q-list bordered padding>
<q-item-label header>More</q-item-label>
<q-item
to="/settings/help"
tag="label"
v-ripple>
<q-item-section>
<q-item-label>Help</q-item-label>
</q-item-section>
<q-item-section side >
<q-icon name="chevron_right"/>
</q-item-section>
</q-item>
</q-list>
pages/PageHelp.vue
<template>
<q-page padding>
<q-btn
to="/settings"
color="primary"
icon="chevron_left"
label="Back"
flat/>
<div>
<h5>How to</h5>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Officia, sint eum quae, consequuntur voluptate sunt minus fugit dicta quam accusamus porro temporibus, quis inventore asperiores beatae eveniet id libero pariatur!</p>
</div>
</q-page>
</template>
OpenURL (opens a new page)
<q-item
@click="visitOurWebsite"
tag="label"
v-ripple>
<q-item-section>
<q-item-label>Visit our website</q-item-label>
</q-item-section>
<q-item-section side >
<q-icon name="chevron_right"/>
</q-item-section>
</q-item>
methods: {
...mapActions('settings', ['setShow12HourTimeFormat', 'setShowTasksInOneList' ]),
visitOurWebsite() {
openURL('<http://www.suratpembaca.com>')
}
}
Create login and register page
Add new route
{
path: '/auth',
component: () => import('pages/PageAuth.vue')
}
Add a button on the top right
Layout.vue
<q-btn
to="/auth"
flat
icon-right="account_circle"
label="Login"
class="absolute-right" />
Create new Auth Page
pages/PageAuth.vue
<template>
<q-page padding>
<q-card class="auth-tabs">
<q-tabs
v-model="tab"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
>
<q-tab name="login" label="Login" />
<q-tab name="register" label="Register" />
</q-tabs>
<q-separator />
<q-tab-panels v-model="tab" animated>
<q-tab-panel name="login">
<login-register :tab="tab" />
</q-tab-panel>
<q-tab-panel name="register">
<login-register :tab="tab" />
</q-tab-panel>
</q-tab-panels>
</q-card>
</q-page>
</template>
<script>
export default {
data () {
return {
tab: 'register'
}
},
components: {
'login-register' : require('components/Auth/LoginRegister.vue').default
}
}
</script>
<style scoped>
.auth-tabs {
max-width: 500px;
margin: 0 auto;
}
</style>
components/Auth/LoginRegister.vue
<template>
<form @submit.prevent="submitForm">
<div class="row q-mb-md">
<q-banner class="bg-grey-3 col">
<template v-slot:avatar>
<q-icon name="account_circle" color="primary" />
</template>
Register to access your Todos anywhere!
</q-banner>
</div>
<div class="row q-mb-md">
<q-input
outlined
v-model="formData.email"
class="col"
ref="email"
:rules="[ val => isValidEmailAddress(val) || 'Please enter a valid email address.']"
label="Email"
stack-label />
</div>
<div class="row q-mb-md">
<q-input
outlined
ref="password"
v-model="formData.password"
:rules="[ val => val.length >= 6 || 'Please enter at least 6 characters.']"
lazy-rules
type="password"
class="col"
label="Password"
stack-label />
</div>
<div class="row">
<q-space />
<q-btn
color="primary"
label="Register"
type="submit" />
</div>
</form>
</template>
<script>
export default {
props: ['tab'],
data() {
return {
formData: {
email: '',
password: ''
}
}
},
methods: {
submitForm() {
this.$refs.email.validate()
this.$refs.password.validate()
if (!this.$refs.email.hasError && !this.$refs.password.hasError) {
if (this.tab == 'login') {
console.log('login the user')
} else {
console.log('register the user')
}
}
},
isValidEmailAddress(email) {
const re = /^(([^<>()[\\]\\\\.,;:\\s@\\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\\"]+)*)|(\\".+\\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
}
}
</script>
Firebase
create app in https://console.firebase.google.com/
create a boot quasar file
quasar new boot <name>
update quasar.conf.js
boot: [
'firebase'
],
boot/firebase.js
import firebase from "firebase/app"
import "firebase/auth"
var firebaseConfig = {
apiKey: "-",
authDomain: "udemy-quasar-awesome-todo.firebaseapp.com",
databaseURL: "<https://udemy-quasar-awesome-todo-default-rtdb.asia-southeast1.firebasedatabase.app>",
projectId: "udemy-quasar-awesome-todo",
storageBucket: "udemy-quasar-awesome-todo.appspot.com",
messagingSenderId: "691846352087",
appId: "1:691846352087:web:b2e897dd2853e1f7b17492"
}
let firebaseApp = firebase.initializeApp(firebaseConfig)
let firebaseAuth = firebaseApp.auth()
export { firebaseAuth }
store/store-auth.js
import { LocalStorage } from 'quasar'
import { firebaseAuth } from 'boot/firebase'
import { showErrorMessage } from 'src/functions/function-show-error-message'
const state = {
loggedIn: false
}
const mutations = {
setLoggedIn(state, value) {
state.loggedIn = value
}
}
const actions = {
registerUser({}, payload) {
firebaseAuth.createUserWithEmailAndPassword(
payload.email, payload.password
).then(response => {
console.log('response: ', response )
})
.catch(error => {
showErrorMessage(error.message)
})
},
loginUser({}, payload) {
firebaseAuth.signInWithEmailAndPassword(
payload.email, payload.password
).then(response => {
console.log('response: ', response )
})
.catch(error => {
showErrorMessage(error.message)
})
},
logoutUser() {
console.log('logoutUser')
firebaseAuth.signOut()
},
handleAuthStateChange({commit}) {
firebaseAuth.onAuthStateChanged(user => {
if (user) {
commit('setLoggedIn', true)
LocalStorage.set('loggedIn', true)
this.$router.push('/').catch(err => {})
} else {
commit('setLoggedIn', false)
LocalStorage.set('loggedIn', false)
this.$router.replace('/auth').catch(err => {})
}
})
}
}
const getters = {
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
components/Auth/LoginRegister.vue
<template>
<form @submit.prevent="submitForm">
<div class="row q-mb-md">
<q-banner class="bg-grey-3 col">
<template v-slot:avatar>
<q-icon name="account_circle" color="primary" />
</template>
{{ tab | titleCase }} to access your Todos anywhere!
</q-banner>
</div>
<div class="row q-mb-md">
<q-input
outlined
v-model="formData.email"
class="col"
ref="email"
:rules="[ val => isValidEmailAddress(val) || 'Please enter a valid email address.']"
label="Email"
stack-label />
</div>
<div class="row q-mb-md">
<q-input
outlined
ref="password"
v-model="formData.password"
:rules="[ val => val.length >= 6 || 'Please enter at least 6 characters.']"
lazy-rules
type="password"
class="col"
label="Password"
stack-label />
</div>
<div class="row">
<q-space />
<q-btn
color="primary"
:label="tab"
type="submit" />
</div>
</form>
</template>
<script>
import { mapActions } from "vuex"
export default {
props: ['tab'],
data() {
return {
formData: {
email: '',
password: ''
}
}
},
methods: {
...mapActions('auth', ['registerUser', 'loginUser']),
submitForm() {
this.$refs.email.validate()
this.$refs.password.validate()
if (!this.$refs.email.hasError && !this.$refs.password.hasError) {
if (this.tab == 'login') {
this.loginUser(this.formData)
} else {
this.registerUser(this.formData)
}
}
},
isValidEmailAddress(email) {
const re = /^(([^<>()[\\]\\\\.,;:\\s@\\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\\"]+)*)|(\\".+\\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
},
filters: {
titleCase(value) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
}
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated>
<q-toolbar>
<q-toolbar-title class="absolute-center">
Awesome App
</q-toolbar-title>
<q-btn
v-if="!loggedIn"
to="/auth"
flat
icon-right="account_circle"
label="Login"
class="absolute-right" />
<q-btn
v-else
@click="logoutUser"
flat
icon-right="account_circle"
label="Logout"
class="absolute-right" />
</q-toolbar>
</q-header>
<q-footer>
<q-tabs>
<q-route-tab
v-for="(nav, index) in navs"
:to="nav.to"
:icon="nav.icon"
:label="nav.label"
:key="index" />
</q-tabs>
</q-footer>
<q-drawer
v-model="leftDrawerOpen"
:width="250"
show-if-above
bordered
:breakpoint="767"
content-class="bg-primary"
>
<q-list dark>
<q-item-label
header
class="text-white"
>
Navigation
</q-item-label>
<q-item
v-for="(nav, index) in navs"
:to="nav.to"
exact
clickable
:key=index
class="text-white">
<q-item-section avatar>
<q-icon :name="nav.icon" />
</q-item-section>
<q-item-section>
{{nav.label}}
</q-item-section>
</q-item>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script>
import {mapState, mapActions} from 'vuex'
import {openURL} from 'quasar'
export default {
name: 'MainLayout',
data () {
return {
leftDrawerOpen: false,
navs: [
{
label: 'Todo',
icon: 'list',
to: '/'
},
{
label: 'Settings',
icon: 'settings',
to: '/settings'
}
]
}
},
computed: {
...mapState('auth', ['loggedIn'])
},
methods: {
...mapActions('auth', ['logoutUser']),
openURL
}
}
</script>
<style scoped lang="scss">
@media screen and (min-width:768px) {
.q-footer {
display:none;
}
}
.q-drawer {
.q-router-link--exact-active {
font-weight:bold;
}
}
</style>
Adding guard
quasar new boot router-auth
boot: [
'firebase',
'router-auth'
],
boot/router-auth.js
import { LocalStorage } from "quasar"
export default ({ router}) => {
router.beforeEach((to, from, next) => {
let loggedIn = LocalStorage.getItem('loggedIn')
if (!loggedIn && to.path !== '/auth') {
next('/auth')
} else {
next()
}
})
}
Handling Error & quasar dialog
functions/function-show-error-message.js
import { Dialog, Loading } from "quasar";
export function showErrorMessage(errorMessage) {
Loading.hide()
Dialog.create({
title: 'Error',
message: errorMessage
})
}
Firebase Reading
store/store-tasks.js
const actions = {
updateTask({ commit }, payload) {
commit('updateTask', payload)
},
deleteTask({ commit }, id) {
commit('deleteTask', id)
delete state.tasks['id']
},
addTask({ commit }, task) {
let taskId = uid()
let payload = {
id: taskId,
task: task
}
commit('addTask', payload)
},
setSearch({ commit}, value) {
commit('setSearch', value)
},
setSort({ commit}, value) {
commit('setSort', value)
},
fbReadData({commit}) {
let userId = firebaseAuth.currentUser.uid
let userTasks = firebaseDb.ref('tasks/' + userId)
//child added
userTasks.on('child_added', snapshot => {
let task = snapshot.val()
let payload = {
id: snapshot.key,
task: task
}
commit('addTask', payload)
})
//child changed
userTasks.on('child_changed', snapshot => {
let task = snapshot.val()
let payload = {
id: snapshot.key,
updates: task
}
commit('updateTask', payload)
})
//child removed
userTasks.on('child_removed', snapshot => {
let taskId = snapshot.key
commit('deleteTask', taskId)
})
}
}
handleAuthStateChange({commit, dispatch}) {
firebaseAuth.onAuthStateChanged(user => {
Loading.hide()
if (user) {
commit('setLoggedIn', true)
LocalStorage.set('loggedIn', true)
this.$router.push('/').catch(err => {})
dispatch('tasks/fbReadData', null, { root: true })
} else {
commit('setLoggedIn', false)
LocalStorage.set('loggedIn', false)
this.$router.replace('/auth').catch(err => {})
}
Loading.hide()
})
Firebase Writing
Change the default from commit to dispatch
addTask({ dispatch }, task) {
let taskId = uid()
let payload = {
id: taskId,
task: task
}
dispatch('fbAddTask', payload)
},
updateTask({ dispatch }, payload) {
dispatch('fbUpdateTask', payload)
},
deleteTask({ dispatch }, id) {
dispatch('fbDeleteTask', id)
},
fbAddTask({}, payload) {
let userId = firebaseAuth.currentUser.uid
let taskRef = firebaseDb.ref('tasks/' + userId + '/' + payload.id)
taskRef.set(payload.task)
},
fbUpdateTask({}, payload) {
let userId = firebaseAuth.currentUser.uid
let taskRef = firebaseDb.ref('tasks/' + userId + '/' + payload.id)
taskRef.update(payload.updates)
},
fbDeleteTask({}, taskId) {
let userId = firebaseAuth.currentUser.uid
let taskRef = firebaseDb.ref('tasks/' + userId + '/' + taskId)
taskRef.remove()
}
Loading Experience
const state = {
tasks: {
},
search: '',
sort: 'name',
tasksDownloaded: false
}
const mutations = {
setTasksDownloaded(state, value) {
state.tasksDownloaded = value
}
}
fbReadData({commit}) {
let userId = firebaseAuth.currentUser.uid
let userTasks = firebaseDb.ref('tasks/' + userId)
// initial check for data
userTasks.once('value', snapshot => {
commit('setTasksDownloaded', true)
})
//child added
userTasks.on('child_added', snapshot => {
let task = snapshot.val()
let payload = {
id: snapshot.key,
task: task
}
commit('addTask', payload)
})
//child changed
userTasks.on('child_changed', snapshot => {
let task = snapshot.val()
let payload = {
id: snapshot.key,
updates: task
}
commit('updateTask', payload)
})
//child removed
userTasks.on('child_removed', snapshot => {
let taskId = snapshot.key
commit('deleteTask', taskId)
})
},
<template v-if="tasksDownloaded">
</template>
<template
v-else
>
<span class="absolute-center">
<q-spinner color="primary" size="3em" />
</span>
</template>
handleAuthStateChange({commit, dispatch}) {
firebaseAuth.onAuthStateChanged(user => {
Loading.hide()
if (user) {
commit('setLoggedIn', true)
LocalStorage.set('loggedIn', true)
this.$router.push('/').catch(err => {})
dispatch('tasks/fbReadData', null, { root: true })
} else {
commit('tasks/setTasksDownloaded', false, {root: true})
commit('setLoggedIn', false)
LocalStorage.set('loggedIn', false)
this.$router.replace('/auth').catch(err => {})
}
Loading.hide()
})
}
Multiple Users
After logout need to set the tasks as empty tasks
const mutations = {
clearTasks(state) {
state.tasks = {}
},
}
handleAuthStateChange({commit, dispatch}) {
firebaseAuth.onAuthStateChanged(user => {
Loading.hide()
if (user) {
commit('setLoggedIn', true)
LocalStorage.set('loggedIn', true)
this.$router.push('/').catch(err => {})
dispatch('tasks/fbReadData', null, { root: true })
} else {
commit('tasks/clearTasks', null, {root: true})
commit('tasks/setTasksDownloaded', false, {root: true})
commit('setLoggedIn', false)
LocalStorage.set('loggedIn', false)
this.$router.replace('/auth').catch(err => {})
}
Loading.hide()
})
}
Rules on firebase to prevent other user write or read one another
{
"rules": {
"tasks": {
"$uid": {
".read" : "auth.uid == $uid",
".write" : "auth.uid == $uid",
}
}
}
}
Handle Errors
const actions = {
fbAddTask({}, payload) {
let userId = firebaseAuth.currentUser.uid
let taskRef = firebaseDb.ref('tasks/' + userId + '/' + payload.id)
taskRef.set(payload.task, error => {
if (error) {
showErrorMessage(error.message)
}
})
},
fbUpdateTask({}, payload) {
let userId = firebaseAuth.currentUser.uid
let taskRef = firebaseDb.ref('tasks/' + userId + '/' + payload.id)
taskRef.update(payload.updates, error => {
if (error) {
showErrorMessage(error.message)
}
})
},
fbDeleteTask({}, taskId) {
let userId = firebaseAuth.currentUser.uid
let taskRef = firebaseDb.ref('tasks/' + userId + '/' + taskId)
taskRef.remove( error => {
if (error) {
showErrorMessage(error.message)
}
})
}
}
To learn about production build