通过查询组件的模板获取 FormControl 实例

Get a hold of a FormControl instance by querying a component's template

我有一个自定义 FormFieldComponent,它封装了表单字段的 HTML 和错误显示逻辑:

@Component({
  selector: 'field',
  template: `
    <div class="form-group">
      <label class="control-label">{{label}}</label>
      <ng-content></ng-content>  <!-- Will display the field -->
      <!-- Here, put error display logic -->
    </div>
  `
})
export class FormFieldComponent {
  @Input() label: string;  // Field label
  @Input() theControl: FormControl;  // Current form control, required to display errors
}

FormFieldComponent 中,我需要一个 FormControl 的实例来显示错误。

我的表单如下所示:

<form [formGroup]="myForm">
  ...
  <field label="Title" [theControl]="myForm.get('title')">
    <input type="text" formControlName="title">
  </field>
  ...
</form>

但我对上面的代码并不完全满意。如您所见,我在两个位置指定字段的键:在 [theControl] 输入 属性 和 formControlName 指令中。

如果我能这样写代码会更简洁:

<field label="Title">
  <input type="text" formControlName="title">
</field>

注意 [theControl] 输入 属性 是如何消失的。 FieldComponent 应该能够获取它包含的 FormControl 实例,但是如何获取?

我曾尝试使用 @ContentChildren 装饰器查询组件的模板以获取 FormControl 指令,但它不起作用:

export class FormFieldComponent {
  @ContentChildren(FormControlDirective) theControl: any;
}

另一种选择是将字段的键作为输入传递给 FormFieldComponent,然后让组件使用该键来:

简答:你不能

你就是做不到。 (好吧,也许你可以,但这会很麻烦!)

长答案:你不能,但是...

FormControl 不可注射。指令是可注入的,但是,您将不得不处理 formControlNamengModelformControl 等,它们不能从包装组件访问,但它的子组件...

对于您的情况,您可以尝试使用 @ContentChildren(FormControlName) theControl: any;,因为您的代码中没有隐含 FormControlDirective,但无论如何您都无法访问 FormControl(属性 _control 是内部的,所以这将是一个 hack)...

所以你应该坚持从处理 FormGroup.

的组件中管理你的错误

BUT 如果您想显示自定义输入(不会按原样显示错误消息,但能够显示此输入处于错误状态(宿主元素将得到 ng-validng-invalid 类,所以这只是样式问题),您可以通过实现 ControlValueAccessor.

A bridge between a control and a native element.

A ControlValueAccessor abstracts the operations of writing a new value to a DOM element representing an input control.

这意味着 directives/components 实现此接口可以与 ngModelformControl 等一起使用...

例如:<my-component [(ngModel)]="foo"></my-component>

这不是您问题的精确再现,但此实现为我解决了同类问题:

export const INPUT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => InputComponent),
  multi: true
};

@Component({
  selector: "field",
  template: `<!--put anything you want in your template-->
            <label>{{label}}</label>
            <input #input (input)="onChange($event.target.value)" (blur)="onTouched()" type="text">`,
  styles: [],
  providers: [INPUT_VALUE_ACCESSOR]
})
export class InputComponent implements ControlValueAccessor {
  @ViewChild("input")
  input: ElementRef;
  @Input()
  label:string;
  onChange = (_: any) => { };
  onTouched = () => { };

  constructor(private _renderer: Renderer) { }

  writeValue(value: any): void {
    const normalizedValue = value == null ? "" : value;
    this._renderer.setElementProperty(this.input.nativeElement, "value", normalizedValue);
  }

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

  setDisabledState(isDisabled: boolean): void {
    this._renderer.setElementProperty(this.input.nativeElement, "disabled", isDisabled);
  }
}

那么你可以:

<field label="Title" formControlName="title"></field>

您可以通过以下方式获取 Form Control Name 实例:

@Component({
  selector: 'field',
  templateUrl: './field.component.html',
  styleUrls: ['./field.component.scss']
})
export class FieldComponent implements AfterContentInit {
  @Input()
  public label: string;

  @ContentChild(FormControlName)
  public controlName: FormControlName;

  public ngAfterContentInit(): void {
    console.log(this.controlName.control);
  }
}

不确定事情是否发生了变化,但我找到了一种看似受支持的方法来做到这一点here

基本上,您可以使用 @ContentChild 装饰器,并根据您的表单控件或您将其链接到模板控件的方式,使用正确的指令作为装饰器的选择器。

例如,如果我正在投影一个使用案例的输入,[formControl],您与该 GitHub 票证的作者在同一条船上。对我来说,我使用的是 'formGroupName' 指令,虽然令人困惑,但该指令的正确用法是 @ContentChild(FormControlName) control;

HTH!