AngularのRouter

(2017-04-30)

@angular/core": 4.1.0

Angular2とangular-cliでTODOを作る - sambaiz.net

angular-cli@angular/cli変更された

routingを行うのでnewで--routingオプションを付けている。

$ npm install -g @angular/cli
$ ng -v
@angular/cli: 1.0.1

$ ng new angular4-routing --routing
$ cd angular4-routing/
$ cat package.json | grep @angular/core
    "@angular/core": "^4.0.0",

$ ng serve
** NG Live Development Server is running on http://localhost:4200 **     

--routingを付けたのでapp-routing.module.tsが作成され、app.module.tsにAppRoutingModuleが追加される。 index.htmlのheadにはpushStateのroutingが働くように base要素が 追加されている

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    children: []
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

また、app.component.htmlrouter-outletが置かれていてroutingによるComponentはこの下に描画される。

<h1>
  {{title}}
</h1>
<router-outlet></router-outlet>

動的に追加されるコンポーネントのstyleは@HostBindingで設定できる。

export class TodoMainComponent implements OnInit {

  @HostBinding('style.width')   width = '100%';

  ...
}

ルーティング定義

childrenchild routeを設定しているが、 これは親コンポーネントのrouter-outletの下に描画される。

$ ng g component todo/todo-main
$ ng g component todo/todo-list
$ ng g component todo/todo-item
$ ng g component not-found
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { TodoMainComponent } from './todo/todo-main/todo-main.component'
import { TodoListComponent } from './todo/todo-list/todo-list.component'
import { TodoItemComponent } from './todo/todo-item/todo-item.component'
import { NotFoundComponent } from './not-found/not-found.component'

const routes: Routes = [
  {
    path: 'todo',
    component: TodoMainComponent,
    children: [
          {
            path: ':id',
            component: TodoItemComponent
          },
          {
            path: '',
            component: TodoListComponent
          }
    ]
  },
  {
    path: '**',
    component: NotFoundComponent,
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
<p>
  todo-main works!
</p>
<router-outlet></router-outlet>

これで http://localhost:4200/todo にアクセスすると、TodoMainComponentTodoListComponentが表示される。

app works!

todo-main works!

todo-list works!

パラメータの取得

ActivatedRouteをDIしてObservableなparamsをSubscribeする。

import { Component, OnInit, HostBinding } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';

@Component({
  selector: 'app-todo-item',
  templateUrl: './todo-item.component.html',
  styleUrls: ['./todo-item.component.css']
})
export class TodoItemComponent implements OnInit {

  id: number;

  constructor(
    private route: ActivatedRoute,
  ) { }

  ngOnInit() {  
    this.route.params.subscribe(
      (params: Params) => this.id = +params['id']
    );
  }
}
<p>
  todo-item ({{id}}) works!
</p>

遷移

タグなら<a routerLink>を、コードならRouter.navigateを使って遷移できる。

<p>
  todo-item ({{id}}) works!
  <button (click)="onClickNext()">Next</button>
  <a routerLink="/todo">todos</a>
</p>
...
export class TodoItemComponent implements OnInit {

  constructor(
    private route: ActivatedRoute,
    private router: Router
  ) { }

  ...

  onClickNext() {
    if(typeof this.id !== 'undefined'){
      this.router.navigate([`/todo/${this.id+1}`, {hoge: "fuga"}]);
    }
  }
}

navigateでhogeという適当なパラメータを付けているが、呼ぶとhttp://localhost:4200/todo/10;hoge=fugaのように クエリパラメータが?, &ではなく;で区切られたURLに遷移する。 これをmatrix URL notationといって、結構由緒正しいものらしい。

Guard

routeの遷移時に何かするためのもの。 具体的にはログインしているかどうかをチェックしたりとか、 遷移する前にデータを一時保存したりとかそういうのに使える。

$ ng g guard auth

canActivate() はrouteに遷移するときに呼ばれ、trueを返すとそのまま続行され、falseを返すと中断される。

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    console.log(`canActivate(): ${state.url}`);
    return true;
  }
}

AppModuleのprovidersにAuthGuardを入れて、routesにもcanActivateとしてAuthGuardを設定すると呼ばれるようになる。

import { AuthGuard } from './auth.guard'

const routes: Routes = [
  {
    path: 'todo',
    component: TodoMainComponent,
    canActivate: [AuthGuard],
    children: [
          {
            path: ':id',
            component: TodoItemComponent
          },
          {
            path: '',
            component: TodoListComponent
          }
    ]
  },
  {
    path: '**',
    component: NotFoundComponent,
  }
];

この設定だと上で追加したNextボタンを押してchild routeに遷移したときには呼ばれない。 canActivateChild()にするとchild routeに遷移したときにも呼ばれるようになる。

canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
  return this.canActivate(route, state);
}
{
    path: 'todo',
    component: TodoMainComponent,
    canActivateChild: [AuthGuard],
    children: [
          {
            path: ':id',
            component: TodoItemComponent
          },
          {
            path: '',
            component: TodoListComponent
          }
    ]
} 

これらの他には

  • canDeactivate(): 今のrouteから離れるとき

  • resolve(): コンポーネントを表示する前。pre-fetchのため。

  • canLoad(): loadChildrenで指定したModuleをlazy load するとき。ドキュメントではAdminModuleに対して、認証されていなかったらロードしないようにしている。

のGuardがある。

アニメーション

app.module.tsBrowserAnimationsModuleを追加。

$ npm install --save @angular/animations
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  ...
  imports: [
    ...
    BrowserAnimationsModule
  ],
  ...
})

animations.tsを作成する。

import { animate, AnimationEntryMetadata, state, style, transition, trigger } from '@angular/core';

// Component transition animations
export const slideInDownAnimation: AnimationEntryMetadata =
  trigger('routeAnimation', [
    state('*',
      style({
        opacity: 1,
        transform: 'translateX(0)'
      })
    ),
    transition(':enter', [
      style({
        opacity: 0,
        transform: 'translateX(-100%)'
      }),
      animate('0.2s ease-in')
    ]),
    transition(':leave', [
      animate('0.5s ease-out', style({
        opacity: 0,
        transform: 'translateY(100%)'
      }))
    ])
  ]);

これを@Componentのanimationsに入れて@HostBindingでanimationのトリガー(routeAnimation)を発火させる。

import { Component, OnInit, HostBinding } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { slideInDownAnimation } from '../../animations';

@Component({
  selector: 'app-todo-item',
  templateUrl: './todo-item.component.html',
  styleUrls: ['./todo-item.component.css'],
  animations: [ slideInDownAnimation ]
})
export class TodoItemComponent implements OnInit {

  @HostBinding('@routeAnimation') routeAnimation = true;
  @HostBinding('style.display')   display = 'block';
  @HostBinding('style.position')  position = 'absolute';

  id: number;

  constructor(
    private route: ActivatedRoute,
    private router: Router
  ) { }

  ngOnInit() {  
    this.route.params.subscribe(
      (params: Params) => this.id = +params['id']
    );
  }

  onClickNext() {
    this.router.navigate(['/todo', {id: this.id + 1, hoge: "fuga"}])
  }
}

こんな感じにアニメーションする。

アニメーション