How to point to wrapping custom element with v-model? - vue.js

I currently have a custom vue-form component:
In my common HTML it kind of looks something like this:
<vue-form>
<input type="text" v-model="test">
<input type="password" v-model="test2">
</vue-form>
Note that this is not a Vue template.
My v-model keeps pointing to my root component when in this case I would like my v-model to point the the actual component that is wrapping my content.
For the vue-form component I am simply using a slot like so:
<template>
<div>
<slot></slot>
</div>
</template>
Is there a way to get the v-model binding to point towards the wrapping component instead of the root element?

You can use Scoped Slots documentation.
Edit:
Here is working example:
<div id="app">
<vue-form v-bind:model="x">
<template scope="props">
<input type="text" v-model="props.model.test">
<input type="password" v-model="props.model.test2">
</template>
</vue-form>
</div>
new Vue({
el: '#app',
data: function() {
return {
x: {
test: 'John',
test2: 'Smith'
}
}
},
components: {
'vue-form': {
template: `<div><slot :model="model"></slot></div>`,
props: ['model']
}
}});
jsfiddle
Just notice that this solution requires from you to propagate property from component to slot by :model="model" part in slot declaration.

Related

Is it possible to use a prop as a v-model value?

Is it possible to use the value of a prop as the input's v-model?
I normally do the following when creating an input:
<template>
<form>
<input v-model="form.email" type="email"/>
</form>
</template>
<script>
export default {
data() {
return {
form: {
email: '',
}
}
}
}
</script>
But now I'm trying to achieve the following where this.myProp is used within the v-model without being displayed as a string on the input:
<template>
<form>
<input v-model="this.myProp" type="email"/>
</form>
</template>
<script>
export default {
props: ['myProp'] // myProp = form.email for example (to be handled in a parent component)
}
</script>
Yes, but while using it in parent component. In child component you need to extract value and #input instead of using v-model (v-model is shortcut for value="" and #input) Here is an example of input with label, error and hint in Vue 3 composition API.
BaseInput.vue
<template>
<div class="flex flex-col">
<label>{{ label }}</label>
<input v-bind="$attrs" :placeholder="label" :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
<span v-for="item of errors" class="text-red-400">{{ item.value }}</span>
<span v-if="hint" class="text-sm">{{ hint }}</span>
</div>
</template>
<script setup>
defineProps({ label: String, modelValue: String | Number, errors: Array, hint: String })
defineEmits(['update:modelValue'])
</script>
Using v-bind="$attrs" you target where attributes like type="email" need to be applied in child component. If you don't do it, it will be added to the top level DOM element. In above scenario <div>.
ParentComponent.vue
<BaseInput type="email" v-model="formData.email" :label="Email" :errors="formErrors.email"/>

Vue.js Child Component not Updating Data in Parent Component

I am using vue components in a Laravel project.
I have taken sample code from https://vuejs.org/guide/components/events.html#usage-with-v-model
I have a child component with an input box:
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
</script>
<template>
<input
:value="modelValue"
#input="$emit('update:modelValue', $event.target.value)"
/>
</template>
This component is being used in a parent component.
<phone-input v-model="phone"/> {{phone}}
The parent component displays the input box with the initial value of the phone variable. However, the changed value is not reflected in the parent's phone variable ( {{phone}} does not update). Am I missing something? I have cleared the cache, but it did not help.
I tried another variation of the code (from vue.js documentation code) as given here. However, this also does not work.
Parent
<MyComponent v-model:title="bookTitle" />. {{bookTitle}}
<!-- Child Component MyComponent.vue -->
<script>
export default {
props: ['title'],
emits: ['update:title']
}
</script>
<template>
<input
type="text"
:value="title"
#input="$emit('update:title', $event.target.value)"
/>
</template>
Thanks for your help.
Take a look at following snippet, looks ok:
const app = Vue.createApp({
data() {
return {
phone: "0123456"
}
},
})
app.component('phoneInput', {
template: `
<input
:value="modelValue"
#input="$emit('update:modelValue', $event.target.value)"
/>
`,
props: ['modelValue'],
emits: ['update:modelValue'],
})
app.mount("#demo")
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<phone-input v-model="phone"></phone-input>
{{ phone }}
</div>
<script>
export default {
props: ['modelValue'],
}
</script>
<template>
<input
:value="modelValue"
#input="$emit('input', $event.target.value)"
/>
</template>
You need specially emit input to make it work

Passing v-model into a checkbox inside a Component in Vue 3?

I want to embed a checkbox inside a Vue3 Component and have the v-model binding passed down to the checkbox.
Inside the Component:
<!-- Tile.vue -->
<template>
<div>
<input type=checkbox v-model="$attrs">
</div>
</template>
<script>
export default {inheritAttrs: false}
</script>
Then in an outside file:
<template>
<Tile value="carrot" v-model="foods" />
<Tile value="tomatoes" v-model="foods" />
</template>
<script setup>
var foods = ref([]);
</script>
How do I achieve this?
The documentation says that v-model is just a shorthand for :modelValue and #update:modelValue but this is not universal as Vue obviously behaves differently for form elements such as smartly listening to onchange instead of oninput and modifying the property checked instead of value depending on the node.
If I use v-model on the outer component, how do I forward it to the checkbox and get the same smart behavior that Vue has?
I have found tons of controversial information. Some recommend using #input event (Vue 3 custom checkbox component with v-model and array of items). Some recommend emitting modelValue:update instead of update:modelValue (https://github.com/vuejs/core/issues/2667#issuecomment-732886315). Etc.. Following worked for me after hour of trial and error on latest Vuejs3
Child
<template>
<div class="form-check noselect">
<input class="form-check-input" type="checkbox" :id="id" :checked="modelValue" #change="$emit('update:modelValue', $event.target.checked)" />
<label class="form-check-label" :for="id"><slot /></label>
</div>
</template>
<script>
import { v4 as uuidv4 } from "uuid";
export default {
inheritAttrs: false,
emits: ["update:modelValue"],
props: {
modelValue: {
type: Boolean,
required: true,
},
},
setup() {
return {
id: uuidv4(),
};
},
};
</script>
Parent:
<Checkbox v-model="someVariable">Is true?</Checkbox>
you can verify that it works but doing this in parent:
var someVariable= ref(false);
watch(someVariable, () => {
console.log(someVariable.value);
});
p.s. The other solution above does not work for me. Author recommends using value property. But in example he passes v-model attribute. So I don't know exactly how it's supposed to work.
You can achieve the behavior by using emits to keep data in sync and behave as default v-model behavior. Checkbox component:
<template>
<div>
<input
type="checkbox"
:checked="value"
#change="$emit('input', $event.target.checked)"
/>
{{ text }}
</div>
</template>
<script>
export default {
name: "inputcheckbox",
props: ["value", "text"],
};
</script>
And in the parent component you can have as many checkboxes you want.
<template>
<div id="app">
<maincontent :showContent="showContent" />
<inputcheckbox text="one" v-model="checkedOne" />
<inputcheckbox text="two" v-model="checkedTwo" />
</div>
</template>
Here is a vue 2 example but is applicable to vue 3 as well. Hope this was helpful. Sandbox with this behavior:
https://codesandbox.io/embed/confident-buck-kith5?fontsize=14&hidenavigation=1&theme=dark

How to pass #blur into child component Vuejs

I created a custom Input component.
I needed pass #blur from vee-validate form to my custom input component.
It works great in normal html input tag. I no idea how could we pass the #blur into custom Input component.
Example 1 work correctly, it triggered the validation after blur the input.
<template>
<form #submit="onSubmit">
<input #blur="emailBlur" v-model="email" type="text" autocomplete="off" name="email" placeholder="email">
<button type="submit" :disabled="isSubmitting">Submit</button>
</form>
</template>
Example 2 with My custom Input Component:
// src/components/Input.vue
<template>
<div class="mt-2">
<label :for="name" class="h5">Name</label>
<input
:type="type"
:value="modelValue"
#change="$emit('update:modelValue', $event.target.value)"
:id="name"
:placeholder="placeholder"
/>
</div>
</template>
<script>
export default {
name: 'Input',
props: ["modelValue", 'name', 'type', 'placeholder'],
setup(props) {
console.log('props :>> ', props); // not receive the #blur
}
}
</script>
Parent Component (App.vue):
<template>
<form>
<Input #blur="emailBlur" v-model="email" type="text" name="email" placeholder="Custom input email" />
<button type="submit" :disabled="isSubmitting">Submit</button>
</form>
</template>
export default {
name: 'App',
components: {
Input
},
setup() {
// the vee validation values v-model to template
}
}
Sending the handler as props from the parent is not a good practice instead need to trigger the handler(present in the parent) from the child component. By doing so you will have an advantage where you can bind different blur handlers based on your requirement inside different parent components
To do so you can follow the below approach
Custom Input Component
// src/components/Input.vue
<template>
<div class="mt-2">
<label :for="name" class="h5">Name</label>
<input
:type="type"
:value="modelValue"
#change="$emit('update:modelValue', $event.target.value)"
:id="name"
:placeholder="placeholder"
#blur="$emit('blur')" //Change added
/>
</div>
</template>
<script>
export default {
name: 'Input',
props: ["modelValue", 'name', 'type', 'placeholder'],
emits: ['blur', 'update:modelValue'], // change added
}
</script>
Note:
for all v-models without arguments, make sure to change props and events name to modelValue and update:modelValue respectively
For Example:
Parent.vue
<ChildComponent v-model="pageTitle" />
and in Child.vue it should be like
export default {
props: {
modelValue: String // previously was `value: String`
},
emits: ['update:modelValue'],
methods: {
changePageTitle(title) {
this.$emit('update:modelValue', title) // previously was `this.$emit('input', title)`
}
}
}
What you are creating is usually called "transparent wrapper" component. What you want from this wrapper is to behave in almost every way as normal input component so the users of the component can work with it as it was normal input (but it is not)
In your case, you want to attach #blur event listener to your wrapper. But the problem is that blur is native browser event i.e. not a Vue event.
When you place event listener on a component for an event not specified in emits option (or v-bind an attribute that is not specified in component's props), Vue will treat it as Non-Prop Attribute. This means it take all such event listeners and non-prop attributes and place it on the root node of the component (div in your case)
But luckily there is a way to tell Vue "Hey, don't place those automatically on root, I know where to put it"
Use inheritAttrs: false option on your component
Put all non-prop attributes (including the event listeners) on the element you want - input in this case - using v-bind="$attrs"
Now you can even remove some props - for example placeholder (if you want), because if you use it directly on your component, Vue place it on input, which is what you want...
Also handling #change event is not optimal - you component allows to specify a type and different input types has different events. Nice trick around it is not to pass value and bind event explicitly, but instead use v-model with computed (see example below)
const app = Vue.createApp({
data() {
return {
text: ""
}
},
methods: {
onBlur() {
console.log("Blur!")
}
}
})
app.component('custom-input', {
inheritAttrs: false,
props: ["modelValue", 'name', 'type'],
emits: ['update:modelValue'],
computed: {
model: {
get() { return this.modelValue },
set(newValue) { this.$emit('update:modelValue', newValue) }
}
},
template: `
<div class="mt-2">
<label :for="name" class="h5">{{ name }}:</label>
<input
:type="type"
v-model="model"
:id="name"
v-bind="$attrs"
/>
</div>
`
})
app.mount("#app")
<script src="https://unpkg.com/vue#3.2.19/dist/vue.global.js"></script>
<div id='app'>
<custom-input type="text" name="email" v-model="text" placeholder="Type something..." #blur="onBlur"></custom-input>
<pre>{{ text }}</pre>
</div>

v-show not working with props

I am trying to hide or show button using props.
Here is the code
View (Blade)
<product-form-component savebutton="false" updatebutton="false"></product-form-component>
Component template
<template>
<div class="form-actions text-right col-md-12">
<button v-show="showsavebutton" class="btn btn-primary">Save</button>
<button v-show="updatemode && showupdatebutton" class="btn btn- primary">Update</button>
</div>
</template>
Javascript
export default {
props: ['showupdatebutton', 'showsavebutton', 'modalid']
}
Two points:
The props you are passing don't work the way you think they do; and
You have to create data variables (or props) in the component with the names you are using in the v-show.
Passing props
When you pass like:
<product-form-component savebutton="false" updatebutton="false"></product-form-component>
inside the component, the savebutton and updatebutton properties will be strings. In the example above, they won't be the boolean false, they will be the string "false".
To bind them to different values, use v-bind:propname or its shorthand :propname:
<product-form-component :savebutton="false" :updatebutton="false"></product-form-component>
That way, inside the component, those properties will really have the value false.
Variables inside component and v-show
The variables you use in the v-shows:
<button v-show="showsavebutton" ...
<button v-show="updatemode && showupdatebutton" ...
Don't exist in your component. You have to create data variables (or props) in the component with the names you are using in the v-show.
Considering you already have some props declared, here's an example of declaring those v-show variables in the data() using the props as initial value:
Vue.component('product-form-component', {
template: "#pfc",
props: ['updatebutton', 'savebutton', 'modalid'],
data() {
return {
updatemode: this.updatebutton, // initialized using props
showupdatebutton: this.updatebutton,
showsavebutton: this.savebutton
}
}
})
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!'
}
})
<script src="https://unpkg.com/vue"></script>
<template id="pfc">
<div class="form-actions text-right col-md-12">
<button v-show="showsavebutton" class="btn btn-primary">Save</button>
<button v-show="updatemode && showupdatebutton" class="btn btn- primary">Update</button>
</div>
</template>
<div id="app">
<p>{{ message }}</p>
<product-form-component :savebutton="true" :updatebutton="true"></product-form-component>
</div>
Props as passed down to child with the bind syntax :, so in your case you forgot to add it:
try:
<product-form-component :savebutton="false" :updatebutton="false"></product-form-component>