Vee-validate: Can't validate form with multiple child components

Created on 26 Oct 2016  路  26Comments  路  Source: logaretm/vee-validate

Hi,

In our app we have some fairly complex dynamic forms that have been broken down into multiple sub components and can be nested a few levels deep. We also currently using vuex too.

I don't seem to be able to get all the components to validate when validateAll is called at the parent level. After having a look at the source code it looks like this only emits and listens on the same component.

Is this something that we should currently be able to do or something that might be looked at in the future?

Regards
James

Most helpful comment

Found a nice way for myself. Maybe it will help somebody. Works with parent-chidren references.

The code below is from a save() component method. Vee-Validate using globally.

let promises = []
for (let child in this.$refs) promises.push(this.$refs[child].$validator.validateAll());

Promise.all(promises)
      .then(this.$validator.validateAll())
      .then(
         () => {//OK CONTINUE SAVING},
         () => {//NOT OK})

@logaretm I am afraid that I absolutely not professional but I would add this snippet in a method sounds something like validateAllWithRefs() in my lib. If you OKay with it I think I can do my first Pull-request))

All 26 comments

Well in Vue 2.0 the component can only communicate via the old API by $emit, both $broadcast and $dispatch were deprecated, so I don't think It will be included in the plugin since there is no distinction between a form component with multiple children acting as custom inputs and say a layout with some inputs in it.

But I think you can do it in a couple of ways, depending on Vue version:

Vue 1.0

In Vue 1.0 you can trigger the event on all child components, you just need to hook into the event and broadcast it again to the children.

ready() {
  this.$on('veeValidate', () => {
    this.$broadcast('veeValidate');
  });
}

// Triggering validateAll should now trigger validations for all child components.
this.$validator.validateAll();

Vue 2.0

You can use an empty Vue instance as an event bus:

// in bus.js
import Vue from 'vue';

const bus = new Vue();

export default bus;

// In your parent component
import bus from './bus';
mounted() {
  this.$on('veeValidate', () => {
    bus.$emit('veeValidate');
  });
}

// in your child components.
import bus from './bus';

mounted() {
  bus.$on('veeValidate', () => {
    this.$validator.validateAll());
  });
}

or you can go ahead and loop over the child components and trigger validateAll individually on each of them:

// in your parent component
// you might want to recursively loop over the instances.
this.$children.forEach(vm => {
  vm.$validator.validateAll();
});

I didn't test those suggestions, but I can build an example if you continue to have trouble solving this.

I would love to offer some better way built into the plugin, but until I figure a nice way to do it, I don't think it will be in time for full release tho.

Thanks for the response,

We are using Vue2 so I thought that an event bus would be the best approach to this. But I was unsure if it was something that might be included in the plugin.

I think we should be good to implement this our selves but it might be good to include something about this in the docs, maybe as an advanced example.

Thanks

Yea sure, I might add an example regarding this, thanks!

Thanks for the great plugin. little doubt. how am i supposed to check if there are any errors in child component this.errors.any() returns false currently. any workaround for this ?

@shakee93 I have just added an example Gist demonstrating the principle of how we passed the error bag bag to the parent element.

https://gist.github.com/sproogen/147d75db261505e8a558a7fd11a20551

@logaretm might have another suggestion on how this could be done though?

@sproogen that is pretty much how I would have handled it, yours are even cleaner. Sorry I couldn't create an example for this, thanks for the gist!

I had already done it, so I thought I would pull that out and stick it in the gist.

Feel free to modify it and use is as an example in the future.

Another option could be to have child components register themselves with the parent to be validated at the same time:

In the code below, App.event is a Vue object used as the event bus.

Parent Component:

...
mounted: function(){
    var self = this;

    this.childValidators = [];

    App.event.$on('child-validator-added', function(component){
        self.childValidators.push(component);
    });

    App.event.$on('validate', function(){
        self.$validator.validateAll();
        self.childValidators.forEach(function(component){
            component.$validator.validateAll();
            component.$validator.getErrors().errors.forEach(function(error){
                self.errors.add(error.field, error.msg);
            })
        })
    });

    $('#resource-form').submit(function(e){
        e.preventDefault();
        App.event.$emit('validate');
        if (self.errors.count()) {
            console.log('ERRORS')
        } else {
            console.log('SUCCESS')
        }
    });
},

Child Component:

...
mounted: function(){
    var self = this;
    this.$nextTick(function(){
        App.event.$emit('child-validator-added', this);
    });
}

@sproogen I have used your example, but I keep getting the error "TypeError: _vm.errors.has is not a function". It is properly not your example, but I cannot solve the error.

The example above doesn't directly call the method "has()" on errors. Is that somewhere in your code by chance? If so, what does it look like?

@daylightstudio I did not use your example, but the example of @sproogen.

https://gist.github.com/sproogen/147d75db261505e8a558a7fd11a20551

But I have a question for you. From where do you get App.Event? Is this a new Vue Instance?

//Rasmus

Yes, App.event is just a new Vue() instance used as the event bus. In my example, I have a global App that it attaches too but it would be different depending on your setup and where you place the event bus.

This is not really usefull... validateAll() is now a promise. So none of these samples really solve the issue.

@pimboden There are already multiple solutions to issues like this depending on your usage or needs, you just have to be consistent in your code.

for example if your custom components serve as a custom input with special behavior then you should use component validation which is supported, no need to transfer errors via events or anything.

if your components serve as organizing or grouping of some inputs, then you should use the events.

I'm considering adding a centralized error object which is optional for such cases, but it will require complex scoping, so I hope I will be able to have a general idea about it after releasing the new version.

@logaretm Thank you. But when you say "there are already multiple solutions to issues like this" , could you tell me where... I have goggled, and I only found 2 or 3 solutions, all working with validateAll() without using it as promise...
My app, uses a component (form-component) that has some input fields,

<template>
    <div>
      <input type = text -..... ></text>
       <vss-inputs...>      </vss-inputs>
    </div>
<template>`

Inside it uses a child-component, that only loops through some array, and depending on the values of the array, this child-component renders its own child-components: So my vss-inputs is something like this

<template v-for="(vss, index) in vssToRender">
    <template v-if="showCheckbox(vss)||showCheckboxRequired(vss)">
          <vss-subcomponent-1...>      </vss-subcomponent-1>
    </template>
    <template v-if="showCheckbox(vss)||showCheckboxRequired(vss)">
      <vss-subcomponent-2>      </vss-subcomponent-2>
    </template>
 </template>

I didn't' find out, how my "form-component" gets informed that all components in the hierarchy have finished validating.
My form. component triggers an event for validating.
All my sub-components, go through their validations.. (they correctly render the error messages)
The problem... how does my "form-component" get informed that all other components finished validating? And how does it know if some sub components didn't pass the validation?

Thanks for your help

I understand that those "solutions" aren't documented, but its hard to document every single solution that may not fit all projects, ideally the plugin should be easy enough for users to implement their own solutions for such problems.

having said that, your issue seems a little bit more complex than the others mentioned, but you can create a dedicated event bus for the errors, whenever a component gets created, it should register itself as an "error provider" meaning the parent knows of such a component.

You can then trigger validation across all registered components, by looping over their container (array) and triggering validateAll then you add the errors in the catch callback to the parent errors, using Promise.all and collecting all promises can tell you when all validations are finished validating.

I have not implemented such a thing, but this comes to my mind as a possible solution for your case.

Found a nice way for myself. Maybe it will help somebody. Works with parent-chidren references.

The code below is from a save() component method. Vee-Validate using globally.

let promises = []
for (let child in this.$refs) promises.push(this.$refs[child].$validator.validateAll());

Promise.all(promises)
      .then(this.$validator.validateAll())
      .then(
         () => {//OK CONTINUE SAVING},
         () => {//NOT OK})

@logaretm I am afraid that I absolutely not professional but I would add this snippet in a method sounds something like validateAllWithRefs() in my lib. If you OKay with it I think I can do my first Pull-request))

@prsth I have just tested this method at my side and it seems to work pretty well, I just need to add the ref property to my custom component. The ref value should be same as name I think?

@coderabsolute I guess you can name it in a way you would like to. It doesn't matter. It even could be an evaluated string like this

<template v-for="(item, n) in myarray">
   <child-component ref="`anyname${n}`"></child-component>
</template>

All the child components would be validated.

Thanks @prsth, I think it's one of the cleanest solution I found here to make validation working with a dynamic and very nested component. :+1:

This is what I am using right now:

const promises = [];
promises.push(this.$validator.validateAll());
// ref `widget-form` can be not rendered, so we resolve `false` by default
promises.push(this.$refs['widget-form'] ? this.$refs['widget-form'].$validator.validateAll() : Promise.resolve(false));

Promise.all(promises).then(validations => {
  // If one validation has failed, we stop here
  if (validations.some(validation => validation === false)) return;

  // Everything is valid
});

For me it works fine by adding inject: ['$validator'] in all nested components

```js
export default {
name: 'child-component',
inject: ['$validator'],
}

@ahmed-essawy Why nobody is reacting/commenting on this solution it is really the cleanest and most obvious so far. Is there any drawbacks in using it, or i am missing something?

@ahmed-essawy Why nobody is reacting/commenting on this solution it is really the cleanest and most obvious so far. Is there any drawbacks in using it, or i am missing something?

Thanks @Thrajnor,
It's working perfectly since I wrote the comment without issues till now

@ahmed-essawy Why nobody is reacting/commenting on this solution it is really the cleanest and most obvious so far. Is there any drawbacks in using it, or i am missing something?

Thanks @Thrajnor,
It's working perfectly since I wrote the comment without issues till now

@ahmed-essawy Hello, I do this, but no working for me, this is my code:

Code parent:

...
<v-form
          novalidate
          @submit.prevent="validateBeforeSubmit">
....
<v-flex md3>
                  <v-text-field
                    v-validate="'required'"
                    v-model="form.sigla"
                    :error-messages="errors.collect('Sigla')"
                    label="Sigla"
                    type="text"
                    data-vv-name="Sigla"
                    required
                  />
                </v-flex>
          <!-- child component -->
          <core-regra-financeira
              v-model="regra"
           />

          <v-btn
                :loading="loadingBtn"
                type="submit"
                color="primary"
              >Submit</v-btn>
...
</form>
...
methods: {
    validateBeforeSubmit () {
      this.$validator.validate()
        .then(result => {
          if (result) {
             // OK
          } else {
            // NOK
          }
        })

Code Child:

      ...

<v-flex
    md2
    sm6
    xs12>
    <v-text-field
      v-formata-moeda="value.limiteCreditoMesMaximo"
      v-money="money"
      v-validate="'required'"
      v-model.lazy="value.limiteCreditoMesMaximo"
      :disabled="disabled"
      :error-messages="errors.collect('Limite Cr茅dito M锚s M谩ximo')"
      data-vv-name="Limite Cr茅dito M锚s M谩ximo"
      required
      type="tel"
      suffix="R$"
      reverse
      label="Limite Cr茅dito M锚s M谩ximo"
    />
</v-flex>

export default {
  inject: ['$validator'],

...

This only works if the user changes the value of the form, otherwise the validation does not work.

Do you have an example?

Hello @robertoconceicao

try to use this.$validator.validateAll().then(); instead of this.$validator.validate().then();

if it still not working

try to use refs as below
<v-form @submit.prevent="validateBeforeSubmit" ref="exampleForm">.....</v-form>
then change validation method to be this.$refs.exampleForm.$validator.validateAll().then(); instead of this.$validator.validate().then();

Thanks @ahmed-essawy

I try the two solution, but not working still.

I resolved passing into child the method save of parent, it's working for me, in this case.

My solution:

Code Parent:

    <form>
        fields parent
        <!-- child component -->
        <core-regra-financeira
          v-model="regra"
          :disabled="disabled"
          :salve="validateBeforeSubmit" <!-- Here, I'm passing into method validate and save of Parent for child -->
          :go-back="goBack"
          :cancel="cancel"
        />
    </form>

Code Child:

  fields childs required
  ....

  export default {
  inject: ['$validator'],
  props: {
    value: {
      type: Object,
      required: true
    },
    save: {
      type: Function,
      required: true
    },
    go-back: {
      type: Function,
      required: true
    },
    cancel: {
      type: Function,
      required: true
    },
    // eslint-disable-next-line vue/require-default-prop
    disabled: false
  },
  methods: {
    validationChild () {
      this.$validator.validate()
        .then(result => {
          if (result) {
            this.save() // callback from Parent 
          } else {
            // NOK alert
          }
        })
    }
  }
Was this page helpful?
0 / 5 - 0 ratings

Related issues

the94air picture the94air  路  3Comments

MaxMilton picture MaxMilton  路  3Comments

Etheryte picture Etheryte  路  3Comments

jagasan picture jagasan  路  3Comments

yyyuuu777 picture yyyuuu777  路  3Comments