将 ngModel 绑定到复杂数据

Binding ngModel to complex data

一段时间以来,我一直在研究是否以及如何将复杂模型绑定到 ngModel。有一些文章展示了如何处理简单数据(例如字符串),例如 this。但我想做的更复杂。假设我有一个 class:

export class MyCoordinates {
    longitude: number;
    latitude: number;
}

现在我要在应用的多个地方使用它,所以我想把它封装成一个组件:

<coordinates-form></coordinates-form>

我还想使用 ngModel 将此模型传递给组件,以利用 angular 表单之类的东西,但到目前为止没有成功。这是一个例子:

<form #myForm="ngForm" (ngSubmit)="onSubmit()">
    <div class="form-group">
        <label for="name">Name</label>
        <input type="text" [(ngModel)]="model.name" name="name">
    </div>
    <div class="form-group">
        <label for="coordinates">Coordinates</label>
        <coordinates-form [(ngModel)]="model.coordinates" name="coordinates"></coordinates-form>
    </div>
    <button type="submit" class="btn btn-success">Submit</button>
</form>

实际上可以这样做还是我的方法完全错误?现在我已经决定使用具有正常输入和更改时发出事件的组件,但我觉得它会很快变得混乱。

import {
  Component,
  Optional,
  Inject,
  Input,
  ViewChild,
} from '@angular/core';

import {
  NgModel,
  NG_VALUE_ACCESSOR,
} from '@angular/forms';

import { ValueAccessorBase } from '../Base/value-accessor';
import { MyCoordinates } from "app/Models/Coordinates";

@Component({
  selector: 'coordinates-form',
  template: `
    <div>
      <label>longitude</label>
      <input
        type="number"
        [(ngModel)]="value.longitude"
      />

      <label>latitude</label>
      <input
        type="number"
        [(ngModel)]="value.latitude"
      />
    </div>
  `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: CoordinatesFormComponent,
    multi: true,
  }],
})
export class CoordinatesFormComponent extends ValueAccessorBase<MyCoordinates> {

  @ViewChild(NgModel) model: NgModel;

  constructor() {
    super();
  }
}

ValueAccessorBase:

import {ControlValueAccessor} from '@angular/forms';

export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
  private innerValue: T;

  private changed = new Array<(value: T) => void>();
  private touched = new Array<() => void>();

  get value(): T {
    return this.innerValue;
  }

  set value(value: T) {
    if (this.innerValue !== value) {
      this.innerValue = value;
      this.changed.forEach(f => f(value));
    }
  }

  writeValue(value: T) {
    this.innerValue = value;
  }

  registerOnChange(fn: (value: T) => void) {
    this.changed.push(fn);
  }

  registerOnTouched(fn: () => void) {
    this.touched.push(fn);
  }

  touch() {
    this.touched.forEach(f => f());
  }
}

用法:

<form #form="ngForm" (ngSubmit)="onSubmit(form.value)">

  <coordinates-form
    required
    hexadecimal
    name="coordinatesModel"
    [(ngModel)]="coordinatesModel">
  </coordinates-form>

  <button type="Submit">Submit</button>
</form>

我遇到的错误 Cannot read property 'longitude' of undefined。对于简单的模型,如字符串或数字,它可以正常工作。

value 属性 首先是 undefined

要解决此问题,您需要像这样更改绑定:

[ngModel]="value?.longitude" (ngModelChange)="value.longitude = $event"

并将其更改为 latitude

[ngModel]="value?.latitude" (ngModelChange)="value.latitude = $event"

更新

刚刚注意到您在设置器中 运行ning onChange 事件,因此您需要更改参考:

[ngModel]="value?.longitude" (ngModelChange)="handleInput('longitude', $event)"

[ngModel]="value?.latitude" (ngModelChange)="handleInput('latitude', $event)"

handleInput(prop, value) {
  this.value[prop] = value;
  this.value = { ...this.value };
}

Updated Plunker

Plunker Example with google map

更新 2

在处理自定义表单控件时需要实现这个接口:

export interface ControlValueAccessor {
  /**
   * Write a new value to the element.
   */
  writeValue(obj: any): void;

  /**
   * Set the function to be called when the control receives a change event.
   */
  registerOnChange(fn: any): void;

  /**
   * Set the function to be called when the control receives a touch event.
   */
  registerOnTouched(fn: any): void;

  /**
   * This function is called when the control status changes to or from "DISABLED".
   * Depending on the value, it will enable or disable the appropriate DOM element.
   *
   * @param isDisabled
   */
  setDisabledState?(isDisabled: boolean): void;
}

这是一个最小的实现:

export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
  // view => control
  onChange = (value: T) => {};
  onTouched = () => {};

  writeValue(value: T) {
    // control -> view
  }

  registerOnChange(fn: (_: any) => void): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }
}

它适用于任何类型的值。只需为您的案例实施它。

你不需要像这样的数组(见更新Plunker

private changed = new Array<(value: T) => void>();

当组件获得新值时,它会 运行 writeValue 您需要更新一些将在您的自定义模板中使用的值。在您的示例中,您正在更新 value 属性,它与模板中的 ngModel 一起使用。

当您需要将更改传播到 AbstractControl 时,您必须调用您在 registerOnChange.

中注册的 onChange 方法

我写了this.value = { ...this.value };因为它只是

this.value = Object.assign({}, this.value)

它将在您调用 onChange 方法的地方调用 setter 另一种方法是直接调用onChange,通常使用

this.onChange(this.value);

您可以在自定义组件中做任何您喜欢的事情。它可以有任何模板和任何嵌套组件。但是您必须为 ControlValueAccessor 实现逻辑才能使用 angular 表单。

如果你打开一些库,如 angular2 material 或 primeng,你可以找到很多如何实现此类控件的示例