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,169 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/advanced by scripts/copy-guides.mjs; online: https://sygnal.js.org/advanced/ -->
|
|
2
|
+
# Error Boundaries
|
|
3
|
+
|
|
4
|
+
Catch and recover from errors in component rendering without crashing the entire application.
|
|
5
|
+
|
|
6
|
+
## The `onError` Static Property
|
|
7
|
+
|
|
8
|
+
```jsx live
|
|
9
|
+
function BrokenComponent({ state }) {
|
|
10
|
+
if (state.count > 5) throw new Error('Count too high!')
|
|
11
|
+
return <div>Count: {state.count} <button className="add">+1</button></div>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
BrokenComponent.initialState = { count: 4 }
|
|
15
|
+
BrokenComponent.intent = ({ DOM }) => ({ ADD: DOM.click('.add') })
|
|
16
|
+
BrokenComponent.model = { ADD: (state) => ({ ...state, count: state.count + 1 }) }
|
|
17
|
+
|
|
18
|
+
BrokenComponent.onError = (error, { componentName }) => (
|
|
19
|
+
<div className="error-fallback">
|
|
20
|
+
<h3>Something went wrong in {componentName}</h3>
|
|
21
|
+
<p>{error.message}</p>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Error boundaries protect three code paths:
|
|
27
|
+
- **View errors** — The view function throws. Renders `onError` fallback or an empty `<div data-sygnal-error>`.
|
|
28
|
+
- **Reducer errors** — A model reducer throws. Returns the previous state unchanged (no state corruption).
|
|
29
|
+
- **Sub-component errors** — A child component fails to instantiate. Replaces with fallback VNode.
|
|
30
|
+
|
|
31
|
+
Without `onError`, errors are logged to `console.error` and a minimal placeholder is rendered.
|
|
32
|
+
|
|
33
|
+
Each case is logged with a [diagnostic code](https://sygnal.js.org/guide/diagnostics/): a view that throws is [SYG406](https://sygnal.js.org/reference/errors/#syg406), a reducer that throws [SYG216](https://sygnal.js.org/reference/errors/#syg216), an EFFECT that throws [SYG214](https://sygnal.js.org/reference/errors/#syg214), a child that fails to instantiate [SYG408](https://sygnal.js.org/reference/errors/#syg408), and an `onError` that throws [SYG407](https://sygnal.js.org/reference/errors/#syg407). When the component has an `onError` and its fallback renders, the view error is reported as a warning rather than an error.
|
|
34
|
+
|
|
35
|
+
## App-level error hook
|
|
36
|
+
|
|
37
|
+
A component's `onError` decides what to render. To *report* errors, for example to an error tracker, give the whole app one hook with `run()`'s `onError` option:
|
|
38
|
+
|
|
39
|
+
```jsx
|
|
40
|
+
import { run } from 'sygnal'
|
|
41
|
+
import App from './App.jsx'
|
|
42
|
+
import { tracker } from './tracker.js'
|
|
43
|
+
|
|
44
|
+
run(App, {}, {
|
|
45
|
+
onError: (error, { componentName, action, phase, driver }) => {
|
|
46
|
+
tracker.captureException(error, { tags: { componentName, action, phase, driver } })
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The hook is for reporting only. It is called once per error, after the component's own `onError` (if any) has picked the fallback, and in every diagnostics mode, production included. Its second argument says where the error happened:
|
|
52
|
+
|
|
53
|
+
| `phase` | When | Also set |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `'view'` | A view threw (the boundary's fallback, or the empty `<div data-sygnal-error>`, renders) | `componentName` |
|
|
56
|
+
| `'reducer'` | A STATE reducer or another sink's reducer threw; the state is unchanged | `componentName`, `action` |
|
|
57
|
+
| `'effect'` | An `EFFECT` threw, or the promise it returned rejected | `componentName`, `action` |
|
|
58
|
+
| `'intent'` | An intent stream errored; it stops emitting (the error is also logged) | `componentName`, `action` (the stream's action) |
|
|
59
|
+
| `'context'` | A `.context` entry threw; the entry keeps its last value ([SYG404](https://sygnal.js.org/reference/errors/#syg404)) | `componentName` |
|
|
60
|
+
| `'declaration'` | A static declaration a driver reads from the component (`connections`, `resources`, a router's `route`, `head`) threw; nothing is sent for it | `componentName` |
|
|
61
|
+
| `'instantiate'` | A child component failed to instantiate (reported after the parent's boundary) | `componentName` (the parent) |
|
|
62
|
+
| `'driver'` | A driver threw while handling a value sent to it; the error is still thrown afterwards, as before | `driver` (the sink name) |
|
|
63
|
+
| `'dispose'` | A stream's `stop()` threw while a removed component's streams were stopped; the other streams still stop (it is also logged) | `componentName` |
|
|
64
|
+
| `'widget'` | A [widget's](./widgets.md#errors) `mount` or `update` threw (the owner's fallback renders in its place), or its `unmount` threw | `componentName` (the component that renders the widget) |
|
|
65
|
+
| `'patch'` | The DOM driver's patch threw (a vnode `hook` of your own or a DOM module). Snabbdom stopped half way, so the app's DOM stops updating (its state and events go on); later patches aren't tried, so the error is reported once. Disposing the app still removes what it mounted (a Portal's content). The mount point is marked `data-sygnal-error="patch"` ([below](#after-a-patch-error)) | none |
|
|
66
|
+
|
|
67
|
+
`'driver'` only covers a driver that throws synchronously while it receives a sink value. Errors inside a driver's own streams, or error events on its sources, are not reported there: handle them where the driver reports them (for HTTP, the `error` [reply action](./http.md)).
|
|
68
|
+
|
|
69
|
+
Each `run()` has its own hook, so two apps on one page report separately. If the hook itself throws, the error is logged once with `console.error` and swallowed; the app keeps running. Without the option nothing changes: errors are logged as before.
|
|
70
|
+
|
|
71
|
+
### After a patch error
|
|
72
|
+
|
|
73
|
+
A component's `onError` covers its view, but an error while the DOM is patched (a vnode `hook` of your own, a DOM module) happens below every component: the DOM is half updated, and no tree Sygnal could patch from matches it. So, after a patch error:
|
|
74
|
+
|
|
75
|
+
- **The app's DOM stops updating.** The screen keeps what the last patch left; later renders aren't patched.
|
|
76
|
+
- **State and events go on.** Clicks and keys still reach the intents, reducers and effects run, drivers get their values (a save still saves).
|
|
77
|
+
- **The error is reported once**, to `onError` with phase `'patch'` (and the hooks' `onError`); without an `onError` it is logged with `console.error`. This holds for a DOM driver you pass to `run()` too (`makeViewTransitionDOMDriver`).
|
|
78
|
+
- **The mount point is marked** `data-sygnal-error="patch"`: the element `run()` renders into (`mountPoint`, `#root` by default). The mark is already there when `onError` runs. Disposing the app removes the mark, along with what the app rendered; a new `run()` into the same element clears a mark that a failed app (not disposed) left. A `DocumentFragment` mount point gets no mark: it has no attributes.
|
|
79
|
+
|
|
80
|
+
The mark lets CSS tell the user, with no JavaScript, that the page is out of date:
|
|
81
|
+
|
|
82
|
+
```css
|
|
83
|
+
#root[data-sygnal-error="patch"] {
|
|
84
|
+
position: relative;
|
|
85
|
+
}
|
|
86
|
+
#root[data-sygnal-error="patch"]::after {
|
|
87
|
+
content: 'This page stopped updating. Reload to continue.';
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset: 0;
|
|
90
|
+
display: grid;
|
|
91
|
+
place-items: center;
|
|
92
|
+
background: rgb(255 255 255 / 0.85);
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or show a banner from the `onError` hook. Put it outside the mount point (the app's DOM no longer updates) and offer a reload:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { run } from 'sygnal'
|
|
101
|
+
import App from './App.jsx'
|
|
102
|
+
|
|
103
|
+
run(App, {}, {
|
|
104
|
+
onError: (error, { phase }) => {
|
|
105
|
+
if (phase !== 'patch') return
|
|
106
|
+
const banner = document.createElement('div')
|
|
107
|
+
banner.setAttribute('role', 'alert')
|
|
108
|
+
banner.className = 'reload-banner'
|
|
109
|
+
banner.textContent = 'Something went wrong displaying this page. '
|
|
110
|
+
const reload = document.createElement('button')
|
|
111
|
+
reload.textContent = 'Reload'
|
|
112
|
+
reload.addEventListener('click', () => location.reload())
|
|
113
|
+
banner.append(reload)
|
|
114
|
+
document.body.prepend(banner)
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Compared with React, which unmounts the whole tree when an error isn't caught by a boundary (the page goes blank), Sygnal keeps the last good screen in place, keeps the app's state and work going, and marks the screen as stale, so you decide what the user sees.
|
|
120
|
+
|
|
121
|
+
### Vike
|
|
122
|
+
|
|
123
|
+
In a [Vike](https://sygnal.js.org/integration/vike/) app, the hook is the `sygnalOnError` config: define it in `+sygnalOnError.js` (or in `+config.js`). It is passed to `renderToString` on the server and to `run()` in the browser. (Vike's own `onError` is a different hook: server-only, called with `(error, pageContext)` for any error during rendering.)
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
// pages/+sygnalOnError.js: used by renderToString on the server and by run() in the browser
|
|
127
|
+
export default function sygnalOnError(error, { componentName, action, phase }) {
|
|
128
|
+
console.error('[' + phase + ']', componentName, action, error)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Astro
|
|
133
|
+
|
|
134
|
+
The [Astro](https://sygnal.js.org/integration/astro/) integration takes the path of a module whose default export is the hook. It is used when islands render on the server and when they start in the browser:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
// astro.config.mjs
|
|
138
|
+
import { defineConfig } from 'astro/config'
|
|
139
|
+
import sygnal from 'sygnal/astro'
|
|
140
|
+
|
|
141
|
+
export default defineConfig({
|
|
142
|
+
integrations: [sygnal({ onError: './src/onError.js' })],
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### renderToString
|
|
147
|
+
|
|
148
|
+
[`renderToString`](./ssr.md) takes the same option. On the server only views run, so every report has the phase `'view'`:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { renderToString } from 'sygnal'
|
|
152
|
+
|
|
153
|
+
const html = renderToString(Page, {
|
|
154
|
+
onError: (error, info) => logger.error({ ...info, message: error.message }),
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Tests
|
|
159
|
+
|
|
160
|
+
`renderComponent` takes it too, to assert on what would be reported:
|
|
161
|
+
|
|
162
|
+
```jsx
|
|
163
|
+
const reported = []
|
|
164
|
+
const t = renderComponent(Checkout, { onError: (error, info) => reported.push(info) })
|
|
165
|
+
t.simulateAction('PAY')
|
|
166
|
+
await t.settle()
|
|
167
|
+
expect(reported).toEqual([{ componentName: 'Checkout', action: 'PAY', phase: 'reducer' }])
|
|
168
|
+
t.dispose()
|
|
169
|
+
```
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/guide by scripts/copy-guides.mjs; online: https://sygnal.js.org/guide/ -->
|
|
2
|
+
# Forms Reference
|
|
3
|
+
|
|
4
|
+
The details of the [`form` behavior](./forms.md). Start with the recipe on [Forms](./forms.md): it covers validation, the submit, pending state, server errors and field arrays.
|
|
5
|
+
|
|
6
|
+
## The slice
|
|
7
|
+
|
|
8
|
+
`state.form` holds:
|
|
9
|
+
|
|
10
|
+
| Field | |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `values`, `initial` | The current values, and the ones the form started with (or was last saved or reset with) |
|
|
13
|
+
| `errors` | Every current schema error by field name, shown or not |
|
|
14
|
+
| `touched`, `server`, `remote`, `pending` | Blurred fields; [server errors](./forms.md#server-errors-and-failed-submits); [check](#async-checks) results; checks running |
|
|
15
|
+
| `submitting`, `submitted`, `submitCount`, `queued`, `validating`, `validated` | Submit state: the request sent and not answered yet; `form.DONE` arrived (or a submit without a request was dispatched); attempts; a submit waits for a check or an async schema; an async schema runs; the schema has answered since the start or the last reset |
|
|
16
|
+
| `fields`, `valid`, `dirty`, `error` | Calculated: per-field view data; no errors as of the schema's last answer (`false` before the first); values differ from `initial`; the form-level message |
|
|
17
|
+
|
|
18
|
+
The options are on [Forms](./forms.md#options).
|
|
19
|
+
|
|
20
|
+
## The actions
|
|
21
|
+
|
|
22
|
+
Named after the `uses` key (`form.CHANGE` for `uses = { form: … }`):
|
|
23
|
+
|
|
24
|
+
| Action | Data |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `form.CHANGE` | `{ name, value }`: from the form element's `input` events ([field types](./forms.md#field-types); a checkbox gives `checked`, and its `value` as `item`) |
|
|
27
|
+
| `form.BLUR` | The field name, from `focusout` |
|
|
28
|
+
| `form.SUBMIT` | From the form element's `submit` (default prevented) |
|
|
29
|
+
| `form.ADD` / `form.REMOVE` | `{ field, value }` / `{ field, id }`: [field array](./forms.md#field-arrays) rows |
|
|
30
|
+
| `form.ERRORS` | Server errors: an error reply, a map or a list of issues |
|
|
31
|
+
| `form.DONE` | The submit's request was saved: `initial` becomes `values`, `submitted` turns on ([a submit without a request](./forms.md#submit-without-a-request) needs none) |
|
|
32
|
+
| `form.RESET` | Back to `initial`, or to the values given |
|
|
33
|
+
| `form.VALIDATE` | Internal: the first validation when the host starts; with `resetOnShow`, the start over (also each time the form element appears again) |
|
|
34
|
+
|
|
35
|
+
As for any behavior, a host model entry with the same name runs after the form's: `'form.DONE': (state) => ({ ...state, done: true })` shows a confirmation. Trigger an action from elsewhere with an intent action of that name, or `t.simulateAction('form.RESET')` in a test.
|
|
36
|
+
|
|
37
|
+
## Any Standard Schema
|
|
38
|
+
|
|
39
|
+
`form()` takes any object with `~standard.validate`: zod (3.24 and later), valibot (1.0 and later), arktype, or one you write. A schema's issue paths become field names, and an issue without a path is a form-level message (`state.form.error`, shown after a submit):
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const EMAIL = /^\S+@\S+\.\S+$/
|
|
43
|
+
|
|
44
|
+
export const signupSchema = {
|
|
45
|
+
'~standard': {
|
|
46
|
+
version: 1,
|
|
47
|
+
vendor: 'my-app',
|
|
48
|
+
validate(values) {
|
|
49
|
+
const issues = []
|
|
50
|
+
if (!values.name.trim()) issues.push({ message: 'Enter your name', path: ['name'] })
|
|
51
|
+
if (!EMAIL.test(values.email)) issues.push({ message: 'Enter a valid email address', path: ['email'] })
|
|
52
|
+
return issues.length ? { issues } : { value: { ...values, email: values.email.trim().toLowerCase() } }
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
An async schema (valibot's `pipeAsync`, zod's async refinements) works too: `state.form.validating` is `true` while it runs (also at the start, until its first answer) and a submit waits for it. `state.form.valid` is `false` until the first answer; after that it keeps the last answer's validity while the schema re-validates, so `<button disabled={!state.form.valid}>` doesn't flicker on every keystroke (show progress with `validating`). The schema first runs when the component starts, not when `form()` is called. Something that isn't a Standard Schema is [SYG231](https://sygnal.js.org/reference/errors/#syg231).
|
|
59
|
+
|
|
60
|
+
## Async checks
|
|
61
|
+
|
|
62
|
+
Some checks need the server: is this email address taken? `check` names a field and the request that checks it. The request goes to the `HTTP` driver ([`makeFetchDriver`](./http.md)) with a reply action the form handles, and `latest: true`, so a check for a newer value replaces the older one.
|
|
63
|
+
|
|
64
|
+
In this demo, a stand-in server answers both requests: `ada@example.com` is already registered, and so is each address that signs up.
|
|
65
|
+
|
|
66
|
+
```js live-server
|
|
67
|
+
const registered = ['ada@example.com']
|
|
68
|
+
|
|
69
|
+
export default {
|
|
70
|
+
'GET /api/email-available': ({ query }) => ({ json: { available: !registered.includes(query.email) } }),
|
|
71
|
+
'POST /api/signup': ({ json }) => {
|
|
72
|
+
if (registered.includes(json.email)) return { status: 422, json: { errors: { email: 'Already registered' } } }
|
|
73
|
+
registered.push(json.email)
|
|
74
|
+
return { status: 201, json: { id: registered.length } }
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The schema checks the address's form, in `./schema.js`:
|
|
80
|
+
|
|
81
|
+
```js live-file=./schema.js
|
|
82
|
+
import { z } from 'zod'
|
|
83
|
+
|
|
84
|
+
export const signupSchema = z.object({
|
|
85
|
+
email: z.string().trim().toLowerCase().email('Enter a valid email address'),
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The form checks the address with the server when the field loses focus:
|
|
90
|
+
|
|
91
|
+
```jsx live
|
|
92
|
+
import { form } from 'sygnal'
|
|
93
|
+
import { signupSchema } from './schema.js'
|
|
94
|
+
|
|
95
|
+
function Signup({ state, uid }) {
|
|
96
|
+
const f = state.form.fields
|
|
97
|
+
return (
|
|
98
|
+
<form className="signup" noValidate>
|
|
99
|
+
<label for={uid('email')}>Email</label>
|
|
100
|
+
<input id={uid('email')} name="email" type="email" value={f.email.value} aria-invalid={f.email.invalid} aria-describedby={uid('email-error')} />
|
|
101
|
+
<p id={uid('email-error')}>{f.email.pending ? 'Checking…' : f.email.error}</p>
|
|
102
|
+
<button type="submit" disabled={state.form.submitting}>Sign up</button>
|
|
103
|
+
<p role="status">{state.form.submitted ? 'Signed up.' : ''}</p>
|
|
104
|
+
</form>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Signup.uses = {
|
|
109
|
+
form: form(signupSchema, {
|
|
110
|
+
values: { email: '' },
|
|
111
|
+
submit: 'SIGN_UP',
|
|
112
|
+
check: {
|
|
113
|
+
email: {
|
|
114
|
+
request: (email) => ({ url: '/api/email-available', query: { email } }),
|
|
115
|
+
error: (body) => !body.available && 'This email is already registered',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Signup.model = {
|
|
122
|
+
SIGN_UP: { HTTP: (state, values) => ({ url: '/api/signup', method: 'POST', json: values, ok: 'form.DONE', error: 'form.ERRORS' }) },
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The message line keeps its height, so the button doesn't move when `Checking…` appears ([why](./forms.md#what-the-form-does)):
|
|
127
|
+
|
|
128
|
+
```css live
|
|
129
|
+
.signup p { min-height: 1.5em; }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- A field is checked when it loses focus, once per value, and only when its schema error is clear and it isn't empty. `error(body)` turns the reply into a message, or a falsy value when the value is fine.
|
|
133
|
+
- A submit while a check runs (or before a field was ever checked) waits for it: `state.form.queued` is `true`, and the submit goes on once every check passed, or stops and focuses the field whose check failed. Checks for several fields run one after the other.
|
|
134
|
+
- Editing the field drops its check: a late reply for the old value is ignored. Editing any field cancels a queued submit.
|
|
135
|
+
- A check that fails (network error, 500) doesn't block the form: the server validates again on submit.
|
|
136
|
+
|
|
137
|
+
A test answers the check like any request:
|
|
138
|
+
|
|
139
|
+
```jsx
|
|
140
|
+
import { renderComponent } from 'sygnal'
|
|
141
|
+
import { it, expect } from 'vitest'
|
|
142
|
+
import Signup from './Signup.jsx'
|
|
143
|
+
|
|
144
|
+
it('validates, checks the email, and posts the cleaned values', async () => {
|
|
145
|
+
const t = renderComponent(Signup, { strict: true })
|
|
146
|
+
await t.ready()
|
|
147
|
+
t.simulateEvent('[name="email"]', 'input', { value: 'nope' })
|
|
148
|
+
t.simulateEvent('[name="email"]', 'focusout')
|
|
149
|
+
await t.settle()
|
|
150
|
+
expect(t.state.form.fields.email.error).toBe('Enter a valid email address')
|
|
151
|
+
|
|
152
|
+
t.simulateEvent('[name="email"]', 'input', { value: 'Ada@Example.com' })
|
|
153
|
+
t.simulateEvent('[name="email"]', 'focusout')
|
|
154
|
+
await t.settle()
|
|
155
|
+
expect(t.requests('HTTP').at(-1)).toMatchObject({ url: '/api/email-available' })
|
|
156
|
+
await t.respond('HTTP', { available: true })
|
|
157
|
+
|
|
158
|
+
t.simulateEvent('.signup', 'submit')
|
|
159
|
+
await t.settle()
|
|
160
|
+
expect(t.requests('HTTP').at(-1)).toMatchObject({ url: '/api/signup', json: { email: 'ada@example.com' } })
|
|
161
|
+
await t.fail('HTTP', { status: 422, body: { errors: { email: 'Already registered' } } })
|
|
162
|
+
expect(t.state.form.fields.email.error).toBe('Already registered')
|
|
163
|
+
t.expectNoDiagnostics()
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Two forms in one component
|
|
168
|
+
|
|
169
|
+
Each `form` use listens on its `form` selector inside the component, so two uses left at the default `'form'` would both hear every form element: typing in one changes the other, and each submit is handled twice. Give each form element a class and pass it ([SYG237](https://sygnal.js.org/reference/errors/#syg237) warns otherwise); the first invalid field focused on submit is then also searched in that form only:
|
|
170
|
+
|
|
171
|
+
```jsx
|
|
172
|
+
import { form } from 'sygnal'
|
|
173
|
+
import { loginSchema, newsSchema } from './schemas.js'
|
|
174
|
+
|
|
175
|
+
function Account({ state }) {
|
|
176
|
+
const login = state.login.fields, news = state.news.fields
|
|
177
|
+
return (
|
|
178
|
+
<div>
|
|
179
|
+
<form className="login" noValidate>
|
|
180
|
+
<input name="email" aria-label="Email" value={login.email.value} />
|
|
181
|
+
<button type="submit">Sign in</button>
|
|
182
|
+
</form>
|
|
183
|
+
<form className="news" noValidate>
|
|
184
|
+
<input name="email" aria-label="Newsletter email" value={news.email.value} />
|
|
185
|
+
<button type="submit">Subscribe</button>
|
|
186
|
+
</form>
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Account.uses = {
|
|
192
|
+
login: form(loginSchema, { values: { email: '' }, submit: 'SIGN_IN', form: '.login' }),
|
|
193
|
+
news: form(newsSchema, { values: { email: '' }, submit: 'SUBSCRIBE', form: '.news' }),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Account.model = {
|
|
197
|
+
SIGN_IN: { HTTP: (state, values) => ({ url: '/api/sign-in', method: 'POST', json: values, ok: 'login.DONE', error: 'login.ERRORS' }) },
|
|
198
|
+
SUBSCRIBE: { HTTP: (state, values) => ({ url: '/api/newsletter', method: 'POST', json: values, ok: 'news.DONE', error: 'news.ERRORS' }) },
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Rows as components
|
|
203
|
+
|
|
204
|
+
The [recipe](./forms.md#the-recipe) renders [field array](./forms.md#field-arrays) rows inline, which needs no child component. When a row is big enough to be a component of its own, render the rows with a [Collection](https://sygnal.js.org/guide/collections/), pass `fields` down, and send the remove up with `PARENT`:
|
|
205
|
+
|
|
206
|
+
```jsx
|
|
207
|
+
import { Collection, form } from 'sygnal'
|
|
208
|
+
import { signupSchema } from './schema.js'
|
|
209
|
+
|
|
210
|
+
function Address({ state, fields, uid }) {
|
|
211
|
+
const city = fields[`addresses.${state.id}.city`]
|
|
212
|
+
return (
|
|
213
|
+
<div className="address">
|
|
214
|
+
<label for={uid('city')}>City</label>
|
|
215
|
+
<input id={uid('city')} name={city.name} value={city.value} aria-invalid={city.invalid} aria-describedby={uid('city-error')} />
|
|
216
|
+
<p id={uid('city-error')}>{city.error}</p>
|
|
217
|
+
<button type="button" className="remove">Remove address</button>
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Address.intent = ({ DOM }) => ({ REMOVE: DOM.click('.remove') })
|
|
223
|
+
Address.model = { REMOVE: { PARENT: (state) => ({ field: 'addresses', id: state.id }) } }
|
|
224
|
+
|
|
225
|
+
function Signup({ state, uid }) {
|
|
226
|
+
const f = state.form.fields
|
|
227
|
+
return (
|
|
228
|
+
<form className="signup" noValidate>
|
|
229
|
+
<label for={uid('email')}>Email</label>
|
|
230
|
+
<input id={uid('email')} name="email" type="email" value={f.email.value} aria-invalid={f.email.invalid} aria-describedby={uid('email-error')} />
|
|
231
|
+
<p id={uid('email-error')}>{f.email.error}</p>
|
|
232
|
+
<fieldset aria-describedby={uid('addresses-error')}>
|
|
233
|
+
<legend>Addresses</legend>
|
|
234
|
+
<Collection of={Address} from={{ get: (s) => s.form.values.addresses }} fields={f} />
|
|
235
|
+
<p id={uid('addresses-error')}>{f.addresses.error}</p>
|
|
236
|
+
<button type="button" className="add-address">Add address</button>
|
|
237
|
+
</fieldset>
|
|
238
|
+
<button type="submit" disabled={state.form.submitting}>Sign up</button>
|
|
239
|
+
</form>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
Signup.uses = {
|
|
244
|
+
form: form(signupSchema, { values: { email: '', addresses: [{ id: 1, city: '' }] }, submit: 'SIGN_UP' }),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
Signup.intent = ({ DOM, CHILD }) => ({
|
|
248
|
+
'form.ADD': DOM.click('.add-address').mapTo({ field: 'addresses', value: { city: '' } }),
|
|
249
|
+
'form.REMOVE': CHILD.select(Address),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
Signup.model = {
|
|
253
|
+
SIGN_UP: { HTTP: (state, values) => ({ url: '/api/signup', method: 'POST', json: values, ok: 'form.DONE', error: 'form.ERRORS' }) },
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
- The Collection reads the rows through a read-only lens (`from={{ get }}`): rows don't write their own state, the form does, because the row's inputs are inside the form element.
|
|
258
|
+
- Fields of child components and Collection items inside the form element count as the form's fields.
|
|
259
|
+
- A failed submit focuses the first invalid field even inside a row.
|
|
260
|
+
|
|
261
|
+
## Saving and resetting
|
|
262
|
+
|
|
263
|
+
`form.DONE` marks the submit saved: `submitting` turns off, `submitted` on, and the saved values become `initial` (so `dirty` is `false` again).
|
|
264
|
+
|
|
265
|
+
Whether a valid submit waits for `form.DONE` depends on the host's entry for the `submit` action, checked when the host starts:
|
|
266
|
+
|
|
267
|
+
- **With the `http` sink** (`SIGN_UP: { HTTP: … }`, or the sink the `http` option names, such as `http: 'API'`): `submitting` is `true` until `form.DONE` or `form.ERRORS`, and a submit meanwhile is dropped ([SYG232](https://sygnal.js.org/reference/errors/#syg232)). Even when the entry returns `ABORT` this time, the form waits.
|
|
268
|
+
- **Anything else** (`STATE`, `PARENT`, `EVENTS`, `EFFECT`, `ROUTER`, a reducer function): no request, so the submit is done at once, as if `form.DONE` had arrived (no host `form.DONE` entry runs). An `EFFECT` that calls `next('form.DONE')` itself still works; the second `DONE` changes nothing. Work that answers later without the `http` sink (a promise in an `EFFECT`) gets no pending state: send it through a driver as a request instead.
|
|
269
|
+
- **A host that unmounts** keeps its slice in the parent's state when it has a `state` prop. When it mounts again, the form clears `submitting`, `queued` and running checks (their replies went to the old host); the values, errors and `submitted` stay. `form.RESET` goes back to `initial`, or to the values it is given (`t.simulateAction('form.RESET', values)`, or an intent action `'form.RESET': DOM.click('.reset')`); it clears errors, touched fields and the submit count.
|
|
270
|
+
- **Starting over when shown** (`resetOnShow: true`): the form goes back to its start values, as a new form (`initial` too; touched fields, errors, server errors, checks and the submit state cleared), when its host starts (mounted again, a Switchable page made again for another `instance`) and each time its form element appears in the host's DOM after being absent (a Switchable page shown again: hidden pages stay alive and keep their state; a form rendered conditionally). Re-renders while the form is shown keep what the user typed. With the mock DOM (and in SSR) only the host's start counts. A `values` function gets the host's whole state and is called at each start over; without `resetOnShow`, once, when the host starts and the slice is still untouched. Before its first call (the first render of a page that reads its parent's state) the fields come from calling it with a blank state, every value read from it `''`.
|
|
271
|
+
|
|
272
|
+
## Without the behavior: the helpers
|
|
273
|
+
|
|
274
|
+
The behavior covers the common form. When a form needs its own state layout or flow, write the actions yourself and use the functions the behavior is built on; each is a pure function for a reducer:
|
|
275
|
+
|
|
276
|
+
| Helper | |
|
|
277
|
+
|---|---|
|
|
278
|
+
| `checkForm(schema, values)` | `{ errors, value }`: errors by field name and the schema's output; a Promise for an async schema |
|
|
279
|
+
| `formErrors(schema, values)` | Only the errors (`{}` when valid) |
|
|
280
|
+
| `setField(values, name, value)`, `getField(values, name)`, `hasField(values, name)` | Immutable set and get by field name (rows by id); whether the field exists (also with an `undefined` value) |
|
|
281
|
+
| `fieldName(values, path)`, `fieldNames(values)` | An issue path as a field name; every field name of `values` |
|
|
282
|
+
| `replyErrors(reply, values?)` | Server errors (a reply, a map or a list of issues) as field errors |
|
|
283
|
+
| `focusInvalid(errors, within?)` | An `ELEMENT` command that focuses the first field with an error, children included; with `within` (the form element's selector), only a field inside that element; `ABORT` when there is none |
|
|
284
|
+
|
|
285
|
+
```jsx
|
|
286
|
+
import { formErrors, setField, focusInvalid, ABORT } from 'sygnal'
|
|
287
|
+
import { profileSchema } from './schema.js'
|
|
288
|
+
|
|
289
|
+
function Profile({ state }) {
|
|
290
|
+
return (
|
|
291
|
+
<form className="profile" noValidate>
|
|
292
|
+
<label>Display name <input name="name" value={state.values.name} /></label>
|
|
293
|
+
<p>{state.submitted ? state.errors.name : ''}</p>
|
|
294
|
+
<button type="submit">Save</button>
|
|
295
|
+
</form>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Profile.initialState = { values: { name: '' }, errors: formErrors(profileSchema, { name: '' }), submitted: false }
|
|
300
|
+
|
|
301
|
+
Profile.intent = ({ DOM }) => ({
|
|
302
|
+
CHANGE: DOM.select('.profile').events('input').map((e) => ({ name: e.target.name, value: e.target.value })),
|
|
303
|
+
SUBMIT: DOM.select('.profile').events('submit', { preventDefault: true }),
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
Profile.model = {
|
|
307
|
+
CHANGE: (state, { name, value }) => {
|
|
308
|
+
const values = setField(state.values, name, value)
|
|
309
|
+
return { ...state, values, errors: formErrors(profileSchema, values) }
|
|
310
|
+
},
|
|
311
|
+
SUBMIT: {
|
|
312
|
+
STATE: (state) => ({ ...state, submitted: true }),
|
|
313
|
+
ELEMENT: (state) => focusInvalid(state.errors, '.profile'),
|
|
314
|
+
HTTP: (state) => (Object.keys(state.errors).length ? ABORT : { url: '/api/profile', method: 'PUT', json: state.values }),
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Diagnostics
|
|
320
|
+
|
|
321
|
+
With the dev checks on (the Vite plugin in dev, `renderComponent` in tests): [SYG230](https://sygnal.js.org/reference/errors/#syg230) a field inside the form whose name isn't in `values`, [SYG231](https://sygnal.js.org/reference/errors/#syg231) a schema that isn't a Standard Schema, [SYG232](https://sygnal.js.org/reference/errors/#syg232) a submit dropped while one is in progress (info), [SYG233](https://sygnal.js.org/reference/errors/#syg233) a value the schema strips from its output, [SYG234](https://sygnal.js.org/reference/errors/#syg234) a `submit` action the model doesn't have, [SYG235](https://sygnal.js.org/reference/errors/#syg235) a `check` for an unknown field, or a check request that sets `ok`/`error`/`latest`, [SYG236](https://sygnal.js.org/reference/errors/#syg236) array rows without an `id`, and [SYG237](https://sygnal.js.org/reference/errors/#syg237) two forms in one component on the same selector. `sygnal-check` knows `form`: an option typo is [SYG127](https://sygnal.js.org/reference/errors/#syg127), and fields inside the form element are not reported as uncontrolled ([SYG111](https://sygnal.js.org/reference/errors/#syg111)).
|
|
322
|
+
|
|
323
|
+
## processForm()
|
|
324
|
+
|
|
325
|
+
For a form without validation, `processForm()` reads every named field of a form element on its events:
|
|
326
|
+
|
|
327
|
+
```jsx
|
|
328
|
+
import { processForm } from 'sygnal'
|
|
329
|
+
|
|
330
|
+
function ContactForm({ state }) {
|
|
331
|
+
return (
|
|
332
|
+
<form className="contact-form">
|
|
333
|
+
<input name="name" aria-label="Name" value={state.name} />
|
|
334
|
+
<input name="email" aria-label="Email" value={state.email} />
|
|
335
|
+
<textarea name="message" aria-label="Message">{state.message}</textarea>
|
|
336
|
+
<button type="submit">Send</button>
|
|
337
|
+
</form>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
ContactForm.initialState = { name: '', email: '', message: '' }
|
|
342
|
+
|
|
343
|
+
ContactForm.intent = ({ DOM }) => ({
|
|
344
|
+
// Listen only to submit events
|
|
345
|
+
SUBMIT: processForm(DOM.select('.contact-form'), { events: 'submit' }),
|
|
346
|
+
|
|
347
|
+
// Listen to all input changes (default: both 'input' and 'submit')
|
|
348
|
+
FIELD_CHANGE: processForm(DOM.select('.contact-form'))
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
ContactForm.model = {
|
|
352
|
+
FIELD_CHANGE: (state, data) => ({
|
|
353
|
+
...state,
|
|
354
|
+
name: data.name,
|
|
355
|
+
email: data.email,
|
|
356
|
+
message: data.message
|
|
357
|
+
}),
|
|
358
|
+
SUBMIT: {
|
|
359
|
+
STATE: (state) => ({ ...state, submitted: true }),
|
|
360
|
+
LOG: (state, data) => data // { name: '...', email: '...', message: '...', eventType: 'submit', event }
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### processForm() Options
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
processForm(DOM.select('.my-form'), { events: 'submit', preventDefault: true })
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
| Option | Type | Default | Description |
|
|
372
|
+
|--------|------|---------|-------------|
|
|
373
|
+
| `events` | String or Array | `['input', 'submit']` | Which form events to listen for |
|
|
374
|
+
| `preventDefault` | Boolean | `true` | Whether to call `preventDefault()` on events |
|
|
375
|
+
|
|
376
|
+
### Return Value
|
|
377
|
+
|
|
378
|
+
The stream emits an object with:
|
|
379
|
+
- All form field values keyed by their `name` attribute
|
|
380
|
+
- `event` — The raw DOM event
|
|
381
|
+
- `eventType` — The event type string (e.g., `'submit'`, `'input'`)
|