Vee validate useForm with nested data - vue.js

I have an object with some nested properties that I would like to bind to a form in vee validate using the composition api. This is what I'm doing right now but it feels pretty verbose. Can I utilize useForm to construct the data model and bind it to the UI?
This is what I have now:
<input type="text" v-model="field1" />
<input type="text" v-model="field2" />
<input type="text" v-model="field3" />
interface MyForm {
field1: string;
nested: { field2: string; field3: string; }
}
useForm({
validationSchema: { /* rules */ }
})
const { value: field1 } = useField<string>('field1')
const { value: field2 } = useField<string>('field2')
const { value: field3 } = useField<string>('field3')
// before sent to backend, the form needs to be reconstructed:
const form = computed<MyForm>(() => ({ field1, nested: { field2, field3 } }))
return { field1, field2, field3 }
What I'm after is something like:
<input type="text" v-model="field1" />
<input type="text" v-model="nested.field2" />
<input type="text" v-model="nested.field3" />
interface MyForm {
field1: string;
nested: { field2: string; field3: string; }
}
const {values} = useForm({
initialValues: { field1: '', nested: { field2: '', field3: '' } }
validationSchema: { /* rules */ }
})
return { ...toRefs(values) }
But according to the specification, values shouldn't be mutated. Do I have any options to my approach? I would like the complete form to be exposed to the UI and available as a Ref that I can pass along to my backend.

Related

Validating Fields of an Object in a Form - Vue 3 + Vee-validate + Yup

I have a form where certain fields are inside an object:
<script setup lang="ts">
...
const schema: yup.SchemaOf<any> =
yup.object({
group: yup.string().nullable().required(),
description: yup.string().nullable().required(),
user: yup
.object({
first_name: yup.string().required(),
last_name: yup.string().required(),
})
.required(),
})
const { errors, handleSubmit, isSubmitting } = useForm({
validationSchema: schema,
initialValues: {
group: '',
description: '',
user: {}
},
});
const { value: group } = useField<string>('group');
const { value: description } = useField<string>('description');
const { value: user } = useField<any>('user');
const isValid = useIsFormValid();
...
</script>
<template>
<div>
<label for="group">Group:</label
><input id="group" v-model="group" type="text" />
</div>
<div>
<label for="description">Description:</label
><input id="description" v-model="description" type="text" />
</div>
<div>
<label for="first-name">First name</label
><input id="first-name" v-model="user.first_name" type="text" />
</div>
<div>
<label for="last-name">Last name</label
><input id="last-name" v-model="user.last_name" type="text" />
</div>
<button :disabled="!isValid">Save</button>
...
</template>
But the data validation of this object is only done after changing a field outside of it, i.e. fill in group, description, first_name, last_name (in this same order) and the form will NOT be considered valid, only if you edit group or description again.
How can I make the validation be done when I change the field myself?
Here is the link to the complete code.
I am using the following versions:
"vue":"^3.2.37",
"vee-validate": "^4.5.11",
"yup": "^0.32.11"
When you use useField() with an Object, the nested properties lose their reactivity connection. So here are two options to resolve this: wrap useField with reactive() or use useField() for each nested property separately.
option 1
const { value: user } =reactive(useField('user'));
option 2
const { value: first_name } = useField('user.first_name');
const { value: last_name } = useField('user.last_name');
here is a working example here

Clearing Vue JS v-for Select Field

I have a simple application that uses a v-for in a select statement that generates two select tags. The groupedSKUAttributes variable that creates the select statement looks like this:
groupedSKUAttributes = {colour: [{id: 1, name: 'colour', value: 'red'},
{id: 2, name: 'colour', value: 'blue'}],
size: [{id: 3, name: 'size', value: '40'},
{id: 4, name: 'size', value: '42'}]}
I also have a button that I want to clear the select fields. How do I get the clear method to make each of the select fields choose their default <option value='null' selected>select a {{ attributeName }}</option> value? I can't figure out if I'm meant to use a v-model here for the groupedSKUAttributes. Any advice would be appreciated.
The template looks like this:
<template>
<div>
<select
v-for='(attribute, attributeName) in groupedSKUAttributes'
:key='attribute'
#change='update(attributeName, $event.target.value)'>
<option value='null' selected>select a {{ attributeName }}</option>
<option
v-for='a in attribute'
:value='a.id'
:label='a.value'
:key='a.id'>
</option>
</select>
</div>
<button #click='clear'>clear</button>
</template>
And the JS script looks like this:
<script>
export default {
name: 'app',
data() {
return {
groupedSKUAttributes: null,
}
},
methods: {
clear() {
console.log('clear');
},
update(attributeName, attributeValue) {
console.log(attributeName, attributeValue);
},
getSKUAttributes() {
API
.get('/sku_attribute/get')
.then((res) => {
this.skuAttributes = res.data;
this.groupedSKUAttributes = this.groupBy(this.skuAttributes, 'name');
})
.catch((error) => {
console.error(error);
});
},
},
created() {
this.getSKUAttributes();
}
}
</script>
The v-model directive works within the v-for without any issues.
<script>
export default {
name: 'app',
data() {
return {
groupedSKUAttributes: null,
selected: {}
}
},
methods: {
clear() {
this.generateDefaultSelected(this.generateDefaultSelected);
},
update(attributeName, attributeValue) {
this.selected[attributeName] = attributeValue;
},
getSKUAttributes() {
API
.get('/sku_attribute/get')
.then((res) => {
this.skuAttributes = res.data;
this.groupedSKUAttributes = this.groupBy(this.skuAttributes, 'name');
// Call this method to reset v-model
this.generateDefaultSelected(this.groupedSKUAttributes);
})
.catch((error) => {
console.error(error);
});
},
generateDefaultSelected(groupedSKUAttributes) {
// Reset the object that maintains the v-model reference;
this.selected = {};
Object.keys(groupedSKUAttributes).forEach((name) => {
// Or, set it to the default value, you need to select
this.selected[name] = '';
});
}
},
created() {
this.getSKUAttributes();
}
}
</script>
In the above code, generateDefaultSelected method resets the selected object that maintains the v-model for all your selects.
In the template, you can use v-model or unidirectional value/#change pair:
<!-- Using v-model -->
<select
v-for='(attribute, attributeName) in groupedSKUAttributes'
:key='attributeName' v-model="selected[attributeName]">
<!-- Unidirection flow without v-model -->
<select
v-for='(attribute, attributeName) in groupedSKUAttributes'
:key='attributeName' :value="selected[attributeName]"
#change='update(attributeName, $event.target.value)'>

Disable a button if an input field is bigger than another one

I am trying to check if an input A is bigger than input B and if this happens disable the button and if it's not enable
Html:
<input v-model="form.a" />
<input v-model="form.b" />
<button :class="{disabled: btnDisabled}">Enviar</button>
VueJs:
<script>
import { required, minLength } from 'vuelidate/lib/validators';
export default {
created() {
},
data: function() {
return {
btnDisabled: false,
form: {
a: '',
b: ''
}
}
},
methods: {
checkEndBillNumber() {
if(this.form.a > this.form.b) {
// I do not know what I should put here
}
else {
// I do not know what I should put here
}
}
}
}
</script>
If you see I do not know what I should and in the vuejs conditional to disable the button if the conditional is true or false.
How can I do that? Thanks!
disabled attribute in buttons get true or false so you can do something like this:
<input v-model="form.a" />
<input v-model="form.b" />
<button :disabled="isDisabled">Enviar</button>
computed: {
isDisabled() {
const result = this.form.a > this.form.b ? true : false;
return result;
}
}
or if you want to add a class you can do this:
<button :class="{ 'yourClassName': isDisabled }">Enviar</button>

Two way data binding with :model.sync when prop has get() and set()

I have a computed property that I use as v-model on an input. I've written it this way to get reactivity -- this calls my setText Vuex action which I then can get with my getter text. It looks like this:
text: {
get() {
return this.text;
},
set(value) {
this.setText(value);
},
},
and I use it in my input like this:
<input class="input" type="text" v-model="text" />
This works well. Now, I've put the input in question into a separate component which I use. This means I have to pass the text v-model as props, which I do with :model.sync, like so:
<myInput :model.sync="text"/>
and in the myInput component I use the props like so:
<input class="input" id="search-order" type="text" :value="model" #input="$emit('update:model', $event)">
But this doesn't seem to work at all, whenever I type into the input, the input says: [object InputEvent] and if I try to see and the value of model it's {isTrusted: true}. I'm assuming it's because of the getters and setters I have on my computed property. How do I pass these down to the child component?
Instead of using the .sync modifier you can support the v-model directive in your custom component. v-model is syntax sugar for a value prop and an input event.
To support v-model just make sure your custom component has a value prop and emits an input event with the new value: this.$emit('input', event.target.value).
Here is an example of a <BaseInput> component I use, it's written in TypeScript:
<template>
<input
:type="type"
:value="value"
class="input"
v-on="listeners"
>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'BaseInput',
props: {
type: {
type: String,
default: 'text',
},
value: {
type: [String, Number],
default: '',
},
lazy: {
type: Boolean,
default: false,
},
number: {
type: Boolean,
default: false,
},
trim: {
type: Boolean,
default: false,
},
},
computed: {
modelEvent(): string {
return this.lazy ? 'change' : 'input'
},
parseModel(): (value: string) => string | number {
return (value: string) => {
if (this.type === 'number' || this.number) {
const res = Number.parseFloat(value)
// If the value cannot be parsed with parseFloat(),
// then the original value is returned.
return Number.isNaN(res) ? value : res
} else if (this.trim) {
return value.trim()
}
return value
}
},
listeners(): Record<string, Function | Function[]> {
return {
...this.$listeners,
[this.modelEvent]: (event: HTMLElementEvent<HTMLInputElement>) =>
this.$emit(this.modelEvent, this.parseModel(event.target.value)),
}
},
})
</script>
You can use it like so:
<BaseInput v-model="text" />

vuelidate async validator - how to debounce?

So I have an issue with async validator on my email/user form element. Every time a letter is typed in, it checks for validation. If the email is 30chars, then that is over 30 calls! Anyone know the best way to debounce a vuelidate custom validator? When I try to use debounce, i get all sorts of errors from vuelidate since it's expecting a response.
<div class="form-group">
<label for="emailAddress">Email address</label>
<input type="email" class="form-control col-md-6 col-sm-12" v-bind:class="{ 'is-invalid': $v.user.email.$error }" id="emailAddress" v-model.trim="$v.user.email.$model" #change="delayTouch($v.user.email)" aria-describedby="emailHelp" placeholder="email#example.com">
<small v-if="!$v.user.email.$error" id="emailHelp" class="form-text text-muted">We'll never share your email with anyone.</small>
<div class="error invalid-feedback" v-if="!$v.user.email.required">Email address is required.</div>
<div class="error invalid-feedback" v-if="!$v.user.email.email">This is not a valid email address.</div>
<div class="error invalid-feedback" v-if="!$v.user.email.uniqueEmail">This email already has an account.</div>
</div>
<script>
import { required, sameAs, minLength, maxLength, email } from 'vuelidate/lib/validators'
import webapi from '../services/WebApiService'
import api from '../services/ApiService'
const touchMap = new WeakMap();
const uniqueEmail = async (value) => {
console.log(value);
if (value === '') return true;
return await api.get('/user/checkEmail/'+value)
.then((response) => {
console.log(response.data);
if (response.data.success !== undefined) {
console.log("Email already has an account");
return false;
}
return true;
});
}
export default {
name: "Register",
data() {
return {
errorMsg: '',
showError: false,
user: {
firstName: '',
lastName: '',
email: '',
password: '',
password2: ''
}
}
},
validations: {
user: {
firstName: {
required,
maxLength: maxLength(64)
},
lastName: {
required,
maxLength: maxLength(64)
},
email: {
required,
email,
uniqueEmail //Custom async validator
},
password: {
required,
minLength: minLength(6)
},
password2: {
sameAsPassword: sameAs('password')
}
}
},
methods: {
onSubmit(user) {
console.log(user);
/*deleted*/
},
delayTouch($v) {
console.log($v);
$v.$reset()
if (touchMap.has($v)) {
clearTimeout(touchMap.get($v))
}
touchMap.set($v, setTimeout($v.$touch, 1250))
}
}
}
</script>
When I try to wrap my async function with the debounce, vuelidate does not like it, so I removed it. Not sure how to limit the custom "uniqueEmail" validator.
as 'Teddy Markov' said, you could call $v.yourValidation.$touch() on your input blur event. I think it's more efficient way to use Vuelidate.
<input type="email" class="form-control col-md-6 col-sm-12"
:class="{ 'is-invalid': !$v.user.email.$anyError }"
id="emailAddress" v-model.trim="$v.user.email.$model"
#change="delayTouch($v.user.email)"
#blur="$v.user.email.$touch()"
aria-describedby="emailHelp"
placeholder="email#example.com"
>
I found the best way was to combine with regex for emails
if regex test passes then proceed to check if unique - else skip the check completely.