diff --git a/.cz-config.js b/.cz-config.js
index 2140e2b4..97a63bf0 100644
--- a/.cz-config.js
+++ b/.cz-config.js
@@ -58,6 +58,7 @@ module.exports = {
confirmCommit: 'Are you sure you want to proceed with the commit above?',
},
+ subjectLimit: 72,
allowCustomScopes: false,
allowBreakingChanges: ['feat', 'fix'],
};
diff --git a/README.md b/README.md
index 6811ce9d..c223a350 100644
--- a/README.md
+++ b/README.md
@@ -18,21 +18,21 @@
-Sunmao(榫卯 /suən mɑʊ/) is a front-end low-code framework. Through Sunmao, you can easily encapsulate any front-end UI components into low-code component libraries, so as to build your own low-code UI development platform, making front-end development as tight as Sunmao.
+Sunmao(榫卯 /suən mɑʊ/) is a front-end low-code framework. Through Sunmao, you can easily encapsulate any front-end UI components into low-code component libraries to build your own low-code UI development platform, making front-end development as tight as Sunmao("mortise and tenon" in Chinese).
[中文](./docs/zh/README.md)
## DEMO
-Sunmao‘s website is developed with Sunmao, look here: [Sunmao website editor](https://sunmao-ui.com/dev.html)
+The offcial website of Sunmao is developed by Sunmao, try it from here: [Sunmao website editor](https://sunmao-ui.com/dev.html)
We also provide an open-to-use template: [Sunmao Starter Kit](https://github.com/webzard-io/sunmao-start)
## Why Sunmao?
-### Reactive low-code framework
+### Reactive rendering low-code framework
-Sunmao chooses a reactive solution that is easy to understand and has excellent performance, making Sunmao intuitive and quick to use.
+Sunmao chooses a reactive rendering solution that is easy to understand and has excellent performance, making Sunmao intuitive and quick to start.
### Powerful low-code GUI editor
@@ -40,7 +40,7 @@ Sunmao has a built-in GUI editor, which almost includes all the capabilities tha
### Extremely Extensible
-Both the UI component library itself and the low-code editor support custom extensions. Developers can register various components to cover application requirements and continue to use the existing visual design system.
+Both the UI component library itself and the low-code editor support custom extensions. Developers can register various components to meet the needs of application and continue to use the existing visual design system.
### Type Safety
@@ -50,13 +50,13 @@ For more details, read [Sunmao: A truly extensible low-code UI framework](./docs
## Tutorial
-Sunmao users are divided into two roles, one is a developer and the other is a user.
+Sunmao users are divided into two roles, one is developer and the other is user.
-The responsibilities of developers are similar to those of common front-end developers. They are responsible for developing UI components and encapsulating common UI components to Sunmao components. Developers need to write code to implement the logic of the component.
+The responsibilities of developers are similar to those of common front-end developers. They are responsible for developing UI components and encapsulating common UI components to Sunmao components. Developers need to write code to implement the logic of components.
-The user's responsibility is to use the Sunmao components encapsulated by developers to build front-end applications in the Sunmao low-code editor. Users do not need front-end knowledge and programming skills. They can complete application construction only through UI interaction.
+The user's responsibility is to use the Sunmao components encapsulated by developers to build front-end applications in the Sunmao low-code editor. Users do not need front-end knowledge and programming skills. They can finish building the application through UI interaction only.
-We have prepared two tutorials for different roles. The user only needs to read the user's tutorial, but the developer has to read both.
+We have prepared two tutorials for user and developer. The user only needs to read the user's tutorial, while the developer must read both.
- [User's Tutorial](./docs/en/user.md)
- [Developer's Tutorial](./docs/en/developer.md)
diff --git a/commitlint.config.js b/commitlint.config.js
index 28fe5c5b..6e4e2f1b 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -1 +1,6 @@
-module.exports = {extends: ['@commitlint/config-conventional']}
+module.exports = {
+ extends: ['@commitlint/config-conventional'],
+ rules: {
+ 'header-max-length': [2, 'always', 72],
+ },
+};
diff --git a/docs/en/component.md b/docs/en/component.md
index 19bc50cc..57ad4b59 100644
--- a/docs/en/component.md
+++ b/docs/en/component.md
@@ -29,7 +29,7 @@ First, let's look at this Input Component *Spec* example.
displayName: "Input",
exampleProperties: {
placeholder: "Input here",
- disabled: fasle,
+ disabled: false,
},
},
spec: {
diff --git a/docs/zh/component.md b/docs/zh/component.md
index c015890b..aec988a2 100644
--- a/docs/zh/component.md
+++ b/docs/zh/component.md
@@ -29,7 +29,7 @@ Spec 本质上是一个 JSON,它的作用是描述组件的参数、行为等
displayName: "Input",
exampleProperties: {
placeholder: "Input here",
- disabled: fasle,
+ disabled: false,
},
},
spec: {
@@ -135,7 +135,7 @@ const InputSpec = {
displayName: 'Input',
exampleProperties: {
placeholder: 'Input here',
- disabled: fasle,
+ disabled: false,
},
},
spec: {
diff --git a/examples/form/basic.json b/examples/form/basic.json
index cbdf3a2a..b8011c33 100644
--- a/examples/form/basic.json
+++ b/examples/form/basic.json
@@ -139,10 +139,29 @@
{
"type": "core/v1/validation",
"properties": {
- "value": "{{ phoneInput.value || \"\" }}",
- "maxLength": 100,
- "minLength": 0,
- "rule": "phoneNumber"
+ "validators": [
+ {
+ "name": "phone",
+ "value": "{{ phoneInput.value || \"\" }}",
+ "rules": [
+ {
+ "type": "regex",
+ "regex": "^1[3456789]\\d{9}$",
+ "error": {
+ "message": "Please input the correct phone number."
+ }
+ },
+ {
+ "type": "length",
+ "minLength": 0,
+ "maxLength": 100,
+ "error": {
+ "message": "Please input the length between 0 and 100."
+ }
+ }
+ ]
+ }
+ ]
}
}
]
diff --git a/examples/form/formInDialog.json b/examples/form/formInDialog.json
index ec686653..d9eaa1ad 100644
--- a/examples/form/formInDialog.json
+++ b/examples/form/formInDialog.json
@@ -314,9 +314,22 @@
{
"type": "core/v1/validation",
"properties": {
- "value": "{{ nameInput.value || \"\" }}",
- "maxLength": 10,
- "minLength": 2
+ "validators": [
+ {
+ "name": "name",
+ "value": "{{ nameInput.value || \"\" }}",
+ "rules": [
+ {
+ "type": "length",
+ "maxLength": 10,
+ "minLength": 2,
+ "error": {
+ "message": "Please input the length between 2 and 10."
+ }
+ }
+ ]
+ }
+ ]
}
}
]
diff --git a/examples/form/formValidation.json b/examples/form/formValidation.json
new file mode 100644
index 00000000..da07f238
--- /dev/null
+++ b/examples/form/formValidation.json
@@ -0,0 +1,855 @@
+{
+ "app": {
+ "version": "sunmao/v1",
+ "kind": "Application",
+ "metadata": {
+ "name": "some App"
+ },
+ "spec": {
+ "components": [
+ {
+ "id": "name_form",
+ "type": "arco/v1/formControl",
+ "properties": {
+ "label": {
+ "format": "plain",
+ "raw": "name"
+ },
+ "layout": "horizontal",
+ "required": true,
+ "hidden": false,
+ "extra": "",
+ "errorMsg": "{{name_form.validatedResult.name?.errors[0]?.message || '';}}",
+ "labelAlign": "left",
+ "colon": false,
+ "help": "",
+ "labelCol": {
+ "span": 3,
+ "offset": 0
+ },
+ "wrapperCol": {
+ "span": 21,
+ "offset": 0
+ }
+ },
+ "traits": [
+ {
+ "type": "core/v1/validation",
+ "properties": {
+ "validators": [
+ {
+ "name": "name",
+ "value": "{{name_input.value;}}",
+ "rules": [
+ {
+ "type": "required",
+ "validate": null,
+ "error": {
+ "message": "Please input the name."
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ },
+ {
+ "type": "length",
+ "validate": null,
+ "error": {
+ "message": "The name is limited in length to between 1 and 10."
+ },
+ "minLength": 1,
+ "maxLength": 10,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "name_input",
+ "type": "arco/v1/input",
+ "properties": {
+ "allowClear": false,
+ "disabled": false,
+ "readOnly": false,
+ "defaultValue": "",
+ "updateWhenDefaultValueChanges": false,
+ "placeholder": "please input the name.",
+ "error": "{{name_form.validatedResult.name.isInvalid}}",
+ "size": "default"
+ },
+ "traits": [
+ {
+ "type": "core/v1/slot",
+ "properties": {
+ "container": {
+ "id": "name_form",
+ "slot": "content"
+ },
+ "ifCondition": true
+ }
+ },
+ {
+ "type": "core/v1/event",
+ "properties": {
+ "handlers": [
+ {
+ "type": "onChange",
+ "componentId": "name_form",
+ "method": {
+ "name": "validateFields",
+ "parameters": {
+ "names": "{{['name']}}"
+ }
+ }
+ },
+ {
+ "type": "onBlur",
+ "componentId": "name_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ },
+ {
+ "type": "onChange",
+ "componentId": "check_name",
+ "method": {
+ "name": "triggerFetch",
+ "parameters": {}
+ }
+ },
+ {
+ "type": "onBlur",
+ "componentId": "check_name",
+ "method": {
+ "name": "triggerFetch",
+ "parameters": {}
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "email_form",
+ "type": "arco/v1/formControl",
+ "properties": {
+ "label": {
+ "format": "plain",
+ "raw": "email"
+ },
+ "layout": "horizontal",
+ "required": true,
+ "hidden": false,
+ "extra": "",
+ "errorMsg": "{{email_form.validatedResult.email?.errors[0]?.message;}}",
+ "labelAlign": "left",
+ "colon": false,
+ "help": "",
+ "labelCol": {
+ "span": 3,
+ "offset": 0
+ },
+ "wrapperCol": {
+ "span": 21,
+ "offset": 0
+ }
+ },
+ "traits": [
+ {
+ "type": "core/v1/validation",
+ "properties": {
+ "validators": [
+ {
+ "name": "email",
+ "value": "{{email_input.value;}}",
+ "rules": [
+ {
+ "type": "required",
+ "validate": null,
+ "error": {
+ "message": "Please input the email."
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ },
+ {
+ "type": "email",
+ "validate": null,
+ "error": {
+ "message": "Please input the correct email."
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "email_input",
+ "type": "arco/v1/input",
+ "properties": {
+ "allowClear": false,
+ "disabled": false,
+ "readOnly": false,
+ "defaultValue": "",
+ "updateWhenDefaultValueChanges": false,
+ "placeholder": "please input the email.",
+ "error": "{{email_form.validatedResult.email.isInvalid}}",
+ "size": "default"
+ },
+ "traits": [
+ {
+ "type": "core/v1/slot",
+ "properties": {
+ "container": {
+ "id": "email_form",
+ "slot": "content"
+ },
+ "ifCondition": true
+ }
+ },
+ {
+ "type": "core/v1/event",
+ "properties": {
+ "handlers": [
+ {
+ "type": "onChange",
+ "componentId": "email_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ },
+ {
+ "type": "onBlur",
+ "componentId": "email_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "url_form",
+ "type": "arco/v1/formControl",
+ "properties": {
+ "label": {
+ "format": "plain",
+ "raw": "URL"
+ },
+ "layout": "horizontal",
+ "required": true,
+ "hidden": false,
+ "extra": "",
+ "errorMsg": "{{url_form.validatedResult.url?.errors[0]?.message}}",
+ "labelAlign": "left",
+ "colon": false,
+ "help": "",
+ "labelCol": {
+ "span": 3,
+ "offset": 0
+ },
+ "wrapperCol": {
+ "span": 21,
+ "offset": 0
+ }
+ },
+ "traits": [
+ {
+ "type": "core/v1/validation",
+ "properties": {
+ "validators": [
+ {
+ "name": "url",
+ "value": "{{url_input.value}}",
+ "rules": [
+ {
+ "type": "required",
+ "validate": null,
+ "error": {
+ "message": "Please input the URL. "
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ },
+ {
+ "type": "url",
+ "validate": null,
+ "error": {
+ "message": "Please input the correct URL."
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "url_input",
+ "type": "arco/v1/input",
+ "properties": {
+ "allowClear": false,
+ "disabled": false,
+ "readOnly": false,
+ "defaultValue": "",
+ "updateWhenDefaultValueChanges": false,
+ "placeholder": "please input the URL.",
+ "error": "{{url_form.validatedResult.url.isInvalid}}",
+ "size": "default"
+ },
+ "traits": [
+ {
+ "type": "core/v1/slot",
+ "properties": {
+ "container": {
+ "id": "url_form",
+ "slot": "content"
+ },
+ "ifCondition": true
+ }
+ },
+ {
+ "type": "core/v1/event",
+ "properties": {
+ "handlers": [
+ {
+ "type": "onChange",
+ "componentId": "url_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ },
+ {
+ "type": "onBlur",
+ "componentId": "url_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "ip_form",
+ "type": "arco/v1/formControl",
+ "properties": {
+ "label": {
+ "format": "plain",
+ "raw": "IP"
+ },
+ "layout": "horizontal",
+ "required": true,
+ "hidden": false,
+ "extra": "",
+ "errorMsg": "{{ip_form.validatedResult.ip?.errors[0]?.message}}",
+ "labelAlign": "left",
+ "colon": false,
+ "help": "",
+ "labelCol": {
+ "span": 3,
+ "offset": 0
+ },
+ "wrapperCol": {
+ "span": 21,
+ "offset": 0
+ }
+ },
+ "traits": [
+ {
+ "type": "core/v1/validation",
+ "properties": {
+ "validators": [
+ {
+ "name": "ip",
+ "value": "{{ip_input.value}}",
+ "rules": [
+ {
+ "type": "required",
+ "validate": null,
+ "error": {
+ "message": "Please input the IP."
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ },
+ {
+ "type": "ipv4",
+ "validate": null,
+ "error": {
+ "message": "Please input the correct IP."
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "ip_input",
+ "type": "arco/v1/input",
+ "properties": {
+ "allowClear": false,
+ "disabled": false,
+ "readOnly": false,
+ "defaultValue": "",
+ "updateWhenDefaultValueChanges": false,
+ "placeholder": "please input the IP.",
+ "error": "{{ip_form.validatedResult.ip.isInvalid}}",
+ "size": "default"
+ },
+ "traits": [
+ {
+ "type": "core/v1/slot",
+ "properties": {
+ "container": {
+ "id": "ip_form",
+ "slot": "content"
+ },
+ "ifCondition": true
+ }
+ },
+ {
+ "type": "core/v1/event",
+ "properties": {
+ "handlers": [
+ {
+ "type": "onChange",
+ "componentId": "ip_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ },
+ {
+ "type": "onBlur",
+ "componentId": "ip_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "phone_form",
+ "type": "arco/v1/formControl",
+ "properties": {
+ "label": {
+ "format": "plain",
+ "raw": "phone"
+ },
+ "layout": "horizontal",
+ "required": true,
+ "hidden": false,
+ "extra": "",
+ "errorMsg": "{{phone_form.validatedResult.phone.errors[0]?.message}}",
+ "labelAlign": "left",
+ "colon": false,
+ "help": "",
+ "labelCol": {
+ "span": 3,
+ "offset": 0
+ },
+ "wrapperCol": {
+ "span": 21,
+ "offset": 0
+ }
+ },
+ "traits": [
+ {
+ "type": "core/v1/validation",
+ "properties": {
+ "validators": [
+ {
+ "name": "phone",
+ "value": "{{phone_input.value}}",
+ "rules": [
+ {
+ "type": "required",
+ "validate": null,
+ "error": {
+ "message": "Please input the phone number."
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ },
+ {
+ "type": "regex",
+ "validate": null,
+ "error": {
+ "message": "Please input the correct phone number."
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "list": [],
+ "min": 0,
+ "max": 0,
+ "regex": "^1[3456789]\\d{9}$",
+ "flags": "",
+ "customOptions": {}
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "phone_input",
+ "type": "arco/v1/input",
+ "properties": {
+ "allowClear": false,
+ "disabled": false,
+ "readOnly": false,
+ "defaultValue": "",
+ "updateWhenDefaultValueChanges": false,
+ "placeholder": "please input the phone number.",
+ "error": "{{phone_form.validatedResult.phone.isInvalid}}",
+ "size": "default"
+ },
+ "traits": [
+ {
+ "type": "core/v1/slot",
+ "properties": {
+ "container": {
+ "id": "phone_form",
+ "slot": "content"
+ },
+ "ifCondition": true
+ }
+ },
+ {
+ "type": "core/v1/event",
+ "properties": {
+ "handlers": [
+ {
+ "type": "onChange",
+ "componentId": "phone_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ },
+ {
+ "type": "onBlur",
+ "componentId": "phone_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "city_form",
+ "type": "arco/v1/formControl",
+ "properties": {
+ "label": {
+ "format": "plain",
+ "raw": "city"
+ },
+ "layout": "horizontal",
+ "required": false,
+ "hidden": false,
+ "extra": "",
+ "errorMsg": "{{city_form.validatedResult.city.errors[0]?.message}}",
+ "labelAlign": "left",
+ "colon": false,
+ "help": "",
+ "labelCol": {
+ "span": 3,
+ "offset": 0
+ },
+ "wrapperCol": {
+ "span": 21,
+ "offset": 0
+ }
+ },
+ "traits": [
+ {
+ "type": "core/v1/validation",
+ "properties": {
+ "validators": [
+ {
+ "name": "city",
+ "value": "{{city_select.value}}",
+ "rules": [
+ {
+ "type": "include",
+ "validate": null,
+ "error": {
+ "message": "Please select \"Beijing\"."
+ },
+ "minLength": 0,
+ "maxLength": 0,
+ "includeList": ["Beijing"],
+ "excludeList": [],
+ "min": 0,
+ "max": 0,
+ "regex": "",
+ "flags": "",
+ "customOptions": {}
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "city_select",
+ "type": "arco/v1/select",
+ "properties": {
+ "allowClear": false,
+ "multiple": false,
+ "allowCreate": false,
+ "bordered": true,
+ "defaultValue": "Beijing",
+ "disabled": false,
+ "labelInValue": false,
+ "loading": false,
+ "showSearch": false,
+ "unmountOnExit": false,
+ "options": [
+ {
+ "value": "Beijing",
+ "text": "Beijing"
+ },
+ {
+ "value": "London",
+ "text": "London"
+ },
+ {
+ "value": "NewYork",
+ "text": "NewYork"
+ }
+ ],
+ "placeholder": "Select city",
+ "size": "default",
+ "error": false,
+ "updateWhenDefaultValueChanges": false
+ },
+ "traits": [
+ {
+ "type": "core/v1/slot",
+ "properties": {
+ "container": {
+ "id": "city_form",
+ "slot": "content"
+ },
+ "ifCondition": true
+ }
+ },
+ {
+ "type": "core/v1/event",
+ "properties": {
+ "handlers": [
+ {
+ "type": "onChange",
+ "componentId": "city_form",
+ "method": {
+ "name": "validateAllFields",
+ "parameters": {}
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "clear_button",
+ "type": "arco/v1/button",
+ "properties": {
+ "type": "default",
+ "status": "default",
+ "long": false,
+ "size": "default",
+ "disabled": false,
+ "loading": false,
+ "shape": "square",
+ "text": "clear"
+ },
+ "traits": [
+ {
+ "type": "core/v1/event",
+ "properties": {
+ "handlers": [
+ {
+ "type": "onClick",
+ "componentId": "name_form",
+ "method": {
+ "name": "clearAllErrors",
+ "parameters": {}
+ }
+ },
+ {
+ "type": "onClick",
+ "componentId": "email_form",
+ "method": {
+ "name": "clearAllErrors",
+ "parameters": {}
+ }
+ },
+ {
+ "type": "onClick",
+ "componentId": "url_form",
+ "method": {
+ "name": "clearAllErrors",
+ "parameters": {}
+ }
+ },
+ {
+ "type": "onClick",
+ "componentId": "ip_form",
+ "method": {
+ "name": "clearErrors",
+ "parameters": {
+ "names": "{{['ip']}}"
+ }
+ }
+ },
+ {
+ "type": "onClick",
+ "componentId": "phone_form",
+ "method": {
+ "name": "clearErrors",
+ "parameters": {
+ "names": "{{['phone']}}"
+ }
+ }
+ },
+ {
+ "type": "onClick",
+ "componentId": "city_form",
+ "method": {
+ "name": "clearAllErrors",
+ "parameters": {}
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "check_name",
+ "type": "core/v1/dummy",
+ "properties": {},
+ "traits": [
+ {
+ "type": "core/v1/fetch",
+ "properties": {
+ "url": "",
+ "method": "get",
+ "lazy": false,
+ "disabled": false,
+ "headers": {},
+ "body": {},
+ "bodyType": "json",
+ "onComplete": [
+ {
+ "componentId": "name_form",
+ "method": {
+ "name": "setErrors",
+ "parameters": {
+ "errorsMap": "{{\n{\n name: [...name_form.validatedResult.name.errors, { message: 'The name is exist.' }]\n}\n}}"
+ }
+ }
+ }
+ ],
+ "onError": []
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+}
diff --git a/examples/input-components/inputValidation.json b/examples/input-components/inputValidation.json
index 21e72bc4..14149f71 100644
--- a/examples/input-components/inputValidation.json
+++ b/examples/input-components/inputValidation.json
@@ -21,10 +21,28 @@
{
"type": "core/v1/validation",
"properties": {
- "value": "{{ emailInput.value || \"\" }}",
- "maxLength": 20,
- "minLength": 10,
- "rule": "email"
+ "validators": [
+ {
+ "name": "email",
+ "value": "{{ emailInput.value || \"\" }}",
+ "rules": [
+ {
+ "type": "email",
+ "error": {
+ "message": "Please input the email."
+ }
+ },
+ {
+ "type": "length",
+ "maxLength": 20,
+ "minLength": 10,
+ "error": {
+ "message": "Please input the length between 10 and 20."
+ }
+ }
+ ]
+ }
+ ]
}
}
]
@@ -34,7 +52,7 @@
"type": "core/v1/text",
"properties": {
"value": {
- "raw": "{{ emailInput.validResult.errorMsg }}",
+ "raw": "{{ emailInput.validatedResult.email.errors[0]?.message }}",
"format": "plain"
}
},
@@ -54,10 +72,29 @@
{
"type": "core/v1/validation",
"properties": {
- "value": "{{ phoneInput.value || \"\" }}",
- "maxLength": 100,
- "minLength": 0,
- "rule": "phoneNumber"
+ "validators": [
+ {
+ "name": "phone",
+ "value": "{{ phoneInput.value || \"\" }}",
+ "rules": [
+ {
+ "type": "regex",
+ "regex": "^1[3456789]\\d{9}$",
+ "error": {
+ "message": "Please input the correct phone number."
+ }
+ },
+ {
+ "type": "length",
+ "maxLength": 100,
+ "minLength": 0,
+ "error": {
+ "message": "Please input the length between 0 and 100."
+ }
+ }
+ ]
+ }
+ ]
}
}
]
@@ -95,7 +132,7 @@
"parameters": "{{ `email:${ emailInput.value } phone:${ phoneInput.value }` }}"
},
"wait": {},
- "disabled": "{{ emailInput.validResult.isInvalid || phoneInput.validResult.isInvalid }}"
+ "disabled": "{{ emailInput.validatedResult.email.isInvalid || phoneInput.validatedResult.phone.isInvalid }}"
}
]
}
diff --git a/examples/table/tableWithForm.json b/examples/table/tableWithForm.json
index c93cd7a0..78756fba 100644
--- a/examples/table/tableWithForm.json
+++ b/examples/table/tableWithForm.json
@@ -264,9 +264,22 @@
{
"type": "core/v1/validation",
"properties": {
- "value": "{{ nameInput.value || \"\" }}",
- "maxLength": 10,
- "minLength": 2
+ "validators": [
+ {
+ "name": "name",
+ "value": "{{ nameInput.value || \"\" }}",
+ "rules": [
+ {
+ "type": "length",
+ "maxLength": 10,
+ "minLength": 2,
+ "error": {
+ "message": "Please input the length between 2 and 10."
+ }
+ }
+ ]
+ }
+ ]
}
}
]
diff --git a/packages/arco-lib/package.json b/packages/arco-lib/package.json
index 372be297..b5f48a96 100644
--- a/packages/arco-lib/package.json
+++ b/packages/arco-lib/package.json
@@ -21,7 +21,7 @@
"scripts": {
"dev": "vite",
"typings": "tsc --emitDeclarationOnly",
- "build": "tsup src/index.ts --format cjs,esm,iife --legacy-output --inject ./react-import.js --clean --no-splitting --sourcemap",
+ "build": "tsup src/index.ts src/widgets/index.ts --format cjs,esm,iife --legacy-output --inject ./react-import.js --clean --no-splitting --sourcemap",
"serve": "vite preview",
"lint": "eslint ./src --ext .ts --ext .tsx",
"fix-lint": "eslint --fix ./src --ext .ts --ext .tsx",
diff --git a/packages/arco-lib/src/components/Table/Table.tsx b/packages/arco-lib/src/components/Table/Table.tsx
index 2c4f3ed2..134ad492 100644
--- a/packages/arco-lib/src/components/Table/Table.tsx
+++ b/packages/arco-lib/src/components/Table/Table.tsx
@@ -74,6 +74,14 @@ const rowClickStyle = css`
export const exampleProperties: Static = {
columns: [
+ {
+ title: 'Key',
+ dataIndex: 'key',
+ type: 'text',
+ displayValue: '',
+ filter: false,
+ componentSlotIndex: 0,
+ },
{
title: 'Name',
dataIndex: 'name',
@@ -107,11 +115,12 @@ export const exampleProperties: Static = {
data: Array(13)
.fill('')
.map((_, index) => ({
- key: `key ${index}`,
+ key: index,
name: `${Math.random() > 0.5 ? 'Kevin Sandra' : 'Naomi Cook'}${index}`,
link: `link${Math.random() > 0.5 ? '-A' : '-B'}`,
salary: Math.floor(Math.random() * 1000),
})),
+ rowKey: 'key',
checkCrossPage: true,
pagination: {
enablePagination: true,
@@ -172,8 +181,15 @@ export const Table = implementRuntimeComponent({
} = props;
const ref = useRef(null);
- const { pagination, rowClick, useDefaultFilter, useDefaultSort, data, ...cProps } =
- getComponentProps(props);
+ const {
+ pagination,
+ rowKey,
+ rowClick,
+ useDefaultFilter,
+ useDefaultSort,
+ data,
+ ...cProps
+ } = getComponentProps(props);
const {
pageSize,
@@ -186,6 +202,7 @@ export const Table = implementRuntimeComponent({
} = pagination;
const rowSelectionType = rowSelectionTypeMap[cProps.rowSelectionType];
+ const currentChecked = useRef<(string | number)[]>([]);
const [currentPage, setCurrentPage] = useStateValue(
defaultCurrent ?? 1,
@@ -196,6 +213,7 @@ export const Table = implementRuntimeComponent({
useEffect(() => {
mergeState({ pageSize });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [sortRule, setSortRule] = useState(null);
@@ -236,9 +254,29 @@ export const Table = implementRuntimeComponent({
// Otherwise it will automatically paginate on the front end based on the current page
return sortedData?.slice(0, pageSize);
}
+
return sortedData;
}, [pageSize, sortedData, enablePagination, useCustomPagination]);
+ // reset state when data changed
+ useEffect(() => {
+ if (!currentPageData.length) {
+ mergeState({
+ selectedRowKeys: [],
+ selectedRows: [],
+ sortRule: {},
+ filterRule: undefined,
+ currentPage: undefined,
+ });
+ }
+
+ mergeState({
+ selectedRows: currentPageData.filter(d =>
+ currentChecked.current.includes(d[rowKey])
+ ),
+ });
+ }, [currentPageData, mergeState, rowKey]);
+
useEffect(() => {
setColumns(
cProps.columns!.map((column, i) => {
@@ -310,13 +348,10 @@ export const Table = implementRuntimeComponent({
case 'button':
const handleClick = () => {
const rawColumns = component.properties.columns;
- const evaledColumns =
- typeof rawColumns === 'string'
- ? (services.stateManager.maskedEval(
- rawColumns,
- evalOptions
- ) as ColumnProperty[])
- : services.stateManager.deepEval(rawColumns, evalOptions);
+ const evaledColumns = services.stateManager.deepEval(
+ rawColumns,
+ evalOptions
+ ) as ColumnProperty[];
const evaledButtonConfig = evaledColumns[i].btnCfg;
if (!evaledButtonConfig) return;
@@ -466,6 +501,7 @@ export const Table = implementRuntimeComponent({
return (
;
+ }
+}
+
+export const TablePrimaryKeyWidget: React.FC<
+ WidgetProps
+> = props => {
+ const { value, onChange, component } = props;
+ const { properties } = component;
+ const columns = properties.columns as Static[];
+
+ const keys = columns.map(c => c.dataIndex);
+
+ return (
+
+ );
+};
+
+export default implementWidget({
+ version: ARCO_V1_VERSION,
+ metadata: {
+ name: 'primaryKey',
+ },
+})(TablePrimaryKeyWidget);
diff --git a/packages/arco-lib/src/widgets/index.ts b/packages/arco-lib/src/widgets/index.ts
new file mode 100644
index 00000000..5febe976
--- /dev/null
+++ b/packages/arco-lib/src/widgets/index.ts
@@ -0,0 +1,3 @@
+import TablePrimaryKeyWidget from './TablePrimaryKeyWidget';
+
+export const widgets = [TablePrimaryKeyWidget];
diff --git a/packages/chakra-ui-lib/src/components/Checkbox.tsx b/packages/chakra-ui-lib/src/components/Checkbox.tsx
index 1d90d5bb..f2e148cf 100644
--- a/packages/chakra-ui-lib/src/components/Checkbox.tsx
+++ b/packages/chakra-ui-lib/src/components/Checkbox.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { Static, Type } from '@sinclair/typebox';
import { Checkbox as BaseCheckbox, useCheckboxGroupContext } from '@chakra-ui/react';
import { implementRuntimeComponent, Text, TextPropertySpec } from '@sunmao-ui/runtime';
@@ -116,7 +116,7 @@ export default implementRuntimeComponent({
colorScheme,
mergeState,
customStyle,
- elementRef,
+ getElement,
}) => {
const groupContext = useCheckboxGroupContext();
let _defaultIsChecked = false;
@@ -127,6 +127,8 @@ export default implementRuntimeComponent({
}
const [checked, setChecked] = useState(_defaultIsChecked);
+ const ref = useRef(null);
+
useEffect(() => {
mergeState({ text: text.raw });
}, [mergeState, text.raw]);
@@ -143,6 +145,12 @@ export default implementRuntimeComponent({
setChecked(!!defaultIsChecked);
}, [setChecked, defaultIsChecked]);
+ useEffect(() => {
+ if (getElement && ref.current) {
+ getElement(ref.current.parentElement as HTMLElement);
+ }
+ }, [getElement, ref]);
+
const args: {
colorScheme?: Static>;
size?: Static;
@@ -170,7 +178,7 @@ export default implementRuntimeComponent({
className={css`
${customStyle?.content}
`}
- ref={elementRef}
+ ref={ref}
>
diff --git a/packages/chakra-ui-lib/src/components/Table/TableTd.tsx b/packages/chakra-ui-lib/src/components/Table/TableTd.tsx
index a025e5e8..beb86bba 100644
--- a/packages/chakra-ui-lib/src/components/Table/TableTd.tsx
+++ b/packages/chakra-ui-lib/src/components/Table/TableTd.tsx
@@ -38,7 +38,7 @@ export const TableTd: React.FC<{
let buttonConfig = column.buttonConfig;
if (column.displayValue) {
- const result = services.stateManager.maskedEval(column.displayValue, evalOptions);
+ const result = services.stateManager.deepEval(column.displayValue, evalOptions);
value = result instanceof ExpressionError ? '' : result;
}
@@ -66,12 +66,10 @@ export const TableTd: React.FC<{
case 'button':
const onClick = () => {
onClickItem();
- const evaledColumns =
- typeof rawColumns === 'string'
- ? (services.stateManager.maskedEval(rawColumns, evalOptions) as Static<
- typeof ColumnsPropertySpec
- >)
- : services.stateManager.deepEval(rawColumns, evalOptions);
+ const evaledColumns = services.stateManager.deepEval(
+ rawColumns,
+ evalOptions
+ ) as Static;
evaledColumns[index].buttonConfig.handlers.forEach(evaledHandler => {
services.apiService.send('uiMethod', {
diff --git a/packages/chakra-ui-lib/src/index.ts b/packages/chakra-ui-lib/src/index.ts
index bd59cd7e..bfdce0eb 100644
--- a/packages/chakra-ui-lib/src/index.ts
+++ b/packages/chakra-ui-lib/src/index.ts
@@ -60,3 +60,5 @@ export const sunmaoChakraUILib: SunmaoLib = {
modules: [],
utilMethods: [ChakraUIToastUtilMethodFactory],
};
+
+export { widgets } from './widgets';
diff --git a/packages/editor-sdk/src/components/Form/ArrayTable.tsx b/packages/editor-sdk/src/components/Form/ArrayTable.tsx
index bf1769b8..2e58e420 100644
--- a/packages/editor-sdk/src/components/Form/ArrayTable.tsx
+++ b/packages/editor-sdk/src/components/Form/ArrayTable.tsx
@@ -147,7 +147,7 @@ export const ArrayTable: React.FC = props => {
/>
))
) : (
-
+
No Data
|
diff --git a/packages/editor-sdk/src/components/Form/RecordEditor.tsx b/packages/editor-sdk/src/components/Form/RecordEditor.tsx
index 6dde22ce..e5200282 100644
--- a/packages/editor-sdk/src/components/Form/RecordEditor.tsx
+++ b/packages/editor-sdk/src/components/Form/RecordEditor.tsx
@@ -155,7 +155,7 @@ const RowItem = (props: RowItemProps) => {
) : (
(() => {
- const evaledResult = stateManager.maskedEval(value);
+ const evaledResult = stateManager.deepEval(value);
return (
diff --git a/packages/editor-sdk/src/components/Widgets/ExpressionWidget.tsx b/packages/editor-sdk/src/components/Widgets/ExpressionWidget.tsx
index 5db1c487..b901c4f0 100644
--- a/packages/editor-sdk/src/components/Widgets/ExpressionWidget.tsx
+++ b/packages/editor-sdk/src/components/Widgets/ExpressionWidget.tsx
@@ -170,7 +170,7 @@ export const ExpressionWidget: React.FC> = pro
try {
const value = getParsedValue(code, type);
const result = isExpression(value)
- ? services.stateManager.maskedEval(value)
+ ? services.stateManager.deepEval(value)
: value;
if (result instanceof ExpressionError) {
diff --git a/packages/editor-sdk/src/types/editor.ts b/packages/editor-sdk/src/types/editor.ts
index 5534f057..b2bdb6e5 100644
--- a/packages/editor-sdk/src/types/editor.ts
+++ b/packages/editor-sdk/src/types/editor.ts
@@ -14,7 +14,7 @@ export interface EditorServices {
};
stateManager: {
store: Record;
- maskedEval: Function;
+ deepEval: Function;
};
widgetManager: WidgetManager;
}
diff --git a/packages/editor/__tests__/model/fieldModel.spec.ts b/packages/editor/__tests__/model/fieldModel.spec.ts
index 161ad68a..11bfee72 100644
--- a/packages/editor/__tests__/model/fieldModel.spec.ts
+++ b/packages/editor/__tests__/model/fieldModel.spec.ts
@@ -20,12 +20,29 @@ describe('Field test', () => {
'value',
]);
expect(field.refComponentInfos['list' as ComponentId].refProperties).toEqual([
- '[0]',
- '[0].text',
+ '0',
+ '0.text',
]);
expect(field.rawValue).toEqual('{{input.value}} + {{list[0].text}}');
});
+ it('allow using question mask in expression', () => {
+ const field = new FieldModel('{{api.fetch?.data }}');
+ expect(field.isDynamic).toEqual(true);
+ expect(field.refComponentInfos['api' as ComponentId].refProperties).toEqual([
+ 'fetch',
+ 'fetch.data',
+ ]);
+ });
+
+ it('stop member expression when meeting other expression ast node', () => {
+ const field = new FieldModel('{{ Array.from([]).fill() }}');
+ expect(field.isDynamic).toEqual(true);
+ expect(field.refComponentInfos['Array' as ComponentId].refProperties).toEqual([
+ 'from',
+ ]);
+ });
+
it('parse inline variable in expression', () => {
const field = new FieldModel('{{ [].length }}');
expect(field.isDynamic).toEqual(true);
@@ -71,6 +88,30 @@ describe('Field test', () => {
expect(field.refComponentInfos).toEqual({});
});
+ it('should not count variables declared in iife in refs', () => {
+ const field = new FieldModel('{{(function() {const foo = "bar"})() }}');
+ expect(field.isDynamic).toEqual(true);
+ expect(field.refComponentInfos).toEqual({});
+ });
+
+ it('should not count object keys in refs', () => {
+ const field = new FieldModel('{{ {foo: 1, bar: 2, baz: 3, } }}');
+ expect(field.isDynamic).toEqual(true);
+ expect(field.refComponentInfos).toEqual({});
+ });
+
+ it('should not count keys of object destructuring assignment in refs', () => {
+ const field = new FieldModel('{{ ({foo: bar}) => bar }}');
+ expect(field.isDynamic).toEqual(true);
+ expect(field.refComponentInfos).toEqual({});
+ });
+
+ it('should not count keys of array destructuring assignment in refs', () => {
+ const field = new FieldModel('{{ ([bar]) => bar }}');
+ expect(field.isDynamic).toEqual(true);
+ expect(field.refComponentInfos).toEqual({});
+ });
+
it('get value by path', () => {
const field = new FieldModel({ foo: [{}, { bar: { baz: 'Hello, world!' } }] });
expect(field.getPropertyByPath('foo.1.bar.baz')?.getValue()).toEqual('Hello, world!');
diff --git a/packages/editor/__tests__/validator/component.spec.ts b/packages/editor/__tests__/validator/component.spec.ts
index ab0002a3..b9394160 100644
--- a/packages/editor/__tests__/validator/component.spec.ts
+++ b/packages/editor/__tests__/validator/component.spec.ts
@@ -5,6 +5,7 @@ import {
UseDependencyInExpressionSchema,
LocalVariableInIIFEExpressionSchema,
DynamicStateTraitAnyTypeSchema,
+ NestedObjectExpressionSchema,
} from './mock';
import { SchemaValidator } from '../../src/validator';
import { registry } from '../services';
@@ -68,5 +69,11 @@ describe('Validate component', () => {
);
expect(_result.length).toBe(0);
});
+ it('allow use nested object value in expression', () => {
+ const _result = schemaValidator.validate(
+ new AppModel(NestedObjectExpressionSchema, registry)
+ );
+ expect(_result.length).toBe(0);
+ });
});
});
diff --git a/packages/editor/__tests__/validator/mock.ts b/packages/editor/__tests__/validator/mock.ts
index 4b25eed5..2b62a848 100644
--- a/packages/editor/__tests__/validator/mock.ts
+++ b/packages/editor/__tests__/validator/mock.ts
@@ -414,3 +414,44 @@ export const DynamicStateTraitSchema: ComponentSchema[] = [
traits: [],
},
];
+
+export const NestedObjectExpressionSchema: ComponentSchema[] = [
+ {
+ id: 'api0',
+ type: 'core/v1/dummy',
+ properties: {},
+ traits: [
+ {
+ type: 'core/v1/fetch',
+ properties: {
+ url: '',
+ method: 'get',
+ lazy: false,
+ disabled: false,
+ headers: {},
+ body: {},
+ bodyType: 'json',
+ onComplete: [
+ {
+ componentId: '',
+ method: {
+ name: '',
+ },
+ },
+ ],
+ onError: [
+ {
+ componentId: '',
+ method: {
+ name: '',
+ parameters: {},
+ },
+ // should not warn api0.fetch.code
+ disabled: '{{ api0.fetch.code !== 401 }}',
+ },
+ ],
+ },
+ },
+ ],
+ },
+];
diff --git a/packages/editor/src/AppModel/FieldModel.ts b/packages/editor/src/AppModel/FieldModel.ts
index 3dcffda8..89cd71de 100644
--- a/packages/editor/src/AppModel/FieldModel.ts
+++ b/packages/editor/src/AppModel/FieldModel.ts
@@ -20,6 +20,16 @@ import escodegen from 'escodegen';
import { JSONSchema7 } from 'json-schema';
export type FunctionNode = ASTNode & { params: ASTNode[] };
+export type DeclaratorNode = ASTNode & { id: ASTNode };
+export type ObjectPatternNode = ASTNode & { properties: PropertyNode[] };
+export type ArrayPatternNode = ASTNode & { elements: ASTNode[] };
+export type PropertyNode = ASTNode & { value: ASTNode };
+export type LiteralNode = ASTNode & { raw: string };
+export type SequenceExpressionNode = ASTNode & { expressions: LiteralNode[] };
+export type ExpressionNode = ASTNode & {
+ object: ExpressionNode;
+ property: ExpressionNode | LiteralNode;
+};
export class FieldModel implements IFieldModel {
isDynamic = false;
refComponentInfos: Record = {};
@@ -158,15 +168,14 @@ export class FieldModel implements IFieldModel {
exps.forEach(exp => {
let lastIdentifier: ComponentId = '' as ComponentId;
const node = (acornLoose as typeof acorn).parse(exp, { ecmaVersion: 2020 });
-
this.astNodes[exp] = node as ASTNode;
-
- // These are varirables of iife, they should be count in refs.
- let localVariables: ASTNode[] = [];
+ // These are variables of iife or other identifiers, they can't be validated
+ // so they should not be added in refs
+ let whiteList: ASTNode[] = [];
simpleWalk(node, {
Function: functionNode => {
- localVariables = [...localVariables, ...(functionNode as FunctionNode).params];
+ whiteList = [...whiteList, ...(functionNode as FunctionNode).params];
},
Expression: expressionNode => {
switch (expressionNode.type) {
@@ -180,7 +189,7 @@ export class FieldModel implements IFieldModel {
this.refComponentInfos[key].componentIdASTNodes.push(
expressionNode as ASTNode
);
- } else {
+ } else if (key) {
this.refComponentInfos[key] = {
componentIdASTNodes: [expressionNode as ASTNode],
refProperties: [],
@@ -190,27 +199,61 @@ export class FieldModel implements IFieldModel {
break;
case 'MemberExpression':
- const str = exp.slice(expressionNode.start, expressionNode.end);
- let path = str.replace(lastIdentifier, '');
- if (path.startsWith('.')) {
- path = path.slice(1, path.length);
+ if (lastIdentifier) {
+ this.refComponentInfos[lastIdentifier]?.refProperties.push(
+ this.genPathFromMemberExpressionNode(expressionNode as ExpressionNode)
+ );
}
- this.refComponentInfos[lastIdentifier]?.refProperties.push(path);
+ break;
+ case 'SequenceExpression':
+ const sequenceExpression = expressionNode as SequenceExpressionNode;
+ whiteList.push(sequenceExpression.expressions[1]);
+ break;
+ case 'Literal':
+ // do nothing, just stop it from going to default
break;
default:
+ // clear lastIdentifier when meet other astNode to break the MemberExpression chain
+ lastIdentifier = '' as ComponentId;
}
},
+ ObjectPattern: objPatternNode => {
+ const propertyNodes = (objPatternNode as ObjectPatternNode).properties;
+ propertyNodes.forEach(property => {
+ whiteList.push(property.value);
+ });
+ },
+ ArrayPattern: arrayPatternNode => {
+ whiteList = [...whiteList, ...(arrayPatternNode as ArrayPatternNode).elements];
+ },
+ VariableDeclarator: declarator => {
+ whiteList.push((declarator as DeclaratorNode).id);
+ },
});
-
- // remove localVariables from refs
+ // remove whiteList from refs
for (const key in this.refComponentInfos) {
- if (localVariables.some(({ name }) => key === name)) {
+ if (whiteList.some(({ name }) => key === name)) {
delete this.refComponentInfos[key as any];
}
}
});
}
+ private genPathFromMemberExpressionNode(expNode: ExpressionNode) {
+ const path: string[] = [];
+ function travel(node: ExpressionNode) {
+ path.unshift(
+ node.property?.name || (node.property as LiteralNode)?.raw || node.name
+ );
+ if (node.object) {
+ travel(node.object);
+ }
+ }
+
+ travel(expNode);
+ return path.slice(1).join('.');
+ }
+
private onReferenceIdChange({ oldId, newId }: AppModelEventType['idChange']) {
if (!this.componentModel) {
return;
diff --git a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx
index 7ebd9fb8..33bd3f25 100644
--- a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx
+++ b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx
@@ -10,7 +10,7 @@ import {
} from '@chakra-ui/react';
import { RegistryInterface } from '@sunmao-ui/runtime';
import React, { useMemo } from 'react';
-import { ignoreTraitsList } from '../../../constants';
+import { hideCreateTraitsList } from '../../../constants';
import { ComponentSchema } from '@sunmao-ui/core';
type Props = {
@@ -30,7 +30,9 @@ export const AddTraitButton: React.FC = props => {
[component]
);
const traitTypes = useMemo(() => {
- return registry.getAllTraitTypes().filter(type => !ignoreTraitsList.includes(type));
+ return registry
+ .getAllTraitTypes()
+ .filter(type => !hideCreateTraitsList.includes(type));
}, [registry]);
const menuItems = traitTypes.map(type => {
diff --git a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/GeneralTraitForm.tsx b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/GeneralTraitForm.tsx
index 3663cd0c..6d0bd7d4 100644
--- a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/GeneralTraitForm.tsx
+++ b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/GeneralTraitForm.tsx
@@ -3,7 +3,6 @@ import { ComponentSchema, TraitSchema } from '@sunmao-ui/core';
import { HStack, IconButton, VStack } from '@chakra-ui/react';
import { generateDefaultValueFromSpec } from '@sunmao-ui/shared';
import { CloseIcon } from '@chakra-ui/icons';
-import { TSchema } from '@sinclair/typebox';
import { formWrapperCSS } from '../style';
import { EditorServices } from '../../../types';
import { SpecWidget } from '@sunmao-ui/editor-sdk';
@@ -28,7 +27,10 @@ export const GeneralTraitForm: React.FC = props => {
);
const fields = Object.keys(properties || []).map((key: string) => {
const value = trait.properties[key];
- const propertySpec = (tImpl.spec.properties as TSchema).properties?.[key];
+ const propertySpec = tImpl.spec.properties.properties?.[key];
+
+ if (!propertySpec) return undefined;
+
const onChange = (newValue: any) => {
const operation = genOperation(registry, 'modifyTraitProperty', {
componentId: component.id,
@@ -41,15 +43,14 @@ export const GeneralTraitForm: React.FC = props => {
eventBus.send('operation', operation);
};
+ const specObj = propertySpec === true ? {} : propertySpec;
+
return (
= props => {
const [search, setSearch] = useState('');
const { components, onSelectComponent, services } = props;
const { editorStore } = services;
- const wrapperRef = useRef(null);
-
+ const scrollWrapper = useRef(null);
const onSelectOption = useCallback(
({ item }: { item: Item }) => {
onSelectComponent(item.value);
@@ -49,10 +48,19 @@ export const StructureTree: React.FC = props => {
// wait the component tree to be expanded
setTimeout(() => {
const selectedElement: HTMLElement | undefined | null =
- wrapperRef.current?.querySelector(`#tree-item-${selectedId}`);
+ scrollWrapper.current?.querySelector(`#tree-item-${selectedId}`);
- if (selectedElement) {
- scrollIntoView(selectedElement, { time: 0, align: { lockX: true } });
+ const wrapperRect = scrollWrapper.current?.getBoundingClientRect();
+ const eleRect = selectedElement?.getBoundingClientRect();
+ if (
+ selectedElement &&
+ eleRect &&
+ wrapperRect &&
+ (eleRect.top < wrapperRect.top ||
+ eleRect.top > wrapperRect.top + wrapperRect?.height)
+ ) {
+ // check selected element is outside of view
+ scrollIntoView(selectedElement, { time: 300, align: { lockX: true } });
}
});
}
@@ -90,7 +98,6 @@ export const StructureTree: React.FC = props => {
return (
= props => {
-
+
{componentEles.length > 0 ? (
componentEles
) : (
diff --git a/packages/editor/src/constants/index.ts b/packages/editor/src/constants/index.ts
index 6bdca94d..7c4812f4 100644
--- a/packages/editor/src/constants/index.ts
+++ b/packages/editor/src/constants/index.ts
@@ -4,14 +4,16 @@ import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared';
export const unremovableTraits = [`${CORE_VERSION}/${CoreTraitName.Slot}`];
-export const ignoreTraitsList = [
+export const hideCreateTraitsList = [
`${CORE_VERSION}/${CoreTraitName.Event}`,
`${CORE_VERSION}/${CoreTraitName.Style}`,
`${CORE_VERSION}/${CoreTraitName.Fetch}`,
+ `${CORE_VERSION}/${CoreTraitName.Slot}`,
];
export const hasSpecialFormTraitList = [
- ...ignoreTraitsList,
+ `${CORE_VERSION}/${CoreTraitName.Event}`,
+ `${CORE_VERSION}/${CoreTraitName.Style}`,
`${CORE_VERSION}/${CoreTraitName.Fetch}`,
];
diff --git a/packages/editor/src/main.tsx b/packages/editor/src/main.tsx
index fa22bfca..e4078d6a 100644
--- a/packages/editor/src/main.tsx
+++ b/packages/editor/src/main.tsx
@@ -1,9 +1,8 @@
import { StrictMode } from 'react';
import ReactDOM from 'react-dom';
import { RegistryInterface } from '@sunmao-ui/runtime';
-import { sunmaoChakraUILib } from '@sunmao-ui/chakra-ui-lib';
-import { widgets as chakraWidgets } from '@sunmao-ui/chakra-ui-lib/dist/esm/widgets/index';
-import { ArcoDesignLib } from '@sunmao-ui/arco-lib';
+import { sunmaoChakraUILib, widgets as chakraWidgets } from '@sunmao-ui/chakra-ui-lib';
+import { ArcoDesignLib, widgets as arcoWidgets } from '@sunmao-ui/arco-lib';
import { initSunmaoUIEditor } from './init';
import { LocalStorageManager } from './LocalStorageManager';
import '@sunmao-ui/arco-lib/dist/index.css';
@@ -17,7 +16,7 @@ type Options = Partial<{
const lsManager = new LocalStorageManager();
const { Editor, registry } = initSunmaoUIEditor({
- widgets: [...chakraWidgets],
+ widgets: [...chakraWidgets, ...arcoWidgets],
storageHandler: {
onSaveApp(app) {
lsManager.saveAppInLS(app);
diff --git a/packages/editor/src/validator/rules/PropertiesRules.ts b/packages/editor/src/validator/rules/PropertiesRules.ts
index 69929d1d..d1e05350 100644
--- a/packages/editor/src/validator/rules/PropertiesRules.ts
+++ b/packages/editor/src/validator/rules/PropertiesRules.ts
@@ -77,7 +77,7 @@ class ExpressionValidatorRule implements PropertiesValidatorRule {
private checkObjHasPath(obj: Record, path: string) {
const arr = path.split('.');
- const curr = obj;
+ let curr = obj;
for (const key of arr) {
const value = curr[key];
if (value === undefined) {
@@ -86,6 +86,7 @@ class ExpressionValidatorRule implements PropertiesValidatorRule {
// if meet AnyTypePlaceholder, return true and skip
return true;
}
+ curr = value;
}
return true;
}
@@ -102,6 +103,7 @@ class ExpressionValidatorRule implements PropertiesValidatorRule {
// validate expression
properties.traverse((fieldModel, key) => {
Object.keys(fieldModel.refComponentInfos).forEach((id: string) => {
+ if (!id) return;
const targetComponent = appModel.getComponentById(id as ComponentId);
const paths = fieldModel.refComponentInfos[id as ComponentId].refProperties;
diff --git a/packages/editor/src/validator/rules/TraitRules.ts b/packages/editor/src/validator/rules/TraitRules.ts
index d62cb7ad..b4180870 100644
--- a/packages/editor/src/validator/rules/TraitRules.ts
+++ b/packages/editor/src/validator/rules/TraitRules.ts
@@ -8,11 +8,28 @@ import { GLOBAL_UTIL_METHOD_ID } from '@sunmao-ui/runtime';
import { isExpression } from '../utils';
import { ComponentId, EventName } from '../../AppModel/IAppModel';
import { CORE_VERSION, CoreTraitName, EventHandlerSpec } from '@sunmao-ui/shared';
+import { get } from 'lodash';
+import { ErrorObject } from 'ajv';
class EventHandlerValidatorRule implements TraitValidatorRule {
kind: 'trait' = 'trait';
traitMethods = ['setValue', 'resetValue', 'triggerFetch'];
+ private isErrorAnExpression(
+ error: ErrorObject,
+ parameters: Record | undefined
+ ) {
+ let path = '';
+ const { instancePath, params } = error;
+ if (instancePath) {
+ path = instancePath.split('/').slice(1).join('.');
+ } else {
+ path = params.missingProperty;
+ }
+ const field = get(parameters, path);
+ return isExpression(field);
+ }
+
validate({
appModel,
trait,
@@ -87,13 +104,16 @@ class EventHandlerValidatorRule implements TraitValidatorRule {
const valid = validate(parameters);
if (!valid) {
validate.errors!.forEach(error => {
- results.push({
- message: error.message || '',
- componentId: component.id,
- property: error.instancePath,
- traitType: trait?.type,
- traitIndex,
- });
+ if (!this.isErrorAnExpression(error, parameters)) {
+ results.push({
+ message: error.message || '',
+ componentId: component.id,
+ property: error.instancePath,
+ traitType: trait?.type,
+ traitIndex,
+ });
+ return results;
+ }
});
}
} else {
@@ -125,22 +145,14 @@ class EventHandlerValidatorRule implements TraitValidatorRule {
// check component method properties type
if (method.parameters && !ajv.validate(method.parameters, parameters)) {
ajv.errors!.forEach(error => {
- if (error.keyword === 'type') {
- const { instancePath } = error;
- const path = instancePath.split('/')[1];
- const value = trait.rawProperties[path];
-
- // if value is an expression, skip it
- if (isExpression(value)) {
- return;
- }
+ if (!this.isErrorAnExpression(error, parameters)) {
+ results.push({
+ message: error.message || '',
+ componentId: component.id,
+ traitType: trait.type,
+ property: `/handlers/${i}/method/parameters${error.instancePath}`,
+ });
}
- results.push({
- message: error.message || '',
- componentId: component.id,
- traitType: trait.type,
- property: `/handlers/${i}/method/parameters${error.instancePath}`,
- });
});
}
}
diff --git a/packages/editor/types/widgets.d.ts b/packages/editor/types/widgets.d.ts
index a86b1479..2db0f345 100644
--- a/packages/editor/types/widgets.d.ts
+++ b/packages/editor/types/widgets.d.ts
@@ -1,3 +1,6 @@
declare module '@sunmao-ui/chakra-ui-lib/dist/esm/widgets/index' {
export * from '@sunmao-ui/chakra-ui-lib/lib/widgets/index';
}
+declare module '@sunmao-ui/arco-lib/dist/esm/widgets/index' {
+ export * from '@sunmao-ui/arco-lib/lib/widgets/index';
+}
diff --git a/packages/runtime/__tests__/ImplWrapper/ImplWrapper.spec.tsx b/packages/runtime/__tests__/ImplWrapper/ImplWrapper.spec.tsx
index bf38700f..2add3789 100644
--- a/packages/runtime/__tests__/ImplWrapper/ImplWrapper.spec.tsx
+++ b/packages/runtime/__tests__/ImplWrapper/ImplWrapper.spec.tsx
@@ -1,6 +1,7 @@
import '@testing-library/jest-dom/extend-expect';
import { render, screen, waitFor, act } from '@testing-library/react';
import produce from 'immer';
+import React from 'react';
import { initSunmaoUI } from '../../src';
import { TestLib } from '../testLib';
import { destroyTimesMap, renderTimesMap, clearTesterMap } from '../testLib/Tester';
@@ -11,6 +12,7 @@ import {
MergeStateSchema,
AsyncMergeStateSchema,
TabsWithSlotsSchema,
+ ParentRerenderSchema,
} from './mockSchema';
// A pure single sunmao component will render twice when it mount.
@@ -68,6 +70,28 @@ describe('hidden trait condition', () => {
});
});
+describe('when parent rerender change', () => {
+ it('the children should not rerender', () => {
+ const { App, stateManager, apiService } = initSunmaoUI({ libs: [TestLib] });
+ stateManager.noConsoleError = true;
+ const { unmount } = render();
+ const childTester = screen.getByTestId('tester');
+ expect(childTester).toHaveTextContent(SingleComponentRenderTimes);
+ act(() => {
+ apiService.send('uiMethod', {
+ componentId: 'input',
+ name: 'setValue',
+ parameters: 'foo',
+ });
+ });
+ expect(childTester).toHaveTextContent(SingleComponentRenderTimes);
+ expect(stateManager.store['input'].value).toBe('foo');
+
+ unmount();
+ clearTesterMap();
+ });
+});
+
describe('when component merge state synchronously', () => {
it('it will not cause extra render', () => {
const { App, stateManager } = initSunmaoUI({ libs: [TestLib] });
diff --git a/packages/runtime/__tests__/ImplWrapper/mockSchema.ts b/packages/runtime/__tests__/ImplWrapper/mockSchema.ts
index 47ba3644..442484c6 100644
--- a/packages/runtime/__tests__/ImplWrapper/mockSchema.ts
+++ b/packages/runtime/__tests__/ImplWrapper/mockSchema.ts
@@ -84,6 +84,55 @@ export const HiddenTraitSchema: Application = {
},
};
+export const ParentRerenderSchema: Application = {
+ version: 'sunmao/v1',
+ kind: 'Application',
+ metadata: {
+ name: 'some App',
+ },
+ spec: {
+ components: [
+ {
+ id: 'input',
+ type: 'test/v1/input',
+ properties: {
+ defaultValue: '',
+ },
+ traits: [],
+ },
+ {
+ id: 'stack6',
+ type: 'core/v1/stack',
+ properties: {
+ spacing: 12,
+ direction: 'horizontal',
+ align: 'auto',
+ wrap: '{{!!input.value}}',
+ justify: 'flex-start',
+ },
+ traits: [],
+ },
+ {
+ id: 'tester',
+ type: 'test/v1/tester',
+ properties: {},
+ traits: [
+ {
+ type: 'core/v1/slot',
+ properties: {
+ container: {
+ id: 'stack6',
+ slot: 'content',
+ },
+ ifCondition: true,
+ },
+ },
+ ],
+ },
+ ],
+ },
+};
+
export const MergeStateSchema: Application = {
version: 'sunmao/v1',
kind: 'Application',
diff --git a/packages/runtime/__tests__/expression.spec.ts b/packages/runtime/__tests__/expression.spec.ts
index 92d72fe6..c8be4539 100644
--- a/packages/runtime/__tests__/expression.spec.ts
+++ b/packages/runtime/__tests__/expression.spec.ts
@@ -1,3 +1,4 @@
+import { isProxy, reactive } from '@vue/reactivity';
import { StateManager, ExpressionError } from '../src/services/StateManager';
describe('evalExpression function', () => {
@@ -24,67 +25,72 @@ describe('evalExpression function', () => {
},
};
const stateManager = new StateManager();
+ stateManager.store = reactive>(scope);
stateManager.noConsoleError = true;
it('can eval {{}} expression', () => {
- const evalOptions = { evalListItem: false, scopeObject: scope };
+ const evalOptions = { evalListItem: false };
- expect(stateManager.maskedEval('value', evalOptions)).toEqual('value');
- expect(stateManager.maskedEval('{{true}}', evalOptions)).toEqual(true);
- expect(stateManager.maskedEval('{{ false }}', evalOptions)).toEqual(false);
- expect(stateManager.maskedEval('{{[]}}', evalOptions)).toEqual([]);
- expect(stateManager.maskedEval('{{ [] }}', evalOptions)).toEqual([]);
- expect(stateManager.maskedEval('{{ [1,2,3] }}', evalOptions)).toEqual([1, 2, 3]);
+ expect(stateManager.deepEval('value', evalOptions)).toEqual('value');
+ expect(stateManager.deepEval('{{true}}', evalOptions)).toEqual(true);
+ expect(stateManager.deepEval('{{ false }}', evalOptions)).toEqual(false);
+ expect(stateManager.deepEval('{{[]}}', evalOptions)).toEqual([]);
+ expect(stateManager.deepEval('{{ [] }}', evalOptions)).toEqual([]);
+ expect(stateManager.deepEval('{{ [1,2,3] }}', evalOptions)).toEqual([1, 2, 3]);
- expect(stateManager.maskedEval('{{ {} }}', evalOptions)).toEqual({});
- expect(stateManager.maskedEval('{{ {id: 123} }}', evalOptions)).toEqual({ id: 123 });
- expect(stateManager.maskedEval('{{nothing}}', evalOptions)).toBeInstanceOf(
+ expect(stateManager.deepEval('{{ {} }}', evalOptions)).toEqual({});
+ expect(stateManager.deepEval('{{ {id: 123} }}', evalOptions)).toEqual({ id: 123 });
+ expect(stateManager.deepEval('{{nothing}}', evalOptions)).toBeInstanceOf(
ExpressionError
);
- expect(stateManager.maskedEval('{{input1.value}}', evalOptions)).toEqual('world');
- expect(stateManager.maskedEval('{{checkbox.value}}', evalOptions)).toEqual(true);
- expect(stateManager.maskedEval('{{fetch.data}}', evalOptions)).toMatchObject([
+ expect(stateManager.deepEval('{{input1.value}}', evalOptions)).toEqual('world');
+ expect(stateManager.deepEval('{{checkbox.value}}', evalOptions)).toEqual(true);
+ expect(stateManager.deepEval('{{fetch.data}}', evalOptions)).toMatchObject([
{ id: 1 },
{ id: 2 },
]);
- expect(stateManager.maskedEval('{{{{}}}}', evalOptions)).toEqual(undefined);
+ expect(stateManager.deepEval('{{{{}}}}', evalOptions)).toEqual(undefined);
expect(
- stateManager.maskedEval('{{ value }}, {{ input1.value }}!', evalOptions)
+ stateManager.deepEval('{{ value }}, {{ input1.value }}!', evalOptions)
).toEqual('Hello, world!');
});
+ it('will not return Proxy', () => {
+ const evalOptions = { evalListItem: false };
+ const fetchData = stateManager.deepEval('{{fetch.data}}', evalOptions);
+ expect(isProxy(fetchData)).toBe(false);
+ });
+
it('can eval $listItem expression', () => {
expect(
- stateManager.maskedEval('{{ $listItem.value }}', {
+ stateManager.deepEval('{{ $listItem.value }}', {
evalListItem: false,
- scopeObject: scope,
})
).toEqual('{{ $listItem.value }}');
expect(
- stateManager.maskedEval('{{ $listItem.value }}', {
+ stateManager.deepEval('{{ $listItem.value }}', {
evalListItem: true,
- scopeObject: scope,
})
).toEqual('foo');
expect(
- stateManager.maskedEval(
+ stateManager.deepEval(
'{{ {{$listItem.value}}Input.value + {{$moduleId}}Fetch.value }}!',
- { evalListItem: true, scopeObject: scope }
+ { evalListItem: true }
)
).toEqual('Yes, ok!');
});
it('can override scope', () => {
expect(
- stateManager.maskedEval('{{value}}', {
+ stateManager.deepEval('{{value}}', {
scopeObject: { override: 'foo' },
overrideScope: true,
})
).toBeInstanceOf(ExpressionError);
expect(
- stateManager.maskedEval('{{override}}', {
+ stateManager.deepEval('{{override}}', {
scopeObject: { override: 'foo' },
overrideScope: true,
})
@@ -93,7 +99,7 @@ describe('evalExpression function', () => {
it('can fallback to specific value when error', () => {
expect(
- stateManager.maskedEval('{{wrongExp}}', {
+ stateManager.deepEval('{{wrongExp}}', {
fallbackWhenError: exp => exp,
})
).toEqual('{{wrongExp}}');
@@ -101,7 +107,7 @@ describe('evalExpression function', () => {
it('can partially eval nest expression, even when some error happens', () => {
expect(
- stateManager.maskedEval('{{text}} {{{{ $moduleId }}__state0.value}}', {
+ stateManager.deepEval('{{text}} {{{{ $moduleId }}__state0.value}}', {
scopeObject: {
$moduleId: 'myModule',
text: 'hello',
@@ -110,4 +116,46 @@ describe('evalExpression function', () => {
})
).toEqual(`hello {{myModule__state0.value}}`);
});
+
+ it('can watch the state change in the object value', () => {
+ const stateManager = new StateManager();
+
+ stateManager.noConsoleError = true;
+ stateManager.store.text = { value: 'hello' };
+
+ return new Promise(resolve => {
+ const { result } = stateManager.deepEvalAndWatch(
+ { text: '{{ text.value }}' },
+ newValue => {
+ expect(newValue.result.text).toBe('bye');
+ resolve();
+ }
+ );
+
+ expect(result.text).toBe('hello');
+
+ stateManager.store.text.value = 'bye';
+ });
+ });
+
+ it('can watch the state change in the expression string', () => {
+ const stateManager = new StateManager();
+
+ stateManager.noConsoleError = true;
+ stateManager.store.text = { value: 'hello' };
+
+ return new Promise(resolve => {
+ const { result, stop } = stateManager.deepEvalAndWatch(
+ '{{ text.value }}',
+ newValue => {
+ expect(newValue.result).toBe('bye');
+ resolve();
+ }
+ );
+
+ expect(result).toBe('hello');
+
+ stateManager.store.text.value = 'bye';
+ });
+ });
});
diff --git a/packages/runtime/index.html b/packages/runtime/index.html
index 731a90fb..4341016c 100644
--- a/packages/runtime/index.html
+++ b/packages/runtime/index.html
@@ -14,7 +14,9 @@
import { initSunmaoUI } from './src';
import { ChakraProvider } from '@chakra-ui/react';
import { sunmaoChakraUILib } from '@sunmao-ui/chakra-ui-lib';
+ import { ArcoDesignLib } from '@sunmao-ui/arco-lib';
import examples from '@example.json';
+ import '@arco-design/web-react/dist/css/arco.css';
const selectEl = document.querySelector('select');
for (const example of examples) {
@@ -29,7 +31,9 @@
const rootEl = document.querySelector('#root');
const render = example => {
ReactDOM.unmountComponentAtNode(rootEl);
- const { App, registry } = initSunmaoUI({ libs: [sunmaoChakraUILib] });
+ const { App, registry } = initSunmaoUI({
+ libs: [sunmaoChakraUILib, ArcoDesignLib],
+ });
const { app, modules = [] } = example.value;
window.registry = registry;
modules.forEach(m => {
diff --git a/packages/runtime/src/components/_internal/ImplWrapper/ImplWrapper.tsx b/packages/runtime/src/components/_internal/ImplWrapper/ImplWrapper.tsx
index 7f2f7921..c667502a 100644
--- a/packages/runtime/src/components/_internal/ImplWrapper/ImplWrapper.tsx
+++ b/packages/runtime/src/components/_internal/ImplWrapper/ImplWrapper.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { ImplWrapperProps } from '../../../types';
import { shallowCompare } from '@sunmao-ui/shared';
import { UnmountImplWrapper } from './UnmountImplWrapper';
+import { isEqual } from 'lodash';
export const ImplWrapper = React.memo(
UnmountImplWrapper,
@@ -10,20 +11,19 @@ export const ImplWrapper = React.memo(
const nextChildren = nextProps.childrenMap[nextProps.component.id]?._grandChildren;
const prevComponent = prevProps.component;
const nextComponent = nextProps.component;
- let isEqual = false;
+ let isComponentEqual = false;
if (prevChildren && nextChildren) {
- isEqual = shallowCompare(prevChildren, nextChildren);
+ isComponentEqual = shallowCompare(prevChildren, nextChildren);
} else if (prevChildren === nextChildren) {
- isEqual = true;
+ isComponentEqual = true;
}
-
return (
- isEqual &&
+ isComponentEqual &&
prevComponent === nextComponent &&
// TODO: keep ImplWrapper memorized and get slot props from store
shallowCompare(prevProps.slotProps, nextProps.slotProps) &&
- shallowCompare(prevProps.slotContext, nextProps.slotContext)
+ isEqual(prevProps.slotContext, nextProps.slotContext)
);
}
);
diff --git a/packages/runtime/src/components/_internal/ModuleRenderer.tsx b/packages/runtime/src/components/_internal/ModuleRenderer.tsx
index 18124584..ebb2725d 100644
--- a/packages/runtime/src/components/_internal/ModuleRenderer.tsx
+++ b/packages/runtime/src/components/_internal/ModuleRenderer.tsx
@@ -38,21 +38,17 @@ const ModuleRendererContent = React.forwardRef<
Props & { moduleSpec: ImplementedRuntimeModule }
>((props, ref) => {
const { moduleSpec, properties, handlers, evalScope, services, app } = props;
- const moduleId = services.stateManager.maskedEval(props.id, {
+ const moduleId = services.stateManager.deepEval(props.id, {
evalListItem: true,
scopeObject: evalScope,
}) as string | ExpressionError;
function evalObject = Record>(
obj: T
- ): PropsAfterEvaled<{ obj: T }>['obj'] {
+ ): PropsAfterEvaled {
const evalOptions = { evalListItem: true, scopeObject: evalScope };
- return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
- if (typeof value === 'string') {
- return services.stateManager.maskedEval(value, evalOptions);
- }
- return value;
- }).obj;
+
+ return services.stateManager.deepEval(obj, evalOptions) as PropsAfterEvaled;
}
// first eval the property, handlers, id of module
diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts
index 72f15665..84cd0f1f 100644
--- a/packages/runtime/src/index.ts
+++ b/packages/runtime/src/index.ts
@@ -70,6 +70,30 @@ export type { StateManagerInterface } from './services/StateManager';
export { ModuleRenderer } from './components/_internal/ModuleRenderer';
export { ImplWrapper } from './components/_internal/ImplWrapper';
export { default as Text, TextPropertySpec } from './components/_internal/Text';
+export {
+ // constants
+ PRESET_PROPERTY_CATEGORY,
+ CORE_VERSION,
+ CoreComponentName,
+ CoreTraitName,
+ CoreWidgetName,
+ StyleWidgetName,
+ EXPRESSION,
+ LIST_ITEM_EXP,
+ LIST_ITEM_INDEX_EXP,
+ SLOT_PROPS_EXP,
+ GLOBAL_UTIL_METHOD_ID,
+ GLOBAL_MODULE_ID,
+ ExpressionKeywords,
+ AnyTypePlaceholder,
+ // specs
+ EventHandlerSpec,
+ EventCallBackHandlerSpec,
+ ModuleRenderSpec,
+ // utils
+ StringUnion,
+ generateDefaultValueFromSpec,
+} from '@sunmao-ui/shared';
// TODO: check this export
export { watch } from './utils/watchReactivity';
diff --git a/packages/runtime/src/services/StateManager.ts b/packages/runtime/src/services/StateManager.ts
index e7160564..bc5a7d4a 100644
--- a/packages/runtime/src/services/StateManager.ts
+++ b/packages/runtime/src/services/StateManager.ts
@@ -29,6 +29,8 @@ type EvalOptions = {
ignoreEvalError?: boolean;
};
+type EvaledResult = T extends string ? unknown : PropsAfterEvaled>;
+
// TODO: use web worker
const DefaultDependencies = {
dayjs,
@@ -94,7 +96,7 @@ export class StateManager {
}
};
- maskedEval(raw: string, options: EvalOptions = {}): unknown | ExpressionError {
+ private _maskedEval(raw: string, options: EvalOptions = {}): unknown | ExpressionError {
const { evalListItem = false, fallbackWhenError } = options;
let result: unknown[] = [];
@@ -117,6 +119,7 @@ export class StateManager {
result = expChunk.map(e => this.evalExp(e, options));
if (result.length === 1) {
+ if (isProxy(result[0])) return toRaw(result[0]);
return result[0];
}
return result.join('');
@@ -134,6 +137,13 @@ export class StateManager {
}
}
+ /**
+ * @deprecated please use the `deepEval` instead
+ */
+ maskedEval(raw: string, options: EvalOptions = {}): unknown | ExpressionError {
+ return this.deepEval(raw, options);
+ }
+
mapValuesDeep(
obj: T,
fn: (params: {
@@ -157,58 +167,74 @@ export class StateManager {
}) as PropsAfterEvaled;
}
- deepEval | any[]>(
- obj: T,
+ deepEval | any[] | string>(
+ value: T,
options: EvalOptions = {}
- ): PropsAfterEvaled {
+ ): EvaledResult {
// just eval
- const evaluated = this.mapValuesDeep(obj, ({ value }) => {
- if (typeof value !== 'string') {
- return value;
- }
- return this.maskedEval(value, options);
- });
-
- return evaluated;
+ if (typeof value !== 'string') {
+ return this.mapValuesDeep(value, ({ value }) => {
+ if (typeof value !== 'string') {
+ return value;
+ }
+ return this._maskedEval(value, options);
+ }) as EvaledResult;
+ } else {
+ return this._maskedEval(value, options) as EvaledResult;
+ }
}
- deepEvalAndWatch | any[]>(
- obj: T,
- watcher: (params: { result: PropsAfterEvaled }) => void,
+ deepEvalAndWatch | any[] | string>(
+ value: T,
+ watcher: (params: { result: EvaledResult }) => void,
options: EvalOptions = {}
) {
const stops: ReturnType[] = [];
// just eval
- const evaluated = this.deepEval(obj, options);
+ const evaluated = this.deepEval(value, options) as T extends string
+ ? unknown
+ : PropsAfterEvaled>;
// watch change
- let resultCache: PropsAfterEvaled = evaluated;
- this.mapValuesDeep(obj, ({ value, path }) => {
- const isDynamicExpression =
- typeof value === 'string' &&
- parseExpression(value).some(exp => typeof exp !== 'string');
+ if (value && typeof value === 'object') {
+ let resultCache = evaluated as PropsAfterEvaled>;
- if (!isDynamicExpression) return;
+ this.mapValuesDeep(value, ({ value, path }) => {
+ const isDynamicExpression =
+ typeof value === 'string' &&
+ parseExpression(value).some(exp => typeof exp !== 'string');
+ if (!isDynamicExpression) return;
+
+ const stop = watch(
+ () => {
+ const result = this._maskedEval(value as string, options);
+
+ return result;
+ },
+ newV => {
+ resultCache = produce(resultCache, draft => {
+ set(draft, path, newV);
+ });
+ watcher({ result: resultCache as EvaledResult });
+ }
+ );
+ stops.push(stop);
+ });
+ } else {
const stop = watch(
() => {
- const result = this.maskedEval(value as string, options);
+ const result = this._maskedEval(value, options);
return result;
},
newV => {
- if (isProxy(newV)) {
- newV = toRaw(newV);
- }
- resultCache = produce(resultCache, draft => {
- set(draft, path, newV);
- });
- watcher({ result: resultCache });
+ watcher({ result: newV as EvaledResult });
}
);
stops.push(stop);
- });
+ }
return {
result: evaluated,
diff --git a/packages/runtime/src/traits/core/Event.tsx b/packages/runtime/src/traits/core/Event.tsx
index 04e08be3..a7db33e9 100644
--- a/packages/runtime/src/traits/core/Event.tsx
+++ b/packages/runtime/src/traits/core/Event.tsx
@@ -31,12 +31,9 @@ export const generateCallback = (
scopeObject: { $slot: slotProps },
evalListItem,
};
- const evaledHandlers =
- typeof rawHandlers === 'string'
- ? (stateManager.maskedEval(rawHandlers, evalOptions) as Static<
- typeof EventCallBackHandlerSpec
- >[])
- : stateManager.deepEval(rawHandlers, evalOptions);
+ const evaledHandlers = stateManager.deepEval(rawHandlers, evalOptions) as Static<
+ typeof EventCallBackHandlerSpec
+ >[];
const evaledHandler = evaledHandlers[index];
if (evaledHandler.disabled && typeof evaledHandler.disabled === 'boolean') {
diff --git a/packages/runtime/src/traits/core/Validation.tsx b/packages/runtime/src/traits/core/Validation.tsx
index 2960c9f7..5e98854f 100644
--- a/packages/runtime/src/traits/core/Validation.tsx
+++ b/packages/runtime/src/traits/core/Validation.tsx
@@ -1,112 +1,384 @@
-import { Static, Type } from '@sinclair/typebox';
-import { isEqual } from 'lodash';
+import { Type, Static, TSchema } from '@sinclair/typebox';
import { implementRuntimeTrait } from '../../utils/buildKit';
-import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared';
+import { CORE_VERSION, CoreTraitName, StringUnion } from '@sunmao-ui/shared';
-type ValidationResult = Static;
-type ValidationRule = (text: string) => ValidationResult;
+type ParseValidateOption<
+ T extends Record,
+ OptionalKeys extends keyof T = ''
+> = {
+ [K in keyof T as K extends OptionalKeys ? never : K]: Static;
+} & {
+ [K in OptionalKeys]?: Static;
+};
+type UnionToIntersection = (
+ TUnion extends unknown ? (params: TUnion) => unknown : never
+) extends (params: infer Params) => unknown
+ ? Params
+ : never;
-const ResultSpec = Type.Object({
- isInvalid: Type.Boolean(),
- errorMsg: Type.String(),
-});
+const validateOptionMap = {
+ length: {
+ minLength: Type.Number({ title: 'Min length' }),
+ maxLength: Type.Number({ title: 'Max length' }),
+ },
+ include: {
+ includeList: Type.Array(Type.String(), { title: 'Include list' }),
+ },
+ exclude: {
+ excludeList: Type.Array(Type.String(), { title: 'Exclude List' }),
+ },
+ number: {
+ min: Type.Number({ title: 'Min' }),
+ max: Type.Number({ title: 'Max' }),
+ },
+ regex: {
+ regex: Type.String({ title: 'Regex', description: 'The regular expression string.' }),
+ flags: Type.String({
+ title: 'Flags',
+ description: 'The flags of the regular expression.',
+ }),
+ },
+};
+const validateFnMap = {
+ required(value: string) {
+ return value !== undefined && value !== null && value !== '';
+ },
+ length(
+ value: string,
+ {
+ minLength,
+ maxLength,
+ }: ParseValidateOption
+ ) {
+ if (minLength !== undefined && value.length < minLength) {
+ return false;
+ }
-export const ValidationTraitStateSpec = Type.Object({
- validResult: ResultSpec,
+ if (maxLength !== undefined && value.length > maxLength) {
+ return false;
+ }
+
+ return true;
+ },
+ include(
+ value: string,
+ { includeList }: ParseValidateOption
+ ) {
+ return includeList.includes(value);
+ },
+ exclude(
+ value: string,
+ { excludeList }: ParseValidateOption
+ ) {
+ return !excludeList.includes(value);
+ },
+ regex(
+ value: string,
+ { regex, flags }: ParseValidateOption
+ ) {
+ return new RegExp(regex, flags).test(value);
+ },
+ number(
+ value: string | number,
+ { min, max }: ParseValidateOption
+ ) {
+ const num = Number(value);
+
+ if (min !== undefined && num < min) {
+ return false;
+ }
+
+ if (max !== undefined && num > max) {
+ return false;
+ }
+
+ return true;
+ },
+ ipv4(value: string) {
+ return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
+ value
+ );
+ },
+ email(value: string) {
+ return /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(value);
+ },
+ url(value: string) {
+ return /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test(
+ value
+ );
+ },
+};
+
+type AllFields = UnionToIntersection<
+ typeof validateOptionMap[keyof typeof validateOptionMap]
+>;
+type AllFieldsKeys = keyof UnionToIntersection;
+
+const ValidatorSpec = Type.Object({
+ name: Type.String({
+ title: 'Name',
+ description: 'The name is used for getting the validated result.',
+ }),
+ value: Type.Any({ title: 'Value', description: 'The value need to be validated.' }),
+ rules: Type.Array(
+ Type.Object(
+ {
+ type: StringUnion(
+ Object.keys(validateFnMap).concat(['custom']) as [
+ keyof typeof validateFnMap,
+ 'custom'
+ ],
+ {
+ type: 'Type',
+ description:
+ 'The type of the rule. Setting it as `custom` to use custom validate function.',
+ }
+ ),
+ validate:
+ Type.Any({
+ title: 'Validate',
+ description:
+ 'The validate function for the rule. Return `false` means it is invalid.',
+ conditions: [
+ {
+ key: 'type',
+ value: 'custom',
+ },
+ ],
+ }) ||
+ Type.Function(
+ [Type.String(), Type.Record(Type.String(), Type.Any())],
+ Type.Boolean(),
+ {
+ conditions: [
+ {
+ key: 'type',
+ value: 'custom',
+ },
+ ],
+ }
+ ),
+ error: Type.Object(
+ {
+ message: Type.String({
+ title: 'Message',
+ description: 'The message to display when the value is invalid.',
+ }),
+ },
+ { title: 'Error' }
+ ),
+ ...(Object.keys(validateOptionMap) as [keyof typeof validateOptionMap]).reduce(
+ (result, key) => {
+ const option = validateOptionMap[key] as AllFields;
+
+ (Object.keys(option) as [AllFieldsKeys]).forEach(optionKey => {
+ if (result[optionKey]) {
+ // if the different validate functions have the same parameter
+ throw Error(
+ "[Validation Trait]: The different validate function has the same parameter, please change the parameter's name."
+ );
+ } else {
+ result[optionKey] = {
+ ...option[optionKey],
+ conditions: [{ key: 'type', value: key }],
+ } as any;
+ }
+ });
+
+ return result;
+ },
+ {} as AllFields
+ ),
+ customOptions: Type.Record(Type.String(), Type.Any(), {
+ title: 'Custom options',
+ description:
+ 'The custom options would pass to the custom validate function as the second parameter.',
+ conditions: [
+ {
+ key: 'type',
+ value: 'custom',
+ },
+ ],
+ }),
+ },
+ {
+ title: 'Rules',
+ }
+ ),
+ {
+ title: 'Rules',
+ widget: 'core/v1/array',
+ widgetOptions: { displayedKeys: ['type'] },
+ }
+ ),
});
export const ValidationTraitPropertiesSpec = Type.Object({
- value: Type.String(),
- rule: Type.Optional(Type.String()),
- maxLength: Type.Optional(Type.Integer()),
- minLength: Type.Optional(Type.Integer()),
+ validators: Type.Array(ValidatorSpec, {
+ title: 'Validators',
+ widget: 'core/v1/array',
+ widgetOptions: { displayedKeys: ['name'] },
+ }),
+});
+
+const ErrorSpec = Type.Object({
+ message: Type.String(),
+});
+
+const ValidatedResultSpec = Type.Record(
+ Type.String(),
+ Type.Object({
+ isInvalid: Type.Boolean(),
+ errors: Type.Array(ErrorSpec),
+ })
+);
+
+export const ValidationTraitStateSpec = Type.Object({
+ validatedResult: ValidatedResultSpec,
});
export default implementRuntimeTrait({
version: CORE_VERSION,
metadata: {
name: CoreTraitName.Validation,
- description: 'validation trait',
+ description: 'A trait for the form validation.',
},
spec: {
properties: ValidationTraitPropertiesSpec,
state: ValidationTraitStateSpec,
- methods: [],
+ methods: [
+ {
+ name: 'setErrors',
+ parameters: Type.Object({
+ errorsMap: Type.Record(Type.String(), Type.Array(ErrorSpec)),
+ }),
+ },
+ {
+ name: 'validateFields',
+ parameters: Type.Object({
+ names: Type.Array(Type.String()),
+ }),
+ },
+ {
+ name: 'validateAllFields',
+ parameters: Type.Object({}),
+ },
+ {
+ name: 'clearErrors',
+ parameters: Type.Object({ names: Type.Array(Type.String()) }),
+ },
+ {
+ name: 'clearAllErrors',
+ parameters: Type.Object({}),
+ },
+ ],
},
})(() => {
- const rules = new Map();
-
- function addValidationRule(name: string, rule: ValidationRule) {
- rules.set(name, rule);
- }
-
- addValidationRule('email', text => {
- if (/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(text)) {
- return {
- isInvalid: false,
- errorMsg: '',
- };
- } else {
- return {
- isInvalid: true,
- errorMsg: 'Please enter valid email.',
- };
- }
- });
-
- addValidationRule('phoneNumber', text => {
- if (/^1[3456789]\d{9}$/.test(text)) {
- return {
- isInvalid: false,
- errorMsg: '',
- };
- } else {
- return {
- isInvalid: true,
- errorMsg: 'Please enter valid phone number.',
- };
- }
- });
- const ValidationResultCache: Record = {};
+ const initialMap = new Map();
return props => {
- const { value, minLength, maxLength, mergeState, componentId, rule } = props;
+ const { validators, componentId, subscribeMethods, mergeState } = props;
+ const validatorMap = validators.reduce((result, validator) => {
+ result[validator.name] = validator;
- const result: ValidationResult = {
- isInvalid: false,
- errorMsg: '',
- };
+ return result;
+ }, {} as Record>);
- if (maxLength !== undefined && value.length > maxLength) {
- result.isInvalid = true;
- result.errorMsg = `Can not be longer than ${maxLength}.`;
- } else if (minLength !== undefined && value.length < minLength) {
- result.isInvalid = true;
- result.errorMsg = `Can not be shorter than ${minLength}.`;
- } else {
- const rulesArr = rule ? rule.split(',') : [];
- for (const ruleName of rulesArr) {
- const validateFunc = rules.get(ruleName);
- if (validateFunc) {
- const { isInvalid, errorMsg } = validateFunc(value);
- if (isInvalid) {
- result.isInvalid = true;
- result.errorMsg = errorMsg;
- break;
- }
- }
- }
+ function setErrors({
+ errorsMap,
+ }: {
+ errorsMap: Record[]>;
+ }) {
+ const validatedResult = Object.keys(errorsMap).reduce(
+ (result: Static, name) => {
+ result[name] = {
+ isInvalid: errorsMap[name].length !== 0,
+ errors: errorsMap[name],
+ };
+
+ return result;
+ },
+ {}
+ );
+
+ mergeState({
+ validatedResult,
+ });
+ }
+ function validateFields({ names }: { names: string[] }) {
+ const validatedResult = names
+ .map(name => {
+ const validator = validatorMap[name];
+ const { value, rules } = validator;
+ const errors = rules
+ .map(rule => {
+ const { type, error, validate, customOptions, ...options } = rule;
+ let isValid = true;
+
+ if (type === 'custom') {
+ isValid = validate(value, customOptions);
+ } else {
+ isValid = validateFnMap[type](value, options);
+ }
+
+ return isValid ? null : { message: error.message };
+ })
+ .filter((error): error is Static => error !== null);
+
+ return {
+ name,
+ isInvalid: errors.length !== 0,
+ errors,
+ };
+ })
+ .reduce((result: Static, validatedResultItem) => {
+ result[validatedResultItem.name] = {
+ isInvalid: validatedResultItem.isInvalid,
+ errors: validatedResultItem.errors,
+ };
+
+ return result;
+ }, {});
+
+ mergeState({ validatedResult });
+ }
+ function validateAllFields() {
+ validateFields({ names: validators.map(({ name }) => name) });
+ }
+ function clearErrors({ names }: { names: string[] }) {
+ setErrors({
+ errorsMap: names.reduce((result: Record, name) => {
+ result[name] = [];
+
+ return result;
+ }, {}),
+ });
+ }
+ function clearAllErrors() {
+ clearErrors({ names: validators.map(({ name }) => name) });
}
- if (!isEqual(result, ValidationResultCache[componentId])) {
- ValidationResultCache[componentId] = result;
- mergeState({
- validResult: result,
- });
+ subscribeMethods({
+ setErrors,
+ validateFields,
+ validateAllFields,
+ clearErrors,
+ clearAllErrors,
+ });
+
+ if (initialMap.has(componentId) === false) {
+ clearAllErrors();
+ initialMap.set(componentId, true);
}
return {
- props: null,
+ props: {
+ componentDidUnmount: [
+ () => {
+ initialMap.delete(componentId);
+ },
+ ],
+ },
};
};
});
diff --git a/packages/shared/__tests__/spec.spec.ts b/packages/shared/__tests__/spec.spec.ts
index 16c56e67..3f57bc10 100644
--- a/packages/shared/__tests__/spec.spec.ts
+++ b/packages/shared/__tests__/spec.spec.ts
@@ -17,14 +17,9 @@ describe('generateDefaultValueFromSpec function', () => {
const type = Type.Number();
expect(generateDefaultValueFromSpec(type)).toEqual(0);
});
- // Type.Optional can only be judged by the modifier feature provided by the typebox,
- // but this would break the consistency of the function,
- // and it doesn't seem to make much sense to deal with non-object optional alone like Type.Optional(Type.String())
- // Therefore it is possible to determine whether an object's property is optional using spec.required,
- // and if the property is within Type.Object is optional then it is not required.
- it('can parse optional', () => {
+ it('can parse optional and the value is the default value of its type', () => {
const type = Type.Optional(Type.Object({ str: Type.Optional(Type.String()) }));
- expect(generateDefaultValueFromSpec(type)).toEqual({ str: undefined });
+ expect(generateDefaultValueFromSpec(type)).toEqual({ str: '' });
});
it('can parse object', () => {
const type = Type.Object({
@@ -98,5 +93,13 @@ describe('generateDefaultValueFromSpec function', () => {
it('can parse any type', () => {
expect(generateDefaultValueFromSpec({})).toEqual('');
expect(generateDefaultValueFromSpec({}, true)).toEqual(AnyTypePlaceholder);
+ expect(
+ (
+ generateDefaultValueFromSpec(
+ Type.Object({ foo: Type.Object({ bar: Type.Any() }) }),
+ true
+ ) as any
+ ).foo.bar
+ ).toEqual(AnyTypePlaceholder);
});
});
diff --git a/packages/shared/src/utils/spec.ts b/packages/shared/src/utils/spec.ts
index ef496aa1..4b67a51f 100644
--- a/packages/shared/src/utils/spec.ts
+++ b/packages/shared/src/utils/spec.ts
@@ -23,32 +23,50 @@ export function StringUnion(values: [...T], options?: any) {
);
}
-function getArray(items: JSONSchema7Definition[]): JSONSchema7Type[] {
+function getArray(
+ items: JSONSchema7Definition[],
+ returnPlaceholderForAny = false
+): JSONSchema7Type[] {
return items.map(item =>
- isJSONSchema(item) ? generateDefaultValueFromSpec(item) : null
+ isJSONSchema(item)
+ ? generateDefaultValueFromSpec(item, returnPlaceholderForAny)
+ : null
);
}
-function getObject(spec: JSONSchema7): JSONSchema7Object {
+function getObject(
+ spec: JSONSchema7,
+ returnPlaceholderForAny = false
+): JSONSchema7Object | string {
const obj: JSONSchema7Object = {};
- const requiredKeys = spec.required;
if (spec.allOf && spec.allOf.length > 0) {
- return (getArray(spec.allOf) as JSONSchema7Object[]).reduce((prev, cur) => {
- prev = Object.assign(prev, cur);
- return prev;
- }, obj);
+ return (getArray(spec.allOf, returnPlaceholderForAny) as JSONSchema7Object[]).reduce(
+ (prev, cur) => {
+ prev = Object.assign(prev, cur);
+ return prev;
+ },
+ obj
+ );
}
- requiredKeys &&
- requiredKeys.forEach(key => {
- const subSpec = spec.properties?.[key];
- if (typeof subSpec === 'boolean') {
- obj[key] = null;
- } else if (subSpec) {
- obj[key] = generateDefaultValueFromSpec(subSpec);
- }
- });
+ // if not specific property, treat it as any type
+ if (!spec.properties) {
+ if (returnPlaceholderForAny) {
+ return AnyTypePlaceholder;
+ }
+
+ return {};
+ }
+
+ for (const key in spec.properties) {
+ const subSpec = spec.properties?.[key];
+ if (typeof subSpec === 'boolean') {
+ obj[key] = null;
+ } else if (subSpec) {
+ obj[key] = generateDefaultValueFromSpec(subSpec, returnPlaceholderForAny);
+ }
+ }
return obj;
}
@@ -60,7 +78,7 @@ export function generateDefaultValueFromSpec(
if ((spec.anyOf && spec.anyOf!.length > 0) || (spec.oneOf && spec.oneOf.length > 0)) {
const subSpec = (spec.anyOf! || spec.oneOf)[0];
if (typeof subSpec === 'boolean') return null;
- return generateDefaultValueFromSpec(subSpec);
+ return generateDefaultValueFromSpec(subSpec, returnPlaceholderForAny);
}
// It is any type
@@ -79,7 +97,7 @@ export function generateDefaultValueFromSpec(
const subSpec = {
type: spec.type[0],
} as JSONSchema7;
- return generateDefaultValueFromSpec(subSpec);
+ return generateDefaultValueFromSpec(subSpec, returnPlaceholderForAny);
}
case spec.type === 'string':
if (spec.enum && spec.enum.length > 0) {
@@ -92,16 +110,16 @@ export function generateDefaultValueFromSpec(
case spec.type === 'array':
return spec.items
? Array.isArray(spec.items)
- ? getArray(spec.items)
+ ? getArray(spec.items, returnPlaceholderForAny)
: isJSONSchema(spec.items)
- ? [generateDefaultValueFromSpec(spec.items)]
+ ? [generateDefaultValueFromSpec(spec.items, returnPlaceholderForAny)]
: null
: [];
case spec.type === 'number':
case spec.type === 'integer':
return 0;
case spec.type === 'object':
- return getObject(spec);
+ return getObject(spec, returnPlaceholderForAny);
case spec.type === 'null':
return null;
default: