events and self referencing components vue.js - vuejs2

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.

Related

Have Parent Component Wait till Child has emitted data back to parent ? (Using Vue 2)

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>

VueJS 2 - Re-useable parent component that can accept different children

I have a parent component, SearchComponent:
<template>
<div>
<div class="relative pl-8 pr-10 rounded bg-white border focus-within:bg-white focus-within:ring-1">
<input v-focus #keyup.escape="clearSearch" #keyup="doSearch" v-model="searchTerm"
class="w-full ml-4 h-12 pl-1 text-gray-700 text-lg rounded-full border-0 bg-transparent focus:outline-none placeholder-gray-400"
placeholder="Search..." autocomplete="off">
</div>
<ul class="bg-white mt-4">
<quick-search-item v-for="item in searchResults" :key="item.id" :item-data="item.itemData">
</quick-search-item>
</ul>
</div>
</template>
This is responsible for receiving user input and getting results from an ajax call, handling errors etc. and generating the result list.
What I'd like to do is to make this generic so that instead of having a quick-search-item child component I can pass in different types of child component (like car-search-item, person-search-item etc.) depending on the context of where the user is in the app and what they're searching for
I've read a number of tutorials and I couldn't find quite what I'm trying to do. This may mean I'm approaching this in the wrong way - but if anyone could point me in the right direction, or has a better approach, I'd be very grateful.
Thanks,
Lenny.
I would try to make use of the <slot> element. Check out the documentation here
<parent-component>
<slot></slot>
</parent-component>
Hope this can put you on the right path.
Schalk Pretorius was quite right: slots are the answer to this, specifically scoped slots. I found the Vue docs a little confusing as it refers to getting data from the child component and I wanted to do it the other way around, so as an aide memoire for myself and to help anyone else here's what I did:
In my parent component I defined the slot like this:
<slot name="results" v-bind:items="searchResults">
</slot>
The v-bind binds searchResults (a data item in the parent component) to the value 'items'. 'items' then becomes available in the slot.
In my child component I have a simple property setup called items:
props: {
items: {type: Array},
},
Then to hook it all together in my Blade file I did this:
<search-component endpoint="{{ route('quick_search.index') }}">
<template v-slot:results="props">
<food-results :items="props.items">
</food-results>
</template>
</search-component>
This creates the search-component. Inside that -as I'm using named slots - we need a template and use v-slot to tell it which slot to use (results), then the ="props" exposes all the properties we've defined on that slot (in this case just one, 'items').
Inside the template we put our child component and then we can bind items to props.items which will be the searchResults in our parent component.
I'm happy to have this working and I can now create lots of different results components while reusing the search component - and at least I learnt something today!
Cheers,
Lenny.

How to pass cell templates to a component with b-table?

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...

vue emit data back to parent when using a slot

I have input on custom component and when i click on the next button on the wrapper component i need to emit details to the parent component.
How is this possible in vue?
wrapper.vue
<template>
<div :id="contentId" class="mt-3">
<div>
<slot></slot>
</div>
<b-row class="float-right">
<b-col>
<button cssClass="e-outline" v-on:click="btnNext">{{nextButtonText}}</button>
</b-col>
</b-row>
</div>
</template>
parent.vue
<template>
<div>
<Wrapper contentId="1">
<CustomComponent1 />
</wrapper>
<Wrapper contentId="2">
<CustomComponent1 />
</wrapper>
</div>
</template>
customComponent1.vue
<template>
<div>
<input v-model="name" />
<input v-model="name2" />
</div>
</template>
code above is for illustrative purposes.
The problem is that the wrapper doesn't innately have access to data of the scoped component, therefore these links have to be created manually. There is no way to tell how many children or slots the component may have, so this kind of functionality is not part of the vue magic.
So in an example where you have parent App component, which holds a Wrapper that has a MyInput component in the slot...
MyInput
The MyInout component doesn't automatically update other components, so it needs to be setup to $emit the internal data.
This can be done using a watch, #change listener for the input, or some other way. You can emit multiple datum as they change, or use a single payload with all the data
this.$emit("input", myData);
App
The App needs to explicitly connect the data between MyInout and Wrapper
<Wrapper> <MyInput #input="onInput" slot-scope="{ onInput }" /> </Wrapper>
The magic/trick happens here, where we bind the input emit function of the input to the onInput function using slot-scope.
Wrapper
The wrapper then needs to listen to the events passed (via App) from Wrapper
<slot :onInput="onInput" />
where onInput is a method that would process the data
see example below
I would recommend the following reading
https://github.com/vuejs/vue/issues/4332 (specifically Evan's response why it's not possible)
https://adamwathan.me/renderless-components-in-vuejs/ Adam has a thoroughly documented way of using render functions and slots to abstract functionality from the UI. While it's not directly related, it's a worthwhile read and may provide more info on using slot-scope as well as some perspective on improving the structure of UI components.

Attach multiple v-models to the loop of dynamic child components in vuejs 2

Recently I ran into a problem, where I had to display many different components inside a loop, but: each of them should share it's state with parent (kinda knockout.js style). I was digging thru the docs where clearly was pointed out, that Vue.js pass properties one way down to childs, and those can eventually speak with some events. Also, docs says that there can be only one v-model per component, so finally I came up with something like this:
<li :is="field.type" v-for="(field, i) in fields" :key="i" :title="field.title" v-on:title-change="title = $event" :somevalue="field.somevalue" v-on:somevalue-change="somevalue = $event"></li>
And so on... Yet, after fifth parameter I quickly realized that the code is basically messy. Is there some less messy way to attach multiple two-way data bindings to child components?
The solution happened to be a .sync method and proper naming of events. While sync has been deprecated and removed in vue.js 2, since 2.3 version has been rewritten and added again in some similar form. As in fact it's only a syntatic sugar, my component now looks more decent I believe.
<ol>
<li :is="field.type"
v-for="(field, i) in fields"
:key="field.id"
v-bind.sync="field"
v-on:remove="fields.splice(i, 1)"></li>
</ol>
Vue.component('bool', {
template: '\
<li>\
<input type="text" v-bind:value="title" #input="$emit(\'update:title\', $event.target.value)">\
<button v-bind:value="value" #click="$emit(\'update:value\', !$event.target.value)" :class="{active: value}">{{value}}</button>\
<input type="checkbox" value="1" v-bind:checked="istrue" #change="$emit(\'update:istrue\', $event.target.checked)">\
<button #click="$emit(\'remove\')">Remove</button>\
</li>\
',
data () {
return {}
},
props: ['title', 'value', 'availablevalues']
})