I'm trying to create custom Input component with composition API in Vue 3 but when I'm trying to update value with v-model I am getting empty string instead of event value and when I replace custom Input component with default HTML input the value is being updated as expected
Input component:
<template>
<input
:type="type"
:id="name"
:name="name"
:placeholder="placeholder"
class="input"
v-model="modelValue"
/>
</template>
<script lang="ts">
import { computed } from 'vue';
export default {
name: 'Input',
props: {
modelValue: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
type: {
type: String,
default: 'text',
},
placeholder: {
type: String,
required: true,
},
},
setup(props: { value: string }, { emit }) {
const modelValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit('input', value);
},
});
return { modelValue };
},
};
</script>
<form #submit.prevent="handleSubmit">
<Input :name="name" placeholder="Name*" v-model="name" />
<Button>Send</Button>
</form>
Setup method:
setup() {
const name = ref('');
function handleSubmit(data: Event) {
console.log(data, name.value);
}
watch(name, (old, newValue) => {
console.log(name, old, newValue);
});
return { name, handleSubmit };
},
There are a couple of errors & warnings in your code:
you should declare emitted events in the emits option (more on this here)
you did not have a value prop passed down from the parent component to Input (thus I removed it)
if you want to do the "easy sync" with v-model, then it's best to emit a custom event called update:modelValue (where modelValue is the value you want to bind as prop, e.g. update:name; more on this here)
you should NOT name a variable in the setup function the same as a prop (this is just sensibility - you'll mess up what is what, eventually)
const {
computed,
ref,
} = Vue
const Input = {
name: 'Input',
props: {
name: {
type: String,
required: true,
},
type: {
type: String,
default: 'text',
},
placeholder: {
type: String,
required: true,
},
},
emits: ['update:name'],
setup(props, { emit }) {
const modelValue = computed({
get() {
return props.name;
},
set(value) {
emit('update:name', value);
},
});
return {
modelValue
};
},
template: `
<input
:type="type"
:id="name"
:name="name"
:placeholder="placeholder"
class="input"
v-model="modelValue"
/>
`
}
const App = {
setup() {
const name = ref('');
function handleSubmit(data) {
console.log(data, name.value);
}
return {
name,
handleSubmit
};
},
template: `
Name Ref: {{ name }}<br />
<form #submit.prevent="handleSubmit">
<Input
:name="name"
placeholder="Name*"
v-model="name"
/>
<button type="submit">Send</button>
</form>
`
}
const vm = Vue.createApp(App)
vm.component('Input', Input)
vm.mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<div id="app"></div>
Related
I'd like to have a list which maps the modified values back to the description in the container. In other way, I want the container to have a prop shortcuts like:
[{action: "foo"}]
with text being editable in the component created from the list.
I've got the container:
Vue.component("shortcuts", {
data: function () {
return {
shortcuts: []
}
},
methods: {
add: function() {
this.shortcuts.push({
action: "",
})
},
},
template: '<div>\
<shortcut-entry v-for="(shortcut, index) in shortcuts" is="shortcut-entry" v-bind:key="index" v-bind="shortcut" #remove="shortcuts.splice(index, 1)"></shortcut-entry>\
<br>\
<button v-on:click=add>Add</button>\
</div>'
})
And the list element:
Vue.component("shortcut-entry", {
methods: {
clearParams: function() {
this.params = {}
},
remove: function() {
this.$emit("remove")
}
},
props: {
action: String,
},
template: '<div>\
<input type="text" v-model="action"></input>\
<button v-on:click="remove">Remove</button>\
</div>'
})
However this results in a warning:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
But if I use data instead of props in the shortcut-entry, the changes do not propagate into the objects in the list either.
What am I missing here? How can I bind the text entry in the item back to the object in the list in the container?
Use v-model to update the state:
Vue.component('shortcuts', {
props: {
value: Array // Of shortcuts
},
methods: {
add: function () {
const newValue = [...this.value, { action: '' }]
this.$emit('input', newValue)
},
remove: function (index) {
const newValue = [...this.value]
newValue.splice(index, 1)
this.$emit('input', newValue)
}
},
template: '<div>\
<shortcut-entry v-for="(shortcut, index) in value" is="shortcut-entry" v-bind:key="index" v-bind="shortcut" #remove="remove(index)"></shortcut-entry>\
<br>\
<button v-on:click=add>Add</button>\
</div>'
And the parent component is something like this:
Vue.component('parent-comp', {
data () {
return {
shortcuts: []
}
},
template: '<shortcut v-model="shortcuts"></shortcut>'
})
*** Use .sync ***
Vue.component('shortcuts', {
props: {
shortcuts: Array // Of shortcuts
},
methods: {
add: function () {
const newValue = [...this.shortcuts, { action: '' }]
this.$emit('update:shortcuts', newValue)
},
remove: function (index) {
const newValue = [...this.shortcuts]
newValue.splice(index, 1)
this.$emit('update:shortcuts', newValue)
}
},
template: '<div>\
<shortcut-entry v-for="(shortcut, index) in shortcuts" is="shortcut-entry" v-bind:key="index" v-bind="shortcut" #remove="remove(index)"></shortcut-entry>\
<br>\
<button v-on:click=add>Add</button>\
</div>'
})
So, the caller component:
Vue.component('parent-comp', {
data () {
return {
shortcuts: []
}
},
template: '<shortcut :shortcuts.sync="shortcuts"></shortcut>'
})
I have an input in a child component and when the user starts typing in the input it updates the data in the parent component, or should. This is my code, I can post other parts if useful.
Child
<input
:keyup="updateFilters(filters[data.field.key])"
:placeholder="data.label"
/>
methods: {
updateFilters(value) {
this.$emit("input", value);
}
}
Parent
data() {
return {
filters: {
name: "",
age: "",
address: "",
},
};
},
You can change the parent from child component the same way you do emiting other events like onchange, onkeyup, onkeydown etc.
Vue.component('parent', {
data() {
return {
parentValue: ''
};
},
template: `
<div>
<label>Parent state value:</label>
{{parentValue}}
<br/><br/>
<label>input is the child component:</label>
<br/>
<child #fromChild="fromChild"></child>
</div>
`,
methods: {
fromChild(value) {
this.parentValue = value
console.log(value) // someValue
}
}
})
Vue.component('child', {
template: `
<input
v-on:keyup="updateParent($event.target.value)"
placeholder="type something"
/>
`,
methods: {
updateParent(value) {
console.log(value)
this.$emit("fromChild", value);
}
},
})
new Vue({
el: "#app",
data: {
label: 'in Vue'
},
methods: {
toggle: function(todo) {
todo.done = !todo.done
}
}
})
I've prepared a working example here.
I'm currently using this UI component from http://www.vue-tags-input.com
I'm planning to create a reusable component for vue-tags-input, here's my current code:
components/UI/BaseInputTag.vue
<template>
<b-form-group :label="label">
<no-ssr>
<vue-tags-input
:value="tags"
#tags-changed="updateValue"/>
</no-ssr>
</b-form-group>
</template>
<script>
export default {
name: 'BaseInputTag',
props: {
label: { type: String, required: true },
value: { type: [String, Number, Array] },
tags: { type: [Array] }
},
methods: {
updateValue(newTags) {
this.$emit('input', newTags);
}
}
}
</script>
and in my parent vue page. I'm calling above component with this code:
pages/users/new.vue
<BaseInputTag v-model="tag" :tags="interests" label="Interests"/>
<script>
export default {
name: 'InsiderForm',
data() {
return {
tag: '',
interests: []
};
}
}
</script>
How can I emit back the child component's newTags to parent's data interests
You're almost there!
Parent component:
<BaseInputTag v-model="tag" :tags="interests" #input="doStuffWithChildValue" label="Interests"/>
<script>
export default {
name: 'InsiderForm',
data() {
return {
tag: '',
interests: []
};
},
methods: {
doStuffWithChildValue (value) {
console.log('Got value from child', value)
}
}
}
</script>
I managed to make it work, here's my code:
child-component
<template>
<b-form-group :label="label">
<no-ssr>
<vue-tags-input
:value="value"
v-model="tag"
placeholder="Add Tag"
:tags="tags"
#tags-changed="updateValue"/>
</no-ssr>
</b-form-group>
</template>
<script>
export default {
name: 'BaseInputTag',
props: {
label: { type: String, required: true },
value: { type: [String, Number, Array] },
tags: { type: [Array, String] },
validations: { type: Object, required: true }
},
data() {
return {
tag: ''
}
},
methods: {
updateValue(newTags) {
this.$emit('updateTags', newTags);
}
}
}
</script>
and to receive the changes to parent component:
<BaseInputTag :tags="interests" #updateTags="interests = $event" label="Interests"/>
<script>
export default {
name: 'InsiderForm',
data() {
return {
tag: '',
interests: []
};
}
}
</script>
My problem is that currently the ref returns a validation message for only the last component, when I want it return from both or either one if the input component is empty.
I have tried to add a index and get undefined value or if I dynamically
added it the ref with a unique value 'name'
I read somewhere that the ref need to be unique - but how do I make it unique?
May aim is trigger a validation function from the child component to show an the errors message on form submit.
can anyone suggest how I can do this.
PARENT
class MainForm extends Component {
handleSubmit(e) {
e.preventDefault();
this.inputRef.handleInputChange(e);
}
render() {
const nameFields = [
{
id: 'firstame', type: 'text', name: 'firstname', label: 'First name', required: true,
},
{
id: 'lastName', type: 'text', name: 'lastname', label: 'Last name', required: true,
},
];
return (
<div>
<form onSubmit={e => (this.handleSubmit(e))} noValidate>
<div className="form__field--background">
<p> Please enter your name so we can match any updates with our database.</p>
<div>
{
nameFields.map(element => (
<div key={element.id} className="form__field--wrapper">
<InputField
type={element.type}
id={element.name}
name={element.name}
required={element.required}
placeholder={element.placeholder}
label={element.label}
pattern={element.pattern}
isValid={(e, name, value) => {
this.inputFieldValidation(e, name, this.handleFieldValues(name, value));
}}
ref={c => this.inputRef[`${element.name}`] = c}
/>
</div>
))
}
</div>
</div>
<button type="submit" className="btn btn--red">Update Preferences</button>
</form>
</div>
);
}
}
export default MainForm;
CHILD
class InputField extends Component {
constructor() {
super();
this.handleInputChange = this.handleInputChange.bind(this);
this.handleOnBlur = this.handleOnBlur.bind(this);
this.state = {
valid: null,
message: '',
};
}
/**
* Calls helper function to validate the input field
* Sets the the state for the validation and validation message
*/
validateField(e) {
const props = {
field: e.target,
value: e.target.value,
label: this.props.label,
required: this.props.required,
min: this.props.min,
max: this.props.max,
pattern: this.props.pattern,
emptyError: this.props.emptyFieldErrorText,
invalidError: this.props.invalidErrorText,
};
let validation = this.state;
// helper function will return an updated validation object
validation = fieldValidation(props, validation);
this.setState(validation);
return validation;
}
/**
* Calls validateField method if field is a checkbox.
* Handles the callback isValid state to parent component.
*/
handleInputChange(e) {
if ((e.target.required && e.target.type === 'checkbox')) {
this.validateField(e);
}
}
handleOnBlur(e) {
if (e.target.type !== 'checkbox') {
this.validateField(e);
}
}
render() {
return (
<div >
<label id={`field-label--${this.props.id}`} htmlFor={`field-input--${this.props.id}`}>
{this.props.label}
</label>
{this.props.helpText &&
<p className="form-help-text">{this.props.helpText}</p>
}
<input
type={this.props.type}
id={`field-input--${this.props.id}`}
name={this.props.name && this.props.name}
required={this.props.required && this.props.required}
placeholder={this.props.placeholder && this.props.placeholder}
onBlur={e => this.handleOnBlur(e)}
onChange={e => this.handleInputChange(e)}
ref={this.props.inputRef}
/>
}
{this.state.valid === false &&
<span className="form-error">
{this.state.message}
</span>
}
</div>
);
}
}
export default InputField;
How to binding parent's model to child in Vue.js?
These codes below is works fine. if i fill the input manually, then child's model return it's value to the parent's model.
But the issue is, if the data set from AJAX request in a parent, the input doesn't automatically filled.
Can anyone help me on this?
Form.vue
<template>
<form-input v-model="o.name" :fieldModel="o.name" #listenChanges="o.name = $event"/>
<form-input v-model="o.address" :fieldModel="o.address" #listenChanges="o.address = $event"/>
</template>
<script>
import FormInput from '../share/FormInput.vue'
export default {
data () {
return {
o: {
name: '',
address: ''
}
}
},
components: { 'form-input': FormInput },
created: function() {
axios.get('http://api.example.com')
.then(response => {
this.o.name = response.data.name
this.o.address = response.data.address
})
.catch(e => { console.log(e) })
}
}
</script>
FormInput.vue
<template>
<input type="text" v-model='fieldModelValue' #input="forceUpper($event, fieldModel)">
</template>
<script>
export default {
props: ['fieldModel'],
data() {
return {
fieldModelValue: ''
}
},
mounted: function() {
this.fieldModelValue = this.fieldModel;
},
methods: {
forceUpper(e, m) {
const start = e.target.selectionStart;
e.target.value = e.target.value.toUpperCase();
this.fieldModelValue = e.target.value.toUpperCase();
this.$emit('listenChanges', this.fieldModelValue)
}
}
}
</script>
Things are more straightforward if you take advantage of v-model in components.
If you put v-model on a component, the component should take a prop named value, and should emit input events to trigger it to update.
I like to make a computed to hide the event emitting, and allow me to just v-model the computed inside my component.
new Vue({
el: '#app',
data: {
o: {
name: '',
address: ''
}
},
components: {
'form-input': {
template: '#form-input',
props: ['value'],
computed: {
fieldModelValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue.toUpperCase());
}
}
}
}
},
// Simulate axios call
created: function() {
setTimeout(() => {
this.o.name = 'the name';
this.o.address = 'and address';
}, 500);
}
});
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
Name ({{o.name}})
<form-input v-model="o.name"></form-input>
Address ({{o.address}})
<form-input v-model="o.address"></form-input>
</div>
<template id="form-input">
<input type="text" v-model='fieldModelValue'>
</template>
The mounted() hook is blocking subsequent updates from the parent.
Remove mounted and change v-model to 'fieldModel'
<template>
<input type="text" :value='fieldModel' #input="forceUpper($event, fieldModel)">
</template>
<script>
export default {
props: ['fieldModel'],
data() {
return {
fieldModelValue: ''
}
},
// mounted: function() {
// this.fieldModelValue = this.fieldModel;
// },
methods: {
forceUpper(e, m) {
const start = e.target.selectionStart;
e.target.value = e.target.value.toUpperCase();
this.fieldModelValue = e.target.value.toUpperCase();
this.$emit('listenChanges', this.fieldModelValue)
}
}
}
</script>
Demo CodeSandbox