mirror of
https://github.com/shoelace-style/shoelace.git
synced 2026-01-12 11:09:13 +00:00
rework @watch decorator
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export default class SlImageComparer extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('position')
|
||||
@watch('position', { waitUntilFirstUpdate: true })
|
||||
handlePositionChange() {
|
||||
this.slChange.emit();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class SlProgressRing extends LitElement {
|
||||
this.updateProgress();
|
||||
}
|
||||
|
||||
@watch('percentage')
|
||||
@watch('percentage', { waitUntilFirstUpdate: true })
|
||||
handlePercentageChange() {
|
||||
this.updateProgress();
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ export default class SlQrCode extends LitElement {
|
||||
@watch('size')
|
||||
@watch('value')
|
||||
generate() {
|
||||
if (!this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
|
||||
QrCreator.render(
|
||||
{
|
||||
text: this.value,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -163,7 +163,7 @@ export default class SlRating extends LitElement {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
this.slChange.emit();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user