Vue keeping specific component alive under router-view - vue.js

In my home page I have two components. I want only one to be kept alive.
HomeView.vue
<template>
<div>
<keep-alive>
<AliveComponent />
</keep-alive>
<DeadComponent />
</div>
</template>
In my App.vue I have kept the <router-view /> alive as well
<template>
<Header />
<main>
<keep-alive include="HomeView">
<router-view />
</keep-alive>
</main>
<Footer />
</template>
The problem is the <keep-alive include="HomeView"> is keeping both components alive. I tried to replace it with include="AliveComponent" but turns out nothing is alive.
Is there any way to do it?

Make sure your components have a valid name that you are planning to use in keep-alive. This could be an issue if you did not provide a name to your "AliveComponent" because the include property matches the component name.
I produced a demo here in which I create two routes. Each route has a component and each component has its state. When changing any component's state and switching to another route, the component which is included in the keep-alive will have preserved state while others will not.
// 1. Define route components.
// These can be imported from other files
const AliveC = {
name: "AliveComponent",
template: `<div>
<button #click="count = count + 1">Increase Alive Count</button>
<h3>Alive Component Count - {{ count }}</h3>
</div>`,
data() {
return {
count: 0,
};
},
}
const DeadC = {
name: "DeadComponent",
template: `<div>
<button #click="count = count + 1">Increase Dead Count</button>
<h3>Dead Component Count - {{ count }}</h3>
</div>`,
data() {
return {
count: 0,
};
},
}
// 2. Define some routes
// Each route should map to a component.
// We'll talk about nested routes later.
const routes = [
{ path: '/', component: AliveC },
{ path: '/dead', component: DeadC },
]
// 3. Create the router instance and pass the `routes` option
// You can pass in additional options here, but let's
// keep it simple for now.
const router = VueRouter.createRouter({
// 4. Provide the history implementation to use. We are using the hash history for simplicity here.
history: VueRouter.createWebHashHistory(),
routes, // short for `routes: routes`
})
// 5. Create and mount the root instance.
const app = Vue.createApp({})
// Make sure to _use_ the router instance to make the
// whole app router-aware.
app.use(router)
app.mount('#app')
// Now the app has started!
<html>
<script src="https://unpkg.com/vue#3"></script>
<script src="https://unpkg.com/vue-router#4"></script>
<div id="app">
<p>
<router-link to="/">Switch To Alive Component Route</router-link>  
<router-link to="/dead">Switch To Dead component Route</router-link>
</p>
<!-- route outlet -->
<!-- component matched by the route will render here -->
<router-view v-slot="{ Component }">
<keep-alive include="AliveComponent">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</html>

Related

How to extend the onClick handler behaviour for a `router-link` in vue3 router

I have a <router-link /> that I want to use to navigate to another page in a Vue 3 application, but I also want to run another function when this link is clicked.
Right now I have to use an extra <span /> element to wrap the <router-link /> and add the #click attribute there to rely on event bubbling. Adding the #click handler on <router-link /> causes the router link not to work and the browser thinks it is just a normal anchor href.
<span #click="handleClose(imageId)">
<router-link
:to="{name: 'image', params: {'imageId': imageId}}"
class="permalink">
Permalink
</router-link>
</span>
Is there a better way?
You must use the .native modifier on the #click event of router-link. The reason is quite simple - router-link is a Vue component but it does not emit a click event. Therefore you have to listen for the native click event coming from the browser.
https://github.com/vuejs/vue-router/issues/800
var router = new VueRouter({
routes:
[
{path: '/', component: {
template: '#first',
methods:
{
showAlert(text)
{
window.alert(text);
}
}
}},
{path: '/second', component: {
template: '#second',
methods:
{
showAlert(text)
{
window.alert(text);
}
}
}},
]
});
new Vue(
{
template: '#main',
router: router,
}).$mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">test</div>
<template id="main">
<div>
<router-view></router-view>
</div>
</template>
<template id="first">
<router-link to="/second" #click.native="showAlert('second')">GOTO second</router-link>
</template>
<template id="second">
<router-link to="/" #click.native="showAlert('first')">GOTO first</router-link>
</template>
To avoid potential weird concurrency bugs (the view change could happen before your method being called, method which would then be attached to an unmounted component), I would do a programmatic navigation in your method:
<template>
<button type="button" #click="handleClose(imageId)">
Permalink
</button>
</template>
<script>
import router from '#/wherever/your/router/is/initialized';
export default {
methods: {
handleClose(imageId) {
// Do your stuff
router.push({ name: 'image', params: {'imageId': imageId}});
}
}
}
</script>

Quasar QTable lost state after returning from child component

I have a QTable that renders link where user can click into detail page and use back button to return to this table later. If I move to page 2 and click on the link to view detail page, on return, the table state is lost and it's showing page 1 again.
I've tried using Keep-Alive in hope that component will not get destroyed but it didn't help.
How do I get around this problem?
If you wrap the table within keep-alive, it will be kept alive as long as that page is alive. But, since you are navigating into another page, the table will be destroyed along with the page that contains it.
So, to achieve your aim, you must keep the whole page alive. You can do it like this:
<template>
<q-page>
<!-- ... -->
<q-table ... />
<!-- ... -->
</q-page>
</template>
<script>
export default {
name: 'ThePageYouWantToKeepAlive',
// ...
}
</script>
keep-alive has some props to manage inclusions/exclusions, you can use include prop to only include the page(s, it accepts a comma-separated list, an array or RegExp) that you want to keep alive. This way you would keep your performance loss to a minimum.
<!-- /src/layouts/MainLayout.vue -->
<!-- ... -->
<q-page-container>
<keep-alive include="ThePageYouWantToKeepAlive">
<router-view></router-view>
</keep-alive>
</q-page-container>
<!-- ... -->
The demonstration below would help to understand the situation better.
// Source: https://codepen.io/yusufkandemir/pen/zYrZOKx?editors=1010
// Adopted from: https://jsfiddle.net/Linusborg/L613xva0/4/
const Foo = {
name: 'foo',
template: '<div><p v-for="n in numbers">{{ n }}</p></div>',
data: function() {
return {
numbers: [Math.round(Math.random() * 10), Math.round(Math.random() * 10)]
}
}
}
const Bar = {
name: 'bar',
template: '<div><p v-for="n in numbers"><strong>{{ n }}</strong></p></div>',
data: function() {
return {
numbers: [Math.round(Math.random() * 10), Math.round(Math.random() * 10)]
}
}
}
const Baz = {
name: 'baz',
components: {
Foo
},
template: '<div><foo></div>'
}
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar },
{ path: '/baz', component: Baz }
]
const router = new VueRouter({
routes
});
const app = new Vue({
router
}).$mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.2.0/vue-router.min.js"></script>
<div id="app">
<ul>
<router-link to="/foo">
<li>Go to Foo</li>
</router-link>
<router-link to="/bar">
<li>Go to Bar</li>
</router-link>
<router-link to="/baz">
<li>Go to Baz</li>
</router-link>
</ul>
<keep-alive include="foo">
<router-view></router-view>
</keep-alive>
</div>
You need to store the state of the table in the parameters of the route or vuex, localStorage.

render a Vue slot in a layout from component

I'm having problems with a named slot. This seems like it should work. In the code below I'm trying to use a named slot "sidebar". I would expect my sidebar slot content to show up between the Sidebar starts and Sidebar ends text but nothing shows up in that slot. Everything renders in the main slot.
Here's my code.
route...
{
path: "/test",
name: "test",
meta: {
layout: "test-layout"
},
component: () =>
import("#/pages/Test.vue")
},
and App.vue template...
<template>
<div id="app">
<component :is="layout">
<router-view />
</component>
</div>
</template>
and test-layout...
<template>
<div>
<div>
<h1>Sidebar starts</h1>
<slot name="sidebar"/>
<h1>Sidebar ends</h1>
</div>
<div class="container">
<h1>Content starts</h1>
<slot/>
<h1>Content ends</h1>
</div>
</div>
</template>
and page Test.vue...
<template>
<test-layout>
<span slot="sidebar">sidebar slot content {{forecast.todaySummary}}</span>
<div>main slot content {{forecast.currentSummary}}</div>
</test-layout>
</template>
<script>
import api from "#/js/web-services";
export default {
data() {
return {
forecast: null
};
},
created() {
api.getDailyForecast().then(response => {
this.forecast = response.data;
});
}
};
</script>
and the import in my main.js
import TestLayout from "./layouts/test-layout.vue";
Vue.component('test-layout', TestLayout);
Why isn't my sidebar slot working?
UPDATE
If I get rid of the two lines in main.js and add
import TestLayout from "#/layouts/test-layout.vue";
and
export default {
components: { TestLayout },
data() {...
to Test.vue then it works.
In your router file you are using layout: "test-layout" this means what ever comes in your vue component will be rendered in base test-layout.
There are two ways as far as I know to render the layouts.
Do not define layout in router file and on parent component define named slots like this<slot #header></slot><slot #body></slot> then every slot will be rendered within this (test-layout) layout, and then in your each component extend like this <test-layout><template header>Header content</template><template body>Body content</template></test-layout>.
Defining layout in router file like you did, you can not further use in slots in that layout, you can just import other components e.g <template><Component1><Component2> </template>

Share an object with another Vue component

I know this has been asked several times before, but as a Vue.js beginner I had trouble interpreting some of the other discussions and applying them to my situation. Using this CodeSandbox example, how would one pass the indicated object from "Hello" to "Goodbye" when the corresponding button is pressed? I'm unsure if I should be trying to use props, a global event bus, a plugin, vuex, or simply some sort of global variable.
Edit:
Here is the code for App.vue, Hello.vue and Goodbye.vue (from the previously linked CodeSandbox example).
App.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "app"
};
</script>
Hello.vue:
<template>
<div class="hello">
<h1>This is Hello</h1>
<div v-for="(obj, index) in objects" :key="index">
<router-link class="button" :to="{ path: '/goodbye'}">Share obj[{{ index }}] with Goodbye</router-link>
</div>
</div>
</template>
<script>
export default {
name: "hello",
data() {
return {
objects: [0, 1, 2, 3]
};
}
};
</script>
Goodbye.vue:
<template>
<div class="goodbye">
<h1>This is Goodbye</h1>
<p>Obj = "???"</p>
<router-link class="button" :to="{ path: '/hello'}">Hello</router-link>
</div>
</template>
<script>
export default {
name: "goodbye"
};
</script>
Props are used to share data with child components. Since the components never exist at the same time, this is not useful for you. Similarly, events are not very useful to you here. You can send an event on a global bus, but since the other component does not exist yet, it cannot listen for the event.
I am not sure what you would want to do with a plugin in this case. You should never use a global variable, unless you have a very good reason to (e.g. you use Google Analytics, which happens to use a global variable, or you want to expose something within Vue in development mode for debugging purposes). In your case, you likely want to change some global app state, which is exactly what Vuex was made for. Call a Vuex mutator or action either when clicking, or in a router hook such as router.beforeEach to save the information in a structured manner so you can then retrieve it with a mapped getter. Keep in mind that you want to structure your vuex store, so don't use a state variable thingsIWantToShareWithGoodbye, but instead split it up in previousPage, lastClickOffset and numberOfClicks.
For example:
// store/index.js
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const state = {
button: null
};
const getters = {
button(state) {
return state.button;
}
};
const mutations = {
setButton(state, payload) {
state.button = payload;
}
};
export default new Vuex.Store({
state,
getters,
mutations
});
// Hello.vue
<template>
<div class="hello">
<h1>This is Hello</h1>
<div v-for="(obj, index) in objects" :key="index">
<router-link #click.native="setButtonState(obj)" class="button" :to="{ path: '/goodbye'}">Share obj[{{ index }}] with Goodbye</router-link>
</div>
</div>
</template>
<script>
export default {
name: "hello",
data() {
return {
objects: [0, 1, 2, 3]
};
},
methods: {
setButtonState (obj) {
this.$store.commit('setButton', obj)
}
}
};
</script>
// Goodbye.vue
<template>
<div class="goodbye">
<h1>This is Goodbye</h1>
<p>Obj = {{ button }}</p>
<router-link class="button" :to="{ path: '/hello'}">Hello</router-link>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: "goodbye",
computed: {
...mapGetters({
button: 'button'
})
}
};
</script>

How to pass props using slots from parent to child -vuejs

I have a parent component and a child component.
The parent component's template uses a slot so that one or more child components can be contained inside the parent.
The child component contains a prop called 'signal'.
I would like to be able to change data called 'parentVal' in the parent component so that the children's signal prop is updated with the parent's value.
This seems like it should be something simple, but I cannot figure out how to do this using slots:
Here is a running example below:
const MyParent = Vue.component('my-parent', {
template: `<div>
<h3>Parent's Children:</h3>
<slot :signal="parentVal"></slot>
</div>`,
data: function() {
return {
parentVal: 'value of parent'
}
}
});
const MyChild = Vue.component('my-child', {
template: '<h3>Showing child {{signal}}</h3>',
props: ['signal']
});
new Vue({
el: '#app',
components: {
MyParent,
MyChild
}
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<my-parent>
<my-child></my-child>
<my-child></my-child>
</my-parent>
</div>
You need to use a scoped slot. You were almost there, I just added the template that creates the scope.
<my-parent>
<template slot-scope="{signal}">
<my-child :signal="signal"></my-child>
<my-child :signal="signal"></my-child>
</template>
</my-parent>
Here is your code updated.
const MyParent = Vue.component('my-parent', {
template: `<div>
<h3>Parent's Children:</h3>
<slot :signal="parentVal"></slot>
</div>`,
data: function() {
return {
parentVal: 'value of parent'
}
}
});
const MyChild = Vue.component('my-child', {
template: '<h3>Showing child {{signal}}</h3>',
props: ['signal']
});
new Vue({
el: '#app',
components: {
MyParent,
MyChild
}
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<my-parent>
<template slot-scope="{signal}">
<my-child :signal="signal"></my-child>
<my-child :signal="signal"></my-child>
</template>
</my-parent>
</div>
The release of Vue 2.6 introduces a unified v-slot directive which can be used for normal or scoped slots. In this case, since you're using the default, unnamed slot, the signal property can be accessed via v-slot="{ signal }":
<my-parent>
<template v-slot="{ signal }">
<my-child :signal="signal"></my-child>
<my-child :signal="signal"></my-child>
</template>
</my-parent>
I added this code inside of <v-data-table></v-data-table>
<template
v-for="slot in slots"
v-slot:[`item.${slot}`]="{ item }"
>
<slot
:name="slot"
:item="item"
/>
</template>
And I added a props called slots. When I call the component I send a slots like:
<my-custom-table-component :slots="['name']">
<template v-slot:name="{ item }">
{{ item.first_name + item.last_name}}
</template>
</my-custom-table-component>
You may try this technique.
In this example.
Let assume a parent component wants to share prop foo with value bar.
Parent component
<parent>
<template v-slot:default="slotProps">
// You can access props as object slotObjects {foo"bar"}
<p>{{slotProps.foo}}</p>
</template>
<parent>
Child
<template>
<div class="parent">
<slot :foo="bar" />
</div>
</template>
Got the idea from this video
I hope it helped you accomplish your tasks.