Home About Contact
Angular

Angular Hello, World! その2

Angular 覚え書きです。 テキストフィールドとその入力内容を表示するだけのコンポーネントをつくる。

fig 2

環境

$ node --version
v18.20.4
$ npm --version
10.7.0

プロジェクトのベースをつくる

Angular v19 を使います。

ng コマンドをグローバルに入れたくないので次の手順で Angular v19 のプロジェクトのベースをつくります。

tmp ディレクトリを作成して、ng コマンドをいれます。

$ mkdir tmp
$ cd tmp
$ npm init -y
$ npm install @angular/cli@19

その後、hello-world プロジェクトを作成:

$ npx ng new hello-world

適当に質問にこたえると ./hello-world にプロジェクトのベースができている。 この ./hello-world 内で Angular アプリをつくる。

作動するか試す

$ cd hello-world
$ npm start

http://localhost:4200 にブラウザでアクセスして作動を確かめます。

textfield コンポーネントを生成して app-root をそれに差し換える

$ npx ng generate component textfield

textfield の雛形が生成されているのでこれを修正します。

src/app/textfield/textfield.component.html の修正

<div>
  <input type="text"/>
</div>

src/index.html の修正

app-root 要素の代わりに app-textfiled に差しかえます。

これは textfield.component.ts の selector に app-textfield と名指しされているので、それをここで指します。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Hello World</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-textfield></app-textfield>
</body>
</html>

src/main.ts の修正

現状はこれ:

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

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

./app/app.component の AppComponent を使っていますが、これを ./app/textfield/textfield.component の TextfieldComponent を使うように修正します。

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { TextfieldComponent } from './app/textfield/textfield.component';

bootstrapApplication(TextfieldComponent, appConfig)
  .catch((err) => console.error(err));

ここまで修正したら npm run build してビルドできるか確認しておきます。 問題がなければ npm start して意図通りテキストフィールドが出現するか確認します。

textfield 1

ボタンを追加してクリックしたらテキストフィールドの内容を表示

ボタンをクリックしたらテキストフィールドの内容をそのすぐ下に表示するようにしてみます。

src/app/textfield/textfield.component.html の修正

<div>
  <input #textfield type="text"/>
  <button (click)="show(textfield.value)">Show</button>
</div>
<div>
  <p>{{textfieldValue}}</p>
</div>

src/app/textfield/textfield.component.ts の修正

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

@Component({
  selector: 'app-textfield',
  imports: [],
  templateUrl: './textfield.component.html',
  styleUrl: './textfield.component.css'
})
export class TextfieldComponent {
  textfieldValue: string = '';

  show(textfieldValue: string): void {
    this.textfieldValue = textfieldValue;
  }
}

TextfieldComponent に次を追加:

npm run して修正した内容が反映できたか確かめます。

textfield 2

コンポーネントを分ける(その1)

現在の振る舞いはそのままで入力する部分 InputComponent と入力結果を表示する部分 ShowComponent をそれぞれ別々のコンポーネントに分けてみます。

fig 1

ShowComponent

$ npx ng generate component show

src/app/show/show.component.html の修正

<div>
  <p>{{textfieldValue}}</p>
</div>

src/app/show/show.component.ts の修正

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

@Component({
  selector: 'app-show',
  imports: [],
  templateUrl: './show.component.html',
  styleUrl: './show.component.css'
})
export class ShowComponent {
  @Input() textfieldValue: string = '';
}

textfieldValue は外挿するつもりなので @Input を付与しておきます。

InputComponent

$ npx ng generate component input

src/app/input/input.component.html の修正

<div>
  <input #textfield type="text"/>
  <button (click)="show(textfield.value)">Show</button>
</div>
<app-show [textfieldValue]="textfieldValue"></app-show>

src/app/input/input.component.ts の修正

import { Component } from '@angular/core';
import { ShowComponent } from '../show/show.component';

@Component({
  selector: 'app-input',
  imports: [ShowComponent],
  templateUrl: './input.component.html',
  styleUrl: './input.component.css'
})
export class InputComponent {
  textfieldValue: string = '';

  show(textfieldValue: string): void {
    this.textfieldValue = textfieldValue;
  }
}

ShowComponent を import してそれを @Component の imports に追加するのを忘れずに。

src/index.html の修正

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Hello World</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-input></app-input>
</body>
</html>

app-textfieldapp-input に変更。

src/main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { InputComponent } from './app/input/input.component';

bootstrapApplication(InputComponent, appConfig)
  .catch((err) => console.error(err));

TextfieldComponent に代えて InputComponent を使います。

変更は以上です。 npm start して作動を確かめます。

textfield 2

振る舞いを変更していないので当然ですが、同じ結果を得ることができました。

コンポーネントを分ける(その2)

先ほどは、InputComponent の中に ShowComponent を含む形で実装しました。 今度はこれを改め ContainerComponent を追加し、 その子要素として InputComponent と ShowComponent を含める形で実装してみます。

fig 2

ContainerComponent を追加

$ npx ng generate component container

src/app/container/container.component.html の修正

<app-input (textfieldValueChanged)="show($event)"></app-input>
<app-show [textfieldValue]="textfieldValue"></app-show>

子要素として InputComponent と ShowComponent を設定しました。

(子コンポーネントから親への変更通知) InputComponent 上で show ボタンがクリックされたときにテキストフィールドに入力された値を受け取ることができるように textfieldValueChanged を設定しています。

<app-input (textfieldValueChanged)="show($event)"></app-input>

$event は組み込みのなにか(変数?)のようです。ここでは $event にテキストフィールドに入力された値(string)がきます。

突然出てきた textfieldValueChanged は、このあと説明する input.component.ts で出現するものです。 InputComponent 上で show ボタンをクリックしたときに、ここ( textfieldValueChanged )にその値をコールバックします。

(親コンポーネントから子への値の受け渡し) ShowComponent の方は先ほどと同じように設定。これで textfieldValue を ContainerComponent から ShowComponent へ渡せます。

<app-show [textfieldValue]="textfieldValue"></app-show>

src/app/container/container.component.ts の修正

import { Component } from '@angular/core';
import { InputComponent } from '../input/input.component';
import { ShowComponent } from '../show/show.component';

@Component({
  selector: 'app-container',
  imports: [InputComponent, ShowComponent],
  templateUrl: './container.component.html',
  styleUrl: './container.component.css'
})
export class ContainerComponent {
  textfieldValue: string = '';

  show(textfieldValue: string): void {
    this.textfieldValue = textfieldValue;
  }
}

InputComponent の修正

src/app/input/input.component.html の修正

<div>
  <input #textfield type="text"/>
  <button (click)="show(textfield.value)">Show</button>
</div>

先ほどはこのあとに app-show コンポーネントを配置していたが、それを削除した。

src/app/input/input.component.ts の修正

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

@Component({
  selector: 'app-input',
  imports: [],
  templateUrl: './input.component.html',
  styleUrl: './input.component.css'
})
export class InputComponent {
  @Output() textfieldValueChanged = new EventEmitter<string>();

  show(textfieldValue: string): void {
    this.textfieldValueChanged.emit( textfieldValue );
  }
}

EventEmitteremit を使っています。これでテキストフィールド値の変更イベントを発生させている。

ここで(この InputComponent で) 次のように記述して...

@Output() textfieldValueChanged = ...

親の container.component.html で次のように記述しているので...

<app-input (textfieldValueChanged)="show($event)"></app-input>

子コンポーネントから親へ値を受け渡すことができる。

src/app/show/show.component.html , src/app/show/show.component.ts は変更なし

これらは先ほどのコードそのままで変更はありません。

src/index.html の修正

app-input から app-container にかえます。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Hello World</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-container></app-container>
</body>
</html>

src/main.ts の修正

InputComponent から ContainerComponent にかえます。

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { ContainerComponent } from './app/container/container.component';

bootstrapApplication(ContainerComponent, appConfig)
  .catch((err) => console.error(err));

変更は以上です。 npm start して作動を確かめます。

textfield 2

振る舞いを変更していないので当然ですが、同じ結果を得ることができました。

まとめ

親コンポーネントから子への値を渡す部分は自然に感じますが、 子コンポーネントの値(で発生した値の変更)を親コンポーネントに伝える部分は 少し難しく感じました。 ただ Java Swing などの世界観で考えれば(説明用の疑似コードなので public とか override は省略)...

InputComponent :

class InputComponent {
  TextfieldValueChangeListener l;
  void addTextfiledValueChangeListener(TextfieldValueChangeListener l){
    this.l =l;
  }
  
  void emit(String value){
    if(l!=null){
      l.textfiledValueChanged(value)
    }
  }
}

ContainerComponent :

class ContainerComponent {
  InputComponent inputComponent = new InputComponent();
  ContainerComponent(){
    inputComponent.addTextfieldValueChangeListener(new TextFiledValueChangeListener(){
      void textfieldValueChanged(String value){
        // do something
      }
    })
  }
}

TextfieldValueChangeListener :

interface TextfieldValueChangeListener {
  void textfieldValueChanged(String value)
}

このように 監視したいコンポーネントにリスナーを設定することで変更が起きたらコールバックを受ける という一連のコードを Angular では暗黙のルールとして名前指しして設定するだけで済ませることができる。

別コンポーネントを監視して変更があったら通知を受ける、という処理として Java Swing のコードは自明で分かりやすくはあるが、とても冗長になる。 Angular はこれを隠蔽してくれているのであろう。