--- url: 'https://garrettcannon.dev/gg-wf-scripts/api/init.md' --- # `init(options)` Returns an app instance with `addQuery`, `addAction`, `addFormAction`, `onError`, `start`, and `dispose` methods. ```js import { init } from "gg-wf-scripts"; const app = init({ context: { sb }, auth: { /* see Auth adapter below */ }, debug: false, }); ``` ## Options | Option | Type | Required | Description | |---|---|---|---| | `context` | `object` | No | Arbitrary object passed to every query and action. Put backend clients or anything else your handlers need on it. Defaults to `{}`. | | `auth` | `object` | No | Auth adapter (see below). If omitted, `gg-auth` / `gg-role` attrs are never set. | | `debug` | `boolean` | No | When `true`, every query and action is logged to the console (trigger/container, data, result, duration). Defaults to `false`. | | `expose` | `boolean` | No | When `true` (default), the app is exposed as `window.ggApp` and a `gg-app-ready` CustomEvent is dispatched on `document`. Set to `false` for tests, multiple instances, or non-browser hosts. | | `transition` | `object` | No | Global fade-in/out for every show/hide the library performs. See [Transitions](#transitions) below. Omit for instant toggles (default). | ## Transitions Every show/hide the library performs — `gg-auth` / `gg-role` gating, `gg-switch-state` cases, `gg-visible-when` form fields, and freshly-rendered `gg-data-list` rows — runs through a single visibility helper. By default it's an instant `display:none` toggle (matching pre-6.0 behavior). Pass a `transition` to fade them instead. ```js init({ transition: { duration: 200, easing: "easeInOut" }, }); ``` | Field | Type | Description | |---|---|---| | `duration` | `number` | Fade duration in **milliseconds**. Defaults to `0` (instant). | | `easing` | `Easing` | Motion's easing keyword set: `"linear"`, `"easeIn"`, `"easeOut"`, `"easeInOut"` (default), `"circIn"` / `"circOut"` / `"circInOut"`, `"backIn"` / `"backOut"` / `"backInOut"`, `"anticipate"`. Or a `[x1, y1, x2, y2]` cubic-bezier tuple. | The system honors `prefers-reduced-motion: reduce` — animations are skipped automatically when the OS setting is on, regardless of `duration`. Hidden elements also get `inert` and `aria-hidden="true"` so they're removed from the tab order and from screen readers — this happens whether or not `transition` is configured. ## Auth adapter | Option | Type | Description | |---|---|---| | `auth.getUser` | `() => string \| null \| Promise` | Returns the current user id, or `null` when signed out. Called once on start. | | `auth.onChange` | `(cb: (userId: string \| null) => void) => void` | Subscribe to auth changes. Optional but recommended — without it, `gg-auth` won't update on sign-in/out. | | `auth.roleQuery` | `async (context, userId) => string \| null` | Returns the user's role string. Called on every auth change. If omitted, `gg-role` is never set. | See [Auth and roles](/attributes/auth) for a full Supabase example. ## `app.start()` Initializes all engines and starts listening for DOM events. Call this **after** registering all queries and actions. ```js app.addQuery(/* ... */); app.addAction(/* ... */); app.addFormAction(/* ... */); app.start(); ``` Engines also pick up elements inserted **after** `start()` — Webflow IX-driven content, CMS templates, or anything appended at runtime — via a shared `MutationObserver`. You don't need to re-run `start()`. ## `app.onError(handler)` Subscribe to handler failures. Fires when: * A registered query, action, or form action throws. * The DOM references a handler id that wasn't registered. * A handler returns `{ ok: false, error }` (or `{ ok: false, field_errors }` for form actions). ```js app.onError(({ prefix, id, error }) => { Sentry.captureException(error, { tags: { handler: `${prefix} ${id}` } }); }); ``` The handler receives a `GgErrorEvent`: | Field | Type | Description | |---|---|---| | `prefix` | `string` | Engine label, e.g. `"[gg-action]"`, `"[gg-data]"`, `"[gg-form-action]"`. | | `id` | `string` | Handler id (the value of the `gg-action` / `gg-data` / `gg-form-action` attribute). | | `error` | `unknown` | The thrown value, or a string describing the non-throw failure. | | `fields` | `object` | Engine-specific context: trigger element, form, params, data. | Returns an unsubscribe function. ## `app.dispose()` Detaches every listener and observer the library installed. Call from SPA route changes, HMR teardown, or test cleanup. Handler registrations are kept, so you can call `start()` again on the same app. ```js const app = init({ context }); app.addQuery(/* ... */); app.start(); // later, e.g. on route change app.dispose(); ``` --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/attributes/actions.md' --- # Actions Run mutations on click. Actions receive the context object and a data object. ```html ``` When an action is inside a `gg-data` or `gg-data-list` container, it automatically receives the record as its data. Explicit `gg-action-data` values are merged on top (and win on conflict). ## Handler signature Action functions should return `{ ok: true }` or `{ ok: false, error }`: ```js app.addAction("delete_post", async ({ sb }, { id }) => { const { error } = await sb.from("posts").delete().eq("id", id); return error ? { ok: false, error } : { ok: true }; }); ``` Handlers receive `(context, data, params)` — `params` is a `URLSearchParams` snapshot of the current URL. See [Loading and confirm](/attributes/loading) for the corresponding loading-state and confirmation prompt attributes. --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/attributes/auth.md' --- # Auth and roles Show or hide elements based on auth state. You provide the auth adapter, so any backend works. ## Markup ```html Log in My account Admin panel Dashboard ``` `gg-auth` is set on `` as `"true"` or `"false"`. `gg-role` is set if you provide a `roleQuery`. Any element with a `gg-auth` or `gg-role` attribute is hidden via inline `display: none` when its value doesn't match the body. Use commas on `gg-role` to match any of several roles. ## Adapter You configure auth via the `auth` option on `init()`: | Option | Type | Description | |---|---|---| | `auth.getUser` | `() => string \| null \| Promise` | Returns the current user id, or `null` when signed out. Called once on start. | | `auth.onChange` | `(cb: (userId: string \| null) => void) => void` | Subscribe to auth changes. Optional but recommended — without it, `gg-auth` won't update on sign-in/out. | | `auth.roleQuery` | `async (context, userId) => string \| null` | Returns the user's role string. Called on every auth change. If omitted, `gg-role` is never set. | ## Example: Supabase ```js import { createClient } from "@supabase/supabase-js"; import { init } from "gg-wf-scripts"; const sb = createClient("...", "..."); const app = init({ context: { sb }, auth: { getUser: async () => (await sb.auth.getUser()).data.user?.id ?? null, onChange: (cb) => sb.auth.onAuthStateChange((_e, session) => cb(session?.user?.id ?? null)), roleQuery: async ({ sb }, userId) => { const { data } = await sb .from("user_roles") .select("role") .eq("user_id", userId) .single(); return data?.role ?? null; }, }, }); ``` ## Avoiding flash on load Visibility is applied by JS once `init()` runs, so an element may briefly render in its default state before being hidden. To avoid the flash, hide them in CSS until auth is ready: ```css [gg-auth], [gg-role] { display: none; } ``` The library will set `display: ""` on elements that match, letting your stylesheet's natural display take over. --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/attributes/switcher.md' --- # Content switcher Show/hide children based on a state value, sourced either from a URL param or a field on the nearest data record. ## Driven by URL ```html
Pick a view
List view
Grid view
``` When `?view=list` is present, only the `gg-case="list"` child is shown. ## Driven by a record field ```html Published Draft Unknown ``` The switcher walks up the DOM to find the nearest `gg-data` / `gg-data-list` row record, then reads `status` from that record. ## Default state `gg-case=""` acts as the default/empty state. It matches when the URL param or field is missing or empty. ## Multiple values Both `gg-switch-query` and `gg-switch-field` accept comma-separated keys / paths. The state becomes the values joined by commas in the same order, and `gg-case` matches positionally (AND). ```html
``` Inside one position, use `|` for OR alternatives: ```html
Live Hidden
``` The two can combine — `gg-case="date|title,asc"` matches when sort is `date asc` or `title asc`. --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/attributes/data.md' --- # Data binding Display data from your registered queries directly in the DOM. ## Single record `gg-data` runs a query and populates `[gg-field]` descendants from the returned object: ```html

Loading...

``` `gg-field` supports dot-paths for nested data (e.g. `author.name`). ## Form pre-fill `gg-data-form` runs a query and sets `value` / `checked` on inputs by their `name` attribute: ```html
``` ## Lists `gg-data-list` runs a query that returns an array, and clones a `[gg-list-template]` element for each record: ```html ``` ## Passing data to web components / React Elements that manage their own DOM (custom elements wrapping React, Lit, etc.) read from the container's `__ggRecord` property. Every `gg-data`, `gg-data-form`, and cloned `gg-data-list` row stamps the record there after the query resolves, and the engine fires a `gg-data-ready` CustomEvent on the same element with `detail.record`. ```html
``` ```js // Inside — wait for the parent record, then hydrate. const form = host.closest("form"); form.addEventListener("gg-data-ready", (e) => { hydrate(e.detail.record); }); if (form.__ggRecord) hydrate(form.__ggRecord); // already populated ``` The event bubbles, so listeners on one form/container never fire for another. From inside a shadow root, the host element's parent chain is unreachable via `closest()` directly — walk out with `getRootNode().host` first. ::: tip If the component re-runs its own search query against the library, it can call any registered query imperatively via `app.queries[id](app.context, params)`. The library exposes the `App` as `window.ggApp` automatically (and dispatches a `gg-app-ready` event on `document` with `detail.app`), so custom-code components can reach it without any plumbing. Pass `expose: false` to `init()` to opt out. ::: ## Re-running on URL changes Add `gg-data-on` to re-run a query when specific URL params change: ```html

``` Combine with `gg-query-bind` (see [URL params](/attributes/url-params)) to drive live search: ```html
``` --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/attributes/dialog.md' --- # Dialog A single `` element on the page is managed automatically via the `modal` URL param. ```html
Delete

Are you sure?

``` ## How it works * Setting `?modal=...` opens the dialog. * Removing the `modal` param closes it. * Pressing `Escape` closes it. * Clicking the backdrop closes it. * Back-button navigation is handled automatically. You can have multiple dialog "views" inside the same `` element by combining with the [content switcher](/attributes/switcher): ```html
Are you sure?
Edit form…
``` --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/api/exports.md' --- # Exports The library exports a small set of utilities for use inside your queries and actions: ```js import { init, getPath, // resolve dot-paths on objects setQueryParams, // programmatically set URL params removeQueryParams, // programmatically remove URL params } from "gg-wf-scripts"; ``` ## `getPath(obj, path)` Resolves a dot-path against an object. Returns `undefined` if any segment is missing. ```js getPath({ author: { name: "Ada" } }, "author.name"); // "Ada" ``` ## `setQueryParams(updates)` / `removeQueryParams(keys)` Programmatic equivalents of `gg-query-set` / `gg-query-remove`. Useful inside an action handler that needs to update the URL after a mutation succeeds. ```js import { setQueryParams } from "gg-wf-scripts"; app.addAction("open_post", async (_ctx, { id }) => { setQueryParams([{ key: "modal", value: "view" }, { key: "id", value: String(id) }]); return { ok: true }; }); ``` ## Types `AuthAdapter`, `Query`, `Action`, `ActionResult`, `FormAction`, `FormActionResult`, `FormFieldError`, `GgErrorEvent`, and `ErrorHandler` are all exported as types. --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/attributes/forms.md' --- # Forms Override a form's submit to run a registered handler instead of the browser default. The handler receives the form's `FormData` directly. ## Form actions ```html
``` ```js app.addFormAction("create_post", async ({ sb }, formData) => { const { error } = await sb.from("posts").insert({ title: formData.get("title"), body: formData.get("body"), }); return error ? { ok: false, error } : { ok: true }; }); ``` The handler receives `(context, formData, params)`. `preventDefault` is called automatically — the form will not submit to its `action` URL. Return `{ ok: true }` or `{ ok: false, error }`. ## Validation errors Form actions can return validation errors and the engine will display them via attributes on your markup. ```js app.addFormAction("create_post", async ({ sb }, formData) => { const title = formData.get("title"); if (!title) { return { ok: false, field_errors: [{ name: "title", message: "Title is required" }], }; } const { error } = await sb.from("posts").insert({ title }); return error ? { ok: false, error: "Could not save — please try again." } : { ok: true }; }); ``` Markup: ```html

  • :

``` What the engine does: * Sets `gg-form-field-invalid="true"` on each invalid input — target with CSS like `input[gg-form-field-invalid="true"] { border-color: red; }`. * Sets the `textContent` of `[gg-form-field-error=""]` elements to the matching message. * Populates `[gg-form-error-list]` using the same template pattern as `gg-data-list` (clones `[gg-list-template]`, applies `gg-field` bindings). * Sets the `textContent` of `[gg-form-error]` to the top-level `error` string. * All errors are cleared at the start of each submit, and a field's invalid attr + message are cleared when the user types in that field. ## Form visibility Conditionally show/hide elements based on form field values. ```html
``` Hidden elements get `display: none`, `inert`, and `aria-hidden="true"`. Transitions are a 200ms opacity fade. Use `gg-form-scope` on a non-`
` ancestor when `closest("form")` can't reach the inputs (e.g. shadow DOM). --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/guide/getting-started.md' --- # Getting started `gg-wf-scripts` is a declarative attribute engine for Webflow sites. Add `gg-*` attributes to your markup and the library handles data binding, URL-driven state, dialogs, auth gating, form visibility, and user actions. Backend-agnostic — bring your own client (Supabase, fetch, anything). ## Quick start Create a site-specific entry file, register your queries and actions, then bundle it with esbuild. ```js import { init } from "gg-wf-scripts"; import { createClient } from "@supabase/supabase-js"; const sb = createClient("https://your-project.supabase.co", "your-publishable-key"); const app = init({ context: { sb }, auth: { getUser: async () => (await sb.auth.getUser()).data.user?.id ?? null, onChange: (cb) => sb.auth.onAuthStateChange((_e, session) => cb(session?.user?.id ?? null)), }, }); app.addQuery("posts_list", async ({ sb }, params) => { const q = params.get("q") ?? ""; const { data } = await sb .from("posts") .select("*") .ilike("title", `%${q}%`) .order("created_at", { ascending: false }); return data ?? []; }); app.addAction("delete_post", async ({ sb }, { id }) => { const { error } = await sb.from("posts").delete().eq("id", id); return error ? { ok: false, error } : { ok: true }; }); app.start(); ``` Bundle with esbuild: ```sh npx esbuild src/index.js --bundle --outfile=dist/site.js --format=iife --target=es2020 --platform=browser ``` Load on your site: ```html ``` ## Optional: fade transitions By default every show/hide is an instant `display:none` flip. Pass a `transition` to `init` to fade everything (auth gating, switch cases, form-visibility, data-list rows) through a single global setting: ```js const app = init({ context: { sb }, transition: { duration: 200, easing: "easeInOut" }, }); ``` See [Transitions in the init reference](/api/init#transitions) for the easing keywords and the `prefers-reduced-motion` behavior. ## Next steps * [Install the library](/guide/installation) * [Data binding attributes](/attributes/data) * [Form actions and validation](/attributes/forms) * [API reference](/api/init) --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/guide/installation.md' --- # Installation Install from npm: ```sh npm install gg-wf-scripts ``` The package ships as ESM with TypeScript declarations. You can import the entry point directly: ```js import { init } from "gg-wf-scripts"; ``` ## Bundling for Webflow Webflow's custom code embed loads a ` ``` ## TypeScript Types are bundled. No extra `@types/*` package is needed. ```ts import { init } from "gg-wf-scripts"; ``` --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/attributes/loading.md' --- # Loading and confirm ## Loading states While an action, query, or form action is in flight, the engine sets `gg-loading="true"` on the relevant element so you can style spinners, skeletons, or disabled visuals purely in CSS. | Trigger | Where `gg-loading` is set | Disabled? | |---|---|---| | `gg-action` | The trigger element itself | Native `disabled` is set if it's a ` ...
``` --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/quick-reference.md' --- # Quick reference Every `gg-*` attribute the library reads or writes, what reads it, and what shape it expects. One page so you (or an LLM helping you) can scan the whole vocabulary at once. ## Data binding | Attribute | On | Reads / writes | Notes | |---|---|---|---| | `gg-data=""` | container | reads | Runs query ``, expects single object. Populates `[gg-field]` and `[gg-switch-field]` descendants. | | `gg-data-list=""` | container | reads | Runs query ``, expects array. Clones `[gg-list-template]` per record. | | `gg-data-form=""` | `
` or container | reads | Runs query ``, expects single object. Pre-fills inputs by `name`. | | `gg-data-on=",..."` | same as above | reads | Re-runs the query whenever any listed URL param changes. | | `gg-field=""` | descendant | writes `textContent` | Set from the parent record. Skipped if the path resolves to `null`/`undefined`. | | `gg-list-template` | child of `gg-data-list` | — | The template element to clone per record. The original stays in the DOM (hidden via the engine). | **Record shape:** plain object. Dot-paths are split by `.` and walked with `getPath`. Arrays for `gg-data` or `gg-data-form` produce a `console.warn`. **Reading the record from custom code:** every `gg-data`, `gg-data-form`, and cloned `gg-data-list` row sets `element.__ggRecord` and dispatches a bubbling `gg-data-ready` CustomEvent (with `detail.record`) on itself after the query resolves. Web components / React islands listen on the container (e.g. `host.closest("form")`) to hydrate their own state. ## Actions | Attribute | On | Reads / writes | Notes | |---|---|---|---| | `gg-action=""` | any element | reads | On click, runs action ``. Walks ancestors for a `__ggRecord` and merges with `gg-action-data`. | | `gg-action-data="k1:v1,k2:v2"` | same element | reads | Inline data merged on top of the parent record. Values are strings. | | `gg-confirm` | element with `gg-action` or `gg-form-action` | reads | If present, calls `window.confirm()` before firing. | | `gg-confirm-text=""` | same | reads | Confirmation message. Defaults to `"Are you sure?"`. | **Action handler shape:** `(context, data, params) => { ok: boolean, error?: unknown } \| void`. Returning `{ ok: false }` triggers `onError`. ## Form actions | Attribute | On | Reads / writes | Notes | |---|---|---|---| | `gg-form-action=""` | `` | reads | Overrides submit, runs form action `` with a `FormData` snapshot. | | `gg-form-field-invalid` | input/select/textarea | written | Set by the engine after a `field_errors` failure. Cleared on next `input`. | | `gg-form-field-error=""` | element | written `textContent` | Receives the message for field ``. | | `gg-form-error` | element | written `textContent` | Receives the top-level `error` message. | | `gg-form-error-list` | container | reads | Cloning target — see `gg-list-template` — for rendering one element per `field_errors` entry. | | `gg-form-scope` | container | reads | Used by `gg-visible-when` to scope its lookups. | **Form action handler shape:** `(context, formData, params) => { ok: boolean, error?: unknown, field_errors?: { name, message }[] } \| void`. ## URL params | Attribute | On | Reads / writes | Notes | |---|---|---|---| | `gg-query-set="k1:v1,k2:v2"` | clickable | reads | On click, sets the listed params. | | `gg-query-remove="k1,k2"` | clickable | reads | On click, removes the listed params. | | `gg-query-bind=""` | input/select/textarea | reads + writes URL | Two-way binding between input value and URL param. | | `gg-query-debounce=""` | same input | reads | Debounce ms for `gg-query-bind`. | **Modal param:** `?modal=...` is reserved — the dialog engine watches it and opens/closes the page's `` element. `?id` is removed alongside `modal` on dismiss. ## Switcher | Attribute | On | Reads / writes | Notes | |---|---|---|---| | `gg-switch-state=""` | container | reads + written | Children with a matching `gg-case` are shown; others hidden. | | `gg-switch-field=""` | container inside `gg-data` | reads | Writes the path's value onto `gg-switch-state`. Comma-separated paths are joined positionally. | | `gg-switch-query=""` | container | reads | Mirrors a URL param onto `gg-switch-state`. Comma-separated keys are joined positionally. | | `gg-case=""` | child of a switch | reads | Visible when its value matches the parent's `gg-switch-state`. Empty string = default. Comma = positional AND, `\|` = OR alternatives within a position. | ## Visibility | Attribute | On | Reads / writes | Notes | |---|---|---|---| | `gg-visible-when="name1:value1,..."` | element inside `` or `[gg-form-scope]` | reads | Shown if **any** condition matches. Fades opacity over 200ms; sets `inert` + `aria-hidden` while hidden. | ## Status | Attribute | On | Reads / writes | Notes | |---|---|---|---| | `gg-loading` | trigger / form | written | Set to `"true"` while a handler is in flight. Buttons / inputs also get the native `disabled` attribute. | | `gg-auth` | `` | written | `"true"` when a user is signed in, `"false"` otherwise. | | `gg-auth="true"` / `"false"` | any element | reads | Hidden via inline `display: none` when its value doesn't match `body[gg-auth]`. | | `gg-role=""` | `` | written | The string returned by `auth.roleQuery`. Removed when no role / signed out. | | `gg-role=""` | any element | reads | Hidden when the value doesn't match `body[gg-role]`. Comma-separate for OR, e.g. `gg-role="admin,editor"`. | ## Inbound events Custom events the library listens for. Dispatch from your own code to drive the engine: | Event | Where to dispatch | Effect | |---|---|---| | `gg:dialog:open` | `document` | Opens the page's `` (without touching the URL). | | `gg:dialog:close` | `document` | Closes the page's `` (without touching the URL). | | `gg:shadow:click` | `document`, with `detail.target = ` | Forwarded click for elements inside a shadow root, since shadow DOM swallows bubbling clicks. | | `webflow:emit` | `document`, with `detail.event = ""` | Bridged into Webflow IX (`Webflow.require("ix3").emit`). | ## Outbound events Custom events the library dispatches. Listen from your own code to react to engine state: | Event | Where dispatched | Detail | When | |---|---|---|---| | `gg-data-ready` | the `gg-data` / `gg-data-form` container or cloned `gg-data-list` row | `{ record }` | After the query resolves and `__ggRecord` has been written. Bubbles, so a single listener on a parent can observe many containers. | | `gg-app-ready` | `document` | `{ app }` | Fired once during `init()` after the App is built. The same App is also set as `window.ggApp`. Skip both with `init({ expose: false })`. | --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/api/registration.md' --- # Registering handlers Three registration methods, each keyed by the string id you reference from your markup. All three accept optional type parameters so you can pin the result/data shape at the call site for autocomplete inside the handler. ## `app.addQuery(id, fn)` Register a data query. ```js app.addQuery("posts_list", async ({ sb }, params) => { const q = params.get("q") ?? ""; const { data } = await sb .from("posts") .select("*") .ilike("title", `%${q}%`); return data ?? []; }); ``` `fn` receives `(context, params)` where `params` is a `URLSearchParams` snapshot of the current URL query string. Use `params.get("id")` for single values or `params.getAll("tag")` for multi-value params. Return: * A single object (or `null`) for use with `gg-data` or `gg-data-form` * An array for use with `gg-data-list` ### Typed result ```ts type Post = { id: string; title: string }; app.addQuery("posts_list", async ({ sb }, params) => { const { data } = await sb.from("posts").select("*"); return data ?? []; // checked against Post[] }); ``` ## `app.addAction(id, fn)` Register an action triggered by `gg-action`. ```js app.addAction("delete_post", async ({ sb }, { id }) => { const { error } = await sb.from("posts").delete().eq("id", id); return error ? { ok: false, error } : { ok: true }; }); ``` `fn` receives `(context, data, params)`. Return `{ ok: true }` or `{ ok: false, error }`. ### Typed data ```ts app.addAction<{ id: string }>("delete_post", async ({ sb }, { id }) => { // `id` is `string` here const { error } = await sb.from("posts").delete().eq("id", id); return error ? { ok: false, error } : { ok: true }; }); ``` ## `app.addFormAction(id, fn)` Register a form action triggered by `gg-form-action` on a ``. ```js app.addFormAction("create_post", async ({ sb }, formData) => { const { error } = await sb.from("posts").insert({ title: formData.get("title"), body: formData.get("body"), }); return error ? { ok: false, error } : { ok: true }; }); ``` `fn` receives `(context, formData, params)` where `formData` is a `FormData` snapshot of the submitted form. The default submit is prevented automatically. Return `{ ok: true }`, `{ ok: false, error }`, or `{ ok: false, field_errors: [...] }` (see [Forms › Validation errors](/attributes/forms#validation-errors)). --- --- url: 'https://garrettcannon.dev/gg-wf-scripts/attributes/url-params.md' --- # URL query params Read and write URL query params declaratively. ## Set params on click ```html ``` ## Remove params on click ```html ``` ## Two-way input binding Mirror an input's value into a URL param as the user types. Empty value removes the param. The input is also populated from the URL on load and back/forward navigation. ```html ``` Combine with `gg-data-on` to re-run a query as the user types: ```html
``` See also: [`setQueryParams` / `removeQueryParams`](/api/exports) for programmatic use from inside an action or query handler.