[{"data":1,"prerenderedAt":820},["ShallowReactive",2],{"content-\u002Fdocs\u002Fschemas\u002Fdictionary-forms":3},{"id":4,"title":5,"body":6,"description":800,"extension":801,"meta":802,"metaRows":803,"navigation":113,"path":815,"seo":816,"source":817,"stem":818,"__hash__":819},"docs\u002Fdocs\u002Fschemas\u002Fdictionary-forms.md","Dictionary forms",{"type":7,"value":8,"toc":789},"minimark",[9,13,38,41,51,55,60,66,217,227,233,239,314,329,335,355,482,505,509,519,573,579,631,634,638,641,669,676,719,723,738,742,785],[10,11,5],"h1",{"id":12},"dictionary-forms",[14,15,16],"blockquote",{},[17,18,19,20,24,25,29,30,33,34,37],"p",{},"When the form ",[21,22,23],"em",{},"is"," a map, make the map the root. A ",[26,27,28],"code",{},"z.record"," schema at the top level gives you a dictionary form: ",[26,31,32],{},"form.values"," is the map itself, and ",[26,35,36],{},"form.record()"," iterates its entries.",[39,40],"docs-meta-table",{},[17,42,43,44,47,48,50],{},"The demo is a team roster keyed by member id. The ids are data you only learn at run time, so the schema root is a ",[26,45,46],{},"z.record(z.string(), …)",", a string-keyed dictionary of members. The whole form is that map: edit a member, add one, drop one, and watch ",[26,49,32],{}," track it below.",[52,53],"docs-demo",{"label":54,"slug":12},"Dictionary Form Demo",[56,57,59],"h2",{"id":58},"the-schema","The schema",[17,61,62,63,65],{},"A dictionary form declares a ",[26,64,28],{}," schema as the root, not nested under a key. The key schema constrains what counts as a valid key; the value schema validates each entry:",[67,68,73],"pre",{"className":69,"code":70,"language":71,"meta":72,"style":72},"language-ts shiki shiki-themes github-light github-dark","import { useForm } from 'attaform\u002Fzod'\nimport { z } from 'zod'\n\nconst schema = z.record(\n  z.string(),\n  z.object({ role: z.enum(['admin', 'editor', 'viewer']), tier: z.number() })\n)\n\nconst form = useForm({ schema })\n","ts","",[26,74,75,95,108,115,138,150,190,196,201],{"__ignoreMap":72},[76,77,80,84,88,91],"span",{"class":78,"line":79},"line",1,[76,81,83],{"class":82},"szBVR","import",[76,85,87],{"class":86},"sVt8B"," { useForm } ",[76,89,90],{"class":82},"from",[76,92,94],{"class":93},"sZZnC"," 'attaform\u002Fzod'\n",[76,96,98,100,103,105],{"class":78,"line":97},2,[76,99,83],{"class":82},[76,101,102],{"class":86}," { z } ",[76,104,90],{"class":82},[76,106,107],{"class":93}," 'zod'\n",[76,109,111],{"class":78,"line":110},3,[76,112,114],{"emptyLinePlaceholder":113},true,"\n",[76,116,118,121,125,128,131,135],{"class":78,"line":117},4,[76,119,120],{"class":82},"const",[76,122,124],{"class":123},"sj4cs"," schema",[76,126,127],{"class":82}," =",[76,129,130],{"class":86}," z.",[76,132,134],{"class":133},"sScJk","record",[76,136,137],{"class":86},"(\n",[76,139,141,144,147],{"class":78,"line":140},5,[76,142,143],{"class":86},"  z.",[76,145,146],{"class":133},"string",[76,148,149],{"class":86},"(),\n",[76,151,153,155,158,161,164,167,170,173,176,178,181,184,187],{"class":78,"line":152},6,[76,154,143],{"class":86},[76,156,157],{"class":133},"object",[76,159,160],{"class":86},"({ role: z.",[76,162,163],{"class":133},"enum",[76,165,166],{"class":86},"([",[76,168,169],{"class":93},"'admin'",[76,171,172],{"class":86},", ",[76,174,175],{"class":93},"'editor'",[76,177,172],{"class":86},[76,179,180],{"class":93},"'viewer'",[76,182,183],{"class":86},"]), tier: z.",[76,185,186],{"class":133},"number",[76,188,189],{"class":86},"() })\n",[76,191,193],{"class":78,"line":192},7,[76,194,195],{"class":86},")\n",[76,197,199],{"class":78,"line":198},8,[76,200,114],{"emptyLinePlaceholder":113},[76,202,204,206,209,211,214],{"class":78,"line":203},9,[76,205,120],{"class":82},[76,207,208],{"class":123}," form",[76,210,127],{"class":82},[76,212,213],{"class":133}," useForm",[76,215,216],{"class":86},"({ schema })\n",[17,218,219,220,222,223,226],{},"There is no wrapper key. ",[26,221,32],{}," reads as ",[26,224,225],{},"Record\u003Cstring, { role: 'admin' | 'editor' | 'viewer'; tier: number }>",", the map itself.",[56,228,230,232],{"id":229},"formvalues-is-the-dictionary",[26,231,32],{}," is the dictionary",[17,234,235,236,238],{},"Because the record is the root, ",[26,237,32],{}," is the whole map, and entries bind through their own key:",[67,240,242],{"className":69,"code":241,"language":71,"meta":72,"style":72},"form.values() \u002F\u002F the whole map: Record\u003Cstring, { role; tier }>\nform.values()['ada'] \u002F\u002F one entry, or undefined\nform.register('ada.role') \u002F\u002F bind an entry sub-field\nform.errors['ada']?.['tier'] \u002F\u002F ValidationError[] for that entry's tier\n",[26,243,244,259,277,296],{"__ignoreMap":72},[76,245,246,249,252,255],{"class":78,"line":79},[76,247,248],{"class":86},"form.",[76,250,251],{"class":133},"values",[76,253,254],{"class":86},"() ",[76,256,258],{"class":257},"sJ8bj","\u002F\u002F the whole map: Record\u003Cstring, { role; tier }>\n",[76,260,261,263,265,268,271,274],{"class":78,"line":97},[76,262,248],{"class":86},[76,264,251],{"class":133},[76,266,267],{"class":86},"()[",[76,269,270],{"class":93},"'ada'",[76,272,273],{"class":86},"] ",[76,275,276],{"class":257},"\u002F\u002F one entry, or undefined\n",[76,278,279,281,284,287,290,293],{"class":78,"line":110},[76,280,248],{"class":86},[76,282,283],{"class":133},"register",[76,285,286],{"class":86},"(",[76,288,289],{"class":93},"'ada.role'",[76,291,292],{"class":86},") ",[76,294,295],{"class":257},"\u002F\u002F bind an entry sub-field\n",[76,297,298,301,303,306,309,311],{"class":78,"line":117},[76,299,300],{"class":86},"form.errors[",[76,302,270],{"class":93},[76,304,305],{"class":86},"]?.[",[76,307,308],{"class":93},"'tier'",[76,310,273],{"class":86},[76,312,313],{"class":257},"\u002F\u002F ValidationError[] for that entry's tier\n",[17,315,316,317,320,321,324,325,328],{},"The ",[26,318,319],{},"| undefined"," on an entry read comes from ",[26,322,323],{},"noUncheckedIndexedAccess",", the same way a record field reads anywhere else. Reach for ",[26,326,327],{},"??"," defaults at the call site when you need one.",[56,330,332,333],{"id":331},"iterating-with-formrecord","Iterating with ",[26,334,36],{},[17,336,337,339,340,347,348,354],{},[26,338,36],{}," with no argument is the root entry view: one ",[341,342,344],"a",{"href":343},"\u002Fdocs\u002Freading-the-form\u002Ffields",[26,345,346],{},"FieldState"," per entry, keyed by the entry's own key. It is the root counterpart of ",[341,349,351],{"href":350},"\u002Fdocs\u002Freading-the-form\u002Frecord",[26,352,353],{},"form.record(path)",", which views a nested record. The keys come from the form, so you render whatever entries exist without keeping a parallel list of your own:",[67,356,360],{"className":357,"code":358,"language":359,"meta":72,"style":72},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Cdiv v-for=\"(member, id) in form.record()\" :key=\"id\">\n    \u003Clabel>{{ id }}\u003C\u002Flabel>\n    \u003Cinput v-register=\"form.register(`${id}.tier`)\" type=\"number\" \u002F>\n    \u003Csmall v-if=\"member.showErrors\">{{ member.firstError?.message }}\u003C\u002Fsmall>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","vue",[26,361,362,374,401,416,442,464,473],{"__ignoreMap":72},[76,363,364,367,371],{"class":78,"line":79},[76,365,366],{"class":86},"\u003C",[76,368,370],{"class":369},"s9eBZ","template",[76,372,373],{"class":86},">\n",[76,375,376,379,382,385,388,391,394,396,399],{"class":78,"line":97},[76,377,378],{"class":86},"  \u003C",[76,380,381],{"class":369},"div",[76,383,384],{"class":133}," v-for",[76,386,387],{"class":86},"=",[76,389,390],{"class":93},"\"(member, id) in form.record()\"",[76,392,393],{"class":133}," :key",[76,395,387],{"class":86},[76,397,398],{"class":93},"\"id\"",[76,400,373],{"class":86},[76,402,403,406,409,412,414],{"class":78,"line":110},[76,404,405],{"class":86},"    \u003C",[76,407,408],{"class":369},"label",[76,410,411],{"class":86},">{{ id }}\u003C\u002F",[76,413,408],{"class":369},[76,415,373],{"class":86},[76,417,418,420,423,426,428,431,434,436,439],{"class":78,"line":117},[76,419,405],{"class":86},[76,421,422],{"class":369},"input",[76,424,425],{"class":133}," v-register",[76,427,387],{"class":86},[76,429,430],{"class":93},"\"form.register(`${id}.tier`)\"",[76,432,433],{"class":133}," type",[76,435,387],{"class":86},[76,437,438],{"class":93},"\"number\"",[76,440,441],{"class":86}," \u002F>\n",[76,443,444,446,449,452,454,457,460,462],{"class":78,"line":140},[76,445,405],{"class":86},[76,447,448],{"class":369},"small",[76,450,451],{"class":133}," v-if",[76,453,387],{"class":86},[76,455,456],{"class":93},"\"member.showErrors\"",[76,458,459],{"class":86},">{{ member.firstError?.message }}\u003C\u002F",[76,461,448],{"class":369},[76,463,373],{"class":86},[76,465,466,469,471],{"class":78,"line":152},[76,467,468],{"class":86},"  \u003C\u002F",[76,470,381],{"class":369},[76,472,373],{"class":86},[76,474,475,478,480],{"class":78,"line":192},[76,476,477],{"class":86},"\u003C\u002F",[76,479,370],{"class":369},[76,481,373],{"class":86},[17,483,484,485,488,489,491,492,497,498,504],{},"Each ",[26,486,487],{},"member"," is a live ",[26,490,346],{},", the same surface ",[341,493,494],{"href":343},[26,495,496],{},"form.fields"," exposes, so every read stays current as the user types. Binding still flows through ",[341,499,501],{"href":500},"\u002Fdocs\u002Fbinding-inputs\u002Fv-register",[26,502,503],{},"form.register"," with the entry path, which the key supplies.",[56,506,508],{"id":507},"adding-editing-and-removing-entries","Adding, editing, and removing entries",[17,510,511,512,518],{},"A dictionary form carries its own keys, so you grow and shrink it through ",[341,513,515],{"href":514},"\u002Fdocs\u002Fwriting-and-mutating\u002Fset-value",[26,516,517],{},"setValue",". Editing and adding are ordinary path writes:",[67,520,522],{"className":69,"code":521,"language":71,"meta":72,"style":72},"form.setValue('ada.tier', 4) \u002F\u002F edit one entry's field\nform.setValue('mae', { role: 'viewer', tier: 1 }) \u002F\u002F add a key that isn't there yet\n",[26,523,524,545],{"__ignoreMap":72},[76,525,526,528,530,532,535,537,540,542],{"class":78,"line":79},[76,527,248],{"class":86},[76,529,517],{"class":133},[76,531,286],{"class":86},[76,533,534],{"class":93},"'ada.tier'",[76,536,172],{"class":86},[76,538,539],{"class":123},"4",[76,541,292],{"class":86},[76,543,544],{"class":257},"\u002F\u002F edit one entry's field\n",[76,546,547,549,551,553,556,559,561,564,567,570],{"class":78,"line":97},[76,548,248],{"class":86},[76,550,517],{"class":133},[76,552,286],{"class":86},[76,554,555],{"class":93},"'mae'",[76,557,558],{"class":86},", { role: ",[76,560,180],{"class":93},[76,562,563],{"class":86},", tier: ",[76,565,566],{"class":123},"1",[76,568,569],{"class":86}," }) ",[76,571,572],{"class":257},"\u002F\u002F add a key that isn't there yet\n",[17,574,575,576,578],{},"Removing is the one case the root changes. A nested record drops a key by rewriting its container path; a record root has no container path, so you rewrite the form itself. Passing a single argument to ",[26,577,517],{}," is a whole-form write, and for a dictionary form the whole form is the map:",[67,580,582],{"className":69,"code":581,"language":71,"meta":72,"style":72},"const next = { ...form.values() }\ndelete next['ada']\nform.setValue(next) \u002F\u002F write the map back without that key\n",[26,583,584,606,619],{"__ignoreMap":72},[76,585,586,588,591,593,596,599,601,603],{"class":78,"line":79},[76,587,120],{"class":82},[76,589,590],{"class":123}," next",[76,592,127],{"class":82},[76,594,595],{"class":86}," { ",[76,597,598],{"class":82},"...",[76,600,248],{"class":86},[76,602,251],{"class":133},[76,604,605],{"class":86},"() }\n",[76,607,608,611,614,616],{"class":78,"line":97},[76,609,610],{"class":82},"delete",[76,612,613],{"class":86}," next[",[76,615,270],{"class":93},[76,617,618],{"class":86},"]\n",[76,620,621,623,625,628],{"class":78,"line":110},[76,622,248],{"class":86},[76,624,517],{"class":133},[76,626,627],{"class":86},"(next) ",[76,629,630],{"class":257},"\u002F\u002F write the map back without that key\n",[17,632,633],{},"Surviving entries keep their field states and component instances across the write; only the dropped row unmounts.",[56,635,637],{"id":636},"defaults","Defaults",[17,639,640],{},"A bare record root defaults to an empty map, ready to grow:",[67,642,644],{"className":69,"code":643,"language":71,"meta":72,"style":72},"const form = useForm({ schema })\nform.values() \u002F\u002F {}\n",[26,645,646,658],{"__ignoreMap":72},[76,647,648,650,652,654,656],{"class":78,"line":79},[76,649,120],{"class":82},[76,651,208],{"class":123},[76,653,127],{"class":82},[76,655,213],{"class":133},[76,657,216],{"class":86},[76,659,660,662,664,666],{"class":78,"line":97},[76,661,248],{"class":86},[76,663,251],{"class":133},[76,665,254],{"class":86},[76,667,668],{"class":257},"\u002F\u002F {}\n",[17,670,671,672,675],{},"Seed initial entries with ",[26,673,674],{},"defaultValues",", the same shape as the form value:",[67,677,679],{"className":69,"code":678,"language":71,"meta":72,"style":72},"const form = useForm({\n  schema,\n  defaultValues: { ada: { role: 'admin', tier: 3 } },\n})\n",[26,680,681,694,699,714],{"__ignoreMap":72},[76,682,683,685,687,689,691],{"class":78,"line":79},[76,684,120],{"class":82},[76,686,208],{"class":123},[76,688,127],{"class":82},[76,690,213],{"class":133},[76,692,693],{"class":86},"({\n",[76,695,696],{"class":78,"line":97},[76,697,698],{"class":86},"  schema,\n",[76,700,701,704,706,708,711],{"class":78,"line":110},[76,702,703],{"class":86},"  defaultValues: { ada: { role: ",[76,705,169],{"class":93},[76,707,563],{"class":86},[76,709,710],{"class":123},"3",[76,712,713],{"class":86}," } },\n",[76,715,716],{"class":78,"line":117},[76,717,718],{"class":86},"})\n",[56,720,722],{"id":721},"when-the-keys-are-fixed-reach-for-an-object","When the keys are fixed, reach for an object",[17,724,725,726,729,730,732,733,737],{},"Dictionary forms are for keys you learn at run time. When the keys are known at schema-write time, a ",[26,727,728],{},"z.object"," root is the better fit: fixed keys, a distinct type per field, and compile-time autocomplete on each one. The two compose freely. An object form can hold a ",[26,731,28],{}," field (see ",[341,734,736],{"href":735},"\u002Fdocs\u002Fschemas\u002Frecords","Records & maps","), and a dictionary form's value schema can be any Zod schema, including objects, arrays, and nested records.",[56,739,741],{"id":740},"where-to-next","Where to next",[743,744,745,758,771,778],"ul",{},[746,747,748,750,751,753,754,757],"li",{},[341,749,736],{"href":735},": ",[26,752,28],{}," as a field inside an object form, plus the ",[26,755,756],{},"z.map"," primitive.",[746,759,760,764,765,767,768,770],{},[341,761,762],{"href":350},[26,763,134],{},": the entry view, ",[26,766,353],{}," for a nested record and ",[26,769,36],{}," for the root.",[746,772,773,777],{},[341,774,775],{"href":514},[26,776,517],{},": the writes that grow, edit, and shrink a dictionary.",[746,779,780,784],{},[341,781,783],{"href":782},"\u002Fdocs\u002Fschemas\u002Fnested-objects","Nested objects",": fixed-shape composition, the alternative when the keys are known.",[786,787,788],"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .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":72,"searchDepth":97,"depth":97,"links":790},[791,792,794,796,797,798,799],{"id":58,"depth":97,"text":59},{"id":229,"depth":97,"text":793},"form.values is the dictionary",{"id":331,"depth":97,"text":795},"Iterating with form.record()",{"id":507,"depth":97,"text":508},{"id":636,"depth":97,"text":637},{"id":721,"depth":97,"text":722},{"id":740,"depth":97,"text":741},"A z.record schema can be the form root, not just a field. form.values is the dictionary itself, form.record() iterates the entries, and a bare record root defaults to an empty map.","md",{},[804,807,810,812],{"label":805,"value":806},"Category","Schema feature",{"label":808,"value":809,"kind":26},"Form root","z.record(keySchema, valueSchema)",{"label":811,"value":36,"kind":26},"Entry view",{"label":813,"value":814},"Default","empty map","\u002Fdocs\u002Fschemas\u002Fdictionary-forms",{"title":5,"description":800},null,"docs\u002Fschemas\u002Fdictionary-forms","LdykayUiNH5SpRRTiOybOLMi_P9xa9RqO5pQqZOMoLo",1781538316078]