From 581f197be67e33189aee44b31465fb8b6d21c640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=89=B2?= Date: Thu, 11 Jan 2024 15:47:42 +0800 Subject: [PATCH] feat(form): add `setDisabled`, `setRequired` method (#1733) --- packages/form/docs/getting-started.en-US.md | 2 + packages/form/docs/getting-started.zh-CN.md | 2 + packages/form/examples/acl/index.en-US.md | 7 ++ .../examples/acl/{index.md => index.zh-CN.md} | 0 packages/form/examples/method/demo/simple.md | 89 +++++++++++++++++++ packages/form/examples/method/index.en-US.md | 7 ++ packages/form/examples/method/index.zh-CN.md | 7 ++ packages/form/examples/modal/index.en-US.md | 18 ++++ .../modal/{index.md => index.zh-CN.md} | 0 packages/form/spec/form.spec.ts | 21 +++++ packages/form/src/model/form.property.ts | 4 +- packages/form/src/sf.component.ts | 42 +++++++++ scripts/site/route-paths.txt | 2 + 13 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 packages/form/examples/acl/index.en-US.md rename packages/form/examples/acl/{index.md => index.zh-CN.md} (100%) create mode 100644 packages/form/examples/method/demo/simple.md create mode 100644 packages/form/examples/method/index.en-US.md create mode 100644 packages/form/examples/method/index.zh-CN.md create mode 100644 packages/form/examples/modal/index.en-US.md rename packages/form/examples/modal/{index.md => index.zh-CN.md} (100%) diff --git a/packages/form/docs/getting-started.en-US.md b/packages/form/docs/getting-started.en-US.md index 46a2d87b19..98f495ec12 100644 --- a/packages/form/docs/getting-started.en-US.md +++ b/packages/form/docs/getting-started.en-US.md @@ -162,6 +162,8 @@ export class HomeComponent { | `getProperty` | Get a form property via path | `FormProperty` | | `getValue` | Get value via path | `any` | | `setValue` | Set value via path, should be throw error when invalid path | `this` | +| `setDisabled` | Set `disabled` status via path, should be throw error when invalid path | `this` | +| `setRequired` | Set `required` status via path, should be throw error when invalid path | `this` | | `updateFeedback` | Set feedback status via path | `this` | > **Note:** All paths are separated by `/`, for example: `/user/name`, `/arr/0/name`. diff --git a/packages/form/docs/getting-started.zh-CN.md b/packages/form/docs/getting-started.zh-CN.md index f7d7a1f9c1..a6536f3689 100644 --- a/packages/form/docs/getting-started.zh-CN.md +++ b/packages/form/docs/getting-started.zh-CN.md @@ -162,6 +162,8 @@ export class HomeComponent { | `getProperty` | 根据路径获取表单元素属性 | `FormProperty` | | `getValue` | 根据路径获取表单元素当前值 | `any` | | `setValue` | 根据路径设置某个表单元素属性值,若路径不存在会产生异常 | `this` | +| `setDisabled` | 根据路径设置某个表单元素 `disabled` 值,若路径不存在会产生异常 | `this` | +| `setRequired` | 根据路径设置某个表单元素 `required` 值,若路径不存在会产生异常 | `this` | | `updateFeedback` | 根据路径设置某个表单元素反馈状态 | `this` | > **注:** 所有 path 采用 `/` 来分隔,例如:`/user/name`、`/arr/0/name`。 diff --git a/packages/form/examples/acl/index.en-US.md b/packages/form/examples/acl/index.en-US.md new file mode 100644 index 0000000000..4cbc2e0ee6 --- /dev/null +++ b/packages/form/examples/acl/index.en-US.md @@ -0,0 +1,7 @@ +--- +title: acl +subtitle: ACL +type: Examples +--- + +Combined with `@delon/acl` permissions, a Schema can be used to build forms for different roles or permission points. diff --git a/packages/form/examples/acl/index.md b/packages/form/examples/acl/index.zh-CN.md similarity index 100% rename from packages/form/examples/acl/index.md rename to packages/form/examples/acl/index.zh-CN.md diff --git a/packages/form/examples/method/demo/simple.md b/packages/form/examples/method/demo/simple.md new file mode 100644 index 0000000000..e838a6767c --- /dev/null +++ b/packages/form/examples/method/demo/simple.md @@ -0,0 +1,89 @@ +--- +title: + zh-CN: 基础样例 + en-US: Basic Usage +order: 0 +--- + +## zh-CN + +最简单的用法。 + +## en-US + +Simplest of usage. + +```ts +import { Component, DestroyRef, ViewChild, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { timer } from 'rxjs'; + +import { DelonFormModule, SFComponent, SFSchema } from '@delon/form'; +import { NzButtonModule } from 'ng-zorro-antd/button'; +import { NzMessageService } from 'ng-zorro-antd/message'; + +@Component({ + selector: 'app-demo', + template: ` +

+ + + + + +

+ + `, + standalone: true, + imports: [DelonFormModule, NzButtonModule] +}) +export class DemoComponent { + @ViewChild('sf') private readonly sf!: SFComponent; + private readonly msg = inject(NzMessageService); + private readonly d$ = inject(DestroyRef); + schema: SFSchema = { + properties: { + userName: { type: 'string', title: '用户名' }, + pwd: { type: 'string', title: '密码' } + }, + required: ['userName', 'pwd'] + }; + + private get userNameRequired(): boolean { + return (this.sf.getProperty('/userName')?.parent?.schema.required ?? []).includes('userName'); + } + + private get userNameDisabled(): boolean { + return this.sf.getProperty('/userName')?.schema?.readOnly === true; + } + + toggleUserNameRequired(): void { + this.sf.setRequired(`/userName`, !this.userNameRequired); + } + + toggleUserNameDisabled(): void { + this.sf.setDisabled(`/userName`, !this.userNameDisabled); + } + + modifyUserNameValue(): void { + this.sf.setValue(`/userName`, `Mock text ${+new Date()}`); + } + + getUserNameValue(): void { + this.msg.info(this.sf.getValue('/userName')); + } + + triggerUserNameAsyncStatus(): void { + this.sf.updateFeedback('/userName', 'validating'); + timer(1000 * 2) + .pipe(takeUntilDestroyed(this.d$)) + .subscribe(() => { + this.sf.updateFeedback('/userName', 'success'); + }); + } + + submit(value: {}): void { + this.msg.success(JSON.stringify(value)); + } +} +``` diff --git a/packages/form/examples/method/index.en-US.md b/packages/form/examples/method/index.en-US.md new file mode 100644 index 0000000000..cc8f28e014 --- /dev/null +++ b/packages/form/examples/method/index.en-US.md @@ -0,0 +1,7 @@ +--- +title: method +subtitle: Built-in methods +type: Examples +--- + +`SFComponent` provides some shortcut methods, such as: `setValue`, `setDisabled`, `setRequired`, etc. diff --git a/packages/form/examples/method/index.zh-CN.md b/packages/form/examples/method/index.zh-CN.md new file mode 100644 index 0000000000..f4d3a4ca30 --- /dev/null +++ b/packages/form/examples/method/index.zh-CN.md @@ -0,0 +1,7 @@ +--- +title: method +subtitle: 内置方法 +type: Examples +--- + +`SFComponent` 提供一些快捷方法,例如:`setValue`、`setDisabled`、`setRequired` 等 diff --git a/packages/form/examples/modal/index.en-US.md b/packages/form/examples/modal/index.en-US.md new file mode 100644 index 0000000000..7f56a86bd3 --- /dev/null +++ b/packages/form/examples/modal/index.en-US.md @@ -0,0 +1,18 @@ +--- +title: modal +subtitle: Modal +type: Examples +--- + +Using a form in a modal box is a very common scenario. In fact, when you run `ng g ng-alain:edit edit`, you will get a complete example; you will get an HTML template like this: + +```html + + + +``` + +`.modal-footer` has been very friendly to integrate custom dynamic boxes. diff --git a/packages/form/examples/modal/index.md b/packages/form/examples/modal/index.zh-CN.md similarity index 100% rename from packages/form/examples/modal/index.md rename to packages/form/examples/modal/index.zh-CN.md diff --git a/packages/form/spec/form.spec.ts b/packages/form/spec/form.spec.ts index 487784485c..eec9eab995 100644 --- a/packages/form/spec/form.spec.ts +++ b/packages/form/spec/form.spec.ts @@ -520,6 +520,27 @@ describe('form: component', () => { context.comp.setValue('/invalid-path', 'name'); }).toThrow(); }); + it('#setDisabled', () => { + context.comp.setDisabled('/name', true); + page.checkSchema('/name', 'readOnly', true); + context.comp.setDisabled('/name', false); + page.checkSchema('/name', 'readOnly', false); + }); + it('#setDisabled, shoule be throw error when invlaid path', () => { + expect(() => { + context.comp.setDisabled('/invalid-path', true); + }).toThrow(); + }); + it('#setRequired', () => { + expect(context.comp.getProperty('/name')?.valid).toBe(false); + context.comp.setRequired('/name', false); + expect(context.comp.getProperty('/name')?.valid).toBe(true); + }); + it('#setRequired, shoule be throw error when invlaid path', () => { + expect(() => { + context.comp.setRequired('/invalid-path', true); + }).toThrow(); + }); it('#updateFeedback', () => { const namePath = '/name'; context.comp.updateFeedback(namePath, 'error'); diff --git a/packages/form/src/model/form.property.ts b/packages/form/src/model/form.property.ts index ab80a34a39..5cff2bb747 100644 --- a/packages/form/src/model/form.property.ts +++ b/packages/form/src/model/form.property.ts @@ -342,7 +342,7 @@ export abstract class FormProperty { // 同步更新 required if (typeof viFnRes === 'object') { const fixViFnRes = { show: false, required: false, ...viFnRes } as SFVisibleIfReturn; - const parentRequired = this.parent?.schema.required!; + const parentRequired = this.parent?.schema.required; if (parentRequired && this.propertyId) { const idx = parentRequired.findIndex(w => w === this.propertyId); if (fixViFnRes.required) { @@ -352,7 +352,7 @@ export abstract class FormProperty { } this.ui._required = fixViFnRes.required; } - return fixViFnRes.show!; + return fixViFnRes.show; } return viFnRes; } diff --git a/packages/form/src/sf.component.ts b/packages/form/src/sf.component.ts index d57c6d80aa..46b4589bea 100644 --- a/packages/form/src/sf.component.ts +++ b/packages/form/src/sf.component.ts @@ -30,6 +30,7 @@ import type { NzSafeAny } from 'ng-zorro-antd/core/types'; import type { NzFormControlStatusType } from 'ng-zorro-antd/form'; import { mergeConfig } from './config'; +import { SF_SEQ } from './const'; import type { ErrorData } from './errors'; import type { SFButton, SFLayout, SFMode, SFValueChange } from './interface'; import { FormProperty, PropertyGroup } from './model/form.property'; @@ -238,6 +239,47 @@ export class SFComponent implements OnInit, OnChanges, OnDestroy { return this; } + /** + * Set form element new `disabled` based on [path](https://ng-alain.com/form/qa#path) + * + * 根据[路径](https://ng-alain.com/form/qa#path)设置某个表单元素 `disabled` 状态 + */ + setDisabled(path: string, status: boolean): this { + const property = this.getProperty(path); + if (!property) { + throw new Error(`Invalid path: ${path}`); + } + property.schema.readOnly = status; + property.widget.detectChanges(); + return this; + } + + /** + * Set form element new `required` based on [path](https://ng-alain.com/form/qa#path) + * + * 根据[路径](https://ng-alain.com/form/qa#path)设置某个表单元素 `required` 状态 + */ + setRequired(path: string, status: boolean): this { + const property = this.getProperty(path); + if (!property) { + throw new Error(`Invalid path: ${path}`); + } + + const key = path.split(SF_SEQ).pop()!; + const parentRequired = property.parent?.schema.required || []; + const idx = parentRequired.findIndex(w => w === key); + if (status) { + if (idx === -1) parentRequired.push(key); + } else { + if (idx !== -1) parentRequired.splice(idx, 1); + } + property.parent!.schema.required = parentRequired; + property.ui._required = status; + property.widget.detectChanges(); + this.validator({ onlyRoot: false }); + return this; + } + /** * Update the feedback status of the widget * diff --git a/scripts/site/route-paths.txt b/scripts/site/route-paths.txt index c004c0ed38..c690461877 100644 --- a/scripts/site/route-paths.txt +++ b/scripts/site/route-paths.txt @@ -223,6 +223,8 @@ /form/layout/zh /form/mention/en /form/mention/zh +/form/method/en +/form/method/zh /form/modal/en /form/modal/zh /form/monaco-editor/en