Vue3 - API Composition : Custom v-model prop - vue.js

In Vue2, the v-model bind to the prop value, but it's possible to define a other prop :
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
I want do the same in Vue 3 with API Composition.
How do you define a custom prop to v-model in Vue 3 with the API Composition?
Example :
<script setup lang="ts">
defineProps({
msg: {
type: String,
}
});
defineEmits(['update:msg'])
defineModel('msg', 'update:msg');
</script>
<template>
<div>
{{ msg }}
</div>
<button #click="$emit('update:msg', 'Clicked!')">Switch</button>
</template>
Of course defineModel don't exist, but I must not be far from the truth.

In Vue 3 the model option are removed and replaced with an argument on v-model
So if you want to change the v-model prop you can by changing the defineProps and defineEmits to use another name than modelValue.
However you cannot implicitly use v-model without adding the new custom prop name after the v-model.
You have to specify it like this : v-model:myCustomPropName.
<template>
<input :value="myCustomPropName" #input="$emit('update:myCustomPropName', $event.target.value)" />
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
defineProps(["myCustomPropName"]);
defineEmits(["update:myCustomPropName"]);
</script>
Usage:
<MyCustomInput v-model:myCustomPropName="msg" />
Vue 3 props Doc : https://vuejs.org/guide/components/v-model.html
See the changes from Vue 2 to Vue 3 : https://v3-migration.vuejs.org/breaking-changes/v-model.html

Related

How to declare local property from composition API in Vue 3?

In Vue 2 I would do this:
<script>
export default {
props: ['initialCounter'],
data() {
return { counter: this.initialCounter }
}
}
</script>
In Vue 3 I tried this:
<script setup>
import { ref } from 'vue';
defineProps({ 'initialCounter': Number })
const counter = ref(props.initialCounter)
</script>
This obviously doesn't work because props is undefined.
How can I bind one-way properties to a local variable in Vue 3?
It seems the result of defineProps is not assigned as a variable. check Vue3 official doc on defineProps. Not really sure what is the use case of ref() here but toRef API can be used as well.
import { ref } from 'vue';
const props = defineProps({ 'initialCounter': Number })
const counter = ref(props.initialCounter)
Retrieving a read-only value and assigning it to another one is a bad practice if your component is not a form but for example styled input. You have an answer to your question, but I want you to point out that the better way to change props value is to emit update:modalValue of parent v-model passed to child component.
And this is how you can use it:
<template>
<div>
<label>{{ label }}</label>
<input v-bind="$attrs" :placeholder="label" :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
<span v-for="item of errors">{{ item.value }}</span>
</div>
</template>
<script setup>
defineProps({ label: String, modelValue: String, errors: Array })
defineEmits(['update:modelValue'])
</script>
v-bind="$attrs" point where passed attributes need to be assigned. Like type="email" attribute/property of a DOM element.
And parent an email field:
<BaseInput type="email" v-model="formData.email" :label="$t('sign.email')" :errors="formErrors.email" #focusout="validate('email')"/>
In this approach, formdata.email in parent will be dynamically updated.

How to correctly pass a v-model down to a Quasar q-input base component?

I am using Quasar to build my Vue app and I want to create a base component (a.k.a. presentational, dumb, or pure component) using q-input.
I have a created a SFC named VInput.vue as my base component, it looks like this:
<template>
<q-input
outlined
class="q-mb-md"
hide-bottom-space
/>
</template>
Then I created a SFC named TestForm.vue that looks like this:
<template>
<q-form>
<v-input label="Email" v-model="email" />
</q-form>
</template>
<script setup lang="ts">
import VInput from './VInput.vue';
import { ref } from 'vue';
const email = ref('john#example.com');
</script>
The label="Email" v-model="email" parts are passed down to my VInput.vue base component and correctly rendered on the page.
But there is a typescript error on q-input of the VInput.vue base component because q-input requires a v-model:
`Type '{ outlined: true; class: string; hideBottomSpace: true; "hide-bottom-space": boolean; }' is not assignable to type 'IntrinsicAttributes & VNodeProps & AllowedComponentProps & ComponentCustomProps & QInputProps'.`
`Property 'modelValue' is missing in type '{ outlined: true; class: string; hideBottomSpace: true; "hide-bottom-space": boolean; }' but required in type 'QInputProps'.ts(2322)`.
So how do I code the VInput.vue base component without knowing the v-model value head of time?
I have come up with the below solution, which seems to work because I think the v-model passed down is overiding the base component v-model.
But I wanted to ask to make sure I wasn't screwing something up.
Is this the correct way of doing things? It seems hacky.
<template>
<q-input v-model="inputText" outlined class="q-mb-md" hide-bottom-space />
</template>
<script setup lang="ts">
const inputText = '';
</script>
I found a couple of solutions:
Solution 1
It involves splitting the v-model into it seperate parts (:model-value and #update:model-value, and then passing in the text value as a prop.
Base component VInput.vue:
<template>
<q-input
outlined
class="q-mb-md"
hide-bottom-space
:model-value="text"
#update:model-value="(value) => emit('update:text', value)"
/>
</template>
<script setup lang="ts">
defineProps({
text: {
required: false,
type: String,
},
});
const emit = defineEmits(['update:text']);
</script>
Solution 2
Extracting the prop and using toRef on it.
<template>
<q-input outlined class="q-mb-md" hide-bottom-space v-model="textProp" />
</template>
<script setup lang="ts">
import { toRef } from 'vue';
const props = defineProps({
text: {
required: false,
type: String,
default: '',
},
});
const textProp = toRef(props, 'text');
</script>

Vue mutate prop binded by v-bind with sync modifier

I have an object in my component data. Now, I'm just binding all the properties of the object as a prop to the child component using v-bind.sync directive. I'm updating these props from the child component using the built-in update event but still, I'm getting Avoid mutation props directly error in the console. Here is the minimal example attached.
Parent Component
<template>
<div>
<oslo v-bind.sync="data" />
</div>
</template>
<script>
import Oslo from '#/components/Oslo.vue'
export default {
components: {
Oslo,
},
name: 'OsloParent',
data() {
return {
data: {
data: {
name: 'Oslo name',
access: 'admin'
}
},
}
},
}
</script>
Child component
<template>
<div>
<input type="text" v-model="name" #keyup="$emit('update:name', name)" />
<input type="text" v-model="access" #keyup="$emit('update:access', access)" />
</div>
</template>
<script>
export default {
props: {
name: String,
access: String
},
name: 'Oslo',
}
</script>
This is just an example component I've created for the reproduction of the problem. The actual component is supposed to handle so many props with two-way binding and that's the reason I'm binding the data with v-bind directive with sync modifier. Here is the Vue warning from the console (most common).
[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: "name"
Any suggestions to improve this or silent the Vue warn for this specific case? The above-given components works as desired, Vue throws error though.
I found two problems with your example that might throw this off.
The use of v-model directly to the property. Use v-bind instead to have it only display. And use v-on:change handler to fire the $emit('update:propertyname', value) and send the new value to update on the object.
The value sent along in the $emit seems empty and thus makes no change. Use $event.target.value instead.
Side note: v-on:keyup might not be the best event to listen to, since input can also be drag-and-dropped. Listening to v-on:change would be beter in that case.
Note on event listeners when using only v-bind.sync instead of v-bind:propertyName.sync:
If you want to listen to the update:propertyName event from the child component on the parent, you have to use the .capture modifier. Otherwise the update event is caught by the v-on:update:propertyName on the child component and this does not bubble up to the parent.
So you can use v-on:update:name.capture="someMethod" on the <oslo> tag for example. And have this someMethod in the parent's methods. After this is called, the event will be triggered on the child component which will update the object and thereby the property.
All together:
let Oslo = {
props: {
name: String,
access: String
},
name: 'Oslo',
template: `<div>
<input type="text" :value="name" #change="$emit('update:name', $event.target.value)" />
<input type="text" :value="access" #change="$emit('update:access', $event.target.value)" />
</div>`
}
new Vue({
el: "#app",
components: {
Oslo,
},
data: {
thedata: {
name: 'Oslo name',
access: 'admin'
}
},
methods: {
nameWillBeUpdated: function(v) {
console.log('New value of name will be:', v);
// After this, the `update:name` event handler of the
// child component is triggered and the value will change.
},
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<span>{{this.thedata.name}} - {{this.thedata.access}}</span>
<oslo
v-bind.sync="thedata"
v-on:update:name.capture="nameWillBeUpdated"
/>
</div>
You can just pass an object and sync it instead of individual properties if you have many properties to listen to from child component. See the example below:
Vue.config.productionTip = false
Vue.config.devtools = false
Vue.component('Oslo', {
template: `
<div>
<input type="text" v-model="comp_name" #keyup="$emit('update:name', comp_name)" />
<input type="text" v-model="comp_access" #keyup="$emit('update:access', comp_access)" />
</div>
`,
props: {
data: {
name: String,
access: String,
}
},
data() {
return {
comp_name: this.data.name,
comp_access: this.data.access
}
}
})
new Vue({
el: '#app',
data() {
return {
doc: {
name: 'Oslo name',
access: 'admin'
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>
<span>---- {{ this.doc.name }}----</span>
<span>---- {{ this.doc.access }}----</span>
<oslo :data="this.doc" v-bind.sync="doc" />
</div>
</div>

How to pass all events to parent in VueJS

Passing Props
In VueJS if you set inheritAttrs to false and use v-bind="$attrs" you pass all props not declarated in a component to its child. Is there a similar way to pass all events coming from child to its parent in VueJS?
Code Example
Wrapper
<template>
<child-component v-bind="$attrs" />
</template>
<script>
module.exports = {
inheritAttrs: false
...
}
</script>
Child
<template>
...
</template>
<script>
module.exports = {
...
props: {
prop1: Boolean,
prop2: Boolean
}
...
}
</script>
Parent
<template>
<wrapper :prop1="false" :prop2="true" />
...
</template>
To pass all events use
v-on="$listeners"
For Vue 3.x:
v-bind="$attrs"
see listeners removed
(thanks to AverageHelper)

Vue js how to use props values to v-model

I have two component namely App.vue and hello.vue
In App component I import the hello component and use props to pass relevant data to the hello component.
there I bind data which are took from the App component.
In my hello component I have a input box bind to the passed value.
My final goal is pass values as props to the hello component and change it and finally
pass that edited values to the backend using the save method.
How do I achive this?
This is what I have done up to now.
App.vue
<template>
<div id="app">
<hello-world :msg="'hello good morning'"></hello-world>
</div>
</template>
<script>
import helloWorld from "./components/HelloWorld";
export default {
components: {
helloWorld
}
};
</script>
hello.vue
<template>
<div>
<input type="text" :value="msg">
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String
}
};
</script>
In my hello component's input field v-model is not possible. I want something similar to the v-model.
You cannot use prop to bind to v-model. Child component is not supposed to modify prop passed by the parent component.
You will have to create a copy of prop in your child component if you wish to use prop with v-model and then watch prop like this:
<template>
<div>
<input type="text" #input="onInput" v-model="msgCopy">
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String
},
data() {
return { msgCopy: '' };
},
methods: {
onInput(newInputValue) {
this.$emit('msgChange', newInputValue);
}
}
watch: {
msg(newVal) {
this.msgCopy = newVal;
}
}
};
</script>
Also, notice the use of event handler #input to pass changed prop back to the parent component via event. As a syntax sugar, you can make your Hello component work as a custom form input control by adopting to v-model lifecycle.