Deactivate Form Group using a custom directive

I'm struggling with implementing a directive that can be applied to elements with the [formGroup] attribute in order to disable the entire form group and its form controls based on a condition, rather than manually calling this.formGroup.disable().

I found a directive for form controls online:

import { Directive, Input } from "@angular/core";
import { NgControl } from "@angular/forms";

// use this directive instead of manually enabling and disabling with reactive forms
@Directive({
  selector: "([formControlName], [formControl])[disabledControl]",
})
export class DisabledControlDirective {
  @Input() set disabledControl(condition: boolean) {
    if (this.disabled !== undefined) {
      this.toggleControl(condition);
    }
    this.disabled = condition;
  }

  disabled: boolean;

  constructor(private readonly ngControl: NgControl) {}

  ngOnInit() {
    this.toggleControl(this.disabled);
  }

  toggleControl(condition: boolean) {
    const action = condition ? "disable" : "enable";
    this.ngControl.control[action]();
  }
}

I attempted something similar for form groups:

import { Directive, Input } from "@angular/core";
import { ControlContainer } from "@angular/forms";

@Directive({
  selector: "([formGroup])[disabledGroup]",
})
export class DisabledGroupDirective {
  @Input() set disabledGroup(condition: boolean) {
    if (this.disabled !== undefined) {
      this.toggleGroup(condition);
    }
    this.disabled = condition;
  }

  disabled: boolean;

  constructor(private readonly controlContainer: ControlContainer) {}

  ngOnInit() {
    this.toggleGroup(this.disabled);
  }

  toggleGroup(condition: boolean) {
    const action = condition ? "disable" : "enable";
    this.controlContainer.control[action]();
  }
}

However, the form group is not being properly disabled. Even though the status shows as "DISABLED" in the debugger, when I resume the application, the form loads without being disabled. Further inspection via a button click reveals that the status has reverted back to "INVALID." Any insights on how to resolve this issue?

Edit: I created a basic Stackblitz demo, where it seems to work fine: https://stackblitz.com/edit/angular-ivy-owlvqu?file=src/app/app.component.ts

Edit2: No other parts of the code manipulate the form group, except for adding it to a parent formGroup using addControl() within ngOnInit(). Even after removing this part, the disabling functionality still doesn't work, unlike in the Stackblitz example. It's puzzling why the status keeps resetting.

As a workaround, I tried switching from ngOnInit() to ngAfterViewInit() in the group directive, and oddly, it worked - the form group got disabled. However, I'm unsure if this is the most elegant solution. Ideally, I would like to understand why the formGroup's status changes unexpectedly, as there are no other triggers in the code.

Answer №1

When applying the directive to a formGroup, it is necessary to have access to the formGroup in the constructor. To achieve this, the FormGroupDirective must be injected.

  constructor(private readonly form: FormGroupDirective) {}
A toggle function can be created that receives a FormGroup as an input parameter. Since a FormGroup is an object, iteration using Object.keys is required.
  toggleGroup(form: FormGroup, condition: boolean) {
    const action = condition ? 'disable' : 'enable';
    Object.keys(form.controls).forEach(key => {
        const control = form.get(key);
        control[action]();
      });
    }
  }

To call this function with the FormGroup (which is under the FormGroupDirective.form), it can be done from our input element.

  @Input() set disabledGroup(condition: boolean) {
    this.toggleGroup(this.form.form, condition);
    this.disabled = condition;
  }

The directive works effectively if only FormControls are present within the FormGroup. However, what happens if there is a sub FormGroup or a FormArray inside our FormGroup? It becomes necessary to cater for these scenarios and iterate differently based on the type of FormControl encountered. This requires making the function recursive. Thus, the function can be transformed into:

  toggleGroup(form: FormGroup | FormArray, condition: boolean) {
    const action = condition ? 'disable' : 'enable';
    if (form instanceof FormArray) {  //if it is a FormArray
      form.controls.forEach(x => {
        if (x instanceof FormControl) x[action]();
        if (x instanceof FormGroup || x instanceof FormArray)
         this.toggleGroup(x, condition);
      });
    } else {  //if it is a FormGroup
      Object.keys(form.controls).forEach(key => {
        const control = form.get(key);
        if (control instanceof FormControl) control[action]();
        if (control instanceof FormGroup || control instanceof FormArray)
         this.toggleGroup(control, condition);
      });
    }
  }

You can find the directive implementation in this stackblitz link.

This updated version simply utilizes the form property directly (note that the formGroup is located under form.form).

  @Input() set disabledGroup(condition: boolean) {
    const action = condition ? 'disable' : 'enable';
    this.form.form[action]();
  }

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can I iterate through a variable in TypeScript?

initialize() { var elements = []; for (let i = 1; i <= 4; i++) { let node = { name: `Node ${i}` }; elements.push({ [`node${i}`]: node }); if (i < 4) { let edge = { source: `node${i}`, target: ...

The Google Cloud storage public URL access permission has been denied

I attempted to access the following URL but encountered an access denied message. Is there a specific permission required for this? Here is the exact error I received: The anonymous caller lacks the necessary storage.objects.get access to retrieve the ...

Insert a new row below an existing row within a Material table

Is it possible to dynamically insert a new row under a specific existing row in a table within the DOM without having to redefine all of the data in the MatTableDataSource? ...

When retrieving objects using Angular's HttpClient, properties may be null or empty

I am working with a service class in Angular that utilizes the HttpClient to retrieve data from a web service. The web service responds with a JSON object structured like this: { "id": "some type of user id", "name": "The name of the user", "permiss ...

I need help differentiating "namespace" from "static attributes" in TypeScript

Separating namespace from static properties in TypeScript can sometimes be tricky. error: 'ClassA' only refers to a type, but is being used as a namespace here. class ClassA { static ClassB = class { }; } type T = ClassA // ok type T = C ...

Experimenting with a VSCode extension that requires the act of launching a folder/workspace

Currently, I am developing a custom VSCode extension that considers the path of the file being opened within the workspace. To create a reproducible test scenario, I want to open the test folder itself in VSCode and then proceed to open the test file with ...

Guide on dynamically loading Chart.js onto Gridster upon clicking a button

I am looking to create a dynamic dashboard that can display information with the option to choose different charts for representation. For my project, I integrated the angular-gridster2 package to enable a dynamic grid layout. You can find more details at ...

Mastering the Art of Sharing PrimgNg Selected Checkboxes with Child Component Dropdown

I am developing a simple application using PrimeNg. In order to pass information from the selected items of a Multi-Select component in the parent element (<p-multiSelect/>) to a Dropdown component in the child element (<p-dropdown/>), I have i ...

Getting the value from an Observable in Angular 2 after using the CanActivate guard and subscribing

I am using a "CanActivate" method in my web service to check if the user is logged in. The "CanActivate" method returns an Observable. My main concern is how to retrieve this boolean value so that I can adjust my view based on whether the user is connecte ...

The recommended filename in Playwright within a Docker environment is incorrectly configured and automatically defaults to "download."

Trying to use Playwright to download a file and set the filename using download.suggestedFilename(). Code snippet: const downloadPromise = page.waitForEvent('download', {timeout:100000}) await page.keyboard.down('Shift') await p ...

Using the Post method in Angular will result in a 400 status code being returned

I am a student developer working with the Angular platform. I have my own API and a post method that looks like this: [HttpPost] [Route("login")] public async Task<ActionResult<User>> LoginUser([FromBody] User _user) { var joinedUser = _co ...

When using Angular, a service can be declared in the viewProviders of the parent component and then accessed in the viewProviders of the child

Imagine a scenario where there is a parent component called AppComponent, a directive named MyDirective, and a service named SimpleService. In this case, MyDirective is utilized in the template of AppComponent and injects SimpleService using the Host deco ...

Using Angular with adal-angular4 and implementing refresh token functionality

I have incorporated the Azure AD authentication in my Angular application using the package mentioned below: https://www.npmjs.com/package/adal-angular4 However, I am facing an issue where the token expires after 10-20 minutes. I have searched through va ...

Guide on incorporating finos/perspective into an Angular 8 project using https://perspective.finos.org/

I am looking for guidance on incorporating Finos/Perspective Datagrid into my Angular 8 application. I need to input data in JSON format and have it output as a Datagrid. Any sample code or examples would be greatly appreciated. You can find the GitHub re ...

Encountering Issues with Route Query Parameters in Nuxt/Vue Template using TypeScript

I am currently working on a Nuxt.js project with TypeScript and am facing an issue with route query parameters. The specific problem arises when TypeScript throws a type error related to a route query parameter called userType. The issue occurs when I att ...

Displaying components on Ionic using Server-side rendering

Can an entire app be rendered from a server? Essentially building only the initial framework as an APK and having the rest dynamically changed? My interpretation is that an APK is similar to a Single Page Application (SPA) in terms of packaging and sendin ...

Dealing with arrays in Typescript and flattening them using the RX

Struggling with a problem involving RXJS transformation in an Ionic 2 application. My goal is to flatten a JSON file into objects, here is the simplified JSON structure: [{ "language": "it", "labels": { "name": "Hi", }, "t ...

Error: 'process' is not defined in this TypeScript environment

Encountering a typescript error while setting up a new project with express+ typescript - unable to find the name 'process'https://i.stack.imgur.com/gyIq0.png package.json "dependencies": { "express": "^4.16.4", "nodemon": "^1.18.7", ...

Construct a string by combining the elements of a multi-dimensional array of children, organized into grouped

My task involves manipulating a complex, deeply nested array of nodes to create a specific query string structure. The desired format for the query string is as follows: (FULL_NAME="x" AND NOT(AGE="30" OR AGE="40" AND (ADDRESS ...

What is the best way to extract data from a request when using an HTTP callable function?

I've integrated the Firebase Admin SDK for handling administrative tasks. The functions I've set up are hosted on Firebase Cloud Function in my console. While I can successfully trigger these functions from my application, I'm facing an issu ...