Can't validate form with multiple child components #56

Closed
sproogen opened this Issue Oct 26, 2016 · 20 comments

Comments

Projects
None yet
9 participants
@sproogen

sproogen commented Oct 26, 2016

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

@logaretm

This comment has been minimized.

Show comment
Hide comment
@logaretm

logaretm Oct 26, 2016

Collaborator

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.

Collaborator

logaretm commented Oct 26, 2016

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.

@sproogen

This comment has been minimized.

Show comment
Hide comment
@sproogen

sproogen Oct 26, 2016

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

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

@logaretm

This comment has been minimized.

Show comment
Hide comment
@logaretm

logaretm Oct 26, 2016

Collaborator

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

Collaborator

logaretm commented Oct 26, 2016

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

@shakee93

This comment has been minimized.

Show comment
Hide comment
@shakee93

shakee93 Nov 9, 2016

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 commented Nov 9, 2016

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 ?

@sproogen

This comment has been minimized.

Show comment
Hide comment
@sproogen

sproogen Nov 9, 2016

@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 commented Nov 9, 2016

@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?

@logaretm

This comment has been minimized.

Show comment
Hide comment
@logaretm

logaretm Nov 9, 2016

Collaborator

@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!

Collaborator

logaretm commented Nov 9, 2016

@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!

@sproogen

This comment has been minimized.

Show comment
Hide comment
@sproogen

sproogen Nov 9, 2016

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.

sproogen commented Nov 9, 2016

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.

@daylightstudio

This comment has been minimized.

Show comment
Hide comment
@daylightstudio

daylightstudio Jan 11, 2017

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);
	});
}

daylightstudio commented Jan 11, 2017

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);
	});
}
@Vaerum

This comment has been minimized.

Show comment
Hide comment
@Vaerum

Vaerum Jan 11, 2017

@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.

Vaerum commented Jan 11, 2017

@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.

@daylightstudio

This comment has been minimized.

Show comment
Hide comment
@daylightstudio

daylightstudio Jan 11, 2017

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?

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?

@Vaerum

This comment has been minimized.

Show comment
Hide comment
@Vaerum

Vaerum Jan 11, 2017

@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

Vaerum commented Jan 11, 2017

@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

@daylightstudio

This comment has been minimized.

Show comment
Hide comment
@daylightstudio

daylightstudio Jan 11, 2017

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.

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.

@pimboden

This comment has been minimized.

Show comment
Hide comment
@pimboden

pimboden Apr 11, 2017

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

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

@logaretm

This comment has been minimized.

Show comment
Hide comment
@logaretm

logaretm Apr 11, 2017

Collaborator

@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.

Collaborator

logaretm commented Apr 11, 2017

@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.

@pimboden

This comment has been minimized.

Show comment
Hide comment
@pimboden

pimboden Apr 11, 2017

@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

pimboden commented Apr 11, 2017

@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

@logaretm

This comment has been minimized.

Show comment
Hide comment
@logaretm

logaretm Apr 11, 2017

Collaborator

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.

Collaborator

logaretm commented Apr 11, 2017

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.

@prsth

This comment has been minimized.

Show comment
Hide comment
@prsth

prsth Jun 14, 2017

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 commented Jun 14, 2017

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))

@coderabsolute

This comment has been minimized.

Show comment
Hide comment
@coderabsolute

coderabsolute Jun 15, 2017

@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 commented Jun 15, 2017

@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?

@prsth

This comment has been minimized.

Show comment
Hide comment
@prsth

prsth Jun 15, 2017

@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.

prsth commented Jun 15, 2017

@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.

@Kocal

This comment has been minimized.

Show comment
Hide comment
@Kocal

Kocal Mar 22, 2018

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. 👍

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
});

Kocal commented Mar 22, 2018

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. 👍

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
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment