Skip to content

Commit b484277

Browse files
committed
refactor: Form associated mixin and validation slots container
Introduced a `_touched` state for form associated components and dropped the previous `_dirty` flag. Changed the behavior of how **invalid** visual state is represented for components. With the current batch of changes, the visual styles are applied if the internal validity of the component is **false** and one of the following is applicable: * the user has interacted with the component through the UI - either by focusing, click, selection, typing or other form of keyboard interaction. * `reportValidity` was invoked on the form associated component. * The parent form of the form associated component is being submitted.
1 parent 914f6c1 commit b484277

File tree

3 files changed

+121
-35
lines changed

3 files changed

+121
-35
lines changed

src/components/common/mixins/forms/associated.ts

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
import type { LitElement } from 'lit';
2-
import { property } from 'lit/decorators.js';
2+
import { property, state } from 'lit/decorators.js';
33
import { addSafeEventListener, isFunction, isString } from '../../util.js';
44
import type { Validator } from '../../validators.js';
55
import type { Constructor } from '../constructor.js';
66
import type { FormValue } from './form-value.js';
7-
import type {
8-
FormAssociatedCheckboxElementInterface,
9-
FormAssociatedElementInterface,
10-
FormRestoreMode,
11-
FormValueType,
7+
import {
8+
type FormAssociatedCheckboxElementInterface,
9+
type FormAssociatedElementInterface,
10+
type FormRestoreMode,
11+
type FormValueType,
12+
InternalInvalidEvent,
13+
InternalResetEvent,
1214
} from './types.js';
1315

16+
const eventOptions = {
17+
bubbles: false,
18+
composed: false,
19+
};
20+
21+
function emitFormInvalidEvent(host: LitElement): void {
22+
host.dispatchEvent(new CustomEvent(InternalInvalidEvent, eventOptions));
23+
}
24+
25+
function emitFormResetEvent(host: LitElement): void {
26+
host.dispatchEvent(new CustomEvent(InternalResetEvent, eventOptions));
27+
}
28+
1429
function BaseFormAssociated<T extends Constructor<LitElement>>(base: T) {
1530
class BaseFormAssociatedElement extends base {
1631
public static readonly formAssociated = true;
@@ -20,9 +35,25 @@ function BaseFormAssociated<T extends Constructor<LitElement>>(base: T) {
2035

2136
protected _disabled = false;
2237
protected _invalid = false;
23-
protected _dirty = false;
2438
protected _pristine = true;
2539

40+
@state()
41+
private _isFormSubmit = false;
42+
43+
@state()
44+
private _touched = false;
45+
46+
@state()
47+
private _isInternalValidation = false;
48+
49+
private get _shouldApplyStyles(): boolean {
50+
return (
51+
this._invalid &&
52+
(this._touched || this._isFormSubmit) &&
53+
!this._isInternalValidation
54+
);
55+
}
56+
2657
protected get __validators(): Validator[] {
2758
return [];
2859
}
@@ -55,13 +86,12 @@ function BaseFormAssociated<T extends Constructor<LitElement>>(base: T) {
5586
* @default false
5687
*/
5788
@property({ type: Boolean, reflect: true })
58-
public set invalid(value: boolean) {
59-
this._invalid = value;
60-
this.toggleAttribute('invalid', Boolean(this._invalid));
89+
public set invalid(_: boolean) {
90+
this._setInvalidStyles();
6191
}
6292

6393
public get invalid(): boolean {
64-
return this._invalid;
94+
return this._shouldApplyStyles;
6595
}
6696

6797
/** Returns the HTMLFormElement associated with this element. */
@@ -97,39 +127,68 @@ function BaseFormAssociated<T extends Constructor<LitElement>>(base: T) {
97127
addSafeEventListener(this, 'invalid', this._handleInvalid);
98128
}
99129

130+
/** @internal */
100131
public override connectedCallback(): void {
101132
super.connectedCallback();
102-
this._dirty = false;
133+
this._pristine = true;
134+
this._touched = false;
103135
this._updateValidity();
136+
this._setInvalidStyles();
104137
}
105138

106-
private _handleInvalid(event: Event) {
139+
private _setInvalidStyles(): void {
140+
this.toggleAttribute('invalid', this._shouldApplyStyles);
141+
this.requestUpdate();
142+
}
143+
144+
private _handleInvalid(event: Event): void {
107145
event.preventDefault();
108-
this.invalid = true;
146+
this._invalid = true;
147+
148+
if (this._isInternalValidation) {
149+
this._isInternalValidation = false;
150+
} else {
151+
this._isFormSubmit = true;
152+
emitFormInvalidEvent(this);
153+
}
154+
155+
this._setInvalidStyles();
156+
this._isFormSubmit = false;
109157
}
110158

111-
private _setInvalidState(): void {
112-
if (this._dirty || !this._pristine) {
113-
this.invalid = !this.checkValidity();
159+
protected _handleBlur(): void {
160+
if (!this._touched) {
161+
this._touched = true;
162+
}
163+
this._validate();
164+
}
165+
166+
protected _setTouchedState(): void {
167+
if (!this._touched) {
168+
this._touched = true;
114169
}
115170
}
116171

117172
private __runValidators() {
118173
const validity: ValidityStateFlags = {};
119174
let message = '';
175+
let isInvalid = false;
120176

121177
for (const validator of this.__validators) {
122178
const isValid = validator.isValid(this);
123179

124180
validity[validator.key] = !isValid;
125181

126182
if (!isValid) {
183+
isInvalid = true;
127184
message = isFunction(validator.message)
128185
? validator.message(this)
129186
: validator.message;
130187
}
131188
}
132189

190+
this._invalid = isInvalid;
191+
133192
return { validity, message };
134193
}
135194

@@ -145,13 +204,12 @@ function BaseFormAssociated<T extends Constructor<LitElement>>(base: T) {
145204

146205
protected _validate(message?: string): void {
147206
this._updateValidity(message);
148-
this._setInvalidState();
149207
}
150208

151209
/**
152210
* Executes the component validators and updates the internal validity state.
153211
*/
154-
protected _updateValidity(error?: string) {
212+
protected _updateValidity(error?: string): void {
155213
let { validity, message } = this.__runValidators();
156214
const hasCustomError = this.validity.customError;
157215

@@ -178,11 +236,15 @@ function BaseFormAssociated<T extends Constructor<LitElement>>(base: T) {
178236
}
179237

180238
this.__internals.setValidity(validity, message);
239+
this._isInternalValidation = true;
240+
this._invalid = !this.__internals.checkValidity();
181241
}
182242

183243
protected _setFormValue(value: FormValueType, state?: FormValueType): void {
184244
this._pristine = false;
185245
this.__internals.setFormValue(value, state);
246+
this._updateValidity();
247+
this._setInvalidStyles();
186248
}
187249

188250
protected formAssociatedCallback(_form: HTMLFormElement): void {}
@@ -195,8 +257,10 @@ function BaseFormAssociated<T extends Constructor<LitElement>>(base: T) {
195257
protected formResetCallback(): void {
196258
this._restoreDefaultValue();
197259
this._pristine = true;
198-
this._dirty = false;
199-
this.invalid = false;
260+
this._touched = false;
261+
this._invalid = false;
262+
this._setInvalidStyles();
263+
emitFormResetEvent(this);
200264
}
201265

202266
/* c8 ignore next 4 */
@@ -206,24 +270,25 @@ function BaseFormAssociated<T extends Constructor<LitElement>>(base: T) {
206270
): void {}
207271

208272
/** Checks for validity of the control and shows the browser message if it invalid. */
209-
public reportValidity() {
273+
public reportValidity(): boolean {
210274
const state = this.__internals.reportValidity();
211-
this.invalid = !state;
275+
this._invalid = !state;
212276
return state;
213277
}
214278

215279
/** Checks for validity of the control and emits the invalid event if it invalid. */
216-
public checkValidity() {
280+
public checkValidity(): boolean {
281+
this._isInternalValidation = true;
217282
const state = this.__internals.checkValidity();
218-
this.invalid = !state;
283+
this._invalid = !state;
219284
return state;
220285
}
221286

222287
/**
223288
* Sets a custom validation message for the control.
224289
* As long as `message` is not empty, the control is considered invalid.
225290
*/
226-
public setCustomValidity(message: string) {
291+
public setCustomValidity(message: string): void {
227292
this._updateValidity(message);
228293
}
229294
}
@@ -250,7 +315,7 @@ export function FormAssociatedMixin<T extends Constructor<LitElement>>(
250315
}
251316
}
252317

253-
public get defaultValue() {
318+
public get defaultValue(): unknown {
254319
return this._formValue.defaultValue;
255320
}
256321

src/components/common/mixins/forms/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ declare class BaseFormAssociatedElement {
1313

1414
// Properties
1515
protected _formValue: unknown;
16-
protected _dirty: boolean;
1716
protected _pristine: boolean;
1817
protected _disabled: boolean;
1918
protected _invalid: boolean;
@@ -60,6 +59,12 @@ declare class BaseFormAssociatedElement {
6059

6160
// Methods
6261

62+
// TODO: Explain
63+
protected _handleBlur(): void;
64+
65+
// TODO: Explain
66+
protected _setTouchedState(): void;
67+
6368
/**
6469
* Sets the default value of the component.
6570
* Called in `attributeChangedCallback`(i.e. when the `value` attribute of the control is set).
@@ -71,13 +76,15 @@ declare class BaseFormAssociatedElement {
7176
*/
7277
protected _restoreDefaultValue(): void;
7378

79+
// TODO: Change docs based on the new behavior
7480
/**
7581
* Executes the {@link BaseFormAssociatedElement._updateValidity | `_updateValidity()`} hook and then applies
7682
* the {@link BaseFormAssociatedElement.invalid | `invalid`} attribute on the control and the associated styles
7783
* if the element has completed the first update cycle or it has been interacted with by the user.
7884
*/
7985
protected _validate(message?: string): void;
8086

87+
// TODO: Drop
8188
/**
8289
* Executes the component's validators and updates the internal validity state.
8390
*/
@@ -160,3 +167,6 @@ export declare class FormRequiredInterface {
160167
public set required(value: boolean);
161168
public get required(): boolean;
162169
}
170+
171+
export const InternalInvalidEvent = 'igc-form-internal-invalid';
172+
export const InternalResetEvent = 'igc-form-internal-reset';

src/components/validation-container/validation-container.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import { property, state } from 'lit/decorators.js';
33
import { cache } from 'lit/directives/cache.js';
44
import { ifDefined } from 'lit/directives/if-defined.js';
55
import { addThemingController } from '../../theming/theming-controller.js';
6+
import { createAbortHandle } from '../common/abort-handler.js';
67
import { registerComponent } from '../common/definitions/register.js';
7-
import type { IgcFormControl } from '../common/mixins/forms/types.js';
8+
import {
9+
type IgcFormControl,
10+
InternalInvalidEvent,
11+
InternalResetEvent,
12+
} from '../common/mixins/forms/types.js';
813
import { partMap } from '../common/part-map.js';
914
import { isEmpty, toKebabCase } from '../common/util.js';
1015
import IgcIconComponent from '../icon/icon.js';
@@ -107,6 +112,8 @@ export default class IgcValidationContainerComponent extends LitElement {
107112
`;
108113
}
109114

115+
private readonly _abortHandle = createAbortHandle();
116+
110117
private _target!: IgcFormControl;
111118

112119
@state()
@@ -121,9 +128,12 @@ export default class IgcValidationContainerComponent extends LitElement {
121128
return;
122129
}
123130

124-
this._target?.removeEventListener('invalid', this);
131+
this._abortHandle.abort();
132+
const { signal } = this._abortHandle;
133+
125134
this._target = value;
126-
this._target.addEventListener('invalid', this);
135+
this._target.addEventListener(InternalInvalidEvent, this, { signal });
136+
this._target.addEventListener(InternalResetEvent, this, { signal });
127137
}
128138

129139
public get target(): IgcFormControl {
@@ -144,10 +154,11 @@ export default class IgcValidationContainerComponent extends LitElement {
144154
/** @internal */
145155
public handleEvent(event: Event): void {
146156
switch (event.type) {
147-
case 'invalid':
148-
if (!this.invalid) {
149-
this.invalid = true;
150-
}
157+
case InternalInvalidEvent:
158+
this.invalid = true;
159+
break;
160+
case InternalResetEvent:
161+
this.invalid = false;
151162
break;
152163
case 'slotchange': {
153164
const newHasSlottedContent = hasProjectedValidation(this);

0 commit comments

Comments
 (0)