Angular Info

Beta.16とBeta.17の変更点

どうも、らこです。
今週はBeta.16とBeta.17の2つのリリースがありまして、特にBeta.16はベータ始まって以来の最大級の変更がリリースされているので、
みなさんにはぜひとも頑張ってBeta.17へのアップデートを乗り越えて欲しいところです。
破壊的変更は多いですが、基本的なAPIについては機械的に修正可能なものがほとんどです。
逆に、Angular 2の深いところまで潜っていた方ほど被害が大きいでしょう。
それでは重要な変更をピックアップしていきます。


CHANGELOG

Locationangular2/platform/common に移動しました

これまで angular2/router モジュールから提供されていた Location クラスが、
新しく生まれた angular2/platform/common モジュールに移動しました。
つまり Core側のパッケージに含まれることになり、 angular2/router に依存せずに使えるようになります。

また、 Location に関連する APP_BASE_HREFLocationStrategy などのAPIも移動しています。
以前は次のようにインポートしていましたが、

import {
  PlatformLocation,
  Location,
  LocationStrategy,
  HashLocationStrategy,
  PathLocationStrategy,
  APP_BASE_HREF}
from 'angular2/router';

今後は次のようにインポートするようになります。

import {
  PlatformLocation,
  Location,
  LocationStrategy,
  HashLocationStrategy,
  PathLocationStrategy,
  APP_BASE_HREF}
from 'angular2/platform/common';

Injector が読み込み専用になり、 ReflectiveInjector が追加されました

Offline Compileの実装に伴い、Injectorに大きな変更が入りました。

Injector - ts

まず第一に、 Injector クラスが抽象クラスとなり、 get メソッドだけを提供するようになりました。
これまで Injectorが提供していた他のメソッドは、具象クラスである ReflectiveInjector が実装しています。

ReflectiveInjector - ts

var injector = ReflectiveInjector.resolveAndCreate([]);
expect(injector.get(Injector)).toBe(injector);

また、getOptional メソッドが廃止され、 get メソッドが第2引数としてデフォルト値を取るようになりました。
デフォルト値を設定しない時にプロバイダが見つからなかった時は今までどおり例外が発生します。

injector.get(optionalDependency, notFoundValue);

Compilerの廃止とComponentFactoryの導入

コンポーネントを動的にコンパイルするためのAPIとして、これまではCompilerが提供されていましたが、
これが廃止され、新たにComponentResolverComponentFactoryという2つのAPIが追加されました。

refactor(core): introduce ComponentFactory. · angular/angular@0c600cf

ComponentResolverは基本的に従来のCompilerとほとんど変わらないAPIを持っています。

// beta.15
export abstract class Compiler {
  abstract compileInHost(componentType: Type): Promise<HostViewFactoryRef>;
  abstract clearCache();
}

// beta.17
export abstract class ComponentResolver {
  abstract resolveComponent(componentType: Type): Promise<ComponentFactory>;
  abstract clearCache();
}

ComponentResolverはプロバイダを記述しなくてもコンポーネントやディレクティブでインジェクト可能です。

ComponentResolver#resolveComponentが返すComponentFactory
後述するViewContainerRef.createComponentのメソッドの引数として使うことができます。
また、ComponentFactory.create メソッドを使えば、ビューへの挿入なしに、ComponentRefだけを生成できます。

ViewContainerRef.createHostView.createComponentに改名されました

よりわかりやすい名前に変わり、戻り値の型も HostViewRef から ComponentRef に変わりました。

さらに、ResolvedProviderクラスが廃止された影響で、第3引数はInjectorを要求するようになりました。
もしInjectorを渡したい場合は、専用の新しい子Injectorを作ってあげるのが推奨されます。

let childInjector = ReflectiveInjector.resolveAndCreate(…);
vcRef.createComponent(cmpFactory, index, childInjector)

DynamicComponentLoader.loadIntoLocation が廃止されました

refactor(core): add Query.read and remove `DynamicComponentLoader.l… · angular/angular@efbd446

指定した要素をコンテナとして動的にコンポーネントをビューに追加するAPIだった DynamicComponentLoader.loadIntoLocation が廃止されました。
代わりに、指定した要素の次の位置に追加する DynamicComponentLoader.loadNextToLocation が引数としてElementRefではなくViewContainerRefを要求するようになりました。
つまり、動的にコンポーネントをビューへ追加するには必ずViewContainerRefが必要になったということです。

ViewContainerRefはコンポーネントやディレクティブが自身のものをDIで取得することができます。
また、コンポーネントのテンプレート中から任意の要素のViewContainerRefを得るには、@ViewChildを使います。
通常、@ViewChild("loc")とすると#locが付与された要素のElementRefが得られますが、
第2引数として {read: ViewContainerRef}とすることでコンテナを取得することができます

@Component({
    selector: 'my-comp',
    template: '<div #loc></div>'
})
class MyComp {
  ctxBoolProp: boolean;

  @ViewChild('loc', {read: ViewContainerRef}) viewContainerRef: ViewContainerRef;

  constructor(private loader: DynamicComponentLoader){}

  loadChildComponent() {
    this.loader.loadNextToLocation(OtherComponent, this.viewContainerRef)
        .then(componentRef => {
            ...
        });
  }
}

AppViewManager が廃止されました

低レイヤーのビュー管理APIだった AppViewManagerが内部専用のAPIになり、外部には公開されなくなりました。
AppViewManagerでできることはDynamicComponentLoaderViewContainerRefで同じことができます。

ComponentRef.disposeComponentRef.destroyに改名されました

名前が変わっただけです。ライフサイクルのngOnDestroyに合わせてわかりやすくした形です。

非同期テストのやりかたが大きく変わりました

angular2/testingでの非同期テストが大きく変わりました。

まず最初に、非同期テストを行うために zone.js/dist/async-testの読み込みが必要になりました。

import "zone.js/dist/async-test";

また、APIも変わっています。これまではDIを行いつつ非同期テストを行うにはinjectAsync関数を使っていましたが、
DIを行うinject関数と、非同期テストを行うasync関数の2つに分離されました

// Before:

it('should wait for returned promises', injectAsync([FancyService], (service) => {
  return service.getAsyncValue().then((value) => { expect(value).toEqual('async value'); });
}));
it('should wait for returned promises', injectAsync([], () => {
  return somePromise.then(() => { expect(true).toEqual(true); });
}));

// After
it('should wait for returned promises', async(inject([FancyService], (service) => {
  service.getAsyncValue().then((value) => { expect(value).toEqual('async value'); });
})));
it('should wait for returned promises', async(() => {
  somePromise.then() => { expect(true).toEqual(true); });
}));

async関数に渡された関数は、そのテスト固有のZoneが生成され、
そのZoneが非同期処理の完了を監視してくれるので、doneのような明示的なテスト終了処理は不要です。
Promiseをreturnする必要もなくなりました。

ちなみに、fakeAsyncも同様です。
fakeAsyncを使う際には追加で zone.js/dist/fake-async-testの読み込みが必要になり、
使い方もfakeAsync(inject([...], (...) => {...}))のように変わります。

Renderer.renderComponentが廃止されました

任意のコンポーネントをレンダリングする低レベルAPIだったRenderer.renderComponentが廃止されました。
同じAPIはRootRenderer.renderComponentとして提供されています。

ビューのクエリの仕様が変わりました

refactor(view_compiler): codegen DI and Queries · angular/angular@2b34c88

@ViewQuery@ViewChild@ContentChildなどのビュークエリは、DynamicComponentLoaderによって動的に読み込まれたコンポーネントには適用されない、という仕様に変わりました。
例えば、<router-outlet>によって読み込まれるコンポーネントは、クエリ対象になりません。
ただし、<router-outlet>activateイベントを発火するので、
新しく読み込まれたコンポーネントのComponentRefを取得することができます。
動的にコンポーネントを読み込む場合は同じようにイベントを発火してあげるようにしましょう。

Change Detectionの処理順序が変わりました

Change Detectionの処理順序が次のようになります。

  1. Inputのチェック
    1. ngOnChanges
    2. ngOnInit (一度のみ)
    3. ngDoCheck
  2. Contentの更新
    1. ContentのChange Detection
    2. ContentChildrenの更新
    3. ngAfterContentChecked
  3. Viewの更新
    1. ViewのChange Detection
    2. ViewChildren/ViewQueryの更新
    3. ngAfterViewChecked

Pipeのパラメータの仕様が変更されました

これまで、Pipeのtransformメソッドには第2引数の型がargs: any[]だったので常に配列が渡されていましたが、
...args: any[]に変わり、直接オブジェクトを受け取れるようになりました。

// Before
@Pipe({name: "repeat"})
class RepeatPipe implemetes PipeTransform {
    transform(value: any, args: any[]): any {
        let times = <number>args[0]; // 常に配列なので0番目を取得する必要があった
        return value.repeat(times);
    }
}

// After
@Pipe({name: "repeat"})
class RepeatPipe implemetes PipeTransform {
    transform(value: any, times: number): any {
        return value.repeat(times);
    }
}

#...シンタックスの仕様変更とlet-/ref-シンタックスの追加

feat(core): separate refs from vars. · angular/angular@d2efac1

これまで、ngForの中で使われる#...は反復中のオブジェクトを示し、それ以外では付与された要素の参照を示していましたが、
これは混乱を招いていました。

そこで、テンプレート内でのローカル変数を作るためのシンタックスとして新しくlet-が追加されました。

<!--Before-->
<li *ngFor="#item of items; #i = index">...</li>
<template ngFor="#item of items; #i = index"></div>
<template ngFor #item [ngForOf]="items" #i="index"><li>...</li></template>

<!--After-->
<li *ngFor="let item of items; let i = index">...</li>
<template ngFor="let item of items; let i = index"></div>
<template ngFor let-item [ngForOf]="items" let-i="index"><li>...</li></template>

また、#...シンタックスはこれまでvar-...と対応していましたが、今後はref-...になります。
通常の要素に付与すればその要素の参照に、<template>要素に付与すればTemplateRefとして扱えます。

var-シンタックスは将来的に廃止される非推奨なAPIとなりました。
let-ref-のどちらかに書き直しましょう。

TemplateRefにコンテキストのジェネリクスが付きました

先述のlet-と関連して、ローカル変数をオブジェクトとして扱うためのコンテキストが導入されました。
TemplateRefは、自身のコンテキストの型をジェネリクスとして宣言する必要があります

例えば、NgForNgForRowというコンテキストを持っています。

export class NgForRow {
  constructor(public $implicit: any, public index: number, public count: number) {}

  get first(): boolean { return this.index === 0; }

  get last(): boolean { return this.index === this.count - 1; }

  get even(): boolean { return this.index % 2 === 0; }

  get odd(): boolean { return !this.even; }
}

...

export class NgFor implements DoCheck {
  ...

  constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef<NgForRow>,
              private _iterableDiffers: IterableDiffers, private _cdr: ChangeDetectorRef) {}

  ...
}

ViewContainerRef.createEmbeddedViewを使ってTemplateRefからビューを作るときに、第2引数としてコンテキストオブジェクトを渡すことができます。

class SomeViewportContext {
  constructor(public someTmpl: string) {}
}

@Directive({selector: '[someViewport]'})
@Injectable()
class SomeViewport {
constructor(container: ViewContainerRef, templateRef: TemplateRef<SomeViewportContext>) {
  container.createEmbeddedView(templateRef, new SomeViewportContext('hello'));
  container.createEmbeddedView(templateRef, new SomeViewportContext('again'));
  }
}

<template someViewport let-greeting="someTmpl">
    <p>{{greeting}}</p>
</template>

コンテキストを使ってローカル変数を設定できるようになったので、
従来のEmbeddedViewRef.setLocalは削除されました。

NgTemplateOutletの追加

TemplateRefを渡すと内部のViewContainerにセットしてくれる便利なディレクティブが追加されました。

<div>
  <template #tmp>
    <h1>Template!!</h1>
  </template>
  <div [ngTemplateOutlet]="tmp"></div>
</div>

簡単にrouter-outletのようなビューの切り替えが実装できるようになります。


長い!!!!!!

お疲れ様でした。Beta.16, 17ではOffline Compileのために基盤部分が大きく変わっており、
深いAPIを使っているほど影響が大きいアップデートです。
冒頭にも言ったように、このアップデートに対応しておかないと今後の追従が難しいので、
被害の大きかった人も頑張って対応しましょう。

Offline Compileの使い方はまだドキュメントがなく、CLIがまだ開発途上らしいのでもうしばらく時間がかかりそうです。

それでは。