I am using vuelidate to validate my form input and display the error messages using vuetifyjs. I managed to do the basic object validation and am able to show the error messages.
However I'm having issues with displaying the error messages when I validate a collection.
ISSUE
Example data structure:
contact: {
websites: [
{
url: 'http://www.something.com',
label: 'Website',
}
]
}
Example validation:
validations: {
websites: {
$each: {
url: {
url,
}
}
},
}
Example template:
<template v-for="(website, index) in websites">
<v-layout row :key="`website${index}`">
<v-flex xs12 sm9 class="pr-3">
<v-text-field
label="Website"
:value="website.url"
#input="$v.websites.$touch()"
#blur="$v.websites.$touch()"
:error-messages="websiteErrors"
></v-text-field>
</v-flex>
</v-layout>
</template>
Example computed error message:
websiteErrors() {
console.log('websites',this.$v.websites) // contains $each
const errors = []
if (!this.$v.websites.$dirty) {
return errors
}
// Issue is that all of them show must be valid, even if they are valid.
// Validation is basically broken.
// I also tried this.$v.websites.$each.url
!this.$v.websites.url && errors.push('Must be valid url')
return errors
},
Example method (Update, also tried method with passing index):
websiteErrors(index) {
console.log('this.$v.entity.websites', this.$v.entity.websites.$each.$iter, this.$v.entity.websites.$each.$iter[index], this.$v.entity.websites.minLength, this.$v.entity.websites.$each.$iter[index].url)
const errors = []
if (!this.$v.entity.websites.$dirty) {
return errors
}
!this.$v.entity.websites.$each.$iter[index].url && errors.push('Must be valid url')
return errors
},
However when I do this, it will always be true and therefore never show the error.
EXPECTED
I would like to have the same example working as seen in vuelidate sub-collection validation The difference is instead of looping in the template I would like to generate the message programmatically.
REFERENCE
Example provided by vuelidate:
import { required, minLength } from 'vuelidate/lib/validators'
export default {
data() {
return {
people: [
{
name: 'John'
},
{
name: ''
}
]
}
},
validations: {
people: {
required,
minLength: minLength(3),
$each: {
name: {
required,
minLength: minLength(2)
}
}
}
}
}
<div>
<div v-for="(v, index) in $v.people.$each.$iter">
<div class="form-group" :class="{ 'form-group--error': v.$error }">
<label class="form__label">Name for {{ index }}</label>
<input class="form__input" v-model.trim="v.name.$model"/>
</div>
<div class="error" v-if="!v.name.required">Name is required.</div>
<div class="error" v-if="!v.name.minLength">Name must have at least {{ v.name.$params.minLength.min }} letters.</div>
</div>
<div>
<button class="button" #click="people.push({name: ''})">Add</button>
<button class="button" #click="people.pop()">Remove</button>
</div>
<div class="form-group" :class="{ 'form-group--error': $v.people.$error }"></div>
<div class="error" v-if="!$v.people.minLength">List must have at least {{ $v.people.$params.minLength.min }} elements.</div>
<div class="error" v-else-if="!$v.people.required">List must not be empty.</div>
<div class="error" v-else-if="$v.people.$error">List is invalid.</div>
<button class="button" #click="$v.people.$touch">$touch</button>
<button class="button" #click="$v.people.$reset">$reset</button>
<tree-view :data="$v.people" :options="{rootObjectKey: '$v.people', maxDepth: 2}"></tree-view>
</div>
WHAT WENT WRONG
Shared computed property which causes the issue where all siblings share the same error message. (Solved by writing it inline)
Reactivity not triggered due to the array not being updated in a "reactive way" (Make sure to take note of Change Detection Caveats in this case instead of updating the index: I copy the array, replace item and then set the whole array.)
Wrong place to use vuelidate $each.$iter: Moved it from computed error message to v-for
SOLUTION
This is how to do it (Fixes 1 & 3):
<template v-for="(v, index) in $v.websites.$each.$iter">
<v-layout row :key="`website${index}`">
<v-flex xs12 sm9 class="pr-3">
<v-text-field
label="Website"
:value="v.$model.url"
#input="$v.websites.$touch()"
#blur="$v.websites.$touch()"
:error-messages="v.$dirty && !v.required ? ['This field is required'] : !v.url ? ['Must be a valid url'] : []"
/>
</v-flex>
</v-layout>
</template>
This is how my update method is now (Fixes 2):
updateWebsite(index, $event) {
const websites = [...this.websites];
websites[index] = $event;
this.updateVuex(`websites`, websites)
this.$v.websites.$touch()
},
Originally it was like this:
updateWebsite(index, $event) {
this.updateVuex(`websites[${index}]`, $event)
this.$v.websites.$touch()
},
ALTERNATIVE
There is another option, which is to wrap in this case website inside a component. That way you can keep the computed error message as it will not be shared.
Related
I created a vuejs custom input that I wanted to use to dynamically display inputs by using props within the custom input. I haven't shown them here because it would be too long.
By clicking on the submit button, which is also part of the custom input, I wanna be able to get the values of each input, but for some reason, I have only been able to get the value of the last input.
What am I doing wrong?
Custom input:
<template>
<div class="form-input">
<label :label="label" :for="name" v-if="label && type !='submit' ">{{label}} <span v-if="required">*</span></label>
<a v-if="multiple" href="#" class="btn">Upload</a>
<input v-model="inputVal" :multiple="multiple" v-if="type != 'textarea' && type != 'submit'" class="form-control" :required="required" :class="classes" :type="type" :name="name" :placeholder="placeHolder">
<textarea v-model="inputVal" :multiple="multiple" v-else-if="type != 'submit'" class="form-control" :required="required" :class="classes" :type="type" :name="name" :placeholder="placeHolder"></textarea>
<button :multiple="multiple" :name="name" v-else type="submit">{{label}}</button>
</div>
</template>
<script>
export default {
name: "Input",
data () {
return {
inputVal: null
}
},
watch: {
inputVal: {
handler: function(newValue, oldValue) {
this.$emit('input', newValue);
},
deep: true,
}
}
}
</script>
Form where custom input is used:
<template>
<div class="form container">
<form v-on:submit.prevent="sendMail" method="post" class="d-flex row shadow bg-dark border-right border-dark">
<h3 class="col-12">Contact me</h3>
<Input v-model="formInput" v-for="input in inputs" v-bind:key="input.name" :label="input.label" :multiple="input.multiple" :type="input.type" :name="input.name" :class="input.classes" :required="input.required"></Input>
</form>
</div>
</template>
<script>
import Input from "../components/Input";
export default {
name: "Contact",
components: {Input},
data() {
return {
formInput: null,
}
},
methods: {
sendMail () {
console.log(this.formInput);
}
}
}
</script>
The issue I see in your code is, you are using only one variable "formInput" ( in case of Contact component ) and "inputVal" ( in case of Input component ) but you have number of input fields from where you need data right.
The simplest way to deal with these kind of cases is to create a datastructure and loop through that.
For eg.
// Contact component ( i am making it simple to make you understand the scenario )
<template>
<div class="form container">
<form v-on:submit.prevent="sendMail" method="post" class="d-flex row shadow bg-dark border-right border-dark">
<h3 class="col-12">Contact me</h3>
<!-- we are looping through our data structure and binding each inputVal to this input -->
<input v-for="(input, i) in formInputs" :key="i" v-model="input.inputVal">
</form>
</div>
</template>
<script>
import Input from "../components/Input";
export default {
name: "Contact",
components: {Input},
data() {
return {
formInputs: [
{inputVal: ''},
{inputVal: ''},
{inputVal: ''},
],
}
},
methods: {
sendMail () {
// You can extract the data from formInputs as per your need
}
}
}
</script>
I have a list of nested comments. Under each comment, I'd like to add a "reply" button that, when click, show a reply form.
For now, everytime I click a "reply" button, it shows the form. But the thing is, I'd like to show only one form on the whole page. So basically, when I click on "reply" it should close the other form alreay opened and open a new one under the right comment.
Edit :
So I was able to make some slight progress. Now I'm able to only have one active form opening on each level of depth in the nested loop. Obviously, what I'm trying to do now is to only have one at all.
What I did was emitting an event from the child component and handle everything in the parent component. The thing is, it would work great in a non-nested comment list but not so much in my case...
Here is the new code:
In the parentComponent, I have a handleSelected method as such:
handleSelected (id) {
if(this.selectedItem === id)
this.selectedItem = null;
else
this.selectedItem = id;
},
And my childComponent:
<template>
<div v-if="comment">
<div v-bind:style=" iAmSelected ? 'background: red;' : 'background: none;' ">
<p>{{ comment.author.name }}<br />{{ comment.created_at }}</p>
<p>{{ comment.content }}</p>
<button class="button" #click="toggle(comment.id)">Répondre</button>
<button class="button" #click="remove(comment.id)">Supprimer</button>
<div v-show="iAmSelected">
<form #submit.prevent="submit">
<div class="form-group">
<label for="comment">Votre réponse</label>
<textarea class="form-control" name="comment" id="comment" rows="5" v-model="fields.comment"></textarea>
<div v-if="errors && errors.comment" class="text-danger">{{ errors.comment[0] }}</div>
</div>
<button type="submit" class="btn btn-primary">Envoyer</button>
<div v-if="success" class="alert alert-success mt-3">
Votre réponse a bien été envoyée !
</div>
</form>
</div>
</div>
<div v-if="comment.hasReply">
<div style="margin-left: 30px;">
<comment v-for="comment in comments"
:key="comment.id"
:comment="comment" #remove-comment="remove"
:is-selected="selectedItem" #selected="handleSelected($event)">
</comment>
</div>
</div>
</div>
</template>
<script>
import comment from './CommentItem'
export default {
name: 'comment',
props: {
isSelected: Number,
comment: {
required: true,
type: Object,
}
},
data () {
return {
comments: null,
fields: {},
errors: {},
success: false,
loaded: true,
selectedItem: null,
}
},
computed: {
iAmSelected () {
return this.isSelected === this.comment.id;
}
},
methods: {
remove(id) {
this.$emit('remove-comment', id)
},
toggle(id) {
this.$emit('selected', id);
},
handleSelected(id) {
if(this.selectedItem === id)
this.selectedItem = null;
else
this.selectedItem = id;
},
},
mounted(){
if (this.comment.hasReply) {
axios.get('/comment/replies/' + this.comment.id)
.then(response => {
this.comments = response.data
})
}
}
}
</script>
Thanks in advance for your help!
My page exist of a table where I can add new rows. If you want to add a new row a pop-up window appear where the new values can be added.
This new data is then saved to the database after submitting. If I again want to add a new row the input fields, they should be cleared.
The method I use, is working but isn't very clear.
Note: My code shows only a part of the input fields, to make it more clear. My pop-up window actually contains 20 input fields.
I would like to clear them all at once instead of clearing them one by one (like I am doing now).
Because I am already doing this for defining the v-model, pushing the new data to the database directly on the page and via post axios request.
Is there a cleaner way to do this?
Thanks for any input you could give me.
This is my code:
html part
<div class="col-2 md-2">
<button class="btn btn-success btn-sx" #click="showModal('add')">Add New</button>
<b-modal :ref="'add'" hide-footer title="Add new" size="lg">
<div class="row" >
<div class="col-4">
<b-form-group label="Category">
<b-form-input type="text" v-model="newCategory"></b-form-input>
</b-form-group>
</div>
<div class="col-4">
<b-form-group label="Name">
<b-form-input type="text" v-model="newName" placeholder="cd4"></b-form-input>
</b-form-group>
</div>
<div class="col-4">
<b-form-group label="Amount">
<b-form-input type="number" v-model="newAmount" ></b-form-input>
</b-form-group>
</div>
</div>
<div class="row" >
<div class="col-8">
</div>
<div class="col-4">
<div class="mt-2">
<b-button #click="hideModal('add')">Close</b-button>
<b-button #click="storeAntibody(antibodies.item)" variant="success">Save New Antibody</b-button>
</div>
</div>
</div>
</b-modal>
</div>
js part
<script>
import { async } from 'q';
export default {
props: ['speciedata'],
data() {
return {
species: this.speciedata,
newCategory: '',
newName: '',
newAmount:'',
}
},
computed: {
},
mounted () {
},
methods: {
showModal: function() {
this.$refs["add"].show()
},
hideModal: function(id, expId) {
this.$refs['add'].hide()
},
addRow: function(){
this.species.push({
category: this.newCategory,
name: this.newName,
amount: this.newAmount,
})
},
storeSpecie: async function() {
axios.post('/specie/store', {
category: this.newCategory,
name: this.newName,
amount: this.newAmount,
})
.then(this.addRow())
// Clear input
.then(
this.newName = '',
this.newCategory = '',
this.newAmount = '',
)
.then(this.hideModal('add'))
},
}
}
</script>
in your data of vuejs app , you have to set one object for displaying modal data like modalData then to reset data you can create one function and set default value by checking type of value using loop through modalData object keys
var app = new Vue({
el: '#app',
data: {
message:"Hi there",
modalData:{
key1:"value1",
key2:"value2",
key3:"value3",
key4:5,
key5:true,
key6:"val6"
}
},
methods: {
resetModalData: function(){
let stringDefault="";
let numberDefault=0;
let booleanDefault=false;
Object.keys(this.modalData).forEach(key => {
if(typeof(this.modalData[key])==="number"){
this.modalData[key]=numberDefault;
}else if(typeof(this.modalData[key])==="boolean") {
this.modalData[key]=booleanDefault;
}else{
// default type string
this.modalData[key]=stringDefault;
}
});
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
{{modalData}}
<br/>
<button #click="resetModalData">Reset Modal Data</button>
</div>
update : in your case :
data:{
species: this.speciedata,
modalData:{
newCategory: '',
newName: '',
newAmount:''
}
},
and after storing data :
storeSpecie: async function() {
axios.post('/specie/store', {
category: this.newCategory,
name: this.newName,
amount: this.newAmount,
})
.then(()=>{
this.addRow();
this.resetModalData();
this.hideModal('add')
}
},
In native Javascript you get the reset() method.
Here is how it is used :
document.getElementById("myForm").reset();
It will clear every input in the form.
I am using form control to apply validation in material angular 6 application.
Below is the code of validator :-
paymentOffBankName: new FormControl('', Validators.compose([
Validators.required,
Validators.maxLength(this.responseMap.get('ev_payment_t.bank_name').values.maxSize),
Validators.minLength(this.responseMap.get('ev_payment_t.bank_name').values.minSize),
Validators.pattern(this.responseMap.get('ev_payment_t.bank_name').values.validationExp)
])),
I have one method to display the message related validator :-
paymentOffBankName': [
{ type: 'required', message: 'Required') },
{ type: 'pattern', message: 'Invalid Name' },
{ type: 'minlength', message: 'Requires atleast 3 letters'
},
],
Here is my html which shows the error :-
<mat-form-field fxFlex="{{responseMap.get('ev_payment_t.bank_name').values.maxSize}}">
<input required matInput placeholder="{{responseMap.get('ev_payment_t.bank_name').values.label}}"
formControlName="paymentOffBankName" maxlength="{{responseMap.get('ev_payment_t.bank_name').values.maxSize}}">
<mat-error *ngFor="let validation of validationMessages.paymentOffBankName">
<mat-error class="error-message" *ngIf="offlinePaymentService.OfflinePayment_form.get('paymentOffBankName').hasError(validation.type) && (offlinePaymentService.OfflinePayment_form.get('paymentOffBankName').dirty || offlinePaymentService.OfflinePayment_form.get('paymentOffBankName').touched)">{{validation.message}}</mat-error>
</mat-error>
</mat-form-field>
My Problem is, If I put invalid name with 2 letters, then it shows two validation messages.
Expectation: It should show only one message, I will put one common message like 'Required, MinLengh-3, Alpha Numeric only'
To show only one validation message at a time, try to use messageKey in which you can pass composeMessageKey which contains all the validation messages of the field.
Declare the composeMessageKey in app component
export class AppComponent implements OnInit {
ngOnInit(){
ReactiveFormConfig.set({"validationMessage":{
"composeMessageKey":"please enter valid input"
}});
}
}
and in component ts:
ngOnInit() {
this.userFormGroup = this.formBuilder.group({
paymentOffBankName:['', RxwebValidators.compose(
{validators:[
RxwebValidators.alpha(),
RxwebValidators.maxLength({value:3}),
RxwebValidators.minLength({value:4}),
],messageKey:"composeMessageKey",
})
]
});
}
For that i have used validators of #rxweb validators(RxwebValidators) and passed the MessageKey
Html :
<div>
<form [formGroup]="userFormGroup">
<div class="form-group">
<label>Payment Off BankName</label>
<input type="text" formControlName="paymentOffBankName" class="form-control" />
</div>
<small class="form-text text-danger" *ngIf="userFormGroup.controls.paymentOffBankName.errors">{{userFormGroup.controls.paymentOffBankName.errors.composeMessageKey?.message}}<br/></small>
<button [disabled]="!userFormGroup.valid" class="btn btn-primary">Submit</button>
</form>
</div>
Here is the stackblitz example : Stackblitz
In my Meteor app i have the TAPi18n package and aldeed:autoform. Im trying to make a form validation using i18n for the placeholder field but i don't know how to do it.
So, I was wondering if it is possible to pass a dynamic parameter (that's how I call it) to a template, using Spacebars. I mean, without using helpers in the JS files, something like this:
<template name="Publish">
<div class="col-md-8 col-lg-8 col-md-offset-2 col-lg-offset-2 well">
{{#autoForm collection="Publications" id="insertPublicationForm" type="insert"}}
<fieldset>
<legend>{{_ "publish_title"}}</legend>
<div class="col-md-12 col-lg-12">
<div class="form-group">
{{> afFieldInput name='name' placeholder={{_ "name"}} }}
</div>
<div class="form-group">
{{> afFieldInput name='description' placeholder={{_ "description"}} }}
</div>
<div class="form-group">
{{> afFieldInput name='price' placeholder={{_ "price"}} }}
</div>
</div>
<div class="form-group">
<button type="submit" id="add_publication" class="btn btn-success center-block">{{_ "publish"}}</button>
</div>
</fieldset>
{{/autoForm}}
</div>
</template>
I could register a helper for every translation but i don't like too much the idea.
I also know that i could use the label field in the SimpleSchema, like this:
Meteor.startup(function() {
Publications.attachSchema(new SimpleSchema({
name: {
type: String,
label: TAPi18n.__("name"),
max: 200
},
description: {
type: String,
label: TAPi18n.__("description")
},
price: {
type: Number,
label: TAPi18n.__("price"),
min: 0
}
}));
});
And then use the afQuickField template instead of afFieldInput.
But i don't want to use a label, i want to use the placeholder of the input.
There is any way to do this?
Well, i don't know why i didn't see it before, but i can do this in the SimpleSchema:
Meteor.startup(function() {
Publications.attachSchema(new SimpleSchema({
name: {
type: String,
label: TAPi18n.__("name"),
max: 200,
autoform: {
afFieldInput: {
placeholder: TAPi18n.__("name")
}
}
},
description: {
type: String,
autoform: {
afFieldInput: {
placeholder: TAPi18n.__("description")
}
}
},
price: {
type: Number,
min: 0,
autoform: {
afFieldInput: {
placeholder: TAPi18n.__("price")
}
}
}
}));
});
That way i can use i18n in the placeholder without making tons of helpers.
Sorry if I made someone waste time on this.