Skip to content

Commit

Permalink
Adds defer-hydration support.
Browse files Browse the repository at this point in the history
Components will now never hydrate when the `defer-hydration` attribute is set. Hydration is only triggered when that attribute is removed or when a component is connected to the DOM without the attribute set.

Two particular nuances here:
1.  Components created imperatively will not trigger hydration unless they are connected to the DOM or `defer-hydration` is added and removed. The reason for this is because otherwise `document.createElement` would immediately trigger hydration before there is an opportunity to set `defer-hydration`.
2.  Removing `defer-hydration` while a component is disconnected from the DOM still triggers hydration. This reason for this is to support components coming from a cloned template which would like to hydrate prior to being connected to the DOM.
  • Loading branch information
dgp1130 committed Dec 3, 2023
1 parent 64c3c97 commit f432fdb
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/component.test.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,13 @@
<template test-case="already-rendered">
<already-rendered></already-rendered>
</template>

<template test-case="deferred">
<deferred-component defer-hydration></deferred-component>
</template>

<template test-case="disconnected-hydration">
<disconnected-hydration defer-hydration></disconnected-hydration>
</template>
</body>
</html>
66 changes: 66 additions & 0 deletions src/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,71 @@ describe('component', () => {
expect(Comp.name).toBe('FooBar');
}
});

describe('`defer-hydration`', () => {
it('defers hydration', testCase('deferred', (el) => {
const hydrate = jasmine.createSpy<HydrateLifecycle>('hydrate');
component('deferred-component', hydrate);

// Should not implicitly hydrate due to `defer-hydration`.
expect(hydrate).not.toHaveBeenCalled();

// Should synchronously hydrate when `defer-hydration` is removed.
el.removeAttribute('defer-hydration');
expect(hydrate).toHaveBeenCalledOnceWith();
}));

it('does not hydrate when imperatively created', () => {
const hydrate = jasmine.createSpy<HydrateLifecycle>('hydrate');
component('imperative-creation', hydrate);

document.createElement('imperative-creation');
expect(hydrate).not.toHaveBeenCalled();
});

it('does not hydrate when a component is upgraded while disconnected', () => {
const el = document.createElement('disconnected-upgrade');

const hydrate = jasmine.createSpy<HydrateLifecycle>('hydrate');
component('disconnected-upgrade', hydrate);
expect(hydrate).not.toHaveBeenCalled();

customElements.upgrade(el);
expect(hydrate).not.toHaveBeenCalled();
});

it('hydrates when `defer-hydration` is removed while disconnected from the DOM', testCase('disconnected-hydration', (el) => {
const hydrate = jasmine.createSpy<HydrateLifecycle>('hydrate');
component('disconnected-hydration', hydrate);
expect(hydrate).not.toHaveBeenCalled();

el.remove();
expect(hydrate).not.toHaveBeenCalled();

// Removing `defer-hydration` should trigger hydration even though the
// element is disconnected.
el.removeAttribute('defer-hydration');
expect(hydrate).toHaveBeenCalledOnceWith();
hydrate.calls.reset();

// Should not re-hydrate when connected to the DOM.
document.body.appendChild(el);
expect(hydrate).not.toHaveBeenCalled();
}));

it('does not hydrate when imperatively connected with `defer-hydration`', () => {
const hydrate = jasmine.createSpy<HydrateLifecycle>('hydrate');
component('imperative-connect', hydrate);

const el = document.createElement('imperative-connect');
el.setAttribute('defer-hydration', '');
expect(hydrate).not.toHaveBeenCalled();

// Since the element has `defer-hydration` set, this should *not*
// trigger hydration.
document.body.appendChild(el);
expect(hydrate).not.toHaveBeenCalled();
});
});
});
});
13 changes: 13 additions & 0 deletions src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,22 @@ abstract class HydroActiveComponent extends HTMLElement {
this.#requestHydration();
}

// Trigger hydration when the `defer-hydration` attribute is removed.
static get observedAttributes(): string[] { return ['defer-hydration']; }
attributeChangedCallback(
name: string,
_oldValue: string | null,
newValue: string | null,
): void {
if (name === 'defer-hydration' && newValue === null) {
this.#requestHydration();
}
}

/** Hydrates the component if not already hydrated. Otherwise does nothing. */
#requestHydration(): void {
if (this.#hydrated) return;
if (this.hasAttribute('defer-hydration')) return;

this.#hydrated = true;
this.hydrate();
Expand Down

0 comments on commit f432fdb

Please sign in to comment.