sygnal 5.3.7 → 6.0.0
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/CHANGELOG.md +646 -0
- package/README.md +77 -47
- package/dist/astro/client.cjs.js +33 -8114
- package/dist/astro/client.cjs.js.map +1 -0
- package/dist/astro/client.mjs +33 -8114
- package/dist/astro/client.mjs.map +1 -0
- package/dist/astro/index.cjs.js +1039 -9
- package/dist/astro/index.cjs.js.map +1 -0
- package/dist/astro/index.mjs +1038 -9
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/server.cjs.js +3400 -166
- package/dist/astro/server.cjs.js.map +1 -0
- package/dist/astro/server.mjs +3400 -166
- package/dist/astro/server.mjs.map +1 -0
- package/dist/devtools.cjs.js +1431 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1419 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +4033 -0
- package/dist/diagnostics.cjs.js.map +1 -0
- package/dist/diagnostics.esm.js +4003 -0
- package/dist/diagnostics.esm.js.map +1 -0
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +257 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +381 -0
- package/dist/guide/forms.md +250 -0
- package/dist/guide/http.md +299 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +33 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +15283 -6279
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +3739 -370
- package/dist/index.esm.js +15230 -6274
- package/dist/index.esm.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs.js +27 -329
- package/dist/jsx-dev-runtime.cjs.js.map +1 -0
- package/dist/jsx-dev-runtime.esm.js +20 -322
- package/dist/jsx-dev-runtime.esm.js.map +1 -0
- package/dist/jsx-runtime.cjs.js +27 -329
- package/dist/jsx-runtime.cjs.js.map +1 -0
- package/dist/jsx-runtime.esm.js +20 -322
- package/dist/jsx-runtime.esm.js.map +1 -0
- package/dist/jsx.cjs.js +11 -309
- package/dist/jsx.cjs.js.map +1 -0
- package/dist/jsx.esm.js +9 -310
- package/dist/jsx.esm.js.map +1 -0
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +2 -1
- package/dist/sygnal.min.js.map +1 -0
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +5 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -0
- package/dist/vike/ClientOnly.mjs +5 -14
- package/dist/vike/ClientOnly.mjs.map +1 -0
- package/dist/vike/{+config.js → config/+config.js} +10 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +166 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -0
- package/dist/vike/onRenderClient.mjs +166 -95
- package/dist/vike/onRenderClient.mjs.map +1 -0
- package/dist/vike/onRenderHtml.cjs.js +60 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -0
- package/dist/vike/onRenderHtml.mjs +61 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -0
- package/dist/vite/plugin.cjs.js +926 -65
- package/dist/vite/plugin.cjs.js.map +1 -0
- package/dist/vite/plugin.mjs +925 -65
- package/dist/vite/plugin.mjs.map +1 -0
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +313 -0
- package/package.json +109 -16
- package/src/astro/client.ts +40 -18
- package/src/astro/index.d.ts +48 -1
- package/src/astro/index.ts +106 -9
- package/src/astro/server.ts +11 -3
- package/src/collection.ts +6 -90
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +13 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +76 -0
- package/src/cycle/dom/controlledInputModule.ts +50 -0
- package/src/cycle/dom/enrichEventStream.ts +29 -45
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -57
- package/src/cycle/dom/mockDOMSource.ts +83 -9
- package/src/cycle/dom/modules.ts +21 -4
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +20 -4
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +151 -0
- package/src/devtools.ts +34 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devtools.ts +109 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/collections.ts +67 -0
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +250 -0
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +83 -0
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +163 -0
- package/src/extra/diagnostics/checks/index.ts +202 -0
- package/src/extra/diagnostics/checks/inspect.ts +427 -0
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +56 -0
- package/src/extra/diagnostics/checks/public.d.ts +302 -0
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
- package/src/extra/diagnostics/checks/shared.ts +203 -0
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +148 -0
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +55 -0
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +121 -0
- package/src/extra/diagnostics/codes.ts +477 -0
- package/src/extra/diagnostics/index.ts +353 -0
- package/src/extra/diagnostics/legacy.ts +76 -0
- package/src/extra/driverFactories.ts +172 -60
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/eventDriver.ts +4 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/flatten.ts +75 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +268 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +13 -6
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +94 -210
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +3158 -190
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/extra/xstreamExtras.ts +269 -0
- package/src/index.d.ts +3012 -87
- package/src/index.ts +32 -10
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +262 -134
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +48 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -119
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +27 -4
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +114 -2
- package/src/vite/plugin.ts +920 -56
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/astro/astro/client.d.ts +0 -5
- package/dist/astro/astro/index.d.ts +0 -16
- package/dist/astro/astro/server.d.ts +0 -12
- package/dist/astro/client.d.ts +0 -5
- package/dist/astro/client.esm.js +0 -4665
- package/dist/astro/collection.d.ts +0 -12
- package/dist/astro/component.d.ts +0 -22
- package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/astro/cycle/dom/index.d.ts +0 -8
- package/dist/astro/cycle/dom/isolate.d.ts +0 -9
- package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/astro/cycle/dom/modules.d.ts +0 -5
- package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
- package/dist/astro/cycle/dom/thunk.d.ts +0 -11
- package/dist/astro/cycle/dom/utils.d.ts +0 -8
- package/dist/astro/cycle/isolate/index.d.ts +0 -42
- package/dist/astro/cycle/run/adapt.d.ts +0 -6
- package/dist/astro/cycle/run/index.d.ts +0 -36
- package/dist/astro/cycle/run/internals.d.ts +0 -8
- package/dist/astro/cycle/run/types.d.ts +0 -49
- package/dist/astro/cycle/state/Collection.d.ts +0 -29
- package/dist/astro/cycle/state/StateSource.d.ts +0 -20
- package/dist/astro/cycle/state/index.d.ts +0 -5
- package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
- package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
- package/dist/astro/cycle/state/types.d.ts +0 -18
- package/dist/astro/cycle/state/withState.d.ts +0 -17
- package/dist/astro/extra/classes.d.ts +0 -7
- package/dist/astro/extra/devtools.d.ts +0 -78
- package/dist/astro/extra/dragDriver.d.ts +0 -20
- package/dist/astro/extra/driverFactories.d.ts +0 -12
- package/dist/astro/extra/eventDriver.d.ts +0 -9
- package/dist/astro/extra/exactState.d.ts +0 -1
- package/dist/astro/extra/hmr.d.ts +0 -17
- package/dist/astro/extra/logDriver.d.ts +0 -2
- package/dist/astro/extra/processDrag.d.ts +0 -19
- package/dist/astro/extra/processForm.d.ts +0 -15
- package/dist/astro/extra/run.d.ts +0 -13
- package/dist/astro/extra/xstreamCompat.d.ts +0 -5
- package/dist/astro/index.d.ts +0 -19
- package/dist/astro/index.esm.js +0 -27
- package/dist/astro/jsx-dev-runtime.d.ts +0 -1
- package/dist/astro/jsx-runtime.d.ts +0 -4
- package/dist/astro/jsx.d.ts +0 -2
- package/dist/astro/pragma/fn.d.ts +0 -7
- package/dist/astro/pragma/index.d.ts +0 -7
- package/dist/astro/pragma/is.d.ts +0 -10
- package/dist/astro/server.d.ts +0 -12
- package/dist/astro/server.esm.js +0 -25
- package/dist/astro/switchable.d.ts +0 -7
- package/dist/collection.d.ts +0 -12
- package/dist/component.d.ts +0 -22
- package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/cycle/dom/index.d.ts +0 -8
- package/dist/cycle/dom/isolate.d.ts +0 -9
- package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/cycle/dom/modules.d.ts +0 -5
- package/dist/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/cycle/dom/styleModule.d.ts +0 -13
- package/dist/cycle/dom/thunk.d.ts +0 -11
- package/dist/cycle/dom/utils.d.ts +0 -8
- package/dist/cycle/isolate/index.d.ts +0 -42
- package/dist/cycle/run/adapt.d.ts +0 -6
- package/dist/cycle/run/index.d.ts +0 -36
- package/dist/cycle/run/internals.d.ts +0 -8
- package/dist/cycle/run/types.d.ts +0 -49
- package/dist/cycle/state/Collection.d.ts +0 -29
- package/dist/cycle/state/StateSource.d.ts +0 -20
- package/dist/cycle/state/index.d.ts +0 -5
- package/dist/cycle/state/pickCombine.d.ts +0 -3
- package/dist/cycle/state/pickMerge.d.ts +0 -3
- package/dist/cycle/state/types.d.ts +0 -18
- package/dist/cycle/state/withState.d.ts +0 -17
- package/dist/extra/classes.d.ts +0 -7
- package/dist/extra/devtools.d.ts +0 -78
- package/dist/extra/dragDriver.d.ts +0 -20
- package/dist/extra/driverFactories.d.ts +0 -12
- package/dist/extra/eventDriver.d.ts +0 -9
- package/dist/extra/exactState.d.ts +0 -1
- package/dist/extra/hmr.d.ts +0 -17
- package/dist/extra/logDriver.d.ts +0 -2
- package/dist/extra/processDrag.d.ts +0 -19
- package/dist/extra/processForm.d.ts +0 -15
- package/dist/extra/run.d.ts +0 -13
- package/dist/extra/xstreamCompat.d.ts +0 -5
- package/dist/jsx-dev-runtime.d.ts +0 -1
- package/dist/jsx-dev-runtime.js +0 -224
- package/dist/jsx-runtime.d.ts +0 -4
- package/dist/jsx-runtime.js +0 -224
- package/dist/jsx.d.ts +0 -2
- package/dist/pragma/fn.d.ts +0 -7
- package/dist/pragma/index.d.ts +0 -7
- package/dist/pragma/is.d.ts +0 -10
- package/dist/switchable.d.ts +0 -7
- package/dist/vike/+config.cjs.js +0 -65
- package/src/component.ts +0 -2256
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -174
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -167
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- package/src/pragma/fn.ts +0 -55
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
|
|
2
|
+
# Charts (Chart.js, ECharts)
|
|
3
|
+
|
|
4
|
+
[Chart.js](https://www.chartjs.org/) draws charts on a `<canvas>`. It doesn't know about Sygnal, and doesn't need to: [`defineWidget`](../widgets.md) turns it into a JSX tag. The data lives in your state, the chart is redrawn when it changes, and a click on a bar comes back as an event the intent reads.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install chart.js
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## The widget
|
|
13
|
+
|
|
14
|
+
```js live-file=./SalesChart.js
|
|
15
|
+
// SalesChart.js
|
|
16
|
+
import { defineWidget } from 'sygnal'
|
|
17
|
+
import { Chart, BarController, BarElement, CategoryScale, LinearScale, Tooltip, Colors } from 'chart.js'
|
|
18
|
+
|
|
19
|
+
Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Colors)
|
|
20
|
+
|
|
21
|
+
export const SalesChart = defineWidget({
|
|
22
|
+
name: 'SalesChart',
|
|
23
|
+
tag: 'canvas',
|
|
24
|
+
mount: (el, props, dispatch) => new Chart(el, {
|
|
25
|
+
type: 'bar',
|
|
26
|
+
data: { labels: props.labels, datasets: [{ label: props.series, data: props.values }] },
|
|
27
|
+
options: {
|
|
28
|
+
maintainAspectRatio: false,
|
|
29
|
+
onClick: (event, bars) => { if (bars.length) dispatch('bar-select', bars[0].index) },
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
update: (chart, props) => {
|
|
33
|
+
chart.data.labels = props.labels
|
|
34
|
+
chart.data.datasets[0].label = props.series
|
|
35
|
+
chart.data.datasets[0].data = props.values
|
|
36
|
+
chart.update()
|
|
37
|
+
},
|
|
38
|
+
unmount: (chart) => chart.destroy(),
|
|
39
|
+
events: ['bar-select'],
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- The `Colors` plugin gives each dataset a colour from Chart.js's palette. Without it, a dataset with no `backgroundColor` is drawn in a 10% black that is hard to see on a dark page.
|
|
44
|
+
- `mount` creates the chart on the `<canvas>` host and returns it: that is the instance `update` and `unmount` get.
|
|
45
|
+
- `update` runs when the props change. It changes the chart's data in place and calls `chart.update()`, so the chart animates to the new values instead of being rebuilt.
|
|
46
|
+
- `unmount` runs when the chart leaves the page and frees the canvas.
|
|
47
|
+
- A click on a bar calls `dispatch('bar-select', index)`, a bubbling DOM event on the canvas with the bar's index as its `detail`.
|
|
48
|
+
|
|
49
|
+
## Using it
|
|
50
|
+
|
|
51
|
+
```jsx live
|
|
52
|
+
// Sales.jsx
|
|
53
|
+
import { SalesChart } from './SalesChart.js'
|
|
54
|
+
|
|
55
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr']
|
|
56
|
+
|
|
57
|
+
export function Sales({ state }) {
|
|
58
|
+
const picked = state.selected === null ? 'Select a bar' : `${MONTHS[state.selected]}: ${state.values[state.selected]}`
|
|
59
|
+
return (
|
|
60
|
+
<section>
|
|
61
|
+
<div className="chart-box">
|
|
62
|
+
<SalesChart className="sales" role="img" aria-label="Sales by month"
|
|
63
|
+
labels={MONTHS} series="Sales" values={state.values} />
|
|
64
|
+
</div>
|
|
65
|
+
<p className="picked">{picked}</p>
|
|
66
|
+
<button className="add-sale">Add a sale in April</button>
|
|
67
|
+
</section>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Sales.initialState = { values: [12, 19, 7, 4], selected: null }
|
|
72
|
+
|
|
73
|
+
Sales.intent = ({ DOM }) => ({
|
|
74
|
+
SELECT: DOM.select('.sales').events('bar-select').detail(),
|
|
75
|
+
ADD_SALE: DOM.click('.add-sale'),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
Sales.model = {
|
|
79
|
+
SELECT: (state, index) => ({ ...state, selected: index }),
|
|
80
|
+
ADD_SALE: (state) => ({ ...state, values: [...state.values.slice(0, 3), state.values[3] + 1] }),
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```css live
|
|
85
|
+
.chart-box { position: relative; height: 240px; }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Chart.js sizes the canvas to its parent, so the parent needs a size (and `position: relative`). With `maintainAspectRatio: false` the chart fills that box.
|
|
89
|
+
|
|
90
|
+
## Testing
|
|
91
|
+
|
|
92
|
+
`renderComponent`'s mock DOM doesn't draw the chart. `t.widget('.sales')` sends the widget's events and shows the props the view passed it, which is what the component is responsible for:
|
|
93
|
+
|
|
94
|
+
```jsx
|
|
95
|
+
// Sales.test.jsx
|
|
96
|
+
import { test, expect } from 'vitest'
|
|
97
|
+
import { renderComponent } from 'sygnal'
|
|
98
|
+
import { Sales } from './Sales.jsx'
|
|
99
|
+
|
|
100
|
+
test('a clicked bar is selected, and a new sale reaches the chart', async () => {
|
|
101
|
+
const t = renderComponent(Sales)
|
|
102
|
+
await t.ready()
|
|
103
|
+
t.widget('.sales').dispatch('bar-select', 1)
|
|
104
|
+
await t.next((state) => state.selected === 1)
|
|
105
|
+
expect(t.query('.picked').textContent).toBe('Feb: 19')
|
|
106
|
+
|
|
107
|
+
t.simulateEvent('.add-sale', 'click')
|
|
108
|
+
await t.next((state) => state.values[3] === 5)
|
|
109
|
+
expect(t.widget('.sales').props.values).toEqual([12, 19, 7, 5])
|
|
110
|
+
t.dispose()
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
To test the chart itself (that a click on the second bar selects it), run the component in a real browser with `renderComponent(Sales, { dom: 'real' })`: `t.widget('.sales').instance` is the Chart.js object, and `Chart.getChart(canvas)` is `undefined` after `t.dispose()`. jsdom has no canvas, so Chart.js can't draw there.
|
|
115
|
+
|
|
116
|
+
## ECharts
|
|
117
|
+
|
|
118
|
+
[Apache ECharts](https://echarts.apache.org/) works the same way. It draws into a `<div>` (the default host), takes one `option` object for everything, and reports clicks through `chart.on('click')`:
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
// SalesEChart.js
|
|
122
|
+
import { defineWidget } from 'sygnal'
|
|
123
|
+
import * as echarts from 'echarts/core'
|
|
124
|
+
import { BarChart } from 'echarts/charts'
|
|
125
|
+
import { GridComponent, TooltipComponent } from 'echarts/components'
|
|
126
|
+
import { CanvasRenderer } from 'echarts/renderers'
|
|
127
|
+
|
|
128
|
+
echarts.use([BarChart, GridComponent, TooltipComponent, CanvasRenderer])
|
|
129
|
+
|
|
130
|
+
const option = (props) => ({
|
|
131
|
+
xAxis: { type: 'category', data: props.labels },
|
|
132
|
+
yAxis: { type: 'value' },
|
|
133
|
+
tooltip: {},
|
|
134
|
+
series: [{ type: 'bar', data: props.values }],
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
export const SalesEChart = defineWidget({
|
|
138
|
+
name: 'SalesEChart',
|
|
139
|
+
mount: (el, props, dispatch) => {
|
|
140
|
+
const chart = echarts.init(el)
|
|
141
|
+
chart.setOption(option(props))
|
|
142
|
+
chart.on('click', (params) => dispatch('bar-select', params.dataIndex))
|
|
143
|
+
// ECharts measures the element once: redraw at the new size when the host resizes
|
|
144
|
+
const observer = new ResizeObserver(() => chart.resize())
|
|
145
|
+
observer.observe(el)
|
|
146
|
+
return { chart, observer }
|
|
147
|
+
},
|
|
148
|
+
update: ({ chart }, props) => chart.setOption(option(props)),
|
|
149
|
+
unmount: ({ chart, observer }) => {
|
|
150
|
+
observer.disconnect()
|
|
151
|
+
chart.dispose()
|
|
152
|
+
},
|
|
153
|
+
events: ['bar-select'],
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`setOption` merges the new option into the chart, so `update` can pass the whole option every time. Give the host a height in CSS (`.sales { height: 240px; }`): ECharts measures the element when it starts, and only again on `chart.resize()`, which the `ResizeObserver` calls when the host changes size (a resized window, a sidebar that opens). The instance is `{ chart, observer }`, so `unmount` can stop both. (Chart.js watches its canvas's container itself; with `maintainAspectRatio: false` it follows the container's height.)
|
|
158
|
+
|
|
159
|
+
## Size
|
|
160
|
+
|
|
161
|
+
Measured with Vite, minified and gzipped, Sygnal not included:
|
|
162
|
+
|
|
163
|
+
| | Adds |
|
|
164
|
+
|---|---|
|
|
165
|
+
| Chart.js bar chart (the registrations above) | 60 KB |
|
|
166
|
+
| ECharts bar chart (the `use` list above) | 180 KB |
|
|
167
|
+
| `defineWidget` (the first widget in an app) | 1.1 KB |
|
|
168
|
+
|
|
169
|
+
Register only what you draw: `import Chart from 'chart.js/auto'` registers every chart type, scale and plugin: 75 KB instead of 60.
|
|
170
|
+
|
|
171
|
+
## Pitfalls
|
|
172
|
+
|
|
173
|
+
- **Register what the chart uses.** Chart.js and ECharts are tree-shaken: a chart type, scale or plugin you don't register isn't there (`"bar" is not a registered controller`).
|
|
174
|
+
- **Pass new arrays.** `update` runs when a prop is a different value. A reducer that changes `state.values` in place passes the same array, and the chart doesn't change. The usual immutable update (`[...values]`, `map`) is enough.
|
|
175
|
+
- **A canvas has no text.** Give the host `role="img"` and an `aria-label`, and show the numbers somewhere a screen reader can reach (a table, a summary line), as `picked` does here.
|
|
176
|
+
- **Clicks on bars are mouse-only.** A keyboard user can't click a bar. Offer the same choice with buttons or a `<select>` that dispatch the same action.
|
|
177
|
+
- **Animations.** `chart.update()` animates and `chart.update('none')` doesn't. To respect `prefers-reduced-motion`, set `options.animation: false` in `mount` when `matchMedia('(prefers-reduced-motion: reduce)').matches`.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
|
|
2
|
+
# Code Editor (CodeMirror)
|
|
3
|
+
|
|
4
|
+
[CodeMirror 6](https://codemirror.net/) is a code editor made of an `EditorView` and extensions. [`defineWidget`](../widgets.md) turns it into a JSX tag: the code is a string in your state, each change comes back as an event, and a `focus` command focuses the editor from the model.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install codemirror @codemirror/state @codemirror/lang-javascript
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## The widget
|
|
13
|
+
|
|
14
|
+
```js live-file=./CodeEditor.js
|
|
15
|
+
// CodeEditor.js
|
|
16
|
+
import { defineWidget } from 'sygnal'
|
|
17
|
+
import { EditorView, basicSetup } from 'codemirror'
|
|
18
|
+
import { Compartment } from '@codemirror/state'
|
|
19
|
+
import { javascript } from '@codemirror/lang-javascript'
|
|
20
|
+
|
|
21
|
+
// the label can change (another language): it lives in a compartment that update() reconfigures
|
|
22
|
+
const labelling = new Compartment()
|
|
23
|
+
const label = (props) => EditorView.contentAttributes.of(props.label == null ? {} : { 'aria-label': props.label })
|
|
24
|
+
|
|
25
|
+
export const CodeEditor = defineWidget({
|
|
26
|
+
name: 'CodeEditor',
|
|
27
|
+
mount: (el, props, dispatch) => new EditorView({
|
|
28
|
+
parent: el,
|
|
29
|
+
doc: props.code,
|
|
30
|
+
extensions: [
|
|
31
|
+
basicSetup,
|
|
32
|
+
javascript(),
|
|
33
|
+
labelling.of(label(props)),
|
|
34
|
+
EditorView.updateListener.of((update) => {
|
|
35
|
+
if (update.docChanged) dispatch('edit', update.state.doc.toString())
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
}),
|
|
39
|
+
update: (view, props) => {
|
|
40
|
+
const code = view.state.doc.toString()
|
|
41
|
+
if (props.code !== code) view.dispatch({ changes: { from: 0, to: code.length, insert: props.code } })
|
|
42
|
+
// getAttribute gives null for no label
|
|
43
|
+
if ((props.label ?? null) !== view.contentDOM.getAttribute('aria-label')) view.dispatch({ effects: labelling.reconfigure(label(props)) })
|
|
44
|
+
},
|
|
45
|
+
unmount: (view) => view.destroy(),
|
|
46
|
+
events: ['edit'],
|
|
47
|
+
commands: { focus: (view) => view.focus() },
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `mount` creates the view inside the host `<div>` (`parent: el`). `basicSetup` brings line numbers, history, bracket matching, search and autocompletion.
|
|
52
|
+
- The update listener sends the whole document as the `edit` event after each change.
|
|
53
|
+
- `update` gets the code from state. When the user typed it, it equals the editor's document and nothing happens; otherwise one transaction replaces the document.
|
|
54
|
+
- `view.dispatch` is CodeMirror's own method for applying a transaction. It has nothing to do with the widget's `dispatch`, the third parameter of `mount`, which sends events to the intent.
|
|
55
|
+
- The `focus` command focuses the editable area. A command the widget declares wins over the host element's method of the same name, so `{ focus: '.code' }` runs this one, not the `<div>`'s.
|
|
56
|
+
|
|
57
|
+
## Using it
|
|
58
|
+
|
|
59
|
+
```jsx live
|
|
60
|
+
// Snippet.jsx
|
|
61
|
+
import { CodeEditor } from './CodeEditor.js'
|
|
62
|
+
|
|
63
|
+
const START = 'const answer = 42\n'
|
|
64
|
+
|
|
65
|
+
export function Snippet({ state }) {
|
|
66
|
+
return (
|
|
67
|
+
<section>
|
|
68
|
+
<CodeEditor className="code" label="Snippet" code={state.code} />
|
|
69
|
+
<p className="lines">{state.code.split('\n').length} lines</p>
|
|
70
|
+
<button type="button" className="edit-code">Edit</button>
|
|
71
|
+
<button type="button" className="reset">Reset</button>
|
|
72
|
+
</section>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Snippet.initialState = { code: START }
|
|
77
|
+
|
|
78
|
+
Snippet.intent = ({ DOM }) => ({
|
|
79
|
+
EDIT: DOM.select('.code').events('edit').detail(),
|
|
80
|
+
FOCUS_CODE: DOM.click('.edit-code'),
|
|
81
|
+
RESET: DOM.click('.reset'),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
Snippet.model = {
|
|
85
|
+
EDIT: (state, code) => ({ ...state, code }),
|
|
86
|
+
FOCUS_CODE: { ELEMENT: { focus: '.code' } },
|
|
87
|
+
RESET: (state) => ({ ...state, code: START }),
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```css live
|
|
92
|
+
.code .cm-editor { background: white; color: black; border: 1px solid #8888; }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
CodeMirror's default theme is a light one: dark syntax colours and a light gutter, on whatever background the page has. On a page that can be dark, give the editor a light background, as here, or add a dark theme extension (`@codemirror/theme-one-dark`) and switch it with a compartment.
|
|
96
|
+
|
|
97
|
+
## Testing
|
|
98
|
+
|
|
99
|
+
```jsx
|
|
100
|
+
// Snippet.test.jsx
|
|
101
|
+
import { test, expect } from 'vitest'
|
|
102
|
+
import { renderComponent } from 'sygnal'
|
|
103
|
+
import { Snippet } from './Snippet.jsx'
|
|
104
|
+
|
|
105
|
+
test('edits reach the state, Reset puts the start code back', async () => {
|
|
106
|
+
const t = renderComponent(Snippet)
|
|
107
|
+
await t.ready()
|
|
108
|
+
t.widget('.code').dispatch('edit', 'const answer = 42\nconsole.log(answer)\n')
|
|
109
|
+
await t.next((state) => state.code.includes('console.log'))
|
|
110
|
+
expect(t.query('.lines').textContent).toBe('3 lines')
|
|
111
|
+
|
|
112
|
+
t.simulateEvent('.reset', 'click')
|
|
113
|
+
await t.next((state) => state.code === 'const answer = 42\n')
|
|
114
|
+
expect(t.widget('.code').props.code).toBe('const answer = 42\n')
|
|
115
|
+
|
|
116
|
+
t.simulateEvent('.edit-code', 'click')
|
|
117
|
+
await t.settle()
|
|
118
|
+
expect(t.commands()).toEqual([{ focus: '.code' }])
|
|
119
|
+
t.dispose()
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
CodeMirror measures text positions with layout APIs jsdom doesn't have, so test typing in a real browser: with `renderComponent(Snippet, { dom: 'real' })` under Playwright, click **Edit**, type, and `t.waitForState((state) => ...)` sees the new code; `t.widget('.code').instance` is the `EditorView`.
|
|
124
|
+
|
|
125
|
+
## Size
|
|
126
|
+
|
|
127
|
+
Measured with Vite, minified and gzipped, Sygnal not included: `basicSetup` with the JavaScript language adds **177 KB**. For a smaller editor, replace `basicSetup` with `minimalSetup` (history, default keymap, special characters) and the extensions you want from `@codemirror/view`, `@codemirror/commands` and the language packages. `defineWidget` adds 1.1 KB for the first widget in an app.
|
|
128
|
+
|
|
129
|
+
## Pitfalls
|
|
130
|
+
|
|
131
|
+
- **Keep the comparison in `update`.** Replacing the document on every render would reset the cursor and the undo history while the user types.
|
|
132
|
+
- **A reset is also an edit.** The transaction `update` dispatches changes the document, so the listener sends it back as an `edit` with the same code. That costs one extra action and changes nothing. To skip it, add an annotation to the transaction (`annotations: Transaction.remote.of(true)` from `@codemirror/state`) and ignore updates that carry it.
|
|
133
|
+
- **Change configuration with compartments.** `mount` builds the extensions once. The label is in a `Compartment` that `update` reconfigures when the `label` prop changes (a translated app switching language); do the same to switch the language mode, the theme or `readOnly` from props (`view.dispatch({ effects: compartment.reconfigure(...) })`). `@codemirror/state` is installed with `codemirror`; list it yourself so there is one copy (two copies break compartments).
|
|
134
|
+
- **Label the editable element.** The host `<div>` is not what receives the keyboard. `EditorView.contentAttributes` puts `aria-label` on CodeMirror's `.cm-content`, the element a screen reader announces.
|
|
135
|
+
- **Tab.** CodeMirror doesn't trap Tab by default, so keyboard users can leave the editor. Adding `indentWithTab` changes that; if you do, tell users that Escape then Tab leaves the editor.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
|
|
2
|
+
# Data Grid (AG Grid)
|
|
3
|
+
|
|
4
|
+
[AG Grid](https://www.ag-grid.com/) is a full spreadsheet-like grid: virtual scrolling for large data sets, column resizing and reordering, in-cell editing, keyboard navigation, CSV export. It renders everything itself, so [`defineWidget`](../widgets.md) turns it into a JSX tag. The rows stay in your state: the grid reports edits and selection as events, and the model decides what changes.
|
|
5
|
+
|
|
6
|
+
This recipe uses the free Community edition (`ag-grid-community`). For a table you style yourself, with a few hundred rows, the lighter [TanStack Table recipe](./data-table.md) is enough.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install ag-grid-community
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## The widget
|
|
15
|
+
|
|
16
|
+
```js live-file=./Grid.js
|
|
17
|
+
// Grid.js
|
|
18
|
+
import { defineWidget } from 'sygnal'
|
|
19
|
+
import {
|
|
20
|
+
createGrid, ModuleRegistry, ClientSideRowModelModule, RowSelectionModule,
|
|
21
|
+
TextEditorModule, NumberEditorModule, CsvExportModule,
|
|
22
|
+
} from 'ag-grid-community'
|
|
23
|
+
|
|
24
|
+
ModuleRegistry.registerModules([
|
|
25
|
+
ClientSideRowModelModule, RowSelectionModule, TextEditorModule, NumberEditorModule, CsvExportModule,
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
export const Grid = defineWidget({
|
|
29
|
+
name: 'Grid',
|
|
30
|
+
mount: (el, props, dispatch) => createGrid(el, {
|
|
31
|
+
columnDefs: props.columns,
|
|
32
|
+
rowData: props.rows,
|
|
33
|
+
getRowId: (params) => params.data.id,
|
|
34
|
+
// edits become events; the grid shows them once the state has them
|
|
35
|
+
readOnlyEdit: true,
|
|
36
|
+
onCellEditRequest: (event) =>
|
|
37
|
+
dispatch('cell-edit', { id: event.data.id, field: event.colDef.field, value: event.newValue }),
|
|
38
|
+
rowSelection: { mode: 'singleRow', checkboxes: false, enableClickSelection: true },
|
|
39
|
+
onSelectionChanged: (event) => dispatch('row-select', event.api.getSelectedRows()[0]?.id ?? null),
|
|
40
|
+
}),
|
|
41
|
+
update: (grid, props) => {
|
|
42
|
+
grid.setGridOption('columnDefs', props.columns)
|
|
43
|
+
grid.setGridOption('rowData', props.rows)
|
|
44
|
+
},
|
|
45
|
+
unmount: (grid) => grid.destroy(),
|
|
46
|
+
events: ['cell-edit', 'row-select'],
|
|
47
|
+
commands: {
|
|
48
|
+
exportCsv: (grid, { fileName }) => grid.exportDataAsCsv({ fileName }),
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- `readOnlyEdit: true` keeps the grid from writing an edit into the row object. It fires `onCellEditRequest` instead, the widget sends that as a `cell-edit` event, and the edit shows up in the grid when the new rows come back through `update`.
|
|
54
|
+
- `getRowId` gives each row its identity, so a new `rowData` array updates only the rows that changed, and keeps the selection, the scroll position and the focused cell.
|
|
55
|
+
- The `exportCsv` command gets the options of the element command (`fileName`).
|
|
56
|
+
|
|
57
|
+
## Using it
|
|
58
|
+
|
|
59
|
+
```jsx live
|
|
60
|
+
// Stock.jsx
|
|
61
|
+
import { Grid } from './Grid.js'
|
|
62
|
+
|
|
63
|
+
const COLUMNS = [
|
|
64
|
+
{ field: 'name', headerName: 'Item', editable: true, flex: 2 },
|
|
65
|
+
{ field: 'count', headerName: 'In stock', editable: true, cellDataType: 'number', flex: 1 },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
export function Stock({ state }) {
|
|
69
|
+
const selected = state.items.find((item) => item.id === state.selected)
|
|
70
|
+
return (
|
|
71
|
+
<section>
|
|
72
|
+
<Grid className="grid" columns={COLUMNS} rows={state.items} />
|
|
73
|
+
<p className="selected">{selected ? `${selected.name}: ${selected.count} in stock` : 'No item selected'}</p>
|
|
74
|
+
<button className="restock" disabled={!selected}>Restock 10</button>
|
|
75
|
+
<button className="export">Export CSV</button>
|
|
76
|
+
</section>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Stock.initialState = {
|
|
81
|
+
items: [
|
|
82
|
+
{ id: 'a1', name: 'Bolts', count: 120 },
|
|
83
|
+
{ id: 'a2', name: 'Nuts', count: 80 },
|
|
84
|
+
{ id: 'a3', name: 'Washers', count: 0 },
|
|
85
|
+
],
|
|
86
|
+
selected: null,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Stock.intent = ({ DOM }) => ({
|
|
90
|
+
EDIT: DOM.select('.grid').events('cell-edit').detail(),
|
|
91
|
+
SELECT: DOM.select('.grid').events('row-select').detail(),
|
|
92
|
+
RESTOCK: DOM.click('.restock'),
|
|
93
|
+
EXPORT: DOM.click('.export'),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const change = (items, id, values) => items.map((item) => (item.id === id ? { ...item, ...values } : item))
|
|
97
|
+
|
|
98
|
+
Stock.model = {
|
|
99
|
+
EDIT: (state, { id, field, value }) => ({ ...state, items: change(state.items, id, { [field]: value }) }),
|
|
100
|
+
SELECT: (state, id) => ({ ...state, selected: id }),
|
|
101
|
+
RESTOCK: (state) => {
|
|
102
|
+
const item = state.items.find((it) => it.id === state.selected)
|
|
103
|
+
return { ...state, items: change(state.items, item.id, { count: item.count + 10 }) }
|
|
104
|
+
},
|
|
105
|
+
EXPORT: { ELEMENT: { exportCsv: '.grid', fileName: 'stock.csv' } },
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```css live
|
|
110
|
+
.grid { height: 400px; }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`COLUMNS` is a module constant, so the `columns` prop keeps its identity and only a change to `items` runs `update`. The grid draws its own theme (Quartz by default); it needs a height to fill.
|
|
114
|
+
|
|
115
|
+
## Testing
|
|
116
|
+
|
|
117
|
+
```jsx
|
|
118
|
+
// Stock.test.jsx
|
|
119
|
+
import { test, expect } from 'vitest'
|
|
120
|
+
import { renderComponent } from 'sygnal'
|
|
121
|
+
import { Stock } from './Stock.jsx'
|
|
122
|
+
|
|
123
|
+
test('edits and restocking change the rows the grid gets', async () => {
|
|
124
|
+
const t = renderComponent(Stock)
|
|
125
|
+
await t.ready()
|
|
126
|
+
t.widget('.grid').dispatch('cell-edit', { id: 'a2', field: 'count', value: 75 })
|
|
127
|
+
await t.next((state) => state.items[1].count === 75)
|
|
128
|
+
|
|
129
|
+
t.widget('.grid').dispatch('row-select', 'a3')
|
|
130
|
+
await t.next((state) => state.selected === 'a3')
|
|
131
|
+
t.simulateEvent('.restock', 'click')
|
|
132
|
+
await t.next((state) => state.items[2].count === 10)
|
|
133
|
+
expect(t.query('.selected').textContent).toBe('Washers: 10 in stock')
|
|
134
|
+
expect(t.widget('.grid').props.rows.map((item) => item.count)).toEqual([120, 75, 10])
|
|
135
|
+
|
|
136
|
+
t.simulateEvent('.export', 'click')
|
|
137
|
+
await t.settle()
|
|
138
|
+
expect(t.commands()).toEqual([{ exportCsv: '.grid', fileName: 'stock.csv' }])
|
|
139
|
+
t.dispose()
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In a real browser (`renderComponent(Stock, { dom: 'real' })` under Playwright), double-click a cell (`.grid [row-id="a2"] [col-id="count"]`), type and press Enter: the state gets the new count, and the cell shows it once the new rows reach the grid. `t.widget('.grid').instance` is the grid API (`getSelectedRows()`, `isDestroyed()`).
|
|
144
|
+
|
|
145
|
+
## Size
|
|
146
|
+
|
|
147
|
+
Measured with Vite, minified and gzipped, Sygnal not included: **240 KB** with the five modules above. `AllCommunityModule` registers every Community feature and makes it 333 KB. `defineWidget` adds 1.1 KB for the first widget in an app.
|
|
148
|
+
|
|
149
|
+
## Pitfalls
|
|
150
|
+
|
|
151
|
+
- **Keep `readOnlyEdit: true`.** Without it, AG Grid writes each edit into the row object it was given, which is the object in your state: the state changes without an action, and the next render can't tell anything changed.
|
|
152
|
+
- **Give rows an id.** Without `getRowId`, each new `rowData` array replaces every row: the selection and the focused cell are lost after each edit.
|
|
153
|
+
- **Register the modules you use.** AG Grid 33 and later are modular. An option whose module isn't registered doesn't work, and the grid logs an error with a link; register `ValidationModule` in development to get messages that name the missing module.
|
|
154
|
+
- **Don't recreate `columns` in the view.** A new array on every render runs `update` every time and makes the grid rebuild its columns. Keep column definitions in a constant, or in state if the user can change them.
|
|
155
|
+
- **Keyboard and screen readers.** The grid has its own keyboard navigation and ARIA grid roles: don't wrap it in elements that capture arrow keys, and label the page section it sits in.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/recipes by scripts/copy-guides.mjs; online: https://sygnal.js.org/recipes/ -->
|
|
2
|
+
# Data Table (TanStack Table)
|
|
3
|
+
|
|
4
|
+
[TanStack Table](https://tanstack.com/table) is headless: it computes which rows to show (sorted, filtered, paged) and renders nothing. That fits Sygnal without a widget. Keep the table's settings (sorting, search, page) in your state, build the table from that state in the view, and render its rows with JSX. Sygnal still renders every element, so the table works in the mock DOM, on the server, and with sygnal-check.
|
|
5
|
+
|
|
6
|
+
This recipe uses **v9** (`@tanstack/table-core` 9.x), whose API differs from v8: `constructTable` and `tableFeatures` replace `createTable` and the `get*RowModel` options.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @tanstack/table-core
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## The table
|
|
15
|
+
|
|
16
|
+
```js live-file=./peopleTable.js
|
|
17
|
+
// peopleTable.js
|
|
18
|
+
import {
|
|
19
|
+
constructTable, createColumnHelper, tableFeatures,
|
|
20
|
+
rowSortingFeature, createSortedRowModel, sortFn_alphanumeric, sortFn_basic,
|
|
21
|
+
columnFilteringFeature, globalFilteringFeature, createFilteredRowModel, filterFn_includesString,
|
|
22
|
+
rowPaginationFeature, createPaginatedRowModel,
|
|
23
|
+
} from '@tanstack/table-core'
|
|
24
|
+
import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings'
|
|
25
|
+
|
|
26
|
+
const features = tableFeatures({
|
|
27
|
+
coreReactivityFeature: storeReactivityBindings(),
|
|
28
|
+
rowSortingFeature,
|
|
29
|
+
sortedRowModel: createSortedRowModel(),
|
|
30
|
+
sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
|
|
31
|
+
columnFilteringFeature,
|
|
32
|
+
globalFilteringFeature,
|
|
33
|
+
filteredRowModel: createFilteredRowModel(),
|
|
34
|
+
filterFns: { includesString: filterFn_includesString },
|
|
35
|
+
rowPaginationFeature,
|
|
36
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const column = createColumnHelper()
|
|
40
|
+
const columns = column.columns([
|
|
41
|
+
column.accessor('name', { header: 'Name', sortFn: 'alphanumeric' }),
|
|
42
|
+
column.accessor('team', { header: 'Team', sortFn: 'alphanumeric' }),
|
|
43
|
+
column.accessor('points', { header: 'Points', sortFn: 'basic' }),
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
export const PAGE_SIZE = 3
|
|
47
|
+
|
|
48
|
+
// A pure function of the component's state: the table never holds state of its own
|
|
49
|
+
export const peopleTable = (state) => constructTable({
|
|
50
|
+
features,
|
|
51
|
+
columns,
|
|
52
|
+
data: state.people,
|
|
53
|
+
getRowId: (person) => person.id,
|
|
54
|
+
globalFilterFn: 'includesString',
|
|
55
|
+
state: {
|
|
56
|
+
sorting: state.sorting,
|
|
57
|
+
globalFilter: state.search,
|
|
58
|
+
pagination: { pageIndex: state.page, pageSize: PAGE_SIZE },
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// click a column: ascending, then descending, then unsorted
|
|
63
|
+
export const nextSorting = (sorting, id) =>
|
|
64
|
+
sorting[0]?.id !== id ? [{ id, desc: false }] : sorting[0].desc ? [] : [{ id, desc: true }]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `features`, `columns` and the sort and filter functions are created once, at module level. v9 registers each feature explicitly; an API you didn't register (say `getCanNextPage` without `rowPaginationFeature`) doesn't exist.
|
|
68
|
+
- `peopleTable(state)` passes every setting through the `state` option. The table owns no state and nobody calls its setters (`setSorting`, `nextPage`): actions change Sygnal's state, and the next render builds the table from it.
|
|
69
|
+
|
|
70
|
+
## Using it
|
|
71
|
+
|
|
72
|
+
```jsx live
|
|
73
|
+
// People.jsx
|
|
74
|
+
import { peopleTable, nextSorting } from './peopleTable.js'
|
|
75
|
+
|
|
76
|
+
const ARIA_SORT = { asc: 'ascending', desc: 'descending' }
|
|
77
|
+
|
|
78
|
+
export function People({ state }) {
|
|
79
|
+
const table = peopleTable(state)
|
|
80
|
+
return (
|
|
81
|
+
<section>
|
|
82
|
+
<label>Search <input className="search" value={state.search} /></label>
|
|
83
|
+
<table>
|
|
84
|
+
<thead>
|
|
85
|
+
{table.getHeaderGroups().map((group) => (
|
|
86
|
+
<tr key={group.id}>
|
|
87
|
+
{group.headers.map((header) => (
|
|
88
|
+
<th key={header.id} aria-sort={ARIA_SORT[header.column.getIsSorted()] || 'none'}>
|
|
89
|
+
<button className="sort" data={{ column: header.id }}>{header.column.columnDef.header}</button>
|
|
90
|
+
</th>
|
|
91
|
+
))}
|
|
92
|
+
</tr>
|
|
93
|
+
))}
|
|
94
|
+
</thead>
|
|
95
|
+
<tbody>
|
|
96
|
+
{table.getRowModel().rows.map((row) => (
|
|
97
|
+
<tr key={row.id}>
|
|
98
|
+
{row.getAllCells().map((cell) => <td key={cell.id}>{cell.getValue()}</td>)}
|
|
99
|
+
</tr>
|
|
100
|
+
))}
|
|
101
|
+
</tbody>
|
|
102
|
+
</table>
|
|
103
|
+
<button className="prev" disabled={!table.getCanPreviousPage()}>Previous</button>
|
|
104
|
+
<span className="page">Page {state.page + 1} of {Math.max(table.getPageCount(), 1)}</span>
|
|
105
|
+
<button className="next" disabled={!table.getCanNextPage()}>Next</button>
|
|
106
|
+
</section>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
People.initialState = {
|
|
111
|
+
people: [
|
|
112
|
+
{ id: 'p1', name: 'Ada', team: 'Engines', points: 36 },
|
|
113
|
+
{ id: 'p2', name: 'Grace', team: 'Compilers', points: 45 },
|
|
114
|
+
{ id: 'p3', name: 'Alan', team: 'Engines', points: 41 },
|
|
115
|
+
{ id: 'p4', name: 'Edsger', team: 'Compilers', points: 12 },
|
|
116
|
+
{ id: 'p5', name: 'Barbara', team: 'Languages', points: 28 },
|
|
117
|
+
],
|
|
118
|
+
sorting: [],
|
|
119
|
+
search: '',
|
|
120
|
+
page: 0,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
People.intent = ({ DOM }) => ({
|
|
124
|
+
SEARCH: DOM.input('.search').value(),
|
|
125
|
+
SORT: DOM.click('.sort').data('column'),
|
|
126
|
+
PREV: DOM.click('.prev'),
|
|
127
|
+
NEXT: DOM.click('.next'),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
People.model = {
|
|
131
|
+
SEARCH: (state, search) => ({ ...state, search, page: 0 }),
|
|
132
|
+
SORT: (state, id) => ({ ...state, sorting: nextSorting(state.sorting, id), page: 0 }),
|
|
133
|
+
PREV: (state) => ({ ...state, page: state.page - 1 }),
|
|
134
|
+
NEXT: (state) => ({ ...state, page: state.page + 1 }),
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The column headers are buttons inside the `<th>`, so they work with the keyboard, and `aria-sort` tells screen readers which column is sorted and how.
|
|
139
|
+
|
|
140
|
+
## Testing
|
|
141
|
+
|
|
142
|
+
Everything is plain markup, so the mock DOM shows the result directly:
|
|
143
|
+
|
|
144
|
+
```jsx
|
|
145
|
+
// People.test.jsx
|
|
146
|
+
import { test, expect } from 'vitest'
|
|
147
|
+
import { renderComponent } from 'sygnal'
|
|
148
|
+
import { People } from './People.jsx'
|
|
149
|
+
|
|
150
|
+
const names = (t) => t.queryAll('tbody tr').map((row) => row.querySelector('td').textContent)
|
|
151
|
+
|
|
152
|
+
test('sorts, searches and pages', async () => {
|
|
153
|
+
const t = renderComponent(People)
|
|
154
|
+
await t.ready()
|
|
155
|
+
expect(names(t)).toEqual(['Ada', 'Grace', 'Alan'])
|
|
156
|
+
expect(t.query('.page').textContent).toBe('Page 1 of 2')
|
|
157
|
+
|
|
158
|
+
t.simulateEvent('.sort[data-column="points"]', 'click')
|
|
159
|
+
t.simulateEvent('.sort[data-column="points"]', 'click')
|
|
160
|
+
await t.next((state) => state.sorting[0]?.desc === true)
|
|
161
|
+
expect(names(t)).toEqual(['Grace', 'Alan', 'Ada'])
|
|
162
|
+
expect(t.queryAll('th')[2].getAttribute('aria-sort')).toBe('descending')
|
|
163
|
+
|
|
164
|
+
t.simulateEvent('.next', 'click')
|
|
165
|
+
await t.next((state) => state.page === 1)
|
|
166
|
+
expect(names(t)).toEqual(['Barbara', 'Edsger'])
|
|
167
|
+
expect(t.query('.next').disabled).toBe(true)
|
|
168
|
+
|
|
169
|
+
t.simulateEvent('.search', 'input', { value: 'compilers' })
|
|
170
|
+
await t.next((state) => state.search === 'compilers')
|
|
171
|
+
expect(names(t)).toEqual(['Grace', 'Edsger'])
|
|
172
|
+
expect(t.query('.page').textContent).toBe('Page 1 of 1')
|
|
173
|
+
t.dispose()
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`peopleTable` is a plain function too: a test can call it with a state object and check `getRowModel().rows` without rendering anything.
|
|
178
|
+
|
|
179
|
+
## Size
|
|
180
|
+
|
|
181
|
+
Measured with Vite, minified and gzipped, Sygnal not included: **16 KB** for `table-core` with sorting, filtering and pagination. Each feature you register adds to it; `stockFeatures` registers all of them.
|
|
182
|
+
|
|
183
|
+
## Performance
|
|
184
|
+
|
|
185
|
+
The view builds a new table on every render. In a browser that took about 1.5–3 ms for 1,000 rows and 9–23 ms for 10,000 rows (sorted, filtered and paged, depending on the engine). That is fine up to a few thousand rows. Beyond that:
|
|
186
|
+
|
|
187
|
+
- page or filter on the server and pass only the visible rows as `data` (`manualPagination`, `manualSorting`, `manualFiltering`);
|
|
188
|
+
- or render all rows with a [`VirtualCollection`](../virtual-collections.md) and sort and filter the array in the model.
|
|
189
|
+
|
|
190
|
+
## Pitfalls
|
|
191
|
+
|
|
192
|
+
- **Register a reactivity binding.** v9's `constructTable` needs `coreReactivityFeature` in `features`. Without it, it throws `Cannot read properties of undefined (reading 'wrapExternalAtoms')`. `storeReactivityBindings()` is the framework-neutral one; the table's own atoms aren't used here, because the state comes from Sygnal.
|
|
193
|
+
- **Create features and columns once.** Build them at module level, not in the view: TanStack expects them to keep their identity, and creating them per render also costs time.
|
|
194
|
+
- **One owner per setting.** Pass each setting through `state` and change it in the model. Calling the table's own setters (`table.setSorting`, `table.nextPage`) changes nothing that lasts: the next render builds a new table from Sygnal's state.
|
|
195
|
+
- **Reset the page.** A new search or sort can leave the current page past the end. The reducers here go back to page 0, as most tables do.
|
|
196
|
+
- **Call methods on their objects.** `row.getValue('name')` works; `const { getValue } = row` loses `this`.
|