Skip to content
Merged
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
5 changes: 5 additions & 0 deletions assets/js/phoenix_live_view/live_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ export default class LiveSocket {
this.connect();
}

/**
* @param {HTMLElement} el
* @param {import("./js_commands").EncodedJS} encodedJS
* @param {string | null} [eventType]
*/
execJS(el, encodedJS, eventType = null) {
const e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
this.owner(el, (view) => JS.exec(e, eventType, encodedJS, view, el));
Expand Down
86 changes: 62 additions & 24 deletions assets/js/phoenix_live_view/view_hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export interface HookInterface<E extends HTMLElement = HTMLElement> {

// based on https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fac1aa75acdddbf4f1a95e98ee2297b54ce4b4c9/types/phoenix_live_view/hooks.d.ts#L26
// licensed under MIT
export interface Hook<out T = object, E extends HTMLElement = HTMLElement> {
export interface Hook<T = object, E extends HTMLElement = HTMLElement> {
/**
* The mounted callback.
*
Expand Down Expand Up @@ -240,11 +240,15 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
implements HookInterface<E>
{
el: E;
liveSocket: LiveSocket;

private __listeners: Set<CallbackRef>;
private __isDisconnected: boolean;
private __view: () => View;
private __view!: () => View;
private __liveSocket!: () => LiveSocket;

get liveSocket(): LiveSocket {
return this.__liveSocket();
}

static makeID() {
return viewHookID++;
Expand Down Expand Up @@ -326,14 +330,18 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
__attachView(view: View | null) {
if (view) {
this.__view = () => view;
this.liveSocket = view.liveSocket;
this.__liveSocket = () => view.liveSocket;
} else {
this.__view = () => {
throw new Error(
`hook not yet attached to a live view: ${this.el.outerHTML}`,
);
};
this.liveSocket = null;
this.__liveSocket = () => {
throw new Error(
`hook not yet attached to a live view: ${this.el.outerHTML}`,
);
};
}
}

Expand Down Expand Up @@ -386,43 +394,70 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
};
}

pushEvent(event: string, payload?: any, onReply?: OnReply) {
pushEvent(event: string, payload: any, onReply: OnReply): void;
pushEvent(event: string, payload?: any): Promise<any>;
pushEvent(
event: string,
payload?: any,
onReply?: OnReply,
): Promise<any> | void {
const promise = this.__view().pushHookEvent(
this.el,
null,
event,
payload || {},
);
if (onReply === undefined) {
return promise.then(({ reply }) => reply);
return promise.then(({ reply }: { reply: any }) => reply);
}
promise.then(({ reply, ref }) => onReply(reply, ref)).catch(() => {});
return;
promise
.then(({ reply, ref }: { reply: any; ref: number }) =>
onReply(reply, ref),
)
.catch(() => {});
}

pushEventTo(
selectorOrTarget: PhxTarget,
event: string,
payload: object,
onReply: OnReply,
): void;
pushEventTo(
selectorOrTarget: PhxTarget,
event: string,
payload?: object,
): Promise<PromiseSettledResult<{ reply: any; ref: number }>[]>;
pushEventTo(
selectorOrTarget: PhxTarget,
event: string,
payload?: object,
onReply?: OnReply,
) {
): Promise<PromiseSettledResult<{ reply: any; ref: number }>[]> | void {
if (onReply === undefined) {
const targetPair: { view: View; targetCtx: any }[] = [];
this.__view().withinTargets(selectorOrTarget, (view, targetCtx) => {
targetPair.push({ view, targetCtx });
});
this.__view().withinTargets(
selectorOrTarget,
(view: View, targetCtx: any) => {
targetPair.push({ view, targetCtx });
},
);
const promises = targetPair.map(({ view, targetCtx }) => {
return view.pushHookEvent(this.el, targetCtx, event, payload || {});
});
return Promise.allSettled(promises);
}
this.__view().withinTargets(selectorOrTarget, (view, targetCtx) => {
view
.pushHookEvent(this.el, targetCtx, event, payload || {})
.then(({ reply, ref }) => onReply(reply, ref))
.catch(() => {});
});
return;
this.__view().withinTargets(
selectorOrTarget,
(view: View, targetCtx: any) => {
view
.pushHookEvent(this.el, targetCtx, event, payload || {})
.then(({ reply, ref }: { reply: any; ref: number }) =>
onReply(reply, ref),
)
.catch(() => {});
},
);
}

handleEvent(event: string, callback: (payload: any) => any): CallbackRef {
Expand Down Expand Up @@ -451,9 +486,12 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
}

uploadTo(selectorOrTarget: PhxTarget, name: string, files: FileList): any {
return this.__view().withinTargets(selectorOrTarget, (view, targetCtx) => {
view.dispatchUploads(targetCtx, name, files);
});
return this.__view().withinTargets(
selectorOrTarget,
(view: View, targetCtx: any) => {
view.dispatchUploads(targetCtx, name, files);
},
);
}

/** @internal */
Expand All @@ -464,6 +502,6 @@ export class ViewHook<E extends HTMLElement = HTMLElement>
}
}

export type HooksOptions = Record<string, typeof ViewHook | Hook>;
export type HooksOptions = Record<string, typeof ViewHook | Hook<any, any>>;

export default ViewHook;
28 changes: 14 additions & 14 deletions assets/test/debounce_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe("debounce", function () {
let calls = 0;
const el: HTMLInputElement = container().querySelector(
"input[name=debounce-200]",
);
)!;

el.addEventListener("input", (e) => {
DOM.debounce(
Expand Down Expand Up @@ -90,7 +90,7 @@ describe("debounce", function () {
let calls = 0;
const el: HTMLInputElement = container().querySelector(
"input[name=debounce-200]",
);
)!;

el.addEventListener("input", (e) => {
DOM.debounce(
Expand Down Expand Up @@ -122,7 +122,7 @@ describe("debounce", function () {
let calls = 0;
const el: HTMLInputElement = container().querySelector(
"input[name=debounce-200]",
);
)!;

el.addEventListener("input", (e) => {
DOM.debounce(
Expand Down Expand Up @@ -164,7 +164,7 @@ describe("debounce", function () {
let calls = 0;
const el: HTMLInputElement = container().querySelector(
"input[name=debounce-200]",
);
)!;
el.setAttribute("phx-debounce", "");

el.addEventListener("input", (e) => {
Expand Down Expand Up @@ -201,7 +201,7 @@ describe("debounce", function () {
const parent = container();
const el: HTMLInputElement = parent.querySelector(
"input[name=debounce-200]",
);
)!;

el.addEventListener("input", (e) => {
DOM.debounce(
Expand All @@ -215,7 +215,7 @@ describe("debounce", function () {
() => calls++,
);
});
el.form.addEventListener("submit", () => {
el.form!.addEventListener("submit", () => {
el.value = "submitted";
});
simulateInput(el, "changed");
Expand All @@ -236,7 +236,7 @@ describe("debounce", function () {
describe("throttle", function () {
test("triggers immediately, then on timeout", (done) => {
let calls = 0;
const el: HTMLButtonElement = container().querySelector("#throttle-200");
const el: HTMLButtonElement = container().querySelector("#throttle-200")!;

el.addEventListener("click", (e) => {
DOM.debounce(
Expand Down Expand Up @@ -274,7 +274,7 @@ describe("throttle", function () {

test("uses default when value is blank", (done) => {
let calls = 0;
const el: HTMLButtonElement = container().querySelector("#throttle-200");
const el: HTMLButtonElement = container().querySelector("#throttle-200")!;
el.setAttribute("phx-throttle", "");

el.addEventListener("click", (e) => {
Expand Down Expand Up @@ -315,7 +315,7 @@ describe("throttle", function () {
let calls = 0;
const el: HTMLInputElement = container().querySelector(
"input[name=throttle-200]",
);
)!;

el.addEventListener("input", (e) => {
DOM.debounce(
Expand All @@ -329,7 +329,7 @@ describe("throttle", function () {
() => calls++,
);
});
el.form.addEventListener("submit", () => {
el.form!.addEventListener("submit", () => {
el.value = "submitted";
});
simulateInput(el, "changed");
Expand All @@ -347,7 +347,7 @@ describe("throttle", function () {

test("triggers only once when there is only one event", (done) => {
let calls = 0;
const el: HTMLButtonElement = container().querySelector("#throttle-200");
const el: HTMLButtonElement = container().querySelector("#throttle-200")!;

el.addEventListener("click", (e) => {
DOM.debounce(
Expand Down Expand Up @@ -377,7 +377,7 @@ describe("throttle", function () {
let calls = 0;
const el: HTMLInputElement = container().querySelector(
"input[name=throttle-range-with-blur]",
);
)!;

el.addEventListener("input", (e) => {
DOM.debounce(
Expand Down Expand Up @@ -422,7 +422,7 @@ describe("throttle", function () {
describe("throttle keydown", function () {
test("when the same key is pressed triggers immediately, then on timeout", (done) => {
const keyPresses = {};
const el: HTMLDivElement = container().querySelector("#throttle-keydown");
const el: HTMLDivElement = container().querySelector("#throttle-keydown")!;

el.addEventListener("keydown", (e) => {
DOM.debounce(
Expand Down Expand Up @@ -457,7 +457,7 @@ describe("throttle keydown", function () {

test("when different key is pressed triggers immediately", (done) => {
const keyPresses = {};
const el: HTMLDivElement = container().querySelector("#throttle-keydown");
const el: HTMLDivElement = container().querySelector("#throttle-keydown")!;

el.addEventListener("keydown", (e) => {
DOM.debounce(
Expand Down
21 changes: 11 additions & 10 deletions assets/test/event_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import LiveSocket from "phoenix_live_view/live_socket";
import View from "phoenix_live_view/view";

import { version as liveview_version } from "../../package.json";
import { HooksOptions } from "phoenix_live_viewview_hook";

let containerId = 0;

Expand Down Expand Up @@ -60,7 +61,7 @@ describe("events", () => {

test("events on join", () => {
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
hooks: <HooksOptions>{
Map: {
mounted() {
this.handleEvent("points", (data) =>
Expand All @@ -86,7 +87,7 @@ describe("events", () => {

test("events on update", () => {
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
hooks: <HooksOptions>{
Game: {
mounted() {
this.handleEvent("scores", (data) =>
Expand Down Expand Up @@ -114,9 +115,9 @@ describe("events", () => {
});

test("events handlers are cleaned up on destroy", () => {
let destroyed = [];
let destroyed: Array<string> = [];
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
hooks: <HooksOptions>{
Handler: {
mounted() {
this.handleEvent("my-event", (data) =>
Expand Down Expand Up @@ -164,7 +165,7 @@ describe("events", () => {

test("removeHandleEvent", () => {
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
hooks: <HooksOptions>{
Remove: {
mounted() {
let ref = this.handleEvent("remove", (data) => {
Expand Down Expand Up @@ -202,7 +203,7 @@ describe("pushEvent replies", () => {
test("reply", (done) => {
let view;
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
hooks: <HooksOptions>{
Gateway: {
mounted() {
stubNextChannelReply(view, { transactionID: "1001" });
Expand Down Expand Up @@ -240,7 +241,7 @@ describe("pushEvent replies", () => {
test("promise", (done) => {
let view;
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
hooks: <HooksOptions>{
Gateway: {
mounted() {
stubNextChannelReply(view, { transactionID: "1001" });
Expand Down Expand Up @@ -276,7 +277,7 @@ describe("pushEvent replies", () => {
test("rejects with error", (done) => {
let view;
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
hooks: <HooksOptions>{
Gateway: {
mounted() {
stubNextChannelReplyWithError(view, "error");
Expand Down Expand Up @@ -305,7 +306,7 @@ describe("pushEvent replies", () => {
test("pushEventTo - promise with multiple targets", (done) => {
let view;
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
hooks: <HooksOptions>{
Gateway: {
mounted() {
stubNextChannelReply(view, { transactionID: "1001" });
Expand Down Expand Up @@ -350,7 +351,7 @@ describe("pushEvent replies", () => {
let view;
const spy = jest.fn();
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {
hooks: <HooksOptions>{
Gateway: {
mounted() {
stubNextChannelReply(view, { transactionID: "1001" });
Expand Down
Loading
Loading