Vue: toggling between two instances of the same component doesn't update the view - vue.js

I have a setup in a Vue-powered UI, where the user can toggle the contents of a certain div between several options, and two of those options happen to be instances of the same child component (with different properties passed in).
Everything works fine when displaying any given content page for the first time, or when toggling between two unrelated content pages. However when toggling between the two pages which both use the same child component, the div content doesn't get updated.
In code it looks (greatly simplified) like this:
Parent component
<template>
<div>
<!-- toggle buttons -->
<div class="page-button" #click="page=1">About</div>
<div class="page-button" #click="page=2">Dog List</div>
<div class="page-button" #click="page=3">Cat List</div>
<!-- page content -->
<div v-if="page===1">some plaintext here...</div>
<div v-if="page===2">
<childComponent :state="state" listName="dogs" />
</div>
<div v-if="page===3">
<childComponent :state="state" listName="cats" />
</div>
<!-- rest of file omitted -->
childComponent.vue
<template>
<div>
<template v-for="(item, index) in items">
<div>{{ index }}: {{ item.label }}</div>
<!-- etc.. -->
</template>
</div>
</template>
<script>
module.exports = {
props: ['state', 'listName'],
data: function () {
return {
items: this.state.lists[this.listName],
}
},
}
</script>
In the above, state is a global state object that all components have access to, with state.lists.dogs and state.lists.cats being regular arrays.
When the UI initializes with page set to 2 or 3, everything works correctly - the dog list shows for page 2, and the cat list shows for page 3. Likewise, when I click page 2, then page 1, then page 3, everything is fine. However when toggling back and forth between page 2/3, the vue doesn't re-render the child component.
I assume it's possible to work around this by changing the underlying data structure or by binding the child component differently. But is there a straightforward way to make Vue re-render the component as expected?

I guess what you see is Vue trying to optimize rendering by reusing existing component instance. Add key attribute on your childComponent with different values...
<!-- page content -->
<div v-if="page===1">some plaintext here...</div>
<div v-if="page===2">
<childComponent :state="state" listName="dogs" key="dogs" />
</div>
<div v-if="page===3">
<childComponent :state="state" listName="cats" key="cats" />
</div>
<!-- rest of file omitted -->
Other solution (and much better IMHO) is to make your component "reactive" to prop changes - instead of using props to initialize the data() (which is "one time" thing - data() is executed only once when component is created), use computed
module.exports = {
props: ['state', 'listName'],
computed: {
items() {
return this.state.lists[this.listName]
}
},
}

You can use v-show if you just want to render it before hand. Its more costly but it should work without any issues.
<template>
<div>
<!-- toggle buttons -->
<div class="page-button" #click="page=1">About</div>
<div class="page-button" #click="page=2">Dog List</div>
<div class="page-button" #click="page=3">Cat List</div>
<!-- page content -->
<div v-show="page===1">some plaintext here...</div>
<div v-show="page===2">
<childComponent :state="state" listName="dogs" />
</div>
<div v-show="page===3">
<childComponent :state="state" listName="cats" />
</div>
<!-- rest of file omitted -->

Related

how to delegate content from vue slots to web components slots?

The Problem
Let's say we have a page template written as a Web component in a shared library to keep the company design system consistent. That page has some slots:
export class PageTemplate extends LitElement {
static properties = {
title: { type: String },
};
render() {
return html`
<div>
<h1>${title}</h1>
<slot name="template-body"></slot>
<div class="some-special-styles">
<slot name="template-buttons"></slot>
</div>
</div>
`;
}
}
customElements.define("page-template", PageTemplate);
Then we use this template in a Vue (v3.2.45) application on a base component to be used in the same app by multiple pages.
//page-base.vue
<template>
<page-template title="My App Name">
<slot name="base-body"></slot>
<slot name="base-buttons"></slot>
</page-template>
</template>
Here, we will use the page base vue component on a specific page.
//login-page.vue
<template>
<PageBase>
<template #base-body>
<div slot="template-body">
<input placeholder="some special code"/>
</div>
</template>
<template #base-buttons>
<button slot="template-buttons">login</button>
<button slot="template-buttons">back</button>
</template>
</PageBase>
</template>
To make the login page components show inside that original page template web component; we need to declare the slot property on the leaf components like in <button slot="template-buttons">
How can I implement the Vue Page Base component to avoid the need to remember to set the slot property in every leaf vue component?
Things I've Tryied
I've tried to solve this using the vanilla web syntax below, but Vue appears not to dispatch that information to the final HTML:
//page-base.vue
<template>
<page-template title="My App Name">
<!-- this does not work -->
<slot name="base-body" slot="template-body"></slot>
<slot name="base-buttons" slot="template-buttons"></slot>
</page-template>
</template>
There was also an attempt (after a suggestion in the comments) to use a template as a ghost intermediate in the page base. But nothing was rendered at runtime.
//page-base.vue
<template>
<page-template title="My App Name">
<!-- i can't have that span because of some-special-styles applied in the template-->
<template slot="template-body"><slot name="base-body"></slot></template>
<template slot="template-buttons"><slot name="base-buttons"></slot></template>
</page-template>
</template>
The approach to using some middle element to make the connection (like below) enables content rendering. Still, it does not work for the project requirements because, for style reasons, I need that the final components be the top-most nodes in the page template slots.
//page-base.vue
<template>
<page-template title="My App Name">
<!-- although it runs, i can't have these spans because of some-special-styles applied in the template -->
<span slot="template-body"><slot name="base-body"></slot></span>
<span slot="template-buttons"><slot name="base-buttons"></slot></span>
</page-template>
</template>

NuxtPage vs slot for Nuxt3

What is the difference between these two components in Nuxt3 and how do I use them correctly?
If I want to use pages/... what is the right approach here to create links and jump from page to page?
Everything is pretty much explained in the documentation: https://v3.nuxtjs.org/migration/pages-and-layouts/
You need to use this in app.vue
<template>
<nuxt-layout>
<nuxt-page /> <!-- used to display the nested pages -->
</nuxt-layout>
</template>
With a default /layouts/default.vue file
<template>
<div>
this is coming from the layout
<slot /> <!-- required here only -->
</div>
</template>
You will get this on / (with /pages/index.vue)
<template>
<div>index page</div>
</template>
And with the following structure, you will achieve dynamic pages
/pages/users/index.vue
<script setup>
definePageMeta({
layout: false
});
function goToDynamicUser() {
return navigateTo({
name: 'users-id',
params: {
id: 23
}
})
}
</script>
<template>
<div>
<p>
index page
</p>
<button #click="goToDynamicUser">navigate to user 23</button>
</div>
</template>
/pages/users/[id].vue
<script setup>
definePageMeta({
layout: false
});
const route = useRoute()
</script>
<template>
<pre>{{ route.params.id }}</pre>
</template>
I've removed the layout here to show how to disable it, but you can totally let the default here or even provide a custom one.
So, nuxt-page is to be used when you want to display the pages in your app (replacing <nuxt /> and <nuxt-child />) while <slot /> is to be used in the layout (as any other component using the slot tag).

Using a component inside another component in VueJS

While I was reviewing headlessui's menu component, I saw the use of 2 components that are nested like the following: (see: https://headlessui.dev/vue/menu)
<template>
<Menu>
<MenuButton>More</MenuButton>
<MenuItems>
<MenuItem v-slot="{ active }">
// some content
</MenuItem>
</MenuItems>
</Menue>
</template>
So as you may see, there is a MenuItem component inside of the MenuItems component. And I need something similar to that so I can use a template and put another component's result into that template.
Here the example of what I am trying to do:
<!-- HeadingComponent.vue -->
<div class="group">
<div class="head">
{{ title }} <button>Create New</button>
</div>
<div class="content">
<!-- I want to put some component's rendered content here -->
</div>
</div>
And this is, let's say, a page where I want to use the common component.
<!-- Blog.vue -->
<HeadingComponent :title="Posts">
<BlogPostsComponent :post="someArray"/> <!-- Some other component which may vary -->
</HeadingComponent>
The question
What kind of changes do I need to do in the component HeadingComponent.vue so it works as I expected?
Slots are a good way to add a component to another or even simple html
docs : https://fr.vuejs.org/v2/guide/components-slots.html
<h1>Vue JS Slots Application</h1>
<div id="app">
<slots>
<template slot="slotA"><pre>Slot A Content from parent.</pre></template>
<template><i>Parent Component Content.</i></template>
</slots>
<hr/>
<slots>
<template slot="slotB">Replace Slot B Default Content</template>
<template><b>Replace Default Slot Content.</b></template>
</slots>
</div>
<template id="aside">
<div>
<h3>My Slots App</h3>
<slot>Default Slot Content</slot><br>
<slot name="slotA"></slot><br>
<slot name="slotB"></slot><br>
</div>
</template>
Example of codepen :
https://codepen.io/brian_kim/pen/NpWKGe
Just in a short time, I found something like slots in VueJS which is definitely what I was looking for.
Here is the guide page:
https://v2.vuejs.org/v2/guide/components-slots
What I did in my problem is that I put <slot></slot> tags inside div whose class is content, and then the last sample I gave (Blog.vue) has worked.
<!-- HeadingComponent.vue -->
<div class="group">
<div class="head">
{{ title }} <button>Create New</button>
</div>
<div class="content">
<!-- I want to put some component's rendered content here -->
<slot></slot>
</div>
</div>

Vue v-if v-if-else time complexity

I'm doing a project vue and I wanted to know if vue v-if v-else-if time complexity is O(n) or O(1)
I'm using a component in vue like a switch case (takes input and returns html) something like
<div v-if="type === 'A'">
<!-- HTML Data -->
</div>
<div v-else-if="type === 'B'">
<!-- HTML Data -->
</div>
<div v-else-if="type === 'C'">
<!-- HTML Data -->
</div>
<div v-else>
<!-- HTML Data -->
</div>
but with a lot more cases.
Is that the recommended way to do something like this? or should I make a lot of small component and load them dynamically? which is O(1)
And the reason I have this component is that it takes in a string input and returns a custom icon for a data that can be changed, so they have to be loaded dynamically
I think it's better to create a type/value map and a computed property:
new Vue({
el:"#app",
data(){
return{
type: 'A',
types: {A:'A',B:'B',C:'C'}
}
},
computed:{
typeVal(){
return this.types[this.type];
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
{{typeVal}}
</div>

Vue.js dynamic layout renderless component layout with multiple slots

I am trying to build a dynamic layout for my application. I have two different layouts, one being DefaultLayout.vue:
<template>
<div>
<main>
<slot/>
</main>
</div>
</template>
and a second one being LayoutWithFooter.vue, with two slots:
<template>
<div>
<main>
<slot/>
</main>
<footer>
<slot name="footer"/>
</footer>
</div>
</template>
My renderless component to handle the dynamic layout looks like this:
<script>
import Vue from 'vue';
import DefaultLayout from './DefaultLayout';
import LayoutWithFooter from './LayoutWithFooter';
export default {
props: {
name: {
type: String,
required: true
}
},
created(){
this.registerComponent("DefaultLayout", DefaultLayout);
this.registerComponent("LayoutWithFooter", LayoutWithFooter);
this.$parent.$emit('update:layout', this.name);
},
methods: {
registerComponent(name, component) {
if(!Vue.options.components[name]) {
Vue.component(name, component);
}
}
},
render() {
return this.$slots.default[0];
},
}
</script>
All of this works fine for the DefaultLayout.vue but when I want to use the LayoutWithFooter.vue, it cannot handle the two slots inside it. Here's an example usage:
<template>
<layout name="LayoutWithFooter">
<div>
<div>some content</div>
<div slot="footer">content for the footer slot</div>
</div>
</layout>
</template>
Problem now is, that the "content for the footer slot" does not get rendered inside of the footer slot of the LayoutWithFooter.vue.
First of all I want you to pay an attention to defining slots level in your example. You provided this code:
<template>
<layout name="LayoutWithFooter">
<div>
<div>some content</div>
<div slot="footer">content for the footer slot</div>
</div>
</layout>
</template>
But actually your div slot="footer" does not refer to footer slot of LayoutWithFooter.vue component. It because of anyway the very first child refers to default slot. And as a result it looks like:
"You want to set content for default slot and inside this default slot you tried to set content for footer slot" - but it's two different scopes.
The right options would look like on the next example:
<template>
<layout name="LayoutWithFooter">
<!-- default slot content -->
<div>
<div>some content</div>
</div>
<!-- footer slot content -->
<div slot="footer">content for the footer slot</div>
</layout>
</template>
I prepared some example based on code and structure you've provided. There you are able to switch layouts and check out how it works and use different components slot in one layout.
Check it here:
https://codesandbox.io/s/sad-fog-zr39m
P.S. Maybe some point are not totally clear, please reply on my answer and I will try to explain more and/or provide you with more links and sources.