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.
Files changed (253) hide show
  1. package/dist/cdn/accordion/snice-accordion.js +1 -1
  2. package/dist/cdn/accordion/snice-accordion.min.js +1 -1
  3. package/dist/cdn/alert/snice-alert.js +1 -1
  4. package/dist/cdn/alert/snice-alert.min.js +1 -1
  5. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  6. package/dist/cdn/app-tiles/snice-app-tiles.min.js +1 -1
  7. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  8. package/dist/cdn/audio-recorder/snice-audio-recorder.min.js +1 -1
  9. package/dist/cdn/avatar/snice-avatar.js +1 -1
  10. package/dist/cdn/avatar/snice-avatar.min.js +1 -1
  11. package/dist/cdn/badge/snice-badge.js +1 -1
  12. package/dist/cdn/badge/snice-badge.min.js +1 -1
  13. package/dist/cdn/banner/snice-banner.js +1 -1
  14. package/dist/cdn/banner/snice-banner.min.js +1 -1
  15. package/dist/cdn/book/snice-book.js +1 -1
  16. package/dist/cdn/book/snice-book.min.js +1 -1
  17. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  18. package/dist/cdn/breadcrumbs/snice-breadcrumbs.min.js +1 -1
  19. package/dist/cdn/button/snice-button.js +1 -1
  20. package/dist/cdn/button/snice-button.min.js +1 -1
  21. package/dist/cdn/calendar/snice-calendar.js +1 -1
  22. package/dist/cdn/calendar/snice-calendar.min.js +1 -1
  23. package/dist/cdn/camera/snice-camera.js +1 -1
  24. package/dist/cdn/camera/snice-camera.min.js +1 -1
  25. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  26. package/dist/cdn/camera-annotate/snice-camera-annotate.min.js +1 -1
  27. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  28. package/dist/cdn/candlestick/snice-candlestick.min.js +1 -1
  29. package/dist/cdn/card/snice-card.js +1 -1
  30. package/dist/cdn/card/snice-card.min.js +1 -1
  31. package/dist/cdn/carousel/snice-carousel.js +1 -1
  32. package/dist/cdn/carousel/snice-carousel.min.js +1 -1
  33. package/dist/cdn/chart/snice-chart.js +1 -1
  34. package/dist/cdn/chart/snice-chart.min.js +1 -1
  35. package/dist/cdn/chat/snice-chat.js +1 -1
  36. package/dist/cdn/chat/snice-chat.min.js +1 -1
  37. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  38. package/dist/cdn/checkbox/snice-checkbox.min.js +1 -1
  39. package/dist/cdn/chip/snice-chip.js +1 -1
  40. package/dist/cdn/chip/snice-chip.min.js +1 -1
  41. package/dist/cdn/code-block/snice-code-block.js +2 -2
  42. package/dist/cdn/code-block/snice-code-block.js.map +1 -1
  43. package/dist/cdn/code-block/snice-code-block.min.js +2 -2
  44. package/dist/cdn/code-block/snice-code-block.min.js.map +1 -1
  45. package/dist/cdn/color-display/snice-color-display.js +1 -1
  46. package/dist/cdn/color-display/snice-color-display.min.js +1 -1
  47. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  48. package/dist/cdn/color-picker/snice-color-picker.min.js +1 -1
  49. package/dist/cdn/command-palette/snice-command-palette.js +1 -1
  50. package/dist/cdn/command-palette/snice-command-palette.min.js +1 -1
  51. package/dist/cdn/comments/snice-comments.js +1 -1
  52. package/dist/cdn/comments/snice-comments.min.js +1 -1
  53. package/dist/cdn/countdown/snice-countdown.js +1 -1
  54. package/dist/cdn/countdown/snice-countdown.min.js +1 -1
  55. package/dist/cdn/cropper/snice-cropper.js +1 -1
  56. package/dist/cdn/cropper/snice-cropper.min.js +1 -1
  57. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  58. package/dist/cdn/date-picker/snice-date-picker.min.js +1 -1
  59. package/dist/cdn/diff/snice-diff.js +1 -1
  60. package/dist/cdn/diff/snice-diff.min.js +1 -1
  61. package/dist/cdn/divider/snice-divider.js +1 -1
  62. package/dist/cdn/divider/snice-divider.min.js +1 -1
  63. package/dist/cdn/doc/snice-doc.js +1 -1
  64. package/dist/cdn/doc/snice-doc.min.js +1 -1
  65. package/dist/cdn/draw/snice-draw.js +1 -1
  66. package/dist/cdn/draw/snice-draw.min.js +1 -1
  67. package/dist/cdn/drawer/snice-drawer.js +1 -1
  68. package/dist/cdn/drawer/snice-drawer.min.js +1 -1
  69. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  70. package/dist/cdn/empty-state/snice-empty-state.min.js +1 -1
  71. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  72. package/dist/cdn/file-gallery/snice-file-gallery.min.js +1 -1
  73. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  74. package/dist/cdn/file-upload/snice-file-upload.min.js +1 -1
  75. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  76. package/dist/cdn/flip-card/snice-flip-card.min.js +1 -1
  77. package/dist/cdn/flow/snice-flow.js +1 -1
  78. package/dist/cdn/flow/snice-flow.min.js +1 -1
  79. package/dist/cdn/funnel/snice-funnel.js +1 -1
  80. package/dist/cdn/funnel/snice-funnel.min.js +1 -1
  81. package/dist/cdn/gantt/snice-gantt.js +1 -1
  82. package/dist/cdn/gantt/snice-gantt.min.js +1 -1
  83. package/dist/cdn/gauge/snice-gauge.js +1 -1
  84. package/dist/cdn/gauge/snice-gauge.min.js +1 -1
  85. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  86. package/dist/cdn/heatmap/snice-heatmap.min.js +1 -1
  87. package/dist/cdn/image/snice-image.js +1 -1
  88. package/dist/cdn/image/snice-image.min.js +1 -1
  89. package/dist/cdn/input/snice-input.js +1 -1
  90. package/dist/cdn/input/snice-input.min.js +1 -1
  91. package/dist/cdn/kanban/snice-kanban.js +1 -1
  92. package/dist/cdn/kanban/snice-kanban.min.js +1 -1
  93. package/dist/cdn/kpi/snice-kpi.js +1 -1
  94. package/dist/cdn/kpi/snice-kpi.min.js +1 -1
  95. package/dist/cdn/layout/snice-layout.js +1 -1
  96. package/dist/cdn/layout/snice-layout.min.js +1 -1
  97. package/dist/cdn/link/snice-link.js +1 -1
  98. package/dist/cdn/link/snice-link.min.js +1 -1
  99. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  100. package/dist/cdn/link-preview/snice-link-preview.min.js +1 -1
  101. package/dist/cdn/list/snice-list.js +1 -1
  102. package/dist/cdn/list/snice-list.min.js +1 -1
  103. package/dist/cdn/location/snice-location.js +1 -1
  104. package/dist/cdn/location/snice-location.min.js +1 -1
  105. package/dist/cdn/login/snice-login.js +1 -1
  106. package/dist/cdn/login/snice-login.min.js +1 -1
  107. package/dist/cdn/map/snice-map.js +1 -1
  108. package/dist/cdn/map/snice-map.min.js +1 -1
  109. package/dist/cdn/markdown/snice-markdown.js +1 -1
  110. package/dist/cdn/markdown/snice-markdown.min.js +1 -1
  111. package/dist/cdn/masonry/snice-masonry.js +1 -1
  112. package/dist/cdn/masonry/snice-masonry.min.js +1 -1
  113. package/dist/cdn/menu/snice-menu.js +1 -1
  114. package/dist/cdn/menu/snice-menu.min.js +1 -1
  115. package/dist/cdn/modal/snice-modal.js +1 -1
  116. package/dist/cdn/modal/snice-modal.min.js +1 -1
  117. package/dist/cdn/music-player/snice-music-player.js +1 -1
  118. package/dist/cdn/music-player/snice-music-player.min.js +1 -1
  119. package/dist/cdn/nav/snice-nav.js +1 -1
  120. package/dist/cdn/nav/snice-nav.min.js +1 -1
  121. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  122. package/dist/cdn/network-graph/snice-network-graph.min.js +1 -1
  123. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  124. package/dist/cdn/notification-center/snice-notification-center.min.js +1 -1
  125. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  126. package/dist/cdn/org-chart/snice-org-chart.min.js +1 -1
  127. package/dist/cdn/pagination/snice-pagination.js +1 -1
  128. package/dist/cdn/pagination/snice-pagination.min.js +1 -1
  129. package/dist/cdn/paint/snice-paint.js +1 -1
  130. package/dist/cdn/paint/snice-paint.min.js +1 -1
  131. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  132. package/dist/cdn/pdf-viewer/snice-pdf-viewer.min.js +1 -1
  133. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  134. package/dist/cdn/podcast-player/snice-podcast-player.min.js +1 -1
  135. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  136. package/dist/cdn/pricing-table/snice-pricing-table.min.js +1 -1
  137. package/dist/cdn/progress/snice-progress.js +1 -1
  138. package/dist/cdn/progress/snice-progress.min.js +1 -1
  139. package/dist/cdn/qr-code/README.md +2 -2
  140. package/dist/cdn/qr-code/snice-qr-code.js +149 -20
  141. package/dist/cdn/qr-code/snice-qr-code.js.map +1 -1
  142. package/dist/cdn/qr-code/snice-qr-code.min.js +3 -3
  143. package/dist/cdn/qr-code/snice-qr-code.min.js.map +1 -1
  144. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  145. package/dist/cdn/qr-reader/snice-qr-reader.min.js +1 -1
  146. package/dist/cdn/radio/snice-radio.js +1 -1
  147. package/dist/cdn/radio/snice-radio.min.js +1 -1
  148. package/dist/cdn/rating/snice-rating.js +1 -1
  149. package/dist/cdn/rating/snice-rating.min.js +1 -1
  150. package/dist/cdn/recipe/snice-recipe.js +1 -1
  151. package/dist/cdn/recipe/snice-recipe.min.js +1 -1
  152. package/dist/cdn/runtime/snice-runtime.esm.js +4 -4
  153. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  154. package/dist/cdn/runtime/snice-runtime.esm.min.js +3 -3
  155. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  156. package/dist/cdn/runtime/snice-runtime.js +4 -4
  157. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  158. package/dist/cdn/runtime/snice-runtime.min.js +3 -3
  159. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  160. package/dist/cdn/sankey/snice-sankey.js +1 -1
  161. package/dist/cdn/sankey/snice-sankey.min.js +1 -1
  162. package/dist/cdn/select/snice-select.js +1 -1
  163. package/dist/cdn/select/snice-select.min.js +1 -1
  164. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  165. package/dist/cdn/skeleton/snice-skeleton.min.js +1 -1
  166. package/dist/cdn/slider/snice-slider.js +2 -2
  167. package/dist/cdn/slider/snice-slider.js.map +1 -1
  168. package/dist/cdn/slider/snice-slider.min.js +5 -5
  169. package/dist/cdn/slider/snice-slider.min.js.map +1 -1
  170. package/dist/cdn/sortable/snice-sortable.js +1 -1
  171. package/dist/cdn/sortable/snice-sortable.min.js +1 -1
  172. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  173. package/dist/cdn/sparkline/snice-sparkline.min.js +1 -1
  174. package/dist/cdn/spinner/snice-spinner.js +1 -1
  175. package/dist/cdn/spinner/snice-spinner.min.js +1 -1
  176. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  177. package/dist/cdn/split-pane/snice-split-pane.min.js +1 -1
  178. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  179. package/dist/cdn/spotlight/snice-spotlight.min.js +1 -1
  180. package/dist/cdn/spreadsheet/snice-spreadsheet.js +1 -1
  181. package/dist/cdn/spreadsheet/snice-spreadsheet.min.js +1 -1
  182. package/dist/cdn/stepper/snice-stepper.js +1 -1
  183. package/dist/cdn/stepper/snice-stepper.min.js +1 -1
  184. package/dist/cdn/switch/snice-switch.js +1 -1
  185. package/dist/cdn/switch/snice-switch.min.js +1 -1
  186. package/dist/cdn/table/snice-table.js +1 -1
  187. package/dist/cdn/table/snice-table.min.js +1 -1
  188. package/dist/cdn/tabs/snice-tabs.js +1 -1
  189. package/dist/cdn/tabs/snice-tabs.min.js +1 -1
  190. package/dist/cdn/tag-input/snice-tag-input.js +1 -1
  191. package/dist/cdn/tag-input/snice-tag-input.min.js +1 -1
  192. package/dist/cdn/terminal/snice-terminal.js +1 -1
  193. package/dist/cdn/terminal/snice-terminal.min.js +1 -1
  194. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  195. package/dist/cdn/testimonial/snice-testimonial.min.js +1 -1
  196. package/dist/cdn/textarea/snice-textarea.js +1 -1
  197. package/dist/cdn/textarea/snice-textarea.min.js +1 -1
  198. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  199. package/dist/cdn/time-range-picker/snice-time-range-picker.min.js +1 -1
  200. package/dist/cdn/timeline/snice-timeline.js +1 -1
  201. package/dist/cdn/timeline/snice-timeline.min.js +1 -1
  202. package/dist/cdn/timer/snice-timer.js +1 -1
  203. package/dist/cdn/timer/snice-timer.min.js +1 -1
  204. package/dist/cdn/toast/snice-toast.js +1 -1
  205. package/dist/cdn/toast/snice-toast.min.js +1 -1
  206. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  207. package/dist/cdn/tooltip/snice-tooltip.min.js +1 -1
  208. package/dist/cdn/tree/snice-tree.js +1 -1
  209. package/dist/cdn/tree/snice-tree.min.js +1 -1
  210. package/dist/cdn/treemap/snice-treemap.js +1 -1
  211. package/dist/cdn/treemap/snice-treemap.min.js +1 -1
  212. package/dist/cdn/video-player/snice-video-player.js +1 -1
  213. package/dist/cdn/video-player/snice-video-player.min.js +1 -1
  214. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  215. package/dist/cdn/virtual-scroller/snice-virtual-scroller.min.js +1 -1
  216. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  217. package/dist/cdn/waterfall/snice-waterfall.min.js +1 -1
  218. package/dist/cdn/weather/snice-weather.js +1 -1
  219. package/dist/cdn/weather/snice-weather.min.js +1 -1
  220. package/dist/components/code-block/snice-code-block.js +1 -1
  221. package/dist/components/code-block/snice-code-block.js.map +1 -1
  222. package/dist/components/qr-code/qrcode.d.ts +1 -0
  223. package/dist/components/qr-code/qrcode.js +16 -8
  224. package/dist/components/qr-code/qrcode.js.map +1 -1
  225. package/dist/components/qr-code/snice-qr-code.d.ts +5 -2
  226. package/dist/components/qr-code/snice-qr-code.js +132 -11
  227. package/dist/components/qr-code/snice-qr-code.js.map +1 -1
  228. package/dist/components/qr-code/snice-qr-code.types.d.ts +3 -2
  229. package/dist/components/slider/snice-slider.js +1 -1
  230. package/dist/components/slider/snice-slider.js.map +1 -1
  231. package/dist/index.cjs +2 -2
  232. package/dist/index.esm.js +2 -2
  233. package/dist/index.iife.js +2 -2
  234. package/dist/symbols.cjs +1 -1
  235. package/dist/symbols.esm.js +1 -1
  236. package/dist/transitions.cjs +1 -1
  237. package/dist/transitions.esm.js +1 -1
  238. package/dist/types/request-options.d.ts +1 -1
  239. package/docs/ai/api.md +1 -1
  240. package/docs/ai/components/code-block.md +1 -1
  241. package/docs/ai/decorators.md +2 -2
  242. package/docs/ai/patterns.md +17 -6
  243. package/docs/code-block.md +1 -3
  244. package/docs/controllers.md +98 -391
  245. package/docs/elements.md +131 -117
  246. package/docs/events.md +74 -83
  247. package/docs/fetcher.md +64 -76
  248. package/docs/observe.md +13 -33
  249. package/docs/placards.md +6 -16
  250. package/docs/request-response.md +171 -693
  251. package/docs/routing.md +67 -136
  252. package/package.json +1 -1
  253. package/docs/migration-v2-to-v3.md +0 -569
@@ -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. Element's `ready` promise is awaited
66
- 4. `attach()` method is called
67
- 5. Event and channel handlers are set up
68
- 6. `@snice/controller-attached` event is dispatched
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. Event and channel handlers are cleaned up
75
- 4. Controller scope is cleaned up
76
- 5. `@snice/controller-detached` event is dispatched
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
- // Fetch data
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
- async detach(element: HTMLTableElement) {
172
- // Clear table
173
- const tbody = element.querySelector('tbody');
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
- private async fetchData() {
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
- Controllers should clean up resources in the `detach` method:
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('dashboard-controller')
296
- class DashboardController implements IController {
297
- element: HTMLElement | null = null;
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
- async attach(element: HTMLElement) {
309
- // Queries work on the attached element
310
- this.updateStatus('Loading...');
280
+ // light: true is required — native elements have no shadow root
281
+ @query('.error-message', { light: true })
282
+ errorEl?: HTMLElement;
311
283
 
312
- // Fetch and display data
313
- await this.loadDashboardData();
284
+ @queryAll('input[required]', { light: true })
285
+ requiredInputs?: NodeListOf<HTMLInputElement>;
314
286
 
315
- this.updateStatus('Ready');
316
- }
287
+ async attach() {}
288
+ async detach() {}
317
289
 
318
- async detach(element: HTMLElement) {
319
- this.updateStatus('Offline');
320
- }
290
+ @on('submit')
291
+ handleSubmit(event: Event) {
292
+ const invalid = Array.from(this.requiredInputs || []).filter(i => !i.value.trim());
321
293
 
322
- private updateStatus(status: string) {
323
- if (this.statusIndicator) {
324
- this.statusIndicator.textContent = status;
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
- // Initial data load
363
- await this.fetchAndRender();
319
+ await this.fetchData();
364
320
 
365
- // Set up polling
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(element: HTMLElement) {
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 fetchAndRender() {
382
- try {
383
- // Cancel previous request if still pending
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
- // Fetch data with timeout
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
- // Render data
402
- this.renderData(data);
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.renderError(error.message);
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
- ### State Management Controller
359
+ ### Theme Controller
451
360
 
452
361
  ```typescript
453
- interface AppState {
454
- user: { id: string; name: string } | null;
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
- // Load initial state
478
- await this.loadState();
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
- // Persist state
539
- this.saveState();
540
- }
541
-
542
- private updateElementWithState(element: HTMLElement, state: AppState) {
543
- // Update element based on state
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('websocket-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.disconnect();
397
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
398
+ this.ws?.close();
597
399
  }
598
400
 
599
401
  private connect() {
600
- try {
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
- // Dispatch event
404
+ this.ws.onmessage = (event) => {
672
405
  this.element?.dispatchEvent(new CustomEvent('ws-message', {
673
- detail: message,
406
+ detail: JSON.parse(event.data),
674
407
  bubbles: true
675
408
  }));
409
+ };
676
410
 
677
- } catch (error) {
678
- console.error('Failed to parse message:', error);
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
- private onConnected() {
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
- private onError(error: Event) {
702
- this.element?.classList.add('error');
703
- }
426
+ ### Via Event
704
427
 
705
- private onReconnectFailed() {
706
- this.element?.classList.add('reconnect-failed');
428
+ Listen for attachment on the element itself (the event does **not** bubble):
707
429
 
708
- // Show user notification
709
- if (this.element) {
710
- const notification = document.createElement('div');
711
- notification.className = 'connection-error';
712
- notification.textContent = 'Connection lost. Please refresh the page.';
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
- ## Controller Registry
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
- // Get controller instance from an element
727
- const element = document.querySelector('#my-element');
728
- const controller = getController(element);
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
- ## Best Practices
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