I made a material design input field as a Vue component. I listen for the focus event and run a function to check the value every time the user focuses out. Here's the code:
<template>
<span class="h-input-container">
<input :type="type" :name="name" v-validate="validation"
#focusout="classHandle" :id="id" :value="value">
<p :class="focusClass"><i :class="icon"></i> {{placeholder}}</p>
</span>
</template>
<script>
export default {
mounted: function(){
if (this.value != '') {
this.focusClass = 'focused'
}
},
props: {
placeholder: {
default: ''
},
name: {
default: 'no-name'
},
type: {
default: 'text'
},
validation: {
default: ''
},
icon: {
default: ''
},
id: {
default: ''
},
value: {
default: ''
}
},
data: function(){
return {
focusClass: '',
}
},
methods: {
classHandle: function(event){
if (event.target.value != '') {
this.focusClass = 'focused'
} else {
this.focusClass = ''
}
}
}
};
</script>
I pass the value as a prop called value and I've used that value for the input field using :value="value". The thing is, every time the method classHandle runs, the input field's value disappears. I can't figure out why.
To clarify the accepted answer, Vue does not "refresh" the value when the focusout handler fires. The property, value, never changed, the input element's value changed.
Changing the focusClass forces Vue to re-render the component to the DOM. Because you've told Vue to use the property, value, as the value of the input via :value="value", it uses the current state of the property, which has never changed as stated above, to render the component and whatever you typed in the input disappears.
The accepted answer goes on to state that you should fix this by updating this.value. In a component, Vue will allow you to do that but it will throw a warning in the console.
[Vue warn]: Avoid mutating a prop directly since the value will be
overwritten whenever the parent component re-renders. Instead, use a
data or computed property based on the prop's value. Prop being
mutated: "value"
Properties of components in Vue are just like function arguments in javascript. Inside the component, you can change them, but that change is limited to the scope of the component. If the parent component ever has to re-render, then the property "value" of your input will be redrawn using the parent's version of the value, and you will lose your changes.
Component communication in Vue "props down, events up". In order to change the value outside your component, you have to $emit it. Here is a version of your component that works, without throwing any warnings, and properly emits your changes.
{
props: {
placeholder: {
default: ''
},
name: {
default: 'no-name'
},
type: {
default: 'text'
},
validation: {
default: ''
},
icon: {
default: ''
},
id: {
default: ''
},
value: {
default: ''
}
},
template:`
<div class="h-input-container" style="background-color: lightgray">
<input :type="type" :name="name"
#focusout="classHandle" :id="id" :value="value" #input="$emit('input', $event.target.value)" />
<p :class="focusClass"><i :class="icon"></i> {{placeholder}}</p>
</div>
`,
data: function() {
return {
focusClass: '',
}
},
methods: {
classHandle: function(event) {
if (event.target.value != '') {
this.focusClass = 'focused'
} else {
this.focusClass = ''
}
}
}
}
I've put together an example to demonstrate the differences in the two approaches.
Typically, I would not use :value="value" #input="$emit('input', $event.target.value)" and use v-model instead, but you are using :type="type" as well, and Vue will throw a warning about using v-model with a dynamic type.
Your current component do not seems to refresh this.value when you make change in the input. Vue cause a component refresh when you focusOut, and because your value is not updated, it shown empy. To resolve your problem, you need to update this.value on event input
new Vue(
{
el: '#app',
props: {
placeholder: {
default: ''
},
name: {
default: 'no-name'
},
type: {
default: 'text'
},
validation: {
default: ''
},
icon: {
default: ''
},
id: {
default: ''
},
value: {
default: ''
}
},
data: function() {
return {
focusClass: '',
}
},
methods: {
updateValue(event) {
this.value = event.target.value
},
classHandle: function(event) {
if (event.target.value != '') {
this.focusClass = 'focused'
} else {
this.focusClass = ''
}
}
}
});
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<span class="h-input-container">
<input :type="type" :name="name" v-validate="validation"
#focusout="classHandle" :id="id" :value="value" #input="updateValue" />
<p :class="focusClass"><i :class="icon"></i> {{placeholder}}</p>
</span>
</div>
So you should put your attention on:
#input="updateValue"
And
updateValue(event) {
this.value = event.target.value
}
Related
I have a text area in a form that I am using to write the description of something. But, the max char limit is 5. I am trying to calculate the max length of my description using the computed property. But, somehow the computed property is not firing when the length of the description crosses the limit of 5 chars. Following is my simple code.
props: {
infoData: {
type: Object,
default: () => {
return {
category: "",
side_categories: "",
description: "",
commentValidationState: null
};
}
},
},
computed: {
descriptionValidation(){
if(this.infoData.description?.length > 5){
alert("Max length exceeded!");
}
}
}
It is noted that I am using the prop directly in the computed property.
My HTML:
<b-form-group
class="col-md-12"
label="Beschreibung"
label-for="description"
invalid-feedback="maximal 750 Zeichen"
:state="infoData.commentValidationState"
>
<b-textarea
class="form-control"
name="description"
id="description"
v-model="infoData.description"
/>
</b-form-group>
Computed properties must return the result of some computation.
Here, a watcher would be more appropriate. In this case, the value to watch would be the length of this.infoData.description. Consequently, I would first use a computed property to get the length of this.infoData.description and then use a watcher to monitor its value.
Here is my implementation:
<template>
<div>
<!--- Component HTML -->
</div>
</template>
<script>
export default {
props: {
infoData: {
type: Object,
default: () => {
return {
category: "",
side_categories: "",
description: "",
commentValidationState: null
};
}
},
},
watch: {
descriptionLength(new_value){
if(new_value> 5){
alert("Max length exceeded!");
}
}
},
computed: {
descriptionLength(){
return this.infoData.description?.length
}
}
}
</script>
And here is its parent:
<template>
<div>
<textarea v-model="infoData.description" />
<MyComponent :infoData="infoData" />
</div>
</template>
<script>
import MyComponent from '#/components/MyComponent.vue'
export default {
components: {
MyComponent,
},
data() {
return {
infoData: {
category: "",
side_categories: "",
description: "",
commentValidationState: null
}
}
},
}
</script>
I have a custom Toggle component, made from scratch in Vue.
<template>
<div :class="{Switch: true, disabled: disabled}" #click="toggle" :title="disabled ? disabledtitle : ''">
<div :class="{Background: true, active: isToggled}"></div>
<div :class="{Toggle: true, active: isToggled}"></div>
</div>
</template>
<script>
export default {
name: 'Toggle',
props: {
toggled: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
disabledtitle: {
type: String,
default: ''
}
},
data () {
return {
isToggled: false
}
},
methods: {
toggle () {
if(this.disabled)
return
this.isToggled = !this.isToggled
this.$emit('input', this.isToggled)
}
},
mounted () {
this.isToggled = this.toggled
}
}
</script>
...
It has multiple instances in an another component, like this:
<template>
<div v-if="category === 'Basic'" class="Basic">
<Toggle :toggled="config.isToggledThisOption" #input="updateConfig($event, 'isToggledThisOption')" />
</div>
<div v-else-if="category === 'Other'" class="Other">
<Toggle :toggled="config.isToggledOtherOption" #input="updateConfig($event, 'isToggledOtherOption')" />
</div>
</template>
...
There is also a menu, that switches between the categories (changes the category variable)
This works well, but the problem is, that the toggled prop does not update when switching between the categories.
I managed to track it down, and the problem is the following:
Whenever the value of the category changes, the <Toggle /> component does not update. It always stays the same, even in inspect element, no updates happen. mounted() also doesn't get executed again, only when the page is reloaded (and even then, not all <Toggle />s' mounted() function get executed).
Do I need to add some kind of id/identificator to the component, so it gets re-mounted/re-rendered/re-initiated?
I'm a newbie of Vue, and I'm trying to simply clear the data of input component once I've submitted, but it seems I'm missing something, because since it's parent data is cleared, I still see the filled value of the input component.
Here is a living example.
I've set to the input child component v-model="title" from it's parent wrapper. Once I submit the data to the parent, I call addItem and in the end, I supposed to clear the data model just by clear it this.title = '', but I probably do something wrong on how to bind data from parent to child.
And above the code, starting from the parent component:
<template>
<form #submit="addItem" class="todo-insert">
<input-item
icon="create"
name="title"
placeholder="Add a ToVue item..."
v-model="title"
/>
<button-item tone="confirm" class="todo-insert__action">
Aggiungi
</button-item>
</form>
</template>
<script>
import ButtonItem from '#vue/Form/ButtonItem/ButtonItem.vue'
import InputItem from '#vue/Form/InputItem/InputItem.vue'
import uuid from 'uuid'
export default {
name: 'TodoInsert',
components: {
ButtonItem,
InputItem
},
data () {
return {
title: ''
}
},
methods: {
addItem (e) {
e.preventDefault()
const todo = {
id: uuid.v4(),
isComplete: false,
title: this.title
}
this.$emit('add-todo', todo)
this.title = ''
}
}
}
</script>
<style lang="scss" scoped src="./TodoList.scss"></style>
This is the child input component:
<template lang="html">
<label class="input">
<div v-if="label" class="input__label text-sans text-sans--label">
{{ label }}
</div>
<div class="input__row">
<input
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
<div v-if="placeholderLabel" class="input__placeholder text-sans text-sans--placeholder">
{{ placeholderLabel }}
</div>
<div v-if="icon" class="input__icon-area">
<icon-item
:name="icon"
/>
</div>
</div>
</label>
</template>
<script>
import IconItem from '../../IconItem/IconItem.vue'
export default {
name: 'InputItem',
props: {
autocomplete: {
type: String,
default: 'off'
},
icon: String,
label: String,
name: {
type: String,
default: 'input-text'
},
placeholder: String,
placeholderLabel: String
},
computed: {
hasPlaceholderLabel () {
return this.placeholderLabel ? 'input__field--placeholder-label' : ''
},
isDirty () {
// return this.$attrs.value ? 'input__field--dirty' : ''
return 'input__field--dirty'
}
},
methods: {
updateValue: function (value) {
this.$emit('input', value)
}
},
components: {
IconItem
}
}
</script>
<style lang="scss" src="./InputItem.scss"></style>
What am I missing?
Your child component is bound unidirectionally. It means that it can change the value, but does not receive any update from the parent component. To receive updates, you need to receive the property value in your child:
props: {
value: String
}
Then, you need to pass the value received to the input :
<input
:value="value"
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
Now the input should update when the parent component changes the value
I'm making a component which is a wrapper around a checkbox (I've done similar with inputs of type 'text' and 'number') but I cannot get my passed in value to bind correctly.
My component is:
<template>
<div class="field">
<label :for="name" class="label">
{{ label }}
</label>
<div class="control">
<input :id="name" :name="name" type="checkbox" class="control" :checked="value" v-on="listeners" />
</div>
<p v-show="this.hasErrors" class="help has-text-danger">
<ul>
<li v-for="error in errors" :key="error">{{ error }}</li>
</ul>
</p>
</div>
</template>
<script>
export default {
name: 'check-edit',
props: {
value: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
errors: {
type: Array,
default: () => []
}
},
mounted () {
},
computed: {
listeners () {
return {
// Pass all component listeners directly to input
...this.$listeners,
// Override input listener to work with v-model
input: event => this.$emit('input', event.target.value)
}
},
hasErrors () {
return this.errors.length > 0
}
},
}
</script>
I've imported it globally; and am invoking it in another view by doing:
<check-edit name="ShowInCalendar" v-model="model.ShowInCalendar" label="Show in calendar?" :errors="this.errors.ShowInCalendar"></check-edit>
My model is in data and the property ShowInCalendar is a boolean and in my test case is true. So when I view the page the box is checked. Using the Vue tools in firefox I can see the model.ShowInCalendar is true, and the box is checked. However, when I click it the box remains checked and the value of ShowInCalendar changes to 'on', then changes thereafter do not change the value of ShowInCalendar.
I found this example here: https://jsfiddle.net/robertkern/oovb8ym7/ and have tried to implement a local data property for it but the result is not working.
The crux of what I'm trying to do is have the initial checkstate of the checkbox be that of ShowInCalendar (or whatever property is bound via v-model on the component) and then have that property be update (to be true or false) when the checkbox is checked.
Can anyone offer me any advice please?
Thank you.
You should not $emit event.target.value, it's the value of the checkbox, it's not a Boolean value. If you want to detect the checkbox is update(to be true or false) or not, You should $emit event.target.checked just like fstep said.
If v-on is the only listener that will be used it might be easier to use v-model as in the checkbox example from the Vue input docs.
However you can use listeners based on Binding-Native-Events-to-Components docs
<template>
<div class="field">
<label :for="name" class="label">
{{ label }}
</label>
<div class="control">
<input :id="name" :name="name" type="checkbox" class="control" checked="value" v-on="listeners" />
</div>
<p v-show="this.hasErrors" class="help has-text-danger">
<ul>
<li v-for="error in errors" :key="error">{{ error }}</li>
</ul>
</p>
</div>
</template>
<script>
export default {
name: 'check-edit',
props: {
value: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
errors: {
type: Array,
default: () => []
}
},
mounted() {},
computed: {
listeners() {
var vm = this;
// `Object.assign` merges objects together to form a new object
return Object.assign(
{},
// We add all the listeners from the parent
this.$listeners,
// Then we can add custom listeners or override the
// behavior of some listeners.
{
// This ensures that the component works with v-model
input: function(event) {
vm.$emit('input', event.target.checked);
}
}
);
},
hasErrors() {
return this.errors.length > 0;
}
}
};
</script>
Don't change props. Your component, having a v-model, should be emitting input events on change. The parent will handle the actual changing of the value.
I have a data structure with nested objects that I want to bind to sub-components, and I'd like these components to edit the data structure directly so that I can save it all from one place. The structure is something like
job = {
id: 1,
uuid: 'a-unique-value',
content_blocks: [
{
id: 5,
uuid: 'some-unique-value',
block_type: 'text',
body: { en: { content: 'Hello' }, fr: { content: 'Bonjour' } }
},
{
id: 9,
uuid: 'some-other-unique-value',
block_type: 'text',
body: { en: { content: 'How are you?' }, fr: { content: 'Comment ça va?' } }
},
]
}
So, I instantiate my sub-components like this
<div v-for="block in job.content_blocks" :key="block.uuid">
<component :data="block" :is="contentTypeToComponentName(block.block_type)" />
</div>
(contentTypeToComponentName goes from text to TextContentBlock, which is the name of the component)
The TextContentBlock goes like this
export default {
props: {
data: {
type: Object,
required: true
}
},
created: function() {
if (!this.data.body) {
this.data.body = {
it: { content: "" },
en: { content: "" }
}
}
}
}
The created() function takes care of adding missing, block-specific data that are unknown to the component adding new content_blocks, for when I want to dynamically add blocks via a special button, which goes like this
addBlock: function(block_type) {
this.job.content_blocks = [...this.job.content_blocks, {
block_type: block_type,
uuid: magic_uuidv4_generator(),
order: this.job.content_blocks.length === 0 ? 1 : _.last(this.job.content_blocks).order + 1
}]
}
The template for TextContentBlock is
<b-tab v-for="l in ['fr', 'en']" :key="`${data.uuid}-${l}`">
<template slot="title">
{{ l.toUpperCase() }} <span class="missing" v-show="!data.body[l] || data.body[l] == ''">(missing)</span>
</template>
<b-form-textarea v-model="data.body[l].content" rows="6" />
<div class="small mt-3">
<code>{{ { block_type: data.block_type, uuid: data.uuid, order: data.order } }}</code>
</div>
</b-tab>
Now, when I load data from the API, I can correctly edit and save the content of these blocks -- which is weird considering that props are supposed to be immutable.
However, when I add new blocks, the textarea above wouldn't let me edit anything. I type stuff into it, and it just deletes it (or, I think, it replaces it with the "previous", or "initial" value). This does not happen when pulling content from the API (say, on page load).
Anyway, this led me to the discovery of immutability, I then created a local copy of the data prop like this
data: function() {
return {
block_data: this.data
}
}
and adjusted every data to be block_data but I get the same behaviour as before.
What exactly am I missing?
As the OP's comments, the root cause should be how to sync textarea value between child and parent component.
The issue the OP met should be caused by parent component always pass same value to the textarea inside the child component, that causes even type in something in the textarea, it still bind the same value which passed from parent component)
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So adjust your codes to like below demo:
for input, textarea HTMLElement, uses v-bind instead of v-model
then uses $emit to popup input event to parent component
In parent component, uses v-model to sync the latest value.
Vue.config.productionTip = false
Vue.component('child', {
template: `<div class="child">
<label>{{value.name}}</label><button #click="changeLabel()">Label +1</button>
<textarea :value="value.body" #input="changeInput($event)"></textarea>
</div>`,
props: ['value'],
methods: {
changeInput: function (ev) {
let newData = Object.assign({}, this.value)
newData.body = ev.target.value
this.$emit('input', newData) //emit whole prop=value object, you can only emit value.body or else based on your design.
// you can comment out `newData.body = ev.target.value`, then you will see the result will be same as the issue you met.
},
changeLabel: function () {
let newData = Object.assign({}, this.value)
newData.name += ' 1'
this.$emit('input', newData)
}
}
});
var vm = new Vue({
el: '#app',
data: () => ({
options: [
{id: 0, name: 'Apple', body: 'Puss in Boots'},
{id: 1, name: 'Banana', body: ''}
]
}),
})
.child {
border: 1px solid green;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<span> Current: {{options}}</span>
<hr>
<div v-for="(item, index) in options" :key="index">
<child v-model="options[index]"></child>
</div>
</div>