Repository: IduxFE/components
Pull request #144 (2021-01-17 14:50:50)

refactor(cdk:forms): add dirty status and marks `valueRef` to readonly
re #115

## PR Checklist

Please check if your PR fulfills the following requirements:

- [x] The commit message follows [our guidelines](https://github.com/IduxFE/components/blob/main/docs/contributing.zh.md#commit)
- [x] Tests for the changes have been added/updated or not needed
- [x] Docs and demo have been added/updated or not needed

## PR Type

What kind of change does this PR introduce?

- [ ] Bugfix
- [x] Feature
- [ ] Component style update
- [ ] Code style update (formatting, local variables)
- [x] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] Application (the showcase website) / infrastructure changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

## What is the new behavior?

## Other information


codecov[bot]: # [Codecov](https://codecov.io/gh/IduxFE/components/pull/144?src=pr&el=h1) Report
> Merging [#144](https://codecov.io/gh/IduxFE/components/pull/144?src=pr&el=desc) (6029040) into [main](https://codecov.io/gh/IduxFE/components/commit/0cdc3ac4070801a5e9e7ee9985b33ac8ff362077?el=desc) (0cdc3ac) will **decrease** coverage by `0.03%`.
> The diff coverage is `98.50%`.

[![Impacted file tree graph](https://codecov.io/gh/IduxFE/components/pull/144/graphs/tree.svg?width=650&height=150&src=pr&token=PGAUXP06V3)](https://codecov.io/gh/IduxFE/components/pull/144?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main     #144      +/-   ##
==========================================
- Coverage   97.42%   97.39%   -0.04%     
==========================================
  Files          59       59              
  Lines        1050     1113      +63     
  Branches      178      189      +11     
==========================================
+ Hits         1023     1084      +61     
  Misses         10       10              
- Partials       17       19       +2     
```


| [Impacted Files](https://codecov.io/gh/IduxFE/components/pull/144?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/cdk/forms/src/controls/abstractControl.ts](https://codecov.io/gh/IduxFE/components/pull/144/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL2Zvcm1zL3NyYy9jb250cm9scy9hYnN0cmFjdENvbnRyb2wudHM=) | `98.13% <96.00%> (-1.87%)` | :arrow_down: |
| [packages/cdk/forms/src/controls/formArray.ts](https://codecov.io/gh/IduxFE/components/pull/144/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL2Zvcm1zL3NyYy9jb250cm9scy9mb3JtQXJyYXkudHM=) | `100.00% <100.00%> (ø)` | |
| [packages/cdk/forms/src/controls/formControl.ts](https://codecov.io/gh/IduxFE/components/pull/144/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL2Zvcm1zL3NyYy9jb250cm9scy9mb3JtQ29udHJvbC50cw==) | `100.00% <100.00%> (ø)` | |
| [packages/cdk/forms/src/controls/formGroup.ts](https://codecov.io/gh/IduxFE/components/pull/144/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL2Zvcm1zL3NyYy9jb250cm9scy9mb3JtR3JvdXAudHM=) | `100.00% <100.00%> (ø)` | |
| [packages/cdk/forms/src/typeof.ts](https://codecov.io/gh/IduxFE/components/pull/144/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL2Zvcm1zL3NyYy90eXBlb2YudHM=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/IduxFE/components/pull/144?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/IduxFE/components/pull/144?src=pr&el=footer). Last update [0cdc3ac...6029040](https://codecov.io/gh/IduxFE/components/pull/144?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/packages/cdk/forms/src/controls/abstractControl.ts b/packages/cdk/forms/src/controls/abstractControl.ts
index 29daccce..5f1f05df 100644
--- a/packages/cdk/forms/src/controls/abstractControl.ts
+++ b/packages/cdk/forms/src/controls/abstractControl.ts
@@ -24,7 +24,7 @@ export abstract class AbstractControl<T = any> {
   /**
    * The ref value for the control.
    */
-  readonly valueRef!: any
+  readonly valueRef!: DeepReadonly<Ref<any>>
 
   /**
    * The validation status of the control, there are three possible validation status values:
@@ -32,37 +32,52 @@ export abstract class AbstractControl<T = any> {
    * * **invalid**: This control has failed at least one validation check.
    * * **validating**: This control is in the midst of conducting a validation check.
    */
-  readonly status: DeepReadonly<Ref<ValidationStatus>>
+  readonly status!: DeepReadonly<Ref<ValidationStatus>>
 
   /**
    * An object containing any errors generated by failing validation, or null if there are no errors.
    */
-  readonly errors: DeepReadonly<Ref<ValidationErrors | null>>
+  readonly errors!: DeepReadonly<Ref<ValidationErrors | null>>
 
   /**
    * A control is valid when its `status` is `valid`.
    */
-  readonly valid: ComputedRef<boolean>
+  readonly valid!: ComputedRef<boolean>
 
   /**
    * A control is invalid when its `status` is `invalid`.
    */
-  readonly invalid: ComputedRef<boolean>
+  readonly invalid!: ComputedRef<boolean>
 
   /**
    * A control is validating when its `status` is `validating`.
    */
-  readonly validating: ComputedRef<boolean>
+  readonly validating!: ComputedRef<boolean>
 
   /**
    * A control is marked `blurred` once the user has triggered a `blur` event on it.
    */
-  readonly blurred: ComputedRef<boolean>
+  readonly blurred!: ComputedRef<boolean>
 
   /**
    * A control is `unblurred` if the user has not yet triggered a `blur` event on it.
    */
-  readonly unblurred: ComputedRef<boolean>
+  readonly unblurred!: ComputedRef<boolean>
+
+  /**
+   * A control is `dirty` if the user has changed the value in the UI.
+   */
+  readonly dirty!: ComputedRef<boolean>
+
+  /**
+   * A control is `pristine` if the user has not yet changed the value in the UI.
+   */
+  readonly pristine!: ComputedRef<boolean>
+
+  /**
+   * A collection of child controls.
+   */
+  readonly controls: Partial<Record<keyof T, AbstractControl>> | AbstractControl[] | null = null
 
   /**
    * The parent control.
@@ -94,13 +109,14 @@ export abstract class AbstractControl<T = any> {
     return this._trigger ?? this._parent?.trigger ?? 'change'
   }
 
-  protected _status = ref<ValidationStatus>('valid')
-  protected _errors = ref<ValidationErrors | null>(null)
+  protected _valueRef!: Ref<any>
+  protected _status!: Ref<ValidationStatus>
+  protected _errors!: Ref<ValidationErrors | null>
   protected _blurred = ref(false)
+  protected _dirty = ref(false)
 
-  protected _validators: ValidatorFn | null = null
-  protected _asyncValidators: AsyncValidatorFn | null = null
-
+  private _validators: ValidatorFn | null = null
+  private _asyncValidators: AsyncValidatorFn | null = null
   private _parent: FormGroup<T> | FormArray<T[]> | null = null
   private _trigger?: TriggerType
 
@@ -108,14 +124,6 @@ export abstract class AbstractControl<T = any> {
     validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null,
     asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
   ) {
-    this.status = readonly(this._status)
-    this.errors = readonly(this._errors)
-    this.valid = computed(() => this.status.value === 'valid')
-    this.invalid = computed(() => this.status.value === 'invalid')
-    this.validating = computed(() => this.status.value === 'validating')
-    this.blurred = computed(() => this._blurred.value)
-    this.unblurred = computed(() => !this._blurred.value)
-
     this._convertOptions(validatorOrOptions, asyncValidator)
   }
 
@@ -146,6 +154,16 @@ export abstract class AbstractControl<T = any> {
    */
   abstract markAsUnblurred(): void
 
+  /**
+   * Marks the control as `dirty`.
+   */
+  abstract markAsDirty(): void
+
+  /**
+   * Marks the control as `pristine`.
+   */
+  abstract markAsPristine(): void
+
   /**
    * Running validations manually, rather than automatically.
    */
@@ -217,7 +235,7 @@ export abstract class AbstractControl<T = any> {
    */
   getError(errorCode: keyof ErrorMessages, path?: Array<string | number> | string): ValidationError | null {
     const control = path ? this.get(path) : this
-    return control?.errors?.value?.[errorCode] || null
+    return control?._errors?.value?.[errorCode] || null
   }
 
   /**
@@ -245,8 +263,8 @@ export abstract class AbstractControl<T = any> {
    * @param cb The callback when the value changes
    * @param options Optional options of watch, the default value of `deep` is `true`
    */
-  watchValue(cb: WatchCallback<T, T | undefined>, options?: WatchOptions): WatchStopHandle {
-    return watch(this.valueRef, cb, { deep: true, ...options })
+  watchValue(cb: WatchCallback<T | null, T | null | undefined>, options?: WatchOptions): WatchStopHandle {
+    return watch(this._valueRef, cb, { deep: true, ...options })
   }
 
   /**
@@ -259,29 +277,68 @@ export abstract class AbstractControl<T = any> {
     cb: WatchCallback<ValidationStatus, ValidationStatus | undefined>,
     options?: WatchOptions,
   ): WatchStopHandle {
-    return watch(this.status, cb, options)
+    return watch(this._status, cb, options)
   }
 
   protected async _validate(): Promise<ValidationErrors | null> {
-    const value = this.getValue()
-    let newErrors = this._runValidator(value)
-    if (isNil(newErrors)) {
-      newErrors = await this._runAsyncValidator(value)
+    let newErrors = null
+    let value = null
+    if (this._validators) {
+      value = this.getValue()
+      newErrors = this._validators(value, this)
+    }
+    if (isNil(newErrors) && this._asyncValidators) {
+      value = this._validators ? value : this.getValue()
+      this._status.value = 'validating'
+      newErrors = await this._asyncValidators(value, this)
     }
     this.setErrors(newErrors)
     return newErrors
   }
 
-  private _runValidator(value: any): ValidationErrors | null {
-    return this._validators ? this._validators(value, this) : null
+  protected _initAllStatus() {
+    ;(this as any).valueRef = readonly(this._valueRef)
+    this._initErrorsAndStatus()
+    ;(this as any).status = readonly(this._status)
+    ;(this as any).errors = readonly(this._errors)
+    ;(this as any).valid = computed(() => this._status.value === 'valid')
+    ;(this as any).invalid = computed(() => this._status.value === 'invalid')
+    ;(this as any).validating = computed(() => this._status.value === 'validating')
+    ;(this as any).blurred = computed(() => this._blurred.value)
+    ;(this as any).unblurred = computed(() => !this._blurred.value)
+    ;(this as any).dirty = computed(() => this._dirty.value)
+    ;(this as any).pristine = computed(() => !this._dirty.value)
   }
 
-  private _runAsyncValidator(value: any): Promise<ValidationErrors | null> {
-    if (!this._asyncValidators) {
-      return Promise.resolve(null)
+  private _initErrorsAndStatus() {
+    let errors = null
+    let value = null
+    if (this._validators) {
+      value = this.getValue()
+      errors = this._validators(value, this)
+    }
+
+    let status: ValidationStatus = errors ? 'invalid' : 'valid'
+    if (status === 'valid' && this.controls) {
+      for (const key in this.controls) {
+        const controlStatus = (this.controls as any)[key].status.value
+        if (controlStatus === 'invalid') {
+          status = 'invalid'
+          break
+        }
+      }
+    }
+
+    this._errors = ref(errors)
+    this._status = ref(status)
+    if (status === 'valid' && this._asyncValidators) {
+      value = this._validators ? value : this.getValue()
+      this._status.value = 'validating'
+      this._asyncValidators(value, this).then(asyncErrors => {
+        this._errors.value = asyncErrors
+        this._status.value = asyncErrors ? 'invalid' : 'valid'
+      })
     }
-    this._status.value = 'validating'
-    return this._asyncValidators(value, this)
   }
 
   private _convertOptions(
diff --git a/packages/cdk/forms/src/controls/formArray.ts b/packages/cdk/forms/src/controls/formArray.ts
index 4c0d8f55..a38cdbf7 100644
--- a/packages/cdk/forms/src/controls/formArray.ts
+++ b/packages/cdk/forms/src/controls/formArray.ts
@@ -1,6 +1,6 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
-import type { Ref, UnwrapRef, WatchStopHandle } from 'vue'
+import type { DeepReadonly, Ref, UnwrapRef, WatchStopHandle } from 'vue'
 import type { AsyncValidatorFn, ValidatorFn, ValidatorOptions, ValidationErrors, ValidationStatus } from '../types'
 
 import { ref, watch, watchEffect } from 'vue'
@@ -10,7 +10,7 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
   /**
    * The ref value for the form array.
    */
-  readonly valueRef: Ref<Array<UnwrapRef<T>>>
+  readonly valueRef!: DeepReadonly<Ref<Array<UnwrapRef<T>>>>
 
   /**
    * Length of the control array.
@@ -19,21 +19,30 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
     return this.controls.length
   }
 
+  protected _valueRef!: Ref<Array<DeepReadonly<UnwrapRef<T>>>>
+
   private _statusWatchStopHandle: WatchStopHandle | null = null
   private _blurredWatchStopHandle: WatchStopHandle | null = null
+  private _dirtyWatchStopHandle: WatchStopHandle | null = null
 
   constructor(
-    public controls: AbstractControl[],
+    /**
+     * An array of child controls. Each child control is given an index where it is registered.
+     */
+    public readonly controls: AbstractControl[],
     validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null,
     asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
   ) {
     super(validatorOrOptions, asyncValidator)
     controls.forEach(control => control.setParent(this as any))
-    this.valueRef = ref(this._calculateValue(controls))
+    this._valueRef = ref(this._calculateValue()) as Ref<Array<DeepReadonly<UnwrapRef<T>>>>
+
+    this._initAllStatus()
 
     this._watchValid()
     this._watchStatus()
     this._watchBlurred()
+    this._watchDirty()
   }
 
   /**
@@ -53,9 +62,7 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
   push(control: AbstractControl): void {
     this.controls.push(control)
     this._registerControl(control)
-    this.valueRef.value = this._calculateValue(this.controls)
-    this._watchStatus()
-    this._watchBlurred()
+    this._refreshValueAndWatch()
   }
 
   /**
@@ -67,9 +74,7 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
   insert(index: number, control: AbstractControl): void {
     this.controls.splice(index, 0, control)
     this._registerControl(control)
-    this.valueRef.value = this._calculateValue(this.controls)
-    this._watchStatus()
-    this._watchBlurred()
+    this._refreshValueAndWatch()
   }
 
   /**
@@ -79,9 +84,7 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
    */
   removeAt(index: number): void {
     this.controls.splice(index, 1)
-    this.valueRef.value = this._calculateValue(this.controls)
-    this._watchStatus()
-    this._watchBlurred()
+    this._refreshValueAndWatch()
   }
 
   /**
@@ -93,9 +96,7 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
   setControl(index: number, control: AbstractControl): void {
     this.controls.splice(index, 1, control)
     this._registerControl(control)
-    this.valueRef.value = this._calculateValue(this.controls)
-    this._watchStatus()
-    this._watchBlurred()
+    this._refreshValueAndWatch()
   }
 
   /**
@@ -139,6 +140,20 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
     this.controls.forEach(control => control.markAsUnblurred())
   }
 
+  /**
+   * Marks all controls of the form array as `dirty`.
+   */
+  markAsDirty(): void {
+    this.controls.forEach(control => control.markAsDirty())
+  }
+
+  /**
+   * Marks all controls of the form array as `pristine`.
+   */
+  markAsPristine(): void {
+    this.controls.forEach(control => control.markAsPristine())
+  }
+
   /**
    * Running validations manually, rather than automatically.
    */
@@ -149,9 +164,9 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
 
   private _watchValid() {
     watch(
-      [this.valueRef, this.blurred],
-      () => {
-        if (this.trigger === 'change' || (this.trigger === 'blur' && this.blurred.value)) {
+      [this._valueRef, this._blurred],
+      ([_, blurred]) => {
+        if (this.trigger === 'change' || (this.trigger === 'blur' && blurred)) {
           this._validate()
         }
       },
@@ -164,7 +179,7 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
       this._statusWatchStopHandle()
     }
     this._statusWatchStopHandle = watchEffect(() => {
-      let status: ValidationStatus = this.errors.value ? 'invalid' : 'valid'
+      let status: ValidationStatus = this._errors.value ? 'invalid' : 'valid'
       if (status === 'valid') {
         for (const control of this.controls) {
           const controlStatus = control.status.value
@@ -196,8 +211,31 @@ export class FormArray<T = any[]> extends AbstractControl<T> {
     })
   }
 
-  private _calculateValue(controls: AbstractControl<T>[]) {
-    return controls.map(control => control.valueRef)
+  private _watchDirty() {
+    if (this._dirtyWatchStopHandle) {
+      this._dirtyWatchStopHandle()
+    }
+    this._dirtyWatchStopHandle = watchEffect(() => {
+      let dirty = false
+      for (const control of this.controls) {
+        if (control.dirty.value) {
+          dirty = true
+          break
+        }
+      }
+      this._dirty.value = dirty
+    })
+  }
+
+  private _calculateValue() {
+    return this.controls.map(control => control.valueRef) as Array<DeepReadonly<UnwrapRef<T>>>
+  }
+
+  private _refreshValueAndWatch() {
+    this._valueRef.value = this._calculateValue()
+    this._watchStatus()
+    this._watchBlurred()
+    this._watchDirty()
   }
 
   private _registerControl(control: AbstractControl<T>) {
diff --git a/packages/cdk/forms/src/controls/formControl.ts b/packages/cdk/forms/src/controls/formControl.ts
index e054a5c8..c1db3f05 100644
--- a/packages/cdk/forms/src/controls/formControl.ts
+++ b/packages/cdk/forms/src/controls/formControl.ts
@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import type { Ref, UnwrapRef } from 'vue'
+import type { DeepReadonly, Ref, UnwrapRef } from 'vue'
 import type { AsyncValidatorFn, ValidatorFn, ValidatorOptions, ValidationErrors } from '../types'
 
 import { ref, watch } from 'vue'
@@ -9,43 +9,52 @@ export class FormControl<T = any> extends AbstractControl<T> {
   /**
    * The ref value for the control.
    */
-  readonly valueRef: Ref<UnwrapRef<T> | null>
+  readonly valueRef!: DeepReadonly<Ref<UnwrapRef<T> | null>>
+
+  protected _valueRef: Ref<UnwrapRef<T> | null>
 
   private _initValue: T | null = null
+
   constructor(
     initValue: T | null = null,
     validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null,
     asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
   ) {
     super(validatorOrOptions, asyncValidator)
-    this.valueRef = ref(initValue)
+    this._valueRef = ref(initValue)
     this._initValue = initValue
 
-    this._watchEffect()
+    this._initAllStatus()
+
+    this._watchValid()
+    this._watchStatus()
   }
 
   /**
-   * Resets the form control, marking it `unblurred`, and setting the value to initialization value.
+   * Resets the form control, marking it `unblurred`, `pristine`,
+   * and setting the value to initialization value.
    */
   reset(): void {
-    this.valueRef.value = this._initValue as any
+    this._valueRef.value = this._initValue as any
     this.markAsUnblurred()
+    this.markAsPristine()
   }
 
   /**
-   * Sets a new value for the form control.
+   * Sets a new value for the form control, marking it `dirty`.
    *
    * @param value The new value.
    */
   setValue(value: T | null): void {
-    this.valueRef.value = value as any
+    this._valueRef.value = value as any
+    this.markAsDirty()
   }
 
   /**
    * The aggregate value of the form control.
    */
   getValue(): T | null {
-    return this.valueRef.value as T
+    return this._valueRef.value as T
   }
 
   /**
@@ -62,6 +71,20 @@ export class FormControl<T = any> extends AbstractControl<T> {
     this._blurred.value = false
   }
 
+  /**
+   *  Marks the control as `dirty`.
+   */
+  markAsDirty(): void {
+    this._dirty.value = true
+  }
+
+  /**
+   *  Marks the control as `pristine`.
+   */
+  markAsPristine(): void {
+    this._dirty.value = false
+  }
+
   /**
    * Running validations manually, rather than automatically.
    */
@@ -69,14 +92,16 @@ export class FormControl<T = any> extends AbstractControl<T> {
     return this._validate()
   }
 
-  private _watchEffect() {
-    watch([this.valueRef, this.blurred], () => {
-      if (this.trigger === 'change' || (this.trigger === 'blur' && this.blurred.value)) {
+  private _watchValid() {
+    watch([this._valueRef, this._blurred], ([_, blurred]) => {
+      if (this.trigger === 'change' || (this.trigger === 'blur' && blurred)) {
         this._validate()
       }
     })
+  }
 
-    watch(this.errors, errors => {
+  private _watchStatus() {
+    watch(this._errors, errors => {
       this._status.value = errors ? 'invalid' : 'valid'
     })
   }
diff --git a/packages/cdk/forms/src/controls/formGroup.ts b/packages/cdk/forms/src/controls/formGroup.ts
index 5666439e..edb42c1e 100644
--- a/packages/cdk/forms/src/controls/formGroup.ts
+++ b/packages/cdk/forms/src/controls/formGroup.ts
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
-import type { Ref, UnwrapRef, WatchStopHandle } from 'vue'
+import type { DeepReadonly, Ref, UnwrapRef, WatchStopHandle } from 'vue'
 import type { AsyncValidatorFn, ValidatorFn, ValidatorOptions, ValidationErrors, ValidationStatus } from '../types'
 
 import { ref, watch, watchEffect } from 'vue'
@@ -12,23 +12,32 @@ export class FormGroup<T = Record<string, any>> extends AbstractControl<T> {
   /**
    * The ref value for the form group.
    */
-  readonly valueRef: Ref<Partial<Record<keyof T, Ref<UnwrapRef<T>>>>>
+  readonly valueRef!: DeepReadonly<Ref<Partial<Record<keyof T, DeepReadonly<Ref<UnwrapRef<T>>>>>>>
+
+  protected _valueRef!: Ref<Partial<Record<keyof T, DeepReadonly<Ref<UnwrapRef<T>>>>>>
 
   private _statusWatchStopHandle: WatchStopHandle | null = null
   private _blurredWatchStopHandle: WatchStopHandle | null = null
+  private _dirtyWatchStopHandle: WatchStopHandle | null = null
 
   constructor(
-    public controls: Partial<Record<keyof T, AbstractControl>>,
+    /**
+     * A collection of child controls. The key for each child is the name under which it is registered.
+     */
+    public readonly controls: Partial<Record<keyof T, AbstractControl>>,
     validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null,
     asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
   ) {
     super(validatorOrOptions, asyncValidator)
     this._forEachChild(control => control.setParent(this as any))
-    this.valueRef = ref(this._calculateValue(controls)) as Ref<Partial<Record<keyof T, Ref<UnwrapRef<T>>>>>
+    this._valueRef = ref(this._calculateValue()) as Ref<Partial<Record<keyof T, DeepReadonly<Ref<UnwrapRef<T>>>>>>
+
+    this._initAllStatus()
 
     this._watchValid()
     this._watchStatus()
     this._watchBlurred()
+    this._watchDirty()
   }
 
   /**
@@ -39,9 +48,7 @@ export class FormGroup<T = Record<string, any>> extends AbstractControl<T> {
    */
   addControl(name: keyof T, control: AbstractControl): void {
     this._registerControl(name, control)
-    this.valueRef.value = this._calculateValue(this.controls)
-    this._watchStatus()
-    this._watchBlurred()
+    this._refreshValueAndWatch()
   }
 
   /**
@@ -51,9 +58,7 @@ export class FormGroup<T = Record<string, any>> extends AbstractControl<T> {
    */
   removeControl(name: keyof T): void {
     delete this.controls[name]
-    this.valueRef.value = this._calculateValue(this.controls)
-    this._watchStatus()
-    this._watchBlurred()
+    this._refreshValueAndWatch()
   }
 
   /**
@@ -65,9 +70,7 @@ export class FormGroup<T = Record<string, any>> extends AbstractControl<T> {
   setControl(name: keyof T, control: AbstractControl): void {
     delete this.controls[name]
     this._registerControl(name, control)
-    this.valueRef.value = this._calculateValue(this.controls)
-    this._watchStatus()
-    this._watchBlurred()
+    this._refreshValueAndWatch()
   }
 
   /**
@@ -113,6 +116,20 @@ export class FormGroup<T = Record<string, any>> extends AbstractControl<T> {
     this._forEachChild(control => control.markAsUnblurred())
   }
 
+  /**
+   * Marks all controls of the form group as `dirty`.
+   */
+  markAsDirty(): void {
+    this._forEachChild(control => control.markAsDirty())
+  }
+
+  /**
+   * Marks all controls of the form group as `pristine`.
+   */
+  markAsPristine(): void {
+    this._forEachChild(control => control.markAsPristine())
+  }
+
   /**
    * Running validations manually, rather than automatically.
    */
@@ -123,9 +140,9 @@ export class FormGroup<T = Record<string, any>> extends AbstractControl<T> {
 
   private _watchValid() {
     watch(
-      [this.valueRef, this.blurred],
-      () => {
-        if (this.trigger === 'change' || (this.trigger === 'blur' && this.blurred.value)) {
+      [this._valueRef, this._blurred],
+      ([_, blurred]) => {
+        if (this.trigger === 'change' || (this.trigger === 'blur' && blurred)) {
           this._validate()
         }
       },
@@ -138,7 +155,7 @@ export class FormGroup<T = Record<string, any>> extends AbstractControl<T> {
       this._statusWatchStopHandle()
     }
     this._statusWatchStopHandle = watchEffect(() => {
-      let status: ValidationStatus = this.errors.value ? 'invalid' : 'valid'
+      let status: ValidationStatus = this._errors.value ? 'invalid' : 'valid'
       if (status === 'valid') {
         for (const key in this.controls) {
           const controlStatus = this.controls[key]!.status.value
@@ -170,16 +187,39 @@ export class FormGroup<T = Record<string, any>> extends AbstractControl<T> {
     })
   }
 
-  private _calculateValue(controls: Partial<Record<keyof T, AbstractControl<T>>>) {
-    const value = {} as Partial<Record<keyof T, Ref<UnwrapRef<T>>>>
+  private _watchDirty() {
+    if (this._dirtyWatchStopHandle) {
+      this._dirtyWatchStopHandle()
+    }
+    this._dirtyWatchStopHandle = watchEffect(() => {
+      let dirty = false
+      for (const key in this.controls) {
+        if (this.controls[key]!.dirty.value) {
+          dirty = true
+          break
+        }
+      }
+      this._dirty.value = dirty
+    })
+  }
+
+  private _calculateValue() {
+    const value = {} as Partial<Record<keyof T, DeepReadonly<Ref<UnwrapRef<T>>>>>
 
-    Object.keys(controls).forEach(key => {
-      value[key as keyof T] = controls[key as keyof T]!.valueRef
+    Object.keys(this.controls).forEach(key => {
+      value[key as keyof T] = this.controls[key as keyof T]!.valueRef
     })
 
     return value
   }
 
+  private _refreshValueAndWatch() {
+    this._valueRef.value = this._calculateValue()
+    this._watchStatus()
+    this._watchBlurred()
+    this._watchDirty()
+  }
+
   private _registerControl(name: keyof T, control: AbstractControl<T>) {
     if (hasOwnProperty(this.controls, name as string)) {
       return
diff --git a/packages/cdk/forms/src/typeof.ts b/packages/cdk/forms/src/typeof.ts
index 085fa02b..411b22fc 100644
--- a/packages/cdk/forms/src/typeof.ts
+++ b/packages/cdk/forms/src/typeof.ts
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import type { FormArray } from './controls/formArray'
-import type { FormControl } from './controls/formControl'
 import type { FormGroup } from './controls/formGroup'
+import type { FormControl } from './controls/formControl'
 
 import { hasOwnProperty, isArray } from '@idux/cdk/utils'
 import { AbstractControl } from './controls/abstractControl'
@@ -17,7 +17,7 @@ export const isFormArray = (val: unknown): val is FormArray => {
 
 // Since AbstractControl be dependent on the function, `val instanceof FormGroup` cannot be used here.
 export const isFormGroup = (val: unknown): val is FormGroup => {
-  return isAbstractControl(val) && hasOwnProperty(val, 'controls') && !isArray((val as FormGroup).controls)
+  return isAbstractControl(val) && !isFormControl(val) && !isFormArray(val)
 }
 
 export const isFormControl = (val: unknown): val is FormControl => {


Test patch:
diff --git a/packages/cdk/forms/__tests__/abstractControl.spec.ts b/packages/cdk/forms/__tests__/abstractControl.spec.ts
index fb1c6df6..bbc04a33 100644
--- a/packages/cdk/forms/__tests__/abstractControl.spec.ts
+++ b/packages/cdk/forms/__tests__/abstractControl.spec.ts
@@ -5,33 +5,37 @@ import { AsyncValidatorFn, ValidationErrors, ValidatorFn, ValidatorOptions } fro
 import { Validators } from '../src/validators'
 
 class Control<T = unknown> extends AbstractControl<T> {
-  readonly valueRef: Ref<T | null> = ref(null)
+  _valueRef: Ref<T | null> = ref(null)
   constructor(
     validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null,
     asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
   ) {
     super(validatorOrOptions, asyncValidator)
+
+    this._initAllStatus()
+
     this._watchEffect()
   }
   reset(): void {}
   setValue(value: T | null): void {
-    this.valueRef.value = value
+    this._valueRef.value = value
   }
   getValue(): T {
-    return this.valueRef.value as T
+    return this._valueRef.value as T
   }
   markAsBlurred(): void {}
   markAsUnblurred(): void {}
+  markAsDirty(): void {}
+  markAsPristine(): void {}
   async validate(): Promise<ValidationErrors | null> {
     return this._validate()
   }
-
   private _watchEffect() {
-    watch([this.valueRef, this.blurred], () => {
+    watch([this._valueRef, this._blurred], () => {
       this._validate()
     })
 
-    watch(this.errors, errors => {
+    watch(this._errors, errors => {
       this._status.value = errors ? 'invalid' : 'valid'
     })
   }
@@ -45,20 +49,16 @@ describe('abstractControl.ts', () => {
       control = new Control()
     })
 
-    test('init status work', () => {
+    test('init all status work', () => {
       expect(control.status.value).toEqual('valid')
+      expect(control.errors.value).toBeNull()
       expect(control.valid.value).toEqual(true)
       expect(control.invalid.value).toEqual(false)
       expect(control.validating.value).toEqual(false)
-    })
-
-    test('init errors work', () => {
-      expect(control.errors.value).toBeNull()
-    })
-
-    test('init blurred work', () => {
       expect(control.blurred.value).toEqual(false)
       expect(control.unblurred.value).toEqual(true)
+      expect(control.dirty.value).toEqual(false)
+      expect(control.pristine.value).toEqual(true)
     })
 
     test('init props work', () => {
@@ -74,7 +74,7 @@ describe('abstractControl.ts', () => {
       expect(await control.validate()).toEqual({ required: { message: '' } })
 
       control.setValidator([email, minLength(5)])
-      control.valueRef.value = 'test'
+      control.setValue('test')
 
       expect(await control.validate()).toEqual({
         email: { message: '', actual: 'test' },
@@ -215,7 +215,7 @@ describe('abstractControl.ts', () => {
       control = new Control(Validators.required, _asyncValidator)
       expect(await control.validate()).toEqual({ required: { message: '' } })
 
-      control.valueRef.value = 'test'
+      control.setValue('test')
       control.validate()
 
       expect(control.status.value).toEqual('validating')
diff --git a/packages/cdk/forms/__tests__/formArray.spec.ts b/packages/cdk/forms/__tests__/formArray.spec.ts
index 17c39341..6aa5323c 100644
--- a/packages/cdk/forms/__tests__/formArray.spec.ts
+++ b/packages/cdk/forms/__tests__/formArray.spec.ts
@@ -143,18 +143,28 @@ describe('formArray.ts', () => {
 
     test('markAsBlurred and markAsUnblurred work', async () => {
       array.markAsBlurred()
-
       await flushPromises()
 
       expect(array.blurred.value).toEqual(true)
 
       array.markAsUnblurred()
-
       await flushPromises()
 
       expect(array.blurred.value).toEqual(false)
     })
 
+    test('markAsDirty and markAsPristine work', async () => {
+      array.markAsDirty()
+      await flushPromises()
+
+      expect(array.dirty.value).toEqual(true)
+
+      array.markAsPristine()
+      await flushPromises()
+
+      expect(array.dirty.value).toEqual(false)
+    })
+
     test('validate work', async () => {
       expect(await array.validate()).toBeNull()
 
@@ -187,9 +197,6 @@ describe('formArray.ts', () => {
     test('default change work', async () => {
       array = new FormArray([newFormGroup()], { validators: _validator })
 
-      array.setValue([{ control: '1234' }])
-      await flushPromises()
-
       expect(array.invalid.value).toEqual(true)
       expect(array.hasError('test')).toEqual(true)
 
@@ -198,28 +205,37 @@ describe('formArray.ts', () => {
 
       expect(array.invalid.value).toEqual(false)
       expect(array.hasError('test')).toEqual(false)
+
+      array.setValue([{ control: '1234' }])
+      await flushPromises()
+
+      expect(array.invalid.value).toEqual(true)
+      expect(array.hasError('test')).toEqual(true)
     })
 
     test('blur trigger validate work', async () => {
       array = new FormArray([newFormGroup()], { trigger: 'blur', validators: _validator })
 
-      array.setValue([{ control: '1234' }])
-      await flushPromises()
-
-      expect(array.invalid.value).toEqual(false)
-      expect(array.hasError('test')).toEqual(false)
+      expect(array.invalid.value).toEqual(true)
+      expect(array.hasError('test')).toEqual(true)
 
-      array.markAsBlurred()
+      array.setValue([{ control: 'test' }])
       await flushPromises()
 
       expect(array.invalid.value).toEqual(true)
       expect(array.hasError('test')).toEqual(true)
 
-      array.setValue([{ control: 'test' }])
+      array.markAsBlurred()
       await flushPromises()
 
       expect(array.invalid.value).toEqual(false)
       expect(array.hasError('test')).toEqual(false)
+
+      array.setValue([{ control: '1234' }])
+      await flushPromises()
+
+      expect(array.invalid.value).toEqual(true)
+      expect(array.hasError('test')).toEqual(true)
     })
 
     test('submit trigger validate work', async () => {
@@ -237,23 +253,17 @@ describe('formArray.ts', () => {
         { trigger: 'submit', validators: _validator },
       )
 
-      array.setValue([{ control: '' }])
-      array.markAsBlurred()
-      await flushPromises()
-
-      expect(array.invalid.value).toEqual(false)
-      expect(array.hasError('test')).toEqual(false)
-      expect(array.hasError('required', [0, 'control'])).toEqual(false)
-      expect(array.hasError('async', [0, 'group', 'control'])).toEqual(false)
-
-      await array.validate()
-
       expect(array.invalid.value).toEqual(true)
       expect(array.hasError('test')).toEqual(true)
       expect(array.hasError('required', [0, 'control'])).toEqual(true)
+      expect(array.hasError('async', [0, 'group', 'control'])).toEqual(false)
+
+      await flushPromises()
+
       expect(array.hasError('async', [0, 'group', 'control'])).toEqual(true)
 
       array.setValue([{ control: 'test' }])
+      array.markAsBlurred()
       await flushPromises()
 
       expect(array.invalid.value).toEqual(true)
@@ -267,6 +277,21 @@ describe('formArray.ts', () => {
       expect(array.hasError('test')).toEqual(false)
       expect(array.hasError('required', [0, 'control'])).toEqual(false)
       expect(array.hasError('async', [0, 'group', 'control'])).toEqual(true)
+
+      array.setValue([{ control: '1234' }])
+      await flushPromises()
+
+      expect(array.invalid.value).toEqual(true)
+      expect(array.hasError('test')).toEqual(false)
+      expect(array.hasError('required', [0, 'control'])).toEqual(false)
+      expect(array.hasError('async', [0, 'group', 'control'])).toEqual(true)
+
+      await array.validate()
+
+      expect(array.invalid.value).toEqual(true)
+      expect(array.hasError('test')).toEqual(true)
+      expect(array.hasError('required', [0, 'control'])).toEqual(false)
+      expect(array.hasError('async', [0, 'group', 'control'])).toEqual(true)
     })
   })
 })
diff --git a/packages/cdk/forms/__tests__/formControl.spec.ts b/packages/cdk/forms/__tests__/formControl.spec.ts
index 701189ac..f24609c9 100644
--- a/packages/cdk/forms/__tests__/formControl.spec.ts
+++ b/packages/cdk/forms/__tests__/formControl.spec.ts
@@ -1,5 +1,6 @@
 import { flushPromises } from '@vue/test-utils'
 import { FormControl } from '../src/controls/formControl'
+import { ValidationErrors } from '../src/types'
 import { Validators } from '../src/validators'
 
 describe('formControl.ts', () => {
@@ -13,14 +14,17 @@ describe('formControl.ts', () => {
     test('reset work', async () => {
       control.setValue('test')
       control.markAsBlurred()
+      control.markAsDirty()
 
       expect(control.valueRef.value).toEqual('test')
       expect(control.blurred.value).toEqual(true)
+      expect(control.dirty.value).toEqual(true)
 
       control.reset()
 
       expect(control.valueRef.value).toBeNull()
       expect(control.blurred.value).toEqual(false)
+      expect(control.dirty.value).toEqual(false)
     })
 
     test('setValue and getValue work', () => {
@@ -45,6 +49,16 @@ describe('formControl.ts', () => {
       expect(control.blurred.value).toEqual(false)
     })
 
+    test('markAsDirty and markAsPristine work', () => {
+      control.markAsDirty()
+
+      expect(control.dirty.value).toEqual(true)
+
+      control.markAsPristine()
+
+      expect(control.dirty.value).toEqual(false)
+    })
+
     test('validate work', async () => {
       expect(await control.validate()).toBeNull()
 
@@ -58,45 +72,66 @@ describe('formControl.ts', () => {
     let control: FormControl
 
     test('default change work', async () => {
-      control = new FormControl(null, { validators: Validators.required })
+      const _asyncValidator = (value: unknown) =>
+        Promise.resolve(value === 'test' ? null : ({ async: { message: 'async' } } as ValidationErrors))
+
+      control = new FormControl('test', { validators: Validators.required, asyncValidators: _asyncValidator })
 
       expect(control.hasError('required')).toEqual(false)
+      expect(control.hasError('async')).toEqual(false)
 
       control.setValue('')
       await flushPromises()
 
       expect(control.hasError('required')).toEqual(true)
+      expect(control.hasError('async')).toEqual(false)
+
+      control.setValue('1234')
+      await flushPromises()
+
+      expect(control.hasError('required')).toEqual(false)
+      expect(control.hasError('async')).toEqual(true)
     })
 
     test('blur trigger validate work', async () => {
       control = new FormControl(null, { trigger: 'blur', validators: Validators.required })
 
-      expect(control.hasError('required')).toEqual(false)
+      expect(control.hasError('required')).toEqual(true)
 
-      control.markAsBlurred()
+      control.setValue('test')
       await flushPromises()
 
       expect(control.hasError('required')).toEqual(true)
 
-      control.setValue('test')
+      control.markAsBlurred()
       await flushPromises()
 
       expect(control.hasError('required')).toEqual(false)
+
+      control.setValue('')
+      await flushPromises()
+
+      expect(control.hasError('required')).toEqual(true)
     })
 
     test('submit trigger validate work', async () => {
       control = new FormControl(null, { trigger: 'submit', validators: Validators.required })
 
-      expect(control.hasError('required')).toEqual(false)
+      expect(control.hasError('required')).toEqual(true)
+
+      control.setValue('test')
+      await flushPromises()
+
+      expect(control.hasError('required')).toEqual(true)
 
       control.markAsBlurred()
       await flushPromises()
 
-      expect(control.hasError('required')).toEqual(false)
+      expect(control.hasError('required')).toEqual(true)
 
       await control.validate()
 
-      expect(control.hasError('required')).toEqual(true)
+      expect(control.hasError('required')).toEqual(false)
     })
   })
 })
diff --git a/packages/cdk/forms/__tests__/formGroup.spec.ts b/packages/cdk/forms/__tests__/formGroup.spec.ts
index 38044e4f..1aec657e 100644
--- a/packages/cdk/forms/__tests__/formGroup.spec.ts
+++ b/packages/cdk/forms/__tests__/formGroup.spec.ts
@@ -104,18 +104,28 @@ describe('formGroup.ts', () => {
 
     test('markAsBlurred and markAsUnblurred work', async () => {
       group.markAsBlurred()
-
       await flushPromises()
 
       expect(group.blurred.value).toEqual(true)
 
       group.markAsUnblurred()
-
       await flushPromises()
 
       expect(group.blurred.value).toEqual(false)
     })
 
+    test('markAsDirty and markAsPristine work', async () => {
+      group.markAsDirty()
+      await flushPromises()
+
+      expect(group.dirty.value).toEqual(true)
+
+      group.markAsPristine()
+      await flushPromises()
+
+      expect(group.dirty.value).toEqual(false)
+    })
+
     test('validate work', async () => {
       expect(await group.validate()).toBeNull()
 
@@ -159,13 +169,17 @@ describe('formGroup.ts', () => {
         }),
       })
 
-      group.setValue({ control: '' })
-      await flushPromises()
-
       expect(group.invalid.value).toEqual(true)
       expect(group.hasError('required', 'control')).toEqual(true)
       expect(group.hasError('test')).toEqual(false)
 
+      group.setValue({ control: 'test' })
+      await flushPromises()
+
+      expect(group.invalid.value).toEqual(false)
+      expect(group.hasError('required', 'control')).toEqual(false)
+      expect(group.hasError('test')).toEqual(false)
+
       group.setValidator(_validator)
       group.setValue({ control: '1234' })
       await flushPromises()
@@ -197,35 +211,31 @@ describe('formGroup.ts', () => {
         { trigger: 'blur', validators: _validator },
       )
 
-      group.setValue({ control: '' })
-
-      await flushPromises()
-
-      expect(group.invalid.value).toEqual(false)
-      expect(group.hasError('test')).toEqual(false)
-      expect(group.hasError('required', 'control')).toEqual(false)
-      expect(group.hasError('required', 'group.control')).toEqual(false)
+      expect(group.invalid.value).toEqual(true)
+      expect(group.hasError('test')).toEqual(true)
+      expect(group.hasError('required', 'control')).toEqual(true)
+      expect(group.hasError('required', 'group.control')).toEqual(true)
 
-      group.markAsBlurred()
+      group.setValue({ control: 'test', group: { control: 'test' } })
       await flushPromises()
 
       expect(group.invalid.value).toEqual(true)
       expect(group.hasError('test')).toEqual(true)
       expect(group.hasError('required', 'control')).toEqual(true)
-      expect(group.hasError('required', 'group.control')).toEqual(false)
+      expect(group.hasError('required', 'group.control')).toEqual(true)
 
-      group.setValue({ control: 'test' })
+      group.markAsBlurred()
       await flushPromises()
 
-      expect(group.invalid.value).toEqual(false)
+      expect(group.invalid.value).toEqual(true)
       expect(group.hasError('test')).toEqual(false)
       expect(group.hasError('required', 'control')).toEqual(false)
-      expect(group.hasError('required', 'group.control')).toEqual(false)
+      expect(group.hasError('required', 'group.control')).toEqual(true)
 
       await group.validate()
 
-      expect(group.invalid.value).toEqual(true)
-      expect(group.hasError('required', 'group.control')).toEqual(true)
+      expect(group.invalid.value).toEqual(false)
+      expect(group.hasError('required', 'group.control')).toEqual(false)
     })
 
     test('submit trigger validate work', async () => {
@@ -242,23 +252,24 @@ describe('formGroup.ts', () => {
         { trigger: 'submit', validators: _validator },
       )
 
-      group.setValue({ control: '' })
-      group.markAsBlurred()
+      expect(group.invalid.value).toEqual(true)
+      expect(group.hasError('test')).toEqual(true)
+      expect(group.hasError('required', 'control')).toEqual(true)
+      expect(group.hasError('async', 'group.control')).toEqual(false)
+
       await flushPromises()
 
-      expect(group.invalid.value).toEqual(false)
-      expect(group.hasError('test')).toEqual(false)
-      expect(group.hasError('required', 'control')).toEqual(false)
-      expect(group.hasError('async', 'group.control')).toEqual(false)
+      expect(group.hasError('async', 'group.control')).toEqual(true)
 
-      await group.validate()
+      group.setValue({ control: 'test' })
+      await flushPromises()
 
       expect(group.invalid.value).toEqual(true)
       expect(group.hasError('test')).toEqual(true)
       expect(group.hasError('required', 'control')).toEqual(true)
       expect(group.hasError('async', 'group.control')).toEqual(true)
 
-      group.setValue({ control: 'test' })
+      group.markAsBlurred()
       await flushPromises()
 
       expect(group.invalid.value).toEqual(true)
diff --git a/packages/cdk/forms/__tests__/useForms.spec.ts b/packages/cdk/forms/__tests__/useForms.spec.ts
index 99f892b8..72ad56ea 100644
--- a/packages/cdk/forms/__tests__/useForms.spec.ts
+++ b/packages/cdk/forms/__tests__/useForms.spec.ts
@@ -4,7 +4,7 @@ import { Validators } from '../src/validators'
 
 interface BasicGroup {
   control1: string
-  control2: number
+  control2: string
   array: string[]
   group: {
     control: string
@@ -25,21 +25,23 @@ describe('useForms.ts', () => {
     })
 
     expect(group.getValue()).toEqual(basicValue)
-    expect(group.invalid.value).toEqual(false)
-    expect(group.hasError('required', 'control1')).toEqual(false)
-    expect(group.hasError('required', 'control2')).toEqual(false)
+    expect(group.invalid.value).toEqual(true)
+    expect(group.hasError('required', 'control1')).toEqual(true)
+    expect(group.hasError('required', 'control2')).toEqual(true)
 
-    group.setValue({ control1: '' })
+    group.setValue({ control1: 'test', control2: 'test' })
     await flushPromises()
 
-    expect(group.getValue()).toEqual({ ...basicValue, control1: '' })
+    expect(group.getValue()).toEqual({ ...basicValue, control1: 'test', control2: 'test' })
     expect(group.invalid.value).toEqual(true)
-    expect(group.hasError('required', 'control1')).toEqual(true)
-    expect(group.hasError('required', 'control2')).toEqual(false)
+    expect(group.hasError('required', 'control1')).toEqual(false)
+    expect(group.hasError('required', 'control2')).toEqual(true)
 
     group.markAsBlurred()
     await flushPromises()
 
-    expect(group.hasError('required', 'control2')).toEqual(true)
+    expect(group.invalid.value).toEqual(false)
+    expect(group.hasError('required', 'control1')).toEqual(false)
+    expect(group.hasError('required', 'control2')).toEqual(false)
   })
 })