将ngModel绑定到复杂数据

时间:2017-05-19 16:33:25

标签: html angular typescript angular-ngmodel angular-components

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

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

现在我将在应用程序的多个位置使用它,所以我想将它封装到一个组件中:

<coordinates-form></coordinates-form>

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

<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。对于简单的模型,如字符串或数字,它没有问题。

1 个答案:

答案 0 :(得分:2)

value属性首先是undefined

要解决此问题,您需要更改绑定,如:

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

并将其更改为latitude

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

更新

刚刚注意到您在settor中运行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时,您必须调用onChange中注册的registerOnChange方法。

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

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

它将调用setter,您可以在其中调用onChange方法 另一种方法是直接调用onChange(通常使用

this.onChange(this.value);

您可以在自定义组件中执行任何操作。它可以包含任何模板和任何嵌套组件。但是你必须为ControlValueAccessor实现逻辑以使用角形式。

如果你打开某些库这样的angular2材料或者primeng你可以找到很多例子来实现这样的控件