native-document 1.0.15 → 1.0.16-8.2
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/.npmrc.example +1 -0
- package/.vitepress/config.js +166 -0
- package/CHANGELOG.md +153 -0
- package/cdn.js +19 -0
- package/components.d.ts +2 -0
- package/components.js +30 -0
- package/devtools/ComponentRegistry.js +113 -0
- package/devtools/index.js +8 -0
- package/devtools/plugin/dev-tools-plugin.js +15 -0
- package/devtools/transformers/nd-vite-devtools.js +55 -0
- package/devtools/transformers/src/transformComponentForHrm.js +73 -0
- package/devtools/transformers/src/transformJsFile.js +9 -0
- package/devtools/transformers/src/utils.js +79 -0
- package/devtools/transformers/templates/hrm.hook.template.js +46 -0
- package/devtools/transformers/templates/hrm.orbservable.hook.template.js +76 -0
- package/devtools/widget/Widget.js +49 -0
- package/devtools/widget/widget.css +81 -0
- package/devtools/widget.js +23 -0
- package/dist/native-document.components.min.css +1 -0
- package/dist/native-document.components.min.js +23847 -0
- package/dist/native-document.dev.js +8421 -1492
- package/dist/native-document.dev.js.map +1 -0
- package/dist/native-document.devtools.min.js +1 -0
- package/dist/native-document.min.js +1 -1
- package/docs/advanced-components.md +419 -0
- package/docs/anchor.md +181 -257
- package/docs/cache.md +180 -0
- 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 +177 -502
- package/docs/contributing.md +300 -25
- package/docs/core-concepts.md +207 -366
- package/docs/elements.md +266 -254
- package/docs/extending-native-document-element.md +259 -0
- package/docs/filters.md +247 -0
- package/docs/getting-started.md +195 -257
- package/docs/i18n.md +241 -0
- package/docs/index.md +76 -0
- package/docs/lifecycle-events.md +146 -67
- package/docs/list-rendering.md +240 -460
- package/docs/memory-management.md +135 -46
- package/docs/native-document-element.md +487 -0
- package/docs/native-fetch.md +213 -0
- package/docs/observable-resource.md +364 -0
- package/docs/observables.md +690 -357
- package/docs/routing.md +246 -646
- package/docs/state-management.md +213 -306
- package/docs/svg-elements.md +231 -0
- package/docs/theming.md +409 -0
- package/docs/tutorials/.gitkeep +0 -0
- package/docs/validation.md +98 -91
- package/docs/vitepress-conventions.md +219 -0
- package/elements.d.ts +7 -0
- package/elements.js +3 -4
- package/eslint.config.js +35 -0
- package/i18n.js +1 -0
- package/i18n.ts +2 -0
- package/index.d.ts +21 -0
- package/index.def.js +1086 -0
- package/index.js +19 -13
- package/package.json +59 -9
- package/readme.md +296 -93
- package/rollup.config.js +52 -3
- package/router.d.ts +7 -0
- package/router.js +0 -0
- package/src/components/$traits/has-draggable/HasDraggable.d.ts +4 -0
- package/src/components/$traits/has-draggable/HasDraggable.js +82 -0
- package/src/components/$traits/has-draggable/has-draggable.css +8 -0
- package/src/components/$traits/has-items/HasItems.d.ts +9 -0
- package/src/components/$traits/has-items/HasItems.js +64 -0
- package/src/components/$traits/has-position/HasFullPosition.d.ts +14 -0
- package/src/components/$traits/has-position/HasFullPosition.js +95 -0
- package/src/components/$traits/has-position/HasPosition.d.ts +7 -0
- package/src/components/$traits/has-position/HasPosition.js +45 -0
- package/src/components/$traits/has-resizable/HasResizable.d.ts +13 -0
- package/src/components/$traits/has-resizable/HasResizable.js +122 -0
- package/src/components/$traits/has-resizable/has-resizable.css +121 -0
- package/src/components/$traits/has-validation/HasValidation.d.ts +17 -0
- package/src/components/$traits/has-validation/HasValidation.js +133 -0
- package/src/components/BaseComponent.d.ts +32 -0
- package/src/components/BaseComponent.js +247 -0
- package/src/components/accordion/Accordion.js +268 -0
- package/src/components/accordion/AccordionItem.js +233 -0
- package/src/components/accordion/index.js +7 -0
- 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 +350 -0
- package/src/components/alert/index.js +6 -0
- package/src/components/alert/types/Alert.d.ts +62 -0
- package/src/components/avatar/Avatar.js +430 -0
- package/src/components/avatar/AvatarGroup.js +97 -0
- package/src/components/avatar/index.js +7 -0
- 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 +245 -0
- package/src/components/badge/index.js +6 -0
- package/src/components/badge/types/Badge.d.ts +51 -0
- package/src/components/base-component.css +0 -0
- package/src/components/breadcrumb/BreadCrumb.js +138 -0
- package/src/components/breadcrumb/index.js +5 -0
- package/src/components/breadcrumb/types/BreadCrumb.d.ts +42 -0
- package/src/components/button/Button.js +320 -0
- package/src/components/button/index.js +5 -0
- package/src/components/button/types/Button.d.ts +62 -0
- package/src/components/card/Card.js +282 -0
- package/src/components/card/index.js +5 -0
- package/src/components/card/types/Card.d.ts +42 -0
- package/src/components/context-menu/ContextMenu.js +127 -0
- package/src/components/context-menu/ContextMenuGroup.js +29 -0
- package/src/components/context-menu/ContextMenuItem.js +28 -0
- package/src/components/context-menu/index.js +10 -0
- 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 +256 -0
- package/src/components/divider/index.js +6 -0
- package/src/components/divider/types/Divider.d.ts +55 -0
- package/src/components/dropdown/Dropdown.js +531 -0
- package/src/components/dropdown/DropdownDivider.js +45 -0
- package/src/components/dropdown/DropdownGroup.js +83 -0
- package/src/components/dropdown/DropdownItem.js +150 -0
- package/src/components/dropdown/DropdownTrigger.js +93 -0
- package/src/components/dropdown/helpers.js +53 -0
- package/src/components/dropdown/index.js +13 -0
- 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 +498 -0
- package/src/components/form/field/Field.js +419 -0
- package/src/components/form/field/FieldCollection.js +292 -0
- package/src/components/form/field/types/AutocompleteField.js +168 -0
- package/src/components/form/field/types/CheckboxField.js +77 -0
- package/src/components/form/field/types/CheckboxGroupField.js +171 -0
- package/src/components/form/field/types/ColorField.js +102 -0
- package/src/components/form/field/types/DateField.js +315 -0
- package/src/components/form/field/types/EmailField.js +104 -0
- package/src/components/form/field/types/FileField.js +276 -0
- package/src/components/form/field/types/HiddenField.js +44 -0
- package/src/components/form/field/types/ImageField.js +138 -0
- package/src/components/form/field/types/NumberField.js +177 -0
- package/src/components/form/field/types/PasswordField.js +200 -0
- package/src/components/form/field/types/RadioField.js +145 -0
- package/src/components/form/field/types/RangeField.js +117 -0
- package/src/components/form/field/types/SearchField.js +66 -0
- package/src/components/form/field/types/SelectField.js +247 -0
- package/src/components/form/field/types/StringField.js +148 -0
- package/src/components/form/field/types/TelField.js +98 -0
- package/src/components/form/field/types/TextAreaField.js +142 -0
- package/src/components/form/field/types/TimeField.js +215 -0
- package/src/components/form/field/types/UrlField.js +115 -0
- package/src/components/form/field/types/file-field-mode/FileAvatarMode.js +183 -0
- package/src/components/form/field/types/file-field-mode/FileDropzoneMode.js +117 -0
- package/src/components/form/field/types/file-field-mode/FileItemPreview.js +150 -0
- package/src/components/form/field/types/file-field-mode/FileNativeMode.js +43 -0
- package/src/components/form/field/types/file-field-mode/FileUploadButtonMode.js +120 -0
- package/src/components/form/field/types/file-field-mode/FileWallMode.js +106 -0
- package/src/components/form/index.js +61 -0
- package/src/components/form/merge +0 -0
- 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/utils.js +17 -0
- package/src/components/form/validation/Validation.js +565 -0
- package/src/components/index.d.ts +160 -0
- package/src/components/list/HasListItem.js +171 -0
- package/src/components/list/List.js +125 -0
- package/src/components/list/ListDivider.js +39 -0
- package/src/components/list/ListGroup.js +135 -0
- package/src/components/list/ListItem.js +212 -0
- package/src/components/list/index.js +12 -0
- 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 +182 -0
- package/src/components/menu/Menu.js +227 -0
- package/src/components/menu/MenuDivider.js +37 -0
- package/src/components/menu/MenuGroup.js +126 -0
- package/src/components/menu/MenuItem.js +190 -0
- package/src/components/menu/MenuLink.js +51 -0
- package/src/components/menu/index.js +14 -0
- 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 +524 -0
- package/src/components/modal/index.js +5 -0
- package/src/components/modal/types/Modal.d.ts +94 -0
- package/src/components/pagination/Pagination.js +411 -0
- package/src/components/pagination/index.js +5 -0
- package/src/components/pagination/types/Pagination.d.ts +68 -0
- package/src/components/popover/Popover.js +459 -0
- package/src/components/popover/PopoverFooter.js +61 -0
- package/src/components/popover/PopoverHeader.js +68 -0
- package/src/components/popover/index.js +10 -0
- 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 +401 -0
- package/src/components/progress/index.js +6 -0
- package/src/components/progress/types/Progress.d.ts +77 -0
- package/src/components/skeleton/Skeleton.js +228 -0
- package/src/components/skeleton/index.js +6 -0
- package/src/components/skeleton/types/Skeleton.d.ts +55 -0
- package/src/components/slider/Slider.js +406 -0
- package/src/components/slider/index.js +5 -0
- package/src/components/slider/types/Slider.d.ts +82 -0
- package/src/components/spacer/Spacer.js +27 -0
- package/src/components/spacer/index.js +5 -0
- package/src/components/spacer/types/Spacer.d.ts +19 -0
- package/src/components/spinner/Spinner.js +350 -0
- package/src/components/spinner/index.js +5 -0
- package/src/components/spinner/types/Spinner.d.ts +71 -0
- package/src/components/splitter/Splitter.js +164 -0
- package/src/components/splitter/SplitterGutter.js +140 -0
- package/src/components/splitter/SplitterPanel.js +143 -0
- package/src/components/splitter/index.js +10 -0
- 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 +53 -0
- package/src/components/stacks/FixedStack.js +53 -0
- package/src/components/stacks/HStack.js +54 -0
- package/src/components/stacks/PositionStack.js +254 -0
- package/src/components/stacks/RelativeStack.js +53 -0
- package/src/components/stacks/Stack.js +166 -0
- package/src/components/stacks/VStack.js +55 -0
- package/src/components/stacks/index.js +21 -0
- 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 +461 -0
- package/src/components/stepper/StepperStep.js +241 -0
- package/src/components/stepper/index.js +8 -0
- 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 +266 -0
- package/src/components/switch/index.js +6 -0
- package/src/components/switch/types/Switch.d.ts +55 -0
- package/src/components/table/Column.js +212 -0
- package/src/components/table/ColumnGroup.js +90 -0
- package/src/components/table/DataTable.js +720 -0
- package/src/components/table/SimpleTable.js +139 -0
- package/src/components/table/index.js +7 -0
- 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 +395 -0
- package/src/components/tabs/index.js +6 -0
- package/src/components/tabs/types/Tabs.d.ts +78 -0
- package/src/components/toast/Toast.js +262 -0
- package/src/components/toast/ToastError.js +0 -0
- package/src/components/toast/ToastInfo.js +0 -0
- package/src/components/toast/ToastSuccess.js +0 -0
- package/src/components/toast/ToastWarning.js +0 -0
- package/src/components/toast/index.js +5 -0
- 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 +359 -0
- package/src/components/tooltip/index.js +5 -0
- package/src/components/tooltip/prototypes.js +6 -0
- package/src/components/tooltip/types/Tooltip.d.ts +65 -0
- package/src/{data → core/data}/MemoryManager.js +2 -3
- package/src/core/data/Observable.js +227 -0
- package/src/core/data/ObservableArray.js +522 -0
- package/src/core/data/ObservableChecker.js +39 -0
- package/src/core/data/ObservableItem.js +611 -0
- package/src/core/data/ObservableObject.js +274 -0
- package/src/core/data/ObservableResource.js +315 -0
- package/src/core/data/ObservableWhen.js +54 -0
- package/src/core/data/Store.js +520 -0
- package/src/core/data/observable-helpers/observable.is-to.js +390 -0
- package/src/core/data/observable-helpers/observable.prototypes.js +145 -0
- package/src/core/elements/anchor/anchor-with-sentinel.js +66 -0
- package/src/core/elements/anchor/anchor.js +210 -0
- package/src/core/elements/anchor/one-child-anchor-overwriting.js +66 -0
- package/src/core/elements/content-formatter.js +169 -0
- package/src/core/elements/control/for-each-array.js +292 -0
- package/src/{elements → core/elements}/control/for-each.js +42 -23
- package/src/core/elements/control/show-if.js +94 -0
- package/src/core/elements/control/show-when.js +54 -0
- package/src/core/elements/control/switch.js +141 -0
- package/src/core/elements/description-list.js +19 -0
- package/src/core/elements/form.js +255 -0
- package/src/core/elements/fragment.js +8 -0
- package/src/core/elements/html5-semantics.js +55 -0
- package/src/core/elements/img.js +59 -0
- package/src/{elements → core/elements}/index.js +4 -4
- package/src/core/elements/interactive.js +25 -0
- package/src/core/elements/list.js +37 -0
- package/src/core/elements/medias.js +37 -0
- package/src/core/elements/meta-data.js +43 -0
- package/src/core/elements/svg.js +61 -0
- package/src/core/elements/table.js +73 -0
- package/src/{errors → core/errors}/ArgTypesError.js +1 -1
- package/src/{errors → core/errors}/NativeDocumentError.js +0 -0
- package/src/core/utils/HasEventEmitter.js +85 -0
- package/src/core/utils/args-types.js +140 -0
- package/src/core/utils/cache.js +5 -0
- package/src/core/utils/callback-handler.js +50 -0
- package/src/core/utils/debug-manager.js +40 -0
- package/src/core/utils/events.js +148 -0
- package/src/core/utils/filters/date.js +178 -0
- package/src/core/utils/filters/index.js +4 -0
- package/src/core/utils/filters/standard.js +263 -0
- package/src/core/utils/filters/strings.js +67 -0
- package/src/core/utils/filters/utils.js +77 -0
- package/src/core/utils/formatters.js +90 -0
- package/src/core/utils/helpers.js +144 -0
- package/src/core/utils/localstorage.js +57 -0
- package/src/core/utils/memoize.js +115 -0
- package/src/core/utils/plugins-manager.js +81 -0
- package/src/core/utils/property-accumulator.js +72 -0
- package/src/core/utils/prototypes.js +44 -0
- package/src/core/utils/shortcut-manager.js +242 -0
- package/src/{utils → core/utils}/validator.js +58 -22
- package/src/core/wrappers/AttributesWrapper.js +98 -0
- package/src/core/wrappers/DocumentObserver.js +182 -0
- package/src/core/wrappers/ElementCreator.js +120 -0
- package/src/core/wrappers/HtmlElementWrapper.js +98 -0
- package/src/core/wrappers/NDElement.js +613 -0
- package/src/core/wrappers/NdPrototype.js +233 -0
- package/src/core/wrappers/SingletonView.js +99 -0
- package/src/core/wrappers/SvgElementWrapper.js +15 -0
- package/src/core/wrappers/TemplateBinding.js +7 -0
- package/src/core/wrappers/constants.js +66 -0
- package/src/core/wrappers/prototypes/attributes-extensions.js +179 -0
- package/src/core/wrappers/prototypes/bind-class-extensions.js +0 -0
- package/src/core/wrappers/prototypes/nd-element-extensions.js +157 -0
- package/src/core/wrappers/prototypes/nd-element.transition.extensions.js +127 -0
- package/src/core/wrappers/template-cloner/NodeCloner.js +209 -0
- package/src/core/wrappers/template-cloner/TemplateCloner.js +192 -0
- package/src/core/wrappers/template-cloner/attributes-hydrator.js +142 -0
- package/src/core/wrappers/template-cloner/utils.js +173 -0
- package/src/fetch/NativeFetch.js +89 -0
- package/src/i18n/bin/scan.js +132 -0
- package/src/i18n/index.d.ts +2 -0
- package/src/i18n/service/I18nService.d.ts +27 -0
- package/src/i18n/service/I18nService.js +46 -0
- package/src/i18n/service/functions.d.ts +22 -0
- package/src/i18n/service/functions.js +29 -0
- package/src/router/Route.js +33 -8
- package/src/router/RouteGroupHelper.js +10 -2
- package/src/router/Router.js +63 -22
- package/src/router/RouterComponent.js +114 -6
- package/src/{errors → router/errors}/RouterError.js +0 -1
- package/src/router/link.js +9 -10
- package/src/router/modes/HashRouter.js +2 -2
- package/src/router/modes/HistoryRouter.js +2 -3
- package/src/router/modes/MemoryRouter.js +1 -1
- package/src/ui/components/accordion/AccordionItemRender.js +63 -0
- package/src/ui/components/accordion/AccordionRender.js +35 -0
- package/src/ui/components/accordion/accordion.css +121 -0
- package/src/ui/components/alert/AlertRender.js +81 -0
- package/src/ui/components/alert/alert.css +163 -0
- package/src/ui/components/avatar/avata-group/AvatarGroupRender.js +50 -0
- package/src/ui/components/avatar/avata-group/avatar-group.css +38 -0
- package/src/ui/components/avatar/avatar/AvatarRender.js +87 -0
- package/src/ui/components/avatar/avatar/avatar.css +189 -0
- package/src/ui/components/badge/BadgeRender.js +25 -0
- package/src/ui/components/badge/badge.css +168 -0
- package/src/ui/components/breadcrumb/BreadcrumbRender.js +44 -0
- package/src/ui/components/breadcrumb/breadcrumb.css +55 -0
- package/src/ui/components/button/ButtonRender.js +65 -0
- package/src/ui/components/button/button.css +296 -0
- package/src/ui/components/card/CardRender.js +133 -0
- package/src/ui/components/card/card.css +169 -0
- package/src/ui/components/contextmenu/ContextmenuRender.js +68 -0
- package/src/ui/components/contextmenu/contextmenu.css +36 -0
- package/src/ui/components/divider/DividerRender.js +70 -0
- package/src/ui/components/divider/divider.css +70 -0
- package/src/ui/components/dropdown/DropdownRender.js +92 -0
- package/src/ui/components/dropdown/divider/DropdownDividerRender.js +9 -0
- package/src/ui/components/dropdown/divider/dropdown-divider.css +0 -0
- package/src/ui/components/dropdown/dropdown.css +179 -0
- package/src/ui/components/dropdown/group/DropdownGroupRender.js +23 -0
- package/src/ui/components/dropdown/group/dropdown-group.css +0 -0
- package/src/ui/components/dropdown/item/DropdownItemRender.js +29 -0
- package/src/ui/components/dropdown/item/dropdown-item.css +0 -0
- package/src/ui/components/form/FieldCollectionRender.js +110 -0
- package/src/ui/components/form/FormControlRender.js +85 -0
- package/src/ui/components/form/field-collection.css +55 -0
- package/src/ui/components/form/fields/AutocompleteFieldRender.js +143 -0
- package/src/ui/components/form/fields/CheckboxFieldRender.js +59 -0
- package/src/ui/components/form/fields/CheckboxGroupFieldRender.js +92 -0
- package/src/ui/components/form/fields/ColorFieldRender.js +30 -0
- package/src/ui/components/form/fields/DateFieldRender.js +155 -0
- package/src/ui/components/form/fields/EmailFieldRender.js +5 -0
- package/src/ui/components/form/fields/FieldRender.js +118 -0
- package/src/ui/components/form/fields/FileFieldRender.js +41 -0
- package/src/ui/components/form/fields/HiddenFieldRender.js +13 -0
- package/src/ui/components/form/fields/ImageFieldRender.js +0 -0
- package/src/ui/components/form/fields/NumberFieldRender.js +52 -0
- package/src/ui/components/form/fields/PasswordFieldRender.js +65 -0
- package/src/ui/components/form/fields/RadioFieldRender.js +77 -0
- package/src/ui/components/form/fields/RangeFieldRender.js +122 -0
- package/src/ui/components/form/fields/SelectFieldRender.js +248 -0
- package/src/ui/components/form/fields/SliderFieldRender.js +359 -0
- package/src/ui/components/form/fields/StringFieldRender.js +6 -0
- package/src/ui/components/form/fields/TelFieldRender.js +6 -0
- package/src/ui/components/form/fields/TextAreaFieldRender.js +96 -0
- package/src/ui/components/form/fields/TimeFieldRender.js +142 -0
- package/src/ui/components/form/fields/UrlFieldRender.js +6 -0
- package/src/ui/components/form/fields/date-field.css +32 -0
- package/src/ui/components/form/fields/field.css +402 -0
- package/src/ui/components/form/fields/file-field.css +79 -0
- package/src/ui/components/form/fields/password-field.css +50 -0
- package/src/ui/components/form/fields/range-field.css +120 -0
- package/src/ui/components/form/fields/slider.css +195 -0
- package/src/ui/components/form/file-upload-mode/FileAvatarModeRender.js +143 -0
- package/src/ui/components/form/file-upload-mode/FileDropzoneModeRender.js +108 -0
- package/src/ui/components/form/file-upload-mode/FileNativeModeRender.js +22 -0
- package/src/ui/components/form/file-upload-mode/FileUploadButtonModeRender.js +89 -0
- package/src/ui/components/form/file-upload-mode/FileWallModeRender.js +90 -0
- package/src/ui/components/form/file-upload-mode/file-avatar-mode.css +139 -0
- package/src/ui/components/form/file-upload-mode/file-dropzone-mode.css +88 -0
- package/src/ui/components/form/file-upload-mode/file-upload-button-mode.css +44 -0
- package/src/ui/components/form/file-upload-mode/file-wall-mode.css +88 -0
- package/src/ui/components/form/form-control.css +40 -0
- package/src/ui/components/form/helpers.js +111 -0
- package/src/ui/components/form/index.js +27 -0
- 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 +12 -0
- package/src/ui/components/menu/MenuGroupRender.js +59 -0
- package/src/ui/components/menu/MenuItemRender.js +57 -0
- package/src/ui/components/menu/MenuLinkRender.js +55 -0
- package/src/ui/components/menu/MenuRender.js +22 -0
- package/src/ui/components/menu/helpers.js +121 -0
- package/src/ui/components/menu/menu.css +308 -0
- package/src/ui/components/modal/ModalRender.js +118 -0
- package/src/ui/components/modal/modal.css +156 -0
- package/src/ui/components/pagination/PaginationRender.js +112 -0
- package/src/ui/components/pagination/pagination.css +63 -0
- package/src/ui/components/popover/PopoverRender.js +233 -0
- package/src/ui/components/popover/popover.css +139 -0
- package/src/ui/components/progress/ProgressRender.js +168 -0
- package/src/ui/components/progress/progress.css +197 -0
- package/src/ui/components/skeleton/SkeletonRender.js +136 -0
- package/src/ui/components/skeleton/skeleton.css +154 -0
- package/src/ui/components/spacer/SpacerRender.js +10 -0
- package/src/ui/components/spinner/SpinnerRender.js +47 -0
- package/src/ui/components/spinner/spinner.css +152 -0
- package/src/ui/components/splitter/SplitterGutterRender.js +94 -0
- package/src/ui/components/splitter/SplitterPanelRender.js +38 -0
- package/src/ui/components/splitter/SplitterRender.js +75 -0
- package/src/ui/components/splitter/splitter.css +128 -0
- package/src/ui/components/stacks/PositionStackRender.js +39 -0
- package/src/ui/components/stacks/StackRender.js +41 -0
- package/src/ui/components/stacks/absolute-stack/AbsoluteStackRender.js +5 -0
- package/src/ui/components/stacks/fixed-stack/FixedStackRender.js +5 -0
- package/src/ui/components/stacks/h-stack/HStackRender.js +7 -0
- package/src/ui/components/stacks/h-stack/h-stack.css +4 -0
- package/src/ui/components/stacks/index.js +5 -0
- package/src/ui/components/stacks/position-stack.css +62 -0
- package/src/ui/components/stacks/relative-stack/RelativeStackRender.js +7 -0
- package/src/ui/components/stacks/relative-stack/relative-stack.css +3 -0
- package/src/ui/components/stacks/stack.css +78 -0
- package/src/ui/components/stacks/v-stack/VStackRender.js +6 -0
- package/src/ui/components/stacks/v-stack/v-stack.css +4 -0
- package/src/ui/components/stepper/StepperRender.js +71 -0
- package/src/ui/components/stepper/StepperStepRender.js +67 -0
- package/src/ui/components/stepper/stepper.css +359 -0
- package/src/ui/components/switch/SwitchRender.js +83 -0
- package/src/ui/components/switch/switch.css +143 -0
- package/src/ui/components/table/data-table/DataTableRender.js +50 -0
- package/src/ui/components/table/data-table/bulk-actions.js +34 -0
- package/src/ui/components/table/data-table/data-table.css +246 -0
- package/src/ui/components/table/data-table/pagination.js +56 -0
- package/src/ui/components/table/data-table/tables.js +368 -0
- package/src/ui/components/table/data-table/toolbar.js +67 -0
- package/src/ui/components/table/simple-table/SimpleTableRender.js +203 -0
- package/src/ui/components/table/simple-table/simple-table.css +50 -0
- package/src/ui/components/tabs/TabsRender.js +226 -0
- package/src/ui/components/tabs/tabs.css +253 -0
- package/src/ui/components/toast/ToastRender.js +99 -0
- package/src/ui/components/toast/toast.css +201 -0
- package/src/ui/components/tooltip/TooltipRender.js +8 -0
- package/src/ui/components/tooltip/tooltip.css +113 -0
- package/src/ui/index.js +47 -0
- package/src/ui/theme.js +0 -0
- package/src/ui/theme.scss +1 -0
- package/src/ui/tokens/animation.scss +36 -0
- package/src/ui/tokens/colors-dark.scss +58 -0
- package/src/ui/tokens/colors.scss +54 -0
- package/src/ui/tokens/components.scss +32 -0
- package/src/ui/tokens/fonts.scss +57 -0
- package/src/ui/tokens/glass.scss +10 -0
- package/src/ui/tokens/index.scss +38 -0
- package/src/ui/tokens/layouts.scss +228 -0
- package/src/ui/tokens/opacity.scss +21 -0
- package/src/ui/tokens/others.scss +11 -0
- package/src/ui/tokens/radius.scss +6 -0
- package/src/ui/tokens/reset.scss +51 -0
- package/src/ui/tokens/shadows.scss +29 -0
- package/src/ui/tokens/spacings.scss +13 -0
- package/src/ui/tokens/vars.scss +35 -0
- package/src/ui/tokens/viewports.scss +30 -0
- package/types/args-types.d.ts +58 -0
- package/types/control-flow.d.ts +62 -0
- package/types/elements.d.ts +231 -0
- package/types/filters/dates.d.ts +43 -0
- package/types/filters/index.d.ts +4 -0
- package/types/filters/standard.d.ts +70 -0
- package/types/filters/strings.d.ts +21 -0
- package/types/filters/types.d.ts +20 -0
- package/types/forms.d.ts +84 -0
- package/types/globals.d.ts +543 -0
- package/types/images.d.ts +23 -0
- package/types/localStorage.ts +102 -0
- package/types/memoize.d.ts +26 -0
- package/types/native-fetch.d.ts +72 -0
- package/types/nd-element.d.ts +407 -0
- package/types/observable-resource.d.ts +3 -0
- package/types/observable.d.ts +227 -0
- package/types/plugins-manager.d.ts +65 -0
- package/types/polyfill.d.ts +18 -0
- package/types/property-accumulator.d.ts +33 -0
- package/types/router.d.ts +85 -0
- package/types/service.d.ts +23 -0
- package/types/singleton.d.ts +19 -0
- package/types/store.d.ts +63 -0
- package/types/template-cloner.ts +43 -0
- package/types/validator.ts +66 -0
- package/ui.js +1 -0
- package/utils.d.ts +4 -0
- package/utils.js +12 -0
- package/src/data/Observable.js +0 -55
- package/src/data/ObservableChecker.js +0 -39
- package/src/data/ObservableItem.js +0 -195
- package/src/data/Store.js +0 -74
- package/src/data/observable-helpers/array.js +0 -74
- package/src/data/observable-helpers/batch.js +0 -22
- package/src/data/observable-helpers/computed.js +0 -28
- package/src/data/observable-helpers/object.js +0 -111
- package/src/elements/anchor.js +0 -129
- package/src/elements/content-formatter.js +0 -32
- package/src/elements/control/for-each-array.js +0 -280
- package/src/elements/control/show-if.js +0 -79
- package/src/elements/control/switch.js +0 -98
- package/src/elements/description-list.js +0 -5
- package/src/elements/form.js +0 -71
- package/src/elements/html5-semantics.js +0 -12
- package/src/elements/img.js +0 -45
- package/src/elements/interactive.js +0 -7
- package/src/elements/list.js +0 -10
- package/src/elements/medias.js +0 -8
- package/src/elements/meta-data.js +0 -9
- package/src/elements/table.js +0 -14
- package/src/utils/args-types.js +0 -100
- package/src/utils/debug-manager.js +0 -31
- package/src/utils/helpers.js +0 -60
- package/src/utils/plugins-manager.js +0 -12
- package/src/utils/prototypes.js +0 -45
- package/src/wrappers/AttributesWrapper.js +0 -144
- package/src/wrappers/DocumentObserver.js +0 -80
- package/src/wrappers/ElementCreator.js +0 -114
- package/src/wrappers/HtmlElementEventsWrapper.js +0 -64
- package/src/wrappers/HtmlElementWrapper.js +0 -50
- package/src/wrappers/NdPrototype.js +0 -109
- package/src/wrappers/constants.js +0 -2
package/docs/observables.md
CHANGED
|
@@ -1,546 +1,879 @@
|
|
|
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
|
-
|
|
9
|
-
const
|
|
10
|
-
const message = Observable("Hello World");
|
|
13
|
+
const count = Observable(0);
|
|
14
|
+
const message = Observable('Hello World');
|
|
11
15
|
const isVisible = Observable(true);
|
|
12
16
|
```
|
|
13
17
|
|
|
14
18
|
## Reading and Modifying Values
|
|
15
19
|
|
|
16
20
|
```javascript
|
|
17
|
-
const name = Observable(
|
|
21
|
+
const name = Observable('John');
|
|
18
22
|
|
|
19
23
|
// Read the current value
|
|
20
|
-
console.log(name.val());
|
|
21
|
-
|
|
22
|
-
// Using the proxy syntax (shorthand)
|
|
23
|
-
console.log(name.$value); // "John"
|
|
24
|
+
console.log(name.val()); // "John"
|
|
25
|
+
console.log(name.$value); // "John" - proxy shorthand
|
|
24
26
|
|
|
25
27
|
// Update the value
|
|
26
|
-
name.set(
|
|
27
|
-
console.log(name.val());
|
|
28
|
+
name.set('Jane');
|
|
29
|
+
console.log(name.val()); // "Jane"
|
|
28
30
|
|
|
29
|
-
// Update
|
|
30
|
-
name.$value =
|
|
31
|
-
console.log(name.val());
|
|
31
|
+
// Update with proxy syntax
|
|
32
|
+
name.$value = 'Bob';
|
|
33
|
+
console.log(name.val()); // "Bob"
|
|
32
34
|
|
|
33
35
|
// Update with a function
|
|
34
|
-
name.set(
|
|
35
|
-
console.log(name.val());
|
|
36
|
+
name.set(current => current.toUpperCase());
|
|
37
|
+
console.log(name.val()); // "BOB"
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
## Listening to Changes
|
|
39
41
|
|
|
42
|
+
`.subscribe()` runs on every value change, receiving the new and old values:
|
|
43
|
+
|
|
40
44
|
```javascript
|
|
41
45
|
const counter = Observable(0);
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.log("Counter is now:", newValue);
|
|
47
|
+
counter.subscribe((newValue, oldValue) => {
|
|
48
|
+
console.log(`Counter: ${oldValue} -> ${newValue}`);
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
//
|
|
49
|
-
counter.set(
|
|
50
|
-
counter.set(2); // Logs: "Counter is now: 2"
|
|
51
|
+
counter.set(1); // "Counter: 0 -> 1"
|
|
52
|
+
counter.set(2); // "Counter: 1 -> 2"
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
##
|
|
55
|
+
## Value-Specific Watchers - `.on()`
|
|
54
56
|
|
|
55
|
-
|
|
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:
|
|
56
58
|
|
|
57
59
|
```javascript
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const status = Observable('idle');
|
|
61
|
+
|
|
62
|
+
status.on('loading', (isActive) => {
|
|
63
|
+
console.log(`Loading state: ${isActive}`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
status.on('success', (isActive) => {
|
|
67
|
+
console.log(`Success state: ${isActive}`);
|
|
62
68
|
});
|
|
63
69
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
status.set('loading'); // "Loading state: true"
|
|
71
|
+
status.set('success'); // "Loading state: false", "Success state: true"
|
|
72
|
+
status.set('idle'); // "Success state: false"
|
|
73
|
+
```
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 |
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
## `.when()` - Lightweight Conditional
|
|
83
|
+
|
|
84
|
+
`.when()` creates a lightweight transitive object (no new observable) - ideal for CSS class binding:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
const status = Observable('loading');
|
|
88
|
+
|
|
89
|
+
const element = Div({
|
|
90
|
+
class: {
|
|
91
|
+
'spinner': status.when('loading'),
|
|
92
|
+
'success': status.when('success'),
|
|
93
|
+
'error': status.when('error')
|
|
94
|
+
}
|
|
76
95
|
});
|
|
96
|
+
```
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
## Observable Checkers
|
|
99
|
+
|
|
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:
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
const age = Observable(17);
|
|
104
|
+
|
|
105
|
+
const isAdult = age.check(value => value >= 18);
|
|
106
|
+
console.log(isAdult.val()); // false
|
|
107
|
+
|
|
108
|
+
age.set(20);
|
|
109
|
+
console.log(isAdult.val()); // true
|
|
81
110
|
```
|
|
82
111
|
|
|
83
|
-
|
|
112
|
+
| Alias | Best suited for |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `.check(fn)` | General conditions |
|
|
115
|
+
| `.is(fn \| value)` | Boolean / state checks |
|
|
116
|
+
| `.select(fn)` | Extracting a field |
|
|
117
|
+
| `.pluck(property)` | Extracting a field (Lodash-style) |
|
|
118
|
+
| `.transform(fn)` | Explicit value transformation |
|
|
119
|
+
|
|
84
120
|
```javascript
|
|
85
|
-
|
|
86
|
-
|
|
121
|
+
ShowIf(user.is(u => u.isAdmin), AdminPanel)
|
|
122
|
+
|
|
123
|
+
const email = user.select(u => u.email);
|
|
124
|
+
const label = status.transform(s => s.toUpperCase());
|
|
125
|
+
const name = user.pluck('name');
|
|
87
126
|
```
|
|
88
127
|
|
|
89
|
-
##
|
|
128
|
+
## Utility Methods
|
|
129
|
+
|
|
130
|
+
### `toggle()` - Boolean toggle
|
|
90
131
|
|
|
91
132
|
```javascript
|
|
92
|
-
const
|
|
93
|
-
name: "Alice",
|
|
94
|
-
age: 25,
|
|
95
|
-
email: "alice@example.com"
|
|
96
|
-
});
|
|
133
|
+
const isVisible = Observable(false);
|
|
97
134
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.log(user.name.$value); // "Alice" (proxy syntax)
|
|
135
|
+
isVisible.toggle(); // true
|
|
136
|
+
isVisible.toggle(); // false
|
|
101
137
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
user.age.$value = 30; // Using proxy syntax
|
|
138
|
+
Button('Toggle').nd.onClick(() => isVisible.toggle());
|
|
139
|
+
```
|
|
105
140
|
|
|
106
|
-
|
|
107
|
-
console.log(user.$value); // { name: "Bob", age: 30, email: "alice@example.com" }
|
|
108
|
-
console.log(Observable.value(user)); // Same as above
|
|
141
|
+
### `equals()` / `toBool()`
|
|
109
142
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log("New name:", newName);
|
|
113
|
-
});
|
|
143
|
+
```javascript
|
|
144
|
+
const num = Observable(5);
|
|
114
145
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
146
|
+
console.log(num.equals(5)); // true
|
|
147
|
+
console.log(num.equals(Observable(5))); // true
|
|
148
|
+
|
|
149
|
+
const text = Observable('');
|
|
150
|
+
console.log(text.toBool()); // false
|
|
151
|
+
text.set('Hello');
|
|
152
|
+
console.log(text.toBool()); // true
|
|
120
153
|
```
|
|
121
154
|
|
|
155
|
+
### Convenience checkers - `is...()`
|
|
156
|
+
|
|
157
|
+
All `is...()` methods return an `ObservableChecker<boolean>` and accept an observable or a plain value where noted:
|
|
158
|
+
|
|
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
|
+
|
|
122
180
|
```javascript
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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'))
|
|
197
|
+
```
|
|
127
198
|
|
|
128
|
-
|
|
129
|
-
todos.push("Clean house");
|
|
199
|
+
### Convenience transformers - `to...()`
|
|
130
200
|
|
|
131
|
-
|
|
132
|
-
|
|
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) |
|
|
133
214
|
|
|
134
|
-
|
|
135
|
-
const
|
|
215
|
+
```javascript
|
|
216
|
+
const name = Observable(' Alice ');
|
|
217
|
+
const score = Observable(72);
|
|
218
|
+
const progress = Observable(350);
|
|
219
|
+
const user = Observable({ address: { city: 'Paris' } });
|
|
220
|
+
|
|
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"
|
|
227
|
+
|
|
228
|
+
name.toLiteral('Hello ${v}!') // "Hello Alice !"
|
|
229
|
+
name.toTrimmed().toLiteral('Hello ${v}!') // "Hello Alice!"
|
|
230
|
+
|
|
231
|
+
name.toLiteral('Hello [name]!', '[name]') // custom placeholder
|
|
136
232
|
```
|
|
137
233
|
|
|
138
|
-
|
|
234
|
+
### `reset()` - Reset to initial value
|
|
139
235
|
|
|
140
|
-
|
|
236
|
+
```javascript
|
|
237
|
+
const name = Observable('Alice', { reset: true });
|
|
238
|
+
|
|
239
|
+
name.set('Bob');
|
|
240
|
+
name.reset();
|
|
241
|
+
console.log(name.val()); // "Alice"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `intercept(callback)` - Transform or abort before setting
|
|
245
|
+
|
|
246
|
+
The interceptor runs before every `.set()` call. Return a new value to replace it, or return `undefined` to abort the assignment entirely:
|
|
141
247
|
|
|
142
248
|
```javascript
|
|
143
|
-
const
|
|
144
|
-
const lastName = Observable("Doe");
|
|
249
|
+
const age = Observable(0);
|
|
145
250
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
251
|
+
age.intercept((newValue, currentValue) => {
|
|
252
|
+
if (newValue < 0) return 0; // clamp minimum
|
|
253
|
+
if (newValue > 120) return 120; // clamp maximum
|
|
254
|
+
return newValue;
|
|
255
|
+
});
|
|
150
256
|
|
|
151
|
-
|
|
257
|
+
age.set(-5); // sets 0
|
|
258
|
+
age.set(150); // sets 120
|
|
259
|
+
age.set(25); // sets 25
|
|
152
260
|
|
|
153
|
-
|
|
154
|
-
|
|
261
|
+
// Abort example - reject the value without changing anything
|
|
262
|
+
const username = Observable('alice');
|
|
263
|
+
|
|
264
|
+
username.intercept((newValue, currentValue) => {
|
|
265
|
+
if (newValue.trim() === '') return undefined; // abort - keeps 'alice'
|
|
266
|
+
return newValue.toLowerCase().trim();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
username.set(''); // aborted - still 'alice'
|
|
270
|
+
username.set(' Bob '); // sets 'bob'
|
|
155
271
|
```
|
|
156
272
|
|
|
157
|
-
|
|
273
|
+
### `interceptMutations(callback)` - Intercept array/object mutations
|
|
274
|
+
|
|
275
|
+
Intercepts mutations (push, splice, etc.) on arrays and objects separately from `.intercept()`:
|
|
158
276
|
|
|
159
277
|
```javascript
|
|
160
|
-
const
|
|
278
|
+
const items = Observable.array([]);
|
|
161
279
|
|
|
162
|
-
|
|
163
|
-
|
|
280
|
+
items.interceptMutations((operations) => {
|
|
281
|
+
console.log('Mutation:', operations);
|
|
282
|
+
});
|
|
164
283
|
|
|
165
|
-
//
|
|
166
|
-
const app = Div({ class: "counter" }, [
|
|
167
|
-
Button("-").nd.onClick(decrement),
|
|
168
|
-
Span({ class: "count" }, count), // Automatic display
|
|
169
|
-
Button("+").nd.onClick(increment)
|
|
170
|
-
]);
|
|
284
|
+
items.push('apple'); // logs mutation details
|
|
171
285
|
```
|
|
172
286
|
|
|
173
|
-
|
|
287
|
+
### `once(predicate, callback)` - Single-time listener
|
|
174
288
|
|
|
175
|
-
|
|
289
|
+
```javascript
|
|
290
|
+
const count = Observable(0);
|
|
291
|
+
|
|
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
|
|
294
|
+
|
|
295
|
+
count.set(5); // "Reached 5!"
|
|
296
|
+
count.set(5); // nothing - already fired
|
|
297
|
+
```
|
|
176
298
|
|
|
177
|
-
###
|
|
299
|
+
### `off(value, callback?)` - Remove watchers
|
|
178
300
|
|
|
179
301
|
```javascript
|
|
180
|
-
const
|
|
181
|
-
const
|
|
302
|
+
const status = Observable('idle');
|
|
303
|
+
const handler = (isActive) => console.log('Loading:', isActive);
|
|
304
|
+
|
|
305
|
+
status.on('loading', handler);
|
|
306
|
+
|
|
307
|
+
status.off('loading', handler); // remove specific callback
|
|
308
|
+
status.off('loading'); // remove all watchers for this value
|
|
309
|
+
```
|
|
182
310
|
|
|
183
|
-
|
|
184
|
-
name.subscribe(value => console.log("Name changed to:", value));
|
|
185
|
-
age.subscribe(value => console.log("Age changed to:", value));
|
|
311
|
+
### `onCleanup(callback)` - Register cleanup callback
|
|
186
312
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
313
|
+
Registers a callback that runs when the observable is cleaned up:
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
const data = Observable('test');
|
|
317
|
+
|
|
318
|
+
data.onCleanup(() => {
|
|
319
|
+
console.log('Observable cleaned up');
|
|
190
320
|
});
|
|
191
321
|
|
|
192
|
-
|
|
322
|
+
data.cleanup(); // triggers the cleanup callback
|
|
193
323
|
```
|
|
194
324
|
|
|
195
|
-
###
|
|
325
|
+
### `persist(key, options?)` - Bind to localStorage
|
|
196
326
|
|
|
197
|
-
|
|
327
|
+
Automatically saves on every change and restores on load:
|
|
198
328
|
|
|
199
329
|
```javascript
|
|
200
|
-
const
|
|
201
|
-
|
|
330
|
+
const theme = Observable('light').persist('theme');
|
|
331
|
+
theme.set('dark'); // saved to localStorage
|
|
202
332
|
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
333
|
+
// On next page load
|
|
334
|
+
const theme = Observable('light').persist('theme');
|
|
335
|
+
theme.val(); // "dark" - restored
|
|
336
|
+
```
|
|
206
337
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
338
|
+
With transform options:
|
|
339
|
+
|
|
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
|
|
211
344
|
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### `clone()` - Create a copy
|
|
212
348
|
|
|
213
|
-
|
|
214
|
-
const fullName = Observable.computed(() => {
|
|
215
|
-
return `${firstName.val()} ${lastName.val()}`;
|
|
216
|
-
}, updateName); // ← Depends on the batch function
|
|
349
|
+
Creates a deep clone of the observable's current value:
|
|
217
350
|
|
|
218
|
-
|
|
351
|
+
```javascript
|
|
352
|
+
const original = Observable({ name: 'Alice', scores: [1, 2, 3] });
|
|
353
|
+
const copy = original.clone();
|
|
219
354
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// Logs:
|
|
223
|
-
// "First name: Alice" ← immediate notification
|
|
224
|
-
// "Last name: Smith" ← immediate notification
|
|
225
|
-
// "Full name: Alice Smith" ← single notification at the end
|
|
355
|
+
copy.set({ ...copy.val(), name: 'Bob' });
|
|
356
|
+
original.val().name; // still "Alice"
|
|
226
357
|
```
|
|
227
358
|
|
|
228
|
-
###
|
|
359
|
+
### `resolve()` - Extract plain value
|
|
360
|
+
|
|
361
|
+
Recursively extracts plain values from observables, arrays, and proxies:
|
|
229
362
|
|
|
230
363
|
```javascript
|
|
231
|
-
const
|
|
232
|
-
|
|
364
|
+
const name = Observable('Alice');
|
|
365
|
+
name.resolve(); // "Alice" - same as Observable.value(name)
|
|
233
366
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}, [score, lives]); // ← Depends on individual observables
|
|
367
|
+
const user = Observable.object({ name: Observable('Bob') });
|
|
368
|
+
Observable.value(user); // { name: 'Bob' } - deeply resolved
|
|
369
|
+
```
|
|
238
370
|
|
|
239
|
-
|
|
240
|
-
const updateGame = Observable.batch(() => {
|
|
241
|
-
score.set(score.val() + 100);
|
|
242
|
-
lives.set(lives.val() - 1);
|
|
243
|
-
});
|
|
371
|
+
### `trigger()` - Force update without value change
|
|
244
372
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
373
|
+
```javascript
|
|
374
|
+
const data = Observable('test');
|
|
375
|
+
data.trigger(); // notifies all subscribers without changing the value
|
|
376
|
+
```
|
|
248
377
|
|
|
249
|
-
|
|
250
|
-
score.set(100); // gameStatus1 recalculates
|
|
251
|
-
lives.set(2); // gameStatus1 recalculates again
|
|
378
|
+
### `cleanup()` - Remove all listeners
|
|
252
379
|
|
|
253
|
-
|
|
254
|
-
|
|
380
|
+
```javascript
|
|
381
|
+
const data = Observable('test');
|
|
382
|
+
data.cleanup(); // removes all listeners and prevents new subscriptions
|
|
255
383
|
```
|
|
256
384
|
|
|
257
|
-
###
|
|
385
|
+
### `deepSubscribe(callback)` - Deep change detection
|
|
386
|
+
|
|
387
|
+
Reacts to changes at any depth - array mutations and property changes on nested observables:
|
|
258
388
|
|
|
259
389
|
```javascript
|
|
260
|
-
const
|
|
261
|
-
const discount = Observable(0);
|
|
262
|
-
const shippingCost = Observable(0);
|
|
390
|
+
const tags = Observable.array([{ label: Observable('admin') }]);
|
|
263
391
|
|
|
264
|
-
|
|
265
|
-
items.subscribe(items => {
|
|
266
|
-
console.log('Items count : '+items.length);
|
|
267
|
-
});
|
|
392
|
+
const unsub = tags.deepSubscribe(value => console.log('changed:', value));
|
|
268
393
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
394
|
+
tags.push({ label: Observable('editor') }); // triggers
|
|
395
|
+
tags.at(0).label.set('superadmin'); // triggers
|
|
396
|
+
tags.splice(0, 1); // triggers + cleans up listener
|
|
272
397
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
items.splice(0); // Clear current items
|
|
276
|
-
cartData.items.forEach(item => items.push(item));
|
|
277
|
-
discount.set(cartData.discount);
|
|
278
|
-
shippingCost.set(cartData.shipping);
|
|
279
|
-
});
|
|
398
|
+
unsub(); // manual cleanup
|
|
399
|
+
```
|
|
280
400
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
],
|
|
294
|
-
discount: 10,
|
|
295
|
-
shipping: 5.99
|
|
296
|
-
});
|
|
297
|
-
// Individual subscribers fire immediately, cartTotal calculates once at the end
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Observable Static Methods
|
|
404
|
+
|
|
405
|
+
### `Observable.setLocale(locale)`
|
|
406
|
+
|
|
407
|
+
Sets the locale observable used by `.format()`. Accepts an observable or a plain string:
|
|
408
|
+
|
|
409
|
+
```javascript
|
|
410
|
+
Observable.setLocale(I18nService.current); // observable (recommended)
|
|
411
|
+
Observable.setLocale(Observable('fr')); // plain observable
|
|
412
|
+
Observable.setLocale('fr'); // plain string - wrapped automatically
|
|
298
413
|
```
|
|
299
414
|
|
|
300
|
-
###
|
|
415
|
+
### `Observable.value(data)`
|
|
301
416
|
|
|
302
|
-
|
|
417
|
+
Recursively extracts plain values from observables, arrays, and proxies:
|
|
303
418
|
|
|
304
419
|
```javascript
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
```
|
|
308
425
|
|
|
309
|
-
|
|
310
|
-
isLoading.subscribe(loading => {
|
|
311
|
-
console.log('Loading.....');
|
|
312
|
-
});
|
|
426
|
+
### `Observable.cleanup(observable)`
|
|
313
427
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
//
|
|
428
|
+
Static alias for `observable.cleanup()`:
|
|
429
|
+
|
|
430
|
+
```javascript
|
|
431
|
+
Observable.cleanup(myObservable);
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### `Observable.autoCleanup(enable, options?)`
|
|
435
|
+
|
|
436
|
+
Enables automatic cleanup on page unload and via a periodic interval:
|
|
437
|
+
|
|
438
|
+
```javascript
|
|
439
|
+
Observable.autoCleanup(true, {
|
|
440
|
+
interval: 60000, // run cleanup every 60s (default)
|
|
441
|
+
threshold: 100 // clean up when > 100 unreferenced observables (default)
|
|
328
442
|
});
|
|
443
|
+
```
|
|
329
444
|
|
|
330
|
-
|
|
331
|
-
const userDisplay = Observable.computed(() => {
|
|
332
|
-
if (isLoading.val()) return "Loading...";
|
|
333
|
-
if (error.val()) return `Error: ${error.val()}`;
|
|
334
|
-
if (userData.val()) return `Hello ${userData.val().name}`;
|
|
335
|
-
return "No user";
|
|
336
|
-
}, fetchUser); // ← Only updates when fetchUser() completes
|
|
445
|
+
### `Observable.getById(id)`
|
|
337
446
|
|
|
338
|
-
|
|
447
|
+
Retrieves a registered observable by its internal memory ID. Useful for debugging:
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
const obs = Observable(42);
|
|
451
|
+
const id = obs.toString(); // "{{#ObItem::(N)}}"
|
|
452
|
+
|
|
453
|
+
Observable.getById(N); // returns obs
|
|
339
454
|
```
|
|
340
455
|
|
|
341
|
-
###
|
|
456
|
+
### `Observable.useValueProperty(propertyName?)`
|
|
342
457
|
|
|
343
|
-
|
|
458
|
+
Defines a property name as an alias for `$value`. Default is `'value'`, but you can pass any name:
|
|
344
459
|
|
|
345
460
|
```javascript
|
|
346
|
-
|
|
347
|
-
|
|
461
|
+
Observable.useValueProperty(); // enables .value
|
|
462
|
+
Observable.useValueProperty('val'); // enables .val (overrides the method!)
|
|
463
|
+
Observable.useValueProperty('current'); // enables .current
|
|
348
464
|
|
|
349
|
-
const
|
|
350
|
-
user.name.set(profileData.name);
|
|
351
|
-
user.email.set(profileData.email);
|
|
352
|
-
settings.theme.set(profileData.theme);
|
|
353
|
-
settings.lang.set(profileData.lang);
|
|
354
|
-
});
|
|
465
|
+
const count = Observable(0);
|
|
355
466
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
settings: settings.$value,
|
|
361
|
-
lastUpdated: Date.now()
|
|
362
|
-
};
|
|
363
|
-
}, updateProfile); // ← Single batch function
|
|
467
|
+
// With default
|
|
468
|
+
Observable.useValueProperty();
|
|
469
|
+
count.value; // 0 - same as count.$value
|
|
470
|
+
count.value = 5; // same as count.$value = 5
|
|
364
471
|
|
|
365
|
-
//
|
|
366
|
-
|
|
472
|
+
// With custom name
|
|
473
|
+
Observable.useValueProperty('current');
|
|
474
|
+
count.current; // 0
|
|
475
|
+
count.current = 5; // sets to 5
|
|
367
476
|
```
|
|
368
477
|
|
|
369
|
-
|
|
478
|
+
> Be careful not to pick a name that conflicts with an existing method - for example `'val'` would shadow the `.val()` method.
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Observable Objects
|
|
483
|
+
|
|
484
|
+
### `Observable(object)` vs `Observable.object(object)`
|
|
370
485
|
|
|
371
486
|
```javascript
|
|
372
|
-
|
|
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 });
|
|
491
|
+
|
|
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
|
+
```
|
|
498
|
+
|
|
499
|
+
`Observable.object` has aliases:
|
|
373
500
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
501
|
+
```javascript
|
|
502
|
+
Observable.object(data) // same as:
|
|
503
|
+
Observable.json(data)
|
|
504
|
+
Observable.init(data)
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Subscribing to an object observable
|
|
508
|
+
|
|
509
|
+
Subscribing to `Observable.object()` reacts to any property change:
|
|
510
|
+
|
|
511
|
+
```javascript
|
|
512
|
+
const user = Observable.object({ name: 'Alice', age: 25 });
|
|
513
|
+
|
|
514
|
+
user.subscribe(value => {
|
|
515
|
+
console.log('User changed:', value);
|
|
387
516
|
});
|
|
388
517
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
.reduce((sum, total) => sum + total, 0);
|
|
395
|
-
}, batchUpdateItems); // ← Depends on batch function
|
|
518
|
+
user.name.set('Bob'); // triggers the parent subscribe
|
|
519
|
+
user.age.set(30); // triggers the parent subscribe
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
---
|
|
396
523
|
|
|
397
|
-
|
|
398
|
-
items.push({ active: true, price: 10, quantity: 2 }); // 🔄 Recalculating...
|
|
399
|
-
items.push({ active: true, price: 15, quantity: 1 }); // 🔄 Recalculating...
|
|
400
|
-
items.push({ active: false, price: 20, quantity: 3 }); // 🔄 Recalculating...
|
|
524
|
+
## Observable Arrays
|
|
401
525
|
|
|
402
|
-
|
|
403
|
-
|
|
526
|
+
```javascript
|
|
527
|
+
const todos = Observable.array(['Buy groceries', 'Call doctor']);
|
|
528
|
+
|
|
529
|
+
todos.push('Clean house');
|
|
530
|
+
todos.pop();
|
|
531
|
+
todos.splice(0, 1);
|
|
532
|
+
|
|
533
|
+
console.log(todos.val().length); // 1
|
|
404
534
|
```
|
|
405
535
|
|
|
406
|
-
###
|
|
536
|
+
### Array-specific methods
|
|
407
537
|
|
|
408
|
-
|
|
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` |
|
|
409
555
|
|
|
410
|
-
|
|
556
|
+
### Filtering with `.where()`
|
|
411
557
|
|
|
412
|
-
|
|
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.
|
|
559
|
+
|
|
560
|
+
**Function predicate** - full item passed to the callback:
|
|
561
|
+
|
|
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
|
+
]);
|
|
413
569
|
|
|
414
|
-
|
|
570
|
+
const adults = users.where(item => item.age >= 18);
|
|
571
|
+
// -> Bob, Carol, Dave
|
|
572
|
+
```
|
|
415
573
|
|
|
416
|
-
|
|
574
|
+
**Object predicate** - filter per field:
|
|
417
575
|
|
|
418
|
-
### State Machine with Batched Transitions
|
|
419
576
|
```javascript
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
lives: 3
|
|
577
|
+
const activeAdmins = users.where({
|
|
578
|
+
role: 'admin', // shorthand for equals
|
|
579
|
+
age: val => val >= 18 // plain function on the field value
|
|
424
580
|
});
|
|
581
|
+
// -> Bob, Dave
|
|
582
|
+
```
|
|
425
583
|
|
|
426
|
-
|
|
427
|
-
gameState.score.subscribe(score => updateScoreDisplay(score));
|
|
428
|
-
gameState.lives.subscribe(lives => updateLivesDisplay(lives));
|
|
584
|
+
**Reactive predicate** - re-filters when an observable changes:
|
|
429
585
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
586
|
+
```javascript
|
|
587
|
+
import { match } from 'native-document/filters';
|
|
588
|
+
|
|
589
|
+
const search = Observable('');
|
|
590
|
+
|
|
591
|
+
const results = users.where({
|
|
592
|
+
name: match(search) // re-filters every time search changes
|
|
435
593
|
});
|
|
436
594
|
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
const state = gameState.$value;
|
|
440
|
-
return `Level ${state.level}: ${state.score} points, ${state.lives} lives remaining`;
|
|
441
|
-
}, levelUp); // ← Only updates when levelUp() is called
|
|
595
|
+
search.set('ali'); // -> Alice
|
|
596
|
+
search.set(''); // -> all users
|
|
442
597
|
```
|
|
443
598
|
|
|
444
|
-
|
|
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.
|
|
445
602
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
- **Debugging**: Batching can make the flow harder to debug
|
|
449
|
-
- **Single observable changes**: No benefit when only one observable changes
|
|
603
|
+
```javascript
|
|
604
|
+
import { equals, greaterThan, lessThan, and, or, not } from 'native-document/filters';
|
|
450
605
|
|
|
451
|
-
|
|
606
|
+
// and - field must pass ALL conditions
|
|
607
|
+
const youngAdults = users.where({
|
|
608
|
+
age: and(greaterThan(18), lessThan(30))
|
|
609
|
+
});
|
|
610
|
+
// -> Bob (25), Dave (28)
|
|
452
611
|
|
|
453
|
-
|
|
612
|
+
// or - field must pass AT LEAST ONE condition
|
|
613
|
+
const adminOrEditor = users.where({
|
|
614
|
+
role: or(equals('admin'), equals('editor'))
|
|
615
|
+
});
|
|
616
|
+
// -> Bob, Dave
|
|
454
617
|
|
|
455
|
-
|
|
618
|
+
// not - inverts a filter
|
|
619
|
+
const nonAdmins = users.where({
|
|
620
|
+
role: not(equals('admin'))
|
|
621
|
+
});
|
|
622
|
+
// -> Alice, Carol
|
|
456
623
|
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
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)
|
|
629
|
+
```
|
|
460
630
|
|
|
461
|
-
|
|
462
|
-
const message = template.use({ name, age });
|
|
631
|
+
**`whereSome(fields, filter)`** - at least one field matches (OR across fields):
|
|
463
632
|
|
|
464
|
-
|
|
633
|
+
```javascript
|
|
634
|
+
import { match } from 'native-document/filters';
|
|
465
635
|
|
|
466
|
-
|
|
467
|
-
|
|
636
|
+
const search = Observable('car');
|
|
637
|
+
|
|
638
|
+
const results = products.whereSome(['name', 'category'], match(search));
|
|
639
|
+
// matches if name OR category contains 'car'
|
|
468
640
|
```
|
|
469
641
|
|
|
470
|
-
|
|
642
|
+
**`whereEvery(fields, filter)`** - all fields match (AND across fields):
|
|
471
643
|
|
|
472
644
|
```javascript
|
|
473
|
-
|
|
474
|
-
const user = Observable("Marie");
|
|
645
|
+
import { equals } from 'native-document/filters';
|
|
475
646
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// Updates when greeting or user changes
|
|
647
|
+
const verified = items.whereEvery(['status', 'verified'], equals('active'));
|
|
648
|
+
// matches only if both status AND verified equal 'active'
|
|
479
649
|
```
|
|
480
650
|
|
|
481
|
-
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
## Computed Observables
|
|
482
654
|
|
|
483
|
-
|
|
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**:
|
|
484
656
|
|
|
485
657
|
```javascript
|
|
486
|
-
const
|
|
487
|
-
const
|
|
658
|
+
const firstName = Observable('John');
|
|
659
|
+
const lastName = Observable('Doe');
|
|
488
660
|
|
|
489
|
-
|
|
661
|
+
const fullName = Observable.computed((first, last) => {
|
|
662
|
+
return `${first} ${last}`;
|
|
663
|
+
}, [firstName, lastName]);
|
|
490
664
|
|
|
491
|
-
|
|
492
|
-
|
|
665
|
+
console.log(fullName.val()); // "John Doe"
|
|
666
|
+
|
|
667
|
+
firstName.set('Jane');
|
|
668
|
+
console.log(fullName.val()); // "Jane Doe"
|
|
493
669
|
```
|
|
494
670
|
|
|
495
|
-
|
|
671
|
+
Another example with more dependencies:
|
|
496
672
|
|
|
497
673
|
```javascript
|
|
498
|
-
const
|
|
674
|
+
const price = Observable(100);
|
|
675
|
+
const quantity = Observable(2);
|
|
676
|
+
const tax = Observable(0.2);
|
|
677
|
+
|
|
678
|
+
const total = Observable.computed((p, q, t) => {
|
|
679
|
+
return p * q * (1 + t);
|
|
680
|
+
}, [price, quantity, tax]);
|
|
499
681
|
|
|
500
|
-
//
|
|
501
|
-
const unsubscribe = data.subscribe(value => console.log(value));
|
|
682
|
+
console.log(total.val()); // 240
|
|
502
683
|
|
|
503
|
-
|
|
504
|
-
|
|
684
|
+
quantity.set(3);
|
|
685
|
+
console.log(total.val()); // 360
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
---
|
|
505
689
|
|
|
506
|
-
|
|
507
|
-
data.cleanup(); // Removes all listeners and prevents new subscriptions
|
|
690
|
+
## Batching Updates
|
|
508
691
|
|
|
509
|
-
|
|
510
|
-
data.trigger(); // Notifies all subscribers without changing the value
|
|
692
|
+
`Observable.batch()` delays notifications to **dependent computed observables** until all changes are done. Individual subscribers still fire immediately.
|
|
511
693
|
|
|
512
|
-
|
|
513
|
-
|
|
694
|
+
```javascript
|
|
695
|
+
const firstName = Observable('John');
|
|
696
|
+
const lastName = Observable('Doe');
|
|
514
697
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
items: [1, 2, 3]
|
|
698
|
+
const updateName = Observable.batch((first, last) => {
|
|
699
|
+
firstName.set(first);
|
|
700
|
+
lastName.set(last);
|
|
519
701
|
});
|
|
520
|
-
|
|
702
|
+
|
|
703
|
+
const fullName = Observable.computed((first, last) => {
|
|
704
|
+
return `${first} ${last}`;
|
|
705
|
+
}, updateName); // depends on the batch - recalculates once
|
|
706
|
+
|
|
707
|
+
fullName.subscribe(name => console.log('Full name:', name));
|
|
708
|
+
|
|
709
|
+
updateName('Alice', 'Smith');
|
|
710
|
+
// "Full name: Alice Smith" - single notification
|
|
521
711
|
```
|
|
522
712
|
|
|
713
|
+
Async batching is also supported:
|
|
714
|
+
|
|
715
|
+
```javascript
|
|
716
|
+
const isLoading = Observable(false);
|
|
717
|
+
const userData = Observable(null);
|
|
718
|
+
const error = Observable(null);
|
|
719
|
+
|
|
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
|
+
```
|
|
742
|
+
|
|
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
|
+
|
|
751
|
+
```javascript
|
|
752
|
+
const userId = Observable(1);
|
|
753
|
+
|
|
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
|
+
]);
|
|
768
|
+
```
|
|
769
|
+
|
|
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
|
+
|
|
776
|
+
```javascript
|
|
777
|
+
import { Observable } from 'native-document';
|
|
778
|
+
import { I18nService } from '@/core/services';
|
|
779
|
+
|
|
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`)
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
Formatted observables react to locale changes automatically:
|
|
819
|
+
|
|
820
|
+
```javascript
|
|
821
|
+
Observable.setLocale(I18nService.current);
|
|
822
|
+
|
|
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` |
|
|
842
|
+
|
|
843
|
+
### Extending Formatters
|
|
844
|
+
|
|
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"
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
523
860
|
## Best Practices
|
|
524
861
|
|
|
525
|
-
1.
|
|
526
|
-
2.
|
|
527
|
-
3.
|
|
528
|
-
4.
|
|
529
|
-
5.
|
|
530
|
-
6.
|
|
531
|
-
7.
|
|
532
|
-
8.
|
|
533
|
-
9. **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
|
|
534
870
|
|
|
535
871
|
## Next Steps
|
|
536
872
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
- **[
|
|
540
|
-
- **[
|
|
541
|
-
- **[
|
|
542
|
-
- **[
|
|
543
|
-
- **[
|
|
544
|
-
- **[Lifecycle Events](lifecycle-events.md)** - Lifecycle events
|
|
545
|
-
- **[Memory Management](memory-management.md)** - Memory management
|
|
546
|
-
- **[Anchor](anchor.md)** - Anchor
|
|
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
|