-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PropertyRequiredMixin #4272
PropertyRequiredMixin #4272
Changes from 2 commits
a2d6176
c495f04
9c98da6
610f215
6d26432
c0d70b0
85db810
0985fcd
0145a8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import '../input-color.js'; | ||
import { expect, fixture, html, oneEvent, runConstructor } from '@brightspace-ui/testing'; | ||
import { createDefaultMessage } from '../../../mixins/property-required/property-required-mixin.js'; | ||
|
||
describe('d2l-input-color', () => { | ||
|
||
|
@@ -128,4 +129,35 @@ describe('d2l-input-color', () => { | |
|
||
}); | ||
|
||
describe('validation', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Example of how a component can now test that the validation is set up properly. |
||
|
||
['foreground', 'background'].forEach(type => { | ||
it(`should not require a label when type is "${type}"`, async() => { | ||
const elem = await fixture(html`<d2l-input-color type="${type}"></d2l-input-color>`); | ||
expect(() => elem.flushRequiredPropertyErrors()).to.not.throw(); | ||
}); | ||
}); | ||
|
||
it('should throw when type is "custom" and no label', async() => { | ||
const elem = await fixture(html`<d2l-input-color type="custom"></d2l-input-color>`); | ||
expect(() => elem.flushRequiredPropertyErrors()) | ||
.to.throw(TypeError, createDefaultMessage('d2l-input-color', 'label')); | ||
}); | ||
|
||
it('should not throw when type is "custom" and label is provided', async() => { | ||
const elem = await fixture(html`<d2l-input-color label="value" type="custom"></d2l-input-color>`); | ||
expect(() => elem.flushRequiredPropertyErrors()).to.not.throw(); | ||
}); | ||
|
||
it('should require a label when type changes to "custom"', async() => { | ||
const elem = await fixture(html`<d2l-input-color type="foreground"></d2l-input-color>`); | ||
expect(() => elem.flushRequiredPropertyErrors()).to.not.throw(); | ||
elem.setAttribute('type', 'custom'); | ||
await elem.updateComplete; | ||
expect(() => elem.flushRequiredPropertyErrors()) | ||
.to.throw(TypeError, createDefaultMessage('d2l-input-color', 'label')); | ||
}); | ||
|
||
}); | ||
|
||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { dedupeMixin } from '@open-wc/dedupe-mixin'; | ||
|
||
const TIMEOUT_DURATION = 3000; | ||
|
||
export function createDefaultMessage(tagName, propertyName) { | ||
return `${tagName}: "${propertyName}" attribute is required.`; | ||
} | ||
|
||
export function createUndefinedPropertyMessage(tagName, propertyName) { | ||
return `PropertyRequiredMixin: "${tagName.toLowerCase()}" has no property "${propertyName}".`; | ||
} | ||
|
||
export function createInvalidPropertyTypeMessage(tagName, propertyName) { | ||
return `PropertyRequiredMixin: only String properties can be required ("${tagName}" required property "${propertyName}".`; | ||
} | ||
|
||
export const PropertyRequiredMixin = dedupeMixin(superclass => class extends superclass { | ||
|
||
constructor() { | ||
super(); | ||
this._allProperties = new Map(); | ||
this._requiredProperties = new Map(); | ||
this._initProperties(Object.getPrototypeOf(this)); | ||
} | ||
|
||
firstUpdated(changedProperties) { | ||
super.firstUpdated(changedProperties); | ||
for (const name of this._requiredProperties.keys()) { | ||
this._validateRequiredProperty(name); | ||
} | ||
} | ||
|
||
updated(changedProperties) { | ||
super.updated(changedProperties); | ||
this._requiredProperties.forEach((value, name) => { | ||
const doValidate = changedProperties.has(name) || | ||
value.dependentProps.includes(name); | ||
if (doValidate) this._validateRequiredProperty(name); | ||
}); | ||
} | ||
|
||
addRequiredProperty(name, opts) { | ||
|
||
const prop = this._allProperties.get(name); | ||
if (prop === undefined) { | ||
throw new Error(createUndefinedPropertyMessage(this.tagName.toLowerCase(), name)); | ||
} | ||
|
||
if (prop.type !== String) { | ||
throw new Error(createInvalidPropertyTypeMessage(this.tagName.toLowerCase(), name)); | ||
} | ||
|
||
opts = { | ||
...{ dependentProps: [], message: defaultMessage => defaultMessage, validator: hasValue => hasValue }, | ||
...opts | ||
}; | ||
|
||
this._requiredProperties.set(name, { | ||
attrName: prop.attribute || name, | ||
dependentProps: opts.dependentProps, | ||
message: opts.message, | ||
thrown: false, | ||
timeout: null, | ||
validator: opts.validator | ||
}); | ||
|
||
} | ||
|
||
flushRequiredPropertyErrors() { | ||
dlockhart marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (const name of this._requiredProperties.keys()) { | ||
this._flushRequiredPropertyError(name); | ||
} | ||
} | ||
|
||
_flushRequiredPropertyError(name) { | ||
|
||
if (!this._requiredProperties.has(name) || !this.isConnected) return; | ||
|
||
const info = this._requiredProperties.get(name); | ||
clearTimeout(info.timeout); | ||
info.timeout = null; | ||
|
||
const hasValue = this[name]?.constructor === String && this[name]?.length > 0; | ||
const success = info.validator(hasValue); | ||
if (!success) { | ||
if (info.thrown) return; | ||
info.thrown = true; | ||
const defaultMessage = createDefaultMessage(this.tagName.toLowerCase(), info.attrName); | ||
throw new TypeError(info.message(defaultMessage)); | ||
} | ||
|
||
} | ||
|
||
_initProperties(base) { | ||
if (base === null) return; | ||
this._initProperties(Object.getPrototypeOf(base)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neat, I figured there had to be a better/cleaner option than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah was going to mention this option to you, as when I was looking up |
||
for (const name in base.constructor.properties) { | ||
this._allProperties.set(name, base.constructor.properties[name]); | ||
} | ||
} | ||
|
||
_validateRequiredProperty(name) { | ||
const info = this._requiredProperties.get(name); | ||
clearTimeout(info.timeout); | ||
info.timeout = setTimeout(() => this._flushRequiredPropertyError(name), TIMEOUT_DURATION); | ||
} | ||
|
||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This use case forced me to add support for "dependent properties" -- essentially other properties to watch and re-validate when they change.