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.
Files changed (108) hide show
  1. package/.vitepress/config.js +166 -0
  2. package/CHANGELOG.md +153 -0
  3. package/components.js +2 -1
  4. package/dist/native-document.components.min.js +495 -228
  5. package/dist/native-document.dev.js +7 -0
  6. package/dist/native-document.dev.js.map +1 -1
  7. package/dist/native-document.min.js +1 -1
  8. package/docs/advanced-components.md +213 -608
  9. package/docs/anchor.md +173 -312
  10. package/docs/cache.md +95 -803
  11. package/docs/cli.md +179 -0
  12. package/docs/components/accordion.md +172 -0
  13. package/docs/components/alert.md +99 -0
  14. package/docs/components/avatar.md +160 -0
  15. package/docs/components/badge.md +102 -0
  16. package/docs/components/breadcrumb.md +89 -0
  17. package/docs/components/button.md +183 -0
  18. package/docs/components/card.md +69 -0
  19. package/docs/components/context-menu.md +118 -0
  20. package/docs/components/data-table.md +345 -0
  21. package/docs/components/dropdown.md +214 -0
  22. package/docs/components/form/autocomplete-field.md +81 -0
  23. package/docs/components/form/checkbox-field.md +41 -0
  24. package/docs/components/form/checkbox-group-field.md +54 -0
  25. package/docs/components/form/color-field.md +64 -0
  26. package/docs/components/form/date-field.md +92 -0
  27. package/docs/components/form/field-collection.md +63 -0
  28. package/docs/components/form/file-field.md +203 -0
  29. package/docs/components/form/form-control.md +87 -0
  30. package/docs/components/form/image-field.md +90 -0
  31. package/docs/components/form/index.md +115 -0
  32. package/docs/components/form/number-field.md +65 -0
  33. package/docs/components/form/radio-field.md +51 -0
  34. package/docs/components/form/select-field.md +123 -0
  35. package/docs/components/form/slider.md +136 -0
  36. package/docs/components/form/string-field.md +134 -0
  37. package/docs/components/form/textarea-field.md +65 -0
  38. package/docs/components/form-fields.md +372 -0
  39. package/docs/components/getting-started.md +264 -0
  40. package/docs/components/index.md +337 -0
  41. package/docs/components/layout.md +279 -0
  42. package/docs/components/list.md +73 -0
  43. package/docs/components/menu.md +215 -0
  44. package/docs/components/modal.md +156 -0
  45. package/docs/components/pagination.md +95 -0
  46. package/docs/components/popover.md +131 -0
  47. package/docs/components/progress.md +111 -0
  48. package/docs/components/shortcut-manager.md +221 -0
  49. package/docs/components/simple-table.md +107 -0
  50. package/docs/components/skeleton.md +155 -0
  51. package/docs/components/spinner.md +100 -0
  52. package/docs/components/splitter.md +133 -0
  53. package/docs/components/stepper.md +163 -0
  54. package/docs/components/switch.md +113 -0
  55. package/docs/components/tabs.md +153 -0
  56. package/docs/components/toast.md +119 -0
  57. package/docs/components/tooltip.md +151 -0
  58. package/docs/components/traits.md +261 -0
  59. package/docs/conditional-rendering.md +170 -588
  60. package/docs/contributing.md +300 -25
  61. package/docs/core-concepts.md +205 -374
  62. package/docs/elements.md +251 -367
  63. package/docs/extending-native-document-element.md +192 -207
  64. package/docs/filters.md +153 -1122
  65. package/docs/getting-started.md +193 -267
  66. package/docs/i18n.md +241 -0
  67. package/docs/index.md +76 -0
  68. package/docs/lifecycle-events.md +143 -75
  69. package/docs/list-rendering.md +227 -852
  70. package/docs/memory-management.md +134 -47
  71. package/docs/native-document-element.md +337 -186
  72. package/docs/native-fetch.md +99 -630
  73. package/docs/observable-resource.md +364 -0
  74. package/docs/observables.md +592 -526
  75. package/docs/routing.md +244 -653
  76. package/docs/state-management.md +134 -241
  77. package/docs/svg-elements.md +231 -0
  78. package/docs/theming.md +409 -0
  79. package/docs/tutorials/.gitkeep +0 -0
  80. package/docs/validation.md +95 -97
  81. package/docs/vitepress-conventions.md +219 -0
  82. package/package.json +34 -13
  83. package/readme.md +269 -89
  84. package/src/components/card/Card.js +93 -39
  85. package/src/components/card/index.js +1 -1
  86. package/src/components/list/HasListItem.js +171 -0
  87. package/src/components/list/List.js +41 -107
  88. package/src/components/list/ListDivider.js +39 -0
  89. package/src/components/list/ListGroup.js +76 -59
  90. package/src/components/list/ListItem.js +117 -69
  91. package/src/components/list/index.js +3 -1
  92. package/src/components/list/types/ListItem.d.ts +45 -34
  93. package/src/components/spacer/Spacer.js +1 -1
  94. package/src/core/data/ObservableResource.js +5 -0
  95. package/src/core/data/observable-helpers/observable.prototypes.js +2 -0
  96. package/src/ui/components/card/CardRender.js +133 -0
  97. package/src/ui/components/card/card.css +169 -0
  98. package/src/ui/components/contextmenu/ContextmenuRender.js +1 -1
  99. package/src/ui/components/list/ListRender.js +18 -0
  100. package/src/ui/components/list/divider/ListDividerRender.js +10 -0
  101. package/src/ui/components/list/divider/list-divider.css +12 -0
  102. package/src/ui/components/list/group/ListGroupRender.js +61 -0
  103. package/src/ui/components/list/group/list-group.css +62 -0
  104. package/src/ui/components/list/item/ListItemRender.js +238 -0
  105. package/src/ui/components/list/item/list-item.css +191 -0
  106. package/src/ui/components/list/list.css +24 -0
  107. package/src/ui/components/spacer/SpacerRender.js +10 -0
  108. 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
+ ```
@@ -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
- ## Basic Lifecycle Hooks
12
+ ## `mounted(callback)`
6
13
 
7
- ### mounted() - Element Added to DOM
14
+ Fires when the element is added to the DOM:
8
15
 
9
16
  ```javascript
10
- const myComponent = Div("Hello World")
11
- .nd.mounted(element => {
12
- console.log("Element is now in the DOM!", element);
13
- // Setup code here
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); // Triggers mounted callback
23
+ document.body.appendChild(myComponent); // triggers mounted
17
24
  ```
18
25
 
19
- ### unmounted() - Element Removed from DOM
26
+ ### Auto-focus example
20
27
 
21
28
  ```javascript
22
- const myComponent = Div("Temporary content")
23
- .nd.unmounted(element => {
24
- console.log("Element removed from DOM!", element);
25
- // Cleanup external resources only
26
- // Element can be re-injected later
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
- ## Combined Lifecycle Management
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
- ## Practical Examples
38
+ ## `unmounted(callback)`
52
39
 
53
- ### Auto-focus Input Field
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 focusInput = Input({ placeholder: "Auto-focused" })
57
- .nd.mounted(element => {
58
- element.focus();
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
- ### Event Listener Cleanup
54
+ ### External event listener cleanup
63
55
 
64
56
  ```javascript
57
+ function MyButton() {
58
+ const handler = () => console.log('Global click');
65
59
 
66
- const MyButton = function() {
67
- const handler = () => console.log("Global click detected");
68
- return Button("Click me")
69
- .nd.mounted(button => {
60
+ return Button('Click me')
61
+ .nd
62
+ .mounted(el => {
70
63
  document.addEventListener('click', handler);
71
64
  })
72
- .nd.unmounted(button => {
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
- ### Observable Management Warning
71
+ ---
72
+
73
+ ## `lifecycle({ mounted, unmounted })`
74
+
75
+ Configure both hooks at once:
81
76
 
82
77
  ```javascript
83
- const reusableComponent = Div()
84
- .nd.unmounted(element => {
85
- // AVOID: Don't cleanup observables if element might be re-injected
86
- // myObservable.cleanup(); // Only do this if element is permanently destroyed
87
-
88
- // GOOD: Only cleanup external resources
89
- clearInterval(element.timerId);
90
- element.websocket?.close();
91
- });
92
-
93
- // Element can be safely re-appended later
94
- document.body.appendChild(reusableComponent);
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
- ## Next Steps
92
+ ---
93
+
94
+ ## `beforeUnmount(id, callback)`
98
95
 
99
- Now that you understand lifecycle events, explore these related topics:
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
- - **[NDElement](native-document-element.md)** - Native Document Element
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
- ## Utilities
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
- - **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
111
- - **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
112
- - **[Filters](docs/utils/filters.md)** - Data filtering helpers
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