-
Notifications
You must be signed in to change notification settings - Fork 907
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Part of adding constraint validation mixins. Each component that participates in constraint validation has different rules and error messages for validating. The `Validator` gives `mixinConstraintValidation()` a way to compute and cache validation. Other validators will be added later, such as `SelectValidator` and `RadioValidator` PiperOrigin-RevId: 583508468
- Loading branch information
1 parent
302d181
commit 8eb1f30
Showing
6 changed files
with
378 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/** | ||
* @license | ||
* Copyright 2023 Google LLC | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import {Validator} from './validator.js'; | ||
|
||
/** | ||
* Constraint validation properties for a checkbox. | ||
*/ | ||
export interface CheckboxState { | ||
/** | ||
* Whether the checkbox is checked. | ||
*/ | ||
checked: boolean; | ||
|
||
/** | ||
* Whether the checkbox is required. | ||
*/ | ||
required: boolean; | ||
} | ||
|
||
/** | ||
* A validator that provides constraint validation that emulates | ||
* `<input type="checkbox">` validation. | ||
*/ | ||
export class CheckboxValidator extends Validator<CheckboxState> { | ||
private checkboxControl?: HTMLInputElement; | ||
|
||
protected override computeValidity(state: CheckboxState) { | ||
if (!this.checkboxControl) { | ||
// Lazily create the platform input | ||
this.checkboxControl = document.createElement('input'); | ||
this.checkboxControl.type = 'checkbox'; | ||
} | ||
|
||
this.checkboxControl.checked = state.checked; | ||
this.checkboxControl.required = state.required; | ||
return { | ||
validity: this.checkboxControl.validity, | ||
validationMessage: this.checkboxControl.validationMessage, | ||
}; | ||
} | ||
|
||
protected override equals(prev: CheckboxState, next: CheckboxState) { | ||
return prev.checked === next.checked && prev.required === next.required; | ||
} | ||
|
||
protected override copy({checked, required}: CheckboxState): CheckboxState { | ||
return {checked, required}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/** | ||
* @license | ||
* Copyright 2023 Google LLC | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
// import 'jasmine'; (google3-only) | ||
|
||
import {CheckboxValidator} from './checkbox-validator.js'; | ||
|
||
describe('CheckboxValidator', () => { | ||
it('is invalid when required and not checked', () => { | ||
const state = { | ||
required: true, | ||
checked: false, | ||
}; | ||
|
||
const validator = new CheckboxValidator(() => state); | ||
const {validity, validationMessage} = validator.getValidity(); | ||
expect(validity.valueMissing).withContext('valueMissing').toBeTrue(); | ||
expect(validationMessage).withContext('validationMessage').not.toBe(''); | ||
}); | ||
|
||
it('is valid when required and checked', () => { | ||
const state = { | ||
required: true, | ||
checked: true, | ||
}; | ||
|
||
const validator = new CheckboxValidator(() => state); | ||
const {validity, validationMessage} = validator.getValidity(); | ||
expect(validity.valueMissing).withContext('valueMissing').toBeFalse(); | ||
expect(validationMessage).withContext('validationMessage').toBe(''); | ||
}); | ||
|
||
it('is valid when not required', () => { | ||
const state = { | ||
required: false, | ||
checked: false, | ||
}; | ||
|
||
const validator = new CheckboxValidator(() => state); | ||
const {validity, validationMessage} = validator.getValidity(); | ||
expect(validity.valueMissing).withContext('valueMissing').toBeFalse(); | ||
expect(validationMessage).withContext('validationMessage').toBe(''); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/** | ||
* @license | ||
* Copyright 2023 Google LLC | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
/** | ||
* A class that computes and caches `ValidityStateFlags` for a component with | ||
* a given `State` interface. | ||
* | ||
* Cached performance before computing validity is important since constraint | ||
* validation must be checked frequently and synchronously when properties | ||
* change. | ||
* | ||
* @template State The expected interface of properties relevant to constraint | ||
* validation. | ||
*/ | ||
export abstract class Validator<State> { | ||
/** | ||
* The previous state, used to determine if state changed and validation needs | ||
* to be re-computed. | ||
*/ | ||
private prevState?: State; | ||
|
||
/** | ||
* The current validity state and message. This is cached and returns if | ||
* constraint validation state does not change. | ||
*/ | ||
private currentValidity: ValidityAndMessage = { | ||
validity: {}, | ||
validationMessage: '', | ||
}; | ||
|
||
/** | ||
* Creates a new validator. | ||
* | ||
* @param getCurrentState A callback that returns the current state of | ||
* constraint validation-related properties. | ||
*/ | ||
constructor(private readonly getCurrentState: () => State) {} | ||
|
||
/** | ||
* Returns the current `ValidityStateFlags` and validation message for the | ||
* validator. | ||
* | ||
* If the constraint validation state has not changed, this will return a | ||
* cached result. This is important since `getValidity()` can be called | ||
* frequently in response to synchronous property changes. | ||
* | ||
* @return The current validity and validation message. | ||
*/ | ||
getValidity(): ValidityAndMessage { | ||
const state = this.getCurrentState(); | ||
const hasStateChanged = | ||
!this.prevState || !this.equals(this.prevState, state); | ||
if (!hasStateChanged) { | ||
return this.currentValidity; | ||
} | ||
|
||
const {validity, validationMessage} = this.computeValidity(state); | ||
this.prevState = this.copy(state); | ||
this.currentValidity = { | ||
validationMessage, | ||
validity: { | ||
// Change any `ValidityState` instances into `ValidityStateFlags` since | ||
// `ValidityState` cannot be easily `{...spread}`. | ||
badInput: validity.badInput, | ||
customError: validity.customError, | ||
patternMismatch: validity.patternMismatch, | ||
rangeOverflow: validity.rangeOverflow, | ||
rangeUnderflow: validity.rangeUnderflow, | ||
stepMismatch: validity.stepMismatch, | ||
tooLong: validity.tooLong, | ||
tooShort: validity.tooShort, | ||
typeMismatch: validity.typeMismatch, | ||
valueMissing: validity.valueMissing, | ||
}, | ||
}; | ||
|
||
return this.currentValidity; | ||
} | ||
|
||
/** | ||
* Computes the `ValidityStateFlags` and validation message for a given set | ||
* of constraint validation properties. | ||
* | ||
* Implementations can use platform elements like `<input>` and `<select>` to | ||
* sync state and compute validation along with i18n'd messages. This function | ||
* may be expensive, and is only called when state changes. | ||
* | ||
* @param state The new state of constraint validation properties. | ||
* @return An object containing a `validity` property with | ||
* `ValidityStateFlags` and a `validationMessage` property. | ||
*/ | ||
protected abstract computeValidity(state: State): ValidityAndMessage; | ||
|
||
/** | ||
* Checks if two states are equal. This is used to check against cached state | ||
* to see if validity needs to be re-computed. | ||
* | ||
* @param prev The previous state. | ||
* @param next The next state. | ||
* @return True if the states are equal, or false if not. | ||
*/ | ||
protected abstract equals(prev: State, next: State): boolean; | ||
|
||
/** | ||
* Creates a copy of a state. This is used to cache state and check if it | ||
* changes. | ||
* | ||
* @param state The state to copy. | ||
* @return A copy of the state. | ||
*/ | ||
protected abstract copy(state: State): State; | ||
} | ||
|
||
/** | ||
* An object containing `ValidityStateFlags` and a corresponding validation | ||
* message. | ||
*/ | ||
export interface ValidityAndMessage { | ||
/** | ||
* Validity flags. | ||
*/ | ||
validity: ValidityStateFlags; | ||
|
||
/** | ||
* The validation message for the associated flags. It may not be an empty | ||
* string if any of the validity flags are `true`. | ||
*/ | ||
validationMessage: string; | ||
} |
Oops, something went wrong.