[{"data":1,"prerenderedAt":1837},["ShallowReactive",2],{"content-\u002Fdocs\u002Fcross-cutting-state\u002Fautosave":3},{"id":4,"title":5,"body":6,"description":1816,"extension":1817,"meta":1818,"metaRows":1819,"navigation":114,"path":1832,"seo":1833,"source":1834,"stem":1835,"__hash__":1836},"docs\u002Fdocs\u002Fcross-cutting-state\u002Fautosave.md","Autosave",{"type":7,"value":8,"toc":1805},"minimark",[9,13,33,36,39,43,48,57,1133,1156,1160,1258,1271,1275,1292,1519,1528,1532,1543,1556,1560,1576,1586,1600,1604,1615,1646,1650,1667,1746,1756,1760,1771,1775,1801],[10,11,5],"h1",{"id":12},"autosave",[14,15,16],"blockquote",{},[17,18,19,20,24,25,28,29,32],"p",{},"A copy-paste ",[21,22,23],"code",{},"useAutosave"," composable built on a Vue ",[21,26,27],{},"watch",": per-field status, an aggregate ",[21,30,31],{},"isSaving",", a validity gate, and debounced writes. Attaform owns the reactive form, you own the save policy.",[34,35],"docs-meta-table",{},[17,37,38],{},"Email saves as a draft: even an invalid address persists as you type, while Attaform still flags it inline below the field. Display name and bio gate on validity instead, holding back until they pass (display name needs two characters). Type a burst and one save lands per pause, the toast confirming it. Tick the box to fail saves and watch the aggregate banner flip. Load the saved profile or reset to watch a write land with no autosave at all.",[40,41],"docs-demo",{"label":42,"slug":12},"Autosave Recipe Demo",[44,45,47],"h2",{"id":46},"the-recipe","The recipe",[17,49,50,52,53,56],{},[21,51,23],{}," watches each path through ",[21,54,55],{},"form.toRef",": it debounces every field, gates the save on the field being valid, tracks per-path status, and rolls that up into an aggregate. Drop it into your app as a composable and own it from there.",[58,59,64],"pre",{"className":60,"code":61,"language":62,"meta":63,"style":63},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F composables\u002FuseAutosave.ts\nimport { computed, nextTick, reactive, watch } from 'vue'\nimport type { FlatPath, GenericForm, UseFormReturnType } from 'attaform'\n\nexport type SaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error'\n\nexport function useAutosave\u003CForm extends GenericForm>(\n  form: UseFormReturnType\u003CForm>,\n  paths: readonly FlatPath\u003CForm>[],\n  save: (path: FlatPath\u003CForm>, value: unknown, signal: AbortSignal) => Promise\u003Cvoid>,\n  options: {\n    debounceMs?: number\n    gateOnValidity?: boolean | ((path: FlatPath\u003CForm>) => boolean)\n  } = {}\n) {\n  const { debounceMs = 600, gateOnValidity = true } = options\n  const status = reactive\u003CRecord\u003Cstring, SaveStatus>>({})\n  for (const path of paths) status[path] = 'idle'\n\n  let paused = false\n\n  async function run(path: FlatPath\u003CForm>, value: unknown, signal: AbortSignal) {\n    try {\n      const gate = typeof gateOnValidity === 'function' ? gateOnValidity(path) : gateOnValidity\n      if (gate && !(await form.validateAsync(path)).success) {\n        status[path] = 'idle'\n        return\n      }\n      if (signal.aborted) return\n      status[path] = 'saving'\n      await save(path, value, signal)\n      if (!signal.aborted) status[path] = 'saved'\n    } catch {\n      if (!signal.aborted) status[path] = 'error'\n    }\n  }\n\n  for (const path of paths) {\n    let timer: ReturnType\u003Ctypeof setTimeout> | undefined\n    let controller: AbortController | undefined\n    watch(form.toRef(path), (value) => {\n      if (paused) return\n      status[path] = 'pending'\n      controller?.abort()\n      controller = new AbortController()\n      const { signal } = controller\n      if (timer) clearTimeout(timer)\n      timer = setTimeout(() => void run(path, value, signal), debounceMs)\n    })\n  }\n\n  function runWithoutAutosave(write: () => void) {\n    paused = true\n    try {\n      write()\n    } finally {\n      void nextTick(() => {\n        paused = false\n      })\n    }\n  }\n\n  return {\n    status,\n    isSaving: computed(() => Object.values(status).some((s) => s === 'saving')),\n    failed: computed(() => paths.filter((path) => status[path] === 'error')),\n    runWithoutAutosave,\n  }\n}\n","ts","",[21,65,66,75,93,109,116,155,160,186,206,227,288,299,311,347,359,365,400,431,456,461,475,480,522,530,567,596,606,612,618,629,640,652,670,681,696,702,708,713,729,757,774,797,807,817,829,844,860,874,898,904,909,914,939,950,957,965,975,990,1000,1006,1011,1016,1021,1029,1035,1080,1116,1122,1127],{"__ignoreMap":63},[67,68,71],"span",{"class":69,"line":70},"line",1,[67,72,74],{"class":73},"sJ8bj","\u002F\u002F composables\u002FuseAutosave.ts\n",[67,76,78,82,86,89],{"class":69,"line":77},2,[67,79,81],{"class":80},"szBVR","import",[67,83,85],{"class":84},"sVt8B"," { computed, nextTick, reactive, watch } ",[67,87,88],{"class":80},"from",[67,90,92],{"class":91},"sZZnC"," 'vue'\n",[67,94,96,98,101,104,106],{"class":69,"line":95},3,[67,97,81],{"class":80},[67,99,100],{"class":80}," type",[67,102,103],{"class":84}," { FlatPath, GenericForm, UseFormReturnType } ",[67,105,88],{"class":80},[67,107,108],{"class":91}," 'attaform'\n",[67,110,112],{"class":69,"line":111},4,[67,113,115],{"emptyLinePlaceholder":114},true,"\n",[67,117,119,122,124,128,131,134,137,140,142,145,147,150,152],{"class":69,"line":118},5,[67,120,121],{"class":80},"export",[67,123,100],{"class":80},[67,125,127],{"class":126},"sScJk"," SaveStatus",[67,129,130],{"class":80}," =",[67,132,133],{"class":91}," 'idle'",[67,135,136],{"class":80}," |",[67,138,139],{"class":91}," 'pending'",[67,141,136],{"class":80},[67,143,144],{"class":91}," 'saving'",[67,146,136],{"class":80},[67,148,149],{"class":91}," 'saved'",[67,151,136],{"class":80},[67,153,154],{"class":91}," 'error'\n",[67,156,158],{"class":69,"line":157},6,[67,159,115],{"emptyLinePlaceholder":114},[67,161,163,165,168,171,174,177,180,183],{"class":69,"line":162},7,[67,164,121],{"class":80},[67,166,167],{"class":80}," function",[67,169,170],{"class":126}," useAutosave",[67,172,173],{"class":84},"\u003C",[67,175,176],{"class":126},"Form",[67,178,179],{"class":80}," extends",[67,181,182],{"class":126}," GenericForm",[67,184,185],{"class":84},">(\n",[67,187,189,193,196,199,201,203],{"class":69,"line":188},8,[67,190,192],{"class":191},"s4XuR","  form",[67,194,195],{"class":80},":",[67,197,198],{"class":126}," UseFormReturnType",[67,200,173],{"class":84},[67,202,176],{"class":126},[67,204,205],{"class":84},">,\n",[67,207,209,212,214,217,220,222,224],{"class":69,"line":208},9,[67,210,211],{"class":191},"  paths",[67,213,195],{"class":80},[67,215,216],{"class":80}," readonly",[67,218,219],{"class":126}," FlatPath",[67,221,173],{"class":84},[67,223,176],{"class":126},[67,225,226],{"class":84},">[],\n",[67,228,230,233,235,238,241,243,245,247,249,252,255,257,261,264,267,269,272,275,278,281,283,286],{"class":69,"line":229},10,[67,231,232],{"class":126},"  save",[67,234,195],{"class":80},[67,236,237],{"class":84}," (",[67,239,240],{"class":191},"path",[67,242,195],{"class":80},[67,244,219],{"class":126},[67,246,173],{"class":84},[67,248,176],{"class":126},[67,250,251],{"class":84},">, ",[67,253,254],{"class":191},"value",[67,256,195],{"class":80},[67,258,260],{"class":259},"sj4cs"," unknown",[67,262,263],{"class":84},", ",[67,265,266],{"class":191},"signal",[67,268,195],{"class":80},[67,270,271],{"class":126}," AbortSignal",[67,273,274],{"class":84},") ",[67,276,277],{"class":80},"=>",[67,279,280],{"class":126}," Promise",[67,282,173],{"class":84},[67,284,285],{"class":259},"void",[67,287,205],{"class":84},[67,289,291,294,296],{"class":69,"line":290},11,[67,292,293],{"class":191},"  options",[67,295,195],{"class":80},[67,297,298],{"class":84}," {\n",[67,300,302,305,308],{"class":69,"line":301},12,[67,303,304],{"class":191},"    debounceMs",[67,306,307],{"class":80},"?:",[67,309,310],{"class":259}," number\n",[67,312,314,317,319,322,324,327,329,331,333,335,337,340,342,344],{"class":69,"line":313},13,[67,315,316],{"class":191},"    gateOnValidity",[67,318,307],{"class":80},[67,320,321],{"class":259}," boolean",[67,323,136],{"class":80},[67,325,326],{"class":84}," ((",[67,328,240],{"class":191},[67,330,195],{"class":80},[67,332,219],{"class":126},[67,334,173],{"class":84},[67,336,176],{"class":126},[67,338,339],{"class":84},">) ",[67,341,277],{"class":80},[67,343,321],{"class":259},[67,345,346],{"class":84},")\n",[67,348,350,353,356],{"class":69,"line":349},14,[67,351,352],{"class":84},"  } ",[67,354,355],{"class":80},"=",[67,357,358],{"class":84}," {}\n",[67,360,362],{"class":69,"line":361},15,[67,363,364],{"class":84},") {\n",[67,366,368,371,374,377,379,382,384,387,389,392,395,397],{"class":69,"line":367},16,[67,369,370],{"class":80},"  const",[67,372,373],{"class":84}," { ",[67,375,376],{"class":259},"debounceMs",[67,378,130],{"class":80},[67,380,381],{"class":259}," 600",[67,383,263],{"class":84},[67,385,386],{"class":259},"gateOnValidity",[67,388,130],{"class":80},[67,390,391],{"class":259}," true",[67,393,394],{"class":84}," } ",[67,396,355],{"class":80},[67,398,399],{"class":84}," options\n",[67,401,403,405,408,410,413,415,418,420,423,425,428],{"class":69,"line":402},17,[67,404,370],{"class":80},[67,406,407],{"class":259}," status",[67,409,130],{"class":80},[67,411,412],{"class":126}," reactive",[67,414,173],{"class":84},[67,416,417],{"class":126},"Record",[67,419,173],{"class":84},[67,421,422],{"class":259},"string",[67,424,263],{"class":84},[67,426,427],{"class":126},"SaveStatus",[67,429,430],{"class":84},">>({})\n",[67,432,434,437,439,442,445,448,451,453],{"class":69,"line":433},18,[67,435,436],{"class":80},"  for",[67,438,237],{"class":84},[67,440,441],{"class":80},"const",[67,443,444],{"class":259}," path",[67,446,447],{"class":80}," of",[67,449,450],{"class":84}," paths) status[path] ",[67,452,355],{"class":80},[67,454,455],{"class":91}," 'idle'\n",[67,457,459],{"class":69,"line":458},19,[67,460,115],{"emptyLinePlaceholder":114},[67,462,464,467,470,472],{"class":69,"line":463},20,[67,465,466],{"class":80},"  let",[67,468,469],{"class":84}," paused ",[67,471,355],{"class":80},[67,473,474],{"class":259}," false\n",[67,476,478],{"class":69,"line":477},21,[67,479,115],{"emptyLinePlaceholder":114},[67,481,483,486,488,491,494,496,498,500,502,504,506,508,510,512,514,516,518,520],{"class":69,"line":482},22,[67,484,485],{"class":80},"  async",[67,487,167],{"class":80},[67,489,490],{"class":126}," run",[67,492,493],{"class":84},"(",[67,495,240],{"class":191},[67,497,195],{"class":80},[67,499,219],{"class":126},[67,501,173],{"class":84},[67,503,176],{"class":126},[67,505,251],{"class":84},[67,507,254],{"class":191},[67,509,195],{"class":80},[67,511,260],{"class":259},[67,513,263],{"class":84},[67,515,266],{"class":191},[67,517,195],{"class":80},[67,519,271],{"class":126},[67,521,364],{"class":84},[67,523,525,528],{"class":69,"line":524},23,[67,526,527],{"class":80},"    try",[67,529,298],{"class":84},[67,531,533,536,539,541,544,547,550,553,556,559,562,564],{"class":69,"line":532},24,[67,534,535],{"class":80},"      const",[67,537,538],{"class":259}," gate",[67,540,130],{"class":80},[67,542,543],{"class":80}," typeof",[67,545,546],{"class":84}," gateOnValidity ",[67,548,549],{"class":80},"===",[67,551,552],{"class":91}," 'function'",[67,554,555],{"class":80}," ?",[67,557,558],{"class":126}," gateOnValidity",[67,560,561],{"class":84},"(path) ",[67,563,195],{"class":80},[67,565,566],{"class":84}," gateOnValidity\n",[67,568,570,573,576,579,582,584,587,590,593],{"class":69,"line":569},25,[67,571,572],{"class":80},"      if",[67,574,575],{"class":84}," (gate ",[67,577,578],{"class":80},"&&",[67,580,581],{"class":80}," !",[67,583,493],{"class":84},[67,585,586],{"class":80},"await",[67,588,589],{"class":84}," form.",[67,591,592],{"class":126},"validateAsync",[67,594,595],{"class":84},"(path)).success) {\n",[67,597,599,602,604],{"class":69,"line":598},26,[67,600,601],{"class":84},"        status[path] ",[67,603,355],{"class":80},[67,605,455],{"class":91},[67,607,609],{"class":69,"line":608},27,[67,610,611],{"class":80},"        return\n",[67,613,615],{"class":69,"line":614},28,[67,616,617],{"class":84},"      }\n",[67,619,621,623,626],{"class":69,"line":620},29,[67,622,572],{"class":80},[67,624,625],{"class":84}," (signal.aborted) ",[67,627,628],{"class":80},"return\n",[67,630,632,635,637],{"class":69,"line":631},30,[67,633,634],{"class":84},"      status[path] ",[67,636,355],{"class":80},[67,638,639],{"class":91}," 'saving'\n",[67,641,643,646,649],{"class":69,"line":642},31,[67,644,645],{"class":80},"      await",[67,647,648],{"class":126}," save",[67,650,651],{"class":84},"(path, value, signal)\n",[67,653,655,657,659,662,665,667],{"class":69,"line":654},32,[67,656,572],{"class":80},[67,658,237],{"class":84},[67,660,661],{"class":80},"!",[67,663,664],{"class":84},"signal.aborted) status[path] ",[67,666,355],{"class":80},[67,668,669],{"class":91}," 'saved'\n",[67,671,673,676,679],{"class":69,"line":672},33,[67,674,675],{"class":84},"    } ",[67,677,678],{"class":80},"catch",[67,680,298],{"class":84},[67,682,684,686,688,690,692,694],{"class":69,"line":683},34,[67,685,572],{"class":80},[67,687,237],{"class":84},[67,689,661],{"class":80},[67,691,664],{"class":84},[67,693,355],{"class":80},[67,695,154],{"class":91},[67,697,699],{"class":69,"line":698},35,[67,700,701],{"class":84},"    }\n",[67,703,705],{"class":69,"line":704},36,[67,706,707],{"class":84},"  }\n",[67,709,711],{"class":69,"line":710},37,[67,712,115],{"emptyLinePlaceholder":114},[67,714,716,718,720,722,724,726],{"class":69,"line":715},38,[67,717,436],{"class":80},[67,719,237],{"class":84},[67,721,441],{"class":80},[67,723,444],{"class":259},[67,725,447],{"class":80},[67,727,728],{"class":84}," paths) {\n",[67,730,732,735,738,740,743,745,748,751,754],{"class":69,"line":731},39,[67,733,734],{"class":80},"    let",[67,736,737],{"class":84}," timer",[67,739,195],{"class":80},[67,741,742],{"class":126}," ReturnType",[67,744,173],{"class":84},[67,746,747],{"class":80},"typeof",[67,749,750],{"class":84}," setTimeout> ",[67,752,753],{"class":80},"|",[67,755,756],{"class":259}," undefined\n",[67,758,760,762,765,767,770,772],{"class":69,"line":759},40,[67,761,734],{"class":80},[67,763,764],{"class":84}," controller",[67,766,195],{"class":80},[67,768,769],{"class":126}," AbortController",[67,771,136],{"class":80},[67,773,756],{"class":259},[67,775,777,780,783,786,789,791,793,795],{"class":69,"line":776},41,[67,778,779],{"class":126},"    watch",[67,781,782],{"class":84},"(form.",[67,784,785],{"class":126},"toRef",[67,787,788],{"class":84},"(path), (",[67,790,254],{"class":191},[67,792,274],{"class":84},[67,794,277],{"class":80},[67,796,298],{"class":84},[67,798,800,802,805],{"class":69,"line":799},42,[67,801,572],{"class":80},[67,803,804],{"class":84}," (paused) ",[67,806,628],{"class":80},[67,808,810,812,814],{"class":69,"line":809},43,[67,811,634],{"class":84},[67,813,355],{"class":80},[67,815,816],{"class":91}," 'pending'\n",[67,818,820,823,826],{"class":69,"line":819},44,[67,821,822],{"class":84},"      controller?.",[67,824,825],{"class":126},"abort",[67,827,828],{"class":84},"()\n",[67,830,832,835,837,840,842],{"class":69,"line":831},45,[67,833,834],{"class":84},"      controller ",[67,836,355],{"class":80},[67,838,839],{"class":80}," new",[67,841,769],{"class":126},[67,843,828],{"class":84},[67,845,847,849,851,853,855,857],{"class":69,"line":846},46,[67,848,535],{"class":80},[67,850,373],{"class":84},[67,852,266],{"class":259},[67,854,394],{"class":84},[67,856,355],{"class":80},[67,858,859],{"class":84}," controller\n",[67,861,863,865,868,871],{"class":69,"line":862},47,[67,864,572],{"class":80},[67,866,867],{"class":84}," (timer) ",[67,869,870],{"class":126},"clearTimeout",[67,872,873],{"class":84},"(timer)\n",[67,875,877,880,882,885,888,890,893,895],{"class":69,"line":876},48,[67,878,879],{"class":84},"      timer ",[67,881,355],{"class":80},[67,883,884],{"class":126}," setTimeout",[67,886,887],{"class":84},"(() ",[67,889,277],{"class":80},[67,891,892],{"class":80}," void",[67,894,490],{"class":126},[67,896,897],{"class":84},"(path, value, signal), debounceMs)\n",[67,899,901],{"class":69,"line":900},49,[67,902,903],{"class":84},"    })\n",[67,905,907],{"class":69,"line":906},50,[67,908,707],{"class":84},[67,910,912],{"class":69,"line":911},51,[67,913,115],{"emptyLinePlaceholder":114},[67,915,917,920,923,925,928,930,933,935,937],{"class":69,"line":916},52,[67,918,919],{"class":80},"  function",[67,921,922],{"class":126}," runWithoutAutosave",[67,924,493],{"class":84},[67,926,927],{"class":126},"write",[67,929,195],{"class":80},[67,931,932],{"class":84}," () ",[67,934,277],{"class":80},[67,936,892],{"class":259},[67,938,364],{"class":84},[67,940,942,945,947],{"class":69,"line":941},53,[67,943,944],{"class":84},"    paused ",[67,946,355],{"class":80},[67,948,949],{"class":259}," true\n",[67,951,953,955],{"class":69,"line":952},54,[67,954,527],{"class":80},[67,956,298],{"class":84},[67,958,960,963],{"class":69,"line":959},55,[67,961,962],{"class":126},"      write",[67,964,828],{"class":84},[67,966,968,970,973],{"class":69,"line":967},56,[67,969,675],{"class":84},[67,971,972],{"class":80},"finally",[67,974,298],{"class":84},[67,976,978,981,984,986,988],{"class":69,"line":977},57,[67,979,980],{"class":80},"      void",[67,982,983],{"class":126}," nextTick",[67,985,887],{"class":84},[67,987,277],{"class":80},[67,989,298],{"class":84},[67,991,993,996,998],{"class":69,"line":992},58,[67,994,995],{"class":84},"        paused ",[67,997,355],{"class":80},[67,999,474],{"class":259},[67,1001,1003],{"class":69,"line":1002},59,[67,1004,1005],{"class":84},"      })\n",[67,1007,1009],{"class":69,"line":1008},60,[67,1010,701],{"class":84},[67,1012,1014],{"class":69,"line":1013},61,[67,1015,707],{"class":84},[67,1017,1019],{"class":69,"line":1018},62,[67,1020,115],{"emptyLinePlaceholder":114},[67,1022,1024,1027],{"class":69,"line":1023},63,[67,1025,1026],{"class":80},"  return",[67,1028,298],{"class":84},[67,1030,1032],{"class":69,"line":1031},64,[67,1033,1034],{"class":84},"    status,\n",[67,1036,1038,1041,1044,1046,1048,1051,1054,1057,1060,1063,1066,1068,1070,1073,1075,1077],{"class":69,"line":1037},65,[67,1039,1040],{"class":84},"    isSaving: ",[67,1042,1043],{"class":126},"computed",[67,1045,887],{"class":84},[67,1047,277],{"class":80},[67,1049,1050],{"class":84}," Object.",[67,1052,1053],{"class":126},"values",[67,1055,1056],{"class":84},"(status).",[67,1058,1059],{"class":126},"some",[67,1061,1062],{"class":84},"((",[67,1064,1065],{"class":191},"s",[67,1067,274],{"class":84},[67,1069,277],{"class":80},[67,1071,1072],{"class":84}," s ",[67,1074,549],{"class":80},[67,1076,144],{"class":91},[67,1078,1079],{"class":84},")),\n",[67,1081,1083,1086,1088,1090,1092,1095,1098,1100,1102,1104,1106,1109,1111,1114],{"class":69,"line":1082},66,[67,1084,1085],{"class":84},"    failed: ",[67,1087,1043],{"class":126},[67,1089,887],{"class":84},[67,1091,277],{"class":80},[67,1093,1094],{"class":84}," paths.",[67,1096,1097],{"class":126},"filter",[67,1099,1062],{"class":84},[67,1101,240],{"class":191},[67,1103,274],{"class":84},[67,1105,277],{"class":80},[67,1107,1108],{"class":84}," status[path] ",[67,1110,549],{"class":80},[67,1112,1113],{"class":91}," 'error'",[67,1115,1079],{"class":84},[67,1117,1119],{"class":69,"line":1118},67,[67,1120,1121],{"class":84},"    runWithoutAutosave,\n",[67,1123,1125],{"class":69,"line":1124},68,[67,1126,707],{"class":84},[67,1128,1130],{"class":69,"line":1129},69,[67,1131,1132],{"class":84},"}\n",[17,1134,1135,1136,263,1139,263,1142,1145,1146,1149,1150,1152,1153,1155],{},"The path and form type helpers (",[21,1137,1138],{},"FlatPath",[21,1140,1141],{},"GenericForm",[21,1143,1144],{},"UseFormReturnType",") come from the core ",[21,1147,1148],{},"attaform"," entry. The composable is adapter-agnostic: it works against a Zod v3, Zod v4, or any other adapter's form, since it only reaches for ",[21,1151,55],{}," and ",[21,1154,592],{},".",[44,1157,1159],{"id":1158},"what-it-gives-you-back","What it gives you back",[1161,1162,1163,1179],"table",{},[1164,1165,1166],"thead",{},[1167,1168,1169,1173,1176],"tr",{},[1170,1171,1172],"th",{},"Return",[1170,1174,1175],{},"Type",[1170,1177,1178],{},"Use",[1180,1181,1182,1214,1228,1243],"tbody",{},[1167,1183,1184,1190,1195],{},[1185,1186,1187],"td",{},[21,1188,1189],{},"status",[1185,1191,1192],{},[21,1193,1194],{},"Record\u003Cpath, SaveStatus>",[1185,1196,1197,1198,1201,1202,1201,1205,1201,1208,1201,1211,1155],{},"Per-field badge: ",[21,1199,1200],{},"idle"," \u002F ",[21,1203,1204],{},"pending",[21,1206,1207],{},"saving",[21,1209,1210],{},"saved",[21,1212,1213],{},"error",[1167,1215,1216,1220,1225],{},[1185,1217,1218],{},[21,1219,31],{},[1185,1221,1222],{},[21,1223,1224],{},"ComputedRef\u003Cboolean>",[1185,1226,1227],{},"Aggregate \"a save is in flight\" for a header spinner or banner.",[1167,1229,1230,1235,1240],{},[1185,1231,1232],{},[21,1233,1234],{},"failed",[1185,1236,1237],{},[21,1238,1239],{},"ComputedRef\u003Cpath[]>",[1185,1241,1242],{},"The paths whose last save errored, for a retry affordance.",[1167,1244,1245,1250,1255],{},[1185,1246,1247],{},[21,1248,1249],{},"runWithoutAutosave",[1185,1251,1252],{},[21,1253,1254],{},"(write) => void",[1185,1256,1257],{},"Run a programmatic write (hydrate, reset) without tripping a save. Below.",[17,1259,1260,1261,1263,1264,1201,1267,1270],{},"None of it lives in form state. ",[21,1262,1189],{}," is a plain reactive map the composable owns: the form's ",[21,1265,1266],{},"dirty",[21,1268,1269],{},"validating"," surface stays about the form, autosave status stays about autosave.",[44,1272,1274],{"id":1273},"wiring-it-up","Wiring it up",[17,1276,1277,1278,1280,1281,1284,1285,1288,1289,1155],{},"Hoist the schema, build the form, then point ",[21,1279,23],{}," at the paths you want saved. The ",[21,1282,1283],{},"save"," callback is yours: it receives the path, its value, and an ",[21,1286,1287],{},"AbortSignal"," to hand straight to ",[21,1290,1291],{},"fetch",[58,1293,1295],{"className":60,"code":1294,"language":62,"meta":63,"style":63},"const schema = z.object({\n  email: z.string().email(),\n  displayName: z.string().min(2),\n  bio: z.string().max(160),\n})\n\nconst form = useForm({ schema })\n\nconst { status, isSaving, failed, runWithoutAutosave } = useAutosave(\n  form,\n  ['email', 'displayName', 'bio'],\n  async (path, value, signal) => {\n    await api.patch(`\u002Fprofile\u002F${path}`, { value }, { signal })\n  },\n  { debounceMs: 600 }\n)\n",[21,1296,1297,1315,1331,1351,1370,1375,1379,1394,1398,1427,1432,1453,1475,1499,1504,1515],{"__ignoreMap":63},[67,1298,1299,1301,1304,1306,1309,1312],{"class":69,"line":70},[67,1300,441],{"class":80},[67,1302,1303],{"class":259}," schema",[67,1305,130],{"class":80},[67,1307,1308],{"class":84}," z.",[67,1310,1311],{"class":126},"object",[67,1313,1314],{"class":84},"({\n",[67,1316,1317,1320,1322,1325,1328],{"class":69,"line":77},[67,1318,1319],{"class":84},"  email: z.",[67,1321,422],{"class":126},[67,1323,1324],{"class":84},"().",[67,1326,1327],{"class":126},"email",[67,1329,1330],{"class":84},"(),\n",[67,1332,1333,1336,1338,1340,1343,1345,1348],{"class":69,"line":95},[67,1334,1335],{"class":84},"  displayName: z.",[67,1337,422],{"class":126},[67,1339,1324],{"class":84},[67,1341,1342],{"class":126},"min",[67,1344,493],{"class":84},[67,1346,1347],{"class":259},"2",[67,1349,1350],{"class":84},"),\n",[67,1352,1353,1356,1358,1360,1363,1365,1368],{"class":69,"line":111},[67,1354,1355],{"class":84},"  bio: z.",[67,1357,422],{"class":126},[67,1359,1324],{"class":84},[67,1361,1362],{"class":126},"max",[67,1364,493],{"class":84},[67,1366,1367],{"class":259},"160",[67,1369,1350],{"class":84},[67,1371,1372],{"class":69,"line":118},[67,1373,1374],{"class":84},"})\n",[67,1376,1377],{"class":69,"line":157},[67,1378,115],{"emptyLinePlaceholder":114},[67,1380,1381,1383,1386,1388,1391],{"class":69,"line":162},[67,1382,441],{"class":80},[67,1384,1385],{"class":259}," form",[67,1387,130],{"class":80},[67,1389,1390],{"class":126}," useForm",[67,1392,1393],{"class":84},"({ schema })\n",[67,1395,1396],{"class":69,"line":188},[67,1397,115],{"emptyLinePlaceholder":114},[67,1399,1400,1402,1404,1406,1408,1410,1412,1414,1416,1418,1420,1422,1424],{"class":69,"line":208},[67,1401,441],{"class":80},[67,1403,373],{"class":84},[67,1405,1189],{"class":259},[67,1407,263],{"class":84},[67,1409,31],{"class":259},[67,1411,263],{"class":84},[67,1413,1234],{"class":259},[67,1415,263],{"class":84},[67,1417,1249],{"class":259},[67,1419,394],{"class":84},[67,1421,355],{"class":80},[67,1423,170],{"class":126},[67,1425,1426],{"class":84},"(\n",[67,1428,1429],{"class":69,"line":229},[67,1430,1431],{"class":84},"  form,\n",[67,1433,1434,1437,1440,1442,1445,1447,1450],{"class":69,"line":290},[67,1435,1436],{"class":84},"  [",[67,1438,1439],{"class":91},"'email'",[67,1441,263],{"class":84},[67,1443,1444],{"class":91},"'displayName'",[67,1446,263],{"class":84},[67,1448,1449],{"class":91},"'bio'",[67,1451,1452],{"class":84},"],\n",[67,1454,1455,1457,1459,1461,1463,1465,1467,1469,1471,1473],{"class":69,"line":301},[67,1456,485],{"class":80},[67,1458,237],{"class":84},[67,1460,240],{"class":191},[67,1462,263],{"class":84},[67,1464,254],{"class":191},[67,1466,263],{"class":84},[67,1468,266],{"class":191},[67,1470,274],{"class":84},[67,1472,277],{"class":80},[67,1474,298],{"class":84},[67,1476,1477,1480,1483,1486,1488,1491,1493,1496],{"class":69,"line":313},[67,1478,1479],{"class":80},"    await",[67,1481,1482],{"class":84}," api.",[67,1484,1485],{"class":126},"patch",[67,1487,493],{"class":84},[67,1489,1490],{"class":91},"`\u002Fprofile\u002F${",[67,1492,240],{"class":84},[67,1494,1495],{"class":91},"}`",[67,1497,1498],{"class":84},", { value }, { signal })\n",[67,1500,1501],{"class":69,"line":349},[67,1502,1503],{"class":84},"  },\n",[67,1505,1506,1509,1512],{"class":69,"line":361},[67,1507,1508],{"class":84},"  { debounceMs: ",[67,1510,1511],{"class":259},"600",[67,1513,1514],{"class":84}," }\n",[67,1516,1517],{"class":69,"line":367},[67,1518,346],{"class":84},[17,1520,1521,1523,1524,1527],{},[21,1522,23],{}," registers its watchers inside ",[21,1525,1526],{},"setup",", so they stop automatically when the component unmounts. No manual teardown.",[44,1529,1531],{"id":1530},"how-the-watch-reacts","How the watch reacts",[17,1533,1534,1535,1538,1539,1542],{},"Each path gets its own ",[21,1536,1537],{},"watch(form.toRef(path), ...)",". ",[21,1540,1541],{},"form.toRef('email')"," is a read-only Vue ref over that path's value, resolved from a dotted string, so the watcher fires whenever the value at that path changes. Writing the same value back is a no-op in Attaform, so a watcher never fires for a write that changed nothing.",[17,1544,1545,1547,1548,1551,1552,1555],{},[21,1546,785],{}," resolves a nested path directly, so ",[21,1549,1550],{},"form.toRef('profile.bio')"," watches a leaf several levels deep with no extra wiring. For a whole container path, add ",[21,1553,1554],{},"{ deep: true }"," to that watcher to react to any field beneath it, since an in-place leaf edit keeps the container's reference stable (Vue's deep-watch behaviour).",[44,1557,1559],{"id":1558},"gating-on-validity","Gating on validity",[17,1561,1562,1563,1565,1566,1569,1570,1575],{},"This is where validation and persistence meet. ",[21,1564,386],{}," (on by default) runs ",[21,1567,1568],{},"form.validateAsync(path)"," before each save and skips the write when the field is invalid, so a value that fails validation never reaches the server, and the field's ",[1571,1572,1574],"a",{"href":1573},"\u002Fdocs\u002Fvalidation\u002Fasync-refinements","async refinements"," run as part of that same check.",[17,1577,1578,1581,1582,1585],{},[21,1579,1580],{},".refine"," decides whether a value is allowed; the autosave decides whether to persist it. Composing them gives you \"save only what passes\" without either concern leaking into the other. Turn the gate off (",[21,1583,1584],{},"{ gateOnValidity: false }",") when you want a true draft autosave that captures even invalid in-progress state.",[17,1587,1588,1589,1592,1593,1595,1596,1599],{},"For per-field control, pass a predicate instead of a boolean: ",[21,1590,1591],{},"gateOnValidity: (path) => path !== 'email'"," keeps ",[21,1594,1327],{}," a draft while gating the rest. The demo above does exactly that, so an invalid email still autosaves while ",[21,1597,1598],{},"fields.email.showErrors"," surfaces the validation message. The write persists the draft, the read flags it, and neither blocks the other.",[44,1601,1603],{"id":1602},"debouncing-and-superseding","Debouncing and superseding",[17,1605,1606,1607,1610,1611,1614],{},"A network round-trip per keystroke is wasteful, so each path gets its own debounced scheduler. The recipe inlines a small ",[21,1608,1609],{},"setTimeout"," debounce to stay dependency-free; if your app already uses VueUse, swap in ",[21,1612,1613],{},"useDebounceFn"," with no other change.",[17,1616,1617,1618,1621,1622,1624,1625,1627,1628,1630,1631,1634,1635,1637,1638,1641,1642,1645],{},"Each watcher also owns an ",[21,1619,1620],{},"AbortController",". When a newer edit lands before the previous save finished, the recipe aborts the prior controller and starts a fresh one, so a stale request to a slow endpoint cancels itself and the latest write wins. The ",[21,1623,1283],{}," callback receives that ",[21,1626,266],{},", so passing it to ",[21,1629,1291],{}," cancels the request in flight; checking ",[21,1632,1633],{},"signal.aborted"," after an ",[21,1636,586],{}," skips a status update for work that has been superseded. Because the scheduler returns immediately, the actual save runs after the watcher has finished, so ",[21,1639,1640],{},"run"," catches its own throw and sets ",[21,1643,1644],{},"status[path] = 'error'"," rather than letting it escape.",[44,1647,1649],{"id":1648},"pausing-autosave-for-hydration-and-reset","Pausing autosave for hydration and reset",[17,1651,1652,1653,1655,1656,1659,1660,1663,1664,1666],{},"A ",[21,1654,27],{}," reacts to every change to its source, including the writes you make yourself: loading a saved record with ",[21,1657,1658],{},"form.setValue",", or a ",[21,1661,1662],{},"form.reset()",". Left unguarded, hydrating ten fields would echo ten autosaves straight back at the server you just loaded from. ",[21,1665,1249],{}," wraps such a write so the watchers skip it: it raises an internal flag, runs your write, then clears the flag on the next tick once the watchers have seen (and ignored) the change.",[58,1668,1670],{"className":60,"code":1669,"language":62,"meta":63,"style":63},"function loadProfile(saved: Profile) {\n  runWithoutAutosave(() => form.setValue(saved))\n}\n\nfunction discard() {\n  runWithoutAutosave(() => form.reset())\n}\n",[21,1671,1672,1691,1708,1712,1716,1726,1742],{"__ignoreMap":63},[67,1673,1674,1677,1680,1682,1684,1686,1689],{"class":69,"line":70},[67,1675,1676],{"class":80},"function",[67,1678,1679],{"class":126}," loadProfile",[67,1681,493],{"class":84},[67,1683,1210],{"class":191},[67,1685,195],{"class":80},[67,1687,1688],{"class":126}," Profile",[67,1690,364],{"class":84},[67,1692,1693,1696,1698,1700,1702,1705],{"class":69,"line":77},[67,1694,1695],{"class":126},"  runWithoutAutosave",[67,1697,887],{"class":84},[67,1699,277],{"class":80},[67,1701,589],{"class":84},[67,1703,1704],{"class":126},"setValue",[67,1706,1707],{"class":84},"(saved))\n",[67,1709,1710],{"class":69,"line":95},[67,1711,1132],{"class":84},[67,1713,1714],{"class":69,"line":111},[67,1715,115],{"emptyLinePlaceholder":114},[67,1717,1718,1720,1723],{"class":69,"line":118},[67,1719,1676],{"class":80},[67,1721,1722],{"class":126}," discard",[67,1724,1725],{"class":84},"() {\n",[67,1727,1728,1730,1732,1734,1736,1739],{"class":69,"line":157},[67,1729,1695],{"class":126},[67,1731,887],{"class":84},[67,1733,277],{"class":80},[67,1735,589],{"class":84},[67,1737,1738],{"class":126},"reset",[67,1740,1741],{"class":84},"())\n",[67,1743,1744],{"class":69,"line":162},[67,1745,1132],{"class":84},[17,1747,1748,1749,1751,1752,1755],{},"The pause belongs to the recipe, not the form. The form has no idea autosave exists, the same separation that keeps ",[21,1750,1189],{}," out of ",[21,1753,1754],{},"form.meta",". You own the autosave, so you own when it pauses.",[44,1757,1759],{"id":1758},"why-a-recipe-not-a-shipped-export","Why a recipe, not a shipped export",[17,1761,1762,1763,1767,1768,1770],{},"Attaform ships ",[1571,1764,1766],{"href":1765},"\u002Fdocs\u002Fgetting-started\u002Fwhy-attaform","zero runtime dependencies",", and a genuinely good autosave wants opinions the core should not make for you: the debounce window, retry and backoff policy, whether a save is optimistic or confirmed, and what \"saved\" even means for your backend. Those are application decisions. The reactive form is the small, sharp primitive Attaform owns; ",[21,1769,23],{}," is the policy you own, sized and tuned to your app. Copy it, change it, delete the parts you do not need.",[44,1772,1774],{"id":1773},"where-to-next","Where to next",[1776,1777,1778,1785,1794],"ul",{},[1779,1780,1781,1784],"li",{},[1571,1782,1783],{"href":1573},"Async refinements",": the server-side validity checks the gate runs.",[1779,1786,1787,1793],{},[1571,1788,1790,1792],{"href":1789},"\u002Fdocs\u002Fwriting-and-mutating\u002Fset-value",[21,1791,1704],{}," patterns",": the write surface the recipe drives, including the hydrating record load.",[1779,1795,1796,1800],{},[1571,1797,1799],{"href":1798},"\u002Fdocs\u002Fcross-cutting-state\u002Fundo-redo","Undo and redo",": the other cross-cutting reaction to form changes, kept inside the form.",[1802,1803,1804],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":63,"searchDepth":77,"depth":77,"links":1806},[1807,1808,1809,1810,1811,1812,1813,1814,1815],{"id":46,"depth":77,"text":47},{"id":1158,"depth":77,"text":1159},{"id":1273,"depth":77,"text":1274},{"id":1530,"depth":77,"text":1531},{"id":1558,"depth":77,"text":1559},{"id":1602,"depth":77,"text":1603},{"id":1648,"depth":77,"text":1649},{"id":1758,"depth":77,"text":1759},{"id":1773,"depth":77,"text":1774},"A copy-paste useAutosave recipe over a watch on form.values. Per-field status, an aggregate isSaving, a validity gate, and debounced writes, with no new dependency and the save policy in your hands.","md",{},[1820,1823,1826,1829],{"label":1821,"value":1822},"Category","Recipe",{"label":1824,"value":1825,"kind":21},"Built on","watch · validateAsync",{"label":1827,"value":1828,"kind":21},"Returns","status · isSaving · runWithoutAutosave",{"label":1830,"value":1831},"Dependency","none (copy-paste, you own it)","\u002Fdocs\u002Fcross-cutting-state\u002Fautosave",{"title":5,"description":1816},null,"docs\u002Fcross-cutting-state\u002Fautosave","2I5cXqErrmYRN5MfcYzCrKpVhjSuCPzQkGzmnlcuUsE",1781745873853]