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" />
Related
I found some code on the internet to develop an input component. The component works great. However, since there is no two-way binding between the parent and the child. I'm wondering what this.$emit("change", value); does. How does this update the parent? I'm not putting 1 and 2 together here. Thank you in advance.
<template>
<div>
<div class="form-control-label" v-if="label">{{ label }}</div>
<div v-for="(option, index) in options" :key="option.text">
<div class="custom-control custom-radio">
<input
:id="id + index"
:name="id"
type="radio"
:value="option.value"
:checked="option.value === value"
:class="inputClass"
class="custom-control-input"
:disabled="disabled"
:required="required"
#change="updateValue(option.value)"
/>
<label :for="id + index" class="custom-control-label">{{
option.text
}}</label>
<slot v-if="option.value === value" :name="option.value" />
</div>
</div>
<validation-provider v-slot="{ errors }" :name="label" rules="required">
<input type="hidden" v-model="selected" />
<slot name="error">
<div v-if="errors[0]" class="invalid-feedback" style="display: block;">
{{ errors[0] }}
</div>
</slot>
</validation-provider>
</div>
</template>
<script>
export default {
model: {
event: "change",
selected: null,
},
props: {
rules: {
type: String,
required: false,
},
id: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
value: {
type: [String, Number, Boolean, Object],
default: null,
},
options: {
type: [Array],
required: true,
},
required: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
inputClass: {
type: [String, Object],
default: "",
},
},
methods: {
updateValue(value) {
this.selected = value;
this.$emit("change", value);
},
},
};
</script>
Normally, v-model works by the component receiving a value prop and emitting an input event that contains the updated value.
Your component implements v-model by taking a value prop, and emitting a change event (instead of input event), as specified by the model option. Whenever the component emits the change event in updateValue(), the component's consumer sets the v-model variable to the event's value.
In the following example, selectedOption is set to "my value" when the radio option in MyOptions is selected:
// App.vue
<MyOptions v-model="selectedOption" />
// MyOptions.vue
<input type="radio" #change="$emit('change', 'my value')">
I'm trying to create a new component:
Vue.component('my-component', {
props: {
displayed: {
type: Boolean
}
},
template: `
<div v-bind:class="{'modal': true, 'auth-required': true, 'show-modal': displayed }">
<div class="modal__content">
<img src="/img/popup/close.svg" v-on:click="displayed = false;" alt="close" class="modal__closeBtn modal__closeBtn-questions" />
<slot></slot>
<img src="/img/popup/dog.png" alt="dog" class="modal__contentImg" />
</div>
</div>`,
data: function () {
return {
isDisplayed: this.displayed
};
},
})
But when i'm trying to bind displayed property to another property from the page it doesn't work when modal.authRequired value changes:
<mycomponent :displayed="modal.authRequired"></mycomponent>
How to make isDisplayed to be reactive when modal.authRequired changes
Add a watch to your prop:
Vue.component('my-component', {
props: {
displayed: {
type: Boolean
}
},
template: `
<div v-bind:class="{'modal': true, 'auth-required': true, 'show-modal': isDisplayed }">
<div class="modal__content">
<img src="/img/popup/close.svg" v-on:click="displayed = false;" alt="close" class="modal__closeBtn modal__closeBtn-questions" />
<slot></slot>
<img src="/img/popup/dog.png" alt="dog" class="modal__contentImg" />
</div>
</div>`,
data: function () {
return {
isDisplayed: this.displayed
};
},
watch: {
displayed(newValue) {
// Update the data value
this.isDisplayed = newValue;
}
}
})
Also, notice that I changed the 'show-modal' binding: 'show-modal': isDisplayed
Reactive properties are the computed one :https://v2.vuejs.org/v2/guide/computed.html
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.
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>
I need to be able to switch between an input field and a label. When the button "Add Location" is clicked (which create a new div), the input field must be visible. But when the div "Expandable" is maximized it must be hidden and the label visible instead!
The input field should only be visible right after the mentioned button is clicked, else the label has to take its place. What is the best way to achieve this? I was thinking about using some sort of toggle since I am using that in other places.
The label and the input field is placed in the div class "switch".
You can also see the code in this jsFiddle!
Html
<div id="lotsOfDivs">
<addingdivs></addingdivs>
</div>
Vue
var gate = 0;
Vue.component('addingdivs', {
template: `
<div>
<div id="header">
<button class="addDiv" type="button" #click="createDiv">ADD LOCATION</button>
</div>
<div class="parent" v-for="div in divs" :style=" div.height ? { 'height': div.height }: null">
<div class="big" v-if="div.expanded" :key="'expanded' + div.id">
<div class="switch">
<input type="text" v-if="inputFieldInfo">
<label class="propertyLabel" v-else>
<div class="firstChild">
<button class="done" #click="increaseLimit">INCREASE</button>
</div>
<div class="secondChild">
<button class="done" #click="expand(div)">EXPAND</button>
</div>
</div>
<div class="small" v-else :key="'collapsed' + div.id">
<button class="done" #click="expand(div)">EXPAND</button>
</div>
</div>
</div>
`,
data: function() {
return {
gate: gate,
height: "",
count: 0,
locationsArr: ["one", "two", "three"],
divs: [],
InputFieldInfo: false
}
},
methods: {
expand: function(div) {
if (div.expanded) {
div.expanded = false
this.height = ''
} else {
div.expanded = true
this.height = '7vh'
}
},
createDiv: function() {
if (this.count <= gate) { // Here you can decide how many divs that will be generated
// this.count++;
this.divs.push({
id: this.count,
expanded: true,
inputFieldInfo: true,
height: '',
});
this.count++
}},
increaseLimit: function() {
// Here you can increase the number of divs that it's possible to generate
gate++;
}
}
});
new Vue({
el: '#lotsOfDivs',
});
The template had a few compilation errors:
The <label> needs a closing tag (and text content to be useful)
The <div class="big"> needs a closing tag
The v-if was bound to inputFieldInfo, but that variable was declared as InputFieldInfo (note the uppercase I), but based on your behavior description, this field should be unique per location container, so a single data property like this wouldn't work (if I understood your description correctly).
Each location container should have a variable to contain the location name (e.g., locationName) and another variable to contain the show/hide Boolean for the <input> and <label> (i.e., inputFieldInfo):
createDiv: function() {
this.divs.push({
// ...
inputFieldInfo: true,
locationName: ''
});
}
Then, we could bind div.inputFieldInfo and div.locationName to the <input>. We bind to v-model so that the user's text is automatically reflected to the div.locationName variable:
<input v-if="div.inputFieldInfo" v-model="div.locationName">
The <label>'s content should be div.locationName so that it contains the text from the <input> when shown:
<label class="propertyLabel" v-else>{{div.locationName}}</label>
To switch the <input> with the <label> when the expand-button is clicked, we update expand() to set div.inputFieldInfo to false but only when div.locationName is not empty (this gives the user a chance to revisit/re-expand the container to fill in the location later if needed):
expand: function(div) {
if (div.expanded) {
div.expanded = false
if (div.locationName) {
div.inputFieldInfo = false
}
// ...
updated jsfiddle
You had some missing closing tags and an error with InputFieldInfo, it should have a lowercase i.
var gate = 0;
Vue.component('addingdivs', {
template: `
<div>
<div id="header">
<button class="addDiv" type="button" #click="createDiv">ADD LOCATION</button>
</div>
<div class="parent" v-for="div in divs" :style=" div.height ? { 'height': div.height }: null">
<div class="big" v-if="div.expanded" :key="'expanded' + div.id">
<div class="switch">
<input type="text" v-if="inputFieldInfo">
<label class="propertyLabel" v-else>Label</label>
<div class="firstChild">
<button class="done" #click="increaseLimit">INCREASE</button>
</div>
<div class="secondChild">
<button class="done" #click="expand(div)">EXPAND</button>
</div>
</div>
</div>
<div class="small" v-else :key="'collapsed' + div.id">
<button class="done" #click="expand(div)">EXPAND</button>
</div>
</div>
</div>
`,
data: function() {
return {
gate: gate,
height: "",
count: 0,
locationsArr: ["one", "two", "three"],
divs: [],
inputFieldInfo: true
}
},
methods: {
expand: function(div) {
this.inputFieldInfo = false
if (div.expanded) {
div.expanded = false
this.height = ''
} else {
div.expanded = true
this.height = '7vh'
}
},
createDiv: function() {
this.inputFieldInfo = true
if (this.count <= gate) { // Here you can decide how many divs that will be generated
// this.count++;
this.divs.push({
id: this.count,
expanded: true,
inputFieldInfo: true,
height: '',
});
this.count++
}
},
increaseLimit: function() {
// Here you can increase the number of divs that it's possible to generate
gate++;
}
}
});
new Vue({
el: '#lotsOfDivs',
});
You just basically toggle the inputFieldInfo data, whenever each button is pressed.
You can do that by using toggle variable like this
Vue.component('addingdivs', {
template: `
<div>
<div>
<input type="text" v-if="takeinput">
<label v-if="!takeinput">
<button #click="toggleInput()">
</div>
</div>
`,
data: function() {
return {
takeinput:true,
}
},
methods: {
toggleInput: function(){
let vm = this;
vm.takeinput = ( vm.takeinput == true) ? false : true
}
}
});
new Vue({
el: '#lotsOfDivs',
});
In this example, we are just toggeling value of takeinput on click , so according the value either label or input will be showed.
This is very basic exmpale. But you can extend it as your need