Home > Enterprise >  What is the accepted way to handle getting Angular Form Control values into the template?
What is the accepted way to handle getting Angular Form Control values into the template?

Time:10-21

The Problem

In Angular when you call a function such as a public get value() { ... } in a template any time the template is re-rendered the function will be called again leading to many different calls. On many different questions that seem to be related to this topic they talk about using various approaches to get the values from a form control such as this.form.controls.your_control_here.value or this.form.get('your_control_here').value. However, either of these these approaches could not be referenced nicely in the template as they are both not very easy to follow (in a template) and using a function like above would lead to the duplication of calls which isn't preferable.

Possible Solutions

From what I could tell you might be able to use something like this.form.controls.your_control_here.valueChanges and pipe tap the value into another exposed variable that your template watches. However, given Angular/RxJS does not publish the value of the Observable that backs valueChanges you would miss out on the initial value of your form control. Along with that as far as I'm aware when you can you don't want to use subscribe in controller logic when possible as you must also manage unsubscribing the value and to instead prefer | async instead, but that goes back to the other valueChanges error of missing the first value.

The Question

What I am wondering is, is there an acceptable way to accomplish getting a value from an angular form into its associated template. While avoiding the issue of calling functions multiple times and preferably keeping to the ideal of using | async? Perhaps there is not but it feels a bit like a hack to add all of this extra stuff just to get a simple value to use in the template.

Here are some examples from a test application I made to try and work through this.

Controller Logic:

import { Component } from "@angular/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms";

@Component({
    selector: "app-inventory-setup-page",
    templateUrl: "./inventory-setup-page.component.html",
    styleUrls: ["./inventory-setup-page.component.scss"],
})
export class InventorySetupPageComponent implements OnInit {
    public createInventory = false;

    public inventoryType = this._formBuilder.group({
        options: new FormControl("/inventory/create/supplies", [Validators.required]),
    });

    constructor(private _formBuilder: FormBuilder) {
    }

    public startInventoryCreation() {
        this.createInventory = !this.createInventory;
    }

    // this would get called many times in the browser
    public get route(): string {
        console.log("This got called");
        return this.inventoryType.controls.options.value!;
    }

    public route$: Observable<string | null>;
    public ngOnInit() {
        // using this with `| async` in the template will miss the initial value of "/inventory/create/supplies" above
        this.route$ = this.inventoryType.controls.options.valueChanges;
    }
}

The Template

<section *ngIf="!createInventory">
  <h1>You do not have any inventory</h1>

  <button mat-button (click)="startInventoryCreation()">Add Inventory Item</button>
  <button mat-button>Import Inventory</button>
</section>

<div *ngIf="createInventory">
    <section>
        <h1>How will you track this item?</h1>

        <form [formGroup]="inventoryType">
            <ng-template matStepLabel>Select Inventory Type</ng-template>

            <mat-label>How will you track this item?</mat-label>
            <mat-radio-group formControlName="options" required>
                <mat-radio-button value="/inventory/create/supplies">Quantity</mat-radio-button>
                <mat-radio-button value="/inventory/create/equipment">Individually</mat-radio-button>
            </mat-radio-group>
        </form>
    </section>

    <section>
        {{ route }}
        <a mat-flat-button color="primary" [routerLink]="route">Create</a>
    </section>
</div>

CodePudding user response:

It's not related you your question, else the use of *ngIf to storing a conditional result in a variable in template.

Remember that you can always use a typical "create an object on fly"

<section *ngIf="{route:route$|async} as data">
    {{ data.route }}
    <a mat-flat-button color="primary" [routerLink]="data.route">Create</a>
</section>

Or the same using directly "inventoryType.get('options').value"

<section *ngIf="{route:inventoryType.get('options').value} as data">
    {{ data.route }}
    <a mat-flat-button color="primary" [routerLink]="data.route">Create</a>
</section>

Or only

<section *ngIf="inventoryType.get('options').value as route">
    {{ route }}
    <a mat-flat-button color="primary" [routerLink]="route">Create</a>
</section>

See that in this last case, until the control has a value, don't show anything

a stackblitz

CodePudding user response:

In Angular when you call a function such as a public get value() { ... } in a template any time the template is re-rendered the function will be called again leading to many different calls.

Correct. Whenever you use data binding, angular will evaluate the expression during every change detection. Therefore, expressions should not perform expensive operations needlessly.

(Note that it doesn't matter whether the code is in a function or in the expression itself, it get's executed either way. It's just that expensive operations often require so much code that people tend to put that code in functions, and that's why the advice is to not call functions in templates. But it's not the function itself that's the problem, it's expensive operations in data bound expressions)

In your case, the expression is very simple:

this.inventoryType.controls.options.value!

I'd estimate it takes at most 0.0000001 seconds to evaluate that, so unless you have millions of form controls, a human won't notice a difference.

Personally, I'd therefore tend to keep it simple, to make my life, and the life of whoever gets to maintain my code, as easy as possible, and therefore use that getter without remorse.

Or as Donald Knuth put it:

Premature optimization is the root of all evil.

CodePudding user response:

You can make use of Behavior Subject to have the initial value check below code which I have tested for you

 On ts file

  public router$: BehaviorSubject<any>;

  public ngOnInit() {
     this.router$ = new BehaviorSubject<any>(
     this.inventoryType.controls.options.value
  );

     this.inventoryType.controls.options.valueChanges.subscribe((v) => {
     this.router$.next(v);
     });

     }
   }

On Template

   <section *ngIf="router$ | async as route">
       {{ route }}
   </section>

check this working demo here

  • Related