At the company I’m working for, we’re developing a large scale application with multiple forms, that the user needs to fill in in order to register for our program. When all
Your approach and Ovangle's one seem to be pretty good but even though this SO question is solved, I want to share my solution because it's a really different approach that I think you might like or might be useful to someone else.
what solutions there are for an app wide form where Components take care of different sub parts to the global form.
We've faced that exact same issue and after months of struggling with huge, nested and sometimes polymorphic forms, we've come up with a solution that pleases us, which is simple to use and which gives us "super powers" (like type safety within both TS and HTML), access to nested errors and others.
We've decided to extract that into a separated library and open source it.
Source code is available here: https://github.com/cloudnc/ngx-sub-form
And the npm package can be installed like that npm i ngx-sub-form
Behind the scenes, our library uses ControlValueAccessor and that allows us to use it on template forms AND reactive forms (you'll get the best out of it by using reactive forms though).
So what is it all about?
Before I start explaining, if you prefer to follow along with a proper editor I've made a Stackblitz example: https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling
Well an example is worth a 1000 words I guess so let's redo one part of your form (the hardest one with nested data): personalDetailsForm$
First thing to do is make sure everything is going to be type safe. Let's create the interfaces for that:
export enum Gender {
MALE = 'Male',
FEMALE = 'Female',
Other = 'Other',
}
export interface Name {
firstname: string;
lastname: string;
}
export interface Address {
streetaddress: string;
city: string;
state: string;
zip: string;
country: string;
}
export interface Phone {
phone: string;
countrycode: string;
}
export interface PersonalDetails {
name: Name;
gender: Gender;
address: Address;
phone: Phone;
}
export interface MainForm {
// this is one example out of what you posted
personalDetails: PersonalDetails;
// you'll probably want to add `parent` and `responsibilities` here too
// which I'm not going to do because `personalDetails` covers it all :)
}
Then, we can create a component that extends NgxSubFormComponent.
Let's call it personal-details-form.component.
@Component({
selector: 'app-personal-details-form',
templateUrl: './personal-details-form.component.html',
styleUrls: ['./personal-details-form.component.css'],
providers: subformComponentProviders(PersonalDetailsFormComponent)
})
export class PersonalDetailsFormComponent extends NgxSubFormComponent {
protected getFormControls(): Controls {
return {
name: new FormControl(null, { validators: [Validators.required] }),
gender: new FormControl(null, { validators: [Validators.required] }),
address: new FormControl(null, { validators: [Validators.required] }),
phone: new FormControl(null, { validators: [Validators.required] }),
};
}
}
Few things to notice here:
NgxSubFormComponent is going to give us type safetygetFormControls methods which expects a dictionary of the top level keys matching an abstract control (here name, gender, address, phone)providers: subformComponentProviders(PersonalDetailsFormComponent) is a small utility function to create the providers necessary to use a ControlValueAccessor (cf Angular doc), you just need to pass as argument the current componentNow, for every entry of name, gender, address, phone that is an object, we create a sub form for it (so in this case everything but gender).
Here's an example with phone:
@Component({
selector: 'app-phone-form',
templateUrl: './phone-form.component.html',
styleUrls: ['./phone-form.component.css'],
providers: subformComponentProviders(PhoneFormComponent)
})
export class PhoneFormComponent extends NgxSubFormComponent {
protected getFormControls(): Controls {
return {
phone: new FormControl(null, { validators: [Validators.required] }),
countrycode: new FormControl(null, { validators: [Validators.required] }),
};
}
}
Now, let's write the template for it:
Notice that:
, the formGroup here is provided by NgxSubFormComponent you don't have to create it yourself
[formControlName]="formControlNames.phone" we use property binding to have a dynamic formControlName and then use formControlNames. This type safety mechanism is offered by NgxSubFormComponent too and if your interface changes at some point (we all know about refactors...), not only your TS will error for missing properties in the form but also the HTML (when you compile with AOT)!
Next step: Let's build the PersonalDetailsFormComponent template but first just add that line into the TS: public Gender: typeof Gender = Gender; so we can safely access the enum from the view
Notice how we delegate the responsibility to a sub component? that's the key point here!
Final step: built the top form component
Good news, we can also use NgxSubFormComponent to enjoy type safety!
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent extends NgxSubFormComponent {
protected getFormControls(): Controls {
return {
personalDetails: new FormControl(null, { validators: [Validators.required] }),
};
}
}
And the template:
Values:
{{ formGroupValues | json }}
Errors:
{{ formGroupErrors | json }}
Takeaway from all of that:
- Type safe forms
- Reusable! Needs to reuse the address one for the parents? Sure, no worries
- Nice utilities to build nested forms, access form control names, form values, form errors (+nested!)
- Have you noticed any complex logic at all? No observables, no service to inject... Just defining interfaces, extending a class, pass an object with the form controls and create the view. That's it
By the way, here's a live demo of everything I've been talking about:
https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling
Also, it was not necessary in that case but for forms a little bit more complex, for example when you need to handle a polymorphic object like type Animal = Cat | Dog we've got another class for that which is NgxSubFormRemapComponent but you can read the README if you need more info.
Hope it helps you scale your forms!
Edit:
If you want to go further, I've just published a blog post to explain a lot of things about forms and ngx-sub-form here https://dev.to/maxime1992/building-scalable-robust-and-type-safe-forms-with-angular-3nf9