Skip to content

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, opts?) ​

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

Re-running on URL changes ​

Pass opts.on to list URL query-param keys that should rerun this query whenever any of them change:

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 ?? [];
}, { on: ["q"] });

This is equivalent to putting gg-data-on="q" on every container that uses posts_list, but declared next to the handler so it can't drift from the markup. A gg-data-on attribute on a specific container still wins as a per-instance override.

Re-running after mutations ​

Pass opts.refreshKeys to list topic strings this query should re-run on whenever app.invalidate(...) publishes one of them:

js
app.addQuery("posts_list", async ({ sb }) => {
  const { data } = await sb.from("posts").select("*");
  return data ?? [];
}, { refreshKeys: ["posts"] });

app.addFormAction("create_post", async ({ sb }, formData) => {
  const { error } = await sb.from("posts").insert({
    title: formData.get("title"),
    body: formData.get("body"),
  });
  if (error) return { ok: false, error };
  app.invalidate("posts");
  return { ok: true };
});

Keys are arbitrary strings — pick one canonical name per entity (e.g. "posts", "users") and use it in every query that reads the entity and every handler that mutates it. on and refreshKeys are independent; a query can use both, either, or neither.

See Data binding › Refreshing after mutations for the full pattern.

Typed result ​

ts
type Post = { id: string; title: string };

app.addQuery<Post[]>("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, helpers). Return { ok: true } or { ok: false, error }.

Helpers ​

helpers.removeItem(predicate) removes matching clone(s) from the gg-data-list that contains the trigger — useful for optimistic deletes without a full list refetch:

js
app.addAction("delete_post", async ({ sb }, { id }, _params, helpers) => {
  const { error } = await sb.from("posts").delete().eq("id", id);
  if (error) return { ok: false, error };
  helpers.removeItem((record) => record.id === id);
  return { ok: true };
});

If a transition is configured on init(), the removed row fades out using the same duration/easing as the rest of the library before detaching. If the trigger isn't inside a list, removeItem warns and no-ops. See Actions › Removing a list item for more.

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 <form>.

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, data) where formData is a FormData snapshot of the submitted form and data is parsed from gg-action-data on the <form> (see Forms › Extra data on the form). The default submit is prevented automatically. Return { ok: true }, { ok: false, error }, or { ok: false, field_errors: [...] } (see Forms › Validation errors).

A successful return resets the form's named inputs to their defaults. Pass { ok: true, reset: false } to keep the values in place.

Released under the MIT License.