天天看點

Angular2 從0到1 (二)用Form表單做一個登入控件

第一節:Angular2 從0到1 (一)

第三節:Angular2 從0到1 (三)

作者:王芃 [email protected]

用Form表單做一個登入控件

對于first元件的小改造

hello-angular\src\app\first\first.component.ts

中更改其模闆為下面的樣子

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

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

  constructor() { }

  ngOnInit() {
  }

}
           

我們增加了一個文本輸入框和一個按鈕,儲存後傳回浏覽器可以看到結果

Angular2 從0到1 (二)用Form表單做一個登入控件

接下來我們嘗試給Login按鈕添加一個處理方法

<button (click)="onClick()">Login</button>

(click)

表示我們要處理這個button的click事件,圓括号是說發生此事件時,調用等号後面的表達式或函數。等号後面的

onClick()

是我們自己定義在FirstComponent中的函數,這個名稱你可以随便定成什麼,不一定叫

onClick()

。下面我們就來定義這個函數,在FirstComponent中寫一個叫

onClick()

的方法,内容很簡單就是把“button was clicked”輸出到Console。

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

傳回浏覽器,并按F12調出開發者工具。當你點選Login時,會發現Console視窗輸出了我們期待的文字。

Angular2 從0到1 (二)用Form表單做一個登入控件

那麼如果要在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被輸出了。

Angular2 從0到1 (二)用Form表單做一個登入控件

好了,現在我們再加一個密碼輸入框,然後改寫onClick方法可以同時接收2個參數:使用者名和密碼。代碼如下:

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

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

  constructor() { }

  ngOnInit() {
  }

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

}
           

看看結果吧,在浏覽器中第一個輸入框輸入“wang”,第二個輸入框輸入“1234567”,觀察Console視窗,Bingo!

Angular2 從0到1 (二)用Form表單做一個登入控件

建立一個服務去完成業務邏輯

如果我們把登入的業務邏輯在onClick方法中完成,當然也可以,但是這樣做的耦合性太強了。設想一下,如果我們增加了微信登入、微網誌登入等,業務邏輯會越來越複雜,顯然我們需要把這個業務邏輯分離出去。那麼我們接下來建立一個AuthService吧, 首先我們在src\app下建立一個services的子檔案夾(

src\app\services

),然後指令行中輸入

ng g s services\auth

(s這裡是service的縮寫,services\auth是說在services的目錄下建立auth服務相關檔案)。

auth.service.ts

auth.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(依賴性注入)的時候,我們自然的想法是這樣的,在

first.component.ts

中import引入AuthService,在構造中初始化service,在onClick中調用service。

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

@Component({
  selector: 'app-first',
  template: `
    <div>
      <input #usernameRef type="text">
      <input #passwordRef type="password">
      <button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
    </div>
  `,
  styles: []
})
export class FirstComponent 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 '../services/auth.service';

@Component({
  selector: 'app-first',
  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 FirstComponent 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了。這樣定義好之後,我們就可以在任意元件中注入這個依賴了。下面我們改動一下

first.component.ts

,去掉頭部的

import { AuthService } from '../services/auth.service';

群組件修飾器中的providers,更改其構造函數為

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

我們去掉了service的類型聲明,但加了一個修飾符

@Inject('auth')

,這個修飾符的意思是請到系統配置中找到名稱為

auth

的那個依賴注入到我修飾的變量中。當然這樣改完後你會發現

Inject

這個修飾符系統不識别,我們需要在

@angular/core

中引用這個修飾符,現在

first.component.ts

看起來應該是下面這個樣子

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

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

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

  ngOnInit() {
  }

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

}
           

雙向資料綁定

接下來的問題是我們是否隻能通過這種方式進行表現層和邏輯之間的資料交換呢?如果我們希望在元件内對資料進行操作後再回報到界面怎麼處理呢?Angular2提供了一個雙向資料綁定的機制。這個機制是這樣的,在元件中提供成員資料變量,然後在模闆中引用這個資料變量。我們來改造一下

first.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方法。現在我們儲存檔案後打開浏覽器看一下,效果和上一節的應該一樣的。本節的完整代碼如下:

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

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

  username = "";
  password = "";

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

  ngOnInit() {
  }

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

}
           

表單資料的驗證

通常情況下,表單的資料是有一定的規則的,我們需要依照其規則對輸入的資料做驗證以及回報驗證結果。Angular2中對表單驗證有非常完善的支援,我們繼續上面的例子,在

first

元件中,我們定義了一個使用者名和密碼的輸入框,現在我們來為它們加上規則。首先我們定義一下規則,使用者名和密碼都是必須輸入的,也就是不能為空。更改

first.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,試着填寫一些字元在兩個輸入框中,看看狀态變化吧。

Angular2 從0到1 (二)用Form表單做一個登入控件

我們是知道了驗證的狀态是什麼,但是如果我們想知道驗證失敗的原因怎麼辦呢?我們隻需要将

{{usernameRef.valid}}

替換成

{{usernameRef.errors | json}}

|

是管道操作符,用于将前面的結果通過管道輸出成另一種格式,這裡就是把errors對象輸出成json格式的意思。看一下結果吧,傳回的結果如下

Angular2 從0到1 (二)用Form表單做一個登入控件

如果除了不能為空,我們為username再添加一個規則試試看呢,比如字元數不能少于3。

<input type="text"
        [(ngModel)]="username"
        #usernameRef="ngModel"
        required 
        minlength="3"
        />
           
Angular2 從0到1 (二)用Form表單做一個登入控件

現在我們試着把

{{表達式}}

替換成友好的錯誤提示,我們想在有錯誤發生時顯示錯誤的提示資訊。那麼我們來改造一下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.required

true

時顯示

div

标簽。那麼那個

?

是幹嘛的呢?因為

errors

可能是個null,如果這個時候調用

errors

required

屬性肯定會引發異常,那麼

?

就是标明

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

中包括了表單所有填寫項的值。

Angular2 從0到1 (二)用Form表單做一個登入控件

有時候在表單項過多時我們需要對表單項進行分組,HTML中提供了

fieldset

标簽用來處理。那麼我們看看怎麼和Angular2結合吧:

<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

對象中。

Angular2 從0到1 (二)用Form表單做一個登入控件

接下來我們改寫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));
  }
           

在浏覽器中試驗一下吧,所有功能正常工作。

驗證結果的樣式自定義

如果我們在開發工具中檢視網頁源碼,可以看到

Angular2 從0到1 (二)用Form表單做一個登入控件

使用者名控件的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: px solid red;
    }
    .ng-valid{
      border: px solid green;
    }
  `]
           

儲存一下,傳回浏覽器可以看到,驗證不通過時

Angular2 從0到1 (二)用Form表單做一個登入控件

驗證通過時是這樣的:

Angular2 從0到1 (二)用Form表單做一個登入控件

最後說一下,我們看到這樣設定完樣式後連form和fieldset都一起設定了,這是由于form和fieldset也在樣式中應用了

.ng-valid

.ng-valid

,那怎麼解決呢?隻需要在

.ng-valid

加上

input

即可,它表明的是應用于input控件。

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

很多開發人員不太了解CSS,其實CSS還是比較簡單的,我建議先從Selector開始看,Selector的概念弄懂後Angular2的開發CSS就會順暢很多。具體可見W3School中對于CSS Selctor的參考和https://css-tricks.com/multiple-class-id-selectors/。

第一節:Angular2 從0到1 (一)

第三節:Angular2 從0到1 (三)