snice 4.13.0 → 4.14.0
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/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/accordion/snice-accordion.min.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/alert/snice-alert.min.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.min.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.min.js +1 -1
- package/dist/cdn/avatar/snice-avatar.js +1 -1
- package/dist/cdn/avatar/snice-avatar.min.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/badge/snice-badge.min.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/banner/snice-banner.min.js +1 -1
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/book/snice-book.min.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.min.js +1 -1
- package/dist/cdn/button/snice-button.js +1 -1
- package/dist/cdn/button/snice-button.min.js +1 -1
- package/dist/cdn/calendar/snice-calendar.js +1 -1
- package/dist/cdn/calendar/snice-calendar.min.js +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera/snice-camera.min.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.min.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.min.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/card/snice-card.min.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/carousel/snice-carousel.min.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chart/snice-chart.min.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/chat/snice-chat.min.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.min.js +1 -1
- package/dist/cdn/chip/snice-chip.js +1 -1
- package/dist/cdn/chip/snice-chip.min.js +1 -1
- package/dist/cdn/code-block/snice-code-block.js +2 -2
- package/dist/cdn/code-block/snice-code-block.js.map +1 -1
- package/dist/cdn/code-block/snice-code-block.min.js +2 -2
- package/dist/cdn/code-block/snice-code-block.min.js.map +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-display/snice-color-display.min.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.min.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.min.js +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/comments/snice-comments.min.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/countdown/snice-countdown.min.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/cropper/snice-cropper.min.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.min.js +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/diff/snice-diff.min.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/divider/snice-divider.min.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/doc/snice-doc.min.js +1 -1
- package/dist/cdn/draw/snice-draw.js +1 -1
- package/dist/cdn/draw/snice-draw.min.js +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/drawer/snice-drawer.min.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.min.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.min.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.min.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.min.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/flow/snice-flow.min.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/funnel/snice-funnel.min.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gantt/snice-gantt.min.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/gauge/snice-gauge.min.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.min.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/image/snice-image.min.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/input/snice-input.min.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/kanban/snice-kanban.min.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/kpi/snice-kpi.min.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/layout/snice-layout.min.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link/snice-link.min.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.min.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/list/snice-list.min.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/location/snice-location.min.js +1 -1
- package/dist/cdn/login/snice-login.js +1 -1
- package/dist/cdn/login/snice-login.min.js +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/map/snice-map.min.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/markdown/snice-markdown.min.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/masonry/snice-masonry.min.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/menu/snice-menu.min.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/modal/snice-modal.min.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/music-player/snice-music-player.min.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/nav/snice-nav.min.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.min.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.min.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.min.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/pagination/snice-pagination.min.js +1 -1
- package/dist/cdn/paint/snice-paint.js +1 -1
- package/dist/cdn/paint/snice-paint.min.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.min.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.min.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.min.js +1 -1
- package/dist/cdn/progress/snice-progress.js +1 -1
- package/dist/cdn/progress/snice-progress.min.js +1 -1
- package/dist/cdn/qr-code/README.md +2 -2
- package/dist/cdn/qr-code/snice-qr-code.js +149 -20
- package/dist/cdn/qr-code/snice-qr-code.js.map +1 -1
- package/dist/cdn/qr-code/snice-qr-code.min.js +3 -3
- package/dist/cdn/qr-code/snice-qr-code.min.js.map +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.min.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/radio/snice-radio.min.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/rating/snice-rating.min.js +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/recipe/snice-recipe.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.js +4 -4
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js +3 -3
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +4 -4
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js +3 -3
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/sankey/snice-sankey.min.js +1 -1
- package/dist/cdn/select/snice-select.js +1 -1
- package/dist/cdn/select/snice-select.min.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.min.js +1 -1
- package/dist/cdn/slider/snice-slider.js +2 -2
- package/dist/cdn/slider/snice-slider.js.map +1 -1
- package/dist/cdn/slider/snice-slider.min.js +5 -5
- package/dist/cdn/slider/snice-slider.min.js.map +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sortable/snice-sortable.min.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.min.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/spinner/snice-spinner.min.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.min.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.min.js +1 -1
- package/dist/cdn/spreadsheet/snice-spreadsheet.js +1 -1
- package/dist/cdn/spreadsheet/snice-spreadsheet.min.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/stepper/snice-stepper.min.js +1 -1
- package/dist/cdn/switch/snice-switch.js +1 -1
- package/dist/cdn/switch/snice-switch.min.js +1 -1
- package/dist/cdn/table/snice-table.js +1 -1
- package/dist/cdn/table/snice-table.min.js +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tabs/snice-tabs.min.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.min.js +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/terminal/snice-terminal.min.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.min.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/textarea/snice-textarea.min.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.min.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timeline/snice-timeline.min.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/timer/snice-timer.min.js +1 -1
- package/dist/cdn/toast/snice-toast.js +1 -1
- package/dist/cdn/toast/snice-toast.min.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.min.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/tree/snice-tree.min.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/treemap/snice-treemap.min.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/video-player/snice-video-player.min.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.min.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.min.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/weather/snice-weather.min.js +1 -1
- package/dist/components/code-block/snice-code-block.js +1 -1
- package/dist/components/code-block/snice-code-block.js.map +1 -1
- package/dist/components/qr-code/qrcode.d.ts +1 -0
- package/dist/components/qr-code/qrcode.js +16 -8
- package/dist/components/qr-code/qrcode.js.map +1 -1
- package/dist/components/qr-code/snice-qr-code.d.ts +5 -2
- package/dist/components/qr-code/snice-qr-code.js +132 -11
- package/dist/components/qr-code/snice-qr-code.js.map +1 -1
- package/dist/components/qr-code/snice-qr-code.types.d.ts +3 -2
- package/dist/components/slider/snice-slider.js +1 -1
- package/dist/components/slider/snice-slider.js.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.esm.js +2 -2
- package/dist/index.iife.js +2 -2
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/request-options.d.ts +1 -1
- package/docs/ai/api.md +1 -1
- package/docs/ai/components/code-block.md +1 -1
- package/docs/ai/decorators.md +2 -2
- package/docs/ai/patterns.md +17 -6
- package/docs/code-block.md +1 -3
- package/docs/controllers.md +98 -391
- package/docs/elements.md +131 -117
- package/docs/events.md +74 -83
- package/docs/fetcher.md +64 -76
- package/docs/observe.md +13 -33
- package/docs/placards.md +6 -16
- package/docs/request-response.md +171 -693
- package/docs/routing.md +67 -136
- package/package.json +1 -1
- package/docs/migration-v2-to-v3.md +0 -569
package/docs/controllers.md
CHANGED
|
@@ -62,18 +62,23 @@ interface IController<T extends HTMLElement = HTMLElement> {
|
|
|
62
62
|
|
|
63
63
|
1. Controller instance is created
|
|
64
64
|
2. `element` property is set
|
|
65
|
-
3.
|
|
66
|
-
4. `
|
|
67
|
-
5.
|
|
68
|
-
6.
|
|
65
|
+
3. Router context is passed (if available)
|
|
66
|
+
4. Element's `ready` promise is awaited
|
|
67
|
+
5. `attach()` method is called
|
|
68
|
+
6. Observers are set up
|
|
69
|
+
7. Channel/response handlers are set up
|
|
70
|
+
8. Event handlers are set up
|
|
71
|
+
9. `@snice/controller-attached` event is dispatched
|
|
69
72
|
|
|
70
73
|
### Detachment Flow
|
|
71
74
|
|
|
72
75
|
1. `detach()` method is called
|
|
73
76
|
2. `element` property is set to null
|
|
74
|
-
3.
|
|
75
|
-
4.
|
|
76
|
-
5.
|
|
77
|
+
3. Observers are cleaned up
|
|
78
|
+
4. Channel/response handlers are cleaned up
|
|
79
|
+
5. Event handlers are cleaned up
|
|
80
|
+
6. Controller scope is cleaned up
|
|
81
|
+
7. `@snice/controller-detached` event is dispatched
|
|
77
82
|
|
|
78
83
|
### Example with Lifecycle Logging
|
|
79
84
|
|
|
@@ -154,53 +159,29 @@ This allows you to attach controllers to any HTML element:
|
|
|
154
159
|
|
|
155
160
|
### Example: Table Controller
|
|
156
161
|
|
|
162
|
+
Controllers provide specific behaviors (data fetching, sorting, filtering) to generic visual components. The component handles rendering — the controller handles data:
|
|
163
|
+
|
|
157
164
|
```typescript
|
|
158
165
|
@controller('table-controller')
|
|
159
166
|
class TableController implements IController<HTMLTableElement> {
|
|
160
167
|
element: HTMLTableElement | null = null;
|
|
161
|
-
private data: any[] = [];
|
|
162
168
|
|
|
163
169
|
async attach(element: HTMLTableElement) {
|
|
164
|
-
|
|
165
|
-
this.data = await this.fetchData();
|
|
166
|
-
|
|
167
|
-
// Render table
|
|
168
|
-
this.renderTable();
|
|
169
|
-
}
|
|
170
|
+
const data = await fetch('/api/data').then(r => r.json());
|
|
170
171
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (tbody) {
|
|
175
|
-
tbody.innerHTML = '';
|
|
172
|
+
// Pass data to the element — if it's a custom element, call its API
|
|
173
|
+
if ('setData' in element && typeof (element as any).setData === 'function') {
|
|
174
|
+
(element as any).setData(data);
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
177
|
|
|
179
|
-
|
|
180
|
-
const response = await fetch('/api/data');
|
|
181
|
-
return response.json();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private renderTable() {
|
|
185
|
-
if (!this.element) return;
|
|
186
|
-
|
|
187
|
-
const tbody = this.element.querySelector('tbody');
|
|
188
|
-
if (!tbody) return;
|
|
189
|
-
|
|
190
|
-
tbody.innerHTML = this.data.map(row => `
|
|
191
|
-
<tr>
|
|
192
|
-
<td>${row.id}</td>
|
|
193
|
-
<td>${row.name}</td>
|
|
194
|
-
<td>${row.status}</td>
|
|
195
|
-
</tr>
|
|
196
|
-
`).join('');
|
|
197
|
-
}
|
|
178
|
+
async detach() {}
|
|
198
179
|
}
|
|
199
180
|
```
|
|
200
181
|
|
|
201
182
|
## Resource Cleanup
|
|
202
183
|
|
|
203
|
-
|
|
184
|
+
The framework auto-cleans `@on`, `@observe`, and `@respond` handlers. Clean up your own resources (WebSockets, timers, manual listeners) in `detach`:
|
|
204
185
|
|
|
205
186
|
```typescript
|
|
206
187
|
import { controller, IController } from 'snice';
|
|
@@ -287,63 +268,37 @@ class FormController implements IController<HTMLFormElement> {
|
|
|
287
268
|
|
|
288
269
|
## Query Selectors in Controllers
|
|
289
270
|
|
|
290
|
-
Controllers can use `@query` and `@queryAll` to access elements:
|
|
271
|
+
Controllers can use `@query` and `@queryAll` to access elements. **Important:** By default, `@query` searches the shadow DOM. When attached to native elements (no shadow root), use `{ light: true }`:
|
|
291
272
|
|
|
292
273
|
```typescript
|
|
293
274
|
import { controller, query, queryAll, IController } from 'snice';
|
|
294
275
|
|
|
295
|
-
@controller('
|
|
296
|
-
class
|
|
297
|
-
element:
|
|
298
|
-
|
|
299
|
-
@query('.status-indicator')
|
|
300
|
-
statusIndicator?: HTMLElement;
|
|
301
|
-
|
|
302
|
-
@query('#refresh-button')
|
|
303
|
-
refreshButton?: HTMLButtonElement;
|
|
304
|
-
|
|
305
|
-
@queryAll('.data-card')
|
|
306
|
-
dataCards?: NodeListOf<HTMLElement>;
|
|
276
|
+
@controller('form-validation-controller')
|
|
277
|
+
class FormValidationController implements IController<HTMLFormElement> {
|
|
278
|
+
element: HTMLFormElement | null = null;
|
|
307
279
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
280
|
+
// light: true is required — native elements have no shadow root
|
|
281
|
+
@query('.error-message', { light: true })
|
|
282
|
+
errorEl?: HTMLElement;
|
|
311
283
|
|
|
312
|
-
|
|
313
|
-
|
|
284
|
+
@queryAll('input[required]', { light: true })
|
|
285
|
+
requiredInputs?: NodeListOf<HTMLInputElement>;
|
|
314
286
|
|
|
315
|
-
|
|
316
|
-
}
|
|
287
|
+
async attach() {}
|
|
288
|
+
async detach() {}
|
|
317
289
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
290
|
+
@on('submit')
|
|
291
|
+
handleSubmit(event: Event) {
|
|
292
|
+
const invalid = Array.from(this.requiredInputs || []).filter(i => !i.value.trim());
|
|
321
293
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
294
|
+
if (invalid.length > 0) {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
invalid[0].focus();
|
|
297
|
+
if (this.errorEl) {
|
|
298
|
+
this.errorEl.textContent = `${invalid.length} required field(s) missing`;
|
|
299
|
+
}
|
|
325
300
|
}
|
|
326
301
|
}
|
|
327
|
-
|
|
328
|
-
private async loadDashboardData() {
|
|
329
|
-
// Load data for each card
|
|
330
|
-
this.dataCards?.forEach(async (card, index) => {
|
|
331
|
-
const data = await this.fetchCardData(index);
|
|
332
|
-
card.innerHTML = this.renderCard(data);
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
private async fetchCardData(index: number) {
|
|
337
|
-
// Simulate API call
|
|
338
|
-
return { title: `Card ${index + 1}`, value: Math.random() * 100 };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
private renderCard(data: any) {
|
|
342
|
-
return `
|
|
343
|
-
<h3>${data.title}</h3>
|
|
344
|
-
<p>${data.value.toFixed(2)}</p>
|
|
345
|
-
`;
|
|
346
|
-
}
|
|
347
302
|
}
|
|
348
303
|
```
|
|
349
304
|
|
|
@@ -351,6 +306,8 @@ class DashboardController implements IController {
|
|
|
351
306
|
|
|
352
307
|
### Data Fetching Controller
|
|
353
308
|
|
|
309
|
+
Controllers own data fetching. Pass results to the element via its API or dispatch events — don't manipulate DOM directly:
|
|
310
|
+
|
|
354
311
|
```typescript
|
|
355
312
|
@controller('data-fetcher')
|
|
356
313
|
class DataFetcherController implements IController {
|
|
@@ -359,219 +316,66 @@ class DataFetcherController implements IController {
|
|
|
359
316
|
private pollingInterval?: number;
|
|
360
317
|
|
|
361
318
|
async attach(element: HTMLElement) {
|
|
362
|
-
|
|
363
|
-
await this.fetchAndRender();
|
|
319
|
+
await this.fetchData();
|
|
364
320
|
|
|
365
|
-
//
|
|
366
|
-
this.pollingInterval = setInterval(() =>
|
|
367
|
-
this.fetchAndRender();
|
|
368
|
-
}, 30000); // Poll every 30 seconds
|
|
321
|
+
// Poll every 30 seconds
|
|
322
|
+
this.pollingInterval = setInterval(() => this.fetchData(), 30000);
|
|
369
323
|
}
|
|
370
324
|
|
|
371
|
-
async detach(
|
|
372
|
-
// Cancel any pending requests
|
|
325
|
+
async detach() {
|
|
373
326
|
this.abortController?.abort();
|
|
374
|
-
|
|
375
|
-
// Stop polling
|
|
376
327
|
if (this.pollingInterval) {
|
|
377
328
|
clearInterval(this.pollingInterval);
|
|
378
329
|
}
|
|
379
330
|
}
|
|
380
331
|
|
|
381
|
-
private async
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
this.abortController?.abort();
|
|
385
|
-
this.abortController = new AbortController();
|
|
386
|
-
|
|
387
|
-
// Show loading state
|
|
388
|
-
this.setLoadingState(true);
|
|
332
|
+
private async fetchData() {
|
|
333
|
+
this.abortController?.abort();
|
|
334
|
+
this.abortController = new AbortController();
|
|
389
335
|
|
|
390
|
-
|
|
336
|
+
try {
|
|
391
337
|
const response = await fetch('/api/data', {
|
|
392
338
|
signal: this.abortController.signal
|
|
393
339
|
});
|
|
394
|
-
|
|
395
|
-
if (!response.ok) {
|
|
396
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
340
|
const data = await response.json();
|
|
400
341
|
|
|
401
|
-
//
|
|
402
|
-
this.
|
|
403
|
-
|
|
342
|
+
// Pass data to element — let the element handle rendering
|
|
343
|
+
this.element?.dispatchEvent(new CustomEvent('data-loaded', {
|
|
344
|
+
detail: data,
|
|
345
|
+
bubbles: true
|
|
346
|
+
}));
|
|
404
347
|
} catch (error: any) {
|
|
405
348
|
if (error.name !== 'AbortError') {
|
|
406
|
-
this.
|
|
349
|
+
this.element?.dispatchEvent(new CustomEvent('data-error', {
|
|
350
|
+
detail: { message: error.message },
|
|
351
|
+
bubbles: true
|
|
352
|
+
}));
|
|
407
353
|
}
|
|
408
|
-
} finally {
|
|
409
|
-
this.setLoadingState(false);
|
|
410
354
|
}
|
|
411
355
|
}
|
|
412
|
-
|
|
413
|
-
private setLoadingState(loading: boolean) {
|
|
414
|
-
if (!this.element) return;
|
|
415
|
-
|
|
416
|
-
if (loading) {
|
|
417
|
-
this.element.classList.add('loading');
|
|
418
|
-
this.element.setAttribute('aria-busy', 'true');
|
|
419
|
-
} else {
|
|
420
|
-
this.element.classList.remove('loading');
|
|
421
|
-
this.element.setAttribute('aria-busy', 'false');
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
private renderData(data: any) {
|
|
426
|
-
if (!this.element) return;
|
|
427
|
-
|
|
428
|
-
// Type guard for custom element
|
|
429
|
-
if ('setData' in this.element && typeof this.element.setData === 'function') {
|
|
430
|
-
this.element.setData(data);
|
|
431
|
-
} else {
|
|
432
|
-
// Fallback for native elements
|
|
433
|
-
this.element.innerHTML = JSON.stringify(data, null, 2);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
private renderError(message: string) {
|
|
438
|
-
if (!this.element) return;
|
|
439
|
-
|
|
440
|
-
const errorDiv = document.createElement('div');
|
|
441
|
-
errorDiv.className = 'error';
|
|
442
|
-
errorDiv.textContent = `Error: ${message}`;
|
|
443
|
-
|
|
444
|
-
this.element.innerHTML = '';
|
|
445
|
-
this.element.appendChild(errorDiv);
|
|
446
|
-
}
|
|
447
356
|
}
|
|
448
357
|
```
|
|
449
358
|
|
|
450
|
-
###
|
|
359
|
+
### Theme Controller
|
|
451
360
|
|
|
452
361
|
```typescript
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
theme: 'light' | 'dark';
|
|
456
|
-
notifications: Notification[];
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
interface Notification {
|
|
460
|
-
id: string;
|
|
461
|
-
message: string;
|
|
462
|
-
type: 'info' | 'warning' | 'error';
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
@controller('state-controller')
|
|
466
|
-
class StateController implements IController {
|
|
362
|
+
@controller('theme-controller')
|
|
363
|
+
class ThemeController implements IController {
|
|
467
364
|
element: HTMLElement | null = null;
|
|
468
|
-
private state: AppState = {
|
|
469
|
-
user: null,
|
|
470
|
-
theme: 'light',
|
|
471
|
-
notifications: []
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
private stateListeners = new Set<(state: AppState) => void>();
|
|
475
365
|
|
|
476
366
|
async attach(element: HTMLElement) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
// Subscribe element to state changes
|
|
481
|
-
const updateElement = (state: AppState) => {
|
|
482
|
-
this.updateElementWithState(element, state);
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
this.stateListeners.add(updateElement);
|
|
486
|
-
|
|
487
|
-
// Initial render
|
|
488
|
-
updateElement(this.state);
|
|
367
|
+
const saved = localStorage.getItem('theme') || 'light';
|
|
368
|
+
element.setAttribute('data-theme', saved);
|
|
489
369
|
}
|
|
490
370
|
|
|
491
|
-
async detach(element: HTMLElement) {
|
|
492
|
-
// Clean up listeners
|
|
493
|
-
this.stateListeners.clear();
|
|
494
|
-
|
|
495
|
-
// Save state
|
|
496
|
-
await this.saveState();
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Public methods for state management
|
|
500
|
-
setUser(user: AppState['user']) {
|
|
501
|
-
this.updateState({ ...this.state, user });
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
setTheme(theme: AppState['theme']) {
|
|
505
|
-
this.updateState({ ...this.state, theme });
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
addNotification(notification: Omit<Notification, 'id'>) {
|
|
509
|
-
const newNotification: Notification = {
|
|
510
|
-
...notification,
|
|
511
|
-
id: Date.now().toString()
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
this.updateState({
|
|
515
|
-
...this.state,
|
|
516
|
-
notifications: [...this.state.notifications, newNotification]
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// Auto-remove after 5 seconds
|
|
520
|
-
setTimeout(() => {
|
|
521
|
-
this.removeNotification(newNotification.id);
|
|
522
|
-
}, 5000);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
removeNotification(id: string) {
|
|
526
|
-
this.updateState({
|
|
527
|
-
...this.state,
|
|
528
|
-
notifications: this.state.notifications.filter(n => n.id !== id)
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
private updateState(newState: AppState) {
|
|
533
|
-
this.state = newState;
|
|
534
|
-
|
|
535
|
-
// Notify all listeners
|
|
536
|
-
this.stateListeners.forEach(listener => listener(this.state));
|
|
371
|
+
async detach(element: HTMLElement) {}
|
|
537
372
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
element.setAttribute('data-theme', state.theme);
|
|
545
|
-
|
|
546
|
-
// If element has state methods, call them
|
|
547
|
-
if ('setState' in element && typeof element.setState === 'function') {
|
|
548
|
-
(element as any).setState(state);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Dispatch state change event
|
|
552
|
-
element.dispatchEvent(new CustomEvent('state-changed', {
|
|
553
|
-
detail: state,
|
|
554
|
-
bubbles: true
|
|
555
|
-
}));
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
private async loadState() {
|
|
559
|
-
try {
|
|
560
|
-
const saved = localStorage.getItem('app-state');
|
|
561
|
-
if (saved) {
|
|
562
|
-
this.state = JSON.parse(saved);
|
|
563
|
-
}
|
|
564
|
-
} catch (error) {
|
|
565
|
-
console.error('Failed to load state:', error);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
private async saveState() {
|
|
570
|
-
try {
|
|
571
|
-
localStorage.setItem('app-state', JSON.stringify(this.state));
|
|
572
|
-
} catch (error) {
|
|
573
|
-
console.error('Failed to save state:', error);
|
|
574
|
-
}
|
|
373
|
+
@on('click', '[data-set-theme]')
|
|
374
|
+
handleThemeToggle(event: MouseEvent) {
|
|
375
|
+
const target = event.target as HTMLElement;
|
|
376
|
+
const theme = target.dataset.setTheme!;
|
|
377
|
+
this.element?.setAttribute('data-theme', theme);
|
|
378
|
+
localStorage.setItem('theme', theme);
|
|
575
379
|
}
|
|
576
380
|
}
|
|
577
381
|
```
|
|
@@ -579,166 +383,69 @@ class StateController implements IController {
|
|
|
579
383
|
### WebSocket Controller
|
|
580
384
|
|
|
581
385
|
```typescript
|
|
582
|
-
@controller('
|
|
386
|
+
@controller('ws-controller')
|
|
583
387
|
class WebSocketController implements IController {
|
|
584
388
|
element: HTMLElement | null = null;
|
|
585
389
|
private ws?: WebSocket;
|
|
586
390
|
private reconnectTimer?: number;
|
|
587
|
-
private reconnectAttempts = 0;
|
|
588
|
-
private maxReconnectAttempts = 5;
|
|
589
|
-
private reconnectDelay = 1000; // Start with 1 second
|
|
590
391
|
|
|
591
392
|
async attach(element: HTMLElement) {
|
|
592
393
|
this.connect();
|
|
593
394
|
}
|
|
594
395
|
|
|
595
396
|
async detach(element: HTMLElement) {
|
|
596
|
-
this.
|
|
397
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
398
|
+
this.ws?.close();
|
|
597
399
|
}
|
|
598
400
|
|
|
599
401
|
private connect() {
|
|
600
|
-
|
|
601
|
-
this.ws = new WebSocket('ws://localhost:8080');
|
|
602
|
-
|
|
603
|
-
this.ws.onopen = () => {
|
|
604
|
-
console.log('WebSocket connected');
|
|
605
|
-
this.reconnectAttempts = 0;
|
|
606
|
-
this.reconnectDelay = 1000;
|
|
607
|
-
this.onConnected();
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
this.ws.onmessage = (event) => {
|
|
611
|
-
this.handleMessage(event.data);
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
this.ws.onerror = (error) => {
|
|
615
|
-
console.error('WebSocket error:', error);
|
|
616
|
-
this.onError(error);
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
this.ws.onclose = () => {
|
|
620
|
-
console.log('WebSocket disconnected');
|
|
621
|
-
this.onDisconnected();
|
|
622
|
-
this.scheduleReconnect();
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
} catch (error) {
|
|
626
|
-
console.error('Failed to create WebSocket:', error);
|
|
627
|
-
this.scheduleReconnect();
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
private disconnect() {
|
|
632
|
-
if (this.reconnectTimer) {
|
|
633
|
-
clearTimeout(this.reconnectTimer);
|
|
634
|
-
this.reconnectTimer = undefined;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
if (this.ws) {
|
|
638
|
-
this.ws.close();
|
|
639
|
-
this.ws = undefined;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
private scheduleReconnect() {
|
|
644
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
645
|
-
console.error('Max reconnection attempts reached');
|
|
646
|
-
this.onReconnectFailed();
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
this.reconnectAttempts++;
|
|
651
|
-
|
|
652
|
-
console.log(`Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
|
|
653
|
-
|
|
654
|
-
this.reconnectTimer = setTimeout(() => {
|
|
655
|
-
this.connect();
|
|
656
|
-
}, this.reconnectDelay);
|
|
657
|
-
|
|
658
|
-
// Exponential backoff
|
|
659
|
-
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
private handleMessage(data: string) {
|
|
663
|
-
try {
|
|
664
|
-
const message = JSON.parse(data);
|
|
665
|
-
|
|
666
|
-
// Update element with message
|
|
667
|
-
if (this.element && 'onMessage' in this.element) {
|
|
668
|
-
(this.element as any).onMessage(message);
|
|
669
|
-
}
|
|
402
|
+
this.ws = new WebSocket('wss://api.example.com/ws');
|
|
670
403
|
|
|
671
|
-
|
|
404
|
+
this.ws.onmessage = (event) => {
|
|
672
405
|
this.element?.dispatchEvent(new CustomEvent('ws-message', {
|
|
673
|
-
detail:
|
|
406
|
+
detail: JSON.parse(event.data),
|
|
674
407
|
bubbles: true
|
|
675
408
|
}));
|
|
409
|
+
};
|
|
676
410
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
411
|
+
this.ws.onclose = () => {
|
|
412
|
+
this.reconnectTimer = setTimeout(() => this.connect(), 3000);
|
|
413
|
+
};
|
|
680
414
|
}
|
|
681
415
|
|
|
682
416
|
send(data: any) {
|
|
683
417
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
684
418
|
this.ws.send(JSON.stringify(data));
|
|
685
|
-
} else {
|
|
686
|
-
console.warn('WebSocket not connected, queuing message');
|
|
687
|
-
// Could implement message queue here
|
|
688
419
|
}
|
|
689
420
|
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
690
423
|
|
|
691
|
-
|
|
692
|
-
this.element?.classList.remove('disconnected');
|
|
693
|
-
this.element?.classList.add('connected');
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
private onDisconnected() {
|
|
697
|
-
this.element?.classList.remove('connected');
|
|
698
|
-
this.element?.classList.add('disconnected');
|
|
699
|
-
}
|
|
424
|
+
## Accessing Controllers
|
|
700
425
|
|
|
701
|
-
|
|
702
|
-
this.element?.classList.add('error');
|
|
703
|
-
}
|
|
426
|
+
### Via Event
|
|
704
427
|
|
|
705
|
-
|
|
706
|
-
this.element?.classList.add('reconnect-failed');
|
|
428
|
+
Listen for attachment on the element itself (the event does **not** bubble):
|
|
707
429
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
this.element.appendChild(notification);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
430
|
+
```typescript
|
|
431
|
+
element.addEventListener('@snice/controller-attached', (e: CustomEvent) => {
|
|
432
|
+
console.log('Name:', e.detail.name); // controller name string
|
|
433
|
+
console.log('Instance:', e.detail.controller); // IController instance
|
|
434
|
+
});
|
|
717
435
|
```
|
|
718
436
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
Controllers are automatically registered when decorated with `@controller`:
|
|
437
|
+
### Via getController()
|
|
722
438
|
|
|
723
439
|
```typescript
|
|
724
440
|
import { getController } from 'snice';
|
|
725
441
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (controller) {
|
|
731
|
-
console.log('Controller found:', controller);
|
|
442
|
+
const ctrl = getController<MyController>(element);
|
|
443
|
+
if (ctrl) {
|
|
444
|
+
ctrl.doSomething();
|
|
732
445
|
}
|
|
733
446
|
```
|
|
734
447
|
|
|
735
|
-
|
|
448
|
+
### Auto-Cleanup
|
|
449
|
+
|
|
450
|
+
The framework automatically cleans up `@on` handlers, observers, and `@respond` handlers during detach. Manual cleanup in `detach()` is only needed for resources you manage yourself (WebSockets, intervals, manual event listeners).
|
|
736
451
|
|
|
737
|
-
1. **Separation of Concerns**: Keep controllers focused on data and business logic
|
|
738
|
-
2. **Cleanup Resources**: Always clean up timers, listeners, and connections
|
|
739
|
-
3. **Error Handling**: Handle errors gracefully in async operations
|
|
740
|
-
4. **Type Safety**: Use TypeScript generics for element types
|
|
741
|
-
5. **State Management**: Consider using a state controller for complex state
|
|
742
|
-
6. **Abort Requests**: Cancel pending requests when detaching
|
|
743
|
-
7. **Memory Management**: Clear references to prevent memory leaks
|
|
744
|
-
8. **Event Delegation**: Use event delegation for dynamic content
|