表单

作者: soojade | 来源:发表于2016-12-16 16:06 被阅读172次

    版本:Angular 5.0.0-alpha

    表单是商业应用的支柱,我们用它来执行登录、求助、下单、预订机票、安排会议,以及不计其数的其它数据录入任务。

    在开发表单时,创建数据方面的体验是非常重要的,它能指引用户明细、高效的完成工作流程。

    开发表单需要设计能力(那超出了本章的范围),而框架支持双向数据绑定、变更检测、验证和错误处理,在本章你将会学习它们。

    本章展示了如何从草稿构建一个简单的表单。在这个过程中你将学会如何:

    • 用组件和模板构建 Angular 表单。
    • ngModel创建双向数据绑定,以读取和写入输入控件的值。
    • 跟踪状态的变化,并验证表单控件。
    • 使用特殊的 CSS 类来跟踪控件的状态并给出视觉反馈。
    • 向用户显示验证错误提示,以及启用/禁用表单控件。
    • 使用模板引用变量在 HTML 元素之间共享信息。

    你可以运行在线示例(查看源代码)。

    模板驱动表单

    你可以使用 Angular 模板语法编写模板,结合本章所描述的表单专用指令和技术来构建表单。

    你还可以使用响应式(也叫模型驱动)的方式来构建表单。不过本章中只介绍模板驱动表单。

    利用 Angular 模板,可以构建几乎所有表单——登录表单、联系人表单, 以及任何非常漂亮的商务表单。可以创造性的摆放各种控件、把它们绑定到数据、指定校验规则、显示校验错误、有条件的禁用或启用特定的控件、触发内置的视觉反馈等等,不胜枚举。

    Angular 通过处理大量重复的、模板化的任务,简化了过程,从而使你不必陷入与自己的斗争中。

    你将学习构建如下的“模板驱动”表单:

    英雄职业介绍所,使用这个表单来维护英雄们的个人信息。每个英雄都需要一份工作。公司的使命就是让合适的英雄去应对合适的危机。

    表单中的三个字段,其中两个是必填的。根据 material design 指南,必填的字段用星号(*)标出。

    如果删除了英雄的名字,表单就会用醒目的样式把验证错误显示出来。

    注意,提交按钮被禁用了,而且输入控件从绿色变为了红色。

    你将一小步一小步地构建此表单:

    1. 创建Hero模型类。
    2. 创建控制此表单的组件。
    3. 创建具有初始表单布局的模板。
    4. 使用 ngModel 双向数据绑定语法把数据属性绑定到每个表单输入控件。
    5. 为每个表单输入控件添加 ngControl 指令。
    6. 添加自定义 CSS 来提供视觉反馈。
    7. 显示和隐藏有效性验证的错误信息。
    8. 使用 ngSubmit 处理表单提交。
    9. 禁用此表单的提交按钮,直到表单变为有效。

    配置

    根据配置的说明创建一个名为forms的新项目。

    添加 angular_forms

    Angular 表单的功能在 angular_forms 库中,它有自己的包,添加包到 pub 依赖中:

    // {quickstart → forms}/pubspec.yaml
    
    dependencies:
        angular: ^5.0.0-alpha       
    +  angular_forms: ^2.0.0-alpha
    

    创建模型

    当用户输入表单数据时,需要捕获它们的变化,并更新到模型的实例中。除非知道模型的样子,否则无法设计表单的布局。

    最简单的模型是个“属性包”,用来存放关于应用重点的资料。这里使用了描述Hero类的三个必备字段 (idnamepower),和一个可选字段 (alterEgo)。

    lib目录,按照已给出的内容创建下面的文件:

    // lib/src/hero.dart
    
    class Hero {
      int id;
      String name, power, alterEgo;
      Hero(this.id, this.name, this.power, [this.alterEgo]);
      String toString() => '$id: $name ($alterEgo). Super power: $power';
    }  
    

    这是一个少量需求和零行为的贫血模型。对演示来说足够了。

    alterEgo是可选的,所以构造函数允许你省略它;注意在[this.alterEgo]中的方括号。

    可以像这样创建新英雄:

    var myHero = new Hero(
        42, 'SkyDog', 'Fetch any object at any distance', 'Leslie Rollover');
    print('My hero is ${myHero.name}.'); // "My hero is SkyDog."
    

    创建基本的表单

    Angular 表单分为两部分:基于 HTML 的模板 ,以及用来处理数据和用户动态交互的组件类。先从这个类开始,是因为它可以简要说明英雄编辑器能做什么。

    创建表单组件

    根据已给出的内容创建下面的文件:

    // lib/src/hero_form_component.dart (v1)
    
    import 'package:angular/angular.dart';
    import 'package:angular_forms/angular_forms.dart';
    
    import 'hero.dart';
    
    const List<String> _powers = [
      'Really Smart',
      'Super Flexible',
      'Super Hot',
      'Weather Changer'
    ];
    
    @Component(
      selector: 'hero-form',
      templateUrl: 'hero_form_component.html',
      directives: [coreDirectives, formDirectives],
    )
    class HeroFormComponent {
      Hero model = new Hero(18, 'Dr IQ', _powers[0], 'Chuck Overstreet');
      bool submitted = false;
    
      List<String> get powers => _powers;
    
      void onSubmit() => submitted = true;
    }
    

    这个组件没有什么特别的地方,没有表单相关的东西,与之前写过的组件没什么不同。

    只需要前面章节中学过的 Angular 概念,就可以完全理解这个组件:

    • 这段代码导入了 Angular 核心库以及你刚刚创建的Hero模型。
    • @Component选择器hero-form表示可以用<hero-form>元素把这个表单放进父模板。
    • templateUrl属性指向一个独立的 HTML 模板文件(稍后创建)。
    • modelpowers定义模拟数据。

    接下来,你可以注入一个数据服务,以获取或保存真实的数据,或者把这些属性暴露为输入属性和输出属性(参见模板语法中的输入和输出属性)来绑定到一个父组件。这不是现在需要关心的问题,未来的更改不会影响到这个表单。

    修改 app component

    AppComponent 是应用的根组件,HeroFormComponent 将被放在其中。

    使用下面的内容替换初始版本:

    // lib/app_component.dart
    
    import 'package:angular/angular.dart';
    
    import 'src/hero_form_component.dart';
    
    @Component(
      selector: 'my-app',
      template: '<hero-form></hero-form>',
      directives: [HeroFormComponent],
    )
    class AppComponent {}
    

    创建初始 HTML 表单模板

    使用下面的内容创建模板文件:

    // lib/src/hero_form_component.html (start)
    
    <div class="container">
      <h1>Hero Form</h1>
      <form>
        <div class="form-group">
          <label for="name">Name&nbsp;*</label>
          <input type="text" class="form-control" id="name" required>
        </div>
        <div class="form-group">
          <label for="alterEgo">Alter Ego</label>
          <input type="text" class="form-control" id="alterEgo">
        </div>
        <div class="row">
          <div class="col-auto">
            <button type="submit" class="btn btn-primary">Submit</button>
          </div>
          <small class="col text-right">*&nbsp;Required</small>
        </div>
      </form>
    </div>
    

    这是一段简单的 HTML5 代码。我们展现了Hero的两个字段,namealterEgo,提供给用户在输入框中输入。

    Name<input>控件具有 HTML5 的required属性;Alter Ego<input>控件没有,因为alterEgo是可选的。

    在底部添加了一个具有一些 CSS 类的提交按钮。

    你还没有用到 Angular。没有绑定,没有额外的指令,只有布局。

    在模板驱动表单中,你只要导入了angular_forms库,就不用对<form>做任何其它的事情来使用库的功能。接下来你会看到它的原理。

    刷新浏览器。你会看到一个简单的,没有样式的表单。

    给表单添加样式

    containerbtn类都来自 Bootstrap。Bootstrap 也有特定的表单类,包括form-controlform-group。它们给表单添加了一点样式。

    Angular 不需要使用 Bootstrap 类或任意外部库的样式。Angular 应用可以使用任意 CSS 库或一点也不用。

    index.html<head>插入下面的链接来添加 Bootstrap 样式。

    // web/index.html (bootstrap)
    
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
    

    刷新浏览器。你会看到一个带有样式的表单。

    使用 *ngFor 添加 powers

    英雄必须从认证过的固定列表中选择一项超能力。你在内部维护这个列表(在HeroFormComponent)。

    在表单中添加select,用ngForpowers列表绑定到列表选项,在之前的显示数据一章中使用过的技术。

    在紧跟着 Alter Ego 组的下方添加如下 HTML:

    // lib/src/hero_form_component.html (powers)
    
    <div class="form-group">
      <label for="power">Hero Power&nbsp;*</label>
      <select class="form-control" id="power" required>
        <option *ngFor="let p of powers" [value]="p">{{p}}</option>
      </select>
    </div>
    

    powers列表中的每一项超能力都会渲染成<option>标签。 模板输入变量p在每个迭代指向不同的超能力,使用双花括号插值表达式语法来显示它的名称。

    使用 ngModel 双向数据绑定

    现在运行此应用,有点令人失望。

    你看不到英雄数据因为还没有绑定到Hero。在前面的章节我们知道怎么去做。显示数据介绍了属性绑定。用户输入展示了如何通过事件绑定来监听 DOM 事件,以及如何用显示的值更新组件的属性。

    现在,需要同时进行显示、监听和提取。

    虽然可以在表单中再次使用这些已知的技术。但是,你将使用新的[(ngModel)]语法,使表单绑定到模型的工作变得更容易。

    找到Name对应的<input>标签,并且像这样更新它:

    // lib/src/hero_form_component.html (name)
    
    <!-- TODO: remove the next diagnostic line -->
    <mark>{{model.name}}</mark><hr>
    <div class="form-group">
      <label for="name">Name&nbsp;*</label>
      <input type="text" class="form-control" id="name" required
             [(ngModel)]="model.name"
             ngControl="name">
    </div>
    

    在 form-group 标签前添加用于诊断的插值表达式,以看清正在发生什么事。给自己留个注释,提醒你完成后移除它。

    聚焦到绑定语法:[(ngModel)]="..."上。

    现在运行应用,开始在Name 输入框中键入,添加和删除字符,我们将看到它们从诊断文本中显示和消失。某一瞬间,它看起来可能是这样:

    诊断信息可以证明,数据确实从输入框流动到模型,再反向流动回来。

    这就是双向数据绑定!。更多信息,参见模板语法章节的使用 NgModel 双向绑定

    注意,<input>标签还添加了ngControl指令,并设置为 "name",表示英雄的名字。使用任何唯一的值都可以,但使用具有描述性的“name”会更有帮助。当在表单组合中使用[(ngModel)]时,必须要定义ngControl指令。

    在内部,Angular 创建了一些NgFormControl,并把它们注册到NgForm指令,再将该指令附加到<form>标签。每个NgFormControl都都以你分配给NgFormControl指令的名称注册。稍后会看到更多 NgForm 的信息。

    Alter EgoHero Power 添加类似的[(ngModel)]绑定和ngControl指令。

    使用model替换诊断绑定表达式。这样就能确认双向数据绑定在整个 Hero 模型上都能正常工作了。

    修改之后,这个表单的核心是这样的:

    // lib/src/hero_form_component.html (controls)
    
    <!-- TODO: remove the next diagnostic line -->
    <mark>{{model}}</mark><hr>
    <div class="form-group">
      <label for="name">Name&nbsp;*</label>
      <input type="text" class="form-control" id="name" required
             [(ngModel)]="model.name"
             ngControl="name">
    </div>
    <div class="form-group">
      <label for="alterEgo">Alter Ego</label>
      <input type="text" class="form-control" id="alterEgo"
             [(ngModel)]="model.alterEgo"
             ngControl="alterEgo">
    </div>
    <div class="form-group">
      <label for="power">Hero Power&nbsp;*</label>
      <select class="form-control" id="power" required
              [(ngModel)]="model.power"
              ngControl="power">
        <option *ngFor="let p of powers" [value]="p">{{p}}</option>
      </select>
    </div>
    
    • 每个 input 元素都有id属性,label元素的for属性用它来匹配到对应的输入控件。
    • 每个 input 元素都有ngControl指令,这是 Angular 表单注册表单控件所必须的。

    如果现在运行本应用,修改每个 Hero 模型的属性,表单可能显示如下:

    表单顶部的诊断信息证实了你所做的一切更改都反映在了 model 中。

    从模板删除诊断绑定因为它已经完成了它的使命。

    基于控件状态提供视觉反馈

    使用 CSS 和类绑定,可以改变表单控件的外观来反映它的状态。

    追踪控件状态

    一个 Angular 表单控件可以告诉你,用户是否碰过此控件,值是否发生改变,以及值是否无效。

    Angular 表单的每个控件(NgControl)追踪自身的状态,并通过检查下面的成员字段使状态可用:

    • dirtypristine表明控件的值是否发生改变。
    • toucheduntouched表明控件是否被访问。
    • valid反映了控件值的有效性。

    控件样式

    valid控件属性是最引人注意的,因为当控件的值无效时,你希望发出强烈的视觉信号。要创建这样的视觉反馈,你需要使用 Bootstrap custom-forms 的类is-validis-invalid

    Name<input>标签添加一个名为name的模板引用变量。使用name和类绑定有条件的指定恰当的表单有效性的类。

    Name<input>标签临时添加另一个名为spy的模板引用变量,用来显示输入框的 CSS 类。

    // lib/src/hero_form_component.html (name)
    
    <input type="text" class="form-control" id="name" required
           [(ngModel)]="model.name"
           #name="ngForm"
           #spy
           [class.is-valid]="name.valid"
           [class.is-invalid]="!name.valid"
           ngControl="name">
    <!-- TODO: remove the next diagnostic line -->
    {{spy.className}}
    
    模板引用变量

    spy模板引用变量绑定到了<input>DOM元素,然而name变量(#name="ngForm")绑定到了与 input 元素相关联的 NgModel。

    为什么是 “ngForm”?指令exportAs 属性告诉 Angular 如何链接模板引用变量到指令。把name设置为 “ngForm” 是因为 ngModel 指令的exportAs属性是 “ngForm”。

    刷新浏览器,遵循下面的步骤:

    1. Name 输入框。
      • 它有一个绿色的边框。
      • 它有form-controlis-valid类。
    2. 添加一些字符来改变 name。类名依然不变。
    3. 删除 name。
      • 输入边框变红。
      • is-invalid类变成了is-valid

    删除#spy模板引用变量和使用到它的诊断信息。

    另一种类绑定的方法,可以使用 NgClass 指令给控件添加样式。首先添加下面的方法来设置控件的状态依赖 CSS 类名:

    // lib/src/hero_form_component.dart (setCssValidityClass)
    
    Map<String, bool> setCssValidityClass(NgControl control) {
      final validityClass = control.valid == true ? 'is-valid' : 'is-invalid';
      return {validityClass: true};
    }
    

    使用上面方法返回的 map 值,绑定到 NgClass 指令——更多关于这个指令及其替代品的信息请看模板语法章节。

    // lib/src/hero_form_component.html (power)
    
    <select class="form-control" id="power" required
            [(ngModel)]="model.power"
            #power="ngForm"
            [ngClass]="setCssValidityClass(power)"
            ngControl="power">
      <option *ngFor="let p of powers" [value]="p">{{p}}</option>
    </select>
    

    显示和隐藏验证错误信息

    你可以改进表单。Name 输入框是必填的,清空它会使输入框变红。这表明有些东西错了,但用户不知道错在哪里,或者如何纠正。利用控件状态来显示有用的信息。

    使用 valid 和 pristine 状态

    当用户删除姓名时,表单看起来应该是这样的:

    要达到这个效果,在紧跟着 Name <input>标签后面添加下面的<div>

    // lib/src/hero_form_component.html (hidden error message)
    
    <div [hidden]="name.valid || name.pristine" class="invalid-feedback">
      Name is required
    </div>
    

    刷新浏览器,删除输入框中的Name。错误信息就显示出来了。

    基于name控件的状态,通过设置divhidden 属性,显式地控制错误信息。

    在这个例子中,当控件是 valid 或 pristine 时,隐藏消息。 “pristine” 意味着从它被显示在表单中开始,用户还从未修改过它的值。

    用户体验取决于开发人员的选择

    有些开发人员会希望任何时候都显示这条消息。如果忽略了 pristine 状态,就会只在值有效时隐藏此消息。如果往这个组件中传入全新(空)的英雄,或者无效的英雄,将立刻看到错误信息 —— 虽然你还什么都没做。

    有些开发人员会希望只有在用户做出无效的更改时才显示这个消息。 如果当控件是 “pristine” 状态时也隐藏消息,就能达到这个目的。在往表单中添加新英雄时,将看到这种选择的重要性。

    Alter Ego 是可选项,所以不用改它。

    英雄的超能力选项是必填的。只要愿意,可以往<select>上添加相同的错误处理。但没有必要,这个选择框已经限制了“超能力”只能选有效值。

    添加清除按钮

    在组件类中添加clear()方法:

    // lib/src/hero_form_component.dart (clear)
    
    void clear() {
      model.name = '';
      model.power = _powers[0];
      model.alterEgo = '';
    }
    

    提交 按钮的右边添加一个绑定click事件的 Clear 按钮:

    // lib/src/hero_form_component.html (Clear button)
    
    <button (click)="clear()" type="button" class="btn">
      Clear
    </button>
    

    刷新浏览器。点击 Clear 按钮。文本域都被清空,如果之前改变了Power 的值,也会重置为默认值。

    使用 ngSubmit 提交表单

    在填表完成之后,用户应该能提交这个表单。Submit 按钮位于表单的底部,它自己不做任何事,但因为它的 type 值 (type="submit"),所以会触发表单提交。

    此时提交表单是无意义的。为了让它有意义,指定表单组件的onSubmit()方法,绑定到表单的ngSubmit事件绑定:

    <form (ngSubmit)="onSubmit()" #heroForm="ngForm">
    

    注意模板引用变量#heroForm。正如前面所说的,变量heroForm被绑定到管理整个表单的NgForm指令。

    NgForm指令

    Angular 自动创建并添加 NgForm 指令到<form>标签。

    NgForm指令给表单元素附加了额外的特性。它会控制那些带有ngModelngControl指令的控件元素,监听他们的属性,包括其有效性。

    你要把表单的总体有效性通过heroForm变量绑定到按钮的disabled属性上。

    <button [disabled]="!heroForm.form.valid" type="submit" class="btn btn-primary">
      Submit
    </button>
    

    刷新浏览器。你会发现按钮是可用的——尽管它还没有做任何有用的事。

    现在如果我们删除 Name,就违反了“required”规则,它会恰当的显示在错误信息中。提交 按钮也被禁用了。

    不觉得了不起吗?再想一想。如果没有 Angular 的帮助,我们又该怎样让按钮的禁用/启用状态和表单的有效性关联起来呢?

    有了 Angular,它就是这么简单:

    1. 在(增强的)form 元素上定义一个模板引用变量。
    2. 在许多行之外的按钮上引用该变量。

    显示模型 (可选)

    此时提交表单没有视觉特效。

    改进 demo 也无法教给我们任何关于表单的新知识。但这是一个练习新学到的绑定技能的好机会。如果你不感兴趣,跳到本章的总结部分。

    作为视觉效果,隐藏掉数据输入区域并显示一些其它东西。

    把表单包裹进<div>中,并把它的hidden属性绑定到HeroFormComponent.submitted属性。

    // lib/src/hero_form_component.html (excerpt)
    
      <div [hidden]="submitted">
        <h1>Hero Form</h1>
        <form (ngSubmit)="onSubmit()" #heroForm="ngForm">
        </form>
      </div>
    

    表单从一开始就是可见的,因为submitted属性是 false,直到我们提交了这个表单,正如下面这段HeroFormComponent的代码展示的:

    // lib/src/hero_form_component.dart (submitted)
    
    bool submitted = false;
    
    void onSubmit() => submitted = true;
    

    现在,在刚写的<div>包裹层下方,添加如下 HTML:

    // lib/src/hero_form_component.html (submitted)
    
    <div [hidden]="!submitted">
      <h1>Hero data</h1>
    
      <table class="table">
        <tr>
          <th>Name</th>
          <td>{{model.name}}</td>
        </tr>
        <tr>
          <th>Alter Ego</th>
          <td>{{model.alterEgo}}</td>
        </tr>
        <tr>
          <th>Power</th>
          <td>{{model.power}}</td>
        </tr>
      </table>
    
      <button (click)="submitted=false" class="btn btn-primary">Edit</button>
    </div>
    

    刷新浏览器,并提交表单。submitted标记变成 true,表单消失了。你会看到英雄模型的值(只读)显示在表格中。

    这个视图包含一个 Edit 按钮,它的点击事件绑定清除submitted标志。当你点击 Edit 按钮时,表格消失,可编辑的表单又出现了。

    总结

    Angular 表单提供了数据修改、验证等支持。在本章中学到了怎样使用下面的特性:

    • 一个 HTML 表单模板和一个带有@Component注解的表单组件类。
    • 通过一个ngSubmit事件绑定处理表单提交。
    • 模板引用变量,例如heroFormname
    • 双向数据绑定([(ngModel)])。
    • 用于验证和追踪表单元素变化的ngControl指令。
    • input 控件的valid属性(通过模板引用变量获取),用于检查控件的有效性和显示/隐藏错误信息。
    • NgForm.form的有效性来设置 提交 按钮的激活状态。
    • 定制 CSS 类来给用户提供控件状态的视觉反馈。

    下一步

    依赖注入

    相关文章

      网友评论

        本文标题:表单

        本文链接:https://www.haomeiwen.com/subject/lrwnmttx.html