Cómo Deshabilitar Controles en un Formulario Reactivo
Tags: angular, spanish, forms
Published: 2023-02-16 14:32:20

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`:

```html
<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.

![Warning de la consola](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tbxeukapf9408nzfhcem.png)

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í._ {% embed https://youtu.be/_KKErXkKOcs %}

---

## 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. 

```typescript
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`

```typescript
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. 

```typescript
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:

```typescript
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:

```typescript
const { tipoDeEntrega, direccion } = this.deliveryForm.controls;

tipoDeEntrega.valueChanges.subscribe(  
  (tipoSeleccionado) => 
    tipoSeleccionado === 'recogidaEnTienda'
      ? direccion.disable()
      : direccion.enable()
);
```

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:

```typescript
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. 

```typescript
@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` 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 `NgControl`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 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:

```typescript
@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.

```html
<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`.

```typescript
@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` 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. 

```html
<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.**

```html
<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 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`.

```html
<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](https://www.paypal.com/donate/?hosted_button_id=8HJNHL8VTB8ZG)_