Skip to content

Commit

Permalink
WIP: ComponentRef
Browse files Browse the repository at this point in the history
  • Loading branch information
dgp1130 committed Dec 4, 2023
1 parent 48fd911 commit 676ae71
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 21 deletions.
63 changes: 63 additions & 0 deletions src/component-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/** @fileoverview TODO */

import { ElementRef } from './element-ref.js';
import { HydroActiveComponent } from './hydroactive-component.js';

/** TODO */
export class ComponentRef {
readonly #host: ElementRef<HydroActiveComponent>;
public get host(): ElementRef<HTMLElement> { return this.#host; }

readonly #connectedCallbacks: Array<OnConnect> = [];
readonly #disconnectedCallbacks: Array<OnDisconnect> = [];

private constructor(host: ElementRef<HydroActiveComponent>) {
this.#host = host;
}

/** TODO */
public /* internal */ static _from(host: ElementRef<HydroActiveComponent>):
ComponentRef {
const ref = new ComponentRef(host);

ref.#host.native._registerLifecycleHooks({
onConnect: () => {
for (const onConnect of ref.#connectedCallbacks) {
ref.#invokeOnConnect(onConnect);
}
},

onDisconnect: () => {
for (const onDisconnect of ref.#disconnectedCallbacks) {
onDisconnect();
}

ref.#disconnectedCallbacks.splice(0, ref.#disconnectedCallbacks.length);
},
});

return ref;
}

/** TODO */
public connected(onConnect: OnConnect): void {
this.#connectedCallbacks.push(onConnect);

if (this.#host.native.isConnected) this.#invokeOnConnect(onConnect);
}

/**
* Invokes the given {@link OnConnect} handler and registers its disconnect
* callback if provided.
*/
#invokeOnConnect(onConnect: OnConnect): void {
const onDisconnect = onConnect();
if (onDisconnect) this.#disconnectedCallbacks.push(onDisconnect);
}
}

/** TODO */
export type OnConnect = () => OnDisconnect | void;

/** TODO */
export type OnDisconnect = () => void;
19 changes: 6 additions & 13 deletions src/component.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { component, HydrateLifecycle } from './component.js';
import { ComponentRef } from './component-ref.js';
import { ElementRef } from './element-ref.js';
import { HydroActiveComponent } from './hydroactive-component.js';
import { testCase, useTestCases } from './testing/test-cases.js';

describe('component', () => {
Expand Down Expand Up @@ -66,21 +68,12 @@ describe('component', () => {
const hydrate = jasmine.createSpy<HydrateLifecycle>('hydrate');
component('host-component', hydrate);

const comp = document.createElement('host-component');
const comp =
document.createElement('host-component') as HydroActiveComponent;
document.body.appendChild(comp);

expect(hydrate).toHaveBeenCalledOnceWith(ElementRef.from(comp));
});

it('invokes hydrate callback with an `ElementRef` typed to `HTMLElement`', () => {
// Type-only test, only needs to compile, not execute.
expect().nothing();
() => {
const Comp = component('host-type', (host) => {
// Host is assignable to an `ElementRef<HTMLElement>`.
const ref: ElementRef<HTMLElement> = host;
});
};
expect(hydrate).toHaveBeenCalledOnceWith(
ComponentRef._from(ElementRef.from(comp)));
});

it('sets the class name', () => {
Expand Down
5 changes: 3 additions & 2 deletions src/component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/** @fileoverview Defines symbols related to component definition. */

import { ComponentRef } from './component-ref.js';
import { ElementRef } from './element-ref.js';
import { HydroActiveComponent } from './hydroactive-component.js';

/** The type of the lifecycle hook invoked when the component hydrates. */
export type HydrateLifecycle = (host: ElementRef<HTMLElement>) => void;
export type HydrateLifecycle = (host: ComponentRef) => void;

/**
* Defines a component of the given tag name with the provided hydration
Expand All @@ -14,7 +15,7 @@ export function component(tagName: string, hydrate: HydrateLifecycle):
Class<HTMLElement> {
const Component = class extends HydroActiveComponent {
override hydrate(): void {
hydrate(ElementRef.from(this));
hydrate(ComponentRef._from(ElementRef.from(this)));
}
};

Expand Down
18 changes: 12 additions & 6 deletions src/demo/counter.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { component } from 'hydroactive';

/** Automatically increments the count over time. */
export const AutoCounter = component('auto-counter', (host) => {
const label = host.query('span')!;
export const AutoCounter = component('auto-counter', (comp) => {
const label = comp.host.query('span')!;
let count = Number(label.text);

setInterval(() => {
count++;
label.native.textContent = count.toString();
}, 1_000);
comp.connected(() => {
const id = setInterval(() => {
count++;
label.native.textContent = count.toString();
}, 1_000);

return () => {
clearInterval(id);
};
});
});
20 changes: 20 additions & 0 deletions src/hydroactive-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,33 @@ export abstract class HydroActiveComponent extends HTMLElement {
/** Whether or not the component has been hydrated. */
#hydrated = false;

readonly #connectListeners: Array<() => void> = [];
readonly #disconnectListeners: Array<() => void> = [];

/** User-defined lifecycle hook invoked on hydration. */
abstract hydrate(): void;

public /* internal */ _registerLifecycleHooks({ onConnect, onDisconnect }: {
onConnect?: () => void,
onDisconnect?: () => void,
}): void {
if (onConnect) this.#connectListeners.push(onConnect);
if (onDisconnect) this.#disconnectListeners.push(onDisconnect);
}

connectedCallback(): void {
// The "connect" event triggers _before_ the "hydrate" event when they
// happen simultaneously. Listeners should know to invoke connect callbacks
// discovered post-connection time, such as during hydration.
for (const listener of this.#connectListeners) listener();

this.#requestHydration();
}

disconnectedCallback(): void {
for (const listener of this.#disconnectListeners) listener();
}

// Trigger hydration when the `defer-hydration` attribute is removed.
static get observedAttributes(): string[] { return ['defer-hydration']; }
attributeChangedCallback(
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { HydrateLifecycle, component } from './component.js';
export { ComponentRef, OnDisconnect, OnConnect } from './component-ref.js';
export { ElementRef } from './element-ref.js';

0 comments on commit 676ae71

Please sign in to comment.