2018 年 7 月

第 33 卷,第 7 期

孜孜不倦的程序员 - 如何成为 MEAN:以动态方式使用 Angular

通过Ted Neward |2018 年 7 月

Ted Neward我们又见面了,MEAN 用户们。

在我的上一篇专栏,"如何为 MEAN:反应式编程"(msdn.com/magazine/mt846724),我研究了 Angular 提供了在其中创建以不同的方式来构造一个窗体和用户对事件做出响应的响应式窗体模块。在那篇文章结束时,我提出一个问题:如果我有其中相当大数量的控件,或创建控件需要进行更改的情况下根据下面的模型对象的不断变化的性质?这是一个例子,其中,坦白地讲,依赖于 Angular 采用传统的"模板"系统无法满足要求。如果我必须创建一个模板针对每个可能的可能组合,它将很长的一天。

让我们认为,从目前会议想要构建与会者评估发言人、 访谈、 会议地点、 Web 站点的轮询系统,你将其命名。他们想要能够推出新的问题非常快速,可能甚至在会议过程本身应该需要出现。这意味着,然后,我想知道如何生成基于被不断问及的问题类型的字段的网页和这些问题类型将来自外部源 (例如 JSON 服务或甚至文件)。

不神奇。在任何 (基于 Web 的或其他) 的 GUI 系统,支持通过 (例如都无法提供"new"控件本身) 的运行时构造的控件的构造的这是合理和非常可行的操作。它是在 Angular 当然可行:我可以构建一个系统,其中生成完全从模型对象的窗体和关联的 (隐式或显式) 元数据。

因此,若要继续调查问卷的示例中,如果我构建一个简单的 Angular 服务知道如何获取一系列的"问题"对象,相关联的 Angular 窗体可以采取一些或所有这些对象并构造相应的集合的窗体元素存在问题,并捕获可能用于某处存储的答案。FormGroup 和 FormControls Angular 使用来表示这些控件在运行时都会向所有这些密钥。

动态模型

让我们开始的所有问题,可帮助捕获我期望 (并且将需要) 某些常见行为的任何问题和其相关的控件的基类。相关代码如下:

export type ControlType = "textbox" | "dropdown";
export abstract class Question {
  constructor(public value: string = '',
    public key: string = '',
    public label: string = '',
    public required: boolean = false,
    public controlType: ControlType = 'textbox')
  { }
}

大多数都要相当简单,因为大部分这个类只是属性 (模式人有时称之为 DTO 或数据传输对象),但键的元素将 controlType 字段。它将对应于哪种 HTML 的描述符的情况下生成的构造。目前,有两种可能的所有: (允许开放的文本输入) 的文本框或下拉列表中 (单-从选定项的可能性有限范围)。

同样明显,问题是问题的一个抽象类,因为我希望派生的类型将在此处创建,另一个用于每种类型。TextboxQuestion 的代码如下所示:

export class TextboxQuestion extends Question {
  constructor(value: string = '',
    key: string = '',
    label: string = '',
    required: boolean = false,
    public type: string = '') {
    super(value, key, label, required, 'textbox');
  }
}

和 DropdownQuestion 如下的代码:

export class DropdownQuestion extends Question {
  constructor(value: string = '',
    key: string = '',
    label: string = '',
    required: boolean = false,
    public options: {key: string, value: string}[] = [])
  {
    super(value, key, label, required, 'dropdown');
  }
}

每个问题将传递基参数由其父组和每个添加到组合的一件事。对于 TextboxQuestion,它将添加文本框中的类型参数,如果我想要表示这是密码或电子邮件文本框。对于 DropdownQuestion,它将添加要作为下拉列表中可能使用的键/值对的数组。

接下来,但是,我必须了解如何启用到 FormControl 和 FormGroup 对象图。可以说,根据的方式 Angular 思考设计,可能是独立的服务,但它更有意义对我而言,使其问题的类,作为静态方法的一部分。(如果不断添加新的问题类型,则此方法需要进行更新,因此它更有意义对我而言,保持其全部中相同的模块分组的。) 代码方面,从创建必要的 FormControl 对象非常简单,如下所示:

export abstract class Question {
  public static toFormGroup(questions: Question[]): FormGroup {
    let group: any = {};
    questions.forEach(question => {
      group[question.key] =
        question.required ? new FormControl(question.value, Validators.required)
                          : new FormControl(question.value);
    });
    return new FormGroup(group);
  }
  // ...
}

此方法基本上采用问题的数组,并将其转换为内部 FormGroup 对象固定于 FormControl 对象的数组。从这方面,请注意,只有真正的问题是该控件是否是必需的;任何其他显示逻辑将需要在模板内捕获。

动态显示

我还需要开始考虑所涉及的 Angular UI 组件从根本上说,轮询或调查表组成一个或多个问题,因此我将其用作工作模式: QuestionnaireComponent 使用一定数量的 QuestionComponents,并且每个 QuestionComponent 将具有作为输入问题对象。

总感觉有点更轻松地从顶部开始绘制和我方式工作,因此,让我们执行该操作。首先,我必须将其自身,如中所示在这种情况下显示调查表,AppComponent图 1

图 1 AppComponent

@Component({
  selector: 'app-root',
  template: `
    <div>
      <h2>How was our conference?</h2>
      <app-questionnaire [questions]="questions"></app-questionnaire>
    </div>
  `,
  providers:  [QuestionService]
})
export class AppComponent {
  questions: Question[];
  constructor(service: QuestionService) {
    this.questions = service.getQuestions();
  }
}

此代码提供了完美组件方案。我只需使用它,并有一个服务,知道如何使代码保持轻型、 简单且轻松地直观 Angular 开发人员需要该组件,提供输入。

接下来,让我们看一下 QuestionnaireComponent,如中所示图 2

图 2 QuestionnaireComponent

@Component({
  selector: 'app-questionnaire',
  templateUrl: './questionnaire.component.html'
})
export class QuestionnaireComponent implements OnInit {
  @Input() questions: Question[] = [];
  form: FormGroup;
  payload = '';
  ngOnInit() {
    this.form = Question.toFormGroup(this.questions);
  }
  onSubmit() {
    this.payload = JSON.stringify(this.form.value);
  }
}

同样,方法是非常简单明了。QuestionnaireComponent 将问题数组作为其输入,并使用 FormGroup 到窗体模板中生成匹配。图 3演示了这一点。

图 3 准备来生成与 FormGroup 窗体

<div>
  <form (ngSubmit)="onSubmit()" [formGroup]="form">
    <div *ngFor="let question of questions" class="form-row">
      <app-question [question]="question" [form]="form"></app-question>
    </div>
    <div class="form-row">
      <button type="submit" [disabled]="!form.valid">Save</button>
    </div>
  </form>
  <div *ngIf="payload" class="form-row">
    <strong>Saved the following values</strong><br>{{payload}}
  </div>
</div>

通常情况下,负载将上传通过 HTTP 通过 Angular 服务,可能要存储在数据库中,但,所花的示例有点不在范围。在这里,显示用于演示数据是验证、 捕获和准备好进行分发。

当然,我仍必须生成在表单内的各个问题元素和的下降到 QuestionComponent 代码,就在这里所示:

@Component({
  selector: 'app-question',
  templateUrl: './question.component.html'
})
export class QuestionComponent {
  @Input() question: Question;
  @Input() form: FormGroup;
  get isValid() { return this.form.controls[this.question.key].valid; }
}

请注意 QuestionComponent 接受的输入 (逻辑) 所属; FormGroup我会尝试查找不同的方法,通过其获取 FormControl (适用于 isValid 属性实现),但这种方式工作,并有助于为简单起见。

此组件的模板是动态的窗体创建最大好处发生的地方。由于在问题对象的 controlType 明智地 ngSwitch,我可以构建 HTML 元素非常简单,如中所示图 4

图 4 生成 HTML 元素

<div [formGroup]="form">
  <label [attr.for]="question.key">{{question.label}}</label>
  <div [ngSwitch]="question.controlType">
    <input *ngSwitchCase="'textbox'" [formControlName]="question.key"
            [id]="question.key" [type]="question.type">
    <select *ngSwitchCase="'dropdown'" [formControlName]="question.key"
            [id]="question.key">
      <option *ngFor="let opt of question.options" [value]="opt.key">
        {{opt.value}}
      </option>
    </select>
  </div>
  <div class="errorMessage" *ngIf="!isValid">{{question.label}} is required</div>
</div>

正如您所看到的它非常简洁是根据这些内容。我切换 controlType 属性,并根据这是一个下拉列表或文本框中键入问题时,生成不同的 HTML。

最后,我只需要提供一些问题,这同样,想通常执行此操作从文件或服务器端 API 等一些外部资源 QuestionService。在此特定示例中,服务将拉出内存中,从问题中所示图 5

图 5 从 QuestionService 获取问题

@Injectable()
export class QuestionService {
  getQuestions() {
    return [
      new TextboxQuestion('', 'firstName',
        'Speaker\'s First name', true),
      new DropdownQuestion('', 'enjoyment',
        'How much did you enjoy this speaker\'s talk?',
        false,
        [
          {key: 'great', value: 'Great'},
          {key: 'good', value: 'Good'},
          {key: 'solid', value: 'Solid'},
          {key: 'uninspiring', value: 'Uninspiring'},
          {key: 'wwyt', value: 'What Were You Thinking?'}
        ]),
    ];
  }
}

显然,在实际调查表中,几个更多问题,有可能,但此示例获取的点。

总结

真正的任何此类系统类型与相关的问题是它的可扩展性:可以将新的调查表添加而无需显著修改?很明显,QuestionnaireService 是项中,有 — 前提是它可以产生各个对象数组的问题,我具有无限数目的调查表我可以问我们会议的与会者。唯一的限制是被限制为进行多项选择或 open ended 文本答案我可以稍后再试,提问的问题类型。

引发第二个问题:难它是添加新类型的系统,如离散数值与分级控件问题?为此,需要创建新的问题子类 (RatingsQuestion) 的数字的范围内,若要使用,若要进行切换,该模板并修改 QuestionComponent 模板的新 ControlType 枚举值若要切换到新的枚举值和(但是看),则相应地显示 HTML。其他所有内容将保持不变,这是任何组件技术的目标 — 保留客户端不知道任何结构更改,除非他们选择充分利用新功能。

Angular 读取器将体验来为提供此整个概念数值调节钮,因此,我将使联网到关闭此处。但是,是我们需要仔细审阅,我们可以将我们 Angular 的覆盖范围,所以,我们将命中该下一步的时间之前的一个更有必要位。在那以前,祝你编码愉快!


Ted Neward位于西雅图的 polytechnology 公司顾问、 发言人和导师,目前担任工程设计和开发者关系主管Smartsheet.com。他撰写了大量文章,创作和与人合著过十几本书,并在世界各地发表演讲。可通过 ted@tedneward.com 与他联系,也可阅读他的博客 blogs.tedneward.com

衷心感谢以下技术专家:Garvice Eakins (Smartsheet)


在 MSDN 杂志论坛讨论这篇文章