0

I have seen a few StackOverflow issues with this question, but none of them seem to have proven answers that work.

I am building a dynamically created Reactive Form Using Angular. StackBlitz here . The user has the option to add "groups" and "conditions" within the groups. Each group can be further nested using FormArrays.

The form works as expected. Here are the main pieces of the form:

  1. AppComponent: The main Parent Component
  2. GroupControlComponent: The child of the parent. Takes care of all the group objects
  3. ConditionFormComponent: The child of the GroupControlComponent. Takes care of all the conditions within the groups

This is what a basic object looks like:

{
  // appcomponent level
  "statement": {
    //groupcontrol level
    "groups": [
      {
        // conditionform level
        "conjunctor": null,
        "conditions": [
          {
            "variable": null
          }
        ],
        "groups": []
      }
    ]
  }
}

When I set up validation, I want everything to be required. So I went through the FormBuilder, on the parent and children components, and set Validators.required to true on all form controls.

this._fb.control({
  conjunctor: null,
  conditions: [[], Validators.required],
  groups: [[], Validators.required]
 })

(this is the case on all controls)

The problem is, the form now always is valid. Even when the form is not actually valid. Is it possible to check form validity while using nested child components?

Here is some more information about this specific form, if required:

Cannot Find Control with Path Using ngIf on Recursive Angular Form

Angular Deeply Nested Reactive Form: Cannot find control with path on nested FormArray

Jeremy
  • 1,038
  • 11
  • 34
  • I have a feeling that it's just a matter of implement the Validator interface in your controls. I think I could take a look at it later, but let me know if you managed to do it before that. – julianobrasil Jun 30 '20 at 18:06
  • Will do.. I'll look into that as well. As of now, it seems the method I am trying is overly complicated. Checking form validations in the children, then emitting a true or false back up to the parent component. It doesn't work in all cases, and I would think there is a better way – Jeremy Jun 30 '20 at 18:08

1 Answers1

2

Stackblitz demo

Basically you must implement the Validator (not Validators - plural) interface. It has one mandatory method:

validate(c: AbstractControl) => ValidationErrors;

The method validate should return null if the control is valid and any other object if it is invalid.

One more thing is that you must provide NG_VALIDATORS (now, using plural) in the same way you provided NG_VALUE_ACCESSOR.

In summary:

@Component({
  ...

  providers: [
    ...
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => GroupControlComponent),
      multi: true
    }
  ]
})
export class GroupControlComponent implements Validator {
  ...

  // Notice that we don't need to use the c parameter passed to the method.
  // We are using the `_form` instance property to verify what we need
  validate(c: AbstractControl): ValidationErrors {
    return this._form.valid ? null : {errors: 'form is invalid'}; 
  }
  ...
}

You must do it to your GroupControlComponent and the ConditionFormComponent.

[UPDATE]: You also need to take care of the dynamically created controls. Inside group control, let's look at the one single component that has an input: the condition control. Instead of:

_addCondition() {
  this._conditionsFormArray.push(
    this._fb.control({variable: null})
  );
}

You should initialize the control with a validator that checks for the validity of variable because the underlying control associated with the variable attribute will only check this out after the user interacts with the input inside the ConditionFormComponent. So, you should write it like this:

_addCondition() {
  this._conditionsFormArray.push(
    this._fb.control({variable: null}, 
      (c: AbstractControl) => 
          c.value && c.value.variable ? null : {'required': true}
    )
  );
}

Where this is a validation function that checks for the validity of the variable attribute:

(c: AbstractControl) => 
    c.value && c.value.variable ? null : {'required': true}

If you don't do that, it'll override the validation in the underlying ConditionForm control (which states that the input is required). And you could have a weird state where the outer form (in GroupControl) is invalid, but the inner form (in the ConditionForm) is valid. As you're validating it on the outer form, you basically could even remove the Validators.required in the inner form and rely just on the outer form validation.

julianobrasil
  • 8,954
  • 2
  • 33
  • 55
  • Thank you as always... this is working almost perfectly as expected. The only issue is when you first click "add group" , it doesn't register as invalid until an additional condition is added. I tried, in the group-control-component, changing the `_addCondition(...` fn to the following: ` _addCondition() { this._conditionsFormArray.push(this._fb.control({ variable: null}, Validators.required )); }` , but this didn't change this issue – Jeremy Jun 30 '20 at 22:08
  • This is the expected behavior, because, usually, forms are considered valid until the user interacts with them. But you can mark them as dirty if you want and then they'll be invalid. – julianobrasil Jul 01 '20 at 01:19
  • I see, I forgot about that. Is there a way to set it against the default? Keep the from invalid until the user enters something? I tried adding `this._form.setErrors({ invalid: true })` but it only is false for a brief moment before going back to true – Jeremy Jul 01 '20 at 17:12
  • I'm having second thoughts about it. As we are building the whole thing dynamically, I think the validation should be called whenever we add a control. I'll take a look today again... in an hour or two. – julianobrasil Jul 02 '20 at 09:06
  • Thank you, as always. I'm very grateful! This is a much better approach – Jeremy Jul 03 '20 at 04:03