forked from dennysjmarquez/angular-nested-forms-service
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathforms.service.ts
More file actions
330 lines (315 loc) · 11 KB
/
forms.service.ts
File metadata and controls
330 lines (315 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import { Injectable } from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormControl,
FormGroup,
} from '@angular/forms';
import { Observable, Subject } from 'rxjs';
/**
* Interfaz que define la estructura de los eventos emitidos por el servicio de formularios.
*/
interface FormEvent {
/**
* Tipo de evento que puede ser 'form' o 'control'.
* - 'form': Indica que se ha registrado un nuevo formulario principal.
* - 'formElement': Indica que se ha registrado un nuevo FormControl |o FormGroup en un formulario.
*/
type: 'form' | 'formElement';
/**
* Ruta del formulario o control registrado.
* - Para eventos de tipo 'form', es el identificador del formulario.
* - Para eventos de tipo 'formElement', es el identificador del formulario y el control en formato
* 'formPath.controlPath'.
*/
path: string;
/**
* Instancia del control registrado. Esta propiedad es opcional y solo se incluye en eventos
* de tipo 'control'.
*/
control?: AbstractControl;
}
/**
* Servicio para gestionar formularios anidados y sus controles.
*
* Este servicio resuelve la problemática de gestionar formularios anidados entre componentes.
* Permite centralizar la gestión de formularios y sus controles, facilitando la interacción
* entre componentes padres, hijos y nietos dentro de un único form principal.
*
* **Importante, correcto uso:** Para evitar problemas de estado compartido entre diferentes pantallas,
* este servicio debe ser proporcionado a nivel del componente principal de la pantalla.
* Los componentes hijos deben inyectar el servicio normalmente para compartir la misma instancia.
* Esto asegura que cada instancia del componente principal tenga su propia instancia del servicio,
* reiniciando su estado al entrar de nuevo en la pantalla y manteniendo la integridad de los datos.
*
* @example
* ```typescript
* import { Component, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
* import { FormBuilder, FormGroup, FormControl, NgForm } from '@angular/forms';
* import { FormService } from './form.service';
* import { Subscription } from 'rxjs';
*
* @Component({
* selector: 'app-ejemplo',
* templateUrl: './ejemplo.component.html',
* providers: [FormService] // Proveedor a nivel de componente principal
* })
* export class EjemploComponent implements AfterViewInit, OnDestroy {
* @ViewChild('f') f!: NgForm;
* private formEventSubscription: Subscription;
*
* constructor(
* private formService: FormService
* ) {}
*
* ngAfterViewInit() {
* // Registrar el formulario en el servicio
* this.formService.registerRootForms('miFormulario', this.f);
*
* // Suscribirse a eventos del servicio
* this.formEventSubscription = this.formService.getFormEventObservable().subscribe(event => {
* console.log('Evento de formulario:', event);
* });
* }
*
* ngOnDestroy() {
* // Desuscribirse del observable para evitar fugas de memoria
* if (this.formEventSubscription) {
* this.formEventSubscription.unsubscribe();
* }
* }
*
* // Método para agregar un control dinámicamente
* addControl() {
* const nuevoControl = new FormControl('');
* this.formService.registerFormElement('miFormulario', 'nuevoControl', nuevoControl);
* }
* }
*
* @Component({
* selector: 'app-hijo',
* templateUrl: './hijo.component.html',
* })
* export class HijoComponent {
* constructor(private formService: FormService) {
* // Se usa la misma instancia del servicio en los hijos sin usar en los hijos nietos etc.
* providers: [FormService]
* }
* }
* ```
*
* @author
* Nombre del Autor: Dennys Jose Marquez Reyes - dennysjmarquez
* Contacto: dennysjmarquez@gmail.com
* Sitio Web: https://dennysjmarquez.dev/
*
* @repository
* URL del Repositorio: https://github.com/dennysjmarquez/angular-nested-forms-service
*/
@Injectable({
providedIn: 'root',
})
export class FormService {
private readonly mainForm: FormGroup;
private formEventSubject = new Subject<FormEvent>();
constructor(private fb: FormBuilder) {
this.mainForm = this.fb.group({});
}
/**
* Registra un formulario en el servicio.
*
* @param name - Nombre del formulario.
* @param formGroup - Instancia del FormGroup que representa el formulario.
*
* @example
* ```typescript
* // Usando FormGroup
* const formGroup = this.fb.group({
* nombre: new FormControl(''),
* edad: new FormControl('')
* });
* this.formService.registerRootForms('miFormulario', formGroup);
*
* // Usando NgForm con @ViewChild
* @ViewChild('f') f!: NgForm;
* this.formService.registerRootForms('miFormulario', this.f);
* ```
*
*/
registerRootForms(name: string, formGroup: FormGroup): void {
this.mainForm.setControl(name, formGroup);
this.formEventSubject.next({ type: 'form', path: name });
}
/**
*
* Registra un control dentro de un formulario anidado.
* @param path - Ruta del formulario anidado en formato "formulario.control".
* @param controlName - Nombre del control.
* @param control - Instancia del FormControl o FormGroup que representa el control.
*
* @example
* ```typescript
* const nuevoControl = new FormControl('');
* this.formService.registerFormElement('miFormulario', 'nuevoControl', nuevoControl);
* ```
*
*/
registerFormElement(
path: string,
controlName: string,
control: FormControl | FormGroup
): void {
const formGroup = this.getNestedFormGroup(path);
if (formGroup) {
if (!formGroup.contains(controlName)) {
formGroup.addControl(controlName, control);
this.formEventSubject.next({
type: 'formElement',
path: `${path}.${controlName}`,
control,
});
}
}
}
/**
*
* Obtiene un observable que emite eventos cuando se registran formularios o controles.
*
* Método proporcionado para que los componentes hijos o descendientes del componente principal
* puedan registrar formularios o controles adicionales. Esto asegura que los formularios y
* controles estén completamente cargados y disponibles antes de que se intente agregar
* cualquier control adicional, siguiendo el flujo del ciclo de vida del componente.
*
* Una forma de asegurarse de que los formularios y controles estén completamente cargados y
* disponibles antes de que se intente agregar cualquier control adicional.
* Esto se logra mediante el uso de un Observable que emite eventos cuando los formularios
* y controles están listos.
*
* Eventos de Registro:
*
* - Tipo de Evento 'form': Indica que se ha registrado un nuevo formulario. El event.path contiene el identificador del formulario registrado.
* - Tipo de Evento 'control': Indica que se ha registrado un nuevo control en un formulario. El event.path contiene el identificador del formulario y el control registrado en formato formPath.controlPath.
*
* Uso de event.path:
*
* - Para los eventos de tipo 'form', event.path será el identificador del formulario, por ejemplo, 'mainForm'.
* - Para los eventos de tipo 'control', event.path será el identificador del formulario y el control, por ejemplo, 'mainForm.newControl'.
*
* Nota:
*
* Es importante asegurarse de destruir las suscripciones al observable utilizando `ngOnDestroy`
* para evitar problemas de rendimiento o comportamiento inesperado.
*
* Además, en algunos casos específicos, puede ser útil desuscribirse dentro del propio `subscribe`
* después de realizar una acción específica para evitar llamadas repetidas. Por ejemplo, si solo
* deseas realizar una acción una vez cuando se registra un formulario específico.
*
*
* @returns Observable que emite eventos de tipo FormEvent.
*
* @example
* Ejemplo 1: Registro de un control adicional cuando se registra un formulario específico.
*
* ```typescript
* this.formService.getFormEventObservable().subscribe(event => {
* if (event.type === 'form' && event.path === 'form-1') {
* this.formService.registerFormElement(
* 'form-1',
* 'listado',
* new FormControl(this.dataTable)
* );
* this.formEventSubscription$.unsubscribe();
* }
* });
* ```
*
* Ejemplo 2: Registro de un control adicional cuando se registra un control específico en un formulario.
*
* ```typescript
* this.formService.getFormEventObservable().subscribe(event => {
* if (event.type === 'control' && event.path === 'mainForm.existingForm') {
* this.formService.registerFormElement(
* 'mainForm.existingForm',
* 'newControl',
* new FormControl('')
* );
* }
* });
* ```
*/
getFormEventObservable(): Observable<FormEvent> {
return this.formEventSubject.asObservable();
}
/**
* Obtiene un control específico dentro del formulario principal o anidado.
*
* @param path - Ruta del control en formato "formulario.control".
* @returns La instancia del AbstractControl correspondiente a la ruta especificada, o null
* si no se encuentra.
*
* @example
* ```typescript
* const control = this.formService.getControl('miFormulario.nuevoControl');
* if (control) {
* console.log('Control encontrado:', control);
* } else {
* console.log('Control no encontrado');
* }
* ```
*/
getControl(path: string): AbstractControl | null {
const keys = path.split('.');
let currentControl: AbstractControl | null = this.mainForm;
for (const key of keys) {
if (currentControl && currentControl.get(key)) {
currentControl = currentControl.get(key);
} else {
return null;
}
}
return currentControl;
}
/**
* Obtiene el formulario principal gestionado por el servicio.
*
* @returns La instancia del FormGroup principal.
*
* @example
* ```typescript
* const mainForm = this.formService.getForm();
* console.log('Formulario principal:', mainForm);
* ```
*/
getForm(): FormGroup {
return this.mainForm;
}
/**
* Obtiene un formulario anidado dentro del formulario principal.
*
* @param path - Ruta del formulario anidado en formato "formulario.control".
* @returns La instancia del FormGroup correspondiente a la ruta especificada, o null
* si no se encuentra.
*
* @example
* ```typescript
* const nestedFormGroup = this.formService.getNestedFormGroup('miFormulario.subFormulario');
* if (nestedFormGroup) {
* console.log('Formulario anidado encontrado:', nestedFormGroup);
* } else {
* console.log('Formulario anidado no encontrado');
* }
* ```
*/
private getNestedFormGroup(path: string): FormGroup | null {
const keys = path.split('.');
let currentGroup: FormGroup | null = this.mainForm;
for (const key of keys) {
if (currentGroup && currentGroup.get(key) instanceof FormGroup) {
currentGroup = currentGroup.get(key) as FormGroup;
} else {
return null;
}
}
return currentGroup;
}
}