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
<form gg-form-action="create_post">
<input name="title" required />
<textarea name="body"></textarea>
<button type="submit">Save</button>
</form>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, data). preventDefault is called automatically — the form will not submit to its action URL. Return { ok: true } or { ok: false, error }.
On a successful submit the engine resets every named input back to its default value (light DOM + open shadow roots) and dispatches input/change so downstream listeners react. Opt out per-submit by returning { ok: true, reset: false } — useful when the form should stay populated, e.g. inline edit forms.
Extra data on the form
Use gg-action-data on the <form> to pass data alongside the submitted fields — handy for ids that aren't user-editable inputs.
<form gg-form-action="update_post" gg-action-data-post-id="abc123">
<input name="title" />
<button type="submit">Save</button>
</form>app.addFormAction("update_post", async ({ sb }, formData, _params, data) => {
const { error } = await sb
.from("posts")
.update({ title: formData.get("title") })
.eq("id", data["post-id"]);
return error ? { ok: false, error } : { ok: true };
});Both forms work the same as on gg-action: gg-action-data="k1:v1,k2:v2" for the csv form, or gg-action-data-{name}="value" per attribute (see Actions for the parsing details).
Validation errors
Form actions can return validation errors and the engine will display them via attributes on your markup.
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:
<form gg-form-action="create_post">
<input name="title" />
<p gg-form-field-error="title"></p>
<textarea name="body"></textarea>
<p gg-form-field-error="body"></p>
<!-- Optional: list every field error in one place -->
<ul gg-form-error-list>
<li gg-list-template>
<strong gg-field="name"></strong>: <span gg-field="message"></span>
</li>
</ul>
<!-- Optional: form-level error (the result.error string) -->
<p gg-form-error></p>
<button type="submit">Save</button>
</form>What the engine does:
- Sets
gg-form-has-error="true"on the<form>itself when any error is shown — use it to toggle a wrapper, e.g.form:not([gg-form-has-error]) .error-banner { display: none; }. - Sets
gg-form-field-invalid="true"on each invalid input — target with CSS likeinput[gg-form-field-invalid="true"] { border-color: red; }. - Sets the
textContentof[gg-form-field-error="<name>"]elements to the matching message. - Populates
[gg-form-error-list]using the same template pattern asgg-data-list(clones[gg-list-template], appliesgg-fieldbindings). - Sets the
textContentof[gg-form-error]to the top-levelerrorstring. - 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.
<form>
<label><input type="radio" name="reason" value="scheduling" /> Scheduling</label>
<label><input type="radio" name="reason" value="other" /> Other</label>
<div gg-visible-when="reason:other">
<input type="text" name="other_reason" placeholder="Please specify" />
</div>
</form>For checkboxes, use the :checked / :unchecked sentinels — these read the input's .checked property directly, since a checkbox's value attribute (often "on" by default) doesn't reflect its ticked state.
<form>
<label><input type="checkbox" name="subscribe" /> Subscribe</label>
<div gg-visible-when="subscribe:checked">Shown only when ticked.</div>
<div gg-visible-when="subscribe:unchecked">Shown only when not ticked.</div>
</form>The strings checked and unchecked are reserved: a checkbox whose value attribute is literally "checked" or "unchecked" will be interpreted as the sentinel rather than a value match.
Hidden elements get display: none, inert, and aria-hidden="true". Transitions are a 200ms opacity fade. Use gg-form-scope on a non-<form> ancestor when closest("form") can't reach the inputs (e.g. shadow DOM).