snice 2.5.4 → 3.1.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/README.md +501 -882
- package/bin/templates/base/src/components/counter-button.ts +13 -26
- package/bin/templates/base/src/controllers/counter-controller.ts +3 -3
- package/dist/components/accordion/snice-accordion-item.d.ts +4 -5
- package/dist/components/accordion/snice-accordion-item.js +37 -39
- package/dist/components/accordion/snice-accordion-item.js.map +1 -1
- package/dist/components/accordion/snice-accordion.d.ts +5 -11
- package/dist/components/accordion/snice-accordion.js +51 -52
- package/dist/components/accordion/snice-accordion.js.map +1 -1
- package/dist/components/alert/snice-alert.d.ts +2 -6
- package/dist/components/alert/snice-alert.js +41 -56
- package/dist/components/alert/snice-alert.js.map +1 -1
- package/dist/components/avatar/snice-avatar.d.ts +2 -6
- package/dist/components/avatar/snice-avatar.js +64 -71
- package/dist/components/avatar/snice-avatar.js.map +1 -1
- package/dist/components/badge/snice-badge.d.ts +2 -3
- package/dist/components/badge/snice-badge.js +22 -23
- package/dist/components/badge/snice-badge.js.map +1 -1
- package/dist/components/breadcrumbs/snice-breadcrumbs.d.ts +5 -12
- package/dist/components/breadcrumbs/snice-breadcrumbs.js +88 -89
- package/dist/components/breadcrumbs/snice-breadcrumbs.js.map +1 -1
- package/dist/components/button/snice-button.d.ts +3 -7
- package/dist/components/button/snice-button.js +37 -58
- package/dist/components/button/snice-button.js.map +1 -1
- package/dist/components/card/snice-card.d.ts +5 -8
- package/dist/components/card/snice-card.js +71 -56
- package/dist/components/card/snice-card.js.map +1 -1
- package/dist/components/checkbox/snice-checkbox.d.ts +4 -13
- package/dist/components/checkbox/snice-checkbox.js +66 -137
- package/dist/components/checkbox/snice-checkbox.js.map +1 -1
- package/dist/components/chip/snice-chip.d.ts +5 -11
- package/dist/components/chip/snice-chip.js +44 -47
- package/dist/components/chip/snice-chip.js.map +1 -1
- package/dist/components/date-picker/snice-date-picker.d.ts +11 -11
- package/dist/components/date-picker/snice-date-picker.js +134 -133
- package/dist/components/date-picker/snice-date-picker.js.map +1 -1
- package/dist/components/divider/snice-divider.d.ts +2 -4
- package/dist/components/divider/snice-divider.js +14 -22
- package/dist/components/divider/snice-divider.js.map +1 -1
- package/dist/components/drawer/snice-drawer.d.ts +4 -4
- package/dist/components/drawer/snice-drawer.js +25 -19
- package/dist/components/drawer/snice-drawer.js.map +1 -1
- package/dist/components/input/snice-input.d.ts +8 -6
- package/dist/components/input/snice-input.js +122 -105
- package/dist/components/input/snice-input.js.map +1 -1
- package/dist/components/layout/snice-layout-blog.d.ts +4 -4
- package/dist/components/layout/snice-layout-blog.js +21 -19
- package/dist/components/layout/snice-layout-blog.js.map +1 -1
- package/dist/components/layout/snice-layout-card.d.ts +2 -2
- package/dist/components/layout/snice-layout-card.js +16 -9
- package/dist/components/layout/snice-layout-card.js.map +1 -1
- package/dist/components/layout/snice-layout-centered.d.ts +2 -2
- package/dist/components/layout/snice-layout-centered.js +14 -7
- package/dist/components/layout/snice-layout-centered.js.map +1 -1
- package/dist/components/layout/snice-layout-dashboard.d.ts +5 -5
- package/dist/components/layout/snice-layout-dashboard.js +38 -30
- package/dist/components/layout/snice-layout-dashboard.js.map +1 -1
- package/dist/components/layout/snice-layout-fullscreen.d.ts +2 -2
- package/dist/components/layout/snice-layout-fullscreen.js +17 -10
- package/dist/components/layout/snice-layout-fullscreen.js.map +1 -1
- package/dist/components/layout/snice-layout-landing.d.ts +4 -4
- package/dist/components/layout/snice-layout-landing.js +21 -19
- package/dist/components/layout/snice-layout-landing.js.map +1 -1
- package/dist/components/layout/snice-layout-minimal.d.ts +2 -2
- package/dist/components/layout/snice-layout-minimal.js +17 -6
- package/dist/components/layout/snice-layout-minimal.js.map +1 -1
- package/dist/components/layout/snice-layout-sidebar.d.ts +5 -4
- package/dist/components/layout/snice-layout-sidebar.js +42 -20
- package/dist/components/layout/snice-layout-sidebar.js.map +1 -1
- package/dist/components/layout/snice-layout-split.d.ts +2 -2
- package/dist/components/layout/snice-layout-split.js +14 -7
- package/dist/components/layout/snice-layout-split.js.map +1 -1
- package/dist/components/layout/snice-layout.d.ts +4 -4
- package/dist/components/layout/snice-layout.js +16 -10
- package/dist/components/layout/snice-layout.js.map +1 -1
- package/dist/components/login/snice-login.d.ts +6 -11
- package/dist/components/login/snice-login.js +97 -71
- package/dist/components/login/snice-login.js.map +1 -1
- package/dist/components/modal/snice-modal.d.ts +5 -9
- package/dist/components/modal/snice-modal.js +47 -78
- package/dist/components/modal/snice-modal.js.map +1 -1
- package/dist/components/nav/snice-nav.d.ts +13 -7
- package/dist/components/nav/snice-nav.js +191 -100
- package/dist/components/nav/snice-nav.js.map +1 -1
- package/dist/components/nav/snice-nav.types.d.ts +3 -3
- package/dist/components/pagination/snice-pagination.d.ts +6 -7
- package/dist/components/pagination/snice-pagination.js +94 -81
- package/dist/components/pagination/snice-pagination.js.map +1 -1
- package/dist/components/progress/snice-progress.d.ts +2 -7
- package/dist/components/progress/snice-progress.js +41 -98
- package/dist/components/progress/snice-progress.js.map +1 -1
- package/dist/components/radio/snice-radio.d.ts +4 -4
- package/dist/components/radio/snice-radio.js +52 -44
- package/dist/components/radio/snice-radio.js.map +1 -1
- package/dist/components/select/snice-option.d.ts +2 -1
- package/dist/components/select/snice-option.js +12 -5
- package/dist/components/select/snice-option.js.map +1 -1
- package/dist/components/select/snice-select.d.ts +9 -21
- package/dist/components/select/snice-select.js +98 -170
- package/dist/components/select/snice-select.js.map +1 -1
- package/dist/components/skeleton/snice-skeleton.d.ts +2 -6
- package/dist/components/skeleton/snice-skeleton.js +18 -49
- package/dist/components/skeleton/snice-skeleton.js.map +1 -1
- package/dist/components/snice-cell-BLFVdxPp.js +4 -0
- package/dist/components/snice-cell-BLFVdxPp.js.map +1 -0
- package/dist/components/switch/snice-switch.d.ts +2 -2
- package/dist/components/switch/snice-switch.js +38 -26
- package/dist/components/switch/snice-switch.js.map +1 -1
- package/dist/components/table/snice-cell-actions.d.ts +24 -0
- package/dist/components/table/snice-cell-actions.js +149 -0
- package/dist/components/table/snice-cell-actions.js.map +1 -0
- package/dist/components/table/snice-cell-boolean.d.ts +2 -2
- package/dist/components/table/snice-cell-boolean.js +13 -7
- package/dist/components/table/snice-cell-boolean.js.map +1 -1
- package/dist/components/table/snice-cell-color.d.ts +18 -0
- package/dist/components/table/snice-cell-color.js +149 -0
- package/dist/components/table/snice-cell-color.js.map +1 -0
- package/dist/components/table/snice-cell-currency.d.ts +24 -0
- package/dist/components/table/snice-cell-currency.js +235 -0
- package/dist/components/table/snice-cell-currency.js.map +1 -0
- package/dist/components/table/snice-cell-date.d.ts +2 -2
- package/dist/components/table/snice-cell-date.js +14 -8
- package/dist/components/table/snice-cell-date.js.map +1 -1
- package/dist/components/table/snice-cell-duration.d.ts +2 -2
- package/dist/components/table/snice-cell-duration.js +12 -6
- package/dist/components/table/snice-cell-duration.js.map +1 -1
- package/dist/components/table/snice-cell-email.d.ts +15 -0
- package/dist/components/table/snice-cell-email.js +125 -0
- package/dist/components/table/snice-cell-email.js.map +1 -0
- package/dist/components/table/snice-cell-filesize.d.ts +2 -2
- package/dist/components/table/snice-cell-filesize.js +12 -6
- package/dist/components/table/snice-cell-filesize.js.map +1 -1
- package/dist/components/table/snice-cell-image.d.ts +20 -0
- package/dist/components/table/snice-cell-image.js +162 -0
- package/dist/components/table/snice-cell-image.js.map +1 -0
- package/dist/components/table/snice-cell-json.d.ts +20 -0
- package/dist/components/table/snice-cell-json.js +186 -0
- package/dist/components/table/snice-cell-json.js.map +1 -0
- package/dist/components/table/snice-cell-link.d.ts +17 -0
- package/dist/components/table/snice-cell-link.js +142 -0
- package/dist/components/table/snice-cell-link.js.map +1 -0
- package/dist/components/table/snice-cell-location.d.ts +19 -0
- package/dist/components/table/snice-cell-location.js +185 -0
- package/dist/components/table/snice-cell-location.js.map +1 -0
- package/dist/components/table/snice-cell-number.d.ts +2 -2
- package/dist/components/table/snice-cell-number.js +12 -6
- package/dist/components/table/snice-cell-number.js.map +1 -1
- package/dist/components/table/snice-cell-percentage.d.ts +22 -0
- package/dist/components/table/snice-cell-percentage.js +208 -0
- package/dist/components/table/snice-cell-percentage.js.map +1 -0
- package/dist/components/table/snice-cell-phone.d.ts +18 -0
- package/dist/components/table/snice-cell-phone.js +153 -0
- package/dist/components/table/snice-cell-phone.js.map +1 -0
- package/dist/components/table/snice-cell-progress.d.ts +2 -2
- package/dist/components/table/snice-cell-progress.js +12 -6
- package/dist/components/table/snice-cell-progress.js.map +1 -1
- package/dist/components/table/snice-cell-rating.d.ts +2 -2
- package/dist/components/table/snice-cell-rating.js +12 -6
- package/dist/components/table/snice-cell-rating.js.map +1 -1
- package/dist/components/table/snice-cell-sparkline.d.ts +2 -2
- package/dist/components/table/snice-cell-sparkline.js +13 -7
- package/dist/components/table/snice-cell-sparkline.js.map +1 -1
- package/dist/components/table/snice-cell-status.d.ts +17 -0
- package/dist/components/table/snice-cell-status.js +144 -0
- package/dist/components/table/snice-cell-status.js.map +1 -0
- package/dist/components/table/snice-cell-tag.d.ts +16 -0
- package/dist/components/table/snice-cell-tag.js +131 -0
- package/dist/components/table/snice-cell-tag.js.map +1 -0
- package/dist/components/table/snice-cell-text.d.ts +2 -2
- package/dist/components/table/snice-cell-text.js +14 -8
- package/dist/components/table/snice-cell-text.js.map +1 -1
- package/dist/components/table/snice-cell.d.ts +2 -2
- package/dist/components/table/snice-cell.js +12 -6
- package/dist/components/table/snice-cell.js.map +1 -1
- package/dist/components/table/snice-column.d.ts +1 -1
- package/dist/components/table/snice-column.js +6 -3
- package/dist/components/table/snice-column.js.map +1 -1
- package/dist/components/table/snice-header.d.ts +5 -5
- package/dist/components/table/snice-header.js +60 -50
- package/dist/components/table/snice-header.js.map +1 -1
- package/dist/components/table/snice-progress.d.ts +2 -2
- package/dist/components/table/snice-progress.js +18 -11
- package/dist/components/table/snice-progress.js.map +1 -1
- package/dist/components/table/snice-rating.d.ts +2 -2
- package/dist/components/table/snice-rating.js +15 -8
- package/dist/components/table/snice-rating.js.map +1 -1
- package/dist/components/table/snice-row.d.ts +17 -6
- package/dist/components/table/snice-row.js +95 -44
- package/dist/components/table/snice-row.js.map +1 -1
- package/dist/components/table/snice-table.d.ts +18 -10
- package/dist/components/table/snice-table.js +355 -173
- package/dist/components/table/snice-table.js.map +1 -1
- package/dist/components/table/snice-table.types.d.ts +101 -2
- package/dist/components/tabs/snice-tab-panel.d.ts +2 -2
- package/dist/components/tabs/snice-tab-panel.js +12 -6
- package/dist/components/tabs/snice-tab-panel.js.map +1 -1
- package/dist/components/tabs/snice-tab.d.ts +6 -5
- package/dist/components/tabs/snice-tab.js +36 -19
- package/dist/components/tabs/snice-tab.js.map +1 -1
- package/dist/components/tabs/snice-tabs.d.ts +5 -5
- package/dist/components/tabs/snice-tabs.js +38 -28
- package/dist/components/tabs/snice-tabs.js.map +1 -1
- package/dist/components/toast/snice-toast-container.d.ts +7 -7
- package/dist/components/toast/snice-toast-container.js +19 -12
- package/dist/components/toast/snice-toast-container.js.map +1 -1
- package/dist/components/toast/snice-toast.d.ts +3 -15
- package/dist/components/toast/snice-toast.js +49 -108
- package/dist/components/toast/snice-toast.js.map +1 -1
- package/dist/components/tooltip/snice-tooltip.d.ts +2 -2
- package/dist/components/tooltip/snice-tooltip.js +14 -7
- package/dist/components/tooltip/snice-tooltip.js.map +1 -1
- package/dist/context.d.ts +44 -0
- package/dist/element-ready.d.ts +40 -0
- package/dist/{types/element.d.ts → element.d.ts} +2 -8
- package/dist/{types/events.d.ts → events.d.ts} +0 -4
- package/dist/index.cjs +2589 -605
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.esm.js +2568 -604
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +2589 -605
- package/dist/index.iife.js.map +1 -1
- package/dist/method-decorators.d.ts +121 -0
- package/dist/on.d.ts +59 -0
- package/dist/parts.d.ts +159 -0
- package/dist/render-debug.d.ts +27 -0
- package/dist/render-tracker.d.ts +14 -0
- package/dist/render.d.ts +96 -0
- package/dist/symbols.cjs +163 -0
- package/dist/symbols.cjs.map +1 -1
- package/dist/{types/symbols.d.ts → symbols.d.ts} +22 -0
- package/dist/symbols.esm.js +27 -3
- package/dist/symbols.esm.js.map +1 -1
- package/dist/template.d.ts +100 -0
- package/dist/transitions.cjs +219 -0
- package/dist/transitions.esm.js +2 -2
- package/dist/types/context.d.ts +48 -0
- package/dist/types/element-options.d.ts +26 -0
- package/dist/types/index.d.ts +25 -9
- package/dist/types/nav-context.d.ts +19 -0
- package/dist/types/{types/on-options.d.ts → on-options.d.ts} +2 -0
- package/dist/types/{types/placard.d.ts → placard.d.ts} +0 -1
- package/docs/ai/README.md +17 -0
- package/docs/ai/api.md +175 -0
- package/docs/ai/architecture.md +160 -0
- package/docs/ai/components/accordion.md +174 -0
- package/docs/ai/components/alert.md +77 -0
- package/docs/ai/components/avatar.md +61 -0
- package/docs/ai/components/badge.md +69 -0
- package/docs/ai/components/breadcrumbs.md +74 -0
- package/docs/ai/components/button.md +75 -0
- package/docs/ai/components/card.md +61 -0
- package/docs/ai/components/checkbox.md +74 -0
- package/docs/ai/components/chip.md +73 -0
- package/docs/ai/components/date-picker.md +75 -0
- package/docs/ai/components/divider.md +66 -0
- package/docs/ai/components/drawer.md +80 -0
- package/docs/ai/components/input.md +111 -0
- package/docs/ai/components/login.md +109 -0
- package/docs/ai/components/modal.md +67 -0
- package/docs/ai/components/nav.md +76 -0
- package/docs/ai/components/pagination.md +55 -0
- package/docs/ai/components/progress.md +72 -0
- package/docs/ai/components/radio.md +79 -0
- package/docs/ai/components/select.md +92 -0
- package/docs/ai/components/skeleton.md +57 -0
- package/docs/ai/components/switch.md +53 -0
- package/docs/ai/components/table.md +227 -0
- package/docs/ai/components/tabs.md +83 -0
- package/docs/ai/components/toast.md +140 -0
- package/docs/ai/components/tooltip.md +146 -0
- package/docs/ai/patterns.md +244 -0
- package/docs/components/accordion.md +558 -0
- package/docs/components/drawer.md +602 -0
- package/docs/components/modal.md +558 -0
- package/docs/components/nav.md +239 -0
- package/docs/components/pagination.md +289 -0
- package/docs/components/select.md +599 -0
- package/docs/components/switch.md +354 -0
- package/docs/components/tabs.md +546 -0
- package/docs/components/toast.md +506 -0
- package/docs/components/tooltip.md +523 -0
- package/docs/controllers.md +744 -0
- package/docs/elements.md +855 -0
- package/docs/events.md +807 -0
- package/docs/migration-v2-to-v3.md +569 -0
- package/docs/observe.md +588 -0
- package/docs/placards.md +401 -0
- package/docs/request-response.md +852 -0
- package/docs/routing.md +1186 -0
- package/package.json +10 -11
- package/dist/components/snice-cell-C9N6yGxQ.js +0 -4
- package/dist/components/snice-cell-C9N6yGxQ.js.map +0 -1
- package/dist/types/types/index.d.ts +0 -23
- /package/dist/{types/controller.d.ts → controller.d.ts} +0 -0
- /package/dist/{types/global.d.ts → global.d.ts} +0 -0
- /package/dist/{types/observe.d.ts → observe.d.ts} +0 -0
- /package/dist/{types/request-response.d.ts → request-response.d.ts} +0 -0
- /package/dist/{types/router.d.ts → router.d.ts} +0 -0
- /package/dist/{types/testing.d.ts → testing.d.ts} +0 -0
- /package/dist/{types/transitions.d.ts → transitions.d.ts} +0 -0
- /package/dist/types/{types/adopted-options.d.ts → adopted-options.d.ts} +0 -0
- /package/dist/types/{types/app-context.d.ts → app-context.d.ts} +0 -0
- /package/dist/types/{types/dispatch-options.d.ts → dispatch-options.d.ts} +0 -0
- /package/dist/types/{types/guard.d.ts → guard.d.ts} +0 -0
- /package/dist/types/{types/i-controller.d.ts → i-controller.d.ts} +0 -0
- /package/dist/types/{types/moved-options.d.ts → moved-options.d.ts} +0 -0
- /package/dist/types/{types/observe-options.d.ts → observe-options.d.ts} +0 -0
- /package/dist/types/{types/page-options.d.ts → page-options.d.ts} +0 -0
- /package/dist/types/{types/part-options.d.ts → part-options.d.ts} +0 -0
- /package/dist/types/{types/property-converter.d.ts → property-converter.d.ts} +0 -0
- /package/dist/types/{types/property-options.d.ts → property-options.d.ts} +0 -0
- /package/dist/types/{types/query-options.d.ts → query-options.d.ts} +0 -0
- /package/dist/types/{types/request-options.d.ts → request-options.d.ts} +0 -0
- /package/dist/types/{types/respond-options.d.ts → respond-options.d.ts} +0 -0
- /package/dist/types/{types/route-params.d.ts → route-params.d.ts} +0 -0
- /package/dist/types/{types/router-instance.d.ts → router-instance.d.ts} +0 -0
- /package/dist/types/{types/router-options.d.ts → router-options.d.ts} +0 -0
- /package/dist/types/{types/simple-array.d.ts → simple-array.d.ts} +0 -0
- /package/dist/types/{types/snice-element.d.ts → snice-element.d.ts} +0 -0
- /package/dist/types/{types/snice-global.d.ts → snice-global.d.ts} +0 -0
- /package/dist/types/{types/transition.d.ts → transition.d.ts} +0 -0
- /package/dist/{types/utils.d.ts → utils.d.ts} +0 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
# Controllers API Documentation
|
|
2
|
+
|
|
3
|
+
Controllers handle data fetching, business logic, and server communication separately from visual components. They can be attached to any HTML element, including native elements.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Basic Usage](#basic-usage)
|
|
7
|
+
- [Controller Lifecycle](#controller-lifecycle)
|
|
8
|
+
- [Native Element Controllers](#native-element-controllers)
|
|
9
|
+
- [Resource Cleanup](#resource-cleanup)
|
|
10
|
+
- [Event Handling in Controllers](#event-handling-in-controllers)
|
|
11
|
+
- [Query Selectors in Controllers](#query-selectors-in-controllers)
|
|
12
|
+
- [Advanced Patterns](#advanced-patterns)
|
|
13
|
+
|
|
14
|
+
## Basic Usage
|
|
15
|
+
|
|
16
|
+
### Creating a Controller
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { controller, IController } from 'snice';
|
|
20
|
+
|
|
21
|
+
@controller('user-controller')
|
|
22
|
+
class UserController implements IController<HTMLElement> {
|
|
23
|
+
element: HTMLElement | null = null;
|
|
24
|
+
|
|
25
|
+
async attach(element: HTMLElement) {
|
|
26
|
+
// Called when controller is attached to an element
|
|
27
|
+
console.log('Controller attached to', element);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async detach(element: HTMLElement) {
|
|
31
|
+
// Called when controller is detached from an element
|
|
32
|
+
console.log('Controller detached from', element);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Attaching Controllers to Elements
|
|
38
|
+
|
|
39
|
+
Controllers can be attached via the `controller` attribute:
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<!-- Custom element -->
|
|
43
|
+
<user-list controller="user-controller"></user-list>
|
|
44
|
+
|
|
45
|
+
<!-- Native element (requires useNativeElementControllers()) -->
|
|
46
|
+
<div controller="user-controller"></div>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### IController Interface
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
interface IController<T extends HTMLElement = HTMLElement> {
|
|
53
|
+
element: T | null | undefined;
|
|
54
|
+
attach(element: T): void | Promise<void>;
|
|
55
|
+
detach(element: T): void | Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Controller Lifecycle
|
|
60
|
+
|
|
61
|
+
### Attachment Flow
|
|
62
|
+
|
|
63
|
+
1. Controller instance is created
|
|
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
|
|
69
|
+
|
|
70
|
+
### Detachment Flow
|
|
71
|
+
|
|
72
|
+
1. `detach()` method is called
|
|
73
|
+
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
|
+
|
|
78
|
+
### Example with Lifecycle Logging
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
@controller('lifecycle-controller')
|
|
82
|
+
class LifecycleController implements IController {
|
|
83
|
+
element: HTMLElement | null = null;
|
|
84
|
+
private intervalId?: number;
|
|
85
|
+
|
|
86
|
+
async attach(element: HTMLElement) {
|
|
87
|
+
console.log('1. Controller attaching to', element.tagName);
|
|
88
|
+
|
|
89
|
+
// Wait for any async initialization
|
|
90
|
+
await this.initialize();
|
|
91
|
+
|
|
92
|
+
// Set up recurring tasks
|
|
93
|
+
this.intervalId = setInterval(() => {
|
|
94
|
+
this.updateData();
|
|
95
|
+
}, 5000);
|
|
96
|
+
|
|
97
|
+
console.log('2. Controller attached');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async detach(element: HTMLElement) {
|
|
101
|
+
console.log('3. Controller detaching from', element.tagName);
|
|
102
|
+
|
|
103
|
+
// Clean up resources
|
|
104
|
+
if (this.intervalId) {
|
|
105
|
+
clearInterval(this.intervalId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Perform async cleanup
|
|
109
|
+
await this.cleanup();
|
|
110
|
+
|
|
111
|
+
console.log('4. Controller detached');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async initialize() {
|
|
115
|
+
// Async initialization logic
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async cleanup() {
|
|
119
|
+
// Async cleanup logic
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private updateData() {
|
|
123
|
+
console.log('Updating data...');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Native Element Controllers
|
|
129
|
+
|
|
130
|
+
Enable controller support for native HTML elements:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { useNativeElementControllers } from 'snice';
|
|
134
|
+
|
|
135
|
+
// Enable at application start
|
|
136
|
+
useNativeElementControllers();
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
This allows you to attach controllers to any HTML element:
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<div controller="content-controller">
|
|
143
|
+
<p>Content managed by controller</p>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<table controller="table-controller">
|
|
147
|
+
<tbody></tbody>
|
|
148
|
+
</table>
|
|
149
|
+
|
|
150
|
+
<form controller="form-controller">
|
|
151
|
+
<input type="text" name="username">
|
|
152
|
+
</form>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Example: Table Controller
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
@controller('table-controller')
|
|
159
|
+
class TableController implements IController<HTMLTableElement> {
|
|
160
|
+
element: HTMLTableElement | null = null;
|
|
161
|
+
private data: any[] = [];
|
|
162
|
+
|
|
163
|
+
async attach(element: HTMLTableElement) {
|
|
164
|
+
// Fetch data
|
|
165
|
+
this.data = await this.fetchData();
|
|
166
|
+
|
|
167
|
+
// Render table
|
|
168
|
+
this.renderTable();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async detach(element: HTMLTableElement) {
|
|
172
|
+
// Clear table
|
|
173
|
+
const tbody = element.querySelector('tbody');
|
|
174
|
+
if (tbody) {
|
|
175
|
+
tbody.innerHTML = '';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
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
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Resource Cleanup
|
|
202
|
+
|
|
203
|
+
Controllers should clean up resources in the `detach` method:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { controller, IController } from 'snice';
|
|
207
|
+
|
|
208
|
+
@controller('resource-controller')
|
|
209
|
+
class ResourceController implements IController {
|
|
210
|
+
element: HTMLElement | null = null;
|
|
211
|
+
private websocket?: WebSocket;
|
|
212
|
+
private eventHandler?: (e: MessageEvent) => void;
|
|
213
|
+
|
|
214
|
+
async attach(element: HTMLElement) {
|
|
215
|
+
// Open websocket
|
|
216
|
+
this.websocket = new WebSocket('ws://localhost:8080');
|
|
217
|
+
|
|
218
|
+
// Set up event listener
|
|
219
|
+
this.eventHandler = (e: MessageEvent) => this.handleMessage(e);
|
|
220
|
+
this.websocket.addEventListener('message', this.eventHandler);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async detach(element: HTMLElement) {
|
|
224
|
+
// Clean up resources
|
|
225
|
+
if (this.websocket) {
|
|
226
|
+
if (this.eventHandler) {
|
|
227
|
+
this.websocket.removeEventListener('message', this.eventHandler);
|
|
228
|
+
}
|
|
229
|
+
this.websocket.close();
|
|
230
|
+
this.websocket = undefined;
|
|
231
|
+
}
|
|
232
|
+
this.eventHandler = undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private handleMessage(event: MessageEvent) {
|
|
236
|
+
console.log('Received:', event.data);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Event Handling in Controllers
|
|
242
|
+
|
|
243
|
+
Controllers can use the `@on` decorator to handle events from their attached element:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { controller, on, IController } from 'snice';
|
|
247
|
+
|
|
248
|
+
@controller('form-controller')
|
|
249
|
+
class FormController implements IController<HTMLFormElement> {
|
|
250
|
+
element: HTMLFormElement | null = null;
|
|
251
|
+
|
|
252
|
+
async attach(element: HTMLFormElement) {
|
|
253
|
+
console.log('Form controller attached');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async detach(element: HTMLFormElement) {
|
|
257
|
+
console.log('Form controller detached');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@on('submit')
|
|
261
|
+
handleSubmit(event: Event) {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
console.log('Form submitted');
|
|
264
|
+
this.processForm();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@on('input', 'input[type="text"]')
|
|
268
|
+
handleTextInput(event: Event) {
|
|
269
|
+
const input = event.target as HTMLInputElement;
|
|
270
|
+
console.log('Text input changed:', input.value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@on('change', 'select')
|
|
274
|
+
handleSelectChange(event: Event) {
|
|
275
|
+
const select = event.target as HTMLSelectElement;
|
|
276
|
+
console.log('Select changed:', select.value);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private processForm() {
|
|
280
|
+
if (!this.element) return;
|
|
281
|
+
|
|
282
|
+
const formData = new FormData(this.element);
|
|
283
|
+
console.log('Processing form data:', Object.fromEntries(formData));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Query Selectors in Controllers
|
|
289
|
+
|
|
290
|
+
Controllers can use `@query` and `@queryAll` to access elements:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { controller, query, queryAll, IController } from 'snice';
|
|
294
|
+
|
|
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>;
|
|
307
|
+
|
|
308
|
+
async attach(element: HTMLElement) {
|
|
309
|
+
// Queries work on the attached element
|
|
310
|
+
this.updateStatus('Loading...');
|
|
311
|
+
|
|
312
|
+
// Fetch and display data
|
|
313
|
+
await this.loadDashboardData();
|
|
314
|
+
|
|
315
|
+
this.updateStatus('Ready');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async detach(element: HTMLElement) {
|
|
319
|
+
this.updateStatus('Offline');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private updateStatus(status: string) {
|
|
323
|
+
if (this.statusIndicator) {
|
|
324
|
+
this.statusIndicator.textContent = status;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
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
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Advanced Patterns
|
|
351
|
+
|
|
352
|
+
### Data Fetching Controller
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
@controller('data-fetcher')
|
|
356
|
+
class DataFetcherController implements IController {
|
|
357
|
+
element: HTMLElement | null = null;
|
|
358
|
+
private abortController?: AbortController;
|
|
359
|
+
private pollingInterval?: number;
|
|
360
|
+
|
|
361
|
+
async attach(element: HTMLElement) {
|
|
362
|
+
// Initial data load
|
|
363
|
+
await this.fetchAndRender();
|
|
364
|
+
|
|
365
|
+
// Set up polling
|
|
366
|
+
this.pollingInterval = setInterval(() => {
|
|
367
|
+
this.fetchAndRender();
|
|
368
|
+
}, 30000); // Poll every 30 seconds
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async detach(element: HTMLElement) {
|
|
372
|
+
// Cancel any pending requests
|
|
373
|
+
this.abortController?.abort();
|
|
374
|
+
|
|
375
|
+
// Stop polling
|
|
376
|
+
if (this.pollingInterval) {
|
|
377
|
+
clearInterval(this.pollingInterval);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
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);
|
|
389
|
+
|
|
390
|
+
// Fetch data with timeout
|
|
391
|
+
const response = await fetch('/api/data', {
|
|
392
|
+
signal: this.abortController.signal
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (!response.ok) {
|
|
396
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const data = await response.json();
|
|
400
|
+
|
|
401
|
+
// Render data
|
|
402
|
+
this.renderData(data);
|
|
403
|
+
|
|
404
|
+
} catch (error: any) {
|
|
405
|
+
if (error.name !== 'AbortError') {
|
|
406
|
+
this.renderError(error.message);
|
|
407
|
+
}
|
|
408
|
+
} finally {
|
|
409
|
+
this.setLoadingState(false);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
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
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### State Management Controller
|
|
451
|
+
|
|
452
|
+
```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 {
|
|
467
|
+
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
|
+
|
|
476
|
+
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);
|
|
489
|
+
}
|
|
490
|
+
|
|
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));
|
|
537
|
+
|
|
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
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### WebSocket Controller
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
@controller('websocket-controller')
|
|
583
|
+
class WebSocketController implements IController {
|
|
584
|
+
element: HTMLElement | null = null;
|
|
585
|
+
private ws?: WebSocket;
|
|
586
|
+
private reconnectTimer?: number;
|
|
587
|
+
private reconnectAttempts = 0;
|
|
588
|
+
private maxReconnectAttempts = 5;
|
|
589
|
+
private reconnectDelay = 1000; // Start with 1 second
|
|
590
|
+
|
|
591
|
+
async attach(element: HTMLElement) {
|
|
592
|
+
this.connect();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async detach(element: HTMLElement) {
|
|
596
|
+
this.disconnect();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
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
|
+
}
|
|
670
|
+
|
|
671
|
+
// Dispatch event
|
|
672
|
+
this.element?.dispatchEvent(new CustomEvent('ws-message', {
|
|
673
|
+
detail: message,
|
|
674
|
+
bubbles: true
|
|
675
|
+
}));
|
|
676
|
+
|
|
677
|
+
} catch (error) {
|
|
678
|
+
console.error('Failed to parse message:', error);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
send(data: any) {
|
|
683
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
684
|
+
this.ws.send(JSON.stringify(data));
|
|
685
|
+
} else {
|
|
686
|
+
console.warn('WebSocket not connected, queuing message');
|
|
687
|
+
// Could implement message queue here
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
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
|
+
}
|
|
700
|
+
|
|
701
|
+
private onError(error: Event) {
|
|
702
|
+
this.element?.classList.add('error');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private onReconnectFailed() {
|
|
706
|
+
this.element?.classList.add('reconnect-failed');
|
|
707
|
+
|
|
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
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
## Controller Registry
|
|
720
|
+
|
|
721
|
+
Controllers are automatically registered when decorated with `@controller`:
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
import { getController } from 'snice';
|
|
725
|
+
|
|
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);
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
## Best Practices
|
|
736
|
+
|
|
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
|