What I have:
I am building an ionic 2 app and have built a basic angular 2 component that contains
An input field
A label to display the inputs title
A label to display any validation errors
I will refer to this as my input component
I have a page component with a form on it, and currently have text inputs. 1 regular input (password) and 1 input wrapped in my input component (username).
this is the relevant portion of my page component
ngOnInit() {
this.loginForm = this.formBuilder.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
This is the page component template
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<!-- My input component -->
<aw-input-text id="username" name="Username" [formInput]="loginForm.controls.username"></aw-input-text>
<!-- A standard input control -->
<ion-item [class.error]="loginForm.controls.password.errors">
<ion-label floating>Password</ion-label>
<ion-input type="text" value="" name="password" formControlName="password"></ion-input>
<p *ngIf="loginForm.controls.password.errors">This field is required!</p>
</ion-item>
<button type="submit" class="custom-button" [disabled]="!loginForm.valid" block>Login</button>
</form>
This is the template for my input component
<!-- Component template -->
<form [formGroup]="formGroup">
<ion-item>
<ion-label floating>{{inputName}}</ion-label>
<ion-input type="text" formControlName="inputValue"></ion-input>
<p *ngIf="!formGroup.controls.inputValue.valid">This field is required!</p>
</ion-item>
</form>
and this is the input component
import {Component, Input} from '@angular/core';
import {FormBuilder} from '@angular/forms';
@Component({
selector: 'aw-input-text',
templateUrl: 'build/shared/aw-input-text/aw-input-text.html'
})
export class AwInputText {
@Input('formInput')
public formInput;
@Input('name')
public inputName;
public formGroup;
constructor(private formBuilder: FormBuilder) {
}
ngOnInit() {
this.formGroup = this.formBuilder.group({
inputValue: this.formInput
});
}
}
The component renders correctly.
The problem:
The input inside the component doesn't update the valid state of the the form it is in.
When I fill out the username then the password the form becomes valid
When I fill out the password then the username the form remains invalid
So the form can see the valid state of the input component, it's just that the input component changing valid state doesn't trigger the form to update.
Possible solution 1
As described in this article and plunk
https://scotch.io/tutorials/how-to-build-nested-model-driven-forms-in-angular-2
https://plnkr.co/edit/clTbNP7MHBbBbrUp20vr?p=preview
I could modify my page component to create a form group that contains a nested form group for every form control that I want to use inside of my input component
ngOnInit() {
this.loginForm = this.formBuilder.group({
username: this.formBuilder.group({
username: ['', Validators.required],
}),
password: ['', Validators.required],
});
}
This solution fits the scenario in the article where they are adding an array of input controls, but in my case I think this feels hacky
Possible solution 2
Another hacky solution I have considered is using an @output directive from my input component to trigger an event on the page component that refreshes the form whenever the input component is updated.
Update to the input component
this.formGroup.controls.inputValue.valueChanges.subscribe(value => {
this.formUpdated.emit({
value: value
})
});
Update to the page component
public onUpdated(value){
this.loginForm.updateValueAndValidity();
}
and an update the page component template
<aw-input-text id="username" name="Username" (updated)="onUpdated($event)" [formInput]="loginForm.controls.username"></aw-input-text>
This does give me the desired functionality, but I think it also seems a bit hacky having an event handler on every form page to make the input component work.
The question
Is there a way for me to make my component update the valid state of the form it is in (keeping in mind that I would want to re-use this component multiple times in each form) without resorting to the solution described above.
You can
@Input
yourloginForm
into nested component, as let's sayparentForm
. Then register nestedformGroup
toparentForm
on child component init, and unregister it on child component destroy.What I did in my case (nested dynamic form as well) is somehow similar to Marcin's response.
I'm passing existing
FormGroup
asparentForm
and myComponent
's view looks like this:It suits my needs. Hope it can help you as well.
UPDATE: I've created a library for speeding up dynamic forms creation. You can take a look to figure out how I use the technique from this answer: https://www.npmjs.com/package/dorf
Thanks for the answers guys. In the end we created two components, a custom-form component and a custom-input component.
We nest as many custom-input components as we need inside the custom-form component and the custom-form component uses @ContentChildren to identify and register all the child custom-input components.
This way we don't have to pass the form into every input, and we don't have a mess of nested form groups for every input.