[{"data":1,"prerenderedAt":801},["ShallowReactive",2],{"content-\u002Fdocs\u002Fcross-cutting-state\u002Fundo-redo":3},{"id":4,"title":5,"body":6,"description":780,"extension":781,"meta":782,"metaRows":783,"navigation":557,"path":796,"seo":797,"source":798,"stem":799,"__hash__":800},"docs\u002Fdocs\u002Fcross-cutting-state\u002Fundo-redo.md","Undo & redo",{"type":7,"value":8,"toc":769},"minimark",[9,13,32,35,56,60,65,116,119,137,140,158,174,178,185,297,301,333,350,353,391,401,405,408,613,616,622,628,682,697,701,726,730,745,749,765],[10,11,5],"h1",{"id":12},"undo-redo",[14,15,16],"blockquote",{},[17,18,19,20,24,25,24,28,31],"p",{},"Opt into a per-form history chain with one option. Every value mutation records a position; the namespace exposes ",[21,22,23],"code",{},"undo()"," \u002F ",[21,26,27],{},"redo()",[21,29,30],{},"clear()"," and the reactive flags that gate your UI.",[33,34],"docs-meta-table",{},[17,36,37,38,24,41,44,45,48,49,52,53,55],{},"Type into any field, append a few tags, then hit ",[21,39,40],{},"⌘Z",[21,42,43],{},"⌘⇧Z"," (or click the buttons) to walk the chain. ",[21,46,47],{},"canUndo"," and ",[21,50,51],{},"canRedo"," gate the buttons reactively; ",[21,54,30],{}," reseeds the chain at the current state, the move you'd make after a \"Save successful\" milestone.",[57,58],"docs-demo",{"label":59,"slug":12},"Undo & Redo Demo",[61,62,64],"h2",{"id":63},"the-option","The option",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-ts shiki shiki-themes github-light github-dark","useForm({\n  schema,\n  history: true, \u002F\u002F default 128-position bounded chain\n})\n","ts","",[21,73,74,87,93,110],{"__ignoreMap":71},[75,76,79,83],"span",{"class":77,"line":78},"line",1,[75,80,82],{"class":81},"sScJk","useForm",[75,84,86],{"class":85},"sVt8B","({\n",[75,88,90],{"class":77,"line":89},2,[75,91,92],{"class":85},"  schema,\n",[75,94,96,99,103,106],{"class":77,"line":95},3,[75,97,98],{"class":85},"  history: ",[75,100,102],{"class":101},"sj4cs","true",[75,104,105],{"class":85},", ",[75,107,109],{"class":108},"sJ8bj","\u002F\u002F default 128-position bounded chain\n",[75,111,113],{"class":77,"line":112},4,[75,114,115],{"class":85},"})\n",[17,117,118],{},"Tune the depth:",[66,120,122],{"className":68,"code":121,"language":70,"meta":71,"style":71},"useForm({ schema, history: { max: 200 } })\n",[21,123,124],{"__ignoreMap":71},[75,125,126,128,131,134],{"class":77,"line":78},[75,127,82],{"class":81},[75,129,130],{"class":85},"({ schema, history: { max: ",[75,132,133],{"class":101},"200",[75,135,136],{"class":85}," } })\n",[17,138,139],{},"Disable explicitly:",[66,141,143],{"className":68,"code":142,"language":70,"meta":71,"style":71},"useForm({ schema, history: false })\n",[21,144,145],{"__ignoreMap":71},[75,146,147,149,152,155],{"class":77,"line":78},[75,148,82],{"class":81},[75,150,151],{"class":85},"({ schema, history: ",[75,153,154],{"class":101},"false",[75,156,157],{"class":85}," })\n",[17,159,160,161,164,165,167,168,24,170,173],{},"When omitted, ",[21,162,163],{},"history"," defaults to ",[21,166,154],{},". The namespace is still present on the form return so templates don't need conditional logic, but every method is a no-op and the flags read ",[21,169,154],{},[21,171,172],{},"0",".",[61,175,177],{"id":176},"the-namespace","The namespace",[17,179,180,181,184],{},"All undo\u002Fredo surface lives under ",[21,182,183],{},"form.history",":",[186,187,188,204],"table",{},[189,190,191],"thead",{},[192,193,194,198,201],"tr",{},[195,196,197],"th",{},"Member",[195,199,200],{},"Type",[195,202,203],{},"What it does",[205,206,207,225,241,255,269,282],"tbody",{},[192,208,209,214,219],{},[210,211,212],"td",{},[21,213,23],{},[210,215,216],{},[21,217,218],{},"() => boolean",[210,220,221,222,224],{},"Step back to the previous state. ",[21,223,154],{}," at baseline.",[192,226,227,231,235],{},[210,228,229],{},[21,230,27],{},[210,232,233],{},[21,234,218],{},[210,236,237,238,240],{},"Replay the next state after an undo. ",[21,239,154],{}," when nothing's queued.",[192,242,243,247,252],{},[210,244,245],{},[21,246,30],{},[210,248,249],{},[21,250,251],{},"() => void",[210,253,254],{},"Wipe the chain; reseed at the current state as the new baseline.",[192,256,257,261,266],{},[210,258,259],{},[21,260,47],{},[210,262,263],{},[21,264,265],{},"boolean",[210,267,268],{},"Gate an \"Undo\" button reactively.",[192,270,271,275,279],{},[210,272,273],{},[21,274,51],{},[210,276,277],{},[21,278,265],{},[210,280,281],{},"Gate a \"Redo\" button reactively.",[192,283,284,289,294],{},[210,285,286],{},[21,287,288],{},"size",[210,290,291],{},[21,292,293],{},"number",[210,295,296],{},"Reachable positions across the chain (useful for debug overlays).",[61,298,300],{"id":299},"what-gets-captured","What gets captured",[17,302,303,304,105,307,310,311,105,314,105,317,105,320,105,323,105,326,105,329,332],{},"Every form value mutation: ",[21,305,306],{},"setValue",[21,308,309],{},"register","-backed input edits, any array helper (",[21,312,313],{},"append",[21,315,316],{},"prepend",[21,318,319],{},"insert",[21,321,322],{},"remove",[21,324,325],{},"swap",[21,327,328],{},"move",[21,330,331],{},"replace","), or a programmatic write. Each recorded position carries:",[334,335,336,340,343],"ul",{},[337,338,339],"li",{},"The form value.",[337,341,342],{},"The error map at the time of the captured position.",[337,344,345,346,349],{},"The ",[21,347,348],{},"blankPaths"," set (so cleared-but-defaulted numeric fields keep showing as empty after an undo, instead of resurrecting their slim default).",[17,351,352],{},"What's NOT captured:",[334,354,355,375,386],{},[337,356,357,361,362,24,365,24,368,24,371,374],{},[358,359,360],"strong",{},"Field interaction state",": ",[21,363,364],{},"touched",[21,366,367],{},"focused",[21,369,370],{},"blurred",[21,372,373],{},"connected",". UI interaction history; it shouldn't rewind. A field that was touched stays touched.",[337,376,377,361,380,105,383,173],{},[358,378,379],{},"Submission lifecycle",[21,381,382],{},"meta.submissionAttempts",[21,384,385],{},"meta.submitError",[337,387,388,173],{},[358,389,390],{},"Validation in-flight state",[17,392,393,394,24,397,400],{},"Calling ",[21,395,396],{},"setErrors",[21,398,399],{},"clearErrors"," does NOT record a position; those only touch the error map. Whatever errors are live when the next mutation lands go into that mutation's delta.",[61,402,404],{"id":403},"keyboard-shortcuts","Keyboard shortcuts",[17,406,407],{},"Not wired by default; wire them in a few lines:",[66,409,413],{"className":410,"code":411,"language":412,"meta":71,"style":71},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup lang=\"ts\">\n  function onKeydown(event: KeyboardEvent) {\n    if ((event.metaKey || event.ctrlKey) && event.key === 'z') {\n      event.preventDefault()\n      event.shiftKey ? form.history.redo() : form.history.undo()\n    }\n  }\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cform @keydown=\"onKeydown\">\n    \u003C!-- … -->\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n","vue",[21,414,415,440,464,492,503,530,536,542,552,559,569,588,594,604],{"__ignoreMap":71},[75,416,417,420,424,427,430,433,437],{"class":77,"line":78},[75,418,419],{"class":85},"\u003C",[75,421,423],{"class":422},"s9eBZ","script",[75,425,426],{"class":81}," setup",[75,428,429],{"class":81}," lang",[75,431,432],{"class":85},"=",[75,434,436],{"class":435},"sZZnC","\"ts\"",[75,438,439],{"class":85},">\n",[75,441,442,446,449,452,456,458,461],{"class":77,"line":89},[75,443,445],{"class":444},"szBVR","  function",[75,447,448],{"class":81}," onKeydown",[75,450,451],{"class":85},"(",[75,453,455],{"class":454},"s4XuR","event",[75,457,184],{"class":444},[75,459,460],{"class":81}," KeyboardEvent",[75,462,463],{"class":85},") {\n",[75,465,466,469,472,475,478,481,484,487,490],{"class":77,"line":95},[75,467,468],{"class":444},"    if",[75,470,471],{"class":85}," ((event.metaKey ",[75,473,474],{"class":444},"||",[75,476,477],{"class":85}," event.ctrlKey) ",[75,479,480],{"class":444},"&&",[75,482,483],{"class":85}," event.key ",[75,485,486],{"class":444},"===",[75,488,489],{"class":435}," 'z'",[75,491,463],{"class":85},[75,493,494,497,500],{"class":77,"line":112},[75,495,496],{"class":85},"      event.",[75,498,499],{"class":81},"preventDefault",[75,501,502],{"class":85},"()\n",[75,504,506,509,512,515,518,521,523,525,528],{"class":77,"line":505},5,[75,507,508],{"class":85},"      event.shiftKey ",[75,510,511],{"class":444},"?",[75,513,514],{"class":85}," form.history.",[75,516,517],{"class":81},"redo",[75,519,520],{"class":85},"() ",[75,522,184],{"class":444},[75,524,514],{"class":85},[75,526,527],{"class":81},"undo",[75,529,502],{"class":85},[75,531,533],{"class":77,"line":532},6,[75,534,535],{"class":85},"    }\n",[75,537,539],{"class":77,"line":538},7,[75,540,541],{"class":85},"  }\n",[75,543,545,548,550],{"class":77,"line":544},8,[75,546,547],{"class":85},"\u003C\u002F",[75,549,423],{"class":422},[75,551,439],{"class":85},[75,553,555],{"class":77,"line":554},9,[75,556,558],{"emptyLinePlaceholder":557},true,"\n",[75,560,562,564,567],{"class":77,"line":561},10,[75,563,419],{"class":85},[75,565,566],{"class":422},"template",[75,568,439],{"class":85},[75,570,572,575,578,581,583,586],{"class":77,"line":571},11,[75,573,574],{"class":85},"  \u003C",[75,576,577],{"class":422},"form",[75,579,580],{"class":81}," @keydown",[75,582,432],{"class":85},[75,584,585],{"class":435},"\"onKeydown\"",[75,587,439],{"class":85},[75,589,591],{"class":77,"line":590},12,[75,592,593],{"class":108},"    \u003C!-- … -->\n",[75,595,597,600,602],{"class":77,"line":596},13,[75,598,599],{"class":85},"  \u003C\u002F",[75,601,577],{"class":422},[75,603,439],{"class":85},[75,605,607,609,611],{"class":77,"line":606},14,[75,608,547],{"class":85},[75,610,566],{"class":422},[75,612,439],{"class":85},[17,614,615],{},"Attaform stays out of the global keydown business so you can layer shortcuts at the right scope (per-form, per-route, global), with the modifier convention that fits your platform.",[61,617,619,621],{"id":618},"clear-at-a-milestone",[21,620,30],{}," at a milestone",[17,623,624,625,627],{},"After a \"save successful\" moment, or any point where consumers should lose access to the prior chain without disturbing the rendered form, call ",[21,626,30],{},". The form value, errors, and blank-paths stay exactly where they are; only the past and future history reset.",[66,629,631],{"className":68,"code":630,"language":70,"meta":71,"style":71},"async function onSaveSuccess() {\n  await api.commit(form.values())\n  form.history.clear()\n}\n",[21,632,633,647,667,677],{"__ignoreMap":71},[75,634,635,638,641,644],{"class":77,"line":78},[75,636,637],{"class":444},"async",[75,639,640],{"class":444}," function",[75,642,643],{"class":81}," onSaveSuccess",[75,645,646],{"class":85},"() {\n",[75,648,649,652,655,658,661,664],{"class":77,"line":89},[75,650,651],{"class":444},"  await",[75,653,654],{"class":85}," api.",[75,656,657],{"class":81},"commit",[75,659,660],{"class":85},"(form.",[75,662,663],{"class":81},"values",[75,665,666],{"class":85},"())\n",[75,668,669,672,675],{"class":77,"line":95},[75,670,671],{"class":85},"  form.history.",[75,673,674],{"class":81},"clear",[75,676,502],{"class":85},[75,678,679],{"class":77,"line":112},[75,680,681],{"class":85},"}\n",[17,683,684,685,361,687,105,690,105,693,696],{},"After ",[21,686,30],{},[21,688,689],{},"canUndo === false",[21,691,692],{},"canRedo === false",[21,694,695],{},"size === 1",". The current position is still reachable; there's just nothing on either side of it.",[61,698,700],{"id":699},"interactions","Interactions",[334,702,703,720],{},[337,704,705,710,711,714,715,717,718,173],{},[358,706,707],{},[21,708,709],{},"reset()"," is itself a mutation; the pre-reset state stays one undo away. Consumers who want a hard wipe call ",[21,712,713],{},"form.history.clear()"," after ",[21,716,709],{},", or pop a confirmation dialog before calling ",[21,719,709],{},[337,721,722,725],{},[358,723,724],{},"Live field validation"," still runs on undo \u002F redo; the restored state validates like any other.",[61,727,729],{"id":728},"memory","Memory",[17,731,732,733,736,737,740,741,744],{},"The default ",[21,734,735],{},"max: 128"," keeps at most 128 reachable positions across the undo + redo halves combined. Bump it for editors with long histories; drop it for memory-constrained targets. Internally history stores one base snapshot plus a chain of forward deltas (per-mutation ",[21,738,739],{},"Patch[]"," from the diff machinery), so each additional position costs ",[21,742,743],{},"O(changed-leaf-count)",". Typing one character into one field allocates a single patch, not a clone of the whole form.",[61,746,748],{"id":747},"where-to-next","Where to next",[334,750,751],{},[337,752,753,764],{},[754,755,757,760,761],"a",{"href":756},"\u002Fdocs\u002Fwriting-and-mutating\u002Freset",[21,758,759],{},"reset"," & ",[21,762,763],{},"resetField",": recorded as positions; the pre-reset state stays one undo away.",[766,767,768],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":71,"searchDepth":89,"depth":89,"links":770},[771,772,773,774,775,777,778,779],{"id":63,"depth":89,"text":64},{"id":176,"depth":89,"text":177},{"id":299,"depth":89,"text":300},{"id":403,"depth":89,"text":404},{"id":618,"depth":89,"text":776},"clear() at a milestone",{"id":699,"depth":89,"text":700},{"id":728,"depth":89,"text":729},{"id":747,"depth":89,"text":748},"history opt-in unlocks a per-form undo\u002Fredo chain. Every mutation records a position, undo() and redo() walk the timeline, clear() reseeds at a milestone.","md",{},[784,787,791,794],{"label":785,"value":786},"Category","Module",{"label":788,"value":789},"Opt in",{"useForm({ history":790,"kind":21},"true })",{"label":792,"value":793},"Default depth","128 positions",{"label":795,"value":183,"kind":21},"Namespace","\u002Fdocs\u002Fcross-cutting-state\u002Fundo-redo",{"title":5,"description":780},null,"docs\u002Fcross-cutting-state\u002Fundo-redo","QVn4Sc3bGNOy8upowTgnCSHaNLkhUIFc--kcdAxT8HY",1781745873787]