Detect vuex state change to execute a method inside a nuxt layout - vue.js

I am trying to show vuetify snackbar alert, once I completed a form submission inside a page or vue component. I use vuex store to manage alert type and message.
my-nuxt-app/store/alerts.js
export const state = () => ({
message: '',
type: ''
});
export const getters = {
hasAlert(state) {
return state.message !== '';
},
alertMessage(state) {
return state.message;
},
alertType(state) {
return state.type;
}
};
export const mutations = {
SET_ALERT(state, payload) {
state.type = payload.type;
state.message = payload.message;
}
};
export const actions = {
setAlert({commit}, payload) {
commit('SET_ALERT', payload);
},
clearAlert({commit}) {
commit('SET_ALERT', {});
}
};
And I created a nuxt plugin to access getters globally in my application.
my-nuxt-app/plugins/alert.js
import Vue from 'vue';
import {mapGetters} from 'vuex';
const Alert = {
install(Vue, options) {
Vue.mixin({
computed: {
...mapGetters({
hasAlert: 'alerts/hasAlert',
alertType: 'alerts/alertType',
alertMessage: 'alerts/alertMessage'
})
}
});
}
};
Vue.use(Alert);
Inside my AccountForm component submit method, I am dispatching my alert information to store like below.
my-nuxt-app/components/form/AccountForm.vue
...
methods: {
async submit () {
try {
await this.$axios.patch("/settings/profile", this.form);
this.$store.dispatch('alerts/setAlert', {
type: 'success',
message: 'You have successfully updated your information.'
});
} catch (e) {
}
}
},
...
}
...
And this AccountForm.vue component is a child component of profile.vue page which is obviously inside the pages folder of my project. And also I have extended the dashboard.vue layout to this profile.vue page and to the most of the pages inside my pages directory as a common layout. Hence, I added the snackbar component into dashboard layout to show a alert message whenever required.
my-nuxt-app/layouts/dashboard.vue
<template>
...
<v-snackbar
:timeout="snackbar.timeout"
:color="snackbar.color"
:top="snackbar.y === 'top'"
:bottom="snackbar.y === 'bottom'"
:right="snackbar.x === 'right'"
:left="snackbar.x === 'left'"
:multi-line="snackbar.mode === 'multi-line'"
:vertical="snackbar.mode === 'vertical'"
v-model="snackbar.show"
>
{{ snackbar.text }}
<v-btn flat icon dark #click.native="snackbar.show = false">
<v-icon>close</v-icon>
</v-btn>
</v-snackbar>
...
</template>
<script>
...
data: () => ({
snackbar: {
show: false,
y: 'top',
x: null,
mode: '',
timeout: 6000,
color: '',
text: ''
},
}),
computed: {
availableAlert: function () {
return this.hasAlert;
}
},
watch: {
availableAlert: function(alert) {
if(alert) {
this.showAlert(this.alertType, this.alertMessage);
this.$store.dispatch('alerts/clearAlert');
}
}
},
methods: {
showAlert(type, message) {
this.snackbar.show = true;
this.snackbar.color = type;
this.snackbar.text = message;
}
}
</script>
I am getting the alert message for the first time submission of the form and after that I have to reload the page and then submit to get the alert. Please enlighten me a way to detect the vuex state change and trigger showAlert method inside the dashboard.vue accordingly.

It's most likely the way you're checking hasAlert
Your clearAlert passes an empty object, your setAlert is trying to assign properties of that empty object, while your hasAlert is checking if it's an empty string.
If you change your clearAlert to:
clearAlert({commit}) {
commit('SET_ALERT', { message: '', type: '' });
}
That should fix your issue.

Related

How do I communicate the error messages to other components of the form?

I am working with Vue and Laravel, the issue is that I made an error class and as the form works with 3 different components.
For example I have the base form of this form and further down I have a child component of the form both are sent in the same form but I can't figure out how to pass the error message to that component that is in another vue file.
I tried to pass the error message as a prop but it doesn't take it and inside that child component I used watch to capture the error but it doesn't capture it at all.
Error Class
class Errors{
constructor(){
this.errors = {};
}
get(field){
if (this.errors[field]) {
return this.errors[field][0];
}
}
record(errors){
this.errors = errors.errors;
}
}
export default Errors;
**Parent component: CreateClient **
<template>
<contract v-for="contract in contractsFields" :key="contract.id"
:id="contract.id"
:resultForm="onResultsContracts"
:hasErrors="errors"
#remove="removeContract"
></contract>
</template>
<script>
import Errors from '../../class/Errors.js'
import Contract from '../contratos/ContractComponent.vue'
export default {
name: 'CreateClient',
props: ['results'],
components:{
Contract
},
data() {
return {
errors: new Errors(),
}
},
sendForm(){
const params = {
client: this.formClient,
company: this.formCompany,
contract: this.formContract
}
axios.post(route('clients.store'),params)
.then((res) => {
this.errors = []
console.log(res)
})
.catch(error => {
if (error.response.status === 422) {
this.errors = error.response.data.errors || {};
}
})
}
}
</script>
Child Component
<script>
import Errors from '../../class/Errors.js'
import AreaClient from '../clients/AreasClientComponent.vue'
export default {
name: 'Contract',
components: {
AreaClient
},
props:['id','resultForm','hasErrors'],
data() {
return {
error: new Errors(),
}
},
watch: {
hasErrors: function(){
console.log(this.hasErrors) // No working
}
},
}
</script>
Have you tried a deep watcher for your object?
hasErrors: {
deep: true,
handler () {
// do whatever you would like to do
}
}

Testing vue watchers with vue-testing-library

Anyone know how I would test a watcher in a component with vue-testing-library?
Here is my component. I want to test that the method is called when the brand vuex state is updated. With vue test utils it would be easy but I have not found a good way to do this with vue testing library. Has anyone did this before using vue testing library.
<template>
<v-data-table
data-testid="builds-table"
:headers="headers"
:items="builds"
:items-per-page="10"
class="elevation-1"
:loading="loading"
>
<template v-slot:[getItemStatus]="{ item }">
<v-chip :color="getStatusColor(item.status)" dark>
{{ item.status }}
</v-chip>
</template>
</v-data-table>
</template>
<script>
import { mapState } from "vuex";
import { getScheduledBuilds } from "../services/buildActivationService";
import { getStatusColor } from "../utils/getStatusColor";
export default {
name: "BuildsTable",
data() {
return {
loading: false,
headers: [
{
text: "Activation Time",
align: "start",
value: "buildActivationTime",
},
{ text: "Build ID", value: "buildId" },
{ text: "Build Label", value: "buildLabel" },
{ text: "Status", value: "status" },
],
error: "",
};
},
async mounted() {
this.getBuilds();
},
computed: {
...mapState(["brand", "builds"]),
getItemStatus() {
return `item.status`;
},
},
watch: {
brand() {
this.getBuilds();
},
},
methods: {
getStatusColor(status) {
return getStatusColor(status);
},
async getBuilds() {
try {
this.loading = true;
const builds = await getScheduledBuilds(this.$store.getters.brand);
this.$store.dispatch("setBuilds", builds);
this.items = this.$store.getters.builds;
this.loading = false;
} catch (error) {
this.loading = false;
this.error = error.message;
this.$store.dispatch("setBuilds", []);
}
},
},
};
</script>
Vue Testing Library is just a wrapper for Vue Test Utils, so the same call verification techniques apply.
Here's how to verify the call with Jest and Vue Testing Library:
Spy on the component method definition before rendering the component:
import { render } from '#testing-library/vue'
import BuildsTable from '#/components/BuildsTable.vue'
const getBuilds = jest.spyOn(BuildsTable.methods, 'getBuilds')
render(BuildsTable)
Render the component with a given store and a callback to capture the Vuex store instance under test:
let store = {
state: {
brand: '',
builds: [],
}
}
const storeCapture = (_, vuexStore) => store = vuexStore
render(BuildsTable, { store }, storeCapture)
Update the store's brand value, and wait a macro tick for the watcher to take effect, then verify the getBuilds spy is called twice (once in mounted() and again in the brand watcher):
store.state.brand = 'foo'
await new Promise(r => setTimeout(r)) // wait for effect
expect(getBuilds).toHaveBeenCalledTimes(2)
The full test would look similar to this:
import { render } from '#testing-library/vue'
import BuildsTable from '#/components/BuildsTable.vue'
describe('BuildsTable.vue', () => {
it('calls getBuilds when brand changes', async() => {
const getBuilds = jest.spyOn(BuildsTable.methods, 'getBuilds')
let store = {
state: {
brand: '',
builds: [],
}
}
const storeCapture = (_, vuexStore) => store = vuexStore
render(BuildsTable, { store }, storeCapture)
store.state.brand = 'foo'
await new Promise(r => setTimeout(r)) // wait for effect
expect(getBuilds).toHaveBeenCalledTimes(2)
})
})

Vue 3 access child component from slots

I am currently working on a custom validation and would like to, if possible, access a child components and call a method in there.
Form wrapper
<template>
<form #submit.prevent="handleSubmit">
<slot></slot>
</form>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup(props, { slots }) {
const validate = (): boolean => {
if (slots.default) {
slots.default().forEach((vNode) => {
if (vNode.props && vNode.props.rules) {
if (vNode.component) {
vNode.component.emit('validate');
}
}
});
}
return false;
};
const handleSubmit = (ev: any): void => {
validate();
};
return {
handleSubmit,
};
},
});
</script>
When I call slot.default() I get proper list of child components and can see their props. However, vNode.component is always null
My code is based from this example but it is for vue 2.
If someone can help me that would be great, or is this even possible to do.
I found another solution, inspired by quasar framework.
Form component provide() bind and unbind function.
bind() push validate function to an array and store in Form component.
Input component inject the bind and unbind function from parent Form component.
run bind() with self validate() function and uid
Form listen submit event from submit button.
run through all those validate() array, if no problem then emit('submit')
Form Component
import {
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
toRefs,
provide
} from "vue";
export default defineComponent({
name: "Form",
emits: ["submit"],
setup(props, { emit }) {
const state = reactive({
validateComponents: []
});
provide("form", {
bind,
unbind
});
onMounted(() => {
state.form.addEventListener("submit", onSubmit);
});
onBeforeUnmount(() => {
state.form.removeEventListener("submit", onSubmit);
});
function bind(component) {
state.validateComponents.push(component);
}
function unbind(uid) {
const index = state.validateComponents.findIndex(c => c.uid === uid);
if (index > -1) {
state.validateComponents.splice(index, 1);
}
}
function validate() {
let valid = true;
for (const component of state.validateComponents) {
const result = component.validate();
if (!result) {
valid = false;
}
}
return valid;
}
function onSubmit() {
const valid = validate();
if (valid) {
emit("submit");
}
}
}
});
Input Component
import { defineComponent } from "vue";
export default defineComponent({
name: "Input",
props: {
rules: {
default: () => [],
type: Array
},
modelValue: {
default: null,
type: String
}
}
setup(props) {
const form = inject("form");
const uid = getCurrentInstance().uid;
onMounted(() => {
form.bind({ validate, uid });
});
onBeforeUnmount(() => {
form.unbind(uid);
});
function validate() {
// validate logic here
let result = true;
props.rules.forEach(rule => {
const value = rule(props.modelValue);
if(!value) result = value;
})
return result;
}
}
});
Usage
<template>
<form #submit="onSubmit">
<!-- rules function -->
<input :rules="[(v) => true]">
<button label="submit form" type="submit">
</form>
</template>
In the link you provided, Linus mentions using $on and $off to do this. These have been removed in Vue 3, but you could use the recommended mitt library.
One way would be to dispatch a submit event to the child components and have them emit a validate event when they receive a submit. But maybe you don't have access to add this to the child components?
JSFiddle Example
<div id="app">
<form-component>
<one></one>
<two></two>
<three></three>
</form-component>
</div>
const emitter = mitt();
const ChildComponent = {
setup(props, { emit }) {
emitter.on('submit', () => {
console.log('Child submit event handler!');
if (props && props.rules) {
emit('validate');
}
});
},
};
function makeChild(name) {
return {
...ChildComponent,
template: `<input value="${name}" />`,
};
}
const formComponent = {
template: `
<form #submit.prevent="handleSubmit">
<slot></slot>
<button type="submit">Submit</button>
</form>
`,
setup() {
const handleSubmit = () => emitter.emit('submit');
return { handleSubmit };
},
};
const app = Vue.createApp({
components: {
formComponent,
one: makeChild('one'),
two: makeChild('two'),
three: makeChild('three'),
}
});
app.mount('#app');

How to use a component multiple times while passing different data to it?

I'm trying to create a snackbar component for showing simple notifications. It can be used at many places in the entire application as well as on a single page as well. I've created a component as child component and imported it in the parent component where i want to use it. In this parent component many times this child can be used. How should i implement in a way that each time this component is called it gets its appropriate data(Ex. for error color=red text="error", for success color="green" message="success).
Any suggestions on how to implement it?
parent.vue----------------------------
<snackbar
:snackbar="snackbar"
:color="color"
:text="message"
v-on:requestClose="close"
/>
data() {
return {
snackbar: false,
color: "orange",
timeout: 3000,
message: "calling from employee compoenent"
};
},
methods: {
hello() {
console.log("button clicked!!!");
this.snackbar = true;
},
close() {
this.snackbar = false;
},
child.vue-----------------------------------------------
<template>
<v-snackbar v-model="snackbar" right top :timeout="timeout" :color="color"
>{{ text }}
<v-btn dark text #click.native="$emit('requestClose')">Close</v-btn>
</v-snackbar>
</template>
<script>
export default {
name: "snackbar",
data() {
return {
timeout: 3000
};
},
props: ["snackbar", "text", "color"],
};
</script>
<style></style>
Recommended would be to create a custom wrapper Vue plugin
plugins/snackbar/index.js
import snackbar from './snackbar.vue'
export default {
install (Vue) {
// INSTALL
if (this.installed) return
this.installed = true
// RENDER
const root = new Vue({ render: h => h(snackbar) })
root.$mount(document.body.appendChild(document.createElement('div')))
// APIs
let apis = Vue.prototype['$snackbar'] = {
show: ({ text="Foo", color="blue" }) => root.$emit('show', { text, color }), // SHOW
hide: () => root.$emit('hide') // HIDE
}
Vue.prototype['$snackbar'] = apis
Vue.snackbar = apis
}
}
plugins/snackbar/snackbar.vue
<template>
<v-snackbar right top v-model="show" :timeout="timeout" :color="color">
{{ text }}
<v-btn dark text #click.native="this.show = false">Close</v-btn>
</v-snackbar>
</template>
<script>
export default {
name: "snackbar",
data() {
return {
show,
timeout: 3000,
text: "",
color: ""
};
},
mounted () {
// LISTENING :: SHOW
this.$root.$on('show', ({ text, color }) => {
this.text = text
this.color = color
this.show = true
})
// LISTENING :: HIDE
this.$root.$on('hide', () => this.show = false)
}
};
</script>
// main.js
import Snackbar from './plugins/snackbar/index.js'
Vue.use(Snackbar)
To show / hide it in any component
this.$snackbar.show({ text: "Foo bar", color: "red" }) // OR
Vue.snackbar.show({ text: "Foo bar", color: "red" })
As per use case, you can keep updating your plugin with more params / APIs.
Alternative: By using an event bus
event-bus/bus.js
// Create an event bus
import Vue from 'vue'
export default new Vue()
app.vue
<template>
// Render the component in app.vue
<v-snackbar
right top
v-model="snackbar.show"
:timeout="snackbar.timeout"
:color="snackbar.color"
>
{{ snackbar.text }}
<v-btn
dark text
#click.native="this.snackbar.show = false"
>
Close
</v-btn>
</v-snackbar>
</template>
<script>
import bus from './event-bus/bus.js'
export default {
data () {
return {
snackbar: {
show: false,
text: '',
color: '',
timeout: 3000
}
}
},
mounted () {
// LISTEN TO SHOW
bus.$on('show', ({ text, color }) => {
this.snackbar.text = 'foo'
this.snackbar.color = 'red'
this.snackbar.show = true
})
// LISTEN TO HIDE
bus.$on('hide', () => this.snackbar.show = false)
}
}
</script>
To show / hide snackbar from any component
import bus from './event-bus/bus.js
export default {
mounted () {
bus.emit('show', { text: 'Foo bar baz', color: 'orange' }) // TO SHOW
// bus.emit('hide') // TO HIDE
}
}
Another way: By using Vuex
Render the <v-snackbar> in app.vue as done in an alternative approach & use Vuex state / getters to pass the value to the props of v-snackbar.
I did it by using combination of global components and Vuex. The answer is a bit lengthy because I provide example along the description, please bear with me :)
I first create a snackbar store with color and text as its state and a setSnackbar() action which receives color and text as params. Then you can create your Snackbar component and don't forget to have your getters, actions mapped into it. Some code snippet:
// snackbar component
<template>
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="6000" bottom right>
{{ snackbar.text }}
<v-btn dark text #click="snackbarClosed()">Close</v-btn>
</v-snackbar>
</template>
<script lang="ts">
import Vue from "vue";
import { mapGetters, mapActions } from "vuex";
export default Vue.extend({
computed: {
...mapGetters(["snackbar"])
},
methods: {
snackbarClosed() {
this.resetSnackbar();
},
...mapActions(["resetSnackbar"])
}
});
</script>
// snackbar store
const state = {
snackbar: {
show: false,
text: '',
color: ''
}
};
const getters = {
snackbar: (state: any) => state.snackbar
};
const actions = {
async setSnackbar({ commit }, params) {
commit('updateSnackbar', Object.assign({}, { show: true }, params))
},
async resetSnackbar({ commit }) {
const setting: SnackbarSetting = {
show: false,
text: '',
color: ''
};
commit('updateSnackbar', setting)
};
const mutations = {
updateSnackbar: (state: any, snackbar: SnackbarSetting) => {
state.show = snackbar.show;
state.text = snackbar.text;
state.color = snackbar.color;
}
};
To make Snackbar component globally available, import your Snackbar component into your main.ts and add the line Vue.component('Snackbar', Snackbar); before new Vue. Its purpose is to register your Snackbar component globally before initializing the Vue instance. Example:
// main.ts
import Snackbar from './components/Snackbar.vue';
Vue.component('Snackbar', Snackbar);
new Vue({
...
Before you want to display your snackbar in the app, by my recommendation you should place <Snackbar /> in your App.vue so that the snackbar can appear before your components and you won't be facing missing snackbar when changing between components.
When you want to display your snackbar, just do this in your component:
// any of your component
methods: {
someEvent() {
this.someApiCall({
// some data passing
}).then(() => {
this.setSnackbar({
text: 'Data has been updated.',
color: 'success'
});
}).catch(() => {
this.setSnackbar({
text: 'Failed to update data.',
color: 'error'
});
});
},
...mapActions(['setSnackbar'])
}
Hope you can work it out, please do not hesitate to let me know if you need something. Here's some extra material for you: Global component registration
you can watch for props in child this'll make color changes when any change happen in parent:
watch: {
color: function(value) {
"add color value to your dom css class"
}
}

Iterating over a Vuex store object

I'm new to Vue.js and Vuex and trying out a sample app.
This is the scenario-
I have a store module for notifications which stores the notifications in an object with a given name as its key.
{
'message1': {
type: 'info',
message: 'This is an info message.',
isShown: true,
},
'message2': {
type: 'success',
message: 'This is a success message.',
isShown: true,
},
'message3': {
type: 'error',
message: 'This is an error message.',
isShown: true,
}
}
And this is my Vuex module that handles notification-
const state = {
notifications: {},
};
const mutations = {
setNotification(state, { message, type, name }) {
state.notifications[name] = {
message,
type,
isShown: true,
}
},
removeNotification(state, name) {
delete state.notifications[name];
}
};
const actions = {
async showNotification(context, options) {
await context.commit('setNotification', options);
},
async removeNotification(context, name) {
await context.commit('removeNotification', name);
}
}
const getters = {
isNotificationShown: (state, getters) => {
return getters.getNotificationMessageList.length > 0;
},
getNotificationMessageList: state => {
return state.notifications;
},
}
export default {
state,
actions,
mutations,
getters,
}
And this is my component-
<template>
<div v-if="isShown">
<div v-for="(notice, name, index) in notificationMessageList" :key="name">
{{ index }} - {{ notice.type }} - {{ notice.message}}
</div>
</div>
</template>
<script>
export default {
computed: {
isShown() {
return this.$store.getters.isNotificationShown;
},
notificationMessageList() {
return this.$store.getters.getNotificationMessageList;
},
},
};
</script>
I checked with the Vue Development tool and found that the store does get updated and so does the component with the notification messages that I'm passing to the store. But the component is not being rendered. But if I use the same data by hardcoding it in the component, it works.
I'm not sure if this is the right way to connect the Vuex store to a component.
It's Vue reactivity problem. You need to update the reference to make Vue reactive. You can use JSON.parse(JSON.stringify()) or use ES6 syntax:
const mutations = {
setNotification(state, { message, type, name }) {
state.notifications = {
...state.notifications,
[name]: {
message,
type,
isShown: true
}
}
},
removeNotification(state, name) {
const newNotifications = {...state.notifications}
delete newNotifications[name]
state.notifications = newNotifications
}
};