Skip to content

Commit 6d4dd9b

Browse files
committed
ability to map validaty state from native input to a custom form control
1 parent a70ff42 commit 6d4dd9b

File tree

3 files changed

+67
-12
lines changed

3 files changed

+67
-12
lines changed

packages/uui-base/lib/mixins/FormControlMixin.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import { UUIFormControlEvent } from '../events';
55

66
type Constructor<T = {}> = new (...args: any[]) => T;
77

8+
type NativeFormControlElement = HTMLInputElement; // Eventually use a specific interface or list multiple options like appending these types: ... | HTMLTextAreaElement | HTMLSelectElement
9+
810
// TODO: make it possible to define FormDataEntryValue type.
911
export declare abstract class FormControlMixinInterface extends LitElement {
1012
formAssociated: boolean;
1113
get value(): FormDataEntryValue;
1214
set value(newValue: FormDataEntryValue);
1315
name: string;
1416
formResetCallback(): void;
15-
checkValidity: () => boolean;
17+
checkValidity(): boolean;
1618
get validationMessage(): string;
1719
protected _value: FormDataEntryValue;
1820
protected _internals: any;
@@ -22,6 +24,7 @@ export declare abstract class FormControlMixinInterface extends LitElement {
2224
getMessageMethod: () => String,
2325
checkMethod: () => boolean
2426
) => void;
27+
protected addFormControlElement(element: NativeFormControlElement): void;
2528
pristine: boolean;
2629
required: boolean;
2730
requiredMessage: string;
@@ -147,6 +150,7 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
147150
private _internals: any;
148151
private _form: HTMLFormElement | null = null;
149152
private _validators: Validator[] = [];
153+
private _formCtrlElements: NativeFormControlElement[] = [];
150154

151155
constructor(...args: any[]) {
152156
super(...args);
@@ -196,7 +200,33 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
196200
});
197201
}
198202

203+
/**
204+
* @method addFormControlElement
205+
* @description Important notice if adding a native form control then ensure that its value and thereby validity is updated when value is changed from the outside.
206+
* @param element {NativeFormControlElement} - element to validate and include as part of this form association.
207+
*/
208+
protected addFormControlElement(element: NativeFormControlElement) {
209+
this._formCtrlElements.push(element);
210+
}
211+
199212
private _runValidators() {
213+
this._validityState = {};
214+
215+
// Loop through inner native form controls to adapt their validityState.
216+
this._formCtrlElements.forEach(formCtrlEl => {
217+
for (const key in formCtrlEl.validity) {
218+
if (key !== 'valid' && (formCtrlEl.validity as any)[key]) {
219+
(this as any)._validityState[key] = true;
220+
this._internals.setValidity(
221+
(this as any)._validityState,
222+
formCtrlEl.validationMessage,
223+
formCtrlEl
224+
);
225+
}
226+
}
227+
});
228+
229+
// Loop through custom validators, currently its intentional to have them overwritten native validity. but might need to be reconsidered (This current way enables to overwrite with custom messages)
200230
this._validators.forEach(validator => {
201231
if (validator.checkMethod()) {
202232
this._validityState[validator.flagKey] = true;
@@ -205,8 +235,6 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
205235
validator.getMessage(),
206236
this.getFormElement()
207237
);
208-
} else {
209-
this._validityState[validator.flagKey] = false;
210238
}
211239
});
212240

@@ -225,13 +253,6 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
225253
updated(changedProperties: Map<string | number | symbol, unknown>) {
226254
super.updated(changedProperties);
227255
this._runValidators();
228-
/*
229-
if(changedProperties.has('pristine')) {
230-
if(changedProperties.get('pristine') === false) {
231-
this._internals.reportValidity();
232-
}
233-
}
234-
*/
235256
}
236257

237258
private _onFormSubmit = () => {
@@ -242,7 +263,7 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
242263
this._removeFormListeners();
243264
this._form = this._internals.form;
244265
if (this._form) {
245-
// This relies on the form begin a 'uui-form':
266+
// This relies on the form begin a child of uui-form:
246267
if (this._form.hasAttribute('invalid-submit')) {
247268
this.pristine = false;
248269
}
@@ -255,6 +276,12 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
255276
}
256277

257278
public checkValidity() {
279+
for (const key in this._formCtrlElements) {
280+
if (this._formCtrlElements[key].checkValidity() === false) {
281+
return false;
282+
}
283+
}
284+
258285
return this._internals?.checkValidity();
259286
}
260287

packages/uui-input/lib/uui-input.element.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
22
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
3-
import { css, html, LitElement } from 'lit';
3+
import { css, html, LitElement, PropertyValueMap } from 'lit';
44
import { property, query } from 'lit/decorators.js';
55

66
import { UUIInputEvent } from './UUIInputEvent';
@@ -246,6 +246,13 @@ export class UUIInputElement extends FormControlMixin(LitElement) {
246246
);
247247
}
248248

249+
protected firstUpdated(
250+
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
251+
): void {
252+
super.firstUpdated(_changedProperties);
253+
this.addFormControlElement(this._input);
254+
}
255+
249256
/**
250257
* This method enables <label for="..."> to focus the input
251258
*/
@@ -287,6 +294,7 @@ export class UUIInputElement extends FormControlMixin(LitElement) {
287294
placeholder=${this.placeholder}
288295
aria-label=${this.label}
289296
.disabled=${this.disabled}
297+
?required=${this.required}
290298
?readonly=${this.readonly}
291299
@input=${this._onInput}
292300
@change=${this._onChange} />

packages/uui-input/lib/uui-input.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,24 @@ describe('UuiInput in Form', () => {
213213
});
214214
});
215215
});
216+
217+
describe('native validation', () => {
218+
element.setAttribute('type', 'email');
219+
220+
it('sets element to invalid when value is empty', async () => {
221+
expect(element.checkValidity()).to.be.false;
222+
});
223+
224+
it('email element is invalid when it has a none compliant value', async () => {
225+
element.value = 'new value';
226+
await elementUpdated(element);
227+
expect(element.checkValidity()).to.be.false;
228+
});
229+
230+
it('email element is valid when it has a email value', async () => {
231+
element.value = '[email protected]';
232+
await elementUpdated(element);
233+
expect(element.checkValidity()).to.be.true;
234+
});
235+
});
216236
});

0 commit comments

Comments
 (0)