如何允许嵌套组件被其父级跟踪并从 Angular 中的父级获取值?
How to allow nested components to be tracked by their parent and get values from their parent in Angular?
我有一系列表单(每个表单由 1 个组件管理)。
我必须将这些形式的输入模式(例如,其中许多要求允许输入地址)重构为可重用的组件,因为它们以多种形式使用,我不想要既不复制他们的逻辑也不复制他们的模板。
每个可重用组件都必须
- 有它的道理
- 其模板包含输入标签但没有
<form>
标签
- 有其客户端验证约束
- 可能从其父项接收初始值
- 能够return其字段的值作为对象传递给父级(例如
address: {street: "...", "city": "...", ...}
)
- 如果不满足其验证约束,则使父表单无效
- 在用户更改其值后创建父表单"touched"
从 this tutorial 对于 Angular2,我了解了如何实现目标 1、2 和 4 .
本教程中的解决方案也可以实现其他目标,但它是通过从父级执行所有操作(参见app.component.ts#initAddress
)来实现的。
如何实现 3、5、6 和7,同时在子项中声明控件及其约束?
如果您想在子组件中提供所有内容,您可以尝试这样的操作。
import { Component, Input } from '@angular/core';
import { FormGroupDirective, ControlContainer, Validators, FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'address',
template: `
<div formGroupName="address">
<input formControlName="city" placeholder="city" (blur)="onTouched" />
<input formControlName="country" placeholder="country" (blur)="onTouched" />
<input formControlName="zipCode" placeholder="zipCode" (blur)="onTouched" />
</div>
`,
styles: [`h1 { font-family: Lato; }`],
viewProviders: [
{ provide: ControlContainer, useExisting: FormGroupDirective }
]
})
export class AddressComponent {
private form: FormGroup;
constructor(private parent: FormGroupDirective) { }
ngOnInit() {
this.form = this.parent.form;
const city = new FormControl('', Validators.required);
const country = new FormControl('', Validators.required);
const zipCode = new FormControl('', Validators.required);
const address = new FormGroup({ city, country, zipCode });
this.form.addControl('address', address);
}
}
用法:
import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'my-app',
template: `
<form [formGroup]="form">
<address></address>
</form>
{{ form.value | json }}
`,
styleUrls: ['./app.component.css'],
})
export class AppComponent {
form: FormGroup;
constructor() {
this.form = new FormGroup({});
}
}
请注意,我使用的是 ReactiveFormsModule
。
另请务必查看 Angular Forms - Kara Erickson。该演示文稿向您展示了如何创建一个可重用的地址组件,其中包含模板驱动和反应式表单的实现。
你不应该使用这样的实现。使用 ControlValueAccessor.
更干净
ControlValueAccessor 是一个接口,允许 angular 表单模块(经典或反应式)写入值或状态并注册回调以检索更改和事件。
writeValue(value: Address): void { } // Allows angular to set a default value to the component (used by FormControl or ngModel)
registerOnChange(fn: (_: any) => void): void {} // Callback to be called when the component value change.
registerOnTouched(fn: (_: any) => void): void { } // Callback to be called when a "touch" event occurs on the component
setDisabledState(isDisabled: boolean): void { } // Allows angular to update the component disable state.
但是你还需要提供一个NG_VALUE_ACCESSOR
我为地址组件编写了这个快速而肮脏的示例:
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Address } from './address';
@Component({
selector: 'app-address-input',
template: `
<label for="number">Num: </label>
<input type="number" [disabled]="disabled" name="number" id="number" (change)="numberUpdate($event)" value="{{value.num}}"/><br />
<label for="street">Street: </label>
<input type="text" [disabled]="disabled" (change)="streetUpdate($event)"name="street" id="street" value="{{value.street}}" /><br />
<label for="city">City: </label>
<input type="text" [disabled]="disabled" name="city" id="city" value="{{value.city}}" (change)="cityUpdate($event)" /><br />
<label for="zipCode">Zip Code: </label>
<input type="text" [disabled]="disabled" name="zipCode" id="zipCode" value="{{value.zipCode}}" (change)="zipCodeUpdate($event)" /><br />
<label for="state">State: </label>
<input type="text" [disabled]="disabled" name="state" id="state" value="{{value.state}}" (change)="stateUpdate($event)" /><br />
<label for="country">Country: </label>
<input type="text" [disabled]="disabled" name="country" id="country" value="{{value.country}}" (change)="countryUpdate($event)" />`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AddressInputComponent) // forward the reference,
multi: true // allow multiple component in the same form
}]
})
export class AddressInputComponent implements ControlValueAccessor {
private _onChange = (_: any) => {};
private _onTouched = (_: any) => {};
disabled = false;
private _value: Address = {num: undefined, street: undefined, city: undefined, state: undefined, zipCode: undefined, country: undefined}; // current value (Address is just an interface)
set value(value: Address) { // interceptor for updating current value
this._value = value;
this._onChange(this._value);
}
get value() {
return this._value;
}
writeValue(value: Address): void {
if (value && value !== null) {
this._value = value;
}
}
registerOnChange(fn: (_: any) => void): void {
this._onChange = fn;
}
registerOnTouched(fn: (_: any) => void): void {
this._onTouched = fn; // didn't used it but you should for touch screen enabled devices.
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
numberUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'num')
}
streetUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'street')
}
cityUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'city')
}
zipCodeUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'zipCode')
}
stateUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'state')
}
countryUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'country');
}
private _updateValue(value: any, field: string) {
const newValue = this._value;
newValue[field] = value;
this.value = newValue;
}
}
然后在表单中像使用任何其他表单元素一样使用它:
<form [formGroup]="registerForm">
<app-address-input formControlName="address"></app-address-input>
</form>
您可以在组件中添加更多逻辑。这是一个工作示例。请记住这是一个简单的示例,应该返工以获得更清晰的代码。
我有一系列表单(每个表单由 1 个组件管理)。
我必须将这些形式的输入模式(例如,其中许多要求允许输入地址)重构为可重用的组件,因为它们以多种形式使用,我不想要既不复制他们的逻辑也不复制他们的模板。
每个可重用组件都必须
- 有它的道理
- 其模板包含输入标签但没有
<form>
标签 - 有其客户端验证约束
- 可能从其父项接收初始值
- 能够return其字段的值作为对象传递给父级(例如
address: {street: "...", "city": "...", ...}
) - 如果不满足其验证约束,则使父表单无效
- 在用户更改其值后创建父表单"touched"
从 this tutorial 对于 Angular2,我了解了如何实现目标 1、2 和 4 .
本教程中的解决方案也可以实现其他目标,但它是通过从父级执行所有操作(参见app.component.ts#initAddress
)来实现的。
如何实现 3、5、6 和7,同时在子项中声明控件及其约束?
如果您想在子组件中提供所有内容,您可以尝试这样的操作。
import { Component, Input } from '@angular/core';
import { FormGroupDirective, ControlContainer, Validators, FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'address',
template: `
<div formGroupName="address">
<input formControlName="city" placeholder="city" (blur)="onTouched" />
<input formControlName="country" placeholder="country" (blur)="onTouched" />
<input formControlName="zipCode" placeholder="zipCode" (blur)="onTouched" />
</div>
`,
styles: [`h1 { font-family: Lato; }`],
viewProviders: [
{ provide: ControlContainer, useExisting: FormGroupDirective }
]
})
export class AddressComponent {
private form: FormGroup;
constructor(private parent: FormGroupDirective) { }
ngOnInit() {
this.form = this.parent.form;
const city = new FormControl('', Validators.required);
const country = new FormControl('', Validators.required);
const zipCode = new FormControl('', Validators.required);
const address = new FormGroup({ city, country, zipCode });
this.form.addControl('address', address);
}
}
用法:
import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'my-app',
template: `
<form [formGroup]="form">
<address></address>
</form>
{{ form.value | json }}
`,
styleUrls: ['./app.component.css'],
})
export class AppComponent {
form: FormGroup;
constructor() {
this.form = new FormGroup({});
}
}
请注意,我使用的是 ReactiveFormsModule
。
另请务必查看 Angular Forms - Kara Erickson。该演示文稿向您展示了如何创建一个可重用的地址组件,其中包含模板驱动和反应式表单的实现。
你不应该使用这样的实现。使用 ControlValueAccessor.
更干净ControlValueAccessor 是一个接口,允许 angular 表单模块(经典或反应式)写入值或状态并注册回调以检索更改和事件。
writeValue(value: Address): void { } // Allows angular to set a default value to the component (used by FormControl or ngModel)
registerOnChange(fn: (_: any) => void): void {} // Callback to be called when the component value change.
registerOnTouched(fn: (_: any) => void): void { } // Callback to be called when a "touch" event occurs on the component
setDisabledState(isDisabled: boolean): void { } // Allows angular to update the component disable state.
但是你还需要提供一个NG_VALUE_ACCESSOR
我为地址组件编写了这个快速而肮脏的示例:
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Address } from './address';
@Component({
selector: 'app-address-input',
template: `
<label for="number">Num: </label>
<input type="number" [disabled]="disabled" name="number" id="number" (change)="numberUpdate($event)" value="{{value.num}}"/><br />
<label for="street">Street: </label>
<input type="text" [disabled]="disabled" (change)="streetUpdate($event)"name="street" id="street" value="{{value.street}}" /><br />
<label for="city">City: </label>
<input type="text" [disabled]="disabled" name="city" id="city" value="{{value.city}}" (change)="cityUpdate($event)" /><br />
<label for="zipCode">Zip Code: </label>
<input type="text" [disabled]="disabled" name="zipCode" id="zipCode" value="{{value.zipCode}}" (change)="zipCodeUpdate($event)" /><br />
<label for="state">State: </label>
<input type="text" [disabled]="disabled" name="state" id="state" value="{{value.state}}" (change)="stateUpdate($event)" /><br />
<label for="country">Country: </label>
<input type="text" [disabled]="disabled" name="country" id="country" value="{{value.country}}" (change)="countryUpdate($event)" />`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AddressInputComponent) // forward the reference,
multi: true // allow multiple component in the same form
}]
})
export class AddressInputComponent implements ControlValueAccessor {
private _onChange = (_: any) => {};
private _onTouched = (_: any) => {};
disabled = false;
private _value: Address = {num: undefined, street: undefined, city: undefined, state: undefined, zipCode: undefined, country: undefined}; // current value (Address is just an interface)
set value(value: Address) { // interceptor for updating current value
this._value = value;
this._onChange(this._value);
}
get value() {
return this._value;
}
writeValue(value: Address): void {
if (value && value !== null) {
this._value = value;
}
}
registerOnChange(fn: (_: any) => void): void {
this._onChange = fn;
}
registerOnTouched(fn: (_: any) => void): void {
this._onTouched = fn; // didn't used it but you should for touch screen enabled devices.
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
numberUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'num')
}
streetUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'street')
}
cityUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'city')
}
zipCodeUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'zipCode')
}
stateUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'state')
}
countryUpdate(event: any) {
// additional check or process
this._updateValue(event.target.value, 'country');
}
private _updateValue(value: any, field: string) {
const newValue = this._value;
newValue[field] = value;
this.value = newValue;
}
}
然后在表单中像使用任何其他表单元素一样使用它:
<form [formGroup]="registerForm">
<app-address-input formControlName="address"></app-address-input>
</form>
您可以在组件中添加更多逻辑。这是一个工作示例。请记住这是一个简单的示例,应该返工以获得更清晰的代码。