@benqoder/beam 0.3.0 → 0.4.1

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.
package/README.md CHANGED
@@ -125,6 +125,42 @@ export function greet(c) {
125
125
  <div id="greeting"></div>
126
126
  ```
127
127
 
128
+ ### Including Input Values
129
+
130
+ Use `beam-include` to collect values from input elements and include them in action params. Elements are found by `beam-id`, `id`, or `name` (in that priority order):
131
+
132
+ ```html
133
+ <!-- Define inputs with beam-id, id, or name -->
134
+ <input beam-id="name" type="text" value="Ben"/>
135
+ <input id="email" type="email" value="ben@example.com"/>
136
+ <input name="age" type="number" value="30"/>
137
+ <input beam-id="subscribe" type="checkbox" checked/>
138
+
139
+ <!-- Button includes specific inputs -->
140
+ <button
141
+ beam-action="saveUser"
142
+ beam-include="name,email,age,subscribe"
143
+ beam-data-source="form"
144
+ beam-target="#result"
145
+ >Save</button>
146
+ ```
147
+
148
+ The action receives merged params with proper type conversion:
149
+ ```json
150
+ {
151
+ "source": "form",
152
+ "name": "Ben",
153
+ "email": "ben@example.com",
154
+ "age": 30,
155
+ "subscribe": true
156
+ }
157
+ ```
158
+
159
+ Type conversion:
160
+ - `checkbox` → `boolean` (checked state)
161
+ - `number`/`range` → `number`
162
+ - All others → `string`
163
+
128
164
  ### Modals
129
165
 
130
166
  Two ways to open modals:
@@ -308,6 +344,7 @@ Async components are awaited automatically - no manual `Promise.resolve()` or he
308
344
  | `beam-action` | Action name to call | `beam-action="increment"` |
309
345
  | `beam-target` | CSS selector for where to render response | `beam-target="#counter"` |
310
346
  | `beam-data-*` | Pass data to the action | `beam-data-id="123"` |
347
+ | `beam-include` | Include values from inputs by beam-id, id, or name | `beam-include="name,email,age"` |
311
348
  | `beam-swap` | How to swap content: `morph`, `append`, `prepend`, `replace` | `beam-swap="append"` |
312
349
  | `beam-confirm` | Show confirmation dialog before action | `beam-confirm="Delete this item?"` |
313
350
  | `beam-confirm-prompt` | Require typing text to confirm | `beam-confirm-prompt="Type DELETE\|DELETE"` |
@@ -373,7 +410,7 @@ return ctx.drawer(render(<MyDrawer />), { position: 'left', size: 'medium' })
373
410
  | `beam-watch-if` | Condition that must be true to trigger | `beam-watch-if="value.length >= 3"` |
374
411
  | `beam-cast` | Cast input value: `number`, `integer`, `boolean`, `trim` | `beam-cast="number"` |
375
412
  | `beam-loading-class` | Add class to input while request is in progress | `beam-loading-class="loading"` |
376
- | `beam-keep` | Preserve input focus and cursor position after morph | `beam-keep` |
413
+ | `beam-keep` | Prevent element from being morphed during updates | `beam-keep` |
377
414
 
378
415
  ### Dirty Form Tracking
379
416
 
@@ -746,9 +783,9 @@ Add a class to the input while the request is in progress:
746
783
  </style>
747
784
  ```
748
785
 
749
- ### Preserving Focus
786
+ ### Preventing Element Replacement
750
787
 
751
- Use `beam-keep` to preserve focus and cursor position after the response morphs the DOM:
788
+ Use `beam-keep` to prevent an element from being morphed/replaced during updates. This keeps the element exactly as-is, preserving its state (focus, value, etc.):
752
789
 
753
790
  ```html
754
791
  <input
@@ -760,6 +797,8 @@ Use `beam-keep` to preserve focus and cursor position after the response morphs
760
797
  />
761
798
  ```
762
799
 
800
+ Since the input isn't replaced, focus and cursor position are naturally preserved.
801
+
763
802
  ### Auto-Save on Blur
764
803
 
765
804
  Trigger action when the user leaves the field:
package/dist/client.d.ts CHANGED
@@ -31,6 +31,8 @@ interface CallOptions {
31
31
  swap?: string;
32
32
  }
33
33
  declare function clearScrollState(actionOrAll?: string | boolean): void;
34
+ declare function checkWsConnected(): boolean;
35
+ declare function manualReconnect(): Promise<BeamServerStub>;
34
36
  declare const beamUtils: {
35
37
  showToast: typeof showToast;
36
38
  closeModal: typeof closeModal;
@@ -38,6 +40,8 @@ declare const beamUtils: {
38
40
  clearCache: typeof clearCache;
39
41
  clearScrollState: typeof clearScrollState;
40
42
  isOnline: () => boolean;
43
+ isConnected: typeof checkWsConnected;
44
+ reconnect: typeof manualReconnect;
41
45
  getSession: () => Promise<BeamServerStub>;
42
46
  };
43
47
  type ActionCaller = (data?: Record<string, unknown>, options?: string | CallOptions) => Promise<ActionResponse>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AA8B9D,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAClE,MAAM,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACvF;AAGD,UAAU,UAAU;IAClB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC7E,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClF;AAQD,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AA82BzC,iBAAS,UAAU,IAAI,IAAI,CAU1B;AAkCD,iBAAS,WAAW,IAAI,IAAI,CAU3B;AAID,iBAAS,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,SAAS,GAAG,OAAmB,GAAG,IAAI,CAsB/E;AAkrBD,iBAAS,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAUzC;AAmlCD,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAMD,iBAAS,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0B9D;AAGD,QAAA,MAAM,SAAS;;;;;;;sBAlpFO,OAAO,CAAC,cAAc,CAAC;CA0pF5C,CAAA;AAGD,KAAK,YAAY,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;AAE/G,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,IAAI,EAAE,OAAO,SAAS,GAAG;YACvB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAA;SAC/B,CAAA;KACF;CACF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AA8B9D,UAAU,cAAc;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAClE,MAAM,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACvF;AAGD,UAAU,UAAU;IAClB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC7E,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClF;AAQD,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAq8BzC,iBAAS,UAAU,IAAI,IAAI,CAU1B;AAkCD,iBAAS,WAAW,IAAI,IAAI,CAU3B;AAID,iBAAS,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,SAAS,GAAG,OAAmB,GAAG,IAAI,CAsB/E;AAkrBD,iBAAS,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAUzC;AAikCD,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAMD,iBAAS,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0B9D;AAGD,iBAAS,gBAAgB,IAAI,OAAO,CAEnC;AAED,iBAAS,eAAe,IAAI,OAAO,CAAC,cAAc,CAAC,CAGlD;AAED,QAAA,MAAM,SAAS;;;;;;;;;sBA1qFO,OAAO,CAAC,cAAc,CAAC;CAorF5C,CAAA;AAGD,KAAK,YAAY,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;AAE/G,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,IAAI,EAAE,OAAO,SAAS,GAAG;YACvB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAA;SAC/B,CAAA;KACF;CACF"}
package/dist/client.js CHANGED
@@ -27,6 +27,10 @@ function getAuthToken() {
27
27
  let isOnline = navigator.onLine;
28
28
  let rpcSession = null;
29
29
  let connectingPromise = null;
30
+ let wsConnected = false;
31
+ let reconnectAttempts = 0;
32
+ const MAX_RECONNECT_ATTEMPTS = 5;
33
+ const RECONNECT_DELAY_BASE = 1000;
30
34
  // Client callback handler for server-initiated updates
31
35
  function handleServerEvent(event, data) {
32
36
  // Dispatch custom event for app to handle
@@ -42,6 +46,43 @@ function handleServerEvent(event, data) {
42
46
  window.dispatchEvent(new CustomEvent('beam:refresh', { detail: { selector } }));
43
47
  }
44
48
  }
49
+ // Handle WebSocket disconnection
50
+ function handleWsDisconnect(error) {
51
+ console.warn('[beam] WebSocket disconnected:', error);
52
+ wsConnected = false;
53
+ rpcSession = null;
54
+ connectingPromise = null;
55
+ // Dispatch event for app to handle
56
+ window.dispatchEvent(new CustomEvent('beam:disconnected', { detail: { error } }));
57
+ document.body.classList.add('beam-disconnected');
58
+ // Show any disconnect indicators
59
+ document.querySelectorAll('[beam-disconnected]').forEach((el) => {
60
+ el.style.display = '';
61
+ });
62
+ // Auto-reconnect with exponential backoff
63
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
64
+ const delay = RECONNECT_DELAY_BASE * Math.pow(2, reconnectAttempts);
65
+ reconnectAttempts++;
66
+ console.log(`[beam] Reconnecting in ${delay}ms (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
67
+ setTimeout(() => {
68
+ connect().then(() => {
69
+ console.log('[beam] Reconnected');
70
+ document.body.classList.remove('beam-disconnected');
71
+ document.querySelectorAll('[beam-disconnected]').forEach((el) => {
72
+ el.style.display = 'none';
73
+ });
74
+ window.dispatchEvent(new CustomEvent('beam:reconnected'));
75
+ }).catch((err) => {
76
+ console.error('[beam] Reconnect failed:', err);
77
+ });
78
+ }, delay);
79
+ }
80
+ else {
81
+ console.error('[beam] Max reconnect attempts reached');
82
+ showToast('Connection lost. Please refresh the page.', 'error');
83
+ window.dispatchEvent(new CustomEvent('beam:reconnect-failed'));
84
+ }
85
+ }
45
86
  function connect() {
46
87
  if (connectingPromise) {
47
88
  return connectingPromise;
@@ -70,8 +111,16 @@ function connect() {
70
111
  authenticatedSession.registerCallback?.(handleServerEvent)?.catch?.(() => {
71
112
  // Server may not support callbacks, that's ok
72
113
  });
114
+ // Handle connection broken (WebSocket disconnect)
115
+ // @ts-ignore - onRpcBroken is available on capnweb stubs
116
+ if (typeof authenticatedSession.onRpcBroken === 'function') {
117
+ authenticatedSession.onRpcBroken(handleWsDisconnect);
118
+ }
73
119
  rpcSession = authenticatedSession;
74
120
  connectingPromise = null;
121
+ wsConnected = true;
122
+ reconnectAttempts = 0;
123
+ window.dispatchEvent(new CustomEvent('beam:connected'));
75
124
  return authenticatedSession;
76
125
  }
77
126
  catch (err) {
@@ -174,8 +223,42 @@ function getParams(el) {
174
223
  }
175
224
  }
176
225
  }
226
+ // Handle beam-include: collect values from referenced inputs
227
+ const includeAttr = el.getAttribute('beam-include');
228
+ if (includeAttr) {
229
+ const ids = includeAttr.split(',').map(id => id.trim());
230
+ for (const id of ids) {
231
+ // Find element by beam-id, id, or name (priority order)
232
+ const inputEl = document.querySelector(`[beam-id="${id}"]`) ||
233
+ document.getElementById(id) ||
234
+ document.querySelector(`[name="${id}"]`);
235
+ if (inputEl) {
236
+ params[id] = getIncludedInputValue(inputEl);
237
+ }
238
+ }
239
+ }
177
240
  return params;
178
241
  }
242
+ // Get value from an included input element with proper type conversion
243
+ function getIncludedInputValue(el) {
244
+ if (el.tagName === 'INPUT') {
245
+ const input = el;
246
+ if (input.type === 'checkbox')
247
+ return input.checked;
248
+ if (input.type === 'radio')
249
+ return input.checked ? input.value : '';
250
+ if (input.type === 'number' || input.type === 'range') {
251
+ const num = parseFloat(input.value);
252
+ return isNaN(num) ? 0 : num;
253
+ }
254
+ return input.value;
255
+ }
256
+ if (el.tagName === 'TEXTAREA')
257
+ return el.value;
258
+ if (el.tagName === 'SELECT')
259
+ return el.value;
260
+ return '';
261
+ }
179
262
  // ============ CONFIRMATION DIALOGS ============
180
263
  // Usage: <button beam-action="delete" beam-confirm="Are you sure?">Delete</button>
181
264
  // Usage: <button beam-action="delete" beam-confirm.prompt="Type DELETE to confirm|DELETE">Delete</button>
@@ -1757,9 +1840,6 @@ function setupInputWatcher(el) {
1757
1840
  }
1758
1841
  }
1759
1842
  }
1760
- // Only restore focus for "input" events, not "change" (blur) events
1761
- const shouldRestoreFocus = htmlEl.hasAttribute('beam-keep') && eventType === 'input';
1762
- const activeElement = document.activeElement;
1763
1843
  // Add loading class if specified
1764
1844
  if (loadingClass)
1765
1845
  htmlEl.classList.add(loadingClass);
@@ -1808,19 +1888,6 @@ function setupInputWatcher(el) {
1808
1888
  if (response.script) {
1809
1889
  executeScript(response.script);
1810
1890
  }
1811
- // Restore focus if beam-keep is set and this was an input event (not change/blur)
1812
- if (shouldRestoreFocus && activeElement instanceof HTMLElement) {
1813
- const newEl = document.querySelector(`[name="${name}"]`);
1814
- if (newEl && newEl !== activeElement) {
1815
- newEl.focus();
1816
- if (newEl instanceof HTMLInputElement || newEl instanceof HTMLTextAreaElement) {
1817
- const cursorPos = activeElement.selectionStart;
1818
- if (cursorPos !== null) {
1819
- newEl.setSelectionRange(cursorPos, cursorPos);
1820
- }
1821
- }
1822
- }
1823
- }
1824
1891
  }
1825
1892
  catch (err) {
1826
1893
  console.error('Input watcher error:', err);
@@ -2482,6 +2549,13 @@ function clearScrollState(actionOrAll) {
2482
2549
  }
2483
2550
  }
2484
2551
  // Base utilities that are always available on window.beam
2552
+ function checkWsConnected() {
2553
+ return wsConnected;
2554
+ }
2555
+ function manualReconnect() {
2556
+ reconnectAttempts = 0;
2557
+ return connect();
2558
+ }
2485
2559
  const beamUtils = {
2486
2560
  showToast,
2487
2561
  closeModal,
@@ -2489,6 +2563,8 @@ const beamUtils = {
2489
2563
  clearCache,
2490
2564
  clearScrollState,
2491
2565
  isOnline: () => isOnline,
2566
+ isConnected: checkWsConnected,
2567
+ reconnect: manualReconnect,
2492
2568
  getSession: api.getSession,
2493
2569
  };
2494
2570
  // Create a Proxy that handles both utility methods and dynamic action calls
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benqoder/beam",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",