Angular 从0到1 (二)

第一节:初识Angular-CLI
第二节:登录组件的构建
第三节:建立一个待办事项应用
第四节:进化!模块化你的应用
第五节:多用户版本的待办事项应用
第六节:使用第三方样式库及模块优化用
第七节:给组件带来活力
Rx--隐藏在Angular 2.x中利剑
Redux你的Angular 2应用
第八节:查缺补漏大合集(上)
第九节:查缺补漏大合集(下)

作者:王芃 wpcfan@gmail.com

第二节:用 Form 表单做一个登录控件

对于 login 组件的小改造

hello-angular\src\app\login\login.component.ts 中更改其模板为下面的样子

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

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input type="text">
      <button>Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

我们增加了一个文本输入框和一个按钮,保存后返回浏览器可以看到结果

c2_s1_input_button_added.png-109.6kB
c2_s1_input_button_added.png-109.6kB

接下来我们尝试给Login按钮添加一个处理方法 <button (click)="onClick()">Login</button>(click) 表示我们要处理这个
button 的 click 事件,圆括号是说发生此事件时,调用等号后面的表达式或函数。等号后面的 onClick() 是我们自己定义在 LoginComponent 中的函数,这个名称你可以随便定成什么,不一定叫 onClick() 。下面我们就来定义这个函数,在
LoginComponent 中写一个叫 onClick() 的方法,内容很简单就是把 button was clicked 输出到 Console。

  onClick() {
    console.log('button was clicked');
  }

返回浏览器,并按 F12 调出开发者工具。当你点击 Login 时,会发现 Console 窗口输出了我们期待的文字。

c2_s1_handle_click_method.png-141kB
c2_s1_handle_click_method.png-141kB

那么如果要在 onClick 中传递一个参数,比如是上面的文本输入框输入的值怎么处理呢?我们可以在文本输入框标签内加一个#usernameRef,这个叫引用( reference )。注意这个 引用是的 input 对象 ,我们如果想传递 input 的值,可以用usernameRef.value ,然后就可以把 onClick() 方法改成 onClick(usernameRef.value)

<div>
  <input #usernameRef type="text">
  <button (click)="onClick(usernameRef.value)">Login</button>
</div>

在Component内部的onClick方法也要随之改写成一个接受username的方法

  onClick(username) {
    console.log(username);
  }

现在我们再看看结果是什么样子,在文本输入框中键入 hello ,点击 Login 按钮,观察 Console 窗口:hello 被输出了。

c2_s1_input_button_ref.png-141.1kB
c2_s1_input_button_ref.png-141.1kB

好了,现在我们再加一个密码输入框,然后改写 onClick 方法可以同时接收2个参数:用户名和密码。代码如下:

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

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input #usernameRef type="text">
      <input #passwordRef type="password">
      <button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

  onClick(username, password) {
    console.log('username:' + username + "\n\r" + "password:" + password);
  }

}

看看结果吧,在浏览器中第一个输入框输入 wang,第二个输入框输入 1234567,观察 Console 窗口,Bingo!

c2_s1_username_password_ref.png-141.8kB
c2_s1_username_password_ref.png-141.8kB

建立一个服务去完成业务逻辑

如果我们把登录的业务逻辑在 onClick 方法中完成,当然也可以,但是这样做的耦合性太强了。设想一下,如果我们增加了微信登录、微博登录等,业务逻辑会越来越复杂,显然我们需要把这个业务逻辑分离出去。那么我们接下来创建一个
AuthService 吧, 首先我们在 src\app 下建立一个 core 的子文件夹( src/app/core ),然后命令行中输入 ng g s core/auth ( s这里是service的缩写,core/auth 是说在 core 的目录下建立 auth 服务相关文件,Windows下使用 core\auth,Linux
和 Mac 下面使用 core/auth)。auth.service.tsauth.service.spec.ts 这个两个文件应该已经出现在你的目录里了。

下面我们为这个 service 添加一个方法,你可能注意到这里我们为这个方法指定了返回类型和参数类型。这就是 TypeScript 带来的好处,有了类型约束,你在别处调用这个方法时,如果给出的参数类型或返回类型不正确,IDE就可以直接告诉你错了。

import { Injectable } from '@angular/core';

@Injectable()
export class AuthService {

  constructor() { }

  loginWithCredentials(username: string, password: string): boolean {
    if(username === 'wangpeng')
      return true;
    return false;
  }

}

等一下,这个service虽然被创建了,但仍然无法在Component中使用。当然你可以在Component中import这个服务,然后实例化后使用,但是这样做并不好,仍然时一个紧耦合的模式,Angular2提供了一种依赖性注入(Dependency Injection)的方法。

什么是依赖性注入?

如果不使用DI(依赖性注入)的时候,我们自然的想法是这样的,在 login.component.ts 中 import 引入 AuthService ,在构造中初始化 service ,在 onClick 中调用 service 。

import { Component, OnInit } from '@angular/core';
//引入AuthService
import { AuthService } from '../core/auth.service';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input #usernameRef type="text">
      <input #passwordRef type="password">
      <button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  //声明成员变量,其类型为AuthService
  service: AuthService;

  constructor() {
    this.service = new AuthService();
  }

  ngOnInit() {
  }

  onClick(username, password) {
    //调用service的方法
    console.log('auth result is: ' + this.service.loginWithCredentials(username, password));
  }

}

这么做呢也可以跑起来,但存在几个问题:

  • 由于实例化是在组件中进行的,意味着我们如果更改service的构造函数的话,组件也需要更改。
  • 如果我们以后需要开发、测试和生产环境配置不同的AuthService,以这种方式实现会非常不方便。

下面我们看看如果使用DI是什么样子的,首先我们需要在组件的修饰器中配置AuthService,然后在组件的构造函数中使用参数进行依赖注入。

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../core/auth.service';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input #usernameRef type="text">
      <input #passwordRef type="password">
      <button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
    </div>
  `,
  styles: [],
  //在providers中配置AuthService
  providers:[AuthService]
})
export class LoginComponent implements OnInit {
  //在构造函数中将AuthService示例注入到成员变量service中
  //而且我们不需要显式声明成员变量service了
  constructor(private service: AuthService) {
  }

  ngOnInit() {
  }

  onClick(username, password) {
    console.log('auth result is: ' + this.service.loginWithCredentials(username, password));
  }

}

看到这里你会发现我们仍然需要 import 相关的服务,这是 import 是要将类型引入进来,而 provider 里面会配置这个类型的实例。当然即使这样还是不太爽,可不可以不引入 AuthService 呢?答案是可以。

我们看一下app.module.ts,这个根模块文件中我们发现也有个providers,根模块中的这个providers是配置在模块中全局可用的service或参数的。

providers: [
    {provide: 'auth',  useClass: AuthService}
    ]

providers 是一个数组,这个数组呢其实是把你想要注入到其他组件中的服务配置在这里。大家注意到我们这里的写法和上面优点区别,没有直接写成

providers:[AuthService]

而是给出了一个对象,里面有两个属性,provide 和 useClass,provide 定义了这个服务的名称,有需要注入这个服务的就引用这个名称就好。useClass 指明这个名称对应的服务是一个类,本例中就是 AuthService 了。这样定义好之后,我们就可以在任意组件中注入这个依赖了。下面我们改动一下 login.component.ts ,去掉头部的 import { AuthService } from '../core/auth.service'; 和组件修饰器中的 providers ,更改其构造函数为

onstructor(@Inject('auth') private service) {
  }

我们去掉了service的类型声明,但加了一个修饰符@Inject('auth'),这个修饰符的意思是请到系统配置中找到名称为auth的那个依赖注入到我修饰的变量中。当然这样改完后你会发现Inject这个修饰符系统不识别,我们需要在@angular/core中引用这个修饰符,现在login.component.ts看起来应该是下面这个样子

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

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input #usernameRef type="text">
      <input #passwordRef type="password">
      <button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  constructor(@Inject('auth') private service) {
  }

  ngOnInit() {
  }

  onClick(username, password) {
    console.log('auth result is: ' + this.service.loginWithCredentials(username, password));
  }

}

但这样做有一个问题,就是在 VSCode 中没有方法的提示和类型检查了,所以这一块如果希望有类型约束的话还是建议使用 providers:[AuthService] 这种方式。

双向数据绑定

接下来的问题是我们是否只能通过这种方式进行表现层和逻辑之间的数据交换呢?如果我们希望在组件内对数据进行操作后再反馈到界面怎么处理呢?Angular 提供了一个双向数据绑定的机制。这个机制是这样的,在组件中提供成员数据变量,然后在模板中引用这个数据变量。我们来改造一下 login.component.ts ,首先在 class 中声明2个数据变量 username 和
password 。

  username = "";
  password = "";

然后去掉 onClick 方法的参数,并将内部的语句改造成如下样子:

console.log('auth result is: '
      + this.service.loginWithCredentials(this.username, this.password));

去掉参数的原因是双向绑定后,我们通过数据成员变量就可以知道用户名和密码了,不需要在传递参数了。而成员变量的引用方式是 this.成员变量
然后我们来改造模板:

    <div>
      <input type="text"
        [(ngModel)]="username"
        />
      <input type="password"
        [(ngModel)]="password"
        />
      <button (click)="onClick()">Login</button>
    </div>

[(ngModel)]="username" 这个看起来很别扭,稍微解释一下,方括号[]的作用是说把等号后面当成表达式来解析而不是当成字符串,如果我们去掉方括号那就等于说是直接给这个 ngModel 赋值成 username 这个字符串了。方括号的含义是单向绑定,就是说我们在组件中给 model 赋的值会设置到 HTML 的 input 控件中。 [()] 是双向绑定的意思,就是说HTML对应控件的状态的改变会反射设置到组件的 model 中。ngModel 是 FormModule 中提供的指令,它负责从Domain Model(这里就是 username 或 password ,以后我们可用绑定更复杂的对象)中创建一个 FormControl 的实例,并将这个实例和表单的控件绑定起来。同样的对于 click 事件的处理,我们不需要传入参数了,因为其调用的是刚刚我们改造的组件中的 onClick 方法。现在我们保存文件后打开浏览器看一下,效果和上一节的应该一样的。本节的完整代码如下:

//login.component.ts
import { Component, OnInit, Inject } from '@angular/core';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input type="text"
        [(ngModel)]="username"
        />
      <input type="password"
        [(ngModel)]="password"
        />
      <button (click)="onClick()">Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  username = '';
  password = '';

  constructor(@Inject('auth') private service) {
  }

  ngOnInit() {
  }

  onClick() {
    console.log('auth result is: '
      + this.service.loginWithCredentials(this.username, this.password));
  }

}

表单数据的验证

通常情况下,表单的数据是有一定的规则的,我们需要依照其规则对输入的数据做验证以及反馈验证结果。Angular 中对表单验证有非常完善的支持,我们继续上面的例子,在 login 组件中,我们定义了一个用户名和密码的输入框,现在我们来为它们加上规则。首先我们定义一下规则,用户名和密码都是必须输入的,也就是不能为空。更改 login.component.ts 中的模板为下面的样子

    <div>
      <input required type="text"
        [(ngModel)]="username"
        #usernameRef="ngModel"
        />
        {{usernameRef.valid}}
      <input required type="password"
        [(ngModel)]="password"
        #passwordRef="ngModel"
        />
        {{passwordRef.valid}}
      <button (click)="onClick()">Login</button>
    </div>

注意到我们只是为 username 和 password 两个控件加上了 required 这个属性,表明这两个控件为必填项。通过
#usernameRef="ngModel" 我们重新又加入了引用,这次的引用指向了 ngModel ,这个引用是要在模板中使用的,所以才加入这个引用如果不需要在模板中使用,可以不要这句。{{表达式}} 双花括号表示解析括号中的表达式,并把这个值输出到模板中。这里我们为了可以显性的看到控件的验证状态,直接在对应控件后输出了验证的状态。初始状态可以看到2个控件的验证状态都是 false,试着填写一些字符在两个输入框中,看看状态变化吧。

c2_s2_form_validation.png-8.5kB
c2_s2_form_validation.png-8.5kB

我们是知道了验证的状态是什么,但是如果我们想知道验证失败的原因怎么办呢?我们只需要将 {{usernameRef.valid}} 替换成 {{usernameRef.errors | json}}| 是管道操作符,用于将前面的结果通过管道输出成另一种格式,这里就是把errors对象输出成json格式的意思。看一下结果吧,返回的结果如下

c2_s2_form_validation_errors.png-11kB
c2_s2_form_validation_errors.png-11kB

如果除了不能为空,我们为username再添加一个规则试试看呢,比如字符数不能少于3。

      <input type="text"
        [(ngModel)]="username"
        #usernameRef="ngModel"
        required 
        minlength="3"
        />
c2_s2_form_validation_errors_multiple.png-14.4kB
c2_s2_form_validation_errors_multiple.png-14.4kB

现在我们试着把{{表达式}}替换成友好的错误提示,我们想在有错误发生时显示错误的提示信息。那么我们来改造一下template。

    <div>
      <input type="text"
        [(ngModel)]="username"
        #usernameRef="ngModel"
        required
        minlength="3"
        />
        {{ usernameRef.errors | json }}
        <div *ngIf="usernameRef.errors?.required">this is required</div>
        <div *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</div>
      <input required type="password"
        [(ngModel)]="password"
        #passwordRef="ngModel"
        />
        <div *ngIf="passwordRef.errors?.required">this is required</div>
      <button (click)="onClick()">Login</button>
    </div>

ngIf也是一个Angular2的指令,顾名思义,是用于做条件判断的。*ngIf="usernameRef.errors?.required"的意思是当usernameRef.errors.requiredtrue时显示div标签。那么那个?是干嘛的呢?因为errors可能是个null,如果这个时候调用errorsrequired属性肯定会引发异常,那么?就是标明errors可能为空,在其为空时就不用调用后面的属性了。

如果我们把用户名和密码整个看成一个表单的话,我们应该把它们放在一对<form></form>标签中,类似的加入一个表单的引用formRef

    <div>
      <form #formRef="ngForm">
        <input type="text"
          [(ngModel)]="username"
          #usernameRef="ngModel"
          required
          minlength="3"
          />
          <div *ngIf="usernameRef.errors?.required">this is required</div>
          <div *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</div>
        <input type="password"
          [(ngModel)]="password"
          #passwordRef="ngModel"
          required
          />
          <div *ngIf="passwordRef.errors?.required">this is required</div>
        <button (click)="onClick()">Login</button>
      </form>
    </div>

这时运行后会发现原本好用的代码出错了,这是由于如果在一个大的表单中,ngModel会注册成Form的一个子控件,注册子控件需要一个name,这要求我们显式的指定对应控件的name,因此我们需要为input增加name属性

    <div>
      <form #formRef="ngForm">
        <input type="text"
          name="username"
          [(ngModel)]="username"
          #usernameRef="ngModel"
          required
          minlength="3"
          />
          <div *ngIf="usernameRef.errors?.required">this is required</div>
          <div *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</div>
        <input type="password"
          name="password"
          [(ngModel)]="password"
          #passwordRef="ngModel"
          required
          />
          <div *ngIf="passwordRef.errors?.required">this is required</div>
        <button (click)="onClick()">Login</button>
        <button type="submit">Submit</button>
      </form>
    </div>

既然我们增加了一个 formRef ,我们就看看 formRef.value 有什么吧。
首先为form增加一个表单提交事件的处理
<form #formRef="ngForm" (ngSubmit)="onSubmit(formRef.value)">

然后在组件中增加一个 onSubmit 方法

  onSubmit(formValue) {
    console.log(formValue);
  }

你会发现 formRef.value 中包括了表单所有填写项的值。

c2_s2_form_validation_form_submit.png-27.7kB
c2_s2_form_validation_form_submit.png-27.7kB

有时候在表单项过多时我们需要对表单项进行分组,HTML中提供了 fieldset 标签用来处理。那么我们看看怎么和 Angular 结合吧:

    <div>
      <form #formRef="ngForm" (ngSubmit)="onSubmit(formRef.value)">
        <fieldset ngModelGroup="login">
          <input type="text"
            name="username"
            [(ngModel)]="username"
            #usernameRef="ngModel"
            required
            minlength="3"
            />
            <div *ngIf="usernameRef.errors?.required">this is required</div>
            <div *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</div>
          <input type="password"
            name="password"
            [(ngModel)]="password"
            #passwordRef="ngModel"
            required
            />
            <div *ngIf="passwordRef.errors?.required">this is required</div>
          <button (click)="onClick()">Login</button>
          <button type="submit">Submit</button>
        </fieldset>
      </form>
    </div>

<fieldset ngModelGroup="login"> 意味着我们对于 fieldset 之内的数据都分组到了 login 对象中。

c2_s2_form_validation_fieldset.png-43.5kB
c2_s2_form_validation_fieldset.png-43.5kB

接下来我们改写 onSubmit 方法用来替代 onClick ,因为看起来这两个按钮重复了,我们需要去掉 onClick 。首先去掉
template 中的 <button (click)="onClick()">Login</button> ,然后把 <button type="submit"> 标签后的 Submit 文本替换成 Login ,最后改写 onSubmit 方法。

  onSubmit(formValue) {
    console.log('auth result is: '
      + this.service.loginWithCredentials(formValue.login.username, formValue.login.password));
  }

在浏览器中试验一下吧,所有功能正常工作。

验证结果的样式自定义

如果我们在开发工具中查看网页源码,可以看到

c2_s2_form_validation_form_styling.png-92.5kB
c2_s2_form_validation_form_styling.png-92.5kB

用户名控件的HTML代码是下面的样子:在验证结果为 false 时 input 的样式是 ng-invalid

<input 
    name="username" 
    class="ng-pristine ng-invalid ng-touched" 
    required="" 
    type="text" 
    minlength="3" 
    ng-reflect-minlength="3" 
    ng-reflect-name="username">

类似的可以实验一下,填入一些字符满足验证要求之后,看 input 的 HTML 是下面的样子:在验证结果为 true 时 input 的样式是 ng-valid

<input 
    name="username" 
    class="ng-touched ng-dirty ng-valid" 
    required="" 
    type="text" 
    ng-reflect-model="ssdsds" 
    minlength="3" 
    ng-reflect-minlength="3" 
    ng-reflect-name="username">

知道这个后,我们可以自定义不同验证状态下的控件样式。在组件的修饰符中把 styles 数组改写一下:

  styles: [`
    .ng-invalid{
      border: 3px solid red;
    }
    .ng-valid{
      border: 3px solid green;
    }
  `]

保存一下,返回浏览器可以看到,验证不通过时

c2_s2_form_validation_style_fail.png-8.9kB
c2_s2_form_validation_style_fail.png-8.9kB

验证通过时是这样的:

c2_s2_form_validation_style_pass.png-4.6kB
c2_s2_form_validation_style_pass.png-4.6kB

最后说一下,我们看到这样设置完样式后连 form 和 fieldset 都一起设置了,这是由于 form 和 fieldset 也在样式中应用了.ng-valid.ng-valid,那怎么解决呢?只需要在 .ng-valid 加上 input 即可,它表明的是应用于 input 类型控件并且 class 引用了 ng-invalid 的元素。

  styles: [`
    input.ng-invalid{
      border: 3px solid red;
    }
    input.ng-valid{
      border: 3px solid green;
    }
  `]

很多开发人员不太了解 CSS ,其实 CSS 还是比较简单的,我建议先从 Selector 开始看,Selector 的概念弄懂后 Angular 的开发 CSS 就会顺畅很多。具体可见 W3School 中对于 CSS Selctor的参考https://css-tricks.com/multiple-class-id-selectors/

本节代码: https://github.com/wpcfan/awesome-tutorials/tree/chap02/angular2/ng2-tut

进一步的练习

  • 练习1:如果我们想给username和password输入框设置默认值。比如“请输入用户名”和“请输入密码”,自己动手试一下吧。
  • 练习2:如果我们想在输入框聚焦时把默认文字清除掉,该怎么做?
  • 练习3:如果我们想把默认文字颜色设置成浅灰色该怎么做?

慕课网 Angular 视频课上线: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner

京东链接:https://item.m.jd.com/product/12059091.html?from=singlemessage&isappinstalled=0

Angular从零到一
Angular从零到一

第一节:初识Angular-CLI
第二节:登录组件的构建
第三节:建立一个待办事项应用
第四节:进化!模块化你的应用
第五节:多用户版本的待办事项应用
第六节:使用第三方样式库及模块优化用
第七节:给组件带来活力
Rx--隐藏在Angular 2.x中利剑
Redux你的Angular 2应用
第八节:查缺补漏大合集(上)
第九节:查缺补漏大合集(下)

推荐阅读更多精彩内容