Skip to content

Commit

Permalink
Adds component function.
Browse files Browse the repository at this point in the history
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
dgp1130 committed Dec 3, 2023
1 parent 6cb9071 commit 6b0e172
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 1 deletion.
12 changes: 12 additions & 0 deletions src/component.test.html
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>
64 changes: 64 additions & 0 deletions src/component.test.ts
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();
});
});
});
46 changes: 46 additions & 0 deletions src/component.ts
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 };
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export {};
export { HydrateLifecycle, component } from './component.js';

0 comments on commit 6b0e172

Please sign in to comment.