Angular 2.x 從 0 到 1 (二)史上最簡單的 Angular2 教程

第一節:Angular 2.0 從0到1 (一)
第二節:Angular 2.0 從0到1 (二)
第三節:Angular 2.0 從0到1 (三)javascript

第二節:用Form表單作一個登陸控件

對於login組件的小改造

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

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
接下來咱們嘗試給Login按鈕添加一個處理方法 <button (click)="onClick()">Login</button>(click)表示咱們要處理這個button的click事件,圓括號是說發生此事件時,調用等號後面的表達式或函數。等號後面的onClick()是咱們本身定義在LoginComponent中的函數,這個名稱你能夠隨便定成什麼,不必定叫onClick()。下面咱們就來定義這個函數,在LoginComponent中寫一個叫onClick()的方法,內容很簡單就是把「button was clicked」輸出到Console。html

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

返回瀏覽器,並按F12調出開發者工具。當你點擊Login時,會發現Console窗口輸出了咱們期待的文字。
c2_s1_handle_click_method.png-141kB
那麼若是要在onClick中傳遞一個參數,好比是上面的文本輸入框輸入的值怎麼處理呢?咱們能夠在文本輸入框標籤內加一個#usernameRef,這個叫引用(reference)。注意這個引用是的input對象,咱們若是想傳遞input的值,能夠用usernameRef.value,而後就能夠把onClick()方法改爲onClick(usernameRef.value)java

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

在Component內部的onClick方法也要隨之改寫成一個接受username的方法git

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

如今咱們再看看結果是什麼樣子,在文本輸入框中鍵入「hello」,點擊Login按鈕,觀察Console窗口:hello被輸出了。
c2_s1_input_button_ref.png-141.1kB
好了,如今咱們再加一個密碼輸入框,而後改寫onClick方法能夠同時接收2個參數:用戶名和密碼。代碼以下:github

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.8kBjson

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

若是咱們把登陸的業務邏輯在onClick方法中完成,固然也能夠,可是這樣作的耦合性太強了。設想一下,若是咱們增長了微信登陸、微博登陸等,業務邏輯會愈來愈複雜,顯然咱們須要把這個業務邏輯分離出去。那麼咱們接下來建立一個AuthService吧, 首先咱們在srcapp下創建一個core的子文件夾(src\app\core),而後命令行中輸入 ng g s core\auth (s這裏是service的縮寫,coreauth是說在core的目錄下創建auth服務相關文件)。auth.service.tsauth.service.spec.ts這個兩個文件應該已經出如今你的目錄裏了。segmentfault

下面咱們爲這個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));
  }

}

雙向數據綁定

接下來的問題是咱們是否只能經過這種方式進行表現層和邏輯之間的數據交換呢?若是咱們但願在組件內對數據進行操做後再反饋到界面怎麼處理呢?Angular2提供了一個雙向數據綁定的機制。這個機制是這樣的,在組件中提供成員數據變量,而後在模板中引用這個數據變量。咱們來改造一下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));
  }

}

表單數據的驗證

一般狀況下,表單的數據是有必定的規則的,咱們須要依照其規則對輸入的數據作驗證以及反饋驗證結果。Angular2中對錶單驗證有很是完善的支持,咱們繼續上面的例子,在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

咱們是知道了驗證的狀態是什麼,可是若是咱們想知道驗證失敗的緣由怎麼辦呢?咱們只須要將{{usernameRef.valid}}替換成{{usernameRef.errors | json}}|是管道操做符,用於將前面的結果經過管道輸出成另外一種格式,這裏就是把errors對象輸出成json格式的意思。看一下結果吧,返回的結果以下
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
如今咱們試着把{{表達式}}替換成友好的錯誤提示,咱們想在有錯誤發生時顯示錯誤的提示信息。那麼咱們來改造一下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
有時候在表單項過多時咱們須要對錶單項進行分組,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對象中。
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
用戶名控件的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_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的概念弄懂後Angular2的開發CSS就會順暢不少。具體可見W3School中對於CSS Selctor的參考https://css-tricks.com/multip...

本節代碼: https://github.com/wpcfan/awe...

進一步的練習

  • 練習1:若是咱們想給username和password輸入框設置默認值。好比「請輸入用戶名」和「請輸入密碼」,本身動手試一下吧。

  • 練習2:若是咱們想在輸入框聚焦時把默認文字清除掉,該怎麼作?

  • 練習3:若是咱們想把默認文字顏色設置成淺灰色該怎麼作?

第一節:Angular 2.0 從0到1 (一)
第二節:Angular 2.0 從0到1 (二)
第三節:Angular 2.0 從0到1 (三)
第四節:Angular 2.0 從0到1 (四)
第五節:Angular 2.0 從0到1 (五)
第六節:Angular 2.0 從0到1 (六)
第七節:Angular 2.0 從0到1 (七)
第八節:Angular 2.0 從0到1 (八)

相關文章
相關標籤/搜索