[{"data":1,"prerenderedAt":700},["ShallowReactive",2],{"content-\u002Fdocs\u002Frecipes\u002Ffield-level-validation":3},{"id":4,"title":5,"body":6,"description":692,"extension":693,"meta":694,"navigation":695,"path":696,"seo":697,"stem":698,"__hash__":699},"docs\u002Fdocs\u002Frecipes\u002Ffield-level-validation.md","Live field validation",{"type":7,"value":8,"toc":677},"minimark",[9,13,30,50,55,58,87,109,113,176,179,259,277,281,309,313,324,403,409,413,419,492,495,499,506,512,519,537,571,577,583,604,612,616,629,646,650,660,664,673],[10,11,5],"h1",{"id":12},"live-field-validation",[14,15,16,17,21,22,25,26,29],"p",{},"Attaform validates as you type by default — ",[18,19,20],"code",{},"validateOn: 'change'"," with\n",[18,23,24],{},"debounceMs: 0"," is implicit. Errors at any path reflect the live\n",[18,27,28],{},"(value, schema)"," continuously, so consumers can render inline\nfeedback without reaching for a separate \"is this field valid?\"\nquery.",[14,31,32,33,37,38,41,42,45,46,49],{},"The data layer (errors as a function of value) and the rendering\nlayer (when to ",[34,35,36],"strong",{},"show"," errors) are separate concerns: the merged\n",[18,39,40],{},"errors"," store is always current; gating display on\n",[18,43,44],{},"form.fields.\u003Cpath>.touched"," \u002F ",[18,47,48],{},"form.meta.submitCount"," \u002F etc. is your\ncall.",[51,52,54],"h2",{"id":53},"default-in-action","Default in action",[14,56,57],{},"No configuration needed:",[59,60,65],"pre",{"className":61,"code":62,"language":63,"meta":64,"style":64},"language-ts shiki shiki-themes github-light github-dark","useForm({ schema, key: 'signup' })\n","ts","",[18,66,67],{"__ignoreMap":64},[68,69,72,76,80,84],"span",{"class":70,"line":71},"line",1,[68,73,75],{"class":74},"sScJk","useForm",[68,77,79],{"class":78},"sVt8B","({ schema, key: ",[68,81,83],{"class":82},"sZZnC","'signup'",[68,85,86],{"class":78}," })\n",[14,88,89,90,93,94,97,98,100,101,104,105,108],{},"Type into an ",[18,91,92],{},"\u003Cinput v-register=\"register('email')\" \u002F>",", see\n",[18,95,96],{},"form.errors.email"," populate (or clear) synchronously after each\nkeystroke (default ",[18,99,24],{}," skips ",[18,102,103],{},"setTimeout"," entirely; the\nschema work itself rides ",[18,106,107],{},"Promise.resolve().then(...)",", so errors\nland on the next microtask).",[51,110,112],{"id":111},"tune-or-opt-out","Tune or opt out",[59,114,116],{"className":61,"code":115,"language":63,"meta":64,"style":64},"useForm({\n  schema,\n  key: 'signup',\n  validateOn: 'change',\n  debounceMs: 500, \u002F\u002F coalesce rapid bursts; useful for slow async refines\n})\n",[18,117,118,125,131,142,153,170],{"__ignoreMap":64},[68,119,120,122],{"class":70,"line":71},[68,121,75],{"class":74},[68,123,124],{"class":78},"({\n",[68,126,128],{"class":70,"line":127},2,[68,129,130],{"class":78},"  schema,\n",[68,132,134,137,139],{"class":70,"line":133},3,[68,135,136],{"class":78},"  key: ",[68,138,83],{"class":82},[68,140,141],{"class":78},",\n",[68,143,145,148,151],{"class":70,"line":144},4,[68,146,147],{"class":78},"  validateOn: ",[68,149,150],{"class":82},"'change'",[68,152,141],{"class":78},[68,154,156,159,163,166],{"class":70,"line":155},5,[68,157,158],{"class":78},"  debounceMs: ",[68,160,162],{"class":161},"sj4cs","500",[68,164,165],{"class":78},", ",[68,167,169],{"class":168},"sJ8bj","\u002F\u002F coalesce rapid bursts; useful for slow async refines\n",[68,171,173],{"class":70,"line":172},6,[68,174,175],{"class":78},"})\n",[14,177,178],{},"Three modes:",[180,181,182,200],"table",{},[183,184,185],"thead",{},[186,187,188,194,197],"tr",{},[189,190,191],"th",{},[18,192,193],{},"validateOn",[189,195,196],{},"When it fires",[189,198,199],{},"Debounced?",[201,202,203,228,244],"tbody",{},[186,204,205,210,217],{},[206,207,208],"td",{},[18,209,150],{},[206,211,212,213,216],{},"(default) Every committed write: directive input, ",[18,214,215],{},"setValue",", etc.",[206,218,219,220,223,224,227],{},"Yes — ",[18,221,222],{},"debounceMs"," (default ",[18,225,226],{},"0"," = sync).",[186,229,230,235,238],{},[206,231,232],{},[18,233,234],{},"'blur'",[206,236,237],{},"Tab away from a registered field.",[206,239,240,241,243],{},"No — immediate. ",[18,242,222],{}," is rejected by the type.",[186,245,246,251,254],{},[206,247,248],{},[18,249,250],{},"'submit'",[206,252,253],{},"Explicit opt-out — submit is the only validator.",[206,255,256,257,243],{},"— ",[18,258,222],{},[14,260,261,262,265,266,268,269,271,272,45,274,276],{},"The TS-level ",[18,263,264],{},"ValidateOnConfig"," discriminated union enforces that\n",[18,267,222],{}," is only valid with ",[18,270,20],{},". Pairing it\nwith ",[18,273,234],{},[18,275,250],{}," is a compile-time error rather than a\nsilent runtime drop.",[51,278,280],{"id":279},"which-mode","Which mode?",[282,283,284,295,302],"ul",{},[285,286,287,291,292,294],"li",{},[34,288,289],{},[18,290,150],{}," — the default. Inline feedback as the user types;\nexpensive async refines (email uniqueness, server-side lookups)\nride on the same ",[18,293,222],{}," window so the network isn't hit on\nevery keystroke.",[285,296,297,301],{},[34,298,299],{},[18,300,234],{}," — quieter. Feedback only after the user leaves the\nfield. Best when the schema is simple and per-keystroke checks\nfeel noisy.",[285,303,304,308],{},[34,305,306],{},[18,307,250],{}," — the explicit opt-out. Submit is the only\nvalidator. Use for small forms + fast-to-submit flows where live\nfeedback would distract.",[51,310,312],{"id":311},"what-you-get","What you get",[14,314,315,316,319,320,323],{},"Each run targets one path at a time. On success, errors at that\npath are cleared; on failure, they're overwritten. Sibling fields\nare untouched — a user typing into ",[18,317,318],{},"email"," won't clear the existing\n",[18,321,322],{},"password"," error.",[59,325,329],{"className":326,"code":327,"language":328,"meta":64,"style":64},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Cinput v-register=\"register('email')\" \u002F>\n  \u003Csmall v-if=\"errors.email?.[0]\">\n    {{ errors.email[0].message }}\n  \u003C\u002Fsmall>\n\u003C\u002Ftemplate>\n","vue",[18,330,331,343,363,380,385,394],{"__ignoreMap":64},[68,332,333,336,340],{"class":70,"line":71},[68,334,335],{"class":78},"\u003C",[68,337,339],{"class":338},"s9eBZ","template",[68,341,342],{"class":78},">\n",[68,344,345,348,351,354,357,360],{"class":70,"line":127},[68,346,347],{"class":78},"  \u003C",[68,349,350],{"class":338},"input",[68,352,353],{"class":74}," v-register",[68,355,356],{"class":78},"=",[68,358,359],{"class":82},"\"register('email')\"",[68,361,362],{"class":78}," \u002F>\n",[68,364,365,367,370,373,375,378],{"class":70,"line":133},[68,366,347],{"class":78},[68,368,369],{"class":338},"small",[68,371,372],{"class":74}," v-if",[68,374,356],{"class":78},[68,376,377],{"class":82},"\"errors.email?.[0]\"",[68,379,342],{"class":78},[68,381,382],{"class":70,"line":144},[68,383,384],{"class":78},"    {{ errors.email[0].message }}\n",[68,386,387,390,392],{"class":70,"line":155},[68,388,389],{"class":78},"  \u003C\u002F",[68,391,369],{"class":338},[68,393,342],{"class":78},[68,395,396,399,401],{"class":70,"line":172},[68,397,398],{"class":78},"\u003C\u002F",[68,400,339],{"class":338},[68,402,342],{"class":78},[14,404,405,406,408],{},"The same ",[18,407,40],{}," store handles submit validation and live\nfield validation — no new reactive surface to wire up.",[51,410,412],{"id":411},"rapid-typing","Rapid typing",[14,414,415,416,418],{},"Type fast, validate once. Successive writes reset the debounce\ntimer (or fire synchronously when ",[18,417,24],{},") and cancel any\nin-flight validation for that path:",[59,420,422],{"className":61,"code":421,"language":63,"meta":64,"style":64},"form.setValue('email', 'a') \u002F\u002F schedules \u002F runs\nform.setValue('email', 'ab') \u002F\u002F cancels prior, reschedules \u002F runs\nform.setValue('email', 'abc') \u002F\u002F cancels prior, reschedules \u002F runs\n\u002F\u002F …debounceMs after the LAST write, validation runs once on 'abc'.\n",[18,423,424,448,468,487],{"__ignoreMap":64},[68,425,426,429,431,434,437,439,442,445],{"class":70,"line":71},[68,427,428],{"class":78},"form.",[68,430,215],{"class":74},[68,432,433],{"class":78},"(",[68,435,436],{"class":82},"'email'",[68,438,165],{"class":78},[68,440,441],{"class":82},"'a'",[68,443,444],{"class":78},") ",[68,446,447],{"class":168},"\u002F\u002F schedules \u002F runs\n",[68,449,450,452,454,456,458,460,463,465],{"class":70,"line":127},[68,451,428],{"class":78},[68,453,215],{"class":74},[68,455,433],{"class":78},[68,457,436],{"class":82},[68,459,165],{"class":78},[68,461,462],{"class":82},"'ab'",[68,464,444],{"class":78},[68,466,467],{"class":168},"\u002F\u002F cancels prior, reschedules \u002F runs\n",[68,469,470,472,474,476,478,480,483,485],{"class":70,"line":133},[68,471,428],{"class":78},[68,473,215],{"class":74},[68,475,433],{"class":78},[68,477,436],{"class":82},[68,479,165],{"class":78},[68,481,482],{"class":82},"'abc'",[68,484,444],{"class":78},[68,486,467],{"class":168},[68,488,489],{"class":70,"line":144},[68,490,491],{"class":168},"\u002F\u002F …debounceMs after the LAST write, validation runs once on 'abc'.\n",[14,493,494],{},"If a slow server reply arrives for \"ab\" after \"abc\" starts\nvalidating, the stale result is dropped.",[51,496,498],{"id":497},"submit-is-still-authoritative","Submit is still authoritative",[14,500,501,502,505],{},"When ",[18,503,504],{},"handleSubmit"," fires, any pending field-level runs are cancelled\nand the submit's full-form validation wins. Your users can't get\n\"my submit said the form was valid, but a stale field-level error\nsneaked in afterwards\".",[14,507,508,511],{},[18,509,510],{},"reset()"," does the same — field-level state is cancelled before the\nfresh form lands.",[51,513,515,518],{"id":514},"metaisvalidating-for-ui",[18,516,517],{},"meta.isValidating"," for UI",[14,520,521,524,525,528,529,532,533,536],{},[18,522,523],{},"form.meta.isValidating"," is ",[18,526,527],{},"true"," while any validation is in flight\n— submit, reactive ",[18,530,531],{},"validate()",", one-shot ",[18,534,535],{},"validateAsync",", or a\nfield-level run. Gate UI:",[59,538,540],{"className":326,"code":539,"language":328,"meta":64,"style":64},"\u003Cbutton :disabled=\"form.meta.isValidating\">Submit\u003C\u002Fbutton>\n",[18,541,542],{"__ignoreMap":64},[68,543,544,546,549,552,555,557,560,562,564,567,569],{"class":70,"line":71},[68,545,335],{"class":78},[68,547,548],{"class":338},"button",[68,550,551],{"class":78}," :",[68,553,554],{"class":74},"disabled",[68,556,356],{"class":78},[68,558,559],{"class":82},"\"",[68,561,523],{"class":78},[68,563,559],{"class":82},[68,565,566],{"class":78},">Submit\u003C\u002F",[68,568,548],{"class":338},[68,570,342],{"class":78},[51,572,574,575],{"id":573},"tuning-debouncems","Tuning ",[18,576,222],{},[14,578,579,580,582],{},"The default ",[18,581,226],{}," feels snappy and matches the obvious mental model.\nFor expensive async checks (DB hit, third-party API), bump it:",[59,584,586],{"className":61,"code":585,"language":63,"meta":64,"style":64},"useForm({ schema, validateOn: 'change', debounceMs: 500 })\n",[18,587,588],{"__ignoreMap":64},[68,589,590,592,595,597,600,602],{"class":70,"line":71},[68,591,75],{"class":74},[68,593,594],{"class":78},"({ schema, validateOn: ",[68,596,150],{"class":82},[68,598,599],{"class":78},", debounceMs: ",[68,601,162],{"class":161},[68,603,86],{"class":78},[14,605,606,608,609,611],{},[18,607,24],{}," is the off switch — when set, validation runs\nsynchronously after each committed write with no ",[18,610,103],{},"\nindirection.",[51,613,615],{"id":614},"storage-commit-timing-vs-validation-timing","Storage commit timing vs. validation timing",[14,617,618,620,621,624,625,628],{},[18,619,222],{}," is purely a VALIDATION debounce. Form storage commits\nhappen at the directive's listener — per keystroke for\n",[18,622,623],{},"\u003Cinput v-register>",", per blur for ",[18,626,627],{},"\u003Cinput v-register.lazy>",". The\nvalidation debounce counts ms since the last committed write,\nregardless of which listener fired.",[14,630,631,632,635,636,638,639,641,642,645],{},"If you want validation to wait for the user to leave the field,\nuse ",[18,633,634],{},"validateOn: 'blur'"," instead of trying to pair ",[18,637,20],{}," with ",[18,640,627],{}," — the latter still fires\non every ",[18,643,644],{},"change"," event and the validation debounce coalesces them\nthe same way.",[51,647,649],{"id":648},"nested-paths","Nested paths",[14,651,652,655,656,659],{},[18,653,654],{},"setValue('user.profile.email', …)"," validates exactly that path,\nnot the containing objects. Your ",[18,657,658],{},"errors['user.profile.email']","\nlookup gets the error.",[51,661,663],{"id":662},"caveat-blur-doesnt-re-validate-on-typing","Caveat: blur doesn't re-validate on typing",[14,665,666,667,669,670,672],{},"With ",[18,668,634],{},", if the user sees an error and edits the\nfield without leaving it, the stale error stays until the next\nblur. Switch to ",[18,671,150],{}," when live feedback matters more than\nkeystroke quiet.",[674,675,676],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":64,"searchDepth":127,"depth":127,"links":678},[679,680,681,682,683,684,685,687,689,690,691],{"id":53,"depth":127,"text":54},{"id":111,"depth":127,"text":112},{"id":279,"depth":127,"text":280},{"id":311,"depth":127,"text":312},{"id":411,"depth":127,"text":412},{"id":497,"depth":127,"text":498},{"id":514,"depth":127,"text":686},"meta.isValidating for UI",{"id":573,"depth":127,"text":688},"Tuning debounceMs",{"id":614,"depth":127,"text":615},{"id":648,"depth":127,"text":649},{"id":662,"depth":127,"text":663},"Attaform validates as you type by default — validateOn: 'change' with\ndebounceMs: 0 is implicit. Errors at any path reflect the live\n(value, schema) continuously, so consumers can render inline\nfeedback without reaching for a separate \"is this field valid?\"\nquery.","md",{},true,"\u002Fdocs\u002Frecipes\u002Ffield-level-validation",{"title":5,"description":692},"docs\u002Frecipes\u002Ffield-level-validation","vyUH7Ke7KqJdroGm4x3guEh4R586dreSfEzGo-bul6o",1777934136428]