Posted on Leave a comment

[功能介紹-20] HttpClient

使用HttpClientModule

官方文件:https://angular.io/api/common/http/HttpClientModule

下面是一個簡單的使用範例,假使我們要讀取這樣格式的JSON資料:
{
“results”: [
“Item 1”,
“Item 2”,
]
}

@Component(...)
export class MyComponent implements OnInit {

  results: string[];

  // Inject HttpClient into your component or service.
  constructor(private http: HttpClient) {}

  ngOnInit(): void {
    // Make the HTTP request:
    this.http.get('/api/items').subscribe(data => {
      // Read the result field from the JSON response.
      this.results = data['results'];
    });
  }
}

this.results即為一個含有["item1","item2"]的陣列

定義回傳格式

首先先建立一個要回傳的物件介面

interface ItemsResponse {
  results: string[];
}

在get後面設定要回傳時可取得的物件或介面

http.get<itemsResponse>('/api/items').subscribe(data => {
  // data is now an instance of type ItemsResponse, so you can do this:
  this.results = data.results;
});

取得完整的HTTP Response

很多時候我們也會需要Http header的資料,而不光是回傳的body,這時候可以更改observe的值為response

http
  .get<myJsonData>('/data.json', {observe: 'response'})
  .subscribe(resp => {
    // Here, resp is of type HttpResponse<myJsonData>.
    // You can inspect its headers:
    console.log(resp.headers.get('X-Custom-Header'));
    // And access the body directly, which is typed as MyJsonData as requested.
    console.log(resp.body.someField);
  });

處理錯誤訊息

可以在.subscribe()下面增加err函數來處理HTTP錯誤的狀況

http
  .get<itemsResponse>('/api/items')
  .subscribe(
    // Successful responses call the first callback.
    data => {...},
    // Errors will call this callback instead:
    err => {
      console.log('Something went wrong!');
    }
  );

err這個含數可以有傳入更詳細的HTTP錯誤訊息物件HttpErrorResponse

http
  .get<itemsResponse>('/api/items')
  .subscribe(
    data => {...},
    (err: HttpErrorResponse) => {
      if (err.error instanceof Error) {
        // A client-side or network error occurred. Handle it accordingly.
        console.log('An error occurred:', err.error.message);
      } else {
        // The backend returned an unsuccessful response code.
        // The response body may contain clues as to what went wrong,
        console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
      }
    }
  );

遇到錯誤時自動重試

RxJS有一個有用的運算符.retry(),可以在遇到錯誤時自動重新嘗試,使用範例如下:
先導入需要的類別

import 'rxjs/add/operator/retry';

使用的方式就是直接.retry()並傳入重試次數

http
  .get<itemsResponse>('/api/items')
  // Retry this request up to 3 times.
  .retry(3)
  // Any errors after the 3rd retry will fall through to the app.
  .subscribe(...);

接收非JSON格式的資料

傳入responseType可以設定預期會接收到的資料格式,angular預設值是json,如果是其他格式則需要設定這個欄位。
可接受的項目有『arraybuffer』、『blob』、『json』、『text』
下面是使用方式:

http
  .get('/textfile.txt', {responseType: 'text'})
  // The Observable returned by get() is of type Observable<string>
  // because a text response was specified. There's no need to pass
  // a <string> type parameter to get().
  .subscribe(data => console.log(data));

發送POST請求

下面是發送POST請求的範例:

const body = {name: 'Brad'};

http
  .post('/api/developers/add', body)
  // See below - subscribe() is still necessary when using post().
  .subscribe(...);

所有rxjs的動作都會在有人subscribe後才會呼叫,因此如果忽略subscribe(),http將不會做任何動作。

const req = http.post('/api/items/add', body);
// 0 requests made - .subscribe() not called.
req.subscribe();
// 1 request made.
req.subscribe();
// 2 requests made.

設置request的header

http
  .post('/api/items/add', body, {
    headers: new HttpHeaders().set('Authorization', 'my-auth-token'),
  })
  .subscribe();

HttpHeaders的內容是不變的,每次set()時會返回一個新的實體,並套用所設定的更改。

設置URL參數

如果想要發送請求至/api/items/add?id=3,可以用以下的寫法

http
  .post('/api/items/add', body, {
    params: new HttpParams().set('id', '3'),
  })
  .subscribe();
Posted on Leave a comment

[功能介紹-12] Angular裡Service的DI

何謂依賴注入

什麼是依賴注入呢?可以參考這篇文章:理解 Dependency Injection 實作原理

Service的DI

以下為一個service的範例

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

@Injectable()
export class HeroService {
  constructor() { }
}

@Injectable()是angular的service使用做依賴注入的裝飾詞,可以使Service成為可被注入的元件。
當我們在provider設定這個服務給這個module使用,如下:

providers: [
  UserService
],

這個service會變成是Singleton的,我們可以在許多的地方去從constructor直接宣告這個服務,在所有地方所取得的都會是同一個實體。

export class HeroListComponent {
  heroes: Hero[];

  constructor(heroService: HeroService) {
    this.heroes = heroService.getHeroes();
  }
}

component的provider

在ngModule裡provider設定的服務可以在該模組下任一元件裡去從建構子取得同一個物件的實體而不需要再在元件中額外去在provider宣告。
如果有一個服務只需要在某個元件下被使用,則可以直接在@Component.providers裡面註冊服務。
組件注入器是相互獨立的,每個組件都創建它自己的組件提供服務的實例。
當Angular銷毀其中一個組件實體時,它也會銷毀組件的注入器和注入器的服務實體

讓建立的類別與providers宣告的不同

providers: [Logger]

當angular在注入這個服務時,會用下面這行來取得實體

constructor(private logger: Logger) {  }

在這個例子中,Logger就是angular在做這個服務依賴注入的token,也可以寫成下面這樣。

providers: [{ provide: Logger, useClass: Logger }]

上面是token與實際物件實體類型相同的狀況。

但是有的時候,或許某一個元件寫好需注入的服務是Logger。
但因為在現有的模組中我們建立了一個繼承Logger並覆寫其部份功能的類別BetterLogger。
這時候可以用這個方式讓angular裡的token與實際產生的物件實體是不同的

providers: [{ provide: Logger, useClass: BetterLogger }]

Aliased class providers

以上面的例子,如果我們在根模組設定Logger為token,實際創建的物件為BetterLogger,會造成其他的子元件也都無法直接在constructor取得BetterLogger這類型的物件。
因此在這樣的狀況下,可以為這個服務取別名,下面這個是錯誤示範,因為事實上這樣的寫法會創建兩個不同的BetterLogger實體:

[ NewLogger,
  { provide: OldLogger, useClass: NewLogger}]

真正正確的寫法如下:

[ NewLogger,
  // 使用OldLogger會去取得NewLogger這個類別的實體
  { provide: OldLogger, useExisting: NewLogger}]

在provide提供現成的物件實體

有的時候我們會希望能夠直接使用一個已建立好的物件實體,可以用下面的方式:

// An object in the shape of the logger service
export function SilentLoggerFn() {}

const silentLogger = {
  logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
  log: SilentLoggerFn
};

下面的宣告方式,可以讓我們在元件下的建構子可以直接使用Logger來取得上面所建的物件實體

[{ provide: Logger, useValue: silentLogger }]

在provide服務時使用工廠模式

如果我們希望針對客戶的層級不同提供不同的服務,可以使用Factory providers
下面是src/app/heroes/hero.service.ts這個檔案的截錄內容

constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }

getHeroes() {
  let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

hero.service.provider.ts裡定義了factory的物件

let heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

然後在providers的宣告是這樣的

export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };

useFactory可讓angular知道provider是一個工廠函數,其實現是heroServiceFactory。
而deps屬性是這個工廠需要用到的服務。angular會將服務中的Logger和UserService注入符合的Factory function參數。

在注入一串文字做為服務

如果有時我們要注入的對象不是物件時,例如要注入下面這個常數

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

我們不能使用下面這個方式來注入

[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]

這是因為javascript是一個弱型別的語言,即便typescript是強型別的,但最終會被轉回弱型別的javascript。
因此用這種方式angular會無法找到這個型別。

正確的使用方式如下:

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

export const APP_CONFIG = new InjectionToken<appConfig>('app.config');

然後用剛剛建的InjectionToken來註冊這個程序

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]

使用這串字串的方式如下:

constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

不用一定要被傳入的服務

有些service可以不一定要被建立,這時可以用這個方式宣告

import { Optional } from '@angular/core';
constructor(@Optional() private logger: Logger) {
  if (this.logger) {
    this.logger.log(some_message);
  }
}

如果使用@Optional(),並且找不到相應的服務時,就不會跳錯誤,而logger的值會是null。

分層注入系統

angular的服務是這樣的,每一次在providers裡註冊一次這個類型,就會產生一次物件實體。
我們可以利用這個特性來讓服務裡的資訊彼此間隔離不受干擾

例如下圖的HeroesListComponent下面有三個HeroTaxReturnComponent實體,裡面各字會有一個這個Hero的稅單資料(HeroTaxReturnService)

但若我們不希望彼此間的服務會受到彼此的干擾,則可以在HeroTaxReturnComponent再宣告一次HeroTaxReturnService,則可以達到這個目的

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { HeroTaxReturn }        from './hero';
import { HeroTaxReturnService } from './hero-tax-return.service';

@Component({
  selector: 'app-hero-tax-return',
  templateUrl: './hero-tax-return.component.html',
  styleUrls: [ './hero-tax-return.component.css' ],
  providers: [ HeroTaxReturnService ]
})
export class HeroTaxReturnComponent {
  message = '';
  @Output() close = new EventEmitter<void>();

  get taxReturn(): HeroTaxReturn {
    return this.heroTaxReturnService.taxReturn;
  }
  @Input()
  set taxReturn (htr: HeroTaxReturn) {
    this.heroTaxReturnService.taxReturn = htr;
  }

  constructor(private heroTaxReturnService: HeroTaxReturnService ) { }

  onCanceled()  {
    this.flashMessage('Canceled');
    this.heroTaxReturnService.restoreTaxReturn();
  };

  onClose()  { this.close.emit(); };

  onSaved() {
    this.flashMessage('Saved');
    this.heroTaxReturnService.saveTaxReturn();
  }

  flashMessage(msg: string) {
    this.message = msg;
    setTimeout(() => this.message = '', 500);
  }
}
Posted on Leave a comment

[功能介紹-11] NgModules

Angular modularity

Angular有提供許多的功能,如FormsModule、HttpModule、RouterModule,都是NgModules。一些第三方資源提供者也有提供許多NgModules可使用,如Material Design、Ionic、AngularFire2等…。

NgModules可將一群功能性一致的components、directives和pipes組合在一起,讓外部可以直接引用並使用這個模組。例如FormsModule裡面會提供許多和表單驗證、表單資料繫結等的功能在裡面。

在要開發一個APP時,我們可以透過@NgModule的metadata去設定這一個APP會使用到的功能模組,設定的樣子如下:
(以src/app/app.module.ts為例)

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
  imports:      [ BrowserModule ],
  providers:    [ Logger ],
  declarations: [ AppComponent ],
  exports:      [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

NgModule的metadata有下面幾項:

  • imports:這個模組所需用到的Angular提供的或第三方提供的Angular資源庫(如FormsModule、HttpModule等)。
  • providers:一些供這個模組使用的service,在此宣告後所有下面的元件都可以直接使用這個服務。
  • declarations:這個Module內部Components/Directives/Pipes的列表,聲明這個Module的內部成員
  • exports:用來控制將哪些內部成員暴露給外部使用。
  • bootstrap:這個屬性只有根模組需要設定,在此設定在一開始要進入的模組成員是那一個。

  • 關於這整個Angular NgModule的介紹也可參考系列文的:[功能介紹-8] Angular架構,會有更概觀的介紹。

    Bootstrapping

    每一個專案都一定會有一個根模組,也就是root module,我們會在main.ts去做Bootstrap這個根模組的動作,讓整個APP可以運行起來。

    import { enableProdMode } from '@angular/core';
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    
    import { AppModule } from './app/app.module';
    import { environment } from './environments/environment';
    
    if (environment.production) {
      enableProdMode();
    }
    
    platformBrowserDynamic().bootstrapModule(AppModule);//用這一串文字來運行根模組AppModule

    在bootstrap的動作裡,會建立好執行環境並把在src/app/app.module.ts裡設定的bootstrap陣列裡的元素取出來並透過在該成員裡設定的selector,讓我們可以在src/index.html來顯示這個元件的VIEW。

    <body>
      <app-root></app-root>
    </body>

    為APP增加一個自製的directive

    下面是一個highlight的範例的directive
    src/app/highlight.directive.ts

    import { Directive, ElementRef } from '@angular/core';
    
    // 將這個區塊的背景顏色改為金色
    @Directive({ selector: '[highlight]' })
    export class HighlightDirective {
      constructor(el: ElementRef) {
        el.nativeElement.style.backgroundColor = 'gold';
      }
    }

    如果想使用這個自定的directive,可以在src/app/app.module.ts的declarations區塊裡增加這一個Directive

    declarations: [
      AppComponent,
      HighlightDirective,//在這邊加上去,這樣整個APP都可以使用這個功能。如<h1 highlight>{{title}}</h1>
    ],

    使用CLI加速開發速度

    這邊是Angular CLI的官網:https://cli.angular.io/
    這邊有很詳細的Angular CLI的教學:https://dotblogs.com.tw/wellwind/2016/09/30/angular2-angular-cli
    直接使用ng –help也可以看到CLI的詳細說明

    如果我們使用CLI來創建這個component, directive或pipe,CLI會自動將這個directive宣告在app.module.ts的metadata裡
    ng generate directive highlight
    可以讓我們在開發上更為加速

    下圖是ng generate可以帶的參數

    Service providers

    一個service可以注入至組件(@NgModule.providers)或者是元件(@Component.providers)。
    如果NgModule與Component同時有分別設定相同的類別,會產生兩個不同的實體,在該元件內則會優先注入使用元件裡自己設定的服務。
    一般而言,如果這個服務被廣泛的使用在很多的元件裡,我們會將providers宣告在根模組上。但如果這個服務只有這個元件在使用,則會放在Component的providers去宣告。

    下面的CLI指令可以創立一個user服務並加到模組裡使用
    ng generate service user –module=app
    可以看到app.module.ts的providers會增加一個名為UserService的類別

    providers: [ UserService ],

    在使用上,如果已經有設定好providers後,只要在元件的constructor裡面宣告一個變數是providers裡面設定好的service,就可以在元件裡直接取用了

     constructor(userService: UserService) {
        this.user = userService.userName;
      }

    其依賴注入的概念大概是這樣

    所有在providers裡面被宣告的物件實體會被存在angular的服務列表裡,因此在下面不同的元件裡可以直接取用到相同的服務實體。

    NgModule imports

    如果要使用ngIf這個directive,我們需要在app.module.ts導入BrowserModule

    imports: [ BrowserModule ],

    我們import了BrowserModule這個模組,就可以使用BrowserModule裡面有在ngModule裡去用exports去設定要曝露出來的directive、component、pipe等。

    這樣就可以在所有的元件中使用像下面這樣的模板語法

    <p *ngIf="user">
      <i>Welcome, {{user}}</i>
    <p>

    Re-exported NgModules

    其實上面的例子裡,ngIf是在CommonModule之下的,但是我們import了BrowserModule,我們可以看看官網對於BrowserModule的介紹

    由於BrowserModule在exports的地方將自己所import的CommonModule的功能再export出去,所以當我們import了BrowserModule後,也可以使用CommonModule所有export的功能,這個就是Re-exported。

    使用ModuleWithProviders

    如果我們有一個子元件,而我們希望那個子元件能夠如同核心元件一般,比所有其他子元件都更優先被載入(就像*ngIf)。
    或者是希望能夠在被所有元件初始化之前,優先設定好裡面的值時,可以自行將元件包成ModuleWithProviders然後在appModule用import的方式來import我們自己寫的核心元件。

    在Angular最廣泛使用這個功能的是Routing,我們必需先傳入一個導航設定的資料來設定Routing。
    我們也可以在自己所自製的模組裡使用這個功能。

    假如我們有一個服務是這樣的

    import { Injectable } from '@angular/core';
    
    @Injectable()
    export class UserService {
      userName = 'Sherlock Holmes';
    }

    我們希望能夠讓模組在使用這個服務前,先設定一個新的userName再引入使用。
    首先,要在剛剛的服務裡加上這些程式碼

    constructor(@Optional() config: UserServiceConfig) {
      if (config) { this._userName = config.userName; }
    }

    然後在CoreModule裡加上forRoot方法,這個forRoot的方法接受一個service class,並回傳一個ModuleWithProviders物件

    static forRoot(config: UserServiceConfig): ModuleWithProviders {
      return {
        ngModule: CoreModule,
        providers: [
          {provide: UserServiceConfig, useValue: config }
        ]
      };
    }

    最後在imports列表中呼叫它:

    imports: [
      BrowserModule,
      CoreModule.forRoot({userName: 'Miss Marple'}),
      AppRoutingModule
    ],

    這時,我們會看到userName是“Miss Marple”,而不是“Sherlock Holmes”。

    注:只在應用的根模塊AppModule中調用forRoot。如果在其它模塊(特別是惰性加載模塊)中調用它則違反了設計意圖,並會導致運行時錯誤。

    若我們想要防止有人重覆import這個CoreModule,可以用下面的方法

    constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
      if (parentModule) {
        throw new Error(
          'CoreModule is already loaded. Import it in the AppModule only');
      }
    }
    Posted on Leave a comment

    [功能介紹-10] Reactive Forms (Model-Driven Forms) 

    Template-Driven Forms與Model-Driven Forms的比較

    Reactive forms

    Reactive forms的驗證大多是直接寫在controller裡的,會是一個明確的、非UI的data flowing。
    Reactive forms的reactive patterns可以讓測試與驗證更加簡單。

    使用Reactive forms可以用一個樹狀的控制物件來binding到表單template的元件上,這讓所有驗證的程式碼都集中在一起,方便維護與管理,在撰寫單元測試時也會較為容易。

    使用Model-Driven Forms也較符合reactive programming的概念(延伸閱讀:Functional Reactive Programming 的入門心得

    Template-driven forms

    Template-driven forms是將組件驗證控制的功能寫在像是<input>或<select>的標籤內,並利用ngModel來確認是否輸入了合法的內容。
    使用表單驅動驗證不需要自己創建control objects,因為angular已經為我們建好了。
    ngModel會處理使用者改變與輸入表單的事件,並更新ngModel裡面的可變數據,讓我們可以去處理後續的事。
    也因此ngModel並不是ReactiveFormsModule的一部份。

    這代表著使用表單驅動驗證,我們需要撰寫的程式碼更少。
    但是如果我們的表單需要很複雜的驗證步驟並且要顯示很多不同的錯誤訊息時,使用表單驅動驗證會使事情變得更複雜並難以維護。

    最大的差異,同步與非同步

    Reactive forms是同步的而Template-driven forms為非同步處理,是這兩者間最大的差異。

    對Reactive forms來說,所有表單的資料是在code裡以tree的方式來呈現,所以在任一個節點可以取得其他表單的資料,並且這些資料是即時同步被更新的。我們也可以在使用者修改了某個input的值時,去為使用者自動update另一個input內的預設值,這是因為所有資料都是隨時可取得的。

    Template-driven forms在每一個表單元件各自透過directive委派檢查的功能,為了避免檢查後修改而造成檢查失效的問題,directive會在更多的時後去檢查輸入的值的正確性,因此並沒有辦法立即的得到回應,而需要一小段的時間才有辦法得到使用者輸入的值是否合法的回應。這會讓我們在撰寫單元測試時更加複雜,我們會需要利用setTimeout去讓取得的檢查結果是正確的

    使用Reactive Forms

    Reactive Forms的功能封裝在ReactiveFormsModule中,和FormsModule同樣在@angular/forms之下。
    如果要使用Reactive Forms需要使用下面的程式碼

    import { ReactiveFormsModule } from '@angular/forms';

    Reactive Forms的四個重要成員

    • AbstractControl:AbstractControl是FormControl、FormGroup、FormArray這三個實例表單類的抽象基類。它提供了他們的通用行為以及屬性,例如observable。
    • FormControl:在單個表單元件中檢查值並驗證狀態(比如input、select等等)。
    • FormGroup一組值與驗證狀態(FormControl),其屬性包含了它們的子控件。例如一個form表單就是一個FormGroup。
    • FormArray:用索引的方式去追蹤檢查表單的驗證狀態。

    Add a FormGroup

    首先要新增FormGroup所需使用的類別

    import { Component }              from '@angular/core';
    import { FormControl, FormGroup } from '@angular/forms';

    然後創建這個Group,並定義裡面的驗證元素

    export class HeroDetailComponent2 {
      heroForm = new FormGroup ({
        name: new FormControl()
      });
    }

    接著,在template裡面的form裡指定這個form要使用heroForm來做表單驗證,並且在input裡面指定他的formControlName

    <h2>Hero Detail</h2>
    <h3><i>FormControl in a FormGroup</i></h3>
    <form &#91;formGroup&#93;="heroForm" novalidate>
      <div class="form-group">
        <label class="center-block">Name:
          <input class="form-control" formControlName="name">
        </label>
      </div>
    </form>

    form標籤下的novalidate屬性,是為了要防止瀏覽器自己執行native的驗證。

    <form &#91;formGroup&#93;="heroForm" novalidate>

    [formGroup]=”heroForm”則是將template內的form元件與controller裡所創的formGroup做關連

    <input class="form-control" formControlName="name">

    這個則是將input與formGroup下名為name的formControl做關連。

    註:bootstrap的form-group以及form-control與angular完全無關。下面是bootstrap為我們設計的form表單樣式範例,但是這只是css,沒辦法讓組件與控制器結合。

    <form>
      <div class="form-group">
        <label for="formGroupExampleInput">Example label</label>
        <input type="text" class="form-control" id="formGroupExampleInput" placeholder="Example input">
      </div>
      <div class="form-group">
        <label for="formGroupExampleInput2">Another label</label>
        <input type="text" class="form-control" id="formGroupExampleInput2" placeholder="Another input">
      </div>
    </form>

    Introduction to FormBuilder

    FormBuilder可以減少我們在創建formGroup時有太多重覆的定義,要使用要先import必要的檔案

    import { Component }              from '@angular/core';
    import { FormBuilder, FormGroup } from '@angular/forms';

    使用formBuild大致要做的事如下:

    • 宣告heroForm為FormGroup
    • 在初始化元件時inject FormBuilder
    • 創建form控件時需要另外去呼叫函數createForm,使用注入的FormBuilder來創建formControl
    • 在創建formGroup時,用this.fb.group來宣告這個formGroup裡所有的formControl。
    export class HeroDetailComponent3 {
      heroForm: FormGroup; // <--- 宣告heroForm為FormGroup
    
      constructor(private fb: FormBuilder) { // <--- 注入FormBuilder
        this.createForm();
      }
    
      createForm() {
        this.heroForm = this.fb.group({
          name: '', // <--- 建立一個名為name,預設值為''的formControl
        });
      }
    }&#91;/code&#93;
    formBuild的宣告方式如上,name控件由其初始數據值(一個空字符串)定義。
    <h3>使用驗證器</h3>
    首先要先import該驗證器
    [code lang="js"]import { Component }                          from '@angular/core';
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';

    然後在建立formControl時指定使用該驗證器

    this.heroForm = this.fb.group({
      name: ['', Validators.required ],
    });

    Nested FormGroups

    有時我們在做地址輸入框時,會有如國家、區、鄉、市、街、郵遞區號等不同的輸入欄位,但他們應該是一個group,這時候就可以用nested formGroup。透過這樣的結構層次,可以讓我們在追蹤表格狀態更為容易清楚。

    export class HeroDetailComponent5 {
      heroForm: FormGroup;
      states = states;
    
      constructor(private fb: FormBuilder) {
        this.createForm();
      }
    
      createForm() {
        this.heroForm = this.fb.group({ // <-- the parent FormGroup
          name: &#91;'', Validators.required &#93;,
          address: this.fb.group({ // <-- the child FormGroup
            street: '',
            city: '',
            state: '',
            zip: ''
          }),
          power: '',
          sidekick: ''
        });
      }
    }&#91;/code&#93;
    我們用一個div將整個地址的區塊包起來,並用formGroupName="address"來與heroForm裡的address做連結
    &#91;code lang="html"&#93;<div formGroupName="address" class="well well-lg">
      <h4>Secret Lair</h4>
      <div class="form-group">
        <label class="center-block">Street:
          <input class="form-control" formControlName="street">
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">City:
          <input class="form-control" formControlName="city">
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">State:
          <select class="form-control" formControlName="state">
            <option *ngFor="let state of states" &#91;value&#93;="state">{{state}}</option>
          </select>
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">Zip Code:
          <input class="form-control" formControlName="zip">
        </label>
      </div>
    </div>

    檢視FormControl裡的屬性

    我們可以用下面的方式來將formControl裡可使用的屬性都印出來

    <p>Form value: {{ heroForm.value | json }}</p>

    基本上會有下面四個屬性可以讓我們使用

    屬性 描述
    myControl.value FormControl使用者輸入的值
    myControl.status 這個FormControl的驗證結果. 可能的值有: VALIDINVALIDPENDING, or DISABLED.
    myControl.pristine 使用者是否有在UI上更動過這個元素,假如沒有的話會是true。相反的屬性為myControl.dirty.
    myControl.untouched 使用者尚未輸入並且從未觸發過blur event時為true。相反的屬性為 myControl.touched.

    The data model and the form model

    以往我們在創建資料類型時是像這樣子的

    export class Hero {
      id = 0;
      name = '';
      addresses: Address[];
    }
    
    export class Address {
      street = '';
      city   = '';
      state  = '';
      zip    = '';
    }

    但是我們在創建formGroup是這樣子的

    this.heroForm = this.fb.group({
      name: ['', Validators.required ],
      address: this.fb.group({
        street: '',
        city: '',
        state: '',
        zip: ''
      }),
      power: '',
      sidekick: ''
    });

    我們可以直接利用class來創建formControl

    this.heroForm = this.fb.group({
      name: ['', Validators.required ],
      address: this.fb.group(new Address()), // <-- a FormGroup with a new address
      power: '',
      sidekick: ''
    });&#91;/code&#93;
    <h3>使用setValue和patchValue為表單填入初始值</h3>
    在上面data model和form model的介紹範例中,可以看到Hero與formGroup建立heroForm模型有兩個顯著的區別:
    <ul>
    	<li>Hero class有id,formGroup沒有。</li>
    	<li>Hero class的地址是一個陣列</li>
    </ul>
    但是我們可以利用setValue來更簡單的將一個class的資料填進表單中。
    [code lang="js"]this.heroForm.setValue({
      name:    this.hero.name,
      address: this.hero.addresses[0] || new Address()//這是因為form元件只能顯示一個地址,如果class內容沒有值時,要預設新建立一個Address物件
    });

    也可以使用patchValue來將單一的值填入表單裡

    this.heroForm.patchValue({
      name: this.hero.name
    });

    如果我們要做一個修改hero資料的列表,當點下某個hero時就可以修改該hero的資料

    <nav>
      <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
    </nav>
    
    <div *ngIf="selectedHero">
      <app-hero-detail &#91;hero&#93;="selectedHero"></app-hero-detail>
    </div>

    然後在controller裡面去監聽ngOnChange事件並且用setValue來設定要修改的值

    ngOnChanges() {
      this.heroForm.reset({
        name: this.hero.name,
        address: this.hero.addresses[0] || new Address()
      });
    }

    會需要使用reset是為了要清除前一個hero的資料

    使用FormArray來呈現一個FormGroups

    如果一個hero可能需要有多組的地址時,就會需要使用formArray。
    原本我們是這樣定義Address的

    this.heroForm = this.fb.group({
      name: ['', Validators.required ],
      address: this.fb.group(new Address()), // <-- a FormGroup with a new address
      power: '',
      sidekick: ''
    });&#91;/code&#93;
    使用formArray則變成這樣
    &#91;code lang="js"&#93;this.heroForm = this.fb.group({
      name: &#91;'', Validators.required &#93;,
      secretLairs: this.fb.array(&#91;&#93;), // <-- secretLairs as an empty FormArray
      power: '',
      sidekick: ''
    });&#91;/code&#93;
    可以用下面的function將很多組的address設定進去formArray成為預設值
    &#91;code lang="js"&#93;setAddresses(addresses: Address&#91;&#93;) {
      const addressFGs = addresses.map(address => this.fb.group(address));
      const addressFormArray = this.fb.array(addressFGs);
      this.heroForm.setControl('secretLairs', addressFormArray);
    }

    要取得formArray可以撰寫下面的方法

    get secretLairs(): FormArray {
      return this.heroForm.get('secretLairs') as FormArray;
    };

    而顯示方式如下:

    <div formArrayName="secretLairs" class="well well-lg">
      <div *ngFor="let address of secretLairs.controls; let i=index" &#91;formGroupName&#93;="i" >
        <!-- The repeated address template -->
      </div>
    </div>

    完整內容如下:

    <div formArrayName="secretLairs" class="well well-lg">
      <div *ngFor="let address of secretLairs.controls; let i=index" &#91;formGroupName&#93;="i" >
        <!-- The repeated address template -->
        <h4>Address #{{i + 1}}</h4>
        <div style="margin-left: 1em;">
          <div class="form-group">
            <label class="center-block">Street:
              <input class="form-control" formControlName="street">
            </label>
          </div>
          <div class="form-group">
            <label class="center-block">City:
              <input class="form-control" formControlName="city">
            </label>
          </div>
          <div class="form-group">
            <label class="center-block">State:
              <select class="form-control" formControlName="state">
                <option *ngFor="let state of states" &#91;value&#93;="state">{{state}}</option>
              </select>
            </label>
          </div>
          <div class="form-group">
            <label class="center-block">Zip Code:
              <input class="form-control" formControlName="zip">
            </label>
          </div>
        </div>
        <br />
        <!-- End of the repeated address template -->
      </div>
    </div>

    要為這個hero新增一個地址可以用下面這個方法

    addLair() {
      this.secretLairs.push(this.fb.group(new Address()));
    }

    按下增加地址按鈕時呼叫這個方法

    <button (click)="addLair()" type="button">Add a Secret Lair</button>

    將formControl的資料用深層複製存回class裡的方法
    prepareSaveHero(): Hero {
    const formModel = this.heroForm.value;

    // deep copy of form model lairs
    const secretLairsDeepCopy: Address[] = formModel.secretLairs.map(
    (address: Address) => Object.assign({}, address)
    );

    // return new `Hero` object containing a combination of original hero value(s)
    // and deep copies of changed form model values
    const saveHero: Hero = {
    id: this.hero.id,
    name: formModel.name as string,
    // addresses: formModel.secretLairs // <-- bad! addresses: secretLairsDeepCopy }; return saveHero; }[/code]

    參考資料

    Posted on Leave a comment

    [功能介紹-9] Template-Driven Forms

    使用Event由template表單傳送資料給Component

    利用事件的$event去傳送相關資訊給component

    <input (keyup)="onKey($event)">
      <p>{{values}}</p>

    接收則可以透過event.target去存取該htmlInputElement的資料(詳細資料請見此
    下面的範例能將value存至一個變數內且顯示在頁面上:

    export class KeyUpComponent_v1 {
      values = '';
    
      onKey(event: any) { // without type info
        this.values += event.target.value + ' | ';
      }
    }

    模版驅動的表單

    使用模版驅動的表單需要在app.module.ts裡面宣告我們要使用FormsModule這個library,宣告方式如下

    import { NgModule }      from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { FormsModule }   from '@angular/forms';//使用模版表單驅動需要import這個模組
    
    import { AppComponent }  from './app.component';
    import { HeroFormComponent } from './hero-form/hero-form.component';
    
    @NgModule({
      imports: [
        BrowserModule,
        FormsModule//在@ngModule裡去加上這個模組,這使應用程序可以訪問所有模板驅動的表單功能,包括。@NgModule、ngModel
      ],
      declarations: [
        AppComponent,
        HeroFormComponent
      ],
      providers: [],
      bootstrap: [ AppComponent ]
    })
    export class AppModule { }

    使用ngModel對input的值與controller裡的變數做雙向繫結

    下面這段code最主要要看的是[(ngModel)]="model.name",用這個標籤會自動將使用者輸入此input的值雙向綁定到model.name上面。

    <input type="text" class="form-control" id="name"
           required
           &#91;(ngModel)&#93;="model.name" name="name">
    你所輸入的資料是: {{model.name}}
    

    然後在component也要有model這個屬性才能夠被綁定

    export class HeroFormComponent {
      model ={};
    }

    成果如下:

    ngModel會附加的class

    狀態(State ) Class if true Class if false
    被點擊接觸過 ng-touched ng-untouched
    值被改變 ng-dirty ng-pristine
    值不符合驗證 ng-valid ng-invalid

    可以用下面的範例來觀察input元素class名稱的改變(#spy代表將這個input存到一個叫做spy的變數內)

    <input type="text" class="form-control" id="name"
      required
      &#91;(ngModel)&#93;="model.name" name="name"
      #spy>
    <br />TODO: remove this:  {{spy.className}}


    下面是類別改變的狀態表

    Show and hide validation error messages

    在做表單驗證時,時常會有如下圖這樣的需求

    若要使用表單驗證,需要在input宣告ngModel(不論有沒有做雙向綁定),或者宣告formControlName或formControl,不然無法驗證。
    下面是一個最簡單的required驗證宣告:

    <input name="fullName" ngModel required>

    如果要用客製化的訊息去告訴使用者那邊輸入錯誤的話,則可以將ngModel的值輸出成一個模版變數#name

    <label for="name">Name</label>
    <input type="text" class="form-control" id="name"
           required
           &#91;(ngModel)&#93;="model.name" name="name"
           #name="ngModel">

    在上面我們看到#name="ngModel",這是因為ngModel這個directive的exportAs的值剛好就是ngModel。
    這代表將input的ngModel存進一個叫做name的變數裡,下面就可以使用這個ngModel的模型顯示相關的錯誤訊息。

    <div &#91;hidden&#93;="name.valid || name.pristine"
         class="alert alert-danger">
      Name is required
    </div>

    pristine的意思是未被改變過的,而name.valid則是檢查所有我們所設定的驗證器,回傳是否合法或者不合法。
    因此[hidden]="name.valid || name.pristine"的意思就是如果尚未填到該選項或者填入的值是合法的,則不顯示錯誤訊息。

    現成的forms Validator

    在上面的例子中,我們在input中下了required來做必填欄位的驗證。
    required是一個directive,我們可以在這邊看到這個directive的詳細使用說明。
    在說明網頁中,有幾個是需要去看的,首先就是selector,代表我們要如何去使用這個驗證器。
    以required為例,有三種選擇required的方法如下圖:

    這個圖代表了required不能用在checkbox,要使用這個directive要在input上加上required以及ngModel(或者formControl, formControlName)屬性。

    其他Angular有提供的驗證器如下

    客製化模板驅動表格驗證器

    除了上述的內建驗證器之外,我們也可以自己製作自製的驗證器directive

    @Directive({
      selector: '[forbiddenName]',
      providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
    })
    export class ForbiddenValidatorDirective implements Validator {
      @Input() forbiddenName: string;
    
      validate(control: AbstractControl): {[key: string]: any} {
        return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
                                  : null;
      }
    }

    在使用上的部份,就可以直接像下面這樣

    <input id="name" name="name" class="form-control"
                   required minlength="2" forbiddenName="bob"
                   &#91;(ngModel)&#93;="hero.name" #name="ngModel" >
    <div *ngIf="name.errors.forbiddenName">
                Name cannot be Bob.
              </div>

    要注意的是,官網範例forbidden-name.directive.ts的selector名稱寫錯,不能使用appForbiddenName,要用forbiddenName,才能對的起來
    這邊有ISSUE回報: Custom Validator example on angular.io directive selector issue.
    詳情請見線上範例:live example

    使用ngSubmit送出表單

    在ngForm裡面,submit的button按下後並不會直接發生任何事,而是會觸發一個ngSubmit的事件,因此需要在form的地方去註冊ngSubmit事件

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

    在上面的程式碼裡,我們將這個表單ngForm存至heroForm這個變數裡,因此在submit的按鈕可以存取ngForm裡的值來判段這個表單是否已通過驗證

    <button type="submit" class="btn btn-success" &#91;disabled&#93;="!heroForm.form.valid">Submit</button>

    參考資料

    Posted on Leave a comment

    [功能介紹-8] Pipes

    Pipes有那些?

    常用的Pipes有DatePipeUpperCasePipeLowerCasePipeCurrencyPipe,和PercentPipe。它們都可用於任何模板。

    下面是angular內建所有的的Pipes說明:

    如何使用Pipes

    管道將數據作為輸入並將其轉換為所需的輸出。下面是使用DatePipe的範例。

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-hero-birthday',
      template: `<p>The hero's birthday is {{ birthday | date }}</p>`
    })
    export class HeroBirthdayComponent {
      birthday = new Date(1988, 4, 15); // April 15, 1988
    }

    在Pipes裡使用參數

    請見下面的範例

    <p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>
    <p>The hero's birthday is {{ birthday | date:"shortDate" }} </p>
    <p>The hero's birthday is {{ birthday | date:"fullDate" }} </p>

    這樣會顯示結果如下:
    MM/dd/yy:04/15/88
    shortDate:04/15/1988
    fullDate:Friday, April 15, 1988

    串接多個管道

    下面為一個例子

    {{ birthday | date:'fullDate' | uppercase}}

    結果會顯示
    FRIDAY, APRIL 15, 1988

    定義自己的Pipes

    下面是一個自製Pipes的例子:

    import { Pipe, PipeTransform } from '@angular/core';
    /*
     * Raise the value exponentially
     * Takes an exponent argument that defaults to 1.
     * Usage:
     *   value | exponentialStrength:exponent
     * Example:
     *   {{ 2 | exponentialStrength:10 }}
     *   formats to: 1024
    */
    @Pipe({name: 'exponentialStrength'})
    export class ExponentialStrengthPipe implements PipeTransform {
      transform(value: number, exponent: string): number {
        let exp = parseFloat(exponent);
        return Math.pow(value, isNaN(exp) ? 1 : exp);
      }
    }

    在上面的例子中可以看到

    • 會以@Pipe({name:’XXXX’})來宣告這個class是一個pipe
    • pipe類別需implements PipeTransform介面並依照要input的值來選擇要實作的transform方法
    • transform有一個可選參數exponent,可讓使用者帶要帶入的參數進Pipes
    • Pipe的名字需要是一個合法的Javascript命名

    下面是一個使用範例

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-power-booster',
      template: `
        <h2>Power Booster</h2>
        <p>Super power boost: {{2 | exponentialStrength: 10}}</p>
      `
    })
    export class PowerBoosterComponent { }

    顯示的結果如下:

    在每次被綁定的值更動時,都會再跑一次Pipes的功能,一般來說,Pipe只會偵測值的變化才會執行pure Pipes。如對象是String、Number、Boolean、Symbol、Date、Array」Function、Object。但如果裡面是一個物件,則pure Pipes會忽略他的更動。
    這是因為效能的考量,若為純粹物件的值的更動在偵測上較快,但是在物件上屬性的更改的偵測效能會較差。會建議改使用元件的方式去偵測改變。

    但angular還是提供了impure pipes的方式可以偵測物件的改變,但使用上要小心不能因此而拖慢系統速度。
    它看起來會是像這樣:

    @Pipe({
      name: 'flyingHeroesImpure',
      pure: false
    })
    export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}
    Posted on Leave a comment

    [功能介紹-7] Structural Directives

    什麼是結構指令

    結構指令負責HTML佈局。他們通常通過添加,刪除或操縱元素來塑造或重塑DOM的結構。結構指令很容易識別。通常會有星號(*)在指令屬性名稱前面。

    以下是三種常件的結構指令的範例

    <div *ngIf="hero" >{{hero.name}}</div>
    
    <ul>
      <li *ngFor="let hero of heroes">{{hero.name}}</li>
    </ul>
    <!--hero?.emotion指的是hero.emotion,?代表如果hero為空時不會因此引發null reference exception-->
    <div &#91;ngSwitch&#93;="hero?.emotion">
      <app-happy-hero    *ngSwitchCase="'happy'"    &#91;hero&#93;="hero"></app-happy-hero>
      <app-sad-hero      *ngSwitchCase="'sad'"      &#91;hero&#93;="hero"></app-sad-hero>
      <app-confused-hero *ngSwitchCase="'app-confused'" &#91;hero&#93;="hero"></app-confused-hero>
      <app-unknown-hero  *ngSwitchDefault           &#91;hero&#93;="hero"></app-unknown-hero>
    </div>

    一個attribute directive改變了元素的外觀或行為。例如,內置的NgStyle指令同時改變幾個元素樣式。
    而Structural Directives則是會自動將裡面的內容儲存成一個ng-template並且操縱它,這也是為什麼這些指令前面會有一個星號(*)。
    其實這個星號會悄悄的讓這個directive成為structure的directive
    例如

    <div *ngIf="hero" >{{hero.name}}</div>

    其實等同於

    <ng-template &#91;ngIf&#93;="hero">
      <div>{{hero.name}}</div>
    </ng-template>

    我們可以觀察到

    • *號將該ngIf改為一個屬性綁定的元素,並綁定一個ng-template。*ngIf <ng-template> [ngIf]
    • 剩下的部分<div>,包括它的class屬性,移到了<ng-template>元素之下。

    我們可以將許多屬性指令寫在同一個host element上,但同一個host element只能夠有一個結構指令。

    ng-template

    從上面*所做的事情,我們可以知道,結構型指令是建立在ng-template之上的。
    一個ng-template並不會一開始就顯示在畫面上,而是通過directive去操作裡面的dom並將要顯示的template添加在dom之中。

    因此,*ngIf隱藏掉的物件,和我們使用css去show、hide在意義上是完全不同的。
    因為它已經不在dom之上,是沒辦法被操作的。

    不過由於若是網頁內的資料量大,angular有足夠的理由這樣做。
    因為這可以避免過多的dom元素拖累網頁效能,若單純使用css去hide、show元素,所有的監聽器、物件依舊會在背景執行,這會讓效能變得不佳。
    如果我們需要在show、hide物件的同時執行一些特殊的指令,可以用Lifecycle Hooks來撰寫此時要做的事情。

    如何套用多個結構指令在元件裡

    上面有提到,我們可以將許多屬性指令寫在同一個host element上,但同一個host element只能夠有一個結構指令。
    所以在一般的狀態下,如果需要兩個標籤,我們會將HTML利用一些不會影響結構的標籤來做多層的Structural Directives控制

    <div *ngIf="hero"><span *ngFor="hero of heroes">{{hero.name}} </span></div>

    但有時候該狀況不允許任何多餘的標籤在裡面,例如下拉選單select。
    若有一個區域選單select裡面的option內容要由*ngFor來產生,但是當city未選擇時,select又希望不要有任何下拉選單,
    這時後我們在option上的確同時會需要放許多個結構指令,但ANGULAR不允許同一個標籤上放兩個結構指令。

    如果我們多包一層span去包裡面的內容,會發現因為select內不允許span標籤,會造成讀不到option下拉選單,即便已經選擇了city

    <div>
      Pick your favorite hero
      (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
    </div>
    <select &#91;(ngModel)&#93;="hero">
      <span *ngFor="let h of heroes">
        <span *ngIf="showSad || h.emotion !== 'sad'">
          <option &#91;ngValue&#93;="h">{{h.name}} ({{h.emotion}})</option>
        </span>
      </span>
    </select>


    這時候就可以改用<ng-container>

    <select &#91;(ngModel)&#93;="hero">
      <ng-container *ngFor="let h of heroes">
        <ng-container *ngIf="showSad || h.emotion !== 'sad'">
          <option &#91;ngValue&#93;="h">{{h.name}} ({{h.emotion}})</option>
        </ng-container>
      </ng-container>
    </select>

    就可以正常顯示了

    自製一個結構Directive

    import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
    
    /**
     * Add the template content to the DOM unless the condition is true.
     */
    @Directive({ selector: '[appUnless]'})
    export class UnlessDirective {
      private hasView = false;
    
      constructor(
        private templateRef: TemplateRef<any>,
        private viewContainer: ViewContainerRef) { }
    
      @Input() set appUnless(condition: boolean) {
        if (!condition && !this.hasView) {
          this.viewContainer.createEmbeddedView(this.templateRef);
          this.hasView = true;
        } else if (condition && this.hasView) {
          this.viewContainer.clear();
          this.hasView = false;
        }
      }
    }

    最重要的地方是在@Input那段,展示了如何去操作dom
    使用的地方如下:

    <p *appUnless="condition" class="unless a">
      (A) This paragraph is displayed because the condition is false.
    </p>

    完整範例請見:live example / download example

    Posted on Leave a comment

    [功能介紹-6] Attribute Directives

    Directive有分為結構型和屬性型的,這一篇要先介紹屬性型的Directive。
    本篇的範例請見:Attribute Directive example / download example

    Angular有三種directive:

    • 元件 – 包含template的directive。
    • 結構指令 – 通過添加和刪除DOM元素來更改DOM佈局。(如ngFor、ngIf)
    • 屬性指令 – 改變元素,組件或其他指令的外觀或行為。(如ngStyle)

    建立一個簡單的屬性型directive

    一個directive最少需要一個帶有@Directive宣告的檔案,告訴angular該如何識別這個directive。
    這邊會用一個high light的directive範例來解說,當用戶將鼠標懸停在該元素上時,設置元素的背景色。你可以像這樣應用它:

    <p appHighlight>Highlight me!</p>

    而下面會一步步創立這一個directive。

    首先,用cli創立一個新的directive

    ng generate directive highlight

    會看到cli自動建立了兩個新的檔案

    產生的src/app/highlight.directive.ts檔案的內容如下:

    import { Directive } from '@angular/core';
    
    @Directive({
      selector: '[appHighlight]'
    })
    export class HighlightDirective {
      constructor() { }
    }

    當我們import了Directive後,就可以在檔案內使用@Directive來設定directive的CSS attribute selector
    方括弧[]可以讓它成為一個屬性選擇器,在這邊為什麼會取為appHighlight而不是highlight,是為了避免與現有的html指令衝突,建議最好在所有的directive前面加上前綴字,以方便識別這是那邊來的屬性。如果沒有前綴字,很容易會發生難以識別的錯誤

    接著看src/app/highlight.directive.ts的內容:

    import { Directive, ElementRef } from '@angular/core';
    
    @Directive({
      selector: '[appHighlight]'
    })
    export class HighlightDirective {
        constructor(el: ElementRef) {
           el.nativeElement.style.backgroundColor = 'yellow';
        }
    }

    這邊可以看到在建構子的地方會傳入ElementRef,並且使用el.nativeElement直接修改DOM元素的背景顏色。

    若希望directive能夠接收使用者事件,可以加載HostListener,如下:

    import { Directive, ElementRef, HostListener } from '@angular/core';
    
    @Directive({
      selector: '[appHighlight]'
    })
    export class HighlightDirective {
      constructor(private el: ElementRef) { }
    
      @HostListener('mouseenter') onMouseEnter() {
        this.highlight('yellow');
      }
    
      @HostListener('mouseleave') onMouseLeave() {
        this.highlight(null);
      }
    
      private highlight(color: string) {
        this.el.nativeElement.style.backgroundColor = color;
      }

    顯示的結果如下圖:

    當然,也可以通過標準的JavaScript來訪問DOM,並手動添加事件監聽器。這種方法至少有三個問題,是不被建議的:

    • 需正確的堅聽到各種平台的事件
    • 當要destroy這個directive時要手動移除事件
    • 直接操作DOM API不是好的方式

    輸入參數至directive

    首先要先import所需的Input

    import { Directive, ElementRef, HostListener, Input } from '@angular/core';

    然後宣告所要輸入的變數

    @Input() highlightColor: string;

    那麼在使用directivet傳入參數的方式有下面幾種

    <p appHighlight highlightColor="yellow">Highlighted in yellow</p>
    <p appHighlight &#91;highlightColor&#93;="'orange'">Highlighted in orange</p>
    <p &#91;appHighlight&#93;="'orange'">Highlight me!</p>

    下面這個範例是使用上面自製的directive,讓使用者選擇highlight的顏色
    template檔案內容如下:

    <h1>My First Attribute Directive</h1>
    
    <h4>Pick a highlight color</h4>
    <div>
      <input type="radio" name="colors" (click)="color='lightgreen'">Green
      <input type="radio" name="colors" (click)="color='yellow'">Yellow
      <input type="radio" name="colors" (click)="color='cyan'">Cyan
    </div>
    <p &#91;appHighlight&#93;="color">Highlight me!</p>

    component內容如下:

    export class AppComponent {
      color: string;
    }

    最後結果如下圖:

    Posted on Leave a comment

    [功能介紹-5] 動態載入元件

    模版組件有的時後會需要能夠動態被載入,在這邊會講要如何利用ComponentFactoryResolver來動態加入組件

    本篇的例子是需要動態載入廣告橫幅,因為廣告內容會由幾個不同的團隊來打造,要在同一個區塊循環播放不同的廣告,因此較難把不同的廣告放在同一個component,這時後就會需要用到動態載入的功能
    完整範例請見: live example / download example

    The anchor directive

    在架構介紹的地方有介紹到,directive是一個沒有view的component,可以使用在html裡。
    在這邊因為需要動態載入元件,會需要先建立一個directive,讓angular知道要把要動態載入的元件放在那邊。
    ad.directive.ts這個檔案的內容如下:

    import { Directive, ViewContainerRef } from '@angular/core';
    
    @Directive({
      selector: '[ad-host]',//這個directive的名字
    })
    export class AdDirective {
      constructor(public viewContainerRef: ViewContainerRef) { }//在directive裡去取得View元件以方便做操作
    }

    首先我們將這個directive命名為[ad-host],然後注入ViewContainerRef,ViewContainerRef可以讓我們得知目前所在的HTML元素中包含的View內容,也可以透過它來改變View的結果(ex: 動態的產生Component、移除某個Component等等)。

    Loading components

    使用上面的directive的html碼如下

    <div class="ad-banner">
       <h3>Advertisements</h3>
       <ng-template ad-host></ng-template>
    </div>

    這邊可以看到我們使用ng-template來應用剛剛所創建的directive,ng-template指令表示一個Angular模板,這個標籤的內容將包含template的一部分,然後可以與其他template一起組成來成為最終的組件模板。使用ng-template標籤只是定義一個template,但是我們還沒有使用它。

    Resolving components

    下面是src/app/ad-banner.component.ts的內容

    export class AdBannerComponent implements AfterViewInit, OnDestroy {
      @Input() ads: AdItem[];//要輪播顯示的廣告
      currentAddIndex: number = -1;
      @ViewChild(AdDirective) adHost: AdDirective;//宣告剛剛建立的Directive,並藉由取得傳入的adHost
      subscription: any;
      interval: any;
    
      constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
    
      ngAfterViewInit() {
        this.loadComponent();
        this.getAds();
      }
    
      ngOnDestroy() {
        clearInterval(this.interval);
      }
      //最重要動態載入廣告的程式碼都在這個function裡
      loadComponent() {
        //決定要顯示那則廣告
        this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
        let adItem = this.ads[this.currentAddIndex];
    
        //透過ComponentFactoryResolver來解析component組件
        let componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);
        //透過剛剛從AdDirective傳來的資訊取得要載入ad的template
        let viewContainerRef = this.adHost.viewContainerRef;
        //將template內容清掉
        viewContainerRef.clear();
        //動態建立template並顯示在畫面上
        let componentRef = viewContainerRef.createComponent(componentFactory);
        (componentRef.instance).data = adItem.data;
      }
    
      //通過這個方法,每三秒會動態載入不同的廣告元件
      getAds() {
        this.interval = setInterval(() => {
          this.loadComponent();
        }, 3000);
      }
    }

    loadComponent是動態載入元件最關鍵的程式碼片段,程式碼裡都有下註解。

    最終出來的成果如下圖

    參考資料

    Posted on Leave a comment

    [功能介紹-4] 組件間的溝通

    通過輸入綁定數據從父項傳遞給子項

    這是接受數據的子元件的內容hero-child.component.ts
    子元件輸要被輸入的屬性有兩個,以@Input開頭來做宣告:

    import { Component, Input } from '@angular/core';
    
    import { Hero } from './hero';
    
    @Component({
      selector: 'app-hero-child',
      template: `
        <h3>{{hero.name}} says:</h3>
        <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
      `
    })
    export class HeroChildComponent {
      @Input() hero: Hero;
      @Input('master') masterName: string;
    }

    而這是父元件的內容,透過屬性綁定將值塞進子元件中

    <app-hero-child *ngFor="let hero of heroes"
          &#91;hero&#93;="hero"
          &#91;master&#93;="master">

    父元件的完整程式碼如下:

    import { Component } from '@angular/core';
    
    import { HEROES } from './hero';
    
    @Component({
      selector: 'app-hero-parent',
      template: `
        <h2>{{master}} controls {{heroes.length}} heroes</h2>
        <app-hero-child *ngFor="let hero of heroes"
          &#91;hero&#93;="hero"
          &#91;master&#93;="master">
        </app-hero-child>
      `
    })
    export class HeroParentComponent {
      heroes = HEROES;
      master = 'Master';
    }

    最後結果呈現:

    攔截屬性的更改與設置

    可將子元件的input宣告改為getter和setter,如下面範例

    import { Component, Input } from '@angular/core';
    
    @Component({
      selector: 'app-name-child',
      template: '<h3>"{{name}}"</h3>'
    })
    export class NameChildComponent {
      private _name = '';
    
      @Input()
      set name(name: string) {
        this._name = (name && name.trim()) || '<no name set>';
      }
    
      get name(): string { return this._name; }
    }

    這樣當父元件傳空字串來時,子元件會自動輸出<no name set>字樣

    Intercept input property changes with ngOnChanges()

    在子組件裡,除了上述使用getter及setter外,也可以利用ngOnChanges()來取得改變的變數並做出回應
    ngOnChanges()會傳入所有被改變的值changes: {[propKey: string]: SimpleChange}
    下面是一個使用範例
    子元件內容

    import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
    
    @Component({
      selector: 'app-version-child',
      template: `
        <h3>Version {{major}}.{{minor}}</h3>
        <h4>Change log:</h4>
        <ul>
          <li *ngFor="let change of changeLog">{{change}}</li>
        </ul>
      `
    })
    export class VersionChildComponent implements OnChanges {
      @Input() major: number;
      @Input() minor: number;
      changeLog: string[] = [];
    
      ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
        let log: string[] = [];
        for (let propName in changes) {
          let changedProp = changes[propName];
          let to = JSON.stringify(changedProp.currentValue);
          if (changedProp.isFirstChange()) {
            log.push(`Initial value of ${propName} set to ${to}`);
          } else {
            let from = JSON.stringify(changedProp.previousValue);
            log.push(`${propName} changed from ${from} to ${to}`);
          }
        }
        this.changeLog.push(log.join(', '));
      }
    }

    父元件內容

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-version-parent',
      template: `
        <h2>Source code version</h2>
        <button (click)="newMinor()">New minor version</button>
        <button (click)="newMajor()">New major version</button>
        <app-version-child &#91;major&#93;="major" &#91;minor&#93;="minor"></app-version-child>
      `
    })
    export class VersionParentComponent {
      major = 1;
      minor = 23;
    
      newMinor() {
        this.minor++;
      }
    
      newMajor() {
        this.major++;
        this.minor = 0;
      }
    }

    最後成果如下圖

    Parent listens for child event

    子元件的EventEmitter財產是一個輸出的屬性,通常用@Output來宣告,如下:

    import { Component, EventEmitter, Input, Output } from '@angular/core';
    
    @Component({
      selector: 'app-voter',
      template: `
        <h4>{{name}}</h4>
        <button (click)="vote(true)"  &#91;disabled&#93;="voted">Agree</button>
        <button (click)="vote(false)" &#91;disabled&#93;="voted">Disagree</button>
      `
    })
    export class VoterComponent {
      @Input()  name: string;
      @Output() onVoted = new EventEmitter<boolean>();
      voted = false;
    
      vote(agreed: boolean) {
        this.onVoted.emit(agreed);
        this.voted = true;
      }
    }

    而父元件的內容如下:

    import { Component }      from '@angular/core';
    
    @Component({
      selector: 'app-vote-taker',
      template: `
        <h2>Should mankind colonize the Universe?</h2>
        <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
        <app-voter *ngFor="let voter of voters"
          &#91;name&#93;="voter"
          (onVoted)="onVoted($event)">
        </app-voter>
      `
    })
    export class VoteTakerComponent {
      agreed = 0;
      disagreed = 0;
      voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];
    
      onVoted(agreed: boolean) {
        agreed ? this.agreed++ : this.disagreed++;
      }
    }

    結果如圖:

    Parent interacts with child via local variable

    下面是範例,將子元件使用#來宣告為template變數,就可以在父元件內自由使用子元件變數:

    <h3>Countdown to Liftoff (via local variable)</h3>
      <button (click)="timer.start()">Start</button>
      <button (click)="timer.stop()">Stop</button>
      <div class="seconds">{{timer.seconds}}</div>
      <app-countdown-timer #timer></app-countdown-timer>

    Parent and children communicate via a service

    服務內容:

    import { Injectable } from '@angular/core';
    import { Subject }    from 'rxjs/Subject';
    
    @Injectable()
    export class MissionService {
    
      // Observable string sources
      private missionAnnouncedSource = new Subject<string>();
      private missionConfirmedSource = new Subject<string>();
    
      // Observable string streams
      missionAnnounced$ = this.missionAnnouncedSource.asObservable();
      missionConfirmed$ = this.missionConfirmedSource.asObservable();
    
      // Service message commands
      announceMission(mission: string) {
        this.missionAnnouncedSource.next(mission);
      }
    
      confirmMission(astronaut: string) {
        this.missionConfirmedSource.next(astronaut);
      }
    }

    父元件內容

    import { Component }          from '@angular/core';
    
    import { MissionService }     from './mission.service';
    
    @Component({
      selector: 'app-mission-control',
      template: `
        <h2>Mission Control</h2>
        <button (click)="announce()">Announce mission</button>
        <app-astronaut *ngFor="let astronaut of astronauts"
          &#91;astronaut&#93;="astronaut">
        </app-astronaut>
        <h3>History</h3>
        <ul>
          <li *ngFor="let event of history">{{event}}</li>
        </ul>
      `,
      providers: [MissionService]
    })
    export class MissionControlComponent {
      astronauts = ['Lovell', 'Swigert', 'Haise'];
      history: string[] = [];
      missions = ['Fly to the moon!',
                  'Fly to mars!',
                  'Fly to Vegas!'];
      nextMission = 0;
    
      constructor(private missionService: MissionService) {
        missionService.missionConfirmed$.subscribe(
          astronaut => {
            this.history.push(`${astronaut} confirmed the mission`);
          });
      }
    
      announce() {
        let mission = this.missions[this.nextMission++];
        this.missionService.announceMission(mission);
        this.history.push(`Mission "${mission}" announced`);
        if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
      }
    }

    子元件內容

    import { Component, Input, OnDestroy } from '@angular/core';
    
    import { MissionService } from './mission.service';
    import { Subscription }   from 'rxjs/Subscription';
    
    @Component({
      selector: 'app-astronaut',
      template: `
        <p>
          {{astronaut}}: <strong>{{mission}}</strong>
          <button
            (click)="confirm()"
            &#91;disabled&#93;="!announced || confirmed">
            Confirm
          </button>
        </p>
      `
    })
    export class AstronautComponent implements OnDestroy {
      @Input() astronaut: string;
      mission = '<no mission announced>';
      confirmed = false;
      announced = false;
      subscription: Subscription;
    
      constructor(private missionService: MissionService) {
        this.subscription = missionService.missionAnnounced$.subscribe(
          mission => {
            this.mission = mission;
            this.announced = true;
            this.confirmed = false;
        });
      }
    
      confirm() {
        this.confirmed = true;
        this.missionService.confirmMission(this.astronaut);
      }
    
      ngOnDestroy() {
        // prevent memory leak when component destroyed
        this.subscription.unsubscribe();
      }
    }

    結果如下圖:

    更多本篇完整範例請見: live example / download example