Skip to content

Commit

Permalink
[ReactiveElement] Adds scheduleUpdate() (#2097)
Browse files Browse the repository at this point in the history
Fixes #2091. Fixes an issue where `performUpdate` was doing double duty as (1) make the element complete a pending update synchronously and (2) override to control update timing. Here `scheduleUpdate()` is added to satisfy (2), reserving (1) for `performUpdate()`. Since  by default `scheduleUpdate()` just returns the value of `performUpdate()` this should not break any existing code. However, the docs are updated to recommend that for scheduling `scheduleUpdate()` be implemented in favor of `performUpdate()`.
  • Loading branch information
Steve Orvell committed Aug 27, 2021
1 parent 043a16f commit 2b8dd1c
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/clean-dancers-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit/reactive-element': patch
---

Adds `scheduleUpdate()` to control update timing. This should be implemented instead of `performUpdate()`; however, existing overrides of `performUpdate()` will continue to work.
39 changes: 30 additions & 9 deletions packages/reactive-element/src/reactive-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,8 +1006,8 @@ export abstract class ReactiveElement
// `window.onunhandledrejection`.
Promise.reject(e);
}
const result = this.performUpdate();
// If `performUpdate` returns a Promise, we await it. This is done to
const result = this.scheduleUpdate();
// If `scheduleUpdate` returns a Promise, we await it. This is done to
// enable coordinating updates with a scheduler. Note, the result is
// checked to avoid delaying an additional microtask unless we need to.
if (result != null) {
Expand All @@ -1017,22 +1017,43 @@ export abstract class ReactiveElement
}

/**
* Performs an element update. Note, if an exception is thrown during the
* update, `firstUpdated` and `updated` will not be called.
*
* You can override this method to change the timing of updates. If this
* method is overridden, `super.performUpdate()` must be called.
* Schedules an element update. You can override this method to change the
* timing of updates by returning a Promise. The update will await the
* returned Promise, and you should resolve the Promise to allow the update
* to proceed. If this method is overridden, `super.scheduleUpdate()`
* must be called.
*
* For instance, to schedule updates to occur just before the next frame:
*
* ```ts
* override protected async performUpdate(): Promise<unknown> {
* override protected async scheduleUpdate(): Promise<unknown> {
* await new Promise((resolve) => requestAnimationFrame(() => resolve()));
* super.performUpdate();
* super.scheduleUpdate();
* }
* ```
* @category updates
*/
protected scheduleUpdate(): void | Promise<unknown> {
return this.performUpdate();
}

/**
* Performs an element update. Note, if an exception is thrown during the
* update, `firstUpdated` and `updated` will not be called.
*
* Call performUpdate() to immediately process a pending update. This should
* generally not be needed, but it can be done in rare cases when you need to
* update synchronously.
*
* Note: To ensure `performUpdate()` synchronously completes a pending update,
* it should not be overridden. In LitElement 2.x it was suggested to override
* `performUpdate()` to also customizing update scheduling. Instead, you should now
* override `scheduleUpdate()`. For backwards compatibility with LitElement 2.x,
* scheduling updates via `performUpdate()` continues to work, but will make
* also calling `performUpdate()` to synchronously process updates difficult.
*
* @category updates
*/
protected performUpdate(): void | Promise<unknown> {
// Abort any update if one is not pending when this is called.
// This can happen if `performUpdate` is called early to "flush"
Expand Down
26 changes: 13 additions & 13 deletions packages/reactive-element/src/test/reactive-element_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2331,17 +2331,17 @@ suite('ReactiveElement', () => {
assert.equal((el as any).zug, objectValue);
});

test('can override performUpdate()', async () => {
test('can override scheduleUpdate()', async () => {
let resolve: ((value?: unknown) => void) | undefined;

class A extends ReactiveElement {
performUpdateCalled = false;
scheduleUpdateCalled = false;
updateCalled = false;

override async performUpdate() {
this.performUpdateCalled = true;
override async scheduleUpdate() {
this.scheduleUpdateCalled = true;
await new Promise((r) => (resolve = r));
await super.performUpdate();
await super.scheduleUpdate();
}

override update(changedProperties: Map<PropertyKey, unknown>) {
Expand All @@ -2365,21 +2365,21 @@ suite('ReactiveElement', () => {
await new Promise((r) => setTimeout(r, 10));
assert.isFalse(a.updateCalled);

// update is called after performUpdate allowed to complete
// update is called after scheduleUpdate allowed to complete
resolve!();
await a.updateComplete;
assert.isTrue(a.updateCalled);
});

test('overriding performUpdate() allows nested invalidations', async () => {
test('overriding scheduleUpdate() allows nested invalidations', async () => {
class A extends ReactiveElement {
performUpdateCalledCount = 0;
scheduleUpdateCalledCount = 0;
updatedCalledCount = 0;

override async performUpdate() {
this.performUpdateCalledCount++;
override async scheduleUpdate() {
this.scheduleUpdateCalledCount++;
await new Promise((r) => setTimeout(r));
super.performUpdate();
super.scheduleUpdate();
}

override updated(_changedProperties: Map<PropertyKey, unknown>) {
Expand All @@ -2399,14 +2399,14 @@ suite('ReactiveElement', () => {
const updateComplete1 = a.updateComplete;
await updateComplete1;
assert.equal(a.updatedCalledCount, 1);
assert.equal(a.performUpdateCalledCount, 1);
assert.equal(a.scheduleUpdateCalledCount, 1);

const updateComplete2 = a.updateComplete;
assert.notStrictEqual(updateComplete1, updateComplete2);

await updateComplete2;
assert.equal(a.updatedCalledCount, 2);
assert.equal(a.performUpdateCalledCount, 2);
assert.equal(a.scheduleUpdateCalledCount, 2);
});

test('update does not occur before element is connected', async () => {
Expand Down

0 comments on commit 2b8dd1c

Please sign in to comment.