@cocreate/validation 1.19.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +1 -1
- package/README.md +140 -91
- package/package.json +6 -5
- package/src/index.js +84 -71
- package/src/messages.js +43 -0
- package/src/validations/matches.js +18 -16
- package/src/validations/unique.js +18 -12
- package/src/validations/value.js +32 -0
- package/test/required-value.test.cjs +89 -0
- package/src/validations/email.js +0 -31
- package/src/validations/length.js +0 -31
- package/src/validations/required.js +0 -29
- package/src/validations/subdomain.js +0 -65
package/CONTRIBUTING.md
CHANGED
|
@@ -13,7 +13,7 @@ Fork the [project on Github](https://github.com/CoCreate-app/CoCreate-validation
|
|
|
13
13
|
```
|
|
14
14
|
git clone https://github.com/contributor/CoCreate-validation.git
|
|
15
15
|
cd CoCreate-validation
|
|
16
|
-
git remote add upstream https://
|
|
16
|
+
git remote add upstream https://gitlab.com/cocreatejs/CoCreate-validation.git
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Create a Topic Branch
|
package/README.md
CHANGED
|
@@ -1,91 +1,140 @@
|
|
|
1
|
-
# CoCreate-validation
|
|
2
|
-
|
|
3
|
-
A simple validation component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API. Take it for a spin in our [playground!](https://cocreate.app/docs/validation)
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-

|
|
7
|
-

|
|
8
|
-

|
|
9
|
-

|
|
10
|
-

|
|
11
|
-
|
|
12
|
-

|
|
13
|
-
|
|
14
|
-
## [Docs & Demo](https://cocreate.app/docs/validation)
|
|
15
|
-
|
|
16
|
-
For a complete guide and working demo refer to the [doumentation](https://cocreate.app/docs/validation)
|
|
17
|
-
|
|
18
|
-
## CDN
|
|
19
|
-
|
|
20
|
-
```html
|
|
21
|
-
<script src="https://cdn.cocreate.app/validation/latest/CoCreate-validation.min.js"></script>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
```html
|
|
25
|
-
<script src="https://cdn.cocreate.app/validation/latest/CoCreate-validation.min.css"></script>
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## NPM
|
|
29
|
-
|
|
30
|
-
```shell
|
|
31
|
-
$ npm i @cocreate/validation
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## yarn
|
|
35
|
-
|
|
36
|
-
```shell
|
|
37
|
-
$ yarn install @cocreate/validation
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
# Table of Contents
|
|
41
|
-
|
|
42
|
-
- [Table of Contents](#table-of-contents)
|
|
43
|
-
- [Announcements](#announcements)
|
|
44
|
-
- [Roadmap](#roadmap)
|
|
45
|
-
- [How to Contribute](#how-to-contribute)
|
|
46
|
-
- [About](#about)
|
|
47
|
-
- [License](#license)
|
|
48
|
-
|
|
49
|
-
<a name="announcements"></a>
|
|
50
|
-
|
|
51
|
-
# Announcements
|
|
52
|
-
|
|
53
|
-
All updates to this library are documented in our [CHANGELOG](https://github.com/CoCreate-app/CoCreate-validation/blob/master/CHANGELOG.md) and [releases](https://github.com/CoCreate-app/CoCreate-validation/releases). You may also subscribe to email for releases and breaking changes.
|
|
54
|
-
|
|
55
|
-
<a name="roadmap"></a>
|
|
56
|
-
|
|
57
|
-
# Roadmap
|
|
58
|
-
|
|
59
|
-
If you are interested in the future direction of this project, please take a look at our open [issues](https://github.com/CoCreate-app/CoCreate-validation/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-validation/pulls). We would love to hear your feedback.
|
|
60
|
-
|
|
61
|
-
<a name="about"></a>
|
|
62
|
-
|
|
63
|
-
# About
|
|
64
|
-
|
|
65
|
-
CoCreate-validation is guided and supported by the CoCreate Developer Experience Team.
|
|
66
|
-
|
|
67
|
-
Please Email the Developer Experience Team [here](mailto:develop@cocreate.app) in case of any queries.
|
|
68
|
-
|
|
69
|
-
CoCreate-validation is maintained and funded by CoCreate. The names and logos for CoCreate are trademarks of CoCreate, LLC.
|
|
70
|
-
|
|
71
|
-
<a name="contribute"></a>
|
|
72
|
-
|
|
73
|
-
# How to Contribute
|
|
74
|
-
|
|
75
|
-
We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/CoCreate-app/CoCreate-validation/blob/master/CONTRIBUTING.md) guide for details.
|
|
76
|
-
|
|
77
|
-
We want this library to be community-driven, and CoCreate led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/CoCreate-app/CoCreate-validation/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-validation/pulls) or merely upvote or comment on existing issues or pull requests.
|
|
78
|
-
|
|
79
|
-
We appreciate your continued support, thank you!
|
|
80
|
-
|
|
81
|
-
<a name="license"></a>
|
|
82
|
-
|
|
83
|
-
# License
|
|
84
|
-
|
|
85
|
-
This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
|
|
86
|
-
|
|
87
|
-
- **Open Source Use**: For open-source projects and non-commercial use, this software is available under the AGPLv3. The AGPLv3 allows you to freely use, modify, and distribute this software, provided that all modifications and derivative works are also licensed under the AGPLv3. For the full license text, see the [LICENSE file](https://github.com/CoCreate-app/CoCreate-validation/blob/master/LICENSE).
|
|
88
|
-
|
|
89
|
-
- **Commercial Use**: For-profit companies and individuals intending to use this software for commercial purposes must obtain a commercial license. The commercial license is available when you sign up for an API key on our [website](https://cocreate.app). This license permits proprietary use and modification of the software without the copyleft requirements of the AGPLv3. It is ideal for integrating this software into proprietary commercial products and applications.
|
|
90
|
-
|
|
91
|
-
If you have not purchased a commercial license and intend to use this software for commercial purposes, you are required to sign up for an API key on our website.
|
|
1
|
+
# CoCreate-validation
|
|
2
|
+
|
|
3
|
+
A simple validation component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API. Take it for a spin in our [playground!](https://cocreate.app/docs/validation)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
## [Docs & Demo](https://cocreate.app/docs/validation)
|
|
15
|
+
|
|
16
|
+
For a complete guide and working demo refer to the [doumentation](https://cocreate.app/docs/validation)
|
|
17
|
+
|
|
18
|
+
## CDN
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<script src="https://cdn.cocreate.app/validation/latest/CoCreate-validation.min.js"></script>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<script src="https://cdn.cocreate.app/validation/latest/CoCreate-validation.min.css"></script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## NPM
|
|
29
|
+
|
|
30
|
+
```shell
|
|
31
|
+
$ npm i @cocreate/validation
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## yarn
|
|
35
|
+
|
|
36
|
+
```shell
|
|
37
|
+
$ yarn install @cocreate/validation
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
# Table of Contents
|
|
41
|
+
|
|
42
|
+
- [Table of Contents](#table-of-contents)
|
|
43
|
+
- [Announcements](#announcements)
|
|
44
|
+
- [Roadmap](#roadmap)
|
|
45
|
+
- [How to Contribute](#how-to-contribute)
|
|
46
|
+
- [About](#about)
|
|
47
|
+
- [License](#license)
|
|
48
|
+
|
|
49
|
+
<a name="announcements"></a>
|
|
50
|
+
|
|
51
|
+
# Announcements
|
|
52
|
+
|
|
53
|
+
All updates to this library are documented in our [CHANGELOG](https://github.com/CoCreate-app/CoCreate-validation/blob/master/CHANGELOG.md) and [releases](https://github.com/CoCreate-app/CoCreate-validation/releases). You may also subscribe to email for releases and breaking changes.
|
|
54
|
+
|
|
55
|
+
<a name="roadmap"></a>
|
|
56
|
+
|
|
57
|
+
# Roadmap
|
|
58
|
+
|
|
59
|
+
If you are interested in the future direction of this project, please take a look at our open [issues](https://github.com/CoCreate-app/CoCreate-validation/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-validation/pulls). We would love to hear your feedback.
|
|
60
|
+
|
|
61
|
+
<a name="about"></a>
|
|
62
|
+
|
|
63
|
+
# About
|
|
64
|
+
|
|
65
|
+
CoCreate-validation is guided and supported by the CoCreate Developer Experience Team.
|
|
66
|
+
|
|
67
|
+
Please Email the Developer Experience Team [here](mailto:develop@cocreate.app) in case of any queries.
|
|
68
|
+
|
|
69
|
+
CoCreate-validation is maintained and funded by CoCreate. The names and logos for CoCreate are trademarks of CoCreate, LLC.
|
|
70
|
+
|
|
71
|
+
<a name="contribute"></a>
|
|
72
|
+
|
|
73
|
+
# How to Contribute
|
|
74
|
+
|
|
75
|
+
We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/CoCreate-app/CoCreate-validation/blob/master/CONTRIBUTING.md) guide for details.
|
|
76
|
+
|
|
77
|
+
We want this library to be community-driven, and CoCreate led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/CoCreate-app/CoCreate-validation/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-validation/pulls) or merely upvote or comment on existing issues or pull requests.
|
|
78
|
+
|
|
79
|
+
We appreciate your continued support, thank you!
|
|
80
|
+
|
|
81
|
+
<a name="license"></a>
|
|
82
|
+
|
|
83
|
+
# License
|
|
84
|
+
|
|
85
|
+
This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
|
|
86
|
+
|
|
87
|
+
- **Open Source Use**: For open-source projects and non-commercial use, this software is available under the AGPLv3. The AGPLv3 allows you to freely use, modify, and distribute this software, provided that all modifications and derivative works are also licensed under the AGPLv3. For the full license text, see the [LICENSE file](https://github.com/CoCreate-app/CoCreate-validation/blob/master/LICENSE).
|
|
88
|
+
|
|
89
|
+
- **Commercial Use**: For-profit companies and individuals intending to use this software for commercial purposes must obtain a commercial license. The commercial license is available when you sign up for an API key on our [website](https://cocreate.app). This license permits proprietary use and modification of the software without the copyleft requirements of the AGPLv3. It is ideal for integrating this software into proprietary commercial products and applications.
|
|
90
|
+
|
|
91
|
+
If you have not purchased a commercial license and intend to use this software for commercial purposes, you are required to sign up for an API key on our website.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
### Action lifecycle and native constraints
|
|
95
|
+
|
|
96
|
+
Use `actions="disabled, validate, save, disabled"` to disable the button while
|
|
97
|
+
validating and saving, then explicitly enable it on success. Validation failure
|
|
98
|
+
or an exception calls `action.abort()`, which releases a button disabled by that
|
|
99
|
+
chain. Validation success calls `action.end()` and continues the chain.
|
|
100
|
+
|
|
101
|
+
Native controls use the browser constraint validation API for `required`, input
|
|
102
|
+
types, `pattern`, ranges, and other supported constraints. Custom messages set
|
|
103
|
+
with `element.setCustomValidity(message)` are preserved; clear them with
|
|
104
|
+
`element.setCustomValidity("")` when resolved. Fields with `aria-errormessage` display errors in their referenced elements.
|
|
105
|
+
Without a valid target, errors render into `template="notification"` and the first
|
|
106
|
+
invalid native field reports its browser message.
|
|
107
|
+
Disabled controls and controls barred from native validation are skipped.
|
|
108
|
+
`novalidate` on the form or `formnovalidate` on the action element skips the native
|
|
109
|
+
checks; explicitly configured CoCreate rules still run.
|
|
110
|
+
|
|
111
|
+
Custom `matches` and `unique` validators are awaited. Use native `required`,
|
|
112
|
+
`type="email"`, `pattern`, and `minlength`/`maxlength` attributes for browser
|
|
113
|
+
constraints; the custom `email`, `subdomain`, and `length` attributes have been
|
|
114
|
+
removed. Non-native elements need their own custom validation.
|
|
115
|
+
Each validator registered in `validations` receives its matching elements and
|
|
116
|
+
returns an array of failed elements (or nothing on success). Results are combined
|
|
117
|
+
so one passing rule cannot erase another rule's failure. `validation` and
|
|
118
|
+
`aria-invalid` reflect the combined result. Non-native elements can still use
|
|
119
|
+
CoCreate attributes and `getValue()`. `validate(selector)` scopes validation to
|
|
120
|
+
the matching elements and their descendants.
|
|
121
|
+
|
|
122
|
+
### Inline field errors
|
|
123
|
+
|
|
124
|
+
```html
|
|
125
|
+
<label for="email">Email</label>
|
|
126
|
+
<input id="email" type="email" required aria-errormessage="email-error" />
|
|
127
|
+
<span id="email-error" hidden></span>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Native and CoCreate validation messages use this same explicit relationship.
|
|
131
|
+
Validation reveals the target and updates `aria-invalid`; successful revalidation
|
|
132
|
+
clears and hides the message. `aria-describedby` help text is left intact.
|
|
133
|
+
Messages are inserted as text, not HTML. Linked errors do not also generate a
|
|
134
|
+
notification. Missing targets fall back to notifications.
|
|
135
|
+
|
|
136
|
+
Custom validators may accept a second `report` callback and call
|
|
137
|
+
`report({ data: [{ type: "rule", status: "error", success: false,
|
|
138
|
+
message: "Explain the error", elements: failedElements }] })` before returning
|
|
139
|
+
`failedElements`. The action collects these reports and presents the first
|
|
140
|
+
reported error per field after all rules finish.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/validation",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "A simple validation component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"validation",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "git+https://
|
|
25
|
+
"url": "git+https://gitlab.com/cocreatejs/CoCreate-validation.git"
|
|
26
26
|
},
|
|
27
27
|
"author": "CoCreate LLC",
|
|
28
28
|
"license": "AGPL-3.0",
|
|
@@ -36,11 +36,12 @@
|
|
|
36
36
|
},
|
|
37
37
|
"main": "./src/index.js",
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cocreate/actions": "^1.
|
|
40
|
-
"@cocreate/
|
|
39
|
+
"@cocreate/actions": "^1.24.0",
|
|
40
|
+
"@cocreate/element-prototype": "^1.35.0",
|
|
41
|
+
"@cocreate/render": "^1.51.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@cocreate/webpack": "^1.
|
|
44
|
+
"@cocreate/webpack": "^1.9.0"
|
|
44
45
|
},
|
|
45
46
|
"allowScripts": {
|
|
46
47
|
"@cocreate/actions@1.21.4": true,
|
package/src/index.js
CHANGED
|
@@ -5,83 +5,96 @@
|
|
|
5
5
|
* SPDX-License-Identifier: MIT
|
|
6
6
|
********************************************************************************/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { unique } from "./validations/unique";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
validations.set("
|
|
20
|
-
validations.set("required", required);
|
|
21
|
-
validations.set("subdomain", subdomain);
|
|
22
|
-
validations.set("unique", unique);
|
|
23
|
-
validations.set("length", length);
|
|
24
|
-
validations.set("email", email);
|
|
25
|
-
|
|
26
|
-
const selectors =
|
|
27
|
-
"[matches], [required], [unique], [subdomain], [length], [email]";
|
|
8
|
+
import actions from "@cocreate/actions";
|
|
9
|
+
import "@cocreate/element-prototype";
|
|
10
|
+
import { render } from "@cocreate/render";
|
|
11
|
+
import { clearError, errorTargets, renderErrors } from "./messages.js";
|
|
12
|
+
import { matches } from "./validations/matches.js";
|
|
13
|
+
import { unique } from "./validations/unique.js";
|
|
14
|
+
import { value } from "./validations/value.js";
|
|
15
|
+
|
|
16
|
+
const validations = new Map();
|
|
17
|
+
validations.set("matches", matches);
|
|
18
|
+
validations.set("unique", unique);
|
|
19
|
+
validations.set("validate", value);
|
|
28
20
|
|
|
29
21
|
async function validate(action) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
try {
|
|
23
|
+
const selectors = [...validations.keys()].map(name => `[${name}]`).concat("[component][required]").join(", ");
|
|
24
|
+
const candidates = new Set();
|
|
25
|
+
const form = action.element.form || action.form || action.element.closest("form");
|
|
26
|
+
const roots = action.params ? document.querySelectorAll(action.params) : [form];
|
|
27
|
+
if (!roots.length || !roots[0]) throw new Error("No validation target found");
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
29
|
+
for (const root of roots) {
|
|
30
|
+
if (root.matches(selectors) || root.willValidate) candidates.add(root);
|
|
31
|
+
for (const element of root.querySelectorAll(selectors)) candidates.add(element);
|
|
32
|
+
for (const element of root.elements || root.querySelectorAll("input, select, textarea, button")) {
|
|
33
|
+
candidates.add(element);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const elements = [...candidates].filter(element =>
|
|
37
|
+
!element.closest("[template]") &&
|
|
38
|
+
!element.matches(":disabled") && element.willValidate !== false
|
|
39
|
+
);
|
|
40
|
+
const failed = new Set();
|
|
41
|
+
const nativeEnabled = !form?.noValidate && !action.element.formNoValidate;
|
|
42
|
+
if (nativeEnabled) {
|
|
43
|
+
for (const element of elements) {
|
|
44
|
+
if (element.willValidate && !element.checkValidity()) failed.add(element);
|
|
45
|
+
if (element.hasAttribute("required")) {
|
|
46
|
+
const value = await element.getValue();
|
|
47
|
+
if (value == null || value === "" || Array.isArray(value) && !value.length)
|
|
48
|
+
failed.add(element);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (failed.size) {
|
|
52
|
+
for (const element of elements) markValidity(element, !failed.has(element));
|
|
53
|
+
action.abort();
|
|
54
|
+
const first = failed.values().next().value;
|
|
55
|
+
renderErrors({
|
|
56
|
+
data: [...failed].map(element => ({
|
|
57
|
+
type: "validate", status: "error", success: false,
|
|
58
|
+
message: element.validationMessage || "A value is required.", elements: [element]
|
|
59
|
+
}))
|
|
60
|
+
});
|
|
61
|
+
if (errorTargets(first).length) first.focus();
|
|
62
|
+
else first.reportValidity?.();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
const errors = [];
|
|
68
|
+
for (const [name, validator] of validations) {
|
|
69
|
+
const targets = elements.filter(element => element.hasAttribute(name));
|
|
70
|
+
if (!targets.length) continue;
|
|
71
|
+
const failures = await validator(targets, ({ data }) => errors.push(...data));
|
|
72
|
+
if (failures) for (const element of failures) failed.add(element);
|
|
73
|
+
}
|
|
74
|
+
for (const element of elements) markValidity(element, !failed.has(element));
|
|
75
|
+
if (failed.size) {
|
|
76
|
+
action.abort();
|
|
77
|
+
renderErrors({ data: errors });
|
|
78
|
+
failed.values().next().value.focus?.();
|
|
79
|
+
} else {
|
|
80
|
+
action.end();
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
action.abort();
|
|
84
|
+
render({
|
|
85
|
+
selector: '[template~="notification"]',
|
|
86
|
+
data: [{ type: "validate", status: "error", success: false,
|
|
87
|
+
message: error.message || "Unable to validate the form. Please try again." }]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
70
91
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
76
|
-
);
|
|
77
|
-
}
|
|
92
|
+
function markValidity(element, valid) {
|
|
93
|
+
element.setAttribute("validation", String(valid));
|
|
94
|
+
element.setAttribute("aria-invalid", String(!valid));
|
|
95
|
+
if (valid) clearError(element);
|
|
78
96
|
}
|
|
79
97
|
|
|
80
|
-
actions.init({
|
|
81
|
-
name: "validate",
|
|
82
|
-
callback: (action) => {
|
|
83
|
-
validate(action);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
98
|
+
actions.init({ name: "validate", callback: validate });
|
|
86
99
|
|
|
87
100
|
export default { validations };
|
package/src/messages.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { render } from "@cocreate/render";
|
|
2
|
+
import "@cocreate/element-prototype";
|
|
3
|
+
|
|
4
|
+
export function errorTargets(element) {
|
|
5
|
+
const root = element.getRootNode();
|
|
6
|
+
return (element.getAttribute("aria-errormessage") || "")
|
|
7
|
+
.trim().split(/\s+/).filter(Boolean)
|
|
8
|
+
.map(id => root.getElementById?.(id)).filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function clearError(element) {
|
|
12
|
+
for (const target of errorTargets(element)) {
|
|
13
|
+
target.setValue("");
|
|
14
|
+
target.hidden = true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function renderErrors({ data }) {
|
|
19
|
+
const shown = new Set();
|
|
20
|
+
const notifications = [];
|
|
21
|
+
for (const error of data) {
|
|
22
|
+
const unlinked = [];
|
|
23
|
+
for (const element of error.elements || []) {
|
|
24
|
+
if (shown.has(element)) continue;
|
|
25
|
+
shown.add(element);
|
|
26
|
+
const targets = errorTargets(element);
|
|
27
|
+
if (!targets.length) {
|
|
28
|
+
unlinked.push(element);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
for (const target of targets) {
|
|
32
|
+
target.setValue(error.message);
|
|
33
|
+
target.hidden = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!error.elements?.length || unlinked.length) {
|
|
37
|
+
notifications.push({ ...error, elements: unlinked });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (notifications.length) {
|
|
41
|
+
render({ selector: '[template~="notification"]', data: notifications });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderErrors } from "../messages.js";
|
|
2
2
|
|
|
3
3
|
function initMatches() {
|
|
4
|
-
let elements = document.querySelectorAll('[
|
|
4
|
+
let elements = document.querySelectorAll('[match-query]');
|
|
5
5
|
for (let element of elements) {
|
|
6
|
-
let targetSelector = element.getAttribute('
|
|
6
|
+
let targetSelector = element.getAttribute('match-query');
|
|
7
7
|
let target = document.querySelector(targetSelector);
|
|
8
8
|
element.validations = { matches: target };
|
|
9
9
|
element.addEventListener('input', validateMatchEvent);
|
|
@@ -15,35 +15,37 @@ function validateMatchEvent(e) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function validateMatch(element) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
let target = element.queryElements();
|
|
19
|
+
if (!target) {
|
|
20
|
+
element.setAttribute('match', 'false');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
20
23
|
if (target.value == element.value) {
|
|
21
|
-
|
|
22
|
-
element.setAttribute('validation', 'true');
|
|
24
|
+
element.setAttribute('match', 'true');
|
|
23
25
|
}
|
|
24
26
|
else {
|
|
25
|
-
|
|
26
|
-
element.setAttribute('validation', 'false');
|
|
27
|
+
element.setAttribute('match', 'false');
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function matches(elements) {
|
|
31
|
+
function matches(elements, report = renderErrors) {
|
|
31
32
|
let failedElements = [];
|
|
32
33
|
for (let element of elements) {
|
|
33
|
-
|
|
34
|
+
validateMatch(element);
|
|
35
|
+
let isValid = element.getAttribute('match');
|
|
34
36
|
if (isValid == 'false') {
|
|
35
37
|
failedElements.push(element);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
if (failedElements.length) {
|
|
39
|
-
|
|
40
|
-
selector:
|
|
41
|
+
report({
|
|
42
|
+
selector: '[template~="notification"]',
|
|
41
43
|
data: [{
|
|
42
44
|
type: 'matches',
|
|
43
|
-
status: '
|
|
45
|
+
status: 'error',
|
|
46
|
+
success: false,
|
|
44
47
|
message: 'One or more values do not match',
|
|
45
|
-
selector: '[
|
|
46
|
-
elements: failedElements
|
|
48
|
+
selector: '[match-query][match="false"]'
|
|
47
49
|
}]
|
|
48
50
|
});
|
|
49
51
|
return failedElements;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { errorTargets, renderErrors } from "../messages.js";
|
|
2
2
|
|
|
3
|
-
export const unique = async (elements) => {
|
|
3
|
+
export const unique = async (elements, report = renderErrors) => {
|
|
4
4
|
let failedElements = [];
|
|
5
5
|
for (let element of elements) {
|
|
6
6
|
let unique = element.getAttribute('unique');
|
|
@@ -11,16 +11,22 @@ export const unique = async (elements) => {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
if (failedElements.length) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
const unlinkedElements = failedElements.filter(
|
|
15
|
+
element => !errorTargets(element).length
|
|
16
|
+
);
|
|
17
|
+
if (unlinkedElements.length) {
|
|
18
|
+
report({
|
|
19
|
+
selector: '[template~="notification"]',
|
|
20
|
+
data: [{
|
|
21
|
+
type: 'unique',
|
|
22
|
+
status: 'error',
|
|
23
|
+
success: false,
|
|
24
|
+
message: 'One or more values are not unique',
|
|
25
|
+
selector: '[unique="false"]',
|
|
26
|
+
elements: unlinkedElements
|
|
27
|
+
}]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
24
30
|
return failedElements;
|
|
25
31
|
}
|
|
26
32
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { renderErrors } from "../messages.js";
|
|
2
|
+
|
|
3
|
+
export async function value(elements, report = renderErrors) {
|
|
4
|
+
const failedElements = [];
|
|
5
|
+
for (const element of elements) {
|
|
6
|
+
try {
|
|
7
|
+
const resolvedValue = await element.getValue();
|
|
8
|
+
if (
|
|
9
|
+
resolvedValue === null ||
|
|
10
|
+
typeof resolvedValue !== "object" ||
|
|
11
|
+
Array.isArray(resolvedValue)
|
|
12
|
+
) {
|
|
13
|
+
failedElements.push(element);
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
failedElements.push(element);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (failedElements.length) {
|
|
21
|
+
report({
|
|
22
|
+
data: failedElements.map((element) => ({
|
|
23
|
+
type: "validate",
|
|
24
|
+
status: "error",
|
|
25
|
+
success: false,
|
|
26
|
+
message: "Value must resolve to an object.",
|
|
27
|
+
elements: [element]
|
|
28
|
+
}))
|
|
29
|
+
});
|
|
30
|
+
return failedElements;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const { readFileSync } = require('node:fs');
|
|
3
|
+
const { join } = require('node:path');
|
|
4
|
+
const vm = require('node:vm');
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
|
|
7
|
+
async function validateRequired(resolvedValue, options = {}) {
|
|
8
|
+
const result = { aborted: false, continued: false, errors: [] };
|
|
9
|
+
const attributes = new Map([['required', '']]);
|
|
10
|
+
const field = {
|
|
11
|
+
value: '$query(#cluster-object-id).getValue()',
|
|
12
|
+
willValidate: true,
|
|
13
|
+
validationMessage: '',
|
|
14
|
+
hasAttribute: name => attributes.has(name),
|
|
15
|
+
setAttribute: (name, value) => attributes.set(name, value),
|
|
16
|
+
closest: () => null,
|
|
17
|
+
matches: selector => selector === ':disabled' && !!options.disabled,
|
|
18
|
+
checkValidity: () => options.nativeValid !== false,
|
|
19
|
+
getValue: async () => {
|
|
20
|
+
if (options.failure) throw options.failure;
|
|
21
|
+
return resolvedValue;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const form = {
|
|
25
|
+
noValidate: options.noValidate,
|
|
26
|
+
matches: () => false,
|
|
27
|
+
querySelectorAll: () => [],
|
|
28
|
+
elements: [field]
|
|
29
|
+
};
|
|
30
|
+
const action = {
|
|
31
|
+
form,
|
|
32
|
+
element: {},
|
|
33
|
+
abort: () => { result.aborted = true; },
|
|
34
|
+
end: () => { result.continued = true; }
|
|
35
|
+
};
|
|
36
|
+
const source = readFileSync(join(__dirname, '../src/index.js'), 'utf8');
|
|
37
|
+
const code = source.slice(source.indexOf('const validations'), source.indexOf('actions.init('));
|
|
38
|
+
const context = vm.createContext({
|
|
39
|
+
action, matches() {}, unique() {}, value() {}, clearError() {},
|
|
40
|
+
errorTargets: () => [],
|
|
41
|
+
renderErrors: ({ data }) => result.errors.push(...data),
|
|
42
|
+
render: ({ data }) => result.errors.push(...data)
|
|
43
|
+
});
|
|
44
|
+
await vm.runInContext(`${code}\nvalidate(action);`, context);
|
|
45
|
+
result.invalid = attributes.get('aria-invalid');
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('required validates the resolved value even when the raw expression is nonempty', async t => {
|
|
50
|
+
for (const value of ['', null, undefined, []]) {
|
|
51
|
+
await t.test(String(value), async () => {
|
|
52
|
+
const result = await validateRequired(value);
|
|
53
|
+
assert.equal(result.aborted, true);
|
|
54
|
+
assert.equal(result.continued, false);
|
|
55
|
+
assert.equal(result.invalid, 'true');
|
|
56
|
+
assert.equal(result.errors[0].message, 'A value is required.');
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('valid resolved values retain normal required semantics', async t => {
|
|
62
|
+
for (const value of ['65f8cb447b39a3c1a8904e22', 0, false, ['selected']]) {
|
|
63
|
+
await t.test(String(value), async () => {
|
|
64
|
+
const result = await validateRequired(value);
|
|
65
|
+
assert.equal(result.aborted, false);
|
|
66
|
+
assert.equal(result.continued, true);
|
|
67
|
+
assert.equal(result.invalid, 'false');
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('native constraint failures still abort even when the resolved value is present', async () => {
|
|
73
|
+
assert.equal((await validateRequired('value', { nativeValid: false })).aborted, true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a rejected value lookup aborts validation', async () => {
|
|
77
|
+
const result = await validateRequired(null, { failure: new Error('Lookup failed') });
|
|
78
|
+
assert.equal(result.aborted, true);
|
|
79
|
+
assert.equal(result.continued, false);
|
|
80
|
+
assert.equal(result.errors[0].message, 'Lookup failed');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('disabled fields and forms opting out of native validation retain their behavior', async () => {
|
|
84
|
+
for (const options of [{ disabled: true }, { noValidate: true }]) {
|
|
85
|
+
const result = await validateRequired('', options);
|
|
86
|
+
assert.equal(result.aborted, false);
|
|
87
|
+
assert.equal(result.continued, true);
|
|
88
|
+
}
|
|
89
|
+
});
|
package/src/validations/email.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { render } from '@cocreate/render';
|
|
2
|
-
|
|
3
|
-
export var email = async (elements) => {
|
|
4
|
-
let failedElements = [];
|
|
5
|
-
for (let element of elements) {
|
|
6
|
-
let length = element.getAttribute('length') || 0
|
|
7
|
-
length = parseFloat(length)
|
|
8
|
-
let value = await element.getValue();
|
|
9
|
-
let isValidEmail = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(value);
|
|
10
|
-
|
|
11
|
-
if (isValidEmail) {
|
|
12
|
-
element.setAttribute('validation', 'true');
|
|
13
|
-
} else {
|
|
14
|
-
element.setAttribute('validation', 'false');
|
|
15
|
-
failedElements.push(element);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
if (failedElements.length) {
|
|
19
|
-
render({
|
|
20
|
-
selector: "[template*='validate']",
|
|
21
|
-
data: [{
|
|
22
|
-
type: 'email',
|
|
23
|
-
status: 'failed',
|
|
24
|
-
message: 'Please enter a valid email address',
|
|
25
|
-
selector: '[required][validation="false"]',
|
|
26
|
-
elements: failedElements
|
|
27
|
-
}]
|
|
28
|
-
});
|
|
29
|
-
return failedElements;
|
|
30
|
-
}
|
|
31
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { render } from '@cocreate/render';
|
|
2
|
-
|
|
3
|
-
export var length = async (elements) => {
|
|
4
|
-
let failedElements = [];
|
|
5
|
-
for (let element of elements) {
|
|
6
|
-
let length = element.getAttribute('length') || 0
|
|
7
|
-
length = parseFloat(length)
|
|
8
|
-
let value = await element.getValue();
|
|
9
|
-
if (element.type === 'password')
|
|
10
|
-
value = atob(value)
|
|
11
|
-
if (value.length >= length) {
|
|
12
|
-
element.setAttribute('validation', 'true');
|
|
13
|
-
} else {
|
|
14
|
-
element.setAttribute('validation', 'false');
|
|
15
|
-
failedElements.push(element);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
if (failedElements.length) {
|
|
19
|
-
render({
|
|
20
|
-
selector: "[template*='validate']",
|
|
21
|
-
data: [{
|
|
22
|
-
type: 'length',
|
|
23
|
-
status: 'failed',
|
|
24
|
-
message: 'Does not meet the minimum character length requirement',
|
|
25
|
-
selector: '[required][validation="false"]',
|
|
26
|
-
elements: failedElements
|
|
27
|
-
}]
|
|
28
|
-
});
|
|
29
|
-
return failedElements;
|
|
30
|
-
}
|
|
31
|
-
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// import {validations} from '../index.js';
|
|
2
|
-
import { render } from '@cocreate/render';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export var required = async (elements) => {
|
|
6
|
-
let failedElements = [];
|
|
7
|
-
for (let element of elements) {
|
|
8
|
-
let value = await element.getValue();
|
|
9
|
-
if (value || value === 0 || (value === false && element.type !== 'checkbox')) {
|
|
10
|
-
element.setAttribute('validation', 'true');
|
|
11
|
-
} else {
|
|
12
|
-
element.setAttribute('validation', 'false');
|
|
13
|
-
failedElements.push(element);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
if (failedElements.length) {
|
|
17
|
-
render({
|
|
18
|
-
selector: "[template*='validate']",
|
|
19
|
-
data: [{
|
|
20
|
-
type: 'unique',
|
|
21
|
-
status: 'failed',
|
|
22
|
-
message: 'One or more required fields are missing values',
|
|
23
|
-
selector: '[required][validation="false"]',
|
|
24
|
-
elements: failedElements
|
|
25
|
-
}]
|
|
26
|
-
});
|
|
27
|
-
return failedElements;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { render } from '@cocreate/render';
|
|
2
|
-
|
|
3
|
-
function initSubdomain() {
|
|
4
|
-
let elements = document.querySelectorAll('[subdomain]');
|
|
5
|
-
for (let element of elements) {
|
|
6
|
-
element.addEventListener('input', validateSubdomainEvent);
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function validateSubdomainEvent(e) {
|
|
11
|
-
validateSubdomain(e.target);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function validateSubdomain(element) {
|
|
15
|
-
let subdomain = await element.getValue();
|
|
16
|
-
let validation = 'false';
|
|
17
|
-
if (Array.isArray(subdomain))
|
|
18
|
-
subdomain = subdomain[0]
|
|
19
|
-
let array = subdomain.split('.');
|
|
20
|
-
if (array.length - 1 == 2) {
|
|
21
|
-
validation = 'true';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const pattern = /^[a-z0-9][a-z0-9\-]*[a-z0-9]$/;
|
|
25
|
-
if (validation != 'false' && pattern.test(array[0])) {
|
|
26
|
-
validation = 'true';
|
|
27
|
-
}
|
|
28
|
-
else
|
|
29
|
-
validation = 'false';
|
|
30
|
-
// let array = subdomain.split('.').length -1;
|
|
31
|
-
// if (validation && array == 2){
|
|
32
|
-
// validation = 'true';
|
|
33
|
-
// }
|
|
34
|
-
|
|
35
|
-
element.setAttribute('validation', validation);
|
|
36
|
-
return validation;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function subdomain(elements) {
|
|
40
|
-
let failedElements = [];
|
|
41
|
-
for (let element of elements) {
|
|
42
|
-
// let isValid = element.getAttribute('subdomain');
|
|
43
|
-
let isValid = validateSubdomain(element);
|
|
44
|
-
if (isValid == 'false') {
|
|
45
|
-
failedElements.push(element);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (failedElements.length) {
|
|
49
|
-
render({
|
|
50
|
-
selector: "[template*='validate']",
|
|
51
|
-
data: [{
|
|
52
|
-
type: 'subdomain',
|
|
53
|
-
status: 'failed',
|
|
54
|
-
message: 'Enter a valid subdomain. subdomain can only be one level.',
|
|
55
|
-
selector: '[subdomain][validation="false"]',
|
|
56
|
-
elements: failedElements
|
|
57
|
-
}]
|
|
58
|
-
});
|
|
59
|
-
return failedElements;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
initSubdomain();
|
|
64
|
-
|
|
65
|
-
export { subdomain };
|