角度形式的复杂对象的信号双向绑定

问题描述 投票:0回答:1

我正在寻求有关使用信号双向绑定的最佳实践的指导,特别是对于模板驱动形式的复杂对象。我们的团队广泛使用模板驱动的表单,并欣赏双向绑定输入的简单性。我们也对新的“模型”输入感到兴奋,它似乎是为此目的量身定制的。

目前,我们使用 viewModel 信号来保存表单的状态。这些 viewModel 通常是对象,这似乎会导致双向绑定问题。

问题:

  1. 对象属性上的双向绑定不会更新信号。
  2. 没有直接的方法来控制它,而不放弃双向绑定或为每个输入使用单独的信号,这对于较大的表单来说变得很麻烦。

示例代码: 您可以将代码粘贴到此处:https://angular.dev/playground

import {
    ChangeDetectionStrategy,
    Component,
    computed,
    effect,
    signal,
    WritableSignal
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
    selector: 'app-root',
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
    imports: [FormsModule],
    template: `
        <!-- using two-way-binding with an object signal -->
        <!-- this will never update the signal "properly" -->
        <input type="text" [(ngModel)]="viewModel().name" />

        <!-- is this the recommended way? -->
        <!-- of course I could use primitive values for two-way-bindings only -->
        <!-- but with larger forms this seems not so nice -->
        <!-- especially when using multiple components with the model input -->
        <input
            type="text"
            [ngModel]="viewModel().name"
            (ngModelChange)="nameChange($event)"
        />

        <br />
        <!-- this will be updated because of the change detection that gets triggered by the input -->
        <!-- the signal never notifies, because the reference is not changed -->
        {{ viewModel().name }}

        <br />
        <button (click)="onClick()">click</button>
        computed: {{ testComputed().name }}
    `
})
export class CookieRecipe {
    viewModel: WritableSignal<{ name: string }> = signal({ name: 'startName' });

    testComputed = computed(() => {
        // this will not be triggered, because the reference of the signal value is not changed.
        console.warn('inside computed', this.viewModel());
        return this.viewModel();
    });

    constructor() {
        effect(() => {
            // this will not be triggered, because the reference of the signal value is not changed.
            console.warn('inside effect', this.viewModel());
        });
    }

    onClick() {
        console.warn('button clicked', this.viewModel());
        // the set here will change the reference and therefore the signal will update the effect, the computed and the view
        this.viewModel.set({ name: 'buttonClick' });
    }

    nameChange(name: string) {
        this.viewModel.set({ ...this.viewModel, name });
    }

    ngDoCheck() {
        console.warn('inside ngDoCheck');
    }
}

bootstrapApplication(CookieRecipe);

问题:

  1. 有推荐的方法来处理这种情况吗?
  2. 有计划在未来的 Angular 版本中解决这个问题吗?
  3. 如果没有,当前使用信号管理复杂表单状态同时保持双向绑定便利性的最佳实践是什么?

任何见解或建议将不胜感激。谢谢!

angular angular-forms angular-signals
1个回答
0
投票

一种方法是采取自下而上的方法。

  1. 将各个属性设置为双向绑定
    model
    ,这将处理绑定问题。
name = model('startName');
password = model('startNamePassword');
      <label for="name">Name:</label>
      <input type="text" [(ngModel)]="name"
          id="name"
          name="name" />
      <br/>
      <label for="password">Password:</label>
      <input
          type="text"
          id="password"
          name="password"
          [(ngModel)]="password"
      />
  1. 然后我们创建一个
    computed
    ,收集各个模型的所有单独发射并为您提供计算值。
  viewModel: Signal<{ name: string; password: string }> = computed(() => {
    console.warn('inside computed');
    return {
      name: this.name(),
      password: this.password(),
    };
  });

完整代码:

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  signal,
  WritableSignal,
  Signal,
  model,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [FormsModule],
  template: `
      <br/>
      <label for="name">Name:</label>
      <input type="text" [(ngModel)]="name"
          id="name"
          name="name" />
      <br/>
      <label for="password">Password:</label>
      <input
          type="text"
          id="password"
          name="password"
          [(ngModel)]="password"
      />
      <br />
      {{ viewModel().name }}
      <br />
      <button (click)="onClick()">click</button>
      computed: {{ viewModel().name }}
  `,
})
export class CookieRecipe {
  name = model('startName');
  password = model('startNamePassword');
  viewModel: Signal<{ name: string; password: string }> = computed(() => {
    console.warn('inside computed');
    return {
      name: this.name(),
      password: this.password(),
    };
  });

  constructor() {
    effect(() => {
      // this will not be triggered, because the reference of the signal value is not changed.
      console.warn('inside effect', this.viewModel());
    });
  }

  onClick() {
    console.warn('button clicked', this.viewModel());
  }

  ngDoCheck() {
    console.warn('inside ngDoCheck');
  }
}

bootstrapApplication(CookieRecipe);

Stackblitz 演示

© www.soinside.com 2019 - 2024. All rights reserved.