[{"data":1,"prerenderedAt":913},["ShallowReactive",2],{"content-\u002Fdocs\u002Fvalidation\u002Fblank":3},{"id":4,"title":5,"body":6,"description":893,"extension":894,"meta":895,"metaRows":896,"navigation":371,"path":908,"seo":909,"source":910,"stem":911,"__hash__":912},"docs\u002Fdocs\u002Fvalidation\u002Fblank.md","The `blank` field-state bit",{"type":7,"value":8,"toc":876},"minimark",[9,19,33,36,54,59,64,74,94,117,145,152,159,263,273,277,287,300,304,310,326,333,339,458,469,515,531,538,541,727,738,742,751,782,798,804,810,816,822,826,872],[10,11,13,14,18],"h1",{"id":12},"the-blank-field-state-bit","The ",[15,16,17],"code",{},"blank"," field-state bit",[20,21,22],"blockquote",{},[23,24,25,26,29,30,32],"p",{},"A storage \u002F display side-channel for cleared numeric inputs. Storage holds ",[15,27,28],{},"0",", the user sees empty, and the form doesn't silently submit ",[15,31,28],{}," for an unfilled required field.",[34,35],"docs-meta-table",{},[23,37,38,39,41,42,45,46,49,50,53],{},"The demo shows four fields with different schemas. Watch the ",[15,40,17],{}," column and the ",[15,43,44],{},"errors"," column as you type. The numeric field starts blank-marked even though ",[15,47,48],{},"values.age === 0","; the required string field uses the schema's refinement instead; the loose string field never raises an error; and the ",[15,51,52],{},"unset","-defaulted country starts blank-marked deliberately, clearing as soon as you type.",[55,56],"docs-demo",{"label":57,"slug":58},"Blank State Demo","blank-field-state",[60,61,63],"h2",{"id":62},"why-it-exists","Why it exists",[23,65,66,67,73],{},"The whole library obeys one principle: ",[68,69,70],"strong",{},[15,71,72],{},"errors = f(schema, state)",". Storage plus the schema tell you whether the form is valid, except for one case.",[23,75,76,77,80,81,84,85,87,88,90,91,93],{},"Numeric inputs lie. A ",[15,78,79],{},"\u003Cinput type=\"number\">"," whose value the user has just cleared shows ",[15,82,83],{},"''"," in the DOM, but the slim shape requires a number, so storage holds ",[15,86,28],{},". The schema can't tell the difference between \"user typed ",[15,89,28],{},"\" and \"user supplied nothing\": both produce ",[15,92,28],{}," in storage. Without a side-channel, the runtime would either:",[95,96,97,108],"ul",{},[98,99,100,101,103,104,107],"li",{},"Trust storage and silently submit ",[15,102,28],{}," for an unfilled required field (the public-housing-form footgun: \"Income? ",[15,105,106],{},"$0",". Approved.\").",[98,109,110,111,113,114,116],{},"Re-define ",[15,112,28],{}," as \"definitely blank,\" which loses the case where the user actually meant ",[15,115,28],{},".",[23,118,119,122,123,126,127,130,131,130,134,130,137,140,141,144],{},[15,120,121],{},"blankPaths"," is the side-channel. It's a reactive ",[15,124,125],{},"BlankPathsView"," (Set-like: ",[15,128,129],{},"size",", ",[15,132,133],{},"has(input)",[15,135,136],{},"values()",[15,138,139],{},"Symbol.iterator",") recording paths where the runtime knows storage and the visible display diverge. The schema author writes ",[15,142,143],{},"z.number()"," and gets the \"empty input\" signal back without inventing a sentinel value.",[60,146,148,149,151],{"id":147},"when-blank-auto-marks","When ",[15,150,17],{}," auto-marks",[23,153,154,155,158],{},"The runtime auto-marks ",[68,156,157],{},"numeric leaves only",". The asymmetry is real:",[160,161,162,181],"table",{},[163,164,165],"thead",{},[166,167,168,172,175,178],"tr",{},[169,170,171],"th",{},"Type",[169,173,174],{},"Storage slim default",[169,176,177],{},"DOM \"empty\"",[169,179,180],{},"Need the side-channel?",[182,183,184,206,227,245],"tbody",{},[166,185,186,192,196,200],{},[187,188,189],"td",{},[15,190,191],{},"number",[187,193,194],{},[15,195,28],{},[187,197,198],{},[15,199,83],{},[187,201,202,205],{},[68,203,204],{},"Yes",": storage and display diverge.",[166,207,208,213,218,222],{},[187,209,210],{},[15,211,212],{},"bigint",[187,214,215],{},[15,216,217],{},"0n",[187,219,220],{},[15,221,83],{},[187,223,224,226],{},[68,225,204],{},": same reason.",[166,228,229,234,238,242],{},[187,230,231],{},[15,232,233],{},"string",[187,235,236],{},[15,237,83],{},[187,239,240],{},[15,241,83],{},[187,243,244],{},"No, they match byte-for-byte.",[166,246,247,252,257,260],{},[187,248,249],{},[15,250,251],{},"boolean",[187,253,254],{},[15,255,256],{},"false",[187,258,259],{},"unchecked",[187,261,262],{},"No, they match.",[23,264,265,266,269,270,272],{},"For strings and booleans the schema sees what the user sees. Require non-empty strings via ",[15,267,268],{},"z.string().min(1)",": the refinement error fires the moment storage is ",[15,271,83],{},", schema speaking.",[60,274,276],{"id":275},"lifecycle-numeric","Lifecycle (numeric)",[278,279,285],"pre",{"className":280,"code":282,"language":283,"meta":284},[281],"language-text","form mounts (no defaults)\n  → blankPaths.add('income')\n  → form.errors.income = [{ code: 'atta:no-value-supplied', … }]\n  → form.fields.income.blank === true\n\nuser types \"5\"\n  → blankPaths.delete('income')\n  → form.errors.income = undefined\n  → form.fields.income.blank === false\n\nuser clears the input (backspace)\n  → directive sees el.value === ''\n  → blankPaths.add('income')\n  → form.errors.income re-appears reactively\n  → form.fields.income.blank === true\n\nuser types \"0\"\n  → blankPaths stays empty (the value is intentional)\n  → form.errors.income stays undefined\n","text","",[15,286,282],{"__ignoreMap":284},[23,288,289,291,292,295,296,299],{},[15,290,72],{}," holds at every step: ",[15,293,294],{},"state"," includes ",[15,297,298],{},"(form.value, blankPaths)",", and the function recomputes whenever either changes.",[60,301,303],{"id":302},"lifecycle-string","Lifecycle (string)",[278,305,308],{"className":306,"code":307,"language":283,"meta":284},[281],"form mounts (no defaults)\n  → blankPaths empty (strings don't auto-mark)\n  → form.errors.email = undefined          (z.string() accepts '')\n  → form.fields.email.blank === false\n\nuser types \"hi\" then deletes\n  → blankPaths still empty\n  → form.errors.email still undefined      (z.string() still accepts '')\n  → form.fields.email.blank === false\n",[15,309,307],{"__ignoreMap":284},[23,311,312,313,315,316,318,319,322,323,325],{},"If the schema is ",[15,314,268],{}," instead, the lifecycle is the same on ",[15,317,121],{},", but ",[15,320,321],{},"form.errors.email"," carries a refinement error whenever storage is ",[15,324,83],{},", because that's the schema speaking. The blank channel stays out of it.",[60,327,329,330,332],{"id":328},"explicit-opt-in-at-any-path-the-unset-sentinel","Explicit opt-in at any path: the ",[15,331,52],{}," sentinel",[23,334,335,336,338],{},"Sometimes you do want a string or boolean leaf to start blank: a \"please choose\" indicator on a checkbox, a deferred-fill text field. That's an explicit consumer signal, not runtime inference. Use the ",[15,337,52],{}," sentinel:",[278,340,344],{"className":341,"code":342,"language":343,"meta":284,"style":284},"language-ts shiki shiki-themes github-light github-dark","import { unset, useForm } from 'attaform\u002Fzod'\n\nuseForm({\n  schema: z.object({ agreed: z.boolean(), note: z.string() }),\n  defaultValues: { agreed: unset, note: unset },\n})\n\n\u002F\u002F Or imperatively:\nform.setValue('agreed', unset)\nform.reset({ note: unset })\n","ts",[15,345,346,366,373,383,405,411,417,422,429,447],{"__ignoreMap":284},[347,348,351,355,359,362],"span",{"class":349,"line":350},"line",1,[347,352,354],{"class":353},"szBVR","import",[347,356,358],{"class":357},"sVt8B"," { unset, useForm } ",[347,360,361],{"class":353},"from",[347,363,365],{"class":364},"sZZnC"," 'attaform\u002Fzod'\n",[347,367,369],{"class":349,"line":368},2,[347,370,372],{"emptyLinePlaceholder":371},true,"\n",[347,374,376,380],{"class":349,"line":375},3,[347,377,379],{"class":378},"sScJk","useForm",[347,381,382],{"class":357},"({\n",[347,384,386,389,392,395,397,400,402],{"class":349,"line":385},4,[347,387,388],{"class":357},"  schema: z.",[347,390,391],{"class":378},"object",[347,393,394],{"class":357},"({ agreed: z.",[347,396,251],{"class":378},[347,398,399],{"class":357},"(), note: z.",[347,401,233],{"class":378},[347,403,404],{"class":357},"() }),\n",[347,406,408],{"class":349,"line":407},5,[347,409,410],{"class":357},"  defaultValues: { agreed: unset, note: unset },\n",[347,412,414],{"class":349,"line":413},6,[347,415,416],{"class":357},"})\n",[347,418,420],{"class":349,"line":419},7,[347,421,372],{"emptyLinePlaceholder":371},[347,423,425],{"class":349,"line":424},8,[347,426,428],{"class":427},"sJ8bj","\u002F\u002F Or imperatively:\n",[347,430,432,435,438,441,444],{"class":349,"line":431},9,[347,433,434],{"class":357},"form.",[347,436,437],{"class":378},"setValue",[347,439,440],{"class":357},"(",[347,442,443],{"class":364},"'agreed'",[347,445,446],{"class":357},", unset)\n",[347,448,450,452,455],{"class":349,"line":449},10,[347,451,434],{"class":357},[347,453,454],{"class":378},"reset",[347,456,457],{"class":357},"({ note: unset })\n",[23,459,460,462,463,465,466,468],{},[15,461,52],{}," works at every position the consumer can address: primitive leaves, containers, arrays, tuples, records, discriminated unions, optional \u002F nullable wrappers, and the root. Container ",[15,464,52],{}," recurses through the schema's slim subtree and adds every primitive descendant to ",[15,467,121],{}," in one call:",[278,470,472],{"className":341,"code":471,"language":343,"meta":284,"style":284},"form.setValue('profile', unset) \u002F\u002F marks profile.name, profile.age, etc.\nform.reset({ cargo: unset }) \u002F\u002F DU stub, marks the discriminator path\nform.reset(unset) \u002F\u002F root: marks every primitive leaf in the schema\n",[15,473,474,491,503],{"__ignoreMap":284},[347,475,476,478,480,482,485,488],{"class":349,"line":350},[347,477,434],{"class":357},[347,479,437],{"class":378},[347,481,440],{"class":357},[347,483,484],{"class":364},"'profile'",[347,486,487],{"class":357},", unset) ",[347,489,490],{"class":427},"\u002F\u002F marks profile.name, profile.age, etc.\n",[347,492,493,495,497,500],{"class":349,"line":368},[347,494,434],{"class":357},[347,496,454],{"class":378},[347,498,499],{"class":357},"({ cargo: unset }) ",[347,501,502],{"class":427},"\u002F\u002F DU stub, marks the discriminator path\n",[347,504,505,507,509,512],{"class":349,"line":375},[347,506,434],{"class":357},[347,508,454],{"class":378},[347,510,511],{"class":357},"(unset) ",[347,513,514],{"class":427},"\u002F\u002F root: marks every primitive leaf in the schema\n",[23,516,517,518,521,522,530],{},"Combined with required schemas, the sentinel surfaces a ",[15,519,520],{},"atta:no-value-supplied"," error reactively at each marked path: same lifecycle as the numeric auto-mark case, just driven by consumer intent rather than runtime inference. See ",[523,524,526,527,529],"a",{"href":525},"\u002Fdocs\u002Fwriting-and-mutating\u002Funset","the ",[15,528,52],{}," page"," for the position-by-position contract.",[60,532,534,535,537],{"id":533},"how-to-read-blank-in-your-ui","How to read ",[15,536,17],{}," in your UI",[23,539,540],{},"Attaform never renders. The signal is exposed; your component decides what to do.",[278,542,546],{"className":543,"code":544,"language":545,"meta":284,"style":284},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup lang=\"ts\">\n  const form = useForm({ schema })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-register=\"form.register('income')\" \u002F>\n\n  \u003C!-- show errors only after the user has touched the field -->\n  \u003Cp v-if=\"form.errors.income && form.fields.income.touched\" class=\"error\">\n    {{ form.errors.income[0].message }}\n  \u003C\u002Fp>\n\n  \u003C!-- separately, an \"unanswered\" hint that distinguishes from errors -->\n  \u003Cspan v-if=\"form.fields.income.blank\" class=\"hint\">Required, please enter a number\u003C\u002Fspan>\n\u003C\u002Ftemplate>\n","vue",[15,547,548,572,590,599,603,612,631,635,640,664,669,679,684,690,718],{"__ignoreMap":284},[347,549,550,553,557,560,563,566,569],{"class":349,"line":350},[347,551,552],{"class":357},"\u003C",[347,554,556],{"class":555},"s9eBZ","script",[347,558,559],{"class":378}," setup",[347,561,562],{"class":378}," lang",[347,564,565],{"class":357},"=",[347,567,568],{"class":364},"\"ts\"",[347,570,571],{"class":357},">\n",[347,573,574,577,581,584,587],{"class":349,"line":368},[347,575,576],{"class":353},"  const",[347,578,580],{"class":579},"sj4cs"," form",[347,582,583],{"class":353}," =",[347,585,586],{"class":378}," useForm",[347,588,589],{"class":357},"({ schema })\n",[347,591,592,595,597],{"class":349,"line":375},[347,593,594],{"class":357},"\u003C\u002F",[347,596,556],{"class":555},[347,598,571],{"class":357},[347,600,601],{"class":349,"line":385},[347,602,372],{"emptyLinePlaceholder":371},[347,604,605,607,610],{"class":349,"line":407},[347,606,552],{"class":357},[347,608,609],{"class":555},"template",[347,611,571],{"class":357},[347,613,614,617,620,623,625,628],{"class":349,"line":413},[347,615,616],{"class":357},"  \u003C",[347,618,619],{"class":555},"input",[347,621,622],{"class":378}," v-register",[347,624,565],{"class":357},[347,626,627],{"class":364},"\"form.register('income')\"",[347,629,630],{"class":357}," \u002F>\n",[347,632,633],{"class":349,"line":419},[347,634,372],{"emptyLinePlaceholder":371},[347,636,637],{"class":349,"line":424},[347,638,639],{"class":427},"  \u003C!-- show errors only after the user has touched the field -->\n",[347,641,642,644,646,649,651,654,657,659,662],{"class":349,"line":431},[347,643,616],{"class":357},[347,645,23],{"class":555},[347,647,648],{"class":378}," v-if",[347,650,565],{"class":357},[347,652,653],{"class":364},"\"form.errors.income && form.fields.income.touched\"",[347,655,656],{"class":378}," class",[347,658,565],{"class":357},[347,660,661],{"class":364},"\"error\"",[347,663,571],{"class":357},[347,665,666],{"class":349,"line":449},[347,667,668],{"class":357},"    {{ form.errors.income[0].message }}\n",[347,670,672,675,677],{"class":349,"line":671},11,[347,673,674],{"class":357},"  \u003C\u002F",[347,676,23],{"class":555},[347,678,571],{"class":357},[347,680,682],{"class":349,"line":681},12,[347,683,372],{"emptyLinePlaceholder":371},[347,685,687],{"class":349,"line":686},13,[347,688,689],{"class":427},"  \u003C!-- separately, an \"unanswered\" hint that distinguishes from errors -->\n",[347,691,693,695,697,699,701,704,706,708,711,714,716],{"class":349,"line":692},14,[347,694,616],{"class":357},[347,696,347],{"class":555},[347,698,648],{"class":378},[347,700,565],{"class":357},[347,702,703],{"class":364},"\"form.fields.income.blank\"",[347,705,656],{"class":378},[347,707,565],{"class":357},[347,709,710],{"class":364},"\"hint\"",[347,712,713],{"class":357},">Required, please enter a number\u003C\u002F",[347,715,347],{"class":555},[347,717,571],{"class":357},[347,719,721,723,725],{"class":349,"line":720},15,[347,722,594],{"class":357},[347,724,609],{"class":555},[347,726,571],{"class":357},[23,728,729,730,733,734,737],{},"Reading ",[15,731,732],{},"form.errors.income"," directly gives you whatever the schema and the blank channel produced. Reading ",[15,735,736],{},"form.fields.income.blank"," gives you the raw \"did the user supply something?\" bit, useful for pre-error indicators or progress meters.",[60,739,741],{"id":740},"submit-time-integration","Submit-time integration",[23,743,744,747,748,750],{},[15,745,746],{},"handleSubmit"," checks ",[15,749,121],{}," against the schema before running the success callback:",[95,752,753,767],{},[98,754,755,756,758,759,762,763,766],{},"If ",[15,757,121],{}," is non-empty AND the schema requires those paths, submission fails. The success callback never runs, ",[15,760,761],{},"meta.submissionAttempts"," ticks, and ",[15,764,765],{},"meta.submitError"," carries the aggregate.",[98,768,755,769,771,772,130,775,130,778,781],{},[15,770,121],{}," is non-empty but the schema accepts the empty case (",[15,773,774],{},".optional()",[15,776,777],{},".nullable()",[15,779,780],{},".default(x)","), submission proceeds.",[23,783,13,784,786,787,790,791,794,795,797],{},[15,785,520],{}," error surfaces in ",[15,788,789],{},"form.errors.\u003Cpath>"," and in ",[15,792,793],{},"form.meta.errors",": same shape as a schema-emitted error, distinct ",[15,796,15],{}," for filtering.",[60,799,801,803],{"id":800},"blank-and-persistence",[15,802,17],{}," and persistence",[23,805,806,807,809],{},"Persistence treats ",[15,808,121],{}," as first-class state. The persisted envelope includes the set, so a cleared numeric field stays visually empty after a reload (storage holds the slim default; the displayed-empty state survives). Without this, a \"Score: \" field a user cleared would resurrect as \"0\" after refresh, which is incorrect for the UX.",[60,811,813,815],{"id":812},"blank-and-history",[15,814,17],{}," and history",[23,817,818,819,821],{},"Every history position captures the ",[15,820,121],{}," set at the time of the snapshot. Undoing a \"type a number, then clear it\" sequence restores both the value AND the blank bit. The field reads as empty the way it did before the undo.",[60,823,825],{"id":824},"where-to-next","Where to next",[95,827,828,839,853],{},[98,829,830,834,835,838],{},[523,831,833],{"href":832},"\u002Fdocs\u002Fschemas\u002Fdefaults","Defaults from the schema",": auto-mark interacts with ",[15,836,837],{},"defaultValues","; explicit values turn it off.",[98,840,841,845,846,130,848,850,851,116],{},[523,842,843],{"href":525},[15,844,52],{},": flag any path blank in ",[15,847,837],{},[15,849,437],{},", or ",[15,852,454],{},[98,854,855,859,860,863,864,867,868,871],{},[523,856,858],{"href":857},"\u002Fdocs\u002Fvalidation\u002Fshowing-errors","Display state and showing errors",": ",[15,861,862],{},"firstError"," includes the ",[15,865,866],{},"no-value-supplied"," entry; ",[15,869,870],{},"getDisplayState"," decides when to render it.",[873,874,875],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":284,"searchDepth":368,"depth":368,"links":877},[878,879,881,882,883,885,887,888,890,892],{"id":62,"depth":368,"text":63},{"id":147,"depth":368,"text":880},"When blank auto-marks",{"id":275,"depth":368,"text":276},{"id":302,"depth":368,"text":303},{"id":328,"depth":368,"text":884},"Explicit opt-in at any path: the unset sentinel",{"id":533,"depth":368,"text":886},"How to read blank in your UI",{"id":740,"depth":368,"text":741},{"id":800,"depth":368,"text":889},"blank and persistence",{"id":812,"depth":368,"text":891},"blank and history",{"id":824,"depth":368,"text":825},"A storage \u002F display side-channel for numeric inputs. Storage holds `0` while the user sees empty; `blankPaths` tracks the divergence so submit doesn't lie.","md",{},[897,900,903,906],{"label":898,"value":899},"Category","Field-state",{"label":901,"value":902},"Auto-marks","numeric primitives only (number, bigint)",{"label":904,"value":905,"kind":15},"Manual opt-in","unset sentinel (any path)",{"label":907,"value":520,"kind":15},"Error code","\u002Fdocs\u002Fvalidation\u002Fblank",{"title":5,"description":893},null,"docs\u002Fvalidation\u002Fblank","wboHF5cjgZYdAXJ31SK0oxeb8MaNPrXaKn0TiHrcGpw",1780949760472]