I'm trying to find the "cleanest" way of passing a property to a child component.
I have a custom component (lets call it parent in this example) that can take any child component via a slot. Imagine the parent component wants to update some property x of a child.
The solution that I've come up with uses scoped slots, like so:
// parent.vue
<template>
… some template stuff
<slot :x="parentX"></slot>
</template>
<script>
...component setup
computed: {
parentX: function() {
return ...some number
}
},
...more component stuff
</script>
--
// child.vue
<template>
...some html that uses property x
</template>
<script>
...component setup
props: { x: Number },
...more component stuff
</script>
--
// app.vue
<template>
<parent>
<template scope="A">
<child :x="A.x" />
</template>
</parent>
</template>
This works, but it's not very nice for a couple of reasons:
The extra markup needed to wire then components together.
The fact that the user of these components needs to know about properties that they shouldn't need to know about. The parent component is sending an internal bit of state to it's child. The fact that I have to manually wire up this is a little annoying, especially because it's just boiler plate.
Is there a better way of doing this so I can say:
<template>
<parent>
<child />
</parent>
</template>
Then all I need to know is that the child has a property x.
UPDATE (28-10-18)
It's possible to remove some of the boiler-plate by using slot-scope directly on the child element. e.g.
<template>
<parent>
<child slot-scope="child" :x="child.x" />
</parent>
</template>
but the real problem still persists. I need to manually wire up the components in the app.vue even though this has already been fully defined in the parent and child components.
A slot is for uncoupled content. Your requirement is that the content be a component that takes a prop. It seems to me that what you want to do is pass the child component to the parent as a prop, and have the parent template do something like
<div :is="childComponent" :x="child.x"></div>
Related
Context -
I have a large form that I've broken up into components. I'm going to focus on only one parent/child component relationship for brevity here.
Doing some validation on each child component. Emitting back to parent when a submit button is clicked on the parent.
I have a submit button on the parent. When clicked I am emitting all child component data back to the parent
I have a method on the parent that receives the emitted data. As well as an object instantiated on the parent data() method to assign to the incoming data.
Problem -
When you hit submit, the emitted data is present on the child component, but it's empty on the parent. The submit method finishes before the emitting method finishes.
Is there a best way of accomplishing this?
I could take all inputs and put them in one giant form.. but I hate that approach.
I've tried setTimeout for a brief second. This seems to work at times, but it feels so hacky. I delay the submit method from finishing.. allowing the emit to finish.. that just doesn't feel sustainable or right.
Is there a clear way of doing this? Thank you so much for the assistance.
<1-- Parent -->
<template>
<!-- CHILD -->
<LabExposureType v-if="lab"
#passLabExposureToParent="exposureOnSubmit">
</LabExposureType>
<div class="submitAndClear d-flex justify-center pb-3">
<v-btn color="success" class="mr-4" #click="submit">submit</v-btn>
</div>
</template>
data(){
exposureVals:{},
}
//Removed some data and others for brevity
methods:{
submit() {
//collect vals before doing this
//Exposure values
console.log('emitted exposure vals', this.exposureVals); <- this of course is empty has the below has not finished
},
//Emitted method
exposureOnSubmit(input) {
this.exposureVals = input
}
}
EDIT - Added code for more clarity
Parent -
<LabExposureType v-if="lab" :labState="exposureState" v-model="standardNum"
#passLabExposureToParent="exposureOnSubmit">
</LabExposureType>
CHILD -
<v-text-field v-if="this.isStandardMethod" v-model="standardNum" label="Organization and Standard Number"
class="text-caption primary--text" required :error-messages="standardNumErrors"
:value="modelValue" #input="$emit('update:modelValue', $event.target.value)"
#blur="$v.standardNum.$touch()"></v-text-field>
props and emits -
props: [ "labState", "modelValue"],
emits:['update:modelValue'],
Your child components should emit their values before the submit is ever clicked. Best practice is to use the v-model directive so that the parent always has the latest value of the child. Define the v-model name on the parent with whatever name you want, just make sure the prop name in the child component is named modelValue and that the value is emitted using event update:modelValue:
<!-- Parent -->
<child v-model="searchText" />
<!-- Child -->
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
</script>
<template>
<input
:value="modelValue"
#input="$emit('update:modelValue', $event.target.value)"
/>
</template>
I created a component that shows table data for various pages. That component uses b-table inside. Now for a couple pages I want to customize rendering of some columns, and Bootstrap Tables allow that using scoped field slots with special syntax:
<template #cell(field)="data">
{{ data.item.value }}
</template>
where field - column name, coming from my array with columns, and data.item - cell item to be rendered.
The problem is that I have different fields for different pages, so this customization should come from parent component, and these templates should be created dynamically.
Here is how I tried to solve it:
Pass via property to MyTableComponent an array with customizable fields and unique slot names
In MyTableComponent dynamically create templates for customization, and inside dynamically create named slots
From parent pass slot data to named slots
MyTableComponent:
<b-table>
<template v-for="slot in myslots" v-bind="cellAttributes(slot)">
<slot :name="slot.name"></slot>
</template>
</b-table>
<script>
...
computed: {
cellAttributes(slot) {
return ['#cell(' + slot.field + ')="data"'];
}
}
...
</script>
Parent:
<MyTableComponent :myslots="myslots" :items="items" :fields="fields">
<template slot="customSlot1">
Hello1
</template>
<template slot="customSlot1">
Hello2
</template>
</MyTableComponent>
<script>
...
items: [...my items...],
fields: [...my columns...],
myslots: [
{ field: "field1", name: "customSlot1" },
{ field: "field2", name: "customSlot2" }
]
...
</script>
Unfortunately, b-table component just ignores my custom slots like if they are not provided. It works if I specify in the MyTableComponent it directly:
<b-table>
<template #cell(field1)="data">
{{ data.item.value }}
</template>
</b-table>
But I need it to be done dynamically via component properties. Please help.
You can use Dynamic Slot Names feature of Vue 2 to pass all (or some) slots from parent to <b-table> inside child like this:
Child:
<b-table>
<template v-for="(_, slotName) of $scopedSlots" v-slot:[slotName]="scope">
<slot :name="slotName" v-bind="scope"/>
</template>
</b-table>
$scopedSlots contains all slots passed to your component.
Now this will work:
<MyTableComponent :items="items" :fields="fields">
<template #cell(field1)="data">
{{ data.item.value }}
</template>
</ MyTableComponent>
UPDATE 2 - Vue 3
To make above code work in Vue 3, just replace $scopedSlots with $slots as suggested by migration guide
UPDATE 1
You can filter $scopedSlots if you want (have some slot specific to your wrapper component you don't want to pass down to <b-table>) by creating computed
I mentioned this possibility in my original answer but it is a bit problematic so it deserves better explanation...
Scoped slots are passed to a component as a functions (generating VNode's when called). Target component just executes those she knows about (by name) and ignores the rest. So lets say your wrapper has b-table (or v-data-table for Vuetify) inside and some other component, let's say pagination. You can use code above inside both of them, passing all slots to each. Unless there is some naming conflict (both components using same slot name), it will work just fine and does not induce any additional cost (all slot functions are already compiled/created when passed to your wrapper component). Target component will use (execute) only the slots it knows by name.
If there is possible naming conflict, it can be solved by using some naming convention like prefixing slot names intended just for b-table with something like table-- and doing filtering inside but be aware that $scopedSlots object does contain some Vue internal properties which must be copied along !! ($stable, $key and $hasNormal for Vue 2 - see the code). So the filtering code below even it's perfectly fine and doesn't throw any error will not work (b-table will not recognize and use the slots)
<b-table>
<template v-for="(_, slotName) of tableSlots" v-slot:[slotName]="scope">
<slot :name="slotName" v-bind="scope"/>
</template>
</b-table>
computed: {
tableSlots() {
const prefix = "table--";
const raw = this.$scopedSlots;
const filtered = Object.keys(raw)
.filter((key) => key.startsWith(prefix))
.reduce(
(obj, key) => ({
...obj,
[key.replace(prefix, "")]: raw[key],
}),
{}
);
return filtered;
},
},
This code can be fixed by including the properties mentioned above but this just too much dependency on Vue internal implementation for my taste and I do not recommend it. If it's possible, stick with the scenario 1...
I have a component that functions as a basis for other components.
//The Basis Component lets call it slotComponent
<template>
<div>
//somestuff
<slot :someProperties="someLocalValues"></slot>
</div>
</template>
As you can see I want to give the component that is replacing the slot some Properties that only this component will know.
However if I do this:
//Some page Component lets call it mainPage
<template>
<slotComponent>
<someOtherComponent/>
</slotComponent>
</template>
Then "someOtherComponent" will not have access to "someProperties". How can I provide this component with said property?
Note that someLocalValues are defined in the scope of slotComponent and not its parent. So I cant provide said information in the mainPage.
You need to specify the v-slot directive in order to bind the props. Check the documentation here: https://v2.vuejs.org/v2/guide/components-slots.html#Scoped-Slots
You also will need to pass the props to your child component.
In your example, you would need to do something like:
<template>
<slotComponent>
<someOtherComponent v-slot="slotData" :data="slotData.someProperties"/>
</slotComponent>
</template>
This is the same, but with an additional <template> for clarity:
<template>
<slotComponent>
<template v-slot="slotData">
<someOtherComponent :data="slotData.someProperties"/>
</template>
</slotComponent>
</template>
What I have done so far is created two componenets which are present in src/components folder and I want to add these components which is contained by parent component present in src/views folder.
I have one component named form.vue and another component background.vue
And what I want is, to show the form.vue (which contains a form) on top of backgound.vue (which is for background purpose). So every time, any changes happen in child,forces the whole parent page to re-render. So is there any way to solve this ?
Below are folder structure:
You can use slots for that, so in your background.vue you would have:
<template>
...
<slot><slot> <!-- place where the external component will be rendered -->
...
</template>
and then in the main component, you would have:
<background>
<my-form></my-form>
</background>
create 3 components and call 2 of them into 3rd component.
For example:
You have
component1.vue
component2.vue
component3.vue
In component3.vue:
<script>
//import components
import componentOne from '/path/to/component1'
import componentTwo from '/path/to/component2'
//now register them locally to use them in this component
export default{
components:{componentOne, componentTwo}
}
//now call these components in <template> section using kebab case
<template>
<div>
<component-one /> //camelCAse name of component in <script> tag is used in kebab case in template section
<component-two /> // means componentTwo will called as <component-two/>
</div>
</template>
</script>
I have commenting system which allows 1 level thread. Meaning 1st level comment will look like
{
...content,
thread: []
}
where thread may have more comments in it. I though this is good for self-referencing component and List with Slots.
But after a while I do not know how to wire this thing up.
SingleComment component is given below
<template>
... *content*
<b-button
v-if="isCommentDeletable"
#click="handleDelete"
</b-button>
<div v-for="item in item.thread" :key="item._id">
<SingleComment class="ml-3"
:item="item"
/>
</div>
</template>
...
methods: {
handleDelete () {
this.$emit('remove')
},
}
...
components: {
'NewComment': NewComment, 'SingleComment': this
},
name: 'SingleComment'
}
</script>
List component classic list is recieving array of items as prop and is given by
<div v-for="item in items" ...
<slot
name="listitem"
:item="item"
/>
</div>
and this is the parent where I want to use these two components with modal
Parent
<AppModal
>
...
<List
class="my-1"
:items="comments.docs"
>
<template v-slot:listitem="{ item }">
<SingleComment
:item="item"
:remove="removeItem"
#remove="removeItem"
/>
</template>
</List>
I want to wire this thing up in Parent so I can use single modal for whole list.
Do I wire thins thing up with events? Or? Any sort of help is welcome. I am stuck. I can make some hacks but I really do not know how to deal with this self referencing components.
If you only have one level of nesting, you could simply pass the component itself as a slot, like so:
<Comment v-for="comment in comments" :key="comment.id" v-bind="comment">
<Comment v-for="thread in comment.thread" :key="thread.id" v-bind="thread" />
</Comment>
Then you will only have to worry about passing props one level deep, as if you only had a single list of comments. I created an example of this on CodeSandbox here: https://codesandbox.io/embed/vue-template-mq24e.
If you want to use a recursive approach, you'll just have to pass props and events around; there's no magic solution that steps around this. Update CodeSandbox example: https://codesandbox.io/embed/vue-template-doy66.
You could avoid explicitly passing the removeitem event listener down by having a removeitem action on your Vuex store that you map to your component.
My opinion here, is that simpler is better, and you don't need recursion for one level of nesting. Put yourself in the shoes of a future developer and make the code easy to read and reason about; that future developer may even be you when you haven't looked at the codebase in a few weeks.