Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion assets/js/phoenix_live_view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,4 +350,59 @@ function createHook(el: HTMLElement, callbacks: Hook): ViewHook {
return hook;
}

export { LiveSocket, isUsedInput, createHook, ViewHook, Hook, HooksOptions };
type HookCallbacks = Pick<
Hook,
| "mounted"
| "beforeUpdate"
| "updated"
| "destroyed"
| "disconnected"
| "reconnected"
>;

// Interface for elements with connectedCallback (web components)
interface CustomElementLike extends HTMLElement {
connectedCallback?(): void;
}

function HookedWebComponent<
T extends new (...args: any[]) => CustomElementLike,
>(Base: T) {
return class extends Base implements HookCallbacks {
hook!: ViewHook;

// Lifecycle callbacks - override in subclass
mounted?(): void;
beforeUpdate?(): void;
updated?(): void;
destroyed?(): void;
disconnected?(): void;
reconnected?(): void;

connectedCallback() {
super.connectedCallback?.();
// Only include callbacks that are actually defined, so ViewHook's
// default implementations aren't overwritten with undefined
const callbacks: Hook = {};
if (this.mounted) callbacks.mounted = this.mounted.bind(this);
if (this.beforeUpdate)
callbacks.beforeUpdate = this.beforeUpdate.bind(this);
if (this.updated) callbacks.updated = this.updated.bind(this);
if (this.destroyed) callbacks.destroyed = this.destroyed.bind(this);
if (this.disconnected)
callbacks.disconnected = this.disconnected.bind(this);
if (this.reconnected) callbacks.reconnected = this.reconnected.bind(this);
this.hook = createHook(this, callbacks);
}
};
}

export {
LiveSocket,
isUsedInput,
createHook,
HookedWebComponent,
ViewHook,
Hook,
HooksOptions,
};
40 changes: 40 additions & 0 deletions assets/test/hook_types_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import type { Hook, HooksOptions } from "phoenix_live_view/view_hook";
import { HookedWebComponent } from "phoenix_live_view";

// Hook with custom state
interface CounterState {
Expand Down Expand Up @@ -121,6 +122,45 @@ const InfiniteScroll: Hook = {

export { InfiniteScroll };

// =============================================================================
// Test for HookedWebComponent mixin
// Verifies that web components can use hook functionality with proper typing
// =============================================================================

class MyCounter extends HookedWebComponent(HTMLElement) {
count = 0;

mounted() {
// this.hook should be typed as ViewHook with all its methods
this.hook.handleEvent("set_count", (payload: { count: number }) => {
this.count = payload.count;
});

// pushEvent should be available
this.hook.pushEvent("ready", { id: this.id });

// pushEventTo should be available
this.hook.pushEventTo("#target", "init", {});

// js() should be available
const js = this.hook.js();
js.show(this);

// liveSocket should be accessible
console.log(this.hook.liveSocket);
}

updated() {
// updated callback works
console.log("updated", this.count);
}
}

// Verify the class can be used as a custom element
customElements.define("my-counter", MyCounter);

export { MyCounter };

// This file is primarily for compile-time type checking via `npm run typecheck:tests`.
// The dummy test below satisfies Jest's requirement for at least one test.
test("hook types compile correctly", () => {
Expand Down
Loading