diff --git a/.gitignore b/.gitignore index 17443a191..a7f6926a2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ datastar_site data .task .idea +.DS_Store node_modules datastar-website *_bin +.DS_Store \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index f3d836887..d28343d28 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,7 +24,7 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/code/go/cmd/tsbuild/main.go", + "program": "${workspaceFolder}/code/go/cmd/build/main.go", "cwd": "${workspaceFolder}" } ] diff --git a/.vscode/settings.json b/.vscode/settings.json index f987b5cab..b14a7c6da 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "go.coverOnSingleTestFile": true, "go.coverOnSingleTest": true, "editor.foldingStrategy": "indentation", - "makefile.configureOnOpen": false + "makefile.configureOnOpen": false, + "editor.formatOnSave": true } diff --git a/CHANGELOG.md b/CHANGELOG.md index fd94ba70c..731e49d3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,30 @@ -# Release Notes for Datastar +# WIP Release Notes for Datastar -## 0.20.1 - 2024-11-25 +## 0.21.0 - Unreleased -VersionClientByteSize = 35970->35789 -VersionClientByteSizeGzip = 12647->12568 +We’ve overhauled Datastar in v0.21.0, doubling down on making nestable signals declarative. To that end, we’ve removed special characters, made the API more explicit and consistent, and fixed some restrictions to nested signals that we discovered. Signal values are now accessed in expressions using the syntax `signalName.value`, actions no longer have a prefix, and attribute keys support nested signals using dot-delimited paths. ### Added -- SDKs - - .NET - - Initial SDK release! #231 - - PHP - - Allow KV pairs - - Author -- DevOps - - added `make test` & `make clean` to development Dockerfile + +- Added the ability to merge one-off signals using the syntax `data-signals-foo="value"`. +- Added the ability to use dot-delimited paths to denote nested signals in applicable attribute keys (`data-signals-foo.bar="value"`). +- Added the ability to use multiple attributes using the syntax `data-attributes="{attrName1: value1, attrName2: value2}"`. +- Added the ability to use a single classes using the syntax `data-class-hidden="foo.value"`. +- Added the ability to use a key instead of a value to denote a signal name in the `data-bind`, `data-indicator` and `data-ref` attributes (`data-bind-foo`, `data-indicator-foo`, `data-ref-foo`). +- Added error codes and links to descriptions in the console for every error thrown. ### Changed -- Client - - Function expression optimizations #234 - - Truthy Attributes were not getting set correctly #234 - - Fix invalid headers sent via SSE #241 - - Added hooks so NPM will package the correct files -- SDKs - - updated README for clarity around contributing - - Go - - Fix inverted logic for ViewTransitions #238 - - PHP - - tagged SDK 1.0.0-alpha.1 - - fixed retry duration - - general cleanup -- Website - - Bundler getting create valid zip for Windows #228 - - General site improvements - - Actions section in getting started - - Fixed broken links for SDKs and CDN #225 - - Try to fix Safari bug around caching SSE connections #239 -- Devops - - fix `make dev` to work cross-platform - - moved development Dockerfile from Alpine to Ubuntu - - -### Removed -- DevOps - - Removed broken Github Actions + +- Signals no longer have the `$` prefix and must be acessed using a `.value` suffix (`signalName.value`). +- Action plugins no longer have the `$` prefix. +- Renamed the `data-store` attribute to `data-signals`. +- Renamed the `data-bind` attribute to `data-attributes`. +- Renamed the `data-model` attribute to `data-bind`. +- Changed the `data-*` attribute modifier delimiter from `.` to `:` (`data-on-keydown:debounce_100ms:throttle_lead="value"`). +- The the `get()`, `post()`, `put()`, and `delete()` plugins have been replaced by a single `sse()` plugin that accepts the method as an option (`sse(url, {method="get"})`). +- The `setAll()` and `toggleAll` plugins now accept a dot-delimited path format, instead of a regular expression. + +### Fixed + +- Fixed headers not merging correctly. +- Fixed new lines in the SDK protocol for paths. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f199f009..774cc2155 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,9 +12,9 @@ Anything outside of this scope may not be accepted. If you have a need for a fea Before submitting bug reports and feature requests, please search the [open issues](https://github.com/starfederation/datastar/issues) and the _#help_ channel in the [Discord server](https://discord.gg/bnRNgZjgPh) to see if it has already been addressed. When submitting a [new issue](https://github.com/starfederation/datastar/issues/new), please use a descriptive title and include a clear description and as much relevant information as possible. -## Documentation +## Documentation -Datastar’s documentation is under active development. All the markdown files live in [this folder](https://github.com/starfederation/datastar/tree/develop/code/go/site/static/md). Improvements to them can be submitted via pull requests. +Datastar’s documentation is under active development. All the markdown files live in [this folder](https://github.com/starfederation/datastar/tree/develop/site/static/md). Improvements to them can be submitted via pull requests. ## Pull Requests diff --git a/README.md b/README.md index 2b42a3b8d..2ca5d2027 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,29 @@ ![Discord](https://img.shields.io/discord/1296224603642925098) ![GitHub Repo stars](https://img.shields.io/github/stars/starfederation/datastar?style=flat) -

+

# Datastar -### A real-time hypermedia framework. +### The hypermedia framework. -Datastar helps you build real-time web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. +Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. + +Getting started is as easy as adding a single script tag to your HTML. + +```html + +``` + +Then start adding frontend reactivity using declarative `data-*` attributes. +Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. Here’s what frontend reactivity looks like using Datastar: ```html - -
- + +
+ ``` Visit the [Datastar Website »](https://data-star.dev/) diff --git a/Taskfile.yml b/Taskfile.yml index 92e5e0e8e..a216d58e4 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -14,17 +14,17 @@ tasks: cmds: - go install github.com/go-task/task/v3/cmd/task@latest - go install github.com/a-h/templ/cmd/templ@latest - - cmd: test -f code/go/site/tailwindcli || wget -O code/go/site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-x64 + - cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-x64 platforms: [linux/amd64] - - cmd: test -f code/go/site/tailwindcli || wget -O code/go/site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-arm64 + - cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-arm64 platforms: [linux/arm64] - - cmd: test -f code/go/site/tailwindcli || wget -O code/go/site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-arm64 + - cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-arm64 platforms: [darwin/arm64] - - cmd: test -f code/go/site/tailwindcli || wget -O code/go/site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-x64 + - cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-x64 platforms: [darwin/amd64] - - cmd: test -f code/go/site/tailwindcli || wget -O code/go/site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-windows-x64.exe + - cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-windows-x64.exe platforms: [windows] - - chmod +x code/go/site/tailwindcli + - chmod +x site/tailwindcli - go install github.com/valyala/quicktemplate/qtc@latest version: @@ -39,7 +39,7 @@ tasks: cmds: - qtc - tsbuild: + build: deps: - qtc sources: @@ -50,20 +50,20 @@ tasks: generates: - "bundles/**/*" cmds: - - go run code/go/cmd/tsbuild/main.go - - cp -r bundles/* code/go/site/static/js/ - - mkdir -p code/ts/library/dist/ - - cp -r bundles/* code/ts/library/dist/ - - mkdir -p code/go/site/static/librarySource - - rm -rf code/go/site/static/librarySource/* - - cp -r code/ts/library/src/* code/go/site/static/librarySource/ + - go run build/cmd/build/main.go + - cp -r bundles/* site/static/js/ + - mkdir -p library/dist/ + - cp -r bundles/* library/dist/ + - mkdir -p site/static/librarySource + - rm -rf site/static/librarySource/* + - cp -r library/src/* site/static/librarySource/ libpub: dir: code/ts/library requires: vars: [VERSION] deps: - - tsbuild + - build cmds: - git push origin - git tag v{{.VERSION}} @@ -77,7 +77,7 @@ tasks: - task: deploy css: - dir: code/go/site + dir: site sources: - "**/*.templ" - "**/*.md" @@ -105,15 +105,15 @@ tasks: support: sources: - - code/go/**/*.templ - - code/go/**/*.go - - code/go/**/*.md - - code/go/site/static/**/* + - "**/*.templ" + - "**/*.go" + - "**/*.md" + - site/static/**/* generates: - ./datastar-website deps: - kill - - tsbuild + - build - templ - css @@ -124,7 +124,7 @@ tasks: - support cmds: - go mod tidy - - go build -o ./datastar-website code/go/cmd/site/main.go + - go build -o ./datastar-website "site/cmd/site/main.go" deploy: method: none @@ -134,7 +134,7 @@ tasks: - fly deploy --local-only test: - dir: code/go/site/smoketests + dir: site/smoketests deps: - support cmds: diff --git a/VERSION b/VERSION index 9d2632160..2c835f025 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.20.1 \ No newline at end of file +0.21.0-beta1 \ No newline at end of file diff --git a/code/go/.gitignore b/build/.gitignore similarity index 100% rename from code/go/.gitignore rename to build/.gitignore diff --git a/code/go/cmd/tsbuild/main.go b/build/cmd/build/main.go similarity index 70% rename from code/go/cmd/tsbuild/main.go rename to build/cmd/build/main.go index 994e2d42d..ed79cdec3 100644 --- a/code/go/cmd/tsbuild/main.go +++ b/build/cmd/build/main.go @@ -4,7 +4,7 @@ import ( "log" "time" - "github.com/starfederation/datastar/code/go/tsbuild" + build "github.com/starfederation/datastar/build" ) func main() { @@ -14,7 +14,7 @@ func main() { log.Printf("Datastar built in %s", time.Since(start)) }() - if err := tsbuild.Build(); err != nil { + if err := build.Build(); err != nil { log.Fatal(err) } diff --git a/code/go/tsbuild/consts.go b/build/consts.go similarity index 97% rename from code/go/tsbuild/consts.go rename to build/consts.go index b3477a7d9..c9d7b00ac 100644 --- a/code/go/tsbuild/consts.go +++ b/build/consts.go @@ -1,4 +1,4 @@ -package tsbuild +package build import ( "time" @@ -84,7 +84,7 @@ var Consts = &ConstTemplateData{ }, { Name: toolbelt.ToCasedString("mergeSignalsOnlyIfMissing"), - Description: "Should a given set of signals merge if they are missing from the store?", + Description: "Should a given set of signals merge if they are missing?", Value: false, }, { @@ -187,7 +187,7 @@ var Consts = &ConstTemplateData{ }, { Name: toolbelt.ToCasedString("MergeSignals"), - Description: "An event for merging signals into the store.", + Description: "An event for merging signals.", Value: "datastar-merge-signals", }, { @@ -197,7 +197,7 @@ var Consts = &ConstTemplateData{ }, { Name: toolbelt.ToCasedString("RemoveSignals"), - Description: "An event for removing signals from the store.", + Description: "An event for removing signals.", Value: "datastar-remove-signals", }, { diff --git a/code/go/tsbuild/consts_datastar_client.qtpl b/build/consts_datastar_client.qtpl similarity index 100% rename from code/go/tsbuild/consts_datastar_client.qtpl rename to build/consts_datastar_client.qtpl diff --git a/code/go/tsbuild/consts_datastar_readme.qtpl b/build/consts_datastar_readme.qtpl similarity index 60% rename from code/go/tsbuild/consts_datastar_readme.qtpl rename to build/consts_datastar_readme.qtpl index 58b02e5fe..90ca21d4d 100644 --- a/code/go/tsbuild/consts_datastar_readme.qtpl +++ b/build/consts_datastar_readme.qtpl @@ -6,20 +6,29 @@ ![Discord](https://img.shields.io/discord/1296224603642925098) ![GitHub Repo stars](https://img.shields.io/github/stars/starfederation/datastar?style=flat) -

+

# Datastar -### A real-time hypermedia framework. +### The hypermedia framework. -Datastar helps you build real-time web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. +Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. + +Getting started is as easy as adding a single script tag to your HTML. + +```html + +``` + +Then start adding frontend reactivity using declarative `data-*` attributes. +Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. Here’s what frontend reactivity looks like using Datastar: ```html - -
- + +
+ ``` Visit the [Datastar Website »](https://data-star.dev/) diff --git a/code/go/tsbuild/consts_dotnet.qtpl b/build/consts_dotnet.qtpl similarity index 100% rename from code/go/tsbuild/consts_dotnet.qtpl rename to build/consts_dotnet.qtpl diff --git a/code/go/tsbuild/consts_go.qtpl b/build/consts_go.qtpl similarity index 100% rename from code/go/tsbuild/consts_go.qtpl rename to build/consts_go.qtpl diff --git a/code/go/tsbuild/consts_php.qtpl b/build/consts_php.qtpl similarity index 100% rename from code/go/tsbuild/consts_php.qtpl rename to build/consts_php.qtpl diff --git a/code/go/tsbuild/run.go b/build/run.go similarity index 80% rename from code/go/tsbuild/run.go rename to build/run.go index a8e1c4b79..79839b363 100644 --- a/code/go/tsbuild/run.go +++ b/build/run.go @@ -1,4 +1,4 @@ -package tsbuild +package build import ( "compress/gzip" @@ -42,7 +42,7 @@ func extractVersion() (string, error) { version := strings.TrimSpace(string(versionBytes)) // Write out the version to the version file. - versionPath := "code/ts/library/src/engine/version.ts" + versionPath := "library/src/engine/version.ts" versionContents := fmt.Sprintf("export const VERSION = '%s';\n", version) if err := os.WriteFile(versionPath, []byte(versionContents), 0644); err != nil { return "", fmt.Errorf("error writing version file: %w", err) @@ -60,8 +60,8 @@ func createBundles() error { result := api.Build(api.BuildOptions{ EntryPoints: []string{ - "code/ts/library/src/bundles/datastar-core.ts", - "code/ts/library/src/bundles/datastar.ts", + "library/src/bundles/datastar-core.ts", + "library/src/bundles/datastar.ts", }, Outdir: outDir, Bundle: true, @@ -127,15 +127,15 @@ func writeOutConsts(version string) error { }) templates := map[string]func(data *ConstTemplateData) string{ - "README.md": datastarREADME, - "code/ts/library/README.md": datastarREADME, - "code/ts/library/src/engine/consts.ts": datastarClientConsts, - "code/ts/library/package.json": datastarClientPackageJSON, - "code/go/sdk/consts.go": goConsts, - "code/dotnet/sdk/src/Consts.fs": dotnetConsts, - "code/php/sdk/src/Consts.php": phpConsts, - "code/php/sdk/src/enums/EventType.php": phpEventType, - "code/php/sdk/src/enums/FragmentMergeMode.php": phpFragmentMergeMode, + "README.md": datastarREADME, + "library/README.md": datastarREADME, + "library/src/engine/consts.ts": datastarClientConsts, + "library/package.json": datastarClientPackageJSON, + "sdk/go/consts.go": goConsts, + "sdk/dotnet/src/Consts.fs": dotnetConsts, + "sdk/php/src/Consts.php": phpConsts, + "sdk/php/src/enums/EventType.php": phpEventType, + "sdk/php/src/enums/FragmentMergeMode.php": phpFragmentMergeMode, } for path, tmplFn := range templates { diff --git a/bundles/datastar-core.js b/bundles/datastar-core.js index 7d7be46ff..3e9daea70 100644 --- a/bundles/datastar-core.js +++ b/bundles/datastar-core.js @@ -1,11 +1,4 @@ -"use strict";(()=>{var te={pluginType:"attribute",name:"star",onLoad:()=>{alert("YOU ARE PROBABLY OVERCOMPLICATING IT")}};var ne={pluginType:"attribute",name:"computed",mustNotEmptyKey:!0,onLoad:t=>{let e=t.store();return e[t.key]=t.reactivity.computed(()=>t.expressionFn(t)),()=>{let r=t.store();delete r[t.key]}}};function re(t,e,r){let n={};if(!r)Object.assign(n,e);else for(let s in e){let o=t[s]?.value;o==null&&(n[s]=e[s])}return n}var se={pluginType:"attribute",name:"store",removeNewLines:!0,preprocessors:{pre:[{pluginType:"preprocessor",name:"store",regexp:/(?.+)/g,replacer:t=>{let{whole:e}=t;return`Object.assign({...ctx.store()}, ${e})`}}]},allowedModifiers:new Set(["ifmissing"]),onLoad:t=>{let e=t.expressionFn(t),r=re(t.store(),e,t.modifiers.has("ifmissing"));t.mergeSignals(r),delete t.el.dataset[t.rawKey]}};var oe="[a-zA-Z_$]+",Te=oe+"[0-9a-zA-Z_$.]*";function L(t,e,r,n=!0){let s=n?Te:oe;return new RegExp(`(?${t}(?<${e}>${s})${r})`,"g")}var ie={name:"action",pluginType:"preprocessor",regexp:L("\\$","action","(?\\((?.*)\\))",!1),replacer:({action:t,args:e})=>{let r=["ctx"];e&&r.push(...e.split(",").map(s=>s.trim()));let n=r.join(",");return`ctx.actions.${t}.method(${n})`}};var ae={name:"signal",pluginType:"preprocessor",regexp:L("\\$","signal","(?\\([^\\)]*\\))?"),replacer:t=>{let{signal:e,method:r}=t,n="ctx.store()";if(!r?.length)return`${n}.${e}.value`;let s=e.split("."),o=s.pop(),a=s.join(".");return`${n}.${a}.value.${o}${r}`}};var k="datastar";var Ee={Morph:"morph",Inner:"inner",Outer:"outer",Prepend:"prepend",Append:"append",Before:"before",After:"after",UpsertAttributes:"upsertAttributes"},Be=Ee.Morph;var v=t=>{let e=new Error;return e.name=`${k}${t}`,e},d=v(400),w=v(409),B=v(404),x=v(403),ue=v(405),Je=v(503);function le(t){if(t.id)return t.id;let e=0,r=s=>(e=(e<<5)-e+s,e&e),n=s=>s.split("").forEach(o=>r(o.charCodeAt(0)));for(;t.parentNode;){if(t.id){n(`${t.id}`);break}else if(t===t.ownerDocument.documentElement)n(t.tagName);else{for(let s=1,o=t;o.previousElementSibling;o=o.previousElementSibling,s++)r(s);t=t.parentNode}t=t.parentNode}return k+e}var Re=Symbol.for("preact-signals"),g=1,T=2,O=4,R=8,C=16,E=32;function M(){D++}function I(){if(D>1){D--;return}let t,e=!1;for(;A!==void 0;){let r=A;for(A=void 0,G++;r!==void 0;){let n=r._nextBatchedEffect;if(r._nextBatchedEffect=void 0,r._flags&=~T,!(r._flags&R)&&pe(r))try{r._callback()}catch(s){e||(t=s,e=!0)}r=n}}if(G=0,D--,e)throw t}function ce(t){if(D>0)return t();M();try{return t()}finally{I()}}var i;var A,D=0,G=0,j=0;function fe(t){if(i===void 0)return;let e=t._node;if(e===void 0||e._target!==i)return e={_version:0,_source:t,_prevSource:i._sources,_nextSource:void 0,_target:i,_prevTarget:void 0,_nextTarget:void 0,_rollbackNode:e},i._sources!==void 0&&(i._sources._nextSource=e),i._sources=e,t._node=e,i._flags&E&&t._subscribe(e),e;if(e._version===-1)return e._version=0,e._nextSource!==void 0&&(e._nextSource._prevSource=e._prevSource,e._prevSource!==void 0&&(e._prevSource._nextSource=e._nextSource),e._prevSource=i._sources,e._nextSource=void 0,i._sources._nextSource=e,i._sources=e),e}function c(t){this._value=t,this._version=0,this._node=void 0,this._targets=void 0}c.prototype.brand=Re;c.prototype._refresh=function(){return!0};c.prototype._subscribe=function(t){this._targets!==t&&t._prevTarget===void 0&&(t._nextTarget=this._targets,this._targets!==void 0&&(this._targets._prevTarget=t),this._targets=t)};c.prototype._unsubscribe=function(t){if(this._targets!==void 0){let e=t._prevTarget,r=t._nextTarget;e!==void 0&&(e._nextTarget=r,t._prevTarget=void 0),r!==void 0&&(r._prevTarget=e,t._nextTarget=void 0),t===this._targets&&(this._targets=r)}};c.prototype.subscribe=function(t){return J(()=>{let e=this.value,r=i;i=void 0;try{t(e)}finally{i=r}})};c.prototype.valueOf=function(){return this.value};c.prototype.toString=function(){return this.value+""};c.prototype.toJSON=function(){return this.value};c.prototype.peek=function(){let t=i;i=void 0;try{return this.value}finally{i=t}};Object.defineProperty(c.prototype,"value",{get(){let t=fe(this);return t!==void 0&&(t._version=this._version),this._value},set(t){if(t!==this._value){if(G>100)throw d;this._value=t,this._version++,j++,M();try{for(let e=this._targets;e!==void 0;e=e._nextTarget)e._target._notify()}finally{I()}}}});function F(t){return new c(t)}function pe(t){for(let e=t._sources;e!==void 0;e=e._nextSource)if(e._source._version!==e._version||!e._source._refresh()||e._source._version!==e._version)return!0;return!1}function de(t){for(let e=t._sources;e!==void 0;e=e._nextSource){let r=e._source._node;if(r!==void 0&&(e._rollbackNode=r),e._source._node=e,e._version=-1,e._nextSource===void 0){t._sources=e;break}}}function ge(t){let e=t._sources,r;for(;e!==void 0;){let n=e._prevSource;e._version===-1?(e._source._unsubscribe(e),n!==void 0&&(n._nextSource=e._nextSource),e._nextSource!==void 0&&(e._nextSource._prevSource=n)):r=e,e._source._node=e._rollbackNode,e._rollbackNode!==void 0&&(e._rollbackNode=void 0),e=n}t._sources=r}function b(t){c.call(this,void 0),this._fn=t,this._sources=void 0,this._globalVersion=j-1,this._flags=O}b.prototype=new c;b.prototype._refresh=function(){if(this._flags&=~T,this._flags&g)return!1;if((this._flags&(O|E))===E||(this._flags&=~O,this._globalVersion===j))return!0;if(this._globalVersion=j,this._flags|=g,this._version>0&&!pe(this))return this._flags&=~g,!0;let t=i;try{de(this),i=this;let e=this._fn();(this._flags&C||this._value!==e||this._version===0)&&(this._value=e,this._flags&=~C,this._version++)}catch(e){this._value=e,this._flags|=C,this._version++}return i=t,ge(this),this._flags&=~g,!0};b.prototype._subscribe=function(t){if(this._targets===void 0){this._flags|=O|E;for(let e=this._sources;e!==void 0;e=e._nextSource)e._source._subscribe(e)}c.prototype._subscribe.call(this,t)};b.prototype._unsubscribe=function(t){if(this._targets!==void 0&&(c.prototype._unsubscribe.call(this,t),this._targets===void 0)){this._flags&=~E;for(let e=this._sources;e!==void 0;e=e._nextSource)e._source._unsubscribe(e)}};b.prototype._notify=function(){if(!(this._flags&T)){this._flags|=O|T;for(let t=this._targets;t!==void 0;t=t._nextTarget)t._target._notify()}};Object.defineProperty(b.prototype,"value",{get(){if(this._flags&g)throw d;let t=fe(this);if(this._refresh(),t!==void 0&&(t._version=this._version),this._flags&C)throw this._value;return this._value}});function he(t){return new b(t)}function _e(t){let e=t._cleanup;if(t._cleanup=void 0,typeof e=="function"){M();let r=i;i=void 0;try{e()}catch(n){throw t._flags&=~g,t._flags|=R,H(t),n}finally{i=r,I()}}}function H(t){for(let e=t._sources;e!==void 0;e=e._nextSource)e._source._unsubscribe(e);t._fn=void 0,t._sources=void 0,_e(t)}function we(t){if(i!==this)throw d;ge(this),i=t,this._flags&=~g,this._flags&R&&H(this),I()}function P(t){this._fn=t,this._cleanup=void 0,this._sources=void 0,this._nextBatchedEffect=void 0,this._flags=E}P.prototype._callback=function(){let t=this._start();try{if(this._flags&R||this._fn===void 0)return;let e=this._fn();typeof e=="function"&&(this._cleanup=e)}finally{t()}};P.prototype._start=function(){if(this._flags&g)throw d;this._flags|=g,this._flags&=~R,_e(this),de(this),M();let t=i;return i=this,we.bind(this,t)};P.prototype._notify=function(){this._flags&T||(this._flags|=T,this._nextBatchedEffect=A,A=this)};P.prototype._dispose=function(){this._flags|=R,this._flags&g||H(this)};function J(t){let e=new P(t);try{e._callback()}catch(r){throw e._dispose(),r}return e._dispose.bind(e)}var $=class{get value(){return K(this)}set value(e){ce(()=>Ae(this,e))}peek(){return K(this,{peek:!0})}},N=t=>Object.assign(new $,Object.entries(t).reduce((e,[r,n])=>{if(["value","peek"].some(s=>s===r))throw x;return typeof n!="object"||n===null||Array.isArray(n)?e[r]=F(n):e[r]=N(n),e},{})),Ae=(t,e)=>Object.keys(e).forEach(r=>t[r].value=e[r]),K=(t,{peek:e=!1}={})=>Object.entries(t).reduce((r,[n,s])=>(s instanceof c?r[n]=e?s.peek():s.value:s instanceof $&&(r[n]=K(s,{peek:e})),r),{});function W(t,e){if(typeof e!="object"||Array.isArray(e)||!e)return JSON.parse(JSON.stringify(e));if(typeof e=="object"&&e.toJSON!==void 0&&typeof e.toJSON=="function")return e.toJSON();let r=t;return typeof t!="object"&&(r={...e}),Object.keys(e).forEach(n=>{r.hasOwnProperty(n)||(r[n]=e[n]),e[n]===null?delete r[n]:r[n]=W(r[n],e[n])}),r}var me="0.20.1";var De=t=>t.pluginType==="preprocessor",Oe=t=>t.pluginType==="watcher",Pe=t=>t.pluginType==="attribute",Ne=t=>t.pluginType==="action",V=class{constructor(){this.plugins=[];this.store=N({});this.preprocessors=new Array;this.actions={};this.watchers=new Array;this.refs={};this.reactivity={signal:F,computed:he,effect:J};this.removals=new Map;this.mergeRemovals=new Array;this.lastMarshalledStore=""}get version(){return me}load(...e){let r=new Set(this.plugins);e.forEach(n=>{if(n.requiredPlugins){for(let o of n.requiredPlugins)if(!r.has(o))throw x}let s;if(De(n)){if(this.preprocessors.includes(n))throw w;this.preprocessors.push(n)}else if(Oe(n)){if(this.watchers.includes(n))throw w;this.watchers.push(n),s=n.onGlobalInit}else if(Ne(n)){if(this.actions[n.name])throw w;this.actions[n.name]=n}else if(Pe(n)){if(this.plugins.includes(n))throw w;this.plugins.push(n),s=n.onGlobalInit}else throw B;s&&s({store:()=>this.store,upsertSignal:this.upsertSignal.bind(this),mergeSignals:this.mergeSignals.bind(this),removeSignals:this.removeSignals.bind(this),actions:this.actions,reactivity:this.reactivity,applyPlugins:this.applyPlugins.bind(this),cleanup:this.cleanup.bind(this)}),r.add(n)}),this.applyPlugins(document.body)}cleanup(e){let r=this.removals.get(e);if(r){for(let n of r.set)n();this.removals.delete(e)}}mergeSignals(e){this.mergeRemovals.forEach(s=>s()),this.mergeRemovals=this.mergeRemovals.slice(0);let r=W(this.store.value,e);this.store=N(r),JSON.stringify(this.store.value),this.lastMarshalledStore}removeSignals(...e){let r={...this.store.value},n=!1;for(let s of e){let o=s.split("."),a=o[0],f=r;for(let l=1;l{this.walkDownDOM(e,o=>{s||this.cleanup(o);for(let a in o.dataset){let f=`${o.dataset[a]}`||"",l=f;if(!a.startsWith(n.name))continue;if(o.id.length||(o.id=le(o)),r.clear(),n.allowedTagRegexps){let u=o.tagName.toLowerCase();if(![...n.allowedTagRegexps].some(h=>u.match(h)))throw x}let m=a.slice(n.name.length),[y,...be]=m.split(".");if(n.mustHaveEmptyKey&&y.length>0)throw d;if(n.mustNotEmptyKey&&y.length===0)throw d;y.length&&(y=y[0].toLowerCase()+y.slice(1));let Y=be.map(u=>{let[S,...h]=u.split("_");return{label:S,args:h}});if(n.allowedModifiers){for(let u of Y)if(!n.allowedModifiers.has(u.label))throw x}let q=new Map;for(let u of Y)q.set(u.label,u.args);if(n.mustHaveEmptyExpression&&l.length)throw d;if(n.mustNotEmptyExpression&&!l.length)throw d;let z=/;|\n/;n.removeNewLines&&(l=l.split(` -`).map(u=>u.trim()).join(" "));let ve=[...n.preprocessors?.pre||[],...this.preprocessors,...n.preprocessors?.post||[]];for(let u of ve){if(r.has(u))continue;r.add(u);let S=l.split(z),h=[];S.forEach(p=>{let _=p,Z=[..._.matchAll(u.regexp)];if(Z.length)for(let Q of Z){if(!Q.groups)continue;let{groups:ee}=Q,{whole:xe}=ee;_=_.replace(xe,u.replacer(ee))}h.push(_)}),l=h.join("; ")}let U={store:()=>this.store,mergeSignals:this.mergeSignals.bind(this),upsertSignal:this.upsertSignal.bind(this),removeSignals:this.removeSignals.bind(this),applyPlugins:this.applyPlugins.bind(this),cleanup:this.cleanup.bind(this),walkSignals:this.walkSignals.bind(this),actions:this.actions,reactivity:this.reactivity,el:o,rawKey:a,key:y,rawExpression:f,expression:l,expressionFn:()=>{throw ue},modifiers:q};if(!n.bypassExpressionFunctionCreation?.(U)&&!n.mustHaveEmptyExpression&&l.length){let u=l.split(z).map(p=>p.trim()).filter(p=>p.length);u[u.length-1]=`return ${u[u.length-1]}`;let S=u.map(p=>` ${p}`).join(`; -`),h=`try{${S}}catch(e){console.error(\`Error evaluating Datastar expression: -${S.replaceAll("`","\\`")} - -Error: \${e.message} - -Check if the expression is valid before raising an issue.\`.trim());debugger}`;try{let p=n.argumentNames||[],_=new Function("ctx",...p,h);U.expressionFn=_}catch(p){let _=new Error(`${p} -with -${h}`);console.error(_);debugger}}let X=n.onLoad(U);X&&(this.removals.has(o)||this.removals.set(o,{id:o.id,set:new Set}),this.removals.get(o).set.add(X))}})})}walkSignalsStore(e,r){let n=Object.keys(e);for(let s=0;s0;if(f){r(o,a);continue}l&&this.walkSignalsStore(a,r)}}walkSignals(e){this.walkSignalsStore(this.store,e)}walkDownDOM(e,r,n=0){if(!e||!(e instanceof HTMLElement||e instanceof SVGElement))return null;for(r(e),n=0,e=e.firstElementChild;e;)this.walkDownDOM(e,r,n++),e=e.nextElementSibling}};var ye=new V;ye.load(ie,ae,se,ne,te);var Se=ye;Se.load();})(); +"use strict";(()=>{var _e="computed",j={type:1,name:_e,keyReq:1,valReq:1,removeOnLoad:!0,onLoad:({key:t,signals:e,genRX:n})=>{let s=n();e.setComputed(t,s)}};var U=t=>t.replace(/(?:^\w|[A-Z]|\b\w)/g,function(e,n){return n==0?e.toLowerCase():e.toUpperCase()}).replace(/\s+/g,""),W=t=>new Function(`return Object.assign({}, ${t})`)();var J={type:1,name:"signals",valReq:1,removeOnLoad:!0,onLoad:t=>{let{key:e,genRX:n,signals:s}=t;if(e!="")s.setValue(e,n()());else{let r=W(t.value);t.value=JSON.stringify(r),s.merge(n()())}}};var z={type:1,name:"star",keyReq:2,valReq:2,onLoad:()=>{alert("YOU ARE PROBABLY OVERCOMPLICATING IT")}};var K={name:"signalValue",type:0,fn:t=>{let e=/(?[\w0-9.]*)((\.value))/gm;return t.replaceAll(e,"ctx.signals.signal('$1').value")}};var H="datastar";var X="0.21.0-beta1";var me={Morph:"morph",Inner:"inner",Outer:"outer",Prepend:"prepend",Append:"append",Before:"before",After:"after",UpsertAttributes:"upsertAttributes"},Le=me.Morph;function Y(t){if(t.id)return t.id;let e=0,n=r=>(e=(e<<5)-e+r,e&e),s=r=>r.split("").forEach(i=>n(i.charCodeAt(0)));for(;t.parentNode;){if(t.id){s(`${t.id}`);break}else if(t===t.ownerDocument.documentElement)s(t.tagName);else{for(let r=1,i=t;i.previousElementSibling;i=i.previousElementSibling,r++)n(r);t=t.parentNode}t=t.parentNode}return H+e}var ve="http://localhost:8080/errors";var u=(t,e)=>{let n=new Error;n.name=`error ${t}`;let s=`${ve}/${t}?${new URLSearchParams(e)}`;return n.message=`for more info see ${s}`,n};var ye=Symbol.for("preact-signals"),g=1,S=2,N=4,b=8,M=16,x=32;function B(){O++}function G(){if(O>1){O--;return}let t,e=!1;for(;T!==void 0;){let n=T;for(T=void 0,L++;n!==void 0;){let s=n._nextBatchedEffect;if(n._nextBatchedEffect=void 0,n._flags&=~S,!(n._flags&b)&&Q(n))try{n._callback()}catch(r){e||(t=r,e=!0)}n=s}}if(L=0,O--,e)throw u("BatchError, error")}var a;var T,O=0,L=0,C=0;function Z(t){if(a===void 0)return;let e=t._node;if(e===void 0||e._target!==a)return e={_version:0,_source:t,_prevSource:a._sources,_nextSource:void 0,_target:a,_prevTarget:void 0,_nextTarget:void 0,_rollbackNode:e},a._sources!==void 0&&(a._sources._nextSource=e),a._sources=e,t._node=e,a._flags&x&&t._subscribe(e),e;if(e._version===-1)return e._version=0,e._nextSource!==void 0&&(e._nextSource._prevSource=e._prevSource,e._prevSource!==void 0&&(e._prevSource._nextSource=e._nextSource),e._prevSource=a._sources,e._nextSource=void 0,a._sources._nextSource=e,a._sources=e),e}function f(t){this._value=t,this._version=0,this._node=void 0,this._targets=void 0}f.prototype.brand=ye;f.prototype._refresh=function(){return!0};f.prototype._subscribe=function(t){this._targets!==t&&t._prevTarget===void 0&&(t._nextTarget=this._targets,this._targets!==void 0&&(this._targets._prevTarget=t),this._targets=t)};f.prototype._unsubscribe=function(t){if(this._targets!==void 0){let e=t._prevTarget,n=t._nextTarget;e!==void 0&&(e._nextTarget=n,t._prevTarget=void 0),n!==void 0&&(n._prevTarget=e,t._nextTarget=void 0),t===this._targets&&(this._targets=n)}};f.prototype.subscribe=function(t){return P(()=>{let e=this.value,n=a;a=void 0;try{t(e)}finally{a=n}})};f.prototype.valueOf=function(){return this.value};f.prototype.toString=function(){return this.value+""};f.prototype.toJSON=function(){return this.value};f.prototype.peek=function(){let t=a;a=void 0;try{return this.value}finally{a=t}};Object.defineProperty(f.prototype,"value",{get(){let t=Z(this);return t!==void 0&&(t._version=this._version),this._value},set(t){if(t!==this._value){if(L>100)throw u("SignalCycleDetected");this._value=t,this._version++,C++,B();try{for(let e=this._targets;e!==void 0;e=e._nextTarget)e._target._notify()}finally{G()}}}});function Q(t){for(let e=t._sources;e!==void 0;e=e._nextSource)if(e._source._version!==e._version||!e._source._refresh()||e._source._version!==e._version)return!0;return!1}function ee(t){for(let e=t._sources;e!==void 0;e=e._nextSource){let n=e._source._node;if(n!==void 0&&(e._rollbackNode=n),e._source._node=e,e._version=-1,e._nextSource===void 0){t._sources=e;break}}}function te(t){let e=t._sources,n;for(;e!==void 0;){let s=e._prevSource;e._version===-1?(e._source._unsubscribe(e),s!==void 0&&(s._nextSource=e._nextSource),e._nextSource!==void 0&&(e._nextSource._prevSource=s)):n=e,e._source._node=e._rollbackNode,e._rollbackNode!==void 0&&(e._rollbackNode=void 0),e=s}t._sources=n}function y(t){f.call(this,void 0),this._fn=t,this._sources=void 0,this._globalVersion=C-1,this._flags=N}y.prototype=new f;y.prototype._refresh=function(){if(this._flags&=~S,this._flags&g)return!1;if((this._flags&(N|x))===x||(this._flags&=~N,this._globalVersion===C))return!0;if(this._globalVersion=C,this._flags|=g,this._version>0&&!Q(this))return this._flags&=~g,!0;let t=a;try{ee(this),a=this;let e=this._fn();(this._flags&M||this._value!==e||this._version===0)&&(this._value=e,this._flags&=~M,this._version++)}catch(e){this._value=e,this._flags|=M,this._version++}return a=t,te(this),this._flags&=~g,!0};y.prototype._subscribe=function(t){if(this._targets===void 0){this._flags|=N|x;for(let e=this._sources;e!==void 0;e=e._nextSource)e._source._subscribe(e)}f.prototype._subscribe.call(this,t)};y.prototype._unsubscribe=function(t){if(this._targets!==void 0&&(f.prototype._unsubscribe.call(this,t),this._targets===void 0)){this._flags&=~x;for(let e=this._sources;e!==void 0;e=e._nextSource)e._source._unsubscribe(e)}};y.prototype._notify=function(){if(!(this._flags&S)){this._flags|=N|S;for(let t=this._targets;t!==void 0;t=t._nextTarget)t._target._notify()}};Object.defineProperty(y.prototype,"value",{get(){if(this._flags&g)throw u("SignalCycleDetected");let t=Z(this);if(this._refresh(),t!==void 0&&(t._version=this._version),this._flags&M)throw u("GetComputedError",{value:this._value});return this._value}});function ne(t){return new y(t)}function se(t){let e=t._cleanup;if(t._cleanup=void 0,typeof e=="function"){B();let n=a;a=void 0;try{e()}catch(s){throw t._flags&=~g,t._flags|=b,$(t),u("CleanupEffectError",{error:s})}finally{a=n,G()}}}function $(t){for(let e=t._sources;e!==void 0;e=e._nextSource)e._source._unsubscribe(e);t._fn=void 0,t._sources=void 0,se(t)}function Se(t){if(a!==this)throw u("EndEffectError");te(this),a=t,this._flags&=~g,this._flags&b&&$(this),G()}function w(t){this._fn=t,this._cleanup=void 0,this._sources=void 0,this._nextBatchedEffect=void 0,this._flags=x}w.prototype._callback=function(){let t=this._start();try{if(this._flags&b||this._fn===void 0)return;let e=this._fn();typeof e=="function"&&(this._cleanup=e)}finally{t()}};w.prototype._start=function(){if(this._flags&g)throw u("SignalCycleDetected");this._flags|=g,this._flags&=~b,se(this),ee(this),B();let t=a;return a=this,Se.bind(this,t)};w.prototype._notify=function(){this._flags&S||(this._flags|=S,this._nextBatchedEffect=T,T=this)};w.prototype._dispose=function(){this._flags|=b,this._flags&g||$(this)};function P(t){let e=new w(t);try{e._callback()}catch(n){throw e._dispose(),u("EffectError",{error:n})}return e._dispose.bind(e)}function re(t,e=!1){let n={};for(let s in t)if(t.hasOwnProperty(s)){let r=t[s];if(r instanceof f){if(e&&s.startsWith("_"))continue;n[s]=r.value}else n[s]=re(r)}return n}function ie(t,e,n=!1){for(let s in e)if(e.hasOwnProperty(s)){let r=e[s];if(r instanceof Object&&!Array.isArray(r))t[s]||(t[s]={}),ie(t[s],r,n);else{if(n&&t[s])continue;t[s]=new f(r)}}}function oe(t,e){for(let n in t)if(t.hasOwnProperty(n)){let s=t[n];s instanceof f?e(n,s):oe(s,e)}}function xe(t,...e){let n={};for(let s of e){let r=s.split("."),i=t,o=n;for(let d=0;dn());this.setSignal(e,s)}value(e){return this.signal(e)?.value}setValue(e,n){let s=this.upsert(e,n);s.value=n}upsert(e,n){let s=e.split("."),r=this._signals;for(let d=0;d{let s;switch(n.type){case 0:this.macros.push(n);break;case 2:let r=n;this.watchers.push(r),s=r.onGlobalInit;break;case 3:this.actions[n.name]=n;break;case 1:let i=n;this.plugins.push(i),s=i.onGlobalInit;break;default:throw u("InvalidPluginType",{name:n.name,type:n.type})}if(s){let r=this;s({get signals(){return r._signals},effect:i=>P(i),actions:this.actions,apply:this.apply.bind(this),cleanup:this.cleanup.bind(this)})}}),this.apply(document.body)}cleanup(e){let n=this.removals.get(e);if(n){for(let s of n.set)s();this.removals.delete(e)}}apply(e){let n=new Set;this.plugins.forEach((s,r)=>{this.walkDownDOM(e,i=>{r||this.cleanup(i);for(let o in i.dataset){if(!o.startsWith(s.name))continue;let c=o.slice(s.name.length),[d,...p]=c.split(":"),h=d.length>0;h&&(d=d[0].toLowerCase()+d.slice(1));let D=`${i.dataset[o]}`||"",_=D,m=_.length>0,l=s.keyReq||0;if(h){if(l===2)throw u(s.name+"KeyNotAllowed")}else if(l===1)throw u(s.name+"KeyRequired");let E=s.valReq||0;if(m){if(E===2)throw u(s.name+"ValueNotAllowed")}else if(E===1)throw u(s.name+"ValueRequired");if(l===3||E===3){if(h&&m)throw u(s.name+"KeyAndValueProvided");if(!h&&!m)throw u(s.name+"KeyOrValueRequired")}i.id.length||(i.id=Y(i)),n.clear();let R=new Map;p.forEach(v=>{let[ge,...he]=v.split("_");R.set(U(ge),new Set(he))});let ue=[...s.macros?.pre||[],...this.macros,...s.macros?.post||[]];for(let v of ue)n.has(v)||(n.add(v),_=v.fn(_));let{actions:ce,apply:fe,cleanup:de}=this,pe=this,F;F={get signals(){return pe._signals},effect:v=>P(v),apply:fe.bind(this),cleanup:de.bind(this),actions:ce,genRX:()=>this.genRX(F,...s.argNames||[]),el:i,rawKey:o,rawValue:D,key:d,value:_,mods:R};let q=s.onLoad(F);q&&(this.removals.has(i)||this.removals.set(i,{id:i.id,set:new Set}),this.removals.get(i).set.add(q)),s?.removeOnLoad&&delete i.dataset[o]}})})}genRX(e,...n){let s=e.value.split(/;|\n/).map(l=>l.trim()).filter(l=>l!=""),r=s.length-1;s[r].startsWith("return")||(s[r]=`return (${s[r]});`);let o=s.join(` +`),c=/(\w*)\(/gm,d=o.matchAll(c),p=new Set;for(let l of d)p.add(l[1]);let h=Object.keys(this.actions).filter(l=>p.has(l)),_=`${h.map(l=>`const ${l} = ctx.actions.${l}.fn;`).join(` +`)}return (()=> {${o}})()`,m=_.trim();h.forEach(l=>{m=m.replaceAll(l+"(",l+"(ctx,")});try{let l=n||[],E=new Function("ctx",...l,m);return(...R)=>E(e,...R)}catch(l){throw u("GeneratingExpressionFailed",{error:l,fnContent:_})}}walkDownDOM(e,n){if(!e||!(e instanceof HTMLElement||e instanceof SVGElement))return null;for(n(e),e=e.firstElementChild;e;)this.walkDownDOM(e,n),e=e.nextElementSibling}};var ae=new V;ae.load(z,K,J,j);var le=ae;le.load();})(); //# sourceMappingURL=datastar-core.js.map diff --git a/bundles/datastar-core.js.map b/bundles/datastar-core.js.map index 20090e5fc..f60c17106 100644 --- a/bundles/datastar-core.js.map +++ b/bundles/datastar-core.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../code/ts/library/src/plugins/official/attributes/core/star.ts", "../code/ts/library/src/plugins/official/attributes/core/computed.ts", "../code/ts/library/src/utils/signals.ts", "../code/ts/library/src/plugins/official/attributes/core/store.ts", "../code/ts/library/src/utils/regex.ts", "../code/ts/library/src/plugins/official/preprocessors/core/actions.ts", "../code/ts/library/src/plugins/official/preprocessors/core/signals.ts", "../code/ts/library/src/engine/consts.ts", "../code/ts/library/src/engine/errors.ts", "../code/ts/library/src/utils/dom.ts", "../code/ts/library/src/vendored/preact-core.ts", "../code/ts/library/src/vendored/deepsignal.ts", "../code/ts/library/src/vendored/ts-merge-patch.ts", "../code/ts/library/src/engine/version.ts", "../code/ts/library/src/engine/engine.ts", "../code/ts/library/src/engine/index.ts", "../code/ts/library/src/bundles/datastar-core.ts"], - "sourcesContent": ["// Authors: Delaney Gillilan\n// Icon: material-symbols:rocket\n// Slug: Star\n// Description: Sage advice for the weary traveler\n\nimport { AttributePlugin } from \"../../../../engine\";\n\nexport const Star: AttributePlugin = {\n pluginType: \"attribute\",\n name: \"star\",\n onLoad: () => {\n alert(\"YOU ARE PROBABLY OVERCOMPLICATING IT\");\n },\n};\n", "// Authors: Delaney Gillilan\n// Icon: fluent:draw-text-24-filled\n// Slug: Create a computed signal\n// Description: This attribute creates a computed signal that updates when its dependencies change.\n\nimport { AttributePlugin } from \"../../../../engine\";\n\nexport const Computed: AttributePlugin = {\n pluginType: \"attribute\",\n name: \"computed\",\n mustNotEmptyKey: true,\n onLoad: (ctx) => {\n const store = ctx.store();\n store[ctx.key] = ctx.reactivity.computed(() => {\n return ctx.expressionFn(ctx);\n });\n\n return () => {\n const store = ctx.store();\n delete store[ctx.key];\n };\n },\n};\n", "export function remoteSignals(obj: Object): Object {\n const res: Record = {};\n\n for (const [k, v] of Object.entries(obj)) {\n if (k.startsWith(\"_\")) {\n continue;\n } else if (typeof v === \"object\" && !Array.isArray(v)) {\n res[k] = remoteSignals(v); // recurse\n } else {\n res[k] = v;\n }\n }\n\n return res;\n}\n\nexport function storeFromPossibleContents(\n currentStore: any,\n contents: any,\n hasIfMissing: boolean,\n) {\n const actual: any = {};\n\n if (!hasIfMissing) {\n Object.assign(actual, contents);\n } else {\n for (const key in contents) {\n const currentValue = currentStore[key]?.value;\n if (currentValue === undefined || currentValue === null) {\n actual[key] = contents[key];\n }\n }\n }\n\n return actual;\n}\n", "// Authors: Delaney Gillilan\n// Icon: material-symbols:home-storage\n// Slug: Store signals into a singleton per page\n// Description: This action stores signals into a singleton per page. This is useful for storing signals that are used across multiple components.\n\nimport {\n AttributeContext,\n AttributePlugin,\n RegexpGroups,\n} from \"../../../../engine\";\nimport { storeFromPossibleContents } from \"../../../../utils/signals\";\n\n// Setup the global store\nexport const Store: AttributePlugin = {\n pluginType: \"attribute\",\n name: \"store\",\n removeNewLines: true,\n preprocessors: {\n pre: [\n {\n pluginType: \"preprocessor\",\n name: \"store\",\n regexp: /(?.+)/g,\n replacer: (groups: RegexpGroups) => {\n const { whole } = groups;\n return `Object.assign({...ctx.store()}, ${whole})`;\n },\n },\n ],\n },\n allowedModifiers: new Set([\"ifmissing\"]),\n onLoad: (ctx: AttributeContext) => {\n const possibleMergeSignals = ctx.expressionFn(ctx);\n const actualMergeSignals = storeFromPossibleContents(\n ctx.store(),\n possibleMergeSignals,\n ctx.modifiers.has(\"ifmissing\"),\n );\n ctx.mergeSignals(actualMergeSignals);\n\n delete ctx.el.dataset[ctx.rawKey];\n },\n};\n", "export const validJSIdentifier = `[a-zA-Z_$]+`;\nexport const validNestedJSIdentifier = validJSIdentifier + `[0-9a-zA-Z_$.]*`;\n\nexport function wholePrefixSuffix(\n rune: string,\n prefix: string,\n suffix: string,\n nestable = true,\n) {\n const identifier = nestable ? validNestedJSIdentifier : validJSIdentifier;\n return new RegExp(\n `(?${rune}(?<${prefix}>${identifier})${suffix})`,\n `g`,\n );\n}\n", "import { PreprocessorPlugin, RegexpGroups } from \"../../../../engine\";\nimport { wholePrefixSuffix } from \"../../../../utils/regex\";\n\n// Replacing $action(args) with ctx.actions.action(ctx, args)\nexport const ActionsProcessor: PreprocessorPlugin = {\n name: \"action\",\n pluginType: \"preprocessor\",\n regexp: wholePrefixSuffix(\n \"\\\\$\",\n \"action\",\n \"(?\\\\((?.*)\\\\))\",\n false,\n ),\n replacer: ({ action, args }: RegexpGroups) => {\n const withCtx = [`ctx`];\n if (args) {\n withCtx.push(...args.split(\",\").map((x) => x.trim()));\n }\n const argsJoined = withCtx.join(\",\");\n return `ctx.actions.${action}.method(${argsJoined})`;\n },\n};\n", "import { PreprocessorPlugin, RegexpGroups } from \"../../../../engine\";\nimport { wholePrefixSuffix } from \"../../../../utils/regex\";\n\n// Replacing $signal with ctx.store.signal.value`\nexport const SignalsProcessor: PreprocessorPlugin = {\n name: \"signal\",\n pluginType: \"preprocessor\",\n regexp: wholePrefixSuffix(\"\\\\$\", \"signal\", \"(?\\\\([^\\\\)]*\\\\))?\"),\n replacer: (groups: RegexpGroups) => {\n const { signal, method } = groups;\n const prefix = `ctx.store()`;\n if (!method?.length) {\n return `${prefix}.${signal}.value`;\n }\n const parts = signal.split(\".\");\n const methodName = parts.pop();\n const nestedSignal = parts.join(\".\");\n return `${prefix}.${nestedSignal}.value.${methodName}${method}`;\n },\n};\n", "// This is auto-generated by Datastar. DO NOT EDIT.\n\nexport const DATASTAR = \"datastar\";\nexport const DATASTAR_EVENT = \"datastar-event\";\nexport const DATASTAR_REQUEST = \"Datastar-Request\";\nexport const VERSION = \"0.20.1\";\n\n// #region Defaults\n\n// #region Default durations\n\n// The default duration for settling during merges. Allows for CSS transitions to complete.\nexport const DefaultSettleDurationMs = 300;\n// The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE.\nexport const DefaultSseRetryDurationMs = 1000;\n\n// #endregion\n\n\n// #region Default strings\n\n// The default attributes for - - - - -
-
-

___ + D* Example

-
-
- - 🚀 - -
- - 5999999999999999999 - -
-
-
-
-
-
-
-
-
-
-
-
-

-        
-
-
- - diff --git a/code/dotnet/sdk/src/Falco/Response.fs b/code/dotnet/sdk/src/Falco/Response.fs deleted file mode 100644 index 5263a79eb..000000000 --- a/code/dotnet/sdk/src/Falco/Response.fs +++ /dev/null @@ -1,39 +0,0 @@ -module StarFederation.Datastar.Falco.Response - -open System.Threading.Tasks -open Microsoft.AspNetCore.Http -open StarFederation.Datastar -open StarFederation.Datastar.DependencyInjection - -let sseMergeFragments<'T when 'T :> IDatastarSignalsStore> (fragment:'T -> string) = fun (ctx:HttpContext) -> - let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService - let signalsStore = ctx.RequestServices.GetService(typedefof) :?> 'T - sseService.MergeFragments (fragment signalsStore) - -let sseRemoveFragments<'T when 'T :> IDatastarSignalsStore> (selector:'T -> string) = fun (ctx:HttpContext) -> - let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService - let signalsStore = ctx.RequestServices.GetService(typedefof) :?> 'T - sseService.RemoveFragments (selector signalsStore) - -let sseMergeSignals<'T when 'T :> IDatastarSignalsStore> (newSignalsStore:'T -> IDatastarSignalsStore) = fun (ctx:HttpContext) -> - let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService - let signalsStore = ctx.RequestServices.GetService(typedefof) :?> 'T - sseService.MergeSignals (newSignalsStore signalsStore) - -let sseRemoveSignals<'T when 'T :> IDatastarSignalsStore> (signals:'T -> string[]) = fun (ctx:HttpContext) -> - let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService - let signalsStore = ctx.RequestServices.GetService(typedefof) :?> 'T - sseService.RemoveSignals (signals signalsStore) - -let sseGenerator<'T when 'T :> IDatastarSignalsStore> (feed:HttpContext -> IServerSentEventService -> 'T -> Task) = (fun (ctx:HttpContext) -> - task { - let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService - let signalsStore = ctx.RequestServices.GetService(typedefof) :?> 'T - try - do! feed ctx sseService signalsStore - finally - try - ctx.Connection.RequestClose() - finally () - } :> Task - ) \ No newline at end of file diff --git a/code/go/sdk/signals-sugar.go b/code/go/sdk/signals-sugar.go deleted file mode 100644 index d0bb855f5..000000000 --- a/code/go/sdk/signals-sugar.go +++ /dev/null @@ -1,35 +0,0 @@ -package datastar - -import ( - "encoding/json" - "fmt" -) - -func (sse *ServerSentEventGenerator) MarshalAndMergeSignals(store any, opts ...MergeSignalsOption) error { - b, err := json.Marshal(store) - if err != nil { - panic(err) - } - if err := sse.MergeSignals(b, opts...); err != nil { - return fmt.Errorf("failed to merge store: %w", err) - } - - return nil -} - -func (sse *ServerSentEventGenerator) MarshalAndMergeSignalsIfMissing(store any, opts ...MergeSignalsOption) error { - if err := sse.MarshalAndMergeSignals( - store, - append(opts, WithOnlyIfMissing(true))..., - ); err != nil { - return fmt.Errorf("failed to merge store if missing: %w", err) - } - return nil -} - -func (sse *ServerSentEventGenerator) MergeSignalsIfMissingRaw(storeJSON string) error { - if err := sse.MergeSignals([]byte(storeJSON), WithOnlyIfMissing(true)); err != nil { - return fmt.Errorf("failed to merge store if missing: %w", err) - } - return nil -} diff --git a/code/go/site/routes_examples_click_to_edit.templ b/code/go/site/routes_examples_click_to_edit.templ deleted file mode 100644 index 39535b88b..000000000 --- a/code/go/site/routes_examples_click_to_edit.templ +++ /dev/null @@ -1,62 +0,0 @@ -package site - -import goaway "github.com/TwiN/go-away" - -type ClickToEditContactStore struct { - FirstName string `json:"firstName,omitempty" san:"trim,xss,max=128"` - LastName string `json:"lastName,omitempty" san:"trim,xss,max=128"` - Email string `json:"email,omitempty" san:"trim,xss,max=128"` -} - -templ setupExamplesClickToEditUserComponent(store *ClickToEditContactStore) { -
- - - -
- - -
-
-} - -templ setupExamplesClickToEditUserEdit(store *ClickToEditContactStore) { -
- - - -
- - -
-
-} diff --git a/code/go/site/routes_examples_click_to_load.go b/code/go/site/routes_examples_click_to_load.go deleted file mode 100644 index 648a35485..000000000 --- a/code/go/site/routes_examples_click_to_load.go +++ /dev/null @@ -1,44 +0,0 @@ -package site - -import ( - "net/http" - - "github.com/go-chi/chi/v5" - datastar "github.com/starfederation/datastar/code/go/sdk" -) - -func setupExamplesClickToLoad(examplesRouter chi.Router) error { - - examplesRouter.Get("/click_to_load/data", func(w http.ResponseWriter, r *http.Request) { - store := &ClickToLoadStore{} - if err := datastar.ReadSignals(r, store); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } - if store.Limit < 1 { - store.Limit = 10 - } else if store.Limit > 100 { - store.Limit = 100 - } - if store.Offset < 0 { - store.Offset = 0 - } - - sse := datastar.NewSSE(w, r) - - if store.Offset == 0 { - sse.MergeFragmentTempl(ClickToEditAgentsTable(store)) - } else { - sse.MergeFragmentTempl(ClickToLoadMoreButton(store)) - for i := 0; i < store.Limit; i++ { - // log.Printf("ClickToLoadAgentRow: %d", store.Offset+i) - sse.MergeFragmentTempl( - ClickToLoadAgentRow(store.Offset+i), - datastar.WithSelectorID("click_to_load_rows"), - datastar.WithMergeAppend(), - ) - } - } - }) - - return nil -} diff --git a/code/go/site/routes_examples_dialogs_browser.templ b/code/go/site/routes_examples_dialogs_browser.templ deleted file mode 100644 index 549067cb1..000000000 --- a/code/go/site/routes_examples_dialogs_browser.templ +++ /dev/null @@ -1,37 +0,0 @@ -package site - -type DialogBrowserStore struct { - Prompt string `json:"prompt"` - Confirm bool `json:"confirm"` -} - -templ DialogBrowserView(store *DialogBrowserStore) { - -} - -templ DialogBrowserSure(store *DialogBrowserStore) { -
- if store.Confirm { -
- You clicked the button and confirmed with prompt of { store.Prompt }! -
- - } else { -
- @icon("material-symbols:error-icon") - You clicked the button and did not confirm! Should not see this -
- } -
-} diff --git a/code/go/site/routes_examples_model_bindings.templ b/code/go/site/routes_examples_model_bindings.templ deleted file mode 100644 index 7d0021cdb..000000000 --- a/code/go/site/routes_examples_model_bindings.templ +++ /dev/null @@ -1,45 +0,0 @@ -package site - -import "fmt" - -type ModelBindingStore struct { - BindText string `json:"bindText"` - BindNumber int `json:"bindNumber"` - BindBool bool `json:"bindBool"` - BindSelection int `json:"bindSelection"` -} - -templ ModelBindingView(optionCount int, store *ModelBindingStore) { -
- - - -
- -
- -
- for i := 1; i <= optionCount; i++ { - {{ str := fmt.Sprint(i) }} -
- -
- } -
-
-} diff --git a/code/go/site/routes_examples_mousemove.go b/code/go/site/routes_examples_mousemove.go deleted file mode 100644 index ead17aefd..000000000 --- a/code/go/site/routes_examples_mousemove.go +++ /dev/null @@ -1,168 +0,0 @@ -package site - -import ( - "context" - "fmt" - "net/http" - "time" - - "github.com/delaneyj/toolbelt" - "github.com/delaneyj/toolbelt/embeddednats" - "github.com/go-chi/chi/v5" - "github.com/goccy/go-json" - "github.com/nats-io/nats.go/jetstream" - datastar "github.com/starfederation/datastar/code/go/sdk" - "github.com/zeebo/xxh3" -) - -type MouseXY struct { - X int `json:"x"` - Y int `json:"y"` - At time.Time `json:"at"` -} - -type MouseXYCollection struct { - Positions map[string]MouseXY `json:"positions"` -} - -func setupExamplesMousemove(setupCtx context.Context, examplesRouter chi.Router, ns *embeddednats.Server) error { - nc, err := ns.Client() - if err != nil { - return fmt.Errorf("error creating nats client: %w", err) - } - js, err := jetstream.New(nc) - if err != nil { - return fmt.Errorf("error creating jetstream client: %w", err) - } - kv, err := js.CreateOrUpdateKeyValue(setupCtx, jetstream.KeyValueConfig{ - Bucket: "cursors", - Description: "Mouse cursor positions", - Compression: true, - TTL: time.Hour, - MaxBytes: 16 * 1024 * 1024, - }) - if err != nil { - return fmt.Errorf("error creating key value: %w", err) - } - const key = "allCursors" - kv.PutString(setupCtx, key, `{"positions": {}}`) - - const maxTime = 3 * time.Second - - decodeCursors := func(entry jetstream.KeyValueEntry) (*MouseXYCollection, error) { - cursors := &MouseXYCollection{} - if err := json.Unmarshal(entry.Value(), cursors); err != nil { - return nil, fmt.Errorf("failed to unmarshal cursors: %w", err) - } - return cursors, nil - } - - cursors := func(ctx context.Context) (*MouseXYCollection, uint64, error) { - entry, err := kv.Get(ctx, key) - if err != nil { - return nil, 0, fmt.Errorf("failed to get key value: %w", err) - } - - cursors, err := decodeCursors(entry) - if err != nil { - return nil, 0, fmt.Errorf("failed to decode cursors: %w", err) - } - - return cursors, entry.Revision(), nil - } - - updateCursors := func(ctx context.Context, id string, cursor MouseXY) error { - cursors, rev, err := cursors(ctx) - if err != nil { - return fmt.Errorf("failed to get cursors: %w", err) - } - - cursors.Positions[id] = cursor - - now := time.Now() - for id, cursor := range cursors.Positions { - if now.Sub(cursor.At) > maxTime { - delete(cursors.Positions, id) - } - } - - b, err := json.Marshal(cursors) - if err != nil { - return fmt.Errorf("failed to marshal mvc: %w", err) - } - - kv.Update(ctx, key, b, rev) - - return nil - } - - examplesRouter.Route("/mouse_move/updates", func(updatesRouter chi.Router) { - // updatesRouter.Use( - // httprate.LimitByIP(2, time.Second), - // ) - updatesRouter.Get("/", func(w http.ResponseWriter, r *http.Request) { - sse := datastar.NewSSE(w, r) - - id := fmt.Sprintf("%x", xxh3.HashString(toolbelt.NextEncodedID())) - - ctx := r.Context() - collection, _, err := cursors(ctx) - if err != nil { - sse.ConsoleError(err) - return - } - - sse.MergeFragmentTempl(MouseMouseUI(id, collection)) - - watcher, err := kv.Watch(ctx, key) - if err != nil { - sse.ConsoleError(err) - return - } - defer watcher.Stop() - - for { - select { - case <-ctx.Done(): - return - case entry := <-watcher.Updates(): - if entry == nil { - continue - } - collection, err := decodeCursors(entry) - if err != nil { - sse.ConsoleError(err) - return - } - - sse.MergeFragmentTempl(cursorSVG(collection.Positions)) - } - } - - }) - - updatesRouter.Put("/", func(w http.ResponseWriter, r *http.Request) { - type Form struct { - ID string `json:"id"` - X int `json:"x"` - Y int `json:"y"` - } - form := &Form{} - if err := datastar.ReadSignals(r, form); err != nil { - datastar.NewSSE(w, r).ConsoleError(err) - return - } - - // log.Printf("Received mouse move: %+v", form) - - updateCursors(r.Context(), form.ID, MouseXY{ - X: form.X, - Y: form.Y, - At: time.Now(), - }) - - }) - }) - - return nil -} diff --git a/code/go/site/routes_examples_mousemove.templ b/code/go/site/routes_examples_mousemove.templ deleted file mode 100644 index 1310ff088..000000000 --- a/code/go/site/routes_examples_mousemove.templ +++ /dev/null @@ -1,45 +0,0 @@ -package site - -import "fmt" - -templ MouseMouseUI(id string, collection *MouseXYCollection) { -
-
My ID: { id }
- @cursorSVG(collection.Positions) -
-} - -templ cursorSVG(cursors map[string]MouseXY) { - - for id, cursor := range cursors { - {{ - x := fmt.Sprintf("%d", cursor.X) - y := fmt.Sprintf("%d", cursor.Y) - }} - - - { id } - - } - - - - -} diff --git a/code/go/site/routes_examples_quick_primer_go.templ b/code/go/site/routes_examples_quick_primer_go.templ deleted file mode 100644 index a9b005f0a..000000000 --- a/code/go/site/routes_examples_quick_primer_go.templ +++ /dev/null @@ -1,46 +0,0 @@ -package site - -import "fmt" - -type QuickPrimerGoStore struct { - Input string `json:"input"` - Show bool `json:"show"` -} - -templ QuickPrimerGoView(store *QuickPrimerGoStore) { -
-

Go Datastar Example

-
- -
- -
- Hello from Datastar! -
-
#output
- -
#output2
- -
- Feed from server: - -
-
-
-} - -templ QuickPrimerGoPut(store *QuickPrimerGoStore) { -
Your input: { store.Input }, is { fmt.Sprint(len(store.Input)) } characters long.
-} - -templ QuickPrimerGoGet(stateStr string) { -
Backend state: { stateStr }
-} - -templ QuickPrimerCheckThisOut() { -
Check this out!
-} - -templ QuickPrimerGoFeed(feed string) { - { feed } -} diff --git a/code/go/site/routes_examples_redirects.templ b/code/go/site/routes_examples_redirects.templ deleted file mode 100644 index 3c2d9be3a..000000000 --- a/code/go/site/routes_examples_redirects.templ +++ /dev/null @@ -1,15 +0,0 @@ -package site - -type RedirectsStore struct { - RedirectTo string `json:"redirectTo"` -} - -templ redirectsView(store *RedirectsStore) { -
- - -
-} diff --git a/code/go/site/routes_examples_shoelace_kitchensink.templ b/code/go/site/routes_examples_shoelace_kitchensink.templ deleted file mode 100644 index 813ac362e..000000000 --- a/code/go/site/routes_examples_shoelace_kitchensink.templ +++ /dev/null @@ -1,50 +0,0 @@ -package site - -import ( - "fmt" - "net/http" -) - -type ShoelaceKitchensinkStore struct { - Nested *ShoelaceKitchensinkNested `json:"nested"` -} - -type ShoelaceKitchensinkNested struct { - Label string `json:"label"` - Selection uint32 `json:"selection"` - IsChecked bool `json:"isChecked"` -} - -type ShoelaceKitchensinkOption struct { - Label string `json:"label"` - Value uint32 `json:"value"` -} - -templ ShoelaceKitchensinkView(r *http.Request, options []ShoelaceKitchensinkOption, store *ShoelaceKitchensinkStore) { -
- - - for _, o := range options { - { o.Label } ({ fmt.Sprint(o.Value) }) - } - - - for _, o := range options { - { o.Label } ({ fmt.Sprint(o.Value) }) - } - - Checkbox - Submit -
-} diff --git a/code/go/site/routes_examples_store_ifmissing.go b/code/go/site/routes_examples_store_ifmissing.go deleted file mode 100644 index d08d6f60f..000000000 --- a/code/go/site/routes_examples_store_ifmissing.go +++ /dev/null @@ -1,42 +0,0 @@ -package site - -import ( - "fmt" - "net/http" - "time" - - "github.com/go-chi/chi/v5" - datastar "github.com/starfederation/datastar/code/go/sdk" -) - -func setupExamplesStoreIfMissing(examplesRouter chi.Router) error { - - examplesRouter.Get("/store_ifmissing/updates", func(w http.ResponseWriter, r *http.Request) { - sse := datastar.NewSSE(w, r) - - t := time.NewTicker(1 * time.Second) - defer t.Stop() - - i := 1234 - for { - select { - case <-r.Context().Done(): - return - case <-t.C: - store := fmt.Sprintf("{id:%d}", i) - - switch i % 2 { - case 0: - fragment := fmt.Sprintf(`
`, store) - sse.MergeFragments(fragment, datastar.WithMergeUpsertAttributes()) - case 1: - sse.MarshalAndMergeSignalsIfMissing(store) - } - i++ - } - } - - }) - - return nil -} diff --git a/code/go/site/routes_examples_toggle_visibility.templ b/code/go/site/routes_examples_toggle_visibility.templ deleted file mode 100644 index 49d5bb11a..000000000 --- a/code/go/site/routes_examples_toggle_visibility.templ +++ /dev/null @@ -1,23 +0,0 @@ -package site - -type ShowStore struct { - BindBool bool `json:"bindBool"` -} - -templ ToggleVisibilityView(store *ShowStore) { -
- -
- Hello! -
-
-} diff --git a/code/go/site/routes_examples_update_store.go b/code/go/site/routes_examples_update_store.go deleted file mode 100644 index 48769fd52..000000000 --- a/code/go/site/routes_examples_update_store.go +++ /dev/null @@ -1,64 +0,0 @@ -package site - -import ( - "fmt" - "math/rand" - "net/http" - "time" - - "github.com/go-chi/chi/v5" - datastar "github.com/starfederation/datastar/code/go/sdk" -) - -func setupExamplesUpdateStore(examplesRouter chi.Router) error { - - examplesRouter.Route("/update_store/data", func(dataRouter chi.Router) { - dataRouter.Route("/patch", func(patchRouter chi.Router) { - patchRouter.Post("/", func(w http.ResponseWriter, r *http.Request) { - store := map[string]any{} - if err := datastar.ReadSignals(r, &store); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - randKey := fmt.Sprintf("%d", rand.Intn(2<<16)) - store[randKey] = time.Now().Format(time.RFC3339Nano) - - datastar.NewSSE(w, r).MarshalAndMergeSignals(store) - }) - - patchRouter.Delete("/", func(w http.ResponseWriter, r *http.Request) { - store := map[string]any{} - if err := datastar.ReadSignals(r, &store); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - sse := datastar.NewSSE(w, r) - - if len(store) == 0 { - return - } - - const maxDeletes = 2 - - keysToDelete := make([]string, 0, len(store)) - for k := range store { - keysToDelete = append(keysToDelete, k) - } - rand.Shuffle(len(keysToDelete), func(i, j int) { - keysToDelete[i], keysToDelete[j] = keysToDelete[j], keysToDelete[i] - }) - - if len(keysToDelete) > maxDeletes { - keysToDelete = keysToDelete[:maxDeletes] - } - - sse.RemoveSignals(keysToDelete...) - }) - }) - - }) - - return nil -} diff --git a/code/go/site/smoketests/mouse_move_test.go b/code/go/site/smoketests/mouse_move_test.go deleted file mode 100644 index 14d0a1f87..000000000 --- a/code/go/site/smoketests/mouse_move_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package smoketests - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestExampleMouseMove(t *testing.T) { - t.Skip("skipping TestExampleMouseMove") - g := setup(t) - - page := g.page("examples/mouse_move") - assert.NotNil(t, page) - -} diff --git a/code/go/site/smoketests/persist_test.go b/code/go/site/smoketests/persist_test.go deleted file mode 100644 index 4a7a85e88..000000000 --- a/code/go/site/smoketests/persist_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package smoketests - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestExamplePersist(t *testing.T) { - t.Skip("skipping TestExamplePersist") - g := setup(t) - - page := g.page("examples/persist") - assert.NotNil(t, page) -} diff --git a/code/go/site/smoketests/session_storage_test.go b/code/go/site/smoketests/session_storage_test.go deleted file mode 100644 index d7108c82f..000000000 --- a/code/go/site/smoketests/session_storage_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package smoketests - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestExampleSessionStorage(t *testing.T) { - t.Skip("skipping TestExampleSessionStorage") - g := setup(t) - - page := g.page("examples/session_storage") - assert.NotNil(t, page) -} diff --git a/code/go/site/static/md/examples/classes.md b/code/go/site/static/md/examples/classes.md deleted file mode 100644 index c0d61ef47..000000000 --- a/code/go/site/static/md/examples/classes.md +++ /dev/null @@ -1,31 +0,0 @@ -## Classes - -## Demo - -
-
Count
-
- Remake blink tag -
-
- -## Explanation - -```html -
-
Count
-
- Remake blink tag -
-
-``` - -Here we are using computed signal to store a boolean then use it to drive classes diff --git a/code/go/site/static/md/examples/img_src_bind.md b/code/go/site/static/md/examples/img_src_bind.md deleted file mode 100644 index de9ee2088..000000000 --- a/code/go/site/static/md/examples/img_src_bind.md +++ /dev/null @@ -1,27 +0,0 @@ -## Demo - -
- - - -
- -## Explanation - -```html -
- - - -
-``` - -A discussion on the Discord channel found a bug in binding images. This is an example to make sure it works going forward. The bind of `src` to a template literal allows for a dynamic url and is hitting the picsum backend, not ours so no HTML fragments are needed. - -The `$fitInt` actions is a nice helper to simplify picking a random valid number. diff --git a/code/go/site/static/md/examples/mouse_move.md b/code/go/site/static/md/examples/mouse_move.md deleted file mode 100644 index a5ef57d96..000000000 --- a/code/go/site/static/md/examples/mouse_move.md +++ /dev/null @@ -1,81 +0,0 @@ -## Mouse Move - -## Demo - -
-
- X: - Y: -
-
-
- -## Explanation - -```html -
-
- X: - Y: -
-
-
-``` - -Here we are doing similar watcher action to the front page TodoMVC. We are loading the example from the server to avoid some markdown parsing issues. - -Here are the templates that are rendered. - -```templ -package site - -import ( - "fmt" -) - -templ MouseMouseUI(id string, collection *MouseXYCollection) { -
-
My ID: {id}
- @cursorSVG( collection.Positions) -
-} - -templ cursorSVG( cursors map[string]MouseXY){ - - for id, cursor := range cursors { - {{ - x := fmt.Sprintf("%d", cursor.X) - y := fmt.Sprintf("%d", cursor.Y) - }} - - - {id} - - } - - - - -} -``` - -On page load we create the whole page and when there are updates the `cursorSVG` is run. This is writing to a KV store, so every update is getting marshalled to JSON for all cursors and then unmarshalled per viewer. Also, this is only happening when a user moves the mouse. In a real game/app you would send a steady stream of data but this is showing you can key of different events depending on your situation of SLA. diff --git a/code/go/site/static/md/examples/multiline_store.md b/code/go/site/static/md/examples/multiline_store.md deleted file mode 100644 index d9596b948..000000000 --- a/code/go/site/static/md/examples/multiline_store.md +++ /dev/null @@ -1,38 +0,0 @@ -## Multi-line Store - -## Demo - -
- - -
- -## Explanation - -```html -
- - -
-``` - -The `data-store` attribute is used to define a store object. The store object is a JS object. Before you could not have a multi-line store object. Now you can. diff --git a/code/go/site/static/md/examples/offline_sync.md b/code/go/site/static/md/examples/offline_sync.md deleted file mode 100644 index fd40b8fc2..000000000 --- a/code/go/site/static/md/examples/offline_sync.md +++ /dev/null @@ -1,27 +0,0 @@ -## Offline Sync - -## Demo - -
-
Go offline, then online to see the store sync
-
- -## Explanation - -```html -
-
-
-``` - -The `data-persist.local` attribute dumps the store to a `datastar` key in localStorage. A similar thing happens with `.session` but gets saved to sessionStorage. Any updates will be saved and reload on page refresh. To fully work this needs a service worker like [workbox](https://developers.google.com/web/tools/workbox/) to be installed. To test it go to the network tab of your browser's dev tools and toggle Online/Offline. diff --git a/code/go/site/static/md/examples/persist.md b/code/go/site/static/md/examples/persist.md deleted file mode 100644 index a1b4c4600..000000000 --- a/code/go/site/static/md/examples/persist.md +++ /dev/null @@ -1,22 +0,0 @@ - -## Demo - -
- -
Replace me
-
- -## Explanation - -```html -
- -
Replace me
-
-``` - -Look at your Local Storage in your browser's developer tools. - -In this example we are caching the `nested.test1` and `nested.test3` values in the Local Storage. - -If you don't use any values it will cache the entire store. diff --git a/code/go/site/static/md/examples/raf_update.md b/code/go/site/static/md/examples/raf_update.md deleted file mode 100644 index 3e08f04f3..000000000 --- a/code/go/site/static/md/examples/raf_update.md +++ /dev/null @@ -1,31 +0,0 @@ -## Request Animation Frame Update - -## Demo - -
-
Current Time: will be replaced by current time
-

-
- -## Explanation - -In the [Title Update Backend](/examples/title_update_backend) example we showed how to update the title of the page using a server sent event fragment. In this example we show how to update the title of the page using a requestAnimationFrame event on the client side. - -```html -
-
- Current Time: - will be replaced by current time -
-

-
-``` - -`data-on-raf` is a special event that is triggered on every requestAnimationFrame event. This is useful for updating the UI at maximum at the rendering refresh rate of the browser. In this example we update the currentTime store with a new Date object. This triggers a re-render of the currentTime span element. You can still use the `throttle` and `debounce` modifiers to control the rate of updates even further. - -In this case we are updating the currentTime store with the current time. This triggers a re-render of the `currentTime` span element, however if you inspect with the browser debugger you will notice that `#time`'s `` element is not updated every frame. This is because the signals are smartly updated only when the value changes. This is a performance optimization that is done by default. diff --git a/code/go/site/static/md/examples/replace_url_from_signals.md b/code/go/site/static/md/examples/replace_url_from_signals.md deleted file mode 100644 index 017b88219..000000000 --- a/code/go/site/static/md/examples/replace_url_from_signals.md +++ /dev/null @@ -1,23 +0,0 @@ -## Replace URL from Backend - -## Demo - -
-
- -## Explanation - -```html -
-
-``` - -The `data-replace-url` attribute is a special attribute that is used to replace the URL in the browser without reloading the page. This is useful for updating the URL when the user interacts with the page. In this example we update the URL with the current page number every second. This is done by incrementing the `page` store every second. The `data-on-raf.throttle_1s` event is triggered every second and increments the `page` store. This triggers a re-render of the `page` store and updates the URL in the browser. You can still use the `throttle` and `debounce` modifiers to control the rate of updates even further. \ No newline at end of file diff --git a/code/go/site/static/md/examples/session_storage.md b/code/go/site/static/md/examples/session_storage.md deleted file mode 100644 index 67da63016..000000000 --- a/code/go/site/static/md/examples/session_storage.md +++ /dev/null @@ -1,19 +0,0 @@ -## Session Storage - -## Demo - -
Look at your DevTools session storage!
- -## Explanation - -```html -
-``` - -A community user wanted the ability to store data in the session storage. This is now possible with the `data-persist.session` attribute. The data will be saved to the session storage and will be available even after a page refresh. The data will be removed when the session ends. - -You'll have to go into the browser's dev tools to see the data in the session storage. The data will be saved in the `datastar` key. In this example, the session storage will have the key `sessionId` be `1234` and the key `count` start at `0`. The `data-on-raf` attribute is used to increment the `count` key every time a requestAnimationFrame is called. diff --git a/code/go/site/static/md/examples/snake.md b/code/go/site/static/md/examples/snake.md deleted file mode 100644 index 4f175513d..000000000 --- a/code/go/site/static/md/examples/snake.md +++ /dev/null @@ -1,28 +0,0 @@ -## Snake Game - -## Demo - -
-
-
-
- - Can use buttons or WASD keys to control the snake -
-
- -## Explanation - -This is a port of a HTMX snake game [that showed up on Reddit](https://www.reddit.com/r/htmx/comments/1eqenc8/snake_game_demo_implemented_with_htmx_no_extra/). Since the original was in Go and I tried to keep the code as close to the original as possible. - -It is a live global game. Any player on this page will be able to control the snake. The snake will move in the direction of the last key pressed. Play with your friends! - -In the original they were talking about - -> It has a framerate of more than 10 on my computer - -I've run this version at 500+ fps but browsers can't keep up with updates that fast. I've set it to 60fps for now. diff --git a/code/go/site/static/md/examples/store_changed.md b/code/go/site/static/md/examples/store_changed.md deleted file mode 100644 index e319bf19d..000000000 --- a/code/go/site/static/md/examples/store_changed.md +++ /dev/null @@ -1,75 +0,0 @@ -## Store Changed - -## Demo - -
-
- - - -
-
Local Clicks:
-
-
- -## Explanation - -```html -
-
- - - -
-
Local Clicks:
-
-
-``` - -`data-on-store-change` is a special event that is triggered when the store changes. This is useful for updating the UI when the store changes. In this example we update the `clicks` store with a new value. This triggers a re-render of the `clicks` span element. You can still use the `throttle` and `debounce` modifiers to control the rate of updates even further. In this case we are sending the store changes to the server to update the lifetime total clicks the server has seen. - -**Note**: The `.remote` modifier is used to only trigger this event when remotely viewable signals are updated. This is useful for not sending data that is not needed to the server. To look at the details run `console.log(JSON.stringify(ds.store.value,null,2))` in the browser console. You should see something like - -```json -{ - "_sidebarOpen": false, - "clicks": 0, - "_localState": { - "bar": 1234 - }, - "_anotherLocalVar": "hello" -} -``` - -Whereas if you look at the Network tab in the browser you should see the following request payload - -```json -{ "clicks": 0 } -``` - -Any signal (or nested set of signals) starting with an underscore `_` is considered local and will not be sent to the server. In this example `_localState` and `_anotherLocalVar` are local only. diff --git a/code/go/site/static/md/examples/store_ifmissing.md b/code/go/site/static/md/examples/store_ifmissing.md deleted file mode 100644 index 15d03900c..000000000 --- a/code/go/site/static/md/examples/store_ifmissing.md +++ /dev/null @@ -1,57 +0,0 @@ -## Store If Missing - -## Demo - -
-
- Should always be 1234: - -
-
- -## Explanation - -```html -
-
- Should always be 1234: - should be replaced -
-
-``` - -The `data-store` attribute is used to set the initial state of the store. You can check your browser's Network Devtools to see the `updates` endpoint alternates between sending fragments and direct signal merges like the following: - -```md -... - -event: datastar-merge-signals -data: ifmissing true -data: store {id:73} - -event: datastar-merge-fragments -data: mergeMode upsertAttributes -data: fragments
- -event: datastar-merge-signals -data: ifmissing true -data: store {id:75} - -event: datastar-merge-fragments -data: mergeMode upsertAttributes -data: fragments
- -event: datastar-merge-signals -data: ifmissing true -data: store {id:77} - -... -``` - -Since the store is already set, the `data-store.ifmissing` attribute will not overwrite the existing value in either case. diff --git a/code/go/site/static/md/examples/update_store.md b/code/go/site/static/md/examples/update_store.md deleted file mode 100644 index 106f8871f..000000000 --- a/code/go/site/static/md/examples/update_store.md +++ /dev/null @@ -1,45 +0,0 @@ -## Update Store directly - -## Demo - -
- - - -
-          Stuff in store
-     
-
- -## Explanation - -This example demonstrates how to update the store directly from the frontend. This is using an SSE event - -```text/event-stream -event: datastar-merge-signals, -data: store { HYAAA4BK7IFQE: "2024-05-23T10:33:14.167189214-07:00", stuffAlreadyInStore: "this is already in the store"} -``` -It will accept anything that is also accepted by the `data-store` attribute. - - -The deletion event looks like -```text/event-stream -event: datastar-delete, -data: paths 12768 stuffAlreadyInStore -``` -Where the paths are `.` delimited paths within the store. For a nested store it might look like `foo.bar.baz`. Using the Go helpers for example this looks like `datastar.DeleteFromStore(sse, keysToDelete...)` diff --git a/code/go/site/static/md/examples/web_component.md b/code/go/site/static/md/examples/web_component.md deleted file mode 100644 index 4f23ef1d5..000000000 --- a/code/go/site/static/md/examples/web_component.md +++ /dev/null @@ -1,48 +0,0 @@ -## Web Component - -## Demo - -
- -
- -
- - -
- -### Explanation - -This is an example of two-way binding with a web component that reverses a string. Normally, the web component would output the reversed value, but in this example, all it does is perform the logic and dispatch an event containing the result, which is then displayed. - -```html -
- - - -
- -``` - -The `name` attribute value is bound to the `$name` store value, and an event listener modifies the `$reversed` store value sent in the `reverse` event. - -The web component observes changes to the `name` attribute and responds by reversing the string and dispatching a `reverse` event containing the resulting value. - -```js -class ReverseComponent extends HTMLElement { - static get observedAttributes() { - return ['name']; - } - - attributeChangedCallback(name, oldValue, newValue) { - const value = newValue.split('').reverse().join(''); - this.dispatchEvent(new CustomEvent('reverse', {detail: {value}})); - } -} - -customElements.define('reverse-component', ReverseComponent); -``` - diff --git a/code/go/site/static/md/guide/getting_started.md b/code/go/site/static/md/guide/getting_started.md deleted file mode 100644 index 29845ccec..000000000 --- a/code/go/site/static/md/guide/getting_started.md +++ /dev/null @@ -1,513 +0,0 @@ -# Getting Started - -Datastar brings the functionality provided by libraries like [Alpine.js](https://alpinejs.dev/) (frontend reactivity) and [htmx](https://htmx.org/) (backend reactivity) together, into one cohesive solution. It's a lightweight, extensible framework that allows you to: - -1. Manage state and build reactivity into your frontend using HTML attributes. -2. Modify the DOM and state by sending events from your backend. - -With Datastar, you can build any UI that a full-stack framework like React, Vue.js or Svelte can, but with a much simpler, hypermedia-driven approach. - -
- -
- We're so confident that Datastar can be used as a JavaScript framework replacement that we challenge anyone to find a use-case for a web app that Datastar cannot realistically be used to build! -
-
- -## Installation - -### Using a Script Tag - -The quickest way to use Datastar is to include it in your HTML using a script tag hosted on a CDN. - -```html - -``` - -If you prefer to host the file yourself, download your own bundle using the [bundler](/bundler), then include it from the appropriate path. - -```html - -``` - -### Using NPM - -You can alternatively install Datastar via [npm](https://www.npmjs.com/package/@starfederation/datastar). We don't recommend this for most use-cases, as it requires a build step, but it can be useful for legacy frontend projects. - -```bash -npm install @starfederation/datastar -``` - -## Data Attributes - -At the core of Datastar are [`data-*`](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) attributes (hence the name). They allow you to add reactivity to your frontend in a declarative way, and interact with your backend. - -Datastar uses signals to manage state. You can think of signals as reactive variables that automatically track and propagate changes from expressions. They can be created and modified using data attributes on the frontend, and using events sent from the backend. Don't worry if this sounds complicated; it will become clearer as we look at some examples. - -### `data-model` - -Datastar provides us with a way to set up two-way data binding on an element using the [`data-model`](/reference/plugins_attributes#model) attribute, which can be placed on any HTML element that users can directly input data or choices from (`input`, `textarea`, `select`, `checkbox` and `radio` elements). - -```html - -``` - -This creates a new signal called `input`, and binds it to the element's value. If either is changed, the other automatically updates. - -### `data-text` - -To see this in action, we can use the [`data-text`](/reference/plugins_attributes#text) attribute. - -```html -
- I will get replaced with the contents of the input signal -
-``` - -
-
-
-
Input:
- -
-
-
Output:
-
-
-
-
- -This sets the text content of an element to the value of the signal `$input`. The `$` is required to denote a signal in the expression. - -The value of the `data-text` attribute is an expression that is evaluated, meaning that we can use JavaScript in it. - -```html -
- Will be replaced with the uppercase contents of the input signal -
-``` - -
-
-
-
Input:
- -
-
-
Output:
-
-
-
-
- -### `data-computed-*` - -The `data-computed-*` attribute creates a new signal that is computed based on an expression. The computed signal is read-only, and its value is automatically updated when any signals in the expression are updated. - -```html -
- -
- Will be replaced with the contents of the repeated signal -
-
-``` - -
-
-
-
Input:
- -
-
-
Output:
-
-
-
-
- - -### `data-show` - -The `data-show` attribute can be used to show or hide an element based on whether a JavaScript expression evaluates to `true` or `false`. - -```html - -``` - -This results in the button being visible only when the input is _not_ empty. - -
-
-
-
Input:
- -
-
-
Output:
-
-
-
- -
- -### `data-class` - -The [`data-class`](/reference/plugins_attributes#class) attribute allows us to add or remove classes from an element using a set of key-value pairs that map to the class name and expression. - -```html - -``` - -Since the expression evaluates to `true` or `false`, we can rewrite this as `!$input`. - -```html - -``` - -
-
-
-
Input:
- -
-
-
Output:
-
-
-
- -
- -### `data-bind-*` - -The `data-bind-*` attribute can be used to bind a JavaScript expression to **any** valid HTML attribute. The becomes even more powerful when combined with [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components). - -```html - -``` - -This results in the button being given the `disabled` attribute whenever the input is empty. - -
-
-
-
Input:
- -
-
-
Output:
-
-
-
- -
- - -### `data-store` - -So far, we've created signals on the fly using `data-model` and `data-computed-*`. All signals are merged into a **store** that is accessible from anywhere in the DOM. - -We can merge signals into the store using the [`data-store`](/reference/plugins_core#store) attribute. - -```html -
-``` - -The `data-store` value must be written as a JavaScript object literal _or_ using JSON syntax. - -Adding `data-store` to multiple elements is allowed, and the signals provided will be _merged_ into the existing store (values defined later in the DOM tree override those defined earlier). - -Signals are nestable, which can be useful for namespacing. - -```html -
-``` - -### `data-on-*` - -The [`data-on-*`](/reference/plugins_attributes#on) attribute can be used to execute a JavaScript expression whenever an event is triggered on an element. - -```html - -``` - -This results in the `$input` signal being set to an empty string when the button element is clicked. If the `$input` signal is used elsewhere, its value will automatically update. This, like `data-bind` can be used with **any** valid event name (e.g. `data-on-keydown`, `data-on-mouseover`, etc.). - -
-
-
-
Input:
- -
-
-
Output:
-
-
-
- -
- -So what else can we do with these expressions? Anything we want, really. -See if you can follow the code below _before_ trying the demo. - -```html -
-
- What do you put in a toaster? -
- -
- You answered “”. - That is correct ✅ - - The correct answer is “ - - ” 🤷 - -
-
-``` - -
-
-
- What do you put in a toaster? -
-
- You answered “”. - That is correct ✅ - - The correct answer is “” 🤷 - -
-
- -
- -We've just scratched the surface of frontend reactivity. Now let's take a look at how we can bring the backend into play. - -## Backend Setup - -Datastar uses [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) or SSE. There's no special backend plumbing required to use SSE, just some special syntax. Fortunately, SSE is straightforward and [provides us with some advantages](/essays/event_streams_all_the_way_down). - -First, set up your backend in the language of your choice. Using one of the helper SDKs (available for Go, PHP and TypeScript) will help you get up and running faster. We're going to use the SDKs in the examples below, which set the appropriate headers and format the events for us, but this is optional. - -The following code would exist in a controller action endpoint in your backend. - -!!!CODE_SNIPPET:getting_started/setup!!! - -The `mergeFragments()` method merges the provided HTML fragment into the DOM, replacing the element with `id="question"`. An element with the ID `question` must already exist in the DOM. - -The `mergeSignals()` method merges the `response` and `answer` signals into the frontend store. - -With our backend in place, we can now use the `data-on-click` attribute to trigger the `$get()` action, which sends a `GET` request to the `/actions/quiz` endpoint on the server when a button is clicked. - -```html -
-
- - -
- You answered “”. - That is correct ✅ - - The correct answer is “” 🤷 - -
-
-``` - -Now when the `Fetch a question` button is clicked, the server will respond with an event to modify the `question` element in the DOM and an event to modify the `response` and `answer` signals. We're driving state from the backend! - -
-
-
-
- You answered “”. - That is correct ✅ - - The correct answer is “” 🤷 - -
- -
- -
- -### `data-indicator` - -The `data-indicator` attribute sets the value of the provided signal name to `true` while the request is in flight. We can use this signal to show a loading indicator, which may be desirable for slower responses. - -Note that elements using the `data-indicator` attribute ***must*** have a unique ID attribute. - -```html -
-
- -``` - -
-
-
-
- -
-
-
-
- -We're not limited to just `GET` requests. We can also send `GET`, `POST`, `PUT`, `PATCH` and `DELETE` requests, using the `$get()`, `$post()`, `$put()`, `$patch()` and `$delete()` actions, respectively. - -Here's how we could send an answer to the server for processing, using a `POST` request. - -```html - -``` - -One of the benefits of using SSE is that we can send multiple events (HTML fragments, signal updates, etc.) in a single response. - -!!!CODE_SNIPPET:getting_started/multiple_events!!! - -## Actions - -Actions in Datastar are helper functions that are available in `data-*` attributes and have the syntax `$actionName()`. We already saw the `$get` action above. Here are a few other common actions. - -### `$setAll()` - -The `$setAll()` action sets the values of multiple signals at once. It takes a regular expression that is used to match against signals, and a value to set them to, as arguments. - -```html - -``` - -This sets the values of all signals containing `form_` to `true`, which could be useful for enabling input fields in a form. - -```html - Checkbox 1 - Checkbox 2 - Checkbox 3 - -``` - -
-
- -
-
- -
-
- -
- -
- -### `$toggleAll()` - -The `$toggleAll()` action toggles the values of multiple signals at once. It takes a regular expression that is used to match against signals, as an argument. - -```html - -``` - -This toggles the values of all signals containing `form_` (to either `true` or `false`), which could be useful for toggling input fields in a form. - -```html - Checkbox 1 - Checkbox 2 - Checkbox 3 - -``` - -
-
- -
-
- -
-
- -
- -
- -## A Quick Overview - -Using [`data-*`](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) attributes, you can introduce reactive state to your frontend and access it anywhere in the DOM and in your backend. You can set up events that trigger requests to backend endpoints that respond with HTML fragment and signal updates. - -- Bind element values to signals: `data-model="foo"` -- Set the text content of an element to an expression.: `data-text="$foo"` -- Create a computed signal: `data-computed-foo="$bar + 1"` -- Show or hide an element using an expression: `data-show="$foo" -- Modify the classes on an element: `data-class="{'font-bold': $foo}"` -- Bind an expression to an HTML attribute: `data-bind-disabled="$foo == ''"` -- Merge signals into the store: `data-store="{foo: ''}"` -- Execute an expression on an event: `data-on-click="$get(/endpoint)"` -- Use signals to track in flight backend requests: `data-indicator="fetching"` -- Replace the URL: `data-replace-url="'/page1'"` -- Persist all signals in local storage: `data-persist` -- Create a reference to an element: `data-ref="alert"` -- Check for intersection with the viewport: `data-intersect="alert('visible')"` -- Scroll programmatically: `data-scroll-into-view` -- Interact with the [View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API): `data-transition="slide"` diff --git a/code/go/site/static/md/reference/expressions.md b/code/go/site/static/md/reference/expressions.md deleted file mode 100644 index ef6531f31..000000000 --- a/code/go/site/static/md/reference/expressions.md +++ /dev/null @@ -1,9 +0,0 @@ -# Expressions - -Expressions are the building blocks of Datastar. In fact, Datastar started as just a way to take `data-*` attributes and turn them into expressions. For exact `data-*` attribute found - -1. All `Preprocesser` plugins are run. This allows for a custom DSL. The included plugins use `$` for signals, `$` for actions, and `~` for refs. The plugins check for regular expressions and replace them with the appropriate value. Some plugins will setup extra state on load like adding CSS classes or setting up event listeners. -2. All Datastar `AttributePlugin` plugins are run in order. Most of the time these plugins are creating `effect()` signals so that that changes to the expression with automatically update the DOM and other parts of the system. -3. Check for any element removals and cancel any effects that are no longer needed. - -Each expression is evaluated in a new Function declaration and not in a call to `eval()`. This is done to prevent access to the global scope and to prevent access to the `Function` constructor. This is done to prevent XSS attacks. Its also why all expressions take a `ctx` which has access to the store, actions, and refs, but not the global scope. This was gleamed from how Alpine.js works but with a different reactive model. diff --git a/code/go/site/static/md/reference/plugins_attributes.md b/code/go/site/static/md/reference/plugins_attributes.md deleted file mode 100644 index fed502a3a..000000000 --- a/code/go/site/static/md/reference/plugins_attributes.md +++ /dev/null @@ -1,103 +0,0 @@ -# Attribute Plugins - -[Source](https://github.com/starfederation/datastar/blob/main/packages/library/src/lib/plugins/official/attributes.ts) - -Primarily used to help hook up the store and the DOM. - -## Attributes Plugins - -### Bind - -```html -
-``` - -Allows any valid attribute to be bound to an expression. This is useful for making elements reactive. Also, can be used as a fallback for any attribute that is not supported by a plugin currently. - -### Model - -```html - -``` - -Sets up two-way data-binding on an element. - -**Note:** Always binds to a signal and therefore should exclude the `$` prefix from the signal name. Event listeners are added for `change`, `input` and `keydown` events on `input`,`textarea`, `select`, `checkbox` and `radio` elements. - -### Text - -```html -
-``` - -Sets the text content of an element to the value of the signal. This is useful for setting the text content of an element to a signal value. Can use any expression that is valid in the system. For example, `data-text="$foo + 'bar'"` would set the text content to the value of `$foo` plus the string `bar`. - -### On - -```html - -``` - -Sets up an event listener on an element. The event listener will trigger the action specified in the expression. The expression can be any valid expression in the system. For example, `data-on-click="$fn('foo','bar',1234)"` would trigger the action `fn` with the arguments `'foo','bar',1234` when the button is clicked. - -If any signal in the expression changes, the event listener will be updated to reflect the new value of the signal automatically. - -An `evt` variable that represents the event object is available in the expression. - -```html -
-``` - -The `data-on-*` matches DOM events, however there are currently a few special cases for custom events. - -1. `data-on-load` which is triggered when the element is loaded into the DOM. -2. `data-on-store-change` which is triggered when the store changes. -3. `data-on-raf` which is triggered on every requestAnimationFrame event. - -#### Modifiers -- `.once`\* - Only trigger the event listener once -- `.passive`\* - Do not call `preventDefault` on the event listener -- `.capture`\* - Use a capture event listener -- `.debounce` - Debounce the event listener - - `_1000ms` - Debounce for 1000ms - - `_1s` - Debounce for 1s - - `_leading` - Debounce with leading edge - - `_noTrail` - Debounce without trailing edge -- `.throttle` - Throttle the event listener - - `_1000ms` - Throttle for 1000ms - - `_1s` - Throttle for 1s - - `_noLead` - Throttle without leading edge - - `_noTrail` - Throttle without trailing edge -- `.window` - Attaches the event listener to the `window` element - -\*only works currently on native events - -### Class - -```html -
-``` - -Adds or removes each of the keys in the set of key-value pairs to the element’s class list, depending on whether the values evaluate to true or false, respectively. - -### Persist - -```html -
-``` - -Persists store values in Local Storage. This is useful for storing values between page loads. - -```html -
-``` - -If one or more values are provided, only those store values will be persisted. - -#### Modifiers -- `.session` - Persists store values in Session Storage -- `.remote` - Persists only remotely viewable store values - -```html -
-``` diff --git a/code/go/site/static/md/reference/plugins_core.md b/code/go/site/static/md/reference/plugins_core.md deleted file mode 100644 index 3be50a2b4..000000000 --- a/code/go/site/static/md/reference/plugins_core.md +++ /dev/null @@ -1,55 +0,0 @@ -# Core Plugins - -[Source](https://github.com/starfederation/datastar/blob/main/packages/library/src/lib/plugins/official/core.ts) - -These are the only plugins that are required in order to have a working system. In the default build all (including core) plugins are included as they cover the most common use cases. - -## Attributes Plugins - -### Store - -```html -
-``` - -Takes the contents of the attribute and runs a BigInt aware JSON parse on it. It then merges the contents into the store. This can be used anywhere as the store is a global singleton. All keys are converted into signals, works with nested objects. - -
-
- Note that `value` and `peek` are reserved words (imposed by the signals library) and cannot be used as store names. -
-
- -#### Modifiers - -- `.ifmissing` - Only set the store if the key does not exist. This is useful for setting defaults without overwriting existing values. - -### Computed - -```html -
-``` - -Allows you to define a computed store value that automatically updates its value based on an expression. This can be used to drive other reactive behaviors, such as updating classes or text content in the DOM. - -### Ref - -```html -
-``` - -Makes an element available as a signal in the store. - -## Preprocessor Plugins - -### SignalProcessor - -Takes a `$var` and converts into a `ctx.store().var.value`. Since all expressions are evaluated within an effect it setups of a reactive system. - -### ActionProcessor - -Takes a `$fn('foo','bar',1234)` and converts into a `ctx.actions.fn('foo','bar',1234()`. This is used to trigger actions plugins. - -### RefProcessor - -Takes a `~foo` and converts into a `ctx.refs.foo`. This is used to access refs similar to how you would in a Vue or Svelte components. diff --git a/code/go/site/static/md/reference/plugins_helpers.md b/code/go/site/static/md/reference/plugins_helpers.md deleted file mode 100644 index 4dcc3428d..000000000 --- a/code/go/site/static/md/reference/plugins_helpers.md +++ /dev/null @@ -1,44 +0,0 @@ -# Action Plugins - -[Source](https://github.com/starfederation/datastar/blob/main/packages/library/src/lib/plugins/official/helpers.ts) - -## `$setAll(regexp: string, value: any)` - -```html -
-``` - -Sets all the signals that start with the prefix to the value of the second argument. This is useful for setting all the values of a form at once. - -## `$toggleAll(regexp: string)` - -```html -
-``` - -Toggles all the signals that start with the prefix. This is useful for toggling all the values of a form at once. - -## `$clipboard(text: string)` - -```html -
-``` - -Copies the text to the clipboard. This is useful for copying text to the clipboard. - -## `$fit(v: number, oldMin:number, oldMax:number, newMin, newMax)` - -Make a value linear interpolate from an original range to new one. - - -## `$fitInt(v: number, oldMin:number, oldMax:number, newMin, newMax)` - -Same as `$fit` but rounded to nearest integer - -## `$clampFit(v: number, oldMin:number, oldMax:number, newMin, newMax)` - -Same as `$fit` but clamped to `newMin` -> `newMax` range - -## `$clampFitInt(v: number, oldMin:number, oldMax:number, newMin, newMax)` - -Same as `$clampFit` but rounded to nearest integer diff --git a/code/php/sdk/src/ReadSignals.php b/code/php/sdk/src/ReadSignals.php deleted file mode 100644 index 9ff0249a8..000000000 --- a/code/php/sdk/src/ReadSignals.php +++ /dev/null @@ -1,19 +0,0 @@ -=14.17'} - hasBin: true - -snapshots: - - typescript@5.6.3: {} diff --git a/code/ts/library/src/bundles/datastar.ts b/code/ts/library/src/bundles/datastar.ts deleted file mode 100644 index 9dcd2ead6..000000000 --- a/code/ts/library/src/bundles/datastar.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Datastar } from "../engine"; -import { DeleteSSE } from "../plugins/official/actions/backend/sseDelete"; -import { GetSSE } from "../plugins/official/actions/backend/sseGet"; -import { PatchSSE } from "../plugins/official/actions/backend/ssePatch"; -import { PostSSE } from "../plugins/official/actions/backend/ssePost"; -import { PutSSE } from "../plugins/official/actions/backend/ssePut"; -import { Clipboard } from "../plugins/official/actions/dom/clipboard"; -import { SetAll } from "../plugins/official/actions/logic/setAll"; -import { ToggleAll } from "../plugins/official/actions/logic/toggleAll"; -import { ClampFit } from "../plugins/official/actions/math/clampFit"; -import { ClampFitInt } from "../plugins/official/actions/math/clampFitInt"; -import { Fit } from "../plugins/official/actions/math/fit"; -import { FitInt } from "../plugins/official/actions/math/fitInt"; -import { Indicator } from "../plugins/official/attributes/backend/indicator"; -import { Bind } from "../plugins/official/attributes/dom/bind"; -import { Class } from "../plugins/official/attributes/dom/class"; -import { Model } from "../plugins/official/attributes/dom/model"; -import { On } from "../plugins/official/attributes/dom/on"; -import { Ref } from "../plugins/official/attributes/dom/ref"; -import { Text } from "../plugins/official/attributes/dom/text"; -import { Persist } from "../plugins/official/attributes/storage/persist"; -import { ReplaceUrl } from "../plugins/official/attributes/url/replaceUrl"; -import { Intersection } from "../plugins/official/attributes/visibility/intersects"; -import { ScrollIntoView } from "../plugins/official/attributes/visibility/scrollIntoView"; -import { Show } from "../plugins/official/attributes/visibility/show"; -import { ViewTransition } from "../plugins/official/attributes/visibility/viewTransition"; -import { ExecuteScript } from "../plugins/official/watchers/backend/sseExecuteScript"; -import { MergeFragments } from "../plugins/official/watchers/backend/sseMergeFragment"; -import { MergeSignals } from "../plugins/official/watchers/backend/sseMergeSignals"; -import { RemoveFragments } from "../plugins/official/watchers/backend/sseRemoveFragments"; -import { RemoveSignals } from "../plugins/official/watchers/backend/sseRemoveSignals"; - -Datastar.load( - // attributes - Model, - Ref, - Indicator, - Bind, - ReplaceUrl, - Class, - On, - Text, - Persist, - Intersection, - ScrollIntoView, - Show, - ViewTransition, - // actions - DeleteSSE, - GetSSE, - PatchSSE, - PostSSE, - PutSSE, - Clipboard, - SetAll, - ToggleAll, - ClampFit, - ClampFitInt, - Fit, - FitInt, - // effects - MergeFragments, - MergeSignals, - RemoveFragments, - RemoveSignals, - ExecuteScript, -); diff --git a/code/ts/library/src/engine/engine.ts b/code/ts/library/src/engine/engine.ts deleted file mode 100644 index 132bb9337..000000000 --- a/code/ts/library/src/engine/engine.ts +++ /dev/null @@ -1,418 +0,0 @@ -import { consistentUniqID } from "../utils/dom"; -import { HTMLorSVGElement } from "../utils/types"; -import { DeepSignal, deepSignal, DeepState } from "../vendored/deepsignal"; -import { computed, effect, Signal, signal } from "../vendored/preact-core"; -import { apply } from "../vendored/ts-merge-patch"; - -import { - ERR_ALREADY_EXISTS, - ERR_BAD_ARGS, - ERR_METHOD_NOT_ALLOWED, - ERR_NOT_ALLOWED, - ERR_NOT_FOUND, -} from "./errors"; -import { - ActionPlugin, - ActionPlugins, - AttribtueExpressionFunction, - AttributeContext, - AttributePlugin, - DatastarPlugin, - InitContext, - OnRemovalFn, - PreprocessorPlugin, - Reactivity, - WatcherPlugin, -} from "./types"; -import { VERSION } from "./version"; - -const isPreprocessorPlugin = (p: DatastarPlugin): p is PreprocessorPlugin => - p.pluginType === "preprocessor"; -const isWatcherPlugin = (p: DatastarPlugin): p is WatcherPlugin => - p.pluginType === "watcher"; -const isAttributePlugin = (p: DatastarPlugin): p is AttributePlugin => - p.pluginType === "attribute"; -const isActionPlugin = (p: DatastarPlugin): p is ActionPlugin => - p.pluginType === "action"; - -export class Engine { - plugins: AttributePlugin[] = []; - store: DeepSignal = deepSignal({}); - preprocessors = new Array(); - actions: ActionPlugins = {}; - watchers = new Array(); - refs: Record = {}; - reactivity: Reactivity = { - signal, - computed, - effect, - }; - removals = new Map }>(); - mergeRemovals = new Array(); - - get version() { - return VERSION; - } - - load(...pluginsToLoad: DatastarPlugin[]) { - const allLoadedPlugins = new Set(this.plugins); - - pluginsToLoad.forEach((plugin) => { - if (plugin.requiredPlugins) { - for ( - const requiredPluginType of plugin - .requiredPlugins - ) { - if ( - !allLoadedPlugins.has(requiredPluginType) - ) { - // requires other plugin to be loaded - throw ERR_NOT_ALLOWED; - } - } - } - - let globalInitializer: ((ctx: InitContext) => void) | undefined; - if (isPreprocessorPlugin(plugin)) { - if (this.preprocessors.includes(plugin)) { - throw ERR_ALREADY_EXISTS; - } - this.preprocessors.push(plugin); - } else if (isWatcherPlugin(plugin)) { - if (this.watchers.includes(plugin)) { - throw ERR_ALREADY_EXISTS; - } - this.watchers.push(plugin); - globalInitializer = plugin.onGlobalInit; - } else if (isActionPlugin(plugin)) { - if (!!this.actions[plugin.name]) { - throw ERR_ALREADY_EXISTS; - } - this.actions[plugin.name] = plugin; - } else if (isAttributePlugin(plugin)) { - if (this.plugins.includes(plugin)) { - throw ERR_ALREADY_EXISTS; - } - this.plugins.push(plugin); - globalInitializer = plugin.onGlobalInit; - } else { - throw ERR_NOT_FOUND; - } - - if (globalInitializer) { - globalInitializer({ - store: () => this.store, - upsertSignal: this.upsertSignal - .bind(this), - mergeSignals: this.mergeSignals.bind(this), - removeSignals: this.removeSignals.bind(this), - actions: this.actions, - reactivity: this.reactivity, - applyPlugins: this.applyPlugins.bind(this), - cleanup: this.cleanup.bind( - this, - ), - }); - } - - allLoadedPlugins.add(plugin); - }); - - this.applyPlugins(document.body); - } - - private cleanup(element: Element) { - const removalSet = this.removals.get(element); - if (removalSet) { - for (const removal of removalSet.set) { - removal(); - } - this.removals.delete(element); - } - } - - lastMarshalledStore = ""; - private mergeSignals(mergeSignals: T) { - this.mergeRemovals.forEach((removal) => removal()); - this.mergeRemovals = this.mergeRemovals.slice(0); - - const revisedStore = apply(this.store.value, mergeSignals) as DeepState; - this.store = deepSignal(revisedStore); - - const marshalledStore = JSON.stringify(this.store.value); - if (marshalledStore === this.lastMarshalledStore) return; - } - - private removeSignals(...keys: string[]) { - const revisedStore = { ...this.store.value }; - let found = false; - for (const key of keys) { - const parts = key.split("."); - let currentID = parts[0]; - let subStore = revisedStore; - for (let i = 1; i < parts.length; i++) { - const part = parts[i]; - if (!subStore[currentID]) { - subStore[currentID] = {}; - } - subStore = subStore[currentID]; - currentID = part; - } - delete subStore[currentID]; - found = true; - } - if (!found) return; - this.store = deepSignal(revisedStore); - this.applyPlugins(document.body); - } - - private upsertSignal(path: string, value: T) { - const parts = path.split("."); - let subStore = this.store as any; - for (let i = 0; i < parts.length - 1; i++) { - const part = parts[i]; - if (!subStore[part]) { - subStore[part] = {}; - } - subStore = subStore[part]; - } - const last = parts[parts.length - 1]; - - const current = subStore[last]; - if (!!current) return current; - - const signal = this.reactivity.signal(value); - subStore[last] = signal; - - return signal; - } - - private applyPlugins(rootElement: Element) { - const appliedProcessors = new Set(); - - this.plugins.forEach((p, pi) => { - this.walkDownDOM(rootElement, (el) => { - if (!pi) this.cleanup(el); - - for (const rawKey in el.dataset) { - const rawExpression = `${el.dataset[rawKey]}` || ""; - let expression = rawExpression; - - if (!rawKey.startsWith(p.name)) continue; - - if (!el.id.length) { - el.id = consistentUniqID(el); - } - - appliedProcessors.clear(); - - if (p.allowedTagRegexps) { - const lowerCaseTag = el.tagName.toLowerCase(); - const allowed = [...p.allowedTagRegexps].some((r) => - lowerCaseTag.match(r) - ); - if (!allowed) { - throw ERR_NOT_ALLOWED; - } - } - - let keyRaw = rawKey.slice(p.name.length); - let [key, ...modifiersWithArgsArr] = keyRaw.split("."); - if (p.mustHaveEmptyKey && key.length > 0) { - // must have empty key - throw ERR_BAD_ARGS; - } - if (p.mustNotEmptyKey && key.length === 0) { - // must have non-empty key - throw ERR_BAD_ARGS; - } - if (key.length) { - key = key[0].toLowerCase() + key.slice(1); - } - - const modifiersArr = modifiersWithArgsArr.map((m) => { - const [label, ...args] = m.split("_"); - return { label, args }; - }); - if (p.allowedModifiers) { - for (const modifier of modifiersArr) { - if (!p.allowedModifiers.has(modifier.label)) { - // modifier not allowed - throw ERR_NOT_ALLOWED; - } - } - } - const modifiers = new Map(); - for (const modifier of modifiersArr) { - modifiers.set(modifier.label, modifier.args); - } - - if (p.mustHaveEmptyExpression && expression.length) { - // must have empty expression - throw ERR_BAD_ARGS; - } - if (p.mustNotEmptyExpression && !expression.length) { - // must have non-empty expression - throw ERR_BAD_ARGS; - } - - const splitRegex = /;|\n/; - - if (p.removeNewLines) { - expression = expression - .split("\n") - .map((p: string) => p.trim()) - .join(" "); - } - - const processors = [ - ...(p.preprocessors?.pre || []), - ...this.preprocessors, - ...(p.preprocessors?.post || []), - ]; - for (const processor of processors) { - if (appliedProcessors.has(processor)) continue; - appliedProcessors.add(processor); - - const expressionParts = expression.split(splitRegex); - const revisedParts: string[] = []; - - expressionParts.forEach((exp) => { - let revised = exp; - const matches = [ - ...revised.matchAll(processor.regexp), - ]; - if (matches.length) { - for (const match of matches) { - if (!match.groups) continue; - const { groups } = match; - const { whole } = groups; - revised = revised.replace( - whole, - processor.replacer(groups), - ); - } - } - revisedParts.push(revised); - }); - // }) - - expression = revisedParts.join("; "); - } - - const ctx: AttributeContext = { - store: () => this.store, - mergeSignals: this.mergeSignals.bind(this), - upsertSignal: this.upsertSignal - .bind(this), - removeSignals: this.removeSignals.bind(this), - applyPlugins: this.applyPlugins.bind(this), - cleanup: this.cleanup - .bind(this), - walkSignals: this.walkSignals.bind(this), - actions: this.actions, - reactivity: this.reactivity, - el, - rawKey, - key, - rawExpression, - expression, - expressionFn: () => { - throw ERR_METHOD_NOT_ALLOWED; - }, - modifiers, - }; - - if ( - !p.bypassExpressionFunctionCreation?.(ctx) && - !p.mustHaveEmptyExpression && expression.length - ) { - const statements = expression - .split(splitRegex) - .map((s) => s.trim()) - .filter((s) => s.length); - statements[statements.length - 1] = `return ${ - statements[statements.length - 1] - }`; - const j = statements.map((s) => ` ${s}`).join(";\n"); - const fnContent = - `try{${j}}catch(e){console.error(\`Error evaluating Datastar expression:\n${ - j.replaceAll("`", "\\`") - }\n\nError: \${e.message}\n\nCheck if the expression is valid before raising an issue.\`.trim());debugger}`; - try { - const argumentNames = p.argumentNames || []; - const fn = new Function( - "ctx", - ...argumentNames, - fnContent, - ) as AttribtueExpressionFunction; - ctx.expressionFn = fn; - } catch (e) { - const err = new Error(`${e}\nwith\n${fnContent}`); - console.error(err); - debugger; - } - } - - const removal = p.onLoad(ctx); - if (removal) { - if (!this.removals.has(el)) { - this.removals.set(el, { - id: el.id, - set: new Set(), - }); - } - this.removals.get(el)!.set.add(removal); - } - } - }); - }); - } - - private walkSignalsStore( - store: any, - callback: (name: string, signal: Signal) => void, - ) { - const keys = Object.keys(store); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = store[key]; - const isSignal = value instanceof Signal; - const hasChildren = typeof value === "object" && - Object.keys(value).length > 0; - - if (isSignal) { - callback(key, value); - continue; - } - - if (!hasChildren) continue; - - this.walkSignalsStore(value, callback); - } - } - - private walkSignals(callback: (name: string, signal: Signal) => void) { - this.walkSignalsStore(this.store, callback); - } - - private walkDownDOM( - element: Element | null, - callback: (el: HTMLorSVGElement) => void, - siblingOffset = 0, - ) { - if ( - !element || - !(element instanceof HTMLElement || element instanceof SVGElement) - ) return null; - - callback(element); - - siblingOffset = 0; - element = element.firstElementChild; - while (element) { - this.walkDownDOM(element, callback, siblingOffset++); - element = element.nextElementSibling; - } - } -} diff --git a/code/ts/library/src/engine/errors.ts b/code/ts/library/src/engine/errors.ts deleted file mode 100644 index 2c1ada71b..000000000 --- a/code/ts/library/src/engine/errors.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { DATASTAR } from "./consts"; - -const err = (code: number) => { - const e = new Error(); - e.name = `${DATASTAR}${code}`; - return e; -}; - -export const ERR_BAD_ARGS = err(400); -export const ERR_ALREADY_EXISTS = err(409); -export const ERR_NOT_FOUND = err(404); -export const ERR_NOT_ALLOWED = err(403); -export const ERR_METHOD_NOT_ALLOWED = err(405); -export const ERR_SERVICE_UNAVAILABLE = err(503); diff --git a/code/ts/library/src/engine/index.ts b/code/ts/library/src/engine/index.ts deleted file mode 100644 index 0463f9ef2..000000000 --- a/code/ts/library/src/engine/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Star } from "../plugins/official/attributes/core/star"; -import { Computed } from "../plugins/official/attributes/core/computed"; -import { Store } from "../plugins/official/attributes/core/store"; -import { ActionsProcessor } from "../plugins/official/preprocessors/core/actions"; -import { SignalsProcessor } from "../plugins/official/preprocessors/core/signals"; -import { Engine } from "./engine"; - -export { VERSION } from "./consts"; - -export type * from "./types"; - -const ds = new Engine(); -ds.load( - ActionsProcessor, - SignalsProcessor, - Store, - Computed, - Star, -); - -export const Datastar = ds; diff --git a/code/ts/library/src/engine/types.ts b/code/ts/library/src/engine/types.ts deleted file mode 100644 index 5b1edeaaa..000000000 --- a/code/ts/library/src/engine/types.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { HTMLorSVGElement } from "../utils/types"; -import { DeepState } from "../vendored/deepsignal"; -import { ReadonlySignal, Signal } from "../vendored/preact-core"; - -export type InitExpressionFunction = ( - ctx: InitContext, - ...args: any -) => any; -export type AttribtueExpressionFunction = ( - ctx: AttributeContext, - ...args: any -) => any; -export type Reactivity = { - signal: (value: T) => Signal; - computed: (fn: () => T) => ReadonlySignal; - effect: (cb: () => void) => OnRemovalFn; -}; - -export type InitContext = { - store: () => any; - upsertSignal: (path: string, value: any) => Signal; - mergeSignals: (store: DeepState) => void; - removeSignals: (...paths: string[]) => void; - actions: Readonly; - reactivity: Reactivity; - applyPlugins: (target: Element) => void; - cleanup: (el: Element) => void; -}; - -export type AttributeContext = InitContext & { - walkSignals: (cb: (name: string, signal: Signal) => void) => void; - el: Readonly; // The element the attribute is on - key: Readonly; // data-* key without the prefix or modifiers - rawKey: Readonly; // raw data-* key - rawExpression: Readonly; // before any preprocessor run, what the user wrote - expression: Readonly; // what the user wrote after any preprocessor run - expressionFn: AttribtueExpressionFunction; // the function constructed from the expression - modifiers: Map; // the modifiers and their arguments -}; - -export type OnRemovalFn = () => void; - -export interface DatastarPlugin { - pluginType: "preprocessor" | "attribute" | "watcher" | "action"; // The type of plugin - name: string; // The name of the plugin - requiredPlugins?: Set; // If not provided, no plugins are required -} - -// A plugin accesible via a `data-${name}` attribute on an element -export interface AttributePlugin extends DatastarPlugin { - pluginType: "attribute"; - onGlobalInit?: (ctx: InitContext) => void; // Called once on registration of the plugin - onLoad: (ctx: AttributeContext) => OnRemovalFn | void; // Return a function to be called on removal - allowedModifiers?: Set; // If not provided, all modifiers are allowed - mustHaveEmptyExpression?: boolean; // The contents of the data-* attribute must be empty - mustNotEmptyExpression?: boolean; // The contents of the data-* attribute must not be empty - mustHaveEmptyKey?: boolean; // The key of the data-* attribute must be empty after the prefix - mustNotEmptyKey?: boolean; // The key of the data-* attribute must not be empty after the prefix - allowedTagRegexps?: Set; // If not provided, all tags are allowed - disallowedTags?: Set; // If not provided, no tags are disallowed - preprocessors?: { - pre?: PreprocessorPlugin[]; - post?: PreprocessorPlugin[]; - }; - removeNewLines?: boolean; // If true, the expression is not split by commas - bypassExpressionFunctionCreation?: (ctx: AttributeContext) => boolean; // If true, the expression function is not created - argumentNames?: Readonly; // The names of the arguments passed to the expression function -} - -export type RegexpGroups = Record; - -// A plugin that runs on the global scope that can effect the contents of a Datastar expression -export interface PreprocessorPlugin extends DatastarPlugin { - pluginType: "preprocessor"; - regexp: RegExp; - replacer: (groups: RegexpGroups) => string; -} - -export type PreprocessorPlugins = Record; - -export type ActionMethod = (ctx: AttributeContext, ...args: any[]) => any; - -export interface ActionPlugin extends DatastarPlugin { - pluginType: "action"; - method: ActionMethod; -} - -export type ActionPlugins = Record; - -// A plugin that runs on the global scope of the DastaStar instance -export interface WatcherPlugin extends DatastarPlugin { - pluginType: "watcher"; - onGlobalInit?: (ctx: InitContext) => void; -} diff --git a/code/ts/library/src/engine/version.ts b/code/ts/library/src/engine/version.ts deleted file mode 100644 index 3eb3d2fd8..000000000 --- a/code/ts/library/src/engine/version.ts +++ /dev/null @@ -1 +0,0 @@ -export const VERSION = '0.20.1'; diff --git a/code/ts/library/src/index.ts b/code/ts/library/src/index.ts deleted file mode 100644 index 4a7e4ee42..000000000 --- a/code/ts/library/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM -export * from "./engine"; -export * from "./plugins"; -export * from "./utils"; -export * from "./vendored"; diff --git a/code/ts/library/src/plugins/index.ts b/code/ts/library/src/plugins/index.ts deleted file mode 100644 index 97fff8130..000000000 --- a/code/ts/library/src/plugins/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./official"; diff --git a/code/ts/library/src/plugins/official/actions/backend/index.ts b/code/ts/library/src/plugins/official/actions/backend/index.ts deleted file mode 100644 index 2108172bf..000000000 --- a/code/ts/library/src/plugins/official/actions/backend/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./sseDelete"; -export * from "./sseGet"; -export * from "./ssePatch"; -export * from "./ssePost"; -export * from "./ssePut"; -export * from "./sseShared"; diff --git a/code/ts/library/src/plugins/official/actions/backend/sseDelete.ts b/code/ts/library/src/plugins/official/actions/backend/sseDelete.ts deleted file mode 100644 index e052998d4..000000000 --- a/code/ts/library/src/plugins/official/actions/backend/sseDelete.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:delete -// Slug: Use a DELETE request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface -// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client. - -import { ActionPlugin } from "../../../../engine"; -import { sendSSERequest } from "./sseShared"; - -export const DeleteSSE: ActionPlugin = { - pluginType: "action", - name: "delete", - method: sendSSERequest("delete"), -}; diff --git a/code/ts/library/src/plugins/official/actions/backend/sseGet.ts b/code/ts/library/src/plugins/official/actions/backend/sseGet.ts deleted file mode 100644 index d488c5dcc..000000000 --- a/code/ts/library/src/plugins/official/actions/backend/sseGet.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: ic:baseline-get-app -// Slug: Use a GET request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface -// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client. - -import { ActionPlugin } from "../../../../engine"; -import { sendSSERequest } from "./sseShared"; - -export const GetSSE: ActionPlugin = { - pluginType: "action", - name: "get", - method: sendSSERequest("get"), -}; diff --git a/code/ts/library/src/plugins/official/actions/backend/ssePatch.ts b/code/ts/library/src/plugins/official/actions/backend/ssePatch.ts deleted file mode 100644 index 75f475284..000000000 --- a/code/ts/library/src/plugins/official/actions/backend/ssePatch.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: mdi:bandage -// Slug: Use a PATCH request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface -// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client. - -import { ActionPlugin } from "../../../../engine"; -import { sendSSERequest } from "./sseShared"; - -export const PatchSSE: ActionPlugin = { - pluginType: "action", - name: "patch", - method: sendSSERequest("patch"), -}; diff --git a/code/ts/library/src/plugins/official/actions/backend/ssePost.ts b/code/ts/library/src/plugins/official/actions/backend/ssePost.ts deleted file mode 100644 index fc408e431..000000000 --- a/code/ts/library/src/plugins/official/actions/backend/ssePost.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:add -// Slug: Use a POST request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface -// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client. - -import { ActionPlugin } from "../../../../engine"; -import { sendSSERequest } from "./sseShared"; - -export const PostSSE: ActionPlugin = { - pluginType: "action", - name: "post", - method: sendSSERequest("post"), -}; diff --git a/code/ts/library/src/plugins/official/actions/backend/ssePut.ts b/code/ts/library/src/plugins/official/actions/backend/ssePut.ts deleted file mode 100644 index d6ff71470..000000000 --- a/code/ts/library/src/plugins/official/actions/backend/ssePut.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:find-replace -// Slug: Use a PUT request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface -// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client. - -import { ActionPlugin } from "../../../../engine"; -import { sendSSERequest } from "./sseShared"; - -export const PutSSE: ActionPlugin = { - pluginType: "action", - name: "put", - method: sendSSERequest("put"), -}; diff --git a/code/ts/library/src/plugins/official/actions/backend/sseShared.ts b/code/ts/library/src/plugins/official/actions/backend/sseShared.ts deleted file mode 100644 index 894255e3e..000000000 --- a/code/ts/library/src/plugins/official/actions/backend/sseShared.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { ActionMethod } from "../../../../engine"; -import { DATASTAR, DATASTAR_REQUEST } from "../../../../engine/consts"; -import { ERR_BAD_ARGS } from "../../../../engine/errors"; -import { remoteSignals } from "../../../../utils/signals"; -import { - fetchEventSource, - FetchEventSourceInit, -} from "../../../../vendored/fetch-event-source"; -import { - DATASTAR_SSE_EVENT, - DatastarSSEEvent, - FINISHED, - STARTED, -} from "../../watchers/backend/sseShared"; - -export type IndicatorReference = { el: HTMLElement; count: number }; - -const isWrongContent = (err: any) => `${err}`.includes(`text/event-stream`); - -export type SSERequestArgs = { - onlyRemoteSignals?: boolean; - headers?: Record; -}; - -function dispatchSSE(type: string, argsRaw: Record) { - document.dispatchEvent( - new CustomEvent(DATASTAR_SSE_EVENT, { - detail: { type, argsRaw }, - }), - ); -} - -export function sendSSERequest( - method: string, -): ActionMethod { - return async ( - ctx, - url, - args?: SSERequestArgs, - ) => { - if (!!!url?.length) throw ERR_BAD_ARGS; - - const onlyRemoteSignals = args?.onlyRemoteSignals ?? true; - const headers = Object.assign({ - "Content-Type": "application/json", - [DATASTAR_REQUEST]: true, - }, args?.headers); - const currentStore = ctx.store().value; - let store = Object.assign({}, currentStore); - if (onlyRemoteSignals) { - store = remoteSignals(store); - } - const storeJSON = JSON.stringify(store); - - const { el: { id: elID } } = ctx; - dispatchSSE(STARTED, { elID }); - - const urlInstance = new URL(url, window.location.origin); - - // https://fetch.spec.whatwg.org/#concept-method-normalize - method = method.toUpperCase(); - - const req: FetchEventSourceInit = { - method, - headers, - onmessage: (evt) => { - if (!evt.event.startsWith(DATASTAR)) { - return; - } - const type = evt.event; - const argsRawLines: Record = {}; - - const lines = evt.data.split("\n"); - for (const line of lines) { - const colonIndex = line.indexOf(" "); - const key = line.slice(0, colonIndex); - let argLines = argsRawLines[key]; - if (!argLines) { - argLines = []; - argsRawLines[key] = argLines; - } - const value = line.slice(colonIndex + 1).trim(); - argLines.push(value); - } - - const argsRaw: Record = {}; - for (const [key, lines] of Object.entries(argsRawLines)) { - argsRaw[key] = lines.join("\n"); - } - - // if you aren't seeing your event you can debug by using this line in the console - // document.addEventListener("datastar-sse",(e) => console.log(e)); - dispatchSSE(type, argsRaw); - }, - onerror: (err) => { - if (isWrongContent(err)) { - // don't retry if the content-type is wrong - throw err; - } - // do nothing and it will retry - if (err) { - console.error(err.message); - } - }, - onclose: () => { - dispatchSSE(FINISHED, { elID }); - }, - }; - - if (method === "GET") { - const queryParams = new URLSearchParams(urlInstance.search); - queryParams.append(DATASTAR, storeJSON); - urlInstance.search = queryParams.toString(); - } else { - req.body = storeJSON; - } - - try { - const revisedURL = urlInstance.toString(); - await fetchEventSource(revisedURL, req); - } catch (err) { - if (!isWrongContent(err)) { - throw err; - } - - // exit gracefully and do nothing if the content-type is wrong - // this can happen if the client is sending a request - // where no response is expected, and they haven't - // set the content-type to text/event-stream - } - }; -} diff --git a/code/ts/library/src/plugins/official/actions/dom/index.ts b/code/ts/library/src/plugins/official/actions/dom/index.ts deleted file mode 100644 index 59e4e3cac..000000000 --- a/code/ts/library/src/plugins/official/actions/dom/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./clipboard"; diff --git a/code/ts/library/src/plugins/official/actions/index.ts b/code/ts/library/src/plugins/official/actions/index.ts deleted file mode 100644 index a66102467..000000000 --- a/code/ts/library/src/plugins/official/actions/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./backend"; -export * from "./dom"; -export * from "./logic"; -export * from "./math"; diff --git a/code/ts/library/src/plugins/official/actions/logic/index.ts b/code/ts/library/src/plugins/official/actions/logic/index.ts deleted file mode 100644 index 8df31da7d..000000000 --- a/code/ts/library/src/plugins/official/actions/logic/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./setAll"; -export * from "./toggleAll"; diff --git a/code/ts/library/src/plugins/official/actions/math/clampFit.ts b/code/ts/library/src/plugins/official/actions/math/clampFit.ts deleted file mode 100644 index 969781fd8..000000000 --- a/code/ts/library/src/plugins/official/actions/math/clampFit.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:fit-screen-outline -// Slug: Clamp a value to a new range -// Description: This action clamps a value to a new range. The value is first scaled to the new range, then clamped to the new range. This is useful for scaling a value to a new range, then clamping it to that range. - -import { ActionPlugin, AttributeContext } from "../../../../engine"; - -export const ClampFit: ActionPlugin = { - pluginType: "action", - name: "clampFit", - method: ( - _: AttributeContext, - v: number, - oldMin: number, - oldMax: number, - newMin: number, - newMax: number, - ) => { - return Math.max( - newMin, - Math.min( - newMax, - ((v - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin, - ), - ); - }, -}; diff --git a/code/ts/library/src/plugins/official/actions/math/clampFitInt.ts b/code/ts/library/src/plugins/official/actions/math/clampFitInt.ts deleted file mode 100644 index aadc8e9b1..000000000 --- a/code/ts/library/src/plugins/official/actions/math/clampFitInt.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:fit-screen -// Slug: Clamp a value to a new range and round to the nearest integer -// Description: This action clamps a value to a new range. The value is first scaled to the new range, then clamped to the new range. This is useful for scaling a value to a new range, then clamping it to that range. The result is then rounded to the nearest integer. - -import { ActionPlugin, AttributeContext } from "../../../../engine"; - -export const ClampFitInt: ActionPlugin = { - pluginType: "action", - name: "clampFitInt", - method: ( - _: AttributeContext, - v: number, - oldMin: number, - oldMax: number, - newMin: number, - newMax: number, - ) => { - return Math.round( - Math.max( - newMin, - Math.min( - newMax, - ((v - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + - newMin, - ), - ), - ); - }, -}; diff --git a/code/ts/library/src/plugins/official/actions/math/fit.ts b/code/ts/library/src/plugins/official/actions/math/fit.ts deleted file mode 100644 index ba4fd0378..000000000 --- a/code/ts/library/src/plugins/official/actions/math/fit.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols-light:fit-width -// Slug: Linearly fit a value to a new range -// Description: This action linearly fits a value to a new range. The value is first scaled to the new range. Note it is not clamped to the new range. - -import { ActionPlugin, AttributeContext } from "../../../../engine"; - -export const Fit: ActionPlugin = { - pluginType: "action", - name: "fit", - method: ( - _: AttributeContext, - v: number, - oldMin: number, - oldMax: number, - newMin: number, - newMax: number, - ) => { - return ((v - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin; - }, -}; diff --git a/code/ts/library/src/plugins/official/actions/math/fitInt.ts b/code/ts/library/src/plugins/official/actions/math/fitInt.ts deleted file mode 100644 index 39fdc7cf5..000000000 --- a/code/ts/library/src/plugins/official/actions/math/fitInt.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:fit-width -// Slug: Linearly fit a value to a new range and round to the nearest integer -// Description: This action linearly fits a value to a new range. The value is first scaled to the new range. Note it is not clamped to the new range. - -import { ActionPlugin, AttributeContext } from "../../../../engine"; - -export const FitInt: ActionPlugin = { - pluginType: "action", - name: "fitInt", - method: ( - _: AttributeContext, - v: number, - oldMin: number, - oldMax: number, - newMin: number, - newMax: number, - ) => { - return Math.round( - ((v - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin, - ); - }, -}; diff --git a/code/ts/library/src/plugins/official/actions/math/index.ts b/code/ts/library/src/plugins/official/actions/math/index.ts deleted file mode 100644 index 15e5b1202..000000000 --- a/code/ts/library/src/plugins/official/actions/math/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./clampFit"; -export * from "./clampFitInt"; -export * from "./fit"; -export * from "./fitInt"; diff --git a/code/ts/library/src/plugins/official/attributes/backend/index.ts b/code/ts/library/src/plugins/official/attributes/backend/index.ts deleted file mode 100644 index d9c4a161d..000000000 --- a/code/ts/library/src/plugins/official/attributes/backend/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./indicator"; diff --git a/code/ts/library/src/plugins/official/attributes/core/computed.ts b/code/ts/library/src/plugins/official/attributes/core/computed.ts deleted file mode 100644 index ff2f58e67..000000000 --- a/code/ts/library/src/plugins/official/attributes/core/computed.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: fluent:draw-text-24-filled -// Slug: Create a computed signal -// Description: This attribute creates a computed signal that updates when its dependencies change. - -import { AttributePlugin } from "../../../../engine"; - -export const Computed: AttributePlugin = { - pluginType: "attribute", - name: "computed", - mustNotEmptyKey: true, - onLoad: (ctx) => { - const store = ctx.store(); - store[ctx.key] = ctx.reactivity.computed(() => { - return ctx.expressionFn(ctx); - }); - - return () => { - const store = ctx.store(); - delete store[ctx.key]; - }; - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/core/index.ts b/code/ts/library/src/plugins/official/attributes/core/index.ts deleted file mode 100644 index 613f3914f..000000000 --- a/code/ts/library/src/plugins/official/attributes/core/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./computed"; -export * from "./star"; -export * from "./store"; diff --git a/code/ts/library/src/plugins/official/attributes/core/star.ts b/code/ts/library/src/plugins/official/attributes/core/star.ts deleted file mode 100644 index 035bcb18c..000000000 --- a/code/ts/library/src/plugins/official/attributes/core/star.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:rocket -// Slug: Star -// Description: Sage advice for the weary traveler - -import { AttributePlugin } from "../../../../engine"; - -export const Star: AttributePlugin = { - pluginType: "attribute", - name: "star", - onLoad: () => { - alert("YOU ARE PROBABLY OVERCOMPLICATING IT"); - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/core/store.ts b/code/ts/library/src/plugins/official/attributes/core/store.ts deleted file mode 100644 index 5244ef09f..000000000 --- a/code/ts/library/src/plugins/official/attributes/core/store.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:home-storage -// Slug: Store signals into a singleton per page -// Description: This action stores signals into a singleton per page. This is useful for storing signals that are used across multiple components. - -import { - AttributeContext, - AttributePlugin, - RegexpGroups, -} from "../../../../engine"; -import { storeFromPossibleContents } from "../../../../utils/signals"; - -// Setup the global store -export const Store: AttributePlugin = { - pluginType: "attribute", - name: "store", - removeNewLines: true, - preprocessors: { - pre: [ - { - pluginType: "preprocessor", - name: "store", - regexp: /(?.+)/g, - replacer: (groups: RegexpGroups) => { - const { whole } = groups; - return `Object.assign({...ctx.store()}, ${whole})`; - }, - }, - ], - }, - allowedModifiers: new Set(["ifmissing"]), - onLoad: (ctx: AttributeContext) => { - const possibleMergeSignals = ctx.expressionFn(ctx); - const actualMergeSignals = storeFromPossibleContents( - ctx.store(), - possibleMergeSignals, - ctx.modifiers.has("ifmissing"), - ); - ctx.mergeSignals(actualMergeSignals); - - delete ctx.el.dataset[ctx.rawKey]; - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/dom/bind.ts b/code/ts/library/src/plugins/official/attributes/dom/bind.ts deleted file mode 100644 index 5858e41a1..000000000 --- a/code/ts/library/src/plugins/official/attributes/dom/bind.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: akar-icons:link-chain -// Slug: Bind attributes to expressions -// Description: Any attribute can be bound to an expression. The attribute will be updated reactively whenever the expression signal changes. - -import { AttributePlugin } from "../../../../engine"; -import { kebabize } from "../../../../utils/text"; - -export const Bind: AttributePlugin = { - pluginType: "attribute", - name: "bind", - mustNotEmptyKey: true, - mustNotEmptyExpression: true, - - onLoad: (ctx) => { - return ctx.reactivity.effect(async () => { - const key = kebabize(ctx.key); - const value = ctx.expressionFn(ctx); - let v: string; - if (typeof value === "string") { - v = value; - } else { - v = JSON.stringify(value); - } - if (!v || v === "false" || v === "null" || v === "undefined") { - ctx.el.removeAttribute(key); - } else { - ctx.el.setAttribute(key, v); - } - }); - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/dom/class.ts b/code/ts/library/src/plugins/official/attributes/dom/class.ts deleted file mode 100644 index 287f954a1..000000000 --- a/code/ts/library/src/plugins/official/attributes/dom/class.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: ic:baseline-format-paint -// Slug: Add or remove classes from an element reactively -// Description: This action adds or removes classes from an element reactively based on the expression provided. The expression should be an object where the keys are the class names and the values are booleans. If the value is true, the class is added. If the value is false, the class is removed. - -import { AttributePlugin } from "../../../../engine"; - -export const Class: AttributePlugin = { - pluginType: "attribute", - name: "class", - mustHaveEmptyKey: true, - mustNotEmptyExpression: true, - - onLoad: (ctx) => { - return ctx.reactivity.effect(() => { - const classes: Object = ctx.expressionFn(ctx); - for (const [k, v] of Object.entries(classes)) { - const classNames = k.split(" "); - if (v) { - ctx.el.classList.add(...classNames); - } else { - ctx.el.classList.remove(...classNames); - } - } - }); - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/dom/index.ts b/code/ts/library/src/plugins/official/attributes/dom/index.ts deleted file mode 100644 index 0bd26780e..000000000 --- a/code/ts/library/src/plugins/official/attributes/dom/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./bind"; -export * from "./class"; -export * from "./model"; -export * from "./on"; -export * from "./ref"; -export * from "./text"; diff --git a/code/ts/library/src/plugins/official/attributes/dom/ref.ts b/code/ts/library/src/plugins/official/attributes/dom/ref.ts deleted file mode 100644 index 9e71bdf57..000000000 --- a/code/ts/library/src/plugins/official/attributes/dom/ref.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: mdi:cursor-pointer -// Slug: Create a reference to an element -// Description: This attribute creates a reference to an element that can be used in other expressions. - -import { AttributePlugin } from "../../../../engine"; - -// Sets the value of the element -export const Ref: AttributePlugin = { - pluginType: "attribute", - name: "ref", - mustHaveEmptyKey: true, - mustNotEmptyExpression: true, - bypassExpressionFunctionCreation: () => true, - onLoad: (ctx) => { - const signalName = ctx.expression; - ctx.upsertSignal(signalName, ctx.el); - - return () => { - ctx.removeSignals(signalName); - }; - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/dom/text.ts b/code/ts/library/src/plugins/official/attributes/dom/text.ts deleted file mode 100644 index 370114709..000000000 --- a/code/ts/library/src/plugins/official/attributes/dom/text.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: tabler:typography -// Slug: Set the text content of an element -// Description: This attribute sets the text content of an element to the result of the expression. - -import { AttributePlugin } from "../../../../engine"; -import { ERR_BAD_ARGS } from "../../../../engine/errors"; - -export const Text: AttributePlugin = { - pluginType: "attribute", - name: "text", - mustHaveEmptyKey: true, - onLoad: (ctx) => { - const { el, expressionFn } = ctx; - if (!(el instanceof HTMLElement)) { - // Element is not HTMLElement - throw ERR_BAD_ARGS; - } - return ctx.reactivity.effect(() => { - const res = expressionFn(ctx); - el.textContent = `${res}`; - }); - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/index.ts b/code/ts/library/src/plugins/official/attributes/index.ts deleted file mode 100644 index 7505f603f..000000000 --- a/code/ts/library/src/plugins/official/attributes/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./backend"; -export * from "./core"; -export * from "./dom"; -export * from "./storage"; -export * from "./url"; -export * from "./visibility"; diff --git a/code/ts/library/src/plugins/official/attributes/storage/index.ts b/code/ts/library/src/plugins/official/attributes/storage/index.ts deleted file mode 100644 index cc1367889..000000000 --- a/code/ts/library/src/plugins/official/attributes/storage/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./persist"; diff --git a/code/ts/library/src/plugins/official/attributes/storage/persist.ts b/code/ts/library/src/plugins/official/attributes/storage/persist.ts deleted file mode 100644 index 731630623..000000000 --- a/code/ts/library/src/plugins/official/attributes/storage/persist.ts +++ /dev/null @@ -1,94 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: mdi:floppy-variant -// Slug: Persist data to local storage or session storage -// Description: This plugin allows you to persist data to local storage or session storage. Once you add this attribute the data will be persisted to local storage or session storage. - -import { AttributePlugin } from "../../../../engine"; -import { DATASTAR, DATASTAR_EVENT } from "../../../../engine/consts"; -import { remoteSignals } from "../../../../utils/signals"; -import { DatastarSSEEvent } from "../../watchers/backend/sseShared"; - -export const Persist: AttributePlugin = { - pluginType: "attribute", - name: "persist", - allowedModifiers: new Set(["local", "session", "remote"]), - onLoad: (ctx) => { - const key = ctx.key || DATASTAR; - const expression = ctx.expression; - const keys = new Set(); - - if (expression.trim() !== "") { - const value = ctx.expressionFn(ctx); - const parts = value.split(" "); - for (const part of parts) { - keys.add(part); - } - } - - let lastMarshalled = ""; - const storageType = ctx.modifiers.has("session") ? "session" : "local"; - const useRemote = ctx.modifiers.has("remote"); - - const storeUpdateHandler = ((_: CustomEvent) => { - let store = ctx.store(); - if (useRemote) { - store = remoteSignals(store); - } - if (keys.size > 0) { - const newStore: Record = {}; - for (const key of keys) { - const parts = key.split("."); - let newSubstore = newStore; - let subStore = store; - for (let i = 0; i < parts.length - 1; i++) { - const part = parts[i]; - if (!newSubstore[part]) { - newSubstore[part] = {}; - } - newSubstore = newSubstore[part]; - subStore = subStore[part]; - } - - const lastPart = parts[parts.length - 1]; - newSubstore[lastPart] = subStore[lastPart]; - } - store = newStore; - } - - const marshalledStore = JSON.stringify(store); - - if (marshalledStore === lastMarshalled) { - return; - } - - if (storageType === "session") { - window.sessionStorage.setItem(key, marshalledStore); - } else { - window.localStorage.setItem(key, marshalledStore); - } - - lastMarshalled = marshalledStore; - }) as EventListener; - - window.addEventListener(DATASTAR_EVENT, storeUpdateHandler); - - let marshalledStore: string | null; - - if (storageType === "session") { - marshalledStore = window.sessionStorage.getItem(key); - } else { - marshalledStore = window.localStorage.getItem(key); - } - - if (!!marshalledStore) { - const store = JSON.parse(marshalledStore); - for (const key in store) { - ctx.upsertSignal(key, store[key]); - } - } - - return () => { - window.removeEventListener(DATASTAR_EVENT, storeUpdateHandler); - }; - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/url/index.ts b/code/ts/library/src/plugins/official/attributes/url/index.ts deleted file mode 100644 index e71763b2e..000000000 --- a/code/ts/library/src/plugins/official/attributes/url/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./replaceUrl"; diff --git a/code/ts/library/src/plugins/official/attributes/url/replaceUrl.ts b/code/ts/library/src/plugins/official/attributes/url/replaceUrl.ts deleted file mode 100644 index 40a84fa84..000000000 --- a/code/ts/library/src/plugins/official/attributes/url/replaceUrl.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: carbon:url -// Slug: Replace the current URL with a new URL -// Description: This plugin allows you to replace the current URL with a new URL. Once you add this attribute the current URL will be replaced with the new URL. - -import { AttributePlugin } from "../../../../engine"; - -export const ReplaceUrl: AttributePlugin = { - pluginType: "attribute", - name: "replaceUrl", - mustHaveEmptyKey: true, - mustNotEmptyExpression: true, - - onLoad: (ctx) => { - return ctx.reactivity.effect(() => { - const value = ctx.expressionFn(ctx); - const baseUrl = window.location.href; - const url = new URL(value, baseUrl).toString(); - - window.history.replaceState({}, "", url); - }); - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/visibility/index.ts b/code/ts/library/src/plugins/official/attributes/visibility/index.ts deleted file mode 100644 index 1853832cb..000000000 --- a/code/ts/library/src/plugins/official/attributes/visibility/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./intersects"; -export * from "./scrollIntoView"; -export * from "./show"; -export * from "./viewTransition"; diff --git a/code/ts/library/src/plugins/official/attributes/visibility/scrollIntoView.ts b/code/ts/library/src/plugins/official/attributes/visibility/scrollIntoView.ts deleted file mode 100644 index 3856bd791..000000000 --- a/code/ts/library/src/plugins/official/attributes/visibility/scrollIntoView.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: hugeicons:mouse-scroll-01 -// Slug: Scroll an element into view -// Description: This attribute scrolls the element into view. - -import { AttributeContext, AttributePlugin } from "../../../../engine"; -import { scrollIntoView } from "../../../../utils/dom"; - -const SMOOTH = "smooth"; -const INSTANT = "instant"; -const AUTO = "auto"; -const HSTART = "hstart"; -const HCENTER = "hcenter"; -const HEND = "hend"; -const HNEAREST = "hnearest"; -const VSTART = "vstart"; -const VCENTER = "vcenter"; -const VEND = "vend"; -const VNEAREST = "vnearest"; -const FOCUS = "focus"; - -const CENTER = "center"; -const START = "start"; -const END = "end"; -const NEAREST = "nearest"; - -// Scrolls the element into view -export const ScrollIntoView: AttributePlugin = { - pluginType: "attribute", - name: "scrollIntoView", - mustHaveEmptyKey: true, - mustHaveEmptyExpression: true, - allowedModifiers: new Set([ - SMOOTH, - INSTANT, - AUTO, - HSTART, - HCENTER, - HEND, - HNEAREST, - VSTART, - VCENTER, - VEND, - VNEAREST, - FOCUS, - ]), - - onLoad: ({ el, modifiers, rawKey }: AttributeContext) => { - if (!el.tabIndex) el.setAttribute("tabindex", "0"); - const opts: ScrollIntoViewOptions = { - behavior: SMOOTH, - block: CENTER, - inline: CENTER, - }; - if (modifiers.has(SMOOTH)) opts.behavior = SMOOTH; - if (modifiers.has(INSTANT)) opts.behavior = INSTANT; - if (modifiers.has(AUTO)) opts.behavior = AUTO; - if (modifiers.has(HSTART)) opts.inline = START; - if (modifiers.has(HCENTER)) opts.inline = CENTER; - if (modifiers.has(HEND)) opts.inline = END; - if (modifiers.has(HNEAREST)) opts.inline = NEAREST; - if (modifiers.has(VSTART)) opts.block = START; - if (modifiers.has(VCENTER)) opts.block = CENTER; - if (modifiers.has(VEND)) opts.block = END; - if (modifiers.has(VNEAREST)) opts.block = NEAREST; - - scrollIntoView(el, opts, modifiers.has("focus")); - delete el.dataset[rawKey]; - return () => {}; - }, -}; diff --git a/code/ts/library/src/plugins/official/attributes/visibility/show.ts b/code/ts/library/src/plugins/official/attributes/visibility/show.ts deleted file mode 100644 index a15f9dba1..000000000 --- a/code/ts/library/src/plugins/official/attributes/visibility/show.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: streamline:interface-edit-view-eye-eyeball-open-view -// Slug: Show or hide an element -// Description: This attribute shows or hides an element based on the value of the expression. If the expression is true, the element is shown. If the expression is false, the element is hidden. The element is hidden by setting the display property to none. - -import { AttributePlugin } from "../../../../engine"; - -export const Show: AttributePlugin = { - pluginType: "attribute", - name: "show", - mustHaveEmptyKey: true, - mustNotEmptyExpression: true, - - onLoad: (ctx) => { - return ctx.reactivity.effect(async () => { - const shouldShow: boolean = ctx.expressionFn(ctx); - - if (shouldShow) { - if (ctx.el.style.display === "none") { - ctx.el.style.removeProperty("display"); - } - } else { - ctx.el.style.setProperty("display", "none"); - } - }); - }, -}; diff --git a/code/ts/library/src/plugins/official/index.ts b/code/ts/library/src/plugins/official/index.ts deleted file mode 100644 index 4c0856981..000000000 --- a/code/ts/library/src/plugins/official/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./actions"; -export * from "./attributes"; -export * from "./preprocessors"; -export * from "./watchers"; diff --git a/code/ts/library/src/plugins/official/preprocessors/core/actions.ts b/code/ts/library/src/plugins/official/preprocessors/core/actions.ts deleted file mode 100644 index 89b212aa7..000000000 --- a/code/ts/library/src/plugins/official/preprocessors/core/actions.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PreprocessorPlugin, RegexpGroups } from "../../../../engine"; -import { wholePrefixSuffix } from "../../../../utils/regex"; - -// Replacing $action(args) with ctx.actions.action(ctx, args) -export const ActionsProcessor: PreprocessorPlugin = { - name: "action", - pluginType: "preprocessor", - regexp: wholePrefixSuffix( - "\\$", - "action", - "(?\\((?.*)\\))", - false, - ), - replacer: ({ action, args }: RegexpGroups) => { - const withCtx = [`ctx`]; - if (args) { - withCtx.push(...args.split(",").map((x) => x.trim())); - } - const argsJoined = withCtx.join(","); - return `ctx.actions.${action}.method(${argsJoined})`; - }, -}; diff --git a/code/ts/library/src/plugins/official/preprocessors/core/index.ts b/code/ts/library/src/plugins/official/preprocessors/core/index.ts deleted file mode 100644 index ad8bc6e10..000000000 --- a/code/ts/library/src/plugins/official/preprocessors/core/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./actions"; -export * from "./signals"; diff --git a/code/ts/library/src/plugins/official/preprocessors/core/signals.ts b/code/ts/library/src/plugins/official/preprocessors/core/signals.ts deleted file mode 100644 index d29a3d4d0..000000000 --- a/code/ts/library/src/plugins/official/preprocessors/core/signals.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PreprocessorPlugin, RegexpGroups } from "../../../../engine"; -import { wholePrefixSuffix } from "../../../../utils/regex"; - -// Replacing $signal with ctx.store.signal.value` -export const SignalsProcessor: PreprocessorPlugin = { - name: "signal", - pluginType: "preprocessor", - regexp: wholePrefixSuffix("\\$", "signal", "(?\\([^\\)]*\\))?"), - replacer: (groups: RegexpGroups) => { - const { signal, method } = groups; - const prefix = `ctx.store()`; - if (!method?.length) { - return `${prefix}.${signal}.value`; - } - const parts = signal.split("."); - const methodName = parts.pop(); - const nestedSignal = parts.join("."); - return `${prefix}.${nestedSignal}.value.${methodName}${method}`; - }, -}; diff --git a/code/ts/library/src/plugins/official/watchers/backend/index.ts b/code/ts/library/src/plugins/official/watchers/backend/index.ts deleted file mode 100644 index 30109665c..000000000 --- a/code/ts/library/src/plugins/official/watchers/backend/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./sseExecuteScript"; -export * from "./sseMergeFragment"; -export * from "./sseMergeSignals"; -export * from "./sseRemoveFragments"; -export * from "./sseRemoveSignals"; -export * from "./sseShared"; diff --git a/code/ts/library/src/plugins/official/watchers/backend/sseMergeFragment.ts b/code/ts/library/src/plugins/official/watchers/backend/sseMergeFragment.ts deleted file mode 100644 index ca2fc9443..000000000 --- a/code/ts/library/src/plugins/official/watchers/backend/sseMergeFragment.ts +++ /dev/null @@ -1,184 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:cloud-download -// Slug: Use Server-Sent Events to fetch data from a server using the Datastar SDK interface -// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client. - -import { InitContext, WatcherPlugin } from "../../../../engine"; -import { - DefaultFragmentMergeMode, - DefaultFragmentsUseViewTransitions, - DefaultSettleDurationMs, - EventTypes, - FragmentMergeModes, -} from "../../../../engine/consts"; -import { ERR_BAD_ARGS } from "../../../../engine/errors"; -import { isBoolString } from "../../../../utils/text"; -import { - docWithViewTransitionAPI, - supportsViewTransitions, -} from "../../../../utils/view-transitions"; -import { idiomorph } from "../../../../vendored/idiomorph"; -import { - datastarSSEEventWatcher, - SETTLING_CLASS, - SWAPPING_CLASS, -} from "./sseShared"; - -export const MergeFragments: WatcherPlugin = { - pluginType: "watcher", - name: EventTypes.MergeFragments, - onGlobalInit: async (ctx) => { - const fragmentContainer = document.createElement("template"); - datastarSSEEventWatcher(EventTypes.MergeFragments, ({ - fragments: fragmentsRaw = "
", - selector = "", - mergeMode = DefaultFragmentMergeMode, - settleDuration: settleDurationRaw = `${DefaultSettleDurationMs}`, - useViewTransition: useViewTransitionRaw = - `${DefaultFragmentsUseViewTransitions}`, - }) => { - const settleDuration = parseInt(settleDurationRaw); - const useViewTransition = isBoolString(useViewTransitionRaw); - - fragmentContainer.innerHTML = fragmentsRaw.trim(); - const fragments = [...fragmentContainer.content.children]; - fragments.forEach((fragment) => { - if (!(fragment instanceof Element)) { - // No fragments found - throw ERR_BAD_ARGS; - } - - const selectorOrID = selector || - `#${fragment.getAttribute("id")}`; - const targets = document.querySelectorAll(selectorOrID) || - []; - const allTargets = [...targets]; - if (!allTargets.length) { - // No targets found - throw ERR_BAD_ARGS; - } - - if (supportsViewTransitions && useViewTransition) { - docWithViewTransitionAPI.startViewTransition(() => - applyToTargets( - ctx, - mergeMode, - settleDuration, - fragment, - allTargets, - ) - ); - } else { - applyToTargets( - ctx, - mergeMode, - settleDuration, - fragment, - allTargets, - ); - } - }); - }); - }, -}; - -function applyToTargets( - ctx: InitContext, - mergeMode: string, - settleDuration: number, - fragment: Element, - capturedTargets: Element[], -) { - for (const initialTarget of capturedTargets) { - initialTarget.classList.add(SWAPPING_CLASS); - const originalHTML = initialTarget.outerHTML; - let modifiedTarget = initialTarget; - switch (mergeMode) { - case FragmentMergeModes.Morph: - const result = idiomorph( - modifiedTarget, - fragment, - { - callbacks: { - beforeNodeRemoved: ( - oldNode: Element, - _: Element, - ) => { - ctx.cleanup( - oldNode, - ); - return true; - }, - }, - }, - ); - if (!result?.length) { - // No morph result - throw ERR_BAD_ARGS; - } - modifiedTarget = result[0] as Element; - break; - case FragmentMergeModes.Inner: - // Replace the contents of the target element with the response - modifiedTarget.innerHTML = fragment.innerHTML; - break; - case FragmentMergeModes.Outer: - // Replace the entire target element with the response - modifiedTarget.replaceWith(fragment); - break; - case FragmentMergeModes.Prepend: - // Insert the response before the first child of the target element - modifiedTarget.prepend(fragment); - break; - case FragmentMergeModes.Append: - // Insert the response after the last child of the target element - modifiedTarget.append(fragment); - break; - case FragmentMergeModes.Before: - // Insert the response before the target element - modifiedTarget.before(fragment); - break; - case FragmentMergeModes.After: - // Insert the response after the target element - modifiedTarget.after(fragment); - break; - case FragmentMergeModes.UpsertAttributes: - // Upsert the attributes of the target element - fragment.getAttributeNames().forEach( - (attrName) => { - const value = fragment.getAttribute( - attrName, - )!; - modifiedTarget.setAttribute( - attrName, - value, - ); - }, - ); - break; - default: - // Unknown merge type - throw ERR_BAD_ARGS; - } - ctx.cleanup(modifiedTarget); - modifiedTarget.classList.add(SWAPPING_CLASS); - - ctx.applyPlugins(document.body); - - setTimeout(() => { - initialTarget.classList.remove(SWAPPING_CLASS); - modifiedTarget.classList.remove(SWAPPING_CLASS); - }, settleDuration); - - const revisedHTML = modifiedTarget.outerHTML; - - if (originalHTML !== revisedHTML) { - modifiedTarget.classList.add(SETTLING_CLASS); - setTimeout(() => { - modifiedTarget.classList.remove( - SETTLING_CLASS, - ); - }, settleDuration); - } - } -} diff --git a/code/ts/library/src/plugins/official/watchers/backend/sseMergeSignals.ts b/code/ts/library/src/plugins/official/watchers/backend/sseMergeSignals.ts deleted file mode 100644 index 8aeb4d787..000000000 --- a/code/ts/library/src/plugins/official/watchers/backend/sseMergeSignals.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:settings-input-antenna -// Slug: Merge fine grain signals store data from a server using the Datastar SDK interface -// Description: Merge store data from a server using the Datastar SDK interface - -import { InitExpressionFunction, WatcherPlugin } from "../../../../engine"; -import { - DefaultMergeSignalsOnlyIfMissing, - EventTypes, -} from "../../../../engine/consts"; -import { storeFromPossibleContents } from "../../../../utils/signals"; -import { isBoolString } from "../../../../utils/text"; -import { datastarSSEEventWatcher } from "./sseShared"; - -export const MergeSignals: WatcherPlugin = { - pluginType: "watcher", - name: EventTypes.MergeSignals, - onGlobalInit: async (ctx) => { - datastarSSEEventWatcher(EventTypes.MergeSignals, ({ - signals = "{}", - onlyIfMissing: onlyIfMissingRaw = - `${DefaultMergeSignalsOnlyIfMissing}`, - }) => { - const onlyIfMissing = isBoolString(onlyIfMissingRaw); - const fnContents = - ` return Object.assign({...ctx.store()}, ${signals})`; - try { - const fn = new Function( - "ctx", - fnContents, - ) as InitExpressionFunction; - const possibleMergeSignals = fn(ctx); - const actualMergeSignals = storeFromPossibleContents( - ctx.store(), - possibleMergeSignals, - onlyIfMissing, - ); - ctx.mergeSignals(actualMergeSignals); - ctx.applyPlugins(document.body); - } catch (e) { - console.log(fnContents); - console.error(e); - debugger; - } - }); - }, -}; diff --git a/code/ts/library/src/plugins/official/watchers/backend/sseRemoveFragments.ts b/code/ts/library/src/plugins/official/watchers/backend/sseRemoveFragments.ts deleted file mode 100644 index fe32c266a..000000000 --- a/code/ts/library/src/plugins/official/watchers/backend/sseRemoveFragments.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:settings-input-antenna -// Slug: Merge fine grain signals store data from a server using the Datastar SDK interface -// Description: Merge store data from a server using the Datastar SDK interface - -import { WatcherPlugin } from "../../../../engine"; -import { - DefaultFragmentsUseViewTransitions, - DefaultSettleDurationMs, - EventTypes, -} from "../../../../engine/consts"; -import { ERR_BAD_ARGS } from "../../../../engine/errors"; -import { isBoolString } from "../../../../utils/text"; -import { - docWithViewTransitionAPI, - supportsViewTransitions, -} from "../../../../utils/view-transitions"; -import { datastarSSEEventWatcher, SWAPPING_CLASS } from "./sseShared"; - -export const RemoveFragments: WatcherPlugin = { - pluginType: "watcher", - name: EventTypes.RemoveFragments, - onGlobalInit: async () => { - datastarSSEEventWatcher(EventTypes.RemoveFragments, ({ - selector, - settleDuration: settleDurationRaw = `${DefaultSettleDurationMs}`, - useViewTransition: useViewTransitionRaw = - `${DefaultFragmentsUseViewTransitions}`, - }) => { - if (!!!selector.length) { - // No selector provided for remove-fragments - throw ERR_BAD_ARGS; - } - - const settleDuration = parseInt(settleDurationRaw); - const useViewTransition = isBoolString(useViewTransitionRaw); - const removeTargets = document.querySelectorAll(selector); - - const applyToTargets = () => { - for (const target of removeTargets) { - target.classList.add(SWAPPING_CLASS); - } - - setTimeout(() => { - for (const target of removeTargets) { - target.remove(); - } - }, settleDuration); - }; - - if (supportsViewTransitions && useViewTransition) { - docWithViewTransitionAPI.startViewTransition(() => - applyToTargets() - ); - } else { - applyToTargets(); - } - }); - }, -}; diff --git a/code/ts/library/src/plugins/official/watchers/backend/sseRemoveSignals.ts b/code/ts/library/src/plugins/official/watchers/backend/sseRemoveSignals.ts deleted file mode 100644 index 03bfdba4d..000000000 --- a/code/ts/library/src/plugins/official/watchers/backend/sseRemoveSignals.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Authors: Delaney Gillilan -// Icon: material-symbols:settings-input-antenna -// Slug: Merge fine grain signals store data from a server using the Datastar SDK interface -// Description: Merge store data from a server using the Datastar SDK interface - -import { EventTypes } from "../../../../engine/consts"; -import { ERR_BAD_ARGS } from "../../../../engine/errors"; -import { WatcherPlugin } from "../../../../engine/types"; -import { datastarSSEEventWatcher } from "./sseShared"; - -export const RemoveSignals: WatcherPlugin = { - pluginType: "watcher", - name: EventTypes.RemoveSignals, - onGlobalInit: async (ctx) => { - datastarSSEEventWatcher( - EventTypes.RemoveSignals, - ({ paths: pathsRaw = "" }) => { - // replace all whitespace with a single space - pathsRaw = pathsRaw.replaceAll(/\s+/g, " "); - if (!!!pathsRaw?.length) { - // No paths provided for remove-signals - throw ERR_BAD_ARGS; - } - const paths = pathsRaw.split(" "); - ctx.removeSignals(...paths); - }, - ); - }, -}; diff --git a/code/ts/library/src/plugins/official/watchers/index.ts b/code/ts/library/src/plugins/official/watchers/index.ts deleted file mode 100644 index 7fb1dba63..000000000 --- a/code/ts/library/src/plugins/official/watchers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./backend"; diff --git a/code/ts/library/src/utils/index.ts b/code/ts/library/src/utils/index.ts deleted file mode 100644 index 04dd27350..000000000 --- a/code/ts/library/src/utils/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./arguments"; -export * from "./dom"; -export * from "./regex"; -export * from "./signals"; -export * from "./text"; -export * from "./timing"; -export * from "./types"; -export * from "./view-transitions"; diff --git a/code/ts/library/src/utils/regex.ts b/code/ts/library/src/utils/regex.ts deleted file mode 100644 index 22d7a1124..000000000 --- a/code/ts/library/src/utils/regex.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const validJSIdentifier = `[a-zA-Z_$]+`; -export const validNestedJSIdentifier = validJSIdentifier + `[0-9a-zA-Z_$.]*`; - -export function wholePrefixSuffix( - rune: string, - prefix: string, - suffix: string, - nestable = true, -) { - const identifier = nestable ? validNestedJSIdentifier : validJSIdentifier; - return new RegExp( - `(?${rune}(?<${prefix}>${identifier})${suffix})`, - `g`, - ); -} diff --git a/code/ts/library/src/utils/signals.ts b/code/ts/library/src/utils/signals.ts deleted file mode 100644 index c43b011ac..000000000 --- a/code/ts/library/src/utils/signals.ts +++ /dev/null @@ -1,36 +0,0 @@ -export function remoteSignals(obj: Object): Object { - const res: Record = {}; - - for (const [k, v] of Object.entries(obj)) { - if (k.startsWith("_")) { - continue; - } else if (typeof v === "object" && !Array.isArray(v)) { - res[k] = remoteSignals(v); // recurse - } else { - res[k] = v; - } - } - - return res; -} - -export function storeFromPossibleContents( - currentStore: any, - contents: any, - hasIfMissing: boolean, -) { - const actual: any = {}; - - if (!hasIfMissing) { - Object.assign(actual, contents); - } else { - for (const key in contents) { - const currentValue = currentStore[key]?.value; - if (currentValue === undefined || currentValue === null) { - actual[key] = contents[key]; - } - } - } - - return actual; -} diff --git a/code/ts/library/src/utils/text.ts b/code/ts/library/src/utils/text.ts deleted file mode 100644 index 898227bf9..000000000 --- a/code/ts/library/src/utils/text.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const kebabize = (str: string) => - str.replace( - /[A-Z]+(?![a-z])|[A-Z]/g, - ($, ofs) => (ofs ? "-" : "") + $.toLowerCase(), - ); - -export const isBoolString = (str: string) => str.trim() === "true"; diff --git a/code/ts/library/src/utils/types.ts b/code/ts/library/src/utils/types.ts deleted file mode 100644 index 59bd81625..000000000 --- a/code/ts/library/src/utils/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type HTMLorSVGElement = Element & (HTMLElement | SVGElement); diff --git a/code/ts/library/src/vendored/deepsignal.ts b/code/ts/library/src/vendored/deepsignal.ts deleted file mode 100644 index bed8a7907..000000000 --- a/code/ts/library/src/vendored/deepsignal.ts +++ /dev/null @@ -1,96 +0,0 @@ -// From https://github.com/EthanStandel/deepsignal/blob/main/packages/core/src/core.ts -import { ERR_NOT_ALLOWED } from "../engine/errors"; -import { batch, Signal, signal } from "./preact-core"; - -export type AtomicState = - | Array - | ((...args: unknown[]) => unknown) - | string - | boolean - | number - | bigint - | symbol - | undefined - | null; - -export type DeepState = { - [key: string]: (() => unknown) | AtomicState | DeepState; -}; - -export type ReadOnlyDeep = { - readonly [P in keyof T]: ReadOnlyDeep; -}; - -export interface DeepSignalAccessors { - value: ReadOnlyDeep; - peek: () => ReadOnlyDeep; -} - -export type DeepSignalType = - & DeepSignalAccessors - & { - [K in keyof T]: T[K] extends AtomicState ? Signal - : T[K] extends DeepState ? DeepSignalType - : Signal; - }; - -export class DeepSignal implements DeepSignalAccessors { - get value(): ReadOnlyDeep { - return getValue(this as DeepSignalType); - } - - set value(payload: ReadOnlyDeep) { - batch(() => setValue(this as DeepSignalType, payload)); - } - - peek(): ReadOnlyDeep { - return getValue(this as DeepSignalType, { peek: true }); - } -} - -export const deepSignal = ( - initialValue: T, -): DeepSignalType => - Object.assign( - new DeepSignal(), - Object.entries(initialValue).reduce( - (acc, [key, value]) => { - if (["value", "peek"].some((iKey) => iKey === key)) { - // console.error(`${key} is a reserved property name`) - throw ERR_NOT_ALLOWED; - } else if ( - typeof value !== "object" || value === null || Array.isArray(value) - ) { - acc[key] = signal(value); - } else { - acc[key] = deepSignal(value); - } - return acc; - }, - {} as { [key: string]: unknown }, - ), - ) as DeepSignalType; - -const setValue = >( - deepSignal: T, - payload: U, -): void => - Object.keys(payload).forEach(( - key: keyof U, - ) => (deepSignal[key].value = payload[key])); - -const getValue = >( - deepSignal: T, - { peek = false }: { peek?: boolean } = {}, -): ReadOnlyDeep => - Object.entries(deepSignal).reduce( - (acc, [key, value]) => { - if (value instanceof Signal) { - acc[key] = peek ? value.peek() : value.value; - } else if (value instanceof DeepSignal) { - acc[key] = getValue(value as DeepSignalType, { peek }); - } - return acc; - }, - {} as { [key: string]: unknown }, - ) as ReadOnlyDeep; diff --git a/code/ts/library/src/vendored/fetch-event-source/fetch.ts b/code/ts/library/src/vendored/fetch-event-source/fetch.ts deleted file mode 100644 index b4737bec7..000000000 --- a/code/ts/library/src/vendored/fetch-event-source/fetch.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { ERR_SERVICE_UNAVAILABLE } from "../../engine/errors"; -import { EventSourceMessage, getBytes, getLines, getMessages } from "./parse"; - -export const EventStreamContentType = "text/event-stream"; - -const DefaultRetryInterval = 1000; -const LastEventId = "last-event-id"; - -export interface FetchEventSourceInit extends RequestInit { - /** - * The request headers. FetchEventSource only supports the Record format. - */ - headers?: Record; - - /** - * Called when a response is received. Use this to validate that the response - * actually matches what you expect (and throw if it doesn't.) If not provided, - * will default to a basic validation to ensure the content-type is text/event-stream. - */ - onopen?: (response: Response) => Promise; - - /** - * Called when a message is received. NOTE: Unlike the default browser - * EventSource.onmessage, this callback is called for _all_ events, - * even ones with a custom `event` field. - */ - onmessage?: (ev: EventSourceMessage) => void; - - /** - * Called when a response finishes. If you don't expect the server to kill - * the connection, you can throw an exception here and retry using onerror. - */ - onclose?: () => void; - - /** - * Called when there is any error making the request / processing messages / - * handling callbacks etc. Use this to control the retry strategy: if the - * error is fatal, rethrow the error inside the callback to stop the entire - * operation. Otherwise, you can return an interval (in milliseconds) after - * which the request will automatically retry (with the last-event-id). - * If this callback is not specified, or it returns undefined, fetchEventSource - * will treat every error as retriable and will try again after 1 second. - */ - onerror?: (err: any) => number | null | undefined | void; - - /** - * If true, will keep the request open even if the document is hidden. - * By default, fetchEventSource will close the request and reopen it - * automatically when the document becomes visible again. - */ - openWhenHidden?: boolean; - - /** The Fetch function to use. Defaults to window.fetch */ - fetch?: typeof fetch; - - /** The scaler for the retry interval. Defaults to 2 */ - retryScaler?: number; - - /** The maximum retry interval in milliseconds. Defaults to 30_000 */ - retryMaxWaitMs?: number; - - /** The maximum number of retries before giving up. Defaults to 10 */ - retryMaxCount?: number; -} - -export function fetchEventSource(input: RequestInfo, { - signal: inputSignal, - headers: inputHeaders, - onopen: inputOnOpen, - onmessage, - onclose, - onerror, - openWhenHidden, - fetch: inputFetch, - retryScaler = 2, - retryMaxWaitMs = 30_000, - retryMaxCount = 10, - ...rest -}: FetchEventSourceInit) { - return new Promise((resolve, reject) => { - let retries = 0; - - // make a copy of the input headers since we may modify it below: - const headers = { ...inputHeaders }; - if (!headers.accept) { - headers.accept = EventStreamContentType; - } - - let curRequestController: AbortController; - function onVisibilityChange() { - curRequestController.abort(); // close existing request on every visibility change - if (!document.hidden) { - create(); // page is now visible again, recreate request. - } - } - - if (!openWhenHidden) { - document.addEventListener("visibilitychange", onVisibilityChange); - } - - let retryInterval = DefaultRetryInterval; - let retryTimer = 0; - function dispose() { - document.removeEventListener( - "visibilitychange", - onVisibilityChange, - ); - window.clearTimeout(retryTimer); - curRequestController.abort(); - } - - // if the incoming signal aborts, dispose resources and resolve: - inputSignal?.addEventListener("abort", () => { - dispose(); - resolve(); // don't waste time constructing/logging errors - }); - - const fetch = inputFetch ?? window.fetch; - const onopen = inputOnOpen ?? - function defaultOnOpen( - // response: Response - ) {}; - - async function create() { - curRequestController = new AbortController(); - try { - const response = await fetch(input, { - ...rest, - headers, - signal: curRequestController.signal, - }); - - await onopen(response); - - await getBytes( - response.body!, - getLines(getMessages((id) => { - if (id) { - // store the id and send it back on the next retry: - headers[LastEventId] = id; - } else { - // don't send the last-event-id header anymore: - delete headers[LastEventId]; - } - }, (retry) => { - retryInterval = retry; - }, onmessage)), - ); - - onclose?.(); - dispose(); - resolve(); - } catch (err) { - if (!curRequestController.signal.aborted) { - // if we haven't aborted the request ourselves: - try { - // check if we need to retry: - const interval: any = onerror?.(err) ?? retryInterval; - window.clearTimeout(retryTimer); - retryTimer = window.setTimeout(create, interval); - retryInterval *= retryScaler; // exponential backoff - retryInterval = Math.min(retryInterval, retryMaxWaitMs); - retries++; - if (retries >= retryMaxCount) { - // we should not retry anymore: - dispose(); - // Max retries hit, check your server or network connection - reject(ERR_SERVICE_UNAVAILABLE); - } else { - console.error( - `Datastar failed to reach ${rest.method}:${input.toString()} retry in ${interval}ms`, - ); - } - } catch (innerErr) { - // we should not retry anymore: - dispose(); - reject(innerErr); - } - } - } - } - - create(); - }); -} diff --git a/code/ts/library/src/vendored/fetch-event-source/index.ts b/code/ts/library/src/vendored/fetch-event-source/index.ts deleted file mode 100644 index 31efe27ed..000000000 --- a/code/ts/library/src/vendored/fetch-event-source/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { EventStreamContentType, fetchEventSource } from "./fetch"; -export type { FetchEventSourceInit } from "./fetch"; -export type { EventSourceMessage } from "./parse"; diff --git a/code/ts/library/src/vendored/index.ts b/code/ts/library/src/vendored/index.ts deleted file mode 100644 index 758453563..000000000 --- a/code/ts/library/src/vendored/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// We don't use these exports, they are purely for access via package managers like NPM - -export * from "./deepsignal"; -export * from "./fetch-event-source"; -export * from "./idiomorph"; -export * from "./preact-core"; -export * from "./ts-merge-patch"; diff --git a/code/ts/library/src/vendored/preact-core.ts b/code/ts/library/src/vendored/preact-core.ts deleted file mode 100644 index ee437e201..000000000 --- a/code/ts/library/src/vendored/preact-core.ts +++ /dev/null @@ -1,841 +0,0 @@ -// An named symbol/brand for detecting Signal instances even when they weren't - -import { ERR_BAD_ARGS } from "../engine/errors"; - -// created using the same signals library version. -const BRAND_SYMBOL = Symbol.for("preact-signals"); - -// Flags for Computed and Effect. -const RUNNING = 1 << 0; -const NOTIFIED = 1 << 1; -const OUTDATED = 1 << 2; -const DISPOSED = 1 << 3; -const HAS_ERROR = 1 << 4; -const TRACKING = 1 << 5; - -// A linked list node used to track dependencies (sources) and dependents (targets). -// Also used to remember the source's last version number that the target saw. -type Node = { - // A source whose value the target depends on. - _source: Signal; - _prevSource?: Node; - _nextSource?: Node; - - // A target that depends on the source and should be notified when the source changes. - _target: Computed | Effect; - _prevTarget?: Node; - _nextTarget?: Node; - - // The version number of the source that target has last seen. We use version numbers - // instead of storing the source value, because source values can take arbitrary amount - // of memory, and computeds could hang on to them forever because they're lazily evaluated. - // Use the special value -1 to mark potentially unused but recyclable nodes. - _version: number; - - // Used to remember & roll back the source's previous `._node` value when entering & - // exiting a new evaluation context. - _rollbackNode?: Node; -}; - -function startBatch() { - batchDepth++; -} - -function endBatch() { - if (batchDepth > 1) { - batchDepth--; - return; - } - - let error: unknown; - let hasError = false; - - while (batchedEffect !== undefined) { - let effect: Effect | undefined = batchedEffect; - batchedEffect = undefined; - - batchIteration++; - - while (effect !== undefined) { - const next: Effect | undefined = effect._nextBatchedEffect; - effect._nextBatchedEffect = undefined; - effect._flags &= ~NOTIFIED; - - if (!(effect._flags & DISPOSED) && needsToRecompute(effect)) { - try { - effect._callback(); - } catch (err) { - if (!hasError) { - error = err; - hasError = true; - } - } - } - effect = next; - } - } - batchIteration = 0; - batchDepth--; - - if (hasError) { - throw error; - } -} - -/** - * Combine multiple value updates into one "commit" at the end of the provided callback. - * - * Batches can be nested and changes are only flushed once the outermost batch callback - * completes. - * - * Accessing a signal that has been modified within a batch will reflect its updated - * value. - * - * @param fn The callback function. - * @returns The value returned by the callback. - */ -function batch(fn: () => T): T { - if (batchDepth > 0) { - return fn(); - } - /*@__INLINE__**/ startBatch(); - try { - return fn(); - } finally { - endBatch(); - } -} - -// Currently evaluated computed or effect. -let evalContext: Computed | Effect | undefined = undefined; - -/** - * Run a callback function that can access signal values without - * subscribing to the signal updates. - * - * @param fn The callback function. - * @returns The value returned by the callback. - */ -function untracked(fn: () => T): T { - const prevContext = evalContext; - evalContext = undefined; - try { - return fn(); - } finally { - evalContext = prevContext; - } -} - -// Effects collected into a batch. -let batchedEffect: Effect | undefined = undefined; -let batchDepth = 0; -let batchIteration = 0; - -// A global version number for signals, used for fast-pathing repeated -// computed.peek()/computed.value calls when nothing has changed globally. -let globalVersion = 0; - -function addDependency(signal: Signal): Node | undefined { - if (evalContext === undefined) { - return undefined; - } - - let node = signal._node; - if (node === undefined || node._target !== evalContext) { - /** - * `signal` is a new dependency. Create a new dependency node, and set it - * as the tail of the current context's dependency list. e.g: - * - * { A <-> B } - * ↑ ↑ - * tail node (new) - * ↓ - * { A <-> B <-> C } - * ↑ - * tail (evalContext._sources) - */ - node = { - _version: 0, - _source: signal, - _prevSource: evalContext._sources, - _nextSource: undefined, - _target: evalContext, - _prevTarget: undefined, - _nextTarget: undefined, - _rollbackNode: node, - }; - - if (evalContext._sources !== undefined) { - evalContext._sources._nextSource = node; - } - evalContext._sources = node; - signal._node = node; - - // Subscribe to change notifications from this dependency if we're in an effect - // OR evaluating a computed signal that in turn has subscribers. - if (evalContext._flags & TRACKING) { - signal._subscribe(node); - } - return node; - } else if (node._version === -1) { - // `signal` is an existing dependency from a previous evaluation. Reuse it. - node._version = 0; - - /** - * If `node` is not already the current tail of the dependency list (i.e. - * there is a next node in the list), then make the `node` the new tail. e.g: - * - * { A <-> B <-> C <-> D } - * ↑ ↑ - * node ┌─── tail (evalContext._sources) - * └─────│─────┐ - * ↓ ↓ - * { A <-> C <-> D <-> B } - * ↑ - * tail (evalContext._sources) - */ - if (node._nextSource !== undefined) { - node._nextSource._prevSource = node._prevSource; - - if (node._prevSource !== undefined) { - node._prevSource._nextSource = node._nextSource; - } - - node._prevSource = evalContext._sources; - node._nextSource = undefined; - - evalContext._sources!._nextSource = node; - evalContext._sources = node; - } - - // We can assume that the currently evaluated effect / computed signal is already - // subscribed to change notifications from `signal` if needed. - return node; - } - return undefined; -} - -/** - * The base class for plain and computed signals. - */ -// @ts-ignore: "Cannot redeclare exported variable 'Signal'." -// -// A function with the same name is defined later, so we need to ignore TypeScript's -// warning about a redeclared variable. -// -// The class is declared here, but later implemented with ES5-style protoTYPEOF_ -// This enables better control of the transpiled output size. -declare class Signal { - /** @internal */ - _value: unknown; - - /** - * @internal - * Version numbers should always be >= 0, because the special value -1 is used - * by Nodes to signify potentially unused but recyclable nodes. - */ - _version: number; - - /** @internal */ - _node?: Node; - - /** @internal */ - _targets?: Node; - - constructor(value?: T); - - /** @internal */ - _refresh(): boolean; - - /** @internal */ - _subscribe(node: Node): void; - - /** @internal */ - _unsubscribe(node: Node): void; - - subscribe(fn: (value: T) => void): () => void; - - valueOf(): T; - - toString(): string; - - toJSON(): T; - - peek(): T; - - brand: typeof BRAND_SYMBOL; - - get value(): T; - set value(value: T); -} - -/** @internal */ -// @ts-ignore: "Cannot redeclare exported variable 'Signal'." -// -// A class with the same name has already been declared, so we need to ignore -// TypeScript's warning about a redeclared variable. -// -// The previously declared class is implemented here with ES5-style protoTYPEOF_ -// This enables better control of the transpiled output size. -function Signal(this: Signal, value?: unknown) { - this._value = value; - this._version = 0; - this._node = undefined; - this._targets = undefined; -} - -Signal.prototype.brand = BRAND_SYMBOL; - -Signal.prototype._refresh = function () { - return true; -}; - -Signal.prototype._subscribe = function (node) { - if (this._targets !== node && node._prevTarget === undefined) { - node._nextTarget = this._targets; - if (this._targets !== undefined) { - this._targets._prevTarget = node; - } - this._targets = node; - } -}; - -Signal.prototype._unsubscribe = function (node) { - // Only run the unsubscribe step if the signal has any subscribers to begin with. - if (this._targets !== undefined) { - const prev = node._prevTarget; - const next = node._nextTarget; - if (prev !== undefined) { - prev._nextTarget = next; - node._prevTarget = undefined; - } - if (next !== undefined) { - next._prevTarget = prev; - node._nextTarget = undefined; - } - if (node === this._targets) { - this._targets = next; - } - } -}; - -Signal.prototype.subscribe = function (fn) { - return effect(() => { - const value = this.value; - - const prevContext = evalContext; - evalContext = undefined; - try { - fn(value); - } finally { - evalContext = prevContext; - } - }); -}; - -Signal.prototype.valueOf = function () { - return this.value; -}; - -Signal.prototype.toString = function () { - return this.value + ""; -}; - -Signal.prototype.toJSON = function () { - return this.value; -}; - -Signal.prototype.peek = function () { - const prevContext = evalContext; - evalContext = undefined; - try { - return this.value; - } finally { - evalContext = prevContext; - } -}; - -Object.defineProperty(Signal.prototype, "value", { - get(this: Signal) { - const node = addDependency(this); - if (node !== undefined) { - node._version = this._version; - } - return this._value; - }, - set(this: Signal, value) { - if (value !== this._value) { - if (batchIteration > 100) { - // Cycle detected - throw ERR_BAD_ARGS; - } - - this._value = value; - this._version++; - globalVersion++; - - /**@__INLINE__*/ startBatch(); - try { - for ( - let node = this._targets; - node !== undefined; - node = node._nextTarget - ) { - node._target._notify(); - } - } finally { - endBatch(); - } - } - }, -}); - -/** - * Create a new plain signal. - * - * @param value The initial value for the signal. - * @returns A new signal. - */ -export function signal(value: T): Signal; -export function signal(): Signal; -export function signal(value?: T): Signal { - return new Signal(value); -} - -function needsToRecompute(target: Computed | Effect): boolean { - // Check the dependencies for changed values. The dependency list is already - // in order of use. Therefore if multiple dependencies have changed values, only - // the first used dependency is re-evaluated at this point. - for ( - let node = target._sources; - node !== undefined; - node = node._nextSource - ) { - // If there's a new version of the dependency before or after refreshing, - // or the dependency has something blocking it from refreshing at all (e.g. a - // dependency cycle), then we need to recompute. - if ( - node._source._version !== node._version || - !node._source._refresh() || - node._source._version !== node._version - ) { - return true; - } - } - // If none of the dependencies have changed values since last recompute then - // there's no need to recompute. - return false; -} - -function prepareSources(target: Computed | Effect) { - /** - * 1. Mark all current sources as re-usable nodes (version: -1) - * 2. Set a rollback node if the current node is being used in a different context - * 3. Point 'target._sources' to the tail of the doubly-linked list, e.g: - * - * { undefined <- A <-> B <-> C -> undefined } - * ↑ ↑ - * │ └──────┐ - * target._sources = A; (node is head) │ - * ↓ │ - * target._sources = C; (node is tail) ─┘ - */ - for ( - let node = target._sources; - node !== undefined; - node = node._nextSource - ) { - const rollbackNode = node._source._node; - if (rollbackNode !== undefined) { - node._rollbackNode = rollbackNode; - } - node._source._node = node; - node._version = -1; - - if (node._nextSource === undefined) { - target._sources = node; - break; - } - } -} - -function cleanupSources(target: Computed | Effect) { - let node = target._sources; - let head = undefined; - - /** - * At this point 'target._sources' points to the tail of the doubly-linked list. - * It contains all existing sources + new sources in order of use. - * Iterate backwards until we find the head node while dropping old dependencies. - */ - while (node !== undefined) { - const prev = node._prevSource; - - /** - * The node was not re-used, unsubscribe from its change notifications and remove itself - * from the doubly-linked list. e.g: - * - * { A <-> B <-> C } - * ↓ - * { A <-> C } - */ - if (node._version === -1) { - node._source._unsubscribe(node); - - if (prev !== undefined) { - prev._nextSource = node._nextSource; - } - if (node._nextSource !== undefined) { - node._nextSource._prevSource = prev; - } - } else { - /** - * The new head is the last node seen which wasn't removed/unsubscribed - * from the doubly-linked list. e.g: - * - * { A <-> B <-> C } - * ↑ ↑ ↑ - * │ │ └ head = node - * │ └ head = node - * └ head = node - */ - head = node; - } - - node._source._node = node._rollbackNode; - if (node._rollbackNode !== undefined) { - node._rollbackNode = undefined; - } - - node = prev; - } - - target._sources = head; -} - -declare class Computed extends Signal { - _fn: () => T; - _sources?: Node; - _globalVersion: number; - _flags: number; - - constructor(fn: () => T); - - _notify(): void; - get value(): T; -} - -function Computed(this: Computed, fn: () => unknown) { - Signal.call(this, undefined); - - this._fn = fn; - this._sources = undefined; - this._globalVersion = globalVersion - 1; - this._flags = OUTDATED; -} - -Computed.prototype = new Signal() as Computed; - -Computed.prototype._refresh = function () { - this._flags &= ~NOTIFIED; - - if (this._flags & RUNNING) { - return false; - } - - // If this computed signal has subscribed to updates from its dependencies - // (TRACKING flag set) and none of them have notified about changes (OUTDATED - // flag not set), then the computed value can't have changed. - if ((this._flags & (OUTDATED | TRACKING)) === TRACKING) { - return true; - } - this._flags &= ~OUTDATED; - - if (this._globalVersion === globalVersion) { - return true; - } - this._globalVersion = globalVersion; - - // Mark this computed signal running before checking the dependencies for value - // changes, so that the RUNNING flag can be used to notice cyclical dependencies. - this._flags |= RUNNING; - if (this._version > 0 && !needsToRecompute(this)) { - this._flags &= ~RUNNING; - return true; - } - - const prevContext = evalContext; - try { - prepareSources(this); - evalContext = this; - const value = this._fn(); - if ( - this._flags & HAS_ERROR || - this._value !== value || - this._version === 0 - ) { - this._value = value; - this._flags &= ~HAS_ERROR; - this._version++; - } - } catch (err) { - this._value = err; - this._flags |= HAS_ERROR; - this._version++; - } - evalContext = prevContext; - cleanupSources(this); - this._flags &= ~RUNNING; - return true; -}; - -Computed.prototype._subscribe = function (node) { - if (this._targets === undefined) { - this._flags |= OUTDATED | TRACKING; - - // A computed signal subscribes lazily to its dependencies when it - // gets its first subscriber. - for ( - let node = this._sources; - node !== undefined; - node = node._nextSource - ) { - node._source._subscribe(node); - } - } - Signal.prototype._subscribe.call(this, node); -}; - -Computed.prototype._unsubscribe = function (node) { - // Only run the unsubscribe step if the computed signal has any subscribers. - if (this._targets !== undefined) { - Signal.prototype._unsubscribe.call(this, node); - - // Computed signal unsubscribes from its dependencies when it loses its last subscriber. - // This makes it possible for unreferences subgraphs of computed signals to get garbage collected. - if (this._targets === undefined) { - this._flags &= ~TRACKING; - - for ( - let node = this._sources; - node !== undefined; - node = node._nextSource - ) { - node._source._unsubscribe(node); - } - } - } -}; - -Computed.prototype._notify = function () { - if (!(this._flags & NOTIFIED)) { - this._flags |= OUTDATED | NOTIFIED; - - for ( - let node = this._targets; - node !== undefined; - node = node._nextTarget - ) { - node._target._notify(); - } - } -}; - -Object.defineProperty(Computed.prototype, "value", { - get(this: Computed) { - if (this._flags & RUNNING) { - // Cycle detected - throw ERR_BAD_ARGS; - } - const node = addDependency(this); - this._refresh(); - if (node !== undefined) { - node._version = this._version; - } - if (this._flags & HAS_ERROR) { - throw this._value; - } - return this._value; - }, -}); - -/** - * An interface for read-only signals. - */ -interface ReadonlySignal { - readonly value: T; - peek(): T; - - subscribe(fn: (value: T) => void): () => void; - valueOf(): T; - toString(): string; - toJSON(): T; - brand: typeof BRAND_SYMBOL; -} - -/** - * Create a new signal that is computed based on the values of other signals. - * - * The returned computed signal is read-only, and its value is automatically - * updated when any signals accessed from within the callback function change. - * - * @param fn The effect callback. - * @returns A new read-only signal. - */ -function computed(fn: () => T): ReadonlySignal { - return new Computed(fn); -} - -function cleanupEffect(effect: Effect) { - const cleanup = effect._cleanup; - effect._cleanup = undefined; - - if (typeof cleanup === "function") { - /*@__INLINE__**/ startBatch(); - - // Run cleanup functions always outside of any context. - const prevContext = evalContext; - evalContext = undefined; - try { - cleanup!(); - } catch (err) { - effect._flags &= ~RUNNING; - effect._flags |= DISPOSED; - disposeEffect(effect); - throw err; - } finally { - evalContext = prevContext; - endBatch(); - } - } -} - -function disposeEffect(effect: Effect) { - for ( - let node = effect._sources; - node !== undefined; - node = node._nextSource - ) { - node._source._unsubscribe(node); - } - effect._fn = undefined; - effect._sources = undefined; - - cleanupEffect(effect); -} - -function endEffect(this: Effect, prevContext?: Computed | Effect) { - if (evalContext !== this) { - // Out-of-order effect - throw ERR_BAD_ARGS; - } - cleanupSources(this); - evalContext = prevContext; - - this._flags &= ~RUNNING; - if (this._flags & DISPOSED) { - disposeEffect(this); - } - endBatch(); -} - -type EffectFn = () => void | (() => void); - -declare class Effect { - _fn?: EffectFn; - _cleanup?: () => void; - _sources?: Node; - _nextBatchedEffect?: Effect; - _flags: number; - - constructor(fn: EffectFn); - - _callback(): void; - _start(): () => void; - _notify(): void; - _dispose(): void; -} - -function Effect(this: Effect, fn: EffectFn) { - this._fn = fn; - this._cleanup = undefined; - this._sources = undefined; - this._nextBatchedEffect = undefined; - this._flags = TRACKING; -} - -Effect.prototype._callback = function () { - const finish = this._start(); - try { - if (this._flags & DISPOSED) return; - if (this._fn === undefined) return; - - const cleanup = this._fn(); - if (typeof cleanup === "function") { - this._cleanup = cleanup!; - } - } finally { - finish(); - } -}; - -Effect.prototype._start = function () { - if (this._flags & RUNNING) { - // Cycle detected - throw ERR_BAD_ARGS; - } - this._flags |= RUNNING; - this._flags &= ~DISPOSED; - cleanupEffect(this); - prepareSources(this); - - /*@__INLINE__**/ startBatch(); - const prevContext = evalContext; - evalContext = this; - return endEffect.bind(this, prevContext); -}; - -Effect.prototype._notify = function () { - if (!(this._flags & NOTIFIED)) { - this._flags |= NOTIFIED; - this._nextBatchedEffect = batchedEffect; - batchedEffect = this; - } -}; - -Effect.prototype._dispose = function () { - this._flags |= DISPOSED; - - if (!(this._flags & RUNNING)) { - disposeEffect(this); - } -}; - -/** - * Create an effect to run arbitrary code in response to signal changes. - * - * An effect tracks which signals are accessed within the given callback - * function `fn`, and re-runs the callback when those signals change. - * - * The callback may return a cleanup function. The cleanup function gets - * run once, either when the callback is next called or when the effect - * gets disposed, whichever happens first. - * - * @param fn The effect callback. - * @returns A function for disposing the effect. - */ -function effect(fn: EffectFn): () => void { - const effect = new Effect(fn); - try { - effect._callback(); - } catch (err) { - effect._dispose(); - throw err; - } - // Return a bound function instead of a wrapper like `() => effect._dispose()`, - // because bound functions seem to be just as fast and take up a lot less memory. - return effect._dispose.bind(effect); -} - -export { batch, computed, effect, Signal, untracked }; -export type { ReadonlySignal }; diff --git a/code/ts/library/src/vendored/ts-merge-patch.ts b/code/ts/library/src/vendored/ts-merge-patch.ts deleted file mode 100644 index f56c79fc1..000000000 --- a/code/ts/library/src/vendored/ts-merge-patch.ts +++ /dev/null @@ -1,54 +0,0 @@ -// From https://github.com/riagominota/ts-merge-patch/blob/main/src/index.ts - -type mpObj = { [k in keyof T | string | number | symbol]: any }; -export function apply( - target: mpObj, - patchItem: mpObj, -): Partial & Partial; -export function apply(target: mpObj, patchItem: mpObj): R; -export function apply(target: mpObj, patchItem: mpObj): {}; -export function apply(target: mpObj, patchItem: null): null; -export function apply(target: mpObj, patchItem: string): string; -export function apply(target: mpObj, patchItem: number): number; -export function apply(target: mpObj, patchItem: undefined): undefined; -export function apply(target: mpObj, patchItem: R[]): R[]; - -export function apply(target: any, patchItem: any): any { - /** - * If the patch is anything other than an object, - * the result will always be to replace - * the entire target with the entire patch. - */ - if ( - typeof patchItem !== "object" || Array.isArray(patchItem) || !patchItem - ) { - return JSON.parse(JSON.stringify(patchItem)); //return new instance of variable - } - - if ( - typeof patchItem === "object" && - patchItem.toJSON !== undefined && - typeof patchItem.toJSON === "function" - ) { - return patchItem.toJSON(); - } - /** Also, it is not possible to - * patch part of a target that is not an object, - * such as to replace just some of the values in an array. - */ - let targetResult = target; - if (typeof target !== "object") { - //Target is empty/not an object, so basically becomes patch, minus any null valued sections (becomes {} + patch) - targetResult = { ...patchItem }; - } - - Object.keys(patchItem).forEach((k) => { - if (!targetResult.hasOwnProperty(k)) targetResult[k] = patchItem[k]; //This ensure the key exists and TS can't throw a wobbly over an undefined key - if (patchItem[k] === null) { - delete targetResult[k]; - } else { - targetResult[k] = apply(targetResult[k], patchItem[k]); - } - }); - return targetResult; -} diff --git a/code/dotnet/samples/CsharpAspServer/CsharpAspServer.csproj b/examples/dotnet/CsharpAspServer/CsharpAspServer.csproj similarity index 100% rename from code/dotnet/samples/CsharpAspServer/CsharpAspServer.csproj rename to examples/dotnet/CsharpAspServer/CsharpAspServer.csproj diff --git a/code/dotnet/samples/CsharpAspServer/Program.cs b/examples/dotnet/CsharpAspServer/Program.cs similarity index 84% rename from code/dotnet/samples/CsharpAspServer/Program.cs rename to examples/dotnet/CsharpAspServer/Program.cs index d87f87902..c77c61721 100644 --- a/code/dotnet/samples/CsharpAspServer/Program.cs +++ b/examples/dotnet/CsharpAspServer/Program.cs @@ -6,7 +6,7 @@ namespace CsharpAspServer; -public record DataSignalsStore : IDatastarSignalsStore +public record DataSignals : IDatastarSignals { [JsonPropertyName("input")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -28,7 +28,7 @@ public static class Program public static void Main(string[] args) { WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - builder.Services.AddDatastar(); + builder.Services.AddDatastar(); WebApplication app = builder.Build(); app.UseDefaultFiles(new DefaultFilesOptions @@ -46,16 +46,16 @@ public static void Main(string[] args) return Task.CompletedTask; }); app.MapGet("/language/{lang:required}", (string lang, IServerSentEventService sseService) => sseService.MergeFragments($"""{lang}""")); - app.MapGet("/patch", async (IServerSentEventService sseService, IDatastarSignalsStore dsStore) => + app.MapGet("/patch", async (IServerSentEventService sseService, IDatastarSignals signals) => { - DataSignalsStore signalsStore = (dsStore as DataSignalsStore) ?? throw new InvalidCastException("Unknown Datastore passed"); - DataSignalsStore mergeSignalsStore = new() { Output = $"Patched Output: {signalsStore.Input}" }; - await sseService.MergeSignals(mergeSignalsStore); + DataSignals signals = (signals as DataSignals) ?? throw new InvalidCastException("Unknown DataSignals passed"); + DataSignals mergeSignals = new() { Output = $"Patched Output: {signals.Input}" }; + await sseService.MergeSignals(mergeSignals); }); app.MapGet("/target", async (IServerSentEventService sseService) => { string today = DateTime.Now.ToString("%y-%M-%d %h:%m:%s"); - await sseService.MergeFragments($"""
{today}
"""); + await sseService.MergeFragments($"""
{today}
"""); }); app.MapGet("/removeDate", (IServerSentEventService sseService) => sseService.RemoveFragments("#date")); app.MapGet("/feed", async (IHttpContextAccessor acc, IServerSentEventService sseService, CancellationToken ct) => diff --git a/code/dotnet/samples/CsharpAspServer/Properties/launchSettings.json b/examples/dotnet/CsharpAspServer/Properties/launchSettings.json similarity index 100% rename from code/dotnet/samples/CsharpAspServer/Properties/launchSettings.json rename to examples/dotnet/CsharpAspServer/Properties/launchSettings.json diff --git a/code/dotnet/samples/CsharpAspServer/appsettings.Development.json b/examples/dotnet/CsharpAspServer/appsettings.Development.json similarity index 100% rename from code/dotnet/samples/CsharpAspServer/appsettings.Development.json rename to examples/dotnet/CsharpAspServer/appsettings.Development.json diff --git a/code/dotnet/samples/CsharpAspServer/appsettings.json b/examples/dotnet/CsharpAspServer/appsettings.json similarity index 100% rename from code/dotnet/samples/CsharpAspServer/appsettings.json rename to examples/dotnet/CsharpAspServer/appsettings.json diff --git a/code/dotnet/samples/FalcoServer/FalcoServer.fsproj b/examples/dotnet/FalcoServer/FalcoServer.fsproj similarity index 100% rename from code/dotnet/samples/FalcoServer/FalcoServer.fsproj rename to examples/dotnet/FalcoServer/FalcoServer.fsproj diff --git a/code/dotnet/samples/FalcoServer/Program.fs b/examples/dotnet/FalcoServer/Program.fs similarity index 85% rename from code/dotnet/samples/FalcoServer/Program.fs rename to examples/dotnet/FalcoServer/Program.fs index 23dd0f9d0..2c569081f 100644 --- a/code/dotnet/samples/FalcoServer/Program.fs +++ b/examples/dotnet/FalcoServer/Program.fs @@ -12,10 +12,10 @@ open global.Falco open Falco.Routing open Falco.HostBuilder -type SignalsStore = { input: string; output: string; show: bool } +type Signals = { input: string; output: string; show: bool } with static member defaults = { input = ""; output = ""; show = false } - interface IDatastarSignalsStore with + interface IDatastarSignals with member this.Serialize () = JsonSerializer.Serialize(this) [] @@ -32,7 +32,7 @@ let main args = webAppBuilder.UseDefaultFiles(defaultFileOptions).UseStaticFiles(staticFileOptions) ) - add_service (ServerSentEventServices.datastarServiceWithCustomDeserializer( JsonSerializer.Deserialize )) + add_service (ServerSentEventServices.datastarServiceWithCustomDeserializer( JsonSerializer.Deserialize )) endpoints [ get "/" (Response.redirectTemporarily "index.html") @@ -41,13 +41,13 @@ let main args = (fun route -> route.GetString "lang") (fun lang -> Response.sseMergeFragments (fun _ -> $"""{lang}""")) ) - get "/patch" (Response.sseMergeSignals (fun signalsStore -> { signalsStore with output = $"Patched Output: {signalsStore.input}" } )) + get "/patch" (Response.sseMergeSignals (fun signals -> { signals with output = $"Patched Output: {signals.input}" } )) get "/target" (Response.sseMergeFragments (fun _ -> let today = System.DateTime.Now.ToString("%y-%M-%d %h:%m:%s"); - $"""
{today}
""" + $"""
{today}
""" )) get "/removeDate" (Response.sseRemoveFragments (fun _ -> "#date")) - get "/feed" (Response.sseGenerator (fun ctx sseService signalsStore -> task { + get "/feed" (Response.sseGenerator (fun ctx sseService signals -> task { let requestCanceled = ctx.RequestAborted while (requestCanceled.IsCancellationRequested |> not) do let rand = Random.Shared.NextInt64(1000000000000000000L, 5999999999999999999L); @@ -56,4 +56,4 @@ let main args = })) ] } - 0 \ No newline at end of file + 0 diff --git a/code/dotnet/samples/FalcoServer/Properties/launchSettings.json b/examples/dotnet/FalcoServer/Properties/launchSettings.json similarity index 100% rename from code/dotnet/samples/FalcoServer/Properties/launchSettings.json rename to examples/dotnet/FalcoServer/Properties/launchSettings.json diff --git a/code/dotnet/samples/FalcoServer/appsettings.Development.json b/examples/dotnet/FalcoServer/appsettings.Development.json similarity index 100% rename from code/dotnet/samples/FalcoServer/appsettings.Development.json rename to examples/dotnet/FalcoServer/appsettings.Development.json diff --git a/code/dotnet/samples/FalcoServer/appsettings.json b/examples/dotnet/FalcoServer/appsettings.json similarity index 100% rename from code/dotnet/samples/FalcoServer/appsettings.json rename to examples/dotnet/FalcoServer/appsettings.json diff --git a/code/dotnet/samples/Samples.sln b/examples/dotnet/Samples.sln similarity index 97% rename from code/dotnet/samples/Samples.sln rename to examples/dotnet/Samples.sln index b20269491..4c6f7cfad 100644 --- a/code/dotnet/samples/Samples.sln +++ b/examples/dotnet/Samples.sln @@ -6,7 +6,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CsharpAspServer", "CsharpAs EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildFiles", "BuildFiles", "{01B23A4D-E3AD-4C90-A321-F8FE8D60F1BE}" ProjectSection(SolutionItems) = preProject - ..\..\go\tsbuild\consts_dotnet.qtpl = ..\..\go\tsbuild\consts_dotnet.qtpl + ..\..\go\build\consts_dotnet.qtpl = ..\..\go\build\consts_dotnet.qtpl ..\Build.ps1 = ..\Build.ps1 EndProjectSection EndProject diff --git a/examples/dotnet/Shared/wwwroot/index.html b/examples/dotnet/Shared/wwwroot/index.html new file mode 100644 index 000000000..d45fa060f --- /dev/null +++ b/examples/dotnet/Shared/wwwroot/index.html @@ -0,0 +1,59 @@ + + + + dotnet Example + + + + +
+
+
+

+ ___ + + D* Example +

+
+
+
+ 🚀 +
+ + 5999999999999999999 + +
+
+
+
+
+
+
+
+
+
+
+
+

+        
+
+
+ + diff --git a/code/dotnet/samples/Shared/wwwroot/style.css b/examples/dotnet/Shared/wwwroot/style.css similarity index 100% rename from code/dotnet/samples/Shared/wwwroot/style.css rename to examples/dotnet/Shared/wwwroot/style.css diff --git a/code/dotnet/assets/datastar_icon.png b/examples/dotnet/assets/datastar_icon.png similarity index 100% rename from code/dotnet/assets/datastar_icon.png rename to examples/dotnet/assets/datastar_icon.png diff --git a/code/ts/library/.gitignore b/library/.gitignore similarity index 100% rename from code/ts/library/.gitignore rename to library/.gitignore diff --git a/code/ts/library/README.md b/library/README.md similarity index 59% rename from code/ts/library/README.md rename to library/README.md index 2b42a3b8d..2ca5d2027 100644 --- a/code/ts/library/README.md +++ b/library/README.md @@ -4,20 +4,29 @@ ![Discord](https://img.shields.io/discord/1296224603642925098) ![GitHub Repo stars](https://img.shields.io/github/stars/starfederation/datastar?style=flat) -

+

# Datastar -### A real-time hypermedia framework. +### The hypermedia framework. -Datastar helps you build real-time web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. +Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. + +Getting started is as easy as adding a single script tag to your HTML. + +```html + +``` + +Then start adding frontend reactivity using declarative `data-*` attributes. +Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. Here’s what frontend reactivity looks like using Datastar: ```html - -
- + +
+ ``` Visit the [Datastar Website »](https://data-star.dev/) diff --git a/code/ts/library/package.json b/library/package.json similarity index 95% rename from code/ts/library/package.json rename to library/package.json index 0432e60a9..a8983bd5e 100644 --- a/code/ts/library/package.json +++ b/library/package.json @@ -2,7 +2,7 @@ "name": "@starfederation/datastar", "author": "Delaney Gillilan", "description": "Hypermedia first SPA replacement framework", - "version": "0.20.1", + "version": "0.21.0-beta1", "license": "MIT", "private": false, "homepage": "https://data-star.dev", diff --git a/code/ts/library/src/bundles/datastar-core.ts b/library/src/bundles/datastar-core.ts similarity index 100% rename from code/ts/library/src/bundles/datastar-core.ts rename to library/src/bundles/datastar-core.ts diff --git a/library/src/bundles/datastar.ts b/library/src/bundles/datastar.ts new file mode 100644 index 000000000..66bd3440c --- /dev/null +++ b/library/src/bundles/datastar.ts @@ -0,0 +1,55 @@ +import { Datastar } from "../engine"; +import { ServerSentEvents as SSE } from "../plugins/official/backend/actions/sse"; +import { Indicator } from "../plugins/official/backend/attributes/indicator"; +import { ExecuteScript } from "../plugins/official/backend/watchers/executeScript"; +import { MergeFragments } from "../plugins/official/backend/watchers/mergeFragments"; +import { MergeSignals } from "../plugins/official/backend/watchers/mergeSignals"; +import { RemoveFragments } from "../plugins/official/backend/watchers/removeFragments"; +import { RemoveSignals } from "../plugins/official/backend/watchers/removeSignals"; +import { Clipboard } from "../plugins/official/browser/actions/clipboard"; +import { Intersects } from "../plugins/official/browser/attributes/intersects"; +import { Persist } from "../plugins/official/browser/attributes/persist"; +import { ReplaceUrl } from "../plugins/official/browser/attributes/replaceUrl"; +import { ScrollIntoView } from "../plugins/official/browser/attributes/scrollIntoView"; +import { Show } from "../plugins/official/browser/attributes/show"; +import { ViewTransition } from "../plugins/official/browser/attributes/viewTransition"; +import { Attributes } from "../plugins/official/dom/attributes/attributes"; +import { Bind } from "../plugins/official/dom/attributes/bind"; +import { Class } from "../plugins/official/dom/attributes/class"; +import { On } from "../plugins/official/dom/attributes/on"; +import { Ref } from "../plugins/official/dom/attributes/ref"; +import { Text } from "../plugins/official/dom/attributes/text"; +import { Fit } from "../plugins/official/logic/actions/fit"; +import { SetAll } from "../plugins/official/logic/actions/setAll"; +import { ToggleAll } from "../plugins/official/logic/actions/toggleAll"; + +Datastar.load( + // Plugins that can create signals must be loaded first + Attributes, + Bind, + Indicator, + Ref, + // DOM + Class, + On, + Show, + Text, + // Backend + SSE, + MergeFragments, + MergeSignals, + RemoveFragments, + RemoveSignals, + ExecuteScript, + // Browser + Clipboard, + Intersects, + Persist, + ReplaceUrl, + ScrollIntoView, + ViewTransition, + // Logic + Fit, + SetAll, + ToggleAll, +); diff --git a/code/ts/library/src/engine/consts.ts b/library/src/engine/consts.ts similarity index 92% rename from code/ts/library/src/engine/consts.ts rename to library/src/engine/consts.ts index 3ecee9bc4..ba26b4dfd 100644 --- a/code/ts/library/src/engine/consts.ts +++ b/library/src/engine/consts.ts @@ -3,7 +3,7 @@ export const DATASTAR = "datastar"; export const DATASTAR_EVENT = "datastar-event"; export const DATASTAR_REQUEST = "Datastar-Request"; -export const VERSION = "0.20.1"; +export const VERSION = "0.21.0-beta1"; // #region Defaults @@ -30,7 +30,7 @@ export const DefaultExecuteScriptAttributes = "type module"; // Should fragments be merged using the ViewTransition API? export const DefaultFragmentsUseViewTransitions = false; -// Should a given set of signals merge if they are missing from the store? +// Should a given set of signals merge if they are missing? export const DefaultMergeSignalsOnlyIfMissing = false; // Should script element remove itself after execution? @@ -68,11 +68,11 @@ export const DefaultFragmentMergeMode = FragmentMergeModes.Morph; export const EventTypes = { // An event for merging HTML fragments into the DOM. MergeFragments: "datastar-merge-fragments", - // An event for merging signals into the store. + // An event for merging signals. MergeSignals: "datastar-merge-signals", // An event for removing HTML fragments from the DOM. RemoveFragments: "datastar-remove-fragments", - // An event for removing signals from the store. + // An event for removing signals. RemoveSignals: "datastar-remove-signals", // An event for executing + + D* Demo - - -
- -
-
- - -
- + + +
+ +
+
+ + +
+ ``` # C# Backend + ```csharp using StarFederation.Datastar; using StarFederation.Datastar.DependencyInjection; using System.Text.Json; using System.Text.Json.Serialization; ... -// define your signals store -public record DatastarSignalsStore : IDatastarSignalsStore +// define your signals +public record DatastarSignals : IDatastarSignals { [JsonPropertyName("input")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -44,8 +54,8 @@ public record DatastarSignalsStore : IDatastarSignalsStore ... // add as an ASP Service // allows injection of IServerSentEventGenerator, to respond to a request with a Datastar friendly ServerSentEvent -// and IDatastarSignalsStore, to read what is in the data-merge-signals of the client -builder.Services.AddDatastar(); +// and IDatastarSignals, to read the signals sent by the client +builder.Services.AddDatastar(); ... app.UseStaticFiles(); @@ -53,13 +63,13 @@ app.UseStaticFiles(); app.MapGet("/displayDate", async (IServerSentEventGenerator sse) => { string today = DateTime.Now.ToString("%y-%M-%d %h:%m:%s"); - await sse.MergeFragments($"""
{today}
"""); + await sse.MergeFragments($"""
{today}
"""); }); app.MapGet("/removeDate", async (IServerSentEventGenerator sse) => { await sse.RemoveFragments("#date"); }); -app.MapPost("/changeOutput", async (IServerSentEventGenerator sse, IDatastarSignalsStore dsStore) => +app.MapPost("/changeOutput", async (IServerSentEventGenerator sse, IDatastarSignals signals) => { - DatastarSignalsStore signalStore = (dsStore as DatastarSignalsStore) ?? throw new InvalidCastException("Unknown IDatastarSignalsStore passed"); - DatastarSignalsStore newSignalsStore = new() { Output = $"Your Input: {signalStore.Input}" }; - await sse.MergeSignals(newSignalsStore); + DatastarSignals signals = (signals as DatastarSignals) ?? throw new InvalidCastException("Unknown IDatastarSignals passed"); + DatastarSignals newSignals = new() { Output = $"Your Input: {signals.Input}" }; + await sse.MergeSignals(newSignals); }); ``` diff --git a/code/dotnet/sdk/src/.gitattributes b/sdk/dotnet/src/.gitattributes similarity index 100% rename from code/dotnet/sdk/src/.gitattributes rename to sdk/dotnet/src/.gitattributes diff --git a/code/dotnet/sdk/src/Consts.fs b/sdk/dotnet/src/Consts.fs similarity index 92% rename from code/dotnet/sdk/src/Consts.fs rename to sdk/dotnet/src/Consts.fs index c32223fab..387982562 100644 --- a/code/dotnet/sdk/src/Consts.fs +++ b/sdk/dotnet/src/Consts.fs @@ -25,11 +25,11 @@ type FragmentMergeMode = type EventType = /// An event for merging HTML fragments into the DOM. | MergeFragments -/// An event for merging signals into the store. +/// An event for merging signals. | MergeSignals /// An event for removing HTML fragments from the DOM. | RemoveFragments -/// An event for removing signals from the store. +/// An event for removing signals. | RemoveSignals /// An event for executing <script/> elements in the browser. | ExecuteScript @@ -37,9 +37,9 @@ type EventType = module Consts = let [] DatastarKey = "datastar" - let [] Version = "0.20.1" - let [] VersionClientByteSize = 35928 - let [] VersionClientByteSizeGzip = 12637 + let [] Version = "0.21.0-beta1" + let [] VersionClientByteSize = 33186 + let [] VersionClientByteSizeGzip = 12206 /// Default: TimeSpan.FromMilliseconds 300 let DefaultSettleDuration = TimeSpan.FromMilliseconds 300 diff --git a/code/dotnet/sdk/src/Datastar.fsproj b/sdk/dotnet/src/Datastar.fsproj similarity index 100% rename from code/dotnet/sdk/src/Datastar.fsproj rename to sdk/dotnet/src/Datastar.fsproj diff --git a/code/dotnet/sdk/src/DependencyInjection/ServerSentEventService.fs b/sdk/dotnet/src/DependencyInjection/ServerSentEventService.fs similarity index 93% rename from code/dotnet/sdk/src/DependencyInjection/ServerSentEventService.fs rename to sdk/dotnet/src/DependencyInjection/ServerSentEventService.fs index f8e22ffdf..a9925b0c9 100644 --- a/code/dotnet/sdk/src/DependencyInjection/ServerSentEventService.fs +++ b/sdk/dotnet/src/DependencyInjection/ServerSentEventService.fs @@ -13,10 +13,10 @@ type IServerSentEventService = abstract MergeFragments: fragment:string * options:ServerSentEventMergeFragmentsOptions -> Task abstract RemoveFragments: selector:Selector -> Task abstract RemoveFragments: selector:Selector * options:ServerSentEventRemoveFragmentsOptions -> Task - abstract MergeSignals: dataSignals:IDatastarSignalsStore -> Task - abstract MergeSignals: dataSignals:IDatastarSignalsStore * options:ServerSentEventOptions -> Task - abstract MergeSignals: dataSignals:IDatastarSignalsStore * onlyIfMissing:bool -> Task - abstract MergeSignals: dataSignals:IDatastarSignalsStore * onlyIfMissing:bool * options:ServerSentEventOptions -> Task + abstract MergeSignals: dataSignals:IDatastarSignals -> Task + abstract MergeSignals: dataSignals:IDatastarSignals * options:ServerSentEventOptions -> Task + abstract MergeSignals: dataSignals:IDatastarSignals * onlyIfMissing:bool -> Task + abstract MergeSignals: dataSignals:IDatastarSignals * onlyIfMissing:bool * options:ServerSentEventOptions -> Task abstract RemoveSignals: paths:DataSignalPath seq -> Task abstract RemoveSignals: paths:DataSignalPath seq * options:ServerSentEventOptions -> Task abstract ExecuteScript: script:string -> Task diff --git a/code/dotnet/sdk/src/DependencyInjection/ServerSentEventServices.fs b/sdk/dotnet/src/DependencyInjection/ServerSentEventServices.fs similarity index 69% rename from code/dotnet/sdk/src/DependencyInjection/ServerSentEventServices.fs rename to sdk/dotnet/src/DependencyInjection/ServerSentEventServices.fs index 108c89d55..75055f9eb 100644 --- a/code/dotnet/sdk/src/DependencyInjection/ServerSentEventServices.fs +++ b/sdk/dotnet/src/DependencyInjection/ServerSentEventServices.fs @@ -18,7 +18,7 @@ module ServerSentEventServices = serviceCollection - let datastarServiceWithCustomDeserializer<'T when 'T :> IDatastarSignalsStore> (signalStoreDeserializer:string -> 'T) (serviceCollection:IServiceCollection) = + let datastarServiceWithCustomDeserializer<'T when 'T :> IDatastarSignals> (signalDeserializer:string -> 'T) (serviceCollection:IServiceCollection) = serviceCollection.AddHttpContextAccessor() |> ignore serviceCollection.AddScoped(fun (svcPvd:IServiceProvider) -> @@ -26,17 +26,17 @@ module ServerSentEventServices = ServerSentEventService(httpContext) ) |> ignore - serviceCollection.AddScoped(fun (svcPvd:IServiceProvider) -> + serviceCollection.AddScoped(fun (svcPvd:IServiceProvider) -> let httpContextAccessor = svcPvd.GetService() - let rawSignals = ServerSentEventHttpHandler.ReadRawSignalStore(httpContextAccessor.HttpContext.Request).GetAwaiter().GetResult() + let rawSignals = ServerSentEventHttpHandler.ReadRawSignals(httpContextAccessor.HttpContext.Request).GetAwaiter().GetResult() match rawSignals with - | Ok rawSignals' -> signalStoreDeserializer(rawSignals') - | Error _ -> signalStoreDeserializer("{}") + | Ok rawSignals' -> signalDeserializer(rawSignals') + | Error _ -> signalDeserializer("{}") ) |> ignore serviceCollection - let datastarService<'T when 'T :> IDatastarSignalsStore> (serviceCollection:IServiceCollection) = + let datastarService<'T when 'T :> IDatastarSignals> (serviceCollection:IServiceCollection) = datastarServiceWithCustomDeserializer JsonSerializer.Deserialize<'T> serviceCollection [] @@ -47,9 +47,9 @@ type ServiceCollectionExtensionMethods() = ServerSentEventServices.datastarServiceWithoutSignals serviceCollection [] - static member AddDatastar<'T when 'T :> IDatastarSignalsStore> serviceCollection = + static member AddDatastar<'T when 'T :> IDatastarSignals> serviceCollection = ServerSentEventServices.datastarService<'T> serviceCollection [] - static member AddDatastar<'T when 'T :> IDatastarSignalsStore> (serviceCollection:ServiceCollection, signalsStoreDeserializer:Func) = - ServerSentEventServices.datastarServiceWithCustomDeserializer<'T> signalsStoreDeserializer.Invoke serviceCollection + static member AddDatastar<'T when 'T :> IDatastarSignals> (serviceCollection:ServiceCollection, signalsDeserializer:Func) = + ServerSentEventServices.datastarServiceWithCustomDeserializer<'T> signalsDeserializer.Invoke serviceCollection diff --git a/sdk/dotnet/src/Falco/Response.fs b/sdk/dotnet/src/Falco/Response.fs new file mode 100644 index 000000000..042cb33bd --- /dev/null +++ b/sdk/dotnet/src/Falco/Response.fs @@ -0,0 +1,39 @@ +module StarFederation.Datastar.Falco.Response + +open System.Threading.Tasks +open Microsoft.AspNetCore.Http +open StarFederation.Datastar +open StarFederation.Datastar.DependencyInjection + +let sseMergeFragments<'T when 'T :> IDatastarSignals> (fragment:'T -> string) = fun (ctx:HttpContext) -> + let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService + let signals = ctx.RequestServices.GetService(typedefof) :?> 'T + sseService.MergeFragments (fragment signals) + +let sseRemoveFragments<'T when 'T :> IDatastarSignals> (selector:'T -> string) = fun (ctx:HttpContext) -> + let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService + let signals = ctx.RequestServices.GetService(typedefof) :?> 'T + sseService.RemoveFragments (selector signals) + +let sseMergeSignals<'T when 'T :> IDatastarSignals> (newSignals:'T -> IDatastarSignals) = fun (ctx:HttpContext) -> + let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService + let signals = ctx.RequestServices.GetService(typedefof) :?> 'T + sseService.MergeSignals (newSignals signals) + +let sseRemoveSignals<'T when 'T :> IDatastarSignals> (signals:'T -> string[]) = fun (ctx:HttpContext) -> + let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService + let signals = ctx.RequestServices.GetService(typedefof) :?> 'T + sseService.RemoveSignals (signals signals) + +let sseGenerator<'T when 'T :> IDatastarSignals> (feed:HttpContext -> IServerSentEventService -> 'T -> Task) = (fun (ctx:HttpContext) -> + task { + let sseService = ctx.RequestServices.GetService(typedefof) :?> IServerSentEventService + let signals = ctx.RequestServices.GetService(typedefof) :?> 'T + try + do! feed ctx sseService signals + finally + try + ctx.Connection.RequestClose() + finally () + } :> Task + ) diff --git a/code/dotnet/sdk/src/Scripts/ServerSentEventExtensions.fs b/sdk/dotnet/src/Scripts/ServerSentEventExtensions.fs similarity index 100% rename from code/dotnet/sdk/src/Scripts/ServerSentEventExtensions.fs rename to sdk/dotnet/src/Scripts/ServerSentEventExtensions.fs diff --git a/code/dotnet/sdk/src/Scripts/ServerSentEventGeneratorExtensions.fs b/sdk/dotnet/src/Scripts/ServerSentEventGeneratorExtensions.fs similarity index 100% rename from code/dotnet/sdk/src/Scripts/ServerSentEventGeneratorExtensions.fs rename to sdk/dotnet/src/Scripts/ServerSentEventGeneratorExtensions.fs diff --git a/code/dotnet/sdk/src/ServerSentEvent.fs b/sdk/dotnet/src/ServerSentEvent.fs similarity index 88% rename from code/dotnet/sdk/src/ServerSentEvent.fs rename to sdk/dotnet/src/ServerSentEvent.fs index 87a747c95..1a2b2736a 100644 --- a/code/dotnet/sdk/src/ServerSentEvent.fs +++ b/sdk/dotnet/src/ServerSentEvent.fs @@ -7,9 +7,9 @@ open System.Threading.Tasks open StarFederation.Datastar.Utility type ISendServerEvent = abstract SendServerEvent: string -> Task -type IReadRawSignalsStore = abstract ReadRawSignalStore: unit -> ValueTask> +type IReadRawSignals = abstract ReadRawSignals: unit -> ValueTask> -type IDatastarSignalsStore = +type IDatastarSignals = abstract Serialize : unit -> string type ServerSentEvent = @@ -33,14 +33,14 @@ module ServerSentEvent = ""; "" } |> String.concat "\n" -module DatastarSignalStore = - let private readRawSignals (env:IReadRawSignalsStore) = env.ReadRawSignalStore() +module DatastarSignals = + let private readRawSignals (env:IReadRawSignals) = env.ReadRawSignals() - let readSignalsWithDeserialize<'T when 'T :> IDatastarSignalsStore> (deserialize:string -> Result<'T, exn>) env = task { + let readSignalsWithDeserialize<'T when 'T :> IDatastarSignals> (deserialize:string -> Result<'T, exn>) env = task { let! rawSignal = readRawSignals env return rawSignal |> Result.bind deserialize } - let readSignals<'T when 'T :> IDatastarSignalsStore> env = readSignalsWithDeserialize (tryDeserialize JsonSerializer.Deserialize<'T>) env + let readSignals<'T when 'T :> IDatastarSignals> env = readSignalsWithDeserialize (tryDeserialize JsonSerializer.Deserialize<'T>) env type DataSignalPath = string module DataSignalPath = diff --git a/code/dotnet/sdk/src/ServerSentEventGenerator.fs b/sdk/dotnet/src/ServerSentEventGenerator.fs similarity index 98% rename from code/dotnet/sdk/src/ServerSentEventGenerator.fs rename to sdk/dotnet/src/ServerSentEventGenerator.fs index e25ee1dfc..3bc5d5752 100644 --- a/code/dotnet/sdk/src/ServerSentEventGenerator.fs +++ b/sdk/dotnet/src/ServerSentEventGenerator.fs @@ -35,7 +35,7 @@ module ServerSentEventGenerator = |> send env let removeFragments env = removeFragmentsWithOptions RemoveFragmentsOptions.defaults env - let mergeSignalsWithOptions options env onlyIfMissing (mergeSignalData:IDatastarSignalsStore) : Task = + let mergeSignalsWithOptions options env onlyIfMissing (mergeSignalData:IDatastarSignals) : Task = { EventType = MergeSignals Id = options.EventId Retry = options.Retry diff --git a/code/dotnet/sdk/src/ServerSentEventHttpHandler.fs b/sdk/dotnet/src/ServerSentEventHttpHandler.fs similarity index 87% rename from code/dotnet/sdk/src/ServerSentEventHttpHandler.fs rename to sdk/dotnet/src/ServerSentEventHttpHandler.fs index 76384a68d..20a6b032b 100644 --- a/code/dotnet/sdk/src/ServerSentEventHttpHandler.fs +++ b/sdk/dotnet/src/ServerSentEventHttpHandler.fs @@ -10,7 +10,7 @@ open Microsoft.Net.Http.Headers type IServerSentEventHandler = interface inherit ISendServerEvent - inherit IReadRawSignalsStore + inherit IReadRawSignals end type ServerSentEventHttpHandler(httpContext:HttpContext) = @@ -28,7 +28,7 @@ type ServerSentEventHttpHandler(httpContext:HttpContext) = member _.HttpContext = httpContext - static member ReadRawSignalStore (httpRequest:HttpRequest) : ValueTask> = + static member ReadRawSignals (httpRequest:HttpRequest) : ValueTask> = let retrieveTask = match httpRequest.Method with | System.Net.WebRequestMethods.Http.Get -> @@ -56,5 +56,5 @@ type ServerSentEventHttpHandler(httpContext:HttpContext) = let bytes = Encoding.UTF8.GetBytes(event) this.HttpContext.Response.BodyWriter.WriteAsync(bytes).AsTask() - interface IReadRawSignalsStore with - member this.ReadRawSignalStore () = ServerSentEventHttpHandler.ReadRawSignalStore(this.HttpContext.Request) + interface IReadRawSignals with + member this.ReadRawSignals () = ServerSentEventHttpHandler.ReadRawSignals(this.HttpContext.Request) diff --git a/code/dotnet/sdk/src/Utility.fs b/sdk/dotnet/src/Utility.fs similarity index 100% rename from code/dotnet/sdk/src/Utility.fs rename to sdk/dotnet/src/Utility.fs diff --git a/code/go/sdk/.gitattributes b/sdk/go/.gitattributes similarity index 100% rename from code/go/sdk/.gitattributes rename to sdk/go/.gitattributes diff --git a/code/go/sdk/consts.go b/sdk/go/consts.go similarity index 92% rename from code/go/sdk/consts.go rename to sdk/go/consts.go index 463714cd6..e6c57a3d8 100644 --- a/code/go/sdk/consts.go +++ b/sdk/go/consts.go @@ -6,9 +6,9 @@ import "time" const ( DatastarKey = "datastar" - Version = "0.20.1" - VersionClientByteSize = 35928 - VersionClientByteSizeGzip = 12637 + Version = "0.21.0-beta1" + VersionClientByteSize = 33186 + VersionClientByteSizeGzip = 12206 //region Default durations @@ -47,7 +47,7 @@ var ( // Should fragments be merged using the ViewTransition API? DefaultFragmentsUseViewTransitions = false - // Should a given set of signals merge if they are missing from the store? + // Should a given set of signals merge if they are missing? DefaultMergeSignalsOnlyIfMissing = false // Should script element remove itself after execution? @@ -100,13 +100,13 @@ const ( // An event for merging HTML fragments into the DOM. EventTypeMergeFragments EventType = "datastar-merge-fragments" - // An event for merging signals into the store. + // An event for merging signals. EventTypeMergeSignals EventType = "datastar-merge-signals" // An event for removing HTML fragments from the DOM. EventTypeRemoveFragments EventType = "datastar-remove-fragments" - // An event for removing signals from the store. + // An event for removing signals. EventTypeRemoveSignals EventType = "datastar-remove-signals" // An event for executing ` }} + {{ + usageSample := ` +
+` + }} @Page( - "Datastar - A real-time hypermedia framework", - "Datastar helps you build real-time web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework.", + "Datastar - The hypermedia framework.", + "Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework.", "/", ) {
- +
Datastar
v{ datastar.Version }
- A real-time hypermedia framework + The hypermedia framework.

- Datastar helps you build real-time web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. + Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework.

{ humanize.CommafWithDigits( datastar.VersionClientByteSizeGzip / 1024.0, 1) }KiB - file and start adding reactivity to your frontend immediately. Write your backend in the language of your choice! Official SDKs are currently available to help you get up and running even faster. More languages are on the way now that we have a stable API! + file and start adding reactivity to your frontend immediately. Write your backend in the language of your choice! Official SDKs are available to help you get up and running even faster, with more languages on the way.
- for _, lang := range tsbuild.Consts.SDKLanguages { + for _, lang := range build.Consts.SDKLanguages { @icon(lang.Icon, "class", "text-9xl")
{ lang.Name }
@@ -67,16 +72,28 @@ templ Home() {
+ +
{ cdnText }
+
+
+
+
+

Then start adding frontend reactivity using declarative data-* attributes.

+

- We are by far the smallest and fastest full-featured framework, even with everything included. But don't fret, we provide a “pay what for what you use” bundling system for when you want a custom build. + We are by far the smallest and fastest full-featured framework, even with everything included. But no worries, we provide a “pay what for what you use” bundling system for when you want a custom build.''

-
+

Todos Example

If you are seeing this message, please clear your cookies and refresh the page.

We recently updated the site and the old cookies are causing issues.

@@ -154,7 +174,7 @@ templ TodosMVCView(mvc *TodoMVC) {

This mini application is driven by a @@ -168,7 +188,7 @@ templ TodosMVCView(mvc *TodoMVC) {

example

- The input is bound to a local store, but this is not a single page application. It is like having HTMX + Alpine.js but with just one API to learn and much easier to extend. + The input is bound to a local signals, but this is not a single page application. It is like having HTMX + Alpine.js but with just one API to learn and much easier to extend.

if hasTodos { @@ -176,10 +196,10 @@ templ TodosMVCView(mvc *TodoMVC) { @@ -217,7 +237,7 @@ templ TodosMVCView(mvc *TodoMVC) { } else { diff --git a/code/go/site/routes_memes.go b/site/routes_memes.go similarity index 100% rename from code/go/site/routes_memes.go rename to site/routes_memes.go diff --git a/code/go/site/routes_memes.templ b/site/routes_memes.templ similarity index 100% rename from code/go/site/routes_memes.templ rename to site/routes_memes.templ diff --git a/code/go/site/routes_reference.go b/site/routes_reference.go similarity index 91% rename from code/go/site/routes_reference.go rename to site/routes_reference.go index d270dc454..b9274d1cf 100644 --- a/code/go/site/routes_reference.go +++ b/site/routes_reference.go @@ -21,16 +21,10 @@ func setupReferenceRoutes(ctx context.Context, router chi.Router) error { Label: "Plugins Included", Links: []*SidebarLink{ {ID: "plugins_core"}, - {ID: "plugins_attributes"}, + {ID: "plugins_dom"}, + {ID: "plugins_browser"}, {ID: "plugins_backend"}, - {ID: "plugins_helpers"}, - {ID: "plugins_visibility"}, - }, - }, - { - Label: "How it Works", - Links: []*SidebarLink{ - {ID: "expressions"}, + {ID: "plugins_logic"}, }, }, } diff --git a/code/go/site/shared.templ b/site/shared.templ similarity index 91% rename from code/go/site/shared.templ rename to site/shared.templ index f7d5666bb..5f04c16c9 100644 --- a/code/go/site/shared.templ +++ b/site/shared.templ @@ -3,7 +3,7 @@ package site import ( "fmt" "github.com/delaneyj/toolbelt" - datastar "github.com/starfederation/datastar/code/go/sdk" + datastar "github.com/starfederation/datastar/sdk/go" "net/http" "strings" ) @@ -49,10 +49,10 @@ templ Page(title, description string, uri string) { } - // data-on-pageshow.window is to combat Safari's aggressive caching + // data-on-pageshow:window is to combat Safari's aggressive caching // https://stackoverflow.com/questions/8788802/prevent-safari-loading-from-cache-when-back-button-is-clicked { children... } @@ -184,13 +184,13 @@ templ SidebarPage( templ sidebarPageContents(sidebarGroups []*SidebarGroup, current *SidebarLink, contents string) {
-