mates 0.0.21 → 0.1.0-beta.1
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/README.md +1598 -257
- package/dist/Directives/$.d.ts +371 -0
- package/dist/Directives/$.d.ts.map +1 -0
- package/dist/Directives/animatedConditional.d.ts +67 -0
- package/dist/Directives/animatedConditional.d.ts.map +1 -0
- package/dist/Directives/animationClasses.d.ts +50 -0
- package/dist/Directives/animationClasses.d.ts.map +1 -0
- package/dist/Directives/attrDirective.d.ts +69 -0
- package/dist/Directives/attrDirective.d.ts.map +1 -0
- package/dist/Directives/classesDirective.d.ts +79 -0
- package/dist/Directives/classesDirective.d.ts.map +1 -0
- package/dist/Directives/eleHook.d.ts +84 -0
- package/dist/Directives/eleHook.d.ts.map +1 -0
- package/dist/Directives/eventBinding.d.ts +29 -0
- package/dist/Directives/eventBinding.d.ts.map +1 -0
- package/dist/Directives/htmlHook.d.ts +101 -0
- package/dist/Directives/htmlHook.d.ts.map +1 -0
- package/dist/Directives/index.d.ts +18 -0
- package/dist/Directives/index.d.ts.map +1 -0
- package/dist/Directives/intersectDirective.d.ts +67 -0
- package/dist/Directives/intersectDirective.d.ts.map +1 -0
- package/dist/Directives/lazyLoadDirective.d.ts +66 -0
- package/dist/Directives/lazyLoadDirective.d.ts.map +1 -0
- package/dist/Directives/lifecycleDirectives.d.ts +56 -0
- package/dist/Directives/lifecycleDirectives.d.ts.map +1 -0
- package/dist/Directives/masonryGrid.d.ts +114 -0
- package/dist/Directives/masonryGrid.d.ts.map +1 -0
- package/dist/Directives/memoTemplate.d.ts +94 -0
- package/dist/Directives/memoTemplate.d.ts.map +1 -0
- package/dist/Directives/onDirective.d.ts +71 -0
- package/dist/Directives/onDirective.d.ts.map +1 -0
- package/dist/Directives/onParentDirective.d.ts +34 -0
- package/dist/Directives/onParentDirective.d.ts.map +1 -0
- package/dist/Directives/renderSwitch.d.ts +55 -0
- package/dist/Directives/renderSwitch.d.ts.map +1 -0
- package/dist/Directives/resolveAttrValue.d.ts +35 -0
- package/dist/Directives/resolveAttrValue.d.ts.map +1 -0
- package/dist/Directives/styleDirective.d.ts +81 -0
- package/dist/Directives/styleDirective.d.ts.map +1 -0
- package/dist/Directives/timerDirective.d.ts +52 -0
- package/dist/Directives/timerDirective.d.ts.map +1 -0
- package/dist/Directives/virtualHelpers.d.ts +10 -0
- package/dist/Directives/virtualHelpers.d.ts.map +1 -0
- package/dist/Fetch/ErrorTypes.d.ts +74 -0
- package/dist/Fetch/ErrorTypes.d.ts.map +1 -0
- package/dist/Fetch/Fetch.d.ts +168 -0
- package/dist/Fetch/Fetch.d.ts.map +1 -0
- package/dist/Fetch/index.d.ts +3 -0
- package/dist/Fetch/index.d.ts.map +1 -0
- package/dist/Mutables/Extended Atoms/cacheAtom.d.ts +156 -0
- package/dist/Mutables/Extended Atoms/cacheAtom.d.ts.map +1 -0
- package/dist/Mutables/Extended Atoms/index.d.ts +4 -0
- package/dist/Mutables/Extended Atoms/index.d.ts.map +1 -0
- package/dist/Mutables/Extended Atoms/paginationAtom.d.ts +32 -0
- package/dist/Mutables/Extended Atoms/paginationAtom.d.ts.map +1 -0
- package/dist/Mutables/Extended Atoms/themeAtom.d.ts +59 -0
- package/dist/Mutables/Extended Atoms/themeAtom.d.ts.map +1 -0
- package/dist/Mutables/atom/atom.d.ts +148 -0
- package/dist/Mutables/atom/atom.d.ts.map +1 -0
- package/dist/Mutables/atom/createTimedAtom.d.ts +43 -0
- package/dist/Mutables/atom/createTimedAtom.d.ts.map +1 -0
- package/dist/Mutables/atom/debouncedAtom.d.ts +37 -0
- package/dist/Mutables/atom/debouncedAtom.d.ts.map +1 -0
- package/dist/Mutables/atom/index.d.ts +10 -0
- package/dist/Mutables/atom/index.d.ts.map +1 -0
- package/dist/Mutables/atom/mapAtom.d.ts +105 -0
- package/dist/Mutables/atom/mapAtom.d.ts.map +1 -0
- package/dist/Mutables/atom/setAtom.d.ts +107 -0
- package/dist/Mutables/atom/setAtom.d.ts.map +1 -0
- package/dist/Mutables/atom/storageAtom.d.ts +44 -0
- package/dist/Mutables/atom/storageAtom.d.ts.map +1 -0
- package/dist/Mutables/atom/throttledAtom.d.ts +37 -0
- package/dist/Mutables/atom/throttledAtom.d.ts.map +1 -0
- package/dist/Mutables/atom/titleAtom.d.ts +20 -0
- package/dist/Mutables/atom/titleAtom.d.ts.map +1 -0
- package/dist/Mutables/effect/effect.d.ts +31 -0
- package/dist/Mutables/effect/effect.d.ts.map +1 -0
- package/dist/Mutables/effect/index.d.ts +2 -0
- package/dist/Mutables/effect/index.d.ts.map +1 -0
- package/dist/Mutables/events/events.d.ts +123 -0
- package/dist/Mutables/events/events.d.ts.map +1 -0
- package/dist/Mutables/events/index.d.ts +2 -0
- package/dist/Mutables/events/index.d.ts.map +1 -0
- package/dist/Mutables/events/xTabEvent.d.ts +61 -0
- package/dist/Mutables/events/xTabEvent.d.ts.map +1 -0
- package/dist/Mutables/form/formAtom.d.ts +56 -0
- package/dist/Mutables/form/formAtom.d.ts.map +1 -0
- package/dist/Mutables/form/index.d.ts +5 -0
- package/dist/Mutables/form/index.d.ts.map +1 -0
- package/dist/Mutables/form/transformers.d.ts +4 -0
- package/dist/Mutables/form/transformers.d.ts.map +1 -0
- package/dist/Mutables/form/validateAll.d.ts +40 -0
- package/dist/Mutables/form/validateAll.d.ts.map +1 -0
- package/dist/Mutables/form/validators.d.ts +61 -0
- package/dist/Mutables/form/validators.d.ts.map +1 -0
- package/dist/Mutables/index.d.ts +13 -0
- package/dist/Mutables/index.d.ts.map +1 -0
- package/dist/Mutables/memo/index.d.ts +2 -0
- package/dist/Mutables/memo/index.d.ts.map +1 -0
- package/dist/Mutables/memo/memo.d.ts +47 -0
- package/dist/Mutables/memo/memo.d.ts.map +1 -0
- package/dist/Mutables/reactiveRunner/index.d.ts +4 -0
- package/dist/Mutables/reactiveRunner/index.d.ts.map +1 -0
- package/dist/Mutables/reactiveRunner/reactiveRunner.d.ts +56 -0
- package/dist/Mutables/reactiveRunner/reactiveRunner.d.ts.map +1 -0
- package/dist/Mutables/reactiveRunner/trackAndSubscribe.d.ts +6 -0
- package/dist/Mutables/reactiveRunner/trackAndSubscribe.d.ts.map +1 -0
- package/dist/Mutables/reactiveRunner/validateReactiveFunction.d.ts +6 -0
- package/dist/Mutables/reactiveRunner/validateReactiveFunction.d.ts.map +1 -0
- package/dist/Mutables/ref/index.d.ts +2 -0
- package/dist/Mutables/ref/index.d.ts.map +1 -0
- package/dist/Mutables/ref/ref.d.ts +74 -0
- package/dist/Mutables/ref/ref.d.ts.map +1 -0
- package/dist/Mutables/scope/index.d.ts +2 -0
- package/dist/Mutables/scope/index.d.ts.map +1 -0
- package/dist/Mutables/scope/scope.d.ts +60 -0
- package/dist/Mutables/scope/scope.d.ts.map +1 -0
- package/dist/Mutables/store/index.d.ts +2 -0
- package/dist/Mutables/store/index.d.ts.map +1 -0
- package/dist/Mutables/store/store.d.ts +73 -0
- package/dist/Mutables/store/store.d.ts.map +1 -0
- package/dist/Mutables/useState/index.d.ts +2 -0
- package/dist/Mutables/useState/index.d.ts.map +1 -0
- package/dist/Mutables/useState/useState.d.ts +54 -0
- package/dist/Mutables/useState/useState.d.ts.map +1 -0
- package/dist/Mutables/useStore/hostContext.d.ts +13 -0
- package/dist/Mutables/useStore/hostContext.d.ts.map +1 -0
- package/dist/Mutables/useStore/index.d.ts +9 -0
- package/dist/Mutables/useStore/index.d.ts.map +1 -0
- package/dist/Mutables/useStore/lifecycle.d.ts +94 -0
- package/dist/Mutables/useStore/lifecycle.d.ts.map +1 -0
- package/dist/Mutables/useStore/onError.d.ts +21 -0
- package/dist/Mutables/useStore/onError.d.ts.map +1 -0
- package/dist/Mutables/useStore/onReferenceChange.d.ts +2 -0
- package/dist/Mutables/useStore/onReferenceChange.d.ts.map +1 -0
- package/dist/Mutables/useStore/safetyCheck.d.ts +2 -0
- package/dist/Mutables/useStore/safetyCheck.d.ts.map +1 -0
- package/dist/Mutables/useStore/setter.d.ts +52 -0
- package/dist/Mutables/useStore/setter.d.ts.map +1 -0
- package/dist/Mutables/useStore/subscription.d.ts +50 -0
- package/dist/Mutables/useStore/subscription.d.ts.map +1 -0
- package/dist/Mutables/useStore/useContext.d.ts +3 -0
- package/dist/Mutables/useStore/useContext.d.ts.map +1 -0
- package/dist/Mutables/useStore/useStore.d.ts +7 -0
- package/dist/Mutables/useStore/useStore.d.ts.map +1 -0
- package/dist/Router/Router.d.ts +54 -0
- package/dist/Router/Router.d.ts.map +1 -0
- package/dist/Router/animatedRouter.d.ts +48 -0
- package/dist/Router/animatedRouter.d.ts.map +1 -0
- package/dist/Router/buildPath.d.ts +20 -0
- package/dist/Router/buildPath.d.ts.map +1 -0
- package/dist/Router/hashAtom.d.ts +12 -0
- package/dist/Router/hashAtom.d.ts.map +1 -0
- package/dist/Router/index.d.ts +11 -0
- package/dist/Router/index.d.ts.map +1 -0
- package/dist/Router/isPathMatching.d.ts +12 -0
- package/dist/Router/isPathMatching.d.ts.map +1 -0
- package/dist/Router/location.d.ts +74 -0
- package/dist/Router/location.d.ts.map +1 -0
- package/dist/Router/navigateTo.d.ts +31 -0
- package/dist/Router/navigateTo.d.ts.map +1 -0
- package/dist/Router/navigationLock.d.ts +50 -0
- package/dist/Router/navigationLock.d.ts.map +1 -0
- package/dist/Router/pathAtom.d.ts +42 -0
- package/dist/Router/pathAtom.d.ts.map +1 -0
- package/dist/Router/pathResolver.d.ts +9 -0
- package/dist/Router/pathResolver.d.ts.map +1 -0
- package/dist/Router/qsAtom.d.ts +12 -0
- package/dist/Router/qsAtom.d.ts.map +1 -0
- package/dist/Template/index.d.ts +6 -0
- package/dist/Template/index.d.ts.map +1 -0
- package/dist/Template/scheduler.d.ts +59 -0
- package/dist/Template/scheduler.d.ts.map +1 -0
- package/dist/Template/x-x.d.ts +60 -0
- package/dist/Template/x-x.d.ts.map +1 -0
- package/dist/Template/x-x.types.d.ts +91 -0
- package/dist/Template/x-x.types.d.ts.map +1 -0
- package/dist/Template/x-x.utils.d.ts +15 -0
- package/dist/Template/x-x.utils.d.ts.map +1 -0
- package/dist/Template/x.d.ts +5 -0
- package/dist/Template/x.d.ts.map +1 -0
- package/dist/{lib → Template}/xProvider.d.ts.map +1 -1
- package/dist/Timers/createManagedTimer.d.ts +12 -0
- package/dist/Timers/createManagedTimer.d.ts.map +1 -0
- package/dist/Timers/index.d.ts +4 -0
- package/dist/Timers/index.d.ts.map +1 -0
- package/dist/Timers/interval.d.ts +2 -0
- package/dist/Timers/interval.d.ts.map +1 -0
- package/dist/Timers/timeout.d.ts +2 -0
- package/dist/Timers/timeout.d.ts.map +1 -0
- package/dist/TrackState/componentStatus.d.ts +105 -0
- package/dist/TrackState/componentStatus.d.ts.map +1 -0
- package/dist/TrackState/index.d.ts +3 -0
- package/dist/TrackState/index.d.ts.map +1 -0
- package/dist/TrackState/readTracking.d.ts +20 -0
- package/dist/TrackState/readTracking.d.ts.map +1 -0
- package/dist/Utils/Context.d.ts.map +1 -0
- package/dist/Utils/Date/__tests__/shared.d.ts +13 -0
- package/dist/Utils/Date/__tests__/shared.d.ts.map +1 -0
- package/dist/Utils/Date/calendar.d.ts +30 -0
- package/dist/Utils/Date/calendar.d.ts.map +1 -0
- package/dist/Utils/Date/core.d.ts +29 -0
- package/dist/Utils/Date/core.d.ts.map +1 -0
- package/dist/Utils/Date/date.d.ts +60 -0
- package/dist/Utils/Date/date.d.ts.map +1 -0
- package/dist/Utils/Date/index.d.ts +3 -0
- package/dist/Utils/Date/index.d.ts.map +1 -0
- package/dist/Utils/deepClone.d.ts +7 -0
- package/dist/Utils/deepClone.d.ts.map +1 -0
- package/dist/Utils/deepFreeze.d.ts +2 -0
- package/dist/Utils/deepFreeze.d.ts.map +1 -0
- package/dist/Utils/getAllProps.d.ts +2 -0
- package/dist/Utils/getAllProps.d.ts.map +1 -0
- package/dist/Utils/helpers.d.ts +13 -0
- package/dist/Utils/helpers.d.ts.map +1 -0
- package/dist/Utils/index.d.ts +13 -0
- package/dist/Utils/index.d.ts.map +1 -0
- package/dist/Utils/is.d.ts +12 -0
- package/dist/Utils/is.d.ts.map +1 -0
- package/dist/Utils/isDefinedAsGetter.d.ts +2 -0
- package/dist/Utils/isDefinedAsGetter.d.ts.map +1 -0
- package/dist/Utils/logger.d.ts +22 -0
- package/dist/Utils/logger.d.ts.map +1 -0
- package/dist/Utils/notificationManager.d.ts +67 -0
- package/dist/Utils/notificationManager.d.ts.map +1 -0
- package/dist/Utils/registerCleanup.d.ts +7 -0
- package/dist/Utils/registerCleanup.d.ts.map +1 -0
- package/dist/Utils/svgIcon.d.ts +95 -0
- package/dist/Utils/svgIcon.d.ts.map +1 -0
- package/dist/Utils/uuid.d.ts +7 -0
- package/dist/Utils/uuid.d.ts.map +1 -0
- package/dist/ZeroPromise/index.d.ts +2 -0
- package/dist/ZeroPromise/index.d.ts.map +1 -0
- package/dist/ZeroPromise/zero.d.ts +23 -0
- package/dist/ZeroPromise/zero.d.ts.map +1 -0
- package/dist/actions/FetchAction.d.ts +2 -0
- package/dist/actions/FetchAction.d.ts.map +1 -0
- package/dist/actions/__benches__/asyncAction.bench.d.ts +24 -0
- package/dist/actions/__benches__/asyncAction.bench.d.ts.map +1 -0
- package/dist/actions/action.d.ts +14 -0
- package/dist/actions/action.d.ts.map +1 -0
- package/dist/actions/animatePresets.d.ts +38 -0
- package/dist/actions/animatePresets.d.ts.map +1 -0
- package/dist/actions/animateSwap.d.ts +79 -0
- package/dist/actions/animateSwap.d.ts.map +1 -0
- package/dist/actions/animateTypes.d.ts +103 -0
- package/dist/actions/animateTypes.d.ts.map +1 -0
- package/dist/actions/asyncAction.d.ts +4 -0
- package/dist/actions/asyncAction.d.ts.map +1 -0
- package/dist/actions/asyncActionCache.d.ts +16 -0
- package/dist/actions/asyncActionCache.d.ts.map +1 -0
- package/dist/actions/asyncActionPolling.d.ts +14 -0
- package/dist/actions/asyncActionPolling.d.ts.map +1 -0
- package/dist/actions/asyncActionTypes.d.ts +74 -0
- package/dist/actions/asyncActionTypes.d.ts.map +1 -0
- package/dist/actions/createTimingFunction.d.ts +23 -0
- package/dist/actions/createTimingFunction.d.ts.map +1 -0
- package/dist/actions/debounce.d.ts +10 -0
- package/dist/actions/debounce.d.ts.map +1 -0
- package/dist/actions/index.d.ts +12 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/paginatedAsyncAction.d.ts +121 -0
- package/dist/actions/paginatedAsyncAction.d.ts.map +1 -0
- package/dist/actions/taskAction.d.ts +105 -0
- package/dist/actions/taskAction.d.ts.map +1 -0
- package/dist/actions/throttle.d.ts +9 -0
- package/dist/actions/throttle.d.ts.map +1 -0
- package/dist/css-in-js/cl.d.ts +37 -0
- package/dist/css-in-js/cl.d.ts.map +1 -0
- package/dist/css-in-js/index.d.ts +7 -0
- package/dist/css-in-js/index.d.ts.map +1 -0
- package/dist/css-in-js/sanitize.d.ts +31 -0
- package/dist/css-in-js/sanitize.d.ts.map +1 -0
- package/dist/css-in-js/serialize.d.ts +98 -0
- package/dist/css-in-js/serialize.d.ts.map +1 -0
- package/dist/css-in-js/stylesheet.bench.d.ts +2 -0
- package/dist/css-in-js/stylesheet.bench.d.ts.map +1 -0
- package/dist/css-in-js/stylesheet.d.ts +188 -0
- package/dist/css-in-js/stylesheet.d.ts.map +1 -0
- package/dist/css-in-js/theme.d.ts +105 -0
- package/dist/css-in-js/theme.d.ts.map +1 -0
- package/dist/devtools/devtoolsHooks.d.ts +101 -0
- package/dist/devtools/devtoolsHooks.d.ts.map +1 -0
- package/dist/devtools/index.d.ts +2 -0
- package/dist/devtools/index.d.ts.map +1 -0
- package/dist/dist/styles.css +1 -0
- package/dist/index.d.ts +5439 -977
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +132 -16
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +135 -18
- package/dist/index.js.map +1 -1
- package/dist/on/hooks.d.ts +319 -0
- package/dist/on/hooks.d.ts.map +1 -0
- package/dist/on/index.d.ts +3 -0
- package/dist/on/index.d.ts.map +1 -0
- package/dist/on/on.d.ts +16 -0
- package/dist/on/on.d.ts.map +1 -0
- package/dist/portals/dialog.d.ts +56 -0
- package/dist/portals/dialog.d.ts.map +1 -0
- package/dist/portals/index.d.ts +9 -0
- package/dist/portals/index.d.ts.map +1 -0
- package/dist/portals/popup.d.ts +37 -0
- package/dist/portals/popup.d.ts.map +1 -0
- package/dist/portals/portal.d.ts +57 -0
- package/dist/portals/portal.d.ts.map +1 -0
- package/dist/portals/tip.d.ts +38 -0
- package/dist/portals/tip.d.ts.map +1 -0
- package/dist/socket/index.d.ts +2 -0
- package/dist/socket/index.d.ts.map +1 -0
- package/dist/socket/ws.d.ts +83 -0
- package/dist/socket/ws.d.ts.map +1 -0
- package/dist/ssr/index.d.ts +3 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/ssr/ssr-bridge.d.ts +89 -0
- package/dist/ssr/ssr-bridge.d.ts.map +1 -0
- package/dist/ssr/ssrFlag.d.ts +38 -0
- package/dist/ssr/ssrFlag.d.ts.map +1 -0
- package/dist/virtualizer/LitVirtualizer.d.ts +62 -0
- package/dist/virtualizer/LitVirtualizer.d.ts.map +1 -0
- package/dist/virtualizer/ScrollerController.d.ts +54 -0
- package/dist/virtualizer/ScrollerController.d.ts.map +1 -0
- package/dist/virtualizer/Virtualizer.d.ts +214 -0
- package/dist/virtualizer/Virtualizer.d.ts.map +1 -0
- package/dist/virtualizer/events.d.ts +27 -0
- package/dist/virtualizer/events.d.ts.map +1 -0
- package/dist/virtualizer/index.d.ts +2 -0
- package/dist/virtualizer/index.d.ts.map +1 -0
- package/dist/virtualizer/layouts/flexWrap.d.ts +65 -0
- package/dist/virtualizer/layouts/flexWrap.d.ts.map +1 -0
- package/dist/virtualizer/layouts/flow.d.ts +123 -0
- package/dist/virtualizer/layouts/flow.d.ts.map +1 -0
- package/dist/virtualizer/layouts/grid.d.ts +25 -0
- package/dist/virtualizer/layouts/grid.d.ts.map +1 -0
- package/dist/virtualizer/layouts/masonry.d.ts +37 -0
- package/dist/virtualizer/layouts/masonry.d.ts.map +1 -0
- package/dist/virtualizer/layouts/shared/BaseLayout.d.ts +204 -0
- package/dist/virtualizer/layouts/shared/BaseLayout.d.ts.map +1 -0
- package/dist/virtualizer/layouts/shared/GridBaseLayout.d.ts +41 -0
- package/dist/virtualizer/layouts/shared/GridBaseLayout.d.ts.map +1 -0
- package/dist/virtualizer/layouts/shared/Layout.d.ts +138 -0
- package/dist/virtualizer/layouts/shared/Layout.d.ts.map +1 -0
- package/dist/virtualizer/layouts/shared/SizeCache.d.ts +19 -0
- package/dist/virtualizer/layouts/shared/SizeCache.d.ts.map +1 -0
- package/dist/virtualizer/layouts/shared/SizeGapPaddingBaseLayout.d.ts +57 -0
- package/dist/virtualizer/layouts/shared/SizeGapPaddingBaseLayout.d.ts.map +1 -0
- package/dist/virtualizer/lit-virtualizer.d.ts +23 -0
- package/dist/virtualizer/lit-virtualizer.d.ts.map +1 -0
- package/dist/virtualizer/mates-adapter.d.ts +201 -0
- package/dist/virtualizer/mates-adapter.d.ts.map +1 -0
- package/dist/virtualizer/support/method-interception.d.ts +33 -0
- package/dist/virtualizer/support/method-interception.d.ts.map +1 -0
- package/dist/virtualizer/support/resize-observer-errors.d.ts +53 -0
- package/dist/virtualizer/support/resize-observer-errors.d.ts.map +1 -0
- package/dist/virtualizer/virtualize.d.ts +52 -0
- package/dist/virtualizer/virtualize.d.ts.map +1 -0
- package/package.json +25 -6
- package/dist/examples/src/Counter/Counter.d.ts +0 -2
- package/dist/examples/src/Counter/Counter.d.ts.map +0 -1
- package/dist/examples/src/Counter/CounterWithAtoms.d.ts +0 -2
- package/dist/examples/src/Counter/CounterWithAtoms.d.ts.map +0 -1
- package/dist/examples/src/Counter/index.d.ts +0 -3
- package/dist/examples/src/Counter/index.d.ts.map +0 -1
- package/dist/examples/src/Scope Examples/MultiScopeDemo.d.ts +0 -2
- package/dist/examples/src/Scope Examples/MultiScopeDemo.d.ts.map +0 -1
- package/dist/examples/src/Scope Examples/ScopeDemo.d.ts +0 -2
- package/dist/examples/src/Scope Examples/ScopeDemo.d.ts.map +0 -1
- package/dist/examples/src/Scope Examples/SimpleScopeExample.d.ts +0 -2
- package/dist/examples/src/Scope Examples/SimpleScopeExample.d.ts.map +0 -1
- package/dist/examples/src/Scope Examples/index.d.ts +0 -4
- package/dist/examples/src/Scope Examples/index.d.ts.map +0 -1
- package/dist/examples/src/Scopes Test/A.d.ts +0 -3
- package/dist/examples/src/Scopes Test/A.d.ts.map +0 -1
- package/dist/examples/src/ThemeContext.ts/ThemeDemo.d.ts +0 -4
- package/dist/examples/src/ThemeContext.ts/ThemeDemo.d.ts.map +0 -1
- package/dist/examples/src/Todo/Todo.d.ts +0 -2
- package/dist/examples/src/Todo/Todo.d.ts.map +0 -1
- package/dist/examples/src/Todo/TodoWithAtoms.d.ts +0 -2
- package/dist/examples/src/Todo/TodoWithAtoms.d.ts.map +0 -1
- package/dist/examples/src/Todo/index.d.ts +0 -3
- package/dist/examples/src/Todo/index.d.ts.map +0 -1
- package/dist/examples/src/main.d.ts +0 -3
- package/dist/examples/src/main.d.ts.map +0 -1
- package/dist/examples/vite.config.d.ts +0 -3
- package/dist/examples/vite.config.d.ts.map +0 -1
- package/dist/lib/Context.d.ts.map +0 -1
- package/dist/lib/atom.d.ts +0 -45
- package/dist/lib/atom.d.ts.map +0 -1
- package/dist/lib/bubbles.d.ts +0 -2
- package/dist/lib/bubbles.d.ts.map +0 -1
- package/dist/lib/compute.d.ts +0 -9
- package/dist/lib/compute.d.ts.map +0 -1
- package/dist/lib/index.d.ts +0 -31
- package/dist/lib/index.d.ts.map +0 -1
- package/dist/lib/molecule.d.ts +0 -16
- package/dist/lib/molecule.d.ts.map +0 -1
- package/dist/lib/pathResolver.d.ts +0 -9
- package/dist/lib/pathResolver.d.ts.map +0 -1
- package/dist/lib/scope.d.ts +0 -14
- package/dist/lib/scope.d.ts.map +0 -1
- package/dist/lib/store.d.ts +0 -2
- package/dist/lib/store.d.ts.map +0 -1
- package/dist/lib/styleDirectives.d.ts +0 -14
- package/dist/lib/styleDirectives.d.ts.map +0 -1
- package/dist/lib/units.d.ts +0 -12
- package/dist/lib/units.d.ts.map +0 -1
- package/dist/lib/useRefEffect.d.ts +0 -2
- package/dist/lib/useRefEffect.d.ts.map +0 -1
- package/dist/lib/useStore.d.ts +0 -26
- package/dist/lib/useStore.d.ts.map +0 -1
- package/dist/lib/view.d.ts +0 -8
- package/dist/lib/view.d.ts.map +0 -1
- package/dist/lib/x-view.d.ts +0 -40
- package/dist/lib/x-view.d.ts.map +0 -1
- /package/dist/{lib → Template}/xProvider.d.ts +0 -0
- /package/dist/{lib → Utils}/Context.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,412 +1,1753 @@
|
|
|
1
|
-
#
|
|
1
|
+
# MATES
|
|
2
|
+
|
|
3
|
+
**MATES** is a lightweight, TypeScript-first front-end framework built on [lit-html](https://lit.dev/docs/libraries/standalone-templates/) to help you build large scale complex web applications that focuses on Developer Experience.
|
|
4
|
+
|
|
5
|
+
What if we had a framework that is just perfect? A framework that's faster than react and without the need of a compiler. A Framework that's super type safe. A framework that cherishes capabilities of Javascript. Mates a Beautiful, simple, Perfect Javascript Framework with **no virtual DOM**, **no compiler step**, but with lots of **goodness**. You can install Mates and run using a build tool like Vite or Bun or just use CDN to load Mates at runtime.
|
|
6
|
+
|
|
7
|
+
MATES is based on an architecture: **M**utable State, **A**ctions, **T**emplates, **E**vents and **S**etups. State is mutable, Actions are trackable functions, Templates are functions that return html template strings, setups are run once before the component is mounted to set up some state or some business logic or effects.
|
|
8
|
+
|
|
9
|
+
Mates is about 50Kb gzipped, but it comes with a Router, really good state management system (you will not need anything else), Date Utils with timezone support (no need for extra library), Fetch and web socket Utils (no axios or socket.io needed), Animation Utils, Form Validation Utils, Virtualisation for lists or tables, built in CSS-in-Js library, portals, tooltips, snackbars, popups (so it'll help you create popups with no effort), UUID utils to generate random ID, Actions, Events, custom hook support, Memoisation, Zero promises (cancellable and reusable promises), built in support for pagination and lazy loading, with tons of built in hooks that are very useful, utilites to handle local storage across all tabs, a Task Manager to handle lots of async tasks in parallel or one by one and many more.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Why Mates?](#why-mates)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Components](#components)
|
|
19
|
+
- [The Two-Layer Model](#the-two-layer-model)
|
|
20
|
+
- [Rendering Components](#rendering-components)
|
|
21
|
+
- [Props](#props)
|
|
22
|
+
- [Children / Slots](#children--slots)
|
|
23
|
+
- [State Management](#state-management)
|
|
24
|
+
- [atom — reactive primitive](#atom--reactive-primitive)
|
|
25
|
+
- [iAtom — immutable signal](#iatom--immutable-signal)
|
|
26
|
+
- [effect — reactive side effect](#effect--reactive-side-effect)
|
|
27
|
+
- [memo — derived / computed value](#memo--derived--computed-value)
|
|
28
|
+
- [useState — local object state](#usestate--local-object-state)
|
|
29
|
+
- [store — module-level shared state](#store--module-level-shared-state)
|
|
30
|
+
- [setAtom — reactive Set](#setatom--reactive-set)
|
|
31
|
+
- [mapAtom — reactive Map](#mapatom--reactive-map)
|
|
32
|
+
- [lsAtom / ssAtom — persistent state](#lsatom--ssatom--persistent-state)
|
|
33
|
+
- [Scopes — Shared State Without Prop Drilling](#scopes--shared-state-without-prop-drilling)
|
|
34
|
+
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
35
|
+
- [DOM & Window Hooks](#dom--window-hooks)
|
|
36
|
+
- [Async Actions](#async-actions)
|
|
37
|
+
- [asyncAction](#asyncaction)
|
|
38
|
+
- [action — synchronous action](#action--synchronous-action)
|
|
39
|
+
- [paginatedAsyncAction](#paginatedasyncaction)
|
|
40
|
+
- [taskAction](#taskaction)
|
|
41
|
+
- [HTTP Client](#http-client)
|
|
42
|
+
- [Routing](#routing)
|
|
43
|
+
- [CSS-in-JS & Theming](#css-in-js--theming)
|
|
44
|
+
- [Directives](#directives)
|
|
45
|
+
- [Portals, Dialogs & Tooltips](#portals-dialogs--tooltips)
|
|
46
|
+
- [Animations](#animations)
|
|
47
|
+
- [WebSocket](#websocket)
|
|
48
|
+
- [Virtualization](#virtualization)
|
|
49
|
+
- [DevTools](#devtools)
|
|
50
|
+
- [TypeScript](#typescript)
|
|
51
|
+
- [Features at a Glance](#features-at-a-glance)
|
|
52
|
+
- [How Mates Compares](#how-mates-compares)
|
|
53
|
+
- [License](#license)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Why Mates?
|
|
58
|
+
|
|
59
|
+
Most frameworks ask you to learn a new mental model — JSX compilers, reactive transforms, or a shadow DOM abstraction. Mates takes a different approach:
|
|
60
|
+
|
|
61
|
+
- **No compiler, no JSX** — just tagged template literals from `lit-html`. Works in any TypeScript project out of the box.
|
|
62
|
+
- **No virtual DOM** — `lit-html` patches only the parts of the DOM that actually changed. Updates are surgical, not diffed from scratch.]
|
|
63
|
+
- **Predictable two-layer components** — the outer function (setup) runs once. The inner function (render) runs on every update. Clear separation between initialization and rendering.
|
|
64
|
+
- **Batteries included** — routing, async state machines, CSS-in-JS, WebSocket, virtualization, portals, and animations are all first-party and designed to work together.
|
|
65
|
+
- **Very Capable** it's shipped with lots of utitlies to help you avoid installing a third party library just for few of it's functions.
|
|
66
|
+
- **TypeScript-first** — every API is fully typed from the ground up.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
2
71
|
|
|
3
|
-
|
|
72
|
+
```bash
|
|
73
|
+
npm install mates
|
|
74
|
+
```
|
|
4
75
|
|
|
5
|
-
|
|
76
|
+
Mates requires `lit-html` as a peer dependency (automatically installed):
|
|
6
77
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **E**vents: Event Utilites for event-driven development
|
|
11
|
-
- **S**etup functions: These are functions to intialise state needed for the view
|
|
78
|
+
```bash
|
|
79
|
+
npm install mates lit-html
|
|
80
|
+
```
|
|
12
81
|
|
|
13
|
-
|
|
82
|
+
---
|
|
14
83
|
|
|
15
|
-
|
|
84
|
+
## Quick Start
|
|
16
85
|
|
|
17
|
-
|
|
86
|
+
```typescript
|
|
87
|
+
import { atom, html, renderX, x } from "mates";
|
|
18
88
|
|
|
19
|
-
|
|
89
|
+
// A simple counter component
|
|
90
|
+
const Counter = () => {
|
|
91
|
+
const count = atom(0);
|
|
20
92
|
|
|
21
|
-
|
|
93
|
+
return () => html`
|
|
94
|
+
<button @click=${() => count.set((n) => n - 1)}>−</button>
|
|
95
|
+
<span>${count()}</span>
|
|
96
|
+
<button @click=${() => count.set((n) => n + 1)}>+</button>
|
|
97
|
+
`;
|
|
98
|
+
};
|
|
22
99
|
|
|
100
|
+
// Mount to the DOM
|
|
101
|
+
renderX(Counter, document.getElementById("app")!);
|
|
23
102
|
```
|
|
24
103
|
|
|
25
|
-
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Components
|
|
26
107
|
|
|
27
|
-
###
|
|
108
|
+
### The Two-Layer Model
|
|
28
109
|
|
|
29
|
-
|
|
110
|
+
Every Mates component is a **closure** with two layers:
|
|
111
|
+
|
|
112
|
+
| Layer | Runs | Purpose |
|
|
113
|
+
|-------|------|---------|
|
|
114
|
+
| **Outer function** | Once on mount | Create atoms, set up subscriptions, register lifecycle hooks |
|
|
115
|
+
| **Inner function** | Every render | Read reactive values and return a `TemplateResult` |
|
|
30
116
|
|
|
31
117
|
```typescript
|
|
32
|
-
import {
|
|
118
|
+
import { atom, html } from "mates";
|
|
119
|
+
import type { Props } from "mates";
|
|
33
120
|
|
|
34
|
-
const
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
const incr = setter(() => count++);
|
|
121
|
+
const Counter = (propsFn: Props<{ label: string }>) => {
|
|
122
|
+
// ── Outer: runs once ────────────────────────────────────
|
|
123
|
+
const count = atom(0);
|
|
38
124
|
|
|
39
|
-
//
|
|
40
|
-
return () => html`
|
|
41
|
-
<
|
|
42
|
-
<button @click=${
|
|
43
|
-
|
|
125
|
+
// ── Inner: runs on every re-render ──────────────────────
|
|
126
|
+
return () => html`
|
|
127
|
+
<p>${propsFn().label}: ${count()}</p>
|
|
128
|
+
<button @click=${() => count.set((n) => n + 1)}>Increment</button>
|
|
129
|
+
`;
|
|
44
130
|
};
|
|
45
|
-
|
|
46
|
-
// Render the view in any html element by passing it's id
|
|
47
|
-
renderView(CounterView, "app");
|
|
48
131
|
```
|
|
49
132
|
|
|
50
|
-
**
|
|
51
|
-
**incr** is a **setter** function, that when called, updates the view. it has to be non-async.
|
|
133
|
+
> **Rule of thumb:** atoms, effects, hooks, and subscriptions always go in the **outer** function, you can also make api calls..etc you can be assumed that this code is only executed once. template function always returns html temlate strings. Modern IDEs have a built in formatting support for the html template strings.
|
|
52
134
|
|
|
53
|
-
|
|
135
|
+
---
|
|
54
136
|
|
|
55
|
-
|
|
137
|
+
### Rendering Components
|
|
56
138
|
|
|
57
|
-
|
|
139
|
+
Use `x()` (also exported as `view`) to embed a component inside a template. Use `renderX()` to mount a component into a real DOM element.
|
|
58
140
|
|
|
59
|
-
|
|
141
|
+
**x** function lets you mount components in another component. you can also use **view** for the same puprose. **x** takes in a component.
|
|
60
142
|
|
|
61
|
-
|
|
143
|
+
```typescript
|
|
144
|
+
import { html, renderX, x } from "mates";
|
|
145
|
+
|
|
146
|
+
// Embed in another component's template
|
|
147
|
+
const App = () => () => html`
|
|
148
|
+
<main>
|
|
149
|
+
<h1>My App</h1>
|
|
150
|
+
${x(Counter, { label: "Clicks" })}
|
|
151
|
+
</main>
|
|
152
|
+
`;
|
|
62
153
|
|
|
63
|
-
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### plain state and settter
|
|
159
|
+
|
|
160
|
+
You don't need to create atoms to handle state for your application. The state can be anything, it can be a plain primitives or objects or sets or maps or something else, the reason we use atoms because they are trackable and you can derive computed values from them. But you can also use plain JS objects as state and you can setter functions to notify the component that something is changed and that it needs to update itself. Here is an example of the same counter without using atoms.
|
|
64
161
|
|
|
65
162
|
```typescript
|
|
66
|
-
|
|
163
|
+
import { setter, html } from "mates";
|
|
164
|
+
import type { Props } from "mates";
|
|
165
|
+
|
|
166
|
+
const Counter = (propsFn: Props<{ label: string }>) => {
|
|
67
167
|
let count = 0;
|
|
68
|
-
|
|
168
|
+
const incr = setter(()=>count++);
|
|
169
|
+
// ── Inner: runs every time the incr is called or parent component is re-rendered.
|
|
69
170
|
return () => html`
|
|
70
|
-
<
|
|
71
|
-
<
|
|
171
|
+
<p>${propsFn().label}: ${count}</p>
|
|
172
|
+
<button @click=${incr}>Increment</button>
|
|
72
173
|
`;
|
|
73
174
|
};
|
|
74
|
-
|
|
75
|
-
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### PropsFn
|
|
178
|
+
|
|
179
|
+
Props are passed as the second argument to `x()` and arrive inside the component as a **function** — `propsFn()`. Calling it inside the **inner** function ensures you always get the latest values when the parent re-renders.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { html, x, renderX } from "mates";
|
|
183
|
+
import type { Props } from "mates";
|
|
184
|
+
|
|
185
|
+
const Greeting = (propsFn: Props<{ name: string; color?: string }>) => {
|
|
186
|
+
// ✅ Read props in the inner function — always up-to-date
|
|
187
|
+
return () => html`
|
|
188
|
+
<p style="color: ${propsFn().color ?? "black"}">
|
|
189
|
+
Hello, ${propsFn().name}!
|
|
190
|
+
</p>
|
|
191
|
+
`;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Props update automatically when the parent re-renders
|
|
195
|
+
const App = () => {
|
|
196
|
+
const name = atom("World");
|
|
197
|
+
return () => html`
|
|
198
|
+
${x(Greeting, { name: name(), color: "royalblue" })}
|
|
199
|
+
<button @click=${() => name.set("Mates")}>Change Name</button>
|
|
200
|
+
`;
|
|
76
201
|
};
|
|
77
202
|
```
|
|
78
203
|
|
|
79
|
-
|
|
204
|
+
> ⚠️ Never destructure props in the outer function (`const { name } = propsFn()`). The value would be captured at mount time and never update.
|
|
80
205
|
|
|
81
|
-
|
|
206
|
+
---
|
|
82
207
|
|
|
83
|
-
|
|
208
|
+
### Children / Slots
|
|
84
209
|
|
|
85
|
-
|
|
210
|
+
Pass children using the `<x-view>` element and render them inside the component with a standard `<slot>` element:
|
|
86
211
|
|
|
87
|
-
|
|
212
|
+
```typescript
|
|
213
|
+
// Parent passes children
|
|
214
|
+
const App = () => () => html`
|
|
215
|
+
<x-view .view=${Card} .props=${{title: "Hello"}}>
|
|
216
|
+
<p>This is child content</p>
|
|
217
|
+
</x-view>
|
|
218
|
+
`;
|
|
219
|
+
|
|
220
|
+
// Component renders children via <slot>
|
|
221
|
+
const Card = (propsFn: Props<{ title: string }>) => () => html`
|
|
222
|
+
<div class="card">
|
|
223
|
+
<h2>${propsFn().title}</h2>
|
|
224
|
+
<slot></slot>
|
|
225
|
+
</div>
|
|
226
|
+
`;
|
|
227
|
+
```
|
|
88
228
|
|
|
89
|
-
|
|
90
|
-
const username = atom("guest");
|
|
229
|
+
---
|
|
91
230
|
|
|
92
|
-
|
|
93
|
-
console.log(username()); // "guest"
|
|
94
|
-
// or
|
|
95
|
-
console.log(username.get()); // "guest"
|
|
231
|
+
## State Management
|
|
96
232
|
|
|
97
|
-
|
|
98
|
-
username.set("alice");
|
|
233
|
+
### `atom` — reactive primitive
|
|
99
234
|
|
|
100
|
-
|
|
101
|
-
username.set((val) => val.toUpperCase());
|
|
235
|
+
`atom` is the core reactive primitive. It holds any JavaScript value. Reading an atom inside the **inner** function (or an `effect`) registers a dependency — that subscriber re-runs whenever the atom changes.
|
|
102
236
|
|
|
103
|
-
|
|
104
|
-
|
|
237
|
+
```typescript
|
|
238
|
+
import { atom } from "mates";
|
|
239
|
+
|
|
240
|
+
// Create
|
|
241
|
+
const username = atom("guest");
|
|
242
|
+
const count = atom(0);
|
|
243
|
+
const user = atom<{ name: string; age: number } | null>(null);
|
|
244
|
+
|
|
245
|
+
// Read
|
|
246
|
+
username(); // "guest" — registers reactive dependency
|
|
247
|
+
username.get(); // "guest" — alias, same behavior
|
|
248
|
+
username.val; // "guest" — non-reactive (snapshot, no tracking)
|
|
249
|
+
|
|
250
|
+
// Write
|
|
251
|
+
username.set("alice"); // replace
|
|
252
|
+
count.set((n) => n + 1); // updater function
|
|
253
|
+
user.set({ name: "Alice", age: 30 });
|
|
254
|
+
|
|
255
|
+
// Mutate objects in-place
|
|
256
|
+
const profile = atom({ name: "Alice", score: 0 });
|
|
257
|
+
profile.update((p) => { p.score++; }); // mutation, triggers update
|
|
258
|
+
```
|
|
105
259
|
|
|
106
|
-
|
|
107
|
-
address.update((s) => (s.street = "newstreet"));
|
|
260
|
+
**Atoms created inside a component's outer function** are scoped to that component — they are created on mount and automatically garbage-collected when the component unmounts.
|
|
108
261
|
|
|
109
|
-
|
|
110
|
-
const nums = atom(new Map());
|
|
111
|
-
// modify the map through update.
|
|
112
|
-
nums.update(m=>m.set(1, "one"));
|
|
262
|
+
**Atoms created at module level** are global — they persist for the lifetime of the application and can be shared between any component.
|
|
113
263
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
264
|
+
```typescript
|
|
265
|
+
// Global state — shared across all components
|
|
266
|
+
export const currentUser = atom<User | null>(null);
|
|
267
|
+
export const cartCount = atom(0);
|
|
118
268
|
```
|
|
119
269
|
|
|
120
|
-
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### `iAtom` — immutable signal
|
|
273
|
+
|
|
274
|
+
`iAtom` (also exported as `signal`) behaves like `atom` but **deep-freezes every value** after each `set()`. Accidental mutations throw in strict mode, making state flows easier to reason about. There is no `.update()` method — you always replace the whole value.
|
|
121
275
|
|
|
122
276
|
```typescript
|
|
123
|
-
|
|
124
|
-
// setup function: initialise state
|
|
125
|
-
const count = atom(0);
|
|
126
|
-
const incr = count.set(count() + 1);
|
|
277
|
+
import { iAtom } from "mates";
|
|
127
278
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
};
|
|
279
|
+
const theme = iAtom<"light" | "dark">("light");
|
|
280
|
+
|
|
281
|
+
theme.set("dark");
|
|
282
|
+
theme(); // "dark"
|
|
283
|
+
|
|
284
|
+
const config = iAtom({ debug: false, version: 1 });
|
|
285
|
+
config.set({ debug: true, version: 2 }); // must replace entirely
|
|
134
286
|
```
|
|
135
287
|
|
|
136
|
-
|
|
288
|
+
---
|
|
137
289
|
|
|
138
|
-
|
|
139
|
-
Units have the following pieces
|
|
290
|
+
### `effect` — reactive side effect
|
|
140
291
|
|
|
141
|
-
-
|
|
142
|
-
- Setters: methods whose name start with (\_)
|
|
143
|
-
- Getters: methods that returns data without changing it
|
|
144
|
-
- Actions: async or non-async methods that call getters or setters for getting, setting data.
|
|
292
|
+
An effect runs **immediately** and automatically re-runs whenever any atom it reads during execution changes. Return a cleanup function to run before the next execution and on disposal.
|
|
145
293
|
|
|
146
294
|
```typescript
|
|
147
|
-
import {
|
|
295
|
+
import { atom, effect } from "mates";
|
|
148
296
|
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
},
|
|
161
|
-
// async action that calls setters to set state
|
|
162
|
-
async loadUsers() {
|
|
163
|
-
this._setIsLoading(true);
|
|
164
|
-
this._setUsers(await fetch("/users").then((d) => d.json()));
|
|
165
|
-
this._setIsLoading(false);
|
|
166
|
-
},
|
|
167
|
-
getUsersCount() {
|
|
168
|
-
return this.users.length;
|
|
169
|
-
},
|
|
297
|
+
const count = atom(0);
|
|
298
|
+
const label = atom("counter");
|
|
299
|
+
|
|
300
|
+
// Runs immediately, then again whenever count or label changes
|
|
301
|
+
const stop = effect(() => {
|
|
302
|
+
document.title = `${label()}: ${count()}`;
|
|
303
|
+
|
|
304
|
+
// Optional: return a cleanup function
|
|
305
|
+
return () => {
|
|
306
|
+
document.title = "App";
|
|
307
|
+
};
|
|
170
308
|
});
|
|
309
|
+
|
|
310
|
+
count.set(5); // document.title → "counter: 5"
|
|
311
|
+
label.set("score"); // document.title → "score: 5"
|
|
312
|
+
|
|
313
|
+
stop(); // dispose — cleanup runs, no more re-runs
|
|
171
314
|
```
|
|
172
315
|
|
|
173
|
-
|
|
316
|
+
When used inside a component's outer function, effects are automatically disposed when the component unmounts.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### `memo` — derived / computed value
|
|
174
321
|
|
|
175
|
-
|
|
322
|
+
`memo` creates a derived atom that stays in sync with its dependencies. It only recomputes when a dependency changes. The result is itself a readable atom.
|
|
176
323
|
|
|
177
324
|
```typescript
|
|
178
|
-
import { atom,
|
|
325
|
+
import { atom, memo } from "mates";
|
|
179
326
|
|
|
180
|
-
const firstName = atom("
|
|
327
|
+
const firstName = atom("Alice");
|
|
328
|
+
const lastName = atom("Smith");
|
|
181
329
|
|
|
182
|
-
const
|
|
330
|
+
const fullName = memo(() => `${firstName()} ${lastName()}`);
|
|
183
331
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
332
|
+
fullName(); // "Alice Smith"
|
|
333
|
+
firstName.set("Bob");
|
|
334
|
+
fullName(); // "Bob Smith" — recomputed automatically
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
### `useState` — local object state
|
|
340
|
+
|
|
341
|
+
`useState` is designed for local component state expressed as a plain object with data properties and setter methods. Every method call triggers a re-render.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { html, renderX, useState } from "mates";
|
|
345
|
+
|
|
346
|
+
const Counter = () => {
|
|
347
|
+
const [state] = useState({
|
|
348
|
+
count: 0,
|
|
349
|
+
step: 1,
|
|
350
|
+
incr() { this.count += this.step; },
|
|
351
|
+
decr() { this.count -= this.step; },
|
|
352
|
+
reset() { this.count = 0; },
|
|
353
|
+
setStep(n: number) { this.step = n; },
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return () => html`
|
|
357
|
+
<div>
|
|
358
|
+
<button @click=${state.decr}>−</button>
|
|
359
|
+
<span>${state.count}</span>
|
|
360
|
+
<button @click=${state.incr}>+</button>
|
|
361
|
+
<button @click=${state.reset}>Reset</button>
|
|
362
|
+
<label>
|
|
363
|
+
Step:
|
|
364
|
+
<input
|
|
365
|
+
type="number"
|
|
366
|
+
.value=${String(state.step)}
|
|
367
|
+
@input=${(e: InputEvent) =>
|
|
368
|
+
state.setStep(Number((e.target as HTMLInputElement).value))}
|
|
369
|
+
/>
|
|
370
|
+
</label>
|
|
371
|
+
</div>
|
|
372
|
+
`;
|
|
373
|
+
};
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
> Async methods inside `useState` are automatically wrapped with `asyncAction`, giving you `.data`, `.isLoading`, `.error`, and `.status` atoms for free on async operations.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### `store` — module-level shared state
|
|
187
381
|
|
|
188
|
-
|
|
382
|
+
`store` creates a **global reactive container** from a plain object. Define it at module level and share it across the entire application. Inside a component, call the store to get a `[state, update]` tuple.
|
|
189
383
|
|
|
190
|
-
|
|
384
|
+
```typescript
|
|
385
|
+
import { store, html, x, renderX } from "mates";
|
|
386
|
+
|
|
387
|
+
// Define once at module level
|
|
388
|
+
const counterStore = store({
|
|
389
|
+
count: 0,
|
|
390
|
+
step: 1,
|
|
391
|
+
incr() { this.count += this.step; },
|
|
392
|
+
decr() { this.count -= this.step; },
|
|
393
|
+
get doubled() { return this.count * 2; },
|
|
394
|
+
});
|
|
191
395
|
|
|
192
|
-
|
|
396
|
+
// Consume in any component
|
|
397
|
+
const Counter = () => {
|
|
398
|
+
const [state, update] = counterStore();
|
|
193
399
|
|
|
194
|
-
|
|
400
|
+
return () => html`
|
|
401
|
+
<button @click=${state.decr}>−</button>
|
|
402
|
+
<span>${state.count} (doubled: ${state.doubled})</span>
|
|
403
|
+
<button @click=${state.incr}>+</button>
|
|
404
|
+
<button @click=${() => update((s) => { s.count = 0; })}>Reset</button>
|
|
405
|
+
`;
|
|
406
|
+
};
|
|
195
407
|
```
|
|
196
408
|
|
|
197
|
-
|
|
409
|
+
The `update` function is for direct external mutations. State methods (`incr`, `decr`) trigger re-renders automatically.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
### `setAtom` — reactive Set
|
|
414
|
+
|
|
415
|
+
A reactive wrapper around JavaScript's `Set`. All mutations trigger component updates.
|
|
198
416
|
|
|
199
417
|
```typescript
|
|
200
|
-
import {
|
|
418
|
+
import { setAtom } from "mates";
|
|
201
419
|
|
|
202
|
-
|
|
203
|
-
name = atom("Guest");
|
|
420
|
+
const selectedIds = setAtom<number>([1, 2]);
|
|
204
421
|
|
|
205
|
-
|
|
422
|
+
// Mutations — trigger re-renders
|
|
423
|
+
selectedIds.add(3);
|
|
424
|
+
selectedIds.delete(1);
|
|
425
|
+
selectedIds.clear();
|
|
206
426
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
427
|
+
// Reads — track as reactive dependencies
|
|
428
|
+
selectedIds.size; // 2
|
|
429
|
+
selectedIds.has(2); // true
|
|
430
|
+
selectedIds.values(); // IterableIterator
|
|
431
|
+
selectedIds.forEach((v) => {}); // iterate
|
|
432
|
+
```
|
|
211
433
|
|
|
212
|
-
|
|
213
|
-
this.name.set("Guest");
|
|
214
|
-
this.isLoggedIn.set(false);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
434
|
+
---
|
|
217
435
|
|
|
218
|
-
|
|
436
|
+
### `mapAtom` — reactive Map
|
|
219
437
|
|
|
220
|
-
|
|
438
|
+
A reactive wrapper around JavaScript's `Map`. All mutations trigger component updates.
|
|
221
439
|
|
|
222
|
-
|
|
440
|
+
```typescript
|
|
441
|
+
import { mapAtom } from "mates";
|
|
223
442
|
|
|
224
|
-
|
|
443
|
+
const cache = mapAtom<string, number>([["apples", 3]]);
|
|
225
444
|
|
|
226
|
-
|
|
445
|
+
// Mutations
|
|
446
|
+
cache.set("bananas", 5);
|
|
447
|
+
cache.delete("apples");
|
|
448
|
+
cache.clear();
|
|
227
449
|
|
|
228
|
-
|
|
450
|
+
// Reads
|
|
451
|
+
cache.size; // 1
|
|
452
|
+
cache.get("bananas"); // 5
|
|
453
|
+
cache.has("bananas"); // true
|
|
454
|
+
cache.entries(); // IterableIterator<[string, number]>
|
|
229
455
|
```
|
|
230
456
|
|
|
231
|
-
|
|
457
|
+
---
|
|
232
458
|
|
|
233
|
-
|
|
459
|
+
### `lsAtom` / `ssAtom` — persistent state
|
|
460
|
+
|
|
461
|
+
`lsAtom` persists to `localStorage`; `ssAtom` persists to `sessionStorage`. Both auto-hydrate from storage on first read and automatically sync writes back.
|
|
234
462
|
|
|
235
463
|
```typescript
|
|
236
|
-
import {
|
|
464
|
+
import { lsAtom, ssAtom } from "mates";
|
|
237
465
|
|
|
238
|
-
|
|
466
|
+
// Persisted across page reloads
|
|
467
|
+
const theme = lsAtom<"light" | "dark">("light");
|
|
468
|
+
const sidebar = lsAtom<boolean>(true);
|
|
239
469
|
|
|
240
|
-
//
|
|
470
|
+
// Session-only — cleared when the tab closes
|
|
471
|
+
const draftText = ssAtom<string>("");
|
|
241
472
|
|
|
242
|
-
|
|
243
|
-
|
|
473
|
+
theme.set("dark"); // saved to localStorage["mates"]
|
|
474
|
+
theme(); // "dark" — even after a page reload
|
|
475
|
+
```
|
|
244
476
|
|
|
245
|
-
|
|
246
|
-
this.theme = this.theme === "light" ? "dark" : "light";
|
|
247
|
-
}
|
|
248
|
-
}
|
|
477
|
+
`lsAtom` also syncs across browser tabs via the `storage` event.
|
|
249
478
|
|
|
250
|
-
|
|
479
|
+
---
|
|
251
480
|
|
|
252
|
-
|
|
253
|
-
(props) => {
|
|
254
|
-
const themeContext = new ThemeContext();
|
|
481
|
+
## Scopes — Shared State Without Prop Drilling
|
|
255
482
|
|
|
256
|
-
|
|
257
|
-
<x-provider .value=${themeContext}> ${props().children} </x-provider>
|
|
258
|
-
`;
|
|
259
|
-
},
|
|
483
|
+
Scopes let any **descendant** component access state from a parent without threading props through every layer in between. Define a scope as a class, initialize it in the parent with `useScope`, and read it anywhere below with `getParentScope`.
|
|
260
484
|
|
|
261
|
-
|
|
262
|
-
|
|
485
|
+
```typescript
|
|
486
|
+
import { atom, effect, getParentScope, html, onMount, repeat, useScope, x } from "mates";
|
|
487
|
+
import type { Props } from "mates";
|
|
488
|
+
|
|
489
|
+
// 1. Define the scope as a class
|
|
490
|
+
class CartScope {
|
|
491
|
+
items = atom<string[]>([]);
|
|
492
|
+
loading = atom(false);
|
|
263
493
|
|
|
264
|
-
|
|
494
|
+
add(item: string) {
|
|
495
|
+
this.items.update((list) => { list.push(item); });
|
|
496
|
+
}
|
|
497
|
+
remove(item: string) {
|
|
498
|
+
this.items.update((list) => {
|
|
499
|
+
const i = list.indexOf(item);
|
|
500
|
+
if (i !== -1) list.splice(i, 1);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
265
503
|
|
|
266
|
-
|
|
267
|
-
|
|
504
|
+
// setup() runs before the component mounts
|
|
505
|
+
setup() {
|
|
506
|
+
effect(() => {
|
|
507
|
+
console.log("Cart has", this.items().length, "items");
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
268
511
|
|
|
269
|
-
|
|
512
|
+
// 2. Parent creates the scope
|
|
513
|
+
const Cart = () => {
|
|
514
|
+
const { items } = useScope(CartScope);
|
|
270
515
|
|
|
271
516
|
return () => html`
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
517
|
+
<div>
|
|
518
|
+
<p>${items().length} item(s) in cart</p>
|
|
519
|
+
${x(ProductList)}
|
|
520
|
+
${x(CartSummary)}
|
|
521
|
+
</div>
|
|
275
522
|
`;
|
|
276
|
-
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// 3. Any descendant reads the scope — no props needed
|
|
526
|
+
const ProductList = () => {
|
|
527
|
+
const { add } = getParentScope(CartScope);
|
|
528
|
+
|
|
529
|
+
return () => html`
|
|
530
|
+
<ul>
|
|
531
|
+
<li><button @click=${() => add("Apple")}>Add Apple</button></li>
|
|
532
|
+
<li><button @click=${() => add("Banana")}>Add Banana</button></li>
|
|
533
|
+
</ul>
|
|
534
|
+
`;
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const CartSummary = () => {
|
|
538
|
+
const { items, remove } = getParentScope(CartScope);
|
|
539
|
+
|
|
540
|
+
return () => html`
|
|
541
|
+
<ul>
|
|
542
|
+
${repeat(
|
|
543
|
+
items(),
|
|
544
|
+
(item) => item,
|
|
545
|
+
(item) => html`
|
|
546
|
+
<li>${item} <button @click=${() => remove(item)}>✕</button></li>
|
|
547
|
+
`,
|
|
548
|
+
)}
|
|
549
|
+
</ul>
|
|
550
|
+
`;
|
|
551
|
+
};
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### `setup()` — scope initialization
|
|
555
|
+
|
|
556
|
+
Add a `setup()` method to a scope class to run initialization logic (effects, timers, subscriptions, lifecycle hooks) before the host component mounts:
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
class TimerScope {
|
|
560
|
+
seconds = atom(0);
|
|
561
|
+
|
|
562
|
+
setup() {
|
|
563
|
+
// Lifecycle hooks work inside setup()
|
|
564
|
+
onMount(() => {
|
|
565
|
+
console.log("timer started");
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
onInterval(() => {
|
|
569
|
+
this.seconds.set((n) => n + 1);
|
|
570
|
+
}, 1000);
|
|
571
|
+
|
|
572
|
+
onCleanup(() => {
|
|
573
|
+
console.log("timer stopped");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Effects are auto-disposed with the component
|
|
577
|
+
effect(() => {
|
|
578
|
+
document.title = `${this.seconds()}s elapsed`;
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
277
582
|
```
|
|
278
583
|
|
|
279
|
-
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Lifecycle Hooks
|
|
280
587
|
|
|
281
|
-
|
|
588
|
+
Lifecycle hooks must be called in the component's **outer function** (or in a scope's `setup()` method). They are automatically cleaned up when the component unmounts.
|
|
282
589
|
|
|
283
590
|
```typescript
|
|
284
|
-
import { html } from "
|
|
591
|
+
import { atom, html, onMount, onCleanup, onPaint } from "mates";
|
|
285
592
|
|
|
286
|
-
|
|
593
|
+
const Timer = () => {
|
|
594
|
+
const seconds = atom(0);
|
|
287
595
|
|
|
288
|
-
//
|
|
596
|
+
// Runs after the component's first render
|
|
597
|
+
onMount(() => {
|
|
598
|
+
const id = setInterval(() => seconds.set((n) => n + 1), 1000);
|
|
289
599
|
|
|
290
|
-
|
|
291
|
-
|
|
600
|
+
// Return a cleanup function — called on unmount
|
|
601
|
+
return () => clearInterval(id);
|
|
602
|
+
});
|
|
292
603
|
|
|
293
|
-
|
|
604
|
+
// Runs after every browser paint (double-RAF)
|
|
605
|
+
onPaint(() => {
|
|
606
|
+
console.log("painted");
|
|
607
|
+
});
|
|
294
608
|
|
|
295
|
-
|
|
296
|
-
|
|
609
|
+
// Explicit cleanup — equivalent to returning a fn from onMount
|
|
610
|
+
onCleanup(() => {
|
|
611
|
+
console.log("component removed");
|
|
297
612
|
});
|
|
298
613
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
614
|
+
return () => html`<p>Elapsed: ${seconds()}s</p>`;
|
|
615
|
+
};
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
| Hook | Timing | Notes |
|
|
619
|
+
|------|--------|-------|
|
|
620
|
+
| `onMount(fn)` | After first render | Return a fn to run on cleanup |
|
|
621
|
+
| `onCleanup(fn)` | On unmount | Equivalent to `onMount` cleanup return |
|
|
622
|
+
| `onPaint(fn)` | After browser paint | Double RAF — element is measured |
|
|
623
|
+
| `onAllMount(fn)` | After all children mount | Safe for cross-component reads |
|
|
624
|
+
| `onError(fn)` | On component error | Receives the Error object |
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## DOM & Window Hooks
|
|
302
629
|
|
|
303
|
-
|
|
630
|
+
These hooks attach listeners **scoped to the component**. They automatically detach when the component unmounts.
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
import {
|
|
634
|
+
onDOMReady,
|
|
635
|
+
onKeyDown,
|
|
636
|
+
onWindowResize,
|
|
637
|
+
onVisibilityChange,
|
|
638
|
+
onInterval,
|
|
639
|
+
onTimeout,
|
|
640
|
+
onNavigate,
|
|
641
|
+
onClickAway,
|
|
642
|
+
onFileDrop,
|
|
643
|
+
onScroll,
|
|
644
|
+
onResize,
|
|
645
|
+
onStorageChange,
|
|
646
|
+
onOnline,
|
|
647
|
+
onOffline,
|
|
648
|
+
} from "mates";
|
|
649
|
+
|
|
650
|
+
const SearchBar = () => {
|
|
651
|
+
const query = atom("");
|
|
652
|
+
|
|
653
|
+
// Fires on every keydown anywhere on the page
|
|
654
|
+
onKeyDown((e) => {
|
|
655
|
+
if (e.key === "/" && !e.target.matches("input")) {
|
|
656
|
+
e.preventDefault();
|
|
657
|
+
inputRef.el?.focus();
|
|
304
658
|
}
|
|
305
659
|
});
|
|
306
660
|
|
|
307
|
-
|
|
308
|
-
|
|
661
|
+
// Fires when the browser window is resized
|
|
662
|
+
onWindowResize((e) => {
|
|
663
|
+
console.log("resized", window.innerWidth);
|
|
309
664
|
});
|
|
310
665
|
|
|
311
|
-
|
|
312
|
-
|
|
666
|
+
// Fires when the tab is hidden or shown
|
|
667
|
+
onVisibilityChange((hidden) => {
|
|
668
|
+
if (hidden) pauseSearch();
|
|
313
669
|
});
|
|
314
670
|
|
|
315
|
-
|
|
316
|
-
|
|
671
|
+
// setInterval — auto-cleared on unmount
|
|
672
|
+
onInterval(() => {
|
|
673
|
+
refreshResults();
|
|
674
|
+
}, 30_000);
|
|
317
675
|
|
|
318
|
-
|
|
676
|
+
// setTimeout — auto-cleared on unmount
|
|
677
|
+
onTimeout(() => {
|
|
678
|
+
showTip();
|
|
679
|
+
}, 2_000);
|
|
680
|
+
|
|
681
|
+
// Fires on every pathAtom change (client-side navigation)
|
|
682
|
+
onNavigate((path) => {
|
|
683
|
+
console.log("navigated to", path);
|
|
684
|
+
});
|
|
319
685
|
|
|
320
|
-
|
|
686
|
+
// Fires when a click happens outside the component's host element
|
|
687
|
+
onClickAway(() => {
|
|
688
|
+
closeDropdown();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Fires when files are dragged + dropped onto the window
|
|
692
|
+
onFileDrop((files) => {
|
|
693
|
+
handleUpload(files);
|
|
694
|
+
});
|
|
321
695
|
|
|
322
|
-
|
|
696
|
+
// Network status
|
|
697
|
+
onOnline(() => syncPendingChanges());
|
|
698
|
+
onOffline(() => showOfflineBanner());
|
|
699
|
+
|
|
700
|
+
return () => html`...`;
|
|
701
|
+
};
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
| Hook | Trigger |
|
|
705
|
+
|------|---------|
|
|
706
|
+
| `onWindow(event, fn)` | Any `window` event |
|
|
707
|
+
| `onWindowScroll(fn)` | Window scroll |
|
|
708
|
+
| `onWindowResize(fn)` | Window resize |
|
|
709
|
+
| `onKeyDown(fn)` | Global `keydown` |
|
|
710
|
+
| `onKeyUp(fn)` | Global `keyup` |
|
|
711
|
+
| `onVisibilityChange(fn)` | Tab show/hide, receives `hidden: boolean` |
|
|
712
|
+
| `onClickAway(fn)` | Click outside host element |
|
|
713
|
+
| `onFileDrop(fn)` | Window file drop |
|
|
714
|
+
| `onScroll(fn, target?)` | Scroll on window or a specific element |
|
|
715
|
+
| `onResize(fn, target?)` | `ResizeObserver` on host element or a specific element |
|
|
716
|
+
| `onNavigate(fn)` | Path changes |
|
|
717
|
+
| `onInterval(fn, ms)` | Repeating timer |
|
|
718
|
+
| `onTimeout(fn, ms)` | One-shot timer |
|
|
719
|
+
| `onOnline(fn)` | Browser goes online |
|
|
720
|
+
| `onOffline(fn)` | Browser goes offline |
|
|
721
|
+
| `onStorageChange(fn)` | Cross-tab `localStorage` changes |
|
|
722
|
+
| `onPaste(fn)` | Clipboard paste |
|
|
723
|
+
| `onCopy(fn)` | Clipboard copy |
|
|
724
|
+
| `onCut(fn)` | Clipboard cut |
|
|
725
|
+
| `onSelectionChange(fn)` | Text selection changes |
|
|
726
|
+
| `onSocket(fn, sockets)` | WebSocket messages (see WebSocket section) |
|
|
727
|
+
| `onUpdate(fn)` | Every re-render of the component |
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## Async Actions
|
|
732
|
+
|
|
733
|
+
### `asyncAction`
|
|
734
|
+
|
|
735
|
+
`asyncAction` wraps an async function and gives it a complete **loading / error / data state machine** with atoms, automatic cancellation of stale calls, caching, polling, and interceptors — all built in.
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
import { asyncAction, html, x, renderX } from "mates";
|
|
739
|
+
import type { Props } from "mates";
|
|
323
740
|
|
|
324
|
-
|
|
741
|
+
const fetchUser = asyncAction(async (id: number) => {
|
|
742
|
+
const res = await fetch(`/api/users/${id}`);
|
|
743
|
+
if (!res.ok) throw new Error("Not found");
|
|
744
|
+
return res.json() as Promise<{ id: number; name: string; email: string }>;
|
|
745
|
+
});
|
|
325
746
|
|
|
326
|
-
|
|
747
|
+
const UserCard = (propsFn: Props<{ userId: number }>) => {
|
|
748
|
+
// Load data when the component mounts
|
|
749
|
+
onMount(() => {
|
|
750
|
+
fetchUser(propsFn().userId);
|
|
327
751
|
});
|
|
752
|
+
|
|
753
|
+
return () => html`
|
|
754
|
+
<div class="card">
|
|
755
|
+
${fetchUser.isLoading()
|
|
756
|
+
? html`<p>Loading…</p>`
|
|
757
|
+
: fetchUser.error()
|
|
758
|
+
? html`<p class="error">${fetchUser.error()!.message}</p>`
|
|
759
|
+
: fetchUser.data()
|
|
760
|
+
? html`
|
|
761
|
+
<h2>${fetchUser.data()!.name}</h2>
|
|
762
|
+
<p>${fetchUser.data()!.email}</p>
|
|
763
|
+
`
|
|
764
|
+
: html`<p>No user loaded</p>`}
|
|
765
|
+
<button @click=${() => fetchUser(propsFn().userId)}>Reload</button>
|
|
766
|
+
</div>
|
|
767
|
+
`;
|
|
768
|
+
};
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
**State atoms on every `asyncAction`:**
|
|
772
|
+
|
|
773
|
+
| Atom | Type | Description |
|
|
774
|
+
|------|------|-------------|
|
|
775
|
+
| `.data` | `AtomType<T \| null>` | The resolved value |
|
|
776
|
+
| `.error` | `AtomType<Error \| null>` | The rejection error |
|
|
777
|
+
| `.isLoading` | `AtomType<boolean>` | `true` while in flight |
|
|
778
|
+
| `.status` | `AtomType<"init" \| "loading" \| "success" \| "error">` | Full state machine status |
|
|
779
|
+
|
|
780
|
+
**Methods on every `asyncAction`:**
|
|
781
|
+
|
|
782
|
+
| Method | Description |
|
|
783
|
+
|--------|-------------|
|
|
784
|
+
| `.cancel()` | Abort the current in-flight call |
|
|
785
|
+
| `.cache(...args)` | Call and cache result by args (LRU, configurable size + TTL) |
|
|
786
|
+
| `.clearCache(...args)` | Evict specific cached entries |
|
|
787
|
+
| `.startPolling(...args)` | Begin polling the function on a fixed interval |
|
|
788
|
+
| `.stopPolling()` | Stop polling |
|
|
789
|
+
| `.subscribe(fn)` | Subscribe to completion events |
|
|
790
|
+
| `.interceptBefore(fn)` | Middleware — transform args before the function runs |
|
|
791
|
+
| `.interceptAfter(fn)` | Transform the resolved value before it hits `.data` |
|
|
792
|
+
|
|
793
|
+
**Options:**
|
|
794
|
+
|
|
795
|
+
```typescript
|
|
796
|
+
const fetchData = asyncAction(myFn, {
|
|
797
|
+
cacheLimit: 20, // Max LRU entries (default: 10)
|
|
798
|
+
cacheDuration: 60_000, // Cache TTL in ms (default: infinite)
|
|
799
|
+
pollInterval: 5_000, // Polling interval in ms (default: 5000)
|
|
328
800
|
});
|
|
801
|
+
```
|
|
329
802
|
|
|
330
|
-
|
|
803
|
+
**Stale-call cancellation is automatic.** If you call the action a second time before the first resolves, the first call is aborted and its result is discarded. Only the latest call's data is ever written to `.data`.
|
|
331
804
|
|
|
332
|
-
|
|
333
|
-
return () => {
|
|
334
|
-
const {
|
|
335
|
-
items,
|
|
805
|
+
---
|
|
336
806
|
|
|
337
|
-
|
|
807
|
+
### `action` — synchronous action
|
|
338
808
|
|
|
339
|
-
|
|
809
|
+
`action` wraps a synchronous function with subscriber notifications, `interceptBefore`/`interceptAfter` middleware, and hot-swappable implementation.
|
|
340
810
|
|
|
341
|
-
|
|
811
|
+
```typescript
|
|
812
|
+
import { action } from "mates";
|
|
342
813
|
|
|
343
|
-
|
|
814
|
+
const addToCart = action((productId: string, quantity: number) => {
|
|
815
|
+
cart.update((c) => { c[productId] = (c[productId] ?? 0) + quantity; });
|
|
816
|
+
return { productId, quantity };
|
|
817
|
+
});
|
|
344
818
|
|
|
345
|
-
|
|
346
|
-
|
|
819
|
+
// Subscribe to every call
|
|
820
|
+
addToCart.__subscribe((result) => {
|
|
821
|
+
console.log("Added:", result);
|
|
822
|
+
analytics.track("add_to_cart", result);
|
|
823
|
+
});
|
|
347
824
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
825
|
+
// Add middleware
|
|
826
|
+
addToCart.interceptBefore((next, productId, quantity) => {
|
|
827
|
+
if (quantity < 1) throw new Error("Quantity must be positive");
|
|
828
|
+
return next(productId, quantity);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
addToCart("prod_123", 2);
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
### `paginatedAsyncAction`
|
|
837
|
+
|
|
838
|
+
`paginatedAsyncAction` extends `asyncAction` with a built-in `page` atom and a `next()` helper.
|
|
839
|
+
|
|
840
|
+
```typescript
|
|
841
|
+
import { paginatedAsyncAction } from "mates";
|
|
842
|
+
|
|
843
|
+
const fetchPosts = paginatedAsyncAction(async () => {
|
|
844
|
+
const page = fetchPosts.page();
|
|
845
|
+
const res = await fetch(`/api/posts?page=${page}&limit=10`);
|
|
846
|
+
return res.json() as Promise<Post[]>;
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// Load first page
|
|
850
|
+
fetchPosts();
|
|
851
|
+
|
|
852
|
+
// Load next page
|
|
853
|
+
fetchPosts.next();
|
|
854
|
+
|
|
855
|
+
// Jump to page
|
|
856
|
+
fetchPosts.page.set(3);
|
|
857
|
+
fetchPosts();
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
Extra atoms: `fetchPosts.page` (`AtomType<number>`), `fetchPosts.totalPages` (`AtomType<number>`).
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
### `taskAction`
|
|
865
|
+
|
|
866
|
+
`taskAction` queues async tasks and runs them one at a time, tracking the running status of each individual task.
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
import { taskAction } from "mates";
|
|
870
|
+
|
|
871
|
+
const processFile = taskAction(async (file: File) => {
|
|
872
|
+
const result = await uploadFile(file);
|
|
873
|
+
return result.url;
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// Queue multiple files — they run serially
|
|
877
|
+
processFile(file1);
|
|
878
|
+
processFile(file2);
|
|
879
|
+
processFile(file3);
|
|
880
|
+
|
|
881
|
+
// Check overall status
|
|
882
|
+
processFile.status(); // "loading" | "success" | "error" | "init"
|
|
883
|
+
processFile.data(); // result of the last completed task
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
## HTTP Client
|
|
889
|
+
|
|
890
|
+
Mates includes a first-party HTTP client with interceptors, URL template substitution, automatic JSON serialization, SSR support, and `asyncAction` integration.
|
|
891
|
+
|
|
892
|
+
### Basic usage
|
|
893
|
+
|
|
894
|
+
```typescript
|
|
895
|
+
import { Fetch, Get, Post, Put, Patch, Delete } from "mates";
|
|
896
|
+
|
|
897
|
+
// GET with query params
|
|
898
|
+
const users = await Get({ url: "/api/users", params: { page: 1, limit: 10 } });
|
|
899
|
+
|
|
900
|
+
// POST with a JSON body
|
|
901
|
+
const created = await Post({
|
|
902
|
+
url: "/api/users",
|
|
903
|
+
body: { name: "Alice", email: "alice@example.com" },
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// URL template substitution — :id is replaced, extra params become query string
|
|
907
|
+
const user = await Get({ url: "/api/users/:id", params: { id: 42, include: "posts" } });
|
|
908
|
+
// → GET /api/users/42?include=posts
|
|
909
|
+
|
|
910
|
+
// Shorthand — pass just a URL string
|
|
911
|
+
const data = await Fetch("/api/health");
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
### FetchClient — per-instance configuration
|
|
915
|
+
|
|
916
|
+
Create a dedicated client for each API with a shared base config and per-instance interceptors:
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
import { FetchClient } from "mates";
|
|
920
|
+
|
|
921
|
+
const api = new FetchClient({
|
|
922
|
+
host: "https://api.example.com",
|
|
923
|
+
headers: { "Accept": "application/json" },
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// Add auth to every request
|
|
927
|
+
api.interceptBefore((url, opts) => ({
|
|
928
|
+
url,
|
|
929
|
+
options: {
|
|
930
|
+
...opts,
|
|
931
|
+
headers: { ...opts.headers, Authorization: `Bearer ${getToken()}` },
|
|
932
|
+
},
|
|
933
|
+
}));
|
|
934
|
+
|
|
935
|
+
// Log every error
|
|
936
|
+
api.interceptError((error) => {
|
|
937
|
+
logger.error(error);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
const users = await api.Get({ url: "/users" });
|
|
941
|
+
const me = await api.Get({ url: "/users/me" });
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### `fetchAction` / HTTP action shortcuts
|
|
945
|
+
|
|
946
|
+
Combine the HTTP client with `asyncAction` in one line:
|
|
947
|
+
|
|
948
|
+
```typescript
|
|
949
|
+
import { getAction, postAction, deleteAction } from "mates";
|
|
950
|
+
|
|
951
|
+
// getAction creates an asyncAction that calls GET
|
|
952
|
+
const loadUsers = getAction({ url: "/api/users" });
|
|
953
|
+
loadUsers();
|
|
954
|
+
|
|
955
|
+
// postAction with dynamic body
|
|
956
|
+
const createUser = postAction<User>();
|
|
957
|
+
createUser.interceptBefore((next) => next({ url: "/api/users", body: form() }));
|
|
958
|
+
createUser();
|
|
959
|
+
|
|
960
|
+
// Per-client action
|
|
961
|
+
const api = new FetchClient({ host: "https://api.example.com" });
|
|
962
|
+
const loadProfile = api.getAction({ url: "/users/me" });
|
|
963
|
+
loadProfile();
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
---
|
|
967
|
+
|
|
968
|
+
## Routing
|
|
969
|
+
|
|
970
|
+
### Navigation
|
|
971
|
+
|
|
972
|
+
```typescript
|
|
973
|
+
import { navigateTo, pathAtom, qsAtom, hashAtom, location } from "mates";
|
|
974
|
+
|
|
975
|
+
// Push a new history entry
|
|
976
|
+
navigateTo("/about");
|
|
977
|
+
|
|
978
|
+
// Replace current history entry (no back button entry)
|
|
979
|
+
navigateTo("/login", true);
|
|
980
|
+
|
|
981
|
+
// With history state data
|
|
982
|
+
navigateTo("/checkout", false, { step: 2 });
|
|
983
|
+
|
|
984
|
+
// Read current location reactively
|
|
985
|
+
pathAtom(); // "/about"
|
|
986
|
+
qsAtom(); // { q: "search term" } — parsed query string
|
|
987
|
+
hashAtom(); // "#section-2"
|
|
988
|
+
location.pathname; // "/about"
|
|
989
|
+
|
|
990
|
+
// Update query string (replaces history state)
|
|
991
|
+
qsAtom.set({ q: "mates framework", page: 2 });
|
|
992
|
+
|
|
993
|
+
// Navigation lock — prevents navigation (e.g. unsaved changes)
|
|
994
|
+
lockNavigation();
|
|
995
|
+
unlockNavigation();
|
|
996
|
+
navigationLocked(); // true/false — reactive
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
### `Router` — declarative route table
|
|
1000
|
+
|
|
1001
|
+
```typescript
|
|
1002
|
+
import { Router, navigateTo, html, renderX } from "mates";
|
|
1003
|
+
|
|
1004
|
+
// Define components
|
|
1005
|
+
const HomePage = () => () => html`<h1>Home</h1>`;
|
|
1006
|
+
const AboutPage = () => () => html`<h1>About</h1>`;
|
|
1007
|
+
const UserPage = (p: Props<{}>) => () => html`<h1>User</h1>`;
|
|
1008
|
+
const NotFound = () => () => html`<h1>404 — Not Found</h1>`;
|
|
1009
|
+
|
|
1010
|
+
// Create the router
|
|
1011
|
+
const appRouter = Router([
|
|
1012
|
+
{ path: "/", component: HomePage },
|
|
1013
|
+
{ path: "/about", component: AboutPage },
|
|
1014
|
+
{ path: "/users/:id", component: UserPage },
|
|
1015
|
+
// Lazy-loaded route — code-split automatically
|
|
1016
|
+
{ path: "/dashboard", component: async () => import("./Dashboard") },
|
|
1017
|
+
], NotFound);
|
|
1018
|
+
|
|
1019
|
+
// Mount
|
|
1020
|
+
const App = () => () => html`
|
|
1021
|
+
<nav>
|
|
1022
|
+
<a @click=${() => navigateTo("/")}>Home</a>
|
|
1023
|
+
<a @click=${() => navigateTo("/about")}>About</a>
|
|
1024
|
+
</nav>
|
|
1025
|
+
<main>${appRouter()}</main>
|
|
1026
|
+
`;
|
|
1027
|
+
|
|
1028
|
+
renderX(App, document.getElementById("app")!);
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### `route` — inline conditional routing
|
|
1032
|
+
|
|
1033
|
+
For simpler scenarios, use `route` directly in a template:
|
|
1034
|
+
|
|
1035
|
+
```typescript
|
|
1036
|
+
import { route, navigateTo, html } from "mates";
|
|
1037
|
+
|
|
1038
|
+
const App = () => () => html`
|
|
1039
|
+
${route("/", { view: HomePage })}
|
|
1040
|
+
${route("/about", { view: AboutPage })}
|
|
1041
|
+
${route("/users/:id", { view: UserPage })}
|
|
1042
|
+
`;
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
### `animatedRouter` — page transitions
|
|
1046
|
+
|
|
1047
|
+
```typescript
|
|
1048
|
+
import { animatedRouter, fadeInPreset, fadeOutPreset } from "mates";
|
|
1049
|
+
|
|
1050
|
+
const router = animatedRouter(routes, {
|
|
1051
|
+
enter: fadeInPreset,
|
|
1052
|
+
exit: fadeOutPreset,
|
|
1053
|
+
scrollToTop: true,
|
|
1054
|
+
});
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### `isPathMatching` — pattern testing
|
|
1058
|
+
|
|
1059
|
+
```typescript
|
|
1060
|
+
import { isPathMatching } from "mates";
|
|
1061
|
+
|
|
1062
|
+
isPathMatching("/"); // true when path is exactly "/"
|
|
1063
|
+
isPathMatching("/users/:id"); // true for "/users/42", "/users/abc", etc.
|
|
1064
|
+
isPathMatching("/posts/:id"); // false when on "/users/42"
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
### `buildPath` — fill URL templates
|
|
1068
|
+
|
|
1069
|
+
```typescript
|
|
1070
|
+
import { buildPath } from "mates";
|
|
1071
|
+
|
|
1072
|
+
buildPath("/users/:id/posts/:postId", { id: 42, postId: 7, highlight: true });
|
|
1073
|
+
// → "/users/42/posts/7?highlight=true"
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
---
|
|
1077
|
+
|
|
1078
|
+
## CSS-in-JS & Theming
|
|
1079
|
+
|
|
1080
|
+
### Scoped stylesheets with `stylesheet`
|
|
1081
|
+
|
|
1082
|
+
`stylesheet()` creates a **scoped** CSS-in-JS instance. Each call generates unique class names so styles from different components never collide. Call it at module level, then call `mount()` inside the component.
|
|
1083
|
+
|
|
1084
|
+
```typescript
|
|
1085
|
+
import { stylesheet, html, renderX } from "mates";
|
|
1086
|
+
|
|
1087
|
+
// Module-level — created once
|
|
1088
|
+
const { css, mount, keyframes } = stylesheet();
|
|
1089
|
+
|
|
1090
|
+
// Define an animation
|
|
1091
|
+
const spin = keyframes("spin", {
|
|
1092
|
+
from: { transform: "rotate(0deg)" },
|
|
1093
|
+
to: { transform: "rotate(360deg)" },
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// Define styles
|
|
1097
|
+
const cl = css({
|
|
1098
|
+
card: {
|
|
1099
|
+
display: "flex",
|
|
1100
|
+
flexDirection: "column",
|
|
1101
|
+
padding: "1.5rem",
|
|
1102
|
+
borderRadius: "12px",
|
|
1103
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
|
|
1104
|
+
"&:hover": { boxShadow: "0 4px 16px rgba(0,0,0,0.15)" },
|
|
1105
|
+
"md": { padding: "2rem" }, // breakpoint shorthand
|
|
1106
|
+
},
|
|
1107
|
+
title: { fontSize: "1.25rem", fontWeight: "600" },
|
|
1108
|
+
loader: { animation: `${spin} 1s linear infinite` },
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
const Card = () => {
|
|
1112
|
+
mount(); // injects styles when component mounts, removes on unmount
|
|
1113
|
+
|
|
1114
|
+
return () => html`
|
|
1115
|
+
<div class="${cl.card}">
|
|
1116
|
+
<h2 class="${cl.title}">Hello</h2>
|
|
1117
|
+
</div>
|
|
1118
|
+
`;
|
|
1119
|
+
};
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
**Supported nested keys inside a block:**
|
|
1123
|
+
|
|
1124
|
+
| Key pattern | Compiled to |
|
|
1125
|
+
|-------------|-------------|
|
|
1126
|
+
| `"&:hover"` | `.block:hover { … }` |
|
|
1127
|
+
| `"&::before"` | `.block::before { … }` |
|
|
1128
|
+
| `"&[disabled]"` | `.block[disabled] { … }` |
|
|
1129
|
+
| `"sm"`, `"md"`, `"lg"`, `"xl"`, `"2xl"` | `@media (min-width: …) { … }` |
|
|
1130
|
+
| `"@media (…)"` | `@media (…) { … }` |
|
|
1131
|
+
|
|
1132
|
+
Configure custom breakpoints globally:
|
|
1133
|
+
|
|
1134
|
+
```typescript
|
|
1135
|
+
import { configureCSS } from "mates";
|
|
1136
|
+
|
|
1137
|
+
configureCSS({ breakpoints: { tablet: "900px", desktop: "1200px" } });
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
### Global styles with `globalCSS`
|
|
1141
|
+
|
|
1142
|
+
```typescript
|
|
1143
|
+
import { globalCSS } from "mates";
|
|
1144
|
+
|
|
1145
|
+
// Singleton — injected once into <head>
|
|
1146
|
+
const g = globalCSS({
|
|
1147
|
+
body: { margin: "0", fontFamily: "'Inter', sans-serif" },
|
|
1148
|
+
"*, *::before, *::after": { boxSizing: "border-box" },
|
|
1149
|
+
a: { color: "inherit", textDecoration: "none" },
|
|
1150
|
+
});
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
### Design tokens and theming with `globalTheme`
|
|
1154
|
+
|
|
1155
|
+
```typescript
|
|
1156
|
+
import { globalTheme } from "mates";
|
|
1157
|
+
|
|
1158
|
+
const { cssVars, themeAtom } = globalTheme({
|
|
1159
|
+
light: {
|
|
1160
|
+
primary: "#3b82f6",
|
|
1161
|
+
background: "#ffffff",
|
|
1162
|
+
surface: "#f8fafc",
|
|
1163
|
+
text: "#111827",
|
|
1164
|
+
border: "#e5e7eb",
|
|
1165
|
+
},
|
|
1166
|
+
dark: {
|
|
1167
|
+
primary: "#60a5fa",
|
|
1168
|
+
background: "#0f172a",
|
|
1169
|
+
surface: "#1e293b",
|
|
1170
|
+
text: "#f1f5f9",
|
|
1171
|
+
border: "#334155",
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
// cssVars maps token names to CSS custom property strings
|
|
1176
|
+
// cssVars.primary → "--primary" cssVars.background → "--background"
|
|
1177
|
+
|
|
1178
|
+
// Use in stylesheet
|
|
1179
|
+
const cl = css({
|
|
1180
|
+
button: {
|
|
1181
|
+
backgroundColor: `var(${cssVars.primary})`,
|
|
1182
|
+
color: `var(${cssVars.background})`,
|
|
1183
|
+
},
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
// Switch theme reactively
|
|
1187
|
+
themeAtom.set("dark"); // adds data-theme="dark" to <html>
|
|
1188
|
+
themeAtom.set("auto"); // uses OS preference via prefers-color-scheme
|
|
1189
|
+
themeAtom.set("light"); // explicit light
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
`globalTheme` injects:
|
|
1193
|
+
- `:root { ... }` — first theme as base variables
|
|
1194
|
+
- `[data-theme="name"] { ... }` — each theme explicitly
|
|
1195
|
+
- `@media (prefers-color-scheme: dark) { :root:not([data-theme]) { ... } }` — OS auto mode
|
|
1196
|
+
|
|
1197
|
+
### `cl` helper — conditional class names
|
|
1198
|
+
|
|
1199
|
+
```typescript
|
|
1200
|
+
import { cl } from "mates";
|
|
1201
|
+
|
|
1202
|
+
// Merge conditional class names into a single string
|
|
1203
|
+
const classes = cl(
|
|
1204
|
+
styles.btn,
|
|
1205
|
+
isActive && styles.active,
|
|
1206
|
+
isLoading && styles.loading,
|
|
1207
|
+
);
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1212
|
+
## Directives
|
|
1213
|
+
|
|
1214
|
+
Directives are attribute and child-position bindings for `lit-html` templates.
|
|
1215
|
+
|
|
1216
|
+
### DOM manipulation directives
|
|
1217
|
+
|
|
1218
|
+
```typescript
|
|
1219
|
+
import { attr, style, classes } from "mates";
|
|
1220
|
+
|
|
1221
|
+
html`
|
|
1222
|
+
<!-- Set / remove attributes declaratively -->
|
|
1223
|
+
<div ${attr({ id: "main", "aria-label": "content", disabled: isDisabled })}>
|
|
1224
|
+
|
|
1225
|
+
<!-- Apply inline styles -->
|
|
1226
|
+
<div ${style({ color: "red", display: isVisible ? "block" : "none" })}>
|
|
1227
|
+
|
|
1228
|
+
<!-- Conditional class management -->
|
|
1229
|
+
<button ${classes([
|
|
1230
|
+
"btn",
|
|
1231
|
+
[isPrimary, "btn-primary", "btn-secondary"], // ternary tuple
|
|
1232
|
+
[isLoading, "btn-loading"], // conditional tuple
|
|
1233
|
+
isDisabled && "btn-disabled", // falsy-safe expression
|
|
1234
|
+
])}>
|
|
1235
|
+
`
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
### Lifecycle directives
|
|
1239
|
+
|
|
1240
|
+
```typescript
|
|
1241
|
+
import { onConnect, onDisconnect, onUpdate, onIntersect, onVisible, lazyLoad } from "mates";
|
|
1242
|
+
|
|
1243
|
+
html`
|
|
1244
|
+
<!-- React to element entering/leaving the DOM -->
|
|
1245
|
+
<div ${onConnect((el) => console.log("connected", el))}>
|
|
1246
|
+
<div ${onDisconnect(() => cleanup())}>
|
|
1247
|
+
|
|
1248
|
+
<!-- Re-run on every re-render of this node -->
|
|
1249
|
+
<canvas ${onUpdate((el) => drawChart(el))}>
|
|
1250
|
+
|
|
1251
|
+
<!-- IntersectionObserver -->
|
|
1252
|
+
<section ${onIntersect({
|
|
1253
|
+
onVisible: (el) => el.classList.add("visible"),
|
|
1254
|
+
onHidden: (el) => el.classList.remove("visible"),
|
|
1255
|
+
rootMargin: "0px 0px -100px 0px",
|
|
1256
|
+
})}>
|
|
1257
|
+
|
|
1258
|
+
<!-- Lazy load when scrolled into view -->
|
|
1259
|
+
<img ${lazyLoad((el) => {
|
|
1260
|
+
(el as HTMLImageElement).src = el.dataset.src!;
|
|
1261
|
+
})} data-src="/images/photo.jpg" />
|
|
1262
|
+
`
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
### Rendering helpers
|
|
1266
|
+
|
|
1267
|
+
```typescript
|
|
1268
|
+
import { renderSwitch, animatedIf } from "mates";
|
|
1269
|
+
|
|
1270
|
+
// Render first matching case
|
|
1271
|
+
html`${renderSwitch([
|
|
1272
|
+
[status() === "loading", html`<spinner-el></spinner-el>`],
|
|
1273
|
+
[status() === "error", html`<error-msg .msg=${error()}></error-msg>`],
|
|
1274
|
+
[status() === "success", html`<user-card .user=${data()}></user-card>`],
|
|
1275
|
+
html`<p>Idle</p>`, // default fallback
|
|
1276
|
+
])}`
|
|
1277
|
+
|
|
1278
|
+
// Conditional rendering with animated transitions
|
|
1279
|
+
html`${animatedIf(
|
|
1280
|
+
isOpen(),
|
|
1281
|
+
() => html`<div class="panel">...</div>`,
|
|
1282
|
+
undefined,
|
|
1283
|
+
{ enter: fadeInPreset, exit: fadeOutPreset },
|
|
1284
|
+
)}`
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
### Event directives
|
|
1288
|
+
|
|
1289
|
+
```typescript
|
|
1290
|
+
import { on } from "mates";
|
|
1291
|
+
|
|
1292
|
+
html`
|
|
1293
|
+
<!-- Declarative event map — cleaned up automatically -->
|
|
1294
|
+
<div ${on({ click: handleClick, mouseover: handleHover })}>
|
|
1295
|
+
`
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
### `eleHook` / `htmlHook` — custom directives
|
|
1299
|
+
|
|
1300
|
+
Build your own reusable directives using the low-level hooks:
|
|
1301
|
+
|
|
1302
|
+
```typescript
|
|
1303
|
+
import { eleHook, htmlHook } from "mates";
|
|
1304
|
+
|
|
1305
|
+
// eleHook — bound to a real element
|
|
1306
|
+
const autoFocus = eleHook(($) => {
|
|
1307
|
+
($.el as HTMLElement).focus();
|
|
1308
|
+
|
|
1309
|
+
return {
|
|
1310
|
+
onCleanup() { /* cleanup */ },
|
|
1311
|
+
};
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
html`<input ${autoFocus()} />`
|
|
1315
|
+
|
|
1316
|
+
// htmlHook — for inline content slots (no element required)
|
|
1317
|
+
const liveTime = htmlHook((render) => {
|
|
1318
|
+
const tick = () => render(html`<span>${new Date().toLocaleTimeString()}</span>`);
|
|
1319
|
+
tick();
|
|
1320
|
+
const id = setInterval(tick, 1000);
|
|
1321
|
+
return { onCleanup: () => clearInterval(id) };
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
html`<p>Current time: ${liveTime()}</p>`
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
### `$` — fluent DOM chain
|
|
1328
|
+
|
|
1329
|
+
Imperatively manipulate elements inside hooks and lifecycle callbacks:
|
|
1330
|
+
|
|
1331
|
+
```typescript
|
|
1332
|
+
import { $ } from "mates";
|
|
1333
|
+
|
|
1334
|
+
$(el)
|
|
1335
|
+
.attr({ "data-active": "true", "aria-expanded": "false" })
|
|
1336
|
+
.style({ color: "red", transform: `translateX(${offset}px)` })
|
|
1337
|
+
.classes(["base", [isActive, "active"], [isError, "error", "ok"]])
|
|
1338
|
+
.on("click", handleClick)
|
|
1339
|
+
.focus()
|
|
1340
|
+
.scroll({ top: 0, behavior: "smooth" });
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
---
|
|
1344
|
+
|
|
1345
|
+
## Portals, Dialogs & Tooltips
|
|
1346
|
+
|
|
1347
|
+
Render content **outside** the normal DOM tree — useful for modals, toasts, context menus, and tooltips that must escape `overflow: hidden` or `transform` stacking contexts.
|
|
1348
|
+
|
|
1349
|
+
### `portal` — fixed-position overlay
|
|
1350
|
+
|
|
1351
|
+
```typescript
|
|
1352
|
+
import { portal, html } from "mates";
|
|
1353
|
+
|
|
1354
|
+
html`
|
|
1355
|
+
${isToastVisible() && portal(
|
|
1356
|
+
html`<div class="toast">Saved ✓</div>`,
|
|
1357
|
+
{ style: { bottom: "16px", right: "16px", position: "fixed" } },
|
|
1358
|
+
)}
|
|
1359
|
+
`
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
### `dialog` — modal with backdrop
|
|
1363
|
+
|
|
1364
|
+
```typescript
|
|
1365
|
+
import { dialog, html } from "mates";
|
|
1366
|
+
|
|
1367
|
+
html`
|
|
1368
|
+
${isOpen() && dialog(
|
|
1369
|
+
html`
|
|
1370
|
+
<div class="modal">
|
|
1371
|
+
<h2>Confirm Delete</h2>
|
|
1372
|
+
<p>This action cannot be undone.</p>
|
|
1373
|
+
<button @click=${() => isOpen.set(false)}>Cancel</button>
|
|
1374
|
+
<button @click=${doDelete}>Delete</button>
|
|
384
1375
|
</div>
|
|
385
|
-
|
|
1376
|
+
`,
|
|
1377
|
+
{
|
|
1378
|
+
onBackdropClick: () => isOpen.set(false),
|
|
1379
|
+
style: { backdropColor: "rgba(0,0,0,0.6)" },
|
|
1380
|
+
dialogStyle: { borderRadius: "16px", padding: "2rem", maxWidth: "480px" },
|
|
1381
|
+
},
|
|
1382
|
+
)}
|
|
1383
|
+
`
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
`dialog` automatically prevents body scroll while open and restores it exactly on close, even with nested dialogs.
|
|
1387
|
+
|
|
1388
|
+
### `tooltip` / `tip`
|
|
1389
|
+
|
|
1390
|
+
```typescript
|
|
1391
|
+
import { tooltip, html } from "mates";
|
|
1392
|
+
|
|
1393
|
+
html`
|
|
1394
|
+
<!-- Plain string tip -->
|
|
1395
|
+
<button ${tooltip("Save changes (Ctrl+S)")}>Save</button>
|
|
1396
|
+
|
|
1397
|
+
<!-- Rich HTML tip -->
|
|
1398
|
+
<button ${tooltip(html`Press <kbd>Ctrl</kbd> + <kbd>S</kbd>`)}>Save</button>
|
|
1399
|
+
|
|
1400
|
+
<!-- Custom style -->
|
|
1401
|
+
<span ${tooltip("Long description", { maxWidth: "280px", placement: "bottom" })}>
|
|
1402
|
+
Hover me
|
|
1403
|
+
</span>
|
|
1404
|
+
`
|
|
1405
|
+
```
|
|
1406
|
+
|
|
1407
|
+
Tooltips auto-position above/below based on available viewport space. They use a singleton overlay element on `<body>` — no portal proliferation.
|
|
1408
|
+
|
|
1409
|
+
---
|
|
1410
|
+
|
|
1411
|
+
## Animations
|
|
1412
|
+
|
|
1413
|
+
Mates provides a first-party animation system built on the Web Animations API (WAAPI).
|
|
1414
|
+
|
|
1415
|
+
```typescript
|
|
1416
|
+
import {
|
|
1417
|
+
animate,
|
|
1418
|
+
animateDirective,
|
|
1419
|
+
fadeInPreset,
|
|
1420
|
+
fadeOutPreset,
|
|
1421
|
+
slideInPreset,
|
|
1422
|
+
slideOutPreset,
|
|
1423
|
+
scaleInPreset,
|
|
1424
|
+
bouncePreset,
|
|
1425
|
+
springInPreset,
|
|
1426
|
+
withStaggerPreset,
|
|
1427
|
+
} from "mates";
|
|
1428
|
+
|
|
1429
|
+
// Imperative — animate an element directly
|
|
1430
|
+
animate(el, fadeInPreset);
|
|
1431
|
+
animate(el, { keyframes: [{ opacity: 0 }, { opacity: 1 }], duration: 300 });
|
|
1432
|
+
|
|
1433
|
+
// Directive — declaratively apply to a template element
|
|
1434
|
+
html`
|
|
1435
|
+
<div ${animateDirective({ enter: fadeInPreset, exit: fadeOutPreset })}>
|
|
1436
|
+
Content
|
|
1437
|
+
</div>
|
|
1438
|
+
`
|
|
1439
|
+
|
|
1440
|
+
// Stagger children
|
|
1441
|
+
html`
|
|
1442
|
+
<ul ${animateDirective(withStaggerPreset(fadeInPreset, { stagger: 50 }))}>
|
|
1443
|
+
${items.map((i) => html`<li>${i}</li>`)}
|
|
1444
|
+
</ul>
|
|
1445
|
+
`
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
**Built-in animation presets:**
|
|
1449
|
+
|
|
1450
|
+
| Preset | Effect |
|
|
1451
|
+
|--------|--------|
|
|
1452
|
+
| `fadeInPreset` / `fadeOutPreset` | Opacity fade |
|
|
1453
|
+
| `slideInPreset` / `slideOutPreset` | Slide from edge |
|
|
1454
|
+
| `scaleInPreset` / `scaleOutPreset` | Scale from center |
|
|
1455
|
+
| `blurInPreset` / `blurOutPreset` | Blur + fade |
|
|
1456
|
+
| `flipInPreset` / `flipOutPreset` | 3D flip |
|
|
1457
|
+
| `bouncePreset` | Elastic bounce |
|
|
1458
|
+
| `pulsePreset` | Heartbeat pulse |
|
|
1459
|
+
| `shakePreset` | Horizontal shake |
|
|
1460
|
+
| `spinPreset` | Full rotation |
|
|
1461
|
+
| `springInPreset` | Spring physics enter |
|
|
1462
|
+
| `withStaggerPreset` | Wrap any preset with stagger |
|
|
1463
|
+
|
|
1464
|
+
---
|
|
1465
|
+
|
|
1466
|
+
## WebSocket
|
|
1467
|
+
|
|
1468
|
+
```typescript
|
|
1469
|
+
import { ws, onSocket, html } from "mates";
|
|
1470
|
+
|
|
1471
|
+
type ChatMessage = { user: string; text: string; ts: number };
|
|
1472
|
+
|
|
1473
|
+
const Chat = () => {
|
|
1474
|
+
const messages = atom<ChatMessage[]>([]);
|
|
1475
|
+
const input = atom("");
|
|
1476
|
+
|
|
1477
|
+
// Create connection — auto-reconnects, auto-cleanup on unmount
|
|
1478
|
+
const socket = ws<ChatMessage>("wss://api.example.com/chat", {
|
|
1479
|
+
reconnect: true,
|
|
1480
|
+
reconnectDelay: 1_000,
|
|
1481
|
+
reconnectMaxDelay: 30_000,
|
|
1482
|
+
auth: () => ({ token: authToken() }), // refreshed on every reconnect
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
// Subscribe to incoming messages
|
|
1486
|
+
onSocket((msg) => {
|
|
1487
|
+
messages.update((list) => { list.push(msg); });
|
|
1488
|
+
}, [socket]);
|
|
1489
|
+
|
|
1490
|
+
const send = () => {
|
|
1491
|
+
socket.send({ user: "me", text: input(), ts: Date.now() });
|
|
1492
|
+
input.set("");
|
|
386
1493
|
};
|
|
387
|
-
}, {});
|
|
388
1494
|
|
|
389
|
-
|
|
1495
|
+
return () => html`
|
|
1496
|
+
<div class="chat">
|
|
1497
|
+
<p>Status: ${socket.status()}</p>
|
|
1498
|
+
<ul>
|
|
1499
|
+
${messages().map((m) => html`<li><b>${m.user}:</b> ${m.text}</li>`)}
|
|
1500
|
+
</ul>
|
|
1501
|
+
<input .value=${input()} @input=${(e: InputEvent) =>
|
|
1502
|
+
input.set((e.target as HTMLInputElement).value)} />
|
|
1503
|
+
<button @click=${send}>Send</button>
|
|
1504
|
+
</div>
|
|
1505
|
+
`;
|
|
1506
|
+
};
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
| Option | Default | Description |
|
|
1510
|
+
|--------|---------|-------------|
|
|
1511
|
+
| `reconnect` | `true` | Auto-reconnect on close/error |
|
|
1512
|
+
| `reconnectDelay` | `1000` | Initial delay in ms (doubles each attempt) |
|
|
1513
|
+
| `reconnectMaxDelay` | `30000` | Exponential backoff cap |
|
|
1514
|
+
| `reconnectMaxAttempts` | `Infinity` | Max attempts before giving up |
|
|
1515
|
+
| `auth` | — | `() => Record<string, string>` — query params added on connect |
|
|
1516
|
+
| `autoConnect` | `true` | Set to `false` to defer until `.connect()` is called manually |
|
|
1517
|
+
|
|
1518
|
+
---
|
|
390
1519
|
|
|
391
|
-
|
|
1520
|
+
## Virtualization
|
|
1521
|
+
|
|
1522
|
+
Render large lists and grids efficiently by only mounting visible items:
|
|
1523
|
+
|
|
1524
|
+
```typescript
|
|
1525
|
+
import { virtualList, virtualGrid, virtualMasonry, masonryGrid } from "mates";
|
|
1526
|
+
|
|
1527
|
+
// Virtualized list — only renders visible rows
|
|
1528
|
+
html`${virtualList({
|
|
1529
|
+
items: bigArray,
|
|
1530
|
+
itemHeight: 60,
|
|
1531
|
+
renderItem: (item, index) => html`
|
|
1532
|
+
<div class="row">${index}: ${item.name}</div>
|
|
1533
|
+
`,
|
|
1534
|
+
})}`
|
|
1535
|
+
|
|
1536
|
+
// Virtualized grid — only renders visible cells
|
|
1537
|
+
html`${virtualGrid({
|
|
1538
|
+
items: products,
|
|
1539
|
+
columns: 4,
|
|
1540
|
+
rowHeight: 240,
|
|
1541
|
+
renderItem: (product) => html`
|
|
1542
|
+
<div class="product-card">
|
|
1543
|
+
<img src=${product.image} />
|
|
1544
|
+
<p>${product.name}</p>
|
|
1545
|
+
</div>
|
|
1546
|
+
`,
|
|
1547
|
+
})}`
|
|
1548
|
+
|
|
1549
|
+
// Masonry layout (non-virtualized)
|
|
1550
|
+
html`${masonryGrid({
|
|
1551
|
+
items: photos,
|
|
1552
|
+
columns: 3,
|
|
1553
|
+
gap: 16,
|
|
1554
|
+
renderItem: (photo) => html`<img src=${photo.url} />`,
|
|
1555
|
+
})}`
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
---
|
|
1559
|
+
|
|
1560
|
+
## DevTools
|
|
1561
|
+
|
|
1562
|
+
Mates DevTools is a companion browser extension. When installed, it provides component tree inspection, atom state history, time-travel debugging, and re-render tracking — all without any code changes.
|
|
1563
|
+
|
|
1564
|
+
To wire up custom DevTools integration at runtime:
|
|
1565
|
+
|
|
1566
|
+
```typescript
|
|
1567
|
+
import { installDevToolsHooks, isDevToolsInstalled } from "mates";
|
|
1568
|
+
|
|
1569
|
+
if (!isDevToolsInstalled()) {
|
|
1570
|
+
installDevToolsHooks({
|
|
1571
|
+
onAtomCreate: (atom) => { /* ... */ },
|
|
1572
|
+
onAtomSet: (atom, prev, next) => { /* ... */ },
|
|
1573
|
+
onRender: (component) => { /* ... */ },
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
392
1576
|
```
|
|
393
1577
|
|
|
394
|
-
|
|
1578
|
+
---
|
|
395
1579
|
|
|
396
|
-
|
|
1580
|
+
## TypeScript
|
|
397
1581
|
|
|
398
|
-
|
|
1582
|
+
All APIs are fully typed. The most common types you'll import:
|
|
399
1583
|
|
|
400
|
-
|
|
1584
|
+
```typescript
|
|
1585
|
+
import type {
|
|
1586
|
+
Props, // propsFn type: () => T
|
|
1587
|
+
Component, // outer fn → inner fn (closure component)
|
|
1588
|
+
TemplateFn, // fn → TemplateResult
|
|
1589
|
+
AtomType, // atom return type: AtomType<T>
|
|
1590
|
+
IAtomType, // iAtom return type
|
|
1591
|
+
AsyncActionReturnType, // asyncAction return type
|
|
1592
|
+
ActionReturnType, // action return type
|
|
1593
|
+
CSSBlock, // css-in-js block type
|
|
1594
|
+
CSSRulesInput, // full css() rules object type
|
|
1595
|
+
WsConfig, // WebSocket config
|
|
1596
|
+
WsConnection, // WebSocket connection handle
|
|
1597
|
+
} from "mates";
|
|
1598
|
+
```
|
|
1599
|
+
|
|
1600
|
+
**Typed component example:**
|
|
1601
|
+
|
|
1602
|
+
```typescript
|
|
1603
|
+
import type { Props, Component } from "mates";
|
|
1604
|
+
|
|
1605
|
+
interface ButtonProps {
|
|
1606
|
+
label: string;
|
|
1607
|
+
onClick: () => void;
|
|
1608
|
+
variant?: "primary" | "ghost" | "danger";
|
|
1609
|
+
disabled?: boolean;
|
|
1610
|
+
}
|
|
401
1611
|
|
|
402
|
-
|
|
1612
|
+
const Button: Component<ButtonProps> = (propsFn) => {
|
|
1613
|
+
return () => {
|
|
1614
|
+
const { label, onClick, variant = "primary", disabled = false } = propsFn();
|
|
1615
|
+
return html`
|
|
1616
|
+
<button
|
|
1617
|
+
class="btn btn--${variant}"
|
|
1618
|
+
?disabled=${disabled}
|
|
1619
|
+
@click=${onClick}
|
|
1620
|
+
>${label}</button>
|
|
1621
|
+
`;
|
|
1622
|
+
};
|
|
1623
|
+
};
|
|
1624
|
+
```
|
|
403
1625
|
|
|
404
|
-
|
|
1626
|
+
---
|
|
1627
|
+
|
|
1628
|
+
## Features at a Glance
|
|
1629
|
+
|
|
1630
|
+
| Category | Feature |
|
|
1631
|
+
|----------|---------|
|
|
1632
|
+
| **Components** | Two-layer closure model, props as function, slot/children support |
|
|
1633
|
+
| **Reactivity** | `atom`, `iAtom`, `effect`, `memo`, `store`, reactive Map/Set |
|
|
1634
|
+
| **Local state** | `useState` with auto-async wrapping |
|
|
1635
|
+
| **Shared state** | Scope classes with `useScope` / `getParentScope` — no prop drilling |
|
|
1636
|
+
| **Persistence** | `lsAtom` (localStorage), `ssAtom` (sessionStorage), cross-tab sync |
|
|
1637
|
+
| **Async** | `asyncAction` with loading/error/data atoms, cancellation, polling, LRU cache |
|
|
1638
|
+
| **Pagination** | `paginatedAsyncAction` with built-in page atom and `next()` |
|
|
1639
|
+
| **Task queue** | `taskAction` for serial async work |
|
|
1640
|
+
| **HTTP** | `FetchClient` with interceptors, URL templates, JSON auto-serialization, SSR support |
|
|
1641
|
+
| **Routing** | `Router`, `route`, `navigateTo`, `pathAtom`, `qsAtom`, animated transitions |
|
|
1642
|
+
| **Navigation lock** | Built-in unsaved-changes guard |
|
|
1643
|
+
| **CSS-in-JS** | Scoped `stylesheet`, `globalCSS`, `keyframes` — zero runtime for static styles |
|
|
1644
|
+
| **Theming** | `globalTheme` with CSS custom properties, dark mode, OS auto-mode |
|
|
1645
|
+
| **Directives** | `attr`, `style`, `classes`, `on`, `onIntersect`, `lazyLoad`, `animatedIf`, etc. |
|
|
1646
|
+
| **Hooks** | `onMount`, `onPaint`, `onCleanup`, `onKeyDown`, `onInterval`, `onNavigate`, … |
|
|
1647
|
+
| **Portals** | `portal`, `dialog`, `tooltip` — escape DOM stacking contexts |
|
|
1648
|
+
| **Animations** | WAAPI wrapper, `animateDirective`, 15+ presets, stagger |
|
|
1649
|
+
| **WebSocket** | `ws()` with reconnect, exponential backoff, auth refresh, `onSocket` hook |
|
|
1650
|
+
| **Virtualization** | `virtualList`, `virtualGrid`, `virtualMasonry`, `masonryGrid` |
|
|
1651
|
+
| **DevTools** | Installable hook bridge for browser extension integration |
|
|
1652
|
+
| **SSR** | `isSSR`, `setSSRMode`, handler registry for zero-network server rendering |
|
|
1653
|
+
| **TypeScript** | Full type coverage, generics throughout, strict-mode safe |
|
|
1654
|
+
| **lit-html** | Full re-export of `html`, `svg`, `render`, `repeat`, `classMap`, `styleMap`, `when`, `cache`, `live`, `keyed`, `guard`, `until`, and more |
|
|
1655
|
+
|
|
1656
|
+
---
|
|
1657
|
+
|
|
1658
|
+
## How Mates Compares
|
|
1659
|
+
|
|
1660
|
+
### vs React
|
|
1661
|
+
|
|
1662
|
+
| | Mates | React |
|
|
1663
|
+
|---|---|---|
|
|
1664
|
+
| **Rendering** | `lit-html` patches the real DOM directly — no virtual DOM, no diffing tree | VDOM diff on every render, reconciler determines what to patch |
|
|
1665
|
+
| **Component model** | Two-layer closure — outer (setup) runs once, inner (render) runs on change | Function components re-run entirely on every render |
|
|
1666
|
+
| **Reactivity** | Fine-grained atoms — only components that read a changed atom re-run | Renders propagate top-down via props and context unless `memo`/`useMemo` are used |
|
|
1667
|
+
| **State** | `atom` (module or component-scoped), `useState`, `store` | `useState`, `useReducer`, `useContext`, external libraries |
|
|
1668
|
+
| **Side effects** | `effect(fn)` is reactive — re-runs when dependencies change | `useEffect` with manual dependency arrays |
|
|
1669
|
+
| **Computed values** | `memo(fn)` — auto-tracked dependencies | `useMemo(fn, deps)` — manual dependency arrays |
|
|
1670
|
+
| **Shared state** | Scopes (class-based, `useScope`/`getParentScope`) | Context API + `useContext` or external store (Zustand, Redux) |
|
|
1671
|
+
| **Compiler** | None — plain TypeScript, standard ESM | JSX transform required |
|
|
1672
|
+
| **Bundle size** | ~50 KB gzipped (framework + lit-html) | ~45 KB gzipped (React + ReactDOM) |
|
|
1673
|
+
| **HTTP** | Built-in `FetchClient` + `asyncAction` | Third-party (Axios, TanStack Query, SWR) |
|
|
1674
|
+
| **Routing** | Built-in `Router` | Third-party (React Router, TanStack Router) |
|
|
1675
|
+
| **CSS** | Built-in `stylesheet` + `globalTheme` | Third-party (Emotion, styled-components, CSS Modules) |
|
|
1676
|
+
| **Animation** | Built-in WAAPI presets + `animateDirective` | Third-party (Framer Motion, react-spring) |
|
|
1677
|
+
| **WebSocket** | Built-in `ws()` with reconnect | Third-party |
|
|
1678
|
+
| **Virtualization** | Built-in `virtualList`, `virtualGrid`, `virtualMasonry` | Third-party (react-window, TanStack Virtual) |
|
|
1679
|
+
|
|
1680
|
+
**Key difference:** In React, calling `setState` schedules a re-render of the whole component tree from that node down. In Mates, changing an atom only re-runs the specific inner functions and effects that read that atom — everything else stays untouched.
|
|
1681
|
+
|
|
1682
|
+
---
|
|
1683
|
+
|
|
1684
|
+
### vs Vue 3
|
|
1685
|
+
|
|
1686
|
+
| | Mates | Vue 3 |
|
|
1687
|
+
|---|---|---|
|
|
1688
|
+
| **Templates** | Tagged template literals — standard JavaScript, no compiler | `.vue` SFC files or JSX with Vite compiler |
|
|
1689
|
+
| **Reactivity** | `atom` — explicit, function-call reads | `ref`/`reactive` — Proxy-based, transparent reads |
|
|
1690
|
+
| **Components** | Plain closure functions | Options API or `setup()` function + `<template>` |
|
|
1691
|
+
| **Scoped styles** | `stylesheet()` — scoped class names | `<style scoped>` — attribute-based scoping |
|
|
1692
|
+
| **Router** | Built-in | Official but separate (`vue-router`) |
|
|
1693
|
+
| **State management** | `atom`, `store`, `scope` built-in | Official but separate (`pinia`) |
|
|
1694
|
+
| **Compiler** | None required | Required for SFC and template directives |
|
|
1695
|
+
|
|
1696
|
+
---
|
|
1697
|
+
|
|
1698
|
+
### vs Svelte
|
|
1699
|
+
|
|
1700
|
+
| | Mates | Svelte |
|
|
1701
|
+
|---|---|---|
|
|
1702
|
+
| **Build step** | None — TypeScript only | Required Svelte compiler |
|
|
1703
|
+
| **Reactivity** | Explicit `atom` calls | Compile-time `$:` labels / `$state` runes |
|
|
1704
|
+
| **Component files** | Plain `.ts` files | `.svelte` files with `<script>`, `<template>`, `<style>` sections |
|
|
1705
|
+
| **Bundle size** | Consistent ~50 KB | Per-component compiled output — small for tiny apps, grows with app size |
|
|
1706
|
+
| **TypeScript** | Native — no extra config | Requires `lang="ts"` + type-checking config |
|
|
1707
|
+
| **Animation** | Built-in WAAPI presets | Built-in `transition:`, `animate:` directives (compile-time) |
|
|
1708
|
+
|
|
1709
|
+
---
|
|
1710
|
+
|
|
1711
|
+
### vs SolidJS
|
|
1712
|
+
|
|
1713
|
+
| | Mates | SolidJS |
|
|
1714
|
+
|---|---|---|
|
|
1715
|
+
| **Rendering** | `lit-html` patches — no VDOM | Compiled fine-grained DOM updates |
|
|
1716
|
+
| **Reactivity** | `atom` — explicit function call to read | `createSignal`, `createEffect` — runs at compile time |
|
|
1717
|
+
| **Compiler** | None | JSX transform + reactivity transform required |
|
|
1718
|
+
| **Component re-runs** | Inner function re-runs on change | Components run once — JSX expressions are reactive subscriptions |
|
|
1719
|
+
| **Ecosystem** | Self-contained (router, HTTP, CSS, WS all built-in) | Growing ecosystem, mostly third-party |
|
|
1720
|
+
| **Learning curve** | Low — plain TypeScript + tagged templates | Moderate — must understand compiled reactive graph |
|
|
1721
|
+
|
|
1722
|
+
---
|
|
1723
|
+
|
|
1724
|
+
### vs Preact / Inferno
|
|
1725
|
+
|
|
1726
|
+
Both are drop-in React replacements with VDOM. Mates takes a fundamentally different approach (lit-html + fine-grained atoms) and ships a complete feature set out of the box rather than relying on the React ecosystem for routing, state, animations, and HTTP.
|
|
1727
|
+
|
|
1728
|
+
---
|
|
1729
|
+
|
|
1730
|
+
### Summary: When to choose Mates
|
|
1731
|
+
|
|
1732
|
+
✅ You want **no compiler magic** — just TypeScript and a build tool you already use
|
|
1733
|
+
✅ You want **everything in one package** — HTTP, routing, CSS, WS, animations, virtualization
|
|
1734
|
+
✅ You want **fine-grained reactivity** without a framework-specific compiler
|
|
1735
|
+
✅ You're building a **single-page application** with complex state and async flows
|
|
1736
|
+
✅ You want **predictable performance** — only what changed is ever re-rendered
|
|
1737
|
+
✅ You prefer **explicit over implicit** — reads and writes are always function calls
|
|
1738
|
+
|
|
1739
|
+
---
|
|
1740
|
+
|
|
1741
|
+
## IDE Support
|
|
1742
|
+
|
|
1743
|
+
Install the **Lit** extension for VS Code / Cursor ([marketplace](https://marketplace.visualstudio.com/items?itemName=lit.lit-plugin)) to get:
|
|
405
1744
|
|
|
406
|
-
|
|
1745
|
+
- Syntax highlighting inside `html\`...\`` template literals
|
|
1746
|
+
- Attribute and property completions on HTML elements
|
|
1747
|
+
- Inline TypeScript errors in templates
|
|
407
1748
|
|
|
408
|
-
|
|
1749
|
+
---
|
|
409
1750
|
|
|
410
|
-
##
|
|
1751
|
+
## License
|
|
411
1752
|
|
|
412
|
-
MIT
|
|
1753
|
+
MIT
|