[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"knowledge-\u002Fapi\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fforms":3,"knowledge-related-\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fforms":2268,"knowledge-series-\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fforms":2291},{"source":4,"connector":5,"page":6},"sqlite","better-sqlite3",{"path":7,"title":8,"description":9,"category":10,"tags":11,"date":14,"pinned":15,"draft":15,"body":16,"seo":2264,"stem":2265,"id":2266,"extension":2267},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fforms","表单","Angular 提供 响应式表单（Reactive Forms） 与 模板驱动表单（Template-driven）。复杂表单推荐 Reactive Forms（模型在 TypeScript 中，易测试）。涵盖 FormGroup\u002FFormArray、Typed Forms、自定义校验器、最佳实践与常见坑。","技能小册",[12,13],"angular","forms","2025-05-26",false,{"type":17,"value":18,"toc":2251},"minimark",[19,22,45,49,52,87,90,497,501,801,1025,1032,1035,1380,1398,1401,1414,1537,1540,1544,1554,1730,1739,1742,1762,1981,1984,2036,2039,2146,2149,2170,2173,2176,2247],[20,21,8],"h1",{"id":8},[23,24,25,26,30,31,34,35,38,39,44],"p",{},"Angular 提供 ",[27,28,29],"strong",{},"响应式表单（Reactive Forms）"," 与 ",[27,32,33],{},"模板驱动表单（Template-driven）","。复杂表单推荐 ",[27,36,37],{},"Reactive Forms","（模型在 TypeScript 中，易测试）。见 ",[40,41,43],"a",{"href":42},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fcomponents","组件与模板","。",[46,47,48],"h2",{"id":48},"核心概念",[23,50,51],{},"两种表单方案的本质区别在于**「表单模型的定义位置」**：",[53,54,55,77],"ul",{},[56,57,58,60,61,65,66,65,69,72,73,76],"li",{},[27,59,37],{},"：在组件类中用 ",[62,63,64],"code",{},"FormControl"," \u002F ",[62,67,68],{},"FormGroup",[62,70,71],{},"FormArray"," 显式构建表单模型，模板只负责绑定 ",[62,74,75],{},"formControlName","，逻辑集中、易单测、类型可推断。",[56,78,79,82,83,86],{},[27,80,81],{},"Template-driven","：表单模型隐式地由模板中的 ",[62,84,85],{},"[(ngModel)]"," 生成，写法更接近传统 HTML 表单，适合简单场景，但复杂校验和动态字段处理起来不如 Reactive 直观。",[46,88,89],{"id":89},"响应式表单",[91,92,97],"pre",{"className":93,"code":94,"language":95,"meta":96,"style":96},"language-ts shiki shiki-themes material-theme-lighter github-light github-dark","import { Component } from \"@angular\u002Fcore\";\nimport { FormControl, FormGroup, ReactiveFormsModule, Validators } from \"@angular\u002Fforms\";\n\n@Component({\n  standalone: true,\n  imports: [ReactiveFormsModule],\n  template: `\n    \u003Cform [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n      \u003Cinput formControlName=\"email\" type=\"email\" \u002F>\n      @if (form.controls.email.invalid && form.controls.email.touched) {\n        \u003Cspan>邮箱无效\u003C\u002Fspan>\n      }\n      \u003Cbutton type=\"submit\" [disabled]=\"form.invalid\">提交\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  `,\n})\nexport class LoginComponent {\n  form = new FormGroup({\n    email: new FormControl(\"\", { nonNullable: true, validators: [Validators.required, Validators.email] }),\n  });\n\n  onSubmit() {\n    if (this.form.valid) {\n      console.log(this.form.getRawValue());\n    }\n  }\n}\n","ts","",[62,98,99,136,175,182,198,215,228,239,245,251,257,263,269,275,281,289,298,315,336,396,406,411,422,450,479,485,491],{"__ignoreMap":96},[100,101,104,108,112,116,119,122,126,130,133],"span",{"class":102,"line":103},"line",1,[100,105,107],{"class":106},"sVHd0","import",[100,109,111],{"class":110},"sP7_E"," {",[100,113,115],{"class":114},"su5hD"," Component",[100,117,118],{"class":110}," }",[100,120,121],{"class":106}," from",[100,123,125],{"class":124},"sjJ54"," \"",[100,127,129],{"class":128},"s_sjI","@angular\u002Fcore",[100,131,132],{"class":124},"\"",[100,134,135],{"class":110},";\n",[100,137,139,141,143,146,149,152,154,157,159,162,164,166,168,171,173],{"class":102,"line":138},2,[100,140,107],{"class":106},[100,142,111],{"class":110},[100,144,145],{"class":114}," FormControl",[100,147,148],{"class":110},",",[100,150,151],{"class":114}," FormGroup",[100,153,148],{"class":110},[100,155,156],{"class":114}," ReactiveFormsModule",[100,158,148],{"class":110},[100,160,161],{"class":114}," Validators",[100,163,118],{"class":110},[100,165,121],{"class":106},[100,167,125],{"class":124},[100,169,170],{"class":128},"@angular\u002Fforms",[100,172,132],{"class":124},[100,174,135],{"class":110},[100,176,178],{"class":102,"line":177},3,[100,179,181],{"emptyLinePlaceholder":180},true,"\n",[100,183,185,188,192,195],{"class":102,"line":184},4,[100,186,187],{"class":110},"@",[100,189,191],{"class":190},"sGLFI","Component",[100,193,194],{"class":114},"(",[100,196,197],{"class":110},"{\n",[100,199,201,205,208,212],{"class":102,"line":200},5,[100,202,204],{"class":203},"skxfh","  standalone",[100,206,207],{"class":110},":",[100,209,211],{"class":210},"syTEX"," true",[100,213,214],{"class":110},",\n",[100,216,218,221,223,226],{"class":102,"line":217},6,[100,219,220],{"class":203},"  imports",[100,222,207],{"class":110},[100,224,225],{"class":114}," [ReactiveFormsModule]",[100,227,214],{"class":110},[100,229,231,234,236],{"class":102,"line":230},7,[100,232,233],{"class":203},"  template",[100,235,207],{"class":110},[100,237,238],{"class":124}," `\n",[100,240,242],{"class":102,"line":241},8,[100,243,244],{"class":128},"    \u003Cform [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n",[100,246,248],{"class":102,"line":247},9,[100,249,250],{"class":128},"      \u003Cinput formControlName=\"email\" type=\"email\" \u002F>\n",[100,252,254],{"class":102,"line":253},10,[100,255,256],{"class":128},"      @if (form.controls.email.invalid && form.controls.email.touched) {\n",[100,258,260],{"class":102,"line":259},11,[100,261,262],{"class":128},"        \u003Cspan>邮箱无效\u003C\u002Fspan>\n",[100,264,266],{"class":102,"line":265},12,[100,267,268],{"class":128},"      }\n",[100,270,272],{"class":102,"line":271},13,[100,273,274],{"class":128},"      \u003Cbutton type=\"submit\" [disabled]=\"form.invalid\">提交\u003C\u002Fbutton>\n",[100,276,278],{"class":102,"line":277},14,[100,279,280],{"class":128},"    \u003C\u002Fform>\n",[100,282,284,287],{"class":102,"line":283},15,[100,285,286],{"class":124},"  `",[100,288,214],{"class":110},[100,290,292,295],{"class":102,"line":291},16,[100,293,294],{"class":110},"}",[100,296,297],{"class":114},")\n",[100,299,301,304,308,312],{"class":102,"line":300},17,[100,302,303],{"class":106},"export",[100,305,307],{"class":306},"sbsja"," class",[100,309,311],{"class":310},"sbgvK"," LoginComponent",[100,313,314],{"class":110}," {\n",[100,316,318,322,326,329,332,334],{"class":102,"line":317},18,[100,319,321],{"class":320},"sucvu","  form",[100,323,325],{"class":324},"smGrS"," =",[100,327,328],{"class":324}," new",[100,330,151],{"class":331},"sVXei",[100,333,194],{"class":114},[100,335,197],{"class":110},[100,337,339,342,344,346,348,350,353,355,357,360,362,364,366,369,371,374,377,380,382,384,386,389,391,394],{"class":102,"line":338},19,[100,340,341],{"class":203},"    email",[100,343,207],{"class":110},[100,345,328],{"class":324},[100,347,145],{"class":331},[100,349,194],{"class":114},[100,351,352],{"class":124},"\"\"",[100,354,148],{"class":110},[100,356,111],{"class":110},[100,358,359],{"class":203}," nonNullable",[100,361,207],{"class":110},[100,363,211],{"class":210},[100,365,148],{"class":110},[100,367,368],{"class":203}," validators",[100,370,207],{"class":110},[100,372,373],{"class":114}," [Validators",[100,375,376],{"class":110},".",[100,378,379],{"class":114},"required",[100,381,148],{"class":110},[100,383,161],{"class":114},[100,385,376],{"class":110},[100,387,388],{"class":114},"email] ",[100,390,294],{"class":110},[100,392,393],{"class":114},")",[100,395,214],{"class":110},[100,397,399,402,404],{"class":102,"line":398},20,[100,400,401],{"class":110},"  }",[100,403,393],{"class":114},[100,405,135],{"class":110},[100,407,409],{"class":102,"line":408},21,[100,410,181],{"emptyLinePlaceholder":180},[100,412,414,417,420],{"class":102,"line":413},22,[100,415,416],{"class":331},"  onSubmit",[100,418,419],{"class":110},"()",[100,421,314],{"class":110},[100,423,425,428,431,435,437,440,442,445,448],{"class":102,"line":424},23,[100,426,427],{"class":106},"    if",[100,429,430],{"class":203}," (",[100,432,434],{"class":433},"s39Yj","this",[100,436,376],{"class":110},[100,438,439],{"class":114},"form",[100,441,376],{"class":110},[100,443,444],{"class":114},"valid",[100,446,447],{"class":203},") ",[100,449,197],{"class":110},[100,451,453,456,458,461,463,465,467,469,471,474,477],{"class":102,"line":452},24,[100,454,455],{"class":114},"      console",[100,457,376],{"class":110},[100,459,460],{"class":190},"log",[100,462,194],{"class":203},[100,464,434],{"class":433},[100,466,376],{"class":110},[100,468,439],{"class":114},[100,470,376],{"class":110},[100,472,473],{"class":190},"getRawValue",[100,475,476],{"class":203},"())",[100,478,135],{"class":110},[100,480,482],{"class":102,"line":481},25,[100,483,484],{"class":110},"    }\n",[100,486,488],{"class":102,"line":487},26,[100,489,490],{"class":110},"  }\n",[100,492,494],{"class":102,"line":493},27,[100,495,496],{"class":110},"}\n",[46,498,500],{"id":499},"formarray动态字段列表","FormArray：动态字段列表",[91,502,504],{"className":93,"code":503,"language":95,"meta":96,"style":96},"form = new FormGroup({\n  contacts: new FormArray([this.createContact()]),\n});\n\ncreateContact() {\n  return new FormGroup({\n    name: new FormControl(\"\", { nonNullable: true, validators: [Validators.required] }),\n    phone: new FormControl(\"\", { nonNullable: true }),\n  });\n}\n\nget contacts() {\n  return this.form.controls.contacts;\n}\n\naddContact() {\n  this.contacts.push(this.createContact());\n}\n\nremoveContact(index: number) {\n  this.contacts.removeAt(index);\n}\n",[62,505,506,522,549,557,561,570,583,633,664,672,676,680,692,714,718,722,731,757,761,765,775,797],{"__ignoreMap":96},[100,507,508,511,514,516,518,520],{"class":102,"line":103},[100,509,510],{"class":114},"form ",[100,512,513],{"class":324},"=",[100,515,328],{"class":324},[100,517,151],{"class":190},[100,519,194],{"class":114},[100,521,197],{"class":110},[100,523,524,527,529,531,534,537,539,541,544,547],{"class":102,"line":138},[100,525,526],{"class":203},"  contacts",[100,528,207],{"class":110},[100,530,328],{"class":324},[100,532,533],{"class":190}," FormArray",[100,535,536],{"class":114},"([",[100,538,434],{"class":433},[100,540,376],{"class":110},[100,542,543],{"class":190},"createContact",[100,545,546],{"class":114},"()])",[100,548,214],{"class":110},[100,550,551,553,555],{"class":102,"line":177},[100,552,294],{"class":110},[100,554,393],{"class":114},[100,556,135],{"class":110},[100,558,559],{"class":102,"line":184},[100,560,181],{"emptyLinePlaceholder":180},[100,562,563,565,568],{"class":102,"line":200},[100,564,543],{"class":190},[100,566,567],{"class":114},"() ",[100,569,197],{"class":110},[100,571,572,575,577,579,581],{"class":102,"line":217},[100,573,574],{"class":106},"  return",[100,576,328],{"class":324},[100,578,151],{"class":190},[100,580,194],{"class":203},[100,582,197],{"class":110},[100,584,585,588,590,592,594,596,598,600,602,604,606,608,610,612,614,617,620,622,624,627,629,631],{"class":102,"line":230},[100,586,587],{"class":203},"    name",[100,589,207],{"class":110},[100,591,328],{"class":324},[100,593,145],{"class":190},[100,595,194],{"class":203},[100,597,352],{"class":124},[100,599,148],{"class":110},[100,601,111],{"class":110},[100,603,359],{"class":203},[100,605,207],{"class":110},[100,607,211],{"class":210},[100,609,148],{"class":110},[100,611,368],{"class":203},[100,613,207],{"class":110},[100,615,616],{"class":203}," [",[100,618,619],{"class":114},"Validators",[100,621,376],{"class":110},[100,623,379],{"class":114},[100,625,626],{"class":203},"] ",[100,628,294],{"class":110},[100,630,393],{"class":203},[100,632,214],{"class":110},[100,634,635,638,640,642,644,646,648,650,652,654,656,658,660,662],{"class":102,"line":241},[100,636,637],{"class":203},"    phone",[100,639,207],{"class":110},[100,641,328],{"class":324},[100,643,145],{"class":190},[100,645,194],{"class":203},[100,647,352],{"class":124},[100,649,148],{"class":110},[100,651,111],{"class":110},[100,653,359],{"class":203},[100,655,207],{"class":110},[100,657,211],{"class":210},[100,659,118],{"class":110},[100,661,393],{"class":203},[100,663,214],{"class":110},[100,665,666,668,670],{"class":102,"line":247},[100,667,401],{"class":110},[100,669,393],{"class":203},[100,671,135],{"class":110},[100,673,674],{"class":102,"line":253},[100,675,496],{"class":110},[100,677,678],{"class":102,"line":259},[100,679,181],{"emptyLinePlaceholder":180},[100,681,682,685,688,690],{"class":102,"line":265},[100,683,684],{"class":114},"get ",[100,686,687],{"class":190},"contacts",[100,689,567],{"class":114},[100,691,197],{"class":110},[100,693,694,696,699,701,703,705,708,710,712],{"class":102,"line":271},[100,695,574],{"class":106},[100,697,698],{"class":433}," this",[100,700,376],{"class":110},[100,702,439],{"class":114},[100,704,376],{"class":110},[100,706,707],{"class":114},"controls",[100,709,376],{"class":110},[100,711,687],{"class":114},[100,713,135],{"class":110},[100,715,716],{"class":102,"line":277},[100,717,496],{"class":110},[100,719,720],{"class":102,"line":283},[100,721,181],{"emptyLinePlaceholder":180},[100,723,724,727,729],{"class":102,"line":291},[100,725,726],{"class":190},"addContact",[100,728,567],{"class":114},[100,730,197],{"class":110},[100,732,733,736,738,740,742,745,747,749,751,753,755],{"class":102,"line":300},[100,734,735],{"class":433},"  this",[100,737,376],{"class":110},[100,739,687],{"class":114},[100,741,376],{"class":110},[100,743,744],{"class":190},"push",[100,746,194],{"class":203},[100,748,434],{"class":433},[100,750,376],{"class":110},[100,752,543],{"class":190},[100,754,476],{"class":203},[100,756,135],{"class":110},[100,758,759],{"class":102,"line":317},[100,760,496],{"class":110},[100,762,763],{"class":102,"line":338},[100,764,181],{"emptyLinePlaceholder":180},[100,766,767,770,773],{"class":102,"line":398},[100,768,769],{"class":190},"removeContact",[100,771,772],{"class":114},"(index: number) ",[100,774,197],{"class":110},[100,776,777,779,781,783,785,788,790,793,795],{"class":102,"line":408},[100,778,735],{"class":433},[100,780,376],{"class":110},[100,782,687],{"class":114},[100,784,376],{"class":110},[100,786,787],{"class":190},"removeAt",[100,789,194],{"class":203},[100,791,792],{"class":114},"index",[100,794,393],{"class":203},[100,796,135],{"class":110},[100,798,799],{"class":102,"line":413},[100,800,496],{"class":110},[91,802,806],{"className":803,"code":804,"language":805,"meta":96,"style":96},"language-html shiki shiki-themes material-theme-lighter github-light github-dark","\u003Cdiv formArrayName=\"contacts\">\n  @for (group of contacts.controls; track $index; let i = $index) {\n    \u003Cdiv [formGroupName]=\"i\">\n      \u003Cinput formControlName=\"name\" placeholder=\"姓名\" \u002F>\n      \u003Cinput formControlName=\"phone\" placeholder=\"电话\" \u002F>\n      \u003Cbutton type=\"button\" (click)=\"removeContact(i)\">删除\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  }\n\u003C\u002Fdiv>\n\u003Cbutton type=\"button\" (click)=\"addContact()\">新增联系人\u003C\u002Fbutton>\n","html",[62,807,808,832,837,858,893,923,966,975,979,987],{"__ignoreMap":96},[100,809,810,813,817,821,823,825,827,829],{"class":102,"line":103},[100,811,812],{"class":110},"\u003C",[100,814,816],{"class":815},"sQzsp","div",[100,818,820],{"class":819},"s9AJx"," formArrayName",[100,822,513],{"class":110},[100,824,132],{"class":124},[100,826,687],{"class":128},[100,828,132],{"class":124},[100,830,831],{"class":110},">\n",[100,833,834],{"class":102,"line":138},[100,835,836],{"class":114},"  @for (group of contacts.controls; track $index; let i = $index) {\n",[100,838,839,842,844,847,849,851,854,856],{"class":102,"line":177},[100,840,841],{"class":110},"    \u003C",[100,843,816],{"class":815},[100,845,846],{"class":819}," [formGroupName]",[100,848,513],{"class":110},[100,850,132],{"class":124},[100,852,853],{"class":128},"i",[100,855,132],{"class":124},[100,857,831],{"class":110},[100,859,860,863,866,869,871,873,876,878,881,883,885,888,890],{"class":102,"line":184},[100,861,862],{"class":110},"      \u003C",[100,864,865],{"class":815},"input",[100,867,868],{"class":819}," formControlName",[100,870,513],{"class":110},[100,872,132],{"class":124},[100,874,875],{"class":128},"name",[100,877,132],{"class":124},[100,879,880],{"class":819}," placeholder",[100,882,513],{"class":110},[100,884,132],{"class":124},[100,886,887],{"class":128},"姓名",[100,889,132],{"class":124},[100,891,892],{"class":110}," \u002F>\n",[100,894,895,897,899,901,903,905,908,910,912,914,916,919,921],{"class":102,"line":200},[100,896,862],{"class":110},[100,898,865],{"class":815},[100,900,868],{"class":819},[100,902,513],{"class":110},[100,904,132],{"class":124},[100,906,907],{"class":128},"phone",[100,909,132],{"class":124},[100,911,880],{"class":819},[100,913,513],{"class":110},[100,915,132],{"class":124},[100,917,918],{"class":128},"电话",[100,920,132],{"class":124},[100,922,892],{"class":110},[100,924,925,927,930,933,935,937,939,941,944,946,948,951,953,956,959,962,964],{"class":102,"line":217},[100,926,862],{"class":110},[100,928,929],{"class":815},"button",[100,931,932],{"class":819}," type",[100,934,513],{"class":110},[100,936,132],{"class":124},[100,938,929],{"class":128},[100,940,132],{"class":124},[100,942,943],{"class":819}," (click)",[100,945,513],{"class":110},[100,947,132],{"class":124},[100,949,950],{"class":128},"removeContact(i)",[100,952,132],{"class":124},[100,954,955],{"class":110},">",[100,957,958],{"class":114},"删除",[100,960,961],{"class":110},"\u003C\u002F",[100,963,929],{"class":815},[100,965,831],{"class":110},[100,967,968,971,973],{"class":102,"line":230},[100,969,970],{"class":110},"    \u003C\u002F",[100,972,816],{"class":815},[100,974,831],{"class":110},[100,976,977],{"class":102,"line":241},[100,978,490],{"class":114},[100,980,981,983,985],{"class":102,"line":247},[100,982,961],{"class":110},[100,984,816],{"class":815},[100,986,831],{"class":110},[100,988,989,991,993,995,997,999,1001,1003,1005,1007,1009,1012,1014,1016,1019,1021,1023],{"class":102,"line":253},[100,990,812],{"class":110},[100,992,929],{"class":815},[100,994,932],{"class":819},[100,996,513],{"class":110},[100,998,132],{"class":124},[100,1000,929],{"class":128},[100,1002,132],{"class":124},[100,1004,943],{"class":819},[100,1006,513],{"class":110},[100,1008,132],{"class":124},[100,1010,1011],{"class":128},"addContact()",[100,1013,132],{"class":124},[100,1015,955],{"class":110},[100,1017,1018],{"class":114},"新增联系人",[100,1020,961],{"class":110},[100,1022,929],{"class":815},[100,1024,831],{"class":110},[23,1026,1027,1029,1030,44],{},[62,1028,71],{}," 适合处理「联系人列表」「多个地址」等数量不固定的字段组，每一项本身可以是 ",[62,1031,68],{},[46,1033,1034],{"id":1034},"自定义校验器",[91,1036,1038],{"className":93,"code":1037,"language":95,"meta":96,"style":96},"import { AbstractControl, ValidationErrors, ValidatorFn } from \"@angular\u002Fforms\";\n\nexport function passwordMatch(passwordKey: string, confirmKey: string): ValidatorFn {\n  return (group: AbstractControl): ValidationErrors | null => {\n    const password = group.get(passwordKey)?.value;\n    const confirm = group.get(confirmKey)?.value;\n    return password === confirm ? null : { passwordMismatch: true };\n  };\n}\n\n\u002F\u002F 使用\nform = new FormGroup(\n  {\n    password: new FormControl(\"\", { nonNullable: true }),\n    confirm: new FormControl(\"\", { nonNullable: true }),\n  },\n  { validators: passwordMatch(\"password\", \"confirm\") }\n);\n",[62,1039,1040,1071,1075,1114,1144,1177,1205,1237,1242,1246,1250,1256,1269,1274,1305,1336,1341,1374],{"__ignoreMap":96},[100,1041,1042,1044,1046,1049,1051,1054,1056,1059,1061,1063,1065,1067,1069],{"class":102,"line":103},[100,1043,107],{"class":106},[100,1045,111],{"class":110},[100,1047,1048],{"class":114}," AbstractControl",[100,1050,148],{"class":110},[100,1052,1053],{"class":114}," ValidationErrors",[100,1055,148],{"class":110},[100,1057,1058],{"class":114}," ValidatorFn",[100,1060,118],{"class":110},[100,1062,121],{"class":106},[100,1064,125],{"class":124},[100,1066,170],{"class":128},[100,1068,132],{"class":124},[100,1070,135],{"class":110},[100,1072,1073],{"class":102,"line":138},[100,1074,181],{"emptyLinePlaceholder":180},[100,1076,1077,1079,1082,1085,1087,1091,1093,1097,1099,1102,1104,1106,1108,1110,1112],{"class":102,"line":177},[100,1078,303],{"class":106},[100,1080,1081],{"class":306}," function",[100,1083,1084],{"class":190}," passwordMatch",[100,1086,194],{"class":110},[100,1088,1090],{"class":1089},"s99_P","passwordKey",[100,1092,207],{"class":324},[100,1094,1096],{"class":1095},"sZMiF"," string",[100,1098,148],{"class":110},[100,1100,1101],{"class":1089}," confirmKey",[100,1103,207],{"class":324},[100,1105,1096],{"class":1095},[100,1107,393],{"class":110},[100,1109,207],{"class":324},[100,1111,1058],{"class":310},[100,1113,314],{"class":110},[100,1115,1116,1118,1120,1123,1125,1127,1129,1131,1133,1136,1139,1142],{"class":102,"line":184},[100,1117,574],{"class":106},[100,1119,430],{"class":110},[100,1121,1122],{"class":1089},"group",[100,1124,207],{"class":324},[100,1126,1048],{"class":310},[100,1128,393],{"class":110},[100,1130,207],{"class":324},[100,1132,1053],{"class":310},[100,1134,1135],{"class":324}," |",[100,1137,1138],{"class":1095}," null",[100,1140,1141],{"class":306}," =>",[100,1143,314],{"class":110},[100,1145,1146,1149,1153,1155,1158,1160,1163,1165,1167,1169,1172,1175],{"class":102,"line":200},[100,1147,1148],{"class":306},"    const",[100,1150,1152],{"class":1151},"s_hVV"," password",[100,1154,325],{"class":324},[100,1156,1157],{"class":114}," group",[100,1159,376],{"class":110},[100,1161,1162],{"class":190},"get",[100,1164,194],{"class":203},[100,1166,1090],{"class":114},[100,1168,393],{"class":203},[100,1170,1171],{"class":110},"?.",[100,1173,1174],{"class":114},"value",[100,1176,135],{"class":110},[100,1178,1179,1181,1184,1186,1188,1190,1192,1194,1197,1199,1201,1203],{"class":102,"line":217},[100,1180,1148],{"class":306},[100,1182,1183],{"class":1151}," confirm",[100,1185,325],{"class":324},[100,1187,1157],{"class":114},[100,1189,376],{"class":110},[100,1191,1162],{"class":190},[100,1193,194],{"class":203},[100,1195,1196],{"class":114},"confirmKey",[100,1198,393],{"class":203},[100,1200,1171],{"class":110},[100,1202,1174],{"class":114},[100,1204,135],{"class":110},[100,1206,1207,1210,1212,1215,1217,1220,1222,1225,1227,1230,1232,1234],{"class":102,"line":230},[100,1208,1209],{"class":106},"    return",[100,1211,1152],{"class":114},[100,1213,1214],{"class":324}," ===",[100,1216,1183],{"class":114},[100,1218,1219],{"class":324}," ?",[100,1221,1138],{"class":433},[100,1223,1224],{"class":324}," :",[100,1226,111],{"class":110},[100,1228,1229],{"class":203}," passwordMismatch",[100,1231,207],{"class":110},[100,1233,211],{"class":210},[100,1235,1236],{"class":110}," };\n",[100,1238,1239],{"class":102,"line":241},[100,1240,1241],{"class":110},"  };\n",[100,1243,1244],{"class":102,"line":247},[100,1245,496],{"class":110},[100,1247,1248],{"class":102,"line":253},[100,1249,181],{"emptyLinePlaceholder":180},[100,1251,1252],{"class":102,"line":259},[100,1253,1255],{"class":1254},"sutJx","\u002F\u002F 使用\n",[100,1257,1258,1260,1262,1264,1266],{"class":102,"line":265},[100,1259,510],{"class":114},[100,1261,513],{"class":324},[100,1263,328],{"class":324},[100,1265,151],{"class":190},[100,1267,1268],{"class":114},"(\n",[100,1270,1271],{"class":102,"line":271},[100,1272,1273],{"class":110},"  {\n",[100,1275,1276,1279,1281,1283,1285,1287,1289,1291,1293,1295,1297,1299,1301,1303],{"class":102,"line":277},[100,1277,1278],{"class":203},"    password",[100,1280,207],{"class":110},[100,1282,328],{"class":324},[100,1284,145],{"class":190},[100,1286,194],{"class":114},[100,1288,352],{"class":124},[100,1290,148],{"class":110},[100,1292,111],{"class":110},[100,1294,359],{"class":203},[100,1296,207],{"class":110},[100,1298,211],{"class":210},[100,1300,118],{"class":110},[100,1302,393],{"class":114},[100,1304,214],{"class":110},[100,1306,1307,1310,1312,1314,1316,1318,1320,1322,1324,1326,1328,1330,1332,1334],{"class":102,"line":283},[100,1308,1309],{"class":203},"    confirm",[100,1311,207],{"class":110},[100,1313,328],{"class":324},[100,1315,145],{"class":190},[100,1317,194],{"class":114},[100,1319,352],{"class":124},[100,1321,148],{"class":110},[100,1323,111],{"class":110},[100,1325,359],{"class":203},[100,1327,207],{"class":110},[100,1329,211],{"class":210},[100,1331,118],{"class":110},[100,1333,393],{"class":114},[100,1335,214],{"class":110},[100,1337,1338],{"class":102,"line":291},[100,1339,1340],{"class":110},"  },\n",[100,1342,1343,1346,1348,1350,1352,1354,1356,1359,1361,1363,1365,1368,1370,1372],{"class":102,"line":300},[100,1344,1345],{"class":110},"  {",[100,1347,368],{"class":203},[100,1349,207],{"class":110},[100,1351,1084],{"class":190},[100,1353,194],{"class":114},[100,1355,132],{"class":124},[100,1357,1358],{"class":128},"password",[100,1360,132],{"class":124},[100,1362,148],{"class":110},[100,1364,125],{"class":124},[100,1366,1367],{"class":128},"confirm",[100,1369,132],{"class":124},[100,1371,447],{"class":114},[100,1373,496],{"class":110},[100,1375,1376,1378],{"class":102,"line":317},[100,1377,393],{"class":114},[100,1379,135],{"class":110},[23,1381,1382,1385,1386,1389,1390,1393,1394,1397],{},[27,1383,1384],{},"异步校验器","（如检查用户名是否已被占用）返回 ",[62,1387,1388],{},"Observable\u003CValidationErrors | null>","，配置在 ",[62,1391,1392],{},"asyncValidators"," 中，Angular 会自动在输入停止一段时间后调用并管理 ",[62,1395,1396],{},"pending"," 状态。",[46,1399,1400],{"id":1400},"模板驱动表单",[23,1402,1403,1404,1407,1408,30,1410,1413],{},"在组件中导入 ",[62,1405,1406],{},"FormsModule","，模板使用 ",[62,1409,85],{},[62,1411,1412],{},"#f=\"ngForm\"","：",[91,1415,1417],{"className":803,"code":1416,"language":805,"meta":96,"style":96},"\u003Cform #f=\"ngForm\" (ngSubmit)=\"onSubmit(f.value)\">\n  \u003Cinput name=\"email\" [(ngModel)]=\"email\" required email \u002F>\n  \u003Cbutton type=\"submit\" [disabled]=\"f.invalid\">提交\u003C\u002Fbutton>\n\u003C\u002Fform>\n",[62,1418,1419,1451,1489,1529],{"__ignoreMap":96},[100,1420,1421,1423,1425,1428,1430,1432,1435,1437,1440,1442,1444,1447,1449],{"class":102,"line":103},[100,1422,812],{"class":110},[100,1424,439],{"class":815},[100,1426,1427],{"class":819}," #f",[100,1429,513],{"class":110},[100,1431,132],{"class":124},[100,1433,1434],{"class":128},"ngForm",[100,1436,132],{"class":124},[100,1438,1439],{"class":819}," (ngSubmit)",[100,1441,513],{"class":110},[100,1443,132],{"class":124},[100,1445,1446],{"class":128},"onSubmit(f.value)",[100,1448,132],{"class":124},[100,1450,831],{"class":110},[100,1452,1453,1456,1458,1461,1463,1465,1468,1470,1473,1475,1477,1479,1481,1484,1487],{"class":102,"line":138},[100,1454,1455],{"class":110},"  \u003C",[100,1457,865],{"class":815},[100,1459,1460],{"class":819}," name",[100,1462,513],{"class":110},[100,1464,132],{"class":124},[100,1466,1467],{"class":128},"email",[100,1469,132],{"class":124},[100,1471,1472],{"class":819}," [(ngModel)]",[100,1474,513],{"class":110},[100,1476,132],{"class":124},[100,1478,1467],{"class":128},[100,1480,132],{"class":124},[100,1482,1483],{"class":819}," required",[100,1485,1486],{"class":819}," email",[100,1488,892],{"class":110},[100,1490,1491,1493,1495,1497,1499,1501,1504,1506,1509,1511,1513,1516,1518,1520,1523,1525,1527],{"class":102,"line":177},[100,1492,1455],{"class":110},[100,1494,929],{"class":815},[100,1496,932],{"class":819},[100,1498,513],{"class":110},[100,1500,132],{"class":124},[100,1502,1503],{"class":128},"submit",[100,1505,132],{"class":124},[100,1507,1508],{"class":819}," [disabled]",[100,1510,513],{"class":110},[100,1512,132],{"class":124},[100,1514,1515],{"class":128},"f.invalid",[100,1517,132],{"class":124},[100,1519,955],{"class":110},[100,1521,1522],{"class":114},"提交",[100,1524,961],{"class":110},[100,1526,929],{"class":815},[100,1528,831],{"class":110},[100,1530,1531,1533,1535],{"class":102,"line":184},[100,1532,961],{"class":110},[100,1534,439],{"class":815},[100,1536,831],{"class":110},[23,1538,1539],{},"适合简单场景（如单个搜索框、几个字段的设置表单）；大型表单仍建议 Reactive，避免校验逻辑散落在模板属性中难以测试。",[46,1541,1543],{"id":1542},"类型安全typed-forms","类型安全（Typed Forms）",[23,1545,1546,1547,1549,1550,1553],{},"Angular 14+ 支持为 ",[62,1548,68],{}," 指定泛型类型，减少 ",[62,1551,1552],{},"get('email')"," 的类型丢失：",[91,1555,1557],{"className":93,"code":1556,"language":95,"meta":96,"style":96},"interface LoginForm {\n  email: FormControl\u003Cstring>;\n  remember: FormControl\u003Cboolean>;\n}\n\nconst form = new FormGroup\u003CLoginForm>({\n  email: new FormControl(\"\", { nonNullable: true }),\n  remember: new FormControl(false, { nonNullable: true }),\n});\n\nform.controls.email.value; \u002F\u002F string，而非 any\n",[62,1558,1559,1569,1586,1602,1606,1610,1635,1665,1696,1704,1708],{"__ignoreMap":96},[100,1560,1561,1564,1567],{"class":102,"line":103},[100,1562,1563],{"class":306},"interface",[100,1565,1566],{"class":310}," LoginForm",[100,1568,314],{"class":110},[100,1570,1571,1574,1576,1578,1580,1583],{"class":102,"line":138},[100,1572,1573],{"class":320},"  email",[100,1575,207],{"class":324},[100,1577,145],{"class":310},[100,1579,812],{"class":110},[100,1581,1582],{"class":1095},"string",[100,1584,1585],{"class":110},">;\n",[100,1587,1588,1591,1593,1595,1597,1600],{"class":102,"line":177},[100,1589,1590],{"class":320},"  remember",[100,1592,207],{"class":324},[100,1594,145],{"class":310},[100,1596,812],{"class":110},[100,1598,1599],{"class":1095},"boolean",[100,1601,1585],{"class":110},[100,1603,1604],{"class":102,"line":184},[100,1605,496],{"class":110},[100,1607,1608],{"class":102,"line":200},[100,1609,181],{"emptyLinePlaceholder":180},[100,1611,1612,1615,1618,1620,1622,1624,1626,1629,1631,1633],{"class":102,"line":217},[100,1613,1614],{"class":306},"const",[100,1616,1617],{"class":1151}," form",[100,1619,325],{"class":324},[100,1621,328],{"class":324},[100,1623,151],{"class":190},[100,1625,812],{"class":110},[100,1627,1628],{"class":310},"LoginForm",[100,1630,955],{"class":110},[100,1632,194],{"class":114},[100,1634,197],{"class":110},[100,1636,1637,1639,1641,1643,1645,1647,1649,1651,1653,1655,1657,1659,1661,1663],{"class":102,"line":230},[100,1638,1573],{"class":203},[100,1640,207],{"class":110},[100,1642,328],{"class":324},[100,1644,145],{"class":190},[100,1646,194],{"class":114},[100,1648,352],{"class":124},[100,1650,148],{"class":110},[100,1652,111],{"class":110},[100,1654,359],{"class":203},[100,1656,207],{"class":110},[100,1658,211],{"class":210},[100,1660,118],{"class":110},[100,1662,393],{"class":114},[100,1664,214],{"class":110},[100,1666,1667,1669,1671,1673,1675,1677,1680,1682,1684,1686,1688,1690,1692,1694],{"class":102,"line":241},[100,1668,1590],{"class":203},[100,1670,207],{"class":110},[100,1672,328],{"class":324},[100,1674,145],{"class":190},[100,1676,194],{"class":114},[100,1678,1679],{"class":210},"false",[100,1681,148],{"class":110},[100,1683,111],{"class":110},[100,1685,359],{"class":203},[100,1687,207],{"class":110},[100,1689,211],{"class":210},[100,1691,118],{"class":110},[100,1693,393],{"class":114},[100,1695,214],{"class":110},[100,1697,1698,1700,1702],{"class":102,"line":247},[100,1699,294],{"class":110},[100,1701,393],{"class":114},[100,1703,135],{"class":110},[100,1705,1706],{"class":102,"line":253},[100,1707,181],{"emptyLinePlaceholder":180},[100,1709,1710,1712,1714,1716,1718,1720,1722,1724,1727],{"class":102,"line":259},[100,1711,439],{"class":114},[100,1713,376],{"class":110},[100,1715,707],{"class":114},[100,1717,376],{"class":110},[100,1719,1467],{"class":114},[100,1721,376],{"class":110},[100,1723,1174],{"class":114},[100,1725,1726],{"class":110},";",[100,1728,1729],{"class":1254}," \u002F\u002F string，而非 any\n",[23,1731,1732,1733,44],{},"详见 ",[40,1734,1738],{"href":1735,"rel":1736},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fforms\u002Ftyped-forms",[1737],"nofollow","官方 Typed Forms",[46,1740,1741],{"id":1741},"与后端的配合",[23,1743,1744,1745,1748,1749,1752,1753,1757,1758,1761],{},"提交前校验 ",[62,1746,1747],{},"form.valid","；异步校验可用 ",[62,1750,1751],{},"AsyncValidator","。与 ",[40,1754,1756],{"href":1755},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fhttp","HttpClient"," 结合发送 ",[62,1759,1760],{},"POST"," 请求：",[91,1763,1765],{"className":93,"code":1764,"language":95,"meta":96,"style":96},"onSubmit() {\n  if (this.form.invalid) {\n    this.form.markAllAsTouched(); \u002F\u002F 显示所有字段的校验错误\n    return;\n  }\n  this.http.post(\"\u002Fapi\u002Flogin\", this.form.getRawValue()).subscribe({\n    next: () => this.router.navigate([\"\u002Fhome\"]),\n    error: (err) => this.serverError.set(err.error?.message ?? \"登录失败\"),\n  });\n}\n",[62,1766,1767,1776,1798,1819,1825,1829,1875,1913,1969,1977],{"__ignoreMap":96},[100,1768,1769,1772,1774],{"class":102,"line":103},[100,1770,1771],{"class":190},"onSubmit",[100,1773,567],{"class":114},[100,1775,197],{"class":110},[100,1777,1778,1781,1783,1785,1787,1789,1791,1794,1796],{"class":102,"line":138},[100,1779,1780],{"class":106},"  if",[100,1782,430],{"class":203},[100,1784,434],{"class":433},[100,1786,376],{"class":110},[100,1788,439],{"class":114},[100,1790,376],{"class":110},[100,1792,1793],{"class":114},"invalid",[100,1795,447],{"class":203},[100,1797,197],{"class":110},[100,1799,1800,1803,1805,1807,1809,1812,1814,1816],{"class":102,"line":177},[100,1801,1802],{"class":433},"    this",[100,1804,376],{"class":110},[100,1806,439],{"class":114},[100,1808,376],{"class":110},[100,1810,1811],{"class":190},"markAllAsTouched",[100,1813,419],{"class":203},[100,1815,1726],{"class":110},[100,1817,1818],{"class":1254}," \u002F\u002F 显示所有字段的校验错误\n",[100,1820,1821,1823],{"class":102,"line":184},[100,1822,1209],{"class":106},[100,1824,135],{"class":110},[100,1826,1827],{"class":102,"line":200},[100,1828,490],{"class":110},[100,1830,1831,1833,1835,1838,1840,1843,1845,1847,1850,1852,1854,1856,1858,1860,1862,1864,1866,1868,1871,1873],{"class":102,"line":217},[100,1832,735],{"class":433},[100,1834,376],{"class":110},[100,1836,1837],{"class":114},"http",[100,1839,376],{"class":110},[100,1841,1842],{"class":190},"post",[100,1844,194],{"class":203},[100,1846,132],{"class":124},[100,1848,1849],{"class":128},"\u002Fapi\u002Flogin",[100,1851,132],{"class":124},[100,1853,148],{"class":110},[100,1855,698],{"class":433},[100,1857,376],{"class":110},[100,1859,439],{"class":114},[100,1861,376],{"class":110},[100,1863,473],{"class":190},[100,1865,476],{"class":203},[100,1867,376],{"class":110},[100,1869,1870],{"class":190},"subscribe",[100,1872,194],{"class":203},[100,1874,197],{"class":110},[100,1876,1877,1880,1882,1885,1887,1889,1891,1894,1896,1899,1901,1903,1906,1908,1911],{"class":102,"line":230},[100,1878,1879],{"class":190},"    next",[100,1881,207],{"class":110},[100,1883,1884],{"class":110}," ()",[100,1886,1141],{"class":306},[100,1888,698],{"class":433},[100,1890,376],{"class":110},[100,1892,1893],{"class":114},"router",[100,1895,376],{"class":110},[100,1897,1898],{"class":190},"navigate",[100,1900,536],{"class":203},[100,1902,132],{"class":124},[100,1904,1905],{"class":128},"\u002Fhome",[100,1907,132],{"class":124},[100,1909,1910],{"class":203},"])",[100,1912,214],{"class":110},[100,1914,1915,1918,1920,1922,1925,1927,1929,1931,1933,1936,1938,1941,1943,1945,1947,1950,1952,1955,1958,1960,1963,1965,1967],{"class":102,"line":241},[100,1916,1917],{"class":190},"    error",[100,1919,207],{"class":110},[100,1921,430],{"class":110},[100,1923,1924],{"class":1089},"err",[100,1926,393],{"class":110},[100,1928,1141],{"class":306},[100,1930,698],{"class":433},[100,1932,376],{"class":110},[100,1934,1935],{"class":114},"serverError",[100,1937,376],{"class":110},[100,1939,1940],{"class":190},"set",[100,1942,194],{"class":203},[100,1944,1924],{"class":114},[100,1946,376],{"class":110},[100,1948,1949],{"class":114},"error",[100,1951,1171],{"class":110},[100,1953,1954],{"class":114},"message",[100,1956,1957],{"class":324}," ??",[100,1959,125],{"class":124},[100,1961,1962],{"class":128},"登录失败",[100,1964,132],{"class":124},[100,1966,393],{"class":203},[100,1968,214],{"class":110},[100,1970,1971,1973,1975],{"class":102,"line":247},[100,1972,401],{"class":110},[100,1974,393],{"class":203},[100,1976,135],{"class":110},[100,1978,1979],{"class":102,"line":253},[100,1980,496],{"class":110},[46,1982,1983],{"id":1983},"最佳实践",[53,1985,1986,1995,2006,2013,2024],{},[56,1987,1988,1989,1991,1992,44],{},"复杂表单（多步骤、动态字段、跨字段校验）一律用 ",[27,1990,37],{}," + ",[27,1993,1994],{},"Typed Forms",[56,1996,1997,1998,2001,2002,2005],{},"校验器写成",[27,1999,2000],{},"纯函数","（如 ",[62,2003,2004],{},"passwordMatch","），便于复用与单测，避免写在组件方法里。",[56,2007,2008,2009,2012],{},"提交前调用 ",[62,2010,2011],{},"markAllAsTouched()","，确保未曾聚焦过的无效字段也显示错误提示。",[56,2014,2015,2016,2019,2020,2023],{},"用 ",[62,2017,2018],{},"nonNullable: true"," 明确控件值不为 ",[62,2021,2022],{},"null","，配合 Typed Forms 获得准确类型推断。",[56,2025,2026,2027,2029,2030,2029,2032,2035],{},"表单状态（",[62,2028,444],{},"\u002F",[62,2031,1396],{},[62,2033,2034],{},"dirty","）用于控制提交按钮禁用态与 loading 态，避免重复提交。",[46,2037,2038],{"id":2038},"常见坑",[2040,2041,2042,2058],"table",{},[2043,2044,2045],"thead",{},[2046,2047,2048,2052,2055],"tr",{},[2049,2050,2051],"th",{},"现象",[2049,2053,2054],{},"常见原因",[2049,2056,2057],{},"处理",[2059,2060,2061,2078,2098,2117,2128],"tbody",{},[2046,2062,2063,2072,2075],{},[2064,2065,2066,2068,2069],"td",{},[62,2067,1552],{}," 返回类型是 ",[62,2070,2071],{},"any",[2064,2073,2074],{},"未使用 Typed Forms（旧版 API 或未标注泛型）",[2064,2076,2077],{},"升级到 Angular 14+ 并显式声明控件类型",[2046,2079,2080,2083,2092],{},[2064,2081,2082],{},"校验错误一开始就显示",[2064,2084,2085,2086,2029,2089,2091],{},"未判断 ",[62,2087,2088],{},"touched",[62,2090,2034],{}," 就直接显示错误",[2064,2093,2094,2095],{},"配合 ",[62,2096,2097],{},"@if (control.invalid && control.touched)",[2046,2099,2100,2105,2114],{},[2064,2101,2102,2104],{},[62,2103,71],{}," 增删后模板渲染错位",[2064,2106,2107,2110,2111],{},[62,2108,2109],{},"@for"," 缺少稳定 ",[62,2112,2113],{},"track",[2064,2115,2116],{},"用索引或控件内部 id 作为 track（数组本身可变时用索引）",[2046,2118,2119,2122,2125],{},[2064,2120,2121],{},"异步校验器一直 pending",[2064,2123,2124],{},"Observable 未正确 complete，或未处理错误",[2064,2126,2127],{},"确保异步校验返回的 Observable 会 emit 并 complete",[2046,2129,2130,2133,2140],{},[2064,2131,2132],{},"表单重置后校验状态未清空",[2064,2134,2135,2136,2139],{},"只用了 ",[62,2137,2138],{},"reset()"," 未清 touched\u002Fdirty 状态",[2064,2141,2142,2145],{},[62,2143,2144],{},"form.reset(initialValue)"," 会同时清空这些状态，确认调用方式正确",[46,2147,2148],{"id":2148},"延伸阅读",[53,2150,2151,2156,2163],{},[56,2152,2153,2155],{},[40,2154,43],{"href":42},"：模板语法基础",[56,2157,2158,2162],{},[40,2159,2161],{"href":2160},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fsignals\u002Fbasics","Signal 基础","：可与表单状态结合",[56,2164,2165,2169],{},[40,2166,2168],{"href":2167},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Ftest","测试","：表单组件的单测写法",[46,2171,2172],{"id":2172},"参考文献",[23,2174,2175],{},"以下链接在编写时均可正常访问：",[2040,2177,2178,2188],{},[2043,2179,2180],{},[2046,2181,2182,2185],{},[2049,2183,2184],{},"资料",[2049,2186,2187],{},"说明",[2059,2189,2190,2202,2213,2225,2235],{},[2046,2191,2192,2199],{},[2064,2193,2194],{},[40,2195,2198],{"href":2196,"rel":2197},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fforms",[1737],"表单简介",[2064,2200,2201],{},"官方",[2046,2203,2204,2210],{},[2064,2205,2206],{},[40,2207,89],{"href":2208,"rel":2209},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fforms\u002Freactive-forms",[1737],[2064,2211,2212],{},"Reactive",[2046,2214,2215,2222],{},[2064,2216,2217],{},[40,2218,2221],{"href":2219,"rel":2220},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fforms\u002Fform-validation",[1737],"验证器",[2064,2223,2224],{},"校验",[2046,2226,2227,2232],{},[2064,2228,2229],{},[40,2230,1994],{"href":1735,"rel":2231},[1737],[2064,2233,2234],{},"类型安全",[2046,2236,2237,2244],{},[2064,2238,2239],{},[40,2240,2243],{"href":2241,"rel":2242},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fforms\u002Fdynamic-forms",[1737],"动态表单",[2064,2245,2246],{},"FormArray 场景",[2248,2249,2250],"style",{},"html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVXei, html code.shiki .sVXei{--shiki-light:#E53935;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sQzsp, html code.shiki .sQzsp{--shiki-light:#E53935;--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s9AJx, html code.shiki .s9AJx{--shiki-light:#9C3EDA;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}",{"title":96,"searchDepth":138,"depth":138,"links":2252},[2253,2254,2255,2256,2257,2258,2259,2260,2261,2262,2263],{"id":48,"depth":138,"text":48},{"id":89,"depth":138,"text":89},{"id":499,"depth":138,"text":500},{"id":1034,"depth":138,"text":1034},{"id":1400,"depth":138,"text":1400},{"id":1542,"depth":138,"text":1543},{"id":1741,"depth":138,"text":1741},{"id":1983,"depth":138,"text":1983},{"id":2038,"depth":138,"text":2038},{"id":2148,"depth":138,"text":2148},{"id":2172,"depth":138,"text":2172},{"title":8,"description":9},"knowledge\u002Fbooklet\u002Fframe\u002FAngular\u002Fforms","knowledge\u002Fknowledge\u002Fbooklet\u002Fframe\u002FAngular\u002Fforms.md","md",{"items":2269},[2270,2274,2276,2280,2284,2287],{"path":2271,"title":2272,"description":2273,"category":10,"score":217},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fecology","生态集成","Angular 官方维护路由、HTTP、表单、动画等；UI 与状态扩展由社区与官方 Material 团队提供。涵盖官方\u002F半官方库、常用第三方库、与 Vue\u002FReact 生态对照、选型建议与常见坑。",{"path":2160,"title":2161,"description":2275,"category":10,"score":217},"Signals 是 Angular 16 引入的响应式原语：可读写的 signal、派生的 computed、副作用 effect。用于组件内状态与跨组件细粒度更新，涵盖 API、input signal、与变更检测的关系、最佳实践与常见坑。",{"path":2277,"title":2278,"description":2279,"category":10,"score":217},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fsignals\u002Frxjs","RxJS 互操作","Angular 内置 RxJS 处理异步流（HTTP、HttpClient、路由事件、FormControl 的 valueChanges 等）。Signal 适合同步\u002F短链路状态；长生命周期流仍常用 Observable。涵盖 async 管道、toSignal\u002FtoObservable、常用操作符、最佳实践与常见坑。",{"path":2281,"title":2282,"description":2283,"category":10,"score":217},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fstate","状态管理","Angular 没有强制单一状态库：服务 + DI、Signal、RxJS 即可覆盖多数场景；大型应用可选用 NgRx。涵盖选型、服务作为状态容器、跨组件共享、NgRx 与 SignalStore、常见坑。",{"path":1755,"title":2285,"description":2286,"category":10,"score":217},"HTTP 与数据","Angular 通过 HttpClient（@angular\u002Fcommon\u002Fhttp）发起 HTTP 请求，返回 RxJS Observable。见 RxJS 互操作、依赖注入。",{"path":2288,"title":2289,"description":2290,"category":10,"score":217},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fperformance\u002Fchange-detection","变更检测","Angular 在数据变化后更新 DOM 的过程称为 变更检测（Change Detection）。理解默认策略、Zone.js 机制、OnPush 与 Signal 协同，有助于性能调优。运行时技巧见 runtime。",{"series":2292,"title":12,"items":2293,"index":217,"prev":2331,"next":2332},"booklet\u002Fframe\u002Fangular",[2294,2298,2300,2302,2304,2308,2310,2311,2313,2317,2321,2325,2327,2329],{"path":2295,"title":2296,"date":2297,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fbasics","Angular 基础","2020-03-07",{"path":2160,"title":2161,"date":2299,"pinned":15},"2026-06-12",{"path":2288,"title":2289,"date":2301,"pinned":15},"2023-10-12",{"path":42,"title":43,"date":2303,"pinned":15},"2021-07-11",{"path":2305,"title":2306,"date":2307,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fdi","依赖注入","2021-01-24",{"path":2271,"title":2272,"date":2309,"pinned":15},"2026-07-12",{"path":7,"title":8,"date":14,"pinned":15},{"path":1755,"title":2285,"date":2312,"pinned":15},"2024-12-27",{"path":2314,"title":2315,"date":2316,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fproject","工程化","2021-09-01",{"path":2318,"title":2319,"date":2320,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Frouter","路由","2023-02-04",{"path":2322,"title":2323,"date":2324,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fperformance\u002Fruntime","运行时优化","2022-11-01",{"path":2277,"title":2278,"date":2326,"pinned":15},"2026-04-06",{"path":2281,"title":2282,"date":2328,"pinned":15},"2025-04-29",{"path":2167,"title":2168,"date":2330,"pinned":15},"2021-08-25",{"path":2271,"title":2272,"date":2309,"pinned":15},{"path":1755,"title":2285,"date":2312,"pinned":15}]