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

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.

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>

Update props in component through router-view in vue js

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.

How to render variable components in vue like jsx?

In jsx we can store components in variables like const comp=<p>Hello</p> and then we can put these variables anywhere, choosing witch component to render.
I was wondering if there is a similar thing in vue. If I had a template like:
<template>
<variable-comp />
</template>
I would like to change what variable-comp is dynamically. I'm aware of v-if and v-for but that's not the same thing.
vue uses this syntax for dynamic components
<component v-bind:is=”currentComponent”/>
where 'currentComponent' is the name (string) of component.
i.e.
<template>
<component v-bind:is=”currentComponent”/>
</template>
import CompA from './CompA.vue'
import CompB from './CompB.vue'
export default {
components: {
CompA,
CompB
},
data() {
isA: true
},
computed: {
currentComponent() {
return isA ? 'CompA' : 'CompB'
}
}
}

how to pass components as props in vue js and how to use it properly?

Hello I have a Child component that the main function is to filter and render a list of items. This child component is to be used in multiple parent components(views) and depending on the parent component the child component need to render a different child component (grand child).
Parent Component
<template>
<main>
//Child Component
<list-component
name="my items"
//List of Items I need to render
:list="items.list"
>
//Slot Passing my grandchild component
<template slot="child-component">
<component :is="child_component" :items="item"></component>
</template>
</list-component>
</main>
</template>
<script>
import ListComponent from '.ListComponent';
import ItemComponent from '.ItemComponent.vue';
export default {
components: {
ListComponent,
ItemComponent
},
data() {
return {
child_component: 'ItemComponent'
};
},
}
</script>
ListComponent.vue (child component)
<template>
<main>
<v-row class="ma-0">
<v-col v-for="(item, index) in list" :key="index" class="pa-0">
// I would like render my grandchild component here.
<slot name="child-component"></slot>
</v-col>
</v-row>
</main>
</template>
<script>
export default {
props: {
name: {
type: String,
required: true
},
list: {
type: Array,
required: true
}
}
};
</script>
ItemComponent.vue (grand child)
<template>
<div>
<v-img
src="item.image"
></v-img>
<v-row>
<span>{{
item.name
}}</span>
</v-row>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true
}
}
}
</script>
So basically I need to be able to pass ItemComponent.vue(grandchild) from the View(grandfather) to the View's ListComponent.vue(child) so that the child component can loop trough the items passed from the parent and use the grand child to render the information.
Also where do I declare the props for the grandchild?
I was able to find a solution after all I will leave this for those who need it.
basically in the child component we need to give access to the attribute to the parent trough the slot by binding the item like:
<slot name="child-component" :item="item"></slot>
and on the parent we can access it by binding the slot and giving a name to the object in this case I chose child and notice that on the component we can access item by declaring child.item
<template v-slot:child-component="child">
<component :is="child_component" :itinerary="child.item"></component>
</template>

Is it possible to pass a component as props and use it in a child Component in Vue?

In a Vue 2.0 app, let's say we have components A, B and C.
A declares, registers and uses B
Is it possible to pass C from A to B?
Something like this:
<template>
<div class="A">
<B :child_component="C" />
</div>
</template>
And use C in B somehow.
<template>
<div class="B">
<C>Something else</C>
</div>
</template>
The motivation: I want to create a generic component B that is used in A but receives from A its child C. Actually A will use B several times passing different 'C's to it.
If this approach is not correct, what is the proper way of doing it in Vue?
Answering #Saurabh
Instead of passing as props, I tried the suggestion inside B.
<!-- this is where I Call the dynamic component in B -->
<component :is="child_component"></component>
//this is what I did in B js
components: {
equip: Equipment
},
data () {
return {
child_component: 'equip',
_list: []
}
}
Basically I'm trying to render Equipment, but the dynamic way
I get 3 errors in console and a blank page
[Vue warn]: Error when rendering component at /home/victor/projetos/tokaai/public/src/components/EquipmentFormItem.vue:
Uncaught TypeError: Cannot read property 'name' of undefined
TypeError: Cannot read property 'setAttribute' of undefined
Apparently I'm doing something wrong
Summing up:
<!-- Component A -->
<template>
<div class="A">
<B>
<component :is="child_component"></component>
</B>
</div>
</template>
<script>
import B from './B.vue';
import Equipment from './Equipment.vue';
export default {
name: 'A',
components: { B, Equipment },
data() {
return { child_component: 'equipment' };
}
};
</script>
<!-- Component B -->
<template>
<div class="B">
<h1>Some content</h1>
<slot></slot> <!-- Component C will appear here -->
</div>
</template>
You can use special attribute is for doing this kind of thing. Example of dynamic component and its usage can be found here.
You can use the same mount point and dynamically switch between multiple components using the reserved element and dynamically bind to its is attribute.
Here's how is can be used with either an imported component or one passed as a prop:
<template>
<div class="B">
<component :is="myImportedComponent">Something</component>
--- or ---
<component :is="myPassedComponent">Something else</component>
</div>
</template>
<script>
import myImportedComponent from "#/components/SomeComponent.vue"
export default {
props: {
myPassedComponent: Object
},
components: {
myImportedComponent
},
}
</script>
Here's solution to forward custom component through props of another component
:is is special attribute and it will be used to replace your actual component and it will be ignored if you try to use it as a prop in your component. Luckily you can use something else like el and then forward this to component like so:
<template>
<div>
<component :is="el">
<slot />
</component>
</div>
</template>
<script>
export default {
name: 'RenderDynamicChild',
props: {
el: {
type: [String, Object],
default: 'div',
},
},
}
</script>
Any valid element you use in el attribute will be used as a child component. It can be html or reference to your custom component or div by default as specified in component declaration.
Passing custom component to prop is little bit tricky. One would assume you declare in a components property of parent component and then use it for el attribute but this doesn't work. Instead you need to have your dynamic component in data or computed property so you can use it in a template as a prop. Also note AnotherComponent doesn't need to be declared in components property.
<template>
<RenderDynamicChild :el="DynamicComponent">
Hello Vue!
</RenderDynamicChild>
</template>
<script>
import RenderDynamicChild from './DynamicChild';
import AnotherComponent from './AnotherComponent';
export default {
name: "ParentComponent",
components: { DynamicChild },
data() {
return {
DynamicComponent: AnotherComponent,
};
},
};
</script>
Using computed property for your dynamic component allows you to switch between components easily:
<script>
import DynamicChild from './DynamicChild';
import AnotherComponent from './AnotherComponent';
export default {
name: "ParentComponent",
components: { DynamicChild },
data() { return { count: 0 } },
computed: {
DynamicComponent() {
return this.count % 2 > 1 ? AnotherComponent : 'article';
},
},
};
</script>
Increase this.count to alternate between AnotherComponent and simple article html element.
Maybe it's too late to answer this question. But I think it could help others with this same issue.
I've been looking for a way to pass components throw others in vue, but it looks that VUE3 have a approach for that using named slots:
Here it's the documentation about that:
https://v3.vuejs.org/guide/component-slots.html#named-slots
Basically you can have:
<template>
<div class="A">
<slot name="ComponentC"></slot> <!-- Here will be rendered your ComponentC -->
</div>
<div class="A">
<slot name="ComponentD"></slot> <!-- Here will be rendered your ComponentD -->
</div>
<div class="A">
<slot></slot> <!-- This is going to be children components -->
</div>
</template>
And from your B component
<template>
<div class="B">
<A>
<template v-slot:ComponentC>
<h1>Title of ComponentC </h1>
</template>
<template v-slot:ComponentD>
<h1>Title of ComponentD </h1>
</template>
<template v-slot:default>
<h1>Title of child component </h1>
</template>
</A>
</div>
</template>
If you would like to use another component within your functional component you can do the following:
<script>
import Vue from 'vue'
import childComponent from './childComponent'
Vue.component('child-component')
export default {}
</script>
<template functional>
<div>
<child-component/>
</div>
</template>
Reference:
https://github.com/vuejs/vue/issues/7492#issue-290242300
If you mean Dynamically importing a component in a parent component, so yes, you can do that in Vue3 using:
<component :is="child_component" />
but to render "child_component" itself dynamically, you can use
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
Let me give you an example:
let's say you have several multiple child components (ChildA, ChildB, ChildC) that you want to load dynamically based on what you pass to the parent component (Parent), so the Parent component will be something like this:
Parent
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
const props = defineProps<{
childComponent?: string;
}>();
const AsyncComp = defineAsyncComponent(() =>
import(`./${props.childComponent}.vue`)
)
</script>
<template>
<component :is="AsyncComp"/>
</template>
and then you can call the Parent component dynamically wherever you want like this:
<Parent :childComponent="child-a"/>
<Parent :childComponent="child-b"/>
<Parent :childComponent="child-c"/>
For a better explanation, you can check this article:
https://medium.com/#pratikpatel_60309/dynamic-importing-component-templates-with-vue-js-78d2167db1e7

Categories

Resources