Skip to content

Commit eba8817

Browse files
committed
Merge remote-tracking branch 'origin/dev' into feature/package-barrels
2 parents 36ae6ef + ea1fd73 commit eba8817

File tree

50 files changed

+1125
-1769
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1125
-1769
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ out-css
3737
*.tgz
3838

3939
# old source
40-
/src/**/*
40+
/src

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
"storybook:analyze": "web-component-analyzer **/*.element.ts --outFile custom-elements.json",
3131
"test": "web-test-runner --coverage",
3232
"test:watch": "web-test-runner --watch",
33+
"test:coverage": "web-test-runner --node-resolve --coverage",
34+
"test:coverage-for": "node ./scripts/test-coverage-package.js",
3335
"dev": "npm run clean && npm run storybook",
3436
"build": "lerna run --scope @umbraco-ui/uui-css build && lerna run build",
3537
"build:prod": "npm run clean && npm run build && npm run test",
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { LitElement } from 'lit';
2+
import { property } from 'lit/decorators.js';
3+
4+
type Constructor<T = {}> = new (...args: any[]) => T;
5+
6+
// TODO: make t possible to define FormDataEntryValue type.
7+
export declare abstract class FormControlMixinInterface {
8+
formAssociated: boolean;
9+
get value(): FormDataEntryValue;
10+
set value(newValue: FormDataEntryValue);
11+
name: string;
12+
formResetCallback(): void;
13+
checkValidity: Function;
14+
protected _value: FormDataEntryValue;
15+
protected _internals: any;
16+
protected abstract getFormElement(): HTMLElement | undefined;
17+
}
18+
19+
type FlagTypes =
20+
| 'valueMissing'
21+
| 'typeMismatch'
22+
| 'patternMismatch'
23+
| 'tooLong'
24+
| 'tooShort'
25+
| 'rangeUnderflow'
26+
| 'stepMismatch'
27+
| 'badInput'
28+
| 'customError';
29+
interface Validator {
30+
flagKey: FlagTypes;
31+
getMessage: () => String;
32+
checkMethod: () => boolean;
33+
}
34+
35+
/**
36+
* The mixin allows a custom element to participate in HTML forms.
37+
*
38+
* @param {Object} superClass - superclass to be extended.
39+
* @mixin
40+
*/
41+
export const FormControlMixin = <T extends Constructor<LitElement>>(
42+
superClass: T
43+
) => {
44+
abstract class FormControlMixinClass extends superClass {
45+
/**
46+
* This is a static class field indicating that the element is can be used inside a native form and participate in its events.
47+
* It may require a polyfill, check support here https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals.
48+
* Read more about form controls here https://web.dev/more-capable-form-controls/
49+
* @type {boolean}
50+
*/
51+
static readonly formAssociated = true;
52+
53+
/**
54+
* This is a name property of the component.
55+
* @type {string}
56+
* @attr
57+
* @default ''
58+
*/
59+
@property({ type: String })
60+
name = '';
61+
62+
/**
63+
* Value of this form control.
64+
* @type {string}
65+
* @attr
66+
* @default ''
67+
*/
68+
@property() // Do not 'reflect' as the attribute is used as fallback.
69+
get value() {
70+
return this._value;
71+
}
72+
set value(newValue) {
73+
const oldValue = this._value;
74+
this._value = newValue;
75+
if (
76+
'ElementInternals' in window &&
77+
//@ts-ignore
78+
'setFormValue' in window.ElementInternals.prototype
79+
) {
80+
this._internals.setFormValue(this._value);
81+
}
82+
this.requestUpdate('value', oldValue);
83+
}
84+
85+
// Validation
86+
private _validityState: any = {};
87+
88+
/**
89+
* Apply validation rule for requiring a value of this form control.
90+
* @type {boolean}
91+
* @attr
92+
* @default false
93+
*/
94+
@property({ type: Boolean, reflect: true })
95+
required = false;
96+
97+
/**
98+
* Required validation message.
99+
* @type {boolean}
100+
* @attr
101+
* @default
102+
*/
103+
@property({ type: String, attribute: 'required-message' })
104+
requiredMessage = 'This field is required';
105+
106+
/**
107+
* Apply custom error on this input.
108+
* @type {boolean}
109+
* @attr
110+
* @default false
111+
*/
112+
@property({ type: Boolean, reflect: true })
113+
error = false;
114+
115+
/**
116+
* Custom error message.
117+
* @type {boolean}
118+
* @attr
119+
* @default
120+
*/
121+
@property({ type: String, attribute: 'error-message' })
122+
errorMessage = 'This field is invalid';
123+
124+
private _value: FormDataEntryValue = '';
125+
private _internals: any;
126+
private _form: HTMLFormElement | null = null;
127+
private _validators: Validator[] = [];
128+
129+
constructor(...args: any[]) {
130+
super(...args);
131+
this._internals = (this as any).attachInternals();
132+
133+
this.addValidator(
134+
'valueMissing',
135+
() => this.requiredMessage,
136+
() => this.hasAttribute('required') && this.hasValue() === false
137+
);
138+
this.addValidator(
139+
'customError',
140+
() => this.errorMessage,
141+
() => this.error
142+
);
143+
}
144+
145+
public hasValue(): boolean {
146+
return this.value !== '';
147+
}
148+
149+
protected abstract getFormElement(): HTMLElement | undefined;
150+
151+
connectedCallback(): void {
152+
super.connectedCallback();
153+
this._removeFormListeners();
154+
// TODO: try using formAssociatedCallback
155+
this._form = this._internals.form;
156+
if (this._form) {
157+
if (this._form.hasAttribute('hide-validation')) {
158+
this.setAttribute('hide-validation', '');
159+
}
160+
this._form.addEventListener('submit', this._onFormSubmit);
161+
this._form.addEventListener('reset', this._onFormReset);
162+
}
163+
}
164+
disconnectedCallback(): void {
165+
super.disconnectedCallback();
166+
this._removeFormListeners();
167+
}
168+
private _removeFormListeners() {
169+
if (this._form) {
170+
this._form.removeEventListener('submit', this._onFormSubmit);
171+
this._form.removeEventListener('reset', this._onFormReset);
172+
}
173+
}
174+
175+
protected addValidator(
176+
flagKey: FlagTypes,
177+
getMessageMethod: () => String,
178+
checkMethod: () => boolean
179+
) {
180+
this._validators.push({
181+
flagKey: flagKey,
182+
getMessage: getMessageMethod,
183+
checkMethod: checkMethod,
184+
});
185+
}
186+
187+
private _runValidators() {
188+
this._validators.forEach(validator => {
189+
if (validator.checkMethod()) {
190+
this._validityState[validator.flagKey] = true;
191+
this._internals.setValidity(
192+
this._validityState,
193+
validator.getMessage(),
194+
this.getFormElement()
195+
);
196+
} else {
197+
this._validityState[validator.flagKey] = false;
198+
}
199+
});
200+
201+
const hasError = Object.values(this._validityState).includes(true);
202+
203+
if (hasError === false) {
204+
this._internals.setValidity({});
205+
}
206+
}
207+
208+
updated(changedProperties: Map<string | number | symbol, unknown>) {
209+
super.updated(changedProperties);
210+
this._runValidators();
211+
}
212+
213+
private _onFormSubmit = () => {
214+
if (this._form && this._form.checkValidity() === false) {
215+
this.removeAttribute('hide-validation');
216+
} else {
217+
this.setAttribute('hide-validation', '');
218+
}
219+
};
220+
private _onFormReset = () => {
221+
this.setAttribute('hide-validation', '');
222+
};
223+
224+
public formResetCallback() {
225+
this.value = this.getAttribute('value') || '';
226+
}
227+
228+
public checkValidity() {
229+
return this._internals?.checkValidity();
230+
}
231+
}
232+
return FormControlMixinClass as unknown as Constructor<FormControlMixinInterface> &
233+
T;
234+
};

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './ActiveMixin';
22
export * from './LabelMixin';
33
export * from './SelectableMixin';
44
export * from './SelectOnlyMixin';
5+
export * from './FormControlMixin';

0 commit comments

Comments
 (0)