Repository: IduxFE/components
Pull request #143 (2021-01-16 15:11:30)

feat(cdk:forms): add watchValue and watchStatus
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)
- [ ] 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/143?src=pr&el=h1) Report
> Merging [#143](https://codecov.io/gh/IduxFE/components/pull/143?src=pr&el=desc) (420e322) into [main](https://codecov.io/gh/IduxFE/components/commit/3ecb23197cd813c8e47c892e1c94cf814fd6bc93?el=desc) (3ecb231) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

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

```diff
@@           Coverage Diff           @@
##             main     #143   +/-   ##
=======================================
  Coverage   97.41%   97.42%           
=======================================
  Files          59       59           
  Lines        1046     1050    +4     
  Branches      178      178           
=======================================
+ Hits         1019     1023    +4     
  Misses         10       10           
  Partials       17       17           
```


| [Impacted Files](https://codecov.io/gh/IduxFE/components/pull/143?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/cdk/forms/src/controls/abstractControl.ts](https://codecov.io/gh/IduxFE/components/pull/143/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL2Zvcm1zL3NyYy9jb250cm9scy9hYnN0cmFjdENvbnRyb2wudHM=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/IduxFE/components/pull/143?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/143?src=pr&el=footer). Last update [3ecb231...420e322](https://codecov.io/gh/IduxFE/components/pull/143?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 964785dd..29daccce 100644
--- a/packages/cdk/forms/src/controls/abstractControl.ts
+++ b/packages/cdk/forms/src/controls/abstractControl.ts
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
-import type { ComputedRef, DeepReadonly, Ref } from 'vue'
+import type { ComputedRef, DeepReadonly, Ref, WatchCallback, WatchOptions, WatchStopHandle } from 'vue'
 import type {
   AsyncValidatorFn,
   ErrorMessages,
@@ -15,7 +15,7 @@ import type {
 import type { FormGroup } from './formGroup'
 import type { FormArray } from './formArray'
 
-import { computed, readonly, ref } from 'vue'
+import { computed, watch, readonly, ref } from 'vue'
 import { hasOwnProperty, isArray, isNil, isObject } from '@idux/cdk/utils'
 import { Validators } from '../validators'
 import { isFormArray, isFormGroup } from '../typeof'
@@ -209,7 +209,6 @@ export abstract class AbstractControl<T = any> {
   }
 
   /**
-   * @description
    * Reports error data for the control with the given path.
    *
    * @param errorCode The code of the error to check
@@ -222,7 +221,6 @@ export abstract class AbstractControl<T = any> {
   }
 
   /**
-   * @description
    * Reports whether the control with the given path has the error specified.
    *
    * @param errorCode The code of the error to check
@@ -241,6 +239,29 @@ export abstract class AbstractControl<T = any> {
     this._parent = parent
   }
 
+  /**
+   * Watch the ref value for the control.
+   *
+   * @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 })
+  }
+
+  /**
+   * Watch the status for the control.
+   *
+   * @param cb The callback when the status changes
+   * @param options Optional options of watch
+   */
+  watchStatus(
+    cb: WatchCallback<ValidationStatus, ValidationStatus | undefined>,
+    options?: WatchOptions,
+  ): WatchStopHandle {
+    return watch(this.status, cb, options)
+  }
+
   protected async _validate(): Promise<ValidationErrors | null> {
     const value = this.getValue()
     let newErrors = this._runValidator(value)


Test patch:
diff --git a/packages/cdk/forms/__tests__/abstractControl.spec.ts b/packages/cdk/forms/__tests__/abstractControl.spec.ts
index eb5c90f5..fb1c6df6 100644
--- a/packages/cdk/forms/__tests__/abstractControl.spec.ts
+++ b/packages/cdk/forms/__tests__/abstractControl.spec.ts
@@ -1,5 +1,5 @@
 import { flushPromises } from '@vue/test-utils'
-import { Ref, ref } from 'vue'
+import { Ref, ref, watch } from 'vue'
 import { AbstractControl } from '../src/controls/abstractControl'
 import { AsyncValidatorFn, ValidationErrors, ValidatorFn, ValidatorOptions } from '../src/types'
 import { Validators } from '../src/validators'
@@ -11,9 +11,12 @@ class Control<T = unknown> extends AbstractControl<T> {
     asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
   ) {
     super(validatorOrOptions, asyncValidator)
+    this._watchEffect()
   }
   reset(): void {}
-  setValue(_: T | null): void {}
+  setValue(value: T | null): void {
+    this.valueRef.value = value
+  }
   getValue(): T {
     return this.valueRef.value as T
   }
@@ -22,6 +25,16 @@ class Control<T = unknown> extends AbstractControl<T> {
   async validate(): Promise<ValidationErrors | null> {
     return this._validate()
   }
+
+  private _watchEffect() {
+    watch([this.valueRef, this.blurred], () => {
+      this._validate()
+    })
+
+    watch(this.errors, errors => {
+      this._status.value = errors ? 'invalid' : 'valid'
+    })
+  }
 }
 
 describe('abstractControl.ts', () => {
@@ -118,6 +131,67 @@ describe('abstractControl.ts', () => {
       parent.setParent(root as any)
       expect(control.root).toEqual(root)
     })
+
+    test('watchValue work', async () => {
+      const log = jest.fn()
+      const stop = control.watchValue(value => log(value))
+
+      control.setValue('test')
+      await flushPromises()
+
+      expect(log).toBeCalledWith('test')
+      expect(log).toBeCalledTimes(1)
+
+      control.setValue('')
+      await flushPromises()
+
+      expect(log).toBeCalledWith('')
+      expect(log).toBeCalledTimes(2)
+
+      stop()
+
+      control.setValue('test')
+      await flushPromises()
+
+      expect(log).toBeCalledTimes(2)
+
+      control.watchValue(value => log(value), { immediate: true })
+      await flushPromises()
+
+      expect(log).toBeCalledWith('test')
+      expect(log).toBeCalledTimes(3)
+    })
+
+    test('watchStatus work', async () => {
+      const log = jest.fn()
+      const stop = control.watchStatus(value => log(value))
+
+      control.setValidator(Validators.required)
+      control.setValue('')
+      await flushPromises()
+
+      expect(log).toBeCalledWith('invalid')
+      expect(log).toBeCalledTimes(1)
+
+      control.setValue('test')
+      await flushPromises()
+
+      expect(log).toBeCalledWith('valid')
+      expect(log).toBeCalledTimes(2)
+
+      stop()
+
+      control.setValue('')
+      await flushPromises()
+
+      expect(log).toBeCalledTimes(2)
+
+      control.watchValue(value => log(value), { immediate: true })
+      await flushPromises()
+
+      expect(log).toBeCalledWith('invalid')
+      expect(log).toBeCalledTimes(3)
+    })
   })
 
   describe('convert options work', () => {