Passing slot to grandchild though dynamic component - javascript

I have a wrapper component(Wrapper.vue).
<component v-for="c of comps" :key="c.componentName" :is="c.componentName">
<slot :name="c.componentName">
</slot>
</component>
const comps =
[
{
componentName: "Child1",
},
{
componentName: "Child2",
}
];
Child1.vue is something like below
<template>
<div class="hello">
<slot name="top">Child1 slot top text</slot>
</div>
</template>
Child2.vue is something like below
<template>
<div class="hello">
<slot name="top">Child2 slot top text</slot>
</div>
</template>
I want to use the wrapper like below in App.vue
<Wrapper>
<template slot="slot1">
<template slot="top">Inner Content1</template>
</template>
<template slot="slot2">
<template slot="top">Inner Content2</template>
</template>
</Wrapper>
CodeSandbox Link:- https://codesandbox.io/s/mystifying-burnell-ir8un
It is showing default text from Child1 and Child2 not from App.vue
What should be done to fix it?

I changed some code and this is the result. The changes I made:
you had nested the <template> tag inside App.vue.
the wrapper itself needed to have a template for slot named 'top' and inside this template I added a slot with the same name of the component
https://codesandbox.io/s/lucid-smoke-uzgn7?file=/src/App.vue

Related

How to extend a Vue component with slots

I want to get a third party library component, add one element more and use this third party with the same way as always.
Example:
<third-party foo="bar" john="doe" propsFromOriginalLibrary="prop">
<template v-slot:top=v-slot:top={ props: test }>
Some text on third-party component slot
</template>
</third-party>
Want to code as:
<my-custom-component propsFromOriginalLibrary="prop">
<template v-slot:top={ props: test }>
Some text on third-party component slot
</template>
</my-custom-component>
And both examples work the same way. I was able to get all the props by using:
<third-party v-bind="$attrs">
but not sure about how to handle the slots
Here is how you can do it:
my-custom-component template:
<template>
<third-party v-bind="$attrs">
<template v-slot:top="slotProps">
<slot name="top" v-bind="slotProps"></slot>
</template>
</third-party>
</template>
What's happening:
Inserting the third party component and v-bind $attrs
Referencing its top slot
Custom component has a slot which is passed into third's slot
Custom slot has the same name so it can be v-slot the same way from a parent
Custom slot v-binds all 3rd party slotProps to pass out to a parent
You can use a v-for to avoid the need for hard-coding an inner template for each slot. For example if you wanted to expose two slots, top and bottom:
<template>
<third-party v-bind="$attrs">
<template v-for="slot in ['top','bottom']" v-slot:[slot]="slotProps">
<slot :name="slot" v-bind="slotProps"></slot>
</template>
</third-party>
</template>
Demo:
Vue.component('third-party', {
props: ['background'],
template: `
<div class="third" :style="{ background }">
3rd party slot:
<div class="third-slot">
<slot name="top" :props="props"></slot>
</div>
</div>
`,
data() {
return {
props: 'Third party Prop'
}
},
})
Vue.component('my-custom-component', {
template: `
<div>
<component is="third-party" v-bind="$attrs">
<template v-for="slot in ['top']" v-slot:[slot]="slotProps">
<slot :name="slot" v-bind="slotProps"></slot>
</template>
</component>
</div>
`
})
/***** APP *****/
new Vue({
el: "#app"
});
.third,.third-slot {
padding: 10px;
}
.third-slot {
background: #cccccc;
border: 1px solid #999999;
font-weight: bold;
}
<script src="https://unpkg.com/vue#2.6.12/dist/vue.js"></script>
<div id="app">
<third-party background="#eeeeff">
<template v-slot:top="{ props: test }">
Some text on third-party component slot
<div>{{ test }}</div>
</template>
</third-party>
<my-custom-component background="red">
<template v-slot:top="{ props: test }">
Some text on third-party component slot
<div>{{ test }}</div>
</template>
</my-custom-component>
</div>
Fun: You could even make the wrapped component dynamic like <component :is="thirdpartyName"> and the slot name array too; even passing this info in from outside for a fully generic wrapper. But there's no need for that here

Created not being triggered in Vue js

I'm implementing an application with Vue Js and I've the following code:
<template>
<simple-page title="list-patient" folder="Patient" page="List Patient" :loading="loading">
<list-patients #patientsLoaded="onPatientsLoaded"/>
</simple-page>
</template>
Both simple-page and list-patients are custom components created by me. Inside ListPatients I've an HTTP request on Create callback, as follows:
created() {
axios.get("...").then(response => {
...
this.$emit('patientsLoaded');
})
},
Then, my objective is to handle the patientsLoaded event and uptade the loading prop on the top parent component, as follows:
data() {
return {
loading: true
}
},
methods: {
onPatientsLoaded(params) {
this.loading = false;
}
}
However, the created method is not being triggered inside the list-patients component. The only way I can make this work is by removing :loading.
Any one can help?
Edit 1
Code of simple page:
<template>
<section :id="id">
<!-- Breadcrumb-->
<breadcumb :page="page" :folder="folder"/>
<!-- Breadcrumb-->
<!-- Simple Card-->
<simple-card :title="page" :icon="icon" :loading="loading" v-slot:body>
<slot>
</slot>
</simple-card>
<!-- Simple Card-->
</section>
</template>
Code of simple card:
<b-card>
<!-- Page body-->
<slot name="body" v-if="!loading">
</slot>
<!--Is loading-->
<div class="loading-container text-center d-block">
<div v-if="loading" class="spinner sm spinner-primary"></div>
</div>
</b-card>
Your list-patients component goes in the slot with name "body". That slot has a v-if directive so basically it is not rendered and hooks are not reachable as well. Maybe changing v-if to v-show will somehow help you in that situation. Anyway, you have deeply nested slots and it is making things messy. I usually declare loading variable inside of the component, where fetching data will be rendered.
For example:
data () {
return {
loading: true;
};
},
mounted() {
axios.get('url')
.then(res => {
this.loading = false;
})
}
and in your template:
<div v-if="!loading">
<p>{{fetchedData}}</p>
</div>
<loading-spinner v-else></loading-spinner>
idk maybe that's not best practise solution
v-slot for named slots can be indicated in template tag only
I suppose you wished to place passed default slot as body slot to simple-card component? If so you should indicate v-slot not in simple-card itself but in a content you passed it it.
<simple-card :title="page" :icon="icon" :loading="loading">
<template v-slot:body>
<slot>
</slot>
</template>
</simple-card>

How to make dynamic object properties available for parent component inside v-for in child one

I have a child component that uses v-for. Here is the child component:
<template>
<div>
<ul>
<li v-for="item in listItems"
:key=item.id>
<span>{{item.name}} - {{item.color}}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
listItems: Array
}
};
</script>
listItems is an Array of objects.
My question is: How do I make the property names in between the <span> tags dynamic from the parent component? Depending on the array of objects passed into the props as listItems, sometimes I may want the text in the <span> tags to be different based on the properties of the objects in the array. For example:
<span>{{item.id}} - {{item.location}}</span>
You could use scoped slots as follows :
<li v-for="item in listItems"
:key=item.id>
<slot v-bind:item="item">
<span>{{item.name}} - {{item.color}}</span>
</slot>
</li>
then you use it as you like :
<child>
<template v-slot:default="{item}">
<span>{{item.id}} - {{item.location}}</span>
</template>
</child>
or
<child>
<template v-slot:default="{item}">
<p>{{item.location}}</p>
</template>
</child>
If you only have a few different ones you're using, you could have a series of <span v-if="item.id">{{item.id}}</span> - <span v-if="item.foo">{{item.foo}}</span>

Vue: Create a parent component that can render a child component

How can I create a parent component that can receive and render a child component?
For example:
<Form>
<Input/>
<Input/>
<Select/>
<Button/>
<Form/>
You can use slots.
Parent.vue
<template>
<form>
<slot/> <!-- The child component will be rendered here -->
</form>
</template>
Child.vue
<template>
<div>
<input/>
<input/>
</div>
</template>
Grandparent.vue
<template>
<div>
<Parent> <!-- Parent component -->
<Child/> <!-- Pass child component to it as slot -->
</Parent>
</div>
</template>
<script>
import Parent from './Parent.vue'
import Child from './Child.vue'
components: {
Parent,
Child
}
</script>
You should import the components as follows:
import Select from "../components/Select";
import Button from "../components/Button";
import Input from "../components/Input";
And then register them in the parent as follows:
components: {
Select,
Button,
Input
},
You can send your child components to another component, say parent component, using slots.
You need to assign a name to the child components like
<Form>
<Input name="input1"/>
<Input name="input2"/>
<Select name="select"/>
<Button name="button"/>
<Form/>
Then inside the parent component, you can receive it like
<template>
<form>
<slot name="input1"/>
<slot name="input2"/>
<slot name="select"/>
<slot name="button"/>
</form>
</template>
The slots with corresponding names will be replaced with the child components.

transition hoooks only called when transition element is root element in component

I've noticed that transition hooks only get fired when the <transition> element is the root element in my component template. Is this by design? Am I missing something?
in my App.vue I have this template:
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
<example v-if = "checked"></example>
My component example.vue:
<template lang="html">
<section class="example">
<transition
v-on:enter = "enter"
v-on:leave = "leave">
<div class = "transition-example"></div>
</transition>
</section>
</template>
<script lang="js">
export default {
name: 'example',
props: [],
mounted() {
},
data() {
return {
}
},
methods: {
enter: function (el, done) {
console.log("enter")
done()
},
leave: function(el, done) {
console.log("leave")
done()
}
}
}
</script>
<style scoped >
</style>
In this current exmaple the enter and leave hooks are never executed when toggling the checkbox.
If I would update the template of example.vue to make sure the <transitions> element is the root element (as shown below) the enter and leave hooks are called.
<template lang="html">
<transition
v-on:enter = "enter"
v-on:leave = "leave">
<div class = "transition-example"></div>
</transition>
</template>
I'd like to have more flexibility in where I put my <transition> element or have multiple transition element in a component, which all have their own hooks.
I'm assuming I am overlooking something that prevents me from doing this.
I've noticed that transition hooks only get fired when the element is the root element in my component template. Is this by design? Am I missing something?
It's because of this line <example v-if="checked"></example>. v-if is applied to real root element of component so when transition is in root, v-if applied to div inside transition and it works fine, but in your first case v-if applied to section which is not under transition. So to make transition work you should provide v-if in element wrapped with transition tag, you can pass checked as prop to indicate visibility:
App.vue
...
<example :visible="checked"></example>
...
Example.vue
<template lang="html">
<section class="example">
<transition
v-on:enter = "enter"
v-on:leave = "leave">
<div v-if="visible" class="transition-example"></div>
</transition>
</section>
</template>

Categories