model-form-root
Build a model edit form (and understand the shared $root data contract behind every list, picker and form) by extending BaseUI, seeding state from the TypeBox…
Skill by IgorAIvanov
Chalk Labs
Need help deploying model-form-root?
Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/igoraivanov-model-form-root
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: documents, data, writing
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
Network hosts it talks to 1
codeload.github.com· found in install command
Files it touches
~/.claude/skills/model-form-root
What's inside
- Skill name
model-form-root- Description
- Build a model edit form (and understand the shared $root data contract behind every list, picker and form) by extending BaseUI, seeding state from the TypeBox schema with Value.Create instead of hand-written empty objects.
- License
- MIT
- Metadata
- audience: app
- Instructions
- 369 lines in SKILL.md · view on GitHub
Files (1)
- SKILL.md
SKILL.md (first lines)
# Model Form / `$root` Contract Skill
Use this skill when:
- creating a `<Model>Edit.ts` form for a catalog or document model
- touching anything that reads or writes `$root` (list, picker, form — they all share it)
- wiring a form to the SQL envelope (`run` / `assign` / `loadInto`)
- you are about to hand-write an "empty item" literal, a `loading` flag, or a `bus.request` call in a screen
**Do not hand-write empty-object literals, transport calls, or per-form loading flags.** All of it
lives in `@client/ui-kit/base/base-ui.ts` → `BaseUI<T>`, which every screen base extends
(`@client/` is the alias for the `@altera/client` package).
## The one contract
```
SQL function → { ok, data, messages } ← envelope, never changes
│
└── assign() → $root ← reactive mirror of `data`
```
- **`$root` is a mirror of the envelope's `data`**, seeded from the model's TypeBox schema by
`Value.Create(schema)` — so every field exists before the first render. Never `{} as T`.
- **Fields without a prefix** are model data returned by SQL: `item`, `rows`, `totals`, `options`.
- **`$`-prefixed fields are service state that round-trips with the DB**: `$query` (search / page /
pageSize / sortBy / sortDir). The client sends it as payload; the server may return the
*effective* (normalized, clamped) query, which mirrors straight back through `assign()`.
- **Transient UI state never enters `$root`**: `running`, `busy`, `messages` (base), `selectedId`,
`pingResult` (subclass `@state`). Rule of thumb — if it is never serialized to or from SQL, it
does not belong in `$root`.
### Why `assign()` skips top-level `null`
The generated SQL envelope always carries **all** data keys and puts `null` / `[]` / `{}` in the ones
the command does not use (`list` returns `item: null`; `get` returns `rows: []`). So at the top level
`null` means *"no data for this key"*, never *"clear it"*. `assign()` therefore ignores `null` and
`undefined`, which is what keeps a `get` of a deleted record from wiping the schema-seeded `$root.item`
and crashing the render.
Meaningful clearing always arrives **inside** an object (`item.counterparty = null`) — nested values
are not filtered.
## `BaseUI` API
| Member | Purpose |
|---|---|
| `$root: T` | Reactive container, seeded by `Value.Create(schema)`, deep-proxied (`signal-utils/deep`). |
| `model` | Abstract. Model key, e.g. `"bank"`. |
| `primaryKey` | Key of the main entity in `data` — `"item"` for edit forms, `null` (default) for lists/pickers. |
| `run(command, payload, kind?)` | Calls the model command over the bus, unwraps the envelope, drives `running` / `messages`. `kind: "save"` routes through `data.save` (which emits `model.changed`). |
| `loadInto(command, payload)` | `run` + not-found check + `assign`. Returns `false` and sets `notFound` when `primaryKey` came back `null`. **Prefer this over `run` in forms.** |
| `assign(patch)` | Partial merge of `data` into `$root`, skipping top-level `null`/`undefined`. |
| `bindTo(obj, field)` | Input binder for a nested node: `@input=${this.bindTo(item, "code")}`. |
| `running` / `busy` | Name of the in-flight command / whether any is running. |
| `notFound` | Request succeeded but the entity is missing (deleted or invalid id). |
| `canSave` | `!busy && !notFound` — use for the Save button. |
| `renderNotice()` | Shared banner: "record not found" + error messages. Drop in once per form. |
| `renderField(label, control, opts)` | Field layout: label + control + required asterisk + error text. Pass `{ field: "code" }` to wire it to validation. |
| `fieldRules()` | Override to declare required / custom checks **in the form**, incl. conditional ones. |
| `isRequired(f)` / `fieldError(f)` | For components that draw their own label (`<ui-picker>`): `?required=` / `.invalid=`. |
| `validate()` | Runs the rules, highlights, scrolls to the first invalid field. Called automatically before save. |
| `trySave()` | `validate()` + `saveItem()`. **Wire custom save buttons here, not to `saveItem`.** |
| `prepareNew()` | Override for **async** defaults of a new record (accounts from the accounting policy, a rate on the date). The base calls it once for an edit form without `modelId`, awaits it, then `markClean()`s — and only then lays a "create on basis" draft. |
| `fillFromBasis(model, id)` | Fill the record from a basis document (`fill_basis`). The base calls it itself for `params.basis`; call it from a "Fill from basis" button. |
| `t` | Localizer. |
## Required fields: declare them in the form
Requiredness often depends on other fields, and a TypeBox schema cannot express that. Override
`fieldRules()` — it is a **method**, called on every render and before every save, so the condition
freely reads `$root`:
```ts
protected override fieldRules(): FieldRules {
const item = this.$root.item;
return {
mfo: item.kind === "bank", // conditional
prefix: false, // drop the schema's requiredness
edrpou: { required: true, check: (v) => /^\d{8}$/.test(String(v)) ? null : t("…") },
};
}
```
Precedence: inline `renderField(…, { required })` → `fieldRules()` → schema. The schema stays the
default, so forms that need no conditions keep working untouched.
Only two sets are validated: fields named in `fieldRules()`, and fields passed to `renderField` as
`{ field }`. Never the whole schema — `id` is schema-required but empty on a new record.
**Tabular parts** declare their rules on the column (`required` / `check`, see
[document-tabular-section](../document-tabular-section/SKILL.md)); the form only names its sections
so `validate()` covers them too:
```ts
protected override sections(): FormSection[] { return [this.lines]; }
```
**Server-side rejections** highlight the field too: an envelope message may carry `field`
(`{ type, text, field }`). SQL sets it via `raise exception … using column = 'code'`; PostgreSQL
sets it itself on not-null and unique violations; a TS command uses `fieldErr(field, text)`. Nothing
to wire in the form — but the field must be one the form actually renders, otherwise the message
stays in the banner rather than vanishing.
Deep dive (framework repository, not part of an application): `docs/ui-form-validation.md`.
## Gotcha: the schema goes through `super()`
Subclass field initializers run **after** `super()`, so an `abstract schema` field is still
`undefined` inside the base constructor. The schema must be passed as a constructor argument:
```ts
constructor() { super(BankEditRootSchema); } // ✅
```
Not as a declared property the base reads at construction time.
## Canonical example — simple form
An edit form for a `bank` catalog (`bankEdit.ts` next to the model's `manifest.json`):From IgorAIvanov/altera03, MIT.
More from IgorAIvanov/altera03
- 0.584score
typebox-model-schema
IgorAIvanov
Define the complete model schema using TypeBox as the single source of truth for backend validation, SQL contract documentation, and frontend form/list/picker…
SkillAFreshness: A★ 1Claude Code +9 - 0.584score
model-feature-architecture
IgorAIvanov
Design or refactor a model into a feature-centered structure where frontend UI files and database SQL source live together under app/<family>/<model>.
SkillAFreshness: A★ 1Claude Code +9 - 0.575score
db-function-contract
IgorAIvanov
Define PostgreSQL function contracts for models that use List, Get, Save, Delete, and Lookup with JSONB payloads and JSONB results.
SkillAFreshness: A★ 1Claude Code +9 - 0.547score
framework-ui-internals
IgorAIvanov
Внутрішні правила client/ui-kit — чому тема перебиває daisyUI, як не зламати граф чанків, пастки Lit і TypeScript у базових класах, де проходить межа між…
SkillBFreshness: B★ 1Claude Code +9 - 0.547score
lit
IgorAIvanov
Rules and patterns for writing Lit Web Components correctly in this project.
SkillBFreshness: B★ 1Claude Code +9 - 0.547score
model-command-access
IgorAIvanov
Declare the permission a model command requires in manifest.json (commands.access) so the runtime can enforce it.
SkillBFreshness: B★ 1Claude Code +9 - 0.547score
model-command-response
IgorAIvanov
Derive the exact shape of a model command's response before consuming it in a screen — never hand-write the interface from memory.
SkillBFreshness: B★ 1Claude Code +9 - 0.547score
daisyui
IgorAIvanov
Official daisyUI component library skill.
SkillBFreshness: B★ 1Claude Code +9 - 0.547score
screen-design-rules
IgorAIvanov
Visual and usability rules for drawing any application screen — density, colour tokens, disabled and read-only states, field widths, status marks, keyboard…
SkillBFreshness: B★ 1Claude Code +9 - 0.547score
altera-new-app
IgorAIvanov
Deploy a new application on the Altera framework into an empty directory — from scaffold to a working login screen — and verify that it actually runs.
SkillBFreshness: B★ 1Claude Code +9 - 0.547score
app-run-and-deploy
IgorAIvanov
Run the application in development or production, fill in .env, publish the schema to a local or remote database, put several instances on one machine, and…
SkillBFreshness: B★ 1Claude Code +9 - 0.544score
document-tabular-section
IgorAIvanov
Build an editable tabular section (табличну частину) of a document — controls that fill the table cell flush with the grid, decimal columns with live totals —…
SkillBFreshness: B★ 1Claude Code +9
Signals
- Stars
- 1
- Forks
- 0
- Open issues
- 0
- Last commit (this folder)
- Oct 7, 2026 (24 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/IgorAIvanov/altera03
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from IgorAIvanov
- 0.584score
typebox-model-schema
IgorAIvanov
Define the complete model schema using TypeBox as the single source of truth for backend validation, SQL contract documentation, and frontend form/list/picker…
SkillAFreshness: A★ 1Claude Code +9 - 0.584score
model-feature-architecture
IgorAIvanov
Design or refactor a model into a feature-centered structure where frontend UI files and database SQL source live together under app/<family>/<model>.
SkillAFreshness: A★ 1Claude Code +9 - 0.575score
db-function-contract
IgorAIvanov
Define PostgreSQL function contracts for models that use List, Get, Save, Delete, and Lookup with JSONB payloads and JSONB results.
SkillAFreshness: A★ 1Claude Code +9 - 0.547score
framework-ui-internals
IgorAIvanov
Внутрішні правила client/ui-kit — чому тема перебиває daisyUI, як не зламати граф чанків, пастки Lit і TypeScript у базових класах, де проходить межа між…
SkillBFreshness: B★ 1Claude Code +9 - 0.547score
altera-new-app
IgorAIvanov
Deploy a new application on the Altera framework into an empty directory — from scaffold to a working login screen — and verify that it actually runs.
SkillBFreshness: B★ 1Claude Code +9 - 0.547score
screen-design-rules
IgorAIvanov
Visual and usability rules for drawing any application screen — density, colour tokens, disabled and read-only states, field widths, status marks, keyboard…
SkillBFreshness: B★ 1Claude Code +9
Done for you
Rather not set it up yourself?
Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.
See how we build AI agents →Read the case studies
Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.
- launches shipped
- 50+
- launches shipped
- media placements
- 500+
- media placements
- founders on every account
- 2
- founders on every account
Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block
Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.
Get model-form-root running in your business
Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.
Cite this page
model-form-root skill. Agent Mart. https://agentmart.dev/skills/igoraivanov-model-form-root (last updated 2026-10-24)