ふらぱいく

備忘録。いい加減つける

Angular で ComponentファイルをRenameしたときに怒られたのでメモ

環境

  • Angular CLI: 8.0.1
  • Node: 12.3.1
  • WebStorm: 2019.1.02

怒られた

Angular入門してみる_6 でAppComponent を Members.Component.ts にリネームした際,以下のようなエラーが出た。

src/app/app.component.spec.ts(1,10): error TS2305: Module '"G:/noinoi/Documents/Angular_study/04-6/src/app/app.com
ponent"' has no exported member 'MembersComponent'.

コンポーネントを見つけられなかった?

原因?

app.component.ts を作成したときに同時に作成される app.component.spec.ts に注目。
memebers.component.ts にリネームした際に,コード内のコンポーネント名だけリネームされて,ファイル名はapp.component.spec.tsのまま残っていたのが原因?

import { MembersComponent } from './app.component';

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By }           from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

describe('MembersComponent', function () {
  let de: DebugElement;
  let comp: MembersComponent;
  let fixture: ComponentFixture<MembersComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ MembersComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(MembersComponent);
    comp = fixture.componentInstance;
    de = fixture.debugElement.query(By.css('h1'));
  });

  it('should create component', () => expect(comp).toBeDefined() );

  it('should have expected <h1> text', () => {
    fixture.detectChanges();
    const h1 = de.nativeElement;
    expect(h1.innerText).toMatch(/angular/i,
      '<h1> should say something about "Angular"');
  });
});

解決方法

MembersComponent を すべて AppComponentにリファクタリング

import { AppComponent } from './app.component';

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By }           from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

describe('AppComponent', function () {
  let de: DebugElement;
  let comp: AppComponent;
  let fixture: ComponentFixture<AppComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ AppComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    comp = fixture.componentInstance;
    de = fixture.debugElement.query(By.css('h1'));
  });

  it('should create component', () => expect(comp).toBeDefined() );

  it('should have expected <h1> text', () => {
    fixture.detectChanges();
    const h1 = de.nativeElement;
    expect(h1.innerText).toMatch(/angular/i,
      '<h1> should say something about "Angular"');
  });
});

なおった。
ちなみに「~.spec.ts」はテストクラスのファイルだそうです。

Angular 入門してみる 7

今回は前回行ったルーティングを用いて新たにDashboard ページに遷移するための実装を行う。
今回はいきなり実装。

環境

  • Angular CLI: 8.0.1
  • Node: 12.3.1

実装

DashboardComponentを作成

中身なんもないけどとりあえず作成。templateUrl は,与えられたurlのhtmlファイルを参照する。htmlファイルについては後ほど実装。

import { Component } from '@angular/core';
@Component({
  selector: 'my-dashboard',
  templateUrl: './dashboard.component.html'
})

export class DashboardComponent {
  
}
リダイレクト

今回はトップページにリンクした際,このDashboardComponent にリダイレクトするよう実装を行う。
app.module.ts の forRoot の属性に以下を追加する。

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

path にリダイレクト元のページ,redirectTo にリダイレクト先のページを,pathMatchにはfullを設定する。pathMatch: 'full' の場合はパスが完全一致した場合遷移する。
パスの照合方法(prefix:前方一致、full:完全一致、デフォルトはprefix)

URLとリンクでコンポーネント表示を切り替える「Angular 2」のルーター (1/3):CodeZine(コードジン)

DashboardComponent をModule に加える

いつもの流れ。先程のリダイレクトを含めた Routing の追加もココで行っておく。

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

import {AppComponent} from './app.component';
import { MembersComponent }  from './members.component';
import { MemberDetailComponent } from './member-detail.component';
import { DashboardComponent} from './dashboard.component';  // 追加
import {MemberService} from './member.service';

@NgModule({
  imports:      [
    BrowserModule,
    FormsModule,
    // routerの設定
    RouterModule.forRoot([
      {
        path: 'members',  // pathにmemberを指定,
        component: MembersComponent  // MembersComponentを指定
      },
      {
        path: 'dashboard', // 追加
        component: DashboardComponent // 追加
      },
      {
        path: '', // 追加
        redirectTo: '/dashboard',  // 追加
        pathMatch: 'full' // 追加
      }
    ])
  ],
  declarations: [
    AppComponent,
    MembersComponent,
    MemberDetailComponent,
    DashboardComponent, // 追加
  ],
  bootstrap:    [
    AppComponent
  ],
  providers: [
    MemberService
  ]
})
export class AppModule { }

実行。
f:id:noinoi_monstS4l:20190609172255p:plain

DashboardComponent に機能を追加

今のままでは味気ないのでMEMBERSの上位4人を表示させるよう実装。
まず,社員一覧ページ同様,member.service を用いて MEMBERS を受け取る。
流れの復習
1. OnInit, MemberService をそれぞれimport
2. OnInit を DashboardComponent に継承
3. コンストラクタでmemberService を注入
4. 強制された ngOnInit メソッドにmemberService を用いて上位4名のmembers を取得

import { Component, OnInit } from '@angular/core';
import {Member} from './member';
import {MemberService} from './member.service';
@Component({
  selector: 'my-dashboard',
  templateUrl: './dashboard.component.html'
})

export class DashboardComponent implements OnInit {

  members: Member[];

  constructor(private memberService: MemberService) {
  }

  ngOnInit(): void {
    this.memberService.getMembers()
      .then(members => this.members = members.slice(1, 5));
  }
}

dashboard.component.html を作成し,取得したmembersを表示。

<h1>ダッシュボード</h1>
<ul class="grid grid-pad">
  <li *ngFor="let member of members" class="col-l-4">
    <div class="module member">
      <h4>{{member.name}}</h4>
    </div>
  </li>
</ul>

実行
f:id:noinoi_monstS4l:20190609180128p:plain

次回はダッシュボードから社員情報の詳細ページへとリンクするための実装を行う。
今まではただクリックしたmember の詳細を*ngIf を用い表示させていたが,今後はidを取得し,そのidのページに遷移するよう実装していく。

Angular 入門してみる 6

ルーティング 1
ルーティングの設定を行い,ページの遷移を実現。
社員一覧と社員の詳細を別のUrlで出力。今回も実装は後の方。

環境

  • Angular CLI: 8.0.1
  • Node: 12.3.1

お勉強

ルーティングとは?

httpやrailsの場合,httpリクエストによるページ遷移だったが,SPA(SinglePageApplication)ではコンポーネントで呼び出されるDOMだけを書き換えることによって,「見た目上」のページ遷移を実現する。
Angular はルーティングに対する便利なモジュールが存在するので,それを用いて実装。

RouteModule のimport

基本的な流れとして,RouteModule を読み込む,import 配列に追記,DOMで表示させたい場所を,ルーティング指定したコンポーネントを表示させる router-outlet タグに変更

import { RouteModule } from '@angular/router';
...
import [
   ...
   RouteModule
]
@Component({
   selector: 'my-app',
   template: `
      <h1>{{ title }} </h1>
      <--    <label> test </label>   --->   
      < router-outlet ></router-outlet> <-- 追記 -->
    `
})
...
export class AppComponent {
   title = 'タイトル';
}

遷移したページのコンポーネント

@Component({
   selector: 'my-page1',
   template: `
      page1に遷移しました.
   `
})
...

だがこのままではパスが指定されていないため遷移できない。

RouterModule の forRoute メソッド

RouterModule には forRoute メソッドが存在し,指定したパスに指定したコンポーネントを呼び出す。これによってDOMの書き換えによる見かけのページ遷移を行う。

import { RouteModule } from '@angular/router';
...
import [
   ...
   RouteModule.forRoot([
      {
         path: 'page1',
         component: ExampleComponent
      },
      {
         path: 'page2',
         component: Example2Component
      }
]

例えばリンクを押した際に指定したパスへ遷移する単純な実装の場合

@Component({
   selector: 'my-app',
   template: `
      <h1>{{ title }} </h1>
      <a routerLink="/page1"> page1へ遷移 </a>
      <a routerLink="/page2"> page2へ遷移 </a>
      <--    <label> test </label>   --->   
      < router-outlet ></router-outlet> <!-- 追記 -->
    `
...
export class AppComponent {
   title = 'タイトル';
}
@Component({
   selector: 'my-page1',
   template: `
      page1に遷移しました。
    `
})
@Component({
   selector: 'my-page2',
   template: `
      page2に遷移しました。
   `
})

実行結果
page1をクリックした場合

タイトル
page1に遷移しました。

page2をクリックした場合

タイトル
page2に遷移しました。
リダイレクト

リダイレクトとは,特定のページにアクセスがあった場合のページ遷移。 例外処理みたいな感じだけど違う。
例えばpage1にアクセスしたとき,page2 のDOMを表示させたいって場合。
redirectTo メソッド内を下記のように記述。

imports: [
   ...
   RouteModule.forRoute([
      {
         path: 'page1',
         redirectTo: 'page2',
         pathmatch: 'full'
      }
   ])
...

先程のページ遷移の例のように,page1のリンクへアクセスすると,page2のDOMが表示される。

タイトル
page2に遷移しました。

path に遷移元のパス,redirectTo に遷移させたいパス
pathmatch はURLが完全一致した場合という条件を意味する。これを記述しない場合,例えば上の例でいうと,page1/tempにアクセスした場合,page2 にリダイレクトをしてしまう。

リダイレクトに関しては次回実装を行う。

実装

実際に社員名簿管理アプリに実装。
今までは,トップページに社員一覧が表示されていたが,トップページには社員一覧というリンクを作り,そのページにアクセスしたときに一覧を映し出すよう実装。

新しいトップページと,社員一覧ページのファイルを作成

新たにトップページを作成し,社員一覧コンポーネントと分離する必要があるため,今まであったapp.component.ts を members.component.ts にリネーム。また,新しくapp.component.ts を作成。

import { Component, OnInit } from '@angular/core';
import { Member } from './member';
import {MemberService} from './member.service';

@Component({
  selector: 'my-members', // 変更
  template: `
    <h1>{{title}}</h1>
    <h2>社員一覧</h2>
    <ul class="members">
      <li *ngFor="let member of members"
          (click)="onSelect(member)"
          [class.selected]="member === selectedMember">
        <span class="badge">{{member.id}}</span> {{member.name}}
      </li>
    </ul>
    <member-detail [member]="selectedMember"></member-detail>
  `,
  style: ...
})
export class MembersComponent implements OnInit { // Component名も忘れず変更,module.tsへの記述も
  title = '自社社員名簿';
  members: Member[];
  selectedMember: Member;

  constructor(private memberService: MemberService) {
  }

  ngOnInit() {
    this.memberService.getMembers()
      .then(members => this.members = members);
  }

  onSelect(member: Member): void {
    this.selectedMember = member;
  }
}

新しくトップページになるapp.component.ts

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

@Component({
  selector: 'my-app',
  template: `
    <h2>{{title}}</h2>
    
    <!-- href の代わりに routerLinkを設定 -->
    <a routerLink="/members">社員一覧</a> 
    
    <!-- 表示させる場所に記述 -->
    <router-outlet></router-outlet>  
  `
})

export class AppComponent {
  title = '自社社員名簿';
}
Routerモジュールのimport
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import {RouterModule} from '@angular/router';

import {AppComponent} from './app.component';
import { MembersComponent }  from './members.component'; // 追加
import { MemberDetailComponent } from './member-detail.component';
import {MemberService} from './member.service';

@NgModule({
  imports:      [
    BrowserModule,
    FormsModule,
    // routerの設定
    RouterModule.forRoot([
      {
        path: 'members',  // pathにmemberを指定,
        component: MembersComponent  // MembersComponentを指定
      }
    ])
  ],
  declarations: [
    AppComponent,
    MembersComponent, // 追加
    MemberDetailComponent,
  ],
  bootstrap:    [
    AppComponent  // AppComponentに戻しておく
  ],
  providers: [
    MemberService
  ]
})
export class AppModule { }

実行結果。
トップページが非常にシンプルなものに。
f:id:noinoi_monstS4l:20190605004623p:plain

リンクをクリックすると,社員一覧が表示される。
URLにもパスが加えられ,ルーティングが正しく動作している。
また,社員をクリックした場合の詳細画面を表示させるイベントも据え置き。
f:id:noinoi_monstS4l:20190605004811p:plain

以上。

Angular 入門してみる5

そういえば先日Angular8がリリースされました。
今回はServiceとLifeCycleHookと,非同期処理,また Promise を用いた非同期処理について少しやる。

はじめは概要,実装はココから

環境
  • Angular CLI: 8.0.1
  • Node: 12.3.1

Service,LifeCycleHook,非同期処理,Promise について

① Serviceとは?

データ共有の方法の一つ。ゆくゆくはデータベースからデータを引っ張ってくるため実装。

② LifeCycleHookとは?

コンポーネントの生成直後から破棄されるまでの流れ。らしい。
constructor実行後
ngOnChanges
ngOnInit
ngDoCheck
ngAfterCntentInit
ngAfterContentChecked
ngAfterViewInit
ngAfterViewChecked
ngOnDestroy
の順で実行される。らしい。

今回扱うのは ngOnInit で,こちらは constructor による初期化後,更に言うと @Input で設定された値が処理された後に一度だけ実行される。らしい。つまり初期化時に,@Input に関係する処理を行いたい場合,間に合わない(constructor では@Input の処理は行われていないため)。
よって,Angularコンポーネントでは,初期化処理はngOnInitを用いる。

③ 非同期処理とは?

同期的でない処理。そのまますぎる。例を見たほうが早い。
まずは同期処理の例。

console.log("start");
sleep(milliSeconds) {
  let startTime = new Date().getTime();
  while (new Date().getTime() < startTime + milliSeconds);

  console.log("sleepが完了。");
}

sleep(10000); // 10秒経過するまで何もしない

console.log("end");

実行。

start
sleepが完了。
end

上から順番に実行される。

次に非同期処理の例。

1
2
console.log("start");

let f = require('f');
f.readFile("example.txt", "utf-8", function(err, data){
  if (err) throw err;
  console.log("ファイルの読み取り準備ができました。");
  console.log(data);
});

console.log("end");

実行。

start
end
ファイルの読み取り準備ができました。
1
2

endが先に出力される。f.readFileは非同期型関数として扱われ,中の処理が完了した段階で呼び出し元に通知する(コールバック)。
コールバック関数は度々ネストが深くなり,複雑になる傾向がある。
したがってできるだけ同期的に書きたい。どうする? → 同期的に書く。
具体的には? → 複数あるが,ここでは Promise を用いる。

④ Promise って?

本来返したい値をプロミスオブジェクトとして持たせ,返せる場合,プロミスオブジェクトを通して呼び出し元に値を返す。
ネストが深くならず,分かりやすい程度しかまだ知らないけどその認識で合ってる?

console.log('start');

function puts(str) {
  return new Promise(function(resolve) {     // 1. Promise を new して、promiseオブジェクトを返す
    setTimeout(function() {
      resolve(str);
    }, 1000);
  });
}

// 2.1 のpromiseオブジェクトに対して .then で値が返ってきた時のコールバックを設定する。
puts('async').then(function(result) {
  console.log(result)
});

console.log('end');

実行

start
end
async

最後にasyncが返り非同期処理が完了した。


実際に社員名簿管理アプリに実装

ようやく本題に入れる。

外部データの作成

まずは,app.component.ts にいたメンバーの配列 MEMBERS を別ファイルに移動。
ここでは mock.members.ts というファイルを新規作成し移動。

import {Member} from './member';  // 外部参照するため export しておく

export const MEMBERS: Member[] = [
  { id: 10, name: 'test10' },
  { id: 11, name: 'test11' },
  { id: 12, name: 'test12' },
  { id: 13, name: 'test13' },
  { id: 14, name: 'test14' },
  { id: 15, name: 'test15' },
];

app.component.ts の members の初期化を,Memberクラスの配列に修正しておく。

export class AppComponent  {
  title = '自社社員名簿';
  members: Member[];  // 修正
  selectedMember: Member;

  onSelect(member: Member): void {
    this.selectedMember = member;
  }
}
Service の実装

つづいて,この mock.members.ts を共有させる役割を持つService を作成する。ここで,ファイル名 member.service.ts を作成。
mock.members.ts の MEMBERS を受け取る getMembers メソッドを定義しておく。これを使い回すことで共有化を実現。

import {Injectable} from '@angular/core';
import {MEMBERS} from './mock.members';
import {Member} from './member';

@Injectable()
export class MemberService {
  getMembers(): Member[] { 
    return MEMBERS;
  }
}

Service は app.module に providerとして記述することで扱うことができる。
NgModule デコレータの中に追記。

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

import { AppComponent }  from './app.component';
import { MemberDetailComponent } from './member-detail.component';
import {MemberService} from './member.service';  // 追記

@NgModule({
  imports:      [
    BrowserModule,
    FormsModule
  ],
  declarations: [
    AppComponent,
    MemberDetailComponent,
  ],
  bootstrap:    [
    AppComponent
  ],
  providers: [        
    MemberService  // 追記
  ]
})
export class AppModule { }

これで Service が提供できる体制が整う。

LifeCycleHook の実装

1. 先述の通りLifeCycleHook は constructor ののち実行されるので,constructor を記述し,ここでmemberServiceを呼び出す。
2. LifeCycleHook の メソッドの1つ ngOnInit() を用いる。インターフェイスであるため,import だけでなく implements で継承しなくてはならない。これで ngOnInit() の実装が強制されるので,getMembers() を発火。

import { Component, OnInit } from '@angular/core';  // import 
import { Member } from './member';
import {MemberService} from './member.service';   // こちらも忘れずにimport 

@Component ({
          ...  
})

export class AppComponent implements OnInit {
  title = '自社社員名簿';
  members: Member[];
  selectedMember: Member;

  constructor(private memberService: MemberService) { 
      // constructor の引数にmemberService を持ってくる
  }

  ngOnInit() {  // OnInit 継承による強制メソッド。constructorによる初期化が終わったら発火。
    this.members = this.memberService.getMembers();   // memberService の getMembers() を発火。
  }

  onSelect(member: Member): void {
    this.selectedMember = member;
  }
}

一旦実行。問題なく動いているが,同期的であり,非同期処理を施す必要がある。

Promise オブジェクトを用いた非同期処理の実現

Promise の実装は思いのほか楽。
まずは Promise 型にしたい memberService の getMembers() から。Promiseで返したい関数の型に Promise 型を指定し,返り値を resolve で返してやる。

import {Injectable} from '@angular/core';
import {MEMBERS} from './mock.members';
import {Member} from './member';

@Injectable()
export class MemberService {
  getMembers(): Promise<Member[]> {
    return Promise.resolve(MEMBERS);
  }
}

次にapp.component.ts の ngOnInit() で,先程の関数から Promise オブジェクトを受け取り非同期処理を実行する。

import { Component, OnInit } from '@angular/core';  // import 
import { Member } from './member';
import {MemberService} from './member.service';   // こちらも忘れずにimport 

@Component ({
          ...  
})

export class AppComponent implements OnInit {
  title = '自社社員名簿';
  members: Member[];
  selectedMember: Member;

  constructor(private memberService: MemberService) { 
      // constructor の引数にmemberService を持ってくる
  }

  ngOnInit() {     
    // this.members = this.memberService.getMembers();   
    this.memberService.getMembers()
         .then(member => this.member = member);
    // then: resolve の場合の処理。
    // thenで帰ってきた Promise 型オブジェクト(member) を this.member に代入。
  }

  onSelect(member: Member): void {
    this.selectedMember = member;
  }
}

ここで,ngOnInit() 内の then() 内の 「members」は Promise で返ったオブジェクトを,「this.members」はAppComponent クラスで定義した members プロパティであることに注意。

実行。動作は変わらないが,内部では非同期処理となっている。
f:id:noinoi_monstS4l:20190604012645p:plain

以上。

Angular 入門してみる4

Angular 入門

今回はコンポーネントの分割,外部データの参照。

template部分を機能ごとに分割

member一覧とclickした際の詳細画面をそれぞれ別のComponentとして扱う。
詳細画面ファイルmember-detail.component.tsを作成し,selectedMemberクラスのdivタグをtemplateに移動。

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

@Component({
  selector: 'member-detail',
  template: `
    <div *ngIf="selectedMember">
      <div>
        id: {{selectedMember.id}}
      </div>
      <div>
        name: {{selectedMember.name}}
      </div>
      <div>
        <input type="text" [(ngModel)]="selectedMember.name">
      </div>
    </div>
  `
})

export class MemberDetailComponent {
}

app.module.ts に追加。

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

import { AppComponent }  from './app.component';
// tslint:disable-next-line:quotemark
import {MemberDetailComponent} from './member-detail.component';

@NgModule({
  imports: [BrowserModule, FormsModule],
  declarations: [ AppComponent, MemberDetailComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

基本的に1ファイル1クラスなので,app.componentのMemberクラスを新ファイル「member.ts」に移動。importを忘れずに。
member-detail.component.tsで,selectedMemberを取得することができなくなったため先程移動したMemberプロパティを参照する。この際,selectedMemberはmemberにリファクタ。

import {Component} from '@angular/core';
import {Member} from './member';

@Component({
  selector: 'member-detail',
  template: `
    <div *ngIf="member">
      <div>
        id: {{member.id}}
      </div>
      <div>
        name: {{member.name}}
      </div>
      <div>
        <input type="text" [(ngModel)]="member.name">
      </div>
    </div>
  `
})

export class MemberDetailComponent {
  member: Member;
}

次に,member-detail.componentのtemplateをapp.componentに追加する。
selector がそのままタグの名前となるので,先程移動した箇所に追記。

<h1>{{title}}</h1>
    
    <ul class="members">
    <li *ngFor="let member of member" 
        (click)="onSelect(member)" 
        [class.selected]="member === selectedMember">
      <span class="badge">{{member.id}}</span>{{member.name}}
    </li>
    </ul>
    <member-detail></member-detail>  <-- 追加 -->
外部データの参照

app.componentでclickイベントによって発火したonSelect()によってselectedMemberが生成されるが,Componentを分割したので詳細templateにデータが反映されない。
このままではmemberをclickしても詳細が出てこないので,外部データを参照する必要がある。
f:id:noinoi_monstS4l:20190601060427p:plain

そこで,Inputデコレータを用いる。
Inputデコレータとは親コンポーネントから子コンポーネントへ,データを受け渡す。
反対に,子コンポーネントから親コンポーネントへのデータの受け渡しにOutput()があるが,実装方法が異なるので割愛。詳しくはココ
f:id:noinoi_monstS4l:20190601055612p:plain


コンポーネントにInputをimportし,受け渡したいプロパティに「@Input」デコレータをセットする。

import {Component, Input} from '@angular/core'; // Angular_coreからimport
import {Member} from './member';

@Component({
  selector: 'member-detail',
  template: `
    <div *ngIf="member">
      <div>
        id: {{member.id}}
      </div>
      <div>
        name: {{member.name}}
      </div>
      <div>
        <input type="text" [(ngModel)]="member.name">
      </div>
    </div>
  `
})

export class MemberDetailComponent {
  @Input() member: Member; // Inputデコレータを追記
}

templateの子Component名のタグの中で,デコレートしたプロパティに受け渡したいデータを代入。

<h1>{{title}}</h1>
    
    <ul class="members">
    <li *ngFor="let member of member" 
        (click)="onSelect(member)" 
        [class.selected]="member === selectedMember">
      <span class="badge">{{member.id}}</span>{{member.name}}
    </li>
    </ul>
    <member-detail [member]="selectedMember"></member-detail> <-- []で囲む -->

f:id:noinoi_monstS4l:20190601060242p:plain

以上。次回はLifeCycleHook。

TypeScript memo

オブジェクト指向の復習も含め初歩の初歩から

型キャスト

()で囲むとオブジェクトになる?

let message;
message = 'abs;
let endsWithC = (<string>message).endsWith('c');
let alternativeWay = (message as string).endsWith('c');

関数

let log = function(message){
   console.log(message);
}

// X
let doLog = (message) => {
   console.log(message);
}

// O
let doLog = () => console.log();

複数の引数の関数

// X
let drawPoint = (x, y) => {
   // ...
}

// O
let drawPoint = (point) =>{
   // ...
}

drawPoint({
   x: 1,
   y: 2
})

型指定インターフェイス

interface Point {
   x: number,
   y: number
}

let drawPoint = (point: Point) => { 
   // ...
}

drawPoint({
  x: 1,
  y: 2
})

クラス

関連性の高い変数(プロパティ)と関数(メソッド)をグループ化したもの

class Point {
  x: number;
  y: number;
 
  draw() {
      console.log('X: ' + this.x + 'Y: ' + this.y); 
   }

  getDistance(another: Point) {
      //...
   }
}

// X
// let point: Point;  // エラー

// O
let point = new Point();
point.x = 1;
point.y = 2;
point.draw();

コンストラク

class Point {
  x: number;
  y: number;

  constructor(x: number, y?: number) {  // ?: nullable
     this.x = x;
     this.y = y;
 
  draw() {
      console.log('X: ' + this.x + 'Y: ' + this.y); 
   }

  getDistance(another: Point) {
      //...
   }
}

// X
let point = new Point();
point.x = 1;
point.y = 2;
point.draw();

// O
let point = new Point(1, 2);
point.draw();

// O
let point = new Point(1);
point.draw();

アクセス修飾子

private, public, protected

class Point {
  constructor(private x: number, private y?: number) {  // ?: nullable
     this.x = x;
     this.y = y;
 
  draw() {
      console.log('X: ' + this.x + 'Y: ' + this.y); 
   }

  getDistance(another: Point) {
      //...
   }
}

// X
let point = new Point(1, 2);
// point.x = 3;
// point.y = 4;

プロパティ(getter, setter)

値の受け渡し程度しか,まだ理解してない。なにしてんのこれけっきょく。

よく分からない解説

以下の2つのコードは同じ処理

class Point {
  constructor(private x: number, private y?: number) {  // ?: nullable

  draw() {
      console.log('X: ' + this.x + 'Y: ' + this.y); 
   }

  getX() {
      return this.x;
  }

  setX(value) {
      if (value < 0) 
         throw new Error('value cannot be less than 0.');
      this.x = value;
}

let point = new Point(1, 2);
let x = point.getX();   // get: 値を受け取る
point.setX(10);           // set: 値をセットする
point.draw();
class Point {
  constructor(private x: number, private y?: number) {  // ?: nullable

  draw() {
      console.log('X: ' + this.x + 'Y: ' + this.y); 
   }

  get X() {
      return this.x;
  }

  set X(value) {
      if (value < 0) 
         throw new Error('value cannot be less than 0.');
      this.x = value;
}

let point = new Point(1, 2);
// let x = point.getX();   // get: 値を受け取る
let x = point.X;

// point.setX(10);           // set: 値をセットする
point.X = 10;

point.draw();

readonlyはset Xをコメントアウトすることでも実現できる(その場合 point.X = 10 がエラーとなる)。
なるほど。

Module

typescriptでは,1ファイル1classと言う規則がある。クラスと呼び出しを別ファイルで行う場合Moduleとして扱う。import, export の関係。

// Moduleとして外部へ提供する場合,アクセス修飾子にexport を付与。
class Point {
  constructor(private x?: number, private y?: number) { 
  draw() {
      console.log('X: ' + this.x + 'Y: ' + this.y); 
   }
}
// import {クラス名} from 'ライブラリのパス'
import { Point } from './point';
let point = new Point(1, 2);
point.draw();

www.youtube.com

Angular 入門してみる3

選択した社員の詳細と名前入力欄とstyleの適用,また「ngModel」による双方向バインディングの実装。

1.選択した社員の詳細と名前入力欄の実装

templateの編集

前回の社員の詳細templateを,選択した社員に適用したいので,選択(クリック)された場合にクラスを付与。
(click)はeventで,onSelectメソッド(後ほど記述)を着火。[class.selected]は,trueの場合にliタグのclassをセットする。
selected-member-detail属性のmemberをselectedMemberにしておく。

    <h1>{{title}}</h1>
    
    <ul class="members">
    <li *ngFor="let member of member" 
        (click)="onSelect(member)" 
        [class.selected]="member === selectedMember">
      <span class="badge">{{member.id}}</span>{{member.name}}
    </li>
    </ul>
    
    <div class="selected-member-detail">
      <div>
        id: {{selectedMember.id}}
      </div>
      <div>
        name: {{selectedMember.name}}
      </div>
    </div>
Componentに追加

templateに基づき記述。
export class AppComponent {
title = '自社社員名簿';
selectedMember: Member; // 選択された社員のプロパティ
member = MEMBERS;

// 画面で社員が選択された時着火されるonSelect()によって,selectedMemberにmemberを代入
onSelect(member: Member): void {
this.selectedMember = member;
}
}

styleの適用

templateやselecterと同じ箇所に,配列で記述

    .selected {
      background-color: #CFD8DC !important;
      color: white;
    }
    .members {
      margin: 0 0 2em 0;
      list-style-type: none;
      padding: 0;
      width: 15em;
    }
    .members li {
      cursor: pointer;
      position: relative;
      left: 0;
      background-color: #EEE;
      margin: .5em;
      padding: .3em 0;
      height: 1.6em;
      border-radius: 4px;
    }
    .members li.selected:hover {
      background-color: #BBD8DC !important;
      color: white;
    }
    .members li:hover {
      color: #607D8B;
      background-color: #DDD;
      left: .1em;
    }
    .members .text {
      position: relative;
      top: -3px;
    }
    .members .badge {
      display: inline-block;
      font-size: small;
      color: white;
      padding: 0.8em 0.7em 0 0.7em;
      background-color: #607D8B;
      line-height: 1em;
      position: relative;
      left: -1px;
      top: -4px;
      height: 1.8em;
      margin-right: .8em;
      border-radius: 4px 0 0 4px;
    }

実行結果
クリックした社員の詳細が出力された。
f:id:noinoi_monstS4l:20190529213758p:plain

2.入力フォームを作成

双方向バインディングによる動的な動作を実装。

双方向バインディング

モデルのプロパティが更新されると,UIも更新され,反対にUIが更新されると,変更内容がモデルにも反映されること。
Angularで「フォーム」の入力値をコンポーネントと同期するには?(双方向バインディング):Angular TIPS - @IT
使用するには,参照するModuleにFormsModuleを追加。

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

import { AppComponent }  from './app.component';
// tslint:disable-next-line:quotemark
import {FormsModule} from "@angular/forms";  // 追加

@NgModule({
  imports: [BrowserModule, FormsModule],  // 追加
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

templateに以下を追加。
属性に*ngIfを追加。「selectedMemberが存在する場合」詳細が有効になる。ここではクリックした場合。
また,名前をバインディングしたいので,inputタグにngModelを追加。WebStormのインテリジェンスつよい。

    <div *ngIf="selectedMember">
      <div>
        id: {{selectedMember.id}}
      </div>
      <div>
        name: {{selectedMember.name}}
      </div>
      <div>
        <input type="text" [(ngModel)]="selectedMember.name">
      </div>
    </div>

実行結果。*ngIfにより,最初の非選択画面では詳細は表示されない。
f:id:noinoi_monstS4l:20190529215259p:plain

バインディングも動作。
f:id:noinoi_monstS4l:20190529215325p:plain

次回はコンポーネントの分割からLifeSycleHookまで?
以上。まだいける。