event conflict for component containing another component - vue.js

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>

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" />

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

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.

Vue-MultiSelect Checkbox binding

The data properties of the multi-select component does not update on change. Check-boxes doesn't update on the front-end.
Expected Behavior: The check-boxes should get ticked, when clicked.
Link to code: https://jsfiddle.net/bzqd19nt/3/
<div id="app">
<multiselect
select-Label=""
selected-Label=""
deselect-Label=""
v-model="value"
:options="options"
:multiple="true"
track-by="library"
:custom-label="customLabel"
:close-on-select="false"
#select=onSelect($event)
#remove=onRemove($event)
>
<span class="checkbox-label" slot="option" slot-scope="scope" #click.self="select(scope.option)">
{{ scope.option.library }}
<input class="test" type="checkbox" v-model="scope.option.checked" #focus.prevent/>
</span>
</multiselect>
<pre>{{ value }}</pre>
</div>
new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
data: {
value: [],
options: [
{ language: 'JavaScript', library: 'Vue.js', checked: false },
{ language: 'JavaScript', library: 'Vue-Multiselect', checked: false },
{ language: 'JavaScript', library: 'Vuelidate', checked: false }
]
},
methods: {
customLabel(option) {
return `${option.library} - ${option.language}`;
},
onSelect(option) {
console.log('Added');
option.checked = true;
console.log(`${option.library} Clicked!! ${option.checked}`);
},
onRemove(option) {
console.log('Removed');
option.checked = false;
console.log(`${option.library} Removed!! ${option.checked}`);
}
}
}).$mount('#app');
In your code you call onSelect and try to change the option argument of this function inside the function:
option.checked = true;
This affects only the local variable option (the function argument). And doesn't affect objects in options array in the data of the Vue instance, the objects bound with checkboxes. That's why nothing happens when you click on an option in the list.
To fix it find the appropriate element in options array and change it:
let index = this.options.findIndex(item => item.library==option.library);
this.options[index].checked = true;
Here is the code snippet with fix:
new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
data: {
value: [],
options: [
{ language: 'JavaScript', library: 'Vue.js', checked: false },
{ language: 'JavaScript', library: 'Vue-Multiselect', checked: false },
{ language: 'JavaScript', library: 'Vuelidate', checked: false }
]
},
methods: {
customLabel (option) {
return `${option.library} - ${option.language}`
},
onSelect (option) {
console.log("Added");
let index = this.options.findIndex(item => item.library==option.library);
this.options[index].checked = true;
console.log(option.library + " Clicked!! " + option.checked);
},
onRemove (option) {
console.log("Removed");
let index = this.options.findIndex(item => item.library==option.library);
this.options[index].checked = false;
console.log(option.library + " Removed!! " + option.checked);
}
}
}).$mount('#app')
* {
font-family: 'Lato', 'Avenir', sans-serif;
}
.checkbox-label {
display: block;
}
.test {
position: absolute;
right: 1vw;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<link href="https://unpkg.com/vue-multiselect#2.0.2/dist/vue-multiselect.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue-multiselect#2.0.3/dist/vue-multiselect.min.js"></script>
<div id="app">
<multiselect
select-Label=""
selected-Label=""
deselect-Label=""
v-model="value"
:options="options"
:multiple="true"
track-by="library"
:custom-label="customLabel"
:close-on-select="false"
#select=onSelect($event)
#remove=onRemove($event)
>
<span class="checkbox-label" slot="option" slot-scope="scope" #click.self="select(scope.option)">
{{ scope.option.library }}
<input class="test" type="checkbox" v-model="scope.option.checked" #focus.prevent/>
</span>
</multiselect>
<pre>{{ value }}</pre>
</div>

VueJS show multiple input fields conditionally

I have a 'Add' button which adds a select box and when I select a value in the select box I want to display an input field so every time I click 'Add' it should show a select box and when I select a value in it I would like to display an input field.
I know I could use a flag 'selected' but when I already add one select box this would change the flag to true and show the input field immediately after I click 'Add' but I want the input field to show only when a value is selected.
<template>
<button #click="onBtnClick">Add<button>
<select>...</select> # This gets added when 'Add' button is clicked
<input v-if="selected" type="text" /> # This should show when a value is selected.
<select>
</template>
data(){
return {
selected: false
}
},
methods: {
onValueSelected(){
this.selected = true
}
}
Do you have any ideas how I could accomplish this?
Use v-for and push new fields onto the collection at will.
new Vue({
el: '#app',
data() {
return {
goods: []
}
},
methods: {
addSelect() {
let item = {
id: this.goods.length,
menus: [
{ value: '', text: '- select -' },
{ value: 1, text: 'Item 1' },
{ value: 2, text: 'Item 2' },
{ value: 3, text: 'Item 3' }
],
input: {
show: false
}
};
this.goods.push(item);
},
showInput(item, e) {
item.input.show = e.currentTarget.selectedIndex > 0;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="addSelect">Add to cart</button>
<div v-for="item in goods" :key="item.id" class="goods">
<select #change="showInput(item, $event)">
<option
v-for="opt in item.menus"
:key="opt.key"
value="opt.value">
{{opt.text}}
</option>
</select>
<input v-if="item.input.show" type="text" value="0" />
</div>
</div>
I figured out that I can render a new component and pass it props. Everytime a new component is rendered it gets selected flag as false by default and I can change it then by selecting a value in the select box.
<b-form-group
v-if="element.elementName === 'Some value'"
label="Select field *:"
label-for="benefitSelectField">
<SalaryField :element="element" />
</b-form-group>
SalaryField component
<template>
<div>
<select #change="onValueSelected" ></select>
<input v-if="selected" type="text" />
</div>
</template>
<script>
export default {
props: {
element: Object
},
data(){
return {
selected: false,
}
},
methods: {
onValueSelected() {
this.selected = true
}
}
}
</script>

vue.js doesn't update on mounted() hook

Edit : Solved, see answer.
So i'm new to vue.js and quasar so it is probably a rookie mistake about vue lifecycle hooks and vue reactivity.
I want to resize an element based on the browser's size. The height of this element is calculated using the height of other elements. I use $refs to get the other element's height and I capture the onResize() event launched by a quasar component.
This event is launched once when the page is loading, and my element's sizes are all 0 because I guess they are not rendered in the DOM yet. I have a method to refresh my calculated height, which I call when "onResize" event is captured, and also at the "mounted()" vue.js hook.
My problem is :
the first onResize() calls the method but all elements have 0px in height.
mounted() calls the method again and here all elements have their height calculated. The results are good, but it does not show on the display, see screenshot #1 : resize events and sizes logged in the console, note that the size is calculated twice, once on onResize() and once on mounted() . the one on mounted() has the good value but it does not show in the DOM.
after I resize the window once, then everything is ok and i don't have any problem anymore. (screenshots #2 (window mode) and #3 (fullscreen again))
My question is : why the height is not update in the DOM when mounted() hook is called even if it is calculated correctly ? (everything is in the same .vue file)
My code :
My problem is with the height of the div that has the "tableRow" ref
<template>
<q-page>
<div class="row" :style="'height: '+pageSize.height*0.95+'px;'">
<div class="col-6 q-pa-lg">
<div class="row" ref="actionsRow">
<div class="col-6 q-mb-sm">
<q-search hide-underline v-model="filter" />
</div>
<div class="col-6">
</div>
</div>
<div class="row" ref="tableHeaderRow">
<q-table class="col-12" :selection="selectionMode" :selected.sync="selectedRows" :data="templateTableData" :columns="templateColumns"
row-key="slug" :pagination.sync="pagination" dense hide-bottom>
<q-tr slot="body" slot-scope="props" :props="props">
</q-tr>
</q-table>
</div>
<div class="row" ref="tableRow" :style="'height: '+tableHeight+'px;'">
<q-scroll-area style="height: 100%" class="col-12 q-mt-sm shadow-3">
<q-table :selection="selectionMode" :selected.sync="selectedRows" :data="templateTableData" :columns="templateColumns" row-key="slug"
:filter="filter" :pagination.sync="pagination" dense hide-bottom hide-header>
<q-tr slot="body" slot-scope="props" :props="props" #click.native="onRowClick(props.row)" class="cursor-pointer">
<q-td auto-width>
<q-checkbox color="primary" v-model="props.selected" />
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</q-table>
</q-scroll-area>
</div>
</div>
<router-view class="col-6 q-pa-lg">
</router-view>
</div>
<q-window-resize-observable #resize="onResize" />
</q-page>
</template>
Script:
var data = []
for (var i = 1; i <= 100; i++) {
data.push({
id: i,
name: 'Template ' + i,
slug: 'template' + i,
active: true
})
}
import { mapActions, mapGetters } from 'vuex'
export default {
data: () => ({
pageSize: {
height: 0,
width: 0
},
tableHeight: 0,
templateColumns: [
{
name: 'templateName',
required: true,
label: 'Name',
align: 'left',
field: 'name',
sortable: true
},
{
name: 'templateSlug',
label: 'Slug',
align: 'left',
field: 'slug',
sortable: true
},
{
name: 'templateActive',
label: 'Active',
align: 'left',
field: 'active',
sortable: true,
sort: (a, b) => {
if ((a && b) || (!a && !b)) {
return 0
} else if (a) {
return 1
} else {
return -1
}
}
}
],
selectionMode: 'multiple',
selectedRows: [],
pagination: {
sortBy: null, // String, column "name" property value
descending: false,
page: 1,
rowsPerPage: 0 // current rows per page being displayed
},
templateTableData: data,
filter: ''
}),
computed: {
...mapGetters('appUtils', [
'getPageTitle',
'allConst'
])
},
methods: {
...mapActions('appUtils', [
'setPageTitle',
'deletePageTitle'
]),
onResize (size) {
this.pageSize.height = size.height - this.getPageTitle.height
this.resizeTable()
console.log('ON RESIZE EVENT:')
console.log('tableHeaderRow:'+
this.$refs.tableHeaderRow.clientHeight)
console.log('actionsRow:' + this.$refs.actionsRow.clientHeight)
console.log('table:' + this.tableHeight)
},
onRowClick (row) {
this.$router.push('/templates/' + row.slug)
},
resizeTable () {
this.tableHeight = this.pageSize.height - this.$refs.actionsRow.clientHeight -
this.$refs.tableHeaderRow.clientHeight - this.getPageTitle.height -
this.allConst.templatePageHeaderMargins
}
},
mounted () {
console.log('MOUNT TEMPLATES')
this.setPageTitle({ text: 'Manage templates', height: this.allConst.titleHeight })
this.resizeTable()
console.log('tableHeaderRow:' + this.$refs.tableHeaderRow.clientHeight)
console.log('actionsRow:' + this.$refs.actionsRow.clientHeight)
console.log('table:' + this.tableHeight)
},
destroyed () {
console.log('DESTROY TEMPLATES')
this.deletePageTitle()
}
}
Another one of my variables (titleSize) was 0, like the other sizes, during the first onResize() event, and I did not take that into account to correct it during the mounted() Hook.