我如何知道自定义表单控件何时在 Angular 中被标记为原始?

问题描述 投票:0回答:5

我的 Angular 应用程序中有几个自定义表单控制组件,它们实现了

ControlValueAccessor
接口并且效果很好。

但是,当在父窗体上或直接在我的自定义控件上调用

markAsPristine()
时,我需要更新它的状态:我的自定义控件实际上有内部控件,我也需要在其上调用
markAsPristine()

那么,我如何知道何时在我的控件上调用

markAsPristine()

ControlValueAccessor
接口没有与此问题相关的成员,我可以实现它。

angular angular-forms
5个回答
24
投票

经过彻底调查,我发现这个功能并不是 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 的访问权限


1
投票

我的解决方法受到 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; } }
    

0
投票
根据 Slava 的回答,另一个建议是替换

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

 位置发出事件,但当然其他方法也可以。


0
投票
还有另一种方法可以检查表格是否脏了。我们可以比较form所绑定的对象。下面的函数可以用于对象属性比较

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 链接。 我已经测试过并且运行良好。

Stackblitz 链接


0
投票
我遇到了一个非常相似的问题,并且无需构建自定义 Angular 组件就能够解决它。

在我的用例中,我需要知道离开已编辑页面时的“脏”状态。编辑后的页面由一个父组件和多个子组件组成。

我认为我可以通过创建一个

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 } } } ];
    
© www.soinside.com 2019 - 2024. All rights reserved.