[{"data":1,"prerenderedAt":670},["ShallowReactive",2],{"content-\u002Fdocs\u002Fpersistence\u002Fper-field-opt-in":3},{"id":4,"title":5,"body":6,"description":644,"extension":645,"meta":646,"metaRows":647,"navigation":198,"path":648,"seo":649,"source":647,"stem":668,"__hash__":669},"docs\u002Fdocs\u002Fpersistence\u002Fper-field-opt-in.md","Per-field opt-in policy",{"type":7,"value":8,"toc":635},"minimark",[9,13,20,23,31,36,41,92,149,161,165,168,283,290,297,301,311,441,454,458,461,487,490,494,501,512,548,551,555,558,582,585,589,631],[10,11,5],"h1",{"id":12},"per-field-opt-in-policy",[14,15,16],"blockquote",{},[17,18,19],"p",{},"Two gates, one rule: a path persists only when the form opens a backend AND the field's register call says yes. Neither alone is enough.",[21,22],"docs-meta-table",{},[17,24,25,26,30],{},"Toggle the per-field checkboxes, type into the inputs, then refresh the page. Only the opted-in fields rehydrate; the others land empty even though the form is persisting to ",[27,28,29],"code",{},"sessionStorage"," and the schema has defaults. That's the two-gate policy in action: adding a new field can't accidentally leak into client-side storage unless its register call site explicitly says so.",[32,33],"docs-demo",{"label":34,"slug":35},"Per-field Persistence Demo","per-field-opt-in",[37,38,40],"h2",{"id":39},"the-two-gates","The two gates",[42,43,48],"pre",{"className":44,"code":45,"language":46,"meta":47,"style":47},"language-ts shiki shiki-themes github-light github-dark","useForm({\n  schema,\n  persist: 'session', \u002F\u002F ← gate 1: form-level backend\n})\n","ts","",[27,49,50,63,69,86],{"__ignoreMap":47},[51,52,55,59],"span",{"class":53,"line":54},"line",1,[51,56,58],{"class":57},"sScJk","useForm",[51,60,62],{"class":61},"sVt8B","({\n",[51,64,66],{"class":53,"line":65},2,[51,67,68],{"class":61},"  schema,\n",[51,70,72,75,79,82],{"class":53,"line":71},3,[51,73,74],{"class":61},"  persist: ",[51,76,78],{"class":77},"sZZnC","'session'",[51,80,81],{"class":61},", ",[51,83,85],{"class":84},"sJ8bj","\u002F\u002F ← gate 1: form-level backend\n",[51,87,89],{"class":53,"line":88},4,[51,90,91],{"class":61},"})\n",[42,93,97],{"className":94,"code":95,"language":96,"meta":47,"style":47},"language-vue shiki shiki-themes github-light github-dark","\u003Cinput v-register=\"form.register('email', { persist: true })\" \u002F>\n\u003C!-- ← gate 2: per-field -->\n","vue",[27,98,99,144],{"__ignoreMap":47},[51,100,101,104,108,111,114,117,120,123,126,129,132,136,139,141],{"class":53,"line":54},[51,102,103],{"class":61},"\u003C",[51,105,107],{"class":106},"s9eBZ","input",[51,109,110],{"class":57}," v-register",[51,112,113],{"class":61},"=",[51,115,116],{"class":77},"\"",[51,118,119],{"class":61},"form.",[51,121,122],{"class":57},"register",[51,124,125],{"class":61},"(",[51,127,128],{"class":77},"'email'",[51,130,131],{"class":61},", { persist: ",[51,133,135],{"class":134},"sj4cs","true",[51,137,138],{"class":61}," })",[51,140,116],{"class":77},[51,142,143],{"class":61}," \u002F>\n",[51,145,146],{"class":53,"line":65},[51,147,148],{"class":84},"\u003C!-- ← gate 2: per-field -->\n",[17,150,151,152,156,157,160],{},"Without both, no writes hit the backend. The form-level opt-in says ",[153,154,155],"em",{},"which"," backend to use; the field-level opt-in says ",[153,158,159],{},"which paths"," go into the sparse payload. The asymmetry is intentional: opt-in beats opt-out for forms that grow new fields over time.",[37,162,164],{"id":163},"the-sparse-payload","The sparse payload",[17,166,167],{},"The persisted envelope contains only opted-in paths:",[42,169,171],{"className":44,"code":170,"language":46,"meta":47,"style":47},"\u002F\u002F Schema:  { email: string, phone: string, cvv: string }\n\u002F\u002F register('email', { persist: true })\n\u002F\u002F register('phone', { persist: true })\n\u002F\u002F register('cvv')                       ← no opt-in\n\n\u002F\u002F Persisted payload, written under key attaform:signup:${fingerprint}\n{\n  v: 4,                                            \u002F\u002F attaform-internal envelope version\n  data: {\n    form: { email: '…', phone: '…' }               \u002F\u002F no `cvv`\n  }\n}\n",[27,172,173,178,183,188,193,200,206,212,230,239,271,277],{"__ignoreMap":47},[51,174,175],{"class":53,"line":54},[51,176,177],{"class":84},"\u002F\u002F Schema:  { email: string, phone: string, cvv: string }\n",[51,179,180],{"class":53,"line":65},[51,181,182],{"class":84},"\u002F\u002F register('email', { persist: true })\n",[51,184,185],{"class":53,"line":71},[51,186,187],{"class":84},"\u002F\u002F register('phone', { persist: true })\n",[51,189,190],{"class":53,"line":88},[51,191,192],{"class":84},"\u002F\u002F register('cvv')                       ← no opt-in\n",[51,194,196],{"class":53,"line":195},5,[51,197,199],{"emptyLinePlaceholder":198},true,"\n",[51,201,203],{"class":53,"line":202},6,[51,204,205],{"class":84},"\u002F\u002F Persisted payload, written under key attaform:signup:${fingerprint}\n",[51,207,209],{"class":53,"line":208},7,[51,210,211],{"class":61},"{\n",[51,213,215,218,221,224,227],{"class":53,"line":214},8,[51,216,217],{"class":57},"  v",[51,219,220],{"class":61},": ",[51,222,223],{"class":134},"4",[51,225,226],{"class":61},",                                            ",[51,228,229],{"class":84},"\u002F\u002F attaform-internal envelope version\n",[51,231,233,236],{"class":53,"line":232},9,[51,234,235],{"class":57},"  data",[51,237,238],{"class":61},": {\n",[51,240,242,245,248,251,253,256,258,261,263,265,268],{"class":53,"line":241},10,[51,243,244],{"class":57},"    form",[51,246,247],{"class":61},": { ",[51,249,250],{"class":57},"email",[51,252,220],{"class":61},[51,254,255],{"class":77},"'…'",[51,257,81],{"class":61},[51,259,260],{"class":57},"phone",[51,262,220],{"class":61},[51,264,255],{"class":77},[51,266,267],{"class":61}," }               ",[51,269,270],{"class":84},"\u002F\u002F no `cvv`\n",[51,272,274],{"class":53,"line":273},11,[51,275,276],{"class":61},"  }\n",[51,278,280],{"class":53,"line":279},12,[51,281,282],{"class":61},"}\n",[17,284,285,286,289],{},"The ",[27,287,288],{},"v"," field is internal to Attaform; it tracks the on-disk format and bumps only when Attaform changes the serialized shape. Drafts saved against a stale envelope version drop on read with a one-time dev warning.",[17,291,292,293,296],{},"On hydration, opted-in fields restore from storage; non-opted-in fields come from schema defaults. The opt-in set can change between mounts: a previously-persisted path that's no longer opted in stays in storage until the next write (which won't include it) or an explicit ",[27,294,295],{},"form.clearPersistedDraft(path)",".",[37,298,300],{"id":299},"reactive-opt-in","Reactive opt-in",[17,302,285,303,306,307,310],{},[27,304,305],{},"persist"," flag is reactive. Pass a ",[27,308,309],{},"ref\u003Cboolean>"," and the directive's update hook adds or removes the opt-in at runtime:",[42,312,314],{"className":94,"code":313,"language":96,"meta":47,"style":47},"\u003Cscript setup lang=\"ts\">\n  const rememberMe = ref(false)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-register=\"form.register('email', { persist: rememberMe })\" \u002F>\n  \u003Clabel>\u003Cinput type=\"checkbox\" v-model=\"rememberMe\" \u002F> Remember me\u003C\u002Flabel>\n\u003C\u002Ftemplate>\n",[27,315,316,337,360,369,373,382,398,433],{"__ignoreMap":47},[51,317,318,320,323,326,329,331,334],{"class":53,"line":54},[51,319,103],{"class":61},[51,321,322],{"class":106},"script",[51,324,325],{"class":57}," setup",[51,327,328],{"class":57}," lang",[51,330,113],{"class":61},[51,332,333],{"class":77},"\"ts\"",[51,335,336],{"class":61},">\n",[51,338,339,343,346,349,352,354,357],{"class":53,"line":65},[51,340,342],{"class":341},"szBVR","  const",[51,344,345],{"class":134}," rememberMe",[51,347,348],{"class":341}," =",[51,350,351],{"class":57}," ref",[51,353,125],{"class":61},[51,355,356],{"class":134},"false",[51,358,359],{"class":61},")\n",[51,361,362,365,367],{"class":53,"line":71},[51,363,364],{"class":61},"\u003C\u002F",[51,366,322],{"class":106},[51,368,336],{"class":61},[51,370,371],{"class":53,"line":88},[51,372,199],{"emptyLinePlaceholder":198},[51,374,375,377,380],{"class":53,"line":195},[51,376,103],{"class":61},[51,378,379],{"class":106},"template",[51,381,336],{"class":61},[51,383,384,387,389,391,393,396],{"class":53,"line":202},[51,385,386],{"class":61},"  \u003C",[51,388,107],{"class":106},[51,390,110],{"class":57},[51,392,113],{"class":61},[51,394,395],{"class":77},"\"form.register('email', { persist: rememberMe })\"",[51,397,143],{"class":61},[51,399,400,402,405,408,410,413,415,418,421,423,426,429,431],{"class":53,"line":208},[51,401,386],{"class":61},[51,403,404],{"class":106},"label",[51,406,407],{"class":61},">\u003C",[51,409,107],{"class":106},[51,411,412],{"class":57}," type",[51,414,113],{"class":61},[51,416,417],{"class":77},"\"checkbox\"",[51,419,420],{"class":57}," v-model",[51,422,113],{"class":61},[51,424,425],{"class":77},"\"rememberMe\"",[51,427,428],{"class":61}," \u002F> Remember me\u003C\u002F",[51,430,404],{"class":106},[51,432,336],{"class":61},[51,434,435,437,439],{"class":53,"line":214},[51,436,364],{"class":61},[51,438,379],{"class":106},[51,440,336],{"class":61},[17,442,443,444,447,448,450,451,453],{},"Flip ",[27,445,446],{},"rememberMe"," ",[27,449,356],{}," → ",[27,452,135],{}," and the directive adds the opt-in. Future writes from this input persist. Flip it back and the opt-in is removed; writes go in-memory only. No remount, no manual cleanup.",[37,455,457],{"id":456},"cross-sfc-behavior","Cross-SFC behavior",[17,459,460],{},"Two SFCs binding the same path under the same form key share the FormStore and the persistence registry. Opt-ins are per-DOM-element, not per-SFC:",[462,463,464,475,484],"ul",{},[465,466,467,468,470,471,474],"li",{},"SFC A renders an input bound to ",[27,469,128],{}," with ",[27,472,473],{},"persist: true"," → A's element opted in.",[465,476,477,478,480,481,483],{},"SFC B renders an input bound to ",[27,479,128],{}," without ",[27,482,305],{}," → B's element NOT opted in.",[465,485,486],{},"Typing in A persists. Typing in B doesn't.",[17,488,489],{},"Unmount SFC A and B's typing stops persisting (no opt-ins remain). Re-mount A and the new element gets a fresh opt-in. The registry tracks elements via WeakMap; rapid mount\u002Funmount cycles auto-clean without any explicit dispose.",[37,491,493],{"id":492},"including-errors","Including errors",[17,495,496,497,500],{},"Default ",[27,498,499],{},"include: 'form'"," persists just the values. Server-side validation errors on reload would be stale by then.",[17,502,503,504,507,508,511],{},"For multi-step wizards where reconstructing errors is expensive, ",[27,505,506],{},"include: 'form+errors'"," persists and re-hydrates the ",[27,509,510],{},"errors"," map alongside the values:",[42,513,515],{"className":44,"code":514,"language":46,"meta":47,"style":47},"useForm({\n  schema,\n  persist: { storage: 'local', include: 'form+errors' },\n})\n",[27,516,517,523,527,544],{"__ignoreMap":47},[51,518,519,521],{"class":53,"line":54},[51,520,58],{"class":57},[51,522,62],{"class":61},[51,524,525],{"class":53,"line":65},[51,526,68],{"class":61},[51,528,529,532,535,538,541],{"class":53,"line":71},[51,530,531],{"class":61},"  persist: { storage: ",[51,533,534],{"class":77},"'local'",[51,536,537],{"class":61},", include: ",[51,539,540],{"class":77},"'form+errors'",[51,542,543],{"class":61}," },\n",[51,545,546],{"class":53,"line":88},[51,547,91],{"class":61},[17,549,550],{},"Errors on non-opted-in paths are dropped from the envelope; a persisted error without a persisted value would dangle on rehydration.",[37,552,554],{"id":553},"dev-mode-footgun-checks","Dev-mode footgun checks",[17,556,557],{},"Two symmetric warnings catch \"wired half the pipeline\" bugs (once per form in dev, silent in production):",[462,559,560,570],{},[465,561,562,569],{},[563,564,565,568],"strong",{},[27,566,567],{},"persist:"," configured but no field opts in"," → drafts never save.",[465,571,572,581],{},[563,573,574,577,578,580],{},[27,575,576],{},"register({ persist: true })"," but no ",[27,579,567],{}," on the form"," → opt-ins recorded, no writes land.",[17,583,584],{},"The warnings name the form key and (where applicable) the offending path.",[37,586,588],{"id":587},"where-to-next","Where to next",[462,590,591,599,617],{},[465,592,593,598],{},[594,595,597],"a",{"href":596},"\u002Fdocs\u002Fpersistence\u002Fstorage-backends","Storage backends",": the first of the two gates.",[465,600,601,605,606,609,610,609,613,616],{},[594,602,604],{"href":603},"\u002Fdocs\u002Fpersistence\u002Fsensitive-names","Sensitive-name protection",": the heuristic that warns and skips the opt-in on ",[27,607,608],{},"password"," \u002F ",[27,611,612],{},"cvv",[27,614,615],{},"ssn"," paths.",[465,618,619,220,623,626,627,630],{},[594,620,622],{"href":621},"\u002Fdocs\u002Fpersistence\u002Fimperative","Imperative persistence",[27,624,625],{},"form.persist()"," and ",[27,628,629],{},"form.clearPersistedDraft()"," for \"Save draft\" buttons and explicit cleanup.",[632,633,634],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":47,"searchDepth":65,"depth":65,"links":636},[637,638,639,640,641,642,643],{"id":39,"depth":65,"text":40},{"id":163,"depth":65,"text":164},{"id":299,"depth":65,"text":300},{"id":456,"depth":65,"text":457},{"id":492,"depth":65,"text":493},{"id":553,"depth":65,"text":554},{"id":587,"depth":65,"text":588},"[object Object]","md",{},null,"\u002Fdocs\u002Fpersistence\u002Fper-field-opt-in",{"title":5,"description":650},{"Persistence requires two opt-ins":651,"metaRows":654},{" Form-level picks the backend, field-level marks each path":652},{" The sparse payload only carries the paths whose register call site says { persist":653},"true }.",[655,658,661,665],{"label":656,"value":657},"Category","Module",{"label":659,"value":660,"kind":27},"Form-level opt-in","useForm({ persist })",{"label":662,"value":663},"Field-level opt-in",{"register(path, { persist":664,"kind":27},"true })",{"label":666,"value":667},"Reactive","persist flag accepts a ref or boolean","docs\u002Fpersistence\u002Fper-field-opt-in","mkt_qLYSqEIgIvX_pF1rihpZiUo9nlCbXU-v9FLiPH4",1780949760909]