Skip to content
Agentmart
Submit a tool

falcondev-form

Building or editing forms with @falcondev-oss/form — the type-safe, reactive, schema-driven form library (packages form-core, form-react, form-vue) built on…

Skill by falcondev-oss

IndexedMIT★ 2
Write a review

Chalk Labs

Need help deploying falcondev-form?

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.

Book a call

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/falcondev-oss-falcondev-form

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.

We use your email only for this newsletter. Privacy

About

Building or editing forms with @falcondev-oss/form — the type-safe, reactive, schema-driven form library (packages form-core, form-react, form-vue) built on @vue/reactivity. Use when working with useForm / useFormCore, form.fields accessors, $use(), field.value / field.handleChange / field.model, form.data, or a Standard Schema (zod v4 / arktype) form in a React or Vue codebase.

Use cases: frontend, data, writing

No 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/falcondev-form

What's inside

Skill name
falcondev-form
Description
Building or editing forms with @falcondev-oss/form — the type-safe, reactive, schema-driven form library (packages form-core, form-react, form-vue) built on @vue/reactivity. Use when working with useForm / useFormCore, form.fields accessors, $use(), field.value / field.handleChange / field.model, form.data, or a Standard Schema (zod v4 / arktype) form in a React or Vue codebase.
License
MIT
Instructions
162 lines in SKILL.md · view on GitHub

Files (3)

  • SKILL.md
  • frameworks.md · reference/
  • patterns.md · reference/

SKILL.md (first lines)

# @falcondev-oss/form

A framework-agnostic form-state library on top of `@vue/reactivity`. You give it a **Standard Schema** (zod v4, arktype, …) and reactive **sourceValues**; it gives you a reactive form **handle** with a tree of field **accessors**.

Three published packages — pick the one for the runtime, never import `-core` directly in app code:

| Package | Entry | Adds |
|---|---|---|
| `@falcondev-oss/form-core` | `useFormCore`, all types, `/reactive` helpers | engine; use directly only outside React/Vue |
| `@falcondev-oss/form-react` | `useForm`, `useField`, `FormFieldMemo` | React re-render integration, `field.model = {value,onUpdate}` |
| `@falcondev-oss/form-vue` | `useForm`, `useFormHandles` | `field.model` writable computed (`v-model`) |

`useForm` from the framework package wraps `useFormCore` — same options, same handle. Everything below is identical across frameworks **except** field binding and re-render (see `reference/frameworks.md`).

## Two ideas that explain everything

1. **Accessors are lazy paths; `$use()` materializes.** `form.fields.address.city` is just a typed path proxy — no field object exists yet. Calling `.$use()` creates (and caches) the actual reactive `FormField`. Navigate with dot/`.at()`, then `.$use()` at the leaf you bind.

2. **Form data is `NullableDeep`.** While editing, *every* value can be `null` — a half-filled form has nulls everywhere. So `form.data`, `field.value`, and accessor types are the schema's **input** type made deeply nullable (objects → `T | null`, arrays → `(T|null)[] | null`). The validated **output** type (non-null) only appears in `submit({ values })`. Write UI against nullable types; trust `submit` for clean data.

## Golden path

```ts
import { useForm } from '@falcondev-oss/form-vue' // or -react
import z from 'zod'

const form = useForm({
  schema: z.object({ name: z.string(), age: z.number() }),
  sourceValues, // initial data (NullableDeep); a value, getter, or ref — React needs a stable reference
  async submit({ values }) {
    // `values` is validated OUTPUT: { name: string, age: number }
    await api.save(values)
    // return nothing → { success: true }; return { success: false } to keep form dirty
  },
})

const nameField = form.fields.name.$use()
nameField.handleChange('Jane')   // programmatic write (validates, marks dirty)
// or write straight through the reactive data:
form.data.name = 'Jane'          // equivalent effect

await form.submit()              // validates whole schema, runs submit if valid
```

## `useForm(options)`

- `schema` — any **Standard Schema** that *also* exposes Standard JSON Schema (zod v4, arktype). JSON Schema drives `field.schema` metadata and required-detection. Non-JSON-representable types (Date, bigint, Map…) need a codec/transform or metadata extraction silently degrades (a console warning fires).
- `sourceValues` — initial `NullableDeep` data. A value, a getter `() => data`, or (Vue) a `ref`. In **React** it must be a *stable reference* — see below. May be `undefined` → form is **pending** (`isLoading`/`field.isPending` true, `data` is `undefined`, writes ignored) until it resolves. When it later changes: if the form is **not dirty**, the form resets to the new values; if dirty, the update is skipped with a warning (except during submit).
- `submit({ values })` — `async`, receives the **validated output**. Return `void`/`{success:true}` → form marked pristine; `{success:false}` → stays dirty. **`await` everything that must finish before the form settles** — most importantly cache/query invalidations, so fresh data flows back into `sourceValues` before the form is marked pristine (an un-awaited invalidation lets the form settle against stale data). With TanStack Query: `await queryClient.invalidateQueries(...)` directly in `submit`, or do it in the mutation's `onSuccess` and `await mutateAsync(...)` in `submit` (awaiting the mutation awaits `onSuccess`).
- `disabled?` — `boolean | Ref | getter`. Blocks `handleChange`/`handleBlur`/`reset` on all fields.
- `hooks?` — lifecycle hooks (see `reference/patterns.md`).

### Writing `sourceValues` (do it this way)

Three ordered branches: still-loading → existing entity → blank defaults. Vue takes them as a getter; React takes them as a `useMemo`ed value.

```ts
// Vue — getter, tracked reactively
useForm({
  schema,
  sourceValues: () => {
    if (isLoading.value) return undefined    // 1. source still loading → form is pending
    if (entity.value) return entity.value    // 2. edit: return the whole entity object
    return { name: null, age: null }         // 3. create: blank defaults
  },
  async submit({ values }) { /* … */ },
})
```

```tsx
// React — same branches, wrapped in useMemo
const sourceValues = useMemo(() => {
  if (isLoading) return undefined
  if (entity) return entity
  return { name: null, age: null }
}, [isLoading, entity])

useForm({ schema, sourceValues, async submit({ values }) { /* … */ } })
```

**In React `sourceValues` must be a stable reference.** Never inline the object (or a getter) in the `useForm` call: React re-runs the component body every render, so a fresh object identity re-seeds the form on every render — resetting a clean form and warning on a dirty one. Memoize it (`useMemo`), or hoist a constant to module scope, or pass query data directly (`sourceValues: query.data` — already stable). Getters are worse than useless here: the adapter captures the getter **once** on mount, so one built over React state or props is frozen at its first-render closure and never sees an update.

- **Return `undefined` while the source is loading** (e.g. the fetch for the entity being edited is pending). This puts the form in the pending state instead of seeding it with a wrong/empty shape you'd have to overwrite later.
- **Return the existing entity as one whole object** — don't hand-build `{ name: entity?.name, age: entity?.age, … }` with optional chaining per key. If the entity doesn't match the schema, spread and override just the divergent fields: `return { ...entity, tags: entity.tags ?? [] }`.
- **Use `null` for fields with no meaningful default** — not `''` or `0` just because the type is string/number. `NullableDeep` makes `null` valid for every field; an empty string is a *real value* that reads as "the user typed nothing", which is different from "untouched".

## The form handle

| Member | Type | Notes |
|---|---|---|
| `form.data` | `NullableDeep<T> \| undefined` | reactive; **read and write** directly (`form.data.x = …`). `undefined` while pending. |
| `form.fields` | accessor tree | navigate then `.$use()` |
| `form.isDirty` | `boolean` | any change made (edit count ≠ 0), reset by `submit` success / `reset` |
| `form.isChanged` | `boolean` | deep-equals current data vs `sourceValues` (false if edited back to original) |
| `form.isLoading` | `boolean` | submitting **or** pending source values |
| `form.isDisabled` | `boolean` | loading or `disabled` option |
| `form.errors` | `Issue[] \| undefined` | all validation issues, or undefined |
| `form.submit()` | `() => Promise<{success:boolean}>` | validates all, runs `submit` |
| `form.reset()` | `() => void` | restore to `sourceValues` |
| `form.hooks` | hookable | `hook`/`hookOnce`/`addHooks` |

## Field accessors

```ts
form.fields.user.email.$use()        // nested object
form.fields.tags.at(0).$use()        // array item (negative index ok: .at(-1))
form.fields.tags.$use()              // the array field itself
for (const item of form.fields.tags) item.$use()  // iterate (reactive)
form.fields.tags.delete(field.key)   // remove array item by its key (see below)
form.fields.$use()                   // root field = whole form value
form.fields['a.b'].$use()            // keys containing dots work (auto-escaped)
```

`.at(i)` accessor exists even before the array/index does — `.$use().value` is `null` until data arrives. `.delete(key)` **must** target an array item's key (`items[2]`), not a nested property — otherwise it throws.

## The `FormField` (result of `$use()`)

| Member | Notes |
|---|---|

From falcondev-oss/skills, MIT.

Signals

Stars
2
Forks
0
Open issues
0
Last commit (this folder)
Aug 11, 2026 (2 months ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from falcondev-oss

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

Goes to the Chalk Labs founders, not the newsletter. Privacy

Need help deploying falcondev-form?

CallGet help
Cite this page

falcondev-form skill. Agent Mart. https://agentmart.dev/skills/falcondev-oss-falcondev-form (last updated 2026-10-24)