native-document 1.0.166 → 1.0.168
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/.vitepress/config.js +166 -0
- package/CHANGELOG.md +153 -0
- package/components.js +2 -1
- package/dist/native-document.components.min.js +495 -228
- package/dist/native-document.dev.js +7 -0
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/advanced-components.md +213 -608
- package/docs/anchor.md +173 -312
- package/docs/cache.md +95 -803
- package/docs/cli.md +179 -0
- package/docs/components/accordion.md +172 -0
- package/docs/components/alert.md +99 -0
- package/docs/components/avatar.md +160 -0
- package/docs/components/badge.md +102 -0
- package/docs/components/breadcrumb.md +89 -0
- package/docs/components/button.md +183 -0
- package/docs/components/card.md +69 -0
- package/docs/components/context-menu.md +118 -0
- package/docs/components/data-table.md +345 -0
- package/docs/components/dropdown.md +214 -0
- package/docs/components/form/autocomplete-field.md +81 -0
- package/docs/components/form/checkbox-field.md +41 -0
- package/docs/components/form/checkbox-group-field.md +54 -0
- package/docs/components/form/color-field.md +64 -0
- package/docs/components/form/date-field.md +92 -0
- package/docs/components/form/field-collection.md +63 -0
- package/docs/components/form/file-field.md +203 -0
- package/docs/components/form/form-control.md +87 -0
- package/docs/components/form/image-field.md +90 -0
- package/docs/components/form/index.md +115 -0
- package/docs/components/form/number-field.md +65 -0
- package/docs/components/form/radio-field.md +51 -0
- package/docs/components/form/select-field.md +123 -0
- package/docs/components/form/slider.md +136 -0
- package/docs/components/form/string-field.md +134 -0
- package/docs/components/form/textarea-field.md +65 -0
- package/docs/components/form-fields.md +372 -0
- package/docs/components/getting-started.md +264 -0
- package/docs/components/index.md +337 -0
- package/docs/components/layout.md +279 -0
- package/docs/components/list.md +73 -0
- package/docs/components/menu.md +215 -0
- package/docs/components/modal.md +156 -0
- package/docs/components/pagination.md +95 -0
- package/docs/components/popover.md +131 -0
- package/docs/components/progress.md +111 -0
- package/docs/components/shortcut-manager.md +221 -0
- package/docs/components/simple-table.md +107 -0
- package/docs/components/skeleton.md +155 -0
- package/docs/components/spinner.md +100 -0
- package/docs/components/splitter.md +133 -0
- package/docs/components/stepper.md +163 -0
- package/docs/components/switch.md +113 -0
- package/docs/components/tabs.md +153 -0
- package/docs/components/toast.md +119 -0
- package/docs/components/tooltip.md +151 -0
- package/docs/components/traits.md +261 -0
- package/docs/conditional-rendering.md +170 -588
- package/docs/contributing.md +300 -25
- package/docs/core-concepts.md +205 -374
- package/docs/elements.md +251 -367
- package/docs/extending-native-document-element.md +192 -207
- package/docs/filters.md +153 -1122
- package/docs/getting-started.md +193 -267
- package/docs/i18n.md +241 -0
- package/docs/index.md +76 -0
- package/docs/lifecycle-events.md +143 -75
- package/docs/list-rendering.md +227 -852
- package/docs/memory-management.md +134 -47
- package/docs/native-document-element.md +337 -186
- package/docs/native-fetch.md +99 -630
- package/docs/observable-resource.md +364 -0
- package/docs/observables.md +592 -526
- package/docs/routing.md +244 -653
- package/docs/state-management.md +134 -241
- package/docs/svg-elements.md +231 -0
- package/docs/theming.md +409 -0
- package/docs/tutorials/.gitkeep +0 -0
- package/docs/validation.md +95 -97
- package/docs/vitepress-conventions.md +219 -0
- package/package.json +34 -13
- package/readme.md +269 -89
- package/src/components/card/Card.js +93 -39
- package/src/components/card/index.js +1 -1
- package/src/components/list/HasListItem.js +171 -0
- package/src/components/list/List.js +41 -107
- package/src/components/list/ListDivider.js +39 -0
- package/src/components/list/ListGroup.js +76 -59
- package/src/components/list/ListItem.js +117 -69
- package/src/components/list/index.js +3 -1
- package/src/components/list/types/ListItem.d.ts +45 -34
- package/src/components/spacer/Spacer.js +1 -1
- package/src/core/data/ObservableResource.js +5 -0
- package/src/core/data/observable-helpers/observable.prototypes.js +2 -0
- package/src/ui/components/card/CardRender.js +133 -0
- package/src/ui/components/card/card.css +169 -0
- package/src/ui/components/contextmenu/ContextmenuRender.js +1 -1
- package/src/ui/components/list/ListRender.js +18 -0
- package/src/ui/components/list/divider/ListDividerRender.js +10 -0
- package/src/ui/components/list/divider/list-divider.css +12 -0
- package/src/ui/components/list/group/ListGroupRender.js +61 -0
- package/src/ui/components/list/group/list-group.css +62 -0
- package/src/ui/components/list/item/ListItemRender.js +238 -0
- package/src/ui/components/list/item/list-item.css +191 -0
- package/src/ui/components/list/list.css +24 -0
- package/src/ui/components/spacer/SpacerRender.js +10 -0
- package/src/ui/index.js +8 -0
package/docs/i18n.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: i18n & Formatting
|
|
3
|
+
description: Built-in internationalization with locale-aware formatting, translation helpers, and automatic locale switching
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# i18n & Formatting
|
|
7
|
+
|
|
8
|
+
NativeDocument has built-in i18n support via `Observable.format()` for locale-aware value formatting, and a `tr()` translation helper for string translations. Both react automatically when the locale changes.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
Call `I18nService.use()` at startup with the user's locale, then pass `I18nService.current` to `Observable.setLocale()`:
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
import { Observable } from 'native-document';
|
|
18
|
+
import { I18nService } from 'native-document/i18n';
|
|
19
|
+
|
|
20
|
+
I18nService.use(navigator.language.split('-')[0] || 'en');
|
|
21
|
+
Observable.setLocale(I18nService.current);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
To switch locale at any time:
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
I18nService.use('en');
|
|
28
|
+
I18nService.use('fr');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## `Observable.format()` - Locale-Aware Formatting
|
|
34
|
+
|
|
35
|
+
Once the locale is set, call `.format()` on any observable to create a derived observable that formats the value reactively:
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
import { Observable } from 'native-document';
|
|
39
|
+
import { I18nService } from 'native-document/i18n';
|
|
40
|
+
|
|
41
|
+
Observable.setLocale(I18nService.current);
|
|
42
|
+
|
|
43
|
+
const price = Observable(15000);
|
|
44
|
+
const date = Observable(new Date());
|
|
45
|
+
const ratio = Observable(0.75);
|
|
46
|
+
const count = Observable(3);
|
|
47
|
+
|
|
48
|
+
// Currency
|
|
49
|
+
Div(price.format('currency')) // "15 000 FCFA"
|
|
50
|
+
Div(price.format('currency', { currency: 'EUR' })) // "15 000,00 €"
|
|
51
|
+
Div(price.format('currency', { notation: 'compact' })) // "15 K FCFA"
|
|
52
|
+
|
|
53
|
+
// Number
|
|
54
|
+
Div(price.format('number')) // "15 000"
|
|
55
|
+
|
|
56
|
+
// Percent
|
|
57
|
+
Div(ratio.format('percent')) // "75 %"
|
|
58
|
+
Div(ratio.format('percent', { decimals: 1 })) // "75,0 %"
|
|
59
|
+
|
|
60
|
+
// Date
|
|
61
|
+
Div(date.format('date')) // "3 mars 2026"
|
|
62
|
+
Div(date.format('date', { dateStyle: 'full' })) // "mardi 3 mars 2026"
|
|
63
|
+
Div(date.format('date', { format: 'DD/MM/YYYY' })) // "03/03/2026"
|
|
64
|
+
|
|
65
|
+
// Time
|
|
66
|
+
Div(date.format('time')) // "20:30"
|
|
67
|
+
Div(date.format('time', { second: '2-digit' })) // "20:30:00"
|
|
68
|
+
|
|
69
|
+
// Datetime
|
|
70
|
+
Div(date.format('datetime')) // "3 mars 2026, 20:30"
|
|
71
|
+
|
|
72
|
+
// Relative
|
|
73
|
+
Div(date.format('relative')) // "dans 11 jours"
|
|
74
|
+
Div(date.format('relative', { unit: 'month' })) // "dans 1 mois"
|
|
75
|
+
|
|
76
|
+
// Plural
|
|
77
|
+
Div(count.format('plural', { singular: 'billet', plural: 'billets' })) // "3 billets"
|
|
78
|
+
|
|
79
|
+
// Custom transform
|
|
80
|
+
Div(price.format(value => `${value.toLocaleString()} FCFA`))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Formatted observables update automatically when the locale changes:
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
Observable.setLocale(I18nService.current);
|
|
87
|
+
|
|
88
|
+
const label = Observable(15000).format('currency', { currency: 'XOF' });
|
|
89
|
+
label.val(); // "15 000 FCFA" (fr)
|
|
90
|
+
|
|
91
|
+
I18nService.use('en-US');
|
|
92
|
+
label.val(); // "$15,000.00"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Format types reference
|
|
96
|
+
|
|
97
|
+
| Type | Input | Key options |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `currency` | `number` | `currency`, `notation`, `minimumFractionDigits`, `maximumFractionDigits` |
|
|
100
|
+
| `number` | `number` | `notation`, `minimumFractionDigits`, `maximumFractionDigits` |
|
|
101
|
+
| `percent` | `number` | `decimals` |
|
|
102
|
+
| `date` | `Date \| number` | `dateStyle`, `format` |
|
|
103
|
+
| `time` | `Date \| number` | `hour`, `minute`, `second`, `format` |
|
|
104
|
+
| `datetime` | `Date \| number` | `dateStyle`, `hour`, `minute`, `second`, `format` |
|
|
105
|
+
| `relative` | `Date \| number` | `unit`, `numeric` |
|
|
106
|
+
| `plural` | `number` | `singular`, `plural` |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Extending Formatters
|
|
111
|
+
|
|
112
|
+
Add custom format types via the `Formatters` object:
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
import { Formatters } from 'native-document';
|
|
116
|
+
|
|
117
|
+
Formatters.duration = (value, locale) => {
|
|
118
|
+
const hours = Math.floor(value / 3600);
|
|
119
|
+
const minutes = Math.floor((value % 3600) / 60);
|
|
120
|
+
return `${hours}h${minutes.toString().padStart(2, '0')}`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const duration = Observable(3661);
|
|
124
|
+
Div(duration.format('duration')); // "1h01"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## `tr()` - Translations
|
|
130
|
+
|
|
131
|
+
The `tr()` helper returns a translated string for a given key. It is reactive - if the locale changes and the translation changes, the DOM updates automatically.
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
import { tr } from 'native-document/i18n';
|
|
135
|
+
|
|
136
|
+
// Basic usage
|
|
137
|
+
Div(tr('welcome_message'))
|
|
138
|
+
P(tr('errors.required'))
|
|
139
|
+
|
|
140
|
+
// With interpolation
|
|
141
|
+
Div(tr('greeting', { name: 'Alice' }))
|
|
142
|
+
// -> "Hello Alice!" (en) or "Bonjour Alice !" (fr)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Translation files
|
|
146
|
+
|
|
147
|
+
The CLI template creates translation files in `src/core/lang/locales/`:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
src/core/lang/locales/
|
|
151
|
+
├── en.json
|
|
152
|
+
└── fr.json
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Example `en.json`:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"welcome_message": "Welcome to my app",
|
|
160
|
+
"greeting": "Hello {{name}}!",
|
|
161
|
+
"errors": {
|
|
162
|
+
"required": "This field is required",
|
|
163
|
+
"invalid_email": "Please enter a valid email address"
|
|
164
|
+
},
|
|
165
|
+
"items": {
|
|
166
|
+
"singular": "{{count}} item",
|
|
167
|
+
"plural": "{{count}} items"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Example `fr.json`:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"welcome_message": "Bienvenue dans mon application",
|
|
177
|
+
"greeting": "Bonjour {{name}} !",
|
|
178
|
+
"errors": {
|
|
179
|
+
"required": "Ce champ est obligatoire",
|
|
180
|
+
"invalid_email": "Veuillez entrer une adresse email valide"
|
|
181
|
+
},
|
|
182
|
+
"items": {
|
|
183
|
+
"singular": "{{count}} élément",
|
|
184
|
+
"plural": "{{count}} éléments"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Scanning for Missing Keys
|
|
192
|
+
|
|
193
|
+
The CLI includes a script that scans your source files for `tr()` calls and reports any keys missing from your translation files:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npm run i18n:scan
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Run this after adding new `tr()` calls to ensure all locales are complete.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Switching Locale
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
import { I18nService } from 'native-document/i18n';
|
|
207
|
+
|
|
208
|
+
// Set a specific locale
|
|
209
|
+
I18nService.use('en');
|
|
210
|
+
I18nService.use('fr');
|
|
211
|
+
|
|
212
|
+
// From user action
|
|
213
|
+
Button('English').nd.onClick(() => I18nService.use('en'))
|
|
214
|
+
Button('Français').nd.onClick(() => I18nService.use('fr'))
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The locale is initialized at app startup via `I18nService.use()`:
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// In your app entry point (e.g. main.js)
|
|
221
|
+
I18nService.use(navigator.language.split('-')[0] || 'en');
|
|
222
|
+
Observable.setLocale(I18nService.current);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Best Practices
|
|
228
|
+
|
|
229
|
+
1. Always call `Observable.setLocale()` before using `.format()` - ideally at app startup
|
|
230
|
+
2. Use `I18nService.current` when using the CLI template
|
|
231
|
+
3. Use nested keys in translation files to organize by feature: `errors.required`, `nav.home`
|
|
232
|
+
4. Run `npm run i18n:scan` after adding new translations
|
|
233
|
+
5. Use `Observable.format('plural', { singular, plural })` for count-based strings instead of manual logic
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Next Steps
|
|
238
|
+
|
|
239
|
+
- **[Observables](./observables.md)** - Full `.format()` reference
|
|
240
|
+
- **[State Management](./state-management.md)** - The built-in `locale` store
|
|
241
|
+
- **[CLI](./cli.md)** - Project setup with i18n pre-configured
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: home
|
|
3
|
+
|
|
4
|
+
hero:
|
|
5
|
+
name: NativeDocument
|
|
6
|
+
text: The declarative rendering of SwiftUI. The power of pure JavaScript.
|
|
7
|
+
tagline: No Virtual DOM. No proprietary syntax. No hook rules. Just modern, performant, predictable Vanilla JS.
|
|
8
|
+
actions:
|
|
9
|
+
- theme: brand
|
|
10
|
+
text: Get Started
|
|
11
|
+
link: /getting-started
|
|
12
|
+
- theme: alt
|
|
13
|
+
text: GitHub
|
|
14
|
+
link: https://github.com/afrocodeur/native-document
|
|
15
|
+
|
|
16
|
+
features:
|
|
17
|
+
- icon: ⚡
|
|
18
|
+
title: Surgical Reactivity
|
|
19
|
+
details: Your component function runs exactly once. When data changes, NativeDocument updates the exact DOM node in the background. Declarative expressiveness with Vanilla JS performance.
|
|
20
|
+
|
|
21
|
+
- icon: 🧠
|
|
22
|
+
title: Zero Cognitive Load
|
|
23
|
+
details: No complex mental model to maintain. No dependency arrays. No hook order rules. Your JavaScript closures stay intact and your execution flow is perfectly linear.
|
|
24
|
+
|
|
25
|
+
- icon: 🧩
|
|
26
|
+
title: Natural Component Communication
|
|
27
|
+
details: A child component is a living object, not a black box. Fluid, transparent parent-child communication without forwardRef, defineExpose, or global state overhead.
|
|
28
|
+
|
|
29
|
+
- icon: 🇬🇧
|
|
30
|
+
title: Fluent UI Logic
|
|
31
|
+
details: Express your rendering conditions as naturally as a sentence in English. When(isAdmin).show(AdminPanel).otherwise(UserPanel) - pure JS, readable at a glance.
|
|
32
|
+
|
|
33
|
+
- icon: 🚀
|
|
34
|
+
title: Official CLI
|
|
35
|
+
details: Scaffold a complete project in one command. nd create my-app. Default and feature-based architectures, i18n, routing and store pre-configured.
|
|
36
|
+
|
|
37
|
+
- icon: 🌲
|
|
38
|
+
title: True Tree-Shaking
|
|
39
|
+
details: Only bundle what you use. NativeDocument is designed for bundlers - Vite, Webpack, Rollup. Every import is individually tree-shakeable.
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Tired of fighting your framework?
|
|
43
|
+
|
|
44
|
+
Building a modern UI should not require constant mental gymnastics. Sound familiar?
|
|
45
|
+
|
|
46
|
+
- **Wild re-renders** - One variable changes and your entire component (or app) recalculates. The larger the app, the more the Virtual DOM weighs on memory.
|
|
47
|
+
- **Hook prison** - Juggling `useEffect` order, managing infinite dependency arrays, hunting memory leaks.
|
|
48
|
+
- **Magic syntax** - Learning pseudo-HTML or depending on a mystical compiler just to display a list or a condition.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## The proof is in the code
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
import { $, Div, Button, Span } from 'native-document';
|
|
56
|
+
|
|
57
|
+
export function Counter() {
|
|
58
|
+
const count = $(0);
|
|
59
|
+
|
|
60
|
+
return Div({ class: 'counter-card' }, [
|
|
61
|
+
Span(['Count: ', count]),
|
|
62
|
+
Button('Increment').nd.onClick(() => count.$value++)
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
No JSX. No compiler. No rules. Just JavaScript.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Ready to lighten your code?
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install -g @native-document/cli
|
|
75
|
+
nd create my-awesome-app
|
|
76
|
+
```
|
package/docs/lifecycle-events.md
CHANGED
|
@@ -1,117 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Lifecycle Events
|
|
3
|
+
description: Execute code when elements are added to or removed from the DOM using mounted, unmounted, beforeUnmount, destroyOnUnmount, and destroy hooks
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Lifecycle Events
|
|
2
7
|
|
|
3
|
-
NativeDocument provides lifecycle hooks that let you execute code when elements are added to or removed from the DOM. This is essential for setup, cleanup, and managing resources.
|
|
8
|
+
NativeDocument provides lifecycle hooks that let you execute code when elements are added to or removed from the DOM. This is essential for setup, cleanup, and managing external resources.
|
|
9
|
+
|
|
10
|
+
---
|
|
4
11
|
|
|
5
|
-
##
|
|
12
|
+
## `mounted(callback)`
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
Fires when the element is added to the DOM:
|
|
8
15
|
|
|
9
16
|
```javascript
|
|
10
|
-
const myComponent = Div(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const myComponent = Div('Hello World')
|
|
18
|
+
.nd
|
|
19
|
+
.mounted(element => {
|
|
20
|
+
console.log('In the DOM:', element);
|
|
21
|
+
});
|
|
15
22
|
|
|
16
|
-
document.body.appendChild(myComponent); //
|
|
23
|
+
document.body.appendChild(myComponent); // triggers mounted
|
|
17
24
|
```
|
|
18
25
|
|
|
19
|
-
###
|
|
26
|
+
### Auto-focus example
|
|
20
27
|
|
|
21
28
|
```javascript
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Later, when element is removed
|
|
30
|
-
myComponent.remove(); // Triggers unmounted callback
|
|
29
|
+
const searchInput = Input({ placeholder: 'Search...' })
|
|
30
|
+
.nd
|
|
31
|
+
.mounted(element => {
|
|
32
|
+
element.focus();
|
|
33
|
+
});
|
|
31
34
|
```
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```javascript
|
|
36
|
-
const timer = Div("Timer: 0")
|
|
37
|
-
.nd.lifecycle({
|
|
38
|
-
mounted(element) {
|
|
39
|
-
console.log("Timer started");
|
|
40
|
-
element.intervalId = setInterval(() => {
|
|
41
|
-
element.textContent = `Timer: ${Date.now()}`;
|
|
42
|
-
}, 1000);
|
|
43
|
-
},
|
|
44
|
-
unmounted(element) {
|
|
45
|
-
console.log("Timer stopped");
|
|
46
|
-
clearInterval(element.intervalId);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
```
|
|
36
|
+
---
|
|
50
37
|
|
|
51
|
-
##
|
|
38
|
+
## `unmounted(callback)`
|
|
52
39
|
|
|
53
|
-
|
|
40
|
+
Fires when the element is removed from the DOM. Only clean up **external resources** here - do not clean up observables unless the element is permanently destroyed:
|
|
54
41
|
|
|
55
42
|
```javascript
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
const myComponent = Div('Content')
|
|
44
|
+
.nd
|
|
45
|
+
.unmounted(element => {
|
|
46
|
+
clearInterval(element.timerId);
|
|
47
|
+
element.websocket?.close();
|
|
48
|
+
|
|
49
|
+
// Do NOT call observable.cleanup() here
|
|
50
|
+
// unless the element will never be re-injected
|
|
51
|
+
});
|
|
60
52
|
```
|
|
61
53
|
|
|
62
|
-
###
|
|
54
|
+
### External event listener cleanup
|
|
63
55
|
|
|
64
56
|
```javascript
|
|
57
|
+
function MyButton() {
|
|
58
|
+
const handler = () => console.log('Global click');
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.nd.mounted(button => {
|
|
60
|
+
return Button('Click me')
|
|
61
|
+
.nd
|
|
62
|
+
.mounted(el => {
|
|
70
63
|
document.addEventListener('click', handler);
|
|
71
64
|
})
|
|
72
|
-
.
|
|
73
|
-
// Clean up external listeners, but keep observables intact
|
|
65
|
+
.unmounted(el => {
|
|
74
66
|
document.removeEventListener('click', handler);
|
|
75
|
-
// DON'T cleanup observables unless element won't be reused
|
|
76
67
|
});
|
|
77
68
|
}
|
|
78
69
|
```
|
|
79
70
|
|
|
80
|
-
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## `lifecycle({ mounted, unmounted })`
|
|
74
|
+
|
|
75
|
+
Configure both hooks at once:
|
|
81
76
|
|
|
82
77
|
```javascript
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
78
|
+
const timer = Div('Timer: 0')
|
|
79
|
+
.nd
|
|
80
|
+
.lifecycle({
|
|
81
|
+
mounted(element) {
|
|
82
|
+
element.intervalId = setInterval(() => {
|
|
83
|
+
element.textContent = `Timer: ${Date.now()}`;
|
|
84
|
+
}, 1000);
|
|
85
|
+
},
|
|
86
|
+
unmounted(element) {
|
|
87
|
+
clearInterval(element.intervalId);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
95
90
|
```
|
|
96
91
|
|
|
97
|
-
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## `beforeUnmount(id, callback)`
|
|
98
95
|
|
|
99
|
-
|
|
96
|
+
Registers an async callback that runs **before** the element is removed from the DOM. Useful for exit animations or saving data before removal.
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
- **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
|
|
103
|
-
- **[Advanced Components](advanced-components.md)** - Template caching and singleton views
|
|
104
|
-
- **[Args Validation](validation.md)** - Function Argument Validation
|
|
105
|
-
- **[Memory Management](memory-management.md)** - Memory management
|
|
106
|
-
- **[Anchor](anchor.md)** - Anchor
|
|
98
|
+
Multiple callbacks can be registered using different IDs - they all run sequentially before the element is removed:
|
|
107
99
|
|
|
108
|
-
|
|
100
|
+
```javascript
|
|
101
|
+
Div('Content')
|
|
102
|
+
.nd
|
|
103
|
+
.beforeUnmount('save', async el => {
|
|
104
|
+
await saveData();
|
|
105
|
+
})
|
|
106
|
+
.beforeUnmount('animate', async el => {
|
|
107
|
+
await playExitAnimation(el);
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The element's `remove()` method is patched to be async when `beforeUnmount` is used - all callbacks are awaited before the element is actually removed:
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
```javascript
|
|
114
|
+
const modal = Div({ class: 'modal' }, 'Content')
|
|
115
|
+
.nd
|
|
116
|
+
.beforeUnmount('fade-out', async el => {
|
|
117
|
+
el.style.opacity = '0';
|
|
118
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await modal.nd.remove();
|
|
122
|
+
```
|
|
113
123
|
|
|
124
|
+
> The `id` parameter lets you register multiple `beforeUnmount` callbacks and also allows replacing a specific one by re-using the same id.
|
|
114
125
|
|
|
126
|
+
---
|
|
115
127
|
|
|
128
|
+
## `destroyOnUnmount()`
|
|
129
|
+
|
|
130
|
+
Registers an `unmounted` callback that calls `destroy()` automatically. Use when the element will **never** be re-injected into the DOM:
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
const widget = Div('Content')
|
|
134
|
+
.nd
|
|
135
|
+
.mounted(el => {
|
|
136
|
+
el.intervalId = setInterval(() => doWork(), 1000);
|
|
137
|
+
})
|
|
138
|
+
.destroyOnUnmount();
|
|
139
|
+
```
|
|
116
140
|
|
|
141
|
+
> Do not use `destroyOnUnmount()` on elements that may be temporarily removed and re-appended (e.g. inside `ShowIf` with `shouldKeepInCache: true`). Use explicit `unmounted()` + `mounted()` pairs in that case.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## `destroy()`
|
|
146
|
+
|
|
147
|
+
Aborts the element's internal `AbortController`, clears all lifecycle observers, and removes all `beforeUnmount` callbacks. The element's `$element` reference is set to `null`:
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
const el = Div('Temporary content')
|
|
151
|
+
.nd
|
|
152
|
+
.beforeUnmount('animate', async () => { /* ... */ });
|
|
153
|
+
|
|
154
|
+
el.nd.destroy();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`destroy()` is called internally by `destroyOnUnmount()`. You rarely need to call it directly.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Element Reuse
|
|
162
|
+
|
|
163
|
+
Elements can be removed and re-appended safely. Observables bound to the element remain intact through remove/re-append cycles:
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
const reusable = Div('Content')
|
|
167
|
+
.nd
|
|
168
|
+
.mounted(el => console.log('Mounted'))
|
|
169
|
+
.unmounted(el => {
|
|
170
|
+
clearInterval(el.timerId);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
document.body.appendChild(reusable); // "Mounted"
|
|
174
|
+
reusable.nd.remove(); // "Unmounted"
|
|
175
|
+
document.body.appendChild(reusable); // "Mounted" again
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Next Steps
|
|
117
181
|
|
|
182
|
+
- **[NDElement](./native-document-element.md)** - Full `.nd` API reference
|
|
183
|
+
- **[Memory Management](./memory-management.md)** - When and how to clean up observables
|
|
184
|
+
- **[Extending NDElement](./extending-native-document-element.md)** - Custom methods guide
|
|
185
|
+
- **[Advanced Components](./advanced-components.md)** - Template caching and singleton views
|