0

I'm trying to migrate a simple template-driven form to use Angular Signals.

I’m on Angular 19 (standalone components, using FormsModule) and I would like to:

  • Keep one object for the product (product)

  • Bind that object directly to the form inputs

  • Keep fields linked: if I change the net price, the gross price should update (and vice versa), using the VAT rate.

Current (working) version with a mutable object + [(ngModel)]

import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; type Product = { name: string; vatRate: number; priceNet: number; priceGross: number; }; @Component({ selector: 'app-product-form', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './product-form.component.html', }) export class ProductFormComponent { product!: Product; ngOnInit(): void { this.product = { name: 'Banana', vatRate: 22, priceNet: 0, priceGross: 0, }; } save() { console.log(this.product); } calculateGrossFromNet() { this.product.priceGross = this.product.priceNet * (1 + this.product.vatRate / 100); } calculateNetFromGross() { this.product.priceNet = this.product.priceGross / (1 + this.product.vatRate / 100); } } 
<form (ngSubmit)="save()"> <div class="row"> <div class="col-6 col-lg-3"> <label for="productName">Name:</label> <input id="productName" name="productName" type="text" class="form-control" [(ngModel)]="product.name" /> </div> <div class="col-6 col-lg-3"> <label for="vatRate">VAT rate (%):</label> <input id="vatRate" name="vatRate" type="number" class="form-control" [(ngModel)]="product.vatRate" (keyup)="calculateGrossFromNet()" /> </div> <div class="col-6 col-lg-3"> <label for="priceNet">Price (net):</label> <input id="priceNet" name="priceNet" type="number" class="form-control" [(ngModel)]="product.priceNet" (keyup)="calculateGrossFromNet()" /> </div> <div class="col-6 col-lg-3"> <label for="priceGross">Price (gross):</label> <input id="priceGross" name="priceGross" type="number" class="form-control" [(ngModel)]="product.priceGross" (keyup)="calculateNetFromGross()" /> </div> </div> <button type="submit" class="btn btn-primary mt-3"> Save product </button> </form> 

This works exactly as I want: the product object is bound directly to the inputs, and the fields are linked (changing one price recalculates the other).

What I would like to do now is rewrite this using Angular Signals only.

Constraints / goals:

  • keep a single source of truth for the state (one `product` object if possible)

  • keep the fields linked: changing `priceNet` recalculates `priceGross` and the other way around, based on `vatRate`

  • avoid RxJS/Subjects/BehaviorSubjects – I want to use Signals as the main state mechanism

  • avoid effect() feedback loops where an effect writes back into the same signal that it depends on

2 Answers 2

0

Looking at your code, you've got an initial product object that you set up in ngOnInit, you bind some of the fields to various inputs, and then you have a keyup event listener that calculates some fields on the object when they change. Updating this to work with signals requires thinking a little bit differently.

You've got two fields that are just using plain two-way data binding. You can do two-way data binding with signals as per the docs. Once you do this you won't need FormModule anymore!

The other two inputs change each other when the other one changes. There's a bit of a problem here because if you just set this up as an effect that runs when the field changes, you've suddenly created an infinite loop (one effect will run, then the other one, then the first one, etc.).

If you keep the code the same and instead just set the signals on keyup like you're already doing, then this should work and you shouldn't run into issues. I'd recommend creating a signal per field instead of one signal for the entire object, as it's a bit more performant and lets you do derived data with computed signals a lot easier.

Sign up to request clarification or add additional context in comments.

1 Comment

Avoid infinite loop is nor dificult. Just store in a variable the old value
0

In Angular 19-20 has not very advantage using signal instead Template driven forms. The new Angular forms signals (in Angular 22) promise a better approach. See, e.g. this link

Your code with signal (see that use (ngModelChange) to call a function instead of keyUp.

@Component({ selector: 'app-root', imports:[FormsModule,JsonPipe], template: ` <h1>Hello from {{ name }}!</h1> <form (ngSubmit)="save()"> <div class="row"> <div class="col-6 col-lg-3"> <label for="productName">Name:</label> <input id="productName" name="productName" type="text" class="form-control" [(ngModel)]="product().name" /> </div> <div class="col-6 col-lg-3"> <label for="vatRate">VAT rate (%):</label> <input id="vatRate" name="vatRate" type="number" class="form-control" [ngModel]="product().vatRate" (ngModelChange)="change('vatRate',$event)" /> </div> <div class="col-6 col-lg-3"> <label for="priceNet">Price (net):</label> <input id="priceNet" name="priceNet" type="number" class="form-control" [ngModel]="product().priceNet" (ngModelChange)="change('priceNet',$event)" /> </div> <div class="col-6 col-lg-3"> <label for="priceGross">Price (gross):</label> <input id="priceGross" name="priceGross" type="number" class="form-control" [ngModel]="product().priceGross" (ngModelChange)="change('priceGross',$event)" /> </div> </div> <button type="submit" class="btn btn-primary mt-3"> Save product </button> </form> <pre>{{product()|json}}</pre> `, }) export class App implements OnInit{ name = 'Angular'; product=model<Product>({ name: 'Banana', vatRate: 22, priceNet: 0, priceGross: 0, }); constructor(){ } ngOnInit(): void { this.product.set({ name: 'Banana', vatRate: 22, priceNet: 0, priceGross: 0, }); } save() { console.log(this.product); } change(field:string,value:any) { const old={...this.product(),[field]:value} if (field=='priceNet' || field=='vatRate') old.priceGross=old.priceNet*(100+old.vatRate)/100 if (field=='priceGross') old.priceNet=100*old.priceGross/(100+old.vatRate) this.product.update(x=>old) } } 

We can try to use individual signal for varRate,priceNet and price and use computed to create the "product"

 vatRate=model(0 priceGross=model(0) priceNet=model(0) name=model('') product = computed(() => ({ name: this.name(), vatRate: this.vatRate(), priceNet: this.priceNet(), priceGross: this.priceGross(), })) 

We can then use effect to see when change any of the signal or when change the product. but we need store in a variable the old value of the signal. so

 productOld:Product={} as Product //define a variable to stroe the oldValue constructor() { effect(() => { const product = this.product(); if ( product.vatRate != this.productOld.vatRate || product.priceNet != this.productOld.priceNet ) { this.priceGross.set( Math.round(product.priceNet * (100 + product.vatRate)) / 100 ); } if (product.priceGross != this.productOld.priceGross) { this.priceNet.set( Math.round((product.priceGross * 10000) / (100 + product.vatRate)) / 100 ); } this.productOld = { ...this.product() }; }); } 

the last piece can be create a function to "patchValue" the signals

 setValue(product: Product) { this.vatRate.set(product.vatRate); this.priceGross.set(product.priceGross); this.priceNet.set(product.priceNet); this.name.set(product.name); } 

a stackblitz

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.