@zeix/le-truc 0.15.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/.ai-context.md +234 -0
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/.editorconfig +12 -0
- package/.github/copilot-instructions.md +62 -0
- package/.github/workflows/codeql.yml +108 -0
- package/.github/workflows/static.yml +43 -0
- package/.prettierrc +17 -0
- package/CLAUDE.md +215 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +160 -0
- package/LICENSE +21 -0
- package/README.md +474 -0
- package/biome.json +295 -0
- package/bun.lock +239 -0
- package/docs/about.html +105 -0
- package/docs/assets/main.css +1 -0
- package/docs/assets/main.js +10 -0
- package/docs/assets/main.js.map +66 -0
- package/docs/components.html +293 -0
- package/docs/data-flow.html +308 -0
- package/docs/examples/basic-button.html +367 -0
- package/docs/examples/basic-counter.html +188 -0
- package/docs/examples/basic-hello.html +138 -0
- package/docs/examples/basic-number.html +271 -0
- package/docs/examples/basic-pluralize.html +214 -0
- package/docs/examples/card-callout.html +152 -0
- package/docs/examples/card-mediaqueries.html +138 -0
- package/docs/examples/context-media.html +198 -0
- package/docs/examples/empty.html +37 -0
- package/docs/examples/form-checkbox.html +233 -0
- package/docs/examples/form-combobox.html +420 -0
- package/docs/examples/form-listbox.html +434 -0
- package/docs/examples/form-radiogroup.html +296 -0
- package/docs/examples/form-spinbutton.html +402 -0
- package/docs/examples/form-textbox.html +361 -0
- package/docs/examples/layout.html +67 -0
- package/docs/examples/module-carousel.html +552 -0
- package/docs/examples/module-catalog.html +241 -0
- package/docs/examples/module-codeblock.html +270 -0
- package/docs/examples/module-dialog.html +343 -0
- package/docs/examples/module-lazyload.html +289 -0
- package/docs/examples/module-list.html +197 -0
- package/docs/examples/module-pagination.html +283 -0
- package/docs/examples/module-scrollarea.html +447 -0
- package/docs/examples/module-tabgroup.html +526 -0
- package/docs/examples/module-todo.html +367 -0
- package/docs/examples/module-with-type.html +63 -0
- package/docs/examples/nested-components.html +88 -0
- package/docs/examples/recursive.html +56 -0
- package/docs/examples/simple-text.html +39 -0
- package/docs/examples/snippet.html +93 -0
- package/docs/examples/with-styles.html +75 -0
- package/docs/getting-started.html +143 -0
- package/docs/index.html +112 -0
- package/docs/sitemap.xml +28 -0
- package/docs/styling.html +160 -0
- package/docs/sw.js +112 -0
- package/docs-src/api/README.md +478 -0
- package/docs-src/api/_media/LICENSE +21 -0
- package/docs-src/api/classes/CircularDependencyError.md +299 -0
- package/docs-src/api/classes/CircularMutationError.md +301 -0
- package/docs-src/api/classes/ContextRequestEvent.md +590 -0
- package/docs-src/api/classes/DependencyTimeoutError.md +301 -0
- package/docs-src/api/classes/InvalidCallbackError.md +303 -0
- package/docs-src/api/classes/InvalidComponentNameError.md +295 -0
- package/docs-src/api/classes/InvalidCustomElementError.md +301 -0
- package/docs-src/api/classes/InvalidEffectsError.md +301 -0
- package/docs-src/api/classes/InvalidPropertyNameError.md +307 -0
- package/docs-src/api/classes/InvalidReactivesError.md +307 -0
- package/docs-src/api/classes/InvalidSignalValueError.md +303 -0
- package/docs-src/api/classes/MissingElementError.md +307 -0
- package/docs-src/api/classes/NullishSignalValueError.md +299 -0
- package/docs-src/api/classes/StoreKeyExistsError.md +303 -0
- package/docs-src/api/classes/StoreKeyRangeError.md +299 -0
- package/docs-src/api/classes/StoreKeyReadonlyError.md +303 -0
- package/docs-src/api/functions/asBoolean.md +21 -0
- package/docs-src/api/functions/asEnum.md +31 -0
- package/docs-src/api/functions/asInteger.md +39 -0
- package/docs-src/api/functions/asJSON.md +49 -0
- package/docs-src/api/functions/asNumber.md +37 -0
- package/docs-src/api/functions/asString.md +37 -0
- package/docs-src/api/functions/createCollection.md +83 -0
- package/docs-src/api/functions/createSensor.md +71 -0
- package/docs-src/api/functions/dangerouslySetInnerHTML.md +48 -0
- package/docs-src/api/functions/defineComponent.md +65 -0
- package/docs-src/api/functions/isCollection.md +37 -0
- package/docs-src/api/functions/isParser.md +41 -0
- package/docs-src/api/functions/match.md +47 -0
- package/docs-src/api/functions/on.md +58 -0
- package/docs-src/api/functions/pass.md +53 -0
- package/docs-src/api/functions/provideContexts.md +47 -0
- package/docs-src/api/functions/read.md +47 -0
- package/docs-src/api/functions/requestContext.md +51 -0
- package/docs-src/api/functions/resolve.md +40 -0
- package/docs-src/api/functions/runEffects.md +51 -0
- package/docs-src/api/functions/runElementEffects.md +57 -0
- package/docs-src/api/functions/schedule.md +33 -0
- package/docs-src/api/functions/setAttribute.md +48 -0
- package/docs-src/api/functions/setProperty.md +52 -0
- package/docs-src/api/functions/setStyle.md +48 -0
- package/docs-src/api/functions/setText.md +42 -0
- package/docs-src/api/functions/show.md +42 -0
- package/docs-src/api/functions/toSignal.md +37 -0
- package/docs-src/api/functions/toggleAttribute.md +48 -0
- package/docs-src/api/functions/toggleClass.md +48 -0
- package/docs-src/api/functions/updateElement.md +53 -0
- package/docs-src/api/globals.md +131 -0
- package/docs-src/api/type-aliases/Cleanup.md +27 -0
- package/docs-src/api/type-aliases/Collection.md +91 -0
- package/docs-src/api/type-aliases/CollectionListener.md +27 -0
- package/docs-src/api/type-aliases/Component.md +17 -0
- package/docs-src/api/type-aliases/ComponentProp.md +11 -0
- package/docs-src/api/type-aliases/ComponentProps.md +11 -0
- package/docs-src/api/type-aliases/ComponentSetup.md +31 -0
- package/docs-src/api/type-aliases/ComponentUI.md +27 -0
- package/docs-src/api/type-aliases/Computed.md +49 -0
- package/docs-src/api/type-aliases/ComputedCallback.md +29 -0
- package/docs-src/api/type-aliases/Context.md +33 -0
- package/docs-src/api/type-aliases/ContextType.md +19 -0
- package/docs-src/api/type-aliases/DangerouslySetInnerHTMLOptions.md +27 -0
- package/docs-src/api/type-aliases/DiffResult.md +61 -0
- package/docs-src/api/type-aliases/Effect.md +35 -0
- package/docs-src/api/type-aliases/EffectCallback.md +23 -0
- package/docs-src/api/type-aliases/Effects.md +21 -0
- package/docs-src/api/type-aliases/ElementEffects.md +21 -0
- package/docs-src/api/type-aliases/ElementFromKey.md +21 -0
- package/docs-src/api/type-aliases/ElementQueries.md +27 -0
- package/docs-src/api/type-aliases/ElementUpdater.md +131 -0
- package/docs-src/api/type-aliases/EventHandler.md +31 -0
- package/docs-src/api/type-aliases/EventType.md +17 -0
- package/docs-src/api/type-aliases/Fallback.md +21 -0
- package/docs-src/api/type-aliases/Initializers.md +21 -0
- package/docs-src/api/type-aliases/LooseReader.md +31 -0
- package/docs-src/api/type-aliases/MatchHandlers.md +77 -0
- package/docs-src/api/type-aliases/MaybeCleanup.md +23 -0
- package/docs-src/api/type-aliases/MaybeSignal.md +17 -0
- package/docs-src/api/type-aliases/Parser.md +39 -0
- package/docs-src/api/type-aliases/ParserOrFallback.md +21 -0
- package/docs-src/api/type-aliases/PassedProp.md +25 -0
- package/docs-src/api/type-aliases/PassedProps.md +21 -0
- package/docs-src/api/type-aliases/Reactive.md +25 -0
- package/docs-src/api/type-aliases/Reader.md +31 -0
- package/docs-src/api/type-aliases/ReservedWords.md +11 -0
- package/docs-src/api/type-aliases/ResolveResult.md +29 -0
- package/docs-src/api/type-aliases/SensorEvents.md +25 -0
- package/docs-src/api/type-aliases/Signal.md +41 -0
- package/docs-src/api/type-aliases/State.md +85 -0
- package/docs-src/api/type-aliases/Store.md +29 -0
- package/docs-src/api/type-aliases/UI.md +11 -0
- package/docs-src/api/type-aliases/UnknownContext.md +13 -0
- package/docs-src/api/variables/CONTEXT_REQUEST.md +11 -0
- package/docs-src/api/variables/UNSET.md +23 -0
- package/docs-src/api/variables/batch.md +25 -0
- package/docs-src/api/variables/createComputed.md +41 -0
- package/docs-src/api/variables/createEffect.md +35 -0
- package/docs-src/api/variables/createState.md +37 -0
- package/docs-src/api/variables/createStore.md +42 -0
- package/docs-src/api/variables/diff.md +43 -0
- package/docs-src/api/variables/isAbortError.md +33 -0
- package/docs-src/api/variables/isAsyncFunction.md +39 -0
- package/docs-src/api/variables/isComputed.md +37 -0
- package/docs-src/api/variables/isEqual.md +49 -0
- package/docs-src/api/variables/isFunction.md +39 -0
- package/docs-src/api/variables/isMutableSignal.md +37 -0
- package/docs-src/api/variables/isNumber.md +33 -0
- package/docs-src/api/variables/isRecord.md +39 -0
- package/docs-src/api/variables/isRecordOrArray.md +39 -0
- package/docs-src/api/variables/isSignal.md +37 -0
- package/docs-src/api/variables/isState.md +37 -0
- package/docs-src/api/variables/isStore.md +37 -0
- package/docs-src/api/variables/isString.md +33 -0
- package/docs-src/api/variables/isSymbol.md +33 -0
- package/docs-src/api/variables/toError.md +33 -0
- package/docs-src/api/variables/valueString.md +33 -0
- package/docs-src/includes/menu.html +44 -0
- package/docs-src/pages/about.md +89 -0
- package/docs-src/pages/components.md +437 -0
- package/docs-src/pages/data-flow.md +449 -0
- package/docs-src/pages/getting-started.md +170 -0
- package/docs-src/pages/index.md +98 -0
- package/docs-src/pages/styling.md +165 -0
- package/eslint.config.js +64 -0
- package/examples/_common/clear.ts +49 -0
- package/examples/_common/fetch.ts +160 -0
- package/examples/_common/focus.ts +45 -0
- package/examples/_common/highlight.ts +5 -0
- package/examples/_global.css +463 -0
- package/examples/basic-button/basic-button.css +176 -0
- package/examples/basic-button/basic-button.html +46 -0
- package/examples/basic-button/basic-button.spec.ts +160 -0
- package/examples/basic-button/basic-button.ts +45 -0
- package/examples/basic-button/copyToClipboard.ts +37 -0
- package/examples/basic-counter/basic-counter.css +21 -0
- package/examples/basic-counter/basic-counter.html +24 -0
- package/examples/basic-counter/basic-counter.spec.ts +85 -0
- package/examples/basic-counter/basic-counter.ts +43 -0
- package/examples/basic-hello/basic-hello.html +34 -0
- package/examples/basic-hello/basic-hello.spec.ts +110 -0
- package/examples/basic-hello/basic-hello.ts +36 -0
- package/examples/basic-number/basic-number.html +79 -0
- package/examples/basic-number/basic-number.spec.ts +175 -0
- package/examples/basic-number/basic-number.ts +124 -0
- package/examples/basic-pluralize/basic-pluralize.html +64 -0
- package/examples/basic-pluralize/basic-pluralize.spec.ts +258 -0
- package/examples/basic-pluralize/basic-pluralize.ts +82 -0
- package/examples/card-callout/card-callout.css +79 -0
- package/examples/card-callout/card-callout.html +5 -0
- package/examples/card-mediaqueries/card-mediaqueries.html +29 -0
- package/examples/card-mediaqueries/card-mediaqueries.spec.ts +300 -0
- package/examples/card-mediaqueries/card-mediaqueries.ts +41 -0
- package/examples/context-media/context-media.html +3 -0
- package/examples/context-media/context-media.ts +127 -0
- package/examples/form-checkbox/form-checkbox.css +70 -0
- package/examples/form-checkbox/form-checkbox.html +13 -0
- package/examples/form-checkbox/form-checkbox.spec.ts +357 -0
- package/examples/form-checkbox/form-checkbox.ts +50 -0
- package/examples/form-checkbox/vanilla-checkbox.ts +101 -0
- package/examples/form-combobox/form-combobox.css +118 -0
- package/examples/form-combobox/form-combobox.html +74 -0
- package/examples/form-combobox/form-combobox.spec.ts +977 -0
- package/examples/form-combobox/form-combobox.ts +128 -0
- package/examples/form-listbox/form-listbox.css +71 -0
- package/examples/form-listbox/form-listbox.html +67 -0
- package/examples/form-listbox/form-listbox.spec.ts +1050 -0
- package/examples/form-listbox/form-listbox.ts +196 -0
- package/examples/form-listbox/mocks/timezones.json +495 -0
- package/examples/form-radiogroup/form-radiogroup.css +87 -0
- package/examples/form-radiogroup/form-radiogroup.html +51 -0
- package/examples/form-radiogroup/form-radiogroup.spec.ts +515 -0
- package/examples/form-radiogroup/form-radiogroup.ts +58 -0
- package/examples/form-spinbutton/form-spinbutton.css +95 -0
- package/examples/form-spinbutton/form-spinbutton.html +96 -0
- package/examples/form-spinbutton/form-spinbutton.spec.ts +688 -0
- package/examples/form-spinbutton/form-spinbutton.ts +111 -0
- package/examples/form-textbox/form-textbox.css +104 -0
- package/examples/form-textbox/form-textbox.html +53 -0
- package/examples/form-textbox/form-textbox.spec.ts +542 -0
- package/examples/form-textbox/form-textbox.ts +104 -0
- package/examples/main.css +22 -0
- package/examples/main.ts +23 -0
- package/examples/module-carousel/module-carousel.css +113 -0
- package/examples/module-carousel/module-carousel.html +208 -0
- package/examples/module-carousel/module-carousel.spec.ts +523 -0
- package/examples/module-carousel/module-carousel.ts +131 -0
- package/examples/module-catalog/module-catalog.css +22 -0
- package/examples/module-catalog/module-catalog.html +82 -0
- package/examples/module-catalog/module-catalog.spec.ts +396 -0
- package/examples/module-catalog/module-catalog.ts +37 -0
- package/examples/module-codeblock/module-codeblock.css +95 -0
- package/examples/module-codeblock/module-codeblock.html +28 -0
- package/examples/module-codeblock/module-codeblock.ts +47 -0
- package/examples/module-demo/module-demo.css +13 -0
- package/examples/module-dialog/module-dialog.css +96 -0
- package/examples/module-dialog/module-dialog.html +66 -0
- package/examples/module-dialog/module-dialog.spec.ts +557 -0
- package/examples/module-dialog/module-dialog.ts +81 -0
- package/examples/module-lazyload/mocks/empty.html +1 -0
- package/examples/module-lazyload/mocks/module-with-type.html +27 -0
- package/examples/module-lazyload/mocks/nested-components.html +52 -0
- package/examples/module-lazyload/mocks/recursive.html +20 -0
- package/examples/module-lazyload/mocks/simple-text.html +3 -0
- package/examples/module-lazyload/mocks/snippet.html +57 -0
- package/examples/module-lazyload/mocks/with-styles.html +39 -0
- package/examples/module-lazyload/module-lazyload.html +132 -0
- package/examples/module-lazyload/module-lazyload.spec.ts +734 -0
- package/examples/module-lazyload/module-lazyload.ts +89 -0
- package/examples/module-list/module-list.html +30 -0
- package/examples/module-list/module-list.spec.ts +592 -0
- package/examples/module-list/module-list.ts +99 -0
- package/examples/module-pagination/module-pagination.css +79 -0
- package/examples/module-pagination/module-pagination.html +16 -0
- package/examples/module-pagination/module-pagination.spec.ts +701 -0
- package/examples/module-pagination/module-pagination.ts +88 -0
- package/examples/module-scrollarea/module-scrollarea.css +77 -0
- package/examples/module-scrollarea/module-scrollarea.html +189 -0
- package/examples/module-scrollarea/module-scrollarea.spec.ts +445 -0
- package/examples/module-scrollarea/module-scrollarea.ts +81 -0
- package/examples/module-tabgroup/module-tabgroup.css +55 -0
- package/examples/module-tabgroup/module-tabgroup.html +269 -0
- package/examples/module-tabgroup/module-tabgroup.spec.ts +631 -0
- package/examples/module-tabgroup/module-tabgroup.ts +102 -0
- package/examples/module-toc/module-toc.css +34 -0
- package/examples/module-todo/module-todo.css +84 -0
- package/examples/module-todo/module-todo.html +92 -0
- package/examples/module-todo/module-todo.spec.ts +528 -0
- package/examples/module-todo/module-todo.ts +91 -0
- package/examples/section-hero/section-hero.css +37 -0
- package/examples/section-menu/section-menu.css +81 -0
- package/examples/server.ts +95 -0
- package/examples/test-setup.md +314 -0
- package/index.dev.js +1688 -0
- package/index.dev.ts +127 -0
- package/index.js +3 -0
- package/index.js.map +42 -0
- package/index.ts +127 -0
- package/package.json +64 -0
- package/playwright.config.ts +31 -0
- package/server/BUILD_SYSTEM.md +428 -0
- package/server/SERVER.md +286 -0
- package/server/build.ts +91 -0
- package/server/config.ts +130 -0
- package/server/effects/api.ts +28 -0
- package/server/effects/css.ts +31 -0
- package/server/effects/examples.ts +109 -0
- package/server/effects/js.ts +32 -0
- package/server/effects/menu.ts +34 -0
- package/server/effects/pages.ts +178 -0
- package/server/effects/service-worker.ts +57 -0
- package/server/effects/sitemap.ts +27 -0
- package/server/file-signals.ts +361 -0
- package/server/file-watcher.ts +77 -0
- package/server/io.ts +174 -0
- package/server/layout-engine.ts +470 -0
- package/server/layout-utils.ts +615 -0
- package/server/layouts/api.html +76 -0
- package/server/layouts/base.html +37 -0
- package/server/layouts/blog.html +115 -0
- package/server/layouts/example.html +104 -0
- package/server/layouts/overview.html +165 -0
- package/server/layouts/page.html +36 -0
- package/server/layouts/test.html +24 -0
- package/server/markdoc-helpers.ts +217 -0
- package/server/markdoc.config.ts +29 -0
- package/server/schema/callout.markdoc.ts +17 -0
- package/server/schema/carousel.markdoc.ts +118 -0
- package/server/schema/demo.markdoc.ts +74 -0
- package/server/schema/fence.markdoc.ts +84 -0
- package/server/schema/heading.markdoc.ts +23 -0
- package/server/schema/hero.markdoc.ts +59 -0
- package/server/schema/section.markdoc.ts +10 -0
- package/server/schema/slide.markdoc.ts +17 -0
- package/server/schema/source.markdoc.ts +53 -0
- package/server/schema/tabgroup.markdoc.ts +102 -0
- package/server/serve.ts +635 -0
- package/server/templates/README.md +352 -0
- package/server/templates/constants.ts +236 -0
- package/server/templates/fragments.ts +159 -0
- package/server/templates/hmr.ts +269 -0
- package/server/templates/menu.ts +33 -0
- package/server/templates/performance-hints.ts +94 -0
- package/server/templates/service-worker.ts +403 -0
- package/server/templates/sitemap.ts +57 -0
- package/server/templates/toc.ts +41 -0
- package/server/templates/utils.ts +378 -0
- package/src/component.ts +215 -0
- package/src/context.ts +156 -0
- package/src/effects/attribute.ts +82 -0
- package/src/effects/class.ts +28 -0
- package/src/effects/event.ts +67 -0
- package/src/effects/html.ts +60 -0
- package/src/effects/method.ts +57 -0
- package/src/effects/pass.ts +103 -0
- package/src/effects/property.ts +57 -0
- package/src/effects/style.ts +34 -0
- package/src/effects/text.ts +28 -0
- package/src/effects.ts +412 -0
- package/src/errors.ts +160 -0
- package/src/parsers/boolean.ts +14 -0
- package/src/parsers/json.ts +33 -0
- package/src/parsers/number.ts +55 -0
- package/src/parsers/string.ts +32 -0
- package/src/parsers.ts +90 -0
- package/src/scheduler.ts +47 -0
- package/src/signals/collection.ts +253 -0
- package/src/signals/sensor.ts +131 -0
- package/src/ui.ts +236 -0
- package/src/util.ts +187 -0
- package/tsconfig.json +34 -0
- package/types/examples/basic-button/basic-button.d.ts +16 -0
- package/types/examples/basic-hello/basic-hello.d.ts +18 -0
- package/types/index.d.ts +27 -0
- package/types/index.dev.d.ts +27 -0
- package/types/src/collection.d.ts +27 -0
- package/types/src/component.d.ts +32 -0
- package/types/src/context.d.ts +85 -0
- package/types/src/effects/attribute.d.ts +23 -0
- package/types/src/effects/callMethod.d.ts +23 -0
- package/types/src/effects/class.d.ts +13 -0
- package/types/src/effects/dangerouslySetInnerHTML.d.ts +18 -0
- package/types/src/effects/event.d.ts +18 -0
- package/types/src/effects/html.d.ts +17 -0
- package/types/src/effects/method.d.ts +22 -0
- package/types/src/effects/pass.d.ts +18 -0
- package/types/src/effects/property.d.ts +22 -0
- package/types/src/effects/setAttribute.d.ts +24 -0
- package/types/src/effects/setProperty.d.ts +23 -0
- package/types/src/effects/setStyle.d.ts +14 -0
- package/types/src/effects/setText.d.ts +13 -0
- package/types/src/effects/style.d.ts +13 -0
- package/types/src/effects/text.d.ts +12 -0
- package/types/src/effects/toggleClass.d.ts +14 -0
- package/types/src/effects.d.ts +153 -0
- package/types/src/errors.d.ts +99 -0
- package/types/src/events.d.ts +27 -0
- package/types/src/extractors.d.ts +23 -0
- package/types/src/parsers/boolean.d.ts +10 -0
- package/types/src/parsers/json.d.ts +13 -0
- package/types/src/parsers/number.d.ts +21 -0
- package/types/src/parsers/string.d.ts +19 -0
- package/types/src/parsers.d.ts +41 -0
- package/types/src/scheduler.d.ts +11 -0
- package/types/src/sensor.d.ts +27 -0
- package/types/src/signals/collection.d.ts +32 -0
- package/types/src/signals/sensor.d.ts +27 -0
- package/types/src/ui.d.ts +37 -0
- package/types/src/util.d.ts +65 -0
package/src/effects.ts
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Cleanup,
|
|
3
|
+
createEffect,
|
|
4
|
+
isFunction,
|
|
5
|
+
isRecord,
|
|
6
|
+
isSignal,
|
|
7
|
+
isString,
|
|
8
|
+
type MaybeCleanup,
|
|
9
|
+
type Signal,
|
|
10
|
+
toError,
|
|
11
|
+
UNSET,
|
|
12
|
+
valueString,
|
|
13
|
+
} from '@zeix/cause-effect'
|
|
14
|
+
import type { Component, ComponentProps } from './component'
|
|
15
|
+
import { InvalidEffectsError } from './errors'
|
|
16
|
+
import { type Collection, isCollection } from './signals/collection'
|
|
17
|
+
import type { ElementFromKey, UI } from './ui'
|
|
18
|
+
import { DEV_MODE, elementName, LOG_ERROR, log } from './util'
|
|
19
|
+
|
|
20
|
+
/* === Types === */
|
|
21
|
+
|
|
22
|
+
type Effect<P extends ComponentProps, E extends Element> = (
|
|
23
|
+
host: Component<P>,
|
|
24
|
+
target: E,
|
|
25
|
+
) => MaybeCleanup
|
|
26
|
+
|
|
27
|
+
type ElementEffects<P extends ComponentProps, E extends Element> =
|
|
28
|
+
| Effect<P, E>
|
|
29
|
+
| Effect<P, E>[]
|
|
30
|
+
|
|
31
|
+
type Effects<
|
|
32
|
+
P extends ComponentProps,
|
|
33
|
+
U extends UI & { host: Component<P> },
|
|
34
|
+
> = {
|
|
35
|
+
[K in keyof U]?: ElementEffects<P, ElementFromKey<U, K>>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Reactive<T, P extends ComponentProps, E extends Element> =
|
|
39
|
+
| keyof P
|
|
40
|
+
| Signal<T & {}>
|
|
41
|
+
| ((target: E) => T | null | undefined)
|
|
42
|
+
|
|
43
|
+
type UpdateOperation = 'a' | 'c' | 'd' | 'h' | 'm' | 'p' | 's' | 't'
|
|
44
|
+
|
|
45
|
+
type ElementUpdater<E extends Element, T> = {
|
|
46
|
+
op: UpdateOperation
|
|
47
|
+
name?: string
|
|
48
|
+
read: (element: E) => T | null
|
|
49
|
+
update: (element: E, value: T) => void
|
|
50
|
+
delete?: (element: E) => void
|
|
51
|
+
resolve?: (element: E) => void
|
|
52
|
+
reject?: (error: unknown) => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* type ElementInserter<E extends Element> = {
|
|
56
|
+
position?: InsertPosition
|
|
57
|
+
create: (parent: E) => Element | null
|
|
58
|
+
resolve?: (parent: E) => void
|
|
59
|
+
reject?: (error: unknown) => void
|
|
60
|
+
} */
|
|
61
|
+
|
|
62
|
+
/* === Constants === */
|
|
63
|
+
|
|
64
|
+
// Special value explicitly marked as any so it can be used as signal value of any type
|
|
65
|
+
const RESET: any = Symbol('RESET')
|
|
66
|
+
|
|
67
|
+
/* === Internal Functions === */
|
|
68
|
+
|
|
69
|
+
const getUpdateDescription = (
|
|
70
|
+
op: UpdateOperation,
|
|
71
|
+
name: string = '',
|
|
72
|
+
): string => {
|
|
73
|
+
const ops: Record<UpdateOperation, string> = {
|
|
74
|
+
a: 'attribute ',
|
|
75
|
+
c: 'class ',
|
|
76
|
+
d: 'dataset ',
|
|
77
|
+
h: 'inner HTML',
|
|
78
|
+
m: 'method call ',
|
|
79
|
+
p: 'property ',
|
|
80
|
+
s: 'style property ',
|
|
81
|
+
t: 'text content',
|
|
82
|
+
}
|
|
83
|
+
return ops[op] + name
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* === Exported Functions === */
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run element effects
|
|
90
|
+
*
|
|
91
|
+
* @since 0.15.0
|
|
92
|
+
* @param {U} host - Host component
|
|
93
|
+
* @param {E} target - Target element
|
|
94
|
+
* @param {ElementEffects<P, E>} effects - Effect functions to run
|
|
95
|
+
* @returns {MaybeCleanup} - Cleanup function that runs collected cleanup functions
|
|
96
|
+
* @throws {InvalidEffectsError} - If the effects are invalid
|
|
97
|
+
*/
|
|
98
|
+
const runElementEffects = <P extends ComponentProps, E extends Element>(
|
|
99
|
+
host: Component<P>,
|
|
100
|
+
target: E,
|
|
101
|
+
effects: ElementEffects<P, E>,
|
|
102
|
+
): MaybeCleanup => {
|
|
103
|
+
const cleanups: Cleanup[] = []
|
|
104
|
+
|
|
105
|
+
const run = (fn: Effect<P, E>) => {
|
|
106
|
+
const cleanup = fn(host, target)
|
|
107
|
+
if (cleanup) cleanups.push(cleanup)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(effects)) for (const fn of effects) run(fn)
|
|
111
|
+
else run(effects)
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
cleanups.forEach(cleanup => cleanup())
|
|
115
|
+
cleanups.length = 0
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Run collection effects
|
|
121
|
+
*
|
|
122
|
+
* @since 0.15.0
|
|
123
|
+
* @param {Component<P>} host - Host component
|
|
124
|
+
* @param {Collection<E>} collection - Collection of elements
|
|
125
|
+
* @param {ElementEffects<P, E>} effects - Element effects
|
|
126
|
+
* @returns {Cleanup} - Cleanup function that runs collected cleanup functions
|
|
127
|
+
* @throws {InvalidEffectsError} - If the effects are invalid
|
|
128
|
+
*/
|
|
129
|
+
const runCollectionEffects = <P extends ComponentProps, E extends Element>(
|
|
130
|
+
host: Component<P>,
|
|
131
|
+
collection: Collection<E>,
|
|
132
|
+
effects: ElementEffects<P, E>,
|
|
133
|
+
): Cleanup => {
|
|
134
|
+
const cleanups: Map<E, Cleanup> = new Map()
|
|
135
|
+
|
|
136
|
+
const attach = (targets: readonly E[]) => {
|
|
137
|
+
for (const target of targets) {
|
|
138
|
+
const cleanup = runElementEffects(host, target, effects)
|
|
139
|
+
if (cleanup) cleanups.set(target, cleanup)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const detach = (targets: readonly E[]) => {
|
|
143
|
+
for (const target of targets) {
|
|
144
|
+
cleanups.get(target)?.()
|
|
145
|
+
cleanups.delete(target)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
collection.on('add', attach)
|
|
150
|
+
collection.on('remove', detach)
|
|
151
|
+
attach(collection.get())
|
|
152
|
+
return () => {
|
|
153
|
+
for (const cleanup of cleanups.values()) cleanup()
|
|
154
|
+
cleanups.clear()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Run component effects
|
|
160
|
+
*
|
|
161
|
+
* @since 0.15.0
|
|
162
|
+
* @param {ComponentUI<P, U>} ui - Component UI
|
|
163
|
+
* @param {Effects<P, U>} effects - Effect functions to run
|
|
164
|
+
* @returns {Cleanup} - Cleanup function that runs collected cleanup functions
|
|
165
|
+
* @throws {InvalidEffectsError} - If the effects are invalid
|
|
166
|
+
*/
|
|
167
|
+
const runEffects = <
|
|
168
|
+
P extends ComponentProps,
|
|
169
|
+
U extends UI & { host: Component<P> },
|
|
170
|
+
>(
|
|
171
|
+
ui: U,
|
|
172
|
+
effects: Effects<P, U>,
|
|
173
|
+
): Cleanup => {
|
|
174
|
+
if (!isRecord(effects)) throw new InvalidEffectsError(ui.host)
|
|
175
|
+
|
|
176
|
+
const cleanups: Cleanup[] = []
|
|
177
|
+
const keys = Object.keys(effects)
|
|
178
|
+
for (const key of keys) {
|
|
179
|
+
const k = key as keyof U
|
|
180
|
+
if (!effects[k]) continue
|
|
181
|
+
|
|
182
|
+
const elementEffects = Array.isArray(effects[k]) ? effects[k] : [effects[k]]
|
|
183
|
+
if (isCollection<ElementFromKey<U, typeof k>>(ui[k])) {
|
|
184
|
+
cleanups.push(runCollectionEffects(ui.host, ui[k], elementEffects))
|
|
185
|
+
} else if (ui[k]) {
|
|
186
|
+
const cleanup = runElementEffects(
|
|
187
|
+
ui.host,
|
|
188
|
+
ui[k] as ElementFromKey<U, typeof k>,
|
|
189
|
+
elementEffects,
|
|
190
|
+
)
|
|
191
|
+
if (cleanup) cleanups.push(cleanup)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return () => {
|
|
195
|
+
for (const cleanup of cleanups) cleanup()
|
|
196
|
+
cleanups.length = 0
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Resolve reactive property name, reader function or signal
|
|
202
|
+
*
|
|
203
|
+
* @param {Reactive<T, P, E>} reactive - Reactive property name, reader function or signal
|
|
204
|
+
* @param {Component<P, U>} host - Component host element
|
|
205
|
+
* @param {E} target - Element to resolve reactive value for
|
|
206
|
+
* @param {string} [context] - Context for error logging
|
|
207
|
+
* @returns {T} - Resolved reactive value
|
|
208
|
+
*/
|
|
209
|
+
const resolveReactive = <
|
|
210
|
+
T extends {},
|
|
211
|
+
P extends ComponentProps,
|
|
212
|
+
E extends Element,
|
|
213
|
+
>(
|
|
214
|
+
reactive: Reactive<T, P, E>,
|
|
215
|
+
host: Component<P>,
|
|
216
|
+
target: E,
|
|
217
|
+
context?: string,
|
|
218
|
+
): T => {
|
|
219
|
+
try {
|
|
220
|
+
return isString(reactive)
|
|
221
|
+
? (host[reactive] as unknown as T)
|
|
222
|
+
: isSignal(reactive)
|
|
223
|
+
? reactive.get()
|
|
224
|
+
: isFunction(reactive)
|
|
225
|
+
? (reactive(target) as unknown as T)
|
|
226
|
+
: RESET
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (context) {
|
|
229
|
+
log(
|
|
230
|
+
error,
|
|
231
|
+
`Failed to resolve value of ${valueString(reactive)}${
|
|
232
|
+
context ? ` for ${context}` : ''
|
|
233
|
+
} in ${elementName(target)}${
|
|
234
|
+
(host as unknown as E) !== target ? ` in ${elementName(host)}` : ''
|
|
235
|
+
}`,
|
|
236
|
+
LOG_ERROR,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
return RESET
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Core effect function for updating element properties based on reactive values.
|
|
245
|
+
* This function handles the lifecycle of reading, updating, and deleting element properties
|
|
246
|
+
* while providing proper error handling and debugging support.
|
|
247
|
+
*
|
|
248
|
+
* @since 0.9.0
|
|
249
|
+
* @param {Reactive<T, P, E>} reactive - The reactive value that drives the element updates
|
|
250
|
+
* @param {ElementUpdater<E, T>} updater - Configuration object defining how to read, update, and delete the element property
|
|
251
|
+
* @returns {Effect<P, E>} Effect function that manages the element property updates
|
|
252
|
+
*/
|
|
253
|
+
const updateElement =
|
|
254
|
+
<T extends {}, P extends ComponentProps, E extends Element>(
|
|
255
|
+
reactive: Reactive<T, P, E>,
|
|
256
|
+
updater: ElementUpdater<E, T>,
|
|
257
|
+
): Effect<P, E> =>
|
|
258
|
+
(host, target): Cleanup => {
|
|
259
|
+
const { op, name = '', read, update } = updater
|
|
260
|
+
const operationDesc = getUpdateDescription(op, name)
|
|
261
|
+
|
|
262
|
+
const ok = (verb: string) => () => {
|
|
263
|
+
if (DEV_MODE && host.debug) {
|
|
264
|
+
log(
|
|
265
|
+
target,
|
|
266
|
+
`${verb} ${operationDesc} of ${elementName(target)} in ${elementName(host)}`,
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
updater.resolve?.(target)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const err = (verb: string) => (error: unknown) => {
|
|
273
|
+
log(
|
|
274
|
+
error,
|
|
275
|
+
`Failed to ${verb} ${operationDesc} of ${elementName(target)} in ${elementName(host)}`,
|
|
276
|
+
LOG_ERROR,
|
|
277
|
+
)
|
|
278
|
+
updater.reject?.(error)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const fallback = read(target)
|
|
282
|
+
|
|
283
|
+
return createEffect(() => {
|
|
284
|
+
const value = resolveReactive(reactive, host, target, operationDesc)
|
|
285
|
+
const resolvedValue =
|
|
286
|
+
value === RESET
|
|
287
|
+
? fallback
|
|
288
|
+
: value === UNSET
|
|
289
|
+
? updater.delete
|
|
290
|
+
? null
|
|
291
|
+
: fallback
|
|
292
|
+
: value
|
|
293
|
+
|
|
294
|
+
if (updater.delete && resolvedValue === null) {
|
|
295
|
+
try {
|
|
296
|
+
updater.delete!(target)
|
|
297
|
+
ok('delete')()
|
|
298
|
+
} catch (error) {
|
|
299
|
+
err('delete')(error)
|
|
300
|
+
}
|
|
301
|
+
} else if (resolvedValue != null) {
|
|
302
|
+
const current = read(target)
|
|
303
|
+
if (Object.is(resolvedValue, current)) return
|
|
304
|
+
try {
|
|
305
|
+
update(target, resolvedValue)
|
|
306
|
+
ok('update')()
|
|
307
|
+
} catch (error) {
|
|
308
|
+
err('update')(error)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Effect for dynamically inserting or removing elements based on a reactive numeric value.
|
|
316
|
+
* Positive values insert elements, negative values remove them.
|
|
317
|
+
*
|
|
318
|
+
* @since 0.12.1
|
|
319
|
+
* @param {Reactive<number, P, E>} reactive - Reactive value determining number of elements to insert (positive) or remove (negative)
|
|
320
|
+
* @param {ElementInserter<E>} inserter - Configuration object defining how to create and position elements
|
|
321
|
+
* @returns {Effect<P, E>} Effect function that manages element insertion and removal
|
|
322
|
+
* /
|
|
323
|
+
const insertOrRemoveElement =
|
|
324
|
+
<P extends ComponentProps, E extends Element = HTMLElement>(
|
|
325
|
+
reactive: Reactive<number, P, E>,
|
|
326
|
+
inserter?: ElementInserter<E>,
|
|
327
|
+
): Effect<P, E> =>
|
|
328
|
+
(host, target) => {
|
|
329
|
+
const ok = (verb: string) => () => {
|
|
330
|
+
if (DEV_MODE && host.debug) {
|
|
331
|
+
log(
|
|
332
|
+
target,
|
|
333
|
+
`${verb} element in ${elementName(target)} in ${elementName(host)}`,
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
if (isFunction(inserter?.resolve)) {
|
|
337
|
+
inserter.resolve(target)
|
|
338
|
+
} else {
|
|
339
|
+
const signal = isSignal<number>(reactive) ? reactive : undefined
|
|
340
|
+
if (isState(signal)) signal.set(0)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const err = (verb: string) => (error: unknown) => {
|
|
345
|
+
log(
|
|
346
|
+
error,
|
|
347
|
+
`Failed to ${verb} element in ${elementName(target)} in ${elementName(host)}`,
|
|
348
|
+
LOG_ERROR,
|
|
349
|
+
)
|
|
350
|
+
inserter?.reject?.(error)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return createEffect(() => {
|
|
354
|
+
const diff = resolveReactive(
|
|
355
|
+
reactive,
|
|
356
|
+
host,
|
|
357
|
+
target,
|
|
358
|
+
'insertion or deletion',
|
|
359
|
+
)
|
|
360
|
+
const resolvedDiff = diff === RESET ? 0 : diff
|
|
361
|
+
|
|
362
|
+
if (resolvedDiff > 0) {
|
|
363
|
+
// Positive diff => insert element
|
|
364
|
+
if (!inserter) throw new TypeError(`No inserter provided`)
|
|
365
|
+
try {
|
|
366
|
+
for (let i = 0; i < resolvedDiff; i++) {
|
|
367
|
+
const element = inserter.create(target)
|
|
368
|
+
if (!element) continue
|
|
369
|
+
target.insertAdjacentElement(
|
|
370
|
+
inserter.position ?? 'beforeend',
|
|
371
|
+
element,
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
ok('insert')()
|
|
375
|
+
} catch (error) {
|
|
376
|
+
err('insert')(error)
|
|
377
|
+
}
|
|
378
|
+
} else if (resolvedDiff < 0) {
|
|
379
|
+
try {
|
|
380
|
+
if (
|
|
381
|
+
inserter &&
|
|
382
|
+
(inserter.position === 'afterbegin' ||
|
|
383
|
+
inserter.position === 'beforeend')
|
|
384
|
+
) {
|
|
385
|
+
for (let i = 0; i > resolvedDiff; i--) {
|
|
386
|
+
if (inserter.position === 'afterbegin')
|
|
387
|
+
target.firstElementChild?.remove()
|
|
388
|
+
else target.lastElementChild?.remove()
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
target.remove()
|
|
392
|
+
}
|
|
393
|
+
ok('remove')()
|
|
394
|
+
} catch (error) {
|
|
395
|
+
err('remove')(error)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
} */
|
|
400
|
+
|
|
401
|
+
export {
|
|
402
|
+
type Effect,
|
|
403
|
+
type Effects,
|
|
404
|
+
type ElementEffects,
|
|
405
|
+
type ElementUpdater,
|
|
406
|
+
type Reactive,
|
|
407
|
+
runEffects,
|
|
408
|
+
runElementEffects,
|
|
409
|
+
resolveReactive,
|
|
410
|
+
updateElement,
|
|
411
|
+
RESET,
|
|
412
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { valueString } from '@zeix/cause-effect'
|
|
2
|
+
import { elementName } from './util'
|
|
3
|
+
|
|
4
|
+
/* === Error Classes === */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown when a circular dependency is detected in a selection signal
|
|
8
|
+
*
|
|
9
|
+
* @since 0.14.0
|
|
10
|
+
*/
|
|
11
|
+
class CircularMutationError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* @param {HTMLElement} host - Host component
|
|
14
|
+
* @param {string} selector - Selector used to find the elements
|
|
15
|
+
*/
|
|
16
|
+
constructor(host: HTMLElement, selector: string) {
|
|
17
|
+
super(
|
|
18
|
+
`Circular dependency detected in selection signal for component ${elementName(host)} with selector "${selector}"`,
|
|
19
|
+
)
|
|
20
|
+
this.name = 'CircularMutationError'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Error thrown when component name violates rules for custom element names
|
|
26
|
+
*
|
|
27
|
+
* @since 0.14.0
|
|
28
|
+
*/
|
|
29
|
+
class InvalidComponentNameError extends TypeError {
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} component - Component name
|
|
32
|
+
*/
|
|
33
|
+
constructor(component: string) {
|
|
34
|
+
super(
|
|
35
|
+
`Invalid component name "${component}". Custom element names must contain a hyphen, start with a lowercase letter, and contain only lowercase letters, numbers, and hyphens.`,
|
|
36
|
+
)
|
|
37
|
+
this.name = 'InvalidComponentNameError'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Error thrown when trying to assign a property name that conflicts with reserved words or inherited HTMLElement properties
|
|
43
|
+
*
|
|
44
|
+
* @since 0.14.0
|
|
45
|
+
*/
|
|
46
|
+
class InvalidPropertyNameError extends TypeError {
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} component - Component name
|
|
49
|
+
* @param {string} prop - Property name
|
|
50
|
+
* @param {string} reason - Explanation why the property is invalid
|
|
51
|
+
*/
|
|
52
|
+
constructor(component: string, prop: string, reason: string) {
|
|
53
|
+
super(
|
|
54
|
+
`Invalid property name "${prop}" for component <${component}>. ${reason}`,
|
|
55
|
+
)
|
|
56
|
+
this.name = 'InvalidPropertyNameError'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Error thrown when setup function does not return effects
|
|
62
|
+
*
|
|
63
|
+
* @since 0.14.0
|
|
64
|
+
*/
|
|
65
|
+
class InvalidEffectsError extends TypeError {
|
|
66
|
+
/**
|
|
67
|
+
* @param {HTMLElement} host - Host component
|
|
68
|
+
* @param {Error} cause - Error that caused the invalid effects
|
|
69
|
+
*/
|
|
70
|
+
constructor(host: HTMLElement, cause?: Error) {
|
|
71
|
+
super(
|
|
72
|
+
`Invalid effects in component ${elementName(host)}. Effects must be a record of effects for UI elements or the component, or a Promise that resolves to effects.`,
|
|
73
|
+
)
|
|
74
|
+
this.name = 'InvalidEffectsError'
|
|
75
|
+
if (cause) this.cause = cause
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Error thrown when a required desacendent element does not exist in a component's DOM subtree
|
|
81
|
+
*
|
|
82
|
+
* @since 0.14.0
|
|
83
|
+
*/
|
|
84
|
+
class MissingElementError extends Error {
|
|
85
|
+
/**
|
|
86
|
+
* @param {HTMLElement} host - Host component
|
|
87
|
+
* @param {string} selector - Selector used to find the elements
|
|
88
|
+
* @param {string} required - Explanation why the element is required
|
|
89
|
+
*/
|
|
90
|
+
constructor(host: HTMLElement, selector: string, required: string) {
|
|
91
|
+
super(
|
|
92
|
+
`Missing required element <${selector}> in component ${elementName(host)}. ${required}`,
|
|
93
|
+
)
|
|
94
|
+
this.name = 'MissingElementError'
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Error when a component's dependencies are not met within a specified timeout
|
|
100
|
+
*
|
|
101
|
+
* @since 0.14.0
|
|
102
|
+
*/
|
|
103
|
+
class DependencyTimeoutError extends Error {
|
|
104
|
+
/**
|
|
105
|
+
* @param {HTMLElement} host - Host component
|
|
106
|
+
* @param {string[]} missing - List of missing dependencies
|
|
107
|
+
*/
|
|
108
|
+
constructor(host: HTMLElement, missing: string[]) {
|
|
109
|
+
super(
|
|
110
|
+
`Timeout waiting for: [${missing.join(', ')}] in component ${elementName(host)}.`,
|
|
111
|
+
)
|
|
112
|
+
this.name = 'DependencyTimeoutError'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Error thrown when reactives passed to a component are invalid
|
|
118
|
+
*
|
|
119
|
+
* @since 0.15.0
|
|
120
|
+
*/
|
|
121
|
+
class InvalidReactivesError extends TypeError {
|
|
122
|
+
/**
|
|
123
|
+
* @param {HTMLElement} host - Host component
|
|
124
|
+
* @param {HTMLElement} target - Target component
|
|
125
|
+
* @param {unknown} reactives - Reactives passed to the component
|
|
126
|
+
*/
|
|
127
|
+
constructor(host: HTMLElement, target: HTMLElement, reactives: unknown) {
|
|
128
|
+
super(
|
|
129
|
+
`Expected reactives passed from ${elementName(host)} to ${elementName(target)} to be a record of signals, reactive property names or functions. Got ${valueString(reactives)}.`,
|
|
130
|
+
)
|
|
131
|
+
this.name = 'InvalidReactivesError'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Error thrown when target element is not a custom element as expected
|
|
137
|
+
*
|
|
138
|
+
* @since 0.15.0
|
|
139
|
+
*/
|
|
140
|
+
class InvalidCustomElementError extends TypeError {
|
|
141
|
+
/**
|
|
142
|
+
* @param {HTMLElement} target - Target component
|
|
143
|
+
* @param {string} where - Location where the error occurred
|
|
144
|
+
*/
|
|
145
|
+
constructor(target: HTMLElement, where: string) {
|
|
146
|
+
super(`Target ${elementName(target)} is not a custom element in ${where}.`)
|
|
147
|
+
this.name = 'InvalidCustomElementError'
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export {
|
|
152
|
+
CircularMutationError,
|
|
153
|
+
DependencyTimeoutError,
|
|
154
|
+
InvalidComponentNameError,
|
|
155
|
+
InvalidCustomElementError,
|
|
156
|
+
InvalidPropertyNameError,
|
|
157
|
+
InvalidEffectsError,
|
|
158
|
+
InvalidReactivesError,
|
|
159
|
+
MissingElementError,
|
|
160
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Parser } from '../parsers'
|
|
2
|
+
import type { UI } from '../ui'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse a boolean attribute as an actual boolean value
|
|
6
|
+
*
|
|
7
|
+
* @since 0.13.1
|
|
8
|
+
* @returns {Parser<boolean, UI>}
|
|
9
|
+
*/
|
|
10
|
+
const asBoolean =
|
|
11
|
+
(): Parser<boolean, UI> => (_: UI, value: string | null | undefined) =>
|
|
12
|
+
value != null && value !== 'false'
|
|
13
|
+
|
|
14
|
+
export { asBoolean }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Fallback, getFallback, type Parser } from '../parsers'
|
|
2
|
+
import type { UI } from '../ui'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse a string as a JSON serialized object with a fallback
|
|
6
|
+
*
|
|
7
|
+
* @since 0.11.0
|
|
8
|
+
* @param {Fallback<T, U>} fallback - Fallback value or reader function
|
|
9
|
+
* @returns {Parser<T, U>} Parser function
|
|
10
|
+
* @throws {TypeError} If the value and fallback are both null or undefined
|
|
11
|
+
* @throws {SyntaxError} If value is not a valid JSON string
|
|
12
|
+
*/
|
|
13
|
+
const asJSON =
|
|
14
|
+
<T extends {}, U extends UI>(fallback: Fallback<T, U>): Parser<T, U> =>
|
|
15
|
+
(ui: U, value: string | null | undefined) => {
|
|
16
|
+
if ((value ?? fallback) == null)
|
|
17
|
+
throw new TypeError(
|
|
18
|
+
'asJSON: Value and fallback are both null or undefined',
|
|
19
|
+
)
|
|
20
|
+
if (value == null) return getFallback(ui, fallback)
|
|
21
|
+
if (value === '') throw new TypeError('Empty string is not valid JSON')
|
|
22
|
+
let result: T | undefined
|
|
23
|
+
try {
|
|
24
|
+
result = JSON.parse(value)
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new SyntaxError(`Failed to parse JSON: ${String(error)}`, {
|
|
27
|
+
cause: error,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
return result ?? getFallback(ui, fallback)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { asJSON }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type Fallback, getFallback, type Parser } from '../parsers'
|
|
2
|
+
import type { UI } from '../ui'
|
|
3
|
+
|
|
4
|
+
/* === Internal Functions === */
|
|
5
|
+
|
|
6
|
+
const parseNumber = (
|
|
7
|
+
parseFn: (v: string) => number,
|
|
8
|
+
value: string | null | undefined,
|
|
9
|
+
) => {
|
|
10
|
+
if (value == null) return
|
|
11
|
+
const parsed = parseFn(value)
|
|
12
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* === Exported Functions === */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a string as a number forced to integer with a fallback
|
|
19
|
+
*
|
|
20
|
+
* Supports hexadecimal and scientific notation
|
|
21
|
+
*
|
|
22
|
+
* @since 0.11.0
|
|
23
|
+
* @param {Fallback<number, U>} [fallback=0] - Fallback value or reader function
|
|
24
|
+
* @returns {Parser<number, U>} Parser function
|
|
25
|
+
*/
|
|
26
|
+
const asInteger =
|
|
27
|
+
<U extends UI>(fallback: Fallback<number, U> = 0): Parser<number, U> =>
|
|
28
|
+
(ui: U, value: string | null | undefined) => {
|
|
29
|
+
if (value == null) return getFallback(ui, fallback)
|
|
30
|
+
|
|
31
|
+
// Handle hexadecimal notation
|
|
32
|
+
const trimmed = value.trim()
|
|
33
|
+
if (trimmed.toLowerCase().startsWith('0x'))
|
|
34
|
+
return (
|
|
35
|
+
parseNumber(v => parseInt(v, 16), trimmed) ?? getFallback(ui, fallback)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// Handle other formats (including scientific notation)
|
|
39
|
+
const parsed = parseNumber(parseFloat, value)
|
|
40
|
+
return parsed != null ? Math.trunc(parsed) : getFallback(ui, fallback)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse a string as a number with a fallback
|
|
45
|
+
*
|
|
46
|
+
* @since 0.11.0
|
|
47
|
+
* @param {Fallback<number, U>} [fallback=0] - Fallback value or reader function
|
|
48
|
+
* @returns {Parser<number, U>} Parser function
|
|
49
|
+
*/
|
|
50
|
+
const asNumber =
|
|
51
|
+
<U extends UI>(fallback: Fallback<number, U> = 0): Parser<number, U> =>
|
|
52
|
+
(ui: U, value: string | null | undefined) =>
|
|
53
|
+
parseNumber(parseFloat, value) ?? getFallback(ui, fallback)
|
|
54
|
+
|
|
55
|
+
export { asInteger, asNumber }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Fallback, getFallback, type Parser } from '../parsers'
|
|
2
|
+
import type { UI } from '../ui'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pass through string with a fallback
|
|
6
|
+
*
|
|
7
|
+
* @since 0.11.0
|
|
8
|
+
* @param {Fallback<string, U>} [fallback=''] - Fallback value or reader function
|
|
9
|
+
* @returns {Parser<string, U>} Parser function
|
|
10
|
+
*/
|
|
11
|
+
const asString =
|
|
12
|
+
<U extends UI>(fallback: Fallback<string, U> = ''): Parser<string, U> =>
|
|
13
|
+
(ui: U, value: string | null | undefined) =>
|
|
14
|
+
value ?? getFallback(ui, fallback)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a string as a multi-state value (for example: ['true', 'false', 'mixed'], defaulting to the first valid option
|
|
18
|
+
*
|
|
19
|
+
* @since 0.9.0
|
|
20
|
+
* @param {[string, ...string[]]} valid - Array of valid values
|
|
21
|
+
* @returns {Parser<string, UI>} Parser function
|
|
22
|
+
*/
|
|
23
|
+
const asEnum =
|
|
24
|
+
(valid: [string, ...string[]]): Parser<string, UI> =>
|
|
25
|
+
(_: UI, value: string | null | undefined) => {
|
|
26
|
+
if (value == null) return valid[0]
|
|
27
|
+
const lowerValue = value.toLowerCase()
|
|
28
|
+
const matchingValid = valid.find(v => v.toLowerCase() === lowerValue)
|
|
29
|
+
return matchingValid ? value : valid[0]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { asString, asEnum }
|