Vuejs - v-bind.sync on resursive components (hierarchical list) - vue.js

I have a hierarchical list component where child items have checkboxes. Checkbox actions(check/uncheck) must keep the parent component in sync with the checkbox's changed state. I cannot figure out how to achieve this using v-bind.sync recursively. My code is as below:
Menu.vue
This component holds the hierarchical list. (Only relevant code included)
HierarchicalCheckboxList is the component that displays the hierarchical list
Property 'value' holds the check/uncheck value (true/false)
Property 'children' contains the child list items
How do I define the .sync attribute on HierarchicalCheckboxList and with what parameter?
<template>
<div>
<HierarchicalCheckboxList
v-for="link in links"
#checked="primaryCheckChanged"
:key="link.id"
v-bind="link">
</HierarchicalCheckboxList>
</div>
</template>
<script>
import HierarchicalCheckboxList from 'components/HierarchicalCheckboxList'
data () {
return {
links: [{
id: 1,
title: 'Home',
caption: 'Feeds, Dashboard & more',
icon: 'account_box',
level: 0,
children: [{
id: 2,
title: 'Feeds',
icon: 'feeds',value: true,
level: 1,
children: [{
id: '3',
title: 'Dashboard',
icon: 'settings',
value: true,
level: 1
}]
}]
}]
}
},
methods: {
primaryCheckChanged (d) {
// A child's checked state is propogated till here
console.log(d)
}
}
</script>
HierarchicalCheckboxList.vue
This component calls itself recursively:
<template>
<div>
<div v-if="children != undefined && children.length == 0">
<!--/admin/user/user-->
<q-item clickable v-ripple :inset-level="level" :to="goto">
<q-item-section>
{{title}}
</q-item-section>
</q-item>
</div>
<div v-else>
<div v-if="children != undefined && children.length > 0">
<!-- {{children}} -->
<q-expansion-item
expand-separator
:icon="icon"
:label="title"
:caption="caption"
:header-inset-level="level"
default-closed>
<template v-slot:header>
<q-item-section>
{{ title }}
</q-item-section>
<q-item-section side>
<div class="row items-center">
<q-btn icon="add" dense flat color="secondary"></q-btn>
</div>
</q-item-section>
</template>
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
#checked="primaryCheckChanged"
v-bind="child">
</HierarchicalCheckboxList>
</q-expansion-item>
</div>
<!-- to="/admin/user/user" -->
<div v-else>
<q-item clickable v-ripple :inset-level="level">
<q-item-section>
<q-checkbox :label="title" v-model="selection" />
</q-item-section>
</q-item>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HierarchicalCheckboxList',
props: {
id: { type: String, required: true },
title: { type: String, required: false },
caption: { type: String, default: '' },
icon: { type: String, default: '' },
value: { type: Boolean, default: false },
level: { type: Number, default: 0 },
children: { type: Array }
},
data () {
return {
localValue: this.$props.value
}
},
computed: {
selection: {
get: function () {
return this.localValue
},
set: function (newvalue) {
this.localValue = newvalue
this.$emit('checked', this.localValue)
// or this.$emit('checked', {id: this.$props.id, value: this.localValue })
}
}
},
methods: {
primaryCheckChanged (d) {
this.$emit('checked', d)
}
}
}
</script>
What works so far
As a work-around I am able to get the checkbox state emitted with $emit('checked'), which I use to send it to the next process. But the parent's state is not updated until I refresh it back from the database.
How do I update the parent component's state using v-bind.sync recursively?
Appreciate any help!!
UI

Figured out how to do it after I broke the code down from the whole 2000 line code to a separate 'trial-n-error' code of 20 lines and then things became simple and clear.
Menu.vue
A few changes in the parent component in the HierarchicalCheckboxList declaration:
Note the sync property
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="link.value"
v-bind="child">
</HierarchicalCheckboxList>
HierarchicalCheckboxList.vue
Change the same line of code in the child component (as its recursive)
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="child.value"
v-bind="child">
</HierarchicalCheckboxList>
And in the computed set property, emit as below:
this.$emit('update:u', this.localValue)
That's it - parent n children components now stay in snyc.

Related

Hide label for particular component in vue / nuxt js

I have created a reusable input component with label, but i want label to hide(if hidden it should not take a space something like display none in css) on some place and label should be visible on some places
here is my code of the input component
<template>
<div>
<label for="" :label="label" class="mb-1 select-label">{{label}}</label> //hide or visible depending on requirement
<div class="custom-select" :tabindex="tabindex" #blur="open = false">
<div class="selected" :class="{ open: open }" #click="open = !open">
{{ selected }}
</div>
<div class="items" :class="{ selectHide: !open }">
<div
v-for="(option, i) of options"
:key="i"
#click="
selected = option;
open = false;
$emit('input', option);
"
class="border-bottom px-3"
>
{{ option }}
</div>
</div>
</div>
</div>
</template>
here is the code of my script
<script>
export default {
props: {
label: {
type: String,
required: false,
default: ''
},
options: {
type: Array,
required: true,
},
default: {
type: String,
required: false,
default: null,
},
tabindex: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
selected: this.default
? this.default
: this.options.length > 0
? this.options[0]
: null,
open: false,
};
},
mounted() {
this.$emit("input", this.selected);
},
}
</script>
You can use v-if directive to conditionally render the label element based on the props value.
As v-if will actually destroy and recreate elements when the conditional is toggled. Hence, all the classes/attributes applied to the element will also destroy.
Demo :
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
showMessage: false
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<p v-if="showMessage">{{ message }}</p>
</div>
If you will run above code snippet and open the developer console. You will see that <p> element will not be there as it has been removed from the DOM.
console screenshot :
You can just add a prop to control label visibility
script:
props: {
showLabel: {
type: Boolean,
default: false
}
}
template:
<label v-show="showLabel" />
parent component:
<MyCustomInput :show-label="false" />
<MyCustomInput :show-label="true" />

Pass component as prop in Vue JS

Intro: I am exploring Vue Js and got stuck while trying to make a dynamic data table component the problem I am facing is that I cannot pass a component via props and render it inside a table.
Problem: So basically what I am trying to do is to pass some custom component from headers prop in v-data-table such as:
headers = [
{ text: 'Name', value: 'name' },
{
text: 'Phone Number',
value: 'phone_number',
render: () => (
<div>
<p>Custom Render</p>
</div>
)
},
{ text: 'Actions', value: 'actions' }
]
So from the code above we can see that I want to render that paragraph from the render function inside Phone Number header, I did this thing in React Js before, but I cannot find a way to do it in Vue Js if someone can point me in the right direction would be fantastic. Thank you in advance.
You have 2 options - slots and dynamic components.
Let's first explore slots:
<template>
<v-data-table :items="dataItems" :headers="headerItems">
<template slot="item.phone_number" slot-scope="{item}">
<v-chip>{{ item.phone_number }}</v-chip>
</template>
<template slot="item.company_name" slot-scope="{item}">
<v-chip color="pink darken-4" text-color="white">{{ item.company_name }}</v-chip>
</template>
</v-data-table>
</template>
The data table provides you slots where you can customize the content. If you want to make your component more reusable and want to populate these slots from your parent component - then you need to re-expose these slots to the parent component:
<template>
<v-data-table :items="dataItems" :headers="headerItems">
<template slot="item.phone_number" slot-scope="props">
<slot name="phone" :props="props" />
</template>
<template slot="item.company_name" slot-scope="props">
<slot name="company" :props="props" />
</template>
</v-data-table>
</template>
If you don't know which slots will be customized - you can re-expose all of the data-table slots:
<template>
<v-data-table
:headers="headers"
:items="items"
:search="search"
hide-default-footer
:options.sync="pagination"
:expanded="expanded"
class="tbl_manage_students"
height="100%"
fixed-header
v-bind="$attrs"
#update:expanded="$emit('update:expanded', $event)"
>
<!-- https://devinduct.com/blogpost/59/vue-tricks-passing-slots-to-child-components -->
<template v-for="(index, name) in $slots" v-slot:[name]>
<slot :name="name" />
</template>
<template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
<slot :name="name" v-bind="data" />
</template>
<v-alert slot="no-results" color="error" icon="warning">
{{ $t("no_results", {term: search}) }}"
</v-alert>
<template #footer="data">
<!-- you can safely skip the "footer" slot override here - so it will be passed through to the parent component -->
<table-footer :info="data" #size="pagination.itemsPerPage = $event" #page="pagination.page = $event" />
</template>
</v-data-table>
</template>
<script>
import tableFooter from '#/components/ui/TableFooter'; // you can safely ignore this component in your own implementation
export default
{
name: 'TeacherTable',
components:
{
tableFooter,
},
props:
{
search:
{
type: String,
default: ''
},
items:
{
type: Array,
default: () => []
},
sort:
{
type: String,
default: ''
},
headers:
{
type: Array,
required: true
},
expanded:
{
type: Array,
default: () => []
}
},
data()
{
return {
pagination:
{
sortDesc: [false],
sortBy: [this.sort],
itemsPerPageOptions: [25, 50, 100],
itemsPerPage: 25,
page: 1,
},
};
},
watch:
{
items()
{
this.pagination.page = 1;
},
sort()
{
this.pagination.sortBy = [this.sort];
this.pagination.sortDesc = [false];
},
}
};
</script>
Dynamic components can be provided by props:
<template>
<v-data-table :items="dataItems" :headers="headerItems">
<template slot="item.phone_number" slot-scope="{item}">
<component :is="compPhone" :phone="item.phone_number" />
</template>
<template slot="item.company_name" slot-scope="{item}">
<component :is="compCompany" :company="item.company_name" />
</template>
</v-data-table>
</template>
<script>
export default
{
name: 'MyTable',
props:
{
compPhone:
{
type: [Object, String], // keep in mind that String type allows you to specify only the HTML tag - but not its contents
default: 'span'
},
compCompany:
{
type: [Object, String],
default: 'span'
},
}
}
</script>
Slots are more powerful than dynamic components as they (slots) use the Dependency Inversion principle. You can read more in the Markus Oberlehner's blog
Okay, I don't believe this is the best way possible but it works for me and maybe it will work for someone else.
What I did was I modified the headers array like this:
headers = [
{ text: 'Name', align: 'start', sortable: false, value: 'name' },
{
text: 'Phone Number',
key: 'phone_number',
value: 'custom_render',
render: Vue.component('phone_number', {
props: ['item'],
template: '<v-chip>{{item}}</v-chip>'
})
},
{ text: 'Bookings', value: 'bookings_count' },
{
text: 'Company',
key: 'company.name',
value: 'custom_render',
render: Vue.component('company_name', {
props: ['item'],
template:
'<v-chip color="pink darken-4" text-color="white">{{item}}</v-chip>'
})
},
{ text: 'Actions', value: 'actions', sortable: false }
]
And inside v-data-table I reference the slot of custom_render and render that component there like this:
<template v-slot:[`item.custom_render`]="{ item, header }">
<component
:is="header.render"
:item="getValue(item, header.key)"
></component>
</template>
To go inside the nested object like company.name I made a function which I called getValue that accepts 2 parametes, the object and the path to that value we need which is stored in headers array as key (ex. company.name) and used loadash to return the value.
getValue function:
getValue (item: any, path: string): any {
return loadash.get(item, path)
}
Note: This is just the initial idea, which worked for me. If someone has better ideas please engage with this post. Take a look at the props that I am passing to those dynamic components, note that you can pass more variables in that way.

event conflict for component containing another component

I have a number input inside a checkbox label, as shown in the screenshot above. When I click the input's plus/minus buttons to change the number, it also changes the checkbox's checked-value as an unintended side effect. How do I prevent the side effect?
<template>
<el-checkbox-group v-model="auditFinding" #change="checkAuditFinding" style="display:flex;flex-direction: column;">
<el-checkbox v-for="item in auditFindings" :key="item.value" :label="item.label">
<el-input-number v-if="item.value !== 'N/A'" v-model="item.num" :disabled="item.disabled" :min="0" :max="99" size="small" />
{{ item.value }}
</el-checkbox>
</el-checkbox-group>
</template>
<script>
export default {
//...
methods: {
checkAuditFinding(val) {
const t = val.toString()
this.auditFindings.map(item => {
if (val.indexOf(item.value) > -1) {
item.disabled = false
} else {
item.disabled = true
}
})
},
}
}
</script>
No. this is incorrect nest for your goal.
clicking on any nested element also fires click event on parent.
All you can do is keep checkbox and number as siblings. not inherited.
<el-checkbox-group v-model="auditFinding" style="display:flex;flex-direction: column;">
<div v-for="item in auditFindings">
<el-checkbox #change="checkAuditFinding" :key="item.value" :label="item.label" />
<el-input-number v-if="item.value !== 'N/A'" v-model="item.num" :disabled="item.disabled" :min="0" :max="99" size="small" />
{{ item.value }}
</div>
</el-checkbox-group>
You could stop the click-event propagation from the el-input-number element by using the #click.native.prevent event modifiers.
.native binds a handler for a native DOM event (click in this case). The caveat to this modifier is it depends on the implementation of el-nput-number (the root element must always emit click event).
.prevent invokes Event.preventDefault to effectively cancel the click-event, preventing it from reaching the parent checkbox.
new Vue({
el: '#app',
data() {
return {
auditFinding: false,
auditFindings: [
{ value: 11, label: 'label A', disabled: false, num: 1 },
{ value: 22, label: 'label B', disabled: false, num: 2 },
{ value: 33, label: 'label C', disabled: false, num: 3 },
]
}
},
methods: {
checkAuditFinding(e) {
console.log('checkAuditFinding', e)
},
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-ui#2.13.0/lib/theme-chalk/index.css">
<script src="https://unpkg.com/element-ui#2.13.0/lib/index.js"></script>
<div id="app">
<el-checkbox-group v-model="auditFinding" #change="checkAuditFinding" style="display:flex;flex-direction: column;">
<el-checkbox v-for="item in auditFindings" :key="item.value" :label="item.label">
<el-input-number #click.native.prevent
v-if="item.value !== 'N/A'"
v-model="item.num"
:disabled="item.disabled"
:min="0"
:max="99"
size="small"
label="item.label" />
{{ item.value }}
</el-checkbox>
</el-checkbox-group>
</div>

Wrapping a ValidationObserver around a v-for loop

I have a v-for loop that allows me to dynamically add new fields to my form. This loop is within a tab which I need to validate before I go onto the next section of my form. It seems as though nothing renders when I place the v-for within my validation observer. Is there another way to accomplish this?
I'm using VeeValidate 3
<template>
<div>
<b-card class="mb-3">
<ValidationObserver :ref="'contact_obs'" v-slot="{ invalid }">
<div
v-for="(contact, index) in this.applicant.contacts"
:key="contact.id"
role="tablist"
>
<b-form-row>
<BTextInputWithValidation
rules="required"
class="col-md-4"
:label="
$t('contact_name', { name: applicant.contacts[index].title })
"
:name="$t('contact_name')"
v-model="applicant.contacts[index].name"
description
placeholder
/>
<BTextInputWithValidation
rules
class="col-md-4"
:label="$t('contact_title')"
:name="$t('contact_title')"
v-model="applicant.contacts[index].title"
description
placeholder
/>
<BTextInputWithValidation
rules
class="col-md-3"
:label="$t('contact_email_address')"
:name="$t('contact_email_address')"
v-model="applicant.contacts[index].email"
description
placeholder
/>
<b-button
variant="outline-danger"
class="float-right mt-4 mb-4 ml-3"
v-on:click="deleteContact(index)"
>
<span class="fas fa-user-minus"></span>
</b-button>
</b-form-row>
</div>
</ValidationObserver>
<b-button
variant="outline-success"
class="float-right mt-4 mb-4 ml-3"
v-on:click="addContact"
>
<span class="fas fa-user-plus"></span>
</b-button>
</b-card>
</div>
</template>
<script>
import { ValidationObserver } from 'vee-validate'
import VeeValidate from 'vee-validate'
import BTextInputWithValidation from './inputs/BTextInputWithValidation'
let id = 10
export default {
components: { ValidationObserver, BTextInputWithValidation },
mounted() {},
data: function() {
return {
applicant: {
contacts: [
{
id: '1',
name: '',
title: 'Primary Principal',
email: ''
},
{
id: '2',
name: '',
title: 'Secondary Principal',
email: ''
},
{
id: '3',
name: '',
title: 'Accounts Receivable',
email: ''
}
]
}
}
},
methods: {
addContact: function(params) {
this.applicant.contacts.push({
id: id,
name: '',
title: '',
email: ''
})
id++
},
deleteContact: function(index) {
this.$delete(this.applicant.contacts, index)
},
validate() {
const isValid = this.$refs.contact_obs.validate()
if (isValid) {
this.$emit('on-validate', this.$data, isValid)
}
return isValid
// return true
}
}
}
</script>
<style lang="scss" scoped></style>
I believe the problem is here:
v-for="(contact, index) in this.applicant.contacts"
In general you should avoid using the this. prefix to access properties in templates but usually it does no harm. This is one of those cases where it actually does break something. this does not refer to the correct object inside a scoped slot.
I'm surprised you don't see an error in your console.

How to defined a array list in props and data

In my project, I use vue.js.
I want to display content of list with nested loop。 In parent page, i have defined:
<template>
<div>
<detail-header></detail-header>
......
<detail-list></detail-list>
</div>
</template>
The component of detail-list is :
<template>
<div>
<div v-for="(item, index) of list" :key="index">
<div class="item-title border-bottom">
<span class="item-title-icon"></span>
{{item.title}}
</div>
<div v-if="item.children" class="item-children">
<detail-list :list="item.children"></detail-list>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DetailList',
props: {
list: Array
},
data () {
return {
list: [{
title: 'adult',
children: [{title: 'threePeople',children: [{ title: 'threePeople-w'}]}, {title: 'fivePeople'}]
}, {
title: 'student'
}, {
title: 'child'
}, {
title: 'offer'
}]
}
}
}
</script>
unlucky, I got a error message:
Duplicated key 'list' of list: [{ in detail-list
who can help me ?
If you want this to work, keep the list in props (and remove it from DetailList's data) and define in your parent page's data.
So the first DetailList and its children will have the list as a prop.
So you'll have in the parent page :
<template>
<div>
<detail-header></detail-header>
......
<detail-list :list="list"></detail-list>
</div>
</template>
<script>
export default {
name: 'Parent',
data () {
return {
list: [{ ... the list ... }]
}
}