A la hora de trabajar con formularios en ocasiones necesitamos deshabilitar dinámicamente uno de sus controles en base al valor de otro de los controles del formulario.
Si alguna vez has intentado deshabilitar un control en un formulario reactivo vinculando a su propiedad disabled
:
<input
type="text"
formControlName="direccion"
[disabled]="tipoDeEntrega.value === 'recogidaEnTienda'"
/>
Te habrás dado cuenta que el control no se deshabilita y además por consola se lanza un warning indicando que este no es el procedimiento para hacerlo.
En este artículo vamos a explorar unas cuantas alternativas que tenemos a la hora de conseguir esta funcionalidad.
Si lo prefieres en formato video, el contenido de este artículo también lo tienes aquí.
Conceptos Básicos
A la hora de habilitar/deshabilitar un control de formulario reactivo, tenemos dos procedimientos básicos:
1- Establecer el estado inicial
Cuando creamos un control de formulario, este se crea siempre por defecto como habilitado.
new FormControl('valor_inicial')
Pero este comportamiento lo podemos modificar fácilmente, utilizando la versión expandida del valor inicial, pasando un objeto con las propiedades value
y disabled
new FormControl({ value: 'valor_inicial', disabled: true})
2- Modificar el estado de habilitación de un control
Para modificar el estado de habilitación de un control ya creado, todos los controles de formulario tienen un par de funciones enable()
y disable()
que nos permiten habilitarlos y deshabilitarlos respectivamente.
control.enable() // habilitará el control
control.disable() // deshabilitará el control
Una vez vistos estos conceptos básicos, veamos como los podemos aplicar para deshabilitar un control dinámicamente cuando el valor de otro control del formulario cambie.
Usando valueChanges
El procedimiento estándar a la hora de implementar esta funcionalidad es haciendo uso del observable valueChanges
del control que queremos usar como base para monitorizar los cambios de su valor.
Por ejemplo en el siguiente extracto de un formulario de una supuesta tienda online:
deliveryForm = new FormGroup({
tipoDeEntrega: new FormControl('envioADomicilio'),
direccion: new FormControl(''),
});
Tenemos dos controles:
-
tipoDeEntrega
cuyos valores pueden ser envioADomicilio o recogidaEnTienda. - y
direccion
que permite al usuario introducir una dirección de entrega.
Bien pues para deshabilitar el control de la direccion
cuando el tipoDeEntrega
seleccionado sea igual a recogidaEnTienda, podemos hacer lo siguiente:
const { tipoDeEntrega, direccion } = this.deliveryForm.controls;
tipoDeEntrega.valueChanges.subscribe(
(tipoSeleccionado) =>
tipoSeleccionado === 'recogidaEnTienda'
? direccion.disable()
: direccion.enable()
);
En donde:
- Primero extraemos los controles del
FormGroup
- A continuación nos estamos subscribiendo al observable
valueChanges
del control del tipo de entrega, el cual emitirá una notificación cada vez que su valor cambie. - Y por último en el callback de esta subscripción chequeamos el tipo seleccionado.
Si es igual a recogidaEnTienda deshabilitamos el control de la dirección llamando a su método
disable()
. Y en caso, contrario llamamos a su métodoenable()
para volver a habilitarlo.
Cómo vemos nos estamos subscribiendo manualmente a un observable, lo que implica que somos también responsables de cancelar dicha subscripción cuando deje de ser necesaria. No lo hemos incluido en el extracto de código anterior para una mayor claridad, pero este sería un ejemplo de uso completo en un componente:
export class SomeComponent implements OnInit, OnDestroy {
deliveryForm = new FormGroup({
tipoDeEntrega: new FormControl('envioADomicilio'),
direccion: new FormControl(''),
});
subscription?: Subscription;
ngOnInit(): void {
const { tipoDeEntrega, direccion } = this.deliveryForm.controls;
this.subscription = tipoDeEntrega.valueChanges.subscribe(
(tipoSeleccionado) =>
tipoSeleccionado === 'recogidaEnTienda'
? direccion.disable()
: direccion.enable()
);
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
Creando una directiva personalizada
Como hemos visto el procedimiento de valueChanges
en sí es relativamente sencillo. Pero si la complejidad del formulario es alta y/o no estamos muy cómodos usando observables, dicho procedimiento se puede volver tedioso y enrevesado.
Por ello vamos a ver una alternativa para devolver está funcionalidad al template, creando una directiva específica que nos proporcione una funcionalidad similar a la del vínculo a la propiedad disabled.
Bien, pues aquí tenemos el código de dicha directiva.
@Directive({
selector: `
[formControl][akoDisabledIf],
[formControlName][akoDisabledIf]
`,
standalone: true,
})
export class ControlDisabledIfDirective {
@Input('akoDisabledIf') set disabledIf(condition: boolean) {
const control = this.ngControl.control;
condition ? control?.disable() : control?.enable();
}
constructor(private ngControl: NgControl) {}
}
Veámosla por partes:
- Lo primero que hemos hecho es limitar el uso de esta directiva únicamente a los elementos que también tengan aplicadas las directivas
formControl
oformControlName
indicando esta circunstancia en el selector. - A continuación para obtener la referencia al
FormControl
asociado para poder habilitarlo/deshabilitarlo, hemos inyectado en el constructorNgControl
como dependencia. Esta es una clase base de la que heredan todas las directivas de asociación de controles (formControl, formControlName y ngModel). En nuestro caso particular lo que nos permitirá es obtener la referencia a esa directivaFormControlDirective
oFormControlName
, dependiendo de la directiva usada en el template para vincular el control de formulario. - Y por último hemos definido un
@Input
con el mismo nombre que el selector de nuestra directiva (akoDisabledIf
), para capturar esa condición booleana que definirá si el control debe ser habilitado o deshabilitado. En nuestro caso en este@Input
hemos definido un setter que acepte dicha condición booleana como argumento. Y en el cuerpo, primero hemos extraído delngControl
que hemos inyectado la referencia alFormControl
a través de su propiedadcontrol
. Y a continuación, estamos deshabilitando el control si la condición proporcionada estrue
y habilitándolo si esta esfalse
.
Para ahora utilizar esta directiva simplemente tenemos que:
1.- Importarla en el contexto del componente que contiene el formulario, ya sea un @NgModule
o un @Component
standalone:
@NgModule({
imports: [ControlDisabledIfDirective],
...
})
// -------
@Component({
standalone: true,
imports: [ControlDisabledIfDirective],
...
})
2.- Y hecho esto ya podemos utilizarla en el template que contiene el formulario, añadiéndola en el input a deshabilitar e indicando en la asignación la condición para deshabilitarlo.
<input
type="text"
formControlName="direccion"
[akoDisabledIf]="tipoDeEntrega.value === 'recogidaEnTienda'"
/>
Directiva para FormGroup
y FormArray
La directiva anterior es específica para ser utilizada únicamente con un FormControl
.
Pero del mismo modo podemos crear otra que nos permita deshabilitar un grupo de controles, tanto si es un FormGroup
como un FormArray
.
@Directive({
selector: `
[formGroup][akoDisabledIf],
[formGroupName][akoDisabledIf],
[formArrayName][akoDisabledIf]
`,
standalone: true,
})
export class ContainerDisabledIfDirective {
@Input('akoDisabledIf') set disabledIf(condition: boolean) {
const container = this.controlContainer.control;
condition ? container?.disable() : container?.enable();
}
constructor(private controlContainer: ControlContainer) {}
}
Las únicas dos diferencias con la anterior son:
- En el selector la hemos limitado para se aplique cuando el elemento también incluya una directiva reactiva de asociación de un contenedor de controles (
formGroup
,formGroupName
,formArrayName
). - Hemos sustituido la dependencia
NgControl
porControlContainer
en el constructor, que es en este caso la clase base de la que heredan las directivas de contenedor.
Esta nueva directiva nos permitiría deshabilitar todo un grupo de controles, asociándola en el template al elemento vinculado a dicho contenedor.
<div
formGroupName="unGrupoDeControles"
[akoDisabledIf]="condicionParaDeshabilitarElGrupo"
>
...
</div>
Usando [attr.disabled]
y por qué no me gusta como opción.
Como hemos visto en el ejemplo inicial el vínculo a la propiedad disabled
no funciona y lanza un warning por consola.
Debido a ello en diversas ocasiones he visto como se sustituye este vínculo a la propiedad por el vínculo al atributo.
<input
type="text"
formControlName="direccion"
[attr.disabled]="tipoDeEntrega.value === 'recogidaEnTienda' ? 'disabled' : null"
/>
Como al vincular al atributo no podemos asignar a
true/false
, ya que eso no generaría un HTML válido, tenemos que modificar la sentencia de la asignación, asignando'disabled'
cuando la condición sea verdadera y anull
cuando esta sea falsa para que angular no añada este atributo.
En principio esta parece ser una alternativa válida ya que si compilamos la aplicación, el input se habilita y deshabilita correctamente. Pero esta opción tiene un problema. Y es que vinculando al atributo, el control de formulario únicamente se deshabilita en el DOM. Lo que quiere decir que el FormControl
de la clase no se ve afectado y por tanto el valor del mismo tampoco elimina del valor del formulario.
Usando esta alternativa lo único que estamos consiguiendo es implementar una especie de función de solo-lectura. Y es precisamente por esto, por lo que esta opción carece de sentido para mí. Ya que si esta es la funcionalidad que buscamos la podríamos conseguir vinculando directamente a la propiedad readonly
.
<input
type="text"
formControlName="direccion"
[readonly]="tipoDeEntrega.value === 'recogidaEnTienda'"
/>
Conclusiones Finales
Bien pues en este artículo hemos visto las dos opciones principales a la hora de habilitar/deshabilitar controles en los formularios reactivos.
La opción estándar y recomendada es usando el observable valueChanges
desde la clase sin necesidad de involucrar al template.
Pero si no estamos del todo cómodos con esta opción siempre podemos hacer uso de unas directivas del estilo de las que hemos visto.
Si deseas apoyar la creación de más contenido de este tipo, puedes hacerlo a través nuestro Paypal
Top comments (0)