Angular 覚え書きです。 テキストフィールドとその入力内容を表示するだけのコンポーネントをつくる。
$ 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 にブラウザでアクセスして作動を確かめます。
$ npx ng generate component textfield
textfield の雛形が生成されているのでこれを修正します。
<div>
<input type="text"/>
</div>
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>
現状はこれ:
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 して意図通りテキストフィールドが出現するか確認します。
ボタンをクリックしたらテキストフィールドの内容をそのすぐ下に表示するようにしてみます。
<div>
<input #textfield type="text"/>
<button (click)="show(textfield.value)">Show</button>
</div>
<div>
<p>{{textfieldValue}}</p>
</div>
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 して修正した内容が反映できたか確かめます。
現在の振る舞いはそのままで入力する部分 InputComponent と入力結果を表示する部分 ShowComponent をそれぞれ別々のコンポーネントに分けてみます。
$ npx ng generate component show
<div>
<p>{{textfieldValue}}</p>
</div>
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 を付与しておきます。
$ npx ng generate component input
<div>
<input #textfield type="text"/>
<button (click)="show(textfield.value)">Show</button>
</div>
<app-show [textfieldValue]="textfieldValue"></app-show>
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 に追加するのを忘れずに。
<!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-textfield を app-input に変更。
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 して作動を確かめます。
振る舞いを変更していないので当然ですが、同じ結果を得ることができました。
先ほどは、InputComponent の中に ShowComponent を含む形で実装しました。 今度はこれを改め ContainerComponent を追加し、 その子要素として InputComponent と ShowComponent を含める形で実装してみます。
$ npx ng generate component container
<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>
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;
}
}
<div>
<input #textfield type="text"/>
<button (click)="show(textfield.value)">Show</button>
</div>
先ほどはこのあとに app-show コンポーネントを配置していたが、それを削除した。
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 );
}
}
EventEmitter や emit を使っています。これでテキストフィールド値の変更イベントを発生させている。
ここで(この InputComponent で) 次のように記述して...
@Output() textfieldValueChanged = ...
親の container.component.html で次のように記述しているので...
<app-input (textfieldValueChanged)="show($event)"></app-input>
子コンポーネントから親へ値を受け渡すことができる。
これらは先ほどのコードそのままで変更はありません。
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>
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 して作動を確かめます。
振る舞いを変更していないので当然ですが、同じ結果を得ることができました。
親コンポーネントから子への値を渡す部分は自然に感じますが、 子コンポーネントの値(で発生した値の変更)を親コンポーネントに伝える部分は 少し難しく感じました。 ただ 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 はこれを隠蔽してくれているのであろう。