一步步封裝完善一個數(shù)據(jù)驅(qū)動型-表單模型

angular schema form 數(shù)據(jù)區(qū)動表單

項目演示地址

項目github地址

  • 需求分析

根據(jù)給定的schema數(shù)據(jù)來生成移動端的數(shù)據(jù)表單。剛想到這個需求,我也沒啥思路!先做一個簡單的,一步一步來,總會實現(xiàn)的!

需求簡化

我們實現(xiàn)一個動態(tài)生成組件的功能,簡化到這一步,我想到了上一篇文章10分鐘快速上手angular cdk,提到cdk里面有一個portal可以實現(xiàn),既然有了思路那就動手吧!

<ng-container *ngFor="let item of list">
  <ng-container [cdkPortalOutlet]="item"></ng-container>
</ng-container>
import { Component, OnInit } from '@angular/core';
import { FieldInputComponent } from 'iwe7/form/src/field-input/field-input.component';
import { ComponentPortal } from '@angular/cdk/portal';
@Component({
  selector: 'form-container',
  templateUrl: './form-container.component.html',
  styleUrls: ['./form-container.component.scss']
})
export class FormContainerComponent implements OnInit {
  list: any[] = [];
  constructor() {}

  ngOnInit() {
    const inputPortal = new ComponentPortal(FieldInputComponent);
    this.list.push(inputPortal);
  }
}

這樣我們就以最簡單的方式生成了一個input表單

繼續(xù)深化-動態(tài)創(chuàng)建組件

第一個小目標我們已經(jīng)實現(xiàn)了,下面接著深度優(yōu)化。這也是拿到一個需求的正常思路,先做著!
說真的,我也是一面寫文章一面整理思路,因為我發(fā)現(xiàn)有的時候,寫出來的東西思路會很清晰,就想以前喜歡蹲在廁所里敲代碼腦子轉(zhuǎn)的很快一樣!

import { Component, OnInit } from '@angular/core';
import { FieldRegisterService } from 'iwe7/form/src/field-register.service';

@Component({
  selector: 'form-container',
  templateUrl: './form-container.component.html',
  styleUrls: ['./form-container.component.scss']
})
export class FormContainerComponent implements OnInit {
  list: any[] = [
    {
      type: 'input'
    }
  ];
  constructor(public register: FieldRegisterService) {}

  ngOnInit() {
    // 這里集成了一個服務(wù),用來提供Portal
    this.list.map(res => {
      res.portal = this.register.getComponentPortal(res.type);
    });
  }
}

服務(wù)實現(xiàn)

import { Injectable, InjectionToken, Type, Injector } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';
import { FieldInputComponent } from './field-input/field-input.component';

export interface FormFieldData {
  type: string;
  component: Type<any>;
}

export const FORM_FIELD_LIBRARY = new InjectionToken<
  Map<string, FormFieldData>
>('FormFieldLibrary', {
  providedIn: 'root',
  factory: () => {
    const map = new Map();
    map.set('input', {
      type: 'input',
      component: FieldInputComponent
    });
    return map;
  }
});
@Injectable()
export class FieldRegisterService {
  constructor(public injector: Injector) {}
  // 通過key索引,得到一個portal
  getComponentPortal(key: string) {
    const libs = this.injector.get(FORM_FIELD_LIBRARY);
    const component = libs.get(key).component;
    return new ComponentPortal(component);
  }
}

繼續(xù)深化-發(fā)現(xiàn)問題,重新整理思路

這樣我們就通過一個給定的list = [{type: 'input'}] 來動態(tài)生成一個組件
接下來,我們繼續(xù)完善這個input,給他加上name[表單提交時的key],placeholder[輸入提醒],label[標題],value[默認之],并正確顯示!
這個時候我們發(fā)現(xiàn),portal沒有提供傳遞input數(shù)據(jù)的地方!那只有換方案了,看來他只適合簡單的動態(tài)生成模板。下面我們自己封裝一個directive用于生成組件。

@Directive({
  selector: '[createComponent],[createComponentProps]'
})
export class CreateComponentDirective
  implements OnInit, AfterViewInit, OnChanges {
  @Input() createComponent: string;
  // 輸入即傳入進來的json
  @Input() createComponentProps: any;

  componentInstance: any;
  constructor(
    public register: FieldRegisterService,
    public view: ViewContainerRef
  ) {}

  ngOnInit() {}
  // 當輸入變化時,重新生成組件
  ngOnChanges(changes: SimpleChanges) {
    if ('createComponent' in changes) {
      this.create();
    }
    if ('createComponentProps' in changes) {
      this.setProps();
    }
  }

  setProps() {
    if (!!this.componentInstance) {
      this.componentInstance.props = this.createComponentProps;
      this.componentInstance.updateValue();
    }
  }

  create() {
    // 清理試圖
    this.view.clear();
    // 創(chuàng)建并插入component
    const component = this.register.getComponent(this.createComponent);
    const elInjector = this.view.parentInjector;
    const componentFactoryResolver = elInjector.get(ComponentFactoryResolver);
    const componentFactory = componentFactoryResolver.resolveComponentFactory(
      component
    );
    const componentRef = this.view.createComponent(componentFactory);
    // 保存一下,方便后面使用
    this.componentInstance = componentRef.instance;
    this.setProps();
  }
}
  • 改造之前的代碼
<ng-container *ngFor="let item of list">
  <ng-container *createComponent="item.type;props item;"></ng-container>
</ng-container>
export class FormContainerComponent implements OnInit {
  list: any[] = [
    {
      type: 'input',
      name: 'realname',
      label: '姓名',
      placeholder: '請輸入姓名',
      value: ''
    }
  ];
  constructor() {}
  ngOnInit() {}
}

改造后的注冊器

import {
  Injectable,
  InjectionToken,
  Type,
  Injector,
  ViewContainerRef,
  NgModuleRef,
  ComponentFactoryResolver
} from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';
import { FieldInputComponent } from './field-input/field-input.component';

export interface FormFieldData {
  type: string;
  component: Type<any>;
}

export const FORM_FIELD_LIBRARY = new InjectionToken<
  Map<string, FormFieldData>
>('FormFieldLibrary', {
  providedIn: 'root',
  factory: () => {
    const map = new Map();
    map.set('input', {
      type: 'input',
      component: FieldInputComponent
    });
    return map;
  }
});
@Injectable()
export class FieldRegisterService {
  constructor(
    public injector: Injector,
    private moduleRef: NgModuleRef<any>
  ) {}
  // 通過key索引,得到一個portal
  getComponent(key: string) {
    const libs = this.injector.get(FORM_FIELD_LIBRARY);
    const component = libs.get(key).component;
    return component;
  }
}
  • Input組件
export class FieldInputComponent implements OnInit, OnChanges {
  label: string = 'label';
  name: string = 'name';
  value: string = '';
  placeholder: string = 'placeholder';
  id: any;

  @Input() props: any;
  constructor(public injector: Injector) {
    this.id = new Date().getTime();
  }

  ngOnChanges(changes: SimpleChanges) {
    if ('props' in changes) {
      this.updateValue();
    }
  }

  // 更新配置項目
  updateValue() {
    const { label, name, value, placeholder } = this.props;
    this.label = label || this.label;
    this.name = name || this.name;
    this.value = value || this.value;
    this.placeholder = placeholder || this.placeholder;
  }

  ngOnInit() {}
}

繼續(xù)深化-加入表單驗證

到目前位置我們已經(jīng)實現(xiàn)了基礎(chǔ)功能,根據(jù)傳入進來的schema成功創(chuàng)建了一個僅有input的表單。
下面我們繼續(xù)深化,加上表單驗證

  • 表單驗證邏輯

export class FormValidators {
  static required(value): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      const result = Validators.required(control);
      if (!result) {
        return null;
      }
      return {
        ...value,
        ...result
      };
    };
  }
  static maxLength(value): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      const result = Validators.maxLength(value.limit)(control);
      if (!result) {
        return null;
      }
      return {
        ...value,
        ...result
      };
    };
  }
  static minLength(value): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      const result = Validators.minLength(value.limit)(control);
      if (!result) {
        return null;
      }
      return {
        ...value,
        ...result
      };
    };
  }
}
@Injectable()
export class ValidatorsHelper {
  getValidator(key: string): ValidatorFn {
    return FormValidators[key];
  }
}
  • html
<label [attr.for]="'input_'+id" [formGroup]="form">
  {{label}}
  <input [formControlName]="name" [attr.id]="'input_'+id" #input [attr.name]="name" [attr.value]="value" [attr.placeholder]="placeholder"
  />
  <div *ngIf="!form.get(name).valid">{{form.get(name).errors.msg}}</div>
</label>
export class FieldInputComponent implements OnInit, OnChanges {
  label: string = 'label';
  name: string = 'name';
  value: string = '';
  placeholder: string = 'placeholder';
  validators: any = {};
  id: any;

  @Input() props: any;

  form: FormGroup;
  control: AbstractControl;

  @ViewChild('input') input: ElementRef;
  constructor(
    public injector: Injector,
    public fb: FormBuilder,
    public validatorsHelper: ValidatorsHelper
  ) {
    this.id = new Date().getTime();
    // 創(chuàng)建動態(tài)表單
    this.form = this.fb.group({});
  }

  ngOnChanges(changes: SimpleChanges) {
    if ('props' in changes) {
      this.updateValue();
    }
  }

  // 更新配置項目
  updateValue() {
    const { label, name, value, placeholder, validators } = this.props;
    this.label = label || this.label;
    this.name = name || this.name;
    this.value = value || this.value;
    this.placeholder = placeholder || this.placeholder;
    this.validators = validators || this.validators;
  }

  ngOnInit() {
    this.control = new FormControl(this.value, {
      validators: [],
      updateOn: 'blur'
    });
    this.control.clearValidators();
    const validators = [];
    Object.keys(this.validators).map(key => {
      const value = this.validators[key];
      const validator = this.validatorsHelper.getValidator(key);
      if (key === 'required') {
        validators.push(validator(value));
      } else {
        validators.push(validator(value));
      }
    });
    this.control.setValidators(validators);
    this.form.addControl(this.name, this.control);
    // 監(jiān)聽變化
    this.form.valueChanges.subscribe(res => {
      console.log(res);
    });
  }
}
list: any[] = [
    {
      type: 'input',
      name: 'realname',
      label: '姓名',
      placeholder: '請輸入姓名',
      value: '',
      validators: {
        required: {
          limit: true,
          msg: '請輸入您的姓名'
        },
        minLength: {
          limit: 3,
          msg: '最小長度為3'
        },
        maxLength: {
          limit: 10,
          msg: '最大長度為10'
        }
      }
    },
    {
      type: 'input',
      name: 'nickname',
      label: '昵稱',
      placeholder: '請輸入昵稱',
      value: '',
      validators: {
        required: {
          limit: true,
          msg: '請輸入您的昵稱'
        },
        minLength: {
          limit: 3,
          msg: '昵稱最小長度為3'
        },
        maxLength: {
          limit: 10,
          msg: '昵稱最大長度為10'
        }
      }
    }
  ];

小結(jié)

目前位置我們已經(jīng)實現(xiàn)了全部的功能,下面進一步規(guī)范后面的開發(fā)流程,編寫相應(yīng)的約束。
為后期擴展做準備

import { Input } from '@angular/core';
// 組件設(shè)置規(guī)范
export abstract class FieldBase {
  // 傳進來的json數(shù)據(jù)
  @Input() props: { [key: string]: string };
  // 更新屬性的值
  abstract updateValue(): void;
}
// json數(shù)據(jù)格式規(guī)范
export interface SchemaInterface {
  type?: string;
  name?: string;
  label?: string;
  placeholder?: string;
  value?: string;
  validators: {
    [key: string]: {
      limit: string;
      msg: string;
    };
  };
}
// 表單規(guī)范
export interface SchemasInterface {
  // 提交的url
  url?: string;
  // 提交成功
  success: {
    // 提交成功提醒
    msg?: string;
    // 提交成功跳轉(zhuǎn)
    url?: string;
  };
  // 提交失敗
  fail: {
    // 提交失敗提醒
    msg?: string;
    url?: string;
  };
  // 表單設(shè)置
  fields: SchemaInterface[];
}

希望有更多的人參與這個項目!一起開發(fā),一起討論,一起進步??!

項目演示地址

項目github地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,680評論 19 139
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,588評論 4 61
  • 對于語言的編輯能力和深入思考,需要微習慣不斷的去實踐,但是沒有實踐就是等于零。 今日小確幸: 和班班說道歉,說自己...
    尹莎閱讀 140評論 0 0
  • 1.你一定有過這樣的體驗——事情發(fā)展著發(fā)展著感覺有點不對,注意到一些異象,但問題不大沒有深究,結(jié)果問題很大,事情搞...
    琢磨概念者閱讀 226評論 0 0
  • 由于python中這些模塊之間具有相互依賴的關(guān)系,故在安裝這些模塊時的順序如下 1.安裝numpy #pipins...
    舒map閱讀 797評論 0 0

友情鏈接更多精彩內(nèi)容