phoenix_live_view 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/assets/js/phoenix_live_view/dom.ts +3 -1
  2. package/assets/js/phoenix_live_view/dom_patch.ts +1 -1
  3. package/assets/js/phoenix_live_view/live_socket.ts +16 -9
  4. package/assets/js/phoenix_live_view/view.ts +6 -0
  5. package/assets/js/types/assets/js/phoenix_live_view/aria.d.ts +9 -0
  6. package/assets/js/types/assets/js/phoenix_live_view/browser.d.ts +20 -0
  7. package/assets/js/types/assets/js/phoenix_live_view/constants.d.ts +98 -0
  8. package/assets/js/types/assets/js/phoenix_live_view/dom.d.ts +82 -0
  9. package/assets/js/types/assets/js/phoenix_live_view/dom_patch.d.ts +65 -0
  10. package/assets/js/types/assets/js/phoenix_live_view/dom_post_morph_restorer.d.ts +8 -0
  11. package/assets/js/types/assets/js/phoenix_live_view/element_ref.d.ts +14 -0
  12. package/assets/js/types/assets/js/phoenix_live_view/entry_uploader.d.ts +16 -0
  13. package/assets/js/types/assets/js/phoenix_live_view/hooks.d.ts +3 -0
  14. package/assets/js/types/assets/js/phoenix_live_view/index.d.ts +48 -0
  15. package/assets/js/types/assets/js/phoenix_live_view/js.d.ts +99 -0
  16. package/assets/js/types/assets/js/phoenix_live_view/js_commands.d.ts +225 -0
  17. package/assets/js/types/assets/js/phoenix_live_view/live_socket.d.ts +315 -0
  18. package/assets/js/types/assets/js/phoenix_live_view/live_uploader.d.ts +29 -0
  19. package/assets/js/types/assets/js/phoenix_live_view/rendered.d.ts +50 -0
  20. package/assets/js/types/assets/js/phoenix_live_view/upload_entry.d.ts +42 -0
  21. package/assets/js/types/assets/js/phoenix_live_view/utils.d.ts +15 -0
  22. package/assets/js/types/assets/js/phoenix_live_view/view.d.ts +1 -0
  23. package/assets/js/types/assets/js/phoenix_live_view/view_hook.d.ts +279 -0
  24. package/package.json +2 -2
  25. package/priv/static/phoenix_live_view.cjs.js +16 -9
  26. package/priv/static/phoenix_live_view.cjs.js.map +2 -2
  27. package/priv/static/phoenix_live_view.esm.js +16 -9
  28. package/priv/static/phoenix_live_view.esm.js.map +2 -2
  29. package/priv/static/phoenix_live_view.js +16 -9
  30. package/priv/static/phoenix_live_view.min.js +4 -4
@@ -0,0 +1,279 @@
1
+ import { HookJSCommands } from "./js_commands";
2
+ import LiveSocket from "./live_socket";
3
+ export type OnReply = (reply: any, ref: number) => any;
4
+ export type CallbackRef = {
5
+ event: string;
6
+ callback: (payload: any) => any;
7
+ };
8
+ export type PhxTarget = string | number | HTMLElement;
9
+ /**
10
+ * Defines the lifecycle callbacks and custom methods for a LiveView hook.
11
+ *
12
+ * @category JavaScript Hooks
13
+ */
14
+ export interface HookInterface<E extends HTMLElement = HTMLElement> {
15
+ /**
16
+ * The DOM element that the hook is attached to.
17
+ */
18
+ el: E;
19
+ /**
20
+ * The LiveSocket instance that the hook is attached to.
21
+ */
22
+ liveSocket: LiveSocket;
23
+ /**
24
+ * The mounted callback.
25
+ *
26
+ * Called when the element has been added to the DOM and its server LiveView has finished mounting.
27
+ */
28
+ mounted?: () => void;
29
+ /**
30
+ * The beforeUpdate callback.
31
+ *
32
+ * Called when the element is about to be updated in the DOM.
33
+ * Note: any call here must be synchronous as the operation cannot be deferred or cancelled.
34
+ */
35
+ beforeUpdate?: () => void;
36
+ /**
37
+ * The updated callback.
38
+ *
39
+ * Called when the element has been updated in the DOM by the server.
40
+ */
41
+ updated?: () => void;
42
+ /**
43
+ * The destroyed callback.
44
+ *
45
+ * Called when the element has been removed from the page, either by a parent update, or by the parent being removed entirely.
46
+ */
47
+ destroyed?: () => void;
48
+ /**
49
+ * The disconnected callback.
50
+ *
51
+ * Called when the element's parent LiveView has disconnected from the server.
52
+ */
53
+ disconnected?: () => void;
54
+ /**
55
+ * The reconnected callback.
56
+ *
57
+ * Called when the element's parent LiveView has reconnected to the server.
58
+ */
59
+ reconnected?: () => void;
60
+ /**
61
+ * Returns an object with methods to manipulate the DOM and execute JavaScript.
62
+ * The applied changes integrate with server DOM patching.
63
+ */
64
+ js(): HookJSCommands;
65
+ /**
66
+ * Pushes an event to the server and invokes a callback with the server's reply.
67
+ *
68
+ * **Note:** this version silently ignores push errors.
69
+ * Use the {@link pushEvent | promise-returning version} to handle errors.
70
+ *
71
+ * @param event - The event name.
72
+ * @param payload - The payload to send to the server. Must be a serializable
73
+ * value (typically JSON-serializable, depends on the Socket configuration).
74
+ * Defaults to an empty object.
75
+ * @param onReply - A callback to handle the server's reply.
76
+ */
77
+ pushEvent(event: string, payload: unknown, onReply: OnReply): void;
78
+ /**
79
+ * Pushes an event to the server and returns a Promise that resolves with the server's reply.
80
+ *
81
+ * The promise will be rejected in case of errors
82
+ * such as a disconnected state, timeout, or the server rejecting the event.
83
+ *
84
+ * @param event - The event name.
85
+ * @param [payload] - The payload to send to the server. Must be a serializable
86
+ * value (typically JSON-serializable, depends on the Socket configuration).
87
+ * Defaults to an empty object.
88
+ * @returns A promise that fulfills or rejects with the server's reply.
89
+ */
90
+ pushEvent(event: string, payload?: unknown): Promise<any>;
91
+ /**
92
+ * Pushes a targeted event to the server and invokes a callback with the server's reply.
93
+ *
94
+ * It sends the event to the LiveComponent or LiveView the `selectorOrTarget` is defined in,
95
+ * where its value can be either a query selector, an actual DOM element, or a CID (component id)
96
+ * returned by the `@myself` assign.
97
+ *
98
+ * If the selector matches multiple elements, the event is sent to all of them,
99
+ * even if they belong to the same LiveComponent or LiveView.
100
+ *
101
+ * **Note:** this version silently ignores push errors.
102
+ * Use the {@link pushEventTo | promise-returning version} to handle errors.
103
+ *
104
+ * @param selectorOrTarget - The selector, element, or CID to target.
105
+ * @param event - The event name.
106
+ * @param payload - The payload to send to the server. Must be a serializable
107
+ * value (typically JSON-serializable, depends on the Socket configuration).
108
+ * Defaults to an empty object.
109
+ * @param onReply - A callback to handle the server's reply.
110
+ */
111
+ pushEventTo(selectorOrTarget: PhxTarget, event: string, payload: unknown, onReply: OnReply): void;
112
+ /**
113
+ * Pushes a targeted event to the server and returns a Promise that resolves with the server's reply.
114
+ *
115
+ * It sends the event to the LiveComponent or LiveView the `selectorOrTarget` is defined in,
116
+ * where its value can be either a query selector, an actual DOM element, or a CID (component id)
117
+ * returned by the `@myself` assign.
118
+ *
119
+ * If the selector matches multiple elements, the event is sent to all of them,
120
+ * even if they belong to the same LiveComponent or LiveView.
121
+ * Because of this, it returns a single promise that matches the return value of
122
+ * [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled#return_value).
123
+ * Individual fulfilled values are of the format `{ reply, ref }`, where `reply` is the server's reply.
124
+ *
125
+ * The individual promises will be rejected in case of errors
126
+ * such as a disconnected state, timeout, or the server rejecting the event.
127
+ *
128
+ * @param selectorOrTarget - The selector, element, or CID to target.
129
+ * @param event - The event name.
130
+ * @param [payload] - The payload to send to the server. Must be a serializable
131
+ * value (typically JSON-serializable, depends on the Socket configuration).
132
+ * Defaults to an empty object.
133
+ * @returns A promise that resolves when the event has been handled by all targets.
134
+ */
135
+ pushEventTo(selectorOrTarget: PhxTarget, event: string, payload?: unknown): Promise<PromiseSettledResult<{
136
+ reply: any;
137
+ ref: number;
138
+ }>[]>;
139
+ /**
140
+ * Allows to register a callback to be called when an event is received from the server.
141
+ *
142
+ * This is used to handle `pushEvent` calls from the server. The callback is called with the payload from the server.
143
+ *
144
+ * @param event - The event name.
145
+ * @param callback - The callback to call when the event is received.
146
+ *
147
+ * @returns A reference to the callback, which can be used in `removeHandleEvent` to remove the callback.
148
+ */
149
+ handleEvent(event: string, callback: (payload: any) => any): CallbackRef;
150
+ /**
151
+ * Removes a callback registered with `handleEvent`.
152
+ *
153
+ * @param ref - The reference to the callback to remove.
154
+ */
155
+ removeHandleEvent(ref: CallbackRef): void;
156
+ /**
157
+ * Allows to trigger a live file upload.
158
+ *
159
+ * @param name - The upload name corresponding to the `Phoenix.LiveView.allow_upload/3` call.
160
+ * @param files - The files to upload.
161
+ */
162
+ upload(name: any, files: any): any;
163
+ /**
164
+ * Allows to trigger a live file upload to a specific target.
165
+ *
166
+ * @param selectorOrTarget - The target to upload the files to.
167
+ * @param name - The upload name corresponding to the `Phoenix.LiveView.allow_upload/3` call.
168
+ * @param files - The files to upload.
169
+ */
170
+ uploadTo(selectorOrTarget: PhxTarget, name: any, files: any): any;
171
+ [key: PropertyKey]: any;
172
+ }
173
+ /**
174
+ * Defines the lifecycle callbacks and custom methods for a LiveView hook.
175
+ *
176
+ * @category JavaScript Hooks
177
+ */
178
+ export interface Hook<T = object, E extends HTMLElement = HTMLElement> {
179
+ /**
180
+ * The mounted callback.
181
+ *
182
+ * Called when the element has been added to the DOM and its server LiveView has finished mounting.
183
+ */
184
+ mounted?: (this: T & HookInterface<E>) => void;
185
+ /**
186
+ * The beforeUpdate callback.
187
+ *
188
+ * Called when the element is about to be updated in the DOM.
189
+ * Note: any call here must be synchronous as the operation cannot be deferred or cancelled.
190
+ */
191
+ beforeUpdate?: (this: T & HookInterface<E>) => void;
192
+ /**
193
+ * The updated callback.
194
+ *
195
+ * Called when the element has been updated in the DOM by the server.
196
+ */
197
+ updated?: (this: T & HookInterface<E>) => void;
198
+ /**
199
+ * The destroyed callback.
200
+ *
201
+ * Called when the element has been removed from the page, either by a parent update, or by the parent being removed entirely.
202
+ */
203
+ destroyed?: (this: T & HookInterface<E>) => void;
204
+ /**
205
+ * The disconnected callback.
206
+ *
207
+ * Called when the element's parent LiveView has disconnected from the server.
208
+ */
209
+ disconnected?: (this: T & HookInterface<E>) => void;
210
+ /**
211
+ * The reconnected callback.
212
+ *
213
+ * Called when the element's parent LiveView has reconnected to the server.
214
+ */
215
+ reconnected?: (this: T & HookInterface<E>) => void;
216
+ [key: PropertyKey]: any;
217
+ }
218
+ /**
219
+ * Base class for LiveView hooks. Users extend this class to define their hooks.
220
+ *
221
+ * Example:
222
+ * ```typescript
223
+ * class MyCustomHook extends ViewHook {
224
+ * myState = "initial";
225
+ *
226
+ * mounted() {
227
+ * console.log("Hook mounted on element:", this.el);
228
+ * this.el.addEventListener("click", () => {
229
+ * this.pushEvent("element-clicked", { state: this.myState });
230
+ * });
231
+ * }
232
+ *
233
+ * updated() {
234
+ * console.log("Hook updated", this.el.id);
235
+ * }
236
+ *
237
+ * myCustomMethod(someArg: string) {
238
+ * console.log("myCustomMethod called with:", someArg, "Current state:", this.myState);
239
+ * }
240
+ * }
241
+ * ```
242
+ *
243
+ * The `this` context within the hook methods (mounted, updated, custom methods, etc.)
244
+ * will refer to the hook instance, providing access to `this.el`, `this.liveSocket`,
245
+ * `this.pushEvent()`, etc., as well as any properties or methods defined on the subclass.
246
+ *
247
+ * @category JavaScript Hooks
248
+ */
249
+ export declare class ViewHook<E extends HTMLElement = HTMLElement> implements HookInterface<E> {
250
+ el: E;
251
+ private __listeners;
252
+ private __isDisconnected;
253
+ private __view;
254
+ private __liveSocket;
255
+ get liveSocket(): LiveSocket;
256
+ mounted(): void;
257
+ beforeUpdate(): void;
258
+ updated(): void;
259
+ destroyed(): void;
260
+ disconnected(): void;
261
+ reconnected(): void;
262
+ js(): HookJSCommands;
263
+ pushEvent(event: string, payload: unknown, onReply: OnReply): void;
264
+ pushEvent(event: string, payload?: unknown): Promise<any>;
265
+ pushEventTo(selectorOrTarget: PhxTarget, event: string, payload: unknown, onReply: OnReply): void;
266
+ pushEventTo(selectorOrTarget: PhxTarget, event: string, payload?: unknown): Promise<PromiseSettledResult<{
267
+ reply: any;
268
+ ref: number;
269
+ }>[]>;
270
+ handleEvent(event: string, callback: (payload: any) => any): CallbackRef;
271
+ removeHandleEvent(ref: CallbackRef): void;
272
+ upload(name: string, files: FileList): any;
273
+ uploadTo(selectorOrTarget: PhxTarget, name: string, files: FileList): any;
274
+ }
275
+ /**
276
+ * @category JavaScript Hooks
277
+ */
278
+ export type HooksOptions = Record<string, typeof ViewHook | Hook<any, any>>;
279
+ export default ViewHook;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "@babel/preset-env": "7.27.2",
38
38
  "@babel/preset-typescript": "^7.27.1",
39
39
  "@eslint/js": "^9.29.0",
40
- "@playwright/test": "^1.59.1",
40
+ "@playwright/test": "^1.60.0",
41
41
  "@types/jest": "^30.0.0",
42
42
  "@types/phoenix": "^1.6.6",
43
43
  "css.escape": "^1.5.1",
@@ -671,7 +671,9 @@ var DOM = {
671
671
  if (this.once(el, "bind-debounce")) {
672
672
  el.addEventListener("blur", () => {
673
673
  clearTimeout(this.private(el, THROTTLED));
674
- this.triggerCycle(el, DEBOUNCE_TRIGGER);
674
+ if (asyncFilter()) {
675
+ this.triggerCycle(el, DEBOUNCE_TRIGGER);
676
+ }
675
677
  });
676
678
  }
677
679
  }
@@ -2825,7 +2827,7 @@ var DOMPatch = class {
2825
2827
  transitionPendingRemoves() {
2826
2828
  const { pendingRemoves, liveSocket } = this;
2827
2829
  if (pendingRemoves.length > 0) {
2828
- liveSocket.transitionRemoves(pendingRemoves, () => {
2830
+ liveSocket.transitionRemoves(pendingRemoves, this.view, () => {
2829
2831
  pendingRemoves.forEach((el) => {
2830
2832
  const child = dom_default.firstPhxChild(el);
2831
2833
  if (child) {
@@ -4453,6 +4455,7 @@ var View = class _View {
4453
4455
  if (container) {
4454
4456
  const [tag, attrs] = container;
4455
4457
  this.el = dom_default.replaceRootContainer(this.el, tag, attrs);
4458
+ dom_default.putPrivate(this.el, "view", this);
4456
4459
  }
4457
4460
  this.childJoins = 0;
4458
4461
  this.joinPending = true;
@@ -4539,6 +4542,7 @@ var View = class _View {
4539
4542
  throw new Error("unable to find root element for view");
4540
4543
  }
4541
4544
  this.el = el;
4545
+ dom_default.putPrivate(this.el, "view", this);
4542
4546
  this.el.setAttribute(PHX_ROOT_ID, this.root.id);
4543
4547
  }
4544
4548
  // this is invoked for dead and live views, so we must filter by
@@ -4727,6 +4731,7 @@ var View = class _View {
4727
4731
  rootEl.setAttribute(PHX_SESSION, this.getSession());
4728
4732
  rootEl.setAttribute(PHX_STATIC, this.getStatic() ?? "");
4729
4733
  this.parent && rootEl.setAttribute(PHX_PARENT_ID, this.parent.id);
4734
+ dom_default.putPrivate(rootEl, "view", this);
4730
4735
  const formsToRecover = (
4731
4736
  // we go over all forms in the new DOM; because this is only the HTML for the current
4732
4737
  // view, we can be sure that all forms are owned by this view:
@@ -6089,7 +6094,7 @@ var LiveSocket = class {
6089
6094
  * Returns the version of the LiveView client.
6090
6095
  */
6091
6096
  version() {
6092
- return "1.2.0";
6097
+ return "1.2.2";
6093
6098
  }
6094
6099
  /**
6095
6100
  * Returns true if profiling is enabled. See {@link enableProfiling} and {@link disableProfiling}.
@@ -6442,11 +6447,12 @@ var LiveSocket = class {
6442
6447
  `[${this.binding("remove")}]`
6443
6448
  ).filter((el) => !dom_default.isChildOfAny(el, stickies));
6444
6449
  const newMainEl = dom_default.cloneNode(this.outgoingMainEl, "");
6445
- this.main.showLoader(this.loaderTimeout);
6446
- this.main.destroy();
6450
+ const oldMainView = this.main;
6451
+ oldMainView.showLoader(this.loaderTimeout);
6452
+ oldMainView.destroy();
6447
6453
  this.main = this.newRootView(newMainEl, flash, liveReferer);
6448
6454
  this.main.setRedirect(href);
6449
- this.transitionRemoves(removeEls);
6455
+ this.transitionRemoves(removeEls, oldMainView);
6450
6456
  this.main.join((joinCount, onDone) => {
6451
6457
  if (joinCount === 1 && this.commitPendingLink(linkRef)) {
6452
6458
  this.requestDOMUpdate(() => {
@@ -6461,7 +6467,7 @@ var LiveSocket = class {
6461
6467
  });
6462
6468
  }
6463
6469
  /** @internal */
6464
- transitionRemoves(elements, callback) {
6470
+ transitionRemoves(elements, view, callback) {
6465
6471
  const removeAttr = this.binding("remove");
6466
6472
  const silenceEvents = (e) => {
6467
6473
  e.preventDefault();
@@ -6471,7 +6477,8 @@ var LiveSocket = class {
6471
6477
  for (const event of this.boundEventNames) {
6472
6478
  el.addEventListener(event, silenceEvents, true);
6473
6479
  }
6474
- this.execJS(el, el.getAttribute(removeAttr), "remove");
6480
+ const e = new CustomEvent("phx:exec", { detail: { sourceElement: el } });
6481
+ js_default.exec(e, "remove", el.getAttribute(removeAttr), view, el);
6475
6482
  });
6476
6483
  this.requestDOMUpdate(() => {
6477
6484
  elements.forEach((el) => {
@@ -6497,7 +6504,7 @@ var LiveSocket = class {
6497
6504
  let view;
6498
6505
  const viewEl = dom_default.closestViewEl(childEl);
6499
6506
  if (viewEl) {
6500
- view = this.getViewByEl(viewEl);
6507
+ view = dom_default.private(viewEl, "view");
6501
6508
  } else {
6502
6509
  if (!childEl.isConnected) {
6503
6510
  return null;