[{"data":1,"prerenderedAt":2853},["ShallowReactive",2],{"navigation":3,"search":187,"$f69oqqw2iyajj":1615,"i-custom:logo":1616,"i-tabler:search":1620,"i-simple-icons:github":1622,"i-tabler:moon":1624,"i-tabler:sun":1626,"i-tabler:menu":1628,"\u002Fdocs\u002Fgetting-started\u002Fintroduction":1630,"\u002Fdocs\u002Fgetting-started\u002Fintroduction-surround":2831,"i-tabler:arrow-right":2835,"i-tabler:play":2837,"i-tabler:clock-code":2839,"i-tabler:heart":2841,"i-tabler:external-link":2843,"i-tabler:copy":2845,"i-tabler:chevron-down":2847,"i-tabler:info-square-rounded":2849,"i-tabler:hash":2851},[4],{"title":5,"path":6,"stem":7,"children":8,"page":11},"Docs","\u002Fdocs","docs",[9,35,72,93,102,119,144,161,170],{"title":10,"icon":11,"path":12,"stem":13,"children":14,"page":11},"Getting started",false,"\u002Fdocs\u002Fgetting-started","docs\u002F1.getting-started",[15,19,23,27,31],{"title":16,"path":17,"stem":18},"Introduction","\u002Fdocs\u002Fgetting-started\u002Fintroduction","docs\u002F1.getting-started\u002F1.introduction",{"title":20,"path":21,"stem":22},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","docs\u002F1.getting-started\u002F2.installation",{"title":24,"path":25,"stem":26},"Quickstart","\u002Fdocs\u002Fgetting-started\u002Fquickstart","docs\u002F1.getting-started\u002F3.quickstart",{"title":28,"path":29,"stem":30},"Nuxt module","\u002Fdocs\u002Fgetting-started\u002Fnuxt-module","docs\u002F1.getting-started\u002F4.nuxt-module",{"title":32,"path":33,"stem":34},"Core concepts","\u002Fdocs\u002Fgetting-started\u002Fcore-concepts","docs\u002F1.getting-started\u002F5.core-concepts",{"title":36,"icon":11,"path":37,"stem":38,"children":39,"page":11},"Guides","\u002Fdocs\u002Fguides","docs\u002F2.guides",[40,44,48,52,56,60,64,68],{"title":41,"path":42,"stem":43},"Binding inputs","\u002Fdocs\u002Fguides\u002Fbinding-inputs","docs\u002F2.guides\u002F1.binding-inputs",{"title":45,"path":46,"stem":47},"Schema input and output","\u002Fdocs\u002Fguides\u002Fschema-input-output","docs\u002F2.guides\u002F2.schema-input-output",{"title":49,"path":50,"stem":51},"Validation","\u002Fdocs\u002Fguides\u002Fvalidation","docs\u002F2.guides\u002F3.validation",{"title":53,"path":54,"stem":55},"Errors","\u002Fdocs\u002Fguides\u002Ferrors","docs\u002F2.guides\u002F4.errors",{"title":57,"path":58,"stem":59},"Submission and reset","\u002Fdocs\u002Fguides\u002Fsubmission-and-reset","docs\u002F2.guides\u002F5.submission-and-reset",{"title":61,"path":62,"stem":63},"Form state","\u002Fdocs\u002Fguides\u002Fform-state","docs\u002F2.guides\u002F6.form-state",{"title":65,"path":66,"stem":67},"Array fields","\u002Fdocs\u002Fguides\u002Farray-fields","docs\u002F2.guides\u002F7.array-fields",{"title":69,"path":70,"stem":71},"TypeScript","\u002Fdocs\u002Fguides\u002Ftypescript","docs\u002F2.guides\u002F8.typescript",{"title":73,"icon":11,"path":74,"stem":75,"children":76,"page":11},"Components","\u002Fdocs\u002Fcomponents","docs\u002F3.components",[77,81,85,89],{"title":78,"path":79,"stem":80},"Form","\u002Fdocs\u002Fcomponents\u002Fform","docs\u002F3.components\u002F1.form",{"title":82,"path":83,"stem":84},"Field","\u002Fdocs\u002Fcomponents\u002Ffield","docs\u002F3.components\u002F2.field",{"title":86,"path":87,"stem":88},"ArrayField","\u002Fdocs\u002Fcomponents\u002Farray-field","docs\u002F3.components\u002F3.array-field",{"title":90,"path":91,"stem":92},"FieldError","\u002Fdocs\u002Fcomponents\u002Ffield-error","docs\u002F3.components\u002F4.field-error",{"title":94,"icon":11,"path":95,"stem":96,"children":97,"page":11},"Composables","\u002Fdocs\u002Fcomposables","docs\u002F4.composables",[98],{"title":99,"path":100,"stem":101},"useForm","\u002Fdocs\u002Fcomposables\u002Fuse-form","docs\u002F4.composables\u002F1.use-form",{"title":103,"icon":11,"path":104,"stem":105,"children":106,"page":11},"Reference","\u002Fdocs\u002Freference","docs\u002F5.reference",[107,111,115],{"title":108,"path":109,"stem":110},"Types","\u002Fdocs\u002Freference\u002Ftypes","docs\u002F5.reference\u002F1.types",{"title":112,"path":113,"stem":114},"Troubleshooting","\u002Fdocs\u002Freference\u002Ftroubleshooting","docs\u002F5.reference\u002F2.troubleshooting",{"title":116,"path":117,"stem":118},"FAQ","\u002Fdocs\u002Freference\u002Ffaq","docs\u002F5.reference\u002F3.faq",{"title":120,"icon":11,"path":121,"stem":122,"children":123,"page":11},"Recipes","\u002Fdocs\u002Frecipes","docs\u002F6.recipes",[124,128,132,136,140],{"title":125,"path":126,"stem":127},"Server errors","\u002Fdocs\u002Frecipes\u002Fserver-errors","docs\u002F6.recipes\u002F1.server-errors",{"title":129,"path":130,"stem":131},"Cross-field validation","\u002Fdocs\u002Frecipes\u002Fcross-field-validation","docs\u002F6.recipes\u002F2.cross-field-validation",{"title":133,"path":134,"stem":135},"Async validation","\u002Fdocs\u002Frecipes\u002Fasync-validation","docs\u002F6.recipes\u002F3.async-validation",{"title":137,"path":138,"stem":139},"Editing existing data","\u002Fdocs\u002Frecipes\u002Fediting-existing-data","docs\u002F6.recipes\u002F4.editing-existing-data",{"title":141,"path":142,"stem":143},"Multi-step forms","\u002Fdocs\u002Frecipes\u002Fmulti-step-forms","docs\u002F6.recipes\u002F5.multi-step-forms",{"title":145,"icon":11,"path":146,"stem":147,"children":148,"page":11},"Integrations","\u002Fdocs\u002Fintegrations","docs\u002F7.integrations",[149,153,157],{"title":150,"path":151,"stem":152},"Custom components","\u002Fdocs\u002Fintegrations\u002Fcustom-components","docs\u002F7.integrations\u002F1.custom-components",{"title":154,"path":155,"stem":156},"Nuxt UI","\u002Fdocs\u002Fintegrations\u002Fnuxt-ui","docs\u002F7.integrations\u002F2.nuxt-ui",{"title":158,"path":159,"stem":160},"ShadcnVue","\u002Fdocs\u002Fintegrations\u002Fshadcn-vue","docs\u002F7.integrations\u002F3.shadcn-vue",{"title":162,"icon":11,"path":163,"stem":164,"children":165,"page":11},"Migrations","\u002Fdocs\u002Fmigrations","docs\u002F8.migrations",[166],{"title":167,"path":168,"stem":169},"Migrating from NotForm v3","\u002Fdocs\u002Fmigrations\u002Fnotform-v3","docs\u002F8.migrations\u002F1.notform-v3",{"title":171,"icon":11,"path":172,"stem":173,"children":174,"page":11},"AI","\u002Fdocs\u002Fai","docs\u002F9.ai",[175,179,183],{"title":176,"path":177,"stem":178},"Working with AI","\u002Fdocs\u002Fai\u002Fworking-with-ai","docs\u002F9.ai\u002F1.working-with-ai",{"title":180,"path":181,"stem":182},"llms.txt and Markdown pages","\u002Fdocs\u002Fai\u002Fllms-txt","docs\u002F9.ai\u002F2.llms-txt",{"title":184,"path":185,"stem":186},"Agent skill","\u002Fdocs\u002Fai\u002Fagent-skill","docs\u002F9.ai\u002F3.agent-skill",[188,192,198,203,208,213,218,221,226,231,236,241,246,251,254,259,264,269,274,279,283,286,291,296,301,306,311,315,318,323,328,333,338,343,348,353,358,362,365,370,375,380,385,390,395,400,405,410,415,420,425,430,434,437,442,447,452,457,462,466,469,474,479,484,489,494,499,504,509,514,519,523,526,531,536,541,546,551,556,561,566,571,575,578,583,588,593,598,603,608,614,619,624,628,631,635,640,645,650,655,660,665,670,674,677,682,687,692,697,702,707,712,717,721,724,729,734,739,744,749,754,758,761,766,771,776,781,786,791,796,801,804,808,812,817,822,827,831,834,838,842,846,851,856,861,865,869,872,876,880,885,890,894,897,902,907,912,917,922,926,930,934,937,942,947,952,957,962,967,970,974,979,984,989,994,999,1004,1009,1013,1018,1023,1028,1033,1038,1041,1046,1051,1056,1061,1066,1069,1074,1079,1084,1087,1092,1097,1101,1106,1111,1114,1119,1124,1129,1134,1139,1144,1149,1154,1159,1164,1169,1174,1179,1184,1189,1192,1197,1202,1207,1212,1216,1219,1224,1229,1234,1239,1242,1247,1252,1256,1261,1266,1269,1274,1279,1284,1289,1294,1299,1302,1307,1311,1316,1320,1325,1328,1333,1338,1343,1348,1353,1358,1363,1367,1370,1375,1380,1385,1390,1395,1399,1403,1408,1412,1416,1421,1425,1430,1434,1437,1442,1446,1450,1454,1458,1462,1466,1470,1474,1478,1482,1486,1489,1494,1499,1504,1509,1514,1519,1523,1526,1531,1536,1540,1543,1548,1553,1558,1563,1568,1573,1576,1581,1586,1591,1596,1601,1606,1611],{"id":17,"title":16,"titles":189,"content":190,"level":191},[],"NotForm is a headless, schema-driven form library for Vue 3 and Nuxt. See what it does, what it leaves to you, and where to start. NotForm manages form state for Vue 3 and Nuxt. You describe your data with a schema, NotForm turns it into a typed reactive form, and you keep full control of the markup and styling. \u003Cscript setup lang=\"ts\">\nimport { Field, FieldError, Form, useForm } from 'notform'\nimport { z } from 'zod'\n\nconst form = useForm({\n  initialValues: { email: '', message: '' },\n  async onSubmit(data) {\n    await fetch('\u002Fapi\u002Fcontact', {\n      body: JSON.stringify(data),\n      headers: { 'content-type': 'application\u002Fjson' },\n      method: 'POST',\n    })\n  },\n  schema: z.object({\n    email: z.email('Enter a valid email address.'),\n    message: z.string().min(10, 'Write at least 10 characters.'),\n  }),\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm :form=\"form\">\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        type=\"email\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('message')\"\n    >\n      \u003Clabel :for=\"field.path\">Message\u003C\u002Flabel>\n\n      \u003Ctextarea\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n      \u002F>\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"form.isSubmitting\"\n    >\n      Send\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",1,{"id":193,"title":194,"titles":195,"content":196,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fintroduction#what-you-get","What you get",[16],"Types from your schema. Field paths, field values, form.values, and the data passed to onSubmit are all inferred. A mistyped path is a compile error. Validation you can tune. Choose, per field, whether blur, change, or input triggers validation, whether errors clear eagerly, and how long to debounce. Stable fields. form.field(path) always returns the same instance for the same path, so touched state and pending validation survive re-renders. Repeatable sections. Array fields give every item a stable identity and keep errors attached to the right item when you add, remove, move, or swap. Plain, reactive state. isDirty, isSubmitting, isValidating, and the rest are ordinary reactive properties. There is nothing to subscribe to. Any validator. Zod, Valibot, ArkType, or any other library that implements Standard Schema.",2,{"id":199,"title":200,"titles":201,"content":202,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fintroduction#what-notform-leaves-to-you","What NotForm leaves to you",[16],"UI. NotForm renders a native \u003Cform> and nothing else. Inputs, labels, layout, and styling are yours, or your component library's. Validation rules. Rules are written in your schema, not in NotForm. Networking. onSubmit receives validated data. How you send it is up to you. Global state. Every useForm call creates an independent form. There is no store to configure.",{"id":204,"title":205,"titles":206,"content":207,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fintroduction#packages","Packages",[16],"Package Use it for notform Vue 3 and Vite applications. Import components and useForm explicitly. @notform\u002Fnuxt Nuxt 4 applications. Registers the same components and useForm as auto-imports. These docs describe NotForm v4. If your project uses NotForm 3.x, start with the v3 migration guide.",{"id":209,"title":210,"titles":211,"content":212,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fintroduction#how-this-docs-is-organized","How this docs is organized",[16],"Install NotForm, build your first form, and learn the mental model. Task-oriented explanations of binding inputs, validation, errors, submission, state, and arrays. Complete solutions to common problems such as server errors, async checks, and multi-step forms. Exact contracts for components, useForm, and every public type.",{"id":214,"title":215,"titles":216,"content":217,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fintroduction#suggested-reading-order","Suggested reading order",[16],"Installation Quickstart Core concepts Binding inputs and Validation Whatever your form needs next: errors, array fields, or a recipe Building with an AI coding assistant? See Working with AI. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":21,"title":20,"titles":219,"content":220,"level":191},[],"Install NotForm and a schema validator in a Vue 3, Vite, or Nuxt application. NotForm ships as two packages that expose the same API. Package Use it for notform Vue 3 and Vite applications. @notform\u002Fnuxt Nuxt 4 applications. Adds auto-imports for the same API.",{"id":222,"title":223,"titles":224,"content":225,"level":197},"\u002Fdocs\u002Fgetting-started\u002Finstallation#requirements","Requirements",[20],"Vue 3.5 or later (declared as a peer dependency of notform). Nuxt 4 or later when using @notform\u002Fnuxt. A Standard Schema-compatible validator, installed separately. The examples use Zod. Node.js 24 or later. Depending on your package manager or environment settings, an older version may warn or fail.",{"id":227,"title":228,"titles":229,"content":230,"level":197},"\u002Fdocs\u002Fgetting-started\u002Finstallation#vue-and-vite","Vue and Vite",[20],"Install notform and a validator: pnpm add notform zod\nnpm install notform zod\nyarn add notform zod\nbun add notform zod NotForm needs no Vue plugin and no global registration. Import what you use: import { ArrayField, Field, FieldError, Form, useForm } from 'notform'",{"id":232,"title":233,"titles":234,"content":235,"level":197},"\u002Fdocs\u002Fgetting-started\u002Finstallation#nuxt","Nuxt",[20],"Install the module and a validator: pnpm add @notform\u002Fnuxt zod\nnpm install @notform\u002Fnuxt zod\nyarn add @notform\u002Fnuxt zod\nbun add @notform\u002Fnuxt zod Register the module, then restart the dev server: export default defineNuxtConfig({\n  modules: ['@notform\u002Fnuxt'],\n}) useForm, Form, Field, ArrayField, and FieldError are now auto-imported. See the Nuxt module guide for aliases and type imports. Code samples in these docs import from notform so they work anywhere. In a Nuxt project, delete the import ... from 'notform' line and keep the rest.",{"id":237,"title":238,"titles":239,"content":240,"level":197},"\u002Fdocs\u002Fgetting-started\u002Finstallation#choose-a-validator","Choose a validator",[20],"NotForm accepts any object schema that implements Standard Schema. It does not bundle a validator and does not translate rules between libraries. Switching validators changes only the schema. import { z } from 'zod'\n\nconst schema = z.object({\n  email: z.email('Enter a valid email address.'),\n})\nimport * as v from 'valibot'\n\nconst schema = v.object({\n  email: v.pipe(v.string(), v.email('Enter a valid email address.')),\n})\nimport { type } from 'arktype'\n\nconst schema = type({\n  email: 'string.email',\n}) Zod examples use the Zod 4 API (z.email()). On Zod 3, write z.string().email() instead. Standard Schema support requires Zod 3.24 or later, and recent releases of Valibot and ArkType. The schema must be an object schema. A bare z.string() is rejected by TypeScript, because form values are always a keyed object.",{"id":242,"title":243,"titles":244,"content":245,"level":197},"\u002Fdocs\u002Fgetting-started\u002Finstallation#verify-the-install","Verify the install",[20],"Render this component. Submitting an empty field should show the message, and a valid email should log the data. \u003Cscript setup lang=\"ts\">\nimport { Field, FieldError, Form, useForm } from 'notform'\nimport { z } from 'zod'\n\nconst form = useForm({\n  initialValues: { email: '' },\n  onSubmit: (data) => {\n    return console.log(data)\n  },\n  schema: z.object({\n    email: z.email('Enter a valid email address.')\n  }),\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm :form=\"form\">\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        type=\"email\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003Cbutton type=\"submit\">\n      Submit\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",{"id":247,"title":248,"titles":249,"content":250,"level":197},"\u002Fdocs\u002Fgetting-started\u002Finstallation#next-steps","Next steps",[20],"Continue with the Quickstart. html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":25,"title":24,"titles":252,"content":253,"level":191},[],"Build a complete sign-in form with schema validation, field errors, a submit handler, and reset. This page builds a sign-in form from start to finish. You will define a schema, create a form, bind two inputs, and handle submission. It assumes you have completed Installation.",{"id":255,"title":256,"titles":257,"content":258,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fquickstart#_1-define-a-schema","1. Define a schema",[24],"The schema describes the values the user edits and the rules they must satisfy. \u003Cscript setup lang=\"ts\">\nimport { z } from 'zod'\n\nconst signInSchema = z.object({\n  email: z.email('Enter a valid email address.'),\n  password: z.string().min(8, 'Use at least 8 characters.'),\n})\n\u003C\u002Fscript>",{"id":260,"title":261,"titles":262,"content":263,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fquickstart#_2-create-the-form","2. Create the form",[24],"useForm takes the schema, the starting values, and a submit handler. \u003Cscript setup lang=\"ts\">\nimport { useForm } from 'notform'\n\nconst form = useForm({\n  initialValues: { email: '', password: '' },\n  async onSubmit(data) {\n    await signIn(data)\n  },\n  schema: signInSchema,\n})\n\u003C\u002Fscript> initialValues are what the inputs show first. See Schema input and output. onSubmit runs only after the whole schema validates. data is the validated output.",{"id":265,"title":266,"titles":267,"content":268,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fquickstart#_3-bind-the-fields","3. Bind the fields",[24],"form.field(path) creates a field. Field hands it to your template through a slot. \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('email')\"\n  >\n    \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n      autocomplete=\"email\"\n      type=\"email\"\n    >\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> v-model=\"field.model\" reads and writes the value in form.values. v-bind=\"field.events\" spreads onBlur, onChange, and onInput, which tell NotForm when to validate. FieldError shows the first error for this field and renders nothing when there is none.",{"id":270,"title":271,"titles":272,"content":273,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fquickstart#_4-render-and-submit","4. Render and submit",[24],"Form renders a native \u003Cform>. Native submit calls form.submit, and native reset calls form.reset. \u003Cscript setup lang=\"ts\">\nimport { Field, FieldError, Form, useForm } from 'notform'\nimport { z } from 'zod'\n\nconst signInSchema = z.object({\n  email: z.email('Enter a valid email address.'),\n  password: z.string().min(8, 'Use at least 8 characters.'),\n})\n\nconst form = useForm({\n  initialValues: { email: '', password: '' },\n  async onSubmit(data) {\n    await signIn(data)\n  },\n  schema: signInSchema,\n})\n\nasync function signIn(data: { email: string, password: string }) {\n  await fetch('\u002Fapi\u002Fsign-in', {\n    body: JSON.stringify(data),\n    headers: { 'content-type': 'application\u002Fjson' },\n    method: 'POST',\n  })\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm\n    :form=\"form\"\n    novalidate\n  >\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        autocomplete=\"email\"\n        type=\"email\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('password')\"\n    >\n      \u003Clabel :for=\"field.path\">Password\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        autocomplete=\"current-password\"\n        type=\"password\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"form.isSubmitting\"\n    >\n      {{ form.isSubmitting ? 'Signing in...' : 'Sign in' }}\n    \u003C\u002Fbutton>\n\n    \u003Cbutton\n      type=\"reset\"\n      :disabled=\"form.isSubmitting\"\n    >\n      Reset\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate> novalidate turns off the browser's built-in constraint validation so your schema controls every message.",{"id":275,"title":276,"titles":277,"content":278,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fquickstart#what-you-just-built","What you just built",[24],"Behavior Why it happens Leaving an empty email field shows an error. Fields validate on blur and change by default. Typing does not show errors on a pristine field. Input events only validate after the field already has an error. After an error appears, fixing the value clears it as you type. Default eager mode re-validates on input once a field has an error. Clicking Sign in with invalid values shows every error and does not call onSubmit. Submit validates the whole form first. The button is disabled while the request runs. form.isSubmitting stays true until onSubmit settles. Reset restores the starting values and clears errors. Form calls form.reset() on native reset.",{"id":280,"title":248,"titles":281,"content":282,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fquickstart#next-steps",[24],"Learn the model behind this in Core concepts. Bind checkboxes, radios, selects, and more with Binding inputs. Tune when errors appear in Validation. Show errors returned by your API with Server errors. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"id":29,"title":28,"titles":284,"content":285,"level":191},[],"Register @notform\u002Fnuxt to use NotForm's components and useForm as Nuxt auto-imports, and resolve naming conflicts with aliases. @notform\u002Fnuxt registers NotForm's public components and composable as Nuxt auto-imports. It does not change validation, field, or submission behavior. Every other page in these docs applies unchanged. The module requires Nuxt 4 or later. For Vue or Vite applications, use the notform package.",{"id":287,"title":288,"titles":289,"content":290,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fnuxt-module#install-and-register","Install and register",[28],"pnpm add @notform\u002Fnuxt zod\nnpm install @notform\u002Fnuxt zod\nyarn add @notform\u002Fnuxt zod\nbun add @notform\u002Fnuxt zod export default defineNuxtConfig({\n  modules: ['@notform\u002Fnuxt'],\n}) Restart the dev server after changing the module list.",{"id":292,"title":293,"titles":294,"content":295,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fnuxt-module#what-is-auto-imported","What is auto-imported",[28],"Name Kind useForm Composable Form Component Field Component ArrayField Component FieldError Component Your schema library is not auto-imported. Import it as usual. \u003Cscript setup lang=\"ts\">\nimport { z } from 'zod'\n\nconst form = useForm({\n  initialValues: { email: '' },\n  onSubmit(data) {\n    console.log(data.email)\n  },\n  schema: z.object({\n    email: z.email('Enter a valid email address.')\n  }),\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm :form=\"form\">\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        type=\"email\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003Cbutton type=\"submit\">\n      Submit\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",{"id":297,"title":298,"titles":299,"content":300,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fnuxt-module#import-types","Import types",[28],"Types are not auto-imported. To import them, add notform as a direct dependency of your app. Package managers such as pnpm do not expose a module's own dependencies to your code. pnpm add notform import type { SchemaInput, SchemaOutput, UseFormInstance } from 'notform' The same applies if you prefer explicit imports of components or useForm over auto-imports. Both approaches work together.",{"id":302,"title":303,"titles":304,"content":305,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fnuxt-module#avoid-name-conflicts","Avoid name conflicts",[28],"If your app, or another module, already defines Form, Field, FieldError, ArrayField, or useForm, register NotForm's version under another name with the aliases option. Add only the aliases you need. export default defineNuxtConfig({\n  modules: ['@notform\u002Fnuxt'],\n  notform: {\n    aliases: {\n      Field: 'NotField',\n      FieldError: 'NotFieldError',\n      Form: 'AppForm',\n      useForm: 'useAppForm',\n    },\n  },\n}) Then use the alias everywhere: \u003Ctemplate>\n  \u003CAppForm :form=\"form\">\n    \u003CNotField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      \u003CNotFieldError :errors=\"field\" \u002F>\n    \u003C\u002FNotField>\n  \u003C\u002FAppForm>\n\u003C\u002Ftemplate> Keys are the original NotForm names. Values are the names you want to use. Component libraries that ship their own Field or FieldError, such as ShadcnVue, are the most common source of conflicts. See ShadcnVue.",{"id":307,"title":308,"titles":309,"content":310,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fnuxt-module#server-side-rendering","Server-side rendering",[28],"NotForm creates form state during setup and does not need browser APIs to do it, so forms render on the server. Validation timers and event handlers only run in the browser, in response to user interaction.",{"id":312,"title":248,"titles":313,"content":314,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fnuxt-module#next-steps",[28],"Build your first form with the Quickstart. Use NotForm with Nuxt UI in the Nuxt UI guide. html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":33,"title":32,"titles":316,"content":317,"level":191},[],"The mental model behind NotForm: the schema, the form instance, fields, paths, the initial-values baseline, and what happens on every keystroke and submit. Everything in NotForm follows from a handful of ideas. Read this page once and the rest of the documentation becomes predictable.",{"id":319,"title":320,"titles":321,"content":322,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fcore-concepts#the-schema-is-the-source-of-truth","The schema is the source of truth",[32],"You pass an object schema to useForm. NotForm derives three things from it: the type of form.values, which is the schema's input; the paths that form.field() and form.arrayField() accept; the type of the data that onSubmit receives, which is the schema's output. The schema also performs all validation. NotForm has no rule syntax of its own. Input and output can differ. A schema that turns the text '42' into the number 42 has input { age: string } and output { age: number }. Inputs bind to the values users edit. Outputs are what your code receives. See Schema input and output.",{"id":324,"title":325,"titles":326,"content":327,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fcore-concepts#one-form-instance-owns-the-state","One form instance owns the state",[32],"useForm returns a reactive form instance. It owns: values: the live, editable data; errors: the current list of validation issues; state flags such as isDirty, isSubmitting, and isValidating; actions such as submit, validate, reset, setError, and setValue. You read state directly (form.isDirty, not form.isDirty.value) and pass the instance to \u003CForm :form=\"form\">.",{"id":329,"title":330,"titles":331,"content":332,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fcore-concepts#fields-are-handles-not-copies","Fields are handles, not copies",[32],"form.field('email') does not store a value. It returns a handle whose model reads and writes form.values.email, and whose errors, isTouched, isDirty, and events are scoped to that path. Fields are cached by path. Calling form.field('email') again, including on every render, returns the same instance with the same id. That is why touched state and pending validation survive re-renders. Consequence: the optional configuration passed to form.field(path, config) is read once, on the first call for that path. Field is a convenience component that hands a field to its slot. It is optional. A field is also usable straight from script.",{"id":334,"title":335,"titles":336,"content":337,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fcore-concepts#paths-are-strings-issue-paths-are-segments","Paths are strings; issue paths are segments",[32],"You address values with dot-notation paths: 'email', 'address.city', 'contacts.0.email'. Paths are checked against the schema input. Validators report where an issue occurred as an array of segments: ['address', 'city']. When you set errors yourself, you write segments: form.setError({ message: 'Already taken.', path: ['username'] }) An issue with no path applies to the whole form.",{"id":339,"title":340,"titles":341,"content":342,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fcore-concepts#initial-values-are-a-baseline","Initial values are a baseline",[32],"initialValues and initialErrors define the baseline. The form compares against it to compute isDirty, and reset() restores it. reset(values, errors) replaces the baseline, which is how you load saved data into an edit form.",{"id":344,"title":345,"titles":346,"content":347,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fcore-concepts#what-happens-when-a-user-types","What happens when a user types",[32],"The input updates field.model, which writes into form.values. The input's event (input, change, or blur) calls the matching handler in field.events. The handler checks the field's configuration: is this event enabled, or is the field in eager mode with an existing error? If a debounce is configured, validation waits for a pause. Blur never waits. NotForm validates a snapshot of the whole form with your schema. Only the issues at this field's exact path replace the field's errors. If the user changed any value while validation was running, the result is discarded as stale and the errors are left alone. Details are in Validation.",{"id":349,"title":350,"titles":351,"content":352,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fcore-concepts#what-happens-when-a-user-submits","What happens when a user submits",[32],"If a submit is already running, the new one is ignored. isSubmitting becomes true and submitCount increases. Every field now counts as touched. The whole form is validated. If a value changed while validating, validation runs again. If the schema reports issues, they replace all errors and onSubmit is not called. If it passes, errors are cleared and onSubmit is awaited with the validated output. isSubmitting returns to false, even if onSubmit throws. Details are in Submission and reset.",{"id":354,"title":355,"titles":356,"content":357,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fcore-concepts#glossary","Glossary",[32],"Term Meaning Schema input The shape of form.values: what users edit, before parsing. Schema output The shape onSubmit receives: the parsed result. Path A dot-notation string such as 'address.city'. Used with form.field() and form.setValue(). Segments A path as an array, such as ['address', 'city']. Used by issues. Issue One validation problem: a message and an optional path. Field instance The cached handle returned by form.field(path). Array field A field for an array, with items and mutation methods. Returned by form.arrayField(path). Item One entry of an array field. It is a field for its own value and has an index. Baseline The current initialValues and initialErrors. Touched A field was blurred, or a submit was attempted. Dirty Current values differ from the baseline. Stale A validation result that was made obsolete by a newer run or a value change. It is discarded.",{"id":359,"title":248,"titles":360,"content":361,"level":197},"\u002Fdocs\u002Fgetting-started\u002Fcore-concepts#next-steps",[32],"Binding inputs shows how to connect every kind of control. Validation explains how to tune the behavior described above. html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":42,"title":41,"titles":363,"content":364,"level":191},[],"Connect text inputs, textareas, numbers, checkboxes, radios, selects, dates, and files to NotForm fields. Every control connects to a field the same way: write the value through field.model, and report interaction through field.events. This page shows the pattern for each native input type. For custom components, see Custom components.",{"id":366,"title":367,"titles":368,"content":369,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#the-pattern","The pattern",[41],"\u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('email')\"\n  >\n    \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n      autocomplete=\"email\"\n      type=\"email\"\n    >\n  \u003C\u002FField>\n\u003C\u002Ftemplate> field.model is a two-way binding to form.values at the field's path. Writing to it does not validate. field.events is { onBlur, onChange, onInput }. These mark the field touched and trigger validation according to the field's configuration. Put v-model before v-bind=\"field.events\". The value must be written before the validation event reads it.",{"id":371,"title":372,"titles":373,"content":374,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#always-give-initial-values","Always give initial values",[41],"Though not mandatory, provide an entry in initialValues for every field you bind. Missing values reach your schema as undefined, so validators report their generic \"expected string, received undefined\" message instead of your custom one, and isDirty can become true after a user types and deletes. const form = useForm({\n  initialValues: {\n    age: undefined,\n    interests: [],\n    name: '',\n    newsletter: false\n  },\n  schema,\n})",{"id":376,"title":377,"titles":378,"content":379,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#text-email-password-search-url","Text, email, password, search, URL",[41],"\u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('name')\"\n  >\n    \u003Clabel :for=\"field.path\">Name\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n      type=\"text\"\n    >\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> Native modifiers work as usual. v-model.trim for example removes surrounding whitespace as the user types.",{"id":381,"title":382,"titles":383,"content":384,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#textarea","Textarea",[41],"\u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('message')\"\n  >\n    \u003Clabel :for=\"field.path\">Message\u003C\u002Flabel>\n\n    \u003Ctextarea\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n      rows=\"4\"\n    \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":386,"title":387,"titles":388,"content":389,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#number","Number",[41],"\u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('quantity')\"\n  >\n    \u003Clabel :for=\"field.path\">Quantity\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      v-model.number=\"field.model\"\n      v-bind=\"field.events\"\n      type=\"number\"\n      min=\"0\"\n    >\n  \u003C\u002FField>\n\u003C\u002Ftemplate> const schema = z.object({ quantity: z.number().min(1, 'Order at least one.') })\n\u002F\u002F initialValues: { quantity: 1 } v-model.number converts typed text to a number. If the user clears the box, the browser reports an empty string, which a z.number() schema rejects with its type message. Set a custom type message in your schema, or keep the input as a string and convert in the schema. See Schema input and output.",{"id":391,"title":392,"titles":393,"content":394,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#single-checkbox","Single checkbox",[41],"Bind a boolean. \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('acceptTerms')\"\n  >\n    \u003Clabel>\n      \u003Cinput\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        type=\"checkbox\"\n      >\n      I accept the terms\n    \u003C\u002Flabel>\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> const schema = z.object({\n  acceptTerms: z.boolean('You must accept the terms.'),\n})\n\u002F\u002F initialValues: { acceptTerms: false }",{"id":396,"title":397,"titles":398,"content":399,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#checkbox-group","Checkbox group",[41],"Bind one field to an array of the selected values. Each checkbox has its own :value. \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('interests')\"\n  >\n    \u003Cfieldset>\n      \u003Clegend>Interests\u003C\u002Flegend>\n\n      \u003Clabel\n        v-for=\"option in ['design', 'code', 'writing']\"\n        :key=\"option\"\n      >\n        \u003Cinput\n          v-model=\"field.model\"\n          v-bind=\"field.events\"\n          type=\"checkbox\"\n          :value=\"option\"\n        >\n        {{ option }}\n      \u003C\u002Flabel>\n    \u003C\u002Ffieldset>\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> const schema = z.object({\n  interests: z.array(z.string()).min(1, 'Pick at least one.'),\n})\n\u002F\u002F initialValues: { interests: [] } A checkbox group is a plain form.field() whose value happens to be an array of fixed choices. Use form.arrayField() when users add, remove, or reorder rows of structured data.",{"id":401,"title":402,"titles":403,"content":404,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#radio-group","Radio group",[41],"All radios share one field and differ by :value. \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('plan')\"\n  >\n    \u003Cfieldset>\n      \u003Clegend>Plan\u003C\u002Flegend>\n\n      \u003Clabel\n        v-for=\"option in ['free', 'pro']\"\n        :key=\"option\"\n      >\n        \u003Cinput\n          v-model=\"field.model\"\n          v-bind=\"field.events\"\n          type=\"radio\"\n          :value=\"option\"\n        >\n        {{ option }}\n      \u003C\u002Flabel>\n    \u003C\u002Ffieldset>\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":406,"title":407,"titles":408,"content":409,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#select","Select",[41],"\u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('nationality')\"\n  >\n    \u003Clabel :for=\"field.path\">Nationality\u003C\u002Flabel>\n\n    \u003Cselect\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n    >\n      \u003Coption\n        value=\"\"\n        disabled\n      >\n        Choose a country\n      \u003C\u002Foption>\n\n      \u003Coption\n        v-for=\"country in countries\"\n        :key=\"country.code\"\n        :value=\"country.code\"\n      >\n        {{ country.name }}\n      \u003C\u002Foption>\n    \u003C\u002Fselect>\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> For \u003Cselect multiple>, bind an array exactly like a checkbox group.",{"id":411,"title":412,"titles":413,"content":414,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#dates-and-times","Dates and times",[41],"Native date and time inputs produce strings such as 2026-11-02. Bind them like text, and validate or transform them in the schema. \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('birthday')\"\n  >\n    \u003Clabel :for=\"field.path\">Birthday\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n      type=\"date\"\n    >\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":416,"title":417,"titles":418,"content":419,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#files","Files",[41],"v-model does not work on file inputs. Write the file with an event handler instead and report the change yourself. \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('avatar')\"\n  >\n    \u003Clabel :for=\"field.path\">Avatar\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      type=\"file\"\n      @change=\"(event) => {\n        field.model = (event.target as HTMLInputElement).files?.[0]\n        field.events.onChange()\n      }\"\n      @blur=\"field.events.onBlur\"\n    >\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":421,"title":422,"titles":423,"content":424,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#skipping-the-field-component","Skipping the Field component",[41],"Field only hands the field to its slot. If you prefer, create the field in script and use it directly. This is common in reusable input components. \u003Cscript setup lang=\"ts\">\nconst email = form.field('email')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput\n    v-bind=\"email.events\"\n    :id=\"email.path\"\n    v-model=\"email.model\"\n    type=\"email\"\n    :name=\"email.path\"\n  >\n\u003C\u002Ftemplate>",{"id":426,"title":427,"titles":428,"content":429,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#labels-and-accessibility","Labels and accessibility",[41],"Use field.path for the control's id and the label's for. field.id is a symbol and is not valid as an HTML id. Mark invalid controls with :aria-invalid=\"!field.isValid\". Link errors to controls with aria-describedby. See Errors.",{"id":431,"title":248,"titles":432,"content":433,"level":197},"\u002Fdocs\u002Fguides\u002Fbinding-inputs#next-steps",[41],"Validation controls when errors appear. Errors covers rendering every kind of error. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":46,"title":45,"titles":435,"content":436,"level":191},[],"Understand how NotForm types the values users edit separately from the validated data your submit handler receives. A schema can accept one shape and produce another. NotForm keeps these two shapes distinct everywhere: Input Output What it is What users edit, before parsing What the schema produces after validating Where it appears form.values, initialValues, field.model, form.setValue() The argument of onSubmit, and { value } from form.validate() Type helper SchemaInput\u003Ctypeof schema> SchemaOutput\u003Ctypeof schema>",{"id":438,"title":439,"titles":440,"content":441,"level":197},"\u002Fdocs\u002Fguides\u002Fschema-input-output#a-transform-changes-the-output","A transform changes the output",[45],"import { z } from 'zod'\n\nconst orderSchema = z.object({\n  quantity: z.string().transform(Number),\n})\n\nconst form = useForm({\n  initialValues: { quantity: '2' },\n  onSubmit(data) {\n    data.quantity.toFixed(0) \u002F\u002F data.quantity is a number\n  },\n  schema: orderSchema,\n})\n\nform.values.quantity \u002F\u002F a string The input control holds text, so the form value stays a string. Your handler receives the converted number. The same distinction applies to every Standard Schema validator.",{"id":443,"title":444,"titles":445,"content":446,"level":197},"\u002Fdocs\u002Fguides\u002Fschema-input-output#common-ways-input-and-output-differ","Common ways input and output differ",[45],"Schema feature Effect on the shapes Transform or coercion The output type differs from the input type. Default value The key is optional in the input and required in the output. Trimming or normalizing form.values keeps what the user typed. The output is normalized. Pipes into another schema The output is the final stage. The input is the first. Because form.values always holds the input, the text a user typed is never silently rewritten. Normalization happens only in the data you receive on submit.",{"id":448,"title":449,"titles":450,"content":451,"level":197},"\u002Fdocs\u002Fguides\u002Fschema-input-output#initial-values","Initial values",[45],"initialValues use the input shape, and may be partial. They are the values controls display, so use the representation your controls and schema expect, not the parsed output. useForm({\n  initialValues: { quantity: '2' }, \u002F\u002F input shape, not a number\n  schema,\n}) Though not mandatory, provide a value for every field you bind. A key missing from initialValues reaches the schema as undefined, and validators then report their generic type message instead of your custom one. Empty strings, false, and [] are the usual starting values for text, checkboxes, and lists.",{"id":453,"title":454,"titles":455,"content":456,"level":197},"\u002Fdocs\u002Fguides\u002Fschema-input-output#typed-paths-and-values","Typed paths and values",[45],"Paths and values for form.field(), form.arrayField(), and form.setValue() come from the input shape. A path that does not exist is a TypeScript error, and setValue checks the value type. form.setValue('quantity', '3') \u002F\u002F ok\nform.setValue('quantity', 3) \u002F\u002F type error: input is a string\nform.field('quantty') \u002F\u002F type error: no such path",{"id":458,"title":459,"titles":460,"content":461,"level":197},"\u002Fdocs\u002Fguides\u002Fschema-input-output#naming-the-types","Naming the types",[45],"Import the helpers when you need to name a shape, for example in a function signature. import type { SchemaInput, SchemaOutput } from 'notform'\n\ntype OrderInput = SchemaInput\u003Ctypeof orderSchema>\ntype OrderOutput = SchemaOutput\u003Ctypeof orderSchema>\n\nasync function saveOrder(order: OrderOutput) {\n  \u002F\u002F ...\n} See TypeScript for more patterns.",{"id":463,"title":248,"titles":464,"content":465,"level":197},"\u002Fdocs\u002Fguides\u002Fschema-input-output#next-steps",[45],"Validation explains when the schema runs. Submission and reset explains how output reaches onSubmit. html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"id":50,"title":49,"titles":467,"content":468,"level":191},[],"How NotForm validates, when each field validates, and how to tune triggers, debounce, async rules, and stale results. Rules live in your schema. NotForm decides when to run the schema and which errors to keep.",{"id":470,"title":471,"titles":472,"content":473,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#how-validation-works","How validation works",[49],"NotForm always validates the whole form against a snapshot of the current values. What differs is which results are kept: A field validation is triggered by a field event or field.validate(). The whole schema runs, but only issues at that field's exact path replace that field's errors. Other errors are untouched. A form validation is triggered by form.validate() or a submit. Every error is replaced with the schema's result. Three consequences are worth remembering: An issue reported on a parent path such as ['address'] is not shown by the address.city field, and the reverse is also true. A rule that spans fields must report its issue on the path of the field that should display it. See Cross-field validation. Every field validation executes the entire schema. This matters for slow or asynchronous rules. See Async rules and performance.",{"id":475,"title":476,"titles":477,"content":478,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#when-fields-validate","When fields validate",[49],"Each field has three trigger events. These are the defaults: Event Default Behavior blur Validates immediately Always marks the field touched, even if validation on blur is turned off. change Validates Fires when a control commits a value, such as picking an option. input Does not validate Fires while typing. Validates only if the field already has an error and the mode is eager. Together with the default eager mode, this gives the most common behavior: no errors while a user first types, an error when they leave the field, and the error clearing as soon as they fix it.",{"id":480,"title":481,"titles":482,"content":483,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#configure-a-field","Configure a field",[49],"Pass a FieldConfig as the second argument to form.field() or form.arrayField(). const username = form.field('username', {\n  debounce: 300,\n  validateOn: { input: true },\n  validationMode: 'lazy',\n}) Option Type Default Meaning validateOn { blur?: boolean, change?: boolean, input?: boolean } blur and change on, input off Events that trigger validation. Omitted keys keep their defaults. validationMode 'eager' | 'lazy' 'eager' eager: once a field has errors, input and change re-validate it. lazy: only events enabled in validateOn validate. debounce number (ms) none Delay for input and change validation. Blur and submit are never delayed. The config is read once, when the field is first created. Later calls with the same path return the cached field and ignore any new config. Create each field with its config in one place, preferably in \u003Cscript setup>.",{"id":485,"title":486,"titles":487,"content":488,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#presets","Presets",[49],"form.field('email')\nform.field('password', { validateOn: { input: true } })\nform.field('notes', {\n  validateOn: { blur: false, change: false },\n  validationMode: 'lazy',\n})\nform.field('username', { debounce: 400, validateOn: { input: true } }) In the submit-only preset, blur still marks the field touched, but nothing validates until the user submits. After a failed submit, errors appear for every field.",{"id":490,"title":491,"titles":492,"content":493,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#debounce","Debounce",[49],"debounce waits for the user to pause before validating on input or change. It collapses a burst of keystrokes into one validation, which is most useful when the schema performs network calls. const username = form.field('username', { debounce: 400 }) Blur and form submission are not delayed. field.validate() skips any pending debounce and validates immediately. Resetting the form cancels pending debounced validations.",{"id":495,"title":496,"titles":497,"content":498,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#validate-explicitly","Validate explicitly",[49],"\u002F\u002F Whole form. Replaces all errors. Does not call onSubmit.\nconst result = await form.validate()\nif (result.issues) {\n  \u002F\u002F invalid\n} else {\n  result.value \u002F\u002F parsed output\n}\n\n\u002F\u002F One field. Skips pending debounce. Updates only that field's errors.\nconst outcome = await form.field('email').validate() form.isValid does not run validation. It only reports whether the current error list is empty. See Form state.",{"id":500,"title":501,"titles":502,"content":503,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#programmatic-changes-do-not-validate","Programmatic changes do not validate",[49],"Writing through field.model, form.setValue(), or form.values changes data but does not validate. Validate afterward if you need fresh errors. form.setValue('country', 'NG')\nawait form.field('country').validate()",{"id":505,"title":506,"titles":507,"content":508,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#async-rules-and-performance","Async rules and performance",[49],"Standard Schema validators can return promises, so async rules, such as \"is this username taken?\", work. Because every field validation runs the whole schema, an async rule runs whenever any field validates, not only the field it belongs to. For network checks: debounce the field that owns the rule; cache results by value so unchanged values do not hit the network again; skip the check when cheaper rules, such as minimum length, already fail. The Async validation recipe shows all three.",{"id":510,"title":511,"titles":512,"content":513,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#stale-results","Stale results",[49],"Validation is asynchronous, so values can change while a run is in flight. NotForm discards such results instead of overwriting newer state: Any value change marks every in-flight validation stale. Its result is dropped and the errors stay as they were. The change's own event normally starts a fresh validation. A newer validation of the same field replaces an older one. A submit in progress is never invalidated by a field or manual validation. It is re-run only when a value changes underneath it. Changing a reactive schema or resetting the form also marks in-flight runs stale. You never need to manage this yourself, but it explains why a result that arrives late may not show up.",{"id":515,"title":516,"titles":517,"content":518,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#reactive-schemas","Reactive schemas",[49],"schema accepts a value, a ref, or a getter. NotForm reads it each time validation runs. const requireTaxId = ref(false)\n\nconst form = useForm({\n  schema: () => {\n    return requireTaxId.value ? businessSchema : personalSchema\n  },\n}) Changing the schema discards in-flight validations, but it does not re-validate or change existing errors. Call form.validate() after switching if you want fresh results. Keep the input shape compatible between schemas, because the typed paths come from the schema's type.",{"id":520,"title":248,"titles":521,"content":522,"level":197},"\u002Fdocs\u002Fguides\u002Fvalidation#next-steps",[49],"Errors shows how to render what validation finds. Cross-field validation covers confirm-password and date ranges. html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":54,"title":53,"titles":524,"content":525,"level":191},[],"Render field, form-level, and section errors, add server errors, and make error messages accessible. All errors are issues: an object with a message and an optional path. { message: 'Enter a valid email address.', path: ['email'] } path is an array of segments. Segments are property names and array indices: ['contacts', 0, 'email']. Depending on the validator, a segment may be a string, a number, or an object with a key. An issue without a path is a form-level issue. form.errors is the full list. Each field exposes the issues at its own exact path as field.errors.",{"id":527,"title":528,"titles":529,"content":530,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#show-a-fields-error","Show a field's error",[53],"FieldError renders the first message and nothing when there is no error. \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('email')\"\n  >\n    \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n    \u003Cinput\n      v-bind=\"field.events\"\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      type=\"email\"\n    >\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> Style it, change its element, or use a slot. See FieldError. \u003Ctemplate>\n  \u003CFieldError\n    :errors=\"field\"\n    as=\"p\"\n    class=\"text-red-600\"\n    role=\"alert\"\n  \u002F>\n\u003C\u002Ftemplate>",{"id":532,"title":533,"titles":534,"content":535,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#show-every-error","Show every error",[53],"FieldError shows only the first message. To list all of them, render field.errors. \u003Ctemplate>\n  \u003Cul v-if=\"field.errors.length > 0\">\n    \u003Cli\n      v-for=\"(issue, index) in field.errors\"\n      :key=\"index\"\n    >\n      {{ issue.message }}\n    \u003C\u002Fli>\n  \u003C\u002Ful>\n\u003C\u002Ftemplate>",{"id":537,"title":538,"titles":539,"content":540,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#show-form-level-errors","Show form-level errors",[53],"Read form.errors and keep the issues without a path. \u003Cscript setup lang=\"ts\">\nimport { computed } from 'vue'\n\nconst formErrors = computed(() => {\n  return form.errors.filter((issue) => {\n    return !issue.path?.length\n  })\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cp\n    v-for=\"(issue, index) in formErrors\"\n    :key=\"index\"\n    role=\"alert\"\n  >\n    {{ issue.message }}\n  \u003C\u002Fp>\n\u003C\u002Ftemplate> You can also pass an issue array straight to FieldError: \u003CFieldError :errors=\"formErrors\" as=\"p\" role=\"alert\" \u002F>.",{"id":542,"title":543,"titles":544,"content":545,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#show-errors-for-a-section","Show errors for a section",[53],"A field shows only issues at its exact path. An issue on ['address'] does not appear on the address.city field. To summarize a whole section, filter by the first segment. function keyOf(segment: PropertyKey | { key: PropertyKey }) {\n  return typeof segment === 'object' ? segment.key : segment\n}\n\nfunction sectionErrors(section: string) {\n  return form.errors.filter((issue) => {\n    return issue.path?.[0] !== undefined && String(keyOf(issue.path[0])) === section\n  })\n}",{"id":547,"title":548,"titles":549,"content":550,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#show-an-error-summary","Show an error summary",[53],"After a failed submit, form.errors holds every problem in the form. It is the natural source for a summary at the top of a long form. \u003Ctemplate>\n  \u003Cdiv\n    v-if=\"form.isSubmitted && form.errors.length > 0\"\n    role=\"alert\"\n  >\n    \u003Cp>Fix {{ form.errors.length }} problem(s) to continue.\u003C\u002Fp>\n\n    \u003Cul>\n      \u003Cli\n        v-for=\"(issue, index) in form.errors\"\n        :key=\"index\"\n      >\n        {{ issue.message }}\n      \u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>",{"id":552,"title":553,"titles":554,"content":555,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#add-errors-yourself","Add errors yourself",[53],"form.setError(issue) adds an error, typically after a server response. form.setError({ message: 'This email address is already in use.', path: ['email'] })\nform.setError({ message: 'We could not reach the server.' }) \u002F\u002F form-level With a path, it replaces the errors already at that exact path. Without a path, it appends a form-level error. Calling it twice adds two. See Server errors for a complete submit flow.",{"id":557,"title":558,"titles":559,"content":560,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#start-with-errors","Start with errors",[53],"Pass initialErrors when a form should open with issues, for example after a server-side render. const form = useForm({\n  initialErrors: [{ message: 'Verify this address.', path: ['email'] }],\n  initialValues: { email: 'jane@example.com' },\n  schema,\n}) reset() returns to the current initial errors. reset(values, errors) replaces them.",{"id":562,"title":563,"titles":564,"content":565,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#when-errors-change","When errors change",[53],"Event Effect on errors A field validates (blur, change, input, field.validate()) Replaces only the errors at that field's exact path. form.validate() Replaces all errors with the schema's result. Submit Replaces all errors with the schema's result. A passing submit clears them before onSubmit runs. form.setError(issue) Adds or replaces errors at one path, or appends a form-level error. An array mutation Errors stay attached to the item they belong to. Removed items lose their errors. See Array fields. form.reset() Restores the initial errors. Because a field validation replaces the errors at its path, a server error on email disappears the next time email validates and the schema reports no problem there. Form-level server errors stay until the next submit or reset. This is usually what you want, since editing a field makes its server error stale.",{"id":567,"title":568,"titles":569,"content":570,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#accessible-error-markup","Accessible error markup",[53],"Connect each message to its control so assistive technology announces it. \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('email')\"\n  >\n    \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n      type=\"email\"\n      :aria-invalid=\"!field.isValid\"\n      :aria-describedby=\"field.isValid ? undefined : `${field.path}-error`\"\n    >\n\n    \u003CFieldError\n      :id=\"`${field.path}-error`\"\n      :errors=\"field\"\n      role=\"alert\"\n    \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> FieldError forwards id, class, role, and other attributes to the element it renders.",{"id":572,"title":248,"titles":573,"content":574,"level":197},"\u002Fdocs\u002Fguides\u002Ferrors#next-steps",[53],"Submission and reset Server errors recipe html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":58,"title":57,"titles":576,"content":577,"level":191},[],"Submit validated data, handle errors thrown by your submit handler, validate without submitting, and reset values, errors, and state.",{"id":579,"title":580,"titles":581,"content":582,"level":197},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#submit-with-form","Submit with Form",[57],"Form renders a native \u003Cform>. It prevents the browser's page navigation on submit, calls form.submit, and handles native reset by calling form.reset. \u003Ctemplate>\n  \u003CForm :form=\"form\">\n    \u003C!-- fields -->\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"form.isSubmitting\"\n    >\n      {{ form.isSubmitting ? 'Saving...' : 'Save' }}\n    \u003C\u002Fbutton>\n\n    \u003Cbutton\n      type=\"reset\"\n      :disabled=\"form.isSubmitting\"\n    >\n      Reset\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",{"id":584,"title":585,"titles":586,"content":587,"level":197},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#what-a-submit-does","What a submit does",[57],"A submit while another is still running is ignored. isSubmitting becomes true, submitCount increases, and every field counts as touched. The whole form is validated. If a value changed while validation was running, validation runs again on the new values. If the schema reports issues, they replace all errors and onSubmit is not called. isSubmitted is still true. If validation passes, errors are cleared and onSubmit is awaited with the validated output. isSubmitting becomes false again, even if onSubmit throws. onSubmit can be sync or async. isSubmitting stays true for the whole duration, so you can use it to disable the button or show a spinner. const form = useForm({\n  initialValues,\n  async onSubmit(data) {\n    await api.save(data) \u002F\u002F data is SchemaOutput\u003Ctypeof schema>\n  },\n  schema,\n})",{"id":589,"title":590,"titles":591,"content":592,"level":197},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#errors-thrown-in-onsubmit","Errors thrown in onSubmit",[57],"If onSubmit throws or rejects, isSubmitting is reset and the error propagates out of form.submit(). When Form triggers the submit, the rejection goes to Vue's error handler (app.config.errorHandler, or vue:error in Nuxt) and your UI gets no error message. When you call form.submit() yourself, the returned promise rejects. Handle expected failures inside onSubmit and turn them into form errors: const form = useForm({\n  async onSubmit(data) {\n    try {\n      await api.save(data)\n    } catch {\n      form.setError({ message: 'We could not save your changes. Try again.' })\n    }\n  }\n}) The Server errors recipe shows how to map field-level responses as well.",{"id":594,"title":595,"titles":596,"content":597,"level":197},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#submit-programmatically","Submit programmatically",[57],"Call form.submit() when a submit does not start from \u003CForm>, for example from a keyboard shortcut or a button outside the form element. It validates before invoking onSubmit. await form.submit() Passed a native submit event, it also calls preventDefault(). If you do not want to use Form, this is the equivalent markup: \u003Ctemplate>\n  \u003Cform\n    @submit=\"form.submit\"\n    @reset.prevent=\"form.reset\"\n  >\n  \u003C!-- fields -->\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>",{"id":599,"title":600,"titles":601,"content":602,"level":197},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#validate-without-submitting","Validate without submitting",[57],"form.validate() validates the whole form, updates form.errors, and does not call onSubmit. const result = await form.validate()\n\nif (result.issues) {\n  \u002F\u002F show a message, scroll to the first error, etc.\n} else {\n  result.value \u002F\u002F parsed output\n}",{"id":604,"title":605,"titles":606,"content":607,"level":197},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#reset-the-form","Reset the form",[57],"form.reset() returns the form to its baseline: values return to the current initialValues; errors return to the current initialErrors; touched state and submitCount are cleared, so isTouched and isSubmitted become false; pending debounced validations and in-flight validations are cancelled, along with any submit in progress; array items receive fresh identities, and resetCount increases. Use type=\"reset\" inside \u003CForm>, or call form.reset() yourself. Reset restores the baseline, not an empty form. If the baseline holds loaded data, reset returns to that data.",{"id":609,"title":610,"titles":611,"content":612,"level":613},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#replace-the-baseline","Replace the baseline",[57,605],"Pass values, errors, or both to start from a new baseline. The values you supply replace the old baseline. They are not merged with it. An omitted argument keeps its current baseline. form.reset(\n  { email: 'jane@example.com' },\n  [{ message: 'Verify this address.', path: ['email'] }],\n) The next plain form.reset() returns to this new baseline. Use this when saved data should become the new starting point for dirty tracking. See Editing existing data.",3,{"id":615,"title":616,"titles":617,"content":618,"level":613},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#remount-inputs-after-reset","Remount inputs after reset",[57,605],"form.resetCount increases on every reset. Use it as a :key on a subtree whose uncontrolled state should be discarded. \u003Ctemplate>\n  \u003CMyThirdPartyEditor\n    :key=\"form.resetCount\"\n    v-model=\"form.values.body\"\n  \u002F>\n\u003C\u002Ftemplate>",{"id":620,"title":621,"titles":622,"content":623,"level":613},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#reset-after-a-successful-submit","Reset after a successful submit",[57,605],"NotForm does not reset on its own after onSubmit. Do it when it suits your flow: const form = useForm({\n  async onSubmit(data) {\n    await api.create(data)\n    form.reset() \u002F\u002F back to a blank form\n  \u002F\u002F or: form.reset(data) to make the saved data the new baseline\n  }\n})",{"id":625,"title":248,"titles":626,"content":627,"level":197},"\u002Fdocs\u002Fguides\u002Fsubmission-and-reset#next-steps",[57],"Form state lists every flag the form exposes. Server errors html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":62,"title":61,"titles":629,"content":630,"level":191},[],"Read reactive form and field state such as dirty, touched, valid, validating, and submitting, and avoid the common pitfalls. State is exposed as plain reactive properties. Read only what your interface needs. Vue tracks those reads in templates and computed values, so there is nothing to subscribe to.",{"id":632,"title":61,"titles":633,"content":634,"level":197},"\u002Fdocs\u002Fguides\u002Fform-state#form-state",[61],"Property Meaning isDirty Current values differ (deep comparison) from the baseline initialValues. Reverting every change makes it false again. isTouched A field has blurred, or a submit has been attempted. isSubmitted At least one submit has been attempted, even an invalid one. isSubmitting Validation or the asynchronous onSubmit is running. isValidating A schema validation is running. isValid The current error list is empty. It does not run validation. submitCount Number of submit attempts. Reset to 0 by reset(). resetCount Number of resets. Useful as a :key. errors The current list of issues. values The live input values. \u003Ctemplate>\n  \u003CForm :form=\"form\">\n    \u003Cp v-if=\"form.isDirty\">\n      You have unsaved changes.\n    \u003C\u002Fp>\n\n    \u003Cp\n      v-if=\"form.isValidating\"\n      role=\"status\"\n    >\n      Checking values...\n    \u003C\u002Fp>\n\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"form.isSubmitting\"\n    >\n      {{ form.isSubmitting ? 'Saving...' : 'Save' }}\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",{"id":636,"title":637,"titles":638,"content":639,"level":197},"\u002Fdocs\u002Fguides\u002Fform-state#isvalid-describes-errors-not-values","isValid describes errors, not values",[61],"isValid is true whenever form.errors is empty. A pristine form with invalid values reports isValid === true until something validates. Two consequences: Do not use :disabled=\"!form.isValid\" to gate submission. A pristine but invalid form reports true, so the button stays enabled and signals nothing. Submitting is already safe, because onSubmit only runs when the schema passes. To get a fresh answer, call await form.validate(), or submit. Prefer disabling the button with isSubmitting, and let validation show errors after the user acts.",{"id":641,"title":642,"titles":643,"content":644,"level":197},"\u002Fdocs\u002Fguides\u002Fform-state#field-state","Field state",[61],"Every field instance exposes its own state. Property Meaning errors Issues reported at exactly this field's path. isValid errors is empty. isDirty The value differs (deep comparison) from the baseline value at this path. isTouched The field blurred, or a submit was attempted. isValidating The whole form, this field, or a field nested inside it is being validated. \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('username')\"\n  >\n    \u003Clabel :for=\"field.path\">Username\u003C\u002Flabel>\n\n    \u003Cinput\n      v-bind=\"field.events\"\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      type=\"text\"\n      autocomplete=\"username\"\n    >\n\n    \u003Cspan v-if=\"field.isValidating\">Checking...\u003C\u002Fspan>\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":646,"title":647,"titles":648,"content":649,"level":613},"\u002Fdocs\u002Fguides\u002Fform-state#array-fields-and-nested-paths","Array fields and nested paths",[61,642],"An array field has the same members, with these specifics: isDirty compares the whole array, so editing a nested item makes the array field dirty. isValidating includes validation of fields nested in its items. errors and isValid refer only to issues at the array's own path, such as a minimum item count. They do not include item errors. isTouched becomes true after a submit, or when the array field's own onBlur fires. It is not derived from item fields. To ask whether any item has an error, check the issue list, as described in Errors.",{"id":651,"title":652,"titles":653,"content":654,"level":197},"\u002Fdocs\u002Fguides\u002Fform-state#common-patterns","Common patterns",[61],"",{"id":656,"title":657,"titles":658,"content":659,"level":613},"\u002Fdocs\u002Fguides\u002Fform-state#unsaved-changes-warning","Unsaved changes warning",[61,652],"import { onBeforeRouteLeave } from 'vue-router'\n\nonBeforeRouteLeave(() => {\n  if (form.isDirty && !confirm('Discard unsaved changes?')) {\n    return false\n  }\n})",{"id":661,"title":662,"titles":663,"content":664,"level":613},"\u002Fdocs\u002Fguides\u002Fform-state#disable-save-until-something-changed","Disable Save until something changed",[61,652],"\u003Ctemplate>\n  \u003Cbutton\n    type=\"submit\"\n    :disabled=\"!form.isDirty || form.isSubmitting\"\n  >\n    Save\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>",{"id":666,"title":667,"titles":668,"content":669,"level":613},"\u002Fdocs\u002Fguides\u002Fform-state#show-errors-only-after-interaction","Show errors only after interaction",[61,652],"Fields do not validate until the user acts, so errors already appear at the right time. If you add your own early validation, for example with validateOn: { input: true }, you can still gate the display on touch: \u003Ctemplate>\n  \u003CFieldError\n    v-if=\"field.isTouched\"\n    :errors=\"field\"\n  \u002F>\n\u003C\u002Ftemplate>",{"id":671,"title":248,"titles":672,"content":673,"level":197},"\u002Fdocs\u002Fguides\u002Fform-state#next-steps",[61],"useForm lists the complete public contract. Field documents the field members. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":66,"title":65,"titles":675,"content":676,"level":191},[],"Build repeatable sections with stable item identity. Add, remove, reorder, validate, and nest arrays without losing state or errors. Use form.arrayField(path) for an array of values that users add, remove, or reorder. ArrayField exposes the array and its items through a slot.",{"id":678,"title":679,"titles":680,"content":681,"level":197},"\u002Fdocs\u002Fguides\u002Farray-fields#render-and-bind-items","Render and bind items",[65],"Each item is a field for its own value, with an id, an index, and field() and arrayField() for nested values. \u003Cscript lang=\"ts\" setup>\nimport { ArrayField, Field, FieldError, useForm } from 'notform'\nimport { z } from 'zod'\n\nconst itemSchema = z.object({\n  email: z.email('Enter a valid email address.')\n})\n\nconst schema = z.object({\n  contacts: z.array(itemSchema)\n    .min(1, 'Add at least one contact.'),\n})\n\nconst form = useForm({\n  initialValues: { contacts: [{ email: '' }] },\n  schema,\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CArrayField\n    v-slot=\"{ arrayField }\"\n    :array-field=\"form.arrayField('contacts')\"\n  >\n    \u003Cdiv\n      v-for=\"item in arrayField.items\"\n      :key=\"item.id\"\n    >\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"item.field('email')\"\n      >\n        \u003Clabel :for=\"field.path\">Contact email\u003C\u002Flabel>\n\n        \u003Cinput\n          :id=\"field.path\"\n          v-model=\"field.model\"\n          v-bind=\"field.events\"\n          type=\"email\"\n        >\n\n        \u003CFieldError :errors=\"field\" \u002F>\n      \u003C\u002FField>\n\n      \u003Cbutton\n        type=\"button\"\n        @click=\"arrayField.remove(item.index)\"\n      >\n        Remove\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\n    \u003CFieldError :errors=\"arrayField\" \u002F>\n\n    \u003Cbutton\n      type=\"button\"\n      @click=\"arrayField.append({ email: '' })\"\n    >\n      Add contact\n    \u003C\u002Fbutton>\n  \u003C\u002FArrayField>\n\u003C\u002Ftemplate> Key the loop with item.id, never with the index. After an insertion or reorder, the same item has a different index, and an index key makes Vue reuse the wrong DOM and component state.",{"id":683,"title":684,"titles":685,"content":686,"level":197},"\u002Fdocs\u002Fguides\u002Farray-fields#what-each-part-shows","What each part shows",[65],"arrayField.errors holds issues at the array's own path, such as the minimum count. \u003CFieldError :errors=\"arrayField\" \u002F> displays the first. item.field('email') exposes the issues at that item's email path. Paths passed to item.field() are relative to the item and typed from the item's shape. For an array of primitives, pass the item itself to Field: \u003CField v-slot=\"{ field }\" :field=\"item\">.",{"id":688,"title":689,"titles":690,"content":691,"level":197},"\u002Fdocs\u002Fguides\u002Farray-fields#mutate-through-the-array-field","Mutate through the array field",[65],"Use these methods for every structural change. They keep item identity, touched state, and errors aligned with the values. Method Effect append(value) Add an item at the end. prepend(value) Add an item at the start. insert(index, value) Insert at an index. Later items shift. The index is clamped to 0..length. remove(index) Remove the item and its state. Later items shift down. move(from, to) Move an item to another index. swap(indexA, indexB) Exchange two items. update(index, value) Replace an item in place. It keeps its identity and touched state, and its old errors are dropped. Details: Values passed to append, prepend, insert, and update are deep-cloned, so two items never share an object. An out-of-range index for remove, move, swap, or update is ignored. If the array does not exist yet, the first insertion creates it. Each mutation counts as a change event on the array field. By default this re-validates the array's own path, so a \"minimum items\" error appears or clears immediately. It does not validate the items' fields. Do not change the array's structure with form.values.contacts.push(), splice(), sort(), or by replacing the array. Direct changes bypass the methods that keep item identity and error paths aligned. Item ids are reconciled by length only, so errors and touched state can end up on the wrong items. Writing to a single item's field through its model is fine.",{"id":693,"title":694,"titles":695,"content":696,"level":197},"\u002Fdocs\u002Fguides\u002Farray-fields#identity-index-and-path","Identity, index, and path",[65],"Member Stable across moves? Use it for item.id Yes The v-for key. item.index No, follows position Passing to remove, move, and swap. item.path No, follows position Input ids and labels, such as contacts.2.email. Fields created from an item, such as item.field('email'), follow the item when it moves.",{"id":698,"title":699,"titles":700,"content":701,"level":197},"\u002Fdocs\u002Fguides\u002Farray-fields#reorder-with-buttons","Reorder with buttons",[65],"\u003Ctemplate>\n  \u003Cdiv\n    v-for=\"item in arrayField.items\"\n    :key=\"item.id\"\n  >\n    \u003C!-- fields -->\n    \u003Cbutton\n      type=\"button\"\n      :disabled=\"item.index === 0\"\n      @click=\"arrayField.move(item.index, item.index - 1)\"\n    >\n      Up\n    \u003C\u002Fbutton>\n\n    \u003Cbutton\n      type=\"button\"\n      :disabled=\"item.index === arrayField.items.length - 1\"\n      @click=\"arrayField.move(item.index, item.index + 1)\"\n    >\n      Down\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>",{"id":703,"title":704,"titles":705,"content":706,"level":197},"\u002Fdocs\u002Fguides\u002Farray-fields#nested-arrays","Nested arrays",[65],"Each item can create nested fields and nested arrays. Paths are relative to the item and follow it as it moves. \u003Cscript lang=\"ts\" setup>\nimport { ArrayField, Field, FieldError, useForm } from 'notform'\nimport { z } from 'zod'\n\nconst membersSchema = z.array(z.object({\n  email: z.email('Enter a valid email address.')\n}))\n  .min(1, 'Add at least one member.')\n\nconst itemSchema = z.object({\n  members: membersSchema,\n  name: z.string().min(1, 'Name the group.'),\n})\n\nconst schema = z.object({\n  groups: z.array(itemSchema).min(1),\n})\n\nconst form = useForm({\n  initialValues: {\n    groups: [{ members: [{ email: '' }], name: '' },],\n  },\n  schema,\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CArrayField\n    v-slot=\"{ arrayField: groups }\"\n    :array-field=\"form.arrayField('groups')\"\n  >\n    \u003Cfieldset\n      v-for=\"group in groups.items\"\n      :key=\"group.id\"\n    >\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"group.field('name')\"\n      >\n        \u003Cinput\n          v-bind=\"field.events\"\n          :id=\"field.path\"\n          v-model=\"field.model\"\n          aria-label=\"Group name\"\n          :name=\"field.path\"\n        >\n\n        \u003CFieldError :errors=\"field\" \u002F>\n      \u003C\u002FField>\n\n      \u003CArrayField\n        v-slot=\"{ arrayField: members }\"\n        :array-field=\"group.arrayField('members')\"\n      >\n        \u003Cdiv\n          v-for=\"member in members.items\"\n          :key=\"member.id\"\n        >\n          \u003CField\n            v-slot=\"{ field }\"\n            :field=\"member.field('email')\"\n          >\n            \u003Cinput\n              v-bind=\"field.events\"\n              :id=\"field.path\"\n              v-model=\"field.model\"\n              type=\"email\"\n              :name=\"field.path\"\n            >\n\n            \u003CFieldError :errors=\"field\" \u002F>\n          \u003C\u002FField>\n\n          \u003Cbutton\n            type=\"button\"\n            @click=\"members.remove(member.index)\"\n          >\n            Remove member\n          \u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n\n        \u003CFieldError :errors=\"members\" \u002F>\n\n        \u003Cbutton\n          type=\"button\"\n          @click=\"members.append({ email: '' })\"\n        >\n          Add member\n        \u003C\u002Fbutton>\n      \u003C\u002FArrayField>\n\n      \u003Cbutton\n        type=\"button\"\n        @click=\"groups.remove(group.index)\"\n      >\n        Remove group\n      \u003C\u002Fbutton>\n    \u003C\u002Ffieldset>\n\n    \u003Cbutton\n      type=\"button\"\n      @click=\"groups.append({ name: '', members: [{ email: '' }] })\"\n    >\n      Add group\n    \u003C\u002Fbutton>\n  \u003C\u002FArrayField>\n\u003C\u002Ftemplate> When an inner array is removed along with its group, its fields are disposed. A stale reference to a removed item is inert: mutations do nothing and its items are empty.",{"id":708,"title":709,"titles":710,"content":711,"level":197},"\u002Fdocs\u002Fguides\u002Farray-fields#errors-follow-items","Errors follow items",[65],"When items are inserted, removed, moved, or swapped, existing errors are re-pointed to the item's new index. Removing an item drops its errors. update drops the replaced item's old errors. The schema's own array-level rules, such as .min(1), stay on the array path.",{"id":713,"title":714,"titles":715,"content":716,"level":197},"\u002Fdocs\u002Fguides\u002Farray-fields#reset","Reset",[65],"form.reset() restores the baseline array and gives every item a fresh id. The keyed list re-renders, so item components are rebuilt. This is expected: reset begins a new editing state from the baseline.",{"id":718,"title":248,"titles":719,"content":720,"level":197},"\u002Fdocs\u002Fguides\u002Farray-fields#next-steps",[65],"ArrayField is the component contract. Validation covers configuring an array field's events. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":70,"title":69,"titles":722,"content":723,"level":191},[],"Use inferred paths and values, name schema types, type wrapper components, and pass forms between components. NotForm is typed from your schema. In most forms you never write a NotForm type by hand.",{"id":725,"title":726,"titles":727,"content":728,"level":197},"\u002Fdocs\u002Fguides\u002Ftypescript#what-is-inferred","What is inferred",[69],"const schema = z.object({\n  address: z.object({\n    city: z.string()\n  }),\n  email: z.string(),\n  tags: z.array(z.string()),\n})\n\nconst form = useForm({\n  initialValues: {\n    address: { city: '' },\n    email: '',\n    tags: []\n  },\n  schema\n})\n\nform.field('email') \u002F\u002F FieldInstance\u003Cstring>\nform.field('address.city') \u002F\u002F FieldInstance\u003Cstring>\nform.field('tags.0') \u002F\u002F FieldInstance\u003Cstring>\nform.arrayField('tags') \u002F\u002F ArrayFieldInstance\u003Cstring>\nform.field('adress') \u002F\u002F type error: path does not exist\nform.arrayField('email') \u002F\u002F type error: 'email' is not an array Inside a slot, field is typed from the path, so field.model has the right type.",{"id":730,"title":731,"titles":732,"content":733,"level":197},"\u002Fdocs\u002Fguides\u002Ftypescript#name-the-schema-types","Name the schema types",[69],"import type { SchemaInput, SchemaOutput } from 'notform'\n\ntype FormValues = SchemaInput\u003Ctypeof schema>\ntype SubmitData = SchemaOutput\u003Ctypeof schema>\n\nasync function save(data: SubmitData) {\n  \u002F\u002F ...\n} Use these when a function takes onSubmit data, or when you build initial values elsewhere: const initialValues: FormValues = {\n  address: { city: '' },\n  email: '',\n  tags: []\n} initialValues accepts a deep-partial of the input, so you can start with only some keys. Field paths still come from the full input shape.",{"id":735,"title":736,"titles":737,"content":738,"level":197},"\u002Fdocs\u002Fguides\u002Ftypescript#path-helpers","Path helpers",[69],"Type Meaning InputPath\u003CTSchema> Every valid path into the input. Accepted by form.field() and form.setValue(). InputArrayPath\u003CTSchema> Paths that point at an array. Accepted by form.arrayField(). InputPathValue\u003CTSchema, TPath> The input type at a path. OutputPath\u003CTSchema> \u002F OutputPathValue\u003CTSchema, TPath> The same, for the output shape. ArrayItem\u003CTArray> The item type of an array type. import type { InputPath } from 'notform'\n\ntype FieldName = InputPath\u003Ctypeof schema> Path types recurse to a depth of 10. Forms nested deeper than that lose path autocomplete beyond it.",{"id":740,"title":741,"titles":742,"content":743,"level":197},"\u002Fdocs\u002Fguides\u002Ftypescript#type-a-reusable-input-component","Type a reusable input component",[69],"A wrapper takes a field instance as a prop. \u003Cscript setup lang=\"ts\">\nimport { FieldError, type FieldInstance } from 'notform'\n\ndefineProps\u003C{\n  field: FieldInstance\u003Cstring>\n  label: string\n}>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Clabel :for=\"field.path\">{{ label }}\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n    >\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate> \u003Ctemplate>\n  \u003CTextField\n    :field=\"form.field('email')\"\n    label=\"Email\"\n  \u002F>\n\u003C\u002Ftemplate> Array items are fields too, so ArrayItemInstance\u003CT> is also accepted where a FieldInstance\u003CT> is expected. If your linter reports vue\u002Fno-mutating-props for v-model=\"field.model\", it is a false positive. A field instance is a reactive object designed to be written through, and you are not replacing the prop.",{"id":745,"title":746,"titles":747,"content":748,"level":197},"\u002Fdocs\u002Fguides\u002Ftypescript#pass-a-form-to-a-child-component","Pass a form to a child component",[69],"Type the prop with UseFormInstance, using the schema's type. \u003Cscript setup lang=\"ts\">\nimport type { UseFormInstance } from 'notform'\nimport type { profileSchema } from '.\u002Fprofile-schema'\n\ndefineProps\u003C{ form: UseFormInstance\u003Ctypeof profileSchema> }>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton\n    type=\"submit\"\n    :disabled=\"form.isSubmitting || !form.isDirty\"\n  >\n    Save\n  \u003C\u002Fbutton>\n\n  \u003Cbutton\n    type=\"button\"\n    @click=\"form.reset()\"\n  >\n    Discard\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate> \u003Ctemplate>\n  \u003CForm :form=\"form\">\n    \u003CFormActions :form=\"form\" \u002F>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate> Put the schema in its own file so both components can import its type. Prefer UseFormInstance over FormInstance. It is the public contract and excludes methods meant to stay internal.",{"id":750,"title":751,"titles":752,"content":753,"level":197},"\u002Fdocs\u002Fguides\u002Ftypescript#schema-requirements","Schema requirements",[69],"useForm accepts a Standard Schema whose input and output are objects. This is what lets paths and form.values be keyed. A schema such as z.string() fails type checking.",{"id":755,"title":248,"titles":756,"content":757,"level":197},"\u002Fdocs\u002Fguides\u002Ftypescript#next-steps",[69],"Types reference lists every exported type. html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":79,"title":78,"titles":759,"content":760,"level":191},[],"Render a native form element wired to a useForm instance for submit and reset. Form renders a native \u003Cform> and connects it to the instance returned by useForm. It is a thin wrapper: it adds no validation of its own.",{"id":762,"title":763,"titles":764,"content":765,"level":197},"\u002Fdocs\u002Fcomponents\u002Fform#usage","Usage",[78],"\u003Cscript setup lang=\"ts\">\nimport { Form, useForm } from 'notform'\nimport { z } from 'zod'\n\nconst form = useForm({\n  initialValues: { email: '' },\n  onSubmit: (data) => {\n    return console.log(data)\n  },\n  schema: z.object({ email: z.email() }),\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm\n    :form=\"form\"\n    novalidate\n    class=\"stack\"\n  >\n    \u003C!-- fields -->\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"form.isSubmitting\"\n    >\n      Submit\n    \u003C\u002Fbutton>\n\n    \u003Cbutton type=\"reset\">\n      Reset\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",{"id":767,"title":768,"titles":769,"content":770,"level":197},"\u002Fdocs\u002Fcomponents\u002Fform#props","Props",[78],"The form instance returned by useForm.",{"id":772,"title":773,"titles":774,"content":775,"level":197},"\u002Fdocs\u002Fcomponents\u002Fform#slots","Slots",[78],"Slot Props Description default none The form's contents. Read state from the form instance you created.",{"id":777,"title":778,"titles":779,"content":780,"level":197},"\u002Fdocs\u002Fcomponents\u002Fform#behavior","Behavior",[78],"Native event What Form does submit Calls form.submit(event), which prevents the browser's page navigation, validates the whole form, and calls onSubmit only when valid. reset Prevents the browser's native value reset and calls form.reset(), restoring the baseline values and errors.",{"id":782,"title":783,"titles":784,"content":785,"level":197},"\u002Fdocs\u002Fcomponents\u002Fform#attributes","Attributes",[78],"Attributes such as id, class, novalidate, and aria-* are forwarded to the native \u003Cform> element. Add novalidate when you want your schema, not the browser, to produce validation messages.",{"id":787,"title":788,"titles":789,"content":790,"level":197},"\u002Fdocs\u002Fcomponents\u002Fform#form-does-not-provide-context","Form does not provide context",[78],"Form does not share the form instance with descendants, and fields do not look it up. Create each field from the instance and pass it explicitly: \u003Ctemplate>\n  \u003CForm :form>\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      ...\n    \u003C\u002FField>\n\n    \u003CArrayField\n      v-slot=\"{ arrayField }\"\n      :array-field=\"form.arrayField('tags')\"\n    >\n      ...\n    \u003C\u002FArrayField>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate> This is what keeps every field fully typed. It also means a field can be created and used anywhere, including outside Form.",{"id":792,"title":793,"titles":794,"content":795,"level":197},"\u002Fdocs\u002Fcomponents\u002Fform#using-a-plain-form-element","Using a plain form element",[78],"Form is optional. A native \u003Cform> with two handlers is equivalent: \u003Cform @submit=\"form.submit\" @reset.prevent=\"form.reset\">\n  \u003C!-- fields -->\n\u003C\u002Fform>",{"id":797,"title":798,"titles":799,"content":800,"level":197},"\u002Fdocs\u002Fcomponents\u002Fform#related","Related",[78],"Submission and reset useForm html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":83,"title":82,"titles":802,"content":803,"level":191},[],"Expose a field instance to a renderless slot to bind its model and events to any control. Field renders no element. It passes a field instance to its default slot so you can connect the value and validation events to native inputs or custom components.",{"id":805,"title":763,"titles":806,"content":807,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield#usage",[82],"\u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('profile.email')\"\n  >\n    \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n      autocomplete=\"email\"\n      type=\"email\"\n    >\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> form.field(path) returns the same cached field for the same path. Its model reads and writes form.values. Its events mark interaction and trigger validation according to the field's configuration.",{"id":809,"title":768,"titles":810,"content":811,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield#props",[82],"The field created by form.field(path), or an item from arrayField.items.",{"id":813,"title":814,"titles":815,"content":816,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield#slot-props","Slot props",[82],"The default slot receives field. Member Type Description model TValue Two-way value. Assigning writes into form.values. Does not validate. events { onBlur, onChange, onInput } Handlers that mark interaction and trigger validation. Spread with v-bind. path string Current dot-notation path. Follows an array item when its position changes. Use it for id and for. id symbol Stable identity. Safe as a Vue key. Not valid as an HTML id. errors ValidationIssue[] Issues reported at exactly this path. isDirty boolean The value differs from the baseline value at this path. isTouched boolean The field blurred, or a submit was attempted. isValid boolean errors is empty. isValidating boolean The whole form, this field, or a field nested inside it is being validated. validate() () => Promise\u003CValidationResult\u003CTValue>> Validate this field now, skipping any pending debounce. Resolves with { value } or { issues }. Extra attributes passed to Field arrive as additional slot props. Because Field renders no element, attach HTML attributes to the control inside the slot.",{"id":818,"title":819,"titles":820,"content":821,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield#configure-validation","Configure validation",[82],"Pass options as the second argument to form.field(path, config). They are read when the field is first created. See Validation. const username = form.field('username', { debounce: 300, validateOn: { input: true } })",{"id":823,"title":824,"titles":825,"content":826,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield#using-a-field-without-field","Using a field without Field",[82],"The component is a convenience. A field instance works directly: \u003Cscript setup lang=\"ts\">\nconst email = form.field('email')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Clabel :for=\"email.path\">Email\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"email.path\"\n      v-model=\"email.model\"\n      v-bind=\"email.events\"\n      autocomplete=\"email\"\n      type=\"email\"\n    >\n\n    \u003CFieldError :errors=\"email\" \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>",{"id":828,"title":798,"titles":829,"content":830,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield#related",[82],"Binding inputs Custom components Form state html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":87,"title":86,"titles":832,"content":833,"level":191},[],"Render array items with stable identities and mutation methods that keep values, state, and errors aligned. ArrayField renders no element. It exposes an array field instance through its default slot. Create the instance with form.arrayField(path).",{"id":835,"title":763,"titles":836,"content":837,"level":197},"\u002Fdocs\u002Fcomponents\u002Farray-field#usage",[86],"\u003Ctemplate>\n  \u003CArrayField\n    v-slot=\"{ arrayField }\"\n    :array-field=\"form.arrayField('tags')\"\n  >\n    \u003Cdiv\n      v-for=\"item in arrayField.items\"\n      :key=\"item.id\"\n    >\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"item\"\n      >\n        \u003Cinput\n          v-model=\"field.model\"\n          v-bind=\"field.events\"\n          aria-label=\"Tag\"\n        >\n\n        \u003CFieldError :errors=\"field\" \u002F>\n      \u003C\u002FField>\n\n      \u003Cbutton\n        type=\"button\"\n        @click=\"arrayField.remove(item.index)\"\n      >\n        Remove\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\n    \u003CFieldError :errors=\"arrayField\" \u002F>\n\n    \u003Cbutton\n      type=\"button\"\n      @click=\"arrayField.append('')\"\n    >\n      Add tag\n    \u003C\u002Fbutton>\n  \u003C\u002FArrayField>\n\u003C\u002Ftemplate> Use item.id as the Vue key. index and path change with position. id does not.",{"id":839,"title":768,"titles":840,"content":841,"level":197},"\u002Fdocs\u002Fcomponents\u002Farray-field#props",[86],"The array field created by form.arrayField(path).",{"id":843,"title":814,"titles":844,"content":845,"level":197},"\u002Fdocs\u002Fcomponents\u002Farray-field#slot-props",[86],"The default slot receives arrayField. Extra attributes passed to ArrayField arrive as additional slot props.",{"id":847,"title":848,"titles":849,"content":850,"level":613},"\u002Fdocs\u002Fcomponents\u002Farray-field#field-members","Field members",[86,814],"An array field has the regular field members model, events, path, id, errors, isDirty, isTouched, isValid, isValidating, and validate(), with these array-specific semantics: errors and isValid refer to issues at the array's own path, such as a minimum item count. isDirty compares the whole array, so nested edits count. isValidating includes validation of fields nested in its items.",{"id":852,"title":853,"titles":854,"content":855,"level":613},"\u002Fdocs\u002Fcomponents\u002Farray-field#array-members","Array members",[86,814],"Member Description items Item instances, in current order. append(value) Add an item at the end. prepend(value) Add an item at the start. insert(index, value) Insert at an index. Clamped to 0..length. remove(index) Remove an item and its state. Ignored if out of range. move(from, to) Move an item. Ignored if either index is out of range. swap(indexA, indexB) Swap two items. Ignored if either index is out of range. update(index, value) Replace an item in place. Keeps its id and touched state, drops its old errors. Ignored if out of range.",{"id":857,"title":858,"titles":859,"content":860,"level":613},"\u002Fdocs\u002Fcomponents\u002Farray-field#item-members","Item members",[86,814],"Each entry in items is a field for the item's value. It has every field member listed above, plus: Member Description index The item's current position. field(path, config?) A field nested in the item, with a path relative to the item. Cached, and follows the item when it moves. arrayField(path, config?) An array field nested in the item.",{"id":862,"title":778,"titles":863,"content":864,"level":197},"\u002Fdocs\u002Fcomponents\u002Farray-field#behavior",[86],"Values passed to append, prepend, insert, and update are deep-cloned. Every mutation counts as a change event on the array field. By default it re-validates the array's own path. Errors inside the array are re-pointed to follow their items on every mutation. Reset creates fresh item ids. A removed item, and anything created from it, becomes inert: mutations do nothing. Change the array only through these methods. Direct mutation such as form.values.tags.push('x') bypasses identity and error tracking. See Array fields.",{"id":866,"title":798,"titles":867,"content":868,"level":197},"\u002Fdocs\u002Fcomponents\u002Farray-field#related",[86],"Array fields guide Field html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":91,"title":90,"titles":870,"content":871,"level":191},[],"Display the first validation message from a field, an array field, or an issue list. FieldError displays the first message from an issue array, or from any object that has an errors array, such as a field, an array field, or an array item. It renders no element when there is no message.",{"id":873,"title":763,"titles":874,"content":875,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield-error#usage",[90],"Pass a field directly: \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('email')\"\n  >\n    \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n    \u003Cinput\n      v-bind=\"field.events\"\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      type=\"email\"\n    >\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> Or pass an issue array: \u003Ctemplate>\n  \u003CFieldError\n    :errors=\"form.errors\"\n    as=\"p\"\n    role=\"alert\"\n  \u002F>\n\u003C\u002Ftemplate>",{"id":877,"title":768,"titles":878,"content":879,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield-error#props",[90],"An issue array, or an object with an errors array. Only the first message is displayed. Element or component that renders the message. Defaults to span. Attributes other than errors and as are forwarded to the rendered element or component. Use class, id, role, and aria-* to match your styling and accessibility needs.",{"id":881,"title":882,"titles":883,"content":884,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield-error#slot","Slot",[90],"The default slot receives message. Use it to wrap or decorate the text. \u003Ctemplate>\n  \u003CFieldError\n    :errors=\"field\"\n    as=\"p\"\n    role=\"alert\"\n  >\n    \u003Ctemplate #default=\"{ message }\">\n      \u003Cstrong>Check this value:\u003C\u002Fstrong> {{ message }}\n    \u003C\u002Ftemplate>\n  \u003C\u002FFieldError>\n\u003C\u002Ftemplate> When as is a component, the message is passed as its default slot content. That component must render its default slot.",{"id":886,"title":887,"titles":888,"content":889,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield-error#notes","Notes",[90],"Which issues a field shows: exactly those at its own path. An issue on a parent path or without a path is not shown. See Errors. To display every message, render field.errors yourself. Pass :errors=\"arrayField\" to show array-level issues such as a minimum item count.",{"id":891,"title":798,"titles":892,"content":893,"level":197},"\u002Fdocs\u002Fcomponents\u002Ffield-error#related",[90],"Errors guide html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":100,"title":99,"titles":895,"content":896,"level":191},[],"Create a typed reactive form from a Standard Schema-compatible object schema. Full configuration and instance reference. useForm creates the form instance that owns values, errors, field instances, and submit state. Call it in a Vue setup context (\u003Cscript setup> or setup()), so its timers and watchers are cleaned up when the component unmounts. import { useForm } from 'notform'\nimport { z } from 'zod'\n\nconst form = useForm({\n  initialValues: { displayName: '', email: '' },\n  onSubmit(data) {\n    console.log(data)\n  },\n  schema: z.object({\n    displayName: z.string().min(1),\n    email: z.email(),\n  }),\n}) In Nuxt, useForm is auto-imported. See the Nuxt module guide.",{"id":898,"title":899,"titles":900,"content":901,"level":197},"\u002Fdocs\u002Fcomposables\u002Fuse-form#configuration","Configuration",[99],"A Standard Schema-compatible object schema. May be a value, a ref, or a getter. It is read each time validation runs. Starting values and the baseline for isDirty and reset(). May be partial, but provide a value for every bound field. Issues shown before the first validation. An issue without a path is form-level. Called after the whole form validates. The argument is the parsed schema output.",{"id":903,"title":904,"titles":905,"content":906,"level":197},"\u002Fdocs\u002Fcomposables\u002Fuse-form#returned-instance","Returned instance",[99],"The returned object is reactive. Read members directly, for example form.isDirty.",{"id":908,"title":909,"titles":910,"content":911,"level":613},"\u002Fdocs\u002Fcomposables\u002Fuse-form#state","State",[99,904],"Member Type Description values SchemaInput\u003CTSchema> Live input values. Editable directly, through setValue, or through field models. errors ValidationIssue[] Current issues. Empty when nothing is wrong. isDirty boolean Values differ from the baseline. isTouched boolean A field blurred or a submit was attempted. isSubmitted boolean At least one submit was attempted. isSubmitting boolean Validation or onSubmit is running. isValidating boolean A validation is running. isValid boolean The error list is empty. Does not validate. submitCount number Submit attempts since the last reset. resetCount number Number of resets.",{"id":913,"title":914,"titles":915,"content":916,"level":613},"\u002Fdocs\u002Fcomposables\u002Fuse-form#fields","Fields",[99,904],"Member Description field(path, config?) Get or create the cached field for a typed input path. arrayField(path, config?) Get or create the cached array field for a typed array path. The first call for a path creates the field and reads config. Later calls return the same instance and ignore any config. See Validation.",{"id":918,"title":919,"titles":920,"content":921,"level":613},"\u002Fdocs\u002Fcomposables\u002Fuse-form#actions","Actions",[99,904],"Member Description submit(event?) Validate the form and call onSubmit if it passes. Prevents the event's default action. Ignored while a submit is running. Rejects if onSubmit throws. validate() Validate the whole form without calling onSubmit. Replaces all errors. Resolves with { value } or { issues }. reset(values?, errors?) Restore the baseline, or replace it when arguments are given. Clears touched and submit state. setError(issue) With a path, replaces errors at that path. Without, appends a form-level error. setValue(path, value) Write a typed input value. Does not validate.",{"id":923,"title":454,"titles":924,"content":925,"level":197},"\u002Fdocs\u002Fcomposables\u002Fuse-form#typed-paths-and-values",[99],"Paths use dot notation and are checked against the schema input. form.field('address.city') targets a nested value, and form.arrayField('contacts') targets an array. Values in form.values use the input type, and onSubmit receives the output type. See Schema input and output and TypeScript.",{"id":927,"title":887,"titles":928,"content":929,"level":197},"\u002Fdocs\u002Fcomposables\u002Fuse-form#notes",[99],"Call useForm once per form. Each call creates an independent form. Outside a Vue setup context, cleanup on unmount does not run. Changing a reactive schema discards in-flight validations but does not re-validate. The returned type is UseFormInstance\u003CTSchema>, the public contract documented here.",{"id":931,"title":798,"titles":932,"content":933,"level":197},"\u002Fdocs\u002Fcomposables\u002Fuse-form#related",[99],"Core concepts Form state Submission and reset html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":109,"title":108,"titles":935,"content":936,"level":191},[],"Every public type exported by notform, with its shape and where it is used. All types are exported from notform and are available through TypeScript in both Vue and Nuxt projects. In Nuxt, add notform as a direct dependency to import them. See Nuxt module.",{"id":938,"title":939,"titles":940,"content":941,"level":197},"\u002Fdocs\u002Freference\u002Ftypes#schema-types","Schema types",[108],"Type Description FormSchema A Standard Schema whose input and output are both objects. The constraint useForm places on schema. SchemaInput\u003CTSchema> The data the schema accepts: the shape of form.values. SchemaOutput\u003CTSchema> The data the schema produces: the shape onSubmit receives.",{"id":943,"title":944,"titles":945,"content":946,"level":197},"\u002Fdocs\u002Freference\u002Ftypes#form-types","Form types",[108],"Type Description FormConfig\u003CTSchema> The options object accepted by useForm: schema, initialValues, initialErrors, onSubmit. UseFormInstance\u003CTSchema> The public form instance returned by useForm. Use this to type props and variables that hold a form. FormInstance\u003CTSchema> The wider form type, including members that are not part of the public contract. Prefer UseFormInstance. FormProps\u003CTSchema>\u002F FormSlots Props and slots of Form.",{"id":948,"title":949,"titles":950,"content":951,"level":197},"\u002Fdocs\u002Freference\u002Ftypes#field-types","Field types",[108],"Type Description FieldConfig Per-field behavior, passed to form.field() and form.arrayField(). FieldEvents The handlers in field.events. FieldValidationEvent 'blur' | 'change' | 'input'. FieldInstance\u003CTValue> A field bound to one value. FieldProps\u003CTValue> \u002F FieldSlots\u003CTValue> Props and slots of Field. FieldErrorProps \u002F FieldErrorSlots Props and slots of FieldError.",{"id":953,"title":954,"titles":955,"content":956,"level":197},"\u002Fdocs\u002Freference\u002Ftypes#array-field-types","Array field types",[108],"Type Description ArrayFieldInstance\u003CTItem> A FieldInstance\u003CTItem[]> plus items, append, prepend, insert, remove, move, swap, update. ArrayItemInstance\u003CTItem> One item. A FieldInstance\u003CTItem> plus index, field(path, config?), and arrayField(path, config?). ArrayFieldProps\u003CTItem> \u002F ArrayFieldSlots\u003CTItem> Props and slots of ArrayField. ArrayItem\u003CTArray> The item type of an array type.",{"id":958,"title":959,"titles":960,"content":961,"level":197},"\u002Fdocs\u002Freference\u002Ftypes#path-types","Path types",[108],"Type Description InputPath\u003CTSchema> Every valid dot-notation path into the input. InputArrayPath\u003CTSchema> Input paths that point at an array. InputPathValue\u003CTSchema, TPath> The input type at a path. OutputPath\u003CTSchema> Every valid path into the output. OutputPathValue\u003CTSchema, TPath> The output type at a path. Path types recurse to a depth of 10.",{"id":963,"title":964,"titles":965,"content":966,"level":197},"\u002Fdocs\u002Freference\u002Ftypes#validation-types","Validation types",[108],"Type Description ValidationIssue A Standard Schema issue: { message: string, path?: ReadonlyArray\u003CPropertyKey | { key: PropertyKey }> }. ValidationResult\u003CTValue> { value: TValue } on success, { issues } on failure. Returned by form.validate() and field.validate().",{"id":113,"title":112,"titles":968,"content":969,"level":191},[],"Fix the most common NotForm problems; errors that do not show or clear, ignored config, submit that does nothing, array glitches, and Nuxt auto-import issues. Find the symptom, read the cause, apply the fix.",{"id":971,"title":972,"titles":973,"content":654,"level":197},"\u002Fdocs\u002Freference\u002Ftroubleshooting#validation-and-errors","Validation and errors",[112],{"id":975,"title":976,"titles":977,"content":978,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#no-error-appears-while-i-type","No error appears while I type",[112,972],"Cause. By design. input events do not validate until the field already has an error. A field validates when the user leaves it or commits a value. Fix. To validate while typing, set validateOn: { input: true } on the field. See Validation.",{"id":980,"title":981,"titles":982,"content":983,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#an-error-does-not-clear-after-i-fix-the-value","An error does not clear after I fix the value",[112,972],"Cause. The field is in validationMode: 'lazy' and the event that would clear it is not enabled, or the error is not at this field's exact path. Fix. Use the default eager mode, or enable the event. Confirm the issue's path matches the field: a field shows only issues at its exact path.",{"id":985,"title":986,"titles":987,"content":988,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#required-fields-say-expected-string-received-undefined","Required fields say \"expected string, received undefined\"",[112,972],"Cause. The field has no entry in initialValues, so the schema receives undefined and reports its generic type message instead of your custom one. Fix. Provide '', false, [], or another starting value for every bound field. See Binding inputs.",{"id":990,"title":991,"titles":992,"content":993,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#a-cross-field-error-never-shows","A cross-field error never shows",[112,972],"Cause. The issue has no path, or the path names a different field. The error is then form-level, or shown by another field. Fix. Report the issue on the field that should display it. See Cross-field validation.",{"id":995,"title":996,"titles":997,"content":998,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#a-form-level-error-is-not-displayed-anywhere","A form-level error is not displayed anywhere",[112,972],"Cause. FieldError :errors=\"field\" shows only issues at that field's own path. Issues without a path, or on a parent path, are in form.errors. Fix. Render them from form.errors. See Errors.",{"id":1000,"title":1001,"titles":1002,"content":1003,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#a-server-error-disappears-when-i-tab-away","A server error disappears when I tab away",[112,972],"Cause. Validating a field replaces the errors at its path. If the schema finds no problem there, the server error is dropped. Fix. This is intended behavior. See Server errors if you need an error that outlives edits.",{"id":1005,"title":1006,"titles":1007,"content":1008,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#isvalid-is-true-on-a-form-that-is-clearly-invalid","isValid is true on a form that is clearly invalid",[112,972],"Cause. isValid means \"the error list is empty\". Nothing has validated yet. Fix. Call await form.validate() or submit. See Form state.",{"id":1010,"title":1011,"titles":1012,"content":654,"level":197},"\u002Fdocs\u002Freference\u002Ftroubleshooting#submission","Submission",[112],{"id":1014,"title":1015,"titles":1016,"content":1017,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#submitting-does-nothing","Submitting does nothing",[112,1011],"Cause. Validation failed, and the failing field has no visible error. Typical reasons: the field is on another step or is hidden, the issue is form-level, or a required key was never given a value. Fix. Inspect form.errors: \u003Ctemplate>\n  \u003Cpre>{{ form.errors }}\u003C\u002Fpre>\n\u003C\u002Ftemplate>",{"id":1019,"title":1020,"titles":1021,"content":1022,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#the-second-click-on-submit-is-ignored","The second click on submit is ignored",[112,1011],"Cause. A submit is already running. Extra submits are ignored until it finishes. Fix. Disable the button with form.isSubmitting.",{"id":1024,"title":1025,"titles":1026,"content":1027,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#my-onsubmit-data-does-not-match-formvalues","My onSubmit data does not match form.values",[112,1011],"Cause. onSubmit receives the schema output, and form.values holds the input. Transforms, defaults, and coercions make them differ. Fix. See Schema input and output.",{"id":1029,"title":1030,"titles":1031,"content":1032,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#an-error-in-onsubmit-shows-nothing-in-the-ui","An error in onSubmit shows nothing in the UI",[112,1011],"Cause. Errors thrown by onSubmit are not turned into form errors. They go to Vue's error handler. Fix. Catch expected failures and call form.setError. See Submission and reset.",{"id":1034,"title":1035,"titles":1036,"content":1037,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#the-reset-button-does-not-clear-the-inputs","The Reset button does not clear the inputs",[112,1011],"Cause. Reset restores the baseline, not blank values. Or the button is outside Form, so no handler is attached. Fix. Keep type=\"reset\" inside \u003CForm>, or call form.reset(). To clear a form that holds loaded data, call form.reset(blankValues) with explicit blank values, such as { name: '', email: '' }, which also makes them the new baseline.",{"id":1039,"title":914,"titles":1040,"content":654,"level":197},"\u002Fdocs\u002Freference\u002Ftroubleshooting#fields",[112],{"id":1042,"title":1043,"titles":1044,"content":1045,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#my-field-config-is-ignored","My field config is ignored",[112,914],"Cause. Fields are cached by path and the config is read on the first call. A call without config, such as one in the template, may have created the field first. Fix. Create each configured field once, in \u003Cscript setup>, and reuse that instance. See Validation.",{"id":1047,"title":1048,"titles":1049,"content":1050,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#a-vue-error-says-field-is-a-required-prop-or-path-is-unknown","A Vue error says field is a required prop or path is unknown",[112,914],"Cause. You are using v3 syntax, such as \u003CField path=\"email\">, with v4. Fix. Pass an instance: \u003CField :field=\"form.field('email')\">. See the v3 migration guide.",{"id":1052,"title":1053,"titles":1054,"content":1055,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#typing-a-number-gives-me-a-string","Typing a number gives me a string",[112,914],"Cause. Native inputs produce strings. v-model.number converts, but an empty input produces ''. Fix. Use v-model.number with a number schema, or keep strings in the form and convert in the schema. See Binding inputs.",{"id":1057,"title":1058,"titles":1059,"content":1060,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#fieldid-breaks-my-html-id","field.id breaks my HTML id",[112,914],"Cause. field.id is a symbol for Vue keys, not a string. Fix. Use field.path for HTML ids and labels.",{"id":1062,"title":1063,"titles":1064,"content":1065,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#i-get-a-typescript-error-on-formfield","I get a TypeScript error on form.field('...')",[112,914],"Cause. The path is not in the schema's input shape. Check spelling, nesting, and whether the key is optional or renamed by a transform. Fix. See TypeScript. The error is NotForm telling you the path does not exist.",{"id":1067,"title":65,"titles":1068,"content":654,"level":197},"\u002Fdocs\u002Freference\u002Ftroubleshooting#array-fields",[112],{"id":1070,"title":1071,"titles":1072,"content":1073,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#items-lose-focus-or-show-the-wrong-state-after-reordering","Items lose focus or show the wrong state after reordering",[112,65],"Cause. The list is keyed by index. Fix. Key by item.id. See Array fields.",{"id":1075,"title":1076,"titles":1077,"content":1078,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#errors-or-touched-state-land-on-the-wrong-item","Errors or touched state land on the wrong item",[112,65],"Cause. The array was changed with push, splice, or sort on form.values instead of the array methods. Fix. Use append, insert, remove, move, swap, and update.",{"id":1080,"title":1081,"titles":1082,"content":1083,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#removing-the-last-item-leaves-an-error","Removing the last item leaves an error",[112,65],"Cause. The array's schema rule, such as .min(1), reports an issue on the array's own path. Fix. That is intended. Show it with \u003CFieldError :errors=\"arrayField\" \u002F>.",{"id":1085,"title":233,"titles":1086,"content":654,"level":197},"\u002Fdocs\u002Freference\u002Ftroubleshooting#nuxt",[112],{"id":1088,"title":1089,"titles":1090,"content":1091,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#components-or-useform-are-not-found","Components or useForm are not found",[112,233],"Cause. The module is not registered, or the dev server was not restarted, or another component or composable has the same name. Fix. Check modules: ['@notform\u002Fnuxt'], restart, and add an alias if there is a conflict. See Nuxt module.",{"id":1093,"title":1094,"titles":1095,"content":1096,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#i-cannot-import-types-from-notform","I cannot import types from notform",[112,233],"Cause. Strict package managers do not expose a module's own dependencies. Fix. Add notform as a direct dependency.",{"id":1098,"title":1099,"titles":1100,"content":654,"level":197},"\u002Fdocs\u002Freference\u002Ftroubleshooting#performance","Performance",[112],{"id":1102,"title":1103,"titles":1104,"content":1105,"level":613},"\u002Fdocs\u002Freference\u002Ftroubleshooting#validation-feels-slow-or-my-api-is-called-too-often","Validation feels slow, or my API is called too often",[112,1099],"Cause. Each field validation runs the whole schema, including any async rules. Fix. Add a debounce to the field that owns the slow rule, and cache lookups. See Async validation.",{"id":1107,"title":1108,"titles":1109,"content":1110,"level":197},"\u002Fdocs\u002Freference\u002Ftroubleshooting#still-stuck","Still stuck?",[112],"Open an issue on the NotForm repository. Include your schema, the form setup, and the behavior you see. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":117,"title":116,"titles":1112,"content":1113,"level":191},[],"Short answers to common questions about NotForm's scope, validators, SSR, multiple forms, and styling.",{"id":1115,"title":1116,"titles":1117,"content":1118,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#does-notform-include-ui-components","Does NotForm include UI components?",[116],"No. NotForm is headless. It renders a native \u003Cform> and passes state to your own markup or your component library. See Integrations.",{"id":1120,"title":1121,"titles":1122,"content":1123,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#which-validators-work","Which validators work?",[116],"Any library that implements Standard Schema and produces an object schema. Zod, Valibot, and ArkType are common choices. NotForm has no validation rules of its own.",{"id":1125,"title":1126,"titles":1127,"content":1128,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#do-i-need-a-schema","Do I need a schema?",[116],"Yes. schema is required. It is how NotForm types your values and how validation runs.",{"id":1130,"title":1131,"titles":1132,"content":1133,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#can-i-use-more-than-one-form-on-a-page","Can I use more than one form on a page?",[116],"Yes. Each useForm call creates an independent form with its own values, errors, and state.",{"id":1135,"title":1136,"titles":1137,"content":1138,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#does-it-work-with-ssr-and-nuxt","Does it work with SSR and Nuxt?",[116],"Yes. NotForm creates its state during setup without needing browser APIs. Use @notform\u002Fnuxt for auto-imports.",{"id":1140,"title":1141,"titles":1142,"content":1143,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#which-vue-version-is-required","Which Vue version is required?",[116],"Vue 3.5 or later.",{"id":1145,"title":1146,"titles":1147,"content":1148,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#can-the-schema-change-at-runtime","Can the schema change at runtime?",[116],"Yes. Pass a ref or a getter as schema. See Validation.",{"id":1150,"title":1151,"titles":1152,"content":1153,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#why-is-the-form-value-a-string-when-my-schema-outputs-a-number","Why is the form value a string when my schema outputs a number?",[116],"form.values holds the schema input, which is what the control edits. Your submit handler receives the output. See Schema input and output.",{"id":1155,"title":1156,"titles":1157,"content":1158,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#how-do-i-validate-on-every-keystroke","How do I validate on every keystroke?",[116],"Set validateOn: { input: true } on the field. See Validation.",{"id":1160,"title":1161,"titles":1162,"content":1163,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#how-do-i-validate-only-on-submit","How do I validate only on submit?",[116],"Disable blur and change validation and use lazy mode. See the \"Validate on submit only\" preset in Validation.",{"id":1165,"title":1166,"titles":1167,"content":1168,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#how-do-i-show-errors-returned-by-my-api","How do I show errors returned by my API?",[116],"Call form.setError({ message, path }). See Server errors.",{"id":1170,"title":1171,"titles":1172,"content":1173,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#how-do-i-load-saved-data-into-a-form","How do I load saved data into a form?",[116],"Pass it as initialValues, or call form.reset(values) when it arrives. See Editing existing data.",{"id":1175,"title":1176,"titles":1177,"content":1178,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#how-do-i-pass-a-form-to-a-child-component","How do I pass a form to a child component?",[116],"Type the prop as UseFormInstance\u003Ctypeof schema>. See TypeScript.",{"id":1180,"title":1181,"titles":1182,"content":1183,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#can-i-use-it-with-my-component-library","Can I use it with my component library?",[116],"Yes. Map the control's value to field.model and its events to field.events. See Custom components, Nuxt UI and ShadcnVue.",{"id":1185,"title":1186,"titles":1187,"content":1188,"level":197},"\u002Fdocs\u002Freference\u002Ffaq#can-i-use-notform-with-an-ai-coding-assistant","Can I use NotForm with an AI coding assistant?",[116],"Yes. See Working with AI.",{"id":126,"title":125,"titles":1190,"content":1191,"level":191},[],"Submit to an API and show field-level and form-level errors that the server returns. Client-side validation cannot know everything. A server may reject an email that is already registered, or fail for reasons unrelated to any field. This recipe turns those responses into errors your form displays.",{"id":1193,"title":1194,"titles":1195,"content":1196,"level":197},"\u002Fdocs\u002Frecipes\u002Fserver-errors#the-flow","The flow",[125],"Submit. The schema validates first, so onSubmit only runs with valid data. Send the data. Inspect the response. For field problems, call form.setError({ message, path }). For everything else, call form.setError({ message }), which has no path and is a form-level error.",{"id":1198,"title":1199,"titles":1200,"content":1201,"level":197},"\u002Fdocs\u002Frecipes\u002Fserver-errors#example","Example",[125],"\u003Cscript setup lang=\"ts\">\nimport { Field, FieldError, Form, useForm } from 'notform'\nimport { computed } from 'vue'\nimport { z } from 'zod'\n\nconst form = useForm({\n  initialValues: { email: '', username: '' },\n  async onSubmit(data) {\n    try {\n      const response = await fetch('\u002Fapi\u002Fregister', {\n        body: JSON.stringify(data),\n        headers: { 'content-type': 'application\u002Fjson' },\n        method: 'POST',\n      })\n\n      if (response.status === 422) {\n        \u002F\u002F { errors: { email: 'Already in use.', username: '...' } }\n        const body = await response.json() as { errors: Record\u003Cstring, string> }\n\n        for (const [name, message] of Object.entries(body.errors)) {\n          form.setError({ message, path: [name] })\n        }\n\n        return\n      }\n\n      if (!response.ok) {\n        form.setError({ message: 'Something went wrong. Please try again.' })\n        return\n      }\n\n      form.reset()\n    } catch {\n      form.setError({ message: 'We could not reach the server. Check your connection.' })\n    }\n  },\n  schema: z.object({\n    email: z.email('Enter a valid email address.'),\n    username: z.string().min(3, 'Use at least 3 characters.'),\n  }),\n})\n\nconst formErrors = computed(() => {\n  return form.errors.filter((issue) => {\n    return !issue.path?.length\n  })\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm\n    :form=\"form\"\n    novalidate\n  >\n    \u003CFieldError\n      :errors=\"formErrors\"\n      as=\"p\"\n      role=\"alert\"\n    \u002F>\n\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('username')\"\n    >\n      \u003Clabel :for=\"field.path\">Username\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        type=\"email\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"form.isSubmitting\"\n    >\n      Create account\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",{"id":1203,"title":1204,"titles":1205,"content":1206,"level":197},"\u002Fdocs\u002Frecipes\u002Fserver-errors#how-long-server-errors-last","How long server errors last",[125],"Error Cleared when A field error from setError({ path }) That field validates again and the schema reports no issue there, for example on blur or when the user edits it. Also on reset(), and on the next submit. A form-level error from setError({ message }) The next submit, a form.validate(), or reset(). Editing a field with a server error therefore removes the error as soon as the schema passes, which is usually right: the server's complaint was about the old value. To keep a server error visible while the user edits, store it yourself and display it next to FieldError.",{"id":1208,"title":1209,"titles":1210,"content":1211,"level":197},"\u002Fdocs\u002Frecipes\u002Fserver-errors#nested-and-array-paths","Nested and array paths",[125],"A path is an array of segments. Use indices for array items. form.setError({ message: 'Email already invited.', path: ['contacts', 1, 'email'] })\nform.setError({ message: 'Invalid city.', path: ['address', 'city'] }) If the server returns dot paths, split them: path: key.split('.'). Numeric strings such as '1' match array indices.",{"id":1213,"title":887,"titles":1214,"content":1215,"level":197},"\u002Fdocs\u002Frecipes\u002Fserver-errors#notes",[125],"setError with a path replaces errors at that path. Without a path it appends, so avoid calling it repeatedly for the same message. Errors thrown (not returned) from onSubmit are not caught by NotForm. Wrap risky calls in try\u002Fcatch, as above. See Submission and reset. isSubmitting stays true until onSubmit settles, so the button stays disabled during the request. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":130,"title":129,"titles":1217,"content":1218,"level":191},[],"Validate one field against another, such as confirm password or date ranges, and keep the dependent field's error up to date. Some rules involve more than one field: a confirmation must match, an end date must follow a start date. NotForm validates with your schema, so you write the rule in the schema and tell it which field should display the error.",{"id":1220,"title":1221,"titles":1222,"content":1223,"level":197},"\u002Fdocs\u002Frecipes\u002Fcross-field-validation#put-the-rule-on-the-schema-and-set-the-path","Put the rule on the schema, and set the path",[129],"An issue is shown by the field whose path matches the issue's path. Report the issue on the field you want to display it. import { z } from 'zod'\n\nconst schema = z.object({\n  confirmPassword: z.string().min(1, 'Confirm your password.'),\n  password: z.string().min(8, 'Use at least 8 characters.'),\n}).refine((values) => {\n  return values.password === values.confirmPassword\n}, {\n  message: 'Passwords do not match.',\n  path: ['confirmPassword'],\n}) Without path, the issue is form-level and no field displays it. Some validators skip object-level rules while a field in the object is invalid, so the mismatch error may only appear after the individual fields pass. Check your validator's documentation if the cross-field message does not show when you expect it.",{"id":1225,"title":1226,"titles":1227,"content":1228,"level":197},"\u002Fdocs\u002Frecipes\u002Fcross-field-validation#keep-the-dependent-field-up-to-date","Keep the dependent field up to date",[129],"Fields validate on their own events. If the user fixes password after confirmPassword showed an error, confirmPassword does not know to re-validate. Watch the source field and re-validate the dependent one. \u003Cscript setup lang=\"ts\">\nimport { watch } from 'vue'\n\nconst password = form.field('password')\nconst confirmPassword = form.field('confirmPassword')\n\nwatch(\n  () => {\n    return password.model\n  },\n  () => {\n  \u002F\u002F only after the user has interacted with the confirm field\n    if (confirmPassword.isTouched) {\n      void confirmPassword.validate()\n    }\n  }\n)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm\n    :form=\"form\"\n    novalidate\n  >\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"password\"\n    >\n      \u003Clabel :for=\"field.path\">Password\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        type=\"password\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"confirmPassword\"\n    >\n      \u003Clabel :for=\"field.path\">Confirm password\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        type=\"password\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate> Passing the pre-created field instances to Field is fine. Field accepts any field instance.",{"id":1230,"title":1231,"titles":1232,"content":1233,"level":197},"\u002Fdocs\u002Frecipes\u002Fcross-field-validation#date-ranges","Date ranges",[129],"The same pattern applies to any dependent pair. const schema = z.object({\n  endDate: z.string().min(1, 'Choose an end date.'),\n  startDate: z.string().min(1, 'Choose a start date.'),\n}).refine((values) => {\n  return values.endDate >= values.startDate\n}, {\n  message: 'End date must be on or after the start date.',\n  path: ['endDate'],\n}) ISO date strings (2026-11-02) compare correctly as text. Watch startDate and re-validate endDate, exactly as above.",{"id":1235,"title":1236,"titles":1237,"content":1238,"level":197},"\u002Fdocs\u002Frecipes\u002Fcross-field-validation#show-a-rule-for-the-whole-form","Show a rule for the whole form",[129],"For a rule that is not about one field, omit path and show it as a form-level error. See Errors. html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":134,"title":133,"titles":1240,"content":1241,"level":191},[],"Check values against a server, such as username availability, with debounce, caching, and loading state. Validators that support asynchronous rules work with NotForm, because validation is already asynchronous. The thing to design around is cost: every field validation runs the entire schema, so an async rule runs whenever any field validates.",{"id":1243,"title":1244,"titles":1245,"content":1246,"level":197},"\u002Fdocs\u002Frecipes\u002Fasync-validation#example-username-availability","Example: username availability",[133],"\u003Cscript setup lang=\"ts\">\nimport { Field, FieldError, Form, useForm } from 'notform'\nimport { z } from 'zod'\n\n\u002F\u002F cache the promise per value so unchanged values never hit the network twice\nconst usernameLookups = new Map\u003Cstring, Promise\u003Cboolean>>()\n\nfunction isUsernameTaken(username: string): Promise\u003Cboolean> {\n  let usernameLookup = usernameLookups.get(username)\n\n  if (!usernameLookup) {\n    try {\n      const response = await fetch(`\u002Fapi\u002Fusernames\u002F${encodeURIComponent(username)}`)\n      const responseToJson = await response.json() as { taken: boolean }\n\n      usernameLookup = responseToJson.taken\n    } catch {\n      \u002F\u002F Handle the error as needed\n    }\n\n    usernameLookups.set(username, usernameLookup)\n  }\n\n  return usernameLookup\n}\n\nconst schema = z.object({\n  email: z.email('Enter a valid email address.'),\n  username: z.string()\n    .min(3, 'Use at least 3 characters.')\n    .refine(async (value) => {\n      \u002F\u002F skip the request when a cheaper rule already fails\n      if (value.length \u003C 3) {\n        return true\n      }\n\n      const checkIfUsernameIsTaken = await isUsernameTaken(value)\n      return !checkIfUsernameIsTaken\n    }, 'That username is taken.'),\n})\n\nconst form = useForm({\n  initialValues: { email: '', username: '' },\n  schema,\n})\n\nconst username = form.field('username', {\n  debounce: 400,\n  validateOn: { input: true },\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm\n    :form=\"form\"\n    novalidate\n  >\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"username\"\n    >\n      \u003Clabel :for=\"field.path\">Username\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        autocomplete=\"username\"\n      >\n\n      \u003Cspan\n        v-if=\"field.isValidating\"\n        role=\"status\"\n      >Checking...\u003C\u002Fspan>\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        type=\"email\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"form.isSubmitting\"\n    >\n      Create account\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",{"id":1248,"title":1249,"titles":1250,"content":1251,"level":197},"\u002Fdocs\u002Frecipes\u002Fasync-validation#why-each-piece-is-there","Why each piece is there",[133],"Piece Reason debounce: 400 Waits for a pause in typing instead of validating on every keystroke. Blur and submit are never delayed. validateOn: { input: true } Validates while typing, not only on blur. The promise cache The email field's validation also runs the username rule. The cache makes that free for unchanged values. The early return true Avoids a request for values the cheaper min(3) rule already rejects. The min message is still reported by that rule. field.isValidating true while any validation that concerns this field runs. Use it for the spinner.",{"id":1253,"title":511,"titles":1254,"content":1255,"level":197},"\u002Fdocs\u002Frecipes\u002Fasync-validation#stale-results",[133],"If the user types again while a lookup is in flight, the old result is discarded and cannot overwrite newer errors. Any value change makes every in-flight validation stale, and the change's own event starts a fresh one. See Validation.",{"id":1257,"title":1258,"titles":1259,"content":1260,"level":197},"\u002Fdocs\u002Frecipes\u002Fasync-validation#submit-still-validates-everything","Submit still validates everything",[133],"Submitting re-runs the whole schema, including the async rule, so a username that became taken after the last check is still caught. If the cache could hold outdated answers, clear it before submit or key it by time.",{"id":1262,"title":1263,"titles":1264,"content":1265,"level":197},"\u002Fdocs\u002Frecipes\u002Fasync-validation#cache-invalidation","Cache invalidation",[133],"The simple Map above lives for the component's lifetime. If availability can change during a session, expire entries after a short time, or clear the cache in onSubmit's failure path. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":138,"title":137,"titles":1267,"content":1268,"level":191},[],"Load a saved record into a form, track unsaved changes, and make the saved result the new baseline. An edit form starts from data that already exists. The key idea is the baseline: the values isDirty compares against and reset() returns to. Loaded data should become the baseline.",{"id":1270,"title":1271,"titles":1272,"content":1273,"level":197},"\u002Fdocs\u002Frecipes\u002Fediting-existing-data#when-the-data-is-ready-before-the-form","When the data is ready before the form",[137],"If the record is available when the form is created, pass it as initialValues. const { data: profile } = await useFetch('\u002Fapi\u002Fprofile') \u002F\u002F Nuxt\n\nconst form = useForm({\n  initialValues: profile.value ?? { bio: '', displayName: '' },\n  async onSubmit(values) {\n    const saved = await $fetch('\u002Fapi\u002Fprofile', { body: values, method: 'PUT' })\n    form.reset(saved)\n  },\n  schema: profileSchema,\n}) In plain Vue, wait to render the form until the data has loaded, then pass it down. \u003Ctemplate>\n  \u003CProfileForm\n    v-if=\"profile\"\n    :profile=\"profile\"\n  \u002F>\n\u003C\u002Ftemplate>",{"id":1275,"title":1276,"titles":1277,"content":1278,"level":197},"\u002Fdocs\u002Frecipes\u002Fediting-existing-data#when-the-data-arrives-after-the-form-exists","When the data arrives after the form exists",[137],"Create the form with defaults and call reset(values) once the data arrives. This replaces the baseline, so isDirty is false right after loading. const form = useForm({\n  initialValues: { bio: '', displayName: '' },\n  async onSubmit(data) {\n    const saved = await api.updateProfile(data)\n    form.reset(saved)\n  },\n  schema: profileSchema,\n})\n\nonMounted(async () => {\n  form.reset(await api.getProfile())\n}) reset(values) also restores errors to the baseline, clears touched state, bumps resetCount, and gives array items fresh ids.",{"id":1280,"title":1281,"titles":1282,"content":1283,"level":197},"\u002Fdocs\u002Frecipes\u002Fediting-existing-data#make-the-saved-data-the-new-baseline","Make the saved data the new baseline",[137],"After a successful save, call form.reset(saved) with what the server stored. The form then reports isDirty === false, and a later plain reset() returns to the saved version, not the original. If the server returns exactly what you sent, form.reset(data) inside onSubmit is enough. Remember that data is the schema output. If your schema transforms values, reset with an object in the input shape instead.",{"id":1285,"title":1286,"titles":1287,"content":1288,"level":197},"\u002Fdocs\u002Frecipes\u002Fediting-existing-data#warn-about-unsaved-changes","Warn about unsaved changes",[137],"\u003Cscript setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted } from 'vue'\nimport { onBeforeRouteLeave } from 'vue-router'\n\nonBeforeRouteLeave(() => {\n  if (form.isDirty && !confirm('Discard unsaved changes?')) {\n    return false\n  }\n})\n\nfunction warnBeforeClosing(event: BeforeUnloadEvent) {\n  if (form.isDirty) {\n    event.preventDefault()\n  }\n}\n\nonMounted(async () => {\n  window.addEventListener('beforeunload', warnBeforeClosing)\n})\nonBeforeUnmount(async () => {\n  window.removeEventListener('beforeunload', warnBeforeClosing)\n})\n\u003C\u002Fscript>",{"id":1290,"title":1291,"titles":1292,"content":1293,"level":197},"\u002Fdocs\u002Frecipes\u002Fediting-existing-data#show-save-state","Show save state",[137],"\u003Ctemplate>\n  \u003Cbutton\n    type=\"submit\"\n    :disabled=\"!form.isDirty || form.isSubmitting\"\n  >\n    {{ form.isSubmitting ? 'Saving...' : 'Save changes' }}\n  \u003C\u002Fbutton>\n\n  \u003Cbutton\n    type=\"reset\"\n    :disabled=\"!form.isDirty\"\n  >\n    Discard changes\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>",{"id":1295,"title":1296,"titles":1297,"content":1298,"level":197},"\u002Fdocs\u002Frecipes\u002Fediting-existing-data#tips","Tips",[137],"Keep every bound field present in the loaded object, using '', false, or [] where the server sends null. Missing keys reach the schema as undefined. Reload the baseline when the underlying record changes. For example, watch(profile, (value) => {\n  form.reset(value)\n}) Input and output shapes can differ. reset and initialValues always use the input shape. html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"id":142,"title":141,"titles":1300,"content":1301,"level":191},[],"Split one form into steps, validate each step before continuing, and submit everything at the end. A multi-step form is one form whose schema changes as the user moves through it. Each step renders a subset of the fields and validates against its own schema. Because values live in the form, they persist as users move between steps.",{"id":1303,"title":1304,"titles":1305,"content":1306,"level":197},"\u002Fdocs\u002Frecipes\u002Fmulti-step-forms#approach","Approach",[141],"Define one schema per step. Each schema extends the previous one, so the last schema describes the whole form. Pick the active schema from the current step index and pass it to useForm. Render a type=\"submit\" button on every step. Submit validates the active schema, then calls onSubmit. In onSubmit, advance to the next step. On the final step, send the values instead.",{"id":1308,"title":1199,"titles":1309,"content":1310,"level":197},"\u002Fdocs\u002Frecipes\u002Fmulti-step-forms#example",[141],"\u003Cscript setup lang=\"ts\">\nimport { Field, FieldError, Form, useForm } from 'notform'\nimport { computed, ref } from 'vue'\nimport { z } from 'zod'\n\nconst plans = ['free', 'pro'] as const\n\n\u002F\u002F Step 1: personal information.\nconst profileStepSchema = z.object({\n  email: z.email('Enter a valid email address.'),\n  name: z.string().min(1, 'Enter your name.'),\n})\n\n\u002F*\n * Step 2: plan selection.\n * Each step's schema extends the previous one, so the final submit\n * receives every field the user entered, not only the last step's.\n *\u002F\nconst planStepSchema = profileStepSchema.extend({\n  plan: z.enum(plans, { error: 'Choose a plan.' }),\n})\n\nconst stepSchemas = [profileStepSchema, planStepSchema]\n\nconst stepIndex = ref(0)\nconst isFirstStep = computed(() => {\n  return stepIndex.value === 0\n})\nconst isLastStep = computed(() => {\n  return stepIndex.value === stepSchemas.length - 1\n})\n\n\u002F\u002F Validate only against the schema of the step the user is on.\nconst activeSchema = computed(() => {\n  return stepSchemas[stepIndex.value]\n})\n\nconst form = useForm({\n  initialValues: { email: '', name: '' },\n  schema: activeSchema,\n\n  \u002F\u002F Runs only after the active step has passed validation.\n  async onSubmit(values) {\n    if (!isLastStep.value) {\n      stepIndex.value++\n      return\n    }\n\n    const response = await fetch('\u002Fapi\u002Fsign-up', {\n      body: JSON.stringify(values),\n      headers: { 'content-type': 'application\u002Fjson' },\n      method: 'POST',\n    })\n\n    if (!response.ok) {\n      throw new Error('Sign-up failed.')\n    }\n  },\n})\n\nfunction goToPreviousStep() {\n  if (!isFirstStep.value) {\n    stepIndex.value--\n  }\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm\n    :form=\"form\"\n    novalidate\n  >\n    \u003C!-- Step 1: Personal information -->\n    \u003Cdiv v-if=\"stepIndex === 0\">\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('name')\"\n      >\n        \u003Clabel :for=\"field.path\">Name\u003C\u002Flabel>\n\n        \u003Cinput\n          :id=\"field.path\"\n          v-model=\"field.model\"\n          v-bind=\"field.events\"\n        >\n\n        \u003CFieldError :errors=\"field\" \u002F>\n      \u003C\u002FField>\n\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('email')\"\n      >\n        \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n        \u003Cinput\n          :id=\"field.path\"\n          v-model=\"field.model\"\n          v-bind=\"field.events\"\n          type=\"email\"\n        >\n\n        \u003CFieldError :errors=\"field\" \u002F>\n      \u003C\u002FField>\n    \u003C\u002Fdiv>\n\n    \u003C!-- Step 2: Plan selection -->\n    \u003Cdiv v-else-if=\"stepIndex === 1\">\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('plan')\"\n      >\n        \u003Cfieldset>\n          \u003Clegend>Plan\u003C\u002Flegend>\n\n          \u003Clabel\n            v-for=\"plan in plans\"\n            :key=\"plan\"\n          >\n            \u003Cinput\n              v-model=\"field.model\"\n              v-bind=\"field.events\"\n              type=\"radio\"\n              :name=\"field.path\"\n              :value=\"plan\"\n            >\n            {{ plan }}\n          \u003C\u002Flabel>\n        \u003C\u002Ffieldset>\n\n        \u003CFieldError :errors=\"field\" \u002F>\n      \u003C\u002FField>\n    \u003C\u002Fdiv>\n\n    \u003C!-- Navigation -->\n    \u003Cbutton\n      v-if=\"!isFirstStep\"\n      type=\"button\"\n      @click=\"goToPreviousStep\"\n    >\n      Previous\n    \u003C\u002Fbutton>\n\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"form.isSubmitting\"\n    >\n      {{ isLastStep ? 'Finish' : 'Next' }}\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",{"id":1312,"title":1313,"titles":1314,"content":1315,"level":197},"\u002Fdocs\u002Frecipes\u002Fmulti-step-forms#why-this-works","Why this works",[141],"Values persist. form.values holds every step's data. Fields on hidden steps simply are not rendered, but their values remain. Step validation is scoped. The form validates against activeSchema, so fields from later steps cannot produce errors early. Next needs no validity check. onSubmit runs only after the active schema passes, so advancing the step there is enough. The final submit is authoritative. The last schema extends every earlier one, so it validates every field and values contains everything the user entered. Every field counts as touched.",{"id":1317,"title":887,"titles":1318,"content":1319,"level":197},"\u002Fdocs\u002Frecipes\u002Fmulti-step-forms#notes",[141],"Give every field a starting value, including steps the user has not reached. undefined is correct for a choice with no default, as with plan. Do not gate navigation on form.isValid. It is true before any validation has run, and validation is async, so reading it right after a click can let an invalid step through. Let onSubmit do the gating, as the example does. Next is a submit button, so pressing Enter in a field behaves like clicking Next: it validates the active step and advances. Previous is type=\"button\", so it never submits or validates. To keep step state in the URL, store stepIndex in a route query. NotForm does not need to know about it.",{"id":1321,"title":1322,"titles":1323,"content":1324,"level":197},"\u002Fdocs\u002Frecipes\u002Fmulti-step-forms#with-pinia","With Pinia",[141],"The same approach works when the form lives in a Pinia store. Create the form in the store, render each step as its own component, and wrap the active step in \u003CKeepAlive> so step components are cached while the user moves between them. This helps when steps are large enough to live in separate files, or when something outside the form, such as a progress sidebar or a route guard, needs to read the current step. import { useForm } from 'notform'\nimport { defineStore } from 'pinia'\nimport { computed, ref } from 'vue'\n\n\u002F\u002F `profileStepSchema` and `planStepSchema` are the same as above.\nconst stepSchemas = [profileStepSchema, planStepSchema]\n\nexport const useSignUpStore = defineStore('sign-up', () => {\n  const stepIndex = ref(0)\n  const isFirstStep = computed(() => {\n    return stepIndex.value === 0\n  })\n  const isLastStep = computed(() => {\n    return stepIndex.value === stepSchemas.length - 1\n  })\n  const activeSchema = computed(() => {\n    return stepSchemas[stepIndex.value]\n  })\n\n  const form = useForm({\n    initialValues: { email: '', name: '' },\n    async onSubmit(values) {\n      if (!isLastStep.value) {\n        stepIndex.value++\n      }\n\n      \u002F\u002F Send `values` to your API.\n    },\n    schema: activeSchema,\n  })\n\n  function goToPreviousStep() {\n    if (!isFirstStep.value) {\n      stepIndex.value--\n    }\n  }\n\n  return { form, goToPreviousStep, isFirstStep, isLastStep, stepIndex }\n})\n\u003Cscript setup lang=\"ts\">\nimport { Form } from 'notform'\nimport { useSignUpStore } from '..\u002Fstores\u002Fsign-up'\nimport PlanStep from '.\u002FPlanStep.vue'\nimport ProfileStep from '.\u002FProfileStep.vue'\n\nconst signUp = useSignUpStore()\nconst steps = [ProfileStep, PlanStep]\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm\n    :form=\"signUp.form\"\n    novalidate\n  >\n    \u003CKeepAlive>\n      \u003Ccomponent :is=\"steps[signUp.stepIndex]\" \u002F>\n    \u003C\u002FKeepAlive>\n\n    \u003Cbutton\n      v-if=\"!signUp.isFirstStep\"\n      type=\"button\"\n      @click=\"signUp.goToPreviousStep\"\n    >\n      Previous\n    \u003C\u002Fbutton>\n\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"signUp.form.isSubmitting\"\n    >\n      {{ signUp.isLastStep ? 'Finish' : 'Next' }}\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate> Each step component reads the store and renders its own Fields, exactly like the single-file example. Values live in the store, not in KeepAlive. The form keeps every step's values either way. KeepAlive only preserves each step's component state, such as focus, scroll position, and local UI state, and skips re-mounting. KeepAlive takes one child at a time. Switching with \u003Ccomponent :is> fits this naturally. The store outlives the page. Reset the form and stepIndex when the wizard finishes or the user leaves, or the next visit starts mid-way. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":151,"title":150,"titles":1326,"content":1327,"level":191},[],"Connect any Vue input component to a NotForm field by mapping its value and events, and build reusable field wrappers. Any control can work with NotForm. You only need to answer two questions about it: How does it take and emit its value? Usually v-model. Which of its events mean \"typed\", \"committed\", and \"left\"? These map to onInput, onChange, and onBlur.",{"id":1329,"title":1330,"titles":1331,"content":1332,"level":197},"\u002Fdocs\u002Fintegrations\u002Fcustom-components#choose-the-binding","Choose the binding",[150],"The control... Bind it like this Forwards native input, change, and blur events v-model=\"field.model\" v-bind=\"field.events\" Emits only update:modelValue when a value is picked v-model=\"field.model\" @update:model-value=\"field.events.onChange\" Emits update:modelValue on every keystroke v-model=\"field.model\" @update:model-value=\"field.events.onInput\" Emits an event when focus leaves, such as blur, focusout, or close @blur=\"field.events.onBlur\" Use the event names from the component's own documentation.",{"id":1334,"title":1335,"titles":1336,"content":1337,"level":197},"\u002Fdocs\u002Fintegrations\u002Fcustom-components#which-handler-means-what","Which handler means what",[150],"onInput: the value changed while the user is still editing, such as typing. By default it only validates if the field already shows an error. onChange: the user committed a value, such as selecting an option. It validates by default. onBlur: focus left the control. It marks the field touched and validates by default. Pick the handler that matches the interaction. You can change what each one triggers with validateOn and validationMode. Keep v-model before the event binding. The value has to be written before onChange or onInput takes its snapshot for validation.",{"id":1339,"title":1340,"titles":1341,"content":1342,"level":197},"\u002Fdocs\u002Fintegrations\u002Fcustom-components#controls-that-emit-a-value","Controls that emit a value",[150],"\u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('country')\"\n  >\n    \u003CCountryPicker\n      v-model=\"field.model\"\n      :invalid=\"!field.isValid\"\n      @update:model-value=\"field.events.onChange\"\n      @focusout=\"field.events.onBlur\"\n    \u002F>\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":1344,"title":1345,"titles":1346,"content":1347,"level":197},"\u002Fdocs\u002Fintegrations\u002Fcustom-components#controls-that-forward-native-events","Controls that forward native events",[150],"\u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('displayName')\"\n  >\n    \u003CTextInput\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n    \u002F>\n\n    \u003CFieldError :errors=\"field\" \u002F>\n  \u003C\u002FField>\n\u003C\u002Ftemplate> field.events contains onBlur, onChange, and onInput. If the component does not forward those events, bind the callbacks explicitly, as above.",{"id":1349,"title":1350,"titles":1351,"content":1352,"level":197},"\u002Fdocs\u002Fintegrations\u002Fcustom-components#controls-that-do-not-use-v-model","Controls that do not use v-model",[150],"If a control exposes a value prop and a callback instead, write through field.model yourself: \u003Ctemplate>\n  \u003CRatingInput\n    :value=\"field.model\"\n    @rate=\"(value) => {\n      field.model = value\n      field.events.onChange()\n    }\"\n  \u002F>\n\u003C\u002Ftemplate> Assign first, then call the event.",{"id":1354,"title":1355,"titles":1356,"content":1357,"level":197},"\u002Fdocs\u002Fintegrations\u002Fcustom-components#a-reusable-field-wrapper","A reusable field wrapper",[150],"Most apps wrap label, input, and error once. Accept a field instance as a prop. \u003Cscript setup lang=\"ts\">\nimport { FieldError, type FieldInstance } from 'notform'\n\ndefineProps\u003C{\n  field: FieldInstance\u003Cstring>\n  label: string\n  type?: string\n}>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"field\">\n    \u003Clabel :for=\"field.path\">{{ label }}\u003C\u002Flabel>\n\n    \u003Cinput\n      :id=\"field.path\"\n      v-model=\"field.model\"\n      v-bind=\"field.events\"\n      :type=\"type ?? 'text'\"\n      :aria-invalid=\"!field.isValid\"\n    >\n\n    \u003CFieldError\n      :errors=\"field\"\n      class=\"field-error\"\n      role=\"alert\"\n    \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate> \u003Ctemplate>\n  \u003CAppTextField\n    :field=\"form.field('email')\"\n    label=\"Email\"\n    type=\"email\"\n  \u002F>\n\u003C\u002Ftemplate> The parent keeps the form instance and passes only the field. Array items are fields too, so \u003CAppTextField :field=\"item.field('email')\" \u002F> works inside ArrayField.",{"id":1359,"title":1360,"titles":1361,"content":1362,"level":197},"\u002Fdocs\u002Fintegrations\u002Fcustom-components#checklist-for-a-new-control","Checklist for a new control",[150],"Does field.model reflect the control's value type? Make sure the schema accepts that type (for example boolean, string[], or a number). Does the control have an element that can take focus, so onBlur can fire? Does it have an accessible name? Use field.path for ids. Does it show field.isValid and the error message?",{"id":1364,"title":798,"titles":1365,"content":1366,"level":197},"\u002Fdocs\u002Fintegrations\u002Fcustom-components#related",[150],"Binding inputs Field html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"id":155,"title":154,"titles":1368,"content":1369,"level":191},[],"Use NotForm with Nuxt UI inputs and form fields, including text, textarea, select, checkbox, checkbox group, radio group, switch, and array fields. Nuxt UI provides styled inputs and a UFormField wrapper for labels and error text. NotForm owns the form state and validation, and Nuxt UI renders the controls.",{"id":1371,"title":1372,"titles":1373,"content":1374,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#setup","Setup",[154],"Install both modules and register them. export default defineNuxtConfig({\n  modules: ['@nuxt\u002Fui', '@notform\u002Fnuxt'],\n})",{"id":1376,"title":1377,"titles":1378,"content":1379,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#use-notforms-form-not-uform","Use NotForm's form, not UForm",[154],"Wrap your controls in NotForm's Form, not Nuxt UI's UForm. UForm has its own validation and submit pipeline. Using both makes two systems compete over the same state. UFormField is safe to use on its own. It lays out the label, description, and error text.",{"id":1381,"title":1382,"titles":1383,"content":1384,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#choose-the-right-binding","Choose the right binding",[154],"Bind the model with v-model:model-value, then report changes and blur to NotForm yourself. Text inputs report with onInput, and the other controls report with onChange. Component Binding UInput, UTextarea v-model=\"field.model\", @update:model-value=\"field.events.onInput\" and @blur=\"field.events.onBlur\" USelect, UCheckbox, UCheckboxGroup, URadioGroup, USwitch v-model=\"field.model\", @update:model-value=\"field.events.onChange\"and @blur=\"field.events.onBlur\"",{"id":1386,"title":1387,"titles":1388,"content":1389,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#show-errors","Show errors",[154],"UFormField renders error text from its error prop, so you do not need FieldError here. Pass it the first message of field.errors with a small helper: import type { ValidationIssue } from 'notform'\n\nfunction getFirstErrorMessage(errors?: ValidationIssue[]) {\n  return errors?.[0]?.message\n} Each section below shows the schema and the template it binds to. The snippets assume the form, getFirstErrorMessage and the items from the full example.",{"id":1391,"title":1392,"titles":1393,"content":1394,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#text-input","Text input",[154],"title: z.string().min(5, 'Bug title must be at least 5 characters.')\n  .max(32) \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('title')\"\n  >\n    \u003CUFormField\n      label=\"Bug Title\"\n      :error=\"getFirstErrorMessage(field.errors)\"\n    >\n      \u003CUInput\n        v-model=\"field.model\"\n        placeholder=\"Login button not working on mobile\"\n        autocomplete=\"off\"\n        class=\"inline-full\"\n        @update:model-value=\"field.events.onInput\"\n        @blur=\"field.events.onBlur\"\n      \u002F>\n    \u003C\u002FUFormField>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":1396,"title":382,"titles":1397,"content":1398,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#textarea",[154],"UTextarea takes the same bindings as a text input. Because field.model is reactive, you can read it anywhere in the slot, which is how the character counter stays in sync. description: z.string().min(20, 'Description must be at least 20 characters.')\n  .max(100) \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('description')\"\n  >\n    \u003CUFormField\n      label=\"Description\"\n      help=\"Include steps to reproduce, expected behavior, and what actually happened.\"\n      :error=\"getFirstErrorMessage(field.errors)\"\n    >\n      \u003CUTextarea\n        v-model=\"field.model\"\n        :rows=\"6\"\n        class=\"inline-full\"\n        placeholder=\"I'm having an issue with the login button on mobile.\"\n        @update:model-value=\"field.events.onInput\"\n        @blur=\"field.events.onBlur\"\n      \u002F>\n\n      \u003Cp class=\"mbs-1 text-xs text-muted tabular-nums\">\n        {{ field.model?.length || 0 }}\u002F100 characters\n      \u003C\u002Fp>\n    \u003C\u002FUFormField>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":1400,"title":407,"titles":1401,"content":1402,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#select",[154],"Pass the options through items. language: z.string().min(1, 'Please select your spoken language.') \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('language')\"\n  >\n    \u003CUFormField\n      label=\"Spoken Language\"\n      description=\"For best results, select the language you speak.\"\n      :error=\"getFirstErrorMessage(field.errors)\"\n    >\n      \u003CUSelect\n        v-model=\"field.model\"\n        :items=\"spokenLanguages\"\n        placeholder=\"Select\"\n        class=\"inline-full\"\n        @update:model-value=\"field.events.onChange\"\n        @blur=\"field.events.onBlur\"\n      \u002F>\n    \u003C\u002FUFormField>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":1404,"title":1405,"titles":1406,"content":1407,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#checkbox","Checkbox",[154],"responses: z.literal(true, 'Notifications are mandatory') \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('responses')\"\n  >\n    \u003CUFormField\n      label=\"Responses\"\n      description=\"Get notified for requests that take time.\"\n      :error=\"getFirstErrorMessage(field.errors)\"\n    >\n      \u003CUCheckbox\n        v-model=\"field.model\"\n        label=\"Push notifications\"\n        @update:model-value=\"field.events.onChange\"\n        @blur=\"field.events.onBlur\"\n      \u002F>\n    \u003C\u002FUFormField>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":1409,"title":397,"titles":1410,"content":1411,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#checkbox-group",[154],"UCheckboxGroup binds the array of selected values directly through v-model:model-value. tasks: z.array(z.string()).min(1, 'Please select at least one notification type.') \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('tasks')\"\n  >\n    \u003CUFormField :error=\"getFirstErrorMessage(field.errors)\">\n      \u003CUCheckboxGroup\n        v-model=\"field.model\"\n        legend=\"Tasks\"\n        :items=\"tasks\"\n        @update:model-value=\"field.events.onChange\"\n        @blur=\"field.events.onBlur\"\n      \u002F>\n    \u003C\u002FUFormField>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":1413,"title":402,"titles":1414,"content":1415,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#radio-group",[154],"plan: z.string().min(1, 'You must select a subscription plan to continue.') \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('plan')\"\n  >\n    \u003CUFormField\n      description=\"You can upgrade or downgrade your plan at any time.\"\n      :error=\"getFirstErrorMessage(field.errors)\"\n    >\n      \u003CURadioGroup\n        v-model=\"field.model\"\n        legend=\"Plan\"\n        variant=\"card\"\n        indicator=\"end\"\n        :items=\"plans\"\n        @update:model-value=\"field.events.onChange\"\n        @blur=\"field.events.onBlur\"\n      \u002F>\n    \u003C\u002FUFormField>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":1417,"title":1418,"titles":1419,"content":1420,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#switch","Switch",[154],"twoFactor: z.boolean().refine((value) => {\n  return value === true\n}, 'It is highly recommended to enable two-factor authentication.') \u003Ctemplate>\n  \u003CField\n    v-slot=\"{ field }\"\n    :field=\"form.field('twoFactor')\"\n  >\n    \u003CUFormField\n      label=\"Multi-factor authentication\"\n      description=\"Enable multi-factor authentication to secure your account.\"\n      :error=\"getFirstErrorMessage(field.errors)\"\n    >\n      \u003CUSwitch\n        v-model=\"field.model\"\n        @update:model-value=\"field.events.onChange\"\n        @blur=\"field.events.onBlur\"\n      \u002F>\n    \u003C\u002FUFormField>\n  \u003C\u002FField>\n\u003C\u002Ftemplate>",{"id":1422,"title":65,"titles":1423,"content":1424,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#array-fields",[154],"Use ArrayField for lists the user can grow, shrink, and reorder. Each item exposes item.field('key'), so you can wrap any of the inputs above in a Field. emails: z.array(z.object({ address: z.email('Enter a valid email address.') }))\n  .min(2, 'Add at least two email addresses.')\n  .max(5, 'You can add up to 5 email addresses.') \u003Ctemplate>\n  \u003CArrayField\n    v-slot=\"{ arrayField }\"\n    :array-field=\"form.arrayField('emails')\"\n  >\n    \u003CUFormField\n      label=\"Email Addresses\"\n      description=\"Add up to 5 email addresses where we can contact you.\"\n      :error=\"getFirstErrorMessage(arrayField.errors)\"\n    >\n      \u003Cdiv class=\"space-y-3\">\n        \u003CField\n          v-for=\"item in arrayField.items\"\n          :key=\"item.id\"\n          v-slot=\"{ field }\"\n          :field=\"item.field('address')\"\n        >\n          \u003CUFormField :error=\"getFirstErrorMessage(field.errors)\">\n            \u003CUInput\n              v-model=\"field.model\"\n              type=\"email\"\n              placeholder=\"name@example.com\"\n              class=\"inline-full\"\n              @update:model-value=\"field.events.onInput\"\n              @blur=\"field.events.onBlur\"\n            >\n              \u003Ctemplate\n                v-if=\"arrayField.items.length > 1\"\n                #trailing\n              >\n                \u003CUButton\n                  icon=\"i-lucide-chevron-up\"\n                  color=\"neutral\"\n                  variant=\"ghost\"\n                  size=\"xs\"\n                  :disabled=\"item.index === 0\"\n                  @click=\"arrayField.move(item.index, item.index - 1)\"\n                \u002F>\n\n                \u003CUButton\n                  icon=\"i-lucide-chevron-down\"\n                  color=\"neutral\"\n                  variant=\"ghost\"\n                  size=\"xs\"\n                  :disabled=\"item.index === arrayField.items.length - 1\"\n                  @click=\"arrayField.move(item.index, item.index + 1)\"\n                \u002F>\n\n                \u003CUButton\n                  icon=\"i-lucide-x\"\n                  color=\"neutral\"\n                  variant=\"ghost\"\n                  size=\"xs\"\n                  @click=\"arrayField.remove(item.index)\"\n                \u002F>\n              \u003C\u002Ftemplate>\n            \u003C\u002FUInput>\n          \u003C\u002FUFormField>\n        \u003C\u002FField>\n\n        \u003CUButton\n          color=\"neutral\"\n          variant=\"outline\"\n          block\n          size=\"sm\"\n          :disabled=\"arrayField.items.length >= 5\"\n          @click=\"arrayField.append({ address: '' })\"\n        >\n          Add Email Address\n        \u003C\u002FUButton>\n      \u003C\u002Fdiv>\n    \u003C\u002FUFormField>\n  \u003C\u002FArrayField>\n\u003C\u002Ftemplate> A few things to note: arrayField.items gives you each item's id and index. Use item.id as the :key, not the index, so inputs keep their state when you reorder or remove items. arrayField.append, arrayField.remove and arrayField.move change the list. Errors for a single item come from its field. Errors for the list as a whole, such as the min and max rules, come from arrayField.errors. Mirror the schema limits in the UI, as the Add button does with arrayField.items.length >= 5, so users can't reach an invalid state by clicking.",{"id":1426,"title":1427,"titles":1428,"content":1429,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#full-example","Full example",[154],"This form combines every input above. \u003Cscript setup lang=\"ts\">\nimport type { ValidationIssue } from 'notform'\nimport { z } from 'zod'\n\nconst spokenLanguages = [\n  { label: 'Auto', value: 'auto' },\n  { type: 'separator' as const },\n  { label: 'English', value: 'en' },\n  { label: 'Spanish', value: 'es' },\n  { label: 'French', value: 'fr' },\n]\n\nconst plans = [\n  { description: 'For everyday use with basic features.', label: 'Starter (100K tokens\u002Fmonth)', value: 'starter' },\n  { description: 'For advanced AI usage with more features.', label: 'Pro (1M tokens\u002Fmonth)', value: 'pro' },\n  { description: 'For large teams and heavy usage.', label: 'Enterprise (Unlimited tokens)', value: 'enterprise' },\n]\n\nconst tasks = [\n  { label: 'Push notifications', value: 'push' },\n  { label: 'Email notifications', value: 'email' },\n]\n\nconst schema = z.object({\n  description: z.string().min(20, 'Description must be at least 20 characters.')\n    .max(100),\n  emails: z.array(z.object({ address: z.email('Enter a valid email address.') }))\n    .min(2, 'Add at least two email addresses.')\n    .max(5, 'You can add up to 5 email addresses.'),\n  language: z.string().min(1, 'Please select your spoken language.')\n    .refine((value) => {\n      return value !== 'auto'\n    }, 'Auto-detection is not allowed.'),\n  plan: z.string().min(1, 'You must select a subscription plan to continue.'),\n  responses: z.literal(true, 'Notifications are mandatory'),\n  tasks: z.array(z.string()).min(1, 'Please select at least one notification type.'),\n  title: z.string().min(5, 'Bug title must be at least 5 characters.')\n    .max(32),\n  twoFactor: z.boolean().refine((value) => {\n    return value === true\n  }, 'It is highly recommended to enable two-factor authentication.'),\n})\n\nconst form = useForm({\n  initialValues: {\n    description: '',\n    emails: [{ address: '' }, { address: '' }],\n    language: '',\n    plan: '',\n    responses: true,\n    tasks: [],\n    title: '',\n    twoFactor: false,\n  },\n  onSubmit(data) {\n    console.log(data)\n  },\n  schema,\n})\n\nfunction getFirstErrorMessage(errors?: ValidationIssue[]) {\n  return errors?.[0]?.message\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CUApp>\n    \u003CForm\n      :form\n      class=\"mx-auto space-y-6 p-8 inline-full max-inline-xl\"\n    >\n      \u003C!-- Text input -->\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('title')\"\n      >\n        \u003CUFormField\n          label=\"Bug Title\"\n          :error=\"getFirstErrorMessage(field.errors)\"\n        >\n          \u003CUInput\n            v-model=\"field.model\"\n            placeholder=\"Login button not working on mobile\"\n            autocomplete=\"off\"\n            class=\"inline-full\"\n            @update:model-value=\"field.events.onInput\"\n            @blur=\"field.events.onBlur\"\n          \u002F>\n        \u003C\u002FUFormField>\n      \u003C\u002FField>\n\n      \u003C!-- Textarea -->\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('description')\"\n      >\n        \u003CUFormField\n          label=\"Description\"\n          help=\"Include steps to reproduce, expected behavior, and what actually happened.\"\n          :error=\"getFirstErrorMessage(field.errors)\"\n        >\n          \u003CUTextarea\n            v-model=\"field.model\"\n            :rows=\"6\"\n            class=\"inline-full\"\n            placeholder=\"I'm having an issue with the login button on mobile.\"\n            @update:model-value=\"field.events.onInput\"\n            @blur=\"field.events.onBlur\"\n          \u002F>\n\n          \u003Cp class=\"mbs-1 text-xs text-muted tabular-nums\">\n            {{ field.model?.length || 0 }}\u002F100 characters\n          \u003C\u002Fp>\n        \u003C\u002FUFormField>\n      \u003C\u002FField>\n\n      \u003C!-- Select -->\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('language')\"\n      >\n        \u003CUFormField\n          label=\"Spoken Language\"\n          description=\"For best results, select the language you speak.\"\n          :error=\"getFirstErrorMessage(field.errors)\"\n        >\n          \u003CUSelect\n            v-model=\"field.model\"\n            :items=\"spokenLanguages\"\n            placeholder=\"Select\"\n            class=\"inline-full\"\n            @update:model-value=\"field.events.onChange\"\n            @blur=\"field.events.onBlur\"\n          \u002F>\n        \u003C\u002FUFormField>\n      \u003C\u002FField>\n\n      \u003C!-- Checkbox -->\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('responses')\"\n      >\n        \u003CUFormField\n          label=\"Responses\"\n          description=\"Get notified for requests that take time.\"\n          :error=\"getFirstErrorMessage(field.errors)\"\n        >\n          \u003CUCheckbox\n            v-model=\"field.model\"\n            label=\"Push notifications\"\n            @update:model-value=\"field.events.onChange\"\n            @blur=\"field.events.onBlur\"\n          \u002F>\n        \u003C\u002FUFormField>\n      \u003C\u002FField>\n\n      \u003C!-- Checkbox group -->\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('tasks')\"\n      >\n        \u003CUFormField :error=\"getFirstErrorMessage(field.errors)\">\n          \u003CUCheckboxGroup\n            v-model=\"field.model\"\n            legend=\"Tasks\"\n            :items=\"tasks\"\n            @update:model-value=\"field.events.onChange\"\n            @blur=\"field.events.onBlur\"\n          \u002F>\n        \u003C\u002FUFormField>\n      \u003C\u002FField>\n\n      \u003C!-- Radio group -->\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('plan')\"\n      >\n        \u003CUFormField\n          description=\"You can upgrade or downgrade your plan at any time.\"\n          :error=\"getFirstErrorMessage(field.errors)\"\n        >\n          \u003CURadioGroup\n            v-model=\"field.model\"\n            legend=\"Plan\"\n            variant=\"card\"\n            indicator=\"end\"\n            :items=\"plans\"\n            @update:model-value=\"field.events.onChange\"\n            @blur=\"field.events.onBlur\"\n          \u002F>\n        \u003C\u002FUFormField>\n      \u003C\u002FField>\n\n      \u003C!-- Switch -->\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"form.field('twoFactor')\"\n      >\n        \u003CUFormField\n          label=\"Multi-factor authentication\"\n          description=\"Enable multi-factor authentication to secure your account.\"\n          :error=\"getFirstErrorMessage(field.errors)\"\n        >\n          \u003CUSwitch\n            v-model=\"field.model\"\n            @update:model-value=\"field.events.onChange\"\n            @blur=\"field.events.onBlur\"\n          \u002F>\n        \u003C\u002FUFormField>\n      \u003C\u002FField>\n\n      \u003C!-- Array field -->\n      \u003CArrayField\n        v-slot=\"{ arrayField }\"\n        :array-field=\"form.arrayField('emails')\"\n      >\n        \u003CUFormField\n          label=\"Email Addresses\"\n          description=\"Add up to 5 email addresses where we can contact you.\"\n          :error=\"getFirstErrorMessage(arrayField.errors)\"\n        >\n          \u003Cdiv class=\"space-y-3\">\n            \u003CField\n              v-for=\"item in arrayField.items\"\n              :key=\"item.id\"\n              v-slot=\"{ field }\"\n              :field=\"item.field('address')\"\n            >\n              \u003CUFormField :error=\"getFirstErrorMessage(field.errors)\">\n                \u003CUInput\n                  v-model=\"field.model\"\n                  type=\"email\"\n                  placeholder=\"name@example.com\"\n                  class=\"inline-full\"\n                  @update:model-value=\"field.events.onInput\"\n                  @blur=\"field.events.onBlur\"\n                >\n                  \u003Ctemplate\n                    v-if=\"arrayField.items.length > 1\"\n                    #trailing\n                  >\n                    \u003CUButton\n                      icon=\"i-lucide-chevron-up\"\n                      color=\"neutral\"\n                      variant=\"ghost\"\n                      size=\"xs\"\n                      :disabled=\"item.index === 0\"\n                      @click=\"arrayField.move(item.index, item.index - 1)\"\n                    \u002F>\n\n                    \u003CUButton\n                      icon=\"i-lucide-chevron-down\"\n                      color=\"neutral\"\n                      variant=\"ghost\"\n                      size=\"xs\"\n                      :disabled=\"item.index === arrayField.items.length - 1\"\n                      @click=\"arrayField.move(item.index, item.index + 1)\"\n                    \u002F>\n\n                    \u003CUButton\n                      icon=\"i-lucide-x\"\n                      color=\"neutral\"\n                      variant=\"ghost\"\n                      size=\"xs\"\n                      @click=\"arrayField.remove(item.index)\"\n                    \u002F>\n                  \u003C\u002Ftemplate>\n                \u003C\u002FUInput>\n              \u003C\u002FUFormField>\n            \u003C\u002FField>\n\n            \u003CUButton\n              color=\"neutral\"\n              variant=\"outline\"\n              block\n              size=\"sm\"\n              :disabled=\"arrayField.items.length >= 5\"\n              @click=\"arrayField.append({ address: '' })\"\n            >\n              Add Email Address\n            \u003C\u002FUButton>\n          \u003C\u002Fdiv>\n        \u003C\u002FUFormField>\n      \u003C\u002FArrayField>\n\n      \u003Cdiv\n        class=\"\n          flex gap-2\n\n          *:flex-1 *:justify-center\n        \"\n      >\n        \u003CUButton\n          type=\"reset\"\n          color=\"neutral\"\n          variant=\"outline\"\n        >\n          Reset\n        \u003C\u002FUButton>\n\n        \u003CUButton type=\"submit\">\n          Submit\n        \u003C\u002FUButton>\n      \u003C\u002Fdiv>\n    \u003C\u002FForm>\n  \u003C\u002FUApp>\n\u003C\u002Ftemplate>",{"id":1431,"title":798,"titles":1432,"content":1433,"level":197},"\u002Fdocs\u002Fintegrations\u002Fnuxt-ui#related",[154],"Custom components Nuxt module html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":159,"title":158,"titles":1435,"content":1436,"level":191},[],"Use NotForm with ShadcnVue inputs, including text, textarea, select, checkbox, radio group, switch, and array fields, and avoid the Field and FieldError naming conflict. ShadcnVue copies component source into your project, so the exact props depend on the version you installed. The mappings below follow the pattern in Custom components. Check your local component files if a binding differs.",{"id":1438,"title":1439,"titles":1440,"content":1441,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#resolve-the-naming-conflict","Resolve the naming conflict",[158],"Recent ShadcnVue releases include Field and FieldError components. NotForm exports components with the same names. Rename one set at the import. \u003Cscript setup lang=\"ts\">\nimport { Form, Field as NotField, FieldError as NotFieldError, useForm } from 'notform'\nimport { Field, FieldError, FieldLabel } from '@\u002Fcomponents\u002Fui\u002Ffield'\n\u003C\u002Fscript>\n\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@notform\u002Fnuxt', 'shadcn-vue\u002Fnuxt'],\n  notform: {\n    aliases: {\n      Field: 'NotField',\n      FieldError: 'NotFieldError'\n    },\n  },\n}) The examples below use NotField for NotForm and Field for ShadcnVue.",{"id":1443,"title":1382,"titles":1444,"content":1445,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#choose-the-right-binding",[158],"Every NotField slot gives you a field with a model, a set of events, and the validation state (isValid and errors). How you connect them depends on what the ShadcnVue component emits. Component Binding Input, Textarea, InputGroupInput, InputGroupTextarea v-model=\"field.model\" and v-bind=\"field.events\" Select v-model=\"field.model\" and @update:model-value=\"field.events.onChange\", with @blur=\"field.events.onBlur\" on SelectTrigger Checkbox, Switch, RadioGroup v-model=\"field.model\", @update:model-value=\"field.events.onChange\" and @blur=\"field.events.onBlur\" Checkbox group (array of values) :model-value and a handler that updates field.model, then calls field.events.onChange() Components that only emit update:modelValue don't fire native input or change events, so v-bind=\"field.events\" does nothing for them. Report changes and blur explicitly instead. Each section below shows the template and the schema it binds to. The snippets assume the form and the imports from the full example.",{"id":1447,"title":1392,"titles":1448,"content":1449,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#text-input",[158],"title: z\n  .string()\n  .min(5, 'Bug title must be at least 5 characters.')\n  .max(32, 'Bug title must be at most 32 characters.') \u003Ctemplate>\n  \u003CNotField\n    v-slot=\"{ field }\"\n    :field=\"form.field('title')\"\n  >\n    \u003CField :data-invalid=\"!field.isValid\">\n      \u003CFieldLabel :for=\"field.path\">\n        Bug Title\n      \u003C\u002FFieldLabel>\n\n      \u003CInput\n        :id=\"field.path\"\n        v-bind=\"field.events\"\n        v-model=\"field.model\"\n        placeholder=\"Login button not working on mobile\"\n        autocomplete=\"off\"\n        :aria-invalid=\"!field.isValid\"\n      \u002F>\n\n      \u003CFieldError\n        v-if=\"!field.isValid\"\n        :errors=\"field.errors\"\n      \u002F>\n    \u003C\u002FField>\n  \u003C\u002FNotField>\n\u003C\u002Ftemplate> field.errors is an array of issues that each have a message, which is the shape ShadcnVue's FieldError renders.",{"id":1451,"title":382,"titles":1452,"content":1453,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#textarea",[158],"Use InputGroupTextarea inside an InputGroup when you want an addon, such as a character counter. It takes the same bindings as a text input. description: z\n  .string()\n  .min(20, 'Description must be at least 20 characters.')\n  .max(100, 'Description must be at most 100 characters.') \u003Ctemplate>\n  \u003CNotField\n    v-slot=\"{ field }\"\n    :field=\"form.field('description')\"\n  >\n    \u003CField :data-invalid=\"!field.isValid\">\n      \u003CFieldLabel :for=\"field.path\">\n        Description\n      \u003C\u002FFieldLabel>\n\n      \u003CInputGroup>\n        \u003CInputGroupTextarea\n          :id=\"field.path\"\n          v-model=\"field.model\"\n          v-bind=\"field.events\"\n          placeholder=\"I'm having an issue with the login button on mobile.\"\n          :rows=\"6\"\n          class=\"resize-none min-block-24\"\n          :aria-invalid=\"!field.isValid\"\n        \u002F>\n\n        \u003CInputGroupAddon align=\"block-end\">\n          \u003CInputGroupText class=\"tabular-nums\">\n            {{ field.model?.length || 0 }}\u002F100 characters\n          \u003C\u002FInputGroupText>\n        \u003C\u002FInputGroupAddon>\n      \u003C\u002FInputGroup>\n\n      \u003CFieldDescription>\n        Include steps to reproduce, expected behavior, and what actually\n        happened.\n      \u003C\u002FFieldDescription>\n\n      \u003CFieldError\n        v-if=\"!field.isValid\"\n        :errors=\"field.errors\"\n      \u002F>\n    \u003C\u002FField>\n  \u003C\u002FNotField>\n\u003C\u002Ftemplate> field.model is reactive, so you can read it anywhere in the slot, which is how the counter stays in sync.",{"id":1455,"title":407,"titles":1456,"content":1457,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#select",[158],"The trigger is the focusable element, so put the id and report blur from it. language: z.string().min(1, 'Please select your spoken language.') \u003Ctemplate>\n  \u003CNotField\n    v-slot=\"{ field }\"\n    :field=\"form.field('language')\"\n  >\n    \u003CField :data-invalid=\"!field.isValid\">\n      \u003CFieldLabel :for=\"field.path\">\n        Spoken Language\n      \u003C\u002FFieldLabel>\n\n      \u003C!-- eslint-disable-next-line vue-a11y\u002Fform-control-has-label -->\n      \u003CSelect\n        v-model=\"field.model\"\n        @update:model-value=\"field.events.onChange\"\n      >\n        \u003CSelectTrigger\n          :id=\"field.path\"\n          :aria-invalid=\"!field.isValid\"\n          @blur=\"field.events.onBlur()\"\n        >\n          \u003CSelectValue placeholder=\"Select\" \u002F>\n        \u003C\u002FSelectTrigger>\n\n        \u003CSelectContent>\n          \u003CSelectItem\n            v-for=\"language in spokenLanguages\"\n            :key=\"language.value\"\n            :value=\"language.value\"\n          >\n            {{ language.label }}\n          \u003C\u002FSelectItem>\n        \u003C\u002FSelectContent>\n      \u003C\u002FSelect>\n\n      \u003CFieldError\n        v-if=\"!field.isValid\"\n        :errors=\"field.errors\"\n      \u002F>\n    \u003C\u002FField>\n  \u003C\u002FNotField>\n\u003C\u002Ftemplate> Keep an empty string as the initial value so the placeholder shows until the user picks something.",{"id":1459,"title":1405,"titles":1460,"content":1461,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#checkbox",[158],"Bind the checkbox with v-model:model-value, then report changes and blur to NotForm yourself: call field.events.onChange from update:model-value and field.events.onBlur from blur. responses: z.literal(true, 'Notifications are mandatory') \u003Ctemplate>\n  \u003CNotField\n    v-slot=\"{ field }\"\n    :field=\"form.field('responses')\"\n  >\n    \u003CField\n      orientation=\"horizontal\"\n      :data-invalid=\"!field.isValid\"\n    >\n      \u003CCheckbox\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        :aria-invalid=\"!field.isValid\"\n        @blur=\"field.events.onBlur\"\n        @update:model-value=\"field.events.onChange\"\n      \u002F>\n\n      \u003CFieldLabel\n        :for=\"field.path\"\n        class=\"font-normal\"\n      >\n        Push notifications\n      \u003C\u002FFieldLabel>\n    \u003C\u002FField>\n\n    \u003CFieldError\n      v-if=\"!field.isValid\"\n      :errors=\"field.errors\"\n    \u002F>\n  \u003C\u002FNotField>\n\u003C\u002Ftemplate> If the checkbox can be in an indeterminate state, its value is not a plain boolean, so your schema must accept that.",{"id":1463,"title":397,"titles":1464,"content":1465,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#checkbox-group",[158],"When several checkboxes share one field, store the selected values in an array. The checkbox doesn't bind to field.model directly. Instead, derive its checked state and write the new array back yourself. tasks: z\n  .array(z.string())\n  .min(1, 'Please select at least one notification type.') \u003Ctemplate>\n  \u003CNotField\n    v-slot=\"{ field }\"\n    :field=\"form.field('tasks')\"\n  >\n    \u003CFieldSet :data-invalid=\"!field.isValid\">\n      \u003CFieldLegend variant=\"label\">\n        Tasks\n      \u003C\u002FFieldLegend>\n\n      \u003CFieldDescription>\n        Get notified when tasks you've created have updates.\n      \u003C\u002FFieldDescription>\n\n      \u003CFieldGroup data-slot=\"checkbox-group\">\n        \u003CField\n          v-for=\"task in tasks\"\n          :key=\"task.id\"\n          orientation=\"horizontal\"\n          :data-invalid=\"!field.isValid\"\n        >\n          \u003CCheckbox\n            :id=\"`${field.path}-${task.id}`\"\n            :aria-invalid=\"!field.isValid\"\n            :model-value=\"field.model?.includes(task.id)\"\n            @blur=\"field.events.onBlur()\"\n            @update:model-value=\"(checked: boolean | 'indeterminate') => {\n              field.model = checked === true\n                ? [...(field.model ?? []), task.id]\n                : (field.model ?? []).filter(value => value !== task.id)\n\n              field.events.onChange()\n            }\"\n          \u002F>\n\n          \u003CFieldLabel\n            :for=\"`${field.path}-${task.id}`\"\n            class=\"font-normal\"\n          >\n            {{ task.label }}\n          \u003C\u002FFieldLabel>\n        \u003C\u002FField>\n      \u003C\u002FFieldGroup>\n\n      \u003CFieldError\n        v-if=\"!field.isValid\"\n        :errors=\"field.errors\"\n      \u002F>\n    \u003C\u002FFieldSet>\n  \u003C\u002FNotField>\n\u003C\u002Ftemplate> Give each checkbox a unique id by appending the item's id to field.path, and call field.events.onChange() after you update the model so validation runs against the new value.",{"id":1467,"title":402,"titles":1468,"content":1469,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#radio-group",[158],"Bind the group, not the items. Each RadioGroupItem only needs its own value. plan: z.string().min(1, 'You must select a subscription plan to continue.') \u003Ctemplate>\n  \u003CNotField\n    v-slot=\"{ field }\"\n    :field=\"form.field('plan')\"\n  >\n    \u003CFieldSet :data-invalid=\"!field.isValid\">\n      \u003CFieldLegend>Plan\u003C\u002FFieldLegend>\n\n      \u003CFieldDescription>\n        You can upgrade or downgrade your plan at any time.\n      \u003C\u002FFieldDescription>\n\n      \u003CRadioGroup\n        v-model=\"field.model\"\n        :aria-invalid=\"!field.isValid\"\n        @blur=\"field.events.onBlur\"\n        @update:model-value=\"field.events.onChange\"\n      >\n        \u003CFieldLabel\n          v-for=\"plan in plans\"\n          :key=\"plan.id\"\n          :for=\"`${field.path}-${plan.id}`\"\n        >\n          \u003CField\n            orientation=\"horizontal\"\n            :data-invalid=\"!field.isValid\"\n          >\n            \u003CFieldContent>\n              \u003CFieldTitle>{{ plan.title }}\u003C\u002FFieldTitle>\n\n              \u003CFieldDescription>\n                {{ plan.description }}\n              \u003C\u002FFieldDescription>\n            \u003C\u002FFieldContent>\n\n            \u003CRadioGroupItem\n              :id=\"`${field.path}-${plan.id}`\"\n              :value=\"plan.id\"\n              :aria-invalid=\"!field.isValid\"\n            \u002F>\n          \u003C\u002FField>\n        \u003C\u002FFieldLabel>\n      \u003C\u002FRadioGroup>\n\n      \u003CFieldError\n        v-if=\"!field.isValid\"\n        :errors=\"field.errors\"\n      \u002F>\n    \u003C\u002FFieldSet>\n  \u003C\u002FNotField>\n\u003C\u002Ftemplate>",{"id":1471,"title":1418,"titles":1472,"content":1473,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#switch",[158],"Switch follows the same pattern as Checkbox: a boolean model plus explicit change and blur handlers. twoFactor: z.boolean().refine((value) => {\n  return value === true\n}, 'It is highly recommended to enable two-factor authentication.') \u003Ctemplate>\n  \u003CNotField\n    v-slot=\"{ field }\"\n    :field=\"form.field('twoFactor')\"\n  >\n    \u003CField\n      orientation=\"horizontal\"\n      :data-invalid=\"!field.isValid\"\n    >\n      \u003CFieldContent>\n        \u003CFieldLabel :for=\"field.path\">\n          Multi-factor authentication\n        \u003C\u002FFieldLabel>\n\n        \u003CFieldDescription>\n          Enable multi-factor authentication to secure your account.\n        \u003C\u002FFieldDescription>\n\n        \u003CFieldError\n          v-if=\"!field.isValid\"\n          :errors=\"field.errors\"\n        \u002F>\n      \u003C\u002FFieldContent>\n\n      \u003CSwitch\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        :aria-invalid=\"!field.isValid\"\n        @blur=\"field.events.onBlur\"\n        @update:model-value=\"field.events.onChange\"\n      \u002F>\n    \u003C\u002FField>\n  \u003C\u002FNotField>\n\u003C\u002Ftemplate>",{"id":1475,"title":65,"titles":1476,"content":1477,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#array-fields",[158],"Use ArrayField for lists the user can grow, shrink, and reorder. Each item exposes item.field('key'), so you can wrap any of the inputs above in a NotField. emails: z\n  .array(z.object({\n    address: z.email('Enter a valid email address.'),\n  }))\n  .min(2, 'Add at least two email addresses.')\n  .max(5, 'You can add up to 5 email addresses.') \u003Ctemplate>\n  \u003CArrayField\n    v-slot=\"{ arrayField }\"\n    :array-field=\"form.arrayField('emails')\"\n  >\n    \u003CFieldSet :data-invalid=\"!arrayField.isValid\">\n      \u003CFieldLegend variant=\"label\">\n        Email Addresses\n      \u003C\u002FFieldLegend>\n\n      \u003CFieldDescription>\n        Add up to 5 email addresses where we can contact you.\n      \u003C\u002FFieldDescription>\n\n      \u003CFieldGroup class=\"gap-4\">\n        \u003CNotField\n          v-for=\"item in arrayField.items\"\n          :key=\"item.id\"\n          v-slot=\"{ field }\"\n          :field=\"item.field('address')\"\n        >\n          \u003CField\n            orientation=\"horizontal\"\n            :data-invalid=\"!field.isValid\"\n          >\n            \u003CFieldContent>\n              \u003CInputGroup>\n                \u003CInputGroupInput\n                  :id=\"field.path\"\n                  v-model=\"field.model\"\n                  v-bind=\"field.events\"\n                  :aria-invalid=\"!field.isValid\"\n                  placeholder=\"name@example.com\"\n                  type=\"email\"\n                  autocomplete=\"email\"\n                \u002F>\n\n                \u003CInputGroupAddon\n                  v-if=\"arrayField.items.length > 1\"\n                  align=\"inline-end\"\n                >\n                  \u003CInputGroupButton\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    :disabled=\"item.index === 0\"\n                    :aria-label=\"`Move email ${item.index + 1} up`\"\n                    @click=\"arrayField.move(item.index, item.index - 1)\"\n                  >\n                    \u003CChevronUp \u002F>\n                  \u003C\u002FInputGroupButton>\n\n                  \u003CInputGroupButton\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    :disabled=\"item.index === arrayField.items.length - 1\"\n                    :aria-label=\"`Move email ${item.index + 1} down`\"\n                    @click=\"arrayField.move(item.index, item.index + 1)\"\n                  >\n                    \u003CChevronDown \u002F>\n                  \u003C\u002FInputGroupButton>\n\n                  \u003CInputGroupButton\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    :aria-label=\"`Remove email ${item.index + 1}`\"\n                    @click=\"arrayField.remove(item.index)\"\n                  >\n                    \u003CX \u002F>\n                  \u003C\u002FInputGroupButton>\n                \u003C\u002FInputGroupAddon>\n              \u003C\u002FInputGroup>\n\n              \u003CFieldError\n                v-if=\"!field.isValid\"\n                :errors=\"field.errors\"\n              \u002F>\n            \u003C\u002FFieldContent>\n          \u003C\u002FField>\n        \u003C\u002FNotField>\n\n        \u003CButton\n          type=\"button\"\n          variant=\"outline\"\n          size=\"sm\"\n          :disabled=\"arrayField.items.length >= 5\"\n          @click=\"arrayField.append({ address: '' })\"\n        >\n          Add Email Address\n        \u003C\u002FButton>\n      \u003C\u002FFieldGroup>\n\n      \u003CFieldError\n        v-if=\"!arrayField.isValid\"\n        :errors=\"arrayField.errors\"\n      \u002F>\n    \u003C\u002FFieldSet>\n  \u003C\u002FArrayField>\n\u003C\u002Ftemplate> A few things to note: arrayField.items gives you each item's id and index. Use item.id as the :key, not the index, so inputs keep their state when you reorder or remove items. arrayField.append, arrayField.remove and arrayField.move change the list. Set type=\"button\" on every button that calls them so they don't submit the form. Errors for a single item come from its field. Errors for the list as a whole, such as the min and max rules, come from arrayField.errors. Mirror the schema limits in the UI, as the Add button does with arrayField.items.length >= 5, so users can't reach an invalid state by clicking.",{"id":1479,"title":1427,"titles":1480,"content":1481,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#full-example",[158],"This form combines every input above. It assumes vue, zod, @lucide\u002Fvue and the ShadcnVue components you've added to your project. \u003Cscript setup lang=\"ts\">\nimport { ChevronDown, ChevronUp, X } from '@lucide\u002Fvue'\nimport { ArrayField, Form, Field as NotField, useForm } from 'notform'\nimport { z } from 'zod'\nimport { Button } from '@\u002Fcomponents\u002Fui\u002Fbutton'\nimport { Checkbox } from '@\u002Fcomponents\u002Fui\u002Fcheckbox'\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel,\n  FieldLegend,\n  FieldSet,\n  FieldTitle,\n} from '@\u002Fcomponents\u002Fui\u002Ffield'\nimport { Input } from '@\u002Fcomponents\u002Fui\u002Finput'\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n  InputGroupText,\n  InputGroupTextarea,\n} from '@\u002Fcomponents\u002Fui\u002Finput-group'\nimport {\n  RadioGroup,\n  RadioGroupItem,\n} from '@\u002Fcomponents\u002Fui\u002Fradio-group'\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n} from '@\u002Fcomponents\u002Fui\u002Fselect'\nimport { Switch } from '@\u002Fcomponents\u002Fui\u002Fswitch'\n\nconst spokenLanguages = [\n  { label: 'English', value: 'en' },\n  { label: 'Spanish', value: 'es' },\n  { label: 'French', value: 'fr' },\n  { label: 'German', value: 'de' },\n  { label: 'Italian', value: 'it' },\n  { label: 'Chinese', value: 'zh' },\n  { label: 'Japanese', value: 'ja' },\n] as const\n\nconst plans = [\n  {\n    description: 'For everyday use with basic features.',\n    id: 'starter',\n    title: 'Starter (100K tokens\u002Fmonth)',\n  },\n  {\n    description: 'For advanced AI usage with more features.',\n    id: 'pro',\n    title: 'Pro (1M tokens\u002Fmonth)',\n  },\n  {\n    description: 'For large teams and heavy usage.',\n    id: 'enterprise',\n    title: 'Enterprise (Unlimited tokens)',\n  },\n] as const\n\nconst tasks = [\n  {\n    id: 'push',\n    label: 'Push notifications',\n  },\n  {\n    id: 'email',\n    label: 'Email notifications',\n  },\n] as const\n\nconst schema = z.object({\n  description: z\n    .string()\n    .min(20, 'Description must be at least 20 characters.')\n    .max(100, 'Description must be at most 100 characters.'),\n  emails: z\n    .array(z.object({\n      address: z.email('Enter a valid email address.'),\n    }))\n    .min(2, 'Add at least two email addresses.')\n    .max(5, 'You can add up to 5 email addresses.'),\n  language: z\n    .string()\n    .min(1, 'Please select your spoken language.')\n    .refine((value) => {\n      return value !== 'auto'\n    }, 'Auto-detection is not allowed. Please select a specific language.'),\n  plan: z.string().min(1, 'You must select a subscription plan to continue.'),\n  responses: z.literal(true, 'Notifications are mandatory'),\n  tasks: z\n    .array(z.string())\n    .min(1, 'Please select at least one notification type.')\n    .refine((value) => {\n      return value.every((taskId) => {\n        return tasks.some((task) => {\n          return task.id === taskId\n        })\n      })\n    }, 'Invalid notification type selected.'),\n  title: z\n    .string()\n    .min(5, 'Bug title must be at least 5 characters.')\n    .max(32, 'Bug title must be at most 32 characters.'),\n  twoFactor: z.boolean().refine((value) => {\n    return value === true\n  }, 'It is highly recommended to enable two-factor authentication.'),\n})\n\nconst form = useForm({\n  initialValues: {\n    description: '',\n    emails: [{ address: '' }, { address: '' }],\n    language: '',\n    plan: '',\n    responses: true,\n    tasks: [],\n    title: '',\n    twoFactor: false,\n  },\n  onSubmit(data) {\n    console.log(data)\n  },\n  schema,\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm\n    :form\n    class=\"\n      mx-auto p-8 inline-full\n\n      sm:max-inline-xl\n    \"\n  >\n    \u003CFieldGroup>\n      \u003C!-- Text input -->\n      \u003CNotField\n        v-slot=\"{ field }\"\n        :field=\"form.field('title')\"\n      >\n        \u003CField :data-invalid=\"!field.isValid\">\n          \u003CFieldLabel :for=\"field.path\">\n            Bug Title\n          \u003C\u002FFieldLabel>\n\n          \u003CInput\n            :id=\"field.path\"\n            v-model=\"field.model\"\n            v-bind=\"field.events\"\n            placeholder=\"Login button not working on mobile\"\n            autocomplete=\"off\"\n            :aria-invalid=\"!field.isValid\"\n          \u002F>\n\n          \u003CFieldError\n            v-if=\"!field.isValid\"\n            :errors=\"field.errors\"\n          \u002F>\n        \u003C\u002FField>\n      \u003C\u002FNotField>\n\n      \u003C!-- Textarea -->\n      \u003CNotField\n        v-slot=\"{ field }\"\n        :field=\"form.field('description')\"\n      >\n        \u003CField :data-invalid=\"!field.isValid\">\n          \u003CFieldLabel :for=\"field.path\">\n            Description\n          \u003C\u002FFieldLabel>\n\n          \u003CInputGroup>\n            \u003CInputGroupTextarea\n              :id=\"field.path\"\n              v-model=\"field.model\"\n              v-bind=\"field.events\"\n              placeholder=\"I'm having an issue with the login button on mobile.\"\n              :rows=\"6\"\n              class=\"resize-none min-block-24\"\n              :aria-invalid=\"!field.isValid\"\n            \u002F>\n\n            \u003CInputGroupAddon align=\"block-end\">\n              \u003CInputGroupText class=\"tabular-nums\">\n                {{ field.model?.length || 0 }}\u002F100 characters\n              \u003C\u002FInputGroupText>\n            \u003C\u002FInputGroupAddon>\n          \u003C\u002FInputGroup>\n\n          \u003CFieldDescription>\n            Include steps to reproduce, expected behavior, and what actually\n            happened.\n          \u003C\u002FFieldDescription>\n\n          \u003CFieldError\n            v-if=\"!field.isValid\"\n            :errors=\"field.errors\"\n          \u002F>\n        \u003C\u002FField>\n      \u003C\u002FNotField>\n\n      \u003C!-- Select -->\n      \u003CNotField\n        v-slot=\"{ field }\"\n        :field=\"form.field('language')\"\n      >\n        \u003CField :data-invalid=\"!field.isValid\">\n          \u003CFieldContent>\n            \u003CFieldLabel :for=\"field.path\">\n              Spoken Language\n            \u003C\u002FFieldLabel>\n\n            \u003CFieldDescription>\n              For best results, select the language you speak.\n            \u003C\u002FFieldDescription>\n\n            \u003CFieldError\n              v-if=\"!field.isValid\"\n              :errors=\"field.errors\"\n            \u002F>\n          \u003C\u002FFieldContent>\n\n          \u003C!-- eslint-disable-next-line vue-a11y\u002Fform-control-has-label -->\n          \u003CSelect\n            v-model=\"field.model\"\n            @update:model-value=\"field.events.onChange\"\n          >\n            \u003CSelectTrigger\n              :id=\"field.path\"\n              :aria-invalid=\"!field.isValid\"\n              class=\"min-inline-[120px]\"\n              @blur=\"field.events.onBlur()\"\n            >\n              \u003CSelectValue placeholder=\"Select\" \u002F>\n            \u003C\u002FSelectTrigger>\n\n            \u003CSelectContent position=\"item-aligned\">\n              \u003CSelectItem value=\"auto\">\n                Auto\n              \u003C\u002FSelectItem>\n\n              \u003CSelectSeparator \u002F>\n\n              \u003CSelectItem\n                v-for=\"language in spokenLanguages\"\n                :key=\"language.value\"\n                :value=\"language.value\"\n              >\n                {{ language.label }}\n              \u003C\u002FSelectItem>\n            \u003C\u002FSelectContent>\n          \u003C\u002FSelect>\n        \u003C\u002FField>\n      \u003C\u002FNotField>\n\n      \u003C!-- Checkbox -->\n      \u003CNotField\n        v-slot=\"{ field }\"\n        :field=\"form.field('responses')\"\n      >\n        \u003CFieldSet :data-invalid=\"!field.isValid\">\n          \u003CFieldLegend variant=\"label\">\n            Responses\n          \u003C\u002FFieldLegend>\n\n          \u003CFieldDescription>\n            Get notified for requests that take time, like research or image\n            generation.\n          \u003C\u002FFieldDescription>\n\n          \u003CFieldGroup data-slot=\"checkbox-group\">\n            \u003CField orientation=\"horizontal\">\n              \u003CCheckbox\n                :id=\"field.path\"\n                v-model=\"field.model\"\n                @blur=\"field.events.onBlur\"\n                @update:model-value=\"field.events.onChange\"\n              \u002F>\n\n              \u003CFieldLabel\n                :for=\"field.path\"\n                class=\"font-normal\"\n              >\n                Push notifications\n              \u003C\u002FFieldLabel>\n            \u003C\u002FField>\n          \u003C\u002FFieldGroup>\n\n          \u003CFieldError\n            v-if=\"!field.isValid\"\n            :errors=\"field.errors\"\n          \u002F>\n        \u003C\u002FFieldSet>\n      \u003C\u002FNotField>\n\n      \u003C!-- Checkbox group -->\n      \u003CNotField\n        v-slot=\"{ field }\"\n        :field=\"form.field('tasks')\"\n      >\n        \u003CFieldSet :data-invalid=\"!field.isValid\">\n          \u003CFieldLegend variant=\"label\">\n            Tasks\n          \u003C\u002FFieldLegend>\n\n          \u003CFieldDescription>\n            Get notified when tasks you've created have updates.\n          \u003C\u002FFieldDescription>\n\n          \u003CFieldGroup data-slot=\"checkbox-group\">\n            \u003CField\n              v-for=\"task in tasks\"\n              :key=\"task.id\"\n              orientation=\"horizontal\"\n              :data-invalid=\"!field.isValid\"\n            >\n              \u003CCheckbox\n                :id=\"`${field.path}-${task.id}`\"\n                :aria-invalid=\"!field.isValid\"\n                :model-value=\"field.model?.includes(task.id)\"\n                @blur=\"field.events.onBlur()\"\n                @update:model-value=\"(checked: boolean | 'indeterminate') => {\n                  field.model = checked === true\n                    ? [...(field.model ?? []), task.id]\n                    : (field.model ?? []).filter(value => value !== task.id)\n\n                  field.events.onChange()\n                }\"\n              \u002F>\n\n              \u003CFieldLabel\n                :for=\"`${field.path}-${task.id}`\"\n                class=\"font-normal\"\n              >\n                {{ task.label }}\n              \u003C\u002FFieldLabel>\n            \u003C\u002FField>\n          \u003C\u002FFieldGroup>\n\n          \u003CFieldError\n            v-if=\"!field.isValid\"\n            :errors=\"field.errors\"\n          \u002F>\n        \u003C\u002FFieldSet>\n      \u003C\u002FNotField>\n\n      \u003C!-- Radio group -->\n      \u003CNotField\n        v-slot=\"{ field }\"\n        :field=\"form.field('plan')\"\n      >\n        \u003CFieldSet :data-invalid=\"!field.isValid\">\n          \u003CFieldLegend>Plan\u003C\u002FFieldLegend>\n\n          \u003CFieldDescription>\n            You can upgrade or downgrade your plan at any time.\n          \u003C\u002FFieldDescription>\n\n          \u003CRadioGroup\n            v-model=\"field.model\"\n            :aria-invalid=\"!field.isValid\"\n            @blur=\"field.events.onBlur\"\n            @update:model-value=\"field.events.onChange\"\n          >\n            \u003CFieldLabel\n              v-for=\"plan in plans\"\n              :key=\"plan.id\"\n              :for=\"`${field.path}-${plan.id}`\"\n            >\n              \u003CField\n                orientation=\"horizontal\"\n                :data-invalid=\"!field.isValid\"\n              >\n                \u003CFieldContent>\n                  \u003CFieldTitle>{{ plan.title }}\u003C\u002FFieldTitle>\n\n                  \u003CFieldDescription>\n                    {{ plan.description }}\n                  \u003C\u002FFieldDescription>\n                \u003C\u002FFieldContent>\n\n                \u003CRadioGroupItem\n                  :id=\"`${field.path}-${plan.id}`\"\n                  :value=\"plan.id\"\n                  :aria-invalid=\"!field.isValid\"\n                \u002F>\n              \u003C\u002FField>\n            \u003C\u002FFieldLabel>\n          \u003C\u002FRadioGroup>\n\n          \u003CFieldError\n            v-if=\"!field.isValid\"\n            :errors=\"field.errors\"\n          \u002F>\n        \u003C\u002FFieldSet>\n      \u003C\u002FNotField>\n\n      \u003C!-- Switch -->\n      \u003CNotField\n        v-slot=\"{ field }\"\n        :field=\"form.field('twoFactor')\"\n      >\n        \u003CField\n          :data-invalid=\"!field.isValid\"\n          orientation=\"horizontal\"\n        >\n          \u003CFieldContent>\n            \u003CFieldLabel :for=\"field.path\">\n              Multi-factor authentication\n            \u003C\u002FFieldLabel>\n\n            \u003CFieldDescription>\n              Enable multi-factor authentication to secure your account.\n            \u003C\u002FFieldDescription>\n\n            \u003CFieldError\n              v-if=\"!field.isValid\"\n              :errors=\"field.errors\"\n            \u002F>\n          \u003C\u002FFieldContent>\n\n          \u003CSwitch\n            :id=\"field.path\"\n            v-model=\"field.model\"\n            :aria-invalid=\"!field.isValid\"\n            @blur=\"field.events.onBlur\"\n            @update:model-value=\"field.events.onChange\"\n          \u002F>\n        \u003C\u002FField>\n      \u003C\u002FNotField>\n\n      \u003C!-- Array field -->\n      \u003CArrayField\n        v-slot=\"{ arrayField }\"\n        :array-field=\"form.arrayField('emails')\"\n      >\n        \u003CFieldSet :data-invalid=\"!arrayField.isValid\">\n          \u003CFieldLegend variant=\"label\">\n            Email Addresses\n          \u003C\u002FFieldLegend>\n\n          \u003CFieldDescription>\n            Add up to 5 email addresses where we can contact you.\n          \u003C\u002FFieldDescription>\n\n          \u003CFieldGroup class=\"gap-4\">\n            \u003CNotField\n              v-for=\"item in arrayField.items\"\n              :key=\"item.id\"\n              v-slot=\"{ field }\"\n              :field=\"item.field('address')\"\n            >\n              \u003CField\n                orientation=\"horizontal\"\n                :data-invalid=\"!field.isValid\"\n              >\n                \u003CFieldContent>\n                  \u003CInputGroup>\n                    \u003CInputGroupInput\n                      :id=\"field.path\"\n                      v-model=\"field.model\"\n                      v-bind=\"field.events\"\n                      :aria-invalid=\"!field.isValid\"\n                      placeholder=\"name@example.com\"\n                      type=\"email\"\n                      autocomplete=\"email\"\n                    \u002F>\n\n                    \u003CInputGroupAddon\n                      v-if=\"arrayField.items.length > 1\"\n                      align=\"inline-end\"\n                    >\n                      \u003CInputGroupButton\n                        type=\"button\"\n                        variant=\"ghost\"\n                        size=\"icon-xs\"\n                        :disabled=\"item.index === 0\"\n                        :aria-label=\"`Move email ${item.index + 1} up`\"\n                        @click=\"arrayField.move(item.index, item.index - 1)\"\n                      >\n                        \u003CChevronUp \u002F>\n                      \u003C\u002FInputGroupButton>\n\n                      \u003CInputGroupButton\n                        type=\"button\"\n                        variant=\"ghost\"\n                        size=\"icon-xs\"\n                        :disabled=\"item.index === arrayField.items.length - 1\"\n                        :aria-label=\"`Move email ${item.index + 1} down`\"\n                        @click=\"arrayField.move(item.index, item.index + 1)\"\n                      >\n                        \u003CChevronDown \u002F>\n                      \u003C\u002FInputGroupButton>\n\n                      \u003CInputGroupButton\n                        type=\"button\"\n                        variant=\"ghost\"\n                        size=\"icon-xs\"\n                        :aria-label=\"`Remove email ${item.index + 1}`\"\n                        @click=\"arrayField.remove(item.index)\"\n                      >\n                        \u003CX \u002F>\n                      \u003C\u002FInputGroupButton>\n                    \u003C\u002FInputGroupAddon>\n                  \u003C\u002FInputGroup>\n\n                  \u003CFieldError\n                    v-if=\"!field.isValid\"\n                    :errors=\"field.errors\"\n                  \u002F>\n                \u003C\u002FFieldContent>\n              \u003C\u002FField>\n            \u003C\u002FNotField>\n\n            \u003CButton\n              type=\"button\"\n              variant=\"outline\"\n              size=\"sm\"\n              :disabled=\"arrayField.items.length >= 5\"\n              @click=\"arrayField.append({ address: '' })\"\n            >\n              Add Email Address\n            \u003C\u002FButton>\n          \u003C\u002FFieldGroup>\n\n          \u003CFieldError\n            v-if=\"!arrayField.isValid\"\n            :errors=\"arrayField.errors\"\n          \u002F>\n        \u003C\u002FFieldSet>\n      \u003C\u002FArrayField>\n\n      \u003CField\n        orientation=\"horizontal\"\n        class=\"*:flex-1\"\n      >\n        \u003CButton\n          type=\"reset\"\n          variant=\"outline\"\n        >\n          Reset\n        \u003C\u002FButton>\n\n        \u003CButton type=\"submit\">\n          Submit\n        \u003C\u002FButton>\n      \u003C\u002FField>\n    \u003C\u002FFieldGroup>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>",{"id":1483,"title":798,"titles":1484,"content":1485,"level":197},"\u002Fdocs\u002Fintegrations\u002Fshadcn-vue#related",[158],"Custom components Nuxt module aliases html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":168,"title":167,"titles":1487,"content":1488,"level":191},[],"Update NotForm v3 code to v4: renamed APIs, explicit field instances, moved validation config, and array fields. NotForm v4 changes the public Vue API. This guide updates forms written with the v3 names useNotForm, NotForm, NotField, NotArrayField, and NotMessage. Check your installed version first. If notform in your package.json is 3.x, the rest of these docs describe the version you are migrating to.",{"id":1490,"title":1491,"titles":1492,"content":1493,"level":197},"\u002Fdocs\u002Fmigrations\u002Fnotform-v3#_1-rename-the-apis","1. Rename the APIs",[167],"NotForm v3 NotForm v4 useNotForm useForm \u003CNotForm> \u003CForm> \u003CNotField> \u003CField> \u003CNotArrayField> \u003CArrayField> \u003CNotMessage> \u003CFieldError> The core package is still notform. For Nuxt, use @notform\u002Fnuxt. Older instructions may reference notform-nuxt, so replace that dependency. In Vue, update imports from notform. In Nuxt, the module auto-imports the v4 names. Rename everywhere with a project-wide search and replace on whole words, then fix the structural changes below.",{"id":1495,"title":1496,"titles":1497,"content":1498,"level":197},"\u002Fdocs\u002Fmigrations\u002Fnotform-v3#_2-pass-explicit-field-instances","2. Pass explicit field instances",[167],"In v3, \u003CNotForm> provided form context to descendants, so a field could be declared with only a path. In v4, Form renders the native form but provides no context. Create each field on the form instance and pass it to Field. \u003Ctemplate>\n  \u003CNotForm :form=\"form\">\n    \u003CNotField\n      v-slot=\"{ events, path }\"\n      path=\"email\"\n    >\n      \u003Cinput\n        v-bind=\"events\"\n        :id=\"path\"\n        v-model=\"form.values.email\"\n        type=\"email\"\n        :name=\"path\"\n      >\n\n      \u003CNotMessage :path=\"path\" \u002F>\n    \u003C\u002FNotField>\n  \u003C\u002FNotForm>\n\u003C\u002Ftemplate>\n\u003Ctemplate>\n  \u003CForm :form=\"form\">\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      \u003Cinput\n        v-bind=\"field.events\"\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        type=\"email\"\n        :name=\"field.path\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate> The v4 Field slot exposes one field object. Use field.model, field.events, field.path, and field.errors instead of the top-level slot props events, path, and errors. Binding field.model also replaces writing to form.values.email directly, though that still works.",{"id":1500,"title":1501,"titles":1502,"content":1503,"level":197},"\u002Fdocs\u002Fmigrations\u002Fnotform-v3#_3-move-validation-config-to-the-field","3. Move validation config to the field",[167],"v3 field components accepted options such as validateOn, validationMode, and debounce as props. In v4, pass a FieldConfig as the second argument when the field is first created. const username = form.field('username', {\n  debounce: 300,\n  validateOn: { input: true },\n  validationMode: 'lazy',\n}) The keys of validateOn are blur, change, and input. A field is cached by path, so later calls with different options do not replace the first. See Validation.",{"id":1505,"title":1506,"titles":1507,"content":1508,"level":197},"\u002Fdocs\u002Fmigrations\u002Fnotform-v3#_4-update-array-fields","4. Update array fields",[167],"v3 NotArrayField was declared by path. In v4, create an explicit array field, and use item.id as the key. \u003Ctemplate>\n  \u003CArrayField\n    v-slot=\"{ arrayField }\"\n    :array-field=\"form.arrayField('todos')\"\n  >\n    \u003Cdiv\n      v-for=\"item in arrayField.items\"\n      :key=\"item.id\"\n    >\n      \u003CField\n        v-slot=\"{ field }\"\n        :field=\"item\"\n      >\n        \u003Cinput\n          v-bind=\"field.events\"\n          :id=\"field.path\"\n          v-model=\"field.model\"\n          type=\"email\"\n          :name=\"field.path\"\n        >\n\n        \u003CFieldError :errors=\"field\" \u002F>\n      \u003C\u002FField>\n\n      \u003Cbutton\n        type=\"button\"\n        @click=\"arrayField.remove(item.index)\"\n      >\n        Remove\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n\n    \u003Cbutton\n      type=\"button\"\n      @click=\"arrayField.append('')\"\n    >\n      Add\n    \u003C\u002Fbutton>\n  \u003C\u002FArrayField>\n\u003C\u002Ftemplate> Each item provides index, path, field(), and arrayField() for nested values. Mutations are methods on arrayField. See Array fields.",{"id":1510,"title":1511,"titles":1512,"content":1513,"level":197},"\u002Fdocs\u002Fmigrations\u002Fnotform-v3#_5-check-the-rest-of-the-form","5. Check the rest of the form",[167],"Form requires a form prop and does not inject it into descendants. FieldError takes an issue array or a field-like value through errors. It no longer resolves a path from form context. onSubmit remains a useForm option, and Form handles native submit and reset. useForm returns the public API in useForm. Do not rely on instance members that are not listed there.",{"id":1515,"title":1516,"titles":1517,"content":1518,"level":197},"\u002Fdocs\u002Fmigrations\u002Fnotform-v3#migration-checklist","Migration checklist",[167],"Dependencies updated (notform, and @notform\u002Fnuxt instead of notform-nuxt). All useNotForm, NotForm, NotField, NotArrayField, and NotMessage renamed. Every Field receives :field=\"form.field(path)\" instead of a path prop. Slot props switched to field.model, field.events, field.path, and field.errors. FieldError receives :errors=\"field\". Validation options moved into form.field(path, config). Array fields use form.arrayField(path), item.id keys, and array methods. Type check and lint pass. Mistyped paths are now compile errors.",{"id":1520,"title":248,"titles":1521,"content":1522,"level":197},"\u002Fdocs\u002Fmigrations\u002Fnotform-v3#next-steps",[167],"Quickstart shows a complete v4 form. Core concepts explains the model that v4 makes explicit. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":177,"title":176,"titles":1524,"content":1525,"level":191},[],"Give AI coding assistants accurate NotForm knowledge using llms.txt, per-page Markdown, and the NotForm agent skill. AI assistants are fast at writing form code, and just as quick to invent APIs that do not exist. NotForm's API is small but specific, so an assistant working from memory will often reach for names from other libraries. NotForm gives assistants three ways to work from the real documentation. Resource What it is Best for llms.txt An index of every documentation page with a one-line description Letting an assistant discover which pages exist Page Markdown Any documentation page as clean Markdown Loading exactly the pages a task needs llms-full.txt The whole documentation in one file Broad tasks, or tools that cannot follow links The agent skill A short set of instructions that routes an agent to the right pages Making the agent do all of the above on its own",{"id":1527,"title":1528,"titles":1529,"content":1530,"level":197},"\u002Fdocs\u002Fai\u002Fworking-with-ai#quick-start","Quick start",[176],"Install the skill. Copy the notform skill into your project so your assistant loads it automatically. See Agent skill. Ask for what you need. Describe the form in plain words. The skill makes the assistant read the matching pages before it writes code. Review against the docs. Ask the assistant which pages it used. If something it wrote is not in those pages, it is a guess. No skill support? Paste https:\u002F\u002Fnotformdocs.vercel.app\u002Fllms.txt into the conversation and ask the assistant to read the pages relevant to your task before writing code.",{"id":1532,"title":1533,"titles":1534,"content":1535,"level":197},"\u002Fdocs\u002Fai\u002Fworking-with-ai#what-assistants-get-right-and-wrong","What assistants get right and wrong",[176],"Assistants do well when the docs are in front of them. These are the mistakes the documentation and skill are designed to prevent: using path props or form context from NotForm v3 instead of explicit field instances; using index instead of item.id as an array key; mutating arrays directly instead of through ArrayField methods; expecting form.setValue or field.model writes to validate; passing field configuration on every render and expecting it to apply; mixing NotForm with another form library on the same form.",{"id":1537,"title":248,"titles":1538,"content":1539,"level":197},"\u002Fdocs\u002Fai\u002Fworking-with-ai#next-steps",[176],"llms.txt and Markdown pages The NotForm agent skill",{"id":181,"title":180,"titles":1541,"content":1542,"level":191},[],"What the NotForm llms.txt, llms-full.txt, and per-page Markdown endpoints contain, and how to use each with an AI tool. llms.txt is a convention for publishing documentation in a form that language models can read. The NotForm docs publish three views. Resource URL Index https:\u002F\u002Fnotformdocs.vercel.app\u002Fllms.txt Full documentation https:\u002F\u002Fnotformdocs.vercel.app\u002Fllms-full.txt A single page https:\u002F\u002Fnotformdocs.vercel.app\u002Fdocs\u002F\u003Csection>\u002F\u003Cpage>.md",{"id":1544,"title":1545,"titles":1546,"content":1547,"level":197},"\u002Fdocs\u002Fai\u002Fllms-txt#the-index","The index",[180],"llms.txt is a short Markdown file that lists the documentation pages with a short description of each. It is the cheapest way for an assistant to learn what exists, and it is small enough to load at the start of any task. Typical use: give the URL to an assistant and ask it to pick the pages it needs. Read https:\u002F\u002Fnotformdocs.vercel.app\u002Fllms.txt, then read the pages about array fields and validation, and build a repeatable contacts form.",{"id":1549,"title":1550,"titles":1551,"content":1552,"level":197},"\u002Fdocs\u002Fai\u002Fllms-txt#read-a-single-page","Read a single page",[180],"Add .md to a documentation page's path to get that page as Markdown. Use the section and page names from the URL in your browser. https:\u002F\u002Fnotformdocs.vercel.app\u002Fdocs\u002Fcomponents\u002Ffield.md\nhttps:\u002F\u002Fnotformdocs.vercel.app\u002Fdocs\u002Fguides\u002Fvalidation.md\nhttps:\u002F\u002Fnotformdocs.vercel.app\u002Fdocs\u002Frecipes\u002Fserver-errors.md Loading focused pages keeps the assistant's context small and its answers specific. This is the preferred way to use the docs with an AI tool.",{"id":1554,"title":1555,"titles":1556,"content":1557,"level":197},"\u002Fdocs\u002Fai\u002Fllms-txt#the-full-documentation","The full documentation",[180],"llms-full.txt contains the whole documentation in a single file. Use it when: the task spans many sections, such as a migration; the tool cannot follow links or fetch several pages; or you want to load everything once and ask many questions. It is large. For a single question, the index plus one or two pages is faster and cheaper.",{"id":1559,"title":1560,"titles":1561,"content":1562,"level":197},"\u002Fdocs\u002Fai\u002Fllms-txt#which-should-i-use","Which should I use?",[180],"Situation Use Build or fix one feature Index, then the relevant pages Migrate from NotForm v3 The migration page, then the pages for each API it names Tool only accepts one URL or file llms-full.txt You already know the page The page's .md URL Repeated work in one project The agent skill",{"id":1564,"title":1565,"titles":1566,"content":1567,"level":197},"\u002Fdocs\u002Fai\u002Fllms-txt#using-the-files-with-common-tools","Using the files with common tools",[180],"Chat assistants: paste the URL into the conversation and ask the assistant to read it. Editors and coding agents: add the URL as a documentation source in the tool's settings, or reference it in your project's agent instructions. Your own scripts: fetch the .md URLs directly. They are plain Markdown.",{"id":1569,"title":1570,"titles":1571,"content":1572,"level":197},"\u002Fdocs\u002Fai\u002Fllms-txt#keep-in-mind","Keep in mind",[180],"The files describe the latest published documentation. If your project pins an older NotForm version, check that the API you rely on exists in your installed package. The index lists pages, not behavior. Always read the page before writing code against it.",{"id":185,"title":184,"titles":1574,"content":1575,"level":191},[],"Install and use the NotForm agent skill, which routes AI coding assistants to the right documentation instead of answering from memory. The NotForm agent skill is a small instruction file for AI coding assistants. It does not contain API documentation. Its job is to make the assistant look things up: identify your setup, find the relevant pages, read them, and only then write code.",{"id":1577,"title":1578,"titles":1579,"content":1580,"level":197},"\u002Fdocs\u002Fai\u002Fagent-skill#why-a-skill-instead-of-more-docs","Why a skill instead of more docs",[184],"A copy of the API inside the skill would go stale and compete with these docs. The skill instead holds the part that does not change: where to look, in what order, and the few rules that assistants break most often. The documentation stays the single source of truth.",{"id":1582,"title":1583,"titles":1584,"content":1585,"level":197},"\u002Fdocs\u002Fai\u002Fagent-skill#install","Install",[184],"The skill lives in the NotForm repository at .agents\u002Fskills\u002Fnotform\u002FSKILL.md. To use it in your project, copy the notform folder into the skills directory your tool reads. your-project\u002F\n└── .agents\u002F\n    └── skills\u002F\n        └── notform\u002F\n            └── SKILL.md Some tools use a different folder. For example, Claude Code reads project skills from .claude\u002Fskills\u002F. Check your tool's documentation for the location it expects, and place the notform folder there. Commit the folder so everyone working on the project gets the same behavior. You can also use the skills CLI to install it. It supports more than 35 agents, including Cursor, Claude Code, Codex, Windsurf, and Cline.",{"id":1587,"title":1588,"titles":1589,"content":1590,"level":197},"\u002Fdocs\u002Fai\u002Fagent-skill#use-it","Use it",[184],"The skill activates when you ask an assistant to build, debug, or migrate a NotForm form. It is also user-invocable, so in tools that support slash commands you can run it by name: \u002Fnotform Then describe the task: Build a registration form with username, email, and a repeatable list of phone numbers. Show server errors under each field.",{"id":1592,"title":1593,"titles":1594,"content":1595,"level":197},"\u002Fdocs\u002Fai\u002Fagent-skill#what-the-skill-makes-the-assistant-do","What the skill makes the assistant do",[184],"Identify your stack and version. It checks whether the project uses notform (Vue) or @notform\u002Fnuxt (Nuxt), and which version. Handle v3 first. If the project is on NotForm 3.x, it reads the migration guide before anything else. Read the index. It loads llms.txt to see which pages exist. Load only what the task needs. It maps the task to pages, for example \"validation or errors\" to the Validation and Errors guides, and fetches those as Markdown. Write from the pages. If something is not documented, it says so instead of inventing options.",{"id":1597,"title":1598,"titles":1599,"content":1600,"level":197},"\u002Fdocs\u002Fai\u002Fagent-skill#rules-the-skill-reinforces","Rules the skill reinforces",[184],"These are the mistakes assistants make most, and each is explained in the docs. Create fields with form.field(path) and form.arrayField(path), and pass the instance to Field or ArrayField. See Field. Bind field.model and spread field.events, with v-model first. See Binding inputs. Schema input is what users edit. Schema output is what onSubmit receives. See Schema input and output. Give initialValues for every bound field. Use item.id as an array key, and change arrays only through their methods. See Array fields. Writes through field.model and form.setValue do not validate. Field configuration is read once, when the field is created. See Validation. NotForm should be the only owner of a form's state. Do not combine it with another form library on the same form.",{"id":1602,"title":1603,"titles":1604,"content":1605,"level":197},"\u002Fdocs\u002Fai\u002Fagent-skill#example-prompts","Example prompts",[184],"Goal Prompt New form \"Create a checkout form with NotForm and Zod. Validate on blur and show errors with accessible markup.\" Integration \"Wire this form to our ShadcnVue inputs. Handle the Field name conflict.\" Debugging \"My error message never clears after I fix the input. Find out why using the NotForm docs.\" Migration \"Migrate this component from NotForm v3 to v4.\" Review \"Review this form against the NotForm docs and list anything that is guessed.\"",{"id":1607,"title":1608,"titles":1609,"content":1610,"level":197},"\u002Fdocs\u002Fai\u002Fagent-skill#limits","Limits",[184],"The skill needs the assistant to be able to fetch URLs. Without network access, it asks you for the relevant pages instead of guessing. The skill is a router. If the docs and your installed package disagree, trust the package's public exports and tell the maintainers.",{"id":1612,"title":798,"titles":1613,"content":1614,"level":197},"\u002Fdocs\u002Fai\u002Fagent-skill#related",[184],"llms.txt and Markdown pages Working with AI",21,{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":1619},0,24,"\u003Cg fill=\"none\">\u003C!--field 1-->\n  \u003Crect x=\"0.5\" y=\"0.5\" width=\"23\" height=\"3\" rx=\"0.3\" stroke=\"currentColor\" stroke-opacity=\"0.5\" stroke-width=\"0.3\"\u002F>\n\u003C!--field 2 — active-->\n  \u003Crect x=\"0.5\" y=\"4.3\" width=\"23\" height=\"3\" rx=\"0.3\" fill=\"currentColor\" fill-opacity=\"0.12\" stroke=\"currentColor\" stroke-opacity=\"0.75\" stroke-width=\"0.3\"\u002F>\n  \u003Crect x=\"1\" y=\"5.7\" width=\"8\" height=\"0.3\" rx=\"0.2\" fill=\"currentColor\" fill-opacity=\"0.5\"\u002F>\n\u003C!--field 3-->\n  \u003Crect x=\"0.5\" y=\"8.6\" width=\"23\" height=\"3\" rx=\"0.3\" stroke=\"currentColor\" stroke-opacity=\"0.5\" stroke-width=\"0.3\"\u002F>\n\u003C!--button-->\n  \u003Crect x=\"0.5\" y=\"12.9\" width=\"9.9\" height=\"3\" rx=\"0.3\" fill=\"currentColor\" fill-opacity=\"0.2\" stroke=\"currentColor\" stroke-width=\"0.2\"\u002F>\n  \u003Crect x=\"2.2\" y=\"14.3\" width=\"5.5\" height=\"0.4\" rx=\"0.2\" fill=\"currentColor\" fill-opacity=\"0.85\"\u002F>\u003C\u002Fg>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":1621},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0m18 11l-6-6\"\u002F>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":1623},"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":1625},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z\"\u002F>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":1627},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 12a4 4 0 1 0 8 0a4 4 0 1 0-8 0m-5 0h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7l-.7.7m0 11.4l.7.7m-12.1-.7l-.7.7\"\u002F>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":1629},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 8h16M4 16h16\"\u002F>",{"id":1631,"title":16,"body":1632,"description":2826,"extension":2827,"meta":2828,"navigation":1754,"path":17,"seo":2829,"stem":18,"__hash__":2830},"docs\u002Fdocs\u002F1.getting-started\u002F1.introduction.md",{"type":1633,"value":1634,"toc":2819},"minimark",[1635,1639,2573,2577,2648,2651,2686,2689,2734,2743,2746,2773,2776,2810,2815],[1636,1637,1638],"p",{},"NotForm manages form state for Vue 3 and Nuxt. You describe your data with a schema, NotForm turns it into a typed reactive form, and you keep full control of the markup and styling.",[1640,1641,1642],"code-collapse",{},[1643,1644,1648],"pre",{"className":1645,"code":1646,"language":1647,"meta":654,"style":654},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Cscript setup lang=\"ts\">\nimport { Field, FieldError, Form, useForm } from 'notform'\nimport { z } from 'zod'\n\nconst form = useForm({\n  initialValues: { email: '', message: '' },\n  async onSubmit(data) {\n    await fetch('\u002Fapi\u002Fcontact', {\n      body: JSON.stringify(data),\n      headers: { 'content-type': 'application\u002Fjson' },\n      method: 'POST',\n    })\n  },\n  schema: z.object({\n    email: z.email('Enter a valid email address.'),\n    message: z.string().min(10, 'Write at least 10 characters.'),\n  }),\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CForm :form=\"form\">\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('email')\"\n    >\n      \u003Clabel :for=\"field.path\">Email\u003C\u002Flabel>\n\n      \u003Cinput\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n        type=\"email\"\n      >\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003CField\n      v-slot=\"{ field }\"\n      :field=\"form.field('message')\"\n    >\n      \u003Clabel :for=\"field.path\">Message\u003C\u002Flabel>\n\n      \u003Ctextarea\n        :id=\"field.path\"\n        v-model=\"field.model\"\n        v-bind=\"field.events\"\n      \u002F>\n\n      \u003CFieldError :errors=\"field\" \u002F>\n    \u003C\u002FField>\n\n    \u003Cbutton\n      type=\"submit\"\n      :disabled=\"form.isSubmitting\"\n    >\n      Send\n    \u003C\u002Fbutton>\n  \u003C\u002FForm>\n\u003C\u002Ftemplate>\n","vue",[1649,1650,1651,1685,1729,1749,1756,1776,1807,1828,1851,1877,1905,1922,1931,1937,1956,1984,2026,2036,2044,2054,2059,2068,2092,2101,2121,2151,2157,2190,2195,2203,2217,2232,2247,2261,2267,2272,2295,2305,2310,2317,2334,2364,2369,2397,2402,2410,2423,2436,2449,2455,2460,2481,2490,2495,2503,2518,2533,2538,2544,2554,2564],"code",{"__ignoreMap":654},[1652,1653,1655,1659,1663,1667,1670,1673,1676,1680,1682],"span",{"class":1654,"line":191},"line",[1652,1656,1658],{"class":1657},"sMK4o","\u003C",[1652,1660,1662],{"class":1661},"swJcz","script",[1652,1664,1666],{"class":1665},"spNyl"," setup",[1652,1668,1669],{"class":1665}," lang",[1652,1671,1672],{"class":1657},"=",[1652,1674,1675],{"class":1657},"\"",[1652,1677,1679],{"class":1678},"sfazB","ts",[1652,1681,1675],{"class":1657},[1652,1683,1684],{"class":1657},">\n",[1652,1686,1687,1691,1694,1698,1701,1704,1706,1709,1711,1714,1717,1720,1723,1726],{"class":1654,"line":197},[1652,1688,1690],{"class":1689},"s7zQu","import",[1652,1692,1693],{"class":1657}," {",[1652,1695,1697],{"class":1696},"sTEyZ"," Field",[1652,1699,1700],{"class":1657},",",[1652,1702,1703],{"class":1696}," FieldError",[1652,1705,1700],{"class":1657},[1652,1707,1708],{"class":1696}," Form",[1652,1710,1700],{"class":1657},[1652,1712,1713],{"class":1696}," useForm",[1652,1715,1716],{"class":1657}," }",[1652,1718,1719],{"class":1689}," from",[1652,1721,1722],{"class":1657}," '",[1652,1724,1725],{"class":1678},"notform",[1652,1727,1728],{"class":1657},"'\n",[1652,1730,1731,1733,1735,1738,1740,1742,1744,1747],{"class":1654,"line":613},[1652,1732,1690],{"class":1689},[1652,1734,1693],{"class":1657},[1652,1736,1737],{"class":1696}," z",[1652,1739,1716],{"class":1657},[1652,1741,1719],{"class":1689},[1652,1743,1722],{"class":1657},[1652,1745,1746],{"class":1678},"zod",[1652,1748,1728],{"class":1657},[1652,1750,1752],{"class":1654,"line":1751},4,[1652,1753,1755],{"emptyLinePlaceholder":1754},true,"\n",[1652,1757,1759,1762,1765,1767,1770,1773],{"class":1654,"line":1758},5,[1652,1760,1761],{"class":1665},"const",[1652,1763,1764],{"class":1696}," form ",[1652,1766,1672],{"class":1657},[1652,1768,1713],{"class":1769},"s2Zo4",[1652,1771,1772],{"class":1696},"(",[1652,1774,1775],{"class":1657},"{\n",[1652,1777,1779,1782,1785,1787,1790,1792,1795,1797,1800,1802,1804],{"class":1654,"line":1778},6,[1652,1780,1781],{"class":1661},"  initialValues",[1652,1783,1784],{"class":1657},":",[1652,1786,1693],{"class":1657},[1652,1788,1789],{"class":1661}," email",[1652,1791,1784],{"class":1657},[1652,1793,1794],{"class":1657}," ''",[1652,1796,1700],{"class":1657},[1652,1798,1799],{"class":1661}," message",[1652,1801,1784],{"class":1657},[1652,1803,1794],{"class":1657},[1652,1805,1806],{"class":1657}," },\n",[1652,1808,1810,1813,1816,1818,1822,1825],{"class":1654,"line":1809},7,[1652,1811,1812],{"class":1665},"  async",[1652,1814,1815],{"class":1661}," onSubmit",[1652,1817,1772],{"class":1657},[1652,1819,1821],{"class":1820},"sHdIc","data",[1652,1823,1824],{"class":1657},")",[1652,1826,1827],{"class":1657}," {\n",[1652,1829,1831,1834,1837,1839,1842,1845,1847,1849],{"class":1654,"line":1830},8,[1652,1832,1833],{"class":1689},"    await",[1652,1835,1836],{"class":1769}," fetch",[1652,1838,1772],{"class":1661},[1652,1840,1841],{"class":1657},"'",[1652,1843,1844],{"class":1678},"\u002Fapi\u002Fcontact",[1652,1846,1841],{"class":1657},[1652,1848,1700],{"class":1657},[1652,1850,1827],{"class":1657},[1652,1852,1854,1857,1859,1862,1865,1868,1870,1872,1874],{"class":1654,"line":1853},9,[1652,1855,1856],{"class":1661},"      body",[1652,1858,1784],{"class":1657},[1652,1860,1861],{"class":1696}," JSON",[1652,1863,1864],{"class":1657},".",[1652,1866,1867],{"class":1769},"stringify",[1652,1869,1772],{"class":1661},[1652,1871,1821],{"class":1696},[1652,1873,1824],{"class":1661},[1652,1875,1876],{"class":1657},",\n",[1652,1878,1880,1883,1885,1887,1889,1892,1894,1896,1898,1901,1903],{"class":1654,"line":1879},10,[1652,1881,1882],{"class":1661},"      headers",[1652,1884,1784],{"class":1657},[1652,1886,1693],{"class":1657},[1652,1888,1722],{"class":1657},[1652,1890,1891],{"class":1661},"content-type",[1652,1893,1841],{"class":1657},[1652,1895,1784],{"class":1657},[1652,1897,1722],{"class":1657},[1652,1899,1900],{"class":1678},"application\u002Fjson",[1652,1902,1841],{"class":1657},[1652,1904,1806],{"class":1657},[1652,1906,1908,1911,1913,1915,1918,1920],{"class":1654,"line":1907},11,[1652,1909,1910],{"class":1661},"      method",[1652,1912,1784],{"class":1657},[1652,1914,1722],{"class":1657},[1652,1916,1917],{"class":1678},"POST",[1652,1919,1841],{"class":1657},[1652,1921,1876],{"class":1657},[1652,1923,1925,1928],{"class":1654,"line":1924},12,[1652,1926,1927],{"class":1657},"    }",[1652,1929,1930],{"class":1661},")\n",[1652,1932,1934],{"class":1654,"line":1933},13,[1652,1935,1936],{"class":1657},"  },\n",[1652,1938,1940,1943,1945,1947,1949,1952,1954],{"class":1654,"line":1939},14,[1652,1941,1942],{"class":1661},"  schema",[1652,1944,1784],{"class":1657},[1652,1946,1737],{"class":1696},[1652,1948,1864],{"class":1657},[1652,1950,1951],{"class":1769},"object",[1652,1953,1772],{"class":1696},[1652,1955,1775],{"class":1657},[1652,1957,1959,1962,1964,1966,1968,1971,1973,1975,1978,1980,1982],{"class":1654,"line":1958},15,[1652,1960,1961],{"class":1661},"    email",[1652,1963,1784],{"class":1657},[1652,1965,1737],{"class":1696},[1652,1967,1864],{"class":1657},[1652,1969,1970],{"class":1769},"email",[1652,1972,1772],{"class":1696},[1652,1974,1841],{"class":1657},[1652,1976,1977],{"class":1678},"Enter a valid email address.",[1652,1979,1841],{"class":1657},[1652,1981,1824],{"class":1696},[1652,1983,1876],{"class":1657},[1652,1985,1987,1990,1992,1994,1996,1999,2002,2004,2007,2009,2013,2015,2017,2020,2022,2024],{"class":1654,"line":1986},16,[1652,1988,1989],{"class":1661},"    message",[1652,1991,1784],{"class":1657},[1652,1993,1737],{"class":1696},[1652,1995,1864],{"class":1657},[1652,1997,1998],{"class":1769},"string",[1652,2000,2001],{"class":1696},"()",[1652,2003,1864],{"class":1657},[1652,2005,2006],{"class":1769},"min",[1652,2008,1772],{"class":1696},[1652,2010,2012],{"class":2011},"sbssI","10",[1652,2014,1700],{"class":1657},[1652,2016,1722],{"class":1657},[1652,2018,2019],{"class":1678},"Write at least 10 characters.",[1652,2021,1841],{"class":1657},[1652,2023,1824],{"class":1696},[1652,2025,1876],{"class":1657},[1652,2027,2029,2032,2034],{"class":1654,"line":2028},17,[1652,2030,2031],{"class":1657},"  }",[1652,2033,1824],{"class":1696},[1652,2035,1876],{"class":1657},[1652,2037,2039,2042],{"class":1654,"line":2038},18,[1652,2040,2041],{"class":1657},"}",[1652,2043,1930],{"class":1696},[1652,2045,2047,2050,2052],{"class":1654,"line":2046},19,[1652,2048,2049],{"class":1657},"\u003C\u002F",[1652,2051,1662],{"class":1661},[1652,2053,1684],{"class":1657},[1652,2055,2057],{"class":1654,"line":2056},20,[1652,2058,1755],{"emptyLinePlaceholder":1754},[1652,2060,2061,2063,2066],{"class":1654,"line":1615},[1652,2062,1658],{"class":1657},[1652,2064,2065],{"class":1661},"template",[1652,2067,1684],{"class":1657},[1652,2069,2071,2074,2076,2079,2082,2084,2086,2088,2090],{"class":1654,"line":2070},22,[1652,2072,2073],{"class":1657},"  \u003C",[1652,2075,78],{"class":1661},[1652,2077,2078],{"class":1657}," :",[1652,2080,2081],{"class":1665},"form",[1652,2083,1672],{"class":1657},[1652,2085,1675],{"class":1657},[1652,2087,2081],{"class":1696},[1652,2089,1675],{"class":1657},[1652,2091,1684],{"class":1657},[1652,2093,2095,2098],{"class":1654,"line":2094},23,[1652,2096,2097],{"class":1657},"    \u003C",[1652,2099,2100],{"class":1661},"Field\n",[1652,2102,2103,2106,2108,2110,2113,2116,2118],{"class":1654,"line":1618},[1652,2104,2105],{"class":1665},"      v-slot",[1652,2107,1672],{"class":1657},[1652,2109,1675],{"class":1657},[1652,2111,2112],{"class":1657},"{ ",[1652,2114,2115],{"class":1696},"field",[1652,2117,1716],{"class":1657},[1652,2119,2120],{"class":1657},"\"\n",[1652,2122,2124,2127,2129,2131,2133,2135,2137,2139,2141,2143,2145,2147,2149],{"class":1654,"line":2123},25,[1652,2125,2126],{"class":1657},"      :",[1652,2128,2115],{"class":1665},[1652,2130,1672],{"class":1657},[1652,2132,1675],{"class":1657},[1652,2134,2081],{"class":1696},[1652,2136,1864],{"class":1657},[1652,2138,2115],{"class":1769},[1652,2140,1772],{"class":1657},[1652,2142,1841],{"class":1657},[1652,2144,1970],{"class":1678},[1652,2146,1841],{"class":1657},[1652,2148,1824],{"class":1657},[1652,2150,2120],{"class":1657},[1652,2152,2154],{"class":1654,"line":2153},26,[1652,2155,2156],{"class":1657},"    >\n",[1652,2158,2160,2163,2166,2169,2171,2173,2176,2178,2181,2184,2186,2188],{"class":1654,"line":2159},27,[1652,2161,2162],{"class":1657},"      \u003C",[1652,2164,2165],{"class":1661},"label",[1652,2167,2168],{"class":1665}," :for",[1652,2170,1672],{"class":1657},[1652,2172,1675],{"class":1657},[1652,2174,2175],{"class":1678},"field.path",[1652,2177,1675],{"class":1657},[1652,2179,2180],{"class":1657},">",[1652,2182,2183],{"class":1696},"Email",[1652,2185,2049],{"class":1657},[1652,2187,2165],{"class":1661},[1652,2189,1684],{"class":1657},[1652,2191,2193],{"class":1654,"line":2192},28,[1652,2194,1755],{"emptyLinePlaceholder":1754},[1652,2196,2198,2200],{"class":1654,"line":2197},29,[1652,2199,2162],{"class":1657},[1652,2201,2202],{"class":1661},"input\n",[1652,2204,2206,2209,2211,2213,2215],{"class":1654,"line":2205},30,[1652,2207,2208],{"class":1665},"        :id",[1652,2210,1672],{"class":1657},[1652,2212,1675],{"class":1657},[1652,2214,2175],{"class":1678},[1652,2216,2120],{"class":1657},[1652,2218,2220,2223,2225,2227,2230],{"class":1654,"line":2219},31,[1652,2221,2222],{"class":1665},"        v-model",[1652,2224,1672],{"class":1657},[1652,2226,1675],{"class":1657},[1652,2228,2229],{"class":1678},"field.model",[1652,2231,2120],{"class":1657},[1652,2233,2235,2238,2240,2242,2245],{"class":1654,"line":2234},32,[1652,2236,2237],{"class":1665},"        v-bind",[1652,2239,1672],{"class":1657},[1652,2241,1675],{"class":1657},[1652,2243,2244],{"class":1678},"field.events",[1652,2246,2120],{"class":1657},[1652,2248,2250,2253,2255,2257,2259],{"class":1654,"line":2249},33,[1652,2251,2252],{"class":1665},"        type",[1652,2254,1672],{"class":1657},[1652,2256,1675],{"class":1657},[1652,2258,1970],{"class":1678},[1652,2260,2120],{"class":1657},[1652,2262,2264],{"class":1654,"line":2263},34,[1652,2265,2266],{"class":1657},"      >\n",[1652,2268,2270],{"class":1654,"line":2269},35,[1652,2271,1755],{"emptyLinePlaceholder":1754},[1652,2273,2275,2277,2279,2281,2284,2286,2288,2290,2292],{"class":1654,"line":2274},36,[1652,2276,2162],{"class":1657},[1652,2278,90],{"class":1661},[1652,2280,2078],{"class":1657},[1652,2282,2283],{"class":1665},"errors",[1652,2285,1672],{"class":1657},[1652,2287,1675],{"class":1657},[1652,2289,2115],{"class":1696},[1652,2291,1675],{"class":1657},[1652,2293,2294],{"class":1657}," \u002F>\n",[1652,2296,2298,2301,2303],{"class":1654,"line":2297},37,[1652,2299,2300],{"class":1657},"    \u003C\u002F",[1652,2302,82],{"class":1661},[1652,2304,1684],{"class":1657},[1652,2306,2308],{"class":1654,"line":2307},38,[1652,2309,1755],{"emptyLinePlaceholder":1754},[1652,2311,2313,2315],{"class":1654,"line":2312},39,[1652,2314,2097],{"class":1657},[1652,2316,2100],{"class":1661},[1652,2318,2320,2322,2324,2326,2328,2330,2332],{"class":1654,"line":2319},40,[1652,2321,2105],{"class":1665},[1652,2323,1672],{"class":1657},[1652,2325,1675],{"class":1657},[1652,2327,2112],{"class":1657},[1652,2329,2115],{"class":1696},[1652,2331,1716],{"class":1657},[1652,2333,2120],{"class":1657},[1652,2335,2337,2339,2341,2343,2345,2347,2349,2351,2353,2355,2358,2360,2362],{"class":1654,"line":2336},41,[1652,2338,2126],{"class":1657},[1652,2340,2115],{"class":1665},[1652,2342,1672],{"class":1657},[1652,2344,1675],{"class":1657},[1652,2346,2081],{"class":1696},[1652,2348,1864],{"class":1657},[1652,2350,2115],{"class":1769},[1652,2352,1772],{"class":1657},[1652,2354,1841],{"class":1657},[1652,2356,2357],{"class":1678},"message",[1652,2359,1841],{"class":1657},[1652,2361,1824],{"class":1657},[1652,2363,2120],{"class":1657},[1652,2365,2367],{"class":1654,"line":2366},42,[1652,2368,2156],{"class":1657},[1652,2370,2372,2374,2376,2378,2380,2382,2384,2386,2388,2391,2393,2395],{"class":1654,"line":2371},43,[1652,2373,2162],{"class":1657},[1652,2375,2165],{"class":1661},[1652,2377,2168],{"class":1665},[1652,2379,1672],{"class":1657},[1652,2381,1675],{"class":1657},[1652,2383,2175],{"class":1678},[1652,2385,1675],{"class":1657},[1652,2387,2180],{"class":1657},[1652,2389,2390],{"class":1696},"Message",[1652,2392,2049],{"class":1657},[1652,2394,2165],{"class":1661},[1652,2396,1684],{"class":1657},[1652,2398,2400],{"class":1654,"line":2399},44,[1652,2401,1755],{"emptyLinePlaceholder":1754},[1652,2403,2405,2407],{"class":1654,"line":2404},45,[1652,2406,2162],{"class":1657},[1652,2408,2409],{"class":1661},"textarea\n",[1652,2411,2413,2415,2417,2419,2421],{"class":1654,"line":2412},46,[1652,2414,2208],{"class":1665},[1652,2416,1672],{"class":1657},[1652,2418,1675],{"class":1657},[1652,2420,2175],{"class":1678},[1652,2422,2120],{"class":1657},[1652,2424,2426,2428,2430,2432,2434],{"class":1654,"line":2425},47,[1652,2427,2222],{"class":1665},[1652,2429,1672],{"class":1657},[1652,2431,1675],{"class":1657},[1652,2433,2229],{"class":1678},[1652,2435,2120],{"class":1657},[1652,2437,2439,2441,2443,2445,2447],{"class":1654,"line":2438},48,[1652,2440,2237],{"class":1665},[1652,2442,1672],{"class":1657},[1652,2444,1675],{"class":1657},[1652,2446,2244],{"class":1678},[1652,2448,2120],{"class":1657},[1652,2450,2452],{"class":1654,"line":2451},49,[1652,2453,2454],{"class":1657},"      \u002F>\n",[1652,2456,2458],{"class":1654,"line":2457},50,[1652,2459,1755],{"emptyLinePlaceholder":1754},[1652,2461,2463,2465,2467,2469,2471,2473,2475,2477,2479],{"class":1654,"line":2462},51,[1652,2464,2162],{"class":1657},[1652,2466,90],{"class":1661},[1652,2468,2078],{"class":1657},[1652,2470,2283],{"class":1665},[1652,2472,1672],{"class":1657},[1652,2474,1675],{"class":1657},[1652,2476,2115],{"class":1696},[1652,2478,1675],{"class":1657},[1652,2480,2294],{"class":1657},[1652,2482,2484,2486,2488],{"class":1654,"line":2483},52,[1652,2485,2300],{"class":1657},[1652,2487,82],{"class":1661},[1652,2489,1684],{"class":1657},[1652,2491,2493],{"class":1654,"line":2492},53,[1652,2494,1755],{"emptyLinePlaceholder":1754},[1652,2496,2498,2500],{"class":1654,"line":2497},54,[1652,2499,2097],{"class":1657},[1652,2501,2502],{"class":1661},"button\n",[1652,2504,2506,2509,2511,2513,2516],{"class":1654,"line":2505},55,[1652,2507,2508],{"class":1665},"      type",[1652,2510,1672],{"class":1657},[1652,2512,1675],{"class":1657},[1652,2514,2515],{"class":1678},"submit",[1652,2517,2120],{"class":1657},[1652,2519,2521,2524,2526,2528,2531],{"class":1654,"line":2520},56,[1652,2522,2523],{"class":1665},"      :disabled",[1652,2525,1672],{"class":1657},[1652,2527,1675],{"class":1657},[1652,2529,2530],{"class":1678},"form.isSubmitting",[1652,2532,2120],{"class":1657},[1652,2534,2536],{"class":1654,"line":2535},57,[1652,2537,2156],{"class":1657},[1652,2539,2541],{"class":1654,"line":2540},58,[1652,2542,2543],{"class":1696},"      Send\n",[1652,2545,2547,2549,2552],{"class":1654,"line":2546},59,[1652,2548,2300],{"class":1657},[1652,2550,2551],{"class":1661},"button",[1652,2553,1684],{"class":1657},[1652,2555,2557,2560,2562],{"class":1654,"line":2556},60,[1652,2558,2559],{"class":1657},"  \u003C\u002F",[1652,2561,78],{"class":1661},[1652,2563,1684],{"class":1657},[1652,2565,2567,2569,2571],{"class":1654,"line":2566},61,[1652,2568,2049],{"class":1657},[1652,2570,2065],{"class":1661},[1652,2572,1684],{"class":1657},[2574,2575,194],"h2",{"id":2576},"what-you-get",[2578,2579,2580,2596,2602,2612,2618,2634],"ul",{},[2581,2582,2583,2587,2588,2591,2592,2595],"li",{},[2584,2585,2586],"strong",{},"Types from your schema."," Field paths, field values, ",[1649,2589,2590],{},"form.values",", and the data passed to ",[1649,2593,2594],{},"onSubmit"," are all inferred. A mistyped path is a compile error.",[2581,2597,2598,2601],{},[2584,2599,2600],{},"Validation you can tune."," Choose, per field, whether blur, change, or input triggers validation, whether errors clear eagerly, and how long to debounce.",[2581,2603,2604,2607,2608,2611],{},[2584,2605,2606],{},"Stable fields."," ",[1649,2609,2610],{},"form.field(path)"," always returns the same instance for the same path, so touched state and pending validation survive re-renders.",[2581,2613,2614,2617],{},[2584,2615,2616],{},"Repeatable sections."," Array fields give every item a stable identity and keep errors attached to the right item when you add, remove, move, or swap.",[2581,2619,2620,2607,2623,2626,2627,2626,2630,2633],{},[2584,2621,2622],{},"Plain, reactive state.",[1649,2624,2625],{},"isDirty",", ",[1649,2628,2629],{},"isSubmitting",[1649,2631,2632],{},"isValidating",", and the rest are ordinary reactive properties. There is nothing to subscribe to.",[2581,2635,2636,2639,2640,1864],{},[2584,2637,2638],{},"Any validator."," Zod, Valibot, ArkType, or any other library that implements ",[2641,2642,2647],"a",{"href":2643,"external":654,"rel":2644,"target":2646},"https:\u002F\u002Fstandardschema.dev",[2645],"nofollow","_blank","Standard Schema",[2574,2649,200],{"id":2650},"what-notform-leaves-to-you",[2578,2652,2653,2663,2669,2677],{},[2581,2654,2655,2658,2659,2662],{},[2584,2656,2657],{},"UI."," NotForm renders a native ",[1649,2660,2661],{},"\u003Cform>"," and nothing else. Inputs, labels, layout, and styling are yours, or your component library's.",[2581,2664,2665,2668],{},[2584,2666,2667],{},"Validation rules."," Rules are written in your schema, not in NotForm.",[2581,2670,2671,2607,2674,2676],{},[2584,2672,2673],{},"Networking.",[1649,2675,2594],{}," receives validated data. How you send it is up to you.",[2581,2678,2679,2682,2683,2685],{},[2584,2680,2681],{},"Global state."," Every ",[1649,2684,99],{}," call creates an independent form. There is no store to configure.",[2574,2687,205],{"id":2688},"packages",[2690,2691,2692,2705],"table",{},[2693,2694,2695],"thead",{},[2696,2697,2698,2702],"tr",{},[2699,2700,2701],"th",{},"Package",[2699,2703,2704],{},"Use it for",[2706,2707,2708,2721],"tbody",{},[2696,2709,2710,2715],{},[2711,2712,2713],"td",{},[1649,2714,1725],{},[2711,2716,2717,2718,2720],{},"Vue 3 and Vite applications. Import components and ",[1649,2719,99],{}," explicitly.",[2696,2722,2723,2728],{},[2711,2724,2725],{},[1649,2726,2727],{},"@notform\u002Fnuxt",[2711,2729,2730,2731,2733],{},"Nuxt 4 applications. Registers the same components and ",[1649,2732,99],{}," as auto-imports.",[2735,2736,2737],"note",{},[1636,2738,2739,2740,1864],{},"These docs describe NotForm v4. If your project uses NotForm 3.x, start with the ",[2641,2741,2742],{"href":168},"v3 migration guide",[2574,2744,210],{"id":2745},"how-this-docs-is-organized",[2747,2748,2749,2755,2760,2765],"card-group",{},[2750,2751,2752],"app-card",{"title":10,"to":21},[1636,2753,2754],{},"Install NotForm, build your first form, and learn the mental model.",[2750,2756,2757],{"title":36,"to":42},[1636,2758,2759],{},"Task-oriented explanations of binding inputs, validation, errors, submission, state, and arrays.",[2750,2761,2762],{"title":120,"to":126},[1636,2763,2764],{},"Complete solutions to common problems such as server errors, async checks, and multi-step forms.",[2750,2766,2767],{"title":103,"to":79},[1636,2768,2769,2770,2772],{},"Exact contracts for components, ",[1649,2771,99],{},", and every public type.",[2574,2774,215],{"id":2775},"suggested-reading-order",[2777,2778,2779,2783,2787,2791,2798],"ol",{},[2581,2780,2781],{},[2641,2782,20],{"href":21},[2581,2784,2785],{},[2641,2786,24],{"href":25},[2581,2788,2789],{},[2641,2790,32],{"href":33},[2581,2792,2793,2795,2796],{},[2641,2794,41],{"href":42}," and ",[2641,2797,49],{"href":50},[2581,2799,2800,2801,2626,2803,2806,2807],{},"Whatever your form needs next: ",[2641,2802,2283],{"href":54},[2641,2804,2805],{"href":66},"array fields",", or a ",[2641,2808,2809],{"href":126},"recipe",[1636,2811,2812,2813,1864],{},"Building with an AI coding assistant? See ",[2641,2814,176],{"href":177},[2816,2817,2818],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":654,"searchDepth":191,"depth":197,"links":2820},[2821,2822,2823,2824,2825],{"id":2576,"depth":197,"text":194},{"id":2650,"depth":197,"text":200},{"id":2688,"depth":197,"text":205},{"id":2745,"depth":197,"text":210},{"id":2775,"depth":197,"text":215},"NotForm is a headless, schema-driven form library for Vue 3 and Nuxt. See what it does, what it leaves to you, and where to start.","md",{},{"title":16,"description":2826},"kBBpmAzWge9ICu7ggIptARDAczDBWWXIlQ6MLrpidhY",[2832,2833],null,{"title":20,"path":21,"stem":22,"description":2834,"children":-1},"Install NotForm and a schema validator in a Vue 3, Vite, or Nuxt application.",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":2836},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-6 6l6-6m-6-6l6 6\"\u002F>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":2838,"hidden":1754},"\u003Cpath d=\"M5 5v14a2 2 0 0 0 2.75 1.84L20 13.74a2 2 0 0 0 0-3.5L7.75 3.14A2 2 0 0 0 5 4.89\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u002F>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":2840},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M20.931 13.111a9 9 0 1 0-9.453 7.874M20 21l2-2l-2-2m-3 0l-2 2l2 2\"\u002F>\u003Cpath d=\"M12 7v5l2 2\"\u002F>\u003C\u002Fg>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":2842},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572\"\u002F>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":2844},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 6H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6m-7 1l9-9m-5 0h5v5\"\u002F>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":2846},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z\"\u002F>\u003Cpath d=\"M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1\"\u002F>\u003C\u002Fg>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":2848},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m6 9l6 6l6-6\"\u002F>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":2850},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M12 9h.01M11 12h1v4h1\"\u002F>\u003Cpath d=\"M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9s-9-1.8-9-9s1.8-9 9-9\"\u002F>\u003C\u002Fg>",{"left":1617,"top":1617,"width":1618,"height":1618,"rotate":1617,"vFlip":11,"hFlip":11,"body":2852},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 9h14M5 15h14M11 4L7 20M17 4l-4 16\"\u002F>",1794299735823]