Angular 高级技巧-表单复用艺术

在日常的应用程序开发中,经常会遇到一些表单场景,它们具有相似但又不完全相同的特征,例如,可能只有一部分字段在不同表单之间共享相似性。在本文中,我们将以联系人管理为例,逐步展示如何运用高级技巧来优化我们的代码,实现表单复用的艺术。通过深入研究这一实例,您将能够更好地理解如何在Angular应用中巧妙地应对类似的表单需求。

本文中的技巧依赖于Angular的依赖注入技巧,思路不仅适用本文的表单复用中还可以应用很多地方,尤其是组件库封装。

场景

在上述场景中可以看到联系人这块的字段是一样的,如果在一个项目中只在这个表单中出现还好,如果在一个项目中只在这个表单中出现, 我们可以使用循环来渲染, 但一般来说会在多个表单中出现,所以封装就有必要了。

改造前的代码如下:

ts 复制代码
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet, NzFormModule, ReactiveFormsModule, NzInputModule, NzButtonModule, NzSelectModule],
  template: `
    <form nz-form [formGroup]="formGroup" class="login-form" (ngSubmit)="submitForm()">
      <nz-form-item>
        <nz-form-control nzErrorTip="Please input your username!">
          <nz-form-label>姓名</nz-form-label>
          <input type="text" nz-input formControlName="username" placeholder="Please input your username" />
        </nz-form-control>
      </nz-form-item>
      <nz-form-item>
        <nz-form-control nzErrorTip="Please input your phone!">
          <nz-form-label>手机号码</nz-form-label>
          <input type="text" nz-input formControlName="phone" placeholder="Please input your phone" />
        </nz-form-control>
      </nz-form-item>
      <fieldset formGroupName="firstContact">
        <legend>第一联系人</legend>
        <nz-form-item>
          <nz-form-control nzErrorTip="Please input your username!">
            <nz-form-label>与本人关系</nz-form-label>
            <nz-select formControlName="relation" class="relation-select">
              <nz-option nzLabel="母子" nzValue="母子"></nz-option>
              <nz-option nzLabel="父子" nzValue="父子"></nz-option>
              <nz-option nzLabel="朋友" nzValue="朋友"></nz-option>
              <nz-option nzLabel="同事" nzValue="同事"></nz-option>
            </nz-select>
          </nz-form-control>
        </nz-form-item>
        <nz-form-item>
          <nz-form-control nzErrorTip="Please input your username!">
            <nz-form-label>姓名</nz-form-label>
            <input type="text" nz-input formControlName="username" placeholder="Please input username" />
          </nz-form-control>
        </nz-form-item>
        <nz-form-item>
          <nz-form-control nzErrorTip="Please input your username!">
            <nz-form-label>手机号码</nz-form-label>
            <input type="text" nz-input formControlName="phone" placeholder="Please input phone" />
          </nz-form-control>
        </nz-form-item>
      </fieldset>
      <fieldset formGroupName="secondContact">
        <legend>第二联系人</legend>
        <nz-form-item>
          <nz-form-control nzErrorTip="Please input your username!">
            <nz-form-label>与本人关系</nz-form-label>
            <nz-select formControlName="relation" class="relation-select">
              <nz-option nzLabel="母子" nzValue="母子"></nz-option>
              <nz-option nzLabel="父子" nzValue="父子"></nz-option>
              <nz-option nzLabel="朋友" nzValue="朋友"></nz-option>
              <nz-option nzLabel="同事" nzValue="同事"></nz-option>
            </nz-select>
          </nz-form-control>
        </nz-form-item>
        <nz-form-item>
          <nz-form-control nzErrorTip="Please input your username!">
            <nz-form-label>姓名</nz-form-label>
            <input type="text" nz-input formControlName="username" placeholder="Please input username" />
          </nz-form-control>
        </nz-form-item>
        <nz-form-item>
          <nz-form-control nzErrorTip="Please input your username!">
            <nz-form-label>手机号码</nz-form-label>
            <input type="text" nz-input formControlName="phone" placeholder="Please input phone" />
          </nz-form-control>
        </nz-form-item>
      </fieldset>
      <fieldset formGroupName="thirdContact">
        <legend>第三联系人</legend>
        <nz-form-item>
          <nz-form-control nzErrorTip="Please input your username!">
            <nz-form-label>与本人关系</nz-form-label>
            <nz-select formControlName="relation" class="relation-select">
              <nz-option nzLabel="母子" nzValue="母子"></nz-option>
              <nz-option nzLabel="父子" nzValue="父子"></nz-option>
              <nz-option nzLabel="朋友" nzValue="朋友"></nz-option>
              <nz-option nzLabel="同事" nzValue="同事"></nz-option>
            </nz-select>
          </nz-form-control>
        </nz-form-item>
        <nz-form-item>
          <nz-form-control nzErrorTip="Please input your username!">
            <nz-form-label>姓名</nz-form-label>
            <input type="text" nz-input formControlName="username" placeholder="Please input username" />
          </nz-form-control>
        </nz-form-item>
        <nz-form-item>
          <nz-form-control nzErrorTip="Please input your username!">
            <nz-form-label>手机号码</nz-form-label>
            <input type="text" nz-input formControlName="phone" placeholder="Please input phone" />
          </nz-form-control>
        </nz-form-item>
      </fieldset>
      <button nz-button class="form-button form-margin" [nzType]="'primary'">submit</button>
    </form>
  `,
  styles: [`
    :host {
      display: flex;
      justify-content: center;

      form {
        min-width: 600px;

        .form-button {
          margin-top: 8px;
          width: 100%;
        }
      }
    }
  `]
})
export class AppComponent {
  fb = inject(FormBuilder);

  formGroup = this.fb.group({
    username: [],
    phone: [],
    firstContact: this.fb.group({
      username: [],
      relation: ['朋友'],
      phone: []
    }),
    secondContact: this.fb.group({
      username: [],
      relation: ['朋友'],
      phone: []
    }),
    thirdContact: this.fb.group({
      username: [],
      relation: ['朋友'],
      phone: []
    })
  })

  submitForm() {
    console.log(this.formGroup);
  }
}

改造

重复部分抽取成一个组件

定义 ContactGroup 组件:

ts 复制代码
@Component({
  selector: 'app-contact-group',
  standalone: true,
  imports: [
    NzFormModule,
    NzSelectModule,
    NzInputModule,
    ReactiveFormsModule
  ],
  template: `
    <fieldset formGroupName="{{groupName}}">
      <legend>{{ legend }}</legend>
      <nz-form-item>
        <nz-form-control nzErrorTip="Please input your username!">
          <nz-form-label>与本人关系</nz-form-label>
          <nz-select formControlName="relation" class="relation-select">
            <nz-option nzLabel="母子" nzValue="母子"></nz-option>
            <nz-option nzLabel="父子" nzValue="父子"></nz-option>
            <nz-option nzLabel="朋友" nzValue="朋友"></nz-option>
            <nz-option nzLabel="同事" nzValue="同事"></nz-option>
          </nz-select>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item>
        <nz-form-control nzErrorTip="Please input your username!">
          <nz-form-label>姓名</nz-form-label>
          <input type="text" nz-input formControlName="username" placeholder="Please input username" />
        </nz-form-control>
      </nz-form-item>
      <nz-form-item>
        <nz-form-control nzErrorTip="Please input your username!">
          <nz-form-label>手机号码</nz-form-label>
          <input type="text" nz-input formControlName="phone" placeholder="Please input phone" />
        </nz-form-control>
      </nz-form-item>
    </fieldset>
  `,
  styles: [``]
})
export class ContactGroup {
  @Input({ required: true }) legend!: string;
  @Input({ required: true }) groupName!: string;
}

接下来,我们在业务代码中使用了这个组件:

ts 复制代码
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, NzFormModule, ReactiveFormsModule, NzInputModule, NzButtonModule, ContactGroup],
  template: `
    <form nz-form [formGroup]="formGroup" class="login-form" (ngSubmit)="submitForm()">
      <nz-form-item>
        <nz-form-control nzErrorTip="Please input your username!">
          <nz-form-label>姓名</nz-form-label>
          <input type="text" nz-input formControlName="username" placeholder="Please input your username" />
        </nz-form-control>
      </nz-form-item>
      <nz-form-item>
        <nz-form-control nzErrorTip="Please input your phone!">
          <nz-form-label>手机号码</nz-form-label>
          <input type="text" nz-input formControlName="phone" placeholder="Please input your phone" />
        </nz-form-control>
      </nz-form-item>
      <app-contact-group legend="第一联系人" groupName="firstContact"></app-contact-group>
      <app-contact-group legend="第二联系人" groupName="secondContact"></app-contact-group>
      <app-contact-group legend="第三联系人" groupName="thirdContact"></app-contact-group>
      <button nz-button class="form-button form-margin" [nzType]="'primary'">submit</button>
    </form>
  `,
  styles: [`
    // 一些代码...
  `]
})
export class AppComponent {
  /*一些代码...*/
}

现在可以看到,通过使用 ContactGroup 组件,我们能够将表单的重复部分抽取出来并进行复用了,看上去很好,可理想有多丰满现实就有多骨感,Angular抛出一个异常并说不认识它。

txt 复制代码
ERROR Error: NG01053: formGroupName must be used with a parent formGroup directive.  You'll want to add a formGroup
    directive and pass it an existing FormGroup instance (you can create one in your class).

    Example:

    
  <div [formGroup]="myGroup">
      <div formGroupName="person">
        <input formControlName="firstName">
      </div>
  </div>

  In your class:

  this.myGroup = new FormGroup({
      person: new FormGroup({ firstName: new FormControl() })
  });
    at groupParentException (forms.mjs:1443:12)
    at FormGroupName._checkParentType (forms.mjs:5069:19)
    at FormGroupName.ngOnInit (forms.mjs:3669:14)
    at callHookInternal (core.mjs:3920:14)
    at callHook (core.mjs:3947:13)
    at callHooks (core.mjs:3902:17)
    at executeInitAndCheckHooks (core.mjs:3852:9)
    at selectIndexInternal (core.mjs:11763:17)
    at Module.ɵɵadvance (core.mjs:11746:5)
    at ContactGroup_Template (app.component.ts:21:15)

从异常信息可以看出,formGroupName 指令必须与父级的 formGroup 指令一起使用,但在我们的代码中,似乎已经使用了 formGroup 指令。为什么 Angular 无法识别呢?

从源码角度分析问题

解决问题的思路:

要解决这个问题,我们需要深入分析 FormGroupName 指令的源码。下面是 FormGroupName 指令的源码片段:

ts 复制代码
const formGroupNameProvider: Provider = {
  provide: ControlContainer,
  useExisting: forwardRef(() => FormGroupName)
};
@Directive({selector: '[formGroupName]', providers: [formGroupNameProvider]})
export class FormGroupName extends AbstractFormGroupDirective implements OnInit, OnDestroy {
  @Input('formGroupName') override name: string|number|null = null;

  constructor(
      // ...
      @Optional() @Host() @SkipSelf() parent: ControlContainer,
      // ...
  ) {
    super();
    this._parent = parent;
    // ...
  }

  /** @internal */
  override _checkParentType(): void {
    if (_hasInvalidParent(this._parent) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
      throw groupParentException();
    }
  }
}

从源码来看 如果_parent无法获取就会触发上述异常

尽管FormGroupName指令也注册了ControlContainer提供商,但是使用了依赖限制查找符@SkipSelf 并不会获取自身的ControlContainer 也不合理

那到底是什么影响FormGroupName指令获取ControlContainer呢?在依赖中除了使用了依赖限制查找符@SkipSelf还使用了@Host() 该装饰器会禁止在宿主组件以上的搜索。

根据思路,看上去示例中的宿主组件是ContactGroupContactGroup组件中确实没有注册ControlContainer提供商,现在我们注册ControlContainer提供商 如下:

ts 复制代码
@Component({
  selector: 'app-contact-group',
  standalone: true,
  providers: [
    { 
        provide: ControlContainer, 
        useFactory: () => inject(ControlContainer, { skipSelf: true })
    }
  ],
  template: `
    <!-- 一些代码 -->
  `,
  styles: [``]
})
export class ContactGroup {
  /*一些代码...*/
}

不出意外还是同样的异常

解决问题的关键

在前面的部分,我们提到了一个关键问题,即宿主组件的定义。在 Angular 中,宿主组件通常是请求某个依赖的组件。然而,当该组件被投影到某个父组件中时,那个父组件就变成了宿主。

在我们的代码中,ContactGroup 组件并不是 FormGroupName 指令的宿主组件,因此,尽管我们在 ContactGroup 组件中注册了 ControlContainer 提供商,但无法获取正确的父级 ControlContainer

所以ContactGroup并不是FormGroupName指令的宿主组件,即便注册了ControlContainer提供商,也无法获取。

解决问题

上一个小节中我们遇到了依赖无法正确解析问题,陷入了困境,为了解决这个问题,我们需要使用 viewProviders 特性来注册 ControlContainer 提供商。下面是修改后的 ContactGroup 组件的代码:

ts 复制代码
@Component({
  selector: 'app-contact-group',
  standalone: true,
  viewProviders: [
    { 
        provide: ControlContainer, 
        useFactory: () => inject(ControlContainer, { skipSelf: true })
    }
  ],
  template: `
    <!-- 一些代码 -->
  `,
  styles: [``]
})
export class ContactGroup {
  /*一些代码...*/
}

viewProviders特性:定义对其视图 DOM 子级注册提供商集

通过添加 viewProviders 配置项,我们成功地解决了问题,异常不再出现了。现在,ContactGroup 组件能够正确地获取父级 ControlContainer,使表单工作如预期。

在实际应用中,理解 Angular 的依赖注入机制以及正确配置提供商非常重要,尤其在处理嵌套表单和组件复用时。通过掌握这些高级技巧,您可以更灵活地构建强大的 Angular 应用程序。

进一步优化代码

前文我们已经将重复部分单独封装成了ContactGroup组件,并在业务组件中如期工作,但还不够,既然获取到了ControlContainer示例,那么我们还可以做到自动注册控件,看如下代码:

ts 复制代码
@Component({
  selector: 'app-contact-group',
  standalone: true,
  viewProviders: [
    { provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true })}
  ],
  template: `
    <!-- 一些代码 -->
  `,
  styles: [``]
})
export class ContactGroup implements OnInit {
  @Input({ required: true }) legend!: string;
  @Input({ required: true }) groupName!: string;
  controlContainer = inject(ControlContainer)

  ngOnInit() {
    const parentFormGroup = this.controlContainer.control as FormGroup;

    parentFormGroup.addControl(this.groupName, new FormGroup({
      username: new FormControl(''),
      relation: new FormControl('朋友'),
      phone: new FormControl(''),
    }))
  }
}

@Component({
  selector: 'app-root',
  standalone: true,
  template: `<!-- 一些代码 -->`,
  styles: [``]
})
export class AppComponent {
  fb = inject(FormBuilder);

  formGroup = this.fb.group({
    username: [],
    phone: [],
  })

  submitForm() {
    console.log(this.formGroup.value);
  }
}

输入的表单结构跟之前一致

总结

本文介绍了在 Angular 应用程序开发中一种强大的技巧,即表单复用艺术,通过将表单的重复部分抽取成一个可重用的组件,提高了代码的可维护性和可读性。我们以联系人管理为例,逐步展示了如何运用这一技巧,同时也解决了可能出现的问题。

通过创建名为 ContactGroup 的组件,我们成功地将表单部分抽象出来,使业务代码更加简洁。然而,在实践中,我们遇到了一个挑战,即 Angular 抛出了一个异常,表示无法识别 formGroupName 指令。我们深入分析了问题,并解释了它的根本原因。

解决问题的关键在于理解 Angular 的依赖注入机制以及正确配置提供商。通过使用 viewProviders 配置项,我们成功地注册了 ControlContainer 提供商,确保 ContactGroup 组件能够正确地获取父级 ControlContainer,从而解决了异常问题。

本文中的技巧依赖于 Angular 的依赖注入技巧,思路不仅适用于本文的表单复用中,还可以应用于很多地方,尤其是组件库封装。 在实际应用中,这些高级技巧将帮助您更灵活地构建强大的 Angular 应用程序,提高开发效率和代码质量。

深入理解 Angular 的依赖注入机制,并善用这些技巧,将使您成为更出色的 Angular 开发者。继续探索 Angular 的世界,并不断提升自己的技能!


源码链接: 在这里查看完整的源码示例

通过源码示例,您可以更详细地了解如何实现表单复用以及解决相关问题。

相关推荐
一个天蝎座 白勺 程序猿4 分钟前
金仓数据库KingbaseES无缝替代MongoDB,实现核心业务系统平稳迁移
数据库·mongodb·架构·时序数据库·kingbasees
m0_748229995 分钟前
Vue3高效学习路线全攻略
前端·javascript·vue.js
谢尔登17 分钟前
React架构演变
前端·react.js·架构
码农三叔17 分钟前
(6-2)手部、足部与末端执行器设计:足部结构
人工智能·架构·机器人·人形机器人
木辰風21 分钟前
vue在IE浏览器下父页面向子页面传输对象时数据丢失
前端·javascript·html
2501_9440328423 分钟前
2026-2029:云端开发环境混战,Sealos DevBox的差异化在哪
架构
Qinti_mm26 分钟前
Linux高性能使用:架构、内核与系统的完美适配
linux·架构·内核·系统
檐下翻书17327 分钟前
PC端免费在线流程图工具新手快速制作专业流程图教程
论文阅读·架构·毕业设计·流程图·论文笔记
小雨青年29 分钟前
Cursor 项目实战:AI播客策划助手(四)—— 产品发布与交付收尾
前端·人工智能
学编程的小程35 分钟前
一库统管全域数据:金仓 KingbaseES 多模融合架构与全栈替代实践
架构