Update props in component through router-view in vue js - javascript

I want to pass a boolean value from one of my views in a router-view up to the root App.vue and then on to one of the components in the App.vue. I was able to do it but I am facing an 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: "drawer"
Below is my code:
Home.vue:
<template>
<div class="home" v-on:click="updateDrawer">
<img src="...">
</div>
</template>
<script>
export default {
name: "Home",
methods:{
updateDrawer:function(){
this.$emit('updateDrawer', true)
}
};
</script>
The above view is in the router-view and I am getting the value in the App.vue below:
<template>
<v-app class="">
<Navbar v-bind:drawer="drawer" />
<v-main class=" main-bg">
<main class="">
<router-view v-on:updateDrawer="changeDrawer($event)"></router-view>
</main>
</v-main>
</v-app>
</template>
<script>
import Navbar from '#/components/Navbar'
export default {
name: 'App',
components: {Navbar},
data() {
return {
drawer: false
}
},
methods:{
changeDrawer:function(drawz){
this.drawer = drawz;
}
},
};
</script>
I am sending the value of drawer by binding it in the navbar component.
Navbar.vue:
<template>
<nav>
<v-app-bar app fixed class="white">
<v-app-bar-nav-icon
class="black--text"
#click="drawer = !drawer"
></v-app-bar-nav-icon>
</v-app-bar>
<v-navigation-drawer
temporary
v-model="drawer"
>
...
</v-navigation-drawer>
</nav>
</template>
<script>
export default {
props:{
drawer:{
type: Boolean
}
},
};
</script>
This will work one time and then it will give me the above error. I appreciate if any one can explain what should I do and how to resolve this issue.

In Navbar.vue you are taking the property "drawer" and whenever someone clicks on "v-app-bar-nav-icon" you change drawer to be !drawer.
The problem here is that you are mutating (changing the value) of a property from the child side (Navbar.vue is the child).
That's not how Vue works, props are only used to pass data down from parent to child (App.vue is parent and Navbar.vue is child) and never the other way around.
The right approach here would be: every time, in Navbar.vue, that you want to change the value of "drawer" you should emit an event just like you do in Home.vue.
Then you can listen for that event in App.vue and change the drawer variable accordingly.
Example:
Navbar.vue
<v-app-bar-nav-icon
class="black--text"
#click="$emit('changedrawer', !drawer)"
></v-app-bar-nav-icon>
App.vue
<Navbar v-bind:drawer="drawer" #changedrawer="changeDrawer"/>
I originally missed the fact that you also used v-model="drawer" in Navbar.vue.
v-model also changes the value of drawer, which again is something we don't want.
We can solve this by splitting up the v-model into v-on:input and :value like so:
<v-navigation-drawer
temporary
:value="drawer"
#input="val => $emit('changedrawer', val)"
>
Some sort of central state management such as Vuex would also be great in this scenario ;)

What you could do, is to watch for changes on the drawer prop in Navbar.vue, like so:
<template>
<nav>
<v-app-bar app fixed class="white">
<v-app-bar-nav-icon
class="black--text"
#click="switchDrawer"
/>
</v-app-bar>
<v-navigation-drawer
temporary
v-model="navbarDrawer"
>
...
</v-navigation-drawer>
</nav>
</template>
<script>
export default {
data () {
return {
navbarDrawer: false
}
},
props: {
drawer: {
type: Boolean
}
},
watch: {
drawer (val) {
this.navbarDrawer = val
}
},
methods: {
switchDrawer () {
this.navbarDrawer = !this.navbarDrawer
}
}
}
</script>
Home.vue
<template>
<div class="home">
<v-btn #click="updateDrawer">Updrate drawer</v-btn>
</div>
</template>
<script>
export default {
name: 'Home',
data () {
return {
homeDrawer: false
}
},
methods: {
updateDrawer: function () {
this.homeDrawer = !this.homeDrawer
this.$emit('updateDrawer', this.homeDrawer)
}
}
}
</script>
App.vue
<template>
<v-app class="">
<Navbar :drawer="drawer" />
<v-main class="main-bg">
<main class="">
<router-view #updateDrawer="changeDrawer"></router-view>
</main>
</v-main>
</v-app>
</template>
<script>
import Navbar from '#/components/Navbar'
export default {
name: 'App',
components: { Navbar },
data () {
return {
drawer: false
}
},
methods: {
changeDrawer (newDrawer) {
this.drawer = newDrawer
}
}
}
</script>
This doesn't mutate the drawer prop, but does respond to changes.

Related

How can I call a method after page transition from component outside of router-view?

I want to call a method after page transition from a ChildComponent.vue which is a child of App.vue.
I'm using VueRouter to re-render content inside <router-view>.
The problem is that ChildComponent.vue is not inside <router-view> thus it is not re-created on route change.
The best solution I've got is that inside of the ChildComponent.vue I'm watching a $route which is triggering a function. The problem with that solution is that, the route change automatically when <router-link> is clicked and after that <router-view> animation is triggered and content is replaced. I know that I could resolve it by using a setTimeout but it then relays strictly on the length of my animation and probably is a bad practice.
Because it is triggering on the route change (before content is changed) I cannot access any of the content that will appear on the next page.
APP COMPONENT
<template>
<ChildComponent />
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component"/>
</transition>
</router-view>
</template>
<script>
import { ChildComponent } from "./components";
export default {
name: "App",
components: {
ChildComponent ,
},
};
</script>
CHILD COMPONENT
<template>
<div>Child component</div>
</template>
<script>
export default {
mounted() {
this.test();
},
watch: {
'$route'(to, from) {
this.test()
}
},
methods: {
test(){
console.log("next page");
}
},
};
</script>
Okey, I think I've got it.
I've added a ref to child component inside App.vue and callback on <transition #enter="enterMethod>- it triggers everytime on the next page at the start of reveal animation
This is the method attached to the callback on <transition>
enterMethod() {
this.$refs.childComponent.someChildFunction()
}
end result:
<template>
<ChildComponent ref="childComponent" />
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in" #enter="enterMethod>
<component :is="Component"/>
</transition>
</router-view>
</template>
<script>
import { ChildComponent } from "./components";
export default {
name: "App",
components: {
ChildComponent ,
},
methods:{
enterMethod() {
this.$refs.childComponent.someChildFunction()
}
}
};
</script>

Wrap received slots (vnodes) in anonymous component in Vue.js

I would like to receive two named slots slotOne and slotTwo. These are located in the child component at this.$scopedSlots.slotOne and this.$scopedSlots.slotTwo and contain vnodes. How can I wrap these slots (vnodes) in a new component so that I can conditionally render them like this:
Child Component:
<template>
<div>
<keep-alive>
<component :is="wrapperComponentContainingProperSlot"></component>
</keep-alive>
</div>
</template>
Parent Component:
<template>
<child>
<template v-slot:slotOne>
...
</template>
<template v-slot:slotTwo>
...
</template>
</child>
</template>
I'm guessing the core of this question is, how do I create a component from vnodes inside of another component?
I believe my motivation for desiring to implement this was based on the false premise that <keep-alive> is NOT destroyed when its parent is destroyed. This was not the case. Nevertheless, I did figure out how to wrap slots into their own components, both anonymous and named.
Child component:
<template>
<component :is="componentToRender"></component>
</template>
<script>
import Vue from 'vue';
export default {
computed: {
componentToRender() {
return this.showSlotOnesGlobally
? new this.SlotOneWrapperComponent(this)
: new this.SlotTwoWrapperComponent(this);
},
},
/* Assume a parent further up in the tree provided this data */
inject: {
showSlotOnesGlobally: 'showSlotOnesGlobally'
},
methods: {
SlotOneWrapperComponent(context) {
return Vue.component('SlotOneContentWrapper', {
render() {
return context.$scopedSlots.slotOne();
},
});
},
SlotTwoWrapperComponent(context) {
return Vue.component('SlotTwoContentWrapper', {
render() {
return context.$scopedSlots.slotTwo();
},
});
},
},
};
</script>
Parent component:
<template>
<child>
<template v-slot:slotOne>
...
</template>
<template v-slot:slotTwo>
...
</template>
</child>
</template>
To make them anonymous components, simply replace Vue.component('SlotOneContentWrapper', and Vue.component('SlotTwoContentWrapper', with Vue.extend(.
If anyone can offer a more concise solution, that would be wonderful.

Access this.$slots while using v-slot (scoped-slot)

In a specific use-case, I have to access the DOM element inside a slot to get its rendered width and height. With normal slots, this works by accessing this.$slots.default[0].elm.
Now I added a scoped-slot to access data inside the component, which led to this.$slots being empty and breaking my code.
How is it possible to access the DOM element of a slot that is using a slot-scope?
Basic example code (notice while using a scoped-slot, this.$slots results in {}; while using a normal slot, it results in {default: Array(1)}):
App.vue:
<template>
<div id="app">
<HelloWorld v-slot="{ someBool }">
<p>{{ someBool }}</p>
<h1>Hello</h1>
</HelloWorld>
<HelloWorld>
<h1>Hello</h1>
</HelloWorld>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue';
export default {
name: 'App',
components: {
HelloWorld,
},
};
</script>
HelloWorld.vue:
<template>
<div class="hello">
<slot :someBool="someBool" />
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
someBool: false,
};
},
mounted() {
console.log(this.$slots);
},
};
</script>
Use $scopedSlots, which includes both scoped slots and non-scoped slots:
export default {
mounted() {
console.log(this.$scopedSlots.default())
}
}
demo
Maybe not directly related but wanted to share how to access slot values.
resources/js/components/product/Filter.vue
<template>
<span class="ml-5">
The Slot Values: <span class="text-yellow-400">{{filterValues}}</span>
</span>
</template>
<script>
export default {
data() {
return {
filterValues: ''
}
},
mounted() {
this.filterValues = this.$slots.default()[0].children
}
}
</script>
resources/views/products.blade.php
...
<product-filter-vue>Instrument, FX</product-filter-vue>
...

Bind drawer state in Vuetify when nav-drawer and app-bar is different components

now i have two components in my Dashboard:
Dashboard
<template>
<v-app>
<Toolbar :drawer="app.drawer"></Toolbar>
<Sidebar :drawer="app.drawer"></Sidebar>
</v-app>
</template>
<script>
import Sidebar from './components/layouts/Sidebar'
import Toolbar from './components/layouts/Toolbar'
import {eventBus} from "./main";
import './main'
export default {
components: {
Sidebar,
Toolbar,
},
data() {
return {
app: {
drawer: null
},
}
},
created() {
eventBus.$on('updateAppDrawer', () => {
this.app.drawer = !this.app.drawer;
});
},
}
</script>
Sidebar
<template>
<div>
<v-navigation-drawer class="app--drawer" app fixed
v-model="drawer"
:clipped="$vuetify.breakpoint.lgAndUp">
</v-navigation-drawer>
</div>
</template>
<script>
import {eventBus} from "../../main";
export default {
props: ['drawer'],
watch: {
drawer(newValue, oldValue) {
eventBus.$emit('updateAppDrawer');
}
},
}
</script>
Toolbar
<template>
<v-app-bar app>
<v-app-bar-nav-icon v-if="$vuetify.breakpoint.mdAndDown"
#click="updateAppDrawer">
</v-app-bar-nav-icon>
</v-app-bar>
</template>
<script>
import {eventBus} from "../../main";
export default {
props: ['drawer'],
methods: {
updateAppDrawer() {
eventBus.$emit('updateAppDrawer');
}
}
}
</script>
So now i have an infinite loop because when i press on icon in app-bar - watch in Sidebar, Sidebar understanding it like changes and starts a loop, updating drawer value in Dashboard, then Sidebar
catching changes in watch and starts new loop.
Also i have this warning, but it is another question.
[Vue warn]: 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: "drawer"
Thanks.
I have similar issue and able to solve by following this link
https://www.oipapio.com/question-33116
Dashboard
<template>
<v-app>
<Toolbar #toggle-drawer="$refs.drawer.drawer = !$refs.drawer.drawer"></Toolbar>
<Sidebar ref="drawer"></Sidebar>
</v-app>
</template>
Sidebar
<template>
<v-navigation-drawer class="app--drawer" app fixed v-model="drawer" clipped></v-navigation-drawer>
</template>
<script>
export default {
data: () => ({
drawer: true
})
}
</script>
Toolbar
<template>
<v-app-bar app>
<v-app-bar-nav-icon #click.stop="$emit('toggle-drawer')">
</v-app-bar-nav-icon>
</v-app-bar>
</template>

Multiple named components with different state

So I am trying to use multiple instances of a component inside a parent component. I am currently using a Vuetify.js tab-control to display each component.
For each child component I want to load different dynamic data from a external Rest API.
My preferd way to approach this is to pass a prop down to child's so that they can figure out what data to load. I have a vuex store for each type of child, but would like to reuse template for both, since the data is identical besides values.
Parent/Wrapper
<template>
<div>
<v-tabs fixed-tabs>
<v-tab>
PackagesType1
</v-tab>
<v-tab>
PackagesType2
</v-tab>
<v-tab-item>
<packages packageType="packageType1"/>
</v-tab-item>
<v-tab-item>
<packages packageType="packageType2"/>
</v-tab-item>
</v-tabs>
</div>
</template>
<script>
import Packages from './Packages'
export default {
components: {
Packages
}
}
</script>
Named child component
<template>
<v-container fluid>
<v-data-table v-if="packageType1"
:headers="headers"
:items="packageType1"
>
<div>
//ITEM TEMPLATE
</div
</v-data-table>
<v-data-table v-else-if="packagesType2"
:items="packagesType2"
>
<div>
//ITEM TEMPLATE
</div
</v-data-table>
</v-container>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data () {
return {
name: 'packages'
}
},
props: ['packageType'],
computed: {
...mapGetters('packageModule1', ['packageType1']),
...mapGetters('packageModule2', ['packagesType2'])
},
methods: {
getPackageByType () {
if (this.packageType === 'packageType1') {
this.getPackageType1()
return
}
this.getPackageType2()
},
getPackageType1 () {
this.$store.dispatch('chocoPackagesModule/getPackageType1')
.then(_ => {
this.isLoading = false
})
},
getPackageType2 () {
this.$store.dispatch('packagesType2Module/getPackageType2')
.then(_ => {
this.isLoading = false
})
}
},
created () {
this.getPackageByType()
}
}
</script>
Right now it renders only packageType1 when loading.
How can I easily differ those types without duplicating everything?
Should I just hard differ those types? Make separate child-components and role with it that way? I am new to this, but would love as much reuse as possible.

Categories

Resources