@zywave/zui-formfield 4.1.7-pre.0 → 4.2.0-pre.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/dist/custom-elements.json +52 -0
- package/dist/zui-formfield-css.js +1 -1
- package/dist/zui-formfield-css.js.map +1 -1
- package/dist/zui-formfield.d.ts +3 -0
- package/dist/zui-formfield.js +61 -1
- package/dist/zui-formfield.js.map +1 -1
- package/lab.html +12 -5
- package/package.json +3 -3
- package/src/zui-formfield-css.js +1 -1
- package/src/zui-formfield.scss +8 -0
- package/src/zui-formfield.ts +89 -1
- package/test/zui-formfield.test.ts +61 -3
|
@@ -54,6 +54,27 @@
|
|
|
54
54
|
"text": "HTMLSlotElement"
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
"kind": "field",
|
|
59
|
+
"name": "#validationMessageField",
|
|
60
|
+
"privacy": "private",
|
|
61
|
+
"type": {
|
|
62
|
+
"text": "string | null | undefined | undefined"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"kind": "field",
|
|
67
|
+
"name": "#validationIntervalId",
|
|
68
|
+
"privacy": "private",
|
|
69
|
+
"type": {
|
|
70
|
+
"text": "number | undefined | undefined"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"kind": "field",
|
|
75
|
+
"name": "#validationMessage",
|
|
76
|
+
"privacy": "private"
|
|
77
|
+
},
|
|
57
78
|
{
|
|
58
79
|
"kind": "field",
|
|
59
80
|
"name": "#control",
|
|
@@ -64,6 +85,37 @@
|
|
|
64
85
|
"kind": "method",
|
|
65
86
|
"name": "#onLabelClick",
|
|
66
87
|
"privacy": "private"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"kind": "method",
|
|
91
|
+
"name": "#onSlotChange",
|
|
92
|
+
"privacy": "private"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"kind": "method",
|
|
96
|
+
"name": "#configureValidation",
|
|
97
|
+
"privacy": "private",
|
|
98
|
+
"parameters": [
|
|
99
|
+
{
|
|
100
|
+
"name": "control",
|
|
101
|
+
"type": {
|
|
102
|
+
"text": "ValidatableLitElement"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"kind": "method",
|
|
109
|
+
"name": "#extractValidationMessage",
|
|
110
|
+
"privacy": "private",
|
|
111
|
+
"parameters": [
|
|
112
|
+
{
|
|
113
|
+
"name": "control",
|
|
114
|
+
"type": {
|
|
115
|
+
"text": "ValidatableLitElement"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
67
119
|
}
|
|
68
120
|
],
|
|
69
121
|
"attributes": [
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { css } from 'lit';
|
|
2
|
-
export const style = css `:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}`;
|
|
2
|
+
export const style = css `:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}:host .validation-message:empty{display:none}:host .validation-message{color:var(--zui-red-500)}`;
|
|
3
3
|
//# sourceMappingURL=zui-formfield-css.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zui-formfield-css.js","sourceRoot":"","sources":["../src/zui-formfield-css.js"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,CAAA,
|
|
1
|
+
{"version":3,"file":"zui-formfield-css.js","sourceRoot":"","sources":["../src/zui-formfield-css.js"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,CAAA,oeAAoe,CAAC","sourcesContent":["import { css } from 'lit';\n\nexport const style = css`:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}:host .validation-message:empty{display:none}:host .validation-message{color:var(--zui-red-500)}`;\n"]}
|
package/dist/zui-formfield.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ZuiBaseElement } from '@zywave/zui-base';
|
|
2
|
+
import type { PropertyValues } from 'lit';
|
|
2
3
|
/**
|
|
3
4
|
* `<zui-formfield>` provides a standardized way of labeling and styling form controls.
|
|
4
5
|
*
|
|
@@ -19,6 +20,8 @@ export declare class ZuiFormField extends ZuiBaseElement {
|
|
|
19
20
|
controlSelector: string;
|
|
20
21
|
_slotEl: HTMLSlotElement;
|
|
21
22
|
static get styles(): (import("lit").CSSResult | import("lit").CSSResultArray)[];
|
|
23
|
+
firstUpdated(changedProps: PropertyValues): Promise<void>;
|
|
24
|
+
disconnectedCallback(): void;
|
|
22
25
|
render(): import("lit-html").TemplateResult<1>;
|
|
23
26
|
}
|
|
24
27
|
declare global {
|
package/dist/zui-formfield.js
CHANGED
|
@@ -29,17 +29,47 @@ export class ZuiFormField extends ZuiBaseElement {
|
|
|
29
29
|
*/
|
|
30
30
|
this.controlSelector = '*';
|
|
31
31
|
}
|
|
32
|
+
#validationMessageField;
|
|
33
|
+
#validationIntervalId;
|
|
34
|
+
get #validationMessage() {
|
|
35
|
+
return this.#validationMessageField;
|
|
36
|
+
}
|
|
37
|
+
set #validationMessage(msg) {
|
|
38
|
+
const oldValue = this.#validationMessageField;
|
|
39
|
+
if (oldValue !== msg) {
|
|
40
|
+
this.#validationMessageField = msg?.trim();
|
|
41
|
+
this.requestUpdate();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
32
44
|
get #control() {
|
|
33
45
|
return findAssignedElement(this._slotEl, this.controlSelector);
|
|
34
46
|
}
|
|
35
47
|
static get styles() {
|
|
36
48
|
return [super.styles, style];
|
|
37
49
|
}
|
|
50
|
+
async firstUpdated(changedProps) {
|
|
51
|
+
super.firstUpdated(changedProps);
|
|
52
|
+
if (this.#control) {
|
|
53
|
+
this.#control.tagName.includes('-')
|
|
54
|
+
? customElements
|
|
55
|
+
.whenDefined(this.#control.tagName.toLowerCase())
|
|
56
|
+
.then(() => this.#configureValidation(this.#control))
|
|
57
|
+
: this.#configureValidation(this.#control);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
disconnectedCallback() {
|
|
61
|
+
super.disconnectedCallback();
|
|
62
|
+
if (this.#validationIntervalId) {
|
|
63
|
+
clearInterval(this.#validationIntervalId);
|
|
64
|
+
this.#validationIntervalId = undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
38
67
|
render() {
|
|
39
68
|
return html `
|
|
40
69
|
<div class="container" part="container">
|
|
41
70
|
${this.label ? html ` <label @click="${this.#onLabelClick}" part="label">${this.label}</label> ` : nothing}
|
|
42
|
-
<slot></slot>
|
|
71
|
+
<slot @slotchange="${this.#onSlotChange}"></slot>
|
|
72
|
+
<div class="validation-message">${this.#validationMessage ?? ''}</div>
|
|
43
73
|
</div>
|
|
44
74
|
`;
|
|
45
75
|
}
|
|
@@ -47,6 +77,36 @@ export class ZuiFormField extends ZuiBaseElement {
|
|
|
47
77
|
this.#control?.focus();
|
|
48
78
|
this.#control?.click();
|
|
49
79
|
}
|
|
80
|
+
#onSlotChange() {
|
|
81
|
+
if (this.#control) {
|
|
82
|
+
this.#configureValidation(this.#control);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
#configureValidation(control) {
|
|
86
|
+
if (!control || typeof control.checkValidity !== 'function') {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
control.addEventListener('invalid', async (event) => {
|
|
90
|
+
await this.#extractValidationMessage(control);
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
});
|
|
93
|
+
control.addEventListener('blur', async () => await this.#extractValidationMessage(control));
|
|
94
|
+
control.addEventListener('input', async () => await this.#extractValidationMessage(control));
|
|
95
|
+
control.addEventListener('change', async () => await this.#extractValidationMessage(control));
|
|
96
|
+
// workaround native elements not necessarily raising events for direct manipulation (i.e. input.value = 'foo' doesn't trigger events)
|
|
97
|
+
if (!control.tagName.includes('-')) {
|
|
98
|
+
window.clearInterval(this.#validationIntervalId);
|
|
99
|
+
this.#validationIntervalId = window.setInterval(async () => {
|
|
100
|
+
await this.#extractValidationMessage(control);
|
|
101
|
+
}, 1000);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async #extractValidationMessage(control) {
|
|
105
|
+
if (control.updateComplete) {
|
|
106
|
+
await control.updateComplete;
|
|
107
|
+
}
|
|
108
|
+
this.#validationMessage = control.validationMessage;
|
|
109
|
+
}
|
|
50
110
|
}
|
|
51
111
|
__decorate([
|
|
52
112
|
property({ type: String })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zui-formfield.js","sourceRoot":"","sources":["../src/zui-formfield.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAC3F,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"zui-formfield.js","sourceRoot":"","sources":["../src/zui-formfield.ts"],"names":[],"mappings":"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","sourcesContent":["import { ZuiBaseElement } from '@zywave/zui-base';\nimport { findAssignedElement } from '@zywave/zui-base/dist/utils/find-assigned-element.js';\nimport { html, nothing } from 'lit';\nimport { property, query } from 'lit/decorators.js';\nimport { style } from './zui-formfield-css.js';\nimport type { PropertyValues } from 'lit';\n\n/**\n * `<zui-formfield>` provides a standardized way of labeling and styling form controls.\n *\n * @slot - Default, unnamed slot; for inserting form controls, such as `<select>`, `<input>`, `<zui-input>`, `<zui-select>`, etc., into `<zui-formfield>`\n *\n * @csspart container - The container for form fields inserted into `zui-formfield`; this is exposed as a CSS shadow part and can be accessed with `::part(container)`.\n * @csspart label - The label for `zui-formfield`; this is exposed as a CSS shadow part and can be accessed with `::part(label)`.\n */\nexport class ZuiFormField extends ZuiBaseElement {\n /**\n * (optional): Label text, for the form control. Alternatively can slot in label text instead. If necessary, can be styled with `::part(label)`.\n */\n @property({ type: String })\n label: string | null = null;\n\n /**\n * (optional): Provide a valid CSS selector to help `zui-formfield` find the correct form control. Defaults to the first child element.\n */\n @property({ type: String, attribute: 'control-selector' })\n controlSelector = '*';\n\n @query('slot')\n _slotEl: HTMLSlotElement;\n\n #validationMessageField?: string | null | undefined;\n\n #validationIntervalId?: number | undefined;\n\n get #validationMessage() {\n return this.#validationMessageField;\n }\n\n set #validationMessage(msg: string | null | undefined) {\n const oldValue = this.#validationMessageField;\n if (oldValue !== msg) {\n this.#validationMessageField = msg?.trim();\n this.requestUpdate();\n }\n }\n\n get #control() {\n return findAssignedElement(this._slotEl, this.controlSelector);\n }\n\n static get styles() {\n return [super.styles, style];\n }\n\n async firstUpdated(changedProps: PropertyValues) {\n super.firstUpdated(changedProps);\n\n if (this.#control) {\n this.#control.tagName.includes('-')\n ? customElements\n .whenDefined(this.#control.tagName.toLowerCase())\n .then(() => this.#configureValidation(this.#control as ValidatableLitElement))\n : this.#configureValidation(this.#control as ValidatableLitElement);\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n\n if (this.#validationIntervalId) {\n clearInterval(this.#validationIntervalId);\n this.#validationIntervalId = undefined;\n }\n }\n\n render() {\n return html`\n <div class=\"container\" part=\"container\">\n ${this.label ? html` <label @click=\"${this.#onLabelClick}\" part=\"label\">${this.label}</label> ` : nothing}\n <slot @slotchange=\"${this.#onSlotChange}\"></slot>\n <div class=\"validation-message\">${this.#validationMessage ?? ''}</div>\n </div>\n `;\n }\n\n #onLabelClick() {\n this.#control?.focus();\n this.#control?.click();\n }\n\n #onSlotChange() {\n if (this.#control) {\n this.#configureValidation(this.#control as ValidatableLitElement);\n }\n }\n\n #configureValidation(control: ValidatableLitElement) {\n if (!control || typeof control.checkValidity !== 'function') {\n return;\n }\n\n control.addEventListener('invalid', async (event) => {\n await this.#extractValidationMessage(control);\n event.preventDefault();\n });\n\n control.addEventListener('blur', async () => await this.#extractValidationMessage(control));\n control.addEventListener('input', async () => await this.#extractValidationMessage(control));\n control.addEventListener('change', async () => await this.#extractValidationMessage(control));\n\n // workaround native elements not necessarily raising events for direct manipulation (i.e. input.value = 'foo' doesn't trigger events)\n if (!control.tagName.includes('-')) {\n window.clearInterval(this.#validationIntervalId);\n\n this.#validationIntervalId = window.setInterval(async () => {\n await this.#extractValidationMessage(control);\n }, 1000);\n }\n }\n\n async #extractValidationMessage(control: ValidatableLitElement) {\n if (control.updateComplete) {\n await control.updateComplete;\n }\n this.#validationMessage = control.validationMessage;\n }\n}\n\nwindow.customElements.define('zui-formfield', ZuiFormField);\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'zui-formfield': ZuiFormField;\n }\n}\n\ntype ValidatableLitElement = ValidatableHTMLElement & {\n updateComplete?: Promise<void>;\n};\n\ntype ValidatableHTMLElement = HTMLElement & {\n setCustomValidity(message: string): void;\n checkValidity(): boolean;\n reportValidity(): boolean;\n validationMessage: string;\n validity: ValidityState;\n};\n"]}
|
package/lab.html
CHANGED
|
@@ -99,10 +99,10 @@
|
|
|
99
99
|
</select>
|
|
100
100
|
</zui-formfield>
|
|
101
101
|
<zui-formfield class="part-demo" label="Native input">
|
|
102
|
-
<input type="text" name="Native-input" />
|
|
102
|
+
<input type="text" name="Native-input" required="" />
|
|
103
103
|
</zui-formfield>
|
|
104
104
|
<zui-formfield class="part-demo" label="Native date input">
|
|
105
|
-
<input type="date" name="Native-date-input" />
|
|
105
|
+
<input type="date" name="Native-date-input" required="" />
|
|
106
106
|
</zui-formfield>
|
|
107
107
|
<zui-formfield class="part-demo" label="Native textarea">
|
|
108
108
|
<textarea name="Native-textarea"></textarea>
|
|
@@ -163,7 +163,12 @@
|
|
|
163
163
|
</zui-select>
|
|
164
164
|
</zui-formfield>
|
|
165
165
|
<zui-formfield class="part-demo" label="ZUI input">
|
|
166
|
-
<zui-input
|
|
166
|
+
<zui-input
|
|
167
|
+
name="zui-input"
|
|
168
|
+
required=""
|
|
169
|
+
minlength="2"
|
|
170
|
+
validation-message-minlength="Please use at least 2 characters."
|
|
171
|
+
></zui-input>
|
|
167
172
|
</zui-formfield>
|
|
168
173
|
<zui-formfield label="ZUI date input">
|
|
169
174
|
<zui-input name="zui-input-date" type="date"></zui-input>
|
|
@@ -261,7 +266,9 @@
|
|
|
261
266
|
<zui-radio name="zui-radio2" value="zui-radio2-value"></zui-radio>
|
|
262
267
|
</zui-formfield>
|
|
263
268
|
<div class="validation">
|
|
264
|
-
<zui-well animated type="error" class="is-hidden"
|
|
269
|
+
<zui-well animated type="error" class="is-hidden"
|
|
270
|
+
>Hold on! You'll need to fix the highlighted items above to continue.</zui-well
|
|
271
|
+
>
|
|
265
272
|
</div>
|
|
266
273
|
<div class="actions">
|
|
267
274
|
<zui-button>
|
|
@@ -368,7 +375,7 @@
|
|
|
368
375
|
document.getElementById("dialog").open();
|
|
369
376
|
});
|
|
370
377
|
|
|
371
|
-
document.getElementById("zuiCheckboxValidate").addEventListener("click", function() {
|
|
378
|
+
document.getElementById("zuiCheckboxValidate").addEventListener("click", function () {
|
|
372
379
|
const formfield = document.getElementById("validation-formfield");
|
|
373
380
|
formfield?.querySelector("zui-input")?.classList.toggle("validation-input");
|
|
374
381
|
formfield?.querySelector(".validation-text")?.classList.toggle("is-hidden");
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zywave/zui-formfield",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0-pre.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@zywave/zui-base": "^4.
|
|
8
|
+
"@zywave/zui-base": "^4.5.0-pre.0"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "npm run build:scss && npm run build:ts",
|
|
@@ -24,5 +24,5 @@
|
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"customElements": "dist/custom-elements.json",
|
|
27
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "75d884e783a80b48c2411cfaf9b55e8b46f267d3"
|
|
28
28
|
}
|
package/src/zui-formfield-css.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { css } from 'lit';
|
|
2
2
|
|
|
3
|
-
export const style = css`:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}`;
|
|
3
|
+
export const style = css`:host .container label,:host .container ::slotted(label){display:inline-flex;margin:0;font-weight:600;color:var(--zui-gray-800)}:host{contain:none}:host .container{display:flex;flex-direction:column;margin-bottom:1.25rem}:host .container ::slotted(*:not(zui-toggle):not(zui-checkbox):not(zui-radio)){margin-top:.25rem !important}:host .container ::slotted(zui-toggle){margin:.9375rem 0}:host .validation-message:empty{display:none}:host .validation-message{color:var(--zui-red-500)}`;
|
package/src/zui-formfield.scss
CHANGED
package/src/zui-formfield.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { findAssignedElement } from '@zywave/zui-base/dist/utils/find-assigned-e
|
|
|
3
3
|
import { html, nothing } from 'lit';
|
|
4
4
|
import { property, query } from 'lit/decorators.js';
|
|
5
5
|
import { style } from './zui-formfield-css.js';
|
|
6
|
+
import type { PropertyValues } from 'lit';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* `<zui-formfield>` provides a standardized way of labeling and styling form controls.
|
|
@@ -28,6 +29,22 @@ export class ZuiFormField extends ZuiBaseElement {
|
|
|
28
29
|
@query('slot')
|
|
29
30
|
_slotEl: HTMLSlotElement;
|
|
30
31
|
|
|
32
|
+
#validationMessageField?: string | null | undefined;
|
|
33
|
+
|
|
34
|
+
#validationIntervalId?: number | undefined;
|
|
35
|
+
|
|
36
|
+
get #validationMessage() {
|
|
37
|
+
return this.#validationMessageField;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set #validationMessage(msg: string | null | undefined) {
|
|
41
|
+
const oldValue = this.#validationMessageField;
|
|
42
|
+
if (oldValue !== msg) {
|
|
43
|
+
this.#validationMessageField = msg?.trim();
|
|
44
|
+
this.requestUpdate();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
get #control() {
|
|
32
49
|
return findAssignedElement(this._slotEl, this.controlSelector);
|
|
33
50
|
}
|
|
@@ -36,11 +53,33 @@ export class ZuiFormField extends ZuiBaseElement {
|
|
|
36
53
|
return [super.styles, style];
|
|
37
54
|
}
|
|
38
55
|
|
|
56
|
+
async firstUpdated(changedProps: PropertyValues) {
|
|
57
|
+
super.firstUpdated(changedProps);
|
|
58
|
+
|
|
59
|
+
if (this.#control) {
|
|
60
|
+
this.#control.tagName.includes('-')
|
|
61
|
+
? customElements
|
|
62
|
+
.whenDefined(this.#control.tagName.toLowerCase())
|
|
63
|
+
.then(() => this.#configureValidation(this.#control as ValidatableLitElement))
|
|
64
|
+
: this.#configureValidation(this.#control as ValidatableLitElement);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
disconnectedCallback() {
|
|
69
|
+
super.disconnectedCallback();
|
|
70
|
+
|
|
71
|
+
if (this.#validationIntervalId) {
|
|
72
|
+
clearInterval(this.#validationIntervalId);
|
|
73
|
+
this.#validationIntervalId = undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
39
77
|
render() {
|
|
40
78
|
return html`
|
|
41
79
|
<div class="container" part="container">
|
|
42
80
|
${this.label ? html` <label @click="${this.#onLabelClick}" part="label">${this.label}</label> ` : nothing}
|
|
43
|
-
<slot></slot>
|
|
81
|
+
<slot @slotchange="${this.#onSlotChange}"></slot>
|
|
82
|
+
<div class="validation-message">${this.#validationMessage ?? ''}</div>
|
|
44
83
|
</div>
|
|
45
84
|
`;
|
|
46
85
|
}
|
|
@@ -49,6 +88,43 @@ export class ZuiFormField extends ZuiBaseElement {
|
|
|
49
88
|
this.#control?.focus();
|
|
50
89
|
this.#control?.click();
|
|
51
90
|
}
|
|
91
|
+
|
|
92
|
+
#onSlotChange() {
|
|
93
|
+
if (this.#control) {
|
|
94
|
+
this.#configureValidation(this.#control as ValidatableLitElement);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#configureValidation(control: ValidatableLitElement) {
|
|
99
|
+
if (!control || typeof control.checkValidity !== 'function') {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
control.addEventListener('invalid', async (event) => {
|
|
104
|
+
await this.#extractValidationMessage(control);
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
control.addEventListener('blur', async () => await this.#extractValidationMessage(control));
|
|
109
|
+
control.addEventListener('input', async () => await this.#extractValidationMessage(control));
|
|
110
|
+
control.addEventListener('change', async () => await this.#extractValidationMessage(control));
|
|
111
|
+
|
|
112
|
+
// workaround native elements not necessarily raising events for direct manipulation (i.e. input.value = 'foo' doesn't trigger events)
|
|
113
|
+
if (!control.tagName.includes('-')) {
|
|
114
|
+
window.clearInterval(this.#validationIntervalId);
|
|
115
|
+
|
|
116
|
+
this.#validationIntervalId = window.setInterval(async () => {
|
|
117
|
+
await this.#extractValidationMessage(control);
|
|
118
|
+
}, 1000);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async #extractValidationMessage(control: ValidatableLitElement) {
|
|
123
|
+
if (control.updateComplete) {
|
|
124
|
+
await control.updateComplete;
|
|
125
|
+
}
|
|
126
|
+
this.#validationMessage = control.validationMessage;
|
|
127
|
+
}
|
|
52
128
|
}
|
|
53
129
|
|
|
54
130
|
window.customElements.define('zui-formfield', ZuiFormField);
|
|
@@ -58,3 +134,15 @@ declare global {
|
|
|
58
134
|
'zui-formfield': ZuiFormField;
|
|
59
135
|
}
|
|
60
136
|
}
|
|
137
|
+
|
|
138
|
+
type ValidatableLitElement = ValidatableHTMLElement & {
|
|
139
|
+
updateComplete?: Promise<void>;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type ValidatableHTMLElement = HTMLElement & {
|
|
143
|
+
setCustomValidity(message: string): void;
|
|
144
|
+
checkValidity(): boolean;
|
|
145
|
+
reportValidity(): boolean;
|
|
146
|
+
validationMessage: string;
|
|
147
|
+
validity: ValidityState;
|
|
148
|
+
};
|
|
@@ -3,21 +3,37 @@
|
|
|
3
3
|
/* eslint-disable no-undef */
|
|
4
4
|
import { ZuiFormField } from '@zywave/zui-formfield';
|
|
5
5
|
import { assert } from '@esm-bundle/chai';
|
|
6
|
+
import { buildForm } from '../../../../test/src/util/form-helpers.js';
|
|
7
|
+
import { sleep } from '../../../../test/src/util/helpers.js';
|
|
6
8
|
|
|
7
9
|
suite('zui-formfield', () => {
|
|
10
|
+
let form: HTMLFormElement;
|
|
8
11
|
let formField: ZuiFormField;
|
|
9
12
|
let formFieldInput: HTMLInputElement;
|
|
10
13
|
|
|
11
|
-
setup(() => {
|
|
14
|
+
setup(async () => {
|
|
12
15
|
formField = document.createElement('zui-formfield') as ZuiFormField;
|
|
13
16
|
formField.label = 'Label 1';
|
|
14
17
|
formFieldInput = document.createElement('input');
|
|
18
|
+
formFieldInput.setAttribute('name', 'input1');
|
|
15
19
|
formField.appendChild(formFieldInput);
|
|
16
|
-
|
|
20
|
+
|
|
21
|
+
form = buildForm({
|
|
22
|
+
appendChildren: [formField],
|
|
23
|
+
numInputs: 0,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await formField.updateComplete;
|
|
17
27
|
});
|
|
18
28
|
|
|
19
29
|
teardown(() => {
|
|
20
|
-
|
|
30
|
+
formField.remove();
|
|
31
|
+
formFieldInput.remove();
|
|
32
|
+
form.remove();
|
|
33
|
+
|
|
34
|
+
formFieldInput = null;
|
|
35
|
+
formField = null;
|
|
36
|
+
form = null;
|
|
21
37
|
});
|
|
22
38
|
|
|
23
39
|
test('initializes as a ZuiFormField', () => {
|
|
@@ -36,4 +52,46 @@ suite('zui-formfield', () => {
|
|
|
36
52
|
'clicking on a label did not set the active element to the associated label control'
|
|
37
53
|
);
|
|
38
54
|
});
|
|
55
|
+
|
|
56
|
+
test('shows validation message when control is invalid', async () => {
|
|
57
|
+
formFieldInput.setAttribute('required', '');
|
|
58
|
+
formFieldInput.value = '';
|
|
59
|
+
|
|
60
|
+
form.requestSubmit();
|
|
61
|
+
await formField.updateComplete;
|
|
62
|
+
|
|
63
|
+
const validationMessage = formField.shadowRoot.querySelector('.validation-message') as HTMLElement;
|
|
64
|
+
assert.isNotEmpty(validationMessage?.textContent, 'validation message text is missing');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("doesn't show validation message when control is valid", async () => {
|
|
68
|
+
formFieldInput.setAttribute('required', '');
|
|
69
|
+
formFieldInput.value = 'value';
|
|
70
|
+
|
|
71
|
+
form.requestSubmit();
|
|
72
|
+
await formField.updateComplete;
|
|
73
|
+
|
|
74
|
+
const validationMessage = formField.shadowRoot.querySelector('.validation-message') as HTMLElement;
|
|
75
|
+
assert.isEmpty(validationMessage?.textContent, 'validation message text should be empty for valid control');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('validation message updates when control validity changes', async () => {
|
|
79
|
+
formFieldInput.setAttribute('required', '');
|
|
80
|
+
formFieldInput.value = '';
|
|
81
|
+
|
|
82
|
+
form.requestSubmit();
|
|
83
|
+
await formField.updateComplete;
|
|
84
|
+
|
|
85
|
+
let validationMessage = formField.shadowRoot.querySelector('.validation-message') as HTMLElement;
|
|
86
|
+
assert.isNotEmpty(validationMessage?.textContent, 'validation message should not be empty for invalid control');
|
|
87
|
+
|
|
88
|
+
formFieldInput.value = 'new value';
|
|
89
|
+
await sleep(1001);
|
|
90
|
+
|
|
91
|
+
form.requestSubmit();
|
|
92
|
+
await formField.updateComplete;
|
|
93
|
+
|
|
94
|
+
validationMessage = formField.shadowRoot.querySelector('.validation-message') as HTMLElement;
|
|
95
|
+
assert.isEmpty(validationMessage?.textContent, 'validation message should be empty for valid control');
|
|
96
|
+
});
|
|
39
97
|
});
|