@cocreate/validation 1.19.0 → 2.0.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.
@@ -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 { render } from '@cocreate/render';
1
+ import { renderErrors } from "../messages.js";
2
2
 
3
3
  function initMatches() {
4
- let elements = document.querySelectorAll('[matches]');
4
+ let elements = document.querySelectorAll('[match-query]');
5
5
  for (let element of elements) {
6
- let targetSelector = element.getAttribute('matches');
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
- // let element = e.target;
19
- let target = element.validations.matches;
18
+ let target = element.queryElements();
19
+ if (!target) {
20
+ element.setAttribute('match', 'false');
21
+ return;
22
+ }
20
23
  if (target.value == element.value) {
21
- target.setAttribute('validation', 'true');
22
- element.setAttribute('validation', 'true');
24
+ element.setAttribute('match', 'true');
23
25
  }
24
26
  else {
25
- target.setAttribute('validation', 'false');
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
- let isValid = element.getAttribute('validation');
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
- render({
40
- selector: "[template*='validate']",
41
+ report({
42
+ selector: '[template~="notification"]',
41
43
  data: [{
42
44
  type: 'matches',
43
- status: 'failed',
45
+ status: 'error',
46
+ success: false,
44
47
  message: 'One or more values do not match',
45
- selector: '[matches][validation="false"]',
46
- elements: failedElements
48
+ selector: '[match-query][match="false"]'
47
49
  }]
48
50
  });
49
51
  return failedElements;
@@ -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
+ });
@@ -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 };
@@ -1,26 +0,0 @@
1
- import { render } from '@cocreate/render';
2
-
3
- export const unique = async (elements) => {
4
- let failedElements = [];
5
- for (let element of elements) {
6
- let unique = element.getAttribute('unique');
7
- if (unique === '')
8
- unique = await CoCreate.unique.isUnique(element)
9
- if (unique === 'false' || unique === false) {
10
- failedElements.push(element);
11
- }
12
- }
13
- if (failedElements.length) {
14
- render({
15
- selector: "[template*='validate']",
16
- data: [{
17
- type: 'unique',
18
- status: 'failed',
19
- message: 'One or more values are not unique',
20
- selector: '[unique="false"]',
21
- elements: failedElements
22
- }]
23
- });
24
- return failedElements;
25
- }
26
- };