forms - Angular 2: manejo de formularios de solicitud a gran escala
architecture (4)
En la empresa para la que estoy trabajando, estamos desarrollando una aplicación a gran escala con múltiples formularios, que el usuario debe completar para registrarse en nuestro programa. Cuando se han respondido todas las preguntas, el usuario llega a una sección que resume todas sus respuestas, resalta las respuestas no válidas y le da la oportunidad de volver a visitar cualquiera de los pasos anteriores del formulario y revisar sus respuestas. Esta lógica se repetirá en una variedad de secciones de nivel superior, cada una con múltiples pasos / páginas y una página de resumen.
Para lograr esto, hemos creado un componente para cada paso del formulario (son categorías como "Detalles personales" o "Calificaciones", etc.) junto con sus rutas respectivas y un componente para la Página de resumen.
Para mantenerlo lo más SECO posible, comenzamos a crear un servicio "maestro" que contiene la información para todos los diferentes pasos del formulario (valores, validez, etc.).
import { Injectable } from ''@angular/core'';
import { Validators } from ''@angular/forms'';
import { ValidationService } from ''../components/validation/index'';
@Injectable()
export class FormControlsService {
static getFormControls() {
return [
{
name: ''personalDetailsForm$'',
groups: {
name$: [
{
name: ''firstname$'',
validations: [
Validators.required,
Validators.minLength(2)
]
},
{
name: ''lastname$'',
validations: [
Validators.required,
Validators.minLength(2)
]
}
],
gender$: [
{
name: ''gender$'',
validations: [
Validators.required
]
}
],
address$: [
{
name: ''streetaddress$'',
validations: [
Validators.required
]
},
{
name: ''city$'',
validations: [
Validators.required
]
},
{
name: ''state$'',
validations: [
Validators.required
]
},
{
name: ''zip$'',
validations: [
Validators.required
]
},
{
name: ''country$'',
validations: [
Validators.required
]
}
],
phone$: [
{
name: ''phone$'',
validations: [
Validators.required
]
},
{
name: ''countrycode$'',
validations: [
Validators.required
]
}
],
}
},
{
name: ''parentForm$'',
groups: {
all: [
{
name: ''parentName$'',
validations: [
Validators.required
]
},
{
name: ''parentEmail$'',
validations: [
ValidationService.emailValidator
]
},
{
name: ''parentOccupation$''
},
{
name: ''parentTelephone$''
}
]
}
},
{
name: ''responsibilitiesForm$'',
groups: {
all: [
{
name: ''hasDrivingLicense$'',
validations: [
Validators.required,
]
},
{
name: ''drivingMonth$'',
validations: [
ValidationService.monthValidator
]
},
{
name: ''drivingYear$'',
validations: [
ValidationService.yearValidator
]
},
{
name: ''driveTimesPerWeek$'',
validations: [
Validators.required
]
},
]
}
}
];
}
}
Todos los componentes utilizan ese servicio para configurar los enlaces de formulario HTML para cada uno, accediendo a la clave de objeto correspondiente y creando grupos de formularios anidados, así como también en la página Resumen, cuya capa de presentación está unida de una sola manera (Modelo -> Ver).
export class FormManagerService {
mainForm: FormGroup;
constructor(private fb: FormBuilder) {
}
setupFormControls() {
let allForms = {};
this.forms = FormControlsService.getFormControls();
for (let form of this.forms) {
let resultingForm = {};
Object.keys(form[''groups'']).forEach(group => {
let formGroup = {};
for (let field of form[''groups''][group]) {
formGroup[field.name] = ['''', this.getFieldValidators(field)];
}
resultingForm[group] = this.fb.group(formGroup);
});
allForms[form.name] = this.fb.group(resultingForm);
}
this.mainForm = this.fb.group(allForms);
}
getFieldValidators(field): Validators[] {
let result = [];
for (let validation of field.validations) {
result.push(validation);
}
return (result.length > 0) ? [Validators.compose(result)] : [];
}
}
Después, comenzamos a usar la siguiente sintaxis en los componentes para alcanzar los controles de formulario especificados en el servicio de formulario maestro:
personalDetailsForm$: AbstractControl;
streetaddress$: AbstractControl;
constructor(private fm: FormManagerService) {
this.personalDetailsForm$ = this.fm.mainForm.controls[''personalDetailsForm$''];
this.streetaddress$ = this.personalDetailsForm$[''controls''][''address$''][''controls''][''streetaddress$''];
}
que parece un olor a código en nuestros ojos inexpertos. Tenemos fuertes preocupaciones sobre cómo se escalará una aplicación como esta, dada la cantidad de secciones que tendremos al final.
Hemos estado discutiendo diferentes soluciones, pero no podemos encontrar una que aproveche el motor de formularios de Angular, nos permita mantener intacta nuestra jerarquía de validación y también es simple.
¿Hay una mejor manera de lograr lo que estamos tratando de hacer?
¿Es realmente necesario mantener los controles de formulario en el servicio?
¿Por qué no simplemente dejar el servicio como el guardián de los datos y tener los controles de formulario en los componentes?
Puede usar la protección
CanDeactivate
para evitar que el usuario se aleje de un componente con datos no válidos.
https://angular.io/docs/ts/latest/api/router/index/CanDeactivate-interface.html
Hice una aplicación similar. El problema es que está creando todas sus entradas al mismo tiempo, lo que probablemente no sea escalable.
En mi caso, hice un FormManagerService que gestiona una matriz de FormGroup. Cada paso tiene un FormGroup que se inicializa una vez en la ejecución en el ngOnInit del componente de paso enviando su configuración FormGroup al FormManagerService. Algo como eso:
stepsForm: Array<FormGroup> = [];
getFormGroup(id:number, config: Object): FormGroup {
let formGroup: FormGroup;
if(this.stepsForm[id]){
formGroup = this.stepsForm[id];
} else {
formGroup = this.createForm(config); // call function to create FormGroup
this.stepsForm[id] = formGroup;
}
return formGroup;
}
Necesitará una identificación para saber qué FormGroup corresponde al paso. Pero después de eso, podrá dividir su configuración de formularios en cada paso (archivos de configuración tan pequeños que son más fáciles de mantener que un archivo enorme). Minimizará el tiempo de carga inicial ya que los FormGroups solo se crean cuando es necesario.
Finalmente, antes de enviar, solo necesita asignar su matriz FormGroup y validar si todas son válidas. Solo asegúrese de que se hayan visitado todos los pasos (de lo contrario, no se creará FormGroup).
Puede que esta no sea la mejor solución, pero encajó bien en mi proyecto, ya que estoy obligando al usuario a seguir mis pasos. Dame tu opinión :)
Su enfoque y el de Ovangle parecen ser bastante buenos, pero a pesar de que esta pregunta SO está resuelta, quiero compartir mi solución porque es un enfoque realmente diferente que creo que le podría gustar o podría ser útil para otra persona.
qué soluciones hay para un formulario de aplicación amplia donde los componentes se encargan de diferentes subparte del formulario global.
Nos hemos enfrentado exactamente al mismo problema y después de meses de luchar con formas enormes, anidadas y a veces polimórficas, hemos encontrado una solución que nos agrada, que es fácil de usar y que nos da "superpoderes" (como tipo seguridad dentro de TS y HTML), acceso a errores anidados y otros.
Hemos decidido extraer eso en una biblioteca separada y de código abierto.
El código fuente está disponible aquí:
https://github.com/cloudnc/ngx-sub-form
Y el paquete npm se puede instalar así
npm i ngx-sub-form
Detrás de escena, nuestra biblioteca usa
ControlValueAccessor
y eso nos permite usarlo en formularios de plantilla Y formularios reactivos (sin embargo, obtendrá el mejor rendimiento al usar formularios reactivos).
Entonces, ¿qué es todo esto?
Antes de comenzar a explicar, si prefiere seguir junto con un editor adecuado, he hecho un ejemplo de Stackblitz: https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling
Bueno, un ejemplo vale más que 1000 palabras, así que vamos a rehacer una parte de su formulario (la más difícil con datos anidados):
personalDetailsForm$
Lo primero que debe hacer es asegurarse de que todo va a ser de tipo seguro. Creemos las interfaces para eso:
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 :)
}
Luego, podemos crear un componente que extienda
NgxSubFormComponent
.
Llamémoslo
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<PersonalDetails> {
protected getFormControls(): Controls<PersonalDetails> {
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] }),
};
}
}
Pocas cosas para notar aquí:
-
NgxSubFormComponent<PersonalDetails>
nos dará seguridad de tipo -
Tenemos
que
implementar los métodos
getFormControls
que esperan un diccionario de las teclas de nivel superior que coincida con un control abstracto (aquíname
,gender
,address
,phone
) - Mantenemos un control total sobre las opciones para crear el control de formulario (validadores, validadores asíncronos, etc.)
-
providers: subformComponentProviders(PersonalDetailsFormComponent)
es una pequeña función de utilidad para crear los proveedores necesarios para usar unControlValueAccessor
(cf Angular doc), solo necesita pasar como argumento el componente actual
Ahora, para cada entrada de
name
,
gender
,
address
,
phone
que es un objeto, creamos un formulario secundario para él (por lo tanto, en este caso, todo menos el
gender
).
Aquí hay un ejemplo con el teléfono:
@Component({
selector: ''app-phone-form'',
templateUrl: ''./phone-form.component.html'',
styleUrls: [''./phone-form.component.css''],
providers: subformComponentProviders(PhoneFormComponent)
})
export class PhoneFormComponent extends NgxSubFormComponent<Phone> {
protected getFormControls(): Controls<Phone> {
return {
phone: new FormControl(null, { validators: [Validators.required] }),
countrycode: new FormControl(null, { validators: [Validators.required] }),
};
}
}
Ahora, escribamos la plantilla para ello:
<div [formGroup]="formGroup">
<input type="text" placeholder="Phone" [formControlName]="formControlNames.phone">
<input type="text" placeholder="Country code" [formControlName]="formControlNames.countrycode">
</div>
Darse cuenta de:
-
Definimos
<div [formGroup]="formGroup">
, elformGroup
aquí lo proporcionaNgxSubFormComponent
, no tiene que crearlo usted mismo -
[formControlName]="formControlNames.phone"
usamos el enlace de propiedad para tener unformControlName
dinámico y luego usamosformControlNames
.NgxSubFormComponent
también ofrece este tipo de mecanismo de seguridad y si su interfaz cambia en algún momento (todos sabemos acerca de los refactores ...), no solo su TS generará errores por falta de propiedades en el formulario, sino también el HTML (cuando compila con AOT )!
Siguiente paso: construyamos la plantilla
PersonalDetailsFormComponent
pero primero agreguemos esa línea al TS:
public Gender: typeof Gender = Gender;
para que podamos acceder de forma segura a la enumeración desde la vista
<div [formGroup]="formGroup">
<app-name-form [formControlName]="formControlNames.name"></app-name-form>
<select [formControlName]="formControlNames.gender">
<option *ngFor="let gender of Gender | keyvalue" [value]="gender.value">{{ gender.value }}</option>
</select>
<app-address-form [formControlName]="formControlNames.address"></app-address-form>
<app-phone-form [formControlName]="formControlNames.phone"></app-phone-form>
</div>
¿Observa cómo delegamos la responsabilidad a un subcomponente?
<app-name-form [formControlName]="formControlNames.name"></app-name-form>
ese es el punto clave aquí!
Paso final : construyó el componente de forma superior
¡Buenas noticias, también podemos usar
NgxSubFormComponent
para disfrutar de la seguridad de tipos!
@Component({
selector: ''my-app'',
templateUrl: ''./app.component.html'',
styleUrls: [''./app.component.css'']
})
export class AppComponent extends NgxSubFormComponent<MainForm> {
protected getFormControls(): Controls<MainForm> {
return {
personalDetails: new FormControl(null, { validators: [Validators.required] }),
};
}
}
Y la plantilla:
<form [formGroup]="formGroup">
<app-personal-details-form [formControlName]="formControlNames.personalDetails"></app-personal-details-form>
</form>
<!-- let see how the form values looks like! -->
<h1>Values:</h1>
<pre>{{ formGroupValues | json }}</pre>
<!-- let see if there''s any error (works with nested ones!) -->
<h1>Errors:</h1>
<pre>{{ formGroupErrors | json }}</pre>
Para llevar de todo eso: - Escriba formularios seguros - ¡Reutilizable!
¿Necesita reutilizar la dirección uno para los
parents
?
Claro, no se preocupe: buenas utilidades para crear formularios anidados, acceder a nombres de control de formularios, valores de formularios, errores de formulario (+ anidados). ¿Ha notado alguna lógica compleja?
Sin observables, sin servicio para inyectar ... Simplemente definiendo interfaces, extendiendo una clase, pasando un objeto con los controles de formulario y creando la vista.
Eso es
Por cierto, aquí hay una
demostración en vivo
de todo lo que he estado hablando:
https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling
Además, no era necesario en ese caso, pero para formas un poco más complejas, por ejemplo, cuando necesita manejar un objeto polimórfico como
type Animal = Cat | Dog
type Animal = Cat | Dog
, tenemos otra clase para eso que es
NgxSubFormRemapComponent
pero puedes leer el
NgxSubFormRemapComponent
https://github.com/cloudnc/ngx-sub-form
si necesitas más información.
¡Espero que te ayude a escalar tus formularios!
Editar:
Si desea ir más allá, acabo de publicar una publicación de blog para explicar muchas cosas sobre formularios y ngx-sub-form aquí https://dev.to/maxime1992/building-scalable-robust-and-type-safe-forms-with-angular-3nf9
@ngrx/store
otra parte sobre
@ngrx/store
, y aunque todavía lo recomiendo, creo que estaba malentendiendo un poco tu problema.
De todos modos, su
FormsControlService
es básicamente una constante global.
En serio, reemplace la
export class FormControlService ...
con
export const formControlsDefinitions = {
// ...
};
¿Y qué diferencia hace? En lugar de obtener un servicio, solo importa el objeto. Y como ahora lo estamos considerando como una constante global tipada, podemos definir las interfaces que usamos ...
export interface ModelControl<T> {
name: string;
validators: ValidatorFn[];
}
export interface ModelGroup<T> {
name: string;
// Any subgroups of the group
groups?: ModelGroup<any>[];
// Any form controls of the group
controls?: ModelControl<any>[];
}
y como lo hemos hecho, podemos mover las definiciones de los grupos de formularios individuales fuera del módulo monolítico único y definir el grupo de formularios donde definimos el modelo. Mucho más limpio.
// personal_details.ts
export interface PersonalDetails {
...
}
export const personalDetailsFormGroup: ModelGroup<PersonalDetails> = {
name: ''personalDetails$'';
groups: [...]
}
Pero ahora tenemos todas estas definiciones de grupos de formularios individuales dispersas en todos nuestros módulos y no hay forma de recopilarlas todas :( Necesitamos alguna forma de conocer todos los grupos de formularios en nuestra aplicación.
Pero no sabemos cuántos módulos tendremos en el futuro, y es posible que queramos cargarlos de manera diferida, por lo que es posible que sus grupos de modelos no estén registrados al inicio de la aplicación.
Inversión de control al rescate! Hagamos un servicio, con una única dependencia inyectada: un proveedor múltiple que puede inyectarse con todos nuestros grupos de formularios dispersos cuando los distribuimos a través de nuestros módulos.
export const MODEL_GROUP = new OpaqueToken(''my_model_group'');
/**
* All the form controls for the application
*/
export class FormControlService {
constructor(
@Inject(MMODEL_GROUP) rootControls: ModelGroup<any>[]
) {}
getControl(name: string): AbstractControl { /etc. }
}
luego cree un módulo de manifiesto en algún lugar (que se inyecta en el módulo de aplicación "central"), construyendo su FormService
@NgModule({
providers : [
{provide: MODEL_GROUP, useValue: personalDetailsFormGroup, multi: true}
// and all your other form groups
// finally inject our service, which knows about all the form controls
// our app will ever use.
FormControlService
]
})
export class CoreFormControlsModule {}
Ahora tenemos una solución que es:
- más local, los controles de formulario se declaran junto a los modelos
- más escalable, solo necesita agregar un control de formulario y luego agregarlo al módulo de manifiesto; y
- menos monolítico, no hay clases de configuración "dios".