[{"data":1,"prerenderedAt":3955},["ShallowReactive",2],{"page-\u002Fvue\u002F09-forms-and-v-model":3},{"id":4,"title":5,"body":6,"description":3948,"extension":3949,"meta":3950,"navigation":75,"path":3951,"seo":3952,"stem":3953,"__hash__":3954},"content\u002Fvue\u002F09-forms-and-v-model.md","Vue 3 Engineering Reference — Forms & v-model",{"type":7,"value":8,"toc":3937},"minimark",[9,14,19,373,377,3933],[10,11,13],"h1",{"id":12},"_09-forms-v-model","09 — Forms & v-model",[15,16,18],"h2",{"id":17},"v-model-modifier-expansion-what-the-compiler-generates","v-model Modifier Expansion — What the Compiler Generates",[20,21,24],"code-wrapper",{"filename":22,"language":23},"ModifierInternals.vue","vue",[25,26,30],"pre",{"className":27,"code":28,"language":23,"meta":29,"style":29},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\nimport { ref } from 'vue'\n\nconst text = ref('')\nconst age = ref(0)\nconst name = ref('')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ── v-model=\"text\" expands to: ──────────────────────── -->\n  \u003C!-- :value=\"text\" @input=\"text = $event.target.value\" -->\n  \u003C!-- Vue uses the vModelText directive, not just attr binding:\n       handles IME composition events, edge cases with type=number, etc. -->\n  \u003Cinput v-model=\"text\" \u002F>\n\n  \u003C!-- ── .lazy: listen to 'change' (on blur) not 'input' (every keystroke) ── -->\n  \u003C!-- Expands to: :value=\"text\" @change=\"text = $event.target.value\" -->\n  \u003C!-- Reduces re-render frequency for expensive form validation -->\n  \u003Cinput v-model.lazy=\"text\" \u002F>\n\n  \u003C!-- ── .number: cast input string to Number ─────────────── -->\n  \u003C!-- If input is \"42\", v-model stores 42 (number), not \"42\" (string). -->\n  \u003C!-- Edge: empty string → \"\" (NOT 0), invalid → original string unchanged. -->\n  \u003C!-- Uses parseFloat internally; NaN results keep the original string. -->\n  \u003Cinput v-model.number=\"age\" type=\"number\" \u002F>\n\n  \u003C!-- ── .trim: strip leading\u002Ftrailing whitespace ────────── -->\n  \u003C!-- \"  hello  \" → \"hello\". Does NOT strip internal spaces. -->\n  \u003Cinput v-model.trim=\"name\" \u002F>\n\n  \u003C!-- ── Chained modifiers: .lazy.number.trim — applied left to right ── -->\n  \u003Cinput v-model.lazy.number.trim=\"age\" \u002F>\n\u003C\u002Ftemplate>\n","",[31,32,33,53,70,77,102,121,139,149,154,164,171,177,183,189,210,215,221,227,233,249,254,260,266,272,278,303,308,314,320,337,342,348,364],"code",{"__ignoreMap":29},[34,35,38,42,46,50],"span",{"class":36,"line":37},"line",1,[34,39,41],{"class":40},"ssxIu","\u003C",[34,43,45],{"class":44},"sk71V","script",[34,47,49],{"class":48},"sIsaT"," setup",[34,51,52],{"class":40},">\n",[34,54,56,60,63,66],{"class":36,"line":55},2,[34,57,59],{"class":58},"svdQ7","import",[34,61,62],{"class":40}," { ref } ",[34,64,65],{"class":58},"from",[34,67,69],{"class":68},"sJ6F3"," 'vue'\n",[34,71,73],{"class":36,"line":72},3,[34,74,76],{"emptyLinePlaceholder":75},true,"\n",[34,78,80,83,87,90,93,96,99],{"class":36,"line":79},4,[34,81,82],{"class":58},"const",[34,84,86],{"class":85},"snvgF"," text",[34,88,89],{"class":58}," =",[34,91,92],{"class":48}," ref",[34,94,95],{"class":40},"(",[34,97,98],{"class":68},"''",[34,100,101],{"class":40},")\n",[34,103,105,107,110,112,114,116,119],{"class":36,"line":104},5,[34,106,82],{"class":58},[34,108,109],{"class":85}," age",[34,111,89],{"class":58},[34,113,92],{"class":48},[34,115,95],{"class":40},[34,117,118],{"class":85},"0",[34,120,101],{"class":40},[34,122,124,126,129,131,133,135,137],{"class":36,"line":123},6,[34,125,82],{"class":58},[34,127,128],{"class":85}," name",[34,130,89],{"class":58},[34,132,92],{"class":48},[34,134,95],{"class":40},[34,136,98],{"class":68},[34,138,101],{"class":40},[34,140,142,145,147],{"class":36,"line":141},7,[34,143,144],{"class":40},"\u003C\u002F",[34,146,45],{"class":44},[34,148,52],{"class":40},[34,150,152],{"class":36,"line":151},8,[34,153,76],{"emptyLinePlaceholder":75},[34,155,157,159,162],{"class":36,"line":156},9,[34,158,41],{"class":40},[34,160,161],{"class":44},"template",[34,163,52],{"class":40},[34,165,167],{"class":36,"line":166},10,[34,168,170],{"class":169},"sdCPZ","  \u003C!-- ── v-model=\"text\" expands to: ──────────────────────── -->\n",[34,172,174],{"class":36,"line":173},11,[34,175,176],{"class":169},"  \u003C!-- :value=\"text\" @input=\"text = $event.target.value\" -->\n",[34,178,180],{"class":36,"line":179},12,[34,181,182],{"class":169},"  \u003C!-- Vue uses the vModelText directive, not just attr binding:\n",[34,184,186],{"class":36,"line":185},13,[34,187,188],{"class":169},"       handles IME composition events, edge cases with type=number, etc. -->\n",[34,190,192,195,198,201,204,207],{"class":36,"line":191},14,[34,193,194],{"class":40},"  \u003C",[34,196,197],{"class":44},"input",[34,199,200],{"class":48}," v-model",[34,202,203],{"class":40},"=",[34,205,206],{"class":68},"\"text\"",[34,208,209],{"class":40}," \u002F>\n",[34,211,213],{"class":36,"line":212},15,[34,214,76],{"emptyLinePlaceholder":75},[34,216,218],{"class":36,"line":217},16,[34,219,220],{"class":169},"  \u003C!-- ── .lazy: listen to 'change' (on blur) not 'input' (every keystroke) ── -->\n",[34,222,224],{"class":36,"line":223},17,[34,225,226],{"class":169},"  \u003C!-- Expands to: :value=\"text\" @change=\"text = $event.target.value\" -->\n",[34,228,230],{"class":36,"line":229},18,[34,231,232],{"class":169},"  \u003C!-- Reduces re-render frequency for expensive form validation -->\n",[34,234,236,238,240,243,245,247],{"class":36,"line":235},19,[34,237,194],{"class":40},[34,239,197],{"class":44},[34,241,242],{"class":48}," v-model.lazy",[34,244,203],{"class":40},[34,246,206],{"class":68},[34,248,209],{"class":40},[34,250,252],{"class":36,"line":251},20,[34,253,76],{"emptyLinePlaceholder":75},[34,255,257],{"class":36,"line":256},21,[34,258,259],{"class":169},"  \u003C!-- ── .number: cast input string to Number ─────────────── -->\n",[34,261,263],{"class":36,"line":262},22,[34,264,265],{"class":169},"  \u003C!-- If input is \"42\", v-model stores 42 (number), not \"42\" (string). -->\n",[34,267,269],{"class":36,"line":268},23,[34,270,271],{"class":169},"  \u003C!-- Edge: empty string → \"\" (NOT 0), invalid → original string unchanged. -->\n",[34,273,275],{"class":36,"line":274},24,[34,276,277],{"class":169},"  \u003C!-- Uses parseFloat internally; NaN results keep the original string. -->\n",[34,279,281,283,285,288,290,293,296,298,301],{"class":36,"line":280},25,[34,282,194],{"class":40},[34,284,197],{"class":44},[34,286,287],{"class":48}," v-model.number",[34,289,203],{"class":40},[34,291,292],{"class":68},"\"age\"",[34,294,295],{"class":48}," type",[34,297,203],{"class":40},[34,299,300],{"class":68},"\"number\"",[34,302,209],{"class":40},[34,304,306],{"class":36,"line":305},26,[34,307,76],{"emptyLinePlaceholder":75},[34,309,311],{"class":36,"line":310},27,[34,312,313],{"class":169},"  \u003C!-- ── .trim: strip leading\u002Ftrailing whitespace ────────── -->\n",[34,315,317],{"class":36,"line":316},28,[34,318,319],{"class":169},"  \u003C!-- \"  hello  \" → \"hello\". Does NOT strip internal spaces. -->\n",[34,321,323,325,327,330,332,335],{"class":36,"line":322},29,[34,324,194],{"class":40},[34,326,197],{"class":44},[34,328,329],{"class":48}," v-model.trim",[34,331,203],{"class":40},[34,333,334],{"class":68},"\"name\"",[34,336,209],{"class":40},[34,338,340],{"class":36,"line":339},30,[34,341,76],{"emptyLinePlaceholder":75},[34,343,345],{"class":36,"line":344},31,[34,346,347],{"class":169},"  \u003C!-- ── Chained modifiers: .lazy.number.trim — applied left to right ── -->\n",[34,349,351,353,355,358,360,362],{"class":36,"line":350},32,[34,352,194],{"class":40},[34,354,197],{"class":44},[34,356,357],{"class":48}," v-model.lazy.number.trim",[34,359,203],{"class":40},[34,361,292],{"class":68},[34,363,209],{"class":40},[34,365,367,369,371],{"class":36,"line":366},33,[34,368,144],{"class":40},[34,370,161],{"class":44},[34,372,52],{"class":40},[15,374,376],{"id":375},"custom-input-component-v-model-contract","Custom Input Component — v-model Contract",[20,378,380,696,915,919,1794,1798,2527,2531,2961,2965,3443,3447,3541,3545,3678,3682,3690,3819],{"filename":379,"language":23},"BaseInput.vue",[25,381,383],{"className":27,"code":382,"language":23,"meta":29,"style":29},"\u003Cscript setup>\nimport { computed } from 'vue'\n\n\u002F\u002F ── defineModel (3.4+): declarative v-model without boilerplate ──\n\u002F\u002F Auto-creates: modelValue prop + update:modelValue emit\n\u002F\u002F Can be used directly in template as a writable ref.\nconst model = defineModel\u003Cstring>({\n  default: '',          \u002F\u002F default value if parent doesn't pass v-model\n  required: false,\n})\n\n\u002F\u002F ── Custom modifiers: modelModifiers prop ──────────────\n\u002F\u002F Parent: \u003CBaseInput v-model.capitalize=\"text\" \u002F>\n\u002F\u002F → modelModifiers = { capitalize: true }\nconst { capitalize } = defineProps({\n  modelModifiers: { default: () => ({}) },\n})\n\n\u002F\u002F ── Apply modifier on write ─────────────────────────────\nfunction onInput(e) {\n  let val = e.target.value\n  if (capitalize) val = val.charAt(0).toUpperCase() + val.slice(1)\n  model.value = val  \u002F\u002F defineModel handles the emit automatically\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput :value=\"model\" @input=\"onInput\" class=\"base-input\" \u002F>\n\u003C\u002Ftemplate>\n",[31,384,385,395,406,410,415,420,425,445,458,469,474,478,483,488,493,514,531,535,539,544,561,574,618,631,636,644,648,656,688],{"__ignoreMap":29},[34,386,387,389,391,393],{"class":36,"line":37},[34,388,41],{"class":40},[34,390,45],{"class":44},[34,392,49],{"class":48},[34,394,52],{"class":40},[34,396,397,399,402,404],{"class":36,"line":55},[34,398,59],{"class":58},[34,400,401],{"class":40}," { computed } ",[34,403,65],{"class":58},[34,405,69],{"class":68},[34,407,408],{"class":36,"line":72},[34,409,76],{"emptyLinePlaceholder":75},[34,411,412],{"class":36,"line":79},[34,413,414],{"class":169},"\u002F\u002F ── defineModel (3.4+): declarative v-model without boilerplate ──\n",[34,416,417],{"class":36,"line":104},[34,418,419],{"class":169},"\u002F\u002F Auto-creates: modelValue prop + update:modelValue emit\n",[34,421,422],{"class":36,"line":123},[34,423,424],{"class":169},"\u002F\u002F Can be used directly in template as a writable ref.\n",[34,426,427,429,432,434,437,439,442],{"class":36,"line":141},[34,428,82],{"class":58},[34,430,431],{"class":85}," model",[34,433,89],{"class":58},[34,435,436],{"class":48}," defineModel",[34,438,41],{"class":40},[34,440,441],{"class":85},"string",[34,443,444],{"class":40},">({\n",[34,446,447,450,452,455],{"class":36,"line":151},[34,448,449],{"class":40},"  default: ",[34,451,98],{"class":68},[34,453,454],{"class":40},",          ",[34,456,457],{"class":169},"\u002F\u002F default value if parent doesn't pass v-model\n",[34,459,460,463,466],{"class":36,"line":156},[34,461,462],{"class":40},"  required: ",[34,464,465],{"class":85},"false",[34,467,468],{"class":40},",\n",[34,470,471],{"class":36,"line":166},[34,472,473],{"class":40},"})\n",[34,475,476],{"class":36,"line":173},[34,477,76],{"emptyLinePlaceholder":75},[34,479,480],{"class":36,"line":179},[34,481,482],{"class":169},"\u002F\u002F ── Custom modifiers: modelModifiers prop ──────────────\n",[34,484,485],{"class":36,"line":185},[34,486,487],{"class":169},"\u002F\u002F Parent: \u003CBaseInput v-model.capitalize=\"text\" \u002F>\n",[34,489,490],{"class":36,"line":191},[34,491,492],{"class":169},"\u002F\u002F → modelModifiers = { capitalize: true }\n",[34,494,495,497,500,503,506,508,511],{"class":36,"line":212},[34,496,82],{"class":58},[34,498,499],{"class":40}," { ",[34,501,502],{"class":85},"capitalize",[34,504,505],{"class":40}," } ",[34,507,203],{"class":58},[34,509,510],{"class":48}," defineProps",[34,512,513],{"class":40},"({\n",[34,515,516,519,522,525,528],{"class":36,"line":217},[34,517,518],{"class":40},"  modelModifiers: { ",[34,520,521],{"class":48},"default",[34,523,524],{"class":40},": () ",[34,526,527],{"class":58},"=>",[34,529,530],{"class":40}," ({}) },\n",[34,532,533],{"class":36,"line":223},[34,534,473],{"class":40},[34,536,537],{"class":36,"line":229},[34,538,76],{"emptyLinePlaceholder":75},[34,540,541],{"class":36,"line":235},[34,542,543],{"class":169},"\u002F\u002F ── Apply modifier on write ─────────────────────────────\n",[34,545,546,549,552,554,558],{"class":36,"line":251},[34,547,548],{"class":58},"function",[34,550,551],{"class":48}," onInput",[34,553,95],{"class":40},[34,555,557],{"class":556},"sCrzJ","e",[34,559,560],{"class":40},") {\n",[34,562,563,566,569,571],{"class":36,"line":256},[34,564,565],{"class":58},"  let",[34,567,568],{"class":40}," val ",[34,570,203],{"class":58},[34,572,573],{"class":40}," e.target.value\n",[34,575,576,579,582,584,587,590,592,594,597,600,603,606,608,611,613,616],{"class":36,"line":262},[34,577,578],{"class":58},"  if",[34,580,581],{"class":40}," (capitalize) val ",[34,583,203],{"class":58},[34,585,586],{"class":40}," val.",[34,588,589],{"class":48},"charAt",[34,591,95],{"class":40},[34,593,118],{"class":85},[34,595,596],{"class":40},").",[34,598,599],{"class":48},"toUpperCase",[34,601,602],{"class":40},"() ",[34,604,605],{"class":58},"+",[34,607,586],{"class":40},[34,609,610],{"class":48},"slice",[34,612,95],{"class":40},[34,614,615],{"class":85},"1",[34,617,101],{"class":40},[34,619,620,623,625,628],{"class":36,"line":268},[34,621,622],{"class":40},"  model.value ",[34,624,203],{"class":58},[34,626,627],{"class":40}," val  ",[34,629,630],{"class":169},"\u002F\u002F defineModel handles the emit automatically\n",[34,632,633],{"class":36,"line":274},[34,634,635],{"class":40},"}\n",[34,637,638,640,642],{"class":36,"line":280},[34,639,144],{"class":40},[34,641,45],{"class":44},[34,643,52],{"class":40},[34,645,646],{"class":36,"line":305},[34,647,76],{"emptyLinePlaceholder":75},[34,649,650,652,654],{"class":36,"line":310},[34,651,41],{"class":40},[34,653,161],{"class":44},[34,655,52],{"class":40},[34,657,658,660,662,665,667,670,673,675,678,681,683,686],{"class":36,"line":316},[34,659,194],{"class":40},[34,661,197],{"class":44},[34,663,664],{"class":48}," :value",[34,666,203],{"class":40},[34,668,669],{"class":68},"\"model\"",[34,671,672],{"class":48}," @input",[34,674,203],{"class":40},[34,676,677],{"class":68},"\"onInput\"",[34,679,680],{"class":48}," class",[34,682,203],{"class":40},[34,684,685],{"class":68},"\"base-input\"",[34,687,209],{"class":40},[34,689,690,692,694],{"class":36,"line":322},[34,691,144],{"class":40},[34,693,161],{"class":44},[34,695,52],{"class":40},[20,697,699],{"filename":698,"language":23},"BaseInputLegacy.vue",[25,700,702],{"className":27,"code":701,"language":23,"meta":29,"style":29},"\u003Cscript setup>\n\u002F\u002F ── Pre-3.4: manual v-model contract ──────────────────\nconst props = defineProps({\n  modelValue: { type: String, default: '' },\n  modelModifiers: { default: () => ({}) },\n})\nconst emit = defineEmits(['update:modelValue'])\n\nfunction onInput(e) {\n  let val = e.target.value\n  if (props.modelModifiers.capitalize) {\n    val = val.charAt(0).toUpperCase() + val.slice(1)\n  }\n  emit('update:modelValue', val)\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput :value=\"modelValue\" @input=\"onInput\" \u002F>\n\u003C\u002Ftemplate>\n",[31,703,704,714,719,732,742,754,758,779,783,795,805,812,845,850,862,866,874,878,886,907],{"__ignoreMap":29},[34,705,706,708,710,712],{"class":36,"line":37},[34,707,41],{"class":40},[34,709,45],{"class":44},[34,711,49],{"class":48},[34,713,52],{"class":40},[34,715,716],{"class":36,"line":55},[34,717,718],{"class":169},"\u002F\u002F ── Pre-3.4: manual v-model contract ──────────────────\n",[34,720,721,723,726,728,730],{"class":36,"line":72},[34,722,82],{"class":58},[34,724,725],{"class":85}," props",[34,727,89],{"class":58},[34,729,510],{"class":48},[34,731,513],{"class":40},[34,733,734,737,739],{"class":36,"line":79},[34,735,736],{"class":40},"  modelValue: { type: String, default: ",[34,738,98],{"class":68},[34,740,741],{"class":40}," },\n",[34,743,744,746,748,750,752],{"class":36,"line":104},[34,745,518],{"class":40},[34,747,521],{"class":48},[34,749,524],{"class":40},[34,751,527],{"class":58},[34,753,530],{"class":40},[34,755,756],{"class":36,"line":123},[34,757,473],{"class":40},[34,759,760,762,765,767,770,773,776],{"class":36,"line":141},[34,761,82],{"class":58},[34,763,764],{"class":85}," emit",[34,766,89],{"class":58},[34,768,769],{"class":48}," defineEmits",[34,771,772],{"class":40},"([",[34,774,775],{"class":68},"'update:modelValue'",[34,777,778],{"class":40},"])\n",[34,780,781],{"class":36,"line":151},[34,782,76],{"emptyLinePlaceholder":75},[34,784,785,787,789,791,793],{"class":36,"line":156},[34,786,548],{"class":58},[34,788,551],{"class":48},[34,790,95],{"class":40},[34,792,557],{"class":556},[34,794,560],{"class":40},[34,796,797,799,801,803],{"class":36,"line":166},[34,798,565],{"class":58},[34,800,568],{"class":40},[34,802,203],{"class":58},[34,804,573],{"class":40},[34,806,807,809],{"class":36,"line":173},[34,808,578],{"class":58},[34,810,811],{"class":40}," (props.modelModifiers.capitalize) {\n",[34,813,814,817,819,821,823,825,827,829,831,833,835,837,839,841,843],{"class":36,"line":179},[34,815,816],{"class":40},"    val ",[34,818,203],{"class":58},[34,820,586],{"class":40},[34,822,589],{"class":48},[34,824,95],{"class":40},[34,826,118],{"class":85},[34,828,596],{"class":40},[34,830,599],{"class":48},[34,832,602],{"class":40},[34,834,605],{"class":58},[34,836,586],{"class":40},[34,838,610],{"class":48},[34,840,95],{"class":40},[34,842,615],{"class":85},[34,844,101],{"class":40},[34,846,847],{"class":36,"line":185},[34,848,849],{"class":40},"  }\n",[34,851,852,855,857,859],{"class":36,"line":191},[34,853,854],{"class":48},"  emit",[34,856,95],{"class":40},[34,858,775],{"class":68},[34,860,861],{"class":40},", val)\n",[34,863,864],{"class":36,"line":212},[34,865,635],{"class":40},[34,867,868,870,872],{"class":36,"line":217},[34,869,144],{"class":40},[34,871,45],{"class":44},[34,873,52],{"class":40},[34,875,876],{"class":36,"line":223},[34,877,76],{"emptyLinePlaceholder":75},[34,879,880,882,884],{"class":36,"line":229},[34,881,41],{"class":40},[34,883,161],{"class":44},[34,885,52],{"class":40},[34,887,888,890,892,894,896,899,901,903,905],{"class":36,"line":235},[34,889,194],{"class":40},[34,891,197],{"class":44},[34,893,664],{"class":48},[34,895,203],{"class":40},[34,897,898],{"class":68},"\"modelValue\"",[34,900,672],{"class":48},[34,902,203],{"class":40},[34,904,677],{"class":68},[34,906,209],{"class":40},[34,908,909,911,913],{"class":36,"line":251},[34,910,144],{"class":40},[34,912,161],{"class":44},[34,914,52],{"class":40},[15,916,918],{"id":917},"production-form-reactive-validation-with-schema","Production Form — Reactive Validation with Schema",[20,920,923],{"filename":921,"language":922},"useFormValidation.ts","typescript",[25,924,927],{"className":925,"code":926,"language":922,"meta":29,"style":29},"language-typescript shiki shiki-themes github-light github-dark","import { reactive, computed, ref, type Ref } from 'vue'\n\n\u002F\u002F ── Type-safe form validation composable ────────────────\n\u002F\u002F Rules: each field has an array of validator functions.\n\u002F\u002F Validator returns: true (valid) | string (error message)\n\ntype Validator\u003CT> = (value: T) => true | string\n\nexport function useForm\u003CT extends Record\u003Cstring, any>>(\n  initial: T,\n  rules: { [K in keyof T]?: Validator\u003CT[K]>[] }\n) {\n  const form = reactive({ ...initial })\n  const touched = reactive({} as Record\u003Ckeyof T, boolean>)\n  const submitting = ref(false)\n\n  \u002F\u002F ── Errors: computed — re-evaluates when form or touched change ──\n  const errors = computed(() => {\n    const result: Partial\u003CRecord\u003Ckeyof T, string>> = {}\n    for (const key in rules) {\n      \u002F\u002F Only validate fields the user has touched (UX: no errors on load)\n      if (!touched[key]) continue\n      for (const validator of rules[key] ?? []) {\n        const res = validator(form[key])\n        if (res !== true) {\n          result[key] = res\n          break  \u002F\u002F first error wins — don't stack messages\n        }\n      }\n    }\n    return result\n  })\n\n  const isValid = computed(() => Object.keys(errors.value).length === 0)\n\n  function touch(key: keyof T) { touched[key] = true }\n  function touchAll() {\n    (Object.keys(form) as (keyof T)[]).forEach(k => touched[k] = true)\n  }\n\n  async function submit(handler: (form: T) => Promise\u003Cvoid>) {\n    touchAll()  \u002F\u002F validate all fields on submit attempt\n    if (!isValid.value) return false\n    submitting.value = true\n    try {\n      await handler(form)\n      return true\n    } finally {\n      submitting.value = false\n    }\n  }\n\n  function reset() {\n    Object.assign(form, initial)\n    Object.keys(touched).forEach(k => touched[k] = false)\n  }\n\n  return { form, errors, isValid, submitting, touch, touchAll, submit, reset }\n}\n\n\u002F\u002F ── Usage: ──────────────────────────────────────────────\n\u002F\u002F const { form, errors, submit } = useForm(\n\u002F\u002F   { email: '', password: '' },\n\u002F\u002F   {\n\u002F\u002F     email: [v => !!v || 'Required', v => \u002F.+@.+\u002F.test(v) || 'Invalid email'],\n\u002F\u002F     password: [v => v.length >= 8 || 'Min 8 characters'],\n\u002F\u002F   }\n\u002F\u002F )\n",[31,928,929,946,950,955,960,965,969,1012,1016,1050,1061,1102,1106,1128,1162,1179,1183,1188,1208,1244,1261,1266,1282,1306,1321,1336,1346,1354,1359,1364,1369,1377,1382,1386,1422,1427,1457,1468,1510,1515,1520,1562,1574,1593,1604,1612,1624,1632,1643,1653,1658,1663,1668,1678,1690,1717,1722,1727,1736,1741,1746,1752,1758,1764,1770,1776,1782,1788],{"__ignoreMap":29},[34,930,931,933,936,939,942,944],{"class":36,"line":37},[34,932,59],{"class":58},[34,934,935],{"class":40}," { reactive, computed, ref, ",[34,937,938],{"class":58},"type",[34,940,941],{"class":40}," Ref } ",[34,943,65],{"class":58},[34,945,69],{"class":68},[34,947,948],{"class":36,"line":55},[34,949,76],{"emptyLinePlaceholder":75},[34,951,952],{"class":36,"line":72},[34,953,954],{"class":169},"\u002F\u002F ── Type-safe form validation composable ────────────────\n",[34,956,957],{"class":36,"line":79},[34,958,959],{"class":169},"\u002F\u002F Rules: each field has an array of validator functions.\n",[34,961,962],{"class":36,"line":104},[34,963,964],{"class":169},"\u002F\u002F Validator returns: true (valid) | string (error message)\n",[34,966,967],{"class":36,"line":123},[34,968,76],{"emptyLinePlaceholder":75},[34,970,971,973,976,978,981,984,986,989,992,995,998,1001,1003,1006,1009],{"class":36,"line":141},[34,972,938],{"class":58},[34,974,975],{"class":48}," Validator",[34,977,41],{"class":40},[34,979,980],{"class":48},"T",[34,982,983],{"class":40},"> ",[34,985,203],{"class":58},[34,987,988],{"class":40}," (",[34,990,991],{"class":556},"value",[34,993,994],{"class":58},":",[34,996,997],{"class":48}," T",[34,999,1000],{"class":40},") ",[34,1002,527],{"class":58},[34,1004,1005],{"class":85}," true",[34,1007,1008],{"class":58}," |",[34,1010,1011],{"class":85}," string\n",[34,1013,1014],{"class":36,"line":151},[34,1015,76],{"emptyLinePlaceholder":75},[34,1017,1018,1021,1024,1027,1029,1031,1034,1037,1039,1041,1044,1047],{"class":36,"line":156},[34,1019,1020],{"class":58},"export",[34,1022,1023],{"class":58}," function",[34,1025,1026],{"class":48}," useForm",[34,1028,41],{"class":40},[34,1030,980],{"class":48},[34,1032,1033],{"class":58}," extends",[34,1035,1036],{"class":48}," Record",[34,1038,41],{"class":40},[34,1040,441],{"class":85},[34,1042,1043],{"class":40},", ",[34,1045,1046],{"class":85},"any",[34,1048,1049],{"class":40},">>(\n",[34,1051,1052,1055,1057,1059],{"class":36,"line":166},[34,1053,1054],{"class":556},"  initial",[34,1056,994],{"class":58},[34,1058,997],{"class":48},[34,1060,468],{"class":40},[34,1062,1063,1066,1068,1071,1074,1077,1080,1082,1085,1088,1090,1092,1094,1097,1099],{"class":36,"line":173},[34,1064,1065],{"class":556},"  rules",[34,1067,994],{"class":58},[34,1069,1070],{"class":40}," { [",[34,1072,1073],{"class":48},"K",[34,1075,1076],{"class":58}," in",[34,1078,1079],{"class":58}," keyof",[34,1081,997],{"class":48},[34,1083,1084],{"class":40},"]",[34,1086,1087],{"class":58},"?:",[34,1089,975],{"class":48},[34,1091,41],{"class":40},[34,1093,980],{"class":48},[34,1095,1096],{"class":40},"[",[34,1098,1073],{"class":48},[34,1100,1101],{"class":40},"]>[] }\n",[34,1103,1104],{"class":36,"line":179},[34,1105,560],{"class":40},[34,1107,1108,1111,1114,1116,1119,1122,1125],{"class":36,"line":185},[34,1109,1110],{"class":58},"  const",[34,1112,1113],{"class":85}," form",[34,1115,89],{"class":58},[34,1117,1118],{"class":48}," reactive",[34,1120,1121],{"class":40},"({ ",[34,1123,1124],{"class":58},"...",[34,1126,1127],{"class":40},"initial })\n",[34,1129,1130,1132,1135,1137,1139,1142,1145,1147,1149,1152,1154,1156,1159],{"class":36,"line":191},[34,1131,1110],{"class":58},[34,1133,1134],{"class":85}," touched",[34,1136,89],{"class":58},[34,1138,1118],{"class":48},[34,1140,1141],{"class":40},"({} ",[34,1143,1144],{"class":58},"as",[34,1146,1036],{"class":48},[34,1148,41],{"class":40},[34,1150,1151],{"class":58},"keyof",[34,1153,997],{"class":48},[34,1155,1043],{"class":40},[34,1157,1158],{"class":85},"boolean",[34,1160,1161],{"class":40},">)\n",[34,1163,1164,1166,1169,1171,1173,1175,1177],{"class":36,"line":212},[34,1165,1110],{"class":58},[34,1167,1168],{"class":85}," submitting",[34,1170,89],{"class":58},[34,1172,92],{"class":48},[34,1174,95],{"class":40},[34,1176,465],{"class":85},[34,1178,101],{"class":40},[34,1180,1181],{"class":36,"line":217},[34,1182,76],{"emptyLinePlaceholder":75},[34,1184,1185],{"class":36,"line":223},[34,1186,1187],{"class":169},"  \u002F\u002F ── Errors: computed — re-evaluates when form or touched change ──\n",[34,1189,1190,1192,1195,1197,1200,1203,1205],{"class":36,"line":229},[34,1191,1110],{"class":58},[34,1193,1194],{"class":85}," errors",[34,1196,89],{"class":58},[34,1198,1199],{"class":48}," computed",[34,1201,1202],{"class":40},"(() ",[34,1204,527],{"class":58},[34,1206,1207],{"class":40}," {\n",[34,1209,1210,1213,1216,1218,1221,1223,1226,1228,1230,1232,1234,1236,1239,1241],{"class":36,"line":235},[34,1211,1212],{"class":58},"    const",[34,1214,1215],{"class":85}," result",[34,1217,994],{"class":58},[34,1219,1220],{"class":48}," Partial",[34,1222,41],{"class":40},[34,1224,1225],{"class":48},"Record",[34,1227,41],{"class":40},[34,1229,1151],{"class":58},[34,1231,997],{"class":48},[34,1233,1043],{"class":40},[34,1235,441],{"class":85},[34,1237,1238],{"class":40},">> ",[34,1240,203],{"class":58},[34,1242,1243],{"class":40}," {}\n",[34,1245,1246,1249,1251,1253,1256,1258],{"class":36,"line":251},[34,1247,1248],{"class":58},"    for",[34,1250,988],{"class":40},[34,1252,82],{"class":58},[34,1254,1255],{"class":85}," key",[34,1257,1076],{"class":58},[34,1259,1260],{"class":40}," rules) {\n",[34,1262,1263],{"class":36,"line":256},[34,1264,1265],{"class":169},"      \u002F\u002F Only validate fields the user has touched (UX: no errors on load)\n",[34,1267,1268,1271,1273,1276,1279],{"class":36,"line":262},[34,1269,1270],{"class":58},"      if",[34,1272,988],{"class":40},[34,1274,1275],{"class":58},"!",[34,1277,1278],{"class":40},"touched[key]) ",[34,1280,1281],{"class":58},"continue\n",[34,1283,1284,1287,1289,1291,1294,1297,1300,1303],{"class":36,"line":268},[34,1285,1286],{"class":58},"      for",[34,1288,988],{"class":40},[34,1290,82],{"class":58},[34,1292,1293],{"class":85}," validator",[34,1295,1296],{"class":58}," of",[34,1298,1299],{"class":40}," rules[key] ",[34,1301,1302],{"class":58},"??",[34,1304,1305],{"class":40}," []) {\n",[34,1307,1308,1311,1314,1316,1318],{"class":36,"line":274},[34,1309,1310],{"class":58},"        const",[34,1312,1313],{"class":85}," res",[34,1315,89],{"class":58},[34,1317,1293],{"class":48},[34,1319,1320],{"class":40},"(form[key])\n",[34,1322,1323,1326,1329,1332,1334],{"class":36,"line":280},[34,1324,1325],{"class":58},"        if",[34,1327,1328],{"class":40}," (res ",[34,1330,1331],{"class":58},"!==",[34,1333,1005],{"class":85},[34,1335,560],{"class":40},[34,1337,1338,1341,1343],{"class":36,"line":305},[34,1339,1340],{"class":40},"          result[key] ",[34,1342,203],{"class":58},[34,1344,1345],{"class":40}," res\n",[34,1347,1348,1351],{"class":36,"line":310},[34,1349,1350],{"class":58},"          break",[34,1352,1353],{"class":169},"  \u002F\u002F first error wins — don't stack messages\n",[34,1355,1356],{"class":36,"line":316},[34,1357,1358],{"class":40},"        }\n",[34,1360,1361],{"class":36,"line":322},[34,1362,1363],{"class":40},"      }\n",[34,1365,1366],{"class":36,"line":339},[34,1367,1368],{"class":40},"    }\n",[34,1370,1371,1374],{"class":36,"line":344},[34,1372,1373],{"class":58},"    return",[34,1375,1376],{"class":40}," result\n",[34,1378,1379],{"class":36,"line":350},[34,1380,1381],{"class":40},"  })\n",[34,1383,1384],{"class":36,"line":366},[34,1385,76],{"emptyLinePlaceholder":75},[34,1387,1389,1391,1394,1396,1398,1400,1402,1405,1408,1411,1414,1417,1420],{"class":36,"line":1388},34,[34,1390,1110],{"class":58},[34,1392,1393],{"class":85}," isValid",[34,1395,89],{"class":58},[34,1397,1199],{"class":48},[34,1399,1202],{"class":40},[34,1401,527],{"class":58},[34,1403,1404],{"class":40}," Object.",[34,1406,1407],{"class":48},"keys",[34,1409,1410],{"class":40},"(errors.value).",[34,1412,1413],{"class":85},"length",[34,1415,1416],{"class":58}," ===",[34,1418,1419],{"class":85}," 0",[34,1421,101],{"class":40},[34,1423,1425],{"class":36,"line":1424},35,[34,1426,76],{"emptyLinePlaceholder":75},[34,1428,1430,1433,1436,1438,1441,1443,1445,1447,1450,1452,1454],{"class":36,"line":1429},36,[34,1431,1432],{"class":58},"  function",[34,1434,1435],{"class":48}," touch",[34,1437,95],{"class":40},[34,1439,1440],{"class":556},"key",[34,1442,994],{"class":58},[34,1444,1079],{"class":58},[34,1446,997],{"class":48},[34,1448,1449],{"class":40},") { touched[key] ",[34,1451,203],{"class":58},[34,1453,1005],{"class":85},[34,1455,1456],{"class":40}," }\n",[34,1458,1460,1462,1465],{"class":36,"line":1459},37,[34,1461,1432],{"class":58},[34,1463,1464],{"class":48}," touchAll",[34,1466,1467],{"class":40},"() {\n",[34,1469,1471,1474,1476,1479,1481,1483,1485,1487,1490,1493,1495,1498,1501,1504,1506,1508],{"class":36,"line":1470},38,[34,1472,1473],{"class":40},"    (Object.",[34,1475,1407],{"class":48},[34,1477,1478],{"class":40},"(form) ",[34,1480,1144],{"class":58},[34,1482,988],{"class":40},[34,1484,1151],{"class":58},[34,1486,997],{"class":48},[34,1488,1489],{"class":40},")[]).",[34,1491,1492],{"class":48},"forEach",[34,1494,95],{"class":40},[34,1496,1497],{"class":556},"k",[34,1499,1500],{"class":58}," =>",[34,1502,1503],{"class":40}," touched[k] ",[34,1505,203],{"class":58},[34,1507,1005],{"class":85},[34,1509,101],{"class":40},[34,1511,1513],{"class":36,"line":1512},39,[34,1514,849],{"class":40},[34,1516,1518],{"class":36,"line":1517},40,[34,1519,76],{"emptyLinePlaceholder":75},[34,1521,1523,1526,1528,1531,1533,1536,1538,1540,1543,1545,1547,1549,1551,1554,1556,1559],{"class":36,"line":1522},41,[34,1524,1525],{"class":58},"  async",[34,1527,1023],{"class":58},[34,1529,1530],{"class":48}," submit",[34,1532,95],{"class":40},[34,1534,1535],{"class":48},"handler",[34,1537,994],{"class":58},[34,1539,988],{"class":40},[34,1541,1542],{"class":556},"form",[34,1544,994],{"class":58},[34,1546,997],{"class":48},[34,1548,1000],{"class":40},[34,1550,527],{"class":58},[34,1552,1553],{"class":48}," Promise",[34,1555,41],{"class":40},[34,1557,1558],{"class":85},"void",[34,1560,1561],{"class":40},">) {\n",[34,1563,1565,1568,1571],{"class":36,"line":1564},42,[34,1566,1567],{"class":48},"    touchAll",[34,1569,1570],{"class":40},"()  ",[34,1572,1573],{"class":169},"\u002F\u002F validate all fields on submit attempt\n",[34,1575,1577,1580,1582,1584,1587,1590],{"class":36,"line":1576},43,[34,1578,1579],{"class":58},"    if",[34,1581,988],{"class":40},[34,1583,1275],{"class":58},[34,1585,1586],{"class":40},"isValid.value) ",[34,1588,1589],{"class":58},"return",[34,1591,1592],{"class":85}," false\n",[34,1594,1596,1599,1601],{"class":36,"line":1595},44,[34,1597,1598],{"class":40},"    submitting.value ",[34,1600,203],{"class":58},[34,1602,1603],{"class":85}," true\n",[34,1605,1607,1610],{"class":36,"line":1606},45,[34,1608,1609],{"class":58},"    try",[34,1611,1207],{"class":40},[34,1613,1615,1618,1621],{"class":36,"line":1614},46,[34,1616,1617],{"class":58},"      await",[34,1619,1620],{"class":48}," handler",[34,1622,1623],{"class":40},"(form)\n",[34,1625,1627,1630],{"class":36,"line":1626},47,[34,1628,1629],{"class":58},"      return",[34,1631,1603],{"class":85},[34,1633,1635,1638,1641],{"class":36,"line":1634},48,[34,1636,1637],{"class":40},"    } ",[34,1639,1640],{"class":58},"finally",[34,1642,1207],{"class":40},[34,1644,1646,1649,1651],{"class":36,"line":1645},49,[34,1647,1648],{"class":40},"      submitting.value ",[34,1650,203],{"class":58},[34,1652,1592],{"class":85},[34,1654,1656],{"class":36,"line":1655},50,[34,1657,1368],{"class":40},[34,1659,1661],{"class":36,"line":1660},51,[34,1662,849],{"class":40},[34,1664,1666],{"class":36,"line":1665},52,[34,1667,76],{"emptyLinePlaceholder":75},[34,1669,1671,1673,1676],{"class":36,"line":1670},53,[34,1672,1432],{"class":58},[34,1674,1675],{"class":48}," reset",[34,1677,1467],{"class":40},[34,1679,1681,1684,1687],{"class":36,"line":1680},54,[34,1682,1683],{"class":40},"    Object.",[34,1685,1686],{"class":48},"assign",[34,1688,1689],{"class":40},"(form, initial)\n",[34,1691,1693,1695,1697,1700,1702,1704,1706,1708,1710,1712,1715],{"class":36,"line":1692},55,[34,1694,1683],{"class":40},[34,1696,1407],{"class":48},[34,1698,1699],{"class":40},"(touched).",[34,1701,1492],{"class":48},[34,1703,95],{"class":40},[34,1705,1497],{"class":556},[34,1707,1500],{"class":58},[34,1709,1503],{"class":40},[34,1711,203],{"class":58},[34,1713,1714],{"class":85}," false",[34,1716,101],{"class":40},[34,1718,1720],{"class":36,"line":1719},56,[34,1721,849],{"class":40},[34,1723,1725],{"class":36,"line":1724},57,[34,1726,76],{"emptyLinePlaceholder":75},[34,1728,1730,1733],{"class":36,"line":1729},58,[34,1731,1732],{"class":58},"  return",[34,1734,1735],{"class":40}," { form, errors, isValid, submitting, touch, touchAll, submit, reset }\n",[34,1737,1739],{"class":36,"line":1738},59,[34,1740,635],{"class":40},[34,1742,1744],{"class":36,"line":1743},60,[34,1745,76],{"emptyLinePlaceholder":75},[34,1747,1749],{"class":36,"line":1748},61,[34,1750,1751],{"class":169},"\u002F\u002F ── Usage: ──────────────────────────────────────────────\n",[34,1753,1755],{"class":36,"line":1754},62,[34,1756,1757],{"class":169},"\u002F\u002F const { form, errors, submit } = useForm(\n",[34,1759,1761],{"class":36,"line":1760},63,[34,1762,1763],{"class":169},"\u002F\u002F   { email: '', password: '' },\n",[34,1765,1767],{"class":36,"line":1766},64,[34,1768,1769],{"class":169},"\u002F\u002F   {\n",[34,1771,1773],{"class":36,"line":1772},65,[34,1774,1775],{"class":169},"\u002F\u002F     email: [v => !!v || 'Required', v => \u002F.+@.+\u002F.test(v) || 'Invalid email'],\n",[34,1777,1779],{"class":36,"line":1778},66,[34,1780,1781],{"class":169},"\u002F\u002F     password: [v => v.length >= 8 || 'Min 8 characters'],\n",[34,1783,1785],{"class":36,"line":1784},67,[34,1786,1787],{"class":169},"\u002F\u002F   }\n",[34,1789,1791],{"class":36,"line":1790},68,[34,1792,1793],{"class":169},"\u002F\u002F )\n",[15,1795,1797],{"id":1796},"dynamic-form-generation-schema-driven","Dynamic Form Generation — Schema-Driven",[20,1799,1801],{"filename":1800,"language":23},"DynamicForm.vue",[25,1802,1804],{"className":27,"code":1803,"language":23,"meta":29,"style":29},"\u003Cscript setup>\nimport { ref, computed } from 'vue'\n\n\u002F\u002F ── Schema-driven form: render inputs from a config array ──\nconst schema = ref([\n  { name: 'email', type: 'email', label: 'Email', required: true, placeholder: 'user@example.com' },\n  { name: 'age', type: 'number', label: 'Age', min: 18, max: 120 },\n  { name: 'bio', type: 'textarea', label: 'Bio', rows: 4 },\n  { name: 'role', type: 'select', label: 'Role', options: ['admin', 'user', 'guest'] },\n  { name: 'subscribe', type: 'checkbox', label: 'Subscribe to newsletter' },\n])\n\nconst formData = ref({})\n\n\u002F\u002F ── Initialize form data from schema defaults ──\nschema.value.forEach(field => {\n  formData.value[field.name] = field.type === 'checkbox' ? false : ''\n})\n\n\u002F\u002F ── Computed: map schema type → component ──\nconst fieldComponents = {\n  text: 'input',\n  email: 'input',\n  number: 'input',\n  textarea: 'textarea',\n  select: 'select',\n  checkbox: 'input',\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cform @submit.prevent>\n    \u003Cdiv v-for=\"field in schema\" :key=\"field.name\">\n      \u003Clabel :for=\"field.name\">{{ field.label }}\u003C\u002Flabel>\n\n      \u003C!-- Text\u002FEmail\u002FNumber inputs -->\n      \u003Cinput\n        v-if=\"['text', 'email', 'number'].includes(field.type)\"\n        :type=\"field.type\"\n        v-model=\"formData[field.name]\"\n        :placeholder=\"field.placeholder\"\n        :required=\"field.required\"\n      \u002F>\n\n      \u003C!-- Textarea -->\n      \u003Ctextarea\n        v-else-if=\"field.type === 'textarea'\"\n        v-model=\"formData[field.name]\"\n        :rows=\"field.rows || 3\"\n      \u002F>\n\n      \u003C!-- Select -->\n      \u003Cselect v-else-if=\"field.type === 'select'\" v-model=\"formData[field.name]\">\n        \u003Coption v-for=\"opt in field.options\" :key=\"opt\" :value=\"opt\">{{ opt }}\u003C\u002Foption>\n      \u003C\u002Fselect>\n\n      \u003C!-- Checkbox -->\n      \u003Cinput\n        v-else-if=\"field.type === 'checkbox'\"\n        type=\"checkbox\"\n        v-model=\"formData[field.name]\"\n      \u002F>\n    \u003C\u002Fdiv>\n\n    \u003Cbutton type=\"submit\">Submit\u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n",[31,1805,1806,1816,1827,1831,1836,1850,1883,1914,1939,1975,1994,1998,2002,2016,2020,2025,2041,2068,2072,2076,2081,2092,2102,2111,2120,2129,2138,2147,2151,2159,2163,2171,2182,2208,2230,2234,2239,2246,2256,2266,2276,2286,2296,2301,2305,2310,2317,2327,2335,2345,2353,2357,2362,2386,2421,2430,2434,2439,2445,2454,2464,2472,2476,2485,2489,2510,2519],{"__ignoreMap":29},[34,1807,1808,1810,1812,1814],{"class":36,"line":37},[34,1809,41],{"class":40},[34,1811,45],{"class":44},[34,1813,49],{"class":48},[34,1815,52],{"class":40},[34,1817,1818,1820,1823,1825],{"class":36,"line":55},[34,1819,59],{"class":58},[34,1821,1822],{"class":40}," { ref, computed } ",[34,1824,65],{"class":58},[34,1826,69],{"class":68},[34,1828,1829],{"class":36,"line":72},[34,1830,76],{"emptyLinePlaceholder":75},[34,1832,1833],{"class":36,"line":79},[34,1834,1835],{"class":169},"\u002F\u002F ── Schema-driven form: render inputs from a config array ──\n",[34,1837,1838,1840,1843,1845,1847],{"class":36,"line":104},[34,1839,82],{"class":58},[34,1841,1842],{"class":85}," schema",[34,1844,89],{"class":58},[34,1846,92],{"class":48},[34,1848,1849],{"class":40},"([\n",[34,1851,1852,1855,1858,1861,1863,1866,1869,1872,1875,1878,1881],{"class":36,"line":123},[34,1853,1854],{"class":40},"  { name: ",[34,1856,1857],{"class":68},"'email'",[34,1859,1860],{"class":40},", type: ",[34,1862,1857],{"class":68},[34,1864,1865],{"class":40},", label: ",[34,1867,1868],{"class":68},"'Email'",[34,1870,1871],{"class":40},", required: ",[34,1873,1874],{"class":85},"true",[34,1876,1877],{"class":40},", placeholder: ",[34,1879,1880],{"class":68},"'user@example.com'",[34,1882,741],{"class":40},[34,1884,1885,1887,1890,1892,1895,1897,1900,1903,1906,1909,1912],{"class":36,"line":141},[34,1886,1854],{"class":40},[34,1888,1889],{"class":68},"'age'",[34,1891,1860],{"class":40},[34,1893,1894],{"class":68},"'number'",[34,1896,1865],{"class":40},[34,1898,1899],{"class":68},"'Age'",[34,1901,1902],{"class":40},", min: ",[34,1904,1905],{"class":85},"18",[34,1907,1908],{"class":40},", max: ",[34,1910,1911],{"class":85},"120",[34,1913,741],{"class":40},[34,1915,1916,1918,1921,1923,1926,1928,1931,1934,1937],{"class":36,"line":151},[34,1917,1854],{"class":40},[34,1919,1920],{"class":68},"'bio'",[34,1922,1860],{"class":40},[34,1924,1925],{"class":68},"'textarea'",[34,1927,1865],{"class":40},[34,1929,1930],{"class":68},"'Bio'",[34,1932,1933],{"class":40},", rows: ",[34,1935,1936],{"class":85},"4",[34,1938,741],{"class":40},[34,1940,1941,1943,1946,1948,1951,1953,1956,1959,1962,1964,1967,1969,1972],{"class":36,"line":156},[34,1942,1854],{"class":40},[34,1944,1945],{"class":68},"'role'",[34,1947,1860],{"class":40},[34,1949,1950],{"class":68},"'select'",[34,1952,1865],{"class":40},[34,1954,1955],{"class":68},"'Role'",[34,1957,1958],{"class":40},", options: [",[34,1960,1961],{"class":68},"'admin'",[34,1963,1043],{"class":40},[34,1965,1966],{"class":68},"'user'",[34,1968,1043],{"class":40},[34,1970,1971],{"class":68},"'guest'",[34,1973,1974],{"class":40},"] },\n",[34,1976,1977,1979,1982,1984,1987,1989,1992],{"class":36,"line":166},[34,1978,1854],{"class":40},[34,1980,1981],{"class":68},"'subscribe'",[34,1983,1860],{"class":40},[34,1985,1986],{"class":68},"'checkbox'",[34,1988,1865],{"class":40},[34,1990,1991],{"class":68},"'Subscribe to newsletter'",[34,1993,741],{"class":40},[34,1995,1996],{"class":36,"line":173},[34,1997,778],{"class":40},[34,1999,2000],{"class":36,"line":179},[34,2001,76],{"emptyLinePlaceholder":75},[34,2003,2004,2006,2009,2011,2013],{"class":36,"line":185},[34,2005,82],{"class":58},[34,2007,2008],{"class":85}," formData",[34,2010,89],{"class":58},[34,2012,92],{"class":48},[34,2014,2015],{"class":40},"({})\n",[34,2017,2018],{"class":36,"line":191},[34,2019,76],{"emptyLinePlaceholder":75},[34,2021,2022],{"class":36,"line":212},[34,2023,2024],{"class":169},"\u002F\u002F ── Initialize form data from schema defaults ──\n",[34,2026,2027,2030,2032,2034,2037,2039],{"class":36,"line":217},[34,2028,2029],{"class":40},"schema.value.",[34,2031,1492],{"class":48},[34,2033,95],{"class":40},[34,2035,2036],{"class":556},"field",[34,2038,1500],{"class":58},[34,2040,1207],{"class":40},[34,2042,2043,2046,2048,2051,2054,2057,2060,2062,2065],{"class":36,"line":223},[34,2044,2045],{"class":40},"  formData.value[field.name] ",[34,2047,203],{"class":58},[34,2049,2050],{"class":40}," field.type ",[34,2052,2053],{"class":58},"===",[34,2055,2056],{"class":68}," 'checkbox'",[34,2058,2059],{"class":58}," ?",[34,2061,1714],{"class":85},[34,2063,2064],{"class":58}," :",[34,2066,2067],{"class":68}," ''\n",[34,2069,2070],{"class":36,"line":229},[34,2071,473],{"class":40},[34,2073,2074],{"class":36,"line":235},[34,2075,76],{"emptyLinePlaceholder":75},[34,2077,2078],{"class":36,"line":251},[34,2079,2080],{"class":169},"\u002F\u002F ── Computed: map schema type → component ──\n",[34,2082,2083,2085,2088,2090],{"class":36,"line":256},[34,2084,82],{"class":58},[34,2086,2087],{"class":85}," fieldComponents",[34,2089,89],{"class":58},[34,2091,1207],{"class":40},[34,2093,2094,2097,2100],{"class":36,"line":262},[34,2095,2096],{"class":40},"  text: ",[34,2098,2099],{"class":68},"'input'",[34,2101,468],{"class":40},[34,2103,2104,2107,2109],{"class":36,"line":268},[34,2105,2106],{"class":40},"  email: ",[34,2108,2099],{"class":68},[34,2110,468],{"class":40},[34,2112,2113,2116,2118],{"class":36,"line":274},[34,2114,2115],{"class":40},"  number: ",[34,2117,2099],{"class":68},[34,2119,468],{"class":40},[34,2121,2122,2125,2127],{"class":36,"line":280},[34,2123,2124],{"class":40},"  textarea: ",[34,2126,1925],{"class":68},[34,2128,468],{"class":40},[34,2130,2131,2134,2136],{"class":36,"line":305},[34,2132,2133],{"class":40},"  select: ",[34,2135,1950],{"class":68},[34,2137,468],{"class":40},[34,2139,2140,2143,2145],{"class":36,"line":310},[34,2141,2142],{"class":40},"  checkbox: ",[34,2144,2099],{"class":68},[34,2146,468],{"class":40},[34,2148,2149],{"class":36,"line":316},[34,2150,635],{"class":40},[34,2152,2153,2155,2157],{"class":36,"line":322},[34,2154,144],{"class":40},[34,2156,45],{"class":44},[34,2158,52],{"class":40},[34,2160,2161],{"class":36,"line":339},[34,2162,76],{"emptyLinePlaceholder":75},[34,2164,2165,2167,2169],{"class":36,"line":344},[34,2166,41],{"class":40},[34,2168,161],{"class":44},[34,2170,52],{"class":40},[34,2172,2173,2175,2177,2180],{"class":36,"line":350},[34,2174,194],{"class":40},[34,2176,1542],{"class":44},[34,2178,2179],{"class":48}," @submit.prevent",[34,2181,52],{"class":40},[34,2183,2184,2187,2190,2193,2195,2198,2201,2203,2206],{"class":36,"line":366},[34,2185,2186],{"class":40},"    \u003C",[34,2188,2189],{"class":44},"div",[34,2191,2192],{"class":48}," v-for",[34,2194,203],{"class":40},[34,2196,2197],{"class":68},"\"field in schema\"",[34,2199,2200],{"class":48}," :key",[34,2202,203],{"class":40},[34,2204,2205],{"class":68},"\"field.name\"",[34,2207,52],{"class":40},[34,2209,2210,2213,2216,2219,2221,2223,2226,2228],{"class":36,"line":1388},[34,2211,2212],{"class":40},"      \u003C",[34,2214,2215],{"class":44},"label",[34,2217,2218],{"class":48}," :for",[34,2220,203],{"class":40},[34,2222,2205],{"class":68},[34,2224,2225],{"class":40},">{{ field.label }}\u003C\u002F",[34,2227,2215],{"class":44},[34,2229,52],{"class":40},[34,2231,2232],{"class":36,"line":1424},[34,2233,76],{"emptyLinePlaceholder":75},[34,2235,2236],{"class":36,"line":1429},[34,2237,2238],{"class":169},"      \u003C!-- Text\u002FEmail\u002FNumber inputs -->\n",[34,2240,2241,2243],{"class":36,"line":1459},[34,2242,2212],{"class":40},[34,2244,2245],{"class":44},"input\n",[34,2247,2248,2251,2253],{"class":36,"line":1470},[34,2249,2250],{"class":48},"        v-if",[34,2252,203],{"class":40},[34,2254,2255],{"class":68},"\"['text', 'email', 'number'].includes(field.type)\"\n",[34,2257,2258,2261,2263],{"class":36,"line":1512},[34,2259,2260],{"class":48},"        :type",[34,2262,203],{"class":40},[34,2264,2265],{"class":68},"\"field.type\"\n",[34,2267,2268,2271,2273],{"class":36,"line":1517},[34,2269,2270],{"class":48},"        v-model",[34,2272,203],{"class":40},[34,2274,2275],{"class":68},"\"formData[field.name]\"\n",[34,2277,2278,2281,2283],{"class":36,"line":1522},[34,2279,2280],{"class":48},"        :placeholder",[34,2282,203],{"class":40},[34,2284,2285],{"class":68},"\"field.placeholder\"\n",[34,2287,2288,2291,2293],{"class":36,"line":1564},[34,2289,2290],{"class":48},"        :required",[34,2292,203],{"class":40},[34,2294,2295],{"class":68},"\"field.required\"\n",[34,2297,2298],{"class":36,"line":1576},[34,2299,2300],{"class":40},"      \u002F>\n",[34,2302,2303],{"class":36,"line":1595},[34,2304,76],{"emptyLinePlaceholder":75},[34,2306,2307],{"class":36,"line":1606},[34,2308,2309],{"class":169},"      \u003C!-- Textarea -->\n",[34,2311,2312,2314],{"class":36,"line":1614},[34,2313,2212],{"class":40},[34,2315,2316],{"class":44},"textarea\n",[34,2318,2319,2322,2324],{"class":36,"line":1626},[34,2320,2321],{"class":48},"        v-else-if",[34,2323,203],{"class":40},[34,2325,2326],{"class":68},"\"field.type === 'textarea'\"\n",[34,2328,2329,2331,2333],{"class":36,"line":1634},[34,2330,2270],{"class":48},[34,2332,203],{"class":40},[34,2334,2275],{"class":68},[34,2336,2337,2340,2342],{"class":36,"line":1645},[34,2338,2339],{"class":48},"        :rows",[34,2341,203],{"class":40},[34,2343,2344],{"class":68},"\"field.rows || 3\"\n",[34,2346,2347,2351],{"class":36,"line":1655},[34,2348,2350],{"class":2349},"sMKGj","      \u002F",[34,2352,52],{"class":40},[34,2354,2355],{"class":36,"line":1660},[34,2356,76],{"emptyLinePlaceholder":75},[34,2358,2359],{"class":36,"line":1665},[34,2360,2361],{"class":169},"      \u003C!-- Select -->\n",[34,2363,2364,2366,2369,2372,2374,2377,2379,2381,2384],{"class":36,"line":1670},[34,2365,2212],{"class":40},[34,2367,2368],{"class":44},"select",[34,2370,2371],{"class":48}," v-else-if",[34,2373,203],{"class":40},[34,2375,2376],{"class":68},"\"field.type === 'select'\"",[34,2378,200],{"class":48},[34,2380,203],{"class":40},[34,2382,2383],{"class":68},"\"formData[field.name]\"",[34,2385,52],{"class":40},[34,2387,2388,2391,2394,2396,2398,2401,2403,2405,2408,2410,2412,2414,2417,2419],{"class":36,"line":1680},[34,2389,2390],{"class":40},"        \u003C",[34,2392,2393],{"class":44},"option",[34,2395,2192],{"class":48},[34,2397,203],{"class":40},[34,2399,2400],{"class":68},"\"opt in field.options\"",[34,2402,2200],{"class":48},[34,2404,203],{"class":40},[34,2406,2407],{"class":68},"\"opt\"",[34,2409,664],{"class":48},[34,2411,203],{"class":40},[34,2413,2407],{"class":68},[34,2415,2416],{"class":40},">{{ opt }}\u003C\u002F",[34,2418,2393],{"class":44},[34,2420,52],{"class":40},[34,2422,2423,2426,2428],{"class":36,"line":1692},[34,2424,2425],{"class":40},"      \u003C\u002F",[34,2427,2368],{"class":44},[34,2429,52],{"class":40},[34,2431,2432],{"class":36,"line":1719},[34,2433,76],{"emptyLinePlaceholder":75},[34,2435,2436],{"class":36,"line":1724},[34,2437,2438],{"class":169},"      \u003C!-- Checkbox -->\n",[34,2440,2441,2443],{"class":36,"line":1729},[34,2442,2212],{"class":40},[34,2444,2245],{"class":44},[34,2446,2447,2449,2451],{"class":36,"line":1738},[34,2448,2321],{"class":48},[34,2450,203],{"class":40},[34,2452,2453],{"class":68},"\"field.type === 'checkbox'\"\n",[34,2455,2456,2459,2461],{"class":36,"line":1743},[34,2457,2458],{"class":48},"        type",[34,2460,203],{"class":40},[34,2462,2463],{"class":68},"\"checkbox\"\n",[34,2465,2466,2468,2470],{"class":36,"line":1748},[34,2467,2270],{"class":48},[34,2469,203],{"class":40},[34,2471,2275],{"class":68},[34,2473,2474],{"class":36,"line":1754},[34,2475,2300],{"class":40},[34,2477,2478,2481,2483],{"class":36,"line":1760},[34,2479,2480],{"class":40},"    \u003C\u002F",[34,2482,2189],{"class":44},[34,2484,52],{"class":40},[34,2486,2487],{"class":36,"line":1766},[34,2488,76],{"emptyLinePlaceholder":75},[34,2490,2491,2493,2496,2498,2500,2503,2506,2508],{"class":36,"line":1772},[34,2492,2186],{"class":40},[34,2494,2495],{"class":44},"button",[34,2497,295],{"class":48},[34,2499,203],{"class":40},[34,2501,2502],{"class":68},"\"submit\"",[34,2504,2505],{"class":40},">Submit\u003C\u002F",[34,2507,2495],{"class":44},[34,2509,52],{"class":40},[34,2511,2512,2515,2517],{"class":36,"line":1778},[34,2513,2514],{"class":40},"  \u003C\u002F",[34,2516,1542],{"class":44},[34,2518,52],{"class":40},[34,2520,2521,2523,2525],{"class":36,"line":1784},[34,2522,144],{"class":40},[34,2524,161],{"class":44},[34,2526,52],{"class":40},[15,2528,2530],{"id":2529},"debounced-search-input-custom-modifier","Debounced Search Input — Custom Modifier",[20,2532,2534],{"filename":2533,"language":23},"DebouncedInput.vue",[25,2535,2537],{"className":27,"code":2536,"language":23,"meta":29,"style":29},"\u003Cscript setup>\nimport { ref, watch, customRef } from 'vue'\n\n\u002F\u002F ── Debounced ref factory: delays updates until typing pauses ──\nfunction useDebouncedRef(initial, delay = 300) {\n  let value = initial\n  let timer\n  return customRef((track, trigger) => ({\n    get() { track(); return value },\n    set(newVal) {\n      clearTimeout(timer)\n      timer = setTimeout(() => { value = newVal; trigger() }, delay)\n    },\n  }))\n}\n\nconst search = useDebouncedRef('', 300)\nconst results = ref([])\n\n\u002F\u002F ── Watch fires 300ms after last keystroke, not on every keystroke ──\nwatch(search, async (q) => {\n  if (!q) { results.value = []; return }\n  const res = await fetch(`\u002Fapi\u002Fsearch?q=${q}`)\n  results.value = await res.json()\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- Input updates immediately (user sees what they type),\n       but the ref (and watchers) only update 300ms after typing stops. -->\n  \u003Cinput v-model=\"search\" placeholder=\"Search…\" \u002F>\n  \u003Cul>\n    \u003Cli v-for=\"r in results\" :key=\"r.id\">{{ r.name }}\u003C\u002Fli>\n  \u003C\u002Ful>\n\u003C\u002Ftemplate>\n",[31,2538,2539,2549,2560,2564,2569,2593,2605,2612,2637,2655,2667,2675,2702,2707,2712,2716,2720,2742,2756,2760,2765,2787,2807,2833,2851,2855,2863,2867,2875,2880,2885,2908,2917,2945,2953],{"__ignoreMap":29},[34,2540,2541,2543,2545,2547],{"class":36,"line":37},[34,2542,41],{"class":40},[34,2544,45],{"class":44},[34,2546,49],{"class":48},[34,2548,52],{"class":40},[34,2550,2551,2553,2556,2558],{"class":36,"line":55},[34,2552,59],{"class":58},[34,2554,2555],{"class":40}," { ref, watch, customRef } ",[34,2557,65],{"class":58},[34,2559,69],{"class":68},[34,2561,2562],{"class":36,"line":72},[34,2563,76],{"emptyLinePlaceholder":75},[34,2565,2566],{"class":36,"line":79},[34,2567,2568],{"class":169},"\u002F\u002F ── Debounced ref factory: delays updates until typing pauses ──\n",[34,2570,2571,2573,2576,2578,2581,2583,2586,2588,2591],{"class":36,"line":104},[34,2572,548],{"class":58},[34,2574,2575],{"class":48}," useDebouncedRef",[34,2577,95],{"class":40},[34,2579,2580],{"class":556},"initial",[34,2582,1043],{"class":40},[34,2584,2585],{"class":556},"delay",[34,2587,89],{"class":58},[34,2589,2590],{"class":85}," 300",[34,2592,560],{"class":40},[34,2594,2595,2597,2600,2602],{"class":36,"line":123},[34,2596,565],{"class":58},[34,2598,2599],{"class":40}," value ",[34,2601,203],{"class":58},[34,2603,2604],{"class":40}," initial\n",[34,2606,2607,2609],{"class":36,"line":141},[34,2608,565],{"class":58},[34,2610,2611],{"class":40}," timer\n",[34,2613,2614,2616,2619,2622,2625,2627,2630,2632,2634],{"class":36,"line":151},[34,2615,1732],{"class":58},[34,2617,2618],{"class":48}," customRef",[34,2620,2621],{"class":40},"((",[34,2623,2624],{"class":556},"track",[34,2626,1043],{"class":40},[34,2628,2629],{"class":556},"trigger",[34,2631,1000],{"class":40},[34,2633,527],{"class":58},[34,2635,2636],{"class":40}," ({\n",[34,2638,2639,2642,2645,2647,2650,2652],{"class":36,"line":156},[34,2640,2641],{"class":48},"    get",[34,2643,2644],{"class":40},"() { ",[34,2646,2624],{"class":48},[34,2648,2649],{"class":40},"(); ",[34,2651,1589],{"class":58},[34,2653,2654],{"class":40}," value },\n",[34,2656,2657,2660,2662,2665],{"class":36,"line":166},[34,2658,2659],{"class":48},"    set",[34,2661,95],{"class":40},[34,2663,2664],{"class":556},"newVal",[34,2666,560],{"class":40},[34,2668,2669,2672],{"class":36,"line":173},[34,2670,2671],{"class":48},"      clearTimeout",[34,2673,2674],{"class":40},"(timer)\n",[34,2676,2677,2680,2682,2685,2687,2689,2692,2694,2697,2699],{"class":36,"line":179},[34,2678,2679],{"class":40},"      timer ",[34,2681,203],{"class":58},[34,2683,2684],{"class":48}," setTimeout",[34,2686,1202],{"class":40},[34,2688,527],{"class":58},[34,2690,2691],{"class":40}," { value ",[34,2693,203],{"class":58},[34,2695,2696],{"class":40}," newVal; ",[34,2698,2629],{"class":48},[34,2700,2701],{"class":40},"() }, delay)\n",[34,2703,2704],{"class":36,"line":185},[34,2705,2706],{"class":40},"    },\n",[34,2708,2709],{"class":36,"line":191},[34,2710,2711],{"class":40},"  }))\n",[34,2713,2714],{"class":36,"line":212},[34,2715,635],{"class":40},[34,2717,2718],{"class":36,"line":217},[34,2719,76],{"emptyLinePlaceholder":75},[34,2721,2722,2724,2727,2729,2731,2733,2735,2737,2740],{"class":36,"line":223},[34,2723,82],{"class":58},[34,2725,2726],{"class":85}," search",[34,2728,89],{"class":58},[34,2730,2575],{"class":48},[34,2732,95],{"class":40},[34,2734,98],{"class":68},[34,2736,1043],{"class":40},[34,2738,2739],{"class":85},"300",[34,2741,101],{"class":40},[34,2743,2744,2746,2749,2751,2753],{"class":36,"line":229},[34,2745,82],{"class":58},[34,2747,2748],{"class":85}," results",[34,2750,89],{"class":58},[34,2752,92],{"class":48},[34,2754,2755],{"class":40},"([])\n",[34,2757,2758],{"class":36,"line":235},[34,2759,76],{"emptyLinePlaceholder":75},[34,2761,2762],{"class":36,"line":251},[34,2763,2764],{"class":169},"\u002F\u002F ── Watch fires 300ms after last keystroke, not on every keystroke ──\n",[34,2766,2767,2770,2773,2776,2778,2781,2783,2785],{"class":36,"line":256},[34,2768,2769],{"class":48},"watch",[34,2771,2772],{"class":40},"(search, ",[34,2774,2775],{"class":58},"async",[34,2777,988],{"class":40},[34,2779,2780],{"class":556},"q",[34,2782,1000],{"class":40},[34,2784,527],{"class":58},[34,2786,1207],{"class":40},[34,2788,2789,2791,2793,2795,2798,2800,2803,2805],{"class":36,"line":262},[34,2790,578],{"class":58},[34,2792,988],{"class":40},[34,2794,1275],{"class":58},[34,2796,2797],{"class":40},"q) { results.value ",[34,2799,203],{"class":58},[34,2801,2802],{"class":40}," []; ",[34,2804,1589],{"class":58},[34,2806,1456],{"class":40},[34,2808,2809,2811,2813,2815,2818,2821,2823,2826,2828,2831],{"class":36,"line":268},[34,2810,1110],{"class":58},[34,2812,1313],{"class":85},[34,2814,89],{"class":58},[34,2816,2817],{"class":58}," await",[34,2819,2820],{"class":48}," fetch",[34,2822,95],{"class":40},[34,2824,2825],{"class":68},"`\u002Fapi\u002Fsearch?q=${",[34,2827,2780],{"class":40},[34,2829,2830],{"class":68},"}`",[34,2832,101],{"class":40},[34,2834,2835,2838,2840,2842,2845,2848],{"class":36,"line":274},[34,2836,2837],{"class":40},"  results.value ",[34,2839,203],{"class":58},[34,2841,2817],{"class":58},[34,2843,2844],{"class":40}," res.",[34,2846,2847],{"class":48},"json",[34,2849,2850],{"class":40},"()\n",[34,2852,2853],{"class":36,"line":280},[34,2854,473],{"class":40},[34,2856,2857,2859,2861],{"class":36,"line":305},[34,2858,144],{"class":40},[34,2860,45],{"class":44},[34,2862,52],{"class":40},[34,2864,2865],{"class":36,"line":310},[34,2866,76],{"emptyLinePlaceholder":75},[34,2868,2869,2871,2873],{"class":36,"line":316},[34,2870,41],{"class":40},[34,2872,161],{"class":44},[34,2874,52],{"class":40},[34,2876,2877],{"class":36,"line":322},[34,2878,2879],{"class":169},"  \u003C!-- Input updates immediately (user sees what they type),\n",[34,2881,2882],{"class":36,"line":339},[34,2883,2884],{"class":169},"       but the ref (and watchers) only update 300ms after typing stops. -->\n",[34,2886,2887,2889,2891,2893,2895,2898,2901,2903,2906],{"class":36,"line":344},[34,2888,194],{"class":40},[34,2890,197],{"class":44},[34,2892,200],{"class":48},[34,2894,203],{"class":40},[34,2896,2897],{"class":68},"\"search\"",[34,2899,2900],{"class":48}," placeholder",[34,2902,203],{"class":40},[34,2904,2905],{"class":68},"\"Search…\"",[34,2907,209],{"class":40},[34,2909,2910,2912,2915],{"class":36,"line":350},[34,2911,194],{"class":40},[34,2913,2914],{"class":44},"ul",[34,2916,52],{"class":40},[34,2918,2919,2921,2924,2926,2928,2931,2933,2935,2938,2941,2943],{"class":36,"line":366},[34,2920,2186],{"class":40},[34,2922,2923],{"class":44},"li",[34,2925,2192],{"class":48},[34,2927,203],{"class":40},[34,2929,2930],{"class":68},"\"r in results\"",[34,2932,2200],{"class":48},[34,2934,203],{"class":40},[34,2936,2937],{"class":68},"\"r.id\"",[34,2939,2940],{"class":40},">{{ r.name }}\u003C\u002F",[34,2942,2923],{"class":44},[34,2944,52],{"class":40},[34,2946,2947,2949,2951],{"class":36,"line":1388},[34,2948,2514],{"class":40},[34,2950,2914],{"class":44},[34,2952,52],{"class":40},[34,2954,2955,2957,2959],{"class":36,"line":1424},[34,2956,144],{"class":40},[34,2958,161],{"class":44},[34,2960,52],{"class":40},[15,2962,2964],{"id":2963},"multiple-v-model-form-component-pattern","Multiple v-model — Form Component Pattern",[20,2966,2968],{"filename":2967,"language":23},"RegistrationForm.vue",[25,2969,2971],{"className":27,"code":2970,"language":23,"meta":29,"style":29},"\u003Cscript setup>\nimport { computed } from 'vue'\n\n\u002F\u002F ── Two named v-models: one for email, one for password ──\n\u002F\u002F Parent: \u003CRegistrationForm v-model:email=\"email\" v-model:password=\"password\" \u002F>\nconst email = defineModel\u003Cstring>('email')\nconst password = defineModel\u003Cstring>('password')\n\n\u002F\u002F ── Computed validation on each model ──\nconst emailError = computed(() => {\n  if (!email.value) return ''\n  return \u002F.+@.+\\..+\u002F.test(email.value) ? '' : 'Invalid email'\n})\n\nconst passwordError = computed(() => {\n  if (!password.value) return ''\n  return password.value.length >= 8 ? '' : 'Min 8 characters'\n})\n\nconst isValid = computed(() =>\n  !emailError.value && !passwordError.value && email.value && password.value\n)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cinput v-model=\"email\" type=\"email\" placeholder=\"Email\" \u002F>\n    \u003Cspan v-if=\"emailError\" class=\"error\">{{ emailError }}\u003C\u002Fspan>\n\n    \u003Cinput v-model=\"password\" type=\"password\" placeholder=\"Password\" \u002F>\n    \u003Cspan v-if=\"passwordError\" class=\"error\">{{ passwordError }}\u003C\u002Fspan>\n\n    \u003Cbutton :disabled=\"!isValid\">Register\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[31,2972,2973,2983,2993,2997,3002,3007,3029,3051,3055,3060,3077,3092,3142,3146,3150,3167,3182,3206,3210,3214,3229,3256,3260,3268,3272,3280,3288,3316,3344,3348,3376,3402,3406,3427,3435],{"__ignoreMap":29},[34,2974,2975,2977,2979,2981],{"class":36,"line":37},[34,2976,41],{"class":40},[34,2978,45],{"class":44},[34,2980,49],{"class":48},[34,2982,52],{"class":40},[34,2984,2985,2987,2989,2991],{"class":36,"line":55},[34,2986,59],{"class":58},[34,2988,401],{"class":40},[34,2990,65],{"class":58},[34,2992,69],{"class":68},[34,2994,2995],{"class":36,"line":72},[34,2996,76],{"emptyLinePlaceholder":75},[34,2998,2999],{"class":36,"line":79},[34,3000,3001],{"class":169},"\u002F\u002F ── Two named v-models: one for email, one for password ──\n",[34,3003,3004],{"class":36,"line":104},[34,3005,3006],{"class":169},"\u002F\u002F Parent: \u003CRegistrationForm v-model:email=\"email\" v-model:password=\"password\" \u002F>\n",[34,3008,3009,3011,3014,3016,3018,3020,3022,3025,3027],{"class":36,"line":123},[34,3010,82],{"class":58},[34,3012,3013],{"class":85}," email",[34,3015,89],{"class":58},[34,3017,436],{"class":48},[34,3019,41],{"class":40},[34,3021,441],{"class":85},[34,3023,3024],{"class":40},">(",[34,3026,1857],{"class":68},[34,3028,101],{"class":40},[34,3030,3031,3033,3036,3038,3040,3042,3044,3046,3049],{"class":36,"line":141},[34,3032,82],{"class":58},[34,3034,3035],{"class":85}," password",[34,3037,89],{"class":58},[34,3039,436],{"class":48},[34,3041,41],{"class":40},[34,3043,441],{"class":85},[34,3045,3024],{"class":40},[34,3047,3048],{"class":68},"'password'",[34,3050,101],{"class":40},[34,3052,3053],{"class":36,"line":151},[34,3054,76],{"emptyLinePlaceholder":75},[34,3056,3057],{"class":36,"line":156},[34,3058,3059],{"class":169},"\u002F\u002F ── Computed validation on each model ──\n",[34,3061,3062,3064,3067,3069,3071,3073,3075],{"class":36,"line":166},[34,3063,82],{"class":58},[34,3065,3066],{"class":85}," emailError",[34,3068,89],{"class":58},[34,3070,1199],{"class":48},[34,3072,1202],{"class":40},[34,3074,527],{"class":58},[34,3076,1207],{"class":40},[34,3078,3079,3081,3083,3085,3088,3090],{"class":36,"line":173},[34,3080,578],{"class":58},[34,3082,988],{"class":40},[34,3084,1275],{"class":58},[34,3086,3087],{"class":40},"email.value) ",[34,3089,1589],{"class":58},[34,3091,2067],{"class":68},[34,3093,3094,3096,3099,3102,3104,3108,3110,3112,3116,3118,3120,3123,3125,3128,3131,3134,3137,3139],{"class":36,"line":179},[34,3095,1732],{"class":58},[34,3097,3098],{"class":68}," \u002F",[34,3100,3101],{"class":85},".",[34,3103,605],{"class":58},[34,3105,3107],{"class":3106},"svAP2","@",[34,3109,3101],{"class":85},[34,3111,605],{"class":58},[34,3113,3115],{"class":3114},"snRuI","\\.",[34,3117,3101],{"class":85},[34,3119,605],{"class":58},[34,3121,3122],{"class":68},"\u002F",[34,3124,3101],{"class":40},[34,3126,3127],{"class":48},"test",[34,3129,3130],{"class":40},"(email.value) ",[34,3132,3133],{"class":58},"?",[34,3135,3136],{"class":68}," ''",[34,3138,2064],{"class":58},[34,3140,3141],{"class":68}," 'Invalid email'\n",[34,3143,3144],{"class":36,"line":185},[34,3145,473],{"class":40},[34,3147,3148],{"class":36,"line":191},[34,3149,76],{"emptyLinePlaceholder":75},[34,3151,3152,3154,3157,3159,3161,3163,3165],{"class":36,"line":212},[34,3153,82],{"class":58},[34,3155,3156],{"class":85}," passwordError",[34,3158,89],{"class":58},[34,3160,1199],{"class":48},[34,3162,1202],{"class":40},[34,3164,527],{"class":58},[34,3166,1207],{"class":40},[34,3168,3169,3171,3173,3175,3178,3180],{"class":36,"line":217},[34,3170,578],{"class":58},[34,3172,988],{"class":40},[34,3174,1275],{"class":58},[34,3176,3177],{"class":40},"password.value) ",[34,3179,1589],{"class":58},[34,3181,2067],{"class":68},[34,3183,3184,3186,3189,3191,3194,3197,3199,3201,3203],{"class":36,"line":223},[34,3185,1732],{"class":58},[34,3187,3188],{"class":40}," password.value.",[34,3190,1413],{"class":85},[34,3192,3193],{"class":58}," >=",[34,3195,3196],{"class":85}," 8",[34,3198,2059],{"class":58},[34,3200,3136],{"class":68},[34,3202,2064],{"class":58},[34,3204,3205],{"class":68}," 'Min 8 characters'\n",[34,3207,3208],{"class":36,"line":229},[34,3209,473],{"class":40},[34,3211,3212],{"class":36,"line":235},[34,3213,76],{"emptyLinePlaceholder":75},[34,3215,3216,3218,3220,3222,3224,3226],{"class":36,"line":251},[34,3217,82],{"class":58},[34,3219,1393],{"class":85},[34,3221,89],{"class":58},[34,3223,1199],{"class":48},[34,3225,1202],{"class":40},[34,3227,3228],{"class":58},"=>\n",[34,3230,3231,3234,3237,3240,3243,3246,3248,3251,3253],{"class":36,"line":256},[34,3232,3233],{"class":58},"  !",[34,3235,3236],{"class":40},"emailError.value ",[34,3238,3239],{"class":58},"&&",[34,3241,3242],{"class":58}," !",[34,3244,3245],{"class":40},"passwordError.value ",[34,3247,3239],{"class":58},[34,3249,3250],{"class":40}," email.value ",[34,3252,3239],{"class":58},[34,3254,3255],{"class":40}," password.value\n",[34,3257,3258],{"class":36,"line":262},[34,3259,101],{"class":40},[34,3261,3262,3264,3266],{"class":36,"line":268},[34,3263,144],{"class":40},[34,3265,45],{"class":44},[34,3267,52],{"class":40},[34,3269,3270],{"class":36,"line":274},[34,3271,76],{"emptyLinePlaceholder":75},[34,3273,3274,3276,3278],{"class":36,"line":280},[34,3275,41],{"class":40},[34,3277,161],{"class":44},[34,3279,52],{"class":40},[34,3281,3282,3284,3286],{"class":36,"line":305},[34,3283,194],{"class":40},[34,3285,2189],{"class":44},[34,3287,52],{"class":40},[34,3289,3290,3292,3294,3296,3298,3301,3303,3305,3307,3309,3311,3314],{"class":36,"line":310},[34,3291,2186],{"class":40},[34,3293,197],{"class":44},[34,3295,200],{"class":48},[34,3297,203],{"class":40},[34,3299,3300],{"class":68},"\"email\"",[34,3302,295],{"class":48},[34,3304,203],{"class":40},[34,3306,3300],{"class":68},[34,3308,2900],{"class":48},[34,3310,203],{"class":40},[34,3312,3313],{"class":68},"\"Email\"",[34,3315,209],{"class":40},[34,3317,3318,3320,3322,3325,3327,3330,3332,3334,3337,3340,3342],{"class":36,"line":316},[34,3319,2186],{"class":40},[34,3321,34],{"class":44},[34,3323,3324],{"class":48}," v-if",[34,3326,203],{"class":40},[34,3328,3329],{"class":68},"\"emailError\"",[34,3331,680],{"class":48},[34,3333,203],{"class":40},[34,3335,3336],{"class":68},"\"error\"",[34,3338,3339],{"class":40},">{{ emailError }}\u003C\u002F",[34,3341,34],{"class":44},[34,3343,52],{"class":40},[34,3345,3346],{"class":36,"line":322},[34,3347,76],{"emptyLinePlaceholder":75},[34,3349,3350,3352,3354,3356,3358,3361,3363,3365,3367,3369,3371,3374],{"class":36,"line":339},[34,3351,2186],{"class":40},[34,3353,197],{"class":44},[34,3355,200],{"class":48},[34,3357,203],{"class":40},[34,3359,3360],{"class":68},"\"password\"",[34,3362,295],{"class":48},[34,3364,203],{"class":40},[34,3366,3360],{"class":68},[34,3368,2900],{"class":48},[34,3370,203],{"class":40},[34,3372,3373],{"class":68},"\"Password\"",[34,3375,209],{"class":40},[34,3377,3378,3380,3382,3384,3386,3389,3391,3393,3395,3398,3400],{"class":36,"line":344},[34,3379,2186],{"class":40},[34,3381,34],{"class":44},[34,3383,3324],{"class":48},[34,3385,203],{"class":40},[34,3387,3388],{"class":68},"\"passwordError\"",[34,3390,680],{"class":48},[34,3392,203],{"class":40},[34,3394,3336],{"class":68},[34,3396,3397],{"class":40},">{{ passwordError }}\u003C\u002F",[34,3399,34],{"class":44},[34,3401,52],{"class":40},[34,3403,3404],{"class":36,"line":350},[34,3405,76],{"emptyLinePlaceholder":75},[34,3407,3408,3410,3412,3415,3417,3420,3423,3425],{"class":36,"line":366},[34,3409,2186],{"class":40},[34,3411,2495],{"class":44},[34,3413,3414],{"class":48}," :disabled",[34,3416,203],{"class":40},[34,3418,3419],{"class":68},"\"!isValid\"",[34,3421,3422],{"class":40},">Register\u003C\u002F",[34,3424,2495],{"class":44},[34,3426,52],{"class":40},[34,3428,3429,3431,3433],{"class":36,"line":1388},[34,3430,2514],{"class":40},[34,3432,2189],{"class":44},[34,3434,52],{"class":40},[34,3436,3437,3439,3441],{"class":36,"line":1424},[34,3438,144],{"class":40},[34,3440,161],{"class":44},[34,3442,52],{"class":40},[15,3444,3446],{"id":3445},"tips-tricks","💡 Tips & Tricks",[20,3448,3450],{"filename":3449,"language":922},"tips.ts",[25,3451,3453],{"className":925,"code":3452,"language":922,"meta":29,"style":29},"\u002F\u002F ── 1. .number edge case: parseFloat(\"abc\") → NaN, keeps original string ──\n\u002F\u002F v-model.number on \"abc\" → ref stays \"abc\" (string), NOT NaN.\n\u002F\u002F Vue checks: if parsed is NaN, uses the original input value.\n\n\u002F\u002F ── 2. Checkbox v-model with true-value\u002Ffalse-value ──\n\u002F\u002F \u003Cinput type=\"checkbox\" v-model=\"agree\" true-value=\"yes\" false-value=\"no\" \u002F>\n\u002F\u002F agree = \"yes\" when checked, \"no\" when unchecked (not true\u002Ffalse)\n\n\u002F\u002F ── 3. Radio button v-model binds to the value attribute ──\n\u002F\u002F \u003Cinput type=\"radio\" v-model=\"plan\" value=\"pro\" \u002F> → plan = \"pro\" when selected\n\n\u002F\u002F ── 4. .lazy on checkboxes\u002Fselects is a no-op ──\n\u002F\u002F Checkboxes and selects fire 'change' natively, not 'input'.\n\u002F\u002F .lazy only matters for text inputs and textareas.\n\n\u002F\u002F ── 5. Form reset: use the form element's reset() method or reassign ──\n\u002F\u002F formRef.value?.reset()  → resets to initial HTML defaults\n\u002F\u002F Object.assign(formData, initialData)  → resets to reactive initial state\n",[31,3454,3455,3460,3465,3470,3474,3479,3484,3489,3493,3498,3503,3507,3512,3517,3522,3526,3531,3536],{"__ignoreMap":29},[34,3456,3457],{"class":36,"line":37},[34,3458,3459],{"class":169},"\u002F\u002F ── 1. .number edge case: parseFloat(\"abc\") → NaN, keeps original string ──\n",[34,3461,3462],{"class":36,"line":55},[34,3463,3464],{"class":169},"\u002F\u002F v-model.number on \"abc\" → ref stays \"abc\" (string), NOT NaN.\n",[34,3466,3467],{"class":36,"line":72},[34,3468,3469],{"class":169},"\u002F\u002F Vue checks: if parsed is NaN, uses the original input value.\n",[34,3471,3472],{"class":36,"line":79},[34,3473,76],{"emptyLinePlaceholder":75},[34,3475,3476],{"class":36,"line":104},[34,3477,3478],{"class":169},"\u002F\u002F ── 2. Checkbox v-model with true-value\u002Ffalse-value ──\n",[34,3480,3481],{"class":36,"line":123},[34,3482,3483],{"class":169},"\u002F\u002F \u003Cinput type=\"checkbox\" v-model=\"agree\" true-value=\"yes\" false-value=\"no\" \u002F>\n",[34,3485,3486],{"class":36,"line":141},[34,3487,3488],{"class":169},"\u002F\u002F agree = \"yes\" when checked, \"no\" when unchecked (not true\u002Ffalse)\n",[34,3490,3491],{"class":36,"line":151},[34,3492,76],{"emptyLinePlaceholder":75},[34,3494,3495],{"class":36,"line":156},[34,3496,3497],{"class":169},"\u002F\u002F ── 3. Radio button v-model binds to the value attribute ──\n",[34,3499,3500],{"class":36,"line":166},[34,3501,3502],{"class":169},"\u002F\u002F \u003Cinput type=\"radio\" v-model=\"plan\" value=\"pro\" \u002F> → plan = \"pro\" when selected\n",[34,3504,3505],{"class":36,"line":173},[34,3506,76],{"emptyLinePlaceholder":75},[34,3508,3509],{"class":36,"line":179},[34,3510,3511],{"class":169},"\u002F\u002F ── 4. .lazy on checkboxes\u002Fselects is a no-op ──\n",[34,3513,3514],{"class":36,"line":185},[34,3515,3516],{"class":169},"\u002F\u002F Checkboxes and selects fire 'change' natively, not 'input'.\n",[34,3518,3519],{"class":36,"line":191},[34,3520,3521],{"class":169},"\u002F\u002F .lazy only matters for text inputs and textareas.\n",[34,3523,3524],{"class":36,"line":212},[34,3525,76],{"emptyLinePlaceholder":75},[34,3527,3528],{"class":36,"line":217},[34,3529,3530],{"class":169},"\u002F\u002F ── 5. Form reset: use the form element's reset() method or reassign ──\n",[34,3532,3533],{"class":36,"line":223},[34,3534,3535],{"class":169},"\u002F\u002F formRef.value?.reset()  → resets to initial HTML defaults\n",[34,3537,3538],{"class":36,"line":229},[34,3539,3540],{"class":169},"\u002F\u002F Object.assign(formData, initialData)  → resets to reactive initial state\n",[15,3542,3544],{"id":3543},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[20,3546,3548],{"filename":3547,"language":922},"edge-cases.ts",[25,3549,3551],{"className":925,"code":3550,"language":922,"meta":29,"style":29},"\u002F\u002F ── 1. .number with type=\"number\" vs type=\"text\" ──\n\u002F\u002F type=\"number\": browser may return \"\" for invalid input, .number keeps \"\"\n\u002F\u002F type=\"text\" + .number: \"42abc\" → parseFloat(\"42abc\") = 42 (partial parse!)\n\u002F\u002F Always use type=\"number\" with .number for proper browser validation.\n\n\u002F\u002F ── 2. v-model on contenteditable elements is not supported ──\n\u002F\u002F Use a manual binding: :textContent + @input handler.\n\u002F\u002F Or use a library like vue-contenteditable.\n\n\u002F\u002F ── 3. v-model + v-for: each item needs its own model ref ──\n\u002F\u002F ❌ \u003Cinput v-for=\"item in items\" v-model=\"item\" \u002F> — binds to item reference\n\u002F\u002F ✅ \u003Cinput v-for=\"item in items\" v-model=\"item.value\" \u002F> — binds to a property\n\n\u002F\u002F ── 4. defineModel is writable — but parent owns the source ──\n\u002F\u002F Writing to model.value emits update:modelValue → parent updates its ref.\n\u002F\u002F The child's model.value reflects the parent's value after the round-trip.\n\u002F\u002F In the same tick, model.value may show the old value until parent updates.\n\n\u002F\u002F ── 5. .trim on number inputs strips whitespace but doesn't affect parsing ──\n\u002F\u002F v-model.trim.number=\"age\" → trims, then parses.\n\u002F\u002F \"  42  \" → trim → \"42\" → number → 42. Works as expected.\n\n\u002F\u002F ── 6. Custom component v-model: modelValue must match exactly ──\n\u002F\u002F defineModel() → prop name is \"modelValue\", emit is \"update:modelValue\"\n\u002F\u002F defineModel('foo') → prop name is \"foo\", emit is \"update:foo\"\n\u002F\u002F Parent: v-model=\"x\" → modelValue, v-model:foo=\"x\" → foo\n",[31,3552,3553,3558,3563,3568,3573,3577,3582,3587,3592,3596,3601,3606,3611,3615,3620,3625,3630,3635,3639,3644,3649,3654,3658,3663,3668,3673],{"__ignoreMap":29},[34,3554,3555],{"class":36,"line":37},[34,3556,3557],{"class":169},"\u002F\u002F ── 1. .number with type=\"number\" vs type=\"text\" ──\n",[34,3559,3560],{"class":36,"line":55},[34,3561,3562],{"class":169},"\u002F\u002F type=\"number\": browser may return \"\" for invalid input, .number keeps \"\"\n",[34,3564,3565],{"class":36,"line":72},[34,3566,3567],{"class":169},"\u002F\u002F type=\"text\" + .number: \"42abc\" → parseFloat(\"42abc\") = 42 (partial parse!)\n",[34,3569,3570],{"class":36,"line":79},[34,3571,3572],{"class":169},"\u002F\u002F Always use type=\"number\" with .number for proper browser validation.\n",[34,3574,3575],{"class":36,"line":104},[34,3576,76],{"emptyLinePlaceholder":75},[34,3578,3579],{"class":36,"line":123},[34,3580,3581],{"class":169},"\u002F\u002F ── 2. v-model on contenteditable elements is not supported ──\n",[34,3583,3584],{"class":36,"line":141},[34,3585,3586],{"class":169},"\u002F\u002F Use a manual binding: :textContent + @input handler.\n",[34,3588,3589],{"class":36,"line":151},[34,3590,3591],{"class":169},"\u002F\u002F Or use a library like vue-contenteditable.\n",[34,3593,3594],{"class":36,"line":156},[34,3595,76],{"emptyLinePlaceholder":75},[34,3597,3598],{"class":36,"line":166},[34,3599,3600],{"class":169},"\u002F\u002F ── 3. v-model + v-for: each item needs its own model ref ──\n",[34,3602,3603],{"class":36,"line":173},[34,3604,3605],{"class":169},"\u002F\u002F ❌ \u003Cinput v-for=\"item in items\" v-model=\"item\" \u002F> — binds to item reference\n",[34,3607,3608],{"class":36,"line":179},[34,3609,3610],{"class":169},"\u002F\u002F ✅ \u003Cinput v-for=\"item in items\" v-model=\"item.value\" \u002F> — binds to a property\n",[34,3612,3613],{"class":36,"line":185},[34,3614,76],{"emptyLinePlaceholder":75},[34,3616,3617],{"class":36,"line":191},[34,3618,3619],{"class":169},"\u002F\u002F ── 4. defineModel is writable — but parent owns the source ──\n",[34,3621,3622],{"class":36,"line":212},[34,3623,3624],{"class":169},"\u002F\u002F Writing to model.value emits update:modelValue → parent updates its ref.\n",[34,3626,3627],{"class":36,"line":217},[34,3628,3629],{"class":169},"\u002F\u002F The child's model.value reflects the parent's value after the round-trip.\n",[34,3631,3632],{"class":36,"line":223},[34,3633,3634],{"class":169},"\u002F\u002F In the same tick, model.value may show the old value until parent updates.\n",[34,3636,3637],{"class":36,"line":229},[34,3638,76],{"emptyLinePlaceholder":75},[34,3640,3641],{"class":36,"line":235},[34,3642,3643],{"class":169},"\u002F\u002F ── 5. .trim on number inputs strips whitespace but doesn't affect parsing ──\n",[34,3645,3646],{"class":36,"line":251},[34,3647,3648],{"class":169},"\u002F\u002F v-model.trim.number=\"age\" → trims, then parses.\n",[34,3650,3651],{"class":36,"line":256},[34,3652,3653],{"class":169},"\u002F\u002F \"  42  \" → trim → \"42\" → number → 42. Works as expected.\n",[34,3655,3656],{"class":36,"line":262},[34,3657,76],{"emptyLinePlaceholder":75},[34,3659,3660],{"class":36,"line":268},[34,3661,3662],{"class":169},"\u002F\u002F ── 6. Custom component v-model: modelValue must match exactly ──\n",[34,3664,3665],{"class":36,"line":274},[34,3666,3667],{"class":169},"\u002F\u002F defineModel() → prop name is \"modelValue\", emit is \"update:modelValue\"\n",[34,3669,3670],{"class":36,"line":280},[34,3671,3672],{"class":169},"\u002F\u002F defineModel('foo') → prop name is \"foo\", emit is \"update:foo\"\n",[34,3674,3675],{"class":36,"line":305},[34,3676,3677],{"class":169},"\u002F\u002F Parent: v-model=\"x\" → modelValue, v-model:foo=\"x\" → foo\n",[15,3679,3681],{"id":3680},"spot-the-bug","🧠 Spot the Bug",[3683,3684,3685,3686,3689],"p",{},"A number input always stores a string even though ",[31,3687,3688],{},".number"," is applied.",[20,3691,3693],{"filename":3692,"language":23},"NumberBug.vue",[25,3694,3696],{"className":27,"code":3695,"language":23,"meta":29,"style":29},"\u003Cscript setup>\nimport { ref, watch } from 'vue'\nconst age = ref(0)\nwatch(age, (v) => console.log(typeof v, v))  \u002F\u002F always logs \"string\"\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- Missing .number modifier — v-model stores the raw string -->\n  \u003Cinput v-model=\"age\" type=\"number\" \u002F>\n\u003C\u002Ftemplate>\n",[31,3697,3698,3708,3719,3735,3766,3774,3778,3786,3791,3811],{"__ignoreMap":29},[34,3699,3700,3702,3704,3706],{"class":36,"line":37},[34,3701,41],{"class":40},[34,3703,45],{"class":44},[34,3705,49],{"class":48},[34,3707,52],{"class":40},[34,3709,3710,3712,3715,3717],{"class":36,"line":55},[34,3711,59],{"class":58},[34,3713,3714],{"class":40}," { ref, watch } ",[34,3716,65],{"class":58},[34,3718,69],{"class":68},[34,3720,3721,3723,3725,3727,3729,3731,3733],{"class":36,"line":72},[34,3722,82],{"class":58},[34,3724,109],{"class":85},[34,3726,89],{"class":58},[34,3728,92],{"class":48},[34,3730,95],{"class":40},[34,3732,118],{"class":85},[34,3734,101],{"class":40},[34,3736,3737,3739,3742,3745,3747,3749,3752,3755,3757,3760,3763],{"class":36,"line":79},[34,3738,2769],{"class":48},[34,3740,3741],{"class":40},"(age, (",[34,3743,3744],{"class":556},"v",[34,3746,1000],{"class":40},[34,3748,527],{"class":58},[34,3750,3751],{"class":40}," console.",[34,3753,3754],{"class":48},"log",[34,3756,95],{"class":40},[34,3758,3759],{"class":58},"typeof",[34,3761,3762],{"class":40}," v, v))  ",[34,3764,3765],{"class":169},"\u002F\u002F always logs \"string\"\n",[34,3767,3768,3770,3772],{"class":36,"line":104},[34,3769,144],{"class":40},[34,3771,45],{"class":44},[34,3773,52],{"class":40},[34,3775,3776],{"class":36,"line":123},[34,3777,76],{"emptyLinePlaceholder":75},[34,3779,3780,3782,3784],{"class":36,"line":141},[34,3781,41],{"class":40},[34,3783,161],{"class":44},[34,3785,52],{"class":40},[34,3787,3788],{"class":36,"line":151},[34,3789,3790],{"class":169},"  \u003C!-- Missing .number modifier — v-model stores the raw string -->\n",[34,3792,3793,3795,3797,3799,3801,3803,3805,3807,3809],{"class":36,"line":156},[34,3794,194],{"class":40},[34,3796,197],{"class":44},[34,3798,200],{"class":48},[34,3800,203],{"class":40},[34,3802,292],{"class":68},[34,3804,295],{"class":48},[34,3806,203],{"class":40},[34,3808,300],{"class":68},[34,3810,209],{"class":40},[34,3812,3813,3815,3817],{"class":36,"line":166},[34,3814,144],{"class":40},[34,3816,161],{"class":44},[34,3818,52],{"class":40},[3820,3821,3822,3826,3846,3856,3900],"details",{},[3823,3824,3825],"summary",{},"Answer",[3683,3827,3828,3831,3832,3834,3835,3838,3839,3842,3843,3845],{},[31,3829,3830],{},"v-model"," without ",[31,3833,3688],{}," stores the input's value as a string (",[31,3836,3837],{},"$event.target.value"," is always a string). ",[31,3840,3841],{},"type=\"number\""," only controls the browser's input UI — it doesn't change what ",[31,3844,3830],{}," stores.",[3683,3847,3848,3852,3853,3855],{},[3849,3850,3851],"strong",{},"Fix"," — add the ",[31,3854,3688],{}," modifier:",[20,3857,3859],{"filename":3858,"language":23},"NumberFixed.vue",[25,3860,3862],{"className":27,"code":3861,"language":23,"meta":29,"style":29},"\u003Ctemplate>\n  \u003Cinput v-model.number=\"age\" type=\"number\" \u002F>\n\u003C\u002Ftemplate>\n",[31,3863,3864,3872,3892],{"__ignoreMap":29},[34,3865,3866,3868,3870],{"class":36,"line":37},[34,3867,41],{"class":40},[34,3869,161],{"class":44},[34,3871,52],{"class":40},[34,3873,3874,3876,3878,3880,3882,3884,3886,3888,3890],{"class":36,"line":55},[34,3875,194],{"class":40},[34,3877,197],{"class":44},[34,3879,287],{"class":48},[34,3881,203],{"class":40},[34,3883,292],{"class":68},[34,3885,295],{"class":48},[34,3887,203],{"class":40},[34,3889,300],{"class":68},[34,3891,209],{"class":40},[34,3893,3894,3896,3898],{"class":36,"line":72},[34,3895,144],{"class":40},[34,3897,161],{"class":44},[34,3899,52],{"class":40},[3683,3901,3902,3903,3906,3907,3910,3911,3914,3915,3918,3919,3922,3923,3926,3927,3929,3930,3932],{},"Now ",[31,3904,3905],{},"age"," is stored as a ",[31,3908,3909],{},"number"," (or ",[31,3912,3913],{},"\"\""," if empty, since ",[31,3916,3917],{},"parseFloat(\"\")"," is ",[31,3920,3921],{},"NaN"," and Vue keeps the original string for that case). ",[3849,3924,3925],{},"The lesson",": ",[31,3928,3841],{}," controls the UI; ",[31,3931,3688],{}," controls the stored type. You need both for correct behavior.",[3934,3935,3936],"style",{},"html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-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 .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .sMKGj, html code.shiki .sMKGj{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-github-dark:#FDAEB7;--shiki-github-dark-font-style:italic}html pre.shiki code .svAP2, html code.shiki .svAP2{--shiki-default:#032F62;--shiki-github-dark:#DBEDFF}html pre.shiki code .snRuI, html code.shiki .snRuI{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-github-dark:#85E89D;--shiki-github-dark-font-weight:bold}",{"title":29,"searchDepth":55,"depth":55,"links":3938},[3939,3940,3941,3942,3943,3944,3945,3946,3947],{"id":17,"depth":55,"text":18},{"id":375,"depth":55,"text":376},{"id":917,"depth":55,"text":918},{"id":1796,"depth":55,"text":1797},{"id":2529,"depth":55,"text":2530},{"id":2963,"depth":55,"text":2964},{"id":3445,"depth":55,"text":3446},{"id":3543,"depth":55,"text":3544},{"id":3680,"depth":55,"text":3681},"v-model modifier internals, multi-field form validation, custom input components, debounced inputs, dynamic form generation, and the .number\u002F.lazy\u002F.trim modifier edge cases.","md",{},"\u002Fvue\u002F09-forms-and-v-model",{"title":5,"description":3948},"vue\u002F09-forms-and-v-model","HNDzi9E3suFnl0vtgGnGoMbbnuVAFFvpNMhjCbltEt8",1789924655518]