Update parent state when Vuetify snackbar timeouts - vuejs2

Currently I have a Parent and a Child component. In the Parent component I set the snackbar state to true after a successful API call so it shows the snackbar. When I click on the close button in the snackbar the state of the Parent gets updated and the snackbar disappears. However, when the snackbar timeouts I get the following error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"snackbar"
How can I update the Parent state when the snackbar timeouts? Is there a clean way to do this?
This is my Parent component:
<Snackbar :snackbar="snackbar" :y="bottom" :text="text" #update-snackbar="updateSnackbar"></Snackbar>
<script>
import Snackbar from "../components/Snackbar";
export default {
name: "AddFriend",
components: {Snackbar},
methods: {
updateSnackbar(e) {
this.snackbar = e;
},
add() {
const username = "test";
axios
.post('/api/friend/add',
{
username: username
})
.then(response => {
if (response.data.success) {
this.snackbar = true;
this.text = 'Request successfully send!';
} else {
this.snackbar = true;
this.text = response.data.error;
}
})
.catch(err => {
console.log('Something went wrong: ' + err);
this.snackbar = true;
this.text = 'test';
})
}
},
data() {
return {
error: null,
valid: false,
text: null,
snackbar: false,
bottom: 'bottom',
}
}
}
</script>
And this is my Child (snackbar) component:
<template>
<v-snackbar
v-model="snackbar"
:bottom="y === 'bottom'"
:timeout="1500"
:vertical="mode === 'vertical'"
>
{{ text }}
<v-btn
color="red"
flat
#click="closeSnackbar(false)"
>
Close
</v-btn>
</v-snackbar>
</template>
<script>
export default {
name: "Snackbar",
props: {
snackbar: Boolean,
y: String,
x: null,
mode: String,
text: String
},
methods: {
closeSnackbar(snackbar) {
this.$emit('update-snackbar', snackbar);
}
},
data() {
return {}
}
}
</script>
If someone knows how to do this please let me know. Also if a CodeSandBox is needed I will create one. Thanks in advance!

I managed to update the parent state on timeout. I did this by adding:
#input="closeSnackbar"
to my v-snackbar. When the timeout occurs it now triggers the closeSnackbar function which updates my parent component. For everyone with the same problems here is my full snackbar component:
<template>
<v-snackbar
:value="snackbar"
#input="closeSnackbar"
bottom
:timeout="1500"
>
{{ text }}
</v-snackbar>
</template>
<script>
export default {
name: "Snackbar",
props: {
snackbar: Boolean,
text: String
},
methods: {
closeSnackbar(snackbar) {
this.$emit('update-snackbar', snackbar);
}
},
}
</script>

Related

Vue received a Component which was made a reactive object

The problem I need to solve: I am writing a little vue-app based on VueJS3.
I got a lot of different sidebars and I need to prevent the case that more than one sidebar is open at the very same time.
To archive this I am following this article.
Now I got a problem:
Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef instead of ref. (6)
This is my code:
SlideOvers.vue
<template>
<component :is="component" :component="component" v-if="open"/>
</template>
<script>
export default {
name: 'SlideOvers',
computed: {
component() {
return this.$store.state.slideovers.sidebarComponent
},
open () {
return this.$store.state.slideovers.sidebarOpen
},
},
}
</script>
UserSlideOver.vue
<template>
<div>test</div>
</template>
<script>
export default {
name: 'UserSlideOver',
components: {},
computed: {
open () {
return this.$store.state.slideovers.sidebarOpen
},
component () {
return this.$store.state.slideovers.sidebarComponent
}
},
}
</script>
slideovers.js (vuex-store)
import * as types from '../mutation-types'
const state = {
sidebarOpen: false,
sidebarComponent: null
}
const getters = {
sidebarOpen: state => state.sidebarOpen,
sidebarComponent: state => state.sidebarComponent
}
const actions = {
toggleSidebar ({commit, state}, component) {
commit (types.TOGGLE_SIDEBAR)
commit (types.SET_SIDEBAR_COMPONENT, component)
},
closeSidebar ({commit, state}, component) {
commit (types.CLOSE_SIDEBAR)
commit (types.SET_SIDEBAR_COMPONENT, component)
}
}
const mutations = {
[types.TOGGLE_SIDEBAR] (state) {
state.sidebarOpen = !state.sidebarOpen
},
[types.CLOSE_SIDEBAR] (state) {
state.sidebarOpen = false
},
[types.SET_SIDEBAR_COMPONENT] (state, component) {
state.sidebarComponent = component
}
}
export default {
state,
getters,
actions,
mutations
}
App.vue
<template>
<SlideOvers/>
<router-view ref="routerView"/>
</template>
<script>
import SlideOvers from "./SlideOvers";
export default {
name: 'app',
components: {SlideOvers},
};
</script>
And this is how I try to toggle one slideover:
<template>
<router-link
v-slot="{ href, navigate }"
to="/">
<a :href="href"
#click="$store.dispatch ('toggleSidebar', userslideover)">
Test
</a>
</router-link>
</template>
<script>
import {defineAsyncComponent} from "vue";
export default {
components: {
},
data() {
return {
userslideover: defineAsyncComponent(() =>
import('../../UserSlideOver')
),
};
},
};
</script>
Following the recommendation of the warning, use markRaw on the value of usersslideover to resolve the warning:
export default {
data() {
return {
userslideover: markRaw(defineAsyncComponent(() => import('../../UserSlideOver.vue') )),
}
}
}
demo
You can use Object.freeze to get rid of the warning.
If you only use shallowRef f.e., the component will only be mounted once and is not usable in a dynamic component.
<script setup>
import InputField from "src/core/components/InputField.vue";
const inputField = Object.freeze(InputField);
const reactiveComponent = ref(undefined);
setTimeout(function() => {
reactiveComponent.value = inputField;
}, 5000);
setTimeout(function() => {
reactiveComponent.value = undefined;
}, 5000);
setTimeout(function() => {
reactiveComponent.value = inputField;
}, 5000);
</script>
<template>
<component :is="reactiveComponent" />
</template>

Parent bind boolean but child recive number

I don't know how to explain... I have this component:
<template>
<base-layout>
<template #main>
<principal-layout>
<template #dreta>
<dot-spinner :is-loading="isLoading"></dot-spinner>
<seleccio-estudis :estudis="estudis" #crearsolicitud="crearSolicitud"></seleccio-estudis>
</template>
</principal-layout>
</template>
</base-layout>
</template>
with script
...
data() {
return {
isLoading: false,
}
},
methods: {
crearSolicitud(estudis_id) {
console.log(typeof(this.isLoading));
this.isLoading = true;
this.isLoading = setTimeout(() => {
this.isLoading = false;
}, 3000);
...
And at child component:
<template>
<div class="vld-parent">
<loading :active.sync="isLoading"
:can-cancel="false"
:is-full-page="true"
:loader="'dots'" :color="'#58b033'"
:width="320" :height="320"></loading>
</div>
</template>
<script>
import Loading from 'vue-loading-overlay'
import 'vue-loading-overlay/dist/vue-loading.css'
export default {
components: {
Loading
},
props: {
isLoading: Boolean,
},
}
</script>
All works fine, when click on button, loader pop up and discarted after 3 seconds after, but I recive this at console:
boolean 2 app.js:41533 [Vue warn]: Invalid prop: type check failed
for prop "isLoading". Expected Boolean, got Number with value 15.
found in
---> at resources/js/Components/Layout/DotSpinner.vue
at resources/js/LayoutsNous/PrincipalLayout.vue
at resources/js/LayoutsNous/BaseLayout.vue
at resources/js/Pages/Proves/Home.vue
Every time i reload the page, the value is 15, but every time i click button that emit the event, value grow up one or two units.
Any idea what's happening? I'm doing the same in other components and works without warnings.
You are setting the value of this.isLoading to your setTimeout:
this.isLoading = setTimeout(() => {
this.isLoading = false;
}, 3000);
Change this to:
setTimeout(() => {
this.isLoading = false;
}, 3000);

Detect vuex state change to execute a method inside a nuxt layout

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.

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"
}
}

How could I get changed value from vuetify select by emitting in atomic design pattern?

I am trying to get the changed value of vuetify v-select by using $emit but it doesn't work.
I divided components by applying atomic design pattern (atoms(child component and not to connect with the store), organisms(parent component)) and vuex stores.
I think $emit data is OK but anything doesn't work after the process.
This is for a new application for management page with
using vue, vuex, vuetify, atomic design connecting to API server.
Components
child component - in atoms folder
<template>
<v-select
:items="list"
:label="label"
v-model="selected"
item-value="id"
item-text="name"
return-object
#change="changeSelected"
></v-select>
</template>
<script>
export default {
props: ["list", "label", "defaultSelected"],
data() {
return {
selected: this.defaultSelected
};
},
methods: {
changeSelected(newValue) {
console.log(newValue); // display changed new data
this.$emit("changeSelected", newValue);
}
}
};
</script>
parent component - in organisms folder
<template>
<v-select-child
:select-label="label"
:select-list="list"
:default-selected="selected"
#change-selected="changeSelected" // problem issue?
>
</v-select-child>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
...
},
computed: {
...mapState({
list: state => state.list
})
},
methods: {
changeSelected() {
console.log("changeSelected"); // doesn't work
this.$store.dispatch("setSelected", { payload: this.selected });
}
}
};
</script>
vuex stores
index.js
export default new Vuex.Store({
modules: {
xxx
},
state: {
list: [
{
name: "aaaaa",
id: "001"
},
{
name: "bbbbb",
id: "002"
}
]
},
getters: {},
mutations: {},
actions: {}
});
xxx.js
export default {
selected: { id: "001" }
},
getters: {
//
},
mutations: {
updateSelected(state, payload) {
console.log("payload"); // doesn't work
console.log(payload);
state.selected = payload;
console.log(state.selected);
}
},
actions: {
setSelected({ commit }, payload) {
console.log("Action"); // doesn't work
commit("updateSelected", payload);
}
}
};
It does not print any console log after changeSelected function.
In document
Unlike components and props, event names don’t provide any automatic
case transformation. Instead, the name of an emitted event must
exactly match the name used to listen to that event.
That means if you emit event like $emit('changeSelected'), then you need to use #changeSelected. #change-selected will not work.
<v-select-child
:select-label="label"
:select-list="list"
:default-selected="selected"
#changeSelected="changeSelected"
>
</v-select-child>
I found a solution below:
child component
<template>
<v-select
:label="label"
:items="list"
v-model="selected"
item-value="id"
item-text="name"
return-object
></v-select>
</template>
<script>
export default {
props: ["list", "label", "defaultSelected"],
computed: {
selected: {
get() {
return this.defaultSelected;
},
set(newVal) {
if (this.selected !== newVal) {
this.$emit("changeSelected", newVal);
}
}
}
}
};
</script>
parent component
<template>
<v-select-child
:label="label"
:list="list"
:defaultSelected="selected"
#changeSelected="changeSelected" // fix the property using camelCase
></v-select-child>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
...
},
computed: {
...mapState({
list: state => state.list
})
},
methods: {
changeSelected(val) { // val: changed object value
this.$store.dispatch("setSelected", { id:val.id });
}
}
};
</script>
You can also use watch;
<v-select
:label="label"
:items="list"
v-model="selected"
item-value="id"
item-text="name"
></v-select>
</template>
...
watch:{
selected(){
this.$emit('changeValue', this.selected.id');
}
}
...
and from parent;
<child #changeValue="id = $event" .. />