snice 2.5.3 → 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/bin/templates/base/src/router.ts +0 -7
- 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
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Snice
|
|
1
|
+
# Snice v3.0.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **AI Assistants:** For token-efficient documentation, read [docs/ai/](./docs/ai/) instead. Same content, 70% fewer tokens.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A TypeScript framework for building sustainable web applications through clear separation of governance.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Quick Start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npx snice create-app my-app
|
|
@@ -12,1101 +12,720 @@ cd my-app
|
|
|
12
12
|
npm run dev
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Philosophy: Sustainable Architecture Through Separation of Concerns
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Most frameworks separate code by technology: HTML, CSS, JavaScript. Snice separates by **concerns of governance and data flow**. This shepherds you toward sustainable development by providing tools that encourage good practices.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
- **Never re-render** automatically
|
|
21
|
-
- Require **explicit method calls** to update visual state
|
|
22
|
-
- Give you **full control** over when and how updates happen
|
|
19
|
+
### The Snice Architecture
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
Every application has four distinct concerns:
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
**1. Cross-Cutting Concerns** - Global state and navigation
|
|
24
|
+
- Authentication/principal
|
|
25
|
+
- Theming and preferences
|
|
26
|
+
- Routing and navigation
|
|
27
|
+
- Application-wide configuration
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
**2. Pages** - Code that handles human intent
|
|
30
|
+
- Understand what the user is trying to accomplish
|
|
31
|
+
- Orchestrate atomic elements to fulfill that intent
|
|
32
|
+
- Handle page-level data fetching and coordination
|
|
33
|
+
- Map URL parameters to user goals
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
**3. Elements** - Pure presentation
|
|
36
|
+
- Display data, nothing more
|
|
37
|
+
- No understanding of business logic or user intent
|
|
38
|
+
- Completely reusable across different contexts
|
|
39
|
+
- Concerned only with how things look and visual behaviors
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
**4. Controllers** - Behavior and data management
|
|
42
|
+
- Handle server communication and data fetching when applicable
|
|
43
|
+
- Manage complex business logic when pages get too large
|
|
44
|
+
- Enable behavior swapping (A/B testing, feature flags)
|
|
45
|
+
- Clear separation of presentation from behavior
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
@element('user-card')
|
|
40
|
-
class UserCard extends HTMLElement implements IUserCard {
|
|
41
|
-
@property({ attribute: 'user-id' })
|
|
42
|
-
userId = '';
|
|
43
|
-
|
|
44
|
-
@query('h3')
|
|
45
|
-
nameElement!: HTMLHeadingElement;
|
|
46
|
-
|
|
47
|
-
@query('p')
|
|
48
|
-
emailElement!: HTMLParagraphElement;
|
|
49
|
-
|
|
50
|
-
html() {
|
|
51
|
-
return `
|
|
52
|
-
<div class="card">
|
|
53
|
-
<h3>Loading...</h3>
|
|
54
|
-
<p>Please wait...</p>
|
|
55
|
-
</div>
|
|
56
|
-
`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
showUser(user: any) {
|
|
60
|
-
this.nameElement.textContent = user.name;
|
|
61
|
-
this.emailElement.textContent = user.email;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
47
|
+
This architecture ensures **pages orchestrate**, **elements present**, and **controllers behave**. Data flows down, events flow up, and every piece knows only what it needs to know.
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
@controller('user-loader')
|
|
67
|
-
class UserLoaderController {
|
|
68
|
-
element!: IUserCard;
|
|
69
|
-
|
|
70
|
-
async attach(element: IUserCard) {
|
|
71
|
-
const response = await fetch(`/api/users/${element.userId}`);
|
|
72
|
-
const user = await response.json();
|
|
73
|
-
|
|
74
|
-
element.showUser(user);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async detach(element: IUserCard) { /* Cleanup */ }
|
|
78
|
-
}
|
|
79
|
-
```
|
|
49
|
+
## Why This Matters
|
|
80
50
|
|
|
81
|
-
|
|
82
|
-
```html
|
|
83
|
-
<user-card user-id="123" controller="user-loader"></user-card>
|
|
84
|
-
```
|
|
51
|
+
Traditional component architectures blur these lines. A "UserProfile" component might handle routing, authentication, API calls, and rendering. When requirements change, you can't swap behavior without touching presentation. When you need to reuse the UI, you can't because it's coupled to specific business logic.
|
|
85
52
|
|
|
86
|
-
|
|
53
|
+
Snice encourages boundaries:
|
|
54
|
+
- Want different behavior? Swap the controller, keep the element
|
|
55
|
+
- Need to reuse UI? Elements don't know about your business logic
|
|
56
|
+
- Debugging data flow? Follow the clear page → element → controller boundaries
|
|
57
|
+
- Onboarding new developers? The architecture guides them to the right place
|
|
87
58
|
|
|
88
|
-
##
|
|
59
|
+
## The Tools
|
|
89
60
|
|
|
90
|
-
Snice provides
|
|
61
|
+
Snice provides decorators and utilities that map directly to these architectural concerns:
|
|
91
62
|
|
|
92
|
-
###
|
|
93
|
-
- **`@element`** - Creates custom HTML elements with encapsulated visual behavior and styling
|
|
94
|
-
- **`@controller`** - Handles data fetching, server communication, and business logic separate from visual components
|
|
95
|
-
- **`@page`** - Defines routable page components that render when their route is active, with URL params passed as attributes
|
|
63
|
+
### 1. Cross-Cutting Concerns: Router + Context
|
|
96
64
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
- **`@ready`** - Runs a method after the element's shadow DOM is ready
|
|
103
|
-
- **`@dispose`** - Runs a method when the element is removed from the DOM
|
|
65
|
+
```typescript
|
|
66
|
+
// app-context.ts
|
|
67
|
+
class AppContext {
|
|
68
|
+
user: User | null = null;
|
|
69
|
+
theme: 'light' | 'dark' = 'light';
|
|
104
70
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
- **`@request`** - Makes requests from elements or controllers
|
|
109
|
-
- **`@respond`** - Responds to requests in elements or controllers
|
|
71
|
+
setUser(user: User) { this.user = user; }
|
|
72
|
+
getUser() { return this.user; }
|
|
73
|
+
}
|
|
110
74
|
|
|
111
|
-
|
|
75
|
+
// main.ts
|
|
76
|
+
import { Router } from 'snice';
|
|
112
77
|
|
|
113
|
-
|
|
78
|
+
const { page, navigate, initialize } = Router({
|
|
79
|
+
target: '#app',
|
|
80
|
+
context: new AppContext() // Global state
|
|
81
|
+
});
|
|
114
82
|
|
|
115
|
-
|
|
116
|
-
|
|
83
|
+
// Any page can access context
|
|
84
|
+
@page({ tag: 'dashboard-page', routes: ['/dashboard'] })
|
|
85
|
+
class DashboardPage extends HTMLElement {
|
|
86
|
+
private appContext?: AppContext;
|
|
117
87
|
|
|
118
|
-
@
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
88
|
+
@context()
|
|
89
|
+
handleContext(ctx: Context) {
|
|
90
|
+
this.appContext = ctx.application;
|
|
91
|
+
cost user = this.getUser();
|
|
122
92
|
}
|
|
93
|
+
// ...
|
|
123
94
|
}
|
|
124
95
|
```
|
|
125
96
|
|
|
126
|
-
|
|
97
|
+
### 2. Pages: Orchestrating Intent
|
|
127
98
|
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
99
|
+
```typescript
|
|
100
|
+
// pages/user-profile-page.ts
|
|
101
|
+
@page({ tag: 'user-profile-page', routes: ['/users/:userId'] })
|
|
102
|
+
class UserProfilePage extends HTMLElement {
|
|
103
|
+
@property()
|
|
104
|
+
userId = ''; // From URL parameter
|
|
133
105
|
|
|
134
|
-
|
|
106
|
+
@property({ type: Object })
|
|
107
|
+
user = null;
|
|
135
108
|
|
|
136
|
-
|
|
137
|
-
|
|
109
|
+
@property({ type: Object })
|
|
110
|
+
userStats = null;
|
|
138
111
|
|
|
139
|
-
@
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return `
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
</div>
|
|
112
|
+
@ready()
|
|
113
|
+
async loadUserData() {
|
|
114
|
+
// Pages handle data fetching, elements just display
|
|
115
|
+
const [user, stats] = await Promise.all([
|
|
116
|
+
fetch(`/api/users/${this.userId}`).then(r => r.json()),
|
|
117
|
+
fetch(`/api/users/${this.userId}/stats`).then(r => r.json())
|
|
118
|
+
]);
|
|
119
|
+
this.user = user;
|
|
120
|
+
this.userStats = stats;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@render()
|
|
124
|
+
renderContent() {
|
|
125
|
+
return html`
|
|
126
|
+
<page-header .user=${this.user}></page-header>
|
|
127
|
+
<user-stats .stats=${this.userStats}></user-stats>
|
|
128
|
+
<user-activity .userId=${this.userId}></user-activity>
|
|
157
129
|
`;
|
|
158
130
|
}
|
|
159
|
-
|
|
160
|
-
// Imperative update methods - YOU control when updates happen
|
|
161
|
-
setCount(newCount: number) {
|
|
162
|
-
this.count = newCount;
|
|
163
|
-
this.countElement.textContent = String(newCount);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
setStatus(status: string) {
|
|
167
|
-
this.statusElement.textContent = status;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
increment() {
|
|
171
|
-
this.setCount(this.count + 1);
|
|
172
|
-
this.setStatus('Incremented!');
|
|
173
|
-
}
|
|
174
131
|
}
|
|
175
132
|
```
|
|
176
133
|
|
|
177
|
-
|
|
178
|
-
- The `html()` method runs **once** when the element connects
|
|
179
|
-
- Updates happen through **explicit method calls** like `setCount()`
|
|
180
|
-
- You have **full control** over what updates and when
|
|
181
|
-
- No surprises, no magic, no hidden re-renders
|
|
182
|
-
|
|
183
|
-
## Properties
|
|
184
|
-
|
|
185
|
-
Properties automatically sync with DOM attributes in both directions. The HTML is rendered once when the element connects to the DOM. Use properties for initial configuration and watch for changes to update the UI.
|
|
134
|
+
### 3. Elements: Pure Presentation
|
|
186
135
|
|
|
187
136
|
```typescript
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
name = 'Anonymous';
|
|
194
|
-
|
|
195
|
-
@property({ attribute: 'user-role' }) // Maps to user-role attribute
|
|
196
|
-
role = 'User';
|
|
137
|
+
// elements/user-stats.ts
|
|
138
|
+
@element('user-stats')
|
|
139
|
+
class UserStats extends HTMLElement {
|
|
140
|
+
@property({ type: Object })
|
|
141
|
+
stats = null;
|
|
197
142
|
|
|
198
|
-
@
|
|
199
|
-
|
|
143
|
+
@render()
|
|
144
|
+
renderContent() {
|
|
145
|
+
if (!this.stats) return html`<div>Loading...</div>`;
|
|
200
146
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
147
|
+
return html`
|
|
148
|
+
<div class="stats">
|
|
149
|
+
<div class="stat">
|
|
150
|
+
<span class="label">Views</span>
|
|
151
|
+
<span class="value">${this.stats.views}</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="stat">
|
|
154
|
+
<span class="label">Followers</span>
|
|
155
|
+
<span class="value">${this.stats.followers}</span>
|
|
156
|
+
</div>
|
|
208
157
|
</div>
|
|
209
158
|
`;
|
|
210
159
|
}
|
|
160
|
+
|
|
161
|
+
@styles()
|
|
162
|
+
statsStyles() {
|
|
163
|
+
return css`
|
|
164
|
+
.stats { display: flex; gap: 2rem; }
|
|
165
|
+
.stat { text-align: center; }
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
211
168
|
}
|
|
212
|
-
```
|
|
213
169
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<!-- Setting attributes automatically updates properties -->
|
|
217
|
-
<user-card name="Jane Doe" user-role="Admin" verified></user-card>
|
|
218
|
-
|
|
219
|
-
<script>
|
|
220
|
-
const card = document.querySelector('user-card');
|
|
221
|
-
card.name = 'John Smith'; // Sets name="John Smith" attribute
|
|
222
|
-
card.verified = true; // Sets verified attribute
|
|
223
|
-
</script>
|
|
170
|
+
// Usage in parent page (which handles data fetching):
|
|
171
|
+
// <user-stats .stats=${this.userStats}></user-stats>
|
|
224
172
|
```
|
|
225
173
|
|
|
226
|
-
|
|
174
|
+
### 4. Controllers: Behavior Management
|
|
227
175
|
|
|
228
176
|
```typescript
|
|
229
|
-
|
|
177
|
+
// controllers/real-time-user-loader.ts
|
|
178
|
+
@controller('real-time-user-loader')
|
|
179
|
+
class RealTimeUserLoader {
|
|
180
|
+
async attach(element: IUserList) {
|
|
181
|
+
this.socket = new WebSocket('/api/users/stream');
|
|
182
|
+
this.socket.onmessage = (e) => {
|
|
183
|
+
element.setUsers(JSON.parse(e.data));
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// ...
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// controllers/cached-user-loader.ts
|
|
190
|
+
@controller('cached-user-loader')
|
|
191
|
+
class CachedUserLoader {
|
|
192
|
+
async attach(element: IUserList) {
|
|
193
|
+
const cached = localStorage.getItem('users');
|
|
194
|
+
if (cached) element.setUsers(JSON.parse(cached));
|
|
195
|
+
}
|
|
196
|
+
// ...
|
|
197
|
+
}
|
|
230
198
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
199
|
+
// elements/user-list.ts - stays the same
|
|
200
|
+
@element('user-list')
|
|
201
|
+
class UserList extends HTMLElement {
|
|
202
|
+
setUsers(users: User[]) {
|
|
203
|
+
this.users = users;
|
|
204
|
+
// ...
|
|
205
|
+
}
|
|
235
206
|
|
|
236
|
-
|
|
237
|
-
|
|
207
|
+
@render()
|
|
208
|
+
renderContent() {
|
|
209
|
+
return html`
|
|
210
|
+
<ul>${this.users.map(u => html`<li>${u.name}</li>`)}</ul>
|
|
211
|
+
`;
|
|
238
212
|
}
|
|
239
213
|
}
|
|
240
214
|
```
|
|
241
215
|
|
|
216
|
+
Usage - swap behavior without touching presentation:
|
|
217
|
+
|
|
242
218
|
```html
|
|
243
|
-
<
|
|
219
|
+
<user-list controller="real-time-user-loader"></user-list>
|
|
220
|
+
<user-list controller="cached-user-loader"></user-list>
|
|
244
221
|
```
|
|
245
222
|
|
|
246
|
-
##
|
|
223
|
+
## Key Features
|
|
247
224
|
|
|
248
|
-
|
|
225
|
+
**Differential Rendering** - Only updates changed parts of the DOM, not entire components
|
|
249
226
|
|
|
250
|
-
|
|
251
|
-
import { element, property, watch, query } from 'snice';
|
|
227
|
+
**Auto-Rendering** - Components automatically re-render when properties change
|
|
252
228
|
|
|
253
|
-
|
|
254
|
-
class ThemeToggle extends HTMLElement {
|
|
255
|
-
@property()
|
|
256
|
-
theme: 'light' | 'dark' = 'light';
|
|
229
|
+
**Template Syntax** - Clean `html\`...\`` and `css\`...\`` tagged templates
|
|
257
230
|
|
|
258
|
-
|
|
259
|
-
icon!: HTMLSpanElement;
|
|
260
|
-
|
|
261
|
-
html() {
|
|
262
|
-
return `
|
|
263
|
-
<button>
|
|
264
|
-
<span class="icon">🌞</span>
|
|
265
|
-
</button>
|
|
266
|
-
`;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
@watch('theme')
|
|
270
|
-
updateTheme(oldValue: string, newValue: string) {
|
|
271
|
-
this.icon.textContent = newValue === 'dark' ? '🌙' : '🌞';
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
@on('click', 'button')
|
|
275
|
-
toggle() {
|
|
276
|
-
this.theme = this.theme === 'light' ? 'dark' : 'light';
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
```
|
|
231
|
+
**Type Safety** - Full TypeScript support with decorator-based APIs
|
|
280
232
|
|
|
281
|
-
**
|
|
282
|
-
- `@watch` methods are called when the property value changes
|
|
283
|
-
- Receives `oldValue`, `newValue`, and `propertyName` as parameters
|
|
284
|
-
- Perfect for imperatively updating DOM elements
|
|
285
|
-
- Can watch multiple properties with multiple decorators
|
|
286
|
-
- Works with both programmatic changes and attribute changes
|
|
233
|
+
**Zero Dependencies** - No external runtime dependencies, small bundle size
|
|
287
234
|
|
|
288
|
-
|
|
235
|
+
**Standards-Based** - Built on web components, works with any framework
|
|
289
236
|
|
|
290
|
-
|
|
291
|
-
@watch('width', 'height', 'scale')
|
|
292
|
-
updateDimensions(_old: number, _new: number, _name: string) {
|
|
293
|
-
// Called when any of these properties change
|
|
294
|
-
console.log(`${_name} changed from ${_old} to ${_new}`);
|
|
295
|
-
this.recalculateLayout();
|
|
296
|
-
}
|
|
297
|
-
```
|
|
237
|
+
## Core APIs
|
|
298
238
|
|
|
299
|
-
|
|
239
|
+
### Class Decorators
|
|
300
240
|
|
|
241
|
+
**`@element('tag-name')`** - Define reusable UI components
|
|
301
242
|
```typescript
|
|
302
|
-
@
|
|
303
|
-
|
|
304
|
-
console.log(`Property ${_name} changed from ${_old} to ${_new}`);
|
|
305
|
-
// Useful for debugging or when all properties affect the same output
|
|
306
|
-
}
|
|
243
|
+
@element('my-button')
|
|
244
|
+
class MyButton extends HTMLElement { }
|
|
307
245
|
```
|
|
308
246
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
Query single elements with `@query`:
|
|
312
|
-
|
|
247
|
+
**`@page({ tag, routes })`** - Define routable pages
|
|
313
248
|
```typescript
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
@element('my-form')
|
|
317
|
-
class MyForm extends HTMLElement {
|
|
318
|
-
@query('input')
|
|
319
|
-
input!: HTMLInputElement;
|
|
320
|
-
|
|
321
|
-
html() {
|
|
322
|
-
return `<input type="text" />`;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
getValue() {
|
|
326
|
-
return this.input.value;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
249
|
+
@page({ tag: 'home-page', routes: ['/'] })
|
|
250
|
+
class HomePage extends HTMLElement { }
|
|
329
251
|
```
|
|
330
252
|
|
|
331
|
-
|
|
332
|
-
|
|
253
|
+
**`@controller('controller-name')`** - Define behavior modules
|
|
333
254
|
```typescript
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
@queryAll('input[type="checkbox"]')
|
|
339
|
-
checkboxes!: NodeListOf<HTMLInputElement>;
|
|
340
|
-
|
|
341
|
-
html() {
|
|
342
|
-
return `
|
|
343
|
-
<input type="checkbox" value="option1" />
|
|
344
|
-
<input type="checkbox" value="option2" />
|
|
345
|
-
<input type="checkbox" value="option3" />
|
|
346
|
-
`;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
getSelectedValues() {
|
|
350
|
-
return Array.from(this.checkboxes)
|
|
351
|
-
.filter(cb => cb.checked)
|
|
352
|
-
.map(cb => cb.value);
|
|
353
|
-
}
|
|
255
|
+
@controller('data-loader')
|
|
256
|
+
class DataLoader {
|
|
257
|
+
async attach(element) { }
|
|
258
|
+
async detach(element) { }
|
|
354
259
|
}
|
|
355
260
|
```
|
|
356
261
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
Listen for events with `@on`:
|
|
262
|
+
### Rendering
|
|
360
263
|
|
|
264
|
+
**`@render(options?)`** - Define component template (auto re-renders on property changes)
|
|
361
265
|
```typescript
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
class MyClicker extends HTMLElement {
|
|
366
|
-
html() {
|
|
367
|
-
return `
|
|
368
|
-
<button>Click me</button>
|
|
369
|
-
<input type="text" placeholder="Press Enter" />
|
|
370
|
-
`;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
@on('click', 'button')
|
|
374
|
-
handleClick() {
|
|
375
|
-
console.log('Button clicked!');
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
@on('keydown:Enter', 'input') // Only plain Enter (no modifiers)
|
|
379
|
-
handleEnter() {
|
|
380
|
-
console.log('Enter pressed!');
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
@on('keydown:ctrl+Enter', 'input') // Only Ctrl+Enter
|
|
384
|
-
handleCtrlEnter() {
|
|
385
|
-
console.log('Ctrl + Enter pressed!');
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
@on('focus') // Listen on the host element itself (no target)
|
|
389
|
-
handleFocus() {
|
|
390
|
-
console.log('Element received focus!');
|
|
391
|
-
}
|
|
266
|
+
@render()
|
|
267
|
+
renderContent() {
|
|
268
|
+
return html`<div>${this.data}</div>`;
|
|
392
269
|
}
|
|
393
270
|
```
|
|
394
271
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
Automatically dispatch custom events with `@dispatch`:
|
|
398
|
-
|
|
272
|
+
**`@styles()`** - Define scoped styles
|
|
399
273
|
```typescript
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
class ToggleSwitch extends HTMLElement {
|
|
404
|
-
private isOn = false;
|
|
405
|
-
|
|
406
|
-
@query('.toggle')
|
|
407
|
-
toggleButton!: HTMLElement;
|
|
408
|
-
|
|
409
|
-
html() {
|
|
410
|
-
return `<button class="toggle">OFF</button>`;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
@on('click', '.toggle')
|
|
414
|
-
@dispatch('toggled')
|
|
415
|
-
toggle() {
|
|
416
|
-
this.isOn = !this.isOn;
|
|
417
|
-
this.toggleButton.textContent = this.isOn ? 'ON' : 'OFF';
|
|
418
|
-
return { on: this.isOn };
|
|
419
|
-
}
|
|
274
|
+
@styles()
|
|
275
|
+
componentStyles() {
|
|
276
|
+
return css`.container { padding: 1rem; }`;
|
|
420
277
|
}
|
|
421
278
|
```
|
|
422
279
|
|
|
423
|
-
|
|
424
|
-
- Dispatches after the method completes
|
|
425
|
-
- Uses the return value as the event detail
|
|
426
|
-
- Works with async methods
|
|
427
|
-
- Bubbles by default
|
|
280
|
+
### Properties & State
|
|
428
281
|
|
|
282
|
+
**`@property(options?)`** - Reactive properties that sync with attributes
|
|
429
283
|
```typescript
|
|
430
|
-
|
|
431
|
-
|
|
284
|
+
@property()
|
|
285
|
+
name = 'default';
|
|
432
286
|
|
|
433
|
-
|
|
434
|
-
|
|
287
|
+
@property({ type: Boolean })
|
|
288
|
+
enabled = false;
|
|
435
289
|
```
|
|
436
290
|
|
|
437
|
-
|
|
438
|
-
|
|
291
|
+
**`@watch(...propertyNames)`** - React to property changes
|
|
439
292
|
```typescript
|
|
440
|
-
@
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return `<div class="card">Hello</div>`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
css() {
|
|
447
|
-
return `
|
|
448
|
-
.card {
|
|
449
|
-
padding: 20px;
|
|
450
|
-
background: #f0f0f0;
|
|
451
|
-
border-radius: 8px;
|
|
452
|
-
}
|
|
453
|
-
`;
|
|
454
|
-
}
|
|
293
|
+
@watch('name')
|
|
294
|
+
onNameChange(oldVal, newVal) {
|
|
295
|
+
console.log(`Name changed from ${oldVal} to ${newVal}`);
|
|
455
296
|
}
|
|
456
297
|
```
|
|
457
298
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
## Routing
|
|
299
|
+
### Lifecycle
|
|
461
300
|
|
|
301
|
+
**`@ready()`** - Runs after initial render completes
|
|
462
302
|
```typescript
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const { page, navigate, initialize } = router;
|
|
468
|
-
|
|
469
|
-
@page({ tag: 'home-page', routes: ['/'] })
|
|
470
|
-
class HomePage extends HTMLElement {
|
|
471
|
-
html() {
|
|
472
|
-
return `<h1>Home</h1>`;
|
|
473
|
-
}
|
|
303
|
+
@ready()
|
|
304
|
+
async initialize() {
|
|
305
|
+
// Fetch data, set up listeners, etc.
|
|
474
306
|
}
|
|
307
|
+
```
|
|
475
308
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Page with URL parameter
|
|
484
|
-
import { property } from 'snice';
|
|
485
|
-
|
|
486
|
-
@page({ tag: 'user-page', routes: ['/users/:userId'] })
|
|
487
|
-
class UserPage extends HTMLElement {
|
|
488
|
-
@property()
|
|
489
|
-
userId = '';
|
|
490
|
-
|
|
491
|
-
html() {
|
|
492
|
-
return `<h1>User ${this.userId}</h1>`;
|
|
493
|
-
}
|
|
309
|
+
**`@dispose()`** - Runs when element is removed from DOM
|
|
310
|
+
```typescript
|
|
311
|
+
@dispose()
|
|
312
|
+
cleanup() {
|
|
313
|
+
// Clean up listeners, close connections, etc.
|
|
494
314
|
}
|
|
495
|
-
|
|
496
|
-
// Start the router
|
|
497
|
-
initialize();
|
|
498
|
-
|
|
499
|
-
// Navigate programmatically
|
|
500
|
-
navigate('/about');
|
|
501
|
-
navigate('/users/123'); // Sets userId="123" on UserPage
|
|
502
315
|
```
|
|
503
316
|
|
|
504
|
-
###
|
|
505
|
-
|
|
506
|
-
Protect routes with guard functions that control access:
|
|
317
|
+
### DOM Queries
|
|
507
318
|
|
|
319
|
+
**`@query(selector)`** - Query single element from shadow DOM
|
|
508
320
|
```typescript
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const router = Router({
|
|
513
|
-
target: '#app',
|
|
514
|
-
type: 'hash',
|
|
515
|
-
context: new AppContext(), // Your app's context object
|
|
516
|
-
});
|
|
321
|
+
@query('input')
|
|
322
|
+
input!: HTMLInputElement;
|
|
323
|
+
```
|
|
517
324
|
|
|
518
|
-
|
|
325
|
+
**`@queryAll(selector)`** - Query multiple elements from shadow DOM
|
|
326
|
+
```typescript
|
|
327
|
+
@queryAll('.item')
|
|
328
|
+
items!: NodeListOf<HTMLElement>;
|
|
329
|
+
```
|
|
519
330
|
|
|
520
|
-
|
|
521
|
-
const isAuthenticated: Guard<AppContext> = (ctx, params) => ctx.getUser() !== null;
|
|
522
|
-
const isAdmin: Guard<AppContext> = (ctx, params) => ctx.getUser()?.role === 'admin';
|
|
331
|
+
### Events & Communication
|
|
523
332
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
333
|
+
**Template Events** - Handle events directly in templates (with keyboard modifiers!)
|
|
334
|
+
```typescript
|
|
335
|
+
html`
|
|
336
|
+
<button @click=${this.handleClick}>Click</button>
|
|
337
|
+
<input @keydown:Enter=${this.submit} />
|
|
338
|
+
<input @keydown:ctrl+s=${this.save} />
|
|
339
|
+
`
|
|
340
|
+
```
|
|
531
341
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
342
|
+
**`@on` Decorator** - Event delegation with selectors
|
|
343
|
+
```typescript
|
|
344
|
+
// Works in both elements AND controllers
|
|
345
|
+
@on('click', 'button') // Event delegation
|
|
346
|
+
handleClick(e: Event) {
|
|
347
|
+
console.log('Button clicked!');
|
|
538
348
|
}
|
|
539
349
|
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (!user) return false;
|
|
544
|
-
|
|
545
|
-
// params.id comes from route '/users/:id/edit'
|
|
546
|
-
return ctx.hasPermission('users.edit', params.id);
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
// Guard that checks ownership
|
|
550
|
-
const ownsItem: Guard<AppContext> = (ctx, params) => {
|
|
551
|
-
const user = ctx.getUser();
|
|
552
|
-
if (!user) return false;
|
|
553
|
-
|
|
554
|
-
// params.itemId comes from route '/items/:itemId'
|
|
555
|
-
return user.ownedItems.includes(parseInt(params.itemId));
|
|
556
|
-
};
|
|
557
|
-
|
|
558
|
-
@page({ tag: 'user-edit', routes: ['/users/:id/edit'], guards: [isAuthenticated, canEditUser] })
|
|
559
|
-
class UserEditPage extends HTMLElement {
|
|
560
|
-
@property()
|
|
561
|
-
id = ''; // Automatically set from route param
|
|
562
|
-
|
|
563
|
-
html() {
|
|
564
|
-
return `<h1>Edit User ${this.id}</h1>`;
|
|
565
|
-
}
|
|
350
|
+
@on('keydown:Enter', 'input') // Keyboard modifiers
|
|
351
|
+
handleEnter(e: KeyboardEvent) {
|
|
352
|
+
this.submit();
|
|
566
353
|
}
|
|
567
354
|
|
|
568
|
-
@
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
itemId = ''; // Automatically set from route param
|
|
572
|
-
|
|
573
|
-
html() {
|
|
574
|
-
return `<h1>Item ${this.itemId}</h1>`;
|
|
575
|
-
}
|
|
355
|
+
@on('input', 'input', { debounce: 300 }) // Debounce support
|
|
356
|
+
handleInput(e: Event) {
|
|
357
|
+
this.search((e.target as HTMLInputElement).value);
|
|
576
358
|
}
|
|
359
|
+
```
|
|
577
360
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
<p>You don't have permission to view this page.</p>
|
|
585
|
-
<a href="#/">Return to home</a>
|
|
586
|
-
`;
|
|
587
|
-
}
|
|
361
|
+
**`@dispatch(eventName)`** - Auto-dispatch custom events after method execution
|
|
362
|
+
```typescript
|
|
363
|
+
@dispatch('value-changed')
|
|
364
|
+
setValue(val: string) {
|
|
365
|
+
this.value = val;
|
|
366
|
+
return { value: val }; // Event detail
|
|
588
367
|
}
|
|
589
|
-
|
|
590
|
-
initialize();
|
|
591
368
|
```
|
|
592
369
|
|
|
593
|
-
|
|
594
|
-
- The 403 page is rendered if defined
|
|
595
|
-
- Otherwise, a default "Unauthorized" message is shown
|
|
596
|
-
- The URL doesn't change (no redirect)
|
|
597
|
-
|
|
598
|
-
## Controllers (Data Fetching)
|
|
599
|
-
|
|
600
|
-
Controllers handle server communication separately from visual components:
|
|
370
|
+
### Global State
|
|
601
371
|
|
|
372
|
+
**`@context(options?)`** - Receive router context updates (global state)
|
|
602
373
|
```typescript
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
374
|
+
// Method decorator that receives context updates
|
|
375
|
+
@context()
|
|
376
|
+
handleContext(ctx: Context) {
|
|
377
|
+
this.appContext = ctx.application;
|
|
378
|
+
this.requestRender();
|
|
607
379
|
}
|
|
608
380
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
async attach(element: IUserElement) {
|
|
614
|
-
const response = await fetch('/api/users');
|
|
615
|
-
const users = await response.json();
|
|
616
|
-
element.setUsers(users);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
async detach(element: IUserElement) { /* Cleanup */ }
|
|
381
|
+
// With timing options
|
|
382
|
+
@context({ debounce: 300 })
|
|
383
|
+
handleContextDebounced(ctx: Context) {
|
|
384
|
+
// Called after 300ms of no updates
|
|
620
385
|
}
|
|
621
386
|
|
|
622
|
-
@
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
html() {
|
|
627
|
-
return `
|
|
628
|
-
<ul>
|
|
629
|
-
${this.users.map(u => `<li>${u.name}</li>`).join('')}
|
|
630
|
-
</ul>
|
|
631
|
-
`;
|
|
632
|
-
}
|
|
387
|
+
@context({ throttle: 100 })
|
|
388
|
+
handleContextThrottled(ctx: Context) {
|
|
389
|
+
// Called at most once per 100ms
|
|
390
|
+
}
|
|
633
391
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
this.shadowRoot.innerHTML = this.html();
|
|
638
|
-
}
|
|
639
|
-
}
|
|
392
|
+
@context({ once: true })
|
|
393
|
+
handleContextOnce(ctx: Context) {
|
|
394
|
+
// Called only once, then unregisters
|
|
640
395
|
}
|
|
641
396
|
```
|
|
642
397
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
398
|
+
**Context Object Structure:**
|
|
399
|
+
```typescript
|
|
400
|
+
interface Context {
|
|
401
|
+
application: AppContext; // Your router context
|
|
402
|
+
navigation: {
|
|
403
|
+
placards: Placard[]; // Page metadata
|
|
404
|
+
route: string; // Current route
|
|
405
|
+
params: Record<string, string>; // Route parameters
|
|
406
|
+
};
|
|
407
|
+
update(): void; // Notify all subscribers
|
|
408
|
+
}
|
|
647
409
|
```
|
|
648
410
|
|
|
649
|
-
|
|
411
|
+
**Triggering Context Updates:**
|
|
650
412
|
|
|
651
|
-
|
|
413
|
+
When you modify the application context, call `update()` to notify all subscribers:
|
|
652
414
|
|
|
653
415
|
```typescript
|
|
654
|
-
|
|
416
|
+
@page({ tag: 'login-page', routes: ['/login'] })
|
|
417
|
+
class LoginPage extends HTMLElement {
|
|
418
|
+
private ctx?: Context<AppContext>;
|
|
655
419
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
@request('fetch-user')
|
|
661
|
-
async *getUser(): Response<{ name: string; email: string }> {
|
|
662
|
-
const user = await (yield { userId: 123 });
|
|
663
|
-
return user;
|
|
420
|
+
@context()
|
|
421
|
+
handleContext(ctx: Context<AppContext>) {
|
|
422
|
+
this.ctx = ctx;
|
|
423
|
+
this.requestRender();
|
|
664
424
|
}
|
|
665
425
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
426
|
+
login(user: User) {
|
|
427
|
+
// Modify the application context
|
|
428
|
+
this.ctx!.application.setUser(user);
|
|
429
|
+
|
|
430
|
+
// Notify all @context subscribers
|
|
431
|
+
this.ctx!.update();
|
|
670
432
|
}
|
|
671
|
-
|
|
672
433
|
}
|
|
434
|
+
```
|
|
673
435
|
|
|
674
|
-
|
|
675
|
-
class UserController {
|
|
436
|
+
**Note:** The router calls `update()` automatically during navigation. Only call it manually when you change application state (like login/logout, theme changes, etc.).
|
|
676
437
|
|
|
677
|
-
|
|
678
|
-
async handleFetchUser(request: { userId: number }) {
|
|
679
|
-
const response = await fetch(`/api/users/${request.userId}`);
|
|
680
|
-
return response.json();
|
|
681
|
-
}
|
|
438
|
+
### Request/Response
|
|
682
439
|
|
|
683
|
-
|
|
684
|
-
|
|
440
|
+
**`@request(channel)`** - Make requests to controllers
|
|
441
|
+
**`@respond(channel)`** - Respond to requests from elements
|
|
685
442
|
|
|
686
|
-
|
|
443
|
+
See [Request/Response documentation](./docs/request-response.md) for details.
|
|
687
444
|
|
|
688
|
-
|
|
445
|
+
## Template Syntax
|
|
689
446
|
|
|
690
|
-
###
|
|
447
|
+
### Auto-Rendering with Differential Updates
|
|
691
448
|
|
|
692
449
|
```typescript
|
|
693
|
-
|
|
450
|
+
@element('counter-display')
|
|
451
|
+
class CounterDisplay extends HTMLElement {
|
|
452
|
+
@property({ type: Number })
|
|
453
|
+
count = 0;
|
|
694
454
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
<a href="#/">Home</a>
|
|
703
|
-
<a href="#/about">About</a>
|
|
704
|
-
</nav>
|
|
705
|
-
</header>
|
|
706
|
-
<main>
|
|
707
|
-
<slot name="page"></slot>
|
|
708
|
-
</main>
|
|
709
|
-
<footer>© 2024 My App</footer>
|
|
455
|
+
@render()
|
|
456
|
+
renderContent() {
|
|
457
|
+
return html`
|
|
458
|
+
<div class="counter">
|
|
459
|
+
<span class="count">${this.count}</span>
|
|
460
|
+
<button @click=${this.increment}>+</button>
|
|
461
|
+
</div>
|
|
710
462
|
`;
|
|
711
463
|
}
|
|
712
|
-
}
|
|
713
464
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
type: 'hash',
|
|
718
|
-
layout: 'app-shell' // All pages use this layout by default
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
const { page, initialize } = router;
|
|
722
|
-
|
|
723
|
-
// Pages automatically render inside the layout
|
|
724
|
-
@page({ tag: 'home-page', routes: ['/'] })
|
|
725
|
-
class HomePage extends HTMLElement {
|
|
726
|
-
html() {
|
|
727
|
-
return `<h1>Home Content</h1>`;
|
|
465
|
+
@styles()
|
|
466
|
+
counterStyles() {
|
|
467
|
+
return css`.counter { display: flex; gap: 1rem; }`;
|
|
728
468
|
}
|
|
729
|
-
}
|
|
730
469
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
html() {
|
|
735
|
-
return `<div>No layout wrapper</div>`;
|
|
470
|
+
increment() {
|
|
471
|
+
this.count++;
|
|
472
|
+
// Auto re-renders! Only <span class="count"> updates
|
|
736
473
|
}
|
|
737
474
|
}
|
|
738
|
-
|
|
739
|
-
initialize();
|
|
740
475
|
```
|
|
741
476
|
|
|
742
|
-
|
|
477
|
+
**Key Points:**
|
|
478
|
+
- Properties trigger automatic re-renders
|
|
479
|
+
- Only changed parts update (differential rendering)
|
|
480
|
+
- Event handlers: `@click=${this.method}`
|
|
481
|
+
- Batched updates (multiple changes = single render)
|
|
743
482
|
|
|
744
|
-
|
|
745
|
-
- **Default layouts**: Set `layout: 'component-name'` in router options
|
|
746
|
-
- **Per-page override**: Use `layout: 'other-layout'` or `layout: false` in page options
|
|
747
|
-
- **Smooth transitions**: Layout persists during page transitions for better UX
|
|
748
|
-
- **Nested layouts**: Layouts can contain other layouts for complex structures
|
|
483
|
+
### Property Binding
|
|
749
484
|
|
|
750
|
-
|
|
485
|
+
Use `.property=${value}` to set element properties directly:
|
|
751
486
|
|
|
752
|
-
|
|
487
|
+
```typescript
|
|
488
|
+
html`
|
|
489
|
+
<input .value=${this.text} />
|
|
490
|
+
<custom-element .complexData=${this.dataObject}></custom-element>
|
|
491
|
+
`
|
|
492
|
+
```
|
|
753
493
|
|
|
754
|
-
###
|
|
494
|
+
### Boolean Attributes
|
|
755
495
|
|
|
756
|
-
|
|
496
|
+
Use `?attribute=${boolean}` for boolean attributes:
|
|
757
497
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
498
|
+
```typescript
|
|
499
|
+
html`
|
|
500
|
+
<button ?disabled=${this.isLoading}>Submit</button>
|
|
501
|
+
<input type="checkbox" ?checked=${this.isChecked} />
|
|
502
|
+
`
|
|
503
|
+
```
|
|
764
504
|
|
|
765
|
-
###
|
|
505
|
+
### Conditionals
|
|
766
506
|
|
|
767
507
|
```typescript
|
|
768
|
-
//
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
508
|
+
// Ternary operator
|
|
509
|
+
html`
|
|
510
|
+
${this.isLoggedIn
|
|
511
|
+
? html`<span>Welcome!</span>`
|
|
512
|
+
: html`<a href="/login">Login</a>`
|
|
513
|
+
}
|
|
514
|
+
`
|
|
515
|
+
|
|
516
|
+
// <if> conditional element
|
|
517
|
+
html`
|
|
518
|
+
<if ${this.isLoggedIn}>
|
|
519
|
+
<span>Welcome, ${this.user.name}!</span>
|
|
520
|
+
<button @click=${this.logout}>Logout</button>
|
|
521
|
+
</if>
|
|
522
|
+
<if ${!this.isLoggedIn}>
|
|
523
|
+
<a href="/login">Login</a>
|
|
524
|
+
</if>
|
|
525
|
+
`
|
|
526
|
+
|
|
527
|
+
// <case>/<when>/<default> for multiple branches
|
|
528
|
+
html`
|
|
529
|
+
<case ${this.status}>
|
|
530
|
+
<when value="loading">
|
|
531
|
+
<span>Loading...</span>
|
|
532
|
+
</when>
|
|
533
|
+
<when value="success">
|
|
534
|
+
<span>Success!</span>
|
|
535
|
+
</when>
|
|
536
|
+
<when value="error">
|
|
537
|
+
<span>Error occurred</span>
|
|
538
|
+
</when>
|
|
539
|
+
<default>
|
|
540
|
+
<span>Unknown status</span>
|
|
541
|
+
</default>
|
|
542
|
+
</case>
|
|
543
|
+
`
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Lists
|
|
788
547
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const user = this.ctx?.getUser();
|
|
798
|
-
if (!user) {
|
|
799
|
-
return `<p>Please log in</p>`;
|
|
800
|
-
}
|
|
801
|
-
return `
|
|
802
|
-
<h1>Welcome, ${user.name}!</h1>
|
|
803
|
-
<p>Email: ${user.email}</p>
|
|
804
|
-
`;
|
|
805
|
-
}
|
|
806
|
-
}
|
|
548
|
+
```typescript
|
|
549
|
+
html`
|
|
550
|
+
<ul>
|
|
551
|
+
${this.items.map(item => html`
|
|
552
|
+
<li @click=${() => this.select(item.id)}>${item.name}</li>
|
|
553
|
+
`)}
|
|
554
|
+
</ul>
|
|
555
|
+
`
|
|
807
556
|
```
|
|
808
557
|
|
|
809
|
-
###
|
|
810
|
-
|
|
811
|
-
Nested elements within pages can also access context:
|
|
558
|
+
### Keyboard Shortcuts
|
|
812
559
|
|
|
813
560
|
```typescript
|
|
814
|
-
|
|
815
|
-
@
|
|
816
|
-
|
|
817
|
-
@
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// Context is available even in nested elements
|
|
822
|
-
const user = this.ctx?.getUser();
|
|
823
|
-
return user
|
|
824
|
-
? `<img src="${user.avatar}" alt="${user.name}">`
|
|
825
|
-
: `<div class="placeholder">?</div>`;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Use it in a page
|
|
830
|
-
@page({ tag: 'dashboard', routes: ['/'] })
|
|
831
|
-
class Dashboard extends HTMLElement {
|
|
832
|
-
html() {
|
|
833
|
-
return `
|
|
834
|
-
<h1>Dashboard</h1>
|
|
835
|
-
<user-avatar></user-avatar> <!-- Will have access to context -->
|
|
836
|
-
`;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
561
|
+
html`
|
|
562
|
+
<input @keydown.enter=${this.submit} />
|
|
563
|
+
<input @keydown.ctrl+s=${this.save} />
|
|
564
|
+
<input @keydown.ctrl+shift+s=${this.saveAs} />
|
|
565
|
+
<input @keydown.escape=${this.cancel} />
|
|
566
|
+
<input @keydown.~enter=${this.submitAny} />
|
|
567
|
+
`
|
|
839
568
|
```
|
|
840
569
|
|
|
841
|
-
|
|
570
|
+
Keyboard syntax:
|
|
571
|
+
- `@keydown.enter` - Plain Enter (no modifiers)
|
|
572
|
+
- `@keydown.ctrl+s` - Ctrl+S combination
|
|
573
|
+
- `@keydown.~enter` - Enter with any modifiers
|
|
574
|
+
- `@keydown.down` - Arrow keys (up, down, left, right)
|
|
575
|
+
- `@keydown.escape` - Escape key
|
|
842
576
|
|
|
843
|
-
|
|
577
|
+
## Router
|
|
844
578
|
|
|
845
579
|
```typescript
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
ctx?: AppContext;
|
|
852
|
-
|
|
853
|
-
attach(element: HTMLElement) {
|
|
854
|
-
// Context is available in controllers too
|
|
855
|
-
if (!this.ctx?.isAuthenticated()) {
|
|
856
|
-
window.location.hash = '#/login';
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
detach(element: HTMLElement) { /* Cleanup */ }
|
|
861
|
-
}
|
|
580
|
+
// main.ts
|
|
581
|
+
const { page, navigate, initialize } = Router({
|
|
582
|
+
target: '#app',
|
|
583
|
+
context: new AppContext()
|
|
584
|
+
});
|
|
862
585
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
586
|
+
// pages/home-page.ts
|
|
587
|
+
@page({ tag: 'home-page', routes: ['/'] })
|
|
588
|
+
class HomePage extends HTMLElement {
|
|
589
|
+
@render()
|
|
590
|
+
renderContent() {
|
|
591
|
+
return html`<h1>Home</h1>`;
|
|
867
592
|
}
|
|
868
593
|
}
|
|
869
|
-
```
|
|
870
594
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
595
|
+
// pages/user-page.ts
|
|
596
|
+
@page({ tag: 'user-page', routes: ['/users/:userId'] })
|
|
597
|
+
class UserPage extends HTMLElement {
|
|
598
|
+
@property()
|
|
599
|
+
userId = ''; // Auto-populated from URL
|
|
600
|
+
// ...
|
|
601
|
+
}
|
|
877
602
|
|
|
878
|
-
|
|
603
|
+
// main.ts
|
|
604
|
+
initialize();
|
|
605
|
+
navigate('/users/123');
|
|
606
|
+
```
|
|
879
607
|
|
|
880
|
-
|
|
608
|
+
### Route Guards
|
|
881
609
|
|
|
882
|
-
|
|
610
|
+
Protect routes with guard functions:
|
|
883
611
|
|
|
884
612
|
```typescript
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
@element('lazy-image')
|
|
888
|
-
class LazyImage extends HTMLElement {
|
|
889
|
-
html() {
|
|
890
|
-
return `
|
|
891
|
-
<img data-src="photo.jpg" class="lazy" />
|
|
892
|
-
<div class="loading">Loading...</div>
|
|
893
|
-
`;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Observe when image enters viewport
|
|
897
|
-
@observe('intersection', '.lazy', { threshold: 0.1 })
|
|
898
|
-
loadImage(entry: IntersectionObserverEntry) {
|
|
899
|
-
if (entry.isIntersecting) {
|
|
900
|
-
const img = entry.target as HTMLImageElement;
|
|
901
|
-
img.src = img.dataset.src!;
|
|
902
|
-
img.classList.add('loaded');
|
|
903
|
-
return false; // Stop observing after loading
|
|
904
|
-
}
|
|
905
|
-
}
|
|
613
|
+
const isAuthenticated: Guard<AppContext> = (ctx) => ctx.getUser() !== null;
|
|
906
614
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
}
|
|
615
|
+
@page({
|
|
616
|
+
tag: 'dashboard-page',
|
|
617
|
+
routes: ['/dashboard'],
|
|
618
|
+
guards: isAuthenticated
|
|
619
|
+
})
|
|
620
|
+
class DashboardPage extends HTMLElement { }
|
|
913
621
|
```
|
|
914
622
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
## Parts - Selective Re-rendering
|
|
623
|
+
## Layouts
|
|
918
624
|
|
|
919
|
-
|
|
625
|
+
Layouts wrap pages with shared UI and dynamically build navigation from page metadata:
|
|
920
626
|
|
|
921
627
|
```typescript
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
user = { name: 'Loading...', stats: { views: 0, likes: 0 } };
|
|
928
|
-
|
|
929
|
-
notifications = [];
|
|
930
|
-
messages = [];
|
|
931
|
-
|
|
932
|
-
html() {
|
|
933
|
-
return `
|
|
934
|
-
<header part="user-info"></header>
|
|
935
|
-
<main>
|
|
936
|
-
<section part="stats"></section>
|
|
937
|
-
<aside part="notifications"></aside>
|
|
938
|
-
<div part="messages"></div>
|
|
939
|
-
</main>
|
|
940
|
-
`;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
@part('user-info')
|
|
944
|
-
renderUserInfo() {
|
|
945
|
-
return `
|
|
946
|
-
<h1>${this.user.name}</h1>
|
|
947
|
-
<button id="refresh-user">Refresh</button>
|
|
948
|
-
`;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
@part('stats')
|
|
952
|
-
renderStats() {
|
|
953
|
-
return `
|
|
954
|
-
<div class="stats">
|
|
955
|
-
<span>Views: ${this.user.stats.views}</span>
|
|
956
|
-
<span>Likes: ${this.user.stats.likes}</span>
|
|
957
|
-
</div>
|
|
958
|
-
`;
|
|
959
|
-
}
|
|
628
|
+
// layouts/app-shell.ts
|
|
629
|
+
@layout('app-shell')
|
|
630
|
+
class AppShell extends HTMLElement implements Layout {
|
|
631
|
+
private placards: Placard[] = [];
|
|
632
|
+
private currentRoute = '';
|
|
960
633
|
|
|
961
|
-
@
|
|
962
|
-
|
|
963
|
-
return `
|
|
964
|
-
<
|
|
965
|
-
|
|
634
|
+
@render()
|
|
635
|
+
renderContent() {
|
|
636
|
+
return html`
|
|
637
|
+
<header>
|
|
638
|
+
<nav>
|
|
639
|
+
${this.placards
|
|
640
|
+
.filter(p => p.show !== false)
|
|
641
|
+
.map(p => html`
|
|
642
|
+
<a href="#/${p.name}"
|
|
643
|
+
class="${this.currentRoute === p.name ? 'active' : ''}">
|
|
644
|
+
${p.icon} ${p.title}
|
|
645
|
+
</a>
|
|
646
|
+
`)}
|
|
647
|
+
</nav>
|
|
648
|
+
</header>
|
|
649
|
+
<main><slot name="page"></slot></main>
|
|
966
650
|
`;
|
|
967
651
|
}
|
|
968
652
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
return this.messages.map(m => `<div class="message">${m}</div>`).join('');
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// Update specific parts without re-rendering everything
|
|
978
|
-
updateUserName(newName) {
|
|
979
|
-
this.user.name = newName;
|
|
980
|
-
this.renderUserInfo(); // Only re-renders the header
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
incrementViews() {
|
|
984
|
-
this.user.stats.views++;
|
|
985
|
-
this.renderStats(); // Only re-renders the stats section
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
addNotification(notification) {
|
|
989
|
-
this.notifications.unshift(notification);
|
|
990
|
-
this.renderNotifications(); // Only re-renders notifications
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
@on('click', '#refresh-user')
|
|
994
|
-
async handleRefreshUser() {
|
|
995
|
-
// Simulate API call
|
|
996
|
-
const userData = await this.fetchUserData();
|
|
997
|
-
this.user = userData;
|
|
998
|
-
this.renderUserInfo(); // Update just the user info part
|
|
999
|
-
this.renderStats(); // Update just the stats part
|
|
653
|
+
// Called when route changes
|
|
654
|
+
update(appContext, placards, currentRoute, routeParams) {
|
|
655
|
+
this.placards = placards;
|
|
656
|
+
this.currentRoute = currentRoute;
|
|
657
|
+
// Property changes trigger re-render
|
|
1000
658
|
}
|
|
1001
659
|
}
|
|
1002
|
-
```
|
|
1003
|
-
|
|
1004
|
-
**Benefits of `@part`:**
|
|
1005
|
-
- **Performance** - Update only what changed instead of re-rendering entire templates
|
|
1006
|
-
- **Granular Control** - Target specific sections for updates
|
|
1007
|
-
- **Complex UIs** - Perfect for dashboards, lists, or components with independent sections
|
|
1008
|
-
- **Async Support** - Part methods can be async for data fetching
|
|
1009
|
-
- **Throttle/Debounce** - Control render frequency to optimize performance
|
|
1010
|
-
|
|
1011
|
-
### Performance Options
|
|
1012
660
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
renderNotifications() { /* ... */ }
|
|
1019
|
-
|
|
1020
|
-
// Debounce: Delay render until 150ms after last call
|
|
1021
|
-
@part('search-results', { debounce: 150 })
|
|
1022
|
-
renderSearchResults() { /* ... */ }
|
|
661
|
+
// main.ts - configure router with layout
|
|
662
|
+
const { page, initialize } = Router({
|
|
663
|
+
target: '#app',
|
|
664
|
+
layout: 'app-shell'
|
|
665
|
+
});
|
|
1023
666
|
```
|
|
1024
667
|
|
|
1025
|
-
|
|
1026
|
-
- **Debounce** - Delays renders until after calls stop for the specified time
|
|
1027
|
-
|
|
1028
|
-
The `@part` decorator is ideal when you have components with multiple independent sections that update at different frequencies or from different data sources.
|
|
1029
|
-
|
|
1030
|
-
## Lifecycle Callbacks
|
|
668
|
+
Pages render inside `<slot name="page"></slot>`. Layout persists, only page content swaps.
|
|
1031
669
|
|
|
1032
|
-
|
|
670
|
+
## Placards
|
|
1033
671
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
The `@moved` decorator runs methods when an element is moved within the DOM using `Element.moveBefore()`. This is useful for handling position changes without full disconnection/reconnection:
|
|
672
|
+
Page metadata that layouts use to build navigation, breadcrumbs, and help systems:
|
|
1037
673
|
|
|
1038
674
|
```typescript
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
onMovedDebounced() {
|
|
1050
|
-
// Only called once after moves stop for 100ms
|
|
1051
|
-
this.recalculateLayout();
|
|
1052
|
-
}
|
|
675
|
+
// pages/dashboard-page.ts
|
|
676
|
+
const placard: Placard<AppContext> = {
|
|
677
|
+
name: 'dashboard',
|
|
678
|
+
title: 'Dashboard',
|
|
679
|
+
icon: '📊',
|
|
680
|
+
order: 1,
|
|
681
|
+
searchTerms: ['home', 'overview', 'stats'],
|
|
682
|
+
hotkeys: ['ctrl+d'],
|
|
683
|
+
visibleOn: [isAuthenticated]
|
|
684
|
+
};
|
|
1053
685
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
}
|
|
686
|
+
@page({
|
|
687
|
+
tag: 'dashboard-page',
|
|
688
|
+
routes: ['/dashboard'],
|
|
689
|
+
placard: placard
|
|
690
|
+
})
|
|
691
|
+
class DashboardPage extends HTMLElement { }
|
|
1060
692
|
```
|
|
1061
693
|
|
|
1062
|
-
|
|
694
|
+
**Features:**
|
|
695
|
+
- **Navigation** - `title`, `icon`, `order`, `show`
|
|
696
|
+
- **Hierarchy** - `parent`, `group`, `breadcrumbs`
|
|
697
|
+
- **Discovery** - `searchTerms`, `hotkeys`, `tooltip`
|
|
698
|
+
- **Visibility** - `visibleOn` guards control who sees what
|
|
1063
699
|
|
|
1064
|
-
|
|
700
|
+
Layouts receive placard data in `update()` and auto-build navigation. See [docs](./docs/placards.md).
|
|
1065
701
|
|
|
1066
|
-
|
|
1067
|
-
@element('portable-element')
|
|
1068
|
-
class PortableElement extends HTMLElement {
|
|
1069
|
-
@adopted()
|
|
1070
|
-
onAdoptedToNewDocument() {
|
|
1071
|
-
console.log('Element moved to new document');
|
|
1072
|
-
this.updateDocumentReferences();
|
|
1073
|
-
}
|
|
702
|
+
## Migrating from v2.x
|
|
1074
703
|
|
|
1075
|
-
|
|
1076
|
-
@adopted({ debounce: 200 })
|
|
1077
|
-
onAdoptedDebounced() {
|
|
1078
|
-
// Debounced for performance during rapid document moves
|
|
1079
|
-
this.reinitializeForNewContext();
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
```
|
|
704
|
+
v3.0.0 introduces template-based rendering with differential updates. Key changes:
|
|
1083
705
|
|
|
1084
|
-
|
|
706
|
+
- **Use `@render()` instead of `html()` method**
|
|
707
|
+
Return `html\`...\`` tagged template instead of string
|
|
1085
708
|
|
|
1086
|
-
|
|
709
|
+
- **Use `@styles()` instead of `css()` method**
|
|
710
|
+
Return `css\`...\`` tagged template instead of string
|
|
1087
711
|
|
|
1088
|
-
-
|
|
1089
|
-
|
|
712
|
+
- **`@on()` decorator available**
|
|
713
|
+
Works in both elements AND controllers with full event delegation, keyboard modifiers, and debounce/throttle support.
|
|
714
|
+
Template event syntax (`@click=${handler}`) is also available as an alternative.
|
|
1090
715
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
@moved({ debounce: 150 }) // Wait 150ms after moves stop
|
|
1094
|
-
@adopted({ throttle: 300 }) // Maximum once per 300ms
|
|
1095
|
-
```
|
|
716
|
+
- **`@part` decorator removed**
|
|
717
|
+
Differential rendering makes selective re-rendering unnecessary
|
|
1096
718
|
|
|
1097
|
-
|
|
1098
|
-
- **Performance optimization** during rapid DOM changes
|
|
1099
|
-
- **Layout recalculation** when elements move
|
|
1100
|
-
- **Context updates** when elements move between documents
|
|
1101
|
-
- **Resource cleanup/setup** during document adoption
|
|
719
|
+
See [Migration Guide](./docs/migration-v2-to-v3.md) for detailed migration guide.
|
|
1102
720
|
|
|
1103
721
|
## Documentation
|
|
1104
722
|
|
|
1105
723
|
- [Elements API](./docs/elements.md) - Complete guide to creating elements with properties, queries, and styling
|
|
1106
724
|
- [Controllers API](./docs/controllers.md) - Data fetching, business logic, and controller patterns
|
|
725
|
+
- [Routing API](./docs/routing.md) - Single-page application routing with transitions
|
|
726
|
+
- [Placards API](./docs/placards.md) - Rich page metadata for dynamic navigation and discovery
|
|
1107
727
|
- [Events API](./docs/events.md) - Event handling, dispatching, and custom events
|
|
1108
728
|
- [Request/Response API](./docs/request-response.md) - Bidirectional communication between elements and controllers
|
|
1109
|
-
- [Routing API](./docs/routing.md) - Single-page application routing with transitions
|
|
1110
729
|
- [Observe API](./docs/observe.md) - Lifecycle-managed observers for external changes
|
|
1111
730
|
|
|
1112
731
|
## License
|