我的 Angular 应用程序中有几个自定义表单控制组件,它们实现了
ControlValueAccessor
接口并且效果很好。
但是,当在父窗体上或直接在我的自定义控件上调用
markAsPristine()
时,我需要更新它的状态:我的自定义控件实际上有内部控件,我也需要在其上调用 markAsPristine()
。
那么,我如何知道何时在我的控件上调用
markAsPristine()
?
ControlValueAccessor
接口没有与此问题相关的成员,我可以实现它。
经过彻底调查,我发现这个功能并不是 Angular 专门提供的。我已经在官方存储库中发布了一个与此相关的问题,并且它已获得功能请求状态。我希望它会在不久的将来实施。
在那之前,这里有两种可能的解决方法:
markAsPristine()
@Component({
selector: 'my-custom-form-component',
templateUrl: './custom-form-component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: MyCustomFormComponent,
multi: true
}]
})
export class MyCustomFormComponent implements ControlValueAccessor, OnInit {
private control: AbstractControl;
ngOnInit() {
const origFunc = this.control.markAsPristine;
this.control.markAsPristine = function() {
origFunc.apply(this, arguments);
console.log('Marked as pristine!');
}
}
}
ngDoCheck
请注意,此解决方案的性能可能较低,但它为您提供了更好的灵活性,因为您可以监视原始状态何时更改。在上面的解决方案中,只有当调用
markAsPristine()
时才会通知您。
@Component({
selector: 'my-custom-form-component',
templateUrl: './custom-form-component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: MyCustomFormComponent,
multi: true
}]
})
export class MyCustomFormComponent implements ControlValueAccessor, DoCheck {
private control: AbstractControl;
private pristine = true;
ngDoCheck(): void {
if (this.pristine !== this.control.pristine) {
this.pristine = this.control.pristine;
if (this.pristine) {
console.log('Marked as pristine!');
}
}
}
}
如果您需要从组件访问
FormControl
实例,请参阅此问题:从 Angular 中的自定义表单组件获取对 FormControl 的访问权限。
我的解决方法受到 Slava 的帖子和 从 Angular 中的自定义表单组件访问 FormControl 的启发,并将模板表单 (ngModel) 和反应式表单混合在一起。
组件内的复选框控件反映脏/原始状态并将其状态报告回外部以形成组。因此,我可以将样式应用于基于类 ng-dirty、ng-valid 等的复选框输入 (<label>
) 控件。 我还没有实现 markAsTouched、markAsUntouched,因为它可以以类似的方式完成。StackBlitz 上的工作演示 示例组件代码为:
import { AfterViewInit, Component, Input, OnInit, Optional, Self, ViewChild } from "@angular/core";
import { ControlValueAccessor, NgControl, NgModel } from "@angular/forms";
@Component({
selector: "app-custom-checkbox-control",
template: '<input id="checkBoxInput"\
#checkBoxNgModel="ngModel"\
type="checkbox"\
name="chkbxname"\
[ngModel]="isChecked"\
(ngModelChange)="checkboxChange($event)"\
>\
<label for="checkBoxInput">\
{{description}}\
</label>\
<div>checkbox dirty state: {{checkBoxNgModel.dirty}}</div>\
<div>checkbox pristine state: {{checkBoxNgModel.pristine}}</div>',
styleUrls: ["./custom-checkbox-control.component.css"]
})
export class CustomCheckboxControlComponent
implements OnInit, AfterViewInit, ControlValueAccessor {
disabled: boolean = false;
isChecked: boolean = false;
@Input() description: string;
@ViewChild('checkBoxNgModel') checkBoxChild: NgModel;
constructor(@Optional() @Self() public ngControl: NgControl) {
if (this.ngControl != null) {
this.ngControl.valueAccessor = this;
}
}
checkboxChange(chk: boolean) {
console.log("in checkbox component: Checkbox changing value to: ", chk);
this.isChecked = chk;
this.onChange(chk);
}
ngOnInit() {}
ngAfterViewInit(): void {
debugger
this.checkBoxChild.control.setValidators(this.ngControl.control.validator);
const origFuncDirty = this.ngControl.control.markAsDirty;
this.ngControl.control.markAsDirty = () => {
origFuncDirty.apply(this.ngControl.control, arguments);
this.checkBoxChild.control.markAsDirty();
console.log('in checkbox component: Checkbox marked as dirty!');
}
const origFuncPristine = this.ngControl.control.markAsPristine;
this.ngControl.control.markAsPristine = () => {
origFuncPristine.apply(this.ngControl.control, arguments);
this.checkBoxChild.control.markAsPristine();
console.log('in checkbox component: Checkbox marked as pristine!');
}
}
//ControlValueAccessor implementations
writeValue(check: boolean): void {
this.isChecked = check;
}
onChange = (val: any) => {};
onTouched = () => {};
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
markAsDirty
类中的
markAsPristine
、
_updatePristine
和
FormGroup
方法:
ngOnInit(): void {
const markAsDirty = this.formGroup.markAsDirty;
this.formGroup.markAsDirty = (opts) => {
markAsDirty.apply(this.formGroup, opts);
console.log('>>>>> markAsDirty');
};
const markAsPristine = this.formGroup.markAsPristine;
this.formGroup.markAsPristine = (opts) => {
markAsPristine.apply(this.formGroup, opts);
console.log('>>>>> markAsPristine');
};
const updatePristine = this.formGroup['_updatePristine'];
this.formGroup['_updatePristine'] = (opts) => {
updatePristine.apply(this.formGroup, opts);
console.log('>>>>> updatePristine');
};
}
我在
console.log
位置发出事件,但当然其他方法也可以。
isEquivalent(a, b) {
// Create arrays of property names
var aProps = Object.getOwnPropertyNames(a);
var bProps = Object.getOwnPropertyNames(b);
// If number of properties is different,
// objects are not equivalent
if (aProps.length != bProps.length) {
return false;
}
for (var i = 0; i < aProps.length; i++) {
var propName = aProps[i];
// If values of same property are not equal,
// objects are not equivalent
if (a[propName] !== b[propName]) {
return false;
}
}
// If we made it this far, objects
// are considered equivalent
return true;
}
如果您想检查此内容,请使用下面的 stackblitz 链接。 我已经测试过并且运行良好。
在我的用例中,我需要知道离开已编辑页面时的“脏”状态。编辑后的页面由一个父组件和多个子组件组成。
我认为我可以通过创建一个
public get isDirty(): boolean
属性并在离开时使用 Angular 的
router
和
guard
访问它来确定其“脏”状态。“isDirty()”属性执行“dirtyCheck()”,查看子组件的“脏”状态并返回 true/false。
当添加到守卫并离开时,我们能够访问页面的脏状态。
export class ParentComponent, IDirtyComponent {
formGroup: FormGroup;
@ViewChild('childComponent1') childComponent1: ChildComponent1;
@ViewChild('childComponent2') childComponent2: ChildComponent2;
@ViewChild('childComponent3') childComponent3: ChildComponent3;
public get isDirty(): boolean {
return this.dirtyCheck();
}
constructor(private readonly fb: FormBuilder) {
this.formGroup = this.fb.group({
company: [null],
website: [null],
});
}
public dirtyCheck(): boolean {
if (this.formGroup.formGroup.dirty
|| this.childComponent1.formGroup.dirty
|| this.childComponent2.formGroup.dirty
|| this.childComponent3.formGroup.dirty) {
return true;
}
return false;
}
}
export class ChildComponent1 {
formGroup: FormGroup;
constructor(private readonly fb: FormBuilder) {
this.formGroup = this.fb.group({
name: [null],
title: [null],
website: [null],
});
this.loadData();
}
public loadData(): void {
this.formGroup.get('name').setValue('John Doe');
this.formGroup.get('title').setValue('CEO');
this.formGroup.get('website').setValue('https://abc123.com');
this.formGroup.markAsPristine();
}
}
export interface IDirtyComponent {
isDirty: boolean;
dirtyCheck(): boolean;
}
@Injectable({ providedIn: "root" })
export class DirtyCheckGuard implements CanDeactivate<IDirtyComponent> {
constructor(private modalSvc: FakeModalSvc) { }
canDeactivate(component: IDirtyComponent) {
if (component.isDirty === false) {
// continue editing
return true;
}
this.modalSvc.showIndicator();
return this.modalSvc.continueEditing$.pipe(
switchMap(continueEditing => {
// leave page
if (continueEditing === false) {
return of(true);
}
// continue editing
return of(false);
}),
take(1)
);
}
}
const routes: Routes =
[
{
path: "create",
component: ParentComponent,
canDeactivate: [DirtyCheckGuard],
data: {
title: "Create Company",
breadcrumb: {
label: "Create Company",
disable: false
}
}
}
];