npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@foormjs/vue

v0.2.5

Published

@foormjs/vue

Readme

@foormjs/vue

Renderless Vue components for ATScript-defined forms. Bring your own UI components and wrap them in OoForm / OoField to get automatic validation, computed properties, and reactive form state — all driven by .as schema files.

Install

pnpm add @foormjs/vue @foormjs/atscript vue @atscript/core @atscript/typescript
# or
npm install @foormjs/vue @foormjs/atscript vue @atscript/core @atscript/typescript

You also need the ATScript Vite plugin for .as file support:

pnpm add -D unplugin-atscript @vitejs/plugin-vue

Quick Start

1. Configure ATScript

// atscript.config.ts
import { defineConfig } from '@atscript/core'
import ts from '@atscript/typescript'
import { foormPlugin } from '@foormjs/atscript/plugin'

export default defineConfig({
  rootDir: 'src',
  plugins: [ts(), foormPlugin()],
})

2. Configure Vite

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import atscript from 'unplugin-atscript/vite'

export default defineConfig({
  plugins: [atscript(), vue()],
})

3. Define a form schema

// src/forms/login.as
@foorm.title 'Sign In'
@foorm.submit.text 'Log In'
export interface LoginForm {
    @meta.label 'Email'
    @meta.placeholder '[email protected]'
    @foorm.autocomplete 'email'
    @meta.required 'Email is required'
    @foorm.order 1
    email: string.email

    @meta.label 'Password'
    @meta.placeholder 'Enter password'
    @foorm.type 'password'
    @meta.required 'Password is required'
    @foorm.order 2
    password: string
}

4. Use in a Vue component

<script setup lang="ts">
import { OoForm, useFoorm } from '@foormjs/vue'
import { LoginForm } from './forms/login.as'

const { def, formData } = useFoorm(LoginForm)

function handleSubmit(data: typeof formData) {
  console.log('submitted', data)
}
</script>

<template>
  <OoForm :def="def" :form-data="formData" :types="{}" @submit="handleSubmit" />
</template>

OoForm renders default HTML inputs for all standard field types out of the box. For production use, you'll want to supply your own components via the types prop.

AI Agent Skills

@foormjs/vue ships an AI agent skill for Claude Code, Cursor, Windsurf, Codex, and other compatible agents. The skill teaches your agent the library's APIs, patterns, and best practices so it can help you write correct code without hallucinating.

Install the skill into your agent:

# Project-local (recommended — version-locked, commits with your repo)
npx @foormjs/vue setup-skills

# Global (available across all your projects)
npx @foormjs/vue setup-skills --global

Restart your agent after installing.

Auto-update on install — to keep the skill in sync whenever you upgrade the package, add this to your project's package.json:

{
  "scripts": {
    "postinstall": "npx @foormjs/vue setup-skills --postinstall",
  },
}

Advanced Usage

Custom Components by Type

Map field types to your UI components:

<script setup lang="ts">
import { OoForm } from '@foormjs/vue'
import MyTextInput from './MyTextInput.vue'
import MySelect from './MySelect.vue'
import MyCheckbox from './MyCheckbox.vue'

const typeComponents = {
  text: MyTextInput,
  select: MySelect,
  checkbox: MyCheckbox,
}
</script>

<template>
  <OoForm :def="def" :form-data="formData" :types="typeComponents" @submit="onSubmit" />
</template>

Every field with type: 'text' will render MyTextInput, every type: 'select' renders MySelect, etc.

Custom Components by Name

Use @foorm.component in your schema to assign a named component to a specific field:

@meta.label 'Rating'
@foorm.component 'StarRating'
@foorm.order 5
rating?: number

Then pass named components via the components prop:

<template>
  <OoForm
    :def="def"
    :form-data="formData"
    :types="typeComponents"
    :components="{ StarRating: MyStarRating }"
    @submit="onSubmit"
  />
</template>

Named components take priority over type-based components.

Building a Custom Component

Custom components receive TFoormComponentProps as their props:

<script setup lang="ts">
import type { TFoormComponentProps } from '@foormjs/vue'

const props = defineProps<TFoormComponentProps<string>>()
</script>

<template>
  <div :class="{ disabled, error: !!error }">
    <label>{{ label }}</label>
    <input
      :value="model.value"
      @input="model.value = ($event.target as HTMLInputElement).value"
      @blur="onBlur"
      :placeholder="placeholder"
      :disabled="disabled"
      :readonly="readonly"
    />
    <span v-if="error" class="error">{{ error }}</span>
    <span v-else-if="hint" class="hint">{{ hint }}</span>
  </div>
</template>

Key props available to your component:

| Prop | Type | Description | | ------------------ | ----------------------------- | ---------------------------------------------------------- | | model | { value: V } | Reactive model — bind with v-model="model.value" | | value | unknown? | Phantom display value (@foorm.value / @foorm.fn.value) | | onBlur | (e: FocusEvent) => void | Triggers validation on blur | | error | string? | Validation error message | | label | string? | Resolved field label | | description | string? | Resolved field description | | hint | string? | Resolved hint text | | placeholder | string? | Resolved placeholder | | disabled | boolean? | Whether the field is disabled | | hidden | boolean? | Whether the field is hidden | | readonly | boolean? | Whether the field is read-only | | optional | boolean? | Whether the field is optional | | required | boolean? | Whether the field is required | | type | string | The field input type | | altAction | TFoormAltAction? | Alternate action { id, label } from @foorm.altAction | | options | TFoormEntryOptions[]? | Options for select/radio fields | | name | string? | Field name | | maxLength | number? | Max length constraint | | autocomplete | string? | HTML autocomplete value | | field | FoormFieldDef? | Full field definition for advanced use | | title | string? | Resolved title for object/array fields | | level | number? | Nesting level (root=0, increments per nested object/array) | | class | string \| object? | CSS class(es) from @foorm.fn.classes | | style | string \| object? | Inline styles from @foorm.fn.styles | | onRemove | () => void? | Callback to remove this item from its parent array | | canRemove | boolean? | Whether removal is allowed (respects minLength) | | removeLabel | string? | Label for the remove button | | arrayIndex | number? | Zero-based index when rendered as an array item | | onToggleOptional | (enabled: boolean) => void? | Toggle an optional field on/off |

Arrays

Array fields are handled automatically. Define them in your .as schema:

@meta.label 'Tags'
@foorm.array.add.label 'Add tag'
@foorm.array.remove.label 'x'
@expect.maxLength 5, 'Maximum 5 tags'
tags: string[]

@meta.label 'Addresses'
@foorm.title 'Addresses'
@foorm.array.add.label 'Add address'
addresses: {
    @meta.label 'Street'
    @meta.required 'Street is required'
    street: string

    @meta.label 'City'
    city: string
}[]

OoForm renders arrays with add/remove buttons, inline editing for primitives, and sub-form cards for objects. Array-level validation (@expect.minLength, @expect.maxLength) is displayed below the add button.

Union arrays ((ObjectType | string)[]) render a variant selector per item and offer one add button per variant.

The remove button is the responsibility of the wrapping component:

  • Object array items: The object component receives onRemove, canRemove, and removeLabel via TFoormComponentProps.
  • Primitive array items: The field component receives onRemove, canRemove, and removeLabel as props (since there is no object wrapper around them).

Nested Groups

Use @foorm.title on a nested object field to render it as a titled, visually distinct section:

@foorm.title 'Settings'
settings: {
    @meta.label 'Notify by email'
    emailNotify: foorm.checkbox

    @meta.label 'Page size'
    @foorm.type 'number'
    pageSize?: number
}

Without @foorm.title, nested object fields flatten into the parent form. With it, they render as an indented group with a title header. Groups can be nested to any depth.

Scoped Slots

OoForm provides scoped slots for full layout control:

<template>
  <OoForm :def="def" :form-data="formData" :types="typeComponents" @submit="onSubmit">
    <!-- Form header (rendered before fields) -->
    <template #form.header="{ clearErrors, reset, setErrors, formContext, disabled }">
      <h1>Form Header</h1>
    </template>

    <!-- Content before/after fields -->
    <template #form.before="{ clearErrors, reset, setErrors }">
      <p>All fields are required unless marked optional.</p>
    </template>

    <template #form.after="{ clearErrors, reset, setErrors, disabled, formContext }">
      <p v-if="disabled">Please fill out all required fields.</p>
    </template>

    <!-- Custom submit button -->
    <template #form.submit="{ text, disabled, clearErrors, reset, setErrors, formContext }">
      <button type="submit" :disabled="disabled" class="my-btn">{{ text }}</button>
    </template>

    <!-- Footer (rendered after submit) -->
    <template #form.footer="{ disabled, clearErrors, reset, setErrors, formContext }">
      <p>By submitting, you agree to our terms.</p>
    </template>
  </OoForm>
</template>

Form Context

Pass runtime data (user session, feature flags, dynamic options) to computed functions and validators:

<script setup lang="ts">
const formContext = {
  cityOptions: [
    { key: 'nyc', label: 'New York' },
    { key: 'la', label: 'Los Angeles' },
  ],
  user: { role: 'admin' },
}
</script>

<template>
  <OoForm
    :def="def"
    :form-data="formData"
    :form-context="formContext"
    :types="typeComponents"
    @submit="onSubmit"
  />
</template>

Context is accessible in ATScript function strings as the third argument:

@foorm.fn.options '(v, data, ctx) => ctx.cityOptions || []'
city?: foorm.select

Server-side Errors

Pass server-side validation errors directly to fields:

<script setup lang="ts">
import { ref } from 'vue'

const serverErrors = ref<Record<string, string>>({})

async function handleSubmit(data: any) {
  const result = await api.submit(data)
  if (result.errors) {
    serverErrors.value = result.errors // e.g. { email: 'Already taken' }
  }
}
</script>

<template>
  <OoForm
    :def="def"
    :form-data="formData"
    :errors="serverErrors"
    :types="typeComponents"
    @submit="handleSubmit"
  />
</template>

Actions

Define alternate submit actions using the foorm.action primitive:

@meta.label 'Reset Password'
@foorm.altAction 'reset-password', 'Reset Password'
resetBtn: foorm.action

Handle the action event:

<template>
  <OoForm
    :def="def"
    :form-data="formData"
    :types="typeComponents"
    @submit="onSubmit"
    @action="onAction"
  />
</template>

<script setup lang="ts">
function onAction(name: string, data: any) {
  if (name === 'reset-password') {
    // handle reset password
  }
}
</script>

Paragraphs

Display static or computed text using the foorm.paragraph primitive:

@foorm.value 'Please fill out all required fields.'
info: foorm.paragraph

@foorm.fn.value '(v, data) => "Hello, " + (data.firstName || "guest") + "!"'
greeting: foorm.paragraph

Paragraphs are phantom fields — they are excluded from form data, validation, and TypeScript types. They only exist for display purposes.

API Reference

useFoorm(type)

Creates a reactive form definition and data object from an ATScript annotated type.

const { def, formData } = useFoorm(MyFormType)
  • defFoormDef with root field, ordered fields, the source type, and a flatMap
  • formData — Vue reactive() object with default values from the schema

OoForm

Renderless form wrapper component.

Props:

| Prop | Type | Required | Description | | ----------------- | ------------------------------------------------------------------------ | -------- | ------------------------------------------------------- | | def | FoormDef | Yes | Form definition from useFoorm() or createFoormDef() | | formData | object | No | Reactive form data (created internally if omitted) | | formContext | object | No | External context for computed functions and validators | | firstValidation | 'on-change' \| 'touched-on-blur' \| 'on-blur' \| 'on-submit' \| 'none' | No | When to trigger first validation | | components | Record<string, Component> | No | Named components (matched by @foorm.component) | | types | Record<string, Component> | Yes | Type-based components (matched by field type) | | errors | Record<string, string> | No | External error messages (e.g. server-side) |

Events:

| Event | Payload | Description | | -------------------- | ----------------------------------------------- | --------------------------------------------------------------- | | submit | formData | Emitted on valid form submission | | error | { path: string; message: string }[] | Emitted when validation fails on submit | | action | name, formData | Emitted when an action button is clicked (supported alt action) | | unsupported-action | name, formData | Emitted for unrecognized action names | | change | type: TFoormChangeType, path, value, formData | Emitted on field update, array add/remove, or union switch |

Slots:

| Slot | Scope | Description | | ------------- | ---------------------------------------------------------------- | --------------------------- | | form.header | { clearErrors, reset, setErrors, formContext, disabled } | Before fields | | form.before | { clearErrors, reset, setErrors } | After header, before fields | | form.after | { clearErrors, reset, setErrors, disabled, formContext } | After fields, before submit | | form.submit | { text, disabled, clearErrors, reset, setErrors, formContext } | Submit button | | form.footer | { disabled, clearErrors, reset, setErrors, formContext } | After submit |

OoField

Universal field renderer. Resolves component, props, validation, and nesting for any field type.

Props:

| Prop | Type | Description | | ------------- | --------------- | -------------------------------------------------------------- | | field | FoormFieldDef | Field definition from def.fields or def.rootField | | error | string? | External error message | | onRemove | () => void? | Callback to remove this item from its parent array | | canRemove | boolean? | Whether removal is allowed (respects minLength) | | removeLabel | string? | Label for the remove button (from @foorm.array.remove.label) | | arrayIndex | number? | Zero-based index when rendered as an array item |

Default Components

All default type components are exported and can be used as-is or as reference implementations:

| Component | Field Type | Description | | ------------- | ----------- | -------------------------------------------------- | | OoInput | text, etc | Text/password/number input with OoFieldShell | | OoSelect | select | Dropdown select with OoFieldShell | | OoRadio | radio | Radio button group with OoFieldShell | | OoCheckbox | checkbox | Boolean checkbox with OoFieldShell | | OoParagraph | paragraph | Read-only text display | | OoAction | action | Action button with altAction support | | OoObject | object | Object container (title + OoIterator) | | OoArray | array | Array container (add/remove + OoIterator per item) | | OoUnion | union | Union variant picker + selected variant rendering | | OoTuple | tuple | Fixed-length tuple via OoIterator |

Composables

| Export | Description | | --------------------------------- | --------------------------------------------------------------------- | | useFoorm(type) | Returns { def, formData } from an ATScript annotated type | | useFoormArray(field, disabled?) | Array state management (keys, add/remove, constraints) | | useFoormUnion(props) | Union variant state management | | useConsumeUnionContext() | Consume and clear the __foorm_union injection | | formatIndexedLabel() | Format label with array index prefix (e.g. "Address #1") | | createDefaultTypes() | Returns a TFoormTypeComponents map with all default type components |

Types

| Export | Description | | -------------------------- | ------------------------------------------------------------- | | TFoormBaseComponentProps | Shared base props (disabled, hidden) | | TFoormComponentProps | Unified props interface for ALL custom field components | | TFoormTypeComponents | Required shape for the types prop on OoForm | | TFoormChangeType | 'update' \| 'array-add' \| 'array-remove' \| 'union-switch' | | TFoormUnionContext | Union context provided via __foorm_union inject |

For ATScript documentation, see atscript.moost.org.

License

MIT