rework @watch decorator

This commit is contained in:
Cory LaViska
2021-06-15 09:26:35 -04:00
parent 3b2b5eed5a
commit 16e7287c24
22 changed files with 61 additions and 97 deletions

View File

@@ -14,6 +14,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
- Fixed a bug in `sl-select` where no selection with `multiple` resulted in an incorrect value [#457](https://github.com/shoelace-style/shoelace/issues/457)
- Fixed a bug in `sl-select` where `sl-change` was emitted immediately after connecting to the DOM [#458](https://github.com/shoelace-style/shoelace/issues/458)
- Fixed a bug in `sl-select` where non-printable keys would cause the menu to open
- Reworked the `@watch` decorator to use `update` instead of `updated` resulting in better performance and flexibility
## 2.0.0-beta.43

View File

@@ -33,7 +33,6 @@ export default class SlAlert extends LitElement {
static styles = unsafeCSS(styles);
private autoHideTimeout: any;
private hasInitialized = false;
@query('[part="base"]') base: HTMLElement;
@@ -67,9 +66,6 @@ export default class SlAlert extends LitElement {
firstUpdated() {
// Set initial visibility
this.base.hidden = !this.open;
// Set the initialized flag after the first render is complete
this.updateComplete.then(() => (this.hasInitialized = true));
}
/** Shows the alert. */
@@ -142,12 +138,8 @@ export default class SlAlert extends LitElement {
this.restartAutoHide();
}
@watch('open')
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (!this.hasInitialized) {
return;
}
if (this.open) {
// Show
this.slShow.emit();
@@ -177,7 +169,7 @@ export default class SlAlert extends LitElement {
}
}
@watch('duration')
@watch('duration', { waitUntilFirstUpdate: true })
handleDurationChange() {
this.restartAutoHide();
}

View File

@@ -93,7 +93,11 @@ export default class SlAnimation extends LitElement {
@watch('iterations')
@watch('iterationsStart')
@watch('keyframes')
handleAnimationChange() {
async handleAnimationChange() {
if (!this.hasUpdated) {
return;
}
this.createAnimation();
}

View File

@@ -111,8 +111,8 @@ export default class SlCheckbox extends LitElement {
this.input.focus();
}
@watch('checked')
@watch('indeterminate')
@watch('checked', { waitUntilFirstUpdate: true })
@watch('indeterminate', { waitUntilFirstUpdate: true })
handleStateChange() {
this.input.checked = this.checked;
this.input.indeterminate = this.indeterminate;

View File

@@ -554,17 +554,17 @@ export default class SlColorPicker extends LitElement {
this.value = this.inputValue;
}
@watch('format')
@watch('format', { waitUntilFirstUpdate: true })
handleFormatChange() {
this.syncValues();
}
@watch('opacity')
@watch('opacity', { waitUntilFirstUpdate: true })
handleOpacityChange() {
this.alpha = 100;
}
@watch('value')
@watch('value', { waitUntilFirstUpdate: true })
handleValueChange(oldValue: string, newValue: string) {
const newColor = this.parseColor(newValue);

View File

@@ -37,7 +37,6 @@ export default class SlDetails extends LitElement {
@query('.details__body') body: HTMLElement;
private componentId = `details-${++id}`;
private hasInitialized = false;
/** Indicates whether or not the details is open. You can use this in lieu of the show/hide methods. */
@property({ type: Boolean, reflect: true }) open = false;
@@ -68,9 +67,6 @@ export default class SlDetails extends LitElement {
firstUpdated() {
this.body.hidden = !this.open;
this.body.style.height = this.open ? 'auto' : '0';
// Set the initialized flag after the first render is complete
this.updateComplete.then(() => (this.hasInitialized = true));
}
disconnectedCallback() {
@@ -122,12 +118,8 @@ export default class SlDetails extends LitElement {
}
}
@watch('open')
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (!this.hasInitialized) {
return;
}
if (this.open) {
// Show
this.slShow.emit();

View File

@@ -54,7 +54,6 @@ export default class SlDialog extends LitElement {
@query('.dialog__overlay') overlay: HTMLElement;
private componentId = `dialog-${++id}`;
private hasInitialized = false;
private modal: Modal;
private originalTrigger: HTMLElement | null;
@@ -106,9 +105,6 @@ export default class SlDialog extends LitElement {
firstUpdated() {
// Set initial visibility
this.dialog.hidden = !this.open;
// Set the initialized flag after the first render is complete
this.updateComplete.then(() => (this.hasInitialized = true));
}
disconnectedCallback() {
@@ -147,12 +143,8 @@ export default class SlDialog extends LitElement {
}
}
@watch('open')
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (!this.hasInitialized) {
return;
}
if (this.open) {
// Show
this.slShow.emit();

View File

@@ -62,7 +62,6 @@ export default class SlDrawer extends LitElement {
@query('.drawer__overlay') overlay: HTMLElement;
private componentId = `drawer-${++id}`;
private hasInitialized = false;
private modal: Modal;
private originalTrigger: HTMLElement | null;
@@ -120,9 +119,6 @@ export default class SlDrawer extends LitElement {
firstUpdated() {
// Set initial visibility
this.drawer.hidden = !this.open;
// Set the initialized flag after the first render is complete
this.updateComplete.then(() => (this.hasInitialized = true));
}
disconnectedCallback() {
@@ -161,12 +157,8 @@ export default class SlDrawer extends LitElement {
}
}
@watch('open')
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (!this.hasInitialized) {
return;
}
if (this.open) {
// Show
this.slShow.emit();

View File

@@ -37,7 +37,6 @@ export default class SlDropdown extends LitElement {
@query('.dropdown__positioner') positioner: HTMLElement;
private componentId = `dropdown-${++id}`;
private hasInitialized = false;
private popover: PopperInstance;
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
@@ -131,9 +130,6 @@ export default class SlDropdown extends LitElement {
firstUpdated() {
// Set initial visibility
this.panel.hidden = !this.open;
// Set the initialized flag after the first render is complete
this.updateComplete.then(() => (this.hasInitialized = true));
}
disconnectedCallback() {
@@ -364,9 +360,9 @@ export default class SlDropdown extends LitElement {
this.popover.update();
}
@watch('open')
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (!this.hasInitialized || this.disabled) {
if (this.disabled) {
return;
}

View File

@@ -92,7 +92,7 @@ export default class SlImageComparer extends LitElement {
}
}
@watch('position')
@watch('position', { waitUntilFirstUpdate: true })
handlePositionChange() {
this.slChange.emit();
}

View File

@@ -30,11 +30,6 @@ export default class SlInclude extends LitElement {
/** Emitted when the included file fails to load due to an error. */
@event('sl-error') slError: EventEmitter<{ status: number }>;
connectedCallback() {
super.connectedCallback();
this.loadSource();
}
executeScript(script: HTMLScriptElement) {
// Create a copy of the script and swap it out so the browser executes it
const newScript = document.createElement('script');
@@ -44,7 +39,7 @@ export default class SlInclude extends LitElement {
}
@watch('src')
async loadSource() {
async handleSrcChange() {
try {
const src = this.src;
const file = await requestInclude(src, this.mode);

View File

@@ -255,7 +255,9 @@ export default class SlInput extends LitElement {
@watch('value')
handleValueChange() {
this.invalid = !this.input.checkValidity();
if (this.input) {
this.invalid = !this.input.checkValidity();
}
}
render() {

View File

@@ -34,7 +34,7 @@ export default class SlProgressRing extends LitElement {
this.updateProgress();
}
@watch('percentage')
@watch('percentage', { waitUntilFirstUpdate: true })
handlePercentageChange() {
this.updateProgress();
}

View File

@@ -49,6 +49,10 @@ export default class SlQrCode extends LitElement {
@watch('size')
@watch('value')
generate() {
if (!this.hasUpdated) {
return;
}
QrCreator.render(
{
text: this.value,

View File

@@ -97,7 +97,7 @@ export default class SlRadio extends LitElement {
return this.getAllRadios().filter(radio => radio !== this) as this[];
}
@watch('checked')
@watch('checked', { waitUntilFirstUpdate: true })
handleCheckedChange() {
if (this.checked) {
this.getSiblingRadios().map(radio => (radio.checked = false));

View File

@@ -163,7 +163,7 @@ export default class SlRating extends LitElement {
event.preventDefault();
}
@watch('value')
@watch('value', { waitUntilFirstUpdate: true })
handleValueChange() {
this.slChange.emit();
}

View File

@@ -32,11 +32,6 @@ export default class SlRelativeTime extends LitElement {
/** Keep the displayed value up to date as time passes. */
@property({ type: Boolean }) sync = false;
connectedCallback() {
super.connectedCallback();
this.updateTime();
}
disconnectedCallback() {
super.disconnectedCallback();
clearTimeout(this.updateTimeout);

View File

@@ -50,7 +50,6 @@ export default class SlSelect extends LitElement {
@query('.select__menu') menu: SlMenu;
private inputId = `select-${++id}`;
private hasInitialized = false;
private helpTextId = `select-help-text-${id}`;
private labelId = `select-label-${id}`;
private resizeObserver: ResizeObserver;
@@ -131,7 +130,6 @@ export default class SlSelect extends LitElement {
this.resizeObserver.observe(this);
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
this.syncItemsFromValue();
this.hasInitialized = true;
});
}
@@ -185,7 +183,7 @@ export default class SlSelect extends LitElement {
this.syncItemsFromValue();
}
@watch('disabled')
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
if (this.disabled && this.isOpen) {
this.dropdown.hide();
@@ -279,7 +277,7 @@ export default class SlSelect extends LitElement {
this.box.focus();
}
@watch('multiple')
@watch('multiple', { waitUntilFirstUpdate: true })
handleMultipleChange() {
// Cast to array | string based on `this.multiple`
const value = this.getValueAsArray();
@@ -287,8 +285,8 @@ export default class SlSelect extends LitElement {
this.syncItemsFromValue();
}
@watch('helpText')
@watch('label')
@watch('helpText', { waitUntilFirstUpdate: true })
@watch('label', { waitUntilFirstUpdate: true })
async handleSlotChange() {
this.hasHelpTextSlot = hasSlot(this, 'help-text');
this.hasLabelSlot = hasSlot(this, 'label');
@@ -314,13 +312,10 @@ export default class SlSelect extends LitElement {
}
}
@watch('value')
@watch('value', { waitUntilFirstUpdate: true })
handleValueChange() {
this.syncItemsFromValue();
if (this.hasInitialized) {
this.slChange.emit();
}
this.slChange.emit();
}
resizeMenu() {

View File

@@ -216,7 +216,7 @@ export default class SlTabGroup extends LitElement {
});
}
@watch('noScrollControls')
@watch('noScrollControls', { waitUntilFirstUpdate: true })
updateScrollControls() {
if (this.noScrollControls) {
this.hasScrollControls = false;
@@ -270,7 +270,7 @@ export default class SlTabGroup extends LitElement {
});
}
@watch('placement')
@watch('placement', { waitUntilFirstUpdate: true })
syncIndicator() {
if (this.indicator) {
const tab = this.getActiveTab();

View File

@@ -238,7 +238,7 @@ export default class SlTextarea extends LitElement {
this.slFocus.emit();
}
@watch('rows')
@watch('rows', { waitUntilFirstUpdate: true })
handleRowsChange() {
this.setTextareaHeight();
}
@@ -250,7 +250,7 @@ export default class SlTextarea extends LitElement {
this.hasLabelSlot = hasSlot(this, 'label');
}
@watch('value')
@watch('value', { waitUntilFirstUpdate: true })
handleValueChange() {
this.invalid = !this.input.checkValidity();
}

View File

@@ -34,7 +34,6 @@ export default class SlTooltip extends LitElement {
@query('.tooltip') tooltip: HTMLElement;
private componentId = `tooltip-${++id}`;
private hasInitialized = false;
private target: HTMLElement;
private popover: PopperInstance;
private hoverTimeout: any;
@@ -116,9 +115,6 @@ export default class SlTooltip extends LitElement {
firstUpdated() {
// Set initial visibility
this.tooltip.hidden = !this.open;
// Set the initialized flag after the first render is complete
this.updateComplete.then(() => (this.hasInitialized = true));
}
disconnectedCallback() {
@@ -210,10 +206,9 @@ export default class SlTooltip extends LitElement {
}
}
@watch('open')
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
if (!this.hasInitialized || this.disabled) {
if (this.disabled) {
return;
}

View File

@@ -63,32 +63,41 @@ export class EventEmitter<T> {
// @watch decorator
//
// Runs after an observed property changes, e.g. @property or @state.
// Runs when an observed property changes, e.g. @property or @state, but before the component updates.
//
// Note that changing props in a watch handler *will* trigger a rerender. To make pre-update changes to observed
// properties, use the `update()` method instead.
// To wait for the update to complete after a change, use `await this.updateComplete` in the handler. To determine if
// the component has previously been updated/rendered, check `this.hasUpdated` in the handler.
//
// Usage:
//
// @watch('propName') handlePropChange(oldValue, newValue) {
// @watch('propName')
// handlePropChange(oldValue, newValue) {
// ...
// }
//
export function watch(propName: string) {
return (protoOrDescriptor: any, name: string): any => {
const { updated } = protoOrDescriptor;
interface WatchOptions {
waitUntilFirstUpdate?: boolean;
}
protoOrDescriptor.updated = function (changedProps: Map<string, any>) {
export function watch(propName: string, options?: WatchOptions) {
return (protoOrDescriptor: any, name: string): any => {
const { update } = protoOrDescriptor;
options = Object.assign({ waitUntilFirstUpdate: false }, options) as WatchOptions;
protoOrDescriptor.update = function (changedProps: Map<string, any>) {
if (changedProps.has(propName)) {
const oldValue = changedProps.get(propName);
const newValue = this[propName];
if (oldValue !== newValue) {
this[name].call(this, oldValue, newValue);
if (!options?.waitUntilFirstUpdate || this.hasUpdated) {
this[name].call(this, oldValue, newValue);
}
}
}
updated.call(this, changedProps);
update.call(this, changedProps);
};
};
}