目录

雏形

主体

逻辑完善

使用公共表单组件

总结

最后


        这几天笔者所接手的angular项目需要重新封装一个公共表单组件,由于之前封装的表单组件自由度不够,每出现一种新的类型的特殊输入框就需要在公共表单组件中增添一个新的子组件,导致公共表单组件愈发臃肿与难以维护,因而笔者查阅网上资料与官方文档后,使用 ng-container 与*ngCompnentOutlet 指令重新封装了一个自由度更高的公共表单组件,在此以demo的形式记录一下开发思想与过程,希望能够提供给大家一种思路。

雏形

        首先我们打开我们的项目,创建一个公共的 form 模块,并分别创建 form-group(公共表单组件), input-form(输入框表单组件),select-form(选择框表单组件),special-form(特殊表单组件)

        将 input-form,select-form,special-form写入form-group,这样我们的组件雏形就写好了。(关于ng-container,请见官网  ng-container

                form-group.component.html

<ng-container *ngFor="let item of querysArray">
  <app-input-form *ngIf="item[1].type===1" [inputLabel]="item[1].label [inputValueName]="item[0]" [querysList]="querysList"></app-input-form>
  <app-select-form *ngIf="item[1].type===2" [selectLabel]="item[1].label [selectOptions]="item[1].options" [selectValueName]="item[0]" [querysList]="querysList"></app-select-form>
  <app-special-form *ngIf="item[1].type===3" [specialLabel]="item[1].label" [specialValueName]="item[0]" [querysList]="querysList" [specialComponent]="item[1].component"></app-special-form>
</ng-container>
<!-- item.type为想要渲染的表单类型,1为input,2为select,3为特殊组件 -->
<!-- querysList则是传入的数据源 -->
<!-- 以上注释以及item等传入的值看不懂的话可以去博客最后看一下我们所定义的数据源 -->

                form-group.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-form-group',
  templateUrl: './form-group.component.html',
  styleUrls: ['./form-group.component.scss']
})
export class FormGroupComponent implements OnInit {
  @Input() querysList!: Map<string, any>; // 传入的数据源
  querysArray: Array<any> = []; // 处理querysList数据源得到的循环体数组
  constructor() { }

  ngOnInit(): void {
  }

}

主体

        下一步我们来编辑 input-form 组件

        该组件作为我们循环输出出来的表单的 input 框 

                input-form.component.html

<div class="form-group">
  <label>{{inputLabel}}</label>
  <input type="text" [(ngModel)]="querysList.get(inputValueName).value">
</div>

                input-form.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-input-form',
  templateUrl: './input-form.component.html',
  styleUrls: ['./input-form.component.scss']
})
export class InputFormComponent implements OnInit {
  @Input() inputLabel!: string;
  @Input() querysList: any;
  @Input() inputValueName: any;
  constructor() { }

  ngOnInit(): void {
  }

}

        接下来我们编辑我们的 select-form 组件

        该组件作为我们循环输出出来的表单的 select 框

                select-form.component.html

<div class="form-group">
  <select name="" id="" [(ngModel)]="querysList.get(selectValueName).value>
    <option [ngValue]="">请输入</option>
    <option *ngFor="let item of selectOptions" [ngValue]="item.label">{{item.value}}</option>
  </select>
</div>

                select-form.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-select-form',
  templateUrl: './select-form.component.html',
  styleUrls: ['./select-form.component.scss']
})
export class SelectFormComponent implements OnInit {
  @Input() selectLabel!: string;
  @Input() querysList: any;
  @Input() selectValueName: any;
  @Input() selectOptions: any;
  constructor() { }

  ngOnInit(): void {
  }

}

        最后也是最重要的,我们来编辑我们的 special-form 组件

        该组件作为特殊组件的预留插槽,当在特殊业务场景下需要特殊定制表单组件时,就用到了这个组件。

        我们借助 ng-container 及 *ngComponentOutlet 来实现将我们自定义的组件渲染到对应表单位置上。
(angular插槽可看这位博主的博客,写的很详细angular9的学习(十二)插槽 - 猫神甜辣酱 - 博客园 (cnblogs.com))
(*ngComponentOutlet 使用方式可见官方文档,此处不再赘述
angular *ngComponentOutlet)

                special-form.component.html

<div class="form-group">
  <label for="">{{specialLabel}}</label>
  <ng-container *ngComponentOutlet="specialComponent"></ng-container>
</div>

                special-form.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-special-form',
  templateUrl: './special-form.component.html',
  styleUrls: ['./special-form.component.scss']
})
export class SpecialFormComponent implements OnInit {
  @Input() specialComponent: any;
  @Input() specialLabel!: string;
  @Input() specialValueName: any;
  @Input() querysList: any;
  constructor() { }

  ngOnInit(): void {
  }

}

        至此我们的组件主体就搭建完毕了

逻辑完善

        接下来我们定义一个类,方便我们创建数据源。
        实例化该类,创建一个组件的数据源demo

                我们在当前demo的module下创建一个 models 文件夹,将我们的数据源demo与定义的类存放在 form.params.ts 文件内。
(注意:组件封装完毕后,数据源应放在使用该公共组件的对应模块下,而不是都存放在该公共组件所在的模块内,方便查找与修改。此处只是写一个demo范例,正常业务场景下数据源不要放在这里。)

                 form.params.ts

export class MapValue {
  label!: string;
  value: any;
  type!: number;
  options!: Array<any>;
  component: any;
  constructor(_value: any, _label: string, _type: number, _options: Array<any> = [], _component?: any) {
    this.label = _label;
    this.value = _value;
    this.options = _options;
    this.type = _type;
    this.component = _component;
  }
}
// 数据源定义格式
// export const QuerysList: Map<any, any> = new Map([
//   ["Input", new MapValue("", "输入框", 1)],
//   ["select", new MapValue("", "选择框", 2,[])], // []为下拉框选项
//   ["special", new MapValue("", "特殊框", 3,[],Component)] // Component 为特殊表单组件 
// ])

        我们使用 Map 与定义的 MapValue 类来实现数据源的构建,关于Map,可见 MDN Web Docs 关于Map的介绍,十分详细( Map - JavaScript | MDN )

        接下来我们需要根据数据源去完善 form-group 内循环的 querysArray 数组,并考虑 querysList 的默认值问题
        该数组的作用是将Map内数据源的value转换到数组内,方便循环遍历(angular版本高的可以使用 keyvalue 管道进行遍历,由于笔者项目的 angular 版本为 angular4 无法使用 keyvalue 管道,因而此处demo虽然能用 keyvalue 管道但笔者还是对Map做了单独处理)。

                form-group.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-form-group',
  templateUrl: './form-group.component.html',
  styleUrls: ['./form-group.component.scss']
})
export class FormGroupComponent implements OnInit {
  @Input() querysList: Map<string, any> = new Map(); // 传入的数据源
  querysArray: Array<any> = []; // 处理querysList数据源得到的循环体数组
  constructor() { }

  ngOnInit(): void {
    this.querysArray = [...(this.querysList.entries())]; // 也可写为 this.querysArray = Array.from(this.querysList);
  }
}

        截至到目前,我们的 input-form 与 select-form 已经实现了值得绑定,但是由于我们的 special-form 组件使用是 ng-container 得形式,因而不能像常规双向绑定一样绑定值,此处有两种方法获得数据源,一种是利用取地址引用的特性,直接操作同一数据源对象(最后总结时会提一嘴),另一种则是通过注入方式实现 ng-container 中的值得传递,我们在这主要介绍选用注入的方式将双向绑定得值传给自定义的组件。
(关于注入,可以见Angular 4 依赖注入教程,博主将依赖注入机制讲解的十分详细,此处不再赘述)

                special-form.component.ts

import { Component, Inject, Injectable, Injector, Input, OnInit } from '@angular/core';

@Injectable()
export class QuerysList {
  constructor() { }
}
@Component({
  selector: 'app-special-form',
  templateUrl: './special-form.component.html',
  styleUrls: ['./special-form.component.scss']
})
export class SpecialFormComponent implements OnInit {
  @Input() specialComponent: any;
  @Input() specialLabel!: string;
  @Input() specialValueName: any;
  @Input() querysList: any;
  injector!: any;
  constructor(private inj: Injector) { }

  ngOnInit(): void {
    this.createInjector();
  }
  /**
   * 创建一个注入器
   */
  createInjector() {
    let options = {
      providers: [
        { provide: QuerysList, useValue: this.querysList }
      ],
      parent: this.inj
    }
    this.injector = Injector.create(options);
  }
}

                special-form.component.html

<div class="form-group">
  <label for="">{{specialLabel}}</label>
  <ng-container *ngComponentOutlet="specialComponent;injector:injector"></ng-container>
</div>
<!-- *ngComponentOutlet="specialComponent;inject:inject" 此处看不懂可见官方文档的简单例子 -->

                现在我们已经实现了 special-form 组件的插槽功能,下面我们就需要在其他模块组件内使用 form-group组件了。

使用公共表单组件

        我们创建一个home模块,并创建 partial 文件夹与 shared 文件夹,在 partial 文件夹下创建 homein 组件,我们将在这个组件内使用我们的公共表单组件
        然后我们在 shared 文件夹下创建 components 与 models 文件夹,在components 文件夹内创建 use-special 组件,作为我们插入公共表单组件的特殊组件,然后再在 models 文件夹内创建use-special.params.ts 文件,用以存放我们的数据源。

        然后我们完善一下特殊组件 use-special

                use-special.component.html

<textarea [(ngModel)]="querysList.get('special').value"></textarea>

                 use-special.component.ts

import { Component, OnInit } from '@angular/core';
import { MapValue } from 'src/app/form-module/models/form.params';
import { QuerysList } from 'src/app/form-module/special-form/special-form.component';


@Component({
  selector: 'app-use-special',
  templateUrl: './use-special.component.html',
  styleUrls: ['./use-special.component.scss']
})
export class UseSpecialComponent implements OnInit {
  querysList!: any;
  constructor(
    private querysMap: QuerysList // 注入我们在 form-group 组件内的 special-form 组件 @Injector() export出去的类
  ) { }

  ngOnInit(): void {
    this.querysList = this.querysMap;
  }

}

        接下来我们定义数据源

                use-special.params.ts

import { MapValue } from "src/app/form-module/models/form.params";
import { UseSpecialComponent } from "../components/use-special/use-special.component";

// 下拉数据源
export let selectOptions = [
  { label: "first", value: 1 },
  { label: "second", value: 2 },
  { label: "third", value: 3 },
]
// 表单数据源
export let UseSpecialMap: Map<string, MapValue> = new Map([
  ["input", new MapValue("", "输入框", 1)],
  ["select", new MapValue("", "选择框", 2, selectOptions)],
  ["special", new MapValue("", "特殊框", 3, [], UseSpecialComponent)]
])

        数据源定义完毕,我们可以在 homein 组件中使用我们的表单组件了

                homein.component.html

<app-form-group [querysList]="querysList"></app-form-group>
<button (click)="consoleData()">console</button>

                homein.component.ts

import { Component, OnInit } from '@angular/core';
import { UseSpecialMap } from '../../shared/models/use-special.params';

@Component({
  selector: 'app-homeinin',
  templateUrl: './homeinin.component.html',
  styleUrls: ['./homeinin.component.scss']
})
export class HomeininComponent implements OnInit {
  querysList: any = UseSpecialMap
  constructor() { }

  ngOnInit(): void {
  }
  consoleData() {
    console.log('querysList :>> ', this.querysList);
  }
}

        至此,我们的公共表单组件封装完毕,并且在另一个组件中使用了这个表单组件,后续功能与表单样式大家可以在自己具体的业务场景中自行配置,我这里就不写样式了(还是因为懒哈哈哈哈)

        运行项目我们来看一下效果

        

        好的我们可以看到现在页面渲染是没有问题的,然后我们来看一下值绑定有没有问题。

         输出结果显示我们的值正确的绑定在了我们所定义的数据源上。

总结

        实际上我在这篇博客介绍的demo最后实现特殊组件上的数据源双向绑定还有一种方法,我们可以利用取地址引用的特性,通过修改同一个数据源实现双向绑定同一个数据源。

                use-special.component.ts

import { Component, OnInit } from '@angular/core';
import { MapValue } from 'src/app/form-module/models/form.params';
import { QuerysList } from 'src/app/form-module/special-form/special-form.component';
import { UseSpecialMap } from '../../models/use-special.params';


@Component({
  selector: 'app-use-special',
  templateUrl: './use-special.component.html',
  styleUrls: ['./use-special.component.scss']
})
export class UseSpecialComponent implements OnInit {
  querysList!: any;
  constructor(
    // private querysMap: QuerysList
  ) { }

  ngOnInit(): void {
    // this.querysList = this.querysMap;
    this.querysList = UseSpecialMap; // 取地址引用,直接操作同一个数据源
  }

}

        但由于笔者承担的项目上定义的数据源实际上并没有这么简单,层层引用后指向的对象并不再是初始的数据源,因而在项目上使用的是注入方式,而非取地址引用方式,大家可以自行斟酌业务场景,判断两种方式哪种更简便易读,更容易后期维护,毕竟代码逻辑可能殊途同归,但业务场景千奇百怪,大家各取所需即可。

        下面贴上我们所有的文件的代码,方便大家查看

                form-group.component.html

<ng-container *ngFor="let item of querysArray">
  <app-input-form *ngIf="item[1].type===1" [inputLabel]="item[1].label" [inputValueName]="item[0]"
    [querysList]="querysList">
  </app-input-form>
  <app-select-form *ngIf="item[1].type===2" [selectLabel]="item[1].label" [selectValueName]="item[0]"
    [selectOptions]="item[1].options" [querysList]="querysList"></app-select-form>
  <app-special-form *ngIf="item[1].type===3" [specialLabel]="item[1].label" [specialValueName]="item[0]"
    [querysList]="querysList" [specialComponent]="item[1].component"></app-special-form>
</ng-container>

                form-group.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-form-group',
  templateUrl: './form-group.component.html',
  styleUrls: ['./form-group.component.scss']
})
export class FormGroupComponent implements OnInit {
  @Input() querysList: Map<string, any> = new Map(); // 传入的数据源
  querysArray: Array<any> = []; // 处理querysList数据源得到的循环体数组
  constructor() { }

  ngOnInit(): void {
    this.querysArray = [...(this.querysList.entries())]; // 也可写为 this.querysArray = Array.from(this.querysList);
  }
}

                input-form.component.html

<div>
  <label for="" class="">{{inputLabel}}</label>
  <input type="text" [(ngModel)]="querysList.get(inputValueName).value">
</div>

                input-form.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-input-form',
  templateUrl: './input-form.component.html',
  styleUrls: ['./input-form.component.scss']
})
export class InputFormComponent implements OnInit {
  @Input() inputLabel!: string;
  @Input() querysList: any;
  @Input() inputValueName: any;
  constructor() { }

  ngOnInit(): void {

  }

}

                select-form.component.html

<div class="form-group">
  <label for="">{{selectLabel}}</label>
  <select name="" id="" [(ngModel)]="querysList.get(selectValueName).value">
    <option [ngValue]="">请输入</option>
    <option *ngFor="let item of selectOptions" [ngValue]="item.label">{{item.value}}</option>
  </select>
</div>

                select-form.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-select-form',
  templateUrl: './select-form.component.html',
  styleUrls: ['./select-form.component.scss']
})
export class SelectFormComponent implements OnInit {
  @Input() selectLabel!: string;
  @Input() querysList: any;
  @Input() selectValueName: any;
  @Input() selectOptions: any;
  constructor() { }

  ngOnInit(): void {
  }

}

                special-form.component.html

<div class="form-group">
  <label for="">{{specialLabel}}</label>
  <ng-container *ngComponentOutlet="specialComponent;injector:injector"></ng-container>
</div>

                special-form.component.ts

import { Component, Inject, Injectable, Injector, Input, OnInit } from '@angular/core';

@Injectable()
export class QuerysList {
  constructor() { }
}
@Component({
  selector: 'app-special-form',
  templateUrl: './special-form.component.html',
  styleUrls: ['./special-form.component.scss']
})
export class SpecialFormComponent implements OnInit {
  @Input() specialComponent: any;
  @Input() specialLabel!: string;
  @Input() specialValueName: any;
  @Input() querysList: any;
  injector!: any;
  constructor(private inj: Injector) { }

  ngOnInit(): void {
    this.createInjector();
  }
  /**
   * 创建一个注入器
   */
  createInjector() {
    let options = {
      providers: [
        { provide: QuerysList, useValue: this.querysList }
      ],
      parent: this.inj
    }
    this.injector = Injector.create(options);
  }
}

                form.params.ts

export class MapValue {
  label!: string;
  value: any;
  type!: number;
  options!: Array<any>;
  component: any;
  constructor(_value: any, _label: string, _type: number, _options: Array<any> = [], _component?: any) {
    this.label = _label;
    this.value = _value;
    this.options = _options;
    this.type = _type;
    this.component = _component;
  }
}
// export const QuerysList: Map<any, any> = new Map([
//   ["Input", new MapValue("", "输入框", 1)],
//   ["select", new MapValue("", "选择框", 2)],
//   ["special", new MapValue("", "特殊框", 3)]
// ])

                homein.component.html

<app-form-group [querysList]="querysList"></app-form-group>
<button (click)="consoleData()">console</button>

                homein.component.ts

import { Component, OnInit } from '@angular/core';
import { UseSpecialMap } from '../../shared/models/use-special.params';

@Component({
  selector: 'app-homeinin',
  templateUrl: './homeinin.component.html',
  styleUrls: ['./homeinin.component.scss']
})
export class HomeininComponent implements OnInit {
  querysList: any = UseSpecialMap
  constructor() { }

  ngOnInit(): void {
  }
  consoleData() {
    console.log('querysList :>> ', this.querysList);
  }
}

                use-special.component.html

<textarea [(ngModel)]="querysList.get('special').value"></textarea>

                use-special.component.ts

import { Component, OnInit } from '@angular/core';
import { MapValue } from 'src/app/form-module/models/form.params';
import { QuerysList } from 'src/app/form-module/special-form/special-form.component';
import { UseSpecialMap } from '../../models/use-special.params';


@Component({
  selector: 'app-use-special',
  templateUrl: './use-special.component.html',
  styleUrls: ['./use-special.component.scss']
})
export class UseSpecialComponent implements OnInit {
  querysList!: any;
  constructor(
    // private querysMap: QuerysList
  ) { }

  ngOnInit(): void {
    // this.querysList = this.querysMap;
    this.querysList = UseSpecialMap;
  }

}

                use-special.params.ts

import { MapValue } from "src/app/form-module/models/form.params";
import { UseSpecialComponent } from "../components/use-special/use-special.component";

export let selectOptions = [
  { label: "first", value: 1 },
  { label: "second", value: 2 },
  { label: "third", value: 3 },
]

export let UseSpecialMap: Map<string, MapValue> = new Map([
  ["input", new MapValue("", "输入框", 1)],
  ["select", new MapValue("", "选择框", 2, selectOptions)],
  ["special", new MapValue("", "特殊框", 3, [], UseSpecialComponent)]
])

最后

        这是我第二次写博客,我也是个前端新手,刚工作半年多不到一年,可能文章中有些错误,如果大家有发现不合适的地方可以评论或者私信我,一起讨论共同成长,谢谢大家啦

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐