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.
- package/.vitepress/config.js +166 -0
- package/CHANGELOG.md +153 -0
- package/components.d.ts +2 -0
- package/components.js +2 -1
- package/devtools/widget.js +1 -1
- package/dist/native-document.components.min.js +11589 -2983
- package/dist/native-document.dev.js +2280 -396
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/advanced-components.md +213 -608
- package/docs/anchor.md +173 -312
- package/docs/cache.md +95 -803
- package/docs/cli.md +179 -0
- package/docs/components/accordion.md +172 -0
- package/docs/components/alert.md +99 -0
- package/docs/components/avatar.md +160 -0
- package/docs/components/badge.md +102 -0
- package/docs/components/breadcrumb.md +89 -0
- package/docs/components/button.md +183 -0
- package/docs/components/card.md +69 -0
- package/docs/components/context-menu.md +118 -0
- package/docs/components/data-table.md +345 -0
- package/docs/components/dropdown.md +214 -0
- package/docs/components/form/autocomplete-field.md +81 -0
- package/docs/components/form/checkbox-field.md +41 -0
- package/docs/components/form/checkbox-group-field.md +54 -0
- package/docs/components/form/color-field.md +64 -0
- package/docs/components/form/date-field.md +92 -0
- package/docs/components/form/field-collection.md +63 -0
- package/docs/components/form/file-field.md +203 -0
- package/docs/components/form/form-control.md +87 -0
- package/docs/components/form/image-field.md +90 -0
- package/docs/components/form/index.md +115 -0
- package/docs/components/form/number-field.md +65 -0
- package/docs/components/form/radio-field.md +51 -0
- package/docs/components/form/select-field.md +123 -0
- package/docs/components/form/slider.md +136 -0
- package/docs/components/form/string-field.md +134 -0
- package/docs/components/form/textarea-field.md +65 -0
- package/docs/components/form-fields.md +372 -0
- package/docs/components/getting-started.md +264 -0
- package/docs/components/index.md +337 -0
- package/docs/components/layout.md +279 -0
- package/docs/components/list.md +73 -0
- package/docs/components/menu.md +215 -0
- package/docs/components/modal.md +156 -0
- package/docs/components/pagination.md +95 -0
- package/docs/components/popover.md +131 -0
- package/docs/components/progress.md +111 -0
- package/docs/components/shortcut-manager.md +221 -0
- package/docs/components/simple-table.md +107 -0
- package/docs/components/skeleton.md +155 -0
- package/docs/components/spinner.md +100 -0
- package/docs/components/splitter.md +133 -0
- package/docs/components/stepper.md +163 -0
- package/docs/components/switch.md +113 -0
- package/docs/components/tabs.md +153 -0
- package/docs/components/toast.md +119 -0
- package/docs/components/tooltip.md +151 -0
- package/docs/components/traits.md +261 -0
- package/docs/conditional-rendering.md +170 -588
- package/docs/contributing.md +300 -25
- package/docs/core-concepts.md +205 -374
- package/docs/elements.md +251 -367
- package/docs/extending-native-document-element.md +192 -207
- package/docs/filters.md +153 -1122
- package/docs/getting-started.md +193 -267
- package/docs/i18n.md +241 -0
- package/docs/index.md +76 -0
- package/docs/lifecycle-events.md +143 -75
- package/docs/list-rendering.md +227 -852
- package/docs/memory-management.md +134 -47
- package/docs/native-document-element.md +337 -186
- package/docs/native-fetch.md +99 -630
- package/docs/observable-resource.md +364 -0
- package/docs/observables.md +592 -526
- package/docs/routing.md +244 -653
- package/docs/state-management.md +134 -241
- package/docs/svg-elements.md +231 -0
- package/docs/theming.md +409 -0
- package/docs/validation.md +95 -97
- package/docs/vitepress-conventions.md +219 -0
- package/eslint.config.js +28 -33
- package/i18n.js +1 -1
- package/i18n.ts +2 -0
- package/index.js +3 -0
- package/package.json +36 -14
- package/readme.md +269 -89
- package/src/components/$traits/has-draggable/HasDraggable.d.ts +4 -0
- package/src/components/$traits/has-draggable/HasDraggable.js +13 -0
- package/src/components/$traits/has-items/HasItems.d.ts +9 -0
- package/src/components/$traits/has-items/HasItems.js +6 -6
- package/src/components/$traits/has-position/HasFullPosition.d.ts +14 -0
- package/src/components/$traits/has-position/HasFullPosition.js +44 -0
- package/src/components/$traits/has-position/HasPosition.d.ts +7 -0
- package/src/components/$traits/has-position/HasPosition.js +23 -1
- package/src/components/$traits/has-resizable/HasResizable.d.ts +13 -0
- package/src/components/$traits/has-resizable/HasResizable.js +9 -0
- package/src/components/$traits/has-validation/HasValidation.d.ts +17 -0
- package/src/components/$traits/has-validation/HasValidation.js +54 -7
- package/src/components/BaseComponent.d.ts +32 -0
- package/src/components/BaseComponent.js +65 -9
- package/src/components/accordion/Accordion.js +39 -14
- package/src/components/accordion/AccordionItem.js +45 -14
- package/src/components/accordion/index.js +2 -2
- package/src/components/accordion/types/Accordion.d.ts +47 -0
- package/src/components/accordion/types/AccordionItem.d.ts +48 -0
- package/src/components/alert/Alert.js +70 -38
- package/src/components/alert/index.js +2 -2
- package/src/components/alert/types/Alert.d.ts +62 -0
- package/src/components/avatar/Avatar.js +49 -12
- package/src/components/avatar/AvatarGroup.js +50 -2
- package/src/components/avatar/index.js +2 -2
- package/src/components/avatar/types/Avatar.d.ts +74 -0
- package/src/components/avatar/types/AvatarGroup.d.ts +32 -0
- package/src/components/badge/Badge.js +125 -5
- package/src/components/badge/index.js +2 -2
- package/src/components/badge/types/Badge.d.ts +51 -0
- package/src/components/breadcrumb/BreadCrumb.js +61 -5
- package/src/components/breadcrumb/index.js +2 -2
- package/src/components/breadcrumb/types/BreadCrumb.d.ts +42 -0
- package/src/components/button/Button.js +164 -9
- package/src/components/button/index.js +1 -1
- package/src/components/button/types/Button.d.ts +62 -0
- package/src/components/card/Card.js +204 -32
- package/src/components/card/index.js +4 -4
- package/src/components/card/types/Card.d.ts +42 -0
- package/src/components/context-menu/ContextMenu.js +49 -5
- package/src/components/context-menu/ContextMenuGroup.js +15 -2
- package/src/components/context-menu/ContextMenuItem.js +14 -2
- package/src/components/context-menu/index.js +5 -5
- package/src/components/context-menu/types/ContextMenu.d.ts +30 -0
- package/src/components/context-menu/types/ContextMenuGroup.d.ts +18 -0
- package/src/components/context-menu/types/ContextMenuItem.d.ts +18 -0
- package/src/components/divider/Divider.js +120 -4
- package/src/components/divider/index.js +3 -3
- package/src/components/divider/types/Divider.d.ts +55 -0
- package/src/components/dropdown/Dropdown.js +239 -16
- package/src/components/dropdown/DropdownDivider.js +22 -2
- package/src/components/dropdown/DropdownGroup.js +44 -5
- package/src/components/dropdown/DropdownItem.js +76 -3
- package/src/components/dropdown/DropdownTrigger.js +49 -20
- package/src/components/dropdown/helpers.js +1 -1
- package/src/components/dropdown/index.js +6 -6
- package/src/components/dropdown/types/Dropdown.d.ts +88 -0
- package/src/components/dropdown/types/DropdownDivider.d.ts +20 -0
- package/src/components/dropdown/types/DropdownGroup.d.ts +25 -0
- package/src/components/dropdown/types/DropdownItem.d.ts +41 -0
- package/src/components/dropdown/types/DropdownTrigger.d.ts +32 -0
- package/src/components/form/FormControl.js +156 -13
- package/src/components/form/field/Field.js +172 -9
- package/src/components/form/field/FieldCollection.js +116 -12
- package/src/components/form/field/types/AutocompleteField.js +92 -2
- package/src/components/form/field/types/CheckboxField.js +43 -2
- package/src/components/form/field/types/CheckboxGroupField.js +83 -6
- package/src/components/form/field/types/ColorField.js +56 -3
- package/src/components/form/field/types/DateField.js +155 -4
- package/src/components/form/field/types/EmailField.js +54 -4
- package/src/components/form/field/types/FileField.js +140 -6
- package/src/components/form/field/types/HiddenField.js +27 -1
- package/src/components/form/field/types/ImageField.js +82 -3
- package/src/components/form/field/types/NumberField.js +97 -4
- package/src/components/form/field/types/PasswordField.js +103 -7
- package/src/components/form/field/types/RadioField.js +75 -4
- package/src/components/form/field/types/RangeField.js +67 -1
- package/src/components/form/field/types/SearchField.js +41 -2
- package/src/components/form/field/types/SelectField.js +133 -4
- package/src/components/form/field/types/StringField.js +91 -2
- package/src/components/form/field/types/TelField.js +55 -4
- package/src/components/form/field/types/TextAreaField.js +76 -2
- package/src/components/form/field/types/TimeField.js +120 -5
- package/src/components/form/field/types/UrlField.js +59 -4
- package/src/components/form/field/types/file-field-mode/FileAvatarMode.js +83 -4
- package/src/components/form/field/types/file-field-mode/FileDropzoneMode.js +61 -3
- package/src/components/form/field/types/file-field-mode/FileItemPreview.js +79 -3
- package/src/components/form/field/types/file-field-mode/FileNativeMode.js +24 -2
- package/src/components/form/field/types/file-field-mode/FileUploadButtonMode.js +64 -3
- package/src/components/form/field/types/file-field-mode/FileWallMode.js +56 -3
- package/src/components/form/index.js +28 -28
- package/src/components/form/types/Field.d.ts +73 -0
- package/src/components/form/types/FieldCollection.d.ts +53 -0
- package/src/components/form/types/FormControl.d.ts +64 -0
- package/src/components/form/types/fields/AutocompleteField.d.ts +48 -0
- package/src/components/form/types/fields/CheckboxField.d.ts +33 -0
- package/src/components/form/types/fields/CheckboxGroupField.d.ts +49 -0
- package/src/components/form/types/fields/ColorField.d.ts +37 -0
- package/src/components/form/types/fields/DateField.d.ts +70 -0
- package/src/components/form/types/fields/EmailField.d.ts +35 -0
- package/src/components/form/types/fields/FileAvatarMode.d.ts +46 -0
- package/src/components/form/types/fields/FileDropzoneMode.d.ts +28 -0
- package/src/components/form/types/fields/FileField.d.ts +56 -0
- package/src/components/form/types/fields/FileItemPreview.d.ts +35 -0
- package/src/components/form/types/fields/FileNativeMode.d.ts +21 -0
- package/src/components/form/types/fields/FileUploadButtonMode.d.ts +34 -0
- package/src/components/form/types/fields/FileWallMode.d.ts +32 -0
- package/src/components/form/types/fields/HiddenField.d.ts +26 -0
- package/src/components/form/types/fields/ImageField.d.ts +45 -0
- package/src/components/form/types/fields/NumberField.d.ts +48 -0
- package/src/components/form/types/fields/PasswordField.d.ts +46 -0
- package/src/components/form/types/fields/RadioField.d.ts +48 -0
- package/src/components/form/types/fields/RangeField.d.ts +44 -0
- package/src/components/form/types/fields/SearchField.d.ts +34 -0
- package/src/components/form/types/fields/SelectField.d.ts +71 -0
- package/src/components/form/types/fields/StringField.d.ts +48 -0
- package/src/components/form/types/fields/TelField.d.ts +37 -0
- package/src/components/form/types/fields/TextAreaField.d.ts +44 -0
- package/src/components/form/types/fields/TimeField.d.ts +51 -0
- package/src/components/form/types/fields/UrlField.d.ts +35 -0
- package/src/components/form/validation/Validation.js +54 -54
- package/src/components/index.d.ts +160 -0
- package/src/components/list/HasListItem.js +171 -0
- package/src/components/list/List.js +85 -67
- package/src/components/list/ListDivider.js +39 -0
- package/src/components/list/ListGroup.js +105 -38
- package/src/components/list/ListItem.js +158 -49
- package/src/components/list/index.js +8 -6
- package/src/components/list/types/List.d.ts +43 -0
- package/src/components/list/types/ListGroup.d.ts +37 -0
- package/src/components/list/types/ListItem.d.ts +53 -0
- package/src/components/menu/HasMenuItem.js +55 -6
- package/src/components/menu/Menu.js +113 -22
- package/src/components/menu/MenuDivider.js +18 -2
- package/src/components/menu/MenuGroup.js +61 -6
- package/src/components/menu/MenuItem.js +95 -11
- package/src/components/menu/MenuLink.js +27 -2
- package/src/components/menu/index.js +6 -6
- package/src/components/menu/types/Menu.d.ts +60 -0
- package/src/components/menu/types/MenuDivider.d.ts +19 -0
- package/src/components/menu/types/MenuGroup.d.ts +44 -0
- package/src/components/menu/types/MenuItem.d.ts +46 -0
- package/src/components/menu/types/MenuLink.d.ts +16 -0
- package/src/components/modal/Modal.js +258 -17
- package/src/components/modal/index.js +3 -3
- package/src/components/modal/types/Modal.d.ts +94 -0
- package/src/components/pagination/Pagination.js +155 -7
- package/src/components/pagination/index.js +3 -3
- package/src/components/pagination/types/Pagination.d.ts +68 -0
- package/src/components/popover/Popover.js +198 -11
- package/src/components/popover/PopoverFooter.js +33 -9
- package/src/components/popover/PopoverHeader.js +33 -8
- package/src/components/popover/index.js +4 -4
- package/src/components/popover/types/Popover.d.ts +83 -0
- package/src/components/popover/types/PopoverFooter.d.ts +24 -0
- package/src/components/popover/types/PopoverHeader.d.ts +26 -0
- package/src/components/progress/Progress.js +182 -13
- package/src/components/progress/index.js +3 -3
- package/src/components/progress/types/Progress.d.ts +77 -0
- package/src/components/skeleton/Skeleton.js +117 -49
- package/src/components/skeleton/index.js +3 -3
- package/src/components/skeleton/types/Skeleton.d.ts +55 -0
- package/src/components/slider/Slider.js +207 -10
- package/src/components/slider/index.js +2 -2
- package/src/components/slider/types/Slider.d.ts +82 -0
- package/src/components/spacer/Spacer.js +12 -3
- package/src/components/spacer/index.js +2 -2
- package/src/components/spacer/types/Spacer.d.ts +19 -0
- package/src/components/spinner/Spinner.js +180 -9
- package/src/components/spinner/index.js +3 -3
- package/src/components/spinner/types/Spinner.d.ts +71 -0
- package/src/components/splitter/Splitter.js +76 -13
- package/src/components/splitter/SplitterGutter.js +67 -5
- package/src/components/splitter/SplitterPanel.js +69 -2
- package/src/components/splitter/index.js +5 -5
- package/src/components/splitter/types/Splitter.d.ts +38 -0
- package/src/components/splitter/types/SplitterGutter.d.ts +38 -0
- package/src/components/splitter/types/SplitterPanel.d.ts +41 -0
- package/src/components/stacks/AbsoluteStack.js +23 -3
- package/src/components/stacks/FixedStack.js +23 -3
- package/src/components/stacks/HStack.js +24 -3
- package/src/components/stacks/PositionStack.js +111 -3
- package/src/components/stacks/RelativeStack.js +23 -3
- package/src/components/stacks/Stack.js +73 -2
- package/src/components/stacks/VStack.js +24 -4
- package/src/components/stacks/index.js +7 -7
- package/src/components/stacks/types/AbsoluteStack.d.ts +16 -0
- package/src/components/stacks/types/FixedStack.d.ts +16 -0
- package/src/components/stacks/types/HStack.d.ts +16 -0
- package/src/components/stacks/types/PositionStack.d.ts +54 -0
- package/src/components/stacks/types/RelativeStack.d.ts +17 -0
- package/src/components/stacks/types/Stack.d.ts +39 -0
- package/src/components/stacks/types/VStack.d.ts +16 -0
- package/src/components/stepper/Stepper.js +152 -12
- package/src/components/stepper/StepperStep.js +104 -3
- package/src/components/stepper/index.js +4 -4
- package/src/components/stepper/types/Stepper.d.ts +68 -0
- package/src/components/stepper/types/StepperStep.d.ts +54 -0
- package/src/components/switch/Switch.js +143 -6
- package/src/components/switch/index.js +1 -1
- package/src/components/switch/types/Switch.d.ts +55 -0
- package/src/components/table/Column.js +105 -6
- package/src/components/table/ColumnGroup.js +48 -3
- package/src/components/table/DataTable.js +256 -19
- package/src/components/table/SimpleTable.js +58 -4
- package/src/components/table/index.js +2 -2
- package/src/components/table/types/Column.d.ts +49 -0
- package/src/components/table/types/ColumnGroup.d.ts +28 -0
- package/src/components/table/types/DataTable.d.ts +97 -0
- package/src/components/table/types/SimpleTable.d.ts +40 -0
- package/src/components/tabs/Tabs.js +192 -5
- package/src/components/tabs/index.js +3 -3
- package/src/components/tabs/types/Tabs.d.ts +78 -0
- package/src/components/toast/Toast.js +133 -5
- package/src/components/toast/index.js +3 -3
- package/src/components/toast/types/Toast.d.ts +57 -0
- package/src/components/toast/types/ToastError.d.ts +7 -0
- package/src/components/toast/types/ToastInfo.d.ts +7 -0
- package/src/components/toast/types/ToastSuccess.d.ts +7 -0
- package/src/components/toast/types/ToastWarning.d.ts +7 -0
- package/src/components/tooltip/Tooltip.js +157 -13
- package/src/components/tooltip/index.js +2 -2
- package/src/components/tooltip/prototypes.js +1 -1
- package/src/components/tooltip/types/Tooltip.d.ts +65 -0
- package/src/core/data/MemoryManager.js +2 -2
- package/src/core/data/Observable.js +15 -18
- package/src/core/data/ObservableArray.js +118 -46
- package/src/core/data/ObservableChecker.js +2 -2
- package/src/core/data/ObservableItem.js +135 -21
- package/src/core/data/ObservableObject.js +126 -35
- package/src/core/data/ObservableResource.js +118 -3
- package/src/core/data/Store.js +142 -26
- package/src/core/data/observable-helpers/observable.is-to.js +196 -1
- package/src/core/data/observable-helpers/observable.prototypes.js +35 -8
- package/src/core/elements/anchor/anchor-with-sentinel.js +23 -2
- package/src/core/elements/anchor/anchor.js +16 -7
- package/src/core/elements/anchor/one-child-anchor-overwriting.js +2 -2
- package/src/core/elements/content-formatter.js +1 -1
- package/src/core/elements/control/for-each-array.js +9 -9
- package/src/core/elements/control/for-each.js +14 -14
- package/src/core/elements/control/show-if.js +11 -11
- package/src/core/elements/control/show-when.js +5 -5
- package/src/core/elements/control/switch.js +14 -14
- package/src/core/elements/description-list.js +1 -1
- package/src/core/elements/form.js +2 -2
- package/src/core/elements/fragment.js +1 -1
- package/src/core/elements/html5-semantics.js +1 -1
- package/src/core/elements/img.js +3 -3
- package/src/core/elements/interactive.js +1 -1
- package/src/core/elements/list.js +1 -1
- package/src/core/elements/medias.js +1 -1
- package/src/core/elements/meta-data.js +1 -1
- package/src/core/elements/svg.js +1 -1
- package/src/core/elements/table.js +1 -1
- package/src/core/errors/ArgTypesError.js +1 -1
- package/src/core/utils/HasEventEmitter.js +36 -2
- package/src/core/utils/args-types.js +9 -9
- package/src/core/utils/cache.js +1 -1
- package/src/core/utils/callback-handler.js +29 -0
- package/src/core/utils/debug-manager.js +6 -6
- package/src/core/utils/events.js +139 -139
- package/src/core/utils/filters/date.js +84 -3
- package/src/core/utils/filters/standard.js +136 -11
- package/src/core/utils/filters/strings.js +34 -2
- package/src/core/utils/filters/utils.js +40 -4
- package/src/core/utils/formatters.js +4 -4
- package/src/core/utils/helpers.js +39 -7
- package/src/core/utils/localstorage.js +11 -11
- package/src/core/utils/memoize.js +56 -3
- package/src/core/utils/plugins-manager.js +3 -3
- package/src/core/utils/property-accumulator.js +6 -6
- package/src/core/utils/prototypes.js +26 -1
- package/src/core/utils/shortcut-manager.js +2 -2
- package/src/core/utils/validator.js +8 -8
- package/src/core/wrappers/AttributesWrapper.js +32 -22
- package/src/core/wrappers/DocumentObserver.js +3 -3
- package/src/core/wrappers/ElementCreator.js +5 -5
- package/src/core/wrappers/HtmlElementWrapper.js +38 -12
- package/src/core/wrappers/NDElement.js +328 -22
- package/src/core/wrappers/NdPrototype.js +60 -16
- package/src/core/wrappers/SingletonView.js +50 -2
- package/src/core/wrappers/SvgElementWrapper.js +1 -1
- package/src/core/wrappers/constants.js +35 -2
- package/src/core/wrappers/prototypes/attributes-extensions.js +7 -7
- package/src/core/wrappers/prototypes/nd-element-extensions.js +72 -6
- package/src/core/wrappers/prototypes/nd-element.transition.extensions.js +42 -2
- package/src/core/wrappers/template-cloner/NodeCloner.js +53 -8
- package/src/core/wrappers/template-cloner/TemplateCloner.js +75 -6
- package/src/core/wrappers/template-cloner/attributes-hydrator.js +58 -2
- package/src/core/wrappers/template-cloner/utils.js +42 -6
- package/src/fetch/NativeFetch.js +3 -3
- package/src/i18n/bin/scan.js +6 -6
- package/src/i18n/index.d.ts +2 -0
- package/src/i18n/service/I18nService.d.ts +27 -0
- package/src/i18n/service/I18nService.js +5 -5
- package/src/i18n/service/functions.d.ts +22 -0
- package/src/i18n/service/functions.js +2 -2
- package/src/router/Route.js +3 -3
- package/src/router/RouteGroupHelper.js +2 -2
- package/src/router/Router.js +15 -15
- package/src/router/RouterComponent.js +33 -7
- package/src/router/link.js +4 -4
- package/src/router/modes/HashRouter.js +2 -2
- package/src/router/modes/HistoryRouter.js +2 -2
- package/src/router/modes/MemoryRouter.js +1 -1
- package/src/ui/components/accordion/AccordionItemRender.js +3 -3
- package/src/ui/components/accordion/AccordionRender.js +1 -1
- package/src/ui/components/alert/AlertRender.js +10 -10
- package/src/ui/components/avatar/avata-group/AvatarGroupRender.js +1 -1
- package/src/ui/components/avatar/avatar/AvatarRender.js +1 -1
- package/src/ui/components/breadcrumb/BreadcrumbRender.js +2 -2
- package/src/ui/components/button/ButtonRender.js +1 -1
- package/src/ui/components/card/CardRender.js +133 -0
- package/src/ui/components/card/card.css +169 -0
- package/src/ui/components/contextmenu/ContextmenuRender.js +6 -6
- package/src/ui/components/dropdown/DropdownRender.js +8 -8
- package/src/ui/components/dropdown/group/DropdownGroupRender.js +2 -2
- package/src/ui/components/dropdown/item/DropdownItemRender.js +1 -1
- package/src/ui/components/form/FieldCollectionRender.js +2 -2
- package/src/ui/components/form/FormControlRender.js +5 -5
- package/src/ui/components/form/fields/AutocompleteFieldRender.js +3 -3
- package/src/ui/components/form/fields/CheckboxFieldRender.js +1 -1
- package/src/ui/components/form/fields/CheckboxGroupFieldRender.js +1 -1
- package/src/ui/components/form/fields/DateFieldRender.js +7 -7
- package/src/ui/components/form/fields/EmailFieldRender.js +1 -1
- package/src/ui/components/form/fields/FieldRender.js +4 -4
- package/src/ui/components/form/fields/FileFieldRender.js +1 -1
- package/src/ui/components/form/fields/PasswordFieldRender.js +2 -2
- package/src/ui/components/form/fields/RadioFieldRender.js +1 -1
- package/src/ui/components/form/fields/RangeFieldRender.js +1 -1
- package/src/ui/components/form/fields/SelectFieldRender.js +2 -2
- package/src/ui/components/form/fields/SliderFieldRender.js +6 -6
- package/src/ui/components/form/fields/StringFieldRender.js +1 -1
- package/src/ui/components/form/fields/TelFieldRender.js +1 -1
- package/src/ui/components/form/fields/TextAreaFieldRender.js +1 -1
- package/src/ui/components/form/fields/TimeFieldRender.js +3 -3
- package/src/ui/components/form/fields/UrlFieldRender.js +1 -1
- package/src/ui/components/form/file-upload-mode/FileAvatarModeRender.js +1 -1
- package/src/ui/components/form/file-upload-mode/FileDropzoneModeRender.js +2 -2
- package/src/ui/components/form/file-upload-mode/FileUploadButtonModeRender.js +2 -2
- package/src/ui/components/form/file-upload-mode/FileWallModeRender.js +1 -1
- package/src/ui/components/form/helpers.js +8 -8
- package/src/ui/components/form/index.js +27 -27
- package/src/ui/components/list/ListRender.js +18 -0
- package/src/ui/components/list/divider/ListDividerRender.js +10 -0
- package/src/ui/components/list/divider/list-divider.css +12 -0
- package/src/ui/components/list/group/ListGroupRender.js +61 -0
- package/src/ui/components/list/group/list-group.css +62 -0
- package/src/ui/components/list/item/ListItemRender.js +238 -0
- package/src/ui/components/list/item/list-item.css +191 -0
- package/src/ui/components/list/list.css +24 -0
- package/src/ui/components/menu/MenuDividerRender.js +1 -1
- package/src/ui/components/menu/MenuGroupRender.js +3 -3
- package/src/ui/components/menu/MenuItemRender.js +2 -2
- package/src/ui/components/menu/MenuLinkRender.js +3 -3
- package/src/ui/components/menu/helpers.js +4 -4
- package/src/ui/components/modal/ModalRender.js +4 -4
- package/src/ui/components/pagination/PaginationRender.js +9 -9
- package/src/ui/components/popover/PopoverRender.js +7 -7
- package/src/ui/components/progress/ProgressRender.js +12 -12
- package/src/ui/components/skeleton/SkeletonRender.js +56 -0
- package/src/ui/components/spacer/SpacerRender.js +10 -0
- package/src/ui/components/splitter/SplitterGutterRender.js +1 -1
- package/src/ui/components/splitter/SplitterPanelRender.js +2 -2
- package/src/ui/components/stacks/PositionStackRender.js +1 -1
- package/src/ui/components/stacks/StackRender.js +1 -1
- package/src/ui/components/stacks/absolute-stack/AbsoluteStackRender.js +1 -1
- package/src/ui/components/stacks/fixed-stack/FixedStackRender.js +1 -1
- package/src/ui/components/stacks/h-stack/HStackRender.js +1 -1
- package/src/ui/components/stacks/index.js +5 -5
- package/src/ui/components/stacks/relative-stack/RelativeStackRender.js +1 -1
- package/src/ui/components/stacks/v-stack/VStackRender.js +1 -1
- package/src/ui/components/stepper/StepperRender.js +2 -2
- package/src/ui/components/stepper/StepperStepRender.js +4 -4
- package/src/ui/components/switch/SwitchRender.js +4 -4
- package/src/ui/components/table/data-table/DataTableRender.js +5 -5
- package/src/ui/components/table/data-table/bulk-actions.js +7 -7
- package/src/ui/components/table/data-table/pagination.js +6 -6
- package/src/ui/components/table/data-table/tables.js +25 -25
- package/src/ui/components/table/data-table/toolbar.js +3 -3
- package/src/ui/components/table/simple-table/SimpleTableRender.js +8 -8
- package/src/ui/components/tabs/TabsRender.js +11 -11
- package/src/ui/components/toast/ToastRender.js +3 -3
- package/src/ui/components/tooltip/TooltipRender.js +1 -1
- package/src/ui/index.js +44 -36
- package/types/elements.d.ts +163 -1037
- package/types/forms.d.ts +16 -20
- package/types/globals.d.ts +543 -0
- package/types/images.d.ts +2 -2
- package/types/observable-resource.d.ts +3 -0
- package/types/property-accumulator.d.ts +4 -4
- package/types/store.d.ts +26 -2
- package/types/validator.ts +3 -3
- package/ui.js +1 -0
- package/src/components/form/field/DefaultRender.js +0 -77
- package/src/components/form/field/FieldFactory.js +0 -107
- package/src/components/skeleton/SkeletonList.js +0 -0
- package/src/components/skeleton/SkeletonParagraph.js +0 -0
- package/src/components/skeleton/SkeletonTable.js +0 -0
- /package/{src/components/skeleton/SkeletonCard.js → docs/tutorials/.gitkeep} +0 -0
package/docs/observables.md
CHANGED
|
@@ -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
|
|
8
|
+
Observables are the reactive core of NativeDocument. They wrap values and automatically update the UI when those values change.
|
|
4
9
|
|
|
5
|
-
## Creating
|
|
10
|
+
## Creating Observables
|
|
6
11
|
|
|
7
12
|
```javascript
|
|
8
|
-
const count
|
|
9
|
-
const message
|
|
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(
|
|
21
|
+
const name = Observable('John');
|
|
17
22
|
|
|
18
23
|
// Read the current value
|
|
19
|
-
console.log(name.val());
|
|
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(
|
|
26
|
-
console.log(name.val());
|
|
28
|
+
name.set('Jane');
|
|
29
|
+
console.log(name.val()); // "Jane"
|
|
27
30
|
|
|
28
|
-
// Update
|
|
29
|
-
name.$value =
|
|
30
|
-
console.log(name.val());
|
|
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(
|
|
34
|
-
console.log(name.val());
|
|
36
|
+
name.set(current => current.toUpperCase());
|
|
37
|
+
console.log(name.val()); // "BOB"
|
|
35
38
|
```
|
|
36
39
|
|
|
37
40
|
## Listening to Changes
|
|
38
41
|
|
|
39
|
-
|
|
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(
|
|
47
|
+
counter.subscribe((newValue, oldValue) => {
|
|
48
|
+
console.log(`Counter: ${oldValue} -> ${newValue}`);
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
counter.set(1); //
|
|
49
|
-
counter.set(2); //
|
|
51
|
+
counter.set(1); // "Counter: 0 -> 1"
|
|
52
|
+
counter.set(2); // "Counter: 1 -> 2"
|
|
50
53
|
```
|
|
51
54
|
|
|
52
|
-
## Value-Specific Watchers
|
|
55
|
+
## Value-Specific Watchers - `.on()`
|
|
53
56
|
|
|
54
|
-
|
|
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(
|
|
60
|
+
const status = Observable('idle');
|
|
58
61
|
|
|
59
|
-
status.on(
|
|
62
|
+
status.on('loading', (isActive) => {
|
|
60
63
|
console.log(`Loading state: ${isActive}`);
|
|
61
64
|
});
|
|
62
65
|
|
|
63
|
-
status.on(
|
|
66
|
+
status.on('success', (isActive) => {
|
|
64
67
|
console.log(`Success state: ${isActive}`);
|
|
65
68
|
});
|
|
66
69
|
|
|
67
|
-
status.set(
|
|
68
|
-
status.set(
|
|
69
|
-
status.set(
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
## `.when()` - Lightweight Conditional
|
|
115
83
|
|
|
116
|
-
|
|
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(
|
|
87
|
+
const status = Observable('loading');
|
|
126
88
|
|
|
127
89
|
const element = Div({
|
|
128
90
|
class: {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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(
|
|
117
|
+
| `.pluck(property)` | Extracting a field (Lodash-style) |
|
|
175
118
|
| `.transform(fn)` | Explicit value transformation |
|
|
176
119
|
|
|
177
120
|
```javascript
|
|
178
|
-
|
|
179
|
-
ShowIf(user.is(u => u.isAdmin), AdminPanel())
|
|
121
|
+
ShowIf(user.is(u => u.isAdmin), AdminPanel)
|
|
180
122
|
|
|
181
|
-
const email
|
|
182
|
-
const label
|
|
183
|
-
const 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
|
-
|
|
188
|
-
|
|
128
|
+
## Utility Methods
|
|
129
|
+
|
|
130
|
+
### `toggle()` - Boolean toggle
|
|
131
|
+
|
|
189
132
|
```javascript
|
|
190
|
-
const
|
|
191
|
-
const date = Observable(new Date());
|
|
192
|
-
const count = Observable(3);
|
|
133
|
+
const isVisible = Observable(false);
|
|
193
134
|
|
|
194
|
-
//
|
|
195
|
-
|
|
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
|
-
|
|
200
|
-
|
|
138
|
+
Button('Toggle').nd.onClick(() => isVisible.toggle());
|
|
139
|
+
```
|
|
201
140
|
|
|
202
|
-
|
|
203
|
-
Observable(0.15).format('percent') // "15,0 %"
|
|
204
|
-
Observable(0.15).format('percent', { decimals: 2}) // "15,00 %"
|
|
141
|
+
### `equals()` / `toBool()`
|
|
205
142
|
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
//
|
|
214
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
149
|
+
const text = Observable('');
|
|
150
|
+
console.log(text.toBool()); // false
|
|
151
|
+
text.set('Hello');
|
|
152
|
+
console.log(text.toBool()); // true
|
|
153
|
+
```
|
|
222
154
|
|
|
223
|
-
|
|
224
|
-
date.format('relative') // "dans 11 jours"
|
|
225
|
-
date.format('relative', { unit: 'month' }) // "dans 1 mois"
|
|
155
|
+
### Convenience checkers - `is...()`
|
|
226
156
|
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
###
|
|
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
|
|
239
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
228
|
+
name.toLiteral('Hello ${v}!') // "Hello Alice !"
|
|
229
|
+
name.toTrimmed().toLiteral('Hello ${v}!') // "Hello Alice!"
|
|
245
230
|
|
|
246
|
-
|
|
247
|
-
label.val(); // "15 000 FCFA"
|
|
231
|
+
name.toLiteral('Hello [name]!', '[name]') // custom placeholder
|
|
248
232
|
```
|
|
249
233
|
|
|
250
|
-
###
|
|
234
|
+
### `reset()` - Reset to initial value
|
|
251
235
|
|
|
252
|
-
Add custom format types via `Formatters`:
|
|
253
236
|
```javascript
|
|
254
|
-
|
|
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
|
-
|
|
263
|
-
|
|
239
|
+
name.set('Bob');
|
|
240
|
+
name.reset();
|
|
241
|
+
console.log(name.val()); // "Alice"
|
|
264
242
|
```
|
|
265
243
|
|
|
266
|
-
###
|
|
244
|
+
### `intercept(callback)` - Transform or abort before setting
|
|
267
245
|
|
|
268
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
257
|
+
age.set(-5); // sets 0
|
|
258
|
+
age.set(150); // sets 120
|
|
259
|
+
age.set(25); // sets 25
|
|
291
260
|
|
|
292
|
-
//
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
303
|
-
|
|
269
|
+
username.set(''); // aborted - still 'alice'
|
|
270
|
+
username.set(' Bob '); // sets 'bob'
|
|
304
271
|
```
|
|
305
272
|
|
|
306
|
-
|
|
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
|
-
|
|
275
|
+
Intercepts mutations (push, splice, etc.) on arrays and objects separately from `.intercept()`:
|
|
313
276
|
|
|
314
277
|
```javascript
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
278
|
+
const items = Observable.array([]);
|
|
279
|
+
|
|
280
|
+
items.interceptMutations((operations) => {
|
|
281
|
+
console.log('Mutation:', operations);
|
|
319
282
|
});
|
|
320
283
|
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
console.log(user.name.$value); // "Alice"
|
|
284
|
+
items.push('apple'); // logs mutation details
|
|
285
|
+
```
|
|
324
286
|
|
|
325
|
-
|
|
326
|
-
user.name.set("Bob");
|
|
327
|
-
user.age.$value = 30;
|
|
287
|
+
### `once(predicate, callback)` - Single-time listener
|
|
328
288
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
console.log(user.val()); // Same as above
|
|
289
|
+
```javascript
|
|
290
|
+
const count = Observable(0);
|
|
332
291
|
|
|
333
|
-
//
|
|
334
|
-
|
|
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
|
-
//
|
|
339
|
-
|
|
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
|
|
347
|
-
|
|
348
|
-
"Call doctor"
|
|
349
|
-
]);
|
|
302
|
+
const status = Observable('idle');
|
|
303
|
+
const handler = (isActive) => console.log('Loading:', isActive);
|
|
350
304
|
|
|
351
|
-
|
|
352
|
-
todos.pop();
|
|
305
|
+
status.on('loading', handler);
|
|
353
306
|
|
|
354
|
-
|
|
307
|
+
status.off('loading', handler); // remove specific callback
|
|
308
|
+
status.off('loading'); // remove all watchers for this value
|
|
355
309
|
```
|
|
356
310
|
|
|
357
|
-
###
|
|
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
|
|
316
|
+
const data = Observable('test');
|
|
362
317
|
|
|
363
|
-
|
|
364
|
-
console.log('
|
|
318
|
+
data.onCleanup(() => {
|
|
319
|
+
console.log('Observable cleaned up');
|
|
365
320
|
});
|
|
366
321
|
|
|
367
|
-
|
|
368
|
-
user.age.set(30); // triggers the parent subscribe
|
|
322
|
+
data.cleanup(); // triggers the cleanup callback
|
|
369
323
|
```
|
|
370
324
|
|
|
371
|
-
|
|
325
|
+
### `persist(key, options?)` - Bind to localStorage
|
|
372
326
|
|
|
373
|
-
|
|
327
|
+
Automatically saves on every change and restores on load:
|
|
374
328
|
|
|
375
329
|
```javascript
|
|
376
|
-
const
|
|
377
|
-
|
|
330
|
+
const theme = Observable('light').persist('theme');
|
|
331
|
+
theme.set('dark'); // saved to localStorage
|
|
378
332
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
333
|
+
// On next page load
|
|
334
|
+
const theme = Observable('light').persist('theme');
|
|
335
|
+
theme.val(); // "dark" - restored
|
|
336
|
+
```
|
|
382
337
|
|
|
383
|
-
|
|
338
|
+
With transform options:
|
|
384
339
|
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
347
|
+
### `clone()` - Create a copy
|
|
390
348
|
|
|
391
|
-
|
|
392
|
-
const count = Observable(0);
|
|
349
|
+
Creates a deep clone of the observable's current value:
|
|
393
350
|
|
|
394
|
-
|
|
395
|
-
const
|
|
351
|
+
```javascript
|
|
352
|
+
const original = Observable({ name: 'Alice', scores: [1, 2, 3] });
|
|
353
|
+
const copy = original.clone();
|
|
396
354
|
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
+
Recursively extracts plain values from observables, arrays, and proxies:
|
|
409
362
|
|
|
410
363
|
```javascript
|
|
411
|
-
const name = Observable(
|
|
412
|
-
|
|
364
|
+
const name = Observable('Alice');
|
|
365
|
+
name.resolve(); // "Alice" - same as Observable.value(name)
|
|
413
366
|
|
|
414
|
-
|
|
415
|
-
|
|
367
|
+
const user = Observable.object({ name: Observable('Bob') });
|
|
368
|
+
Observable.value(user); // { name: 'Bob' } - deeply resolved
|
|
369
|
+
```
|
|
416
370
|
|
|
417
|
-
|
|
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
|
-
|
|
373
|
+
```javascript
|
|
374
|
+
const data = Observable('test');
|
|
375
|
+
data.trigger(); // notifies all subscribers without changing the value
|
|
423
376
|
```
|
|
424
377
|
|
|
425
|
-
###
|
|
378
|
+
### `cleanup()` - Remove all listeners
|
|
426
379
|
|
|
427
380
|
```javascript
|
|
428
|
-
const
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
389
|
+
```javascript
|
|
390
|
+
const tags = Observable.array([{ label: Observable('admin') }]);
|
|
444
391
|
|
|
445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
const lives = Observable(3);
|
|
398
|
+
unsub(); // manual cleanup
|
|
399
|
+
```
|
|
457
400
|
|
|
458
|
-
|
|
459
|
-
const gameStatus1 = Observable.computed(() => {
|
|
460
|
-
return `Score: ${score.val()}, Lives: ${lives.val()}`;
|
|
461
|
-
}, [score, lives]);
|
|
401
|
+
---
|
|
462
402
|
|
|
463
|
-
|
|
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
|
-
|
|
470
|
-
return `Score: ${score.val()}, Lives: ${lives.val()}`;
|
|
471
|
-
}, updateGame);
|
|
405
|
+
### `Observable.setLocale(locale)`
|
|
472
406
|
|
|
473
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
415
|
+
### `Observable.value(data)`
|
|
416
|
+
|
|
417
|
+
Recursively extracts plain values from observables, arrays, and proxies:
|
|
480
418
|
|
|
481
419
|
```javascript
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
487
|
-
discount.subscribe(discount => console.log(`Discount: ${discount}%`));
|
|
426
|
+
### `Observable.cleanup(observable)`
|
|
488
427
|
|
|
489
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
###
|
|
434
|
+
### `Observable.autoCleanup(enable, options?)`
|
|
435
|
+
|
|
436
|
+
Enables automatic cleanup on page unload and via a periodic interval:
|
|
513
437
|
|
|
514
438
|
```javascript
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
445
|
+
### `Observable.getById(id)`
|
|
520
446
|
|
|
521
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
453
|
+
Observable.getById(N); // returns obs
|
|
544
454
|
```
|
|
545
455
|
|
|
546
|
-
###
|
|
456
|
+
### `Observable.useValueProperty(propertyName?)`
|
|
547
457
|
|
|
548
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
//
|
|
565
|
-
|
|
472
|
+
// With custom name
|
|
473
|
+
Observable.useValueProperty('current');
|
|
474
|
+
count.current; // 0
|
|
475
|
+
count.current = 5; // sets to 5
|
|
566
476
|
```
|
|
567
477
|
|
|
568
|
-
|
|
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
|
-
|
|
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
|
-
|
|
482
|
+
## Observable Objects
|
|
576
483
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
+
`Observable.object` has aliases:
|
|
585
500
|
|
|
586
501
|
```javascript
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
591
|
-
const message = template.use({ name, age });
|
|
509
|
+
Subscribing to `Observable.object()` reacts to any property change:
|
|
592
510
|
|
|
593
|
-
|
|
511
|
+
```javascript
|
|
512
|
+
const user = Observable.object({ name: 'Alice', age: 25 });
|
|
594
513
|
|
|
595
|
-
|
|
596
|
-
console.log(
|
|
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
|
-
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Observable Arrays
|
|
600
525
|
|
|
601
526
|
```javascript
|
|
602
|
-
const
|
|
603
|
-
const user = Observable("Marie");
|
|
527
|
+
const todos = Observable.array(['Buy groceries', 'Call doctor']);
|
|
604
528
|
|
|
605
|
-
|
|
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
|
-
|
|
536
|
+
### Array-specific methods
|
|
609
537
|
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
614
|
-
data.subscribe(handler);
|
|
556
|
+
### Filtering with `.where()`
|
|
615
557
|
|
|
616
|
-
|
|
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
|
-
|
|
620
|
-
data.cleanup(); // Removes all listeners and prevents new subscriptions
|
|
560
|
+
**Function predicate** - full item passed to the callback:
|
|
621
561
|
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
|
|
574
|
+
**Object predicate** - filter per field:
|
|
631
575
|
|
|
632
|
-
|
|
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
|
-
|
|
587
|
+
import { match } from 'native-document/filters';
|
|
588
|
+
|
|
589
|
+
const search = Observable('');
|
|
636
590
|
|
|
637
|
-
const
|
|
638
|
-
|
|
591
|
+
const results = users.where({
|
|
592
|
+
name: match(search) // re-filters every time search changes
|
|
593
|
+
});
|
|
639
594
|
|
|
640
|
-
|
|
641
|
-
|
|
595
|
+
search.set('ali'); // -> Alice
|
|
596
|
+
search.set(''); // -> all users
|
|
642
597
|
```
|
|
643
598
|
|
|
644
|
-
|
|
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
|
-
|
|
604
|
+
import { equals, greaterThan, lessThan, and, or, not } from 'native-document/filters';
|
|
648
605
|
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
654
|
-
|
|
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
|
-
|
|
658
|
-
|
|
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
|
-
|
|
631
|
+
**`whereSome(fields, filter)`** - at least one field matches (OR across fields):
|
|
662
632
|
|
|
663
633
|
```javascript
|
|
664
|
-
|
|
634
|
+
import { match } from 'native-document/filters';
|
|
665
635
|
|
|
666
|
-
|
|
667
|
-
isVisible.toggle(); // false
|
|
636
|
+
const search = Observable('car');
|
|
668
637
|
|
|
669
|
-
|
|
638
|
+
const results = products.whereSome(['name', 'category'], match(search));
|
|
639
|
+
// matches if name OR category contains 'car'
|
|
670
640
|
```
|
|
671
641
|
|
|
672
|
-
|
|
642
|
+
**`whereEvery(fields, filter)`** - all fields match (AND across fields):
|
|
673
643
|
|
|
674
644
|
```javascript
|
|
675
|
-
|
|
645
|
+
import { equals } from 'native-document/filters';
|
|
676
646
|
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
|
|
690
|
-
const
|
|
691
|
-
|
|
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(
|
|
694
|
-
|
|
695
|
-
|
|
665
|
+
console.log(fullName.val()); // "John Doe"
|
|
666
|
+
|
|
667
|
+
firstName.set('Jane');
|
|
668
|
+
console.log(fullName.val()); // "Jane Doe"
|
|
696
669
|
```
|
|
697
670
|
|
|
698
|
-
|
|
671
|
+
Another example with more dependencies:
|
|
699
672
|
|
|
700
673
|
```javascript
|
|
701
|
-
const
|
|
702
|
-
|
|
674
|
+
const price = Observable(100);
|
|
675
|
+
const quantity = Observable(2);
|
|
676
|
+
const tax = Observable(0.2);
|
|
703
677
|
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
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
|
|
695
|
+
const firstName = Observable('John');
|
|
696
|
+
const lastName = Observable('Doe');
|
|
712
697
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
return newValue;
|
|
698
|
+
const updateName = Observable.batch((first, last) => {
|
|
699
|
+
firstName.set(first);
|
|
700
|
+
lastName.set(last);
|
|
717
701
|
});
|
|
718
702
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
703
|
+
const fullName = Observable.computed((first, last) => {
|
|
704
|
+
return `${first} ${last}`;
|
|
705
|
+
}, updateName); // depends on the batch - recalculates once
|
|
722
706
|
|
|
723
|
-
|
|
724
|
-
username.intercept((value) => value.toLowerCase().trim());
|
|
707
|
+
fullName.subscribe(name => console.log('Full name:', name));
|
|
725
708
|
|
|
726
|
-
|
|
727
|
-
|
|
709
|
+
updateName('Alice', 'Smith');
|
|
710
|
+
// "Full name: Alice Smith" - single notification
|
|
728
711
|
```
|
|
729
712
|
|
|
730
|
-
|
|
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
|
-
|
|
735
|
-
const
|
|
736
|
-
|
|
716
|
+
const isLoading = Observable(false);
|
|
717
|
+
const userData = Observable(null);
|
|
718
|
+
const error = Observable(null);
|
|
737
719
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
const columns = Observable(['name', 'date']).persist('table:columns');
|
|
777
|
+
import { Observable } from 'native-document';
|
|
778
|
+
import { I18nService } from '@/core/services';
|
|
763
779
|
|
|
764
|
-
|
|
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
|
-
|
|
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
|
-
|
|
821
|
+
Observable.setLocale(I18nService.current);
|
|
773
822
|
|
|
774
|
-
const
|
|
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
|
-
|
|
777
|
-
tags[0].label.set('superadmin'); // ✅ déclenche
|
|
778
|
-
tags.splice(0, 1); // ✅ déclenche + cleanup du listener
|
|
843
|
+
### Extending Formatters
|
|
779
844
|
|
|
780
|
-
|
|
781
|
-
|
|
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.
|
|
787
|
-
2.
|
|
788
|
-
3.
|
|
789
|
-
4.
|
|
790
|
-
5.
|
|
791
|
-
6.
|
|
792
|
-
7.
|
|
793
|
-
8.
|
|
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)** -
|
|
800
|
-
- **[
|
|
801
|
-
- **[
|
|
802
|
-
- **[
|
|
803
|
-
- **[
|
|
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
|