DEV Community

Cover image for Cómo Deshabilitar Controles en un Formulario Reactivo
akotech
akotech

Posted on

Cómo Deshabilitar Controles en un Formulario Reactivo

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'"
/>
Enter fullscreen mode Exit fullscreen mode

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.

Warning de la consola

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')
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(''),
});
Enter fullscreen mode Exit fullscreen mode

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()
);
Enter fullscreen mode Exit fullscreen mode

En donde:

  1. Primero extraemos los controles del FormGroup
  2. 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.
  3. 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étodo enable() 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

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 o formControlName indicando esta circunstancia en el selector.
  • A continuación para obtener la referencia al FormControl asociado para poder habilitarlo/deshabilitarlo, hemos inyectado en el constructor NgControlcomo 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 directiva FormControlDirective o FormControlName, 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 del ngControl que hemos inyectado la referencia al FormControl a través de su propiedad control. Y a continuación, estamos deshabilitando el control si la condición proporcionada es true y habilitándolo si esta es false.

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],
  ...
})

Enter fullscreen mode Exit fullscreen mode

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'"
/>
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

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 por ControlContainer 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>
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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 a null 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'"
/>
Enter fullscreen mode Exit fullscreen mode

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)