How to create a queue of alert messages in Vue.js/Vuetify? - vue.js

I have written some code that requires some amendments to cater for multiple alerts. It should also be able to accept a timeout duration so by default it needs to be 5000 but you should be able to override this property. Please could someone help me with this as I'm not sure how to go about it from this point.
~/store/toast-messages.js
export const state = () => ({
color: '',
message: '',
type: ''
})
export const mutations = {
showToast (state, payload) {
state.color = payload.color
state.message = payload.message
state.type = payload.type
}
}
~/plugins/toasts.js
export default ({ store }, inject) => {
inject('toasts', {
showToast ({ color = '', message = '', type = '' }) {
store.commit('toast-messages/showToast', { color, message, type })
}
})
}
nuxt.config.js
export default {
...
plugins: [
'~/plugins/toasts.js'
],
...
}
~components/toasts/Toasts.vue
<template>
<v-alert
v-model="show"
transition="slide-y-transition"
:color="color"
:type="type"
dense
prominent>
{{ message }}
</v-alert>
</template>
<script>
export default {
name: 'Toasts',
data: () => {
return {
show: false,
color: '',
message: '',
type: 'error',
}
},
watch: {
show (val) {
if (val) {
this.hideToast()
}
},
},
created () {
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'toast-messages/showToast') {
this.color = state['toast-messages'].color
this.message = state['toast-messages'].message
this.type = state['toast-messages'].type
this.show = true
}
})
},
methods: {
hideToast () {
window.setInterval(() => {
this.show = false
}, 3000)
},
},
}
</script>
Called like this from anywhere in the app
this.$toasts.showToast({
color: 'red',
type: 'error',
message: err.message,
})

You can use the excellent component Vue-SNotify - example here.

Related

Don't send Axios request when user selects a result

I'm using v-autocomplete from vuetify.js to retrieve a list of values from API Server.
It works fine and my list of values is not empty.
But my problem is when I select the correct value from this list. My script sends another request to server to retrieve another autocomplete list.
Do you have any idea to avoid to send request when a result is selected by the user ? Or to send request only when a key is down ?
My component :
<template>
<div>
<v-autocomplete
v-model="selectValeur"
:loading="loading"
:search-input.sync="search"
:items="resultatsAutocomplete"
class="mb-4"
hide-no-data
hide-details
:label="recherche.label"
></v-autocomplete>
</div>
</template>
<script>
export default {
props: {
recherche: {
type: Object,
default: null,
},
},
data: () => ({
selectValeur: null,
loading: false,
search: null,
resultatsAutocomplete: [],
}),
watch: {
selectValeur(oldval, val) {
console.log(oldval)
console.log(val)
},
search(val) {
val && val !== this.selectValeur && this.fetchEntriesDebounced(val)
console.log(val)
if (!val) {
this.resultatsAutocomplete = []
}
},
},
methods: {
fetchEntriesDebounced(val) {
// cancel pending call
clearTimeout(this._timerId)
// delay new call 500ms
this._timerId = setTimeout(() => {
this.querySelections(val)
}, 500)
},
async querySelections(v) {
if (v.length > 1) {
this.loading = true
try {
const result = await this.$axios.$get(
'myapi/myurl',
{
params: {
racine: v,
},
}
)
this.resultatsAutocomplete = result
console.log(this.resultatsAutocomplete)
this.loading = false
} catch (err) {
console.log(err)
this.loading = false
}
} else {
this.resultatsAutocomplete = []
}
},
},
}
</script>
Thanks,
selectValeur would no longer be null if the user has selected a value, so you could update search() to return if selectValeur is truthy:
export default {
watch: {
search(val) {
if (this.selectValeur) {
// value already selected
return
}
//...
}
}
}
Or you could use vm.$watch on the search property to be able to stop the watcher when selectValeur is set:
export default {
mounted() {
this._unwatchSearch = this.$watch('search', val => {
val && val !== this.selectValeur && this.fetchEntriesDebounced(val)
if (!val) {
this.resultatsAutocomplete = []
}
})
},
watch: {
selectValeur(val) {
if (val && this._unwatchSearch) {
this._unwatchSearch()
}
}
}
}
I found a solution to my problem.
I used the #keyup event to send the axios request and I deleted the watcher on search.
So, the API request are only sent when I press a key.
<template>
<div>
<v-autocomplete
v-model="selectValeur"
:loading="loading"
:items="resultatsAutocomplete"
:search-input.sync="search"
class="mb-4"
hide-no-data
hide-details
:label="recherche.label"
#keyup="keyupSearch"
></v-autocomplete>
</div>
</template>
<script>
export default {
props: {
recherche: {
type: Object,
default: null,
},
},
data: () => ({
selectValeur: null,
loading: false,
resultatsAutocomplete: [],
search: '',
}),
methods: {
keyupSearch(val) {
val &&
val !== this.selectValeur &&
this.fetchEntriesDebounced(this.search)
if (!val) {
this.resultatsAutocomplete = []
}
},
fetchEntriesDebounced(val) {
// cancel pending call
clearTimeout(this._timerId)
// delay new call 500ms
this._timerId = setTimeout(() => {
this.querySelections(val)
}, 500)
},
async querySelections(v) {
if (v.length > 1) {
this.loading = true
try {
const result = await this.$axios.$get(
'my-api/my-url',
{
params: {
sid: this.$route.params.sid,
service: this.$route.params.service,
type: this.recherche.mode,
racine: v,
},
}
)
this.resultatsAutocomplete = result
console.log(this.resultatsAutocomplete)
this.loading = false
} catch (err) {
console.log(err)
this.loading = false
}
} else {
this.resultatsAutocomplete = []
}
},
},
}
</script>

vuetify vee-validate scroll to the first validation error

If the submit button is clicked in a form, it should automatically scroll to the first validation error if an error exists.
I've read that I can use "scrolltoview" for this, but I don't know exactly how.
I have already tried it with a simple ScrollTo (0.0) to simply scroll up in the event of errors and it works perfectly.
However, this is not the solution I would like to have.
< script >
...
let name = 'm-form-user';
export default {
name: name,
mixins: [baseForm],
props: {
name: {
type: String,
default: name
},
title: {
type: String,
default: ''
},
type: {
type: String,
default: 'create',
validator: function(value) {
return ['edit', 'create'].indexOf(value) !== -1
}
},
},
data: () => ({
form: {
firstName: '',
lastName: '',
position: '',
email: '',
mobile: '',
roles: []
}
}),
async created() {
if (!this.isCreationForm && this.$route.params.id) {
if (!this.editingUser.length) {
await this.requestUser({
id: this.$route.params.id
});
}
Object.assign(this.form, this.editingUser);
this.form.roles.pop()
}
},
computed: {
...mapGetters({
getUser: "users/read"
}),
text() {
return {
cancel: this.$t('modules.forms.m-form-user.buttons.cancel'),
submit: this.$t('modules.forms.m-form-user.buttons.submit')
}
},
editingUser() {
return this.getUser(this.$route.params.id)
},
isCreationForm() {
return this.type === 'create'
}
},
methods: {
...mapActions({
requestCreateUser: 'users/create',
requestUpdateUser: 'users/update',
requestUser: 'users/read'
}),
async submit() {
const validAll = await this.$validator.validateAll();
const validIdentify = this.validateIdentify();
if (!validAll || !validIdentify) {
// ScrolltoView
return;
}
try {
this.setOrganizationRelation();
let user = this.isCreationForm ? await this.createUser() : await this.updateUser();
this.notify.success(this.$t(`notifications.account.userManagement.${ this.isCreationForm ? 'created':'edited'}`, {
firstName: user.firstName,
lastName: user.lastName
}))
this.redirect(this.nav.dashboard.account.users.view.name, {
id: user._id
})
} catch (e) {
if (e.response && e.response.status === 400) {
e.response.data.violations.forEach(violation => {
if (violation.propertyPath === 'username') return; //TODO temporary workaround, remove it when we get correct response from server
this.$validator.errors.add({
id: violation.propertyPath,
field: violation.propertyPath,
msg: violation.message
});
const field = this.$validator.fields.find({
name: violation.propertyPath
});
if (!field) {
throw `Field "${violation.propertyPath}" in "${this.$options._componentTag}" component don't have validation on client side!`;
}
field.setFlags({
invalid: true,
valid: false,
validated: true
});
});
} else {
this.notify.processUnhandledError(e);
}
}
},
async createUser() {
return await this.requestCreateUser({ ...this.form,
password: passwordGenerator.generate()
});
},
async updateUser() {
return await this.requestUpdateUser(this.form)
},
cancel() {
this.goBack();
},
validateIdentify() {
if (!this.form.email && !this.form.mobile) {
const fields = (({
email,
mobile
}) => ({
email,
mobile
}))(this.$refs);
Object.keys(fields).forEach((key) => {
let field = this.$validator.fields.find({
name: fields[key].name
});
this.$validator.errors.add({
id: field.id,
field: field.name,
msg: this.$t('modules.forms.m-form-user.sections.contacts.emptyContacts')
});
field.setFlags({
invalid: true,
valid: false,
validated: true
});
this.$refs.emailBlock.open();
this.$refs.mobileBlock.open();
});
return false;
}
return true;
},
setOrganizationRelation() {
const rel = {
organization: this.$user.relationships.organization
};
setRelations(this.form, rel)
}
}
} <
/script>
<m-block-form-fields :required="false">
<template #title>
{{$t('modules.forms.m-form-user.sections.personal.title')}}
</template>
<template>
<v-layout wrap>
<v-flex xs12>
<e-input-user-name v-model="form.firstName" rules="required" required-style/>
</v-flex>
<v-flex xs12>
<e-input-user-surname v-model="form.lastName" rules="required" required-style/>
</v-flex>
<v-flex xs12>
<e-input-user-position-function v-model="form.position"/>
</v-flex>
</v-layout>
</template>
</m-block-form-fields>
Try using document.querySelector to locate the first error message like below.
if (!validAll || !validIdentify) {
const el = document.querySelector(".v-messages.error--text:first-of-type");
el.scrollIntoView();
return;
}
This is based on #Eldar's answer.
Because you're changing the DOM you only want to look for the new element after the DOM has been updated.
I was able to get this to work with nextTick.
if(!validAll || !validIdentify) {
// Run after the next update cycle
this.$nextTick(() => {
const el = this.$el.querySelector(".v-messages.error--text:first-of-type");
this.$vuetify.goTo(el);
return;
});
}
First, put all your fields inside "v-form" tag
Second, give it a ref="form" as in:
<v-form
ref="form"
v-model="valid"
lazy-validation
#submit.prevent="() => {}"
>
... all your fields ...
</v-form>
Finally, for the handler of your submit method, do this as a guard clause:
async submit() {
// if a field is invalid => scroll to it
if (!this.$refs.form.validate()) {
const invalidField = this.$refs.form.$children.find((e) => !e.valid)
if (invalidField)
invalidField.$el.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
return
}
// here, your form is valid and you can continue
}

Nuxt asyncData result is undefined if using global mixin head() method

I'm would like to get titles for my pages dynamically in Nuxt.js in one place.
For that I've created a plugin, which creates global mixin which requests title from server for every page. I'm using asyncData for that and put the response into storage, because SSR is important here.
To show the title on the page I'm using Nuxt head() method and store getter, but it always returns undefined.
If I place this getter on every page it works well, but I would like to define it only once in the plugin.
Is that a Nuxt bug or I'm doing something wrong?
Here's the plugin I wrote:
import Vue from 'vue'
import { mapGetters } from "vuex";
Vue.mixin({
async asyncData({ context, route, store, error }) {
const meta = await store.dispatch('pageMeta/setMetaFromServer', { path: route.path })
return {
pageMetaTitle: meta
}
},
...mapGetters('pageMeta', ['getTitle']),
head() {
return {
title: this.getTitle, // undefined
// title: this.pageMetaTitle - still undefined
};
},
})
I would like to set title in plugin correctly, now it's undefined
Update:
Kinda solved it by using getter and head() in global layout:
computed: {
...mapGetters('pageMeta', ['getTitle']),
}
head() {
return {
title: this.getTitle,
};
},
But still is there an option to use it only in the plugin?
Update 2
Here's the code of setMetaFromServer action
import SeoPagesConnector from '../../connectors/seoPages/v1/seoPagesConnector';
const routesMeta = [
{
path: '/private/kredity',
dynamic: true,
data: {
title: 'TEST 1',
}
},
{
path: '/private/kreditnye-karty',
dynamic: false,
data: {
title: 'TEST'
}
}
];
const initialState = () => ({
title: 'Юником 24',
description: '',
h1: '',
h2: '',
h3: '',
h4: '',
h5: '',
h6: '',
content: '',
meta_robots_content: '',
og_description: '',
og_image: '',
og_title: '',
url: '',
url_canonical: '',
});
export default {
state: initialState,
namespaced: true,
getters: {
getTitle: state => state.title,
getDescription: state => state.description,
getH1: state => state.h1,
},
mutations: {
SET_META_FIELDS(state, { data }) {
if (data) {
Object.entries(data).forEach(([key, value]) => {
state[key] = value;
})
}
},
},
actions: {
async setMetaFromServer(info, { path }) {
const routeMeta = routesMeta.find(route => route.path === path);
let dynamicMeta;
if (routeMeta) {
if (!routeMeta.dynamic) {
info.commit('SET_META_FIELDS', routeMeta);
} else {
try {
dynamicMeta = await new SeoPagesConnector(this.$axios).getSeoPage({ path })
info.commit('SET_META_FIELDS', dynamicMeta);
return dynamicMeta && dynamicMeta.data;
} catch (e) {
info.commit('SET_META_FIELDS', routeMeta);
return routeMeta && routeMeta.data;
}
}
} else {
info.commit('SET_META_FIELDS', { data: initialState() });
return { data: initialState() };
}
return false;
},
}
}

How to pass multiple mutations to update Vuex store?

If I mutate the state only once (by committing either of the two mutations shown below), there is no error and chart is updated correctly.
If I run both mutations:
commit('SET_COURSE_MATERIAL', data)
commit('SET_TOOLS_EQUIPMENT', data)
then I get Maximum call stack exceeded: RangeError.
If I comment out the code in the watch property of chart.vue, there are no errors and I can see the state with correct values in console.log
I am getting the error regarding maximum call stack only when I run "npm run dev". When I deploy it to Google Cloud, the site works as expected and I don't get any errors. I even re-checked this by editing some code and re-deploying it twice while also noticing the time in the build logs.
summary.vue
<v-card-text>
<chart
:chart-config.sync="this.$store.state.summary.courseMaterial"
/>
</v-card-text>
...
<v-card-text>
<chart
:chart-config.sync="this.$store.state.summary.toolsEquipment"
/>
</v-card-text>
chart.vue
<template>
<v-flex>
<no-ssr><vue-c3 :handler="handler"/></no-ssr>
</v-flex>
</template>
<script>
import Vue from 'vue'
export default {
name: 'chart',
props: ['chartConfig'],
data() {
return {
handler: new Vue()
}
},
watch: {
chartConfig: function(val) {
console.log('chart component > watch > chartConfig', val)
this.drawChart()
}
},
created() {
this.drawChart()
},
methods: {
drawChart() {
this.handler.$emit('init', this.chartConfig)
}
}
}
</script>
store/summary.js
import axios from 'axios'
import _ from 'underscore'
import Vue from 'vue'
import {
courseMaterialChartConfig,
toolsEquipmentChartConfig,
} from './helpers/summary.js'
axios.defaults.baseURL = process.env.BASE_URL
Object.filter = (obj, predicate) =>
Object.assign(
...Object.keys(obj)
.filter(key => predicate(obj[key]))
.map(key => ({ [key]: obj[key] }))
)
export const state = () => ({
courseMaterial: '',
toolsEquipment: '',
})
export const getters = {
courseMaterial(state) {
return state.courseMaterial
},
toolsEquipment(state) {
return state.toolsEquipment
}
}
export const actions = {
async fetchData({ state, commit, rootState, dispatch }, payload) {
axios.defaults.baseURL = process.env.BASE_URL
let { data: initialData } = await axios.post(
'summary/fetchInitialData',
payload
)
console.log('initialData', initialData)
let [counterData, pieChartData, vtvcData, guestFieldData] = initialData
//dispatch('setCourseMaterial', pieChartData.coureMaterialStatus)
//dispatch('setToolsEquipment', pieChartData.toolsEquipmentStatus)
},
setCourseMaterial({ commit }, data) {
commit('SET_COURSE_MATERIAL', courseMaterialChartConfig(data))
},
setToolsEquipment({ commit }, data) {
commit('SET_TOOLS_EQUIPMENT', toolsEquipmentChartConfig(data))
}
}
export const mutations = {
// mutations to set user in state
SET_COURSE_MATERIAL(state, courseMaterial) {
console.log('[STORE MUTATIONS] - SET_COURSEMATERIAL:', courseMaterial)
state.courseMaterial = courseMaterial
},
SET_TOOLS_EQUIPMENT(state, toolsEquipment) {
console.log('[STORE MUTATIONS] - SET_TOOLSEQUIPMENT:', toolsEquipment)
state.toolsEquipment = toolsEquipment
},
}
helpers/summary.js
export const courseMaterialChartConfig = data => {
return {
data: {
type: 'pie',
json: data,
names: {
received: 'Received',
notReceived: 'Not Received',
notReported: 'Not Reported'
}
},
title: {
text: 'Classes',
position: 'right'
},
legend: {
position: 'right'
},
size: {
height: 200
}
}
}
export const toolsEquipmentChartConfig = data => {
return {
data: {
type: 'pie',
json: data,
names: {
received: 'Received',
notReceived: 'Not Received',
notReported: 'Not Reported'
}
},
title: {
text: 'Job Role Units',
position: 'right'
},
legend: {
position: 'right'
},
size: {
height: 200
}
}
}
Deep copy the chart config.
methods: {
drawChart() {
this.handler.$emit('init', {...this.chartConfig})
}
}

Computed property "main_image" was assigned to but it has no setter

How can I fix this error "Computed property "main_image" was assigned to but it has no setter"?
I'm trying to switch main_image every 5s (random). This is my code, check created method and setInterval.
<template>
<div class="main-image">
<img v-bind:src="main_image">
</div>
<div class="image-list>
<div v-for="img in images" class="item"><img src="img.image"></div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Item',
data () {
return {
item: [],
images: [],
}
},
methods: {
fetchImages() {
axios.get(`/api/item/${this.$route.params.id}/${this.$route.params.attribute}/images/`)
.then(response => {
this.images = response.data
})
.catch(e => {
this.images = []
this.errors.push(e)
})
},
},
computed: {
main_image() {
if (typeof this.item[this.$route.params.attribute] !== 'undefined') {
return this.item[this.$route.params.attribute].image_url
}
},
},
watch: {
'$route' (to, from) {
this.fetchImages()
}
},
created () {
axios.get(`/api/item/${this.$route.params.id}/`)
.then(response => {
this.item = response.data
})
.catch(e => {
this.errors.push(e)
})
this.fetchImages();
self = this
setInterval(function(){
self.main_image = self.images[Math.floor(Math.random()*self.images.length)].image;
}, 5000);
},
}
</script>
Looks like you want the following to happen...
main_image is initially null / undefined
After the request to /api/item/${this.$route.params.id}/ completes, it should be this.item[this.$route.params.attribute].image_url (if it exists)
After the request to /api/item/${this.$route.params.id}/${this.$route.params.attribute}/images/ completes, it should randomly pick one of the response images every 5 seconds.
I'd forget about using a computed property as that is clearly not what you want. Instead, try this
data() {
return {
item: [],
images: [],
main_image: '',
intervalId: null
}
},
methods: {
fetchImages() {
return axios.get(...)...
}
},
created () {
axios.get(`/api/item/${this.$route.params.id}/`).then(res => {
this.item = res.data
this.main_image = this.item[this.$route.params.attribute] && this.item[this.$route.params.attribute].image_url
this.fetchImages().then(() => {
this.intervalId = setInterval(() => {
this.main_image = this.images[Math.floor(Math.random()*this.images.length)].image;
})
})
}).catch(...)
},
beforeDestroy () {
clearInterval(this.intervalId) // very important
}
You have to add setter and getter for your computed proterty.
computed: {
main_image: {
get() {
return typeof this.item[this.$route.params.attribute] !== 'undefined' && this.item[this.$route.params.attribute].image_url
},
set(newValue) {
return newValue;
},
},
},