native-document 1.0.165 → 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 (488) hide show
  1. package/.vitepress/config.js +166 -0
  2. package/CHANGELOG.md +153 -0
  3. package/components.d.ts +2 -0
  4. package/components.js +2 -1
  5. package/devtools/widget.js +1 -1
  6. package/dist/native-document.components.min.js +11589 -2983
  7. package/dist/native-document.dev.js +2280 -396
  8. package/dist/native-document.dev.js.map +1 -1
  9. package/dist/native-document.min.js +1 -1
  10. package/docs/advanced-components.md +213 -608
  11. package/docs/anchor.md +173 -312
  12. package/docs/cache.md +95 -803
  13. package/docs/cli.md +179 -0
  14. package/docs/components/accordion.md +172 -0
  15. package/docs/components/alert.md +99 -0
  16. package/docs/components/avatar.md +160 -0
  17. package/docs/components/badge.md +102 -0
  18. package/docs/components/breadcrumb.md +89 -0
  19. package/docs/components/button.md +183 -0
  20. package/docs/components/card.md +69 -0
  21. package/docs/components/context-menu.md +118 -0
  22. package/docs/components/data-table.md +345 -0
  23. package/docs/components/dropdown.md +214 -0
  24. package/docs/components/form/autocomplete-field.md +81 -0
  25. package/docs/components/form/checkbox-field.md +41 -0
  26. package/docs/components/form/checkbox-group-field.md +54 -0
  27. package/docs/components/form/color-field.md +64 -0
  28. package/docs/components/form/date-field.md +92 -0
  29. package/docs/components/form/field-collection.md +63 -0
  30. package/docs/components/form/file-field.md +203 -0
  31. package/docs/components/form/form-control.md +87 -0
  32. package/docs/components/form/image-field.md +90 -0
  33. package/docs/components/form/index.md +115 -0
  34. package/docs/components/form/number-field.md +65 -0
  35. package/docs/components/form/radio-field.md +51 -0
  36. package/docs/components/form/select-field.md +123 -0
  37. package/docs/components/form/slider.md +136 -0
  38. package/docs/components/form/string-field.md +134 -0
  39. package/docs/components/form/textarea-field.md +65 -0
  40. package/docs/components/form-fields.md +372 -0
  41. package/docs/components/getting-started.md +264 -0
  42. package/docs/components/index.md +337 -0
  43. package/docs/components/layout.md +279 -0
  44. package/docs/components/list.md +73 -0
  45. package/docs/components/menu.md +215 -0
  46. package/docs/components/modal.md +156 -0
  47. package/docs/components/pagination.md +95 -0
  48. package/docs/components/popover.md +131 -0
  49. package/docs/components/progress.md +111 -0
  50. package/docs/components/shortcut-manager.md +221 -0
  51. package/docs/components/simple-table.md +107 -0
  52. package/docs/components/skeleton.md +155 -0
  53. package/docs/components/spinner.md +100 -0
  54. package/docs/components/splitter.md +133 -0
  55. package/docs/components/stepper.md +163 -0
  56. package/docs/components/switch.md +113 -0
  57. package/docs/components/tabs.md +153 -0
  58. package/docs/components/toast.md +119 -0
  59. package/docs/components/tooltip.md +151 -0
  60. package/docs/components/traits.md +261 -0
  61. package/docs/conditional-rendering.md +170 -588
  62. package/docs/contributing.md +300 -25
  63. package/docs/core-concepts.md +205 -374
  64. package/docs/elements.md +251 -367
  65. package/docs/extending-native-document-element.md +192 -207
  66. package/docs/filters.md +153 -1122
  67. package/docs/getting-started.md +193 -267
  68. package/docs/i18n.md +241 -0
  69. package/docs/index.md +76 -0
  70. package/docs/lifecycle-events.md +143 -75
  71. package/docs/list-rendering.md +227 -852
  72. package/docs/memory-management.md +134 -47
  73. package/docs/native-document-element.md +337 -186
  74. package/docs/native-fetch.md +99 -630
  75. package/docs/observable-resource.md +364 -0
  76. package/docs/observables.md +592 -526
  77. package/docs/routing.md +244 -653
  78. package/docs/state-management.md +134 -241
  79. package/docs/svg-elements.md +231 -0
  80. package/docs/theming.md +409 -0
  81. package/docs/validation.md +95 -97
  82. package/docs/vitepress-conventions.md +219 -0
  83. package/eslint.config.js +28 -33
  84. package/i18n.js +1 -1
  85. package/i18n.ts +2 -0
  86. package/index.js +3 -0
  87. package/package.json +36 -14
  88. package/readme.md +269 -89
  89. package/src/components/$traits/has-draggable/HasDraggable.d.ts +4 -0
  90. package/src/components/$traits/has-draggable/HasDraggable.js +13 -0
  91. package/src/components/$traits/has-items/HasItems.d.ts +9 -0
  92. package/src/components/$traits/has-items/HasItems.js +6 -6
  93. package/src/components/$traits/has-position/HasFullPosition.d.ts +14 -0
  94. package/src/components/$traits/has-position/HasFullPosition.js +44 -0
  95. package/src/components/$traits/has-position/HasPosition.d.ts +7 -0
  96. package/src/components/$traits/has-position/HasPosition.js +23 -1
  97. package/src/components/$traits/has-resizable/HasResizable.d.ts +13 -0
  98. package/src/components/$traits/has-resizable/HasResizable.js +9 -0
  99. package/src/components/$traits/has-validation/HasValidation.d.ts +17 -0
  100. package/src/components/$traits/has-validation/HasValidation.js +54 -7
  101. package/src/components/BaseComponent.d.ts +32 -0
  102. package/src/components/BaseComponent.js +65 -9
  103. package/src/components/accordion/Accordion.js +39 -14
  104. package/src/components/accordion/AccordionItem.js +45 -14
  105. package/src/components/accordion/index.js +2 -2
  106. package/src/components/accordion/types/Accordion.d.ts +47 -0
  107. package/src/components/accordion/types/AccordionItem.d.ts +48 -0
  108. package/src/components/alert/Alert.js +70 -38
  109. package/src/components/alert/index.js +2 -2
  110. package/src/components/alert/types/Alert.d.ts +62 -0
  111. package/src/components/avatar/Avatar.js +49 -12
  112. package/src/components/avatar/AvatarGroup.js +50 -2
  113. package/src/components/avatar/index.js +2 -2
  114. package/src/components/avatar/types/Avatar.d.ts +74 -0
  115. package/src/components/avatar/types/AvatarGroup.d.ts +32 -0
  116. package/src/components/badge/Badge.js +125 -5
  117. package/src/components/badge/index.js +2 -2
  118. package/src/components/badge/types/Badge.d.ts +51 -0
  119. package/src/components/breadcrumb/BreadCrumb.js +61 -5
  120. package/src/components/breadcrumb/index.js +2 -2
  121. package/src/components/breadcrumb/types/BreadCrumb.d.ts +42 -0
  122. package/src/components/button/Button.js +164 -9
  123. package/src/components/button/index.js +1 -1
  124. package/src/components/button/types/Button.d.ts +62 -0
  125. package/src/components/card/Card.js +204 -32
  126. package/src/components/card/index.js +4 -4
  127. package/src/components/card/types/Card.d.ts +42 -0
  128. package/src/components/context-menu/ContextMenu.js +49 -5
  129. package/src/components/context-menu/ContextMenuGroup.js +15 -2
  130. package/src/components/context-menu/ContextMenuItem.js +14 -2
  131. package/src/components/context-menu/index.js +5 -5
  132. package/src/components/context-menu/types/ContextMenu.d.ts +30 -0
  133. package/src/components/context-menu/types/ContextMenuGroup.d.ts +18 -0
  134. package/src/components/context-menu/types/ContextMenuItem.d.ts +18 -0
  135. package/src/components/divider/Divider.js +120 -4
  136. package/src/components/divider/index.js +3 -3
  137. package/src/components/divider/types/Divider.d.ts +55 -0
  138. package/src/components/dropdown/Dropdown.js +239 -16
  139. package/src/components/dropdown/DropdownDivider.js +22 -2
  140. package/src/components/dropdown/DropdownGroup.js +44 -5
  141. package/src/components/dropdown/DropdownItem.js +76 -3
  142. package/src/components/dropdown/DropdownTrigger.js +49 -20
  143. package/src/components/dropdown/helpers.js +1 -1
  144. package/src/components/dropdown/index.js +6 -6
  145. package/src/components/dropdown/types/Dropdown.d.ts +88 -0
  146. package/src/components/dropdown/types/DropdownDivider.d.ts +20 -0
  147. package/src/components/dropdown/types/DropdownGroup.d.ts +25 -0
  148. package/src/components/dropdown/types/DropdownItem.d.ts +41 -0
  149. package/src/components/dropdown/types/DropdownTrigger.d.ts +32 -0
  150. package/src/components/form/FormControl.js +156 -13
  151. package/src/components/form/field/Field.js +172 -9
  152. package/src/components/form/field/FieldCollection.js +116 -12
  153. package/src/components/form/field/types/AutocompleteField.js +92 -2
  154. package/src/components/form/field/types/CheckboxField.js +43 -2
  155. package/src/components/form/field/types/CheckboxGroupField.js +83 -6
  156. package/src/components/form/field/types/ColorField.js +56 -3
  157. package/src/components/form/field/types/DateField.js +155 -4
  158. package/src/components/form/field/types/EmailField.js +54 -4
  159. package/src/components/form/field/types/FileField.js +140 -6
  160. package/src/components/form/field/types/HiddenField.js +27 -1
  161. package/src/components/form/field/types/ImageField.js +82 -3
  162. package/src/components/form/field/types/NumberField.js +97 -4
  163. package/src/components/form/field/types/PasswordField.js +103 -7
  164. package/src/components/form/field/types/RadioField.js +75 -4
  165. package/src/components/form/field/types/RangeField.js +67 -1
  166. package/src/components/form/field/types/SearchField.js +41 -2
  167. package/src/components/form/field/types/SelectField.js +133 -4
  168. package/src/components/form/field/types/StringField.js +91 -2
  169. package/src/components/form/field/types/TelField.js +55 -4
  170. package/src/components/form/field/types/TextAreaField.js +76 -2
  171. package/src/components/form/field/types/TimeField.js +120 -5
  172. package/src/components/form/field/types/UrlField.js +59 -4
  173. package/src/components/form/field/types/file-field-mode/FileAvatarMode.js +83 -4
  174. package/src/components/form/field/types/file-field-mode/FileDropzoneMode.js +61 -3
  175. package/src/components/form/field/types/file-field-mode/FileItemPreview.js +79 -3
  176. package/src/components/form/field/types/file-field-mode/FileNativeMode.js +24 -2
  177. package/src/components/form/field/types/file-field-mode/FileUploadButtonMode.js +64 -3
  178. package/src/components/form/field/types/file-field-mode/FileWallMode.js +56 -3
  179. package/src/components/form/index.js +28 -28
  180. package/src/components/form/types/Field.d.ts +73 -0
  181. package/src/components/form/types/FieldCollection.d.ts +53 -0
  182. package/src/components/form/types/FormControl.d.ts +64 -0
  183. package/src/components/form/types/fields/AutocompleteField.d.ts +48 -0
  184. package/src/components/form/types/fields/CheckboxField.d.ts +33 -0
  185. package/src/components/form/types/fields/CheckboxGroupField.d.ts +49 -0
  186. package/src/components/form/types/fields/ColorField.d.ts +37 -0
  187. package/src/components/form/types/fields/DateField.d.ts +70 -0
  188. package/src/components/form/types/fields/EmailField.d.ts +35 -0
  189. package/src/components/form/types/fields/FileAvatarMode.d.ts +46 -0
  190. package/src/components/form/types/fields/FileDropzoneMode.d.ts +28 -0
  191. package/src/components/form/types/fields/FileField.d.ts +56 -0
  192. package/src/components/form/types/fields/FileItemPreview.d.ts +35 -0
  193. package/src/components/form/types/fields/FileNativeMode.d.ts +21 -0
  194. package/src/components/form/types/fields/FileUploadButtonMode.d.ts +34 -0
  195. package/src/components/form/types/fields/FileWallMode.d.ts +32 -0
  196. package/src/components/form/types/fields/HiddenField.d.ts +26 -0
  197. package/src/components/form/types/fields/ImageField.d.ts +45 -0
  198. package/src/components/form/types/fields/NumberField.d.ts +48 -0
  199. package/src/components/form/types/fields/PasswordField.d.ts +46 -0
  200. package/src/components/form/types/fields/RadioField.d.ts +48 -0
  201. package/src/components/form/types/fields/RangeField.d.ts +44 -0
  202. package/src/components/form/types/fields/SearchField.d.ts +34 -0
  203. package/src/components/form/types/fields/SelectField.d.ts +71 -0
  204. package/src/components/form/types/fields/StringField.d.ts +48 -0
  205. package/src/components/form/types/fields/TelField.d.ts +37 -0
  206. package/src/components/form/types/fields/TextAreaField.d.ts +44 -0
  207. package/src/components/form/types/fields/TimeField.d.ts +51 -0
  208. package/src/components/form/types/fields/UrlField.d.ts +35 -0
  209. package/src/components/form/validation/Validation.js +54 -54
  210. package/src/components/index.d.ts +160 -0
  211. package/src/components/list/HasListItem.js +171 -0
  212. package/src/components/list/List.js +85 -67
  213. package/src/components/list/ListDivider.js +39 -0
  214. package/src/components/list/ListGroup.js +105 -38
  215. package/src/components/list/ListItem.js +158 -49
  216. package/src/components/list/index.js +8 -6
  217. package/src/components/list/types/List.d.ts +43 -0
  218. package/src/components/list/types/ListGroup.d.ts +37 -0
  219. package/src/components/list/types/ListItem.d.ts +53 -0
  220. package/src/components/menu/HasMenuItem.js +55 -6
  221. package/src/components/menu/Menu.js +113 -22
  222. package/src/components/menu/MenuDivider.js +18 -2
  223. package/src/components/menu/MenuGroup.js +61 -6
  224. package/src/components/menu/MenuItem.js +95 -11
  225. package/src/components/menu/MenuLink.js +27 -2
  226. package/src/components/menu/index.js +6 -6
  227. package/src/components/menu/types/Menu.d.ts +60 -0
  228. package/src/components/menu/types/MenuDivider.d.ts +19 -0
  229. package/src/components/menu/types/MenuGroup.d.ts +44 -0
  230. package/src/components/menu/types/MenuItem.d.ts +46 -0
  231. package/src/components/menu/types/MenuLink.d.ts +16 -0
  232. package/src/components/modal/Modal.js +258 -17
  233. package/src/components/modal/index.js +3 -3
  234. package/src/components/modal/types/Modal.d.ts +94 -0
  235. package/src/components/pagination/Pagination.js +155 -7
  236. package/src/components/pagination/index.js +3 -3
  237. package/src/components/pagination/types/Pagination.d.ts +68 -0
  238. package/src/components/popover/Popover.js +198 -11
  239. package/src/components/popover/PopoverFooter.js +33 -9
  240. package/src/components/popover/PopoverHeader.js +33 -8
  241. package/src/components/popover/index.js +4 -4
  242. package/src/components/popover/types/Popover.d.ts +83 -0
  243. package/src/components/popover/types/PopoverFooter.d.ts +24 -0
  244. package/src/components/popover/types/PopoverHeader.d.ts +26 -0
  245. package/src/components/progress/Progress.js +182 -13
  246. package/src/components/progress/index.js +3 -3
  247. package/src/components/progress/types/Progress.d.ts +77 -0
  248. package/src/components/skeleton/Skeleton.js +117 -49
  249. package/src/components/skeleton/index.js +3 -3
  250. package/src/components/skeleton/types/Skeleton.d.ts +55 -0
  251. package/src/components/slider/Slider.js +207 -10
  252. package/src/components/slider/index.js +2 -2
  253. package/src/components/slider/types/Slider.d.ts +82 -0
  254. package/src/components/spacer/Spacer.js +12 -3
  255. package/src/components/spacer/index.js +2 -2
  256. package/src/components/spacer/types/Spacer.d.ts +19 -0
  257. package/src/components/spinner/Spinner.js +180 -9
  258. package/src/components/spinner/index.js +3 -3
  259. package/src/components/spinner/types/Spinner.d.ts +71 -0
  260. package/src/components/splitter/Splitter.js +76 -13
  261. package/src/components/splitter/SplitterGutter.js +67 -5
  262. package/src/components/splitter/SplitterPanel.js +69 -2
  263. package/src/components/splitter/index.js +5 -5
  264. package/src/components/splitter/types/Splitter.d.ts +38 -0
  265. package/src/components/splitter/types/SplitterGutter.d.ts +38 -0
  266. package/src/components/splitter/types/SplitterPanel.d.ts +41 -0
  267. package/src/components/stacks/AbsoluteStack.js +23 -3
  268. package/src/components/stacks/FixedStack.js +23 -3
  269. package/src/components/stacks/HStack.js +24 -3
  270. package/src/components/stacks/PositionStack.js +111 -3
  271. package/src/components/stacks/RelativeStack.js +23 -3
  272. package/src/components/stacks/Stack.js +73 -2
  273. package/src/components/stacks/VStack.js +24 -4
  274. package/src/components/stacks/index.js +7 -7
  275. package/src/components/stacks/types/AbsoluteStack.d.ts +16 -0
  276. package/src/components/stacks/types/FixedStack.d.ts +16 -0
  277. package/src/components/stacks/types/HStack.d.ts +16 -0
  278. package/src/components/stacks/types/PositionStack.d.ts +54 -0
  279. package/src/components/stacks/types/RelativeStack.d.ts +17 -0
  280. package/src/components/stacks/types/Stack.d.ts +39 -0
  281. package/src/components/stacks/types/VStack.d.ts +16 -0
  282. package/src/components/stepper/Stepper.js +152 -12
  283. package/src/components/stepper/StepperStep.js +104 -3
  284. package/src/components/stepper/index.js +4 -4
  285. package/src/components/stepper/types/Stepper.d.ts +68 -0
  286. package/src/components/stepper/types/StepperStep.d.ts +54 -0
  287. package/src/components/switch/Switch.js +143 -6
  288. package/src/components/switch/index.js +1 -1
  289. package/src/components/switch/types/Switch.d.ts +55 -0
  290. package/src/components/table/Column.js +105 -6
  291. package/src/components/table/ColumnGroup.js +48 -3
  292. package/src/components/table/DataTable.js +256 -19
  293. package/src/components/table/SimpleTable.js +58 -4
  294. package/src/components/table/index.js +2 -2
  295. package/src/components/table/types/Column.d.ts +49 -0
  296. package/src/components/table/types/ColumnGroup.d.ts +28 -0
  297. package/src/components/table/types/DataTable.d.ts +97 -0
  298. package/src/components/table/types/SimpleTable.d.ts +40 -0
  299. package/src/components/tabs/Tabs.js +192 -5
  300. package/src/components/tabs/index.js +3 -3
  301. package/src/components/tabs/types/Tabs.d.ts +78 -0
  302. package/src/components/toast/Toast.js +133 -5
  303. package/src/components/toast/index.js +3 -3
  304. package/src/components/toast/types/Toast.d.ts +57 -0
  305. package/src/components/toast/types/ToastError.d.ts +7 -0
  306. package/src/components/toast/types/ToastInfo.d.ts +7 -0
  307. package/src/components/toast/types/ToastSuccess.d.ts +7 -0
  308. package/src/components/toast/types/ToastWarning.d.ts +7 -0
  309. package/src/components/tooltip/Tooltip.js +157 -13
  310. package/src/components/tooltip/index.js +2 -2
  311. package/src/components/tooltip/prototypes.js +1 -1
  312. package/src/components/tooltip/types/Tooltip.d.ts +65 -0
  313. package/src/core/data/MemoryManager.js +2 -2
  314. package/src/core/data/Observable.js +15 -18
  315. package/src/core/data/ObservableArray.js +118 -46
  316. package/src/core/data/ObservableChecker.js +2 -2
  317. package/src/core/data/ObservableItem.js +135 -21
  318. package/src/core/data/ObservableObject.js +126 -35
  319. package/src/core/data/ObservableResource.js +118 -3
  320. package/src/core/data/Store.js +142 -26
  321. package/src/core/data/observable-helpers/observable.is-to.js +196 -1
  322. package/src/core/data/observable-helpers/observable.prototypes.js +35 -8
  323. package/src/core/elements/anchor/anchor-with-sentinel.js +23 -2
  324. package/src/core/elements/anchor/anchor.js +16 -7
  325. package/src/core/elements/anchor/one-child-anchor-overwriting.js +2 -2
  326. package/src/core/elements/content-formatter.js +1 -1
  327. package/src/core/elements/control/for-each-array.js +9 -9
  328. package/src/core/elements/control/for-each.js +14 -14
  329. package/src/core/elements/control/show-if.js +11 -11
  330. package/src/core/elements/control/show-when.js +5 -5
  331. package/src/core/elements/control/switch.js +14 -14
  332. package/src/core/elements/description-list.js +1 -1
  333. package/src/core/elements/form.js +2 -2
  334. package/src/core/elements/fragment.js +1 -1
  335. package/src/core/elements/html5-semantics.js +1 -1
  336. package/src/core/elements/img.js +3 -3
  337. package/src/core/elements/interactive.js +1 -1
  338. package/src/core/elements/list.js +1 -1
  339. package/src/core/elements/medias.js +1 -1
  340. package/src/core/elements/meta-data.js +1 -1
  341. package/src/core/elements/svg.js +1 -1
  342. package/src/core/elements/table.js +1 -1
  343. package/src/core/errors/ArgTypesError.js +1 -1
  344. package/src/core/utils/HasEventEmitter.js +36 -2
  345. package/src/core/utils/args-types.js +9 -9
  346. package/src/core/utils/cache.js +1 -1
  347. package/src/core/utils/callback-handler.js +29 -0
  348. package/src/core/utils/debug-manager.js +6 -6
  349. package/src/core/utils/events.js +139 -139
  350. package/src/core/utils/filters/date.js +84 -3
  351. package/src/core/utils/filters/standard.js +136 -11
  352. package/src/core/utils/filters/strings.js +34 -2
  353. package/src/core/utils/filters/utils.js +40 -4
  354. package/src/core/utils/formatters.js +4 -4
  355. package/src/core/utils/helpers.js +39 -7
  356. package/src/core/utils/localstorage.js +11 -11
  357. package/src/core/utils/memoize.js +56 -3
  358. package/src/core/utils/plugins-manager.js +3 -3
  359. package/src/core/utils/property-accumulator.js +6 -6
  360. package/src/core/utils/prototypes.js +26 -1
  361. package/src/core/utils/shortcut-manager.js +2 -2
  362. package/src/core/utils/validator.js +8 -8
  363. package/src/core/wrappers/AttributesWrapper.js +32 -22
  364. package/src/core/wrappers/DocumentObserver.js +3 -3
  365. package/src/core/wrappers/ElementCreator.js +5 -5
  366. package/src/core/wrappers/HtmlElementWrapper.js +38 -12
  367. package/src/core/wrappers/NDElement.js +328 -22
  368. package/src/core/wrappers/NdPrototype.js +60 -16
  369. package/src/core/wrappers/SingletonView.js +50 -2
  370. package/src/core/wrappers/SvgElementWrapper.js +1 -1
  371. package/src/core/wrappers/constants.js +35 -2
  372. package/src/core/wrappers/prototypes/attributes-extensions.js +7 -7
  373. package/src/core/wrappers/prototypes/nd-element-extensions.js +72 -6
  374. package/src/core/wrappers/prototypes/nd-element.transition.extensions.js +42 -2
  375. package/src/core/wrappers/template-cloner/NodeCloner.js +53 -8
  376. package/src/core/wrappers/template-cloner/TemplateCloner.js +75 -6
  377. package/src/core/wrappers/template-cloner/attributes-hydrator.js +58 -2
  378. package/src/core/wrappers/template-cloner/utils.js +42 -6
  379. package/src/fetch/NativeFetch.js +3 -3
  380. package/src/i18n/bin/scan.js +6 -6
  381. package/src/i18n/index.d.ts +2 -0
  382. package/src/i18n/service/I18nService.d.ts +27 -0
  383. package/src/i18n/service/I18nService.js +5 -5
  384. package/src/i18n/service/functions.d.ts +22 -0
  385. package/src/i18n/service/functions.js +2 -2
  386. package/src/router/Route.js +3 -3
  387. package/src/router/RouteGroupHelper.js +2 -2
  388. package/src/router/Router.js +15 -15
  389. package/src/router/RouterComponent.js +33 -7
  390. package/src/router/link.js +4 -4
  391. package/src/router/modes/HashRouter.js +2 -2
  392. package/src/router/modes/HistoryRouter.js +2 -2
  393. package/src/router/modes/MemoryRouter.js +1 -1
  394. package/src/ui/components/accordion/AccordionItemRender.js +3 -3
  395. package/src/ui/components/accordion/AccordionRender.js +1 -1
  396. package/src/ui/components/alert/AlertRender.js +10 -10
  397. package/src/ui/components/avatar/avata-group/AvatarGroupRender.js +1 -1
  398. package/src/ui/components/avatar/avatar/AvatarRender.js +1 -1
  399. package/src/ui/components/breadcrumb/BreadcrumbRender.js +2 -2
  400. package/src/ui/components/button/ButtonRender.js +1 -1
  401. package/src/ui/components/card/CardRender.js +133 -0
  402. package/src/ui/components/card/card.css +169 -0
  403. package/src/ui/components/contextmenu/ContextmenuRender.js +6 -6
  404. package/src/ui/components/dropdown/DropdownRender.js +8 -8
  405. package/src/ui/components/dropdown/group/DropdownGroupRender.js +2 -2
  406. package/src/ui/components/dropdown/item/DropdownItemRender.js +1 -1
  407. package/src/ui/components/form/FieldCollectionRender.js +2 -2
  408. package/src/ui/components/form/FormControlRender.js +5 -5
  409. package/src/ui/components/form/fields/AutocompleteFieldRender.js +3 -3
  410. package/src/ui/components/form/fields/CheckboxFieldRender.js +1 -1
  411. package/src/ui/components/form/fields/CheckboxGroupFieldRender.js +1 -1
  412. package/src/ui/components/form/fields/DateFieldRender.js +7 -7
  413. package/src/ui/components/form/fields/EmailFieldRender.js +1 -1
  414. package/src/ui/components/form/fields/FieldRender.js +4 -4
  415. package/src/ui/components/form/fields/FileFieldRender.js +1 -1
  416. package/src/ui/components/form/fields/PasswordFieldRender.js +2 -2
  417. package/src/ui/components/form/fields/RadioFieldRender.js +1 -1
  418. package/src/ui/components/form/fields/RangeFieldRender.js +1 -1
  419. package/src/ui/components/form/fields/SelectFieldRender.js +2 -2
  420. package/src/ui/components/form/fields/SliderFieldRender.js +6 -6
  421. package/src/ui/components/form/fields/StringFieldRender.js +1 -1
  422. package/src/ui/components/form/fields/TelFieldRender.js +1 -1
  423. package/src/ui/components/form/fields/TextAreaFieldRender.js +1 -1
  424. package/src/ui/components/form/fields/TimeFieldRender.js +3 -3
  425. package/src/ui/components/form/fields/UrlFieldRender.js +1 -1
  426. package/src/ui/components/form/file-upload-mode/FileAvatarModeRender.js +1 -1
  427. package/src/ui/components/form/file-upload-mode/FileDropzoneModeRender.js +2 -2
  428. package/src/ui/components/form/file-upload-mode/FileUploadButtonModeRender.js +2 -2
  429. package/src/ui/components/form/file-upload-mode/FileWallModeRender.js +1 -1
  430. package/src/ui/components/form/helpers.js +8 -8
  431. package/src/ui/components/form/index.js +27 -27
  432. package/src/ui/components/list/ListRender.js +18 -0
  433. package/src/ui/components/list/divider/ListDividerRender.js +10 -0
  434. package/src/ui/components/list/divider/list-divider.css +12 -0
  435. package/src/ui/components/list/group/ListGroupRender.js +61 -0
  436. package/src/ui/components/list/group/list-group.css +62 -0
  437. package/src/ui/components/list/item/ListItemRender.js +238 -0
  438. package/src/ui/components/list/item/list-item.css +191 -0
  439. package/src/ui/components/list/list.css +24 -0
  440. package/src/ui/components/menu/MenuDividerRender.js +1 -1
  441. package/src/ui/components/menu/MenuGroupRender.js +3 -3
  442. package/src/ui/components/menu/MenuItemRender.js +2 -2
  443. package/src/ui/components/menu/MenuLinkRender.js +3 -3
  444. package/src/ui/components/menu/helpers.js +4 -4
  445. package/src/ui/components/modal/ModalRender.js +4 -4
  446. package/src/ui/components/pagination/PaginationRender.js +9 -9
  447. package/src/ui/components/popover/PopoverRender.js +7 -7
  448. package/src/ui/components/progress/ProgressRender.js +12 -12
  449. package/src/ui/components/skeleton/SkeletonRender.js +56 -0
  450. package/src/ui/components/spacer/SpacerRender.js +10 -0
  451. package/src/ui/components/splitter/SplitterGutterRender.js +1 -1
  452. package/src/ui/components/splitter/SplitterPanelRender.js +2 -2
  453. package/src/ui/components/stacks/PositionStackRender.js +1 -1
  454. package/src/ui/components/stacks/StackRender.js +1 -1
  455. package/src/ui/components/stacks/absolute-stack/AbsoluteStackRender.js +1 -1
  456. package/src/ui/components/stacks/fixed-stack/FixedStackRender.js +1 -1
  457. package/src/ui/components/stacks/h-stack/HStackRender.js +1 -1
  458. package/src/ui/components/stacks/index.js +5 -5
  459. package/src/ui/components/stacks/relative-stack/RelativeStackRender.js +1 -1
  460. package/src/ui/components/stacks/v-stack/VStackRender.js +1 -1
  461. package/src/ui/components/stepper/StepperRender.js +2 -2
  462. package/src/ui/components/stepper/StepperStepRender.js +4 -4
  463. package/src/ui/components/switch/SwitchRender.js +4 -4
  464. package/src/ui/components/table/data-table/DataTableRender.js +5 -5
  465. package/src/ui/components/table/data-table/bulk-actions.js +7 -7
  466. package/src/ui/components/table/data-table/pagination.js +6 -6
  467. package/src/ui/components/table/data-table/tables.js +25 -25
  468. package/src/ui/components/table/data-table/toolbar.js +3 -3
  469. package/src/ui/components/table/simple-table/SimpleTableRender.js +8 -8
  470. package/src/ui/components/tabs/TabsRender.js +11 -11
  471. package/src/ui/components/toast/ToastRender.js +3 -3
  472. package/src/ui/components/tooltip/TooltipRender.js +1 -1
  473. package/src/ui/index.js +44 -36
  474. package/types/elements.d.ts +163 -1037
  475. package/types/forms.d.ts +16 -20
  476. package/types/globals.d.ts +543 -0
  477. package/types/images.d.ts +2 -2
  478. package/types/observable-resource.d.ts +3 -0
  479. package/types/property-accumulator.d.ts +4 -4
  480. package/types/store.d.ts +26 -2
  481. package/types/validator.ts +3 -3
  482. package/ui.js +1 -0
  483. package/src/components/form/field/DefaultRender.js +0 -77
  484. package/src/components/form/field/FieldFactory.js +0 -107
  485. package/src/components/skeleton/SkeletonList.js +0 -0
  486. package/src/components/skeleton/SkeletonParagraph.js +0 -0
  487. package/src/components/skeleton/SkeletonTable.js +0 -0
  488. /package/{src/components/skeleton/SkeletonCard.js → docs/tutorials/.gitkeep} +0 -0
@@ -1,162 +1,107 @@
1
+ ---
2
+ title: Observables
3
+ description: Reactive state management at the core of NativeDocument - create values that automatically update the UI when they change
4
+ ---
5
+
1
6
  # Observables
2
7
 
3
- Observables are the reactive core of NativeDocument. They allow you to create values that automatically update in the user interface when they change.
8
+ Observables are the reactive core of NativeDocument. They wrap values and automatically update the UI when those values change.
4
9
 
5
- ## Creating Simple Observables
10
+ ## Creating Observables
6
11
 
7
12
  ```javascript
8
- const count = Observable(0);
9
- const message = Observable("Hello World");
13
+ const count = Observable(0);
14
+ const message = Observable('Hello World');
10
15
  const isVisible = Observable(true);
11
16
  ```
12
17
 
13
18
  ## Reading and Modifying Values
14
19
 
15
20
  ```javascript
16
- const name = Observable("John");
21
+ const name = Observable('John');
17
22
 
18
23
  // Read the current value
19
- console.log(name.val()); // "John"
20
-
21
- // Using the proxy syntax (shorthand)
22
- console.log(name.$value); // "John"
24
+ console.log(name.val()); // "John"
25
+ console.log(name.$value); // "John" - proxy shorthand
23
26
 
24
27
  // Update the value
25
- name.set("Jane");
26
- console.log(name.val()); // "Jane"
28
+ name.set('Jane');
29
+ console.log(name.val()); // "Jane"
27
30
 
28
- // Update using proxy syntax
29
- name.$value = "Bob";
30
- console.log(name.val()); // "Bob"
31
+ // Update with proxy syntax
32
+ name.$value = 'Bob';
33
+ console.log(name.val()); // "Bob"
31
34
 
32
35
  // Update with a function
33
- name.set(currentName => currentName.toUpperCase());
34
- console.log(name.val()); // "BOB"
36
+ name.set(current => current.toUpperCase());
37
+ console.log(name.val()); // "BOB"
35
38
  ```
36
39
 
37
40
  ## Listening to Changes
38
41
 
39
- The `.subscribe()` method allows you to listen to every change in an observable. The callback receives both the new value and the previous value.
42
+ `.subscribe()` runs on every value change, receiving the new and old values:
40
43
 
41
44
  ```javascript
42
45
  const counter = Observable(0);
43
46
 
44
- counter.subscribe(newValue => {
45
- console.log("Counter is now:", newValue);
47
+ counter.subscribe((newValue, oldValue) => {
48
+ console.log(`Counter: ${oldValue} -> ${newValue}`);
46
49
  });
47
50
 
48
- counter.set(1); // Logs: "Counter is now: 1"
49
- counter.set(2); // Logs: "Counter is now: 2"
51
+ counter.set(1); // "Counter: 0 -> 1"
52
+ counter.set(2); // "Counter: 1 -> 2"
50
53
  ```
51
54
 
52
- ## Value-Specific Watchers with .on()
55
+ ## Value-Specific Watchers - `.on()`
53
56
 
54
- The `.on()` method allows you to watch for specific values in an observable. The callback is triggered twice: once with `true` when the value is reached, and once with `false` when the value changes to something else.
57
+ `.on()` fires only when a specific value is entered (passes `true`) or left (passes `false`). More efficient than `.subscribe()` when you only care about specific states:
55
58
 
56
59
  ```javascript
57
- const status = Observable("idle");
60
+ const status = Observable('idle');
58
61
 
59
- status.on("loading", (isActive) => {
62
+ status.on('loading', (isActive) => {
60
63
  console.log(`Loading state: ${isActive}`);
61
64
  });
62
65
 
63
- status.on("success", (isActive) => {
66
+ status.on('success', (isActive) => {
64
67
  console.log(`Success state: ${isActive}`);
65
68
  });
66
69
 
67
- status.set("loading"); // Logs: "Loading state: true"
68
- status.set("success"); // Logs: "Loading state: false", "Success state: true"
69
- status.set("idle"); // Logs: "Success state: false"
70
- ```
71
-
72
- **Key Features:**
73
- - Works with all value types (string, number, boolean...)
74
- - Can use an observable as callback (will be set to true/false automatically)
75
- - Returns an unsubscribe function for cleanup
76
- - More efficient than `.subscribe()` when only watching specific values
77
-
78
- **Comparison with .subscribe():**
79
- - `.subscribe()`: called on EVERY change with old/new values
80
- - `.on()`: called when **entering** a specific value (true) and when **leaving** it (false)
81
-
82
- ## .on() vs .subscribe() Comparison
83
-
84
- | Aspect | `.on(value, callback)` | `.subscribe(callback)` |
85
- |--------|------------------------|------------------------|
86
- | **When Called** | Only when entering/leaving specific values | On every value change |
87
- | **Callback Signature** | `(isActive: boolean) => void` | `(newValue, oldValue) => void` |
88
- | **Performance** | ✅ Efficient with many watchers | ❌ Slow with many subscribers |
89
- | **Use Case** | Watching specific states/values | General change detection |
90
- | **Example** | `status.on("loading", show => ...)` | `status.subscribe((new, old) => ...)` |
91
-
92
- ### Performance Impact Example
93
-
94
- ```javascript
95
- const status = Observable("idle");
96
-
97
- // ❌ .subscribe() — ALL 1000 callbacks run on EVERY change
98
- for (let i = 0; i < 1000; i++) {
99
- status.subscribe(value => {
100
- if (value === `state-${i}`) updateComponent(i);
101
- });
102
- }
103
-
104
- // ✅ .on() — Only relevant callback runs
105
- for (let i = 0; i < 1000; i++) {
106
- status.on(`state-${i}`, (isActive) => {
107
- if (isActive) updateComponent(i);
108
- });
109
- }
70
+ status.set('loading'); // "Loading state: true"
71
+ status.set('success'); // "Loading state: false", "Success state: true"
72
+ status.set('idle'); // "Success state: false"
110
73
  ```
111
74
 
112
- ## Observable .when() Method
75
+ | | `.on(value, callback)` | `.subscribe(callback)` |
76
+ |---|---|---|
77
+ | **When called** | Entering / leaving a specific value | Every change |
78
+ | **Signature** | `(isActive: boolean) => void` | `(newValue, oldValue) => void` |
79
+ | **Performance** | Only relevant callbacks run | All callbacks run on every change |
80
+ | **Use case** | Specific states | General change detection |
113
81
 
114
- The `.when()` method creates a transitive object that passes the observable and target value without creating additional observables. It's memory-efficient for conditional operations like CSS class binding.
82
+ ## `.when()` - Lightweight Conditional
115
83
 
116
- ```javascript
117
- observable.when(targetValue)
118
- ```
119
-
120
- Returns an object with `{$target: targetValue, $observer: observable}` that can be used with conditional operations.
121
-
122
- ### Primary Use Case: CSS Class Binding
84
+ `.when()` creates a lightweight transitive object (no new observable) - ideal for CSS class binding:
123
85
 
124
86
  ```javascript
125
- const status = Observable("loading");
87
+ const status = Observable('loading');
126
88
 
127
89
  const element = Div({
128
90
  class: {
129
- "spinner": status.when("loading"),
130
- "success": status.when("success"),
131
- "error": status.when("error")
91
+ 'spinner': status.when('loading'),
92
+ 'success': status.when('success'),
93
+ 'error': status.when('error')
132
94
  }
133
95
  });
134
96
  ```
135
97
 
136
- ### Benefits
137
-
138
- - **Zero memory overhead**: No new observables created
139
- - **Optimized for class binding**: Works seamlessly with NativeDocument's class system
140
- - **Simple API**: Just pass the value to watch for
141
-
142
- ## Method Comparison
143
-
144
- | Method | Memory Impact | Use Case | Return Value |
145
- |--------|---------------|----------|----------------------------|
146
- | `.when(value)` | ✅ Zero — transitive object | CSS classes, conditional checks | `{$target, $observer}` |
147
- | `.on(value, callback)` | ✅ Minimal — single listener per value | Specific value watching | void |
148
- | `.check(callback)` | ❌ Creates new ObservableChecker | Complex conditions | ObservableChecker instance |
149
- | `.subscribe(callback)` | ❌ Creates listener for all changes | General change detection | void |
150
- | `.persist(key, options?)` | ✅ No new observable | localStorage binding | `this` (chainable) |
151
-
152
98
  ## Observable Checkers
153
99
 
154
- Create derived observables with conditions or transformations. All aliases point to the same underlying `ObservableChecker` use whichever reads most naturally for your use case.
100
+ Observable checkers are derived observables that transform or evaluate the current value. The aliases `.check()`, `.is()`, `.select()`, `.pluck()`, and `.transform()` all create the same `ObservableChecker` - they exist purely to make code more expressive and readable:
155
101
 
156
102
  ```javascript
157
103
  const age = Observable(17);
158
104
 
159
- // check — canonical name
160
105
  const isAdult = age.check(value => value >= 18);
161
106
  console.log(isAdult.val()); // false
162
107
 
@@ -164,650 +109,771 @@ age.set(20);
164
109
  console.log(isAdult.val()); // true
165
110
  ```
166
111
 
167
- ### Available Aliases
168
-
169
112
  | Alias | Best suited for |
170
- |-------|-----------------|
113
+ |---|---|
171
114
  | `.check(fn)` | General conditions |
172
- | `.is(fn)` | Boolean / state checks |
115
+ | `.is(fn \| value)` | Boolean / state checks |
173
116
  | `.select(fn)` | Extracting a field |
174
- | `.pluck(fn)` | Extracting a field (Lodash style) |
117
+ | `.pluck(property)` | Extracting a field (Lodash-style) |
175
118
  | `.transform(fn)` | Explicit value transformation |
176
119
 
177
120
  ```javascript
178
- // All return an ObservableChecker — pick what reads best
179
- ShowIf(user.is(u => u.isAdmin), AdminPanel())
121
+ ShowIf(user.is(u => u.isAdmin), AdminPanel)
180
122
 
181
- const email = user.select(u => u.email);
182
- const label = status.transform(s => s.toUpperCase());
183
- const name = user.pluck(u => u.name);
123
+ const email = user.select(u => u.email);
124
+ const label = status.transform(s => s.toUpperCase());
125
+ const name = user.pluck('name');
184
126
  ```
185
- ## Observable.format()
186
127
 
187
- Creates a derived observable that formats the current value using `Intl`.
188
- Automatically reacts to both value changes and locale changes via `Store.setLocale()`.
128
+ ## Utility Methods
129
+
130
+ ### `toggle()` - Boolean toggle
131
+
189
132
  ```javascript
190
- const price = Observable(15000);
191
- const date = Observable(new Date());
192
- const count = Observable(3);
133
+ const isVisible = Observable(false);
193
134
 
194
- // Currency
195
- price.format('currency') // "15 000 FCFA"
196
- price.format('currency', { currency: 'EUR' }) // "15 000,00 €"
197
- price.format('currency', { notation: 'compact' }) // "15 K FCFA"
135
+ isVisible.toggle(); // true
136
+ isVisible.toggle(); // false
198
137
 
199
- // Number
200
- price.format('number') // "15 000"
138
+ Button('Toggle').nd.onClick(() => isVisible.toggle());
139
+ ```
201
140
 
202
- // Percent
203
- Observable(0.15).format('percent') // "15,0 %"
204
- Observable(0.15).format('percent', { decimals: 2}) // "15,00 %"
141
+ ### `equals()` / `toBool()`
205
142
 
206
- // Date
207
- date.format('date') // "3 mars 2026"
208
- date.format('date', { dateStyle: 'full' }) // "mardi 3 mars 2026"
209
- date.format('date', { format: 'DD/MM/YYYY' }) // "03/03/2026"
210
- date.format('date', { format: 'DD MMM YYYY' }) // "03 mar 2026"
211
- date.format('date', { format: 'DD MMMM YYYY' }) // "03 mars 2026"
143
+ ```javascript
144
+ const num = Observable(5);
212
145
 
213
- // Time
214
- date.format('time') // "20:30"
215
- date.format('time', { second: '2-digit' }) // "20:30:00"
216
- date.format('time', { format: 'HH:mm:ss' }) // "20:30:00"
146
+ console.log(num.equals(5)); // true
147
+ console.log(num.equals(Observable(5))); // true
217
148
 
218
- // Datetime
219
- date.format('datetime') // "3 mars 2026, 20:30"
220
- date.format('datetime', { dateStyle: 'full' }) // "mardi 3 mars 2026, 20:30"
221
- date.format('datetime', { format: 'DD/MM/YYYY HH:mm' }) // "03/03/2026 20:30"
149
+ const text = Observable('');
150
+ console.log(text.toBool()); // false
151
+ text.set('Hello');
152
+ console.log(text.toBool()); // true
153
+ ```
222
154
 
223
- // Relative
224
- date.format('relative') // "dans 11 jours"
225
- date.format('relative', { unit: 'month' }) // "dans 1 mois"
155
+ ### Convenience checkers - `is...()`
226
156
 
227
- // Plural
228
- count.format('plural', { singular: 'billet', plural: 'billets' }) // "3 billets"
157
+ All `is...()` methods return an `ObservableChecker<boolean>` and accept an observable or a plain value where noted:
229
158
 
230
- // Custom formatter works like transform()
231
- price.format(value => `${value.toLocaleString()} FCFA`)
159
+ | Method | Returns `true` when... |
160
+ |---|---|
161
+ | `isTruthy()` | value is truthy |
162
+ | `isFalsy()` | value is falsy |
163
+ | `isNull()` | value is `null` or `undefined` |
164
+ | `isEmpty()` | value is `null`, `''`, or an empty array |
165
+ | `isNotEmpty()` | value is not `null`, not `''`, and not an empty array |
166
+ | `isEqualTo(value)` | value equals `value` (observable-aware) |
167
+ | `isNotEqualTo(value)` | value does not equal `value` (observable-aware) |
168
+ | `isGreaterThan(value)` | value > `value` (observable-aware) |
169
+ | `isGreaterThanOrEqualTo(value)` | value >= `value` (observable-aware) |
170
+ | `isLessThan(value)` | value < `value` (observable-aware) |
171
+ | `isLessThanOrEqualTo(value)` | value <= `value` (observable-aware) |
172
+ | `isBetween(min, max)` | min <= value <= max (observable-aware) |
173
+ | `isStartingWith(str)` | string starts with `str` (observable-aware) |
174
+ | `isEndingWith(str)` | string ends with `str` (observable-aware) |
175
+ | `isMatchingPattern(regex)` | string matches `regex` (observable-aware) |
176
+ | `isIncludes(value)` | array includes `value`, or string contains `value` (observable-aware) |
177
+ | `isIncludedIn(array)` / `isOneOf(array)` | value is in `array` (observable-aware) |
178
+ | `isHaving(key)` | object has property `key` (observable-aware) |
179
+
180
+ ```javascript
181
+ const score = Observable(72);
182
+ const list = Observable.array([]);
183
+ const name = Observable('');
184
+ const status = Observable('active');
185
+ const user = Observable({ role: 'admin' });
186
+ const minAge = Observable(18);
187
+
188
+ ShowIf(list.isEmpty(), Div('No items yet'))
189
+ ShowIf(name.isFalsy(), Div('Name is required'))
190
+ ShowIf(name.isTruthy(), Div(['Hello, ', name]))
191
+ ShowIf(score.isGreaterThan(50), Div('Passing grade'))
192
+ ShowIf(score.isBetween(0, 100), Div('Valid score'))
193
+ ShowIf(score.isBetween(minAge, 100), Div('Reactive min'))
194
+ ShowIf(name.isStartingWith('A'), Div('Starts with A'))
195
+ ShowIf(status.isOneOf(['active', 'pending']), Div('In progress'))
196
+ ShowIf(user.isHaving('role'), Div('Has a role'))
232
197
  ```
233
198
 
234
- ### Locale Reactivity
199
+ ### Convenience transformers - `to...()`
200
+
201
+ All `to...()` methods return an `ObservableChecker<any>` - a derived observable with the transformed value:
202
+
203
+ | Method | Returns... |
204
+ |---|---|
205
+ | `toUpperCase()` | uppercased string |
206
+ | `toLowerCase()` | lowercased string |
207
+ | `toTrimmed()` | trimmed string |
208
+ | `toBoolean()` | `!!value` |
209
+ | `toLiteral(template, placeholder?)` / `toFormatted(...)` | string with value interpolated into template |
210
+ | `toProperty(key)` | deep property via dot-path (e.g. `'address.city'`) |
211
+ | `toLength()` | length of string or array (`0` if null) |
212
+ | `toClamped(min, max)` | value clamped between min and max (observable-aware) |
213
+ | `toPercent(total)` | `(value / total) * 100` (observable-aware) |
235
214
 
236
- All formatted observables automatically recalculate when the locale changes:
237
215
  ```javascript
238
- const price = Observable(15000);
239
- const label = price.format('currency', { currency: 'XOF' });
216
+ const name = Observable(' Alice ');
217
+ const score = Observable(72);
218
+ const progress = Observable(350);
219
+ const user = Observable({ address: { city: 'Paris' } });
240
220
 
241
- label.val(); // "15 000 FCFA"
221
+ name.toTrimmed() // "Alice"
222
+ name.toUpperCase() // " ALICE "
223
+ score.toClamped(0, 100) // 72
224
+ score.toClamped(Observable(0), 100) // reactive min
225
+ progress.toPercent(500) // 70
226
+ user.toProperty('address.city') // "Paris"
242
227
 
243
- Store.get('locale').set('en-US');
244
- label.val(); // "$15,000.00"
228
+ name.toLiteral('Hello ${v}!') // "Hello Alice !"
229
+ name.toTrimmed().toLiteral('Hello ${v}!') // "Hello Alice!"
245
230
 
246
- Store.get('locale').set('fr-TG');
247
- label.val(); // "15 000 FCFA"
231
+ name.toLiteral('Hello [name]!', '[name]') // custom placeholder
248
232
  ```
249
233
 
250
- ### Extending Formatters
234
+ ### `reset()` - Reset to initial value
251
235
 
252
- Add custom format types via `Formatters`:
253
236
  ```javascript
254
- import { Formatters } from 'native-document';
255
-
256
- Formatters.duration = (value, locale) => {
257
- const hours = Math.floor(value / 3600);
258
- const minutes = Math.floor((value % 3600) / 60);
259
- return `${hours}h${minutes < 10 ? '0' : ''}${minutes}`;
260
- };
237
+ const name = Observable('Alice', { reset: true });
261
238
 
262
- const duration = Observable(3661);
263
- duration.format('duration'); // "1h01"
239
+ name.set('Bob');
240
+ name.reset();
241
+ console.log(name.val()); // "Alice"
264
242
  ```
265
243
 
266
- ### Available Format Types
244
+ ### `intercept(callback)` - Transform or abort before setting
267
245
 
268
- | Type | Input | Options |
269
- |------|-------|---------|
270
- | `currency` | `number` | `currency`, `notation`, `minimumFractionDigits`, `maximumFractionDigits` |
271
- | `number` | `number` | `notation`, `minimumFractionDigits`, `maximumFractionDigits` |
272
- | `percent` | `number` | `decimals` |
273
- | `date` | `Date \| number` | `dateStyle`, `format` |
274
- | `time` | `Date \| number` | `hour`, `minute`, `second`, `format` |
275
- | `datetime` | `Date \| number` | `dateStyle`, `hour`, `minute`, `second`, `format` |
276
- | `relative` | `Date \| number` | `unit`, `numeric` |
277
- | `plural` | `number` | `singular`, `plural` |
278
-
279
- ## Observable Objects vs Simple Objects
246
+ The interceptor runs before every `.set()` call. Return a new value to replace it, or return `undefined` to abort the assignment entirely:
280
247
 
281
248
  ```javascript
282
- // Observable.object() creates a PROXY with reactive properties
283
- const userProxy = Observable.object({
284
- name: "Alice",
285
- age: 25
249
+ const age = Observable(0);
250
+
251
+ age.intercept((newValue, currentValue) => {
252
+ if (newValue < 0) return 0; // clamp minimum
253
+ if (newValue > 120) return 120; // clamp maximum
254
+ return newValue;
286
255
  });
287
256
 
288
- // Each property is an individual observable
289
- console.log(userProxy.name.val()); // "Alice"
290
- userProxy.name.set("Bob");
257
+ age.set(-5); // sets 0
258
+ age.set(150); // sets 120
259
+ age.set(25); // sets 25
291
260
 
292
- // Get all values as plain object
293
- console.log(userProxy.$value); // { name: "Bob", age: 25 }
294
- console.log(userProxy.val()); // { name: "Bob", age: 25 }
261
+ // Abort example - reject the value without changing anything
262
+ const username = Observable('alice');
295
263
 
296
- // Observable(object) creates a SINGLE observable containing the whole object
297
- const userSingle = Observable({
298
- name: "Alice",
299
- age: 25
264
+ username.intercept((newValue, currentValue) => {
265
+ if (newValue.trim() === '') return undefined; // abort - keeps 'alice'
266
+ return newValue.toLowerCase().trim();
300
267
  });
301
268
 
302
- console.log(userSingle.val()); // { name: "Alice", age: 25 }
303
- userSingle.set({ name: "Bob", age: 30 });
269
+ username.set(''); // aborted - still 'alice'
270
+ username.set(' Bob '); // sets 'bob'
304
271
  ```
305
272
 
306
- **Observable.object is an alias:**
307
- ```javascript
308
- // These are identical
309
- Observable.object(data) === Observable.json(data) === Observable.init(data)
310
- ```
273
+ ### `interceptMutations(callback)` - Intercept array/object mutations
311
274
 
312
- ## Working with Object Observable
275
+ Intercepts mutations (push, splice, etc.) on arrays and objects separately from `.intercept()`:
313
276
 
314
277
  ```javascript
315
- const user = Observable.object({
316
- name: "Alice",
317
- age: 25,
318
- email: "alice@example.com"
278
+ const items = Observable.array([]);
279
+
280
+ items.interceptMutations((operations) => {
281
+ console.log('Mutation:', operations);
319
282
  });
320
283
 
321
- // Access individual properties
322
- console.log(user.name.val()); // "Alice"
323
- console.log(user.name.$value); // "Alice"
284
+ items.push('apple'); // logs mutation details
285
+ ```
324
286
 
325
- // Update individual properties
326
- user.name.set("Bob");
327
- user.age.$value = 30;
287
+ ### `once(predicate, callback)` - Single-time listener
328
288
 
329
- // Get the complete object value
330
- console.log(user.$value); // { name: "Bob", age: 30, email: "alice@example.com" }
331
- console.log(user.val()); // Same as above
289
+ ```javascript
290
+ const count = Observable(0);
332
291
 
333
- // Listen to individual property changes
334
- user.name.subscribe(newName => {
335
- console.log("New name:", newName);
336
- });
292
+ count.once(5, () => console.log('Reached 5!')); // fires once when value === 5
293
+ count.once(val => val > 10, () => console.log('Over 10!')); // fires once on condition
337
294
 
338
- // Update multiple properties at once
339
- user.set({
340
- name: "Charlie",
341
- age: 35
342
- });
295
+ count.set(5); // "Reached 5!"
296
+ count.set(5); // nothing - already fired
343
297
  ```
344
298
 
299
+ ### `off(value, callback?)` - Remove watchers
300
+
345
301
  ```javascript
346
- const todos = Observable.array([
347
- "Buy groceries",
348
- "Call doctor"
349
- ]);
302
+ const status = Observable('idle');
303
+ const handler = (isActive) => console.log('Loading:', isActive);
350
304
 
351
- todos.push("Clean house");
352
- todos.pop();
305
+ status.on('loading', handler);
353
306
 
354
- const completed = todos.filter(todo => todo.includes("✓"));
307
+ status.off('loading', handler); // remove specific callback
308
+ status.off('loading'); // remove all watchers for this value
355
309
  ```
356
310
 
357
- ### Subscribing to an Observable Object
311
+ ### `onCleanup(callback)` - Register cleanup callback
312
+
313
+ Registers a callback that runs when the observable is cleaned up:
358
314
 
359
- Subscribing to an `Observable.object()` reacts to changes on any individual property — you don't need to subscribe to each property separately.
360
315
  ```javascript
361
- const user = Observable.object({ name: 'Alice', age: 25 });
316
+ const data = Observable('test');
362
317
 
363
- user.subscribe(value => {
364
- console.log('User changed:', value); // { name: 'Bob', age: 25 }
318
+ data.onCleanup(() => {
319
+ console.log('Observable cleaned up');
365
320
  });
366
321
 
367
- user.name.set('Bob'); // triggers the parent subscribe
368
- user.age.set(30); // triggers the parent subscribe
322
+ data.cleanup(); // triggers the cleanup callback
369
323
  ```
370
324
 
371
- ## Computed Observables
325
+ ### `persist(key, options?)` - Bind to localStorage
372
326
 
373
- Computed observables automatically recalculate when their dependencies change.
327
+ Automatically saves on every change and restores on load:
374
328
 
375
329
  ```javascript
376
- const firstName = Observable("John");
377
- const lastName = Observable("Doe");
330
+ const theme = Observable('light').persist('theme');
331
+ theme.set('dark'); // saved to localStorage
378
332
 
379
- const fullName = Observable.computed(() => {
380
- return `${firstName.val()} ${lastName.val()}`;
381
- }, [firstName, lastName]);
333
+ // On next page load
334
+ const theme = Observable('light').persist('theme');
335
+ theme.val(); // "dark" - restored
336
+ ```
382
337
 
383
- console.log(fullName.val()); // "John Doe"
338
+ With transform options:
384
339
 
385
- firstName.set("Jane");
386
- console.log(fullName.val()); // "Jane Doe"
340
+ ```javascript
341
+ const selectedDate = Observable(new Date()).persist('event:date', {
342
+ get: value => new Date(value), // transform on load
343
+ set: value => value.toISOString() // transform on save
344
+ });
387
345
  ```
388
346
 
389
- ## Practical Example: Simple Counter
347
+ ### `clone()` - Create a copy
390
348
 
391
- ```javascript
392
- const count = Observable(0);
349
+ Creates a deep clone of the observable's current value:
393
350
 
394
- const increment = () => count.set(count.val() + 1);
395
- const decrement = () => (count.$value--);
351
+ ```javascript
352
+ const original = Observable({ name: 'Alice', scores: [1, 2, 3] });
353
+ const copy = original.clone();
396
354
 
397
- const app = Div({ class: "counter" }, [
398
- Button("-").nd.onClick(decrement),
399
- Span({ class: "count" }, count),
400
- Button("+").nd.onClick(increment)
401
- ]);
355
+ copy.set({ ...copy.val(), name: 'Bob' });
356
+ original.val().name; // still "Alice"
402
357
  ```
403
358
 
404
- ## Batching Operations
405
-
406
- Batching is a performance optimization technique that delays notifications to **dependent computed observables** until the end of a batch operation. Individual observable subscribers still receive their notifications immediately.
359
+ ### `resolve()` - Extract plain value
407
360
 
408
- ### Understanding Batch Behavior
361
+ Recursively extracts plain values from observables, arrays, and proxies:
409
362
 
410
363
  ```javascript
411
- const name = Observable("John");
412
- const age = Observable(25);
364
+ const name = Observable('Alice');
365
+ name.resolve(); // "Alice" - same as Observable.value(name)
413
366
 
414
- name.subscribe(value => console.log("Name changed to:", value));
415
- age.subscribe(value => console.log("Age changed to:", value));
367
+ const user = Observable.object({ name: Observable('Bob') });
368
+ Observable.value(user); // { name: 'Bob' } - deeply resolved
369
+ ```
416
370
 
417
- const updateProfile = Observable.batch(() => {
418
- name.set("Alice"); // Logs: "Name changed to: Alice"
419
- age.set(30); // Logs: "Age changed to: 30"
420
- });
371
+ ### `trigger()` - Force update without value change
421
372
 
422
- updateProfile(); // Individual subscribers are notified immediately
373
+ ```javascript
374
+ const data = Observable('test');
375
+ data.trigger(); // notifies all subscribers without changing the value
423
376
  ```
424
377
 
425
- ### Batching with Computed Dependencies
378
+ ### `cleanup()` - Remove all listeners
426
379
 
427
380
  ```javascript
428
- const firstName = Observable("John");
429
- const lastName = Observable("Doe");
430
-
431
- firstName.subscribe(name => console.log("First name:", name));
432
- lastName.subscribe(name => console.log("Last name:", name));
381
+ const data = Observable('test');
382
+ data.cleanup(); // removes all listeners and prevents new subscriptions
383
+ ```
433
384
 
434
- const updateName = Observable.batch((first, last) => {
435
- firstName.set(first); // Logs: "First name: Alice"
436
- lastName.set(last); // Logs: "Last name: Smith"
437
- });
385
+ ### `deepSubscribe(callback)` - Deep change detection
438
386
 
439
- const fullName = Observable.computed(() => {
440
- return `${firstName.val()} ${lastName.val()}`;
441
- }, updateName); // ← Depends on the batch function
387
+ Reacts to changes at any depth - array mutations and property changes on nested observables:
442
388
 
443
- fullName.subscribe(name => console.log("Full name:", name));
389
+ ```javascript
390
+ const tags = Observable.array([{ label: Observable('admin') }]);
444
391
 
445
- updateName("Alice", "Smith");
446
- // Logs:
447
- // "First name: Alice" ← immediate
448
- // "Last name: Smith" ← immediate
449
- // "Full name: Alice Smith" ← single notification at the end
450
- ```
392
+ const unsub = tags.deepSubscribe(value => console.log('changed:', value));
451
393
 
452
- ### Comparison: Normal vs Batch Dependencies
394
+ tags.push({ label: Observable('editor') }); // triggers
395
+ tags.at(0).label.set('superadmin'); // triggers
396
+ tags.splice(0, 1); // triggers + cleans up listener
453
397
 
454
- ```javascript
455
- const score = Observable(0);
456
- const lives = Observable(3);
398
+ unsub(); // manual cleanup
399
+ ```
457
400
 
458
- // Method 1: depends on individual observables — recalculates twice
459
- const gameStatus1 = Observable.computed(() => {
460
- return `Score: ${score.val()}, Lives: ${lives.val()}`;
461
- }, [score, lives]);
401
+ ---
462
402
 
463
- // Method 2: depends on batch function — recalculates once
464
- const updateGame = Observable.batch(() => {
465
- score.set(score.val() + 100);
466
- lives.set(lives.val() - 1);
467
- });
403
+ ## Observable Static Methods
468
404
 
469
- const gameStatus2 = Observable.computed(() => {
470
- return `Score: ${score.val()}, Lives: ${lives.val()}`;
471
- }, updateGame);
405
+ ### `Observable.setLocale(locale)`
472
406
 
473
- score.set(100); // gameStatus1 recalculates
474
- lives.set(2); // gameStatus1 recalculates again
407
+ Sets the locale observable used by `.format()`. Accepts an observable or a plain string:
475
408
 
476
- updateGame(); // gameStatus2 recalculates only once
409
+ ```javascript
410
+ Observable.setLocale(I18nService.current); // observable (recommended)
411
+ Observable.setLocale(Observable('fr')); // plain observable
412
+ Observable.setLocale('fr'); // plain string - wrapped automatically
477
413
  ```
478
414
 
479
- ### Practical Example: Shopping Cart
415
+ ### `Observable.value(data)`
416
+
417
+ Recursively extracts plain values from observables, arrays, and proxies:
480
418
 
481
419
  ```javascript
482
- const items = Observable.array([]);
483
- const discount = Observable(0);
484
- const shippingCost = Observable(0);
420
+ Observable.value(Observable(42)); // 42
421
+ Observable.value(Observable.array([Observable(1)])); // [1]
422
+ Observable.value(Observable.object({ n: Observable('x') })); // { n: 'x' }
423
+ Observable.value('plain string'); // 'plain string'
424
+ ```
485
425
 
486
- items.subscribe(items => console.log('Items count: ' + items.length));
487
- discount.subscribe(discount => console.log(`Discount: ${discount}%`));
426
+ ### `Observable.cleanup(observable)`
488
427
 
489
- const updateCart = Observable.batch((cartData) => {
490
- items.splice(0);
491
- cartData.items.forEach(item => items.push(item));
492
- discount.set(cartData.discount);
493
- shippingCost.set(cartData.shipping);
494
- });
428
+ Static alias for `observable.cleanup()`:
495
429
 
496
- const cartTotal = Observable.computed(() => {
497
- const itemsTotal = items.val().reduce((sum, item) => sum + (item.price * item.quantity), 0);
498
- const discountAmount = itemsTotal * (discount.val() / 100);
499
- return itemsTotal - discountAmount + shippingCost.val();
500
- }, updateCart);
501
-
502
- updateCart({
503
- items: [
504
- { name: "Product A", price: 29.99, quantity: 2 },
505
- { name: "Product B", price: 19.99, quantity: 1 }
506
- ],
507
- discount: 10,
508
- shipping: 5.99
509
- });
430
+ ```javascript
431
+ Observable.cleanup(myObservable);
510
432
  ```
511
433
 
512
- ### Async Batching
434
+ ### `Observable.autoCleanup(enable, options?)`
435
+
436
+ Enables automatic cleanup on page unload and via a periodic interval:
513
437
 
514
438
  ```javascript
515
- const isLoading = Observable(false);
516
- const userData = Observable(null);
517
- const error = Observable(null);
439
+ Observable.autoCleanup(true, {
440
+ interval: 60000, // run cleanup every 60s (default)
441
+ threshold: 100 // clean up when > 100 unreferenced observables (default)
442
+ });
443
+ ```
518
444
 
519
- isLoading.subscribe(loading => console.log('Loading.....'));
445
+ ### `Observable.getById(id)`
520
446
 
521
- const fetchUser = Observable.batch(async (userId) => {
522
- isLoading.set(true);
523
- error.set(null);
524
-
525
- try {
526
- const response = await fetch(`/api/users/${userId}`);
527
- const data = await response.json();
528
- userData.set(data);
529
- } catch (err) {
530
- error.set(err.message);
531
- } finally {
532
- isLoading.set(false);
533
- }
534
- });
447
+ Retrieves a registered observable by its internal memory ID. Useful for debugging:
535
448
 
536
- const userDisplay = Observable.computed(() => {
537
- if (isLoading.val()) return "Loading...";
538
- if (error.val()) return `Error: ${error.val()}`;
539
- if (userData.val()) return `Hello ${userData.val().name}`;
540
- return "No user";
541
- }, fetchUser);
449
+ ```javascript
450
+ const obs = Observable(42);
451
+ const id = obs.toString(); // "{{#ObItem::(N)}}"
542
452
 
543
- await fetchUser(123);
453
+ Observable.getById(N); // returns obs
544
454
  ```
545
455
 
546
- ### Single Batch Dependency Only
456
+ ### `Observable.useValueProperty(propertyName?)`
547
457
 
548
- Computed observables can only depend on **one batch function**, not multiple:
458
+ Defines a property name as an alias for `$value`. Default is `'value'`, but you can pass any name:
549
459
 
550
460
  ```javascript
551
- const updateProfile = Observable.batch((profileData) => {
552
- user.name.set(profileData.name);
553
- user.email.set(profileData.email);
554
- settings.theme.set(profileData.theme);
555
- });
461
+ Observable.useValueProperty(); // enables .value
462
+ Observable.useValueProperty('val'); // enables .val (overrides the method!)
463
+ Observable.useValueProperty('current'); // enables .current
556
464
 
557
- // Single batch dependency
558
- const profileSummary = Observable.computed(() => ({
559
- user: user.$value,
560
- settings: settings.$value,
561
- lastUpdated: Date.now()
562
- }), updateProfile);
465
+ const count = Observable(0);
466
+
467
+ // With default
468
+ Observable.useValueProperty();
469
+ count.value; // 0 - same as count.$value
470
+ count.value = 5; // same as count.$value = 5
563
471
 
564
- // Not supported
565
- // Observable.computed(callback, [batch1, batch2])
472
+ // With custom name
473
+ Observable.useValueProperty('current');
474
+ count.current; // 0
475
+ count.current = 5; // sets to 5
566
476
  ```
567
477
 
568
- ### Best Practices
478
+ > Be careful not to pick a name that conflicts with an existing method - for example `'val'` would shadow the `.val()` method.
569
479
 
570
- - Use batch dependencies for expensive computations that shouldn't recalculate on every individual change
571
- - Keep individual subscribers for immediate feedback (input validation, UI updates)
572
- - Group logically connected updates that should trigger dependent computations together
573
- - Don't over-batch — only use when computed observables benefit from delayed updates
480
+ ---
574
481
 
575
- ### When NOT to Use Batch Dependencies
482
+ ## Observable Objects
576
483
 
577
- - Real-time updates where computed observables need to update immediately
578
- - Simple computations where the cost is minimal
579
- - Debugging contexts — batching can make the flow harder to trace
580
- - Single observable changes no benefit when only one observable changes
484
+ ### `Observable(object)` vs `Observable.object(object)`
485
+
486
+ ```javascript
487
+ // Observable(object) - single observable wrapping the whole object
488
+ const userSingle = Observable({ name: 'Alice', age: 25 });
489
+ userSingle.val();
490
+ userSingle.set({ name: 'Bob', age: 30 });
581
491
 
582
- ## String Templates with Observables
492
+ // Observable.object() - proxy with each property as its own observable
493
+ const userProxy = Observable.object({ name: 'Alice', age: 25 });
494
+ userProxy.name.val(); // "Alice"
495
+ userProxy.name.set('Bob');
496
+ userProxy.$value; // { name: 'Bob', age: 25 }
497
+ ```
583
498
 
584
- ### The .use() Method
499
+ `Observable.object` has aliases:
585
500
 
586
501
  ```javascript
587
- const name = Observable("Alice");
588
- const age = Observable(25);
502
+ Observable.object(data) // same as:
503
+ Observable.json(data)
504
+ Observable.init(data)
505
+ ```
506
+
507
+ ### Subscribing to an object observable
589
508
 
590
- const template = "Hello ${name}, you are ${age} years old";
591
- const message = template.use({ name, age });
509
+ Subscribing to `Observable.object()` reacts to any property change:
592
510
 
593
- console.log(message.val()); // "Hello Alice, you are 25 years old"
511
+ ```javascript
512
+ const user = Observable.object({ name: 'Alice', age: 25 });
594
513
 
595
- name.set("Bob");
596
- console.log(message.val()); // "Hello Bob, you are 25 years old"
514
+ user.subscribe(value => {
515
+ console.log('User changed:', value);
516
+ });
517
+
518
+ user.name.set('Bob'); // triggers the parent subscribe
519
+ user.age.set(30); // triggers the parent subscribe
597
520
  ```
598
521
 
599
- ### Automatic Template Resolution
522
+ ---
523
+
524
+ ## Observable Arrays
600
525
 
601
526
  ```javascript
602
- const greeting = Observable("Hello");
603
- const user = Observable("Marie");
527
+ const todos = Observable.array(['Buy groceries', 'Call doctor']);
604
528
 
605
- const element = Div(null, `${greeting} ${user}!`);
529
+ todos.push('Clean house');
530
+ todos.pop();
531
+ todos.splice(0, 1);
532
+
533
+ console.log(todos.val().length); // 1
606
534
  ```
607
535
 
608
- ## Memory Management
536
+ ### Array-specific methods
609
537
 
610
- ```javascript
611
- const data = Observable("test");
538
+ | Method | Description |
539
+ |---|---|
540
+ | `push(item)` | Add item at the end |
541
+ | `pop()` | Remove last item |
542
+ | `splice(index, count)` | Remove items at index |
543
+ | `at(index)` | Get observable at index |
544
+ | `merge(values)` | Merge new values |
545
+ | `clear()` | Empty the array |
546
+ | `empty()` | Alias for `clear()` |
547
+ | `count(condition?)` | Count items matching a condition |
548
+ | `swap(indexA, indexB)` | Swap two items by index |
549
+ | `swapItems(itemA, itemB)` | Swap two items by reference |
550
+ | `insertAfter(data, target)` | Insert after a target item |
551
+ | `remove(index)` | Remove item at index |
552
+ | `removeItem(item)` | Remove item by reference |
553
+ | `clone()` | Deep clone the array |
554
+ | `isEmpty()` | Returns an `ObservableChecker` |
612
555
 
613
- const handler = value => console.log(value);
614
- data.subscribe(handler);
556
+ ### Filtering with `.where()`
615
557
 
616
- // Remove specific subscription
617
- data.unsubscribe(handler);
558
+ > `.where()`, `.whereSome()`, and `.whereEvery()` are available on **`ObservableArray` only** - not on plain observables. They return a new live `ObservableArray` that re-filters automatically when the source or any reactive predicate changes.
618
559
 
619
- // Complete observable cleanup
620
- data.cleanup(); // Removes all listeners and prevents new subscriptions
560
+ **Function predicate** - full item passed to the callback:
621
561
 
622
- // Manual trigger — forces update without changing the value
623
- data.trigger();
562
+ ```javascript
563
+ const users = Observable.array([
564
+ { name: 'Alice', age: 17, role: 'user' },
565
+ { name: 'Bob', age: 25, role: 'admin' },
566
+ { name: 'Carol', age: 32, role: 'user' },
567
+ { name: 'Dave', age: 28, role: 'admin' },
568
+ ]);
624
569
 
625
- // Extract values from any observable structure
626
- const complexData = Observable.object({ user: "John", items: [1, 2, 3] });
627
- console.log(complexData.val()); // Plain object with extracted values
570
+ const adults = users.where(item => item.age >= 18);
571
+ // -> Bob, Carol, Dave
628
572
  ```
629
573
 
630
- ## Utility Methods
574
+ **Object predicate** - filter per field:
631
575
 
632
- ### `off(value, callback?)` — Remove Watchers
576
+ ```javascript
577
+ const activeAdmins = users.where({
578
+ role: 'admin', // shorthand for equals
579
+ age: val => val >= 18 // plain function on the field value
580
+ });
581
+ // -> Bob, Dave
582
+ ```
583
+
584
+ **Reactive predicate** - re-filters when an observable changes:
633
585
 
634
586
  ```javascript
635
- const status = Observable("idle");
587
+ import { match } from 'native-document/filters';
588
+
589
+ const search = Observable('');
636
590
 
637
- const loadingHandler = (isActive) => console.log("Loading:", isActive);
638
- status.on("loading", loadingHandler);
591
+ const results = users.where({
592
+ name: match(search) // re-filters every time search changes
593
+ });
639
594
 
640
- status.off("loading", loadingHandler); // Remove specific callback
641
- status.off("loading"); // Remove all watchers for this value
595
+ search.set('ali'); // -> Alice
596
+ search.set(''); // -> all users
642
597
  ```
643
598
 
644
- ### `once(predicate, callback)` Single-Time Listener
599
+ **`and` / `or` / `not`** - composable per-field filters:
600
+
601
+ > `and`, `or`, and `not` operate on filter result objects (the kind returned by `equals`, `greaterThan`, `match`, etc.) **within a single field**. For cross-field logic, use the `_` key with a plain function.
645
602
 
646
603
  ```javascript
647
- const count = Observable(0);
604
+ import { equals, greaterThan, lessThan, and, or, not } from 'native-document/filters';
648
605
 
649
- count.once(5, (value) => {
650
- console.log("Reached 5!"); // Only called once
606
+ // and - field must pass ALL conditions
607
+ const youngAdults = users.where({
608
+ age: and(greaterThan(18), lessThan(30))
651
609
  });
610
+ // -> Bob (25), Dave (28)
652
611
 
653
- count.once(val => val > 10, (value) => {
654
- console.log("Greater than 10!"); // Only called once
612
+ // or - field must pass AT LEAST ONE condition
613
+ const adminOrEditor = users.where({
614
+ role: or(equals('admin'), equals('editor'))
655
615
  });
616
+ // -> Bob, Dave
656
617
 
657
- count.set(5); // Callback fires and unsubscribes
658
- count.set(5); // Callback doesn't fire again
618
+ // not - inverts a filter
619
+ const nonAdmins = users.where({
620
+ role: not(equals('admin'))
621
+ });
622
+ // -> Alice, Carol
623
+
624
+ // Cross-field OR - use the _ key with a plain function
625
+ const adminsOrMinors = users.where({
626
+ _: item => item.role === 'admin' || item.age < 18
627
+ });
628
+ // -> Alice (minor), Bob (admin), Dave (admin)
659
629
  ```
660
630
 
661
- ### `toggle()` Boolean Toggle
631
+ **`whereSome(fields, filter)`** - at least one field matches (OR across fields):
662
632
 
663
633
  ```javascript
664
- const isVisible = Observable(false);
634
+ import { match } from 'native-document/filters';
665
635
 
666
- isVisible.toggle(); // true
667
- isVisible.toggle(); // false
636
+ const search = Observable('car');
668
637
 
669
- Button("Toggle").nd.onClick(() => isVisible.toggle());
638
+ const results = products.whereSome(['name', 'category'], match(search));
639
+ // matches if name OR category contains 'car'
670
640
  ```
671
641
 
672
- ### `reset()` Reset to Initial Value
642
+ **`whereEvery(fields, filter)`** - all fields match (AND across fields):
673
643
 
674
644
  ```javascript
675
- const name = Observable("Alice", { reset: true });
645
+ import { equals } from 'native-document/filters';
676
646
 
677
- name.set("Bob");
678
- name.reset();
679
- console.log(name.val()); // "Alice"
680
-
681
- const user = Observable({ name: "Alice", age: 25 }, { reset: true });
682
- user.set({ name: "Bob", age: 30 });
683
- user.reset(); // Back to { name: "Alice", age: 25 }
647
+ const verified = items.whereEvery(['status', 'verified'], equals('active'));
648
+ // matches only if both status AND verified equal 'active'
684
649
  ```
685
650
 
686
- ### `equals(other)` — Value Comparison
651
+ ---
652
+
653
+ ## Computed Observables
654
+
655
+ Computed observables automatically recalculate when their dependencies change. The callback receives the current value of each dependency **as arguments, in the order they are declared**:
687
656
 
688
657
  ```javascript
689
- const num1 = Observable(5);
690
- const num2 = Observable(5);
691
- const num3 = Observable(10);
658
+ const firstName = Observable('John');
659
+ const lastName = Observable('Doe');
660
+
661
+ const fullName = Observable.computed((first, last) => {
662
+ return `${first} ${last}`;
663
+ }, [firstName, lastName]);
692
664
 
693
- console.log(num1.equals(num2)); // true
694
- console.log(num1.equals(5)); // true
695
- console.log(num1.equals(num3)); // false
665
+ console.log(fullName.val()); // "John Doe"
666
+
667
+ firstName.set('Jane');
668
+ console.log(fullName.val()); // "Jane Doe"
696
669
  ```
697
670
 
698
- ### `toBool()` Boolean Conversion
671
+ Another example with more dependencies:
699
672
 
700
673
  ```javascript
701
- const text = Observable("");
702
- console.log(text.toBool()); // false
674
+ const price = Observable(100);
675
+ const quantity = Observable(2);
676
+ const tax = Observable(0.2);
703
677
 
704
- text.set("Hello");
705
- console.log(text.toBool()); // true
678
+ const total = Observable.computed((p, q, t) => {
679
+ return p * q * (1 + t);
680
+ }, [price, quantity, tax]);
681
+
682
+ console.log(total.val()); // 240
683
+
684
+ quantity.set(3);
685
+ console.log(total.val()); // 360
706
686
  ```
707
687
 
708
- ### `intercept(callback)` — Value Interception
688
+ ---
689
+
690
+ ## Batching Updates
691
+
692
+ `Observable.batch()` delays notifications to **dependent computed observables** until all changes are done. Individual subscribers still fire immediately.
709
693
 
710
694
  ```javascript
711
- const age = Observable(0);
695
+ const firstName = Observable('John');
696
+ const lastName = Observable('Doe');
712
697
 
713
- age.intercept((newValue, oldValue) => {
714
- if (newValue < 0) return 0;
715
- if (newValue > 120) return 120;
716
- return newValue;
698
+ const updateName = Observable.batch((first, last) => {
699
+ firstName.set(first);
700
+ lastName.set(last);
717
701
  });
718
702
 
719
- age.set(-5); // Sets 0
720
- age.set(150); // Sets 120
721
- age.set(25); // Sets 25
703
+ const fullName = Observable.computed((first, last) => {
704
+ return `${first} ${last}`;
705
+ }, updateName); // depends on the batch - recalculates once
722
706
 
723
- const username = Observable("");
724
- username.intercept((value) => value.toLowerCase().trim());
707
+ fullName.subscribe(name => console.log('Full name:', name));
725
708
 
726
- username.set(" JohnDoe ");
727
- console.log(username.val()); // "johndoe"
709
+ updateName('Alice', 'Smith');
710
+ // "Full name: Alice Smith" - single notification
728
711
  ```
729
712
 
730
- ### `persist(key, options?)` Persist to localStorage
713
+ Async batching is also supported:
731
714
 
732
- Binds an observable to localStorage. The value is automatically restored on load and saved on every change.
733
715
  ```javascript
734
- // Simple persistence — key defaults to the variable name
735
- const theme = Observable('light').persist('theme');
736
- theme.set('dark'); // saved to localStorage automatically
716
+ const isLoading = Observable(false);
717
+ const userData = Observable(null);
718
+ const error = Observable(null);
737
719
 
738
- // On next page load
739
- const theme = Observable('light').persist('theme');
740
- theme.val(); // "dark" — restored from localStorage
720
+ const fetchUser = Observable.batch(async (userId) => {
721
+ isLoading.set(true);
722
+ error.set(null);
723
+ try {
724
+ const data = await fetch(`/api/users/${userId}`).then(r => r.json());
725
+ userData.set(data);
726
+ } catch (err) {
727
+ error.set(err.message);
728
+ } finally {
729
+ isLoading.set(false);
730
+ }
731
+ });
732
+
733
+ const userDisplay = Observable.computed((loading, data, err) => {
734
+ if (loading) return 'Loading...';
735
+ if (err) return `Error: ${err}`;
736
+ if (data) return `Hello ${data.name}`;
737
+ return 'No user';
738
+ }, fetchUser);
739
+
740
+ await fetchUser(123);
741
741
  ```
742
742
 
743
- With transform options useful when the stored format differs from the runtime format:
743
+ > A computed observable can depend on **one batch function** only, not multiple.
744
+
745
+ ---
746
+
747
+ ## Observable Resources
748
+
749
+ `Observable.resource()` manages async data fetching with built-in states, `AbortController` support, and reactive dependencies. See **[Observable Resource](./observable-resource.md)** for the full guide.
750
+
744
751
  ```javascript
745
- // Store as ISO string, restore as Date object
746
- const selectedDate = Observable(new Date()).persist('event:date', {
747
- get: value => new Date(value), // transform on load
748
- set: value => value.toISOString() // transform on save
749
- });
752
+ const userId = Observable(1);
750
753
 
751
- // Store only the user id, restore the full object later
752
- const user = Observable(null).persist('session:user', {
753
- get: value => value ? JSON.parse(value) : null,
754
- set: value => value ? JSON.stringify(value) : null
755
- });
754
+ const user = Observable.resource(
755
+ async (id, signal) => {
756
+ const res = await fetch(`/api/users/${id}`, { signal });
757
+ return res.json();
758
+ },
759
+ [userId],
760
+ { auto: true }
761
+ );
762
+
763
+ Div([
764
+ ShowIf(user.loading, () => Div('Loading...')),
765
+ ShowIf(user.isReady(), () => Div(user.data.select(u => u.name))),
766
+ ShowIf(user.isErrored(), () => Div(['Error: ', user.error]))
767
+ ]);
756
768
  ```
757
769
 
758
- **Without Store** — use `.persist()` directly on any observable for component-level persistence without polluting the global store:
770
+ ---
771
+
772
+ ## Observable.format()
773
+
774
+ Creates a derived observable that formats the current value using `Intl`. You must set a locale observable before using `format()`. When using the CLI template, pass `I18nService.current` as the locale:
775
+
759
776
  ```javascript
760
- const FiltersPanel = () => {
761
- const expanded = Observable(false).persist('filters:expanded');
762
- const columns = Observable(['name', 'date']).persist('table:columns');
777
+ import { Observable } from 'native-document';
778
+ import { I18nService } from '@/core/services';
763
779
 
764
- return Div([/* ... */]);
765
- };
780
+ // Using the CLI template i18n service (recommended)
781
+ Observable.setLocale(I18nService.current);
782
+
783
+ // Or with a plain observable
784
+ const $locale = Observable('fr');
785
+ Observable.setLocale($locale);
786
+
787
+ const price = Observable(15000);
788
+ const date = Observable(new Date());
789
+ const count = Observable(3);
790
+
791
+ price.format('currency') // "15 000 FCFA"
792
+ price.format('currency', { currency: 'EUR' }) // "15 000,00 €"
793
+ price.format('currency', { notation: 'compact' }) // "15 K FCFA"
794
+
795
+ price.format('number') // "15 000"
796
+
797
+ Observable(0.15).format('percent') // "15,0 %"
798
+ Observable(0.15).format('percent', { decimals: 2 }) // "15,00 %"
799
+
800
+ date.format('date') // "3 mars 2026"
801
+ date.format('date', { dateStyle: 'full' }) // "mardi 3 mars 2026"
802
+ date.format('date', { format: 'DD/MM/YYYY' }) // "03/03/2026"
803
+
804
+ date.format('time') // "20:30"
805
+ date.format('time', { second: '2-digit' }) // "20:30:00"
806
+
807
+ date.format('datetime') // "3 mars 2026, 20:30"
808
+
809
+ date.format('relative') // "dans 11 jours"
810
+ date.format('relative', { unit: 'month' }) // "dans 1 mois"
811
+
812
+ count.format('plural', { singular: 'billet', plural: 'billets' }) // "3 billets"
813
+
814
+ // Custom formatter - works like transform()
815
+ price.format(value => `${value.toLocaleString()} FCFA`)
766
816
  ```
767
817
 
768
- ### `deepSubscribe(callback)` Deep Change Detection
818
+ Formatted observables react to locale changes automatically:
769
819
 
770
- Reacts to changes at any depth — array mutations, and property changes on nested observables.
771
820
  ```javascript
772
- const tags = Observable.array([{ label: 'admin' }]);
821
+ Observable.setLocale(I18nService.current);
773
822
 
774
- const unsub = tags.deepSubscribe(value => console.log('changed:', value));
823
+ const label = Observable(15000).format('currency', { currency: 'XOF' });
824
+ label.val(); // "15 000 FCFA"
825
+
826
+ I18nService.current.set('en-US');
827
+ label.val(); // "$15,000.00"
828
+ ```
829
+
830
+ ### Available format types
831
+
832
+ | Type | Input | Key options |
833
+ |---|---|---|
834
+ | `currency` | `number` | `currency`, `notation`, `minimumFractionDigits`, `maximumFractionDigits` |
835
+ | `number` | `number` | `notation`, `minimumFractionDigits`, `maximumFractionDigits` |
836
+ | `percent` | `number` | `decimals` |
837
+ | `date` | `Date \| number` | `dateStyle`, `format` |
838
+ | `time` | `Date \| number` | `hour`, `minute`, `second`, `format` |
839
+ | `datetime` | `Date \| number` | `dateStyle`, `hour`, `minute`, `second`, `format` |
840
+ | `relative` | `Date \| number` | `unit`, `numeric` |
841
+ | `plural` | `number` | `singular`, `plural` |
775
842
 
776
- tags.push({ label: 'editor' }); // ✅ déclenche
777
- tags[0].label.set('superadmin'); // ✅ déclenche
778
- tags.splice(0, 1); // ✅ déclenche + cleanup du listener
843
+ ### Extending Formatters
779
844
 
780
- // Cleanup
781
- unsub();
845
+ ```javascript
846
+ import { Formatters } from 'native-document';
847
+
848
+ Formatters.duration = (value, locale) => {
849
+ const hours = Math.floor(value / 3600);
850
+ const minutes = Math.floor((value % 3600) / 60);
851
+ return `${hours}h${minutes < 10 ? '0' : ''}${minutes}`;
852
+ };
853
+
854
+ const duration = Observable(3661);
855
+ duration.format('duration'); // "1h01"
782
856
  ```
783
857
 
858
+ ---
859
+
784
860
  ## Best Practices
785
861
 
786
- 1. **Use descriptive names** for your observables
787
- 2. **Understand the difference**: `Observable(object)` vs `Observable.object(object)`
788
- 3. **Group related data** with `Observable.object()` for individual property reactivity
789
- 4. **Use `Observable.value()`** to extract plain values from complex structures
790
- 5. **Prefer computed** for derived values
791
- 6. **Clean up** unused observables to prevent memory leaks
792
- 7. **Use `trigger()`** when you need to force updates without value changes
793
- 8. **Avoid** direct modifications in subscription callbacks
862
+ 1. Use descriptive names for your observables
863
+ 2. Know the difference between `Observable(object)` (single observable) and `Observable.object(object)` (per-property observables)
864
+ 3. Use `Observable.computed()` for derived values instead of manual subscriptions
865
+ 4. Use `.persist()` for component-level localStorage binding instead of the global Store
866
+ 5. Use `.intercept()` to sanitize, clamp, or abort values at the source
867
+ 6. Use `.on()` instead of `.subscribe()` when you only care about specific values
868
+ 7. Use `Observable.autoCleanup(true)` in long-running applications
869
+ 8. Use `Observable.resource()` for async data instead of manual fetch + observable patterns
794
870
 
795
871
  ## Next Steps
796
872
 
797
- - **[Elements](elements.md)** - Creating and composing UI
798
- - **[Conditional Rendering](conditional-rendering.md)** - Dynamic content
799
- - **[List Rendering](list-rendering.md)** - (ForEach | ForEachArray) and dynamic lists
800
- - **[Routing](routing.md)** - Navigation and URL management
801
- - **[State Management](state-management.md)** - Global state patterns
802
- - **[Lifecycle Events](lifecycle-events.md)** - Lifecycle events
803
- - **[NDElement](native-document-element.md)** - Native Document Element
804
- - **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
805
- - **[Args Validation](validation.md)** - Function Argument Validation
806
- - **[Memory Management](memory-management.md)** - Memory management
807
- - **[Anchor](anchor.md)** - Anchor
808
-
809
- ## Utilities
810
-
811
- - **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
812
- - **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
813
- - **[Filters](docs/utils/filters.md)** - Data filtering helpers
873
+ - **[Elements](./elements.md)** - Creating and composing UI
874
+ - **[Conditional Rendering](./conditional-rendering.md)** - Dynamic content
875
+ - **[List Rendering](./list-rendering.md)** - ForEach and dynamic lists
876
+ - **[State Management](./state-management.md)** - Global state with Store
877
+ - **[Observable Resource](./observable-resource.md)** - Async data fetching
878
+ - **[i18n & Formatting](./i18n.md)** - Locale-aware formatting
879
+ - **[Filters](./filters.md)** - Data filtering helpers