@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 +42 -3
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +92 -16
- package/package.json +1 -1
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` |
|
|
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
|
-
###
|
|
786
|
+
### Preventing Element Replacement
|
|
750
787
|
|
|
751
|
-
Use `beam-keep` to
|
|
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>;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|