I cannot watch Vuex state in components within v-tabs until first visit - vue.js

vuetify: ^2.5.10
vuex: ^3.6.2
vue: ^2.6.14
I have an application developed in v-tabs. In the child component in the first of these tabs, I have a
<v-file-input
v-model="file"
outlined
:rules="selector_rules"
:clearable="false"
:show-size="1000">
</v-file-input>
<v-btn color="primary" #click="commitData" class="mr-2">Commit</v-btn>
with
commitData() {
this.$store.commit('setFile', this.file)
},
The store gets correctly set because the following computed property in the parent component
computed: {
...mapGetters({
file: 'getFile',
}),
},
seems to work: I watch it in this way:
file: {
deep: true,
immediate: true,
handler() {
console.log("Received file: " + this.file);
}
},
The surprising thing, at least to me, is that the very same watch above, implemented in another component in the second tab, does not work until I visit (i.e., switch to) the second panel. However, the computed property works in this component because in the template I can see the new property.
After the first visit to the tab, the watch continues to work without problems.
Is there a reason for that and a way to avoid visiting it to make the watch work?

v-tabs loads content only on visit. Which means component will mount only when it's active.
You can use eager prop on your v-tab-item components to make sure they're always loaded and mounted.
API Docs reference: https://vuetifyjs.com/en/api/v-tab-item/#props-eager

Related

Get v-slot's value

I got an component like this:
<template>
<Popover v-slot="{ open }" :ref="`${name}-parent`">
<div>
<PopoverButton :ref="name">
<div>
<slot name="buttoncontent"></slot>
</div>
<ChevronDownIcon/>
</PopoverButton>
</div>
<transition>
<PopoverPanel>
<slot name="popovercontent"></slot>
</PopoverPanel>
</transition>
</Popover>
</template>
<script>
import {Popover, PopoverButton, PopoverPanel} from '#headlessui/vue'
import {ChevronDownIcon} from '#heroicons/vue/solid'
export default {
name: 'PopoverMenu',
props: {
name: {
type: String,
},
title: {
type: String,
default: ''
},
},
components: {
Popover,
PopoverButton,
PopoverPanel,
ChevronDownIcon,
},
setup () {
return {}
},
watch: {
'$route' () {
// this.$refs[this.name] ... do fancy stuff on route change here
},
},
mounted () {
console.log(this.$refs[`${this.name}-parent`])
}
}
</script>
Now I'd like to change the open state depending on the change of the route. Ergo: If the user clicks a link the popover should close.
The Popover, PopoverButton and PopoverPanel are provided by headlessui and only offer the open slot just within the component. My idea was to access the open property and change it manually.
My idea was to access the open property
Accessing the open property is easy, in a way you already have it in your template. If you want to hold on to it (e.g. keep a reference to open so that you can use it some time later), you can convert your slot content into a component and receive open as the props.
and change it manually.
However, this is prohibited. The moment you try to mutate the open you will get a warning saying it is readonly. In general, properties are always readonly, this is enforced by vue. Scoped slots are just anonymous components very similar to lambda functions, and the open variable is just one of the properties for the slot.
Ideally the library you are using should expose not just an open state but also two more methods (open() and close()) to the slot. Unfortunately, not all libraries are built that thoughtful.
You can try to move the focus to some other elements when your route changes and see if that can close the popover. If not, you can manually implement the vue wrapper for popover. This is something I would do btw, for the simple use cases (i.e. pop some panel), it is trivial to implement using popover.

Why does a prop provided with a static object re-renders the component on change?

I encountered this trivial but head-scratching case that feels uncomfortably counter-intuitive.
Imagine a Vue component that accepts 2 props: one object (filters) and one string (button). The component will be able to change the button prop by emitting an update:button event. If the parent is concerned about the changes, it's expected to use the .sync modifier.
The child component also has a deep watcher on the object prop (filters).
Vue.component('child-component', {
template: `<button #click="onClick">{{ button }}</button>`,
props: { filters: Object, button: String, },
watch: {
filters: {
deep: true,
handler() {
console.log('filters changed')
},
},
},
methods: {
onClick() {
this.$emit('update:button',
Math.random().toString(36).substring(7)
)
console.log('button clicked')
},
},
});
----
new Vue({
data: {
filters: {
search: ''
},
button: 'Click me!',
}
});
It seems now that there's an important difference in behavior of the child component, based on the way of defining the filters prop in the parent:
<!-- Filters prop passed entirely -->
<child-component :filters="filters" :button.sync="button" />
<!-- Filters prop constructed in place -->
<child-component :filters="{ search: filters.search }" :button.sync="button" />
In the first scenario: everything works as expected. The filters watcher in the child behaves properly, the synced button prop behaves properly. But it starts getting weird in the latter scenario: as soon as an update for the button prop syncs, the filters watcher is triggered as well!
I'm having trouble wrapping my head around this. Is this expected? If so, why?
Here's a contrived JSFiddle showing this behavior:
https://jsfiddle.net/5eqzbsm4/
I suppose this is a regular behavior because each time a button changes VueJs rerenders child-component and thus recreates { search: filters.search } because it's constructed right in the template

Stop full page re-render on route change? (Nuxt.js)

I'm trying to implement custom routing in Nuxt using _.vue. My _.vue page file has two child components (let's call them Category and Product), each of which is displayed when their data is present in the store by using v-if. I'm using middleware for the _.vue component to process custom routing.
My problem is that all my components get re-rendered on each route change, which causes delays and image flickering.
Let me explain what I'm trying to achieve. There's a list of products in a category. When you click on a product, it opens in a modal window with the category still in the background, and also changes the current page URL, hence the routing thing. When you close the product, it should go back to the category in the same state as before. Everything seems to be working as needed, except when I open or close a product, all my components components get re-rendered (including _.vue). I tried naming them, using key(), etc. with no results.
Is there any way to keep current components on route change without rerendering? Or is there a workaround?
<template>
<div>
<Category v-if="current_category" />
<Product v-if="current_product" />
</div>
</template>
<script>
import {mapState} from 'vuex'
import Product from '~/components/product/Product'
import Category from '~/components/category/Category'
export default {
middleware: 'router',
scrollToTop: false,
components: {
Product,
Category,
},
computed: {
...mapState({
current_category: state => state.current_category,
current_product: state => state.current_product,
}),
},
mounted: function() {
console.log('_ component mounted')
},
}
</script>
You should use "key" option in page components. By default value is "route.fullPath", so you see rerendering after changing URL parameters.
https://nuxtjs.org/docs/2.x/features/nuxt-components#the-nuxt-component

Cleanest way to re-render component in Vue

I have a Keyboard.vue component containing many Key.vue child component instances (one for each key).
In Key.vue, the key is actually a html <button> element that can get disabled.
By clicking a certain button in my app, I want to reset keyboard and make all keys enabled again. I thought that setting a v-if to false then to true again (<keyboard v-if="BooleanValue" />) would re-render Keyboard.vue and all its Key.vue child component instances.
It doesn't. Why not?
App.vue
<template>
<div class="app">
...
<keyboard v-if="!gameIsOver && showKeyboard" />
...
</div>
</template>
<script>
export default {
components: {
Keyboard
},
computed: {
gameIsOver () {
return this.$store.state.gameIsOver
},
showKeyboard () {
return this.$store.state.showKeyboard
}
}
Keyboard.vue
<template>
<section>
<key class="letter" v-for="(letter) in letters" :key="letter" :letter="letter" />
</section>
</template>
Key.vue
<template>
<button :disabled="disabled" #click="checkLetter(letter)">
{{ letter }}
</button>
</template>
<script>
export default {
...
data () {
return {
disabled: false
}
}
My button resetting keyboard triggers:
this.$store.commit('SET_KEYBOARD_VISIBILITY', false)
this.$store.commit('SET_KEYBOARD_VISIBILITY', true)
To answer your question first, the cleanest way to re-render a Vue component or any element is to bind it's key attribute to something reactive that will control the re-renders, whenever the key value changes it will trigger a re-render.
To make such a unique key per render, I would probably use an incremented number and whenever I would like to re-render I would increment it.
<template>
<div>
<div :key="renderKey">
</div>
</div>
</template.
<script>
export default {
data: () => ({
renderKey: 0
}),
methods: {
reRender() {
this.renderKey++;
}
}
};
</script>
Now as for why toggling v-if didn't work: Toggling a reactive property between true and false doesn't necessarily trigger 2 re-renders because Vue has an async update queue which applies DOM changes in patches in certain time frames, not per individual update. This why Vue is so fast and efficient.
So you trigger disabled to false, then to true. The renderer will decide not to update the DOM because the final value has not changed from the last time, the timing is about 16ms If I recall correctly. So you could make that work by waiting more than 16ms between toggling your prop between true and false, I say "could" but not "should".
The cleanest way is to have the disabled state somewhere where you can reset it, because re-rendering your component to reset it is making use of a side-effect of destroying and re-creating your components. It makes it hard for someone to figure out why the buttons are enabled again, because there is no code changing the disabled variable to false anywhere that is being called when you rerender.
That said, you see your current behaviour because Vue aggregates all changes of the current "tick", and only rerenders at the end of that tick. That means if you set your variable to false, then to true, it will only use the last value.
// Nothing happens
this.showSomething = false
this.showSomething = true
To force it to re-render, you can use the trick Amitha shows, using key. Since Vue will use an instance per key value, changing the key will destroy the old one and create a new one. Alternatively, you can use this.$nextTick(() => { ... }) to force some of your code to run on the next tick.
// Destroy all the things
this.showSomething = false
this.$nextTick(() => {
// Okay, now that everything is destroyed, lets build it up again
this.showSomething = true
});
Could you please give a try to set a :key for keyboard like below
<keyboard v-if="!gameIsOver && showKeyboard" :key="increment" />
"increment": local component property
and increase the "increment" property value by one when you need to re-render view. use the vuex store property to sync with local "increment" property.
how to sync vuex value changes with local property: add a "watcher" to watch the vuex store property changes and assigned that change to the local "increment" property which we setted as the "key" for keyboard

Vue.js best practice to remove component

My website overwrites a specific DOM instead of asking for a page load. Sometimes, inside the DOM there will be some Vue.js components (all are single page components), which will be overwritten. Most of them reside in an app created for the sole purpose of building the component.
Two questions:
do I need to destroy those components? If YES: how?
Is there any better solution to this approach?
There is a destroy command to erase components:
Completely destroy a vm. Clean up its connections with other existing vms, unbind all its directives, turn off all event listeners.
Triggers the beforeDestroy and destroyed hooks.
https://v2.vuejs.org/v2/api/#vm-destroy
But if you only want the remove/hide the components, which are inside of one or many instances (i.e. the ones that are mounted via .$mount() or el: "#app" ) you should try to remove the components via v-if or hide via v-show.
In the instance template this will look like:
<my-component v-if="showMyComponent"></my-component>
<my-component v-show="showMyComponent"></my-component>
The v-show will just set display:none while v-if will remove the component from the DOM in the same way as $destroy() does. https://v2.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show
You must use a local data prop and use in in directives v-show and v-if
<div id="application">
<vm-test v-if="active"></vm-test>
<div #click="toggle()">toggle</div>
</div>
Vue.component('vm-test', {
template: '<div>test</div>'
});
var vm = new Vue ({
el: "#application",
data: function() {
return {
active:true
}
},
methods: {
toggle: function() {
this.active = !this.active;
}
}
})
https://jsfiddle.net/99yxdacy/2/