-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This defines a component with the given name and invokes the given callback at hydration time. For now this just specifies the timing of the `hydrate` function, not much else of use is done.
- Loading branch information
Showing
4 changed files
with
123 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>`component` tests</title> | ||
<meta charset="utf8"> | ||
</head> | ||
<body> | ||
<template test-case="already-rendered"> | ||
<already-rendered></already-rendered> | ||
</template> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { testCase, useTestCases } from './testing/test-cases.js'; | ||
import { component, HydrateLifecycle } from './component.js'; | ||
|
||
describe('component', () => { | ||
useTestCases(); | ||
|
||
afterEach(() => { | ||
for (const child of Array.from(document.body.childNodes)) { | ||
child.remove(); | ||
} | ||
}); | ||
|
||
describe('component', () => { | ||
it('upgrades already rendered components', testCase('already-rendered', () => { | ||
const hydrate = jasmine.createSpy<HydrateLifecycle>('hydrate'); | ||
component('already-rendered', hydrate); | ||
|
||
expect(hydrate).toHaveBeenCalledOnceWith(); | ||
})); | ||
|
||
it('upgrades components rendered after definition', () => { | ||
const hydrate = jasmine.createSpy<HydrateLifecycle>('hydrate'); | ||
|
||
component('new-component', hydrate); | ||
expect(hydrate).not.toHaveBeenCalled(); | ||
|
||
const comp = document.createElement('new-component'); | ||
expect(hydrate).not.toHaveBeenCalled(); | ||
|
||
document.body.appendChild(comp); | ||
expect(hydrate).toHaveBeenCalledOnceWith(); | ||
}); | ||
|
||
it('does not hydrate a second time when moved in the DOM', () => { | ||
const hydrate = jasmine.createSpy<HydrateLifecycle>('hydrate'); | ||
component('another-component', hydrate); | ||
|
||
const comp = document.createElement('another-component'); | ||
document.body.appendChild(comp); | ||
expect(hydrate).toHaveBeenCalledOnceWith(); | ||
hydrate.calls.reset(); | ||
|
||
comp.remove(); | ||
document.body.appendChild(comp); | ||
expect(hydrate).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('invokes hydrate callback without a `this` value', () => { | ||
// Can't use Jasmine spies here because they will default `this` to `window` | ||
// because they are run in "sloppy mode". | ||
let self: unknown = 'defined' /* initial value other than undefined */; | ||
function hydrate(this: unknown): void { | ||
self = this; | ||
} | ||
|
||
component('this-component', hydrate); | ||
|
||
const comp = document.createElement('this-component'); | ||
document.body.appendChild(comp); | ||
|
||
expect(self).toBeUndefined(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/** @fileoverview Defines symbols related to HydroActive components. */ | ||
|
||
/** The type of the lifecycle hook invoked when the component hydrates. */ | ||
export type HydrateLifecycle = () => void; | ||
|
||
/** | ||
* Defines a component of the given tag name with the provided hydration | ||
* callback. | ||
*/ | ||
export function component(tagName: string, hydrate: HydrateLifecycle): | ||
Class<HTMLElement> { | ||
const Component = class extends HydroActiveComponent { | ||
override hydrate = hydrate.bind(undefined /* strip `this` value */); | ||
} | ||
|
||
customElements.define(tagName, Component); | ||
|
||
return Component; | ||
} | ||
|
||
/** Abstract base class for all HydroActive components. */ | ||
abstract class HydroActiveComponent extends HTMLElement { | ||
/** Whether or not the component has been hydrated. */ | ||
#hydrated = false; | ||
|
||
/** User-defined lifecycle hook invoked on hydration. */ | ||
abstract hydrate(): void; | ||
|
||
connectedCallback(): void { | ||
this.#requestHydration(); | ||
} | ||
|
||
/** Hydrates the component if not already hydrated. Otherwise does nothing. */ | ||
#requestHydration(): void { | ||
if (this.#hydrated) return; | ||
|
||
this.#hydrated = true; | ||
this.hydrate(); | ||
} | ||
} | ||
|
||
/** | ||
* Analogous to `Class<T>` in Java. Represents the class object of the given | ||
* instance type. | ||
*/ | ||
type Class<Instance> = { new(): Instance }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export {}; | ||
export { HydrateLifecycle, component } from './component.js'; |