okinawa

ITと英語の勉強メモがメイン

Angular入門したメモ

参考

Udemyの講座。

英語だけど日本語字幕あり。

www.udemy.com

環境構築

・Node.jsのインストール

Node.js — Run JavaScript Everywhere

・AngularCLIのインストール

npm install -g @angular/cli

・プロジェクトの新規作成

ng new プロジェクト名

・プロジェクトの起動

npm start

開発用サーバーが起動する。

http://localhost:4200/ にアクセスで画面開く↓

Ctrl+Cでサーバー停止。

既存プロジェクトのインポート

Git等で既存プロジェクトをclone

コマンドプロンプトでプロジェクトフォルダに移動

npm installで依存関係をインストール

npm start で起動

ファイル構成

tsconfig~はtypeScriptをJavaScriptコンパイルする用の設定ファイル。

package~は依存関係の設定用ファイル。

angular.jsonはAngular提供ツール全般の追加設定。

.editorconfigはコードの書式設定。(文字コードやインデント設定など)

root>srcにあるstyle.cssはアプリ全体に適用するCSS

root>srcにあるindex.htmlは最初に訪問するページ。

root>srcにあるmain.tsはリクエストが来たときに最初に呼び出される処理。

root>src>app以下のディレクトリにソースコードのファイルを置いていくよ。

Angularの仕組み

index.htmlの中身はほとんど空なのにどうやって画面表示してるの?

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Essentials</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

body要素にこれしかない↓

<app-root></app-root>

しかし、実際に画面を開いて検証モードを開くと↓

body要素に色々追加されている。

例えばscriptタグの中で、main.tsが呼ばれているのがわかる。

こんな感じでAngularが良い感じにhtmlを書き換えて、ブラウザが解析可能な形にしているらしい。

main.ts

import { bootstrapApplication } from '@angular/platform-browser';

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

bootstrapApplication(AppComponent).catch((err) => console.error(err));

bootstrapApplication関数は「AppComponent」を引数に取る。

AppComponentとは何なのか?

app.componentをimportしたもののようで↓

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

app.component.tsの中身を見てみる

AppComponentをエクスポートしている。これがmain.tsのbootstrapApplication関数に渡されている。

@Componentはデコレーターと呼ばれるもので、TypeScriptの機能でメタデータを追加するもの。

デコレーターとは?
既存のクラスやメソッドにデコレーションする(=追加機能を入れる)イメージ。

もう怖くないTypeScriptのDecorator機能

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

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {}

@Componentの中身

selector: 'app-root',

これは、html内のどこにレンダリングされるかを示す。

この場合はindex.html内のここ↓

下記の2行は、Componetに適用するhtmlとCSSの場所。

templateUrl: './app.component.html'

styleUrl: './app.component.css'

新規コンポーネントの作り方

AngularCLI使うと簡単。

ng generate component コンポーネント名
ng g c コンポーネント名 --省略形
ng g c users/user --usersコンポーネント以下にuserコンポーネントを作成する場合

VsCodeのターミナルだとエラー出たので、コマンドプロンプトを管理者モードで実行したらいけた。

userコンポーネントを自動で作ってくれる↓

VsCodeならimportも自動で追記してくれる
Quick fixをクリックする↓

tsファイルにUserComponentのimport文が追記された↓

html内で変数を扱う

プロパティバインディング構文。

{{ 変数名 }}

user.component.ts

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

import { DUMMY_USERS } from '../dummy-users';

const randomIndex = Math.floor(Math.random() * DUMMY_USERS.length)

@Component({
  selector: 'app-user',
  standalone: true,
  imports: [],
  templateUrl: './user.component.html',
  styleUrl: './user.component.css'
})
export class UserComponent {
  selectedUser =  DUMMY_USERS[randomIndex]; //ここ
}

user.component.html

<div>
    <button>
        <img />
        <span>{{ selectedUser.name }}</span> <!-- ここ -->
    </button>
</div>

htmlの属性に対して変数を使う時は

[ 属性名 ] = "変数名"

    <button>
        <img [alt]="selectedUser.name" [src]="'assets/users/' + selectedUser.avatar" />
    </button>
</div>

属性とは、id class alt などHTMLの要素に何かしらの設定をするもの。

属性名=”属性値”

HTMLの属性について | HTML 入門 | レッスン | CreatorQuest

定数の扱い(URLやパスなどhtmlに直書きしたくないもの)

これを書き換えたい。

users.component.html(修正前)

    <button>
        <img [src]="'assets/users/' + selectedUser.avatar" />
    </button>
</div>

user.component.ts

//省略
export class UserComponent {
  selectedUser =  DUMMY_USERS[randomIndex];

  // いわゆるgetter。これをhtml側で呼び出す。
  get imagePath() {
    return 'assets/users/' + this.selectedUser.avatar;
  }
}

users.component.html(修正後)

    <button>
        <img [src]="imagePath" />
    </button>
</div>

イベントリスナーの設定方法

1.tsファイルに関数を定義。

  onSelectUser() {
    console.log('Clicked!')
  }

2.htmlで呼び出し

(イベント) = "関数名()"

<div>
    <button (click)="onSelectUser()">
    </button>
</div>

Stateの更新(値の更新)

Stateはコンポーネント内の変数のこと。

つまり、コンポーネント内の変数の更新方法。

ReactだとuseState()→setState()が必要だったところ。

Stateの更新方法1(値の代入)

特別な処理は必要なく値を代入するだけ。

export class UserComponent {
  selectedUser = DUMMY_USERS[randomIndex];

  onSelectUser() {
    const randomIndex = Math.floor(Math.random() * DUMMY_USERS.length)
    this.selectedUser = DUMMY_USERS[randomIndex];// 普通に代入するだけ
  }
}
State更新と画面更新の仕組み

Stateが更新されると、アプリ内の全てのコンポーネントをチェックして、画面に変化があるかチェックする。

画面変化がある場合は、画面を更新する。

けっこう重い処理なのだろうか?

Stateの更新方法2(Signal)

ReactのState更新に似た方法。

こちらは変更したStateからAngularに対して、ここのState変更しましたよーとSignalを投げる。

そのため、全てのコンポーネントの変更チェックは不要で処理が軽い。

useer.component.ts

export class UserComponent {
  // selectedUser = DUMMY_USERS[randomIndex];

//signal関数に置き換え。(ReactのuseStateと同じ)
//signal関数は関数を返すので、selectedUserには関数が代入される。
//signalの引数には初期値を渡す
  selectedUser = signal(DUMMY_USERS[randomIndex]);

   //signalではgetは使わない。
  // いわゆるgetter。これをhtml側で呼び出す。
  // get imagePath() {
  //   return 'assets/users/' + this.selectedUser().avatar;
  // }

  //getからcomputed関数に置き換え(戻り値は関数)
  //signalを使う場合、getではなくcomputed関数を使う
  //computed関数にはsignalが仕込まれており、selectedUserが更新される度に、signalが変更を検知してimagePathも更新される
  imagePath = computed(() => 'assets/users/' + this.selectedUser().avatar);

  onSelectUser() {
    const randomIndex = Math.floor(Math.random() * DUMMY_USERS.length)
    
    // this.selectedUser = DUMMY_USERS[randomIndex];
    this.selectedUser.set(DUMMY_USERS[randomIndex]);//set関数に置き換え。
  }
}

user.component.html

selectedUser関数とimagePath関数を呼び出す。

<div>
    <button (click)="onSelectUser()">
        <img [alt]="selectedUser().name" [src]="imagePath()" />
        <span>{{ selectedUser().name }}</span>
    </button>
</div>

@Input @Output(親子コンポーネントで値受け渡し)

@Input()は 親コンポーネントから子コンポーネントに値を引き渡す 役割を持ちます。

@Output()は 子コンポーネントから親コンポーネントにイベント(値)を引き渡す。

Angularの@Input(), @Output()を理解する。 #Angular - Qiita

いまいち理解できていない。

@Input例

1,app.component.html(親コンポーネント)から子に[avatar]と[name]の値を渡す。

<app-header />
<main>
    <ul id="users">
        <li>
            <app-user [avatar]="users[0].avatar" [name]="users[0].name" />
        </li>
        <li>
            <app-user [avatar]="users[1].avatar" [name]="users[1].name" />
        </li>
        <li>
            <app-user [avatar]="users[2].avatar" [name]="users[2].name" />
        </li>
        <li>
            <app-user [avatar]="users[3].avatar" [name]="users[3].name" />
        </li>
    </ul>
</main>

2,user.component.ts(子コンポーネントで[avatar]と[name]の値を受け取る

export class UserComponent {
//avatar受取。
//avatar!の!はこの変数はundefinedやnullになることはありません、とTypeScriptに教える記述。
//{required: true}は親から子への値渡しを必須にする。渡さないとエラー出るようになる便利機能。
  @Input({required: true}) avatar!: string; 
  @Input({required: true}) name!: string; //name受取

  get imagePath() {
    return 'assets/users/' + this.avatar; //avatarの値をimagePathに代入
  }

  onSelectUser() {}
}

3,user.component.htmlで表示

<div>
    <button (click)="onSelectUser()">
        <img [src]="imagePath" [alt]="name" />
        <span>{{ name }}</span>
    </button>
</div>

画面はこんな感じ↓

@InputをSignal版に書き換えよう

user.component.ts

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

export class UserComponent {
  // @Input({required: true}) avatar!: string;
  // @Input({required: true}) name!: string;
//@Inputを置き換え
  avatar = input.required<string>();
  name = input.required<string>();

  // get imagePath() {
  //   return 'assets/users/' + this.avatar;
  // }

//getを置き換え
  imagePath = computed(() => {
    return 'assets/users/' + this.avatar();
  })

  onSelectUser() {}
}

Signalを使ったほうが処理が軽いらしい。

@Output例

ちょっとわかりにくいので、こちらのリンクを見るほうが良い。

Angularの@Input(), @Output()を理解する。 #Angular - Qiita

■要点

  • @Outputで定義した変数は親コンポーネントでイベントとして使用できる。ここではapp.component.htmlの(select)のこと。
  • イベントが発火すると、まず子コンポーネントのイベントが発火。ここではuser.component.htmlの(click)="onSelectUserChild()"
  • 次に子コンポーネントのonClickChild関数で、this.select.emit(this.id)により親にidを渡す。
  • idは親コンポーネント$eventに渡される。
  • 次に親コンポーネント(select)="onSelectUser($event)"が発火。コンソールにidが表示。

ボタンを押すと、コンソールにidを表示する処理↓

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

export class UserComponent {
  @Input({required: true}) id!: string;
  @Input({required: true}) avatar!: string;
  @Input({required: true}) name!: string;
  @Output() select: EventEmitter<string> = new EventEmitter(); //@Output()は宣言時、 EventEmitterをインスタンス化 します。

  get imagePath() {
    return 'assets/users/' + this.avatar;
  }

  onSelectUserChild() {
    return this.select.emit(this.id);//emitは放出という意味。親にidを渡す。
  }
}

user.component.html(子コンポーネント

<div>
    <button (click)="onSelectUserChild()"> //最初にこっちが発火
        <img [src]="imagePath" [alt]="name" />
        <span>{{ name }}</span>
    </button>
</div>

app.component.ts(親コンポーネント

export class AppComponent {
  users = DUMMY_USERS;

  onSelectUser(id: string) {
    console.log('Selected user with id ' + id);
  }
}

app.component.html(親コンポーネント

<app-header />
<main>
  <ul id="users">
    <li>
      <app-user
        [id]="users[0].id"
        [avatar]="users[0].avatar"
        [name]="users[0].name"
        (select)="onSelectUser($event)"//子の後にこっちが発火
      />
    </li>
    <li>
      <app-user
        [id]="users[1].id"
        [avatar]="users[1].avatar"
        [name]="users[1].name"
        (select)="onSelectUser($event)"
      />
    </li>
    <li>
      <app-user
        [id]="users[2].id"
        [avatar]="users[2].avatar"
        [name]="users[2].name"
        (select)="onSelectUser($event)"
      />
    </li>
    <li>
      <app-user
        [id]="users[3].id"
        [avatar]="users[3].avatar"
        [name]="users[3].name"
        (select)="onSelectUser($event)"
      />
    </li>
  </ul>
</main>

for文の書き方(html内)

usersリストを表示する。

  <ul id="users">
    @for (user of users; track user.id) {
      <li>
        <app-user
          [user]="user"
          (select)="onSelectUser($event)"
        />
      </li>
    }
  </ul>

■trackとは?

一意のキー(ユニークキー)をtrackに指定する必要がある。

レンダリング効率化のための機能。

リストが変更される度にリスト全体を再作成するのではなく、既存のリストを再利用するためにtrackを指定する。

Reactでも同じ機能があった。

ngFor(昔の書き方)

htmlだけでなく、tsファイルでimportも必要で手間が多い。

app.component.html

      <li *ngFor="let user of users">
        <app-user
          [user]="user"
          (select)="onSelectUser($event)"
        />
      </li>

htmlだけに追加すると下記のメッセージが出る。templateではngFor使ってるけど、Moduleでimportしてないよ的なの↓

app.component.ts(必要部分だけ抜粋)

importを追加。

import { NgFor } from '@angular/common'; //ここ

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [HeaderComponent, UserComponent, TasksComponent, NgFor], //ここ
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})

if else文の書き方(html内)

JavaScriptでは「0, null, undefined」はfalseになる。

  @if (selectedUser) {
    <app-tasks [name]="selectedUser.name"></app-tasks>
  } @else {
    <p id="fallback">Select a user to see their tasks!</p>
  }

ngIf(昔の書き方)

参考↓
【Angular】ngIfディレクティブとは?「使い方」や「プログラム例」などを解説! - IT Information

*ngIf="selectedUserがtrueなら<app-tasks>~</app-tasks>"が表示される。

*ngIf="selectedUserがfalseなら<ng-template #noSelectUser>~</ng-template>"が表示される。

app.component.html

  <app-tasks *ngIf="selectedUser; else noSelectUser" [name]="selectedUser.name"></app-tasks>
  <ng-template #noSelectUser>
    <p id="fallback">Select a user to see their tasks!</p>
  </ng-template>

app.component.ts(必要部分だけ抜粋)

importを追加。

import { NgFor, NgIf } from '@angular/common';//ここ

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [HeaderComponent, UserComponent, TasksComponent, NgFor, NgIf],//ここ
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})