Git Product home page Git Product logo

angular-app's Introduction

Angular

Angularでヒーローを表示するアプリ(CRUDモデルの練習)を開発。(チュートリアル)

公式サイト:Angular

インストール

$ ng new angular-app
$ cd angular-app
$ ng serve --open

これでhttp://localhost:4200/にアクセスできる。(開発者サーバの立ち上げ)

Bootstrapのインストール

公式サイト:Angular powered Bootstrap

$ ng add @ng-bootstrap/ng-bootstrap

Angularアプリの仕組み

アプリケーションのタイトル変更

src/app/app.component.ts:アプリケーションのタイトルを変更

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
}

src/app/app.component.html:コンポーネントのテンプレートファイル。

Angular CLIで生成されたデフォルトのテンプレートを削除する。代わりに以下のHTMLを置く。

<h1>{{ title }}</h1>

AugularではVueと同様に{{}}で変数を挿入する。この際、ブラウザがページを更新して新しいアプリのタイトルが表示される。

アプリケーションのスタイルを追加する

大半のアプリケーションは、アプリ全体で一貫した見た目を担保している。その際、空のsrc/style.cssを追加する。

src/styles.css:アプリ全体のデザインを書く。

/* Application-wide Styles */
h1 {
  color: #369;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 250%;
}
h2, h3 {
  color: #444;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[type="text"], button {
  color: #333;
  font-family: Cambria, Georgia, serif;
}
/* everywhere else */
* {
  font-family: Arial, Helvetica, sans-serif;
}

エディタの作成

heroesコンポーネントの作成

Angular CLIを活用して、heroesという名前の新しいコンポーネントを生成する。

$ ng generate component heroes

CLIはsrc/app/heroes/という新しいフォルダを作成し、HeroesComponentに関する3つのファイルをテストファイルと一緒に生成する。

HeroesComponentのクラスファイルは以下の通り。

app/heroes/heroes.component.ts

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

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

  constructor() { }

  ngOnInit(): void {
  }

}

このとき、常にAngularコアライブラリからComponentシンボルをインポートし、コンポーネントクラスに@Componentで注釈をつける。

@ComponentはコンポーネントのAngularメタデータを指定するデコレータ関数である。

CLIは次の3つのメタデータプロパティを生成する。

  • selector―コンポーネントのCSS要素セレクタ
  • templateUrl―コンポーネントのテンプレートファイルの場所
  • styleUrls―コンポーネントのプライベートCSSスタイルの場所

ngOnInit()はライフサイクルフックである。ライフサイクルフックとは、Angularがコンポーネントクラスをインスタンス化してコンポーネントビューとその子ビューをレンダリングするときに開始する機能である。

heroプロパティの追加

src/app/heroes/heroes.component.ts

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

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
  hero = 'Windstorm'
  constructor() {}

  ngOnInit(): void {
  }

}

ヒーローを表示する

hero.component.htmlテンプレートファイルを開く。Angular CLIで生成されたデフォルトのテキストを削除し、それを新しいheroプロパティへのデータバインディングに置換する。

<p>heroes works!</p>
<h2>{{ hero }}</h2>

HeroesComponentビューの表示

HeroesComponentを表示するには、それをアプリケーションシェルのAppComponentのテンプレートに追加する必要がある。

AppComponentのテンプレートファイルで、タイトルの直下に<app-heroes>要素を追加する。

<h1>{{ title }}</h1>
<app-heroes></app-heroes>

これでコンポーネントビューを表示できる。

Heroインターフェイスの作成

src/appフォルダ内にhero.tsを作成し、Heroインターフェイスを作成する。それにidnameをそれぞれ与える。

src/app/hero.ts

export interface Hero {
    id: number
    name: string
}

HeroesComponentクラスに戻って、Heroインターフェイスをインポート。コンポーネントのheroプロパティをHero型に**リファクタリング(ソフトウェアの挙動を変えることなく、その内部構造を整理すること)**する。それを1というidWindstormというnameで初期化する。

HeroesComponentのクラスファイルは以下の通り。

src/app/heroes/heroes.component.ts

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero'; //hero.tsからHeroインターフェイスをインポート

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
// この際、ヒーローを文字列からオブジェクトに変更したので、ページが正しく表示されない。
export class HeroesComponent implements OnInit {
  hero: Hero = {
    id: 1,
    name: 'Windstorm'
  }
  constructor() {}

  ngOnInit(): void {
  }

}

ヒーローオブジェクトの表示

heroes.component.html

<p>heroes works!</p>
<h2>{{ hero.name }} Details</h2>
<div>
    <span>id: </span>{{ hero.id }}
</div>
<div>
    <span>name: </span>{{ hero.name }}
</div>

UppercasePipeで書式設定

hero.nameのバインディングを以下のように修正する。

heroes.component.html

<h2>{{ hero.name | uppercase }} Details</h2>

ブラウザが更新され、ヒーローの名前が大文字で表示されるようになる。

パイプは、文字列、通貨金額、日付やその他の表示データの書式設定に最適。Angularには複数のパイプが備わっているので、オリジナルのパイプを作成できる。

AppModule

Angularでは、アプリケーションの商品がどのように合わさるか、アプリケーションが必要としている他のファイルやライブラリを知る必要がある。この情報をメタデータと呼ぶ。

一部のメタデータは、コンポーネントクラスに追加した@Componentデコレータ内にある。最も重要な@NgModuleデコレータは、トップレベルのAppModuleクラスに注釈をつける。

Angular CLIでは、プロジェクトを新規作成する際にsrc/app/app.module.tsAppModuleクラスを作成する。ここでFormsModuleをオプトインする。

FormsModuleのインポート

AppModule(app.module.ts)を開いて、@angular/formsライブラリからFormsModuleシンボルをインポートする。

app.module.ts

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

それから、FormModule@NgModuleメタデータをimports配列に追加する。この配列には、アプリケーションに必要な外部モジュールのリストが含まれる。

app.module.ts

imports: [
  BrowserModule,
  FormsModule
],

ヒーローの編集

HeroesComponentテンプレートの詳細エリアをリファクタリングすると、以下のようになる。

heroes.component.html

<div>
    <label for="name">Hero name: </label>
    <input id="name" [(ngModel)]="hero.name" placeholder="name">
</div>

[(ngModel)]はAngularの双方向データバインディング構文である。

これでhero.nameプロパティをHTMLのテキストボックスにバインドするので、hero.nameプロパティからテキストボックスへ、テキストボックスからhero.nameプロパティへ双方向へデータを流せる。

HeroesComponentの宣言

すべてのコンポーネントは、たった一つのNgModuleで宣言される必要がある。しかし、HeroesComponentを宣言していないのに、どうしてアプリは作動したのか?

それは、AngularがHeroesComponentを生成した際に、AppModuleでそのコンポーネントの宣言を行っていたから。

src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { HeroesComponent } from './heroes/heroes.component'; // 自動追加

@NgModule({
  declarations: [
    AppComponent,
    HeroesComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule,
    NgbModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

ヒーローのモックを作成

最終的には、リモートのデータサーバからそれらのヒーローを取得する。

src/app/mock-heroes.tsを作成し、以下のプログラムを書く。

import { Hero } from './hero';

export const HEROES: Hero[] = [
  { id: 11, name: 'Dr Nice' },
  { id: 12, name: 'Narco' },
  { id: 13, name: 'Bombasto' },
  { id: 14, name: 'Celeritas' },
  { id: 15, name: 'Magneta' },
  { id: 16, name: 'RubberMan' },
  { id: 17, name: 'Dynama' },
  { id: 18, name: 'Dr IQ' },
  { id: 19, name: 'Magma' },
  { id: 20, name: 'Tornado' }
];

ヒーローを表示する

HeroesComponentクラスのファイルを開いて、HEROESモックをインポートする。

heroes.component.ts

import { Component, OnInit } from '@angular/core';
import { HEROES } from '../mock-heroes';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
  heroes = HEROES
  constructor() {}

  ngOnInit(): void {
  }

}

*ngForでヒーローを表示

HeroesComponentテンプレートを開き、次のように変更する。

heroes.component.html

<h2>My Heroes</h2>
<ul class="heroes">
    <li>
        <span class="badge">{{ hero.id }}</span> {{ hero.name }}
    </li>
</ul>

これは一つのヒーローしか表示しないので、リスト化して表示するにはヒーローのリストを反復処理する必要がある。*ngFor<li>要素に追加。

<li *ngFor="let hero of heroes">

この際、プロパティheroが存在しないので、エラーが表示されます。この際、ngForの前の*(アスタリスク)を必ずつける。これがないと動かないので注意しよう。

ヒーローの装飾

ヒーローをCSSファイル等で装飾する際には、CSSファイルとして特定の@Component.styleUrls配列の中で識別されるCSSファイルとして定義する。

src/app/heroes/heroes.component.ts

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})

詳細の表示

クリックイベントのバインディング

クリックイベントのバインディングを<li>にこのように追加する。

heroes.component.html

<h2>My Heroes</h2>
<ul class="heroes">
  <!--追加-->
  <li *ngFor="let hero of heroes" (click)="onSelect(hero)">
      <span class="badge">{{ hero.id }}</span> {{ hero.name }}
  </li>
</ul>

クリックイベントのハンドラー

コンポーネントのheroプロパティをselectedHeroにリネームするが、この場合はまだ割り当てない。

以下のようにしてonSelect()メソッドを追加し、クリックされたヒーローをテンプレートからコンポーネントのselectedHeroに割り当てる。

src/app/heroes/heroes.component.ts

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HEROES } from '../mock-heroes';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
  // 追加
  heroes = HEROES
  selectedHero?: Hero
  onSelect(hero: Hero):void {
    this.selectedHero = hero
  }
  // 追加ここまで
  constructor() {}

  ngOnInit(): void {
  }

}

詳細セクションの追加

現在、コンポーネントテンプレートにはリストがある。リストのヒーローをクリックして、そのヒーローの詳細を表示するには、それをテンプレートでレンダリングするための詳細セクションを追加する必要がある。

heroes.component.htmlのリストセクションの下に以下を追加。

<h2>{{selectedHero.name | uppercase}} Details</h2>
<div><span>id: </span>{{selectedHero.id}}</div>
<div>
  <label for="hero-name">Hero name: </label>
  <input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name">
</div>

アプリケーションを実行すると、エラーが表示されてしまう。これを修正するには、*ngIfを使って空のdetailsを非表示にする必要がある。この際も、ngIfの前にある*を忘れないようにする。

src/app/heroes/heroes.component.html

<div *ngIf="selectedHero">

  <h2>{{selectedHero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{selectedHero.id}}</div>
  <div>
    <label for="hero-name">Hero name: </label>
    <input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name">
  </div>

</div>

この際、ブラウザを更新すると名前の一覧が再度表示される。詳細のエリアは空白になっている。ヒーローのリストの中からヒーローをクリックし、詳細を表示する。

挙動原理

seelctedHeroが定義されていない時、ngIfはDOMからヒーローの詳細を削除する。心配するselectedHeroへのバインディングは存在しない。

ユーザがヒーローを選択するとselectedHeroは値を持ってngIfはヒーローの詳細をDOMの中に挿入する。

選択されたヒーローを装飾する

選択されたヒーローを装飾するために、先に追加したスタイルの中にある.selectedというCSSクラスを追加できる。ユーザがクリックした時に.selectedクラスを<li>に適用するためには、クラスバインディングを使用する。

Angularのクラスバインディングは条件に応じてCSSクラスを追加したり削除したりできる。装飾したい要素に[class.some-css-class]="some-condition"を追加するだけで動く。

heroes.component.html

<h2>My Heroes</h2>
<ul class="heroes">
    <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)">
        <span class="badge">{{hero.id}}</span> {{hero.name}}
    </li>
</ul>

<div *ngIf="selectedHero">

    <h2>{{selectedHero.name | uppercase}} Details</h2>
    <div><span>id: </span>{{selectedHero.id}}</div>
    <div>
        <label for="hero-name">Hero name: </label>
        <input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name">
    </div>

</div>

src/app/heroes/heroes.component.css

.heroes {
  margin: 0 0 2em 0;
  list-style-type: none;
  padding: 0;
  width: 15em;
}
.heroes li {
  cursor: pointer;
  position: relative;
  left: 0;
  background-color: #EEE;
  margin: .5em;
  padding: .3em 0;
  height: 1.6em;
  border-radius: 4px;
}
.heroes li:hover {
  color: #2c3a41;
  background-color: #e6e6e6;
  left: .1em;
}
.heroes li.selected {
  background-color: black;
  color: white;
}
.heroes li.selected:hover {
  background-color: #505050;
  color: white;
}
.heroes li.selected:active {
  background-color: black;
  color: white;
}
.heroes .badge {
  display: inline-block;
  font-size: small;
  color: white;
  padding: 0.8em 0.7em 0 0.7em;
  background-color:#405061;
  line-height: 1em;
  position: relative;
  left: -1px;
  top: -4px;
  height: 1.8em;
  margin-right: .8em;
  border-radius: 4px 0 0 4px;
}

input {
  padding: .5rem;
}

これで選択されたヒーローをハイライト表示し、その詳細を表示するSPAを簡単に開発できる。

フィーチャーコンポーネントの作成

単一のコンポーネントにすべての機能を保持しておくと、アプリケーションが成長するにつれて維持できなくなる。大きなコンポーネントを、特定のタスクやワークフローに焦点を当てた小さなサブコンポーネントに分割したいと考えることがある。

HeroDetailComponentの表示

Angular CLIを使用して、hero-detailという新しい名前のコンポーネントを作成する。

$ ng generate component hero-detail

templateの記述

ヒーローの詳細が記されているHeroesComponentのテンプレートの下部から切り取り、HeroDetailComponentテンプレートに生成されたボイラープレートへ貼り付ける。

この際に貼り付けられたHTMLはselectedHeroを参照する。新しいHeroDetailComponentは、選択されたヒーローだけではなく、どんなヒーローにも表示される。したがって、テンプレート内すべてのselectedHeroheroに置換してください。

src/app/hero-detail/hero-detail.component.html

<div *ngIf="hero">

    <h2>{{ hero.name | uppercase }} Details</h2>
    <div><span>id: </span>{{ hero.id }}</div>
    <div>
        <label for="hero-name">Hero name: </label>
        <input id="hero-name" [(ngModel)]="hero.name" placeholder="name">
    </div>

</div>

@Input()heroプロパティを追加

HerodetailComponentクラスのファイルを追加して、Heroシンボルをインポートする。

src/app/hero-detail/hero-detail.component.ts

import { Hero } from '../hero';

heroプロパティは@Input()デコレータで注釈されたinputプロパティでなければなりません。これは、外側のHeroesComponentがこのようにバインドするため。

<app-hero-detail [hero]="selectedHero"></app-hero-detail>

Inputシンボルを含めるために、@angular/coreのimport文を修正する。

hero-detail.component.ts

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

@Inputデコレータが前についたheroプロパティを追加する。

src/app/hero-detail/hero-detail.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {
  @Input() hero?: Hero; // 追加

  constructor() { }

  ngOnInit(): void {
  }

}

HeroDetailComponentを表示する

HeroesComponentテンプレートの更新

HeroDetailComponentのセレクターはapp-hero-detailだ。

ヒーローの詳細ビューがかつて存在したHeroesComponentテンプレートの下部に<app-hero-detail>を追加する。

以下のように、HeroesComponent.selectedHeroを、この要素のheroプロパティにバインドさせる。

heroes.component.html

<app-hero-detail [hero]="selectedHero"></app-hero-detail> 

[hero]="selectedHero"はAngularのプロパティバインディング。

これは、HeroesComponentselectedHeroプロパティから、ターゲット要素のheroプロパティへの単方向データバインディングである。これは、HeroDetailComponentheroプロパティがマッピングされる。

ユーザがリスト内のヒーローをクリックすると、selectedHeroが変更される。selectedHeroが変更されると、プロパティバインディングはheroを更新して、HeroDetailComponentは新しいヒーローを表示する。

heroes.component.html

<h2>My Heroes</h2>

<ul class="heroes">
  <li *ngFor="let hero of heroes"
    [class.selected]="hero === selectedHero"
    (click)="onSelect(hero)">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

<app-hero-detail [hero]="selectedHero"></app-hero-detail>

変更点

以前は、ユーザがヒーロー名をクリックする際に、ヒーローのリストの下にヒーローの詳細が表示されていた。

今ではHeroDetailComponentHeroesComponentの代わりにそれらの詳細を示している。

  • HeroComponentのスコープを減少。
  • 親のHeroesComponentに触れることなく、HeroDetailComponentをリッチなヒーローエディタに進化。
  • ヒーローの詳細ビューに触れることなく。HeroesComponentを進化。
  • 将来のコンポーネントのテンプレートで、HeroDetailComponentを再利用。

サービスの追加

アプリケーションを開発する際に、コンポーネント内で直接データの保存や取得を行うべきではない。コンポーネントはデータの受け渡しだけに集中し、その他の処理はサービスクラスへ委譲するべき。

HeroServiceの作成

Angular CLIを作成し、Hero Serviceを作成する。

$ ng generate service hero

このコマンドはHeroServiceのスケルトンファイルをsrc/app/hero.serive.tsに以下のように保存する。

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

@Injectable({
  providedIn: 'root'
})
export class HeroService {

  constructor() { }
}

@Injectable()サービス

生成されたファイルの中でAngularのInjectableシンボルがインポートされ、@Injectable()デコレータとしてクラスを注釈することに注目する。これは、クラスを依存関係システムに参加するものとしてマークする。HeroSeriveクラスは、注入可能なサービスを提供する予定で、それ自身が依存関係を持てる。

ヒーローデータの取得

HeroServiceは様々な場所からヒーローデータを取得することがある。

コンポーネントからデータ取得ロジックを切り離すことで、そのようなサービス側の事情に関係なくいつでも実装方針の変更ができます。コンポーネント側は、サービスがどのように動いているのか関係ありません。

HeroHEROESをインポート。

src/app/hero.service.ts

import { Hero } from './hero';
import { HEROES } from './mock-heroes';

getHeroesメソッドを追加し、モックヒーローを完成する。

src/app/hero.service.ts

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';

@Injectable({
  providedIn: 'root'
})
export class HeroService {

  // 追加
  getHeroes(): Hero[] {
    return HEROES
  }

  constructor() { }
}

HeroServiceの提供

AngularがHeroesComponentへ注入する前に、プロバイダを登録することでHeroServiceが依存性の注入システムで利用できる必要がある。プロバイダーとは、サービスを作成あるいは提供できるもの。この場合は、HeroServiceクラスをインスタンス化してサービスを提供する。

HeroServiceをインジェクター(必要な場所でプロバイダーを選択して注入するためのオブジェクト)に登録することで、サービスを提供できるようになる。

デフォルトでは、Angular CLIコマンドng generate serviceは、プロバイダーのメタデータ、言い換えればprovidedIn: 'root'@Injectable()デコレータに含めることで、プロバイダーをサービスのルートインジェクターに登録できる。

@Injectable({
  providedIn: 'root',
})

ルートレベルでサービスを提供すると、AngularはHeroServiceの単一の共有インスタンスを作成し、それを要求する任意のクラスに注入する。@Injectableメタデータでプロバイダーを登録すると、Angularはサービスが使用されなくなった際にそれを削除することでアプリケーションを最適化できる。

HeroComponentの更新

heroes.component.ts

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service'; // HEROESのインポートを削除し、HeroServiceを代わりにインポートする。

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
  heroes: Hero[] = [] // heroesプロパティの定義を宣言に置換する
  selectedHero?: Hero
  onSelect(hero: Hero):void {
    this.selectedHero = hero
  }
  constructor(private heroService: HeroService) {} // HeroServiceの注入

  getHeroes(): void {
    this.heroes = this.heroService.getHeroes() // サービスからヒーローデータを取得するためのメソッドを作成する
  }

  ngOnInit(): void {
    this.getHeroes() // 作成したメソッドはngOnInit()で呼び出す。
  }

}

【補足】
getHeroes()はコンストラクターでも呼び出せるが、これは最適な方法ではない。

原則、Angularにおけるコンストラクターではプロパティ定義の簡単な初期化だけを行い、それ以外は何もするべきではない。実際にデータを取得する際に行うサーバへのHTTPリクエストを行う関数は呼び出すべきではない。

getHeroes()はコンストラクターではなく、ngOnInitライフサイクルフックで呼び出す。

Observableデータ

HeroService.getHeroes()は同期的なメソッドで、これはHeroServiceが即座にヒーローデータを取得できることを意味する。

また、HeroesComponentgetHeroes()の返り値がまるで同期的に取得できるかのように扱える。

src/app/heroes/heroes.component.ts

this.heroes = this.heroService.getHeroes();

しかし、これは本番環境のアプリケーションでは機能しない。

今のアプリケーションはモックヒーローを返しているのでこれを免れていますが、リモートサーバからヒーローデータを取得するにあたってこの処理は非同期ということに気づく。

**HeroServiceはサーバのレスポンスを持つ必要があり、getHeroes()は即座にヒーローデータを返せない。**そしてそのサービスが待機している間は、ブラウザはブロックされないだろう。

HeroService.getHeroes()は何らかの非同期処理を実装する必要がある。

Observable HeroService

ObservableRxJSライブラリで重要なクラスの一つ。RxJSのof()を使ってサーバからのデータ取得を行う。

HeroServiceを開いて、Observable及びofRxJSからインポートする。

src/app/hero.service.ts

import { Observable, of } from 'rxjs';

getHeroes()を以下のように書き直す。

src/app/hero.service.ts

getHeroes(): Observable<Hero[]> {
  const heroes = of(HEROES);
  return heroes;
}

of(HEROES)は一つの値、すなわちモックヒーローの配列を出力するObservable<Hero[]>を返す。

HeroesComponentでのSubscribe

HeroService.getHeroesメソッドはHero[]を返していたが、現在の返り値はObservable<Hero[]>である。そのため、これらの違いを修正する必要がある。

getHeroesを開いて、以下のコードに変更する。

heroes.component.ts(Observable)

getHeroes(): void {
  this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes)
}

heroes.component.ts(Original)

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

Observable.subscribe()は重要な違い。

変更後のバージョンでは、Observableがヒーローの配列を出力するのを待つ。これは現在あるいは数分後に起こる可能性が高い。

そのとき、subscribe()メソッドは出力された配列をコールバックに渡し、コンポーネントのheroesプロパティを設定する。

この非同期的手法は、HeroServiceがサーバからヒーローを取得する際に正常に動作。

メッセージの表示

このセクションでは、以下の方法について詳細に説明する。

  • メッセージを表示するためのMessageComponentを画面下部に表示
  • 表示するメッセージを送信するために、アプリケーション全体で注入可能なMessageServiceを作成
  • HeroServiceMessageServiceを注入
  • HeroServiceのデータ取得成功時にメッセージを表示

MessagesComponentの作成

Angular CLIでMessagesComponentの作成。

$ ng generate component messages

Angular CLIはsrc/app/messages配下にコンポーネントファイル群を生成し、AppModule内にMessagesComponentを宣言する。

作成したMessagesComponentを表示するために、AppComponentのテンプレートを修正する。

src/app/app.component.html

<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>

MessageServiceの作成

Angular CLIを作成し、src/app配下にMessageServiceを作成する。

$ ng generate service message

MessageServiceを開いて、以下のコードへ修正する。

src/app/message.service.ts

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

@Injectable({
  providedIn: 'root',
})
export class MessageService {
  messages: string[] = [];

  add(message: string) {
    this.messages.push(message);
  }

  clear() {
    this.messages = [];
  }
}

HeroServiceへの注入

HeroServiceMessageServiceをインポート。

src/app/hero.service.ts

import { MessageService } from './message.service';

プライベートなmessageServiceプロパティを宣言するパラメータを使ってコンストラクタを変更する。AngularはHeroServiceを生成する際に、そのプロパティへシングルトンなMessageServiceを注入する。

src/app/hero.service.ts

constructor(private messageService: MessageService) { }

HeroServiceからメッセージを送る

ヒーローが取得された時にメッセージを送信するようにgetHeroes()メソッドを変更。

src/app/hero.service.ts

getHeroes(): Observable<Hero[]> {
  const heroes = of(HEROES);
  this.messageService.add('HeroService: fetched heroes');
  return heroes;
}

HeroServiceからのメッセージを表示する

MessagesComponentHeroServiceがヒーローを取得した際に送信するメッセージを含めて、全てのメッセージを表示しなければならない。

MessagesComponentを開いて、MessageServiceをインポート。

src/app/messages/messages.component.ts

import { MessageService } from '../message.service';

MessageServiceへのバインド

Angular CLIで生成されたMessagesComponentのテンプレートを下記コードへ置き換えましょう。

src/app/messages/messages.component.html

<div *ngIf="messageService.messages.length">

  <h2>Messages</h2>
  <button class="clear"
          (click)="messageService.clear()">Clear messages</button>
  <div *ngFor='let message of messageService.messages'> {{message}} </div>

</div>

messages.component.cssをコンポーネントのスタイルに追加すると、このメッセージのUIの外観はより良いものになるだろう。

HeroServiceにメッセージの追加

ユーザがヒーローをクリックする際に、メッセージを送信、表示してユーザの選択履歴を表示する方法を示す。

src/app/heroes/heroes.component.ts

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

import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { MessageService } from '../message.service';

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

  selectedHero?: Hero;

  heroes: Hero[] = [];

  constructor(private heroService: HeroService, private messageService: MessageService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
    this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);
  }

  getHeroes(): void {
    this.heroService.getHeroes()
        .subscribe(heroes => this.heroes = heroes);
  }
}

ヒーローリストを見るためにブラウザを更新し、一番下までスクロールするとHeroServiceからのメッセージが表示される。

ヒーローをクリックするたびに、新しいメッセージが選択を登録して表示されるようになる。

ルーティングを使ったナビゲーションの追加

以下の機能を実装する。

  • ダッシュボードビューを追加
  • ヒーローズビューとダッシュボードビューのあいだで行き来できる機能を追加
  • ユーザが各ビューでヒーロー名をクリックした際に、選択されたヒーローの詳細ビューを表示
  • ユーザがemail上でリンクをクリックした時、特定のヒーローの詳細ビューを表示

AppRoutingModuleの追加

Angularのベストプラクティスは、ルートのAppModuleからインポートされるルーティング専用のトップレベルモジュールで、ルーターをロードして管理すること。

モジュールのクラス名はAppRoutingModuleとし、src/appフォルダのapp-routing.module.tsに書く。

CLIで生成できる。

$ ng generate module app-routing --module=app

生成されたファイルは以下のようになる

src/app/app-routing.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';


@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ]
})
export class AppRoutingModule { }

こちらのプログラムを以下のように書き換える。

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

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

最初にアプリケーションにルーティング機能をもたせられるRouterModuleRoutesをインポートする。次のインポートであるHeroesComponentは、ルートを設定することでルーターに向かう場所を教える。

CommonModuleの参照とdeclarations配列は不要。

ルート

ファイルの次の部分は、ルートを構成する場所である。ルートは、ユーザがリンクをクリックした際に、またはURLをブラウザのアドレスバーに貼り付けた際に表示するビューをルーターに伝える。

app-routing.module.tsはすでにHeroesComponentをインポートしているので、routes配列で使える。

src/app/app-routing.module.ts

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

典型的なAngularのRouteは2つの要素を持つ。

  • path:ブラウザのアドレスバーにあるURLにマッチする配列
  • component:そのルートに遷移する際にルーターが作成すべきコンポーネント

これによって、ルーターはそのURLをpath: 'heroes'に一致させて、URLがlocalhost:4200/heroesのようなものに限ってHeroesComponentを表示できる。

@NgModuleメタデータはルーターを初期化してブラウザのロケーションの変更を待機する。

以下の行は、RouterModuleAppRoutingModuleimports配列に追加し、RouterModule.forRoot()を呼び出してワンステップでroutesに追加。

src/app/app-routing.module.ts

imports: [ RouterModule.forRoot(routes) ],

【解説】


アプリケーションのルートのレベルでルーターを設定しているので、このメソッドはforRoot()と呼ばれる。このメソッドは、ルーティングに必要なサービスやプロバイダーとディレクティブを提供し、ブラウザの現在のURLをベースに最初の遷移を行う。

次に、AppRoutingModuleRouterModuleをエクスポートして、アプリケーション全体で利用できるようにする。

src/app/app-routing.module.ts

exports: [ RouterModule ]

RouterOutletの追加

AppComponentテンプレートを開いて、<app-heroes>要素を<router-oulet>に置換

src/app/app.component.html

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

試す

こちらのCLIコマンドを入力

$ ng serve

ブラウザを更新するとアプリケーションのタイトルは表示されるが、ヒーローのリストは表示されない。

ブラウザのアドレスバーを見ると、URLが/で終了。HeroesComponentへのルーターのパスは/heroes。これを入力すればおなじみのヒーローのマスター/詳細ビューが表示されるはず。(実際には表示されていない)

ナビゲーションのリンクを追加(routerLink)

理想的には、ルートのURLをアドレスバーに貼り付けるのではなく、ユーザがリンクをクリックして遷移できるようにする必要がある。

<nav>要素を追加して、その中にクリックされるとHeroesComponentへ遷移するトリガーになるアンカー要素を追加。修正されたAppComponentテンプレートは以下のようになる。

src/app/app.component.html

<h1>{{title}}</h1>
<nav>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

この際、routerLink属性は、ルーターがHeroesComponentへのルートとして一致する文字列である"/heroes"に設定される。このrouterLinkは、ユーザのクリックをルーターのナビゲーションへ変換するRouterLinkディレクティブのためのセレクターである。

このとき、ブラウザを更新するとアプリケーションのタイトルとヒーローのリンクは表示されるが、ヒーローのリストは表示されない。

ダッシュボードビューを追加

ルーティングは、複数のビューがある場合に更に意味を持つ。CLIを使ってDashBoardComponentを追加する。

$ ng generate component dashboard

CLIは、DashBoardComponentのためのファイルを生成し、AppModuleの中でそれを宣言する。これら3つのファイルのデフォルト内容を以下のように書き換える。

src/app/dashboard/dashboard.component.html

<h2>Top Heroes</h2>
<div class="heroes-menu">
  <a *ngFor="let hero of heroes">
    {{hero.name}}
  </a>
</div>

src/app/dashboard/dashboard.component.ts

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(private heroService: HeroService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes.slice(1, 5));
  }
}

src/app/dashboard/dashboard.component.css

/* DashboardComponent's private CSS styles */

h2 {
  text-align: center;
}

.heroes-menu {
  padding: 0;
  margin: auto;
  max-width: 1000px;

  /* flexbox */
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-around;
  align-content: flex-start;
  align-items: flex-start;
}

a {
  background-color: #3f525c;
  border-radius: 2px;
  padding: 1rem;
  font-size: 1.2rem;
  text-decoration: none;
  display: inline-block;
  color: #fff;
  text-align: center;
  width: 100%;
  min-width: 70px;
  margin: .5rem auto;
  box-sizing: border-box;

  /* flexbox */
  order: 0;
  flex: 0 1 auto;
  align-self: auto;
}

@media (min-width: 600px) {
  a {
    width: 18%;
    box-sizing: content-box;
  }
}

a:hover {
  background-color: #000;
}

ダッシュボードのルートを追加

ダッシュボードに遷移するには、ルーターに適切なルートが必要である。app-routiung.module.tsDashboardComponentをインポート。

src/app/app-routing.module.ts

import { DashboardComponent } from './dashboard/dashboard.component';

routes配列に、DashboardComponentへのパスにマッチするルートを追加。

src/app/app-routing.module.ts

{ path: 'dashboard', component: DashboardComponent },

デフォルトルートの追加

アプリケーションを起動すると、ブラウザのアドレスバーはWebサイトのルートを指す。これは既存のルートと一致しないので、ルーターはどこにも移動しない。<router-outlet>の下のスペースが空白になっているからだ。

アプリケーションをダッシュボードに自動的に遷移するには、以下のルートをroutes配列に追加。

src/app/app-routing.module.ts

{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },

このルートは、空のパスと完全に一致するURLを、パスが/dashboardであるルートにリダイレクトする。

ブラウザが更新されると、ルーターはDashboardComponentをロードし、ブラウザのアドレスバーには/dashboardのURLが表示される。

ダッシュボードのリンクをシェルに追加

ユーザはページのトップにあるナビゲーション領域のリンクをクリックすることで、DashboardComponentHeroesComponentのあいだを行き来することができます。

Heroesリンクの上、AppComponentシェルテンプレートにダッシュボードのナビゲーションリンクを追加する。

src/app/app.component.html

<h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

ブラウザが更新されると、リンクをクリックすることで二つのビューの間を自由に遷移できるようになる。

ヒーローの詳細ページを表示する

HeroDetailComponentは選択されたヒーローの詳細を表示する。

  1. ダッシュボードのヒーローをクリックする
  2. ヒーローリストのヒーローをクリックする
  3. 表示するヒーローを識別するブラウザのアドレスバーにディープリンクURLを貼り付ける

HeroesComponentからヒーローの詳細を削除する

ユーザがHeroesComponentで一つのヒーローをクリックすると、アプリはHeroDetailComponentに遷移する必要があり、ヒーローリストビューをヒーロー詳細ビューに置換する。

HeroesComponentテンプレート(heroes/heroes.component.html)を開いて、<app-hero-detail>要素を一番下から削除する

ヒーローの詳細を表示するルートを設定

app-routing.module.tsを開いて、HeroDetailComponentをインポートする。

src/app/app-routing.module.ts

import { HeroDetailComponent } from './hero-detail/hero-detail.component';

次に、ヒーロー詳細ビューへのパスのパターンと一致するパラメータ付きルートをroutes配列に追加する。

src/app/app-routing.module.ts

{ path: 'detail/:id', component: HeroDetailComponent },

▲上記のプログラムと同じファイル

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent }
];

DashboardComponentヒーローへのリンク

現時点ではDashboardComponentヒーローへのリンクは何もしない。

ルーターはHeroDetailComponentへのルートを持っているので、ダッシュボードのリンクを修正してパラメータ付きダッシュボードのルート経由で遷移する。

src/app/dashboard/dashboard.component.html

<a *ngFor="let hero of heroes"
  routerLink="/detail/{{hero.id}}">
  {{hero.name}}
</a>

*ngForリピーター内でAngularの補間バインディングを活用し、現在の繰り返しのhero.idを個々のrouterLinkに挿入する。

HeroesComponentヒーローへのリンク

HeroesComponentのヒーローのアイテムは、コンポーネントのonSelect()メソッドにバインディングされたクリックイベントを持つ<li>要素。

src/app/heroes/heroes.component.html

<ul class="heroes">
  <li *ngFor="let hero of heroes"
    [class.selected]="hero === selectedHero"
    (click)="onSelect(hero)">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

こちらの<li>要素を*ngForだけを持つように戻して、アンカー要素(<a>)でバッジと名前を囲み、ダッシュボードのテンプレートと同じようにアンカーにrouterLink要素を追加。

src/app/heroes/heroes.component.html

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
  </li>
</ul>

プライベートなスタイルシート(heroes.component.css)を修正して、これまでと同じようにリストが見えるようにする。

不要なコードを削除

HeroesComponentクラスはまだ動作するが、onSelect()メソッドとselectedHeroプロパティはもはや使われない。

不要なコードを削除する。(必要最低限の機能で実装するため)

src/app/heroes/heroes.component.ts

export class HeroesComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(private heroService: HeroService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes);
  }
}

ルーティング可能なHeroDetailComponent

HeroesComponentは表示するヒーローを取得するための新しい方法が必要。

  • それを作成したルートを取得
  • ルートからidを抽出
  • HeroServiceを経由してサーバからそのidでヒーローを取得する

src/app/hero-detail/hero-detail.component.ts

import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';

import { HeroService } from '../hero.service';

ActivatedRouteHeroServiceLocationサービスをコンストラクタに入れて、それらの値をプライベートフィールドに保存。

src/app/hero-detail/hero-detail.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { Hero } from '../hero';

import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';

import { HeroService } from '../hero.service';

@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {
  @Input() hero?: Hero;

  // 追加
  constructor(
    private route: ActivatedRoute, // HeroDetailComponentのインスタンスのルートに関する情報を保持
    private heroService: HeroService, // リモートサーバからヒーローのデータを取得し、このコンポーネントはそれを使用して表示するヒーローを取得
    private location: Location // ブラウザと対話するためのAngularサービス。
  ) { }

  ngOnInit(): void {
  }

}

idルートパラメータを抽出

ngOnInit()ライフサイクルハックで、getHero()を呼び出して以下のように定義する。

src/app/hero-detail/hero-detail.component.ts

ngOnInit(): void {
  this.getHero();
}

getHero(): void {
  const id = Number(this.route.snapshot.paramMap.get('id'));
  this.heroService.getHero(id)
    .subscribe(hero => this.hero = hero);
}

route.snapshotは、コンポーネントが作成された直後のルート情報の静的イメージ。paramMapは、URLから抽出されたルートパラメータ値の辞書。idキーは、fetchするヒーローのidを返す。

ルートパラメータは常に文字列。JavaScriptのNumber関数は文字列を数値に変換。

しかし、上記のプログラムではプライベート変数heroServicegetHero()メソッドが定義されていないので、コンパイルエラーが表示される。

HeroService.getHero()の追加

HeroServiceを開いて、getHeroes()メソッドの後にidとともに次のgetHero()メソッドを追加する。

src/app/hero.service.ts

getHero(id: number): Observable<Hero> {
  // 現時点では、このプログラムではHeroとidの型が一致しない型エラーが発生する。対処法は後述
  const hero = HEROES.find(h => h.id === id)!;
  this.messageService.add(`HeroService: fetched hero id=${id}`);
  return of(hero);
}

このとき、idを埋め込むためのJavaScriptのテンプレートリテラルを定義するバッククォートに注意すること。

上記のプログラムでは、getHero()を呼び出すHeroDetailComponentを変更することなく、実際のHttpリクエストとしてgetHero()を再実装できる。

戻る方法を探す

ブラウザの戻るボタンをクリックすると、詳細ビューに来た時の経路によって、ヒーローリストまたはダッシュボード画面に戻れる。

HeroDetailビュー上にそのような挙動を実装できるボタンを表示。コンポーネントのテンプレートの最後に戻るボタンを追加して、コンポーネントのgoBack()メソッドにバインド。

src/app/hero-detail/hero-detail.component.html

<button (click)="goBack()">go back</button>

前述の通り導入したLocationサービスで、ブラウザの履歴の一つ前にナビゲートするgoBack()メソッドをコンポーネントのクラスに追加。

src/app/hero-detail/hero-detail.component.ts

goBack(): void {
  this.location.back();
}

最後に、独自のCSSスタイルをhero-detail.component.cssに追加すると、詳細がより美しく表示される

src/app/hero-detail/hero-detail.component.css

/* HeroDetailComponent's private CSS styles */
label {
  color: #435960;
  font-weight: bold;
}
input {
  font-size: 1em;
  padding: .5rem;
}
button {
  margin-top: 20px;
  background-color: #eee;
  padding: 1rem;
  border-radius: 4px;
  font-size: 1rem;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #ccc;
  cursor: auto;
}

サーバからデータの取得

AngularのHttpClientを使ってCRUDモデルを構築する

  • HeroServiceはHTTPリクエストを介してヒーローデータを取得
  • ユーザはヒーロー情報を追加、編集、削除でき、その変更をHTTP経由で伝達できる。
  • ユーザは名前でヒーロー情報を検索できる

HTTPサービスの有効化

HttpClientはHTTPを通してリモートサーバと通信するための仕組み。

HttpClientをインポートしてルートのAppModuleに追加。

src/app/app.module.ts

import { HttpClientModule } from '@angular/common/http';

また、AppModuleHttpClientModuleimports配列に追加する。

src/app/app.module.ts

import { HttpClientModule } from '@angular/common/http';
...
@NgModule({
  imports: [
    HttpClientModule,
  ],
})

データサーバをシミュレートする

In-memory Web APIでリモートサーバとの通信を再現する。

このモジュールをインストールすると、アプリケーションはインメモリWeb APIがリクエストをインターセプトして、そのリクエストをインメモリデータストアに適用してシミュレートされたレスポンスを返えさずにHttpClientでリクエストを送信、レスポンスを受信できます。

以下のコマンドを使って、npmからAPIをインストール。

npm install angular-in-memory-web-api --save

AppModuleで、HttpClientInMemoryWebApiModuleと、これからすぐに作成するInMemoryDataServiceクラスをインポート。

src/app/app.module.ts

import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';

HttpClientModuleの後に、HttpClientInMemoryWebApiModuleAppModuleimports配列に追加し、InMemoryDataServiceで設定。

HttpClientModule,


HttpClientInMemoryWebApiModule.forRoot(
  InMemoryDataService, { dataEncapsulation: false }
)

forRoot()設定メソッドは、インメモリデータベースを準備するInMemoryDataServiceクラスを取る。

以下のコマンドでsrc/app/in-memory-data.service.tsクラスを生成

ng generate service InMemoryData

in-memory-data.service.tsのデフォルトの内容を以下のものに置き換える。

src/app/in-memory-data.service.ts

import { Injectable } from '@angular/core';
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hero } from './hero';

@Injectable({
  providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    const heroes = [
      { id: 11, name: 'Dr Nice' },
      { id: 12, name: 'Narco' },
      { id: 13, name: 'Bombasto' },
      { id: 14, name: 'Celeritas' },
      { id: 15, name: 'Magneta' },
      { id: 16, name: 'RubberMan' },
      { id: 17, name: 'Dynama' },
      { id: 18, name: 'Dr IQ' },
      { id: 19, name: 'Magma' },
      { id: 20, name: 'Tornado' }
    ];
    return {heroes};
  }

  // Overrides the genId method to ensure that a hero always has an id.
  // If the heroes array is empty,
  // the method below returns the initial number (11).
  // if the heroes array is not empty, the method below returns the highest
  // hero id + 1.
  genId(heroes: Hero[]): number {
    return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
  }
}

この際、in-memory-data.service.tsファイルはmock-heroes.tsの機能を継承。しかし、まだmock-heroes.tsは削除しない。

サーバが用意できたら、アプリケーションのリクエストはサーバに送信される。

余談

Angularはコマンド入力だけで機能を実装するのに必要なプロジェクトを簡単に出力できるのが最大の特徴。フロントエンドフレームワークと呼ばれているので、柔軟性は低いけど簡単にフロントエンドを構築できる。

最近の開発でハマっているNestはAngularのバックエンドバージョンみたいなものである。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.