とんかつ時々あんどーなつ

〜たとえ低空でも飛行していられるように〜

EMConf JP 2026 行ってきました

はじめに

EMConf JP 2026 に参加してきたので感想を書く。

去年参加したときにとても刺激を受けたので今年も行ってきた。

聞いたセッション

印象に残っていること

基調講演はオープニングもクロージングもどちらもよかった。

オープニングは世界観のパラダイムシフトの話から 4 つのマネジメントの話までずっと面白かった。 特に会議のマネジメントにおける問いのデザインみたいな話はすごく納得感があり、明日から実践できそうな内容だった。

クロージングは AI の登場により変わるこれからの話で少し未来を見た気がした。 数年後を予想することは無理だが、変わっていくことは確実なので、いかに素早く適応していくかが重要だと感じた。 数十年後にこの大きな転換期を楽しかったと言えるように今を頑張りたい。

セッションも複数聞いたが、面白かったのは「経営と会計とエンジニアリング」だった。 去年の発表がすごく刺さったので、タイトルではなく完全に発表者で選んで聞きに行った感じだけど今年も刺激を受けた。

普段の業務ではあまり会計のことを考えて働くことがないので、お金の動きがわかることでここまで働き方が変わるのかという印象を持った。 じゃあ明日から実践、というにはかなりのハードルがある内容だったが、頭の片隅に残しながら少しずつ知識をつけていきたい。

セッション以外だとお悩み相談室であらたまさんに 1on1 をお願いして、日々の悩みを壁打ちさせてもらった。自分のやることを決めたり、役割を言語化してみようと思う。

他にも lacolaco さんのところで書籍を購入したり、何人かの知人と話したりと楽しい一日だった。

まとめ

毎度のことながらインプットが多くて情報過多だが、モチベーションは上がるので参加できてよかった。 こんなに大きなイベントを 2 年連続開催してくれて、運営の方には感謝しかない。

Claude Code のコンテキストウィンドウを知る

はじめに

Claude Code のメモリ(記憶)について調べたので、備忘録として残す。

今回は特にコンテキストウィンドウにフォーカスしている。

CLAUDE.md

コンテキストウィンドウの話をする前に CLAUDE.md について触れておく。

Claude Code は起動するたびにコンテキストを持たない状態で開始する。しかし、これでは常に意識して欲しいことを毎回伝える必要が出てしまう。

これを解決するのが CLAUDE.md である。CLAUDE.md がある場合、Claude Code が起動した時に自動でその Markdown ファイルを読み、メモリとして管理してくれるようになる。

メモリには複数のタイプが存在するが、代表的なものにユーザーメモリとプロジェクトメモリ、そしてプロジェクトローカルメモリがある。それぞれ以下のような違いがある。

メモリタイプ ファイルの場所 影響範囲と対象ユーザー
ユーザーメモリ ~/.claude/CLAUDE.md すべてのプロジェクトに適用される個人用の指示
プロジェクトメモリ ./CLAUDE.md プロジェクトに閉じたチーム共有の指示
プロジェクトローカルメモリ ./CLAUDE.local.md プロジェクトに閉じていてかつ個人用の指示

それ以外のファイルもインポートしたいときは CLAUDE.md の中に @path/to/file の構文で記載しておくとインポートされるようになる。

コンテキストウィンドウ

Claude Code はコンテキスト収集の過程で色々なコードを読み込んだり、会話の内容を保持してやりとりをおこなっている。この一度に処理できるテキスト量(トークン数)の上限をコンテキストウィンドウと呼ぶ。コンテキストウィンドウ自体は LLM 全般に共通する概念であり、上限を超えると古い情報から切り捨てられる。 たとえば Opus 4.6 の上限は 200k tokens である。

先に CLAUDE.md の話をしたが CLAUDE.md は長期記憶であり、コンテキストウィンドウは短期記憶と捉えるとイメージがしやすい。

会話が長くなるとコンテキストウィンドウが逼迫するため、Claude Code にはコンパクションという仕組みがある。コンパクションは過去の会話履歴を要約し、トークン数を削減することで会話を継続可能にする機能である。

/compact コマンドで手動実行できるほか、コンテキスト使用量が 95% に達すると自動的に実行する auto-compact もある。ただし、要約の過程で詳細な情報が失われる可能性がある点には注意が必要である。

コンテキストの使用量を見る

コンテキスト使用量(内訳)を知りたくなったときは /context コマンドを実行すると確認することができる。

  ⎿  Context Usage
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   claude-opus-4-6 · 26k/200k tokens (13%)
     ⛁ ⛁ ⛁ ⛀ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶
     ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶   Estimated usage by category
     ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶   ⛁ System prompt: 3.7k tokens (1.9%)
     ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶   ⛁ System tools: 14.2k tokens (7.1%)
     ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶   ⛁ MCP tools: 3.6k tokens (1.8%)
     ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶   ⛁ Memory files: 1.9k tokens (0.9%)
     ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶   ⛁ Skills: 2.5k tokens (1.3%)
     ⛶ ⛶ ⛶ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝   ⛁ Messages: 8 tokens (0.0%)
     ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝   ⛶ Free space: 141k (70.5%)
                              ⛝ Autocompact buffer: 33k tokens (16.5%)

今回使用しているのは Opus 4.6 なので上限は 200k と表示されている。

Context Usage の内訳を見ていく。

一番上にある System prompt は Claude Code を動かすためにデフォルトで内蔵されているプロンプトである。 次にある System tools は Claude Code の持つツールであり、Bash や Read, Edit, WebSearch といったものを指す。Claude Code を使っているとよく目にすると思う。

さらに最後の行には Autocompact buffer というものがある。これは前述したコンパクションをおこなうためのバッファとして確保されている。

つまり、特にカスタマイズしていなくても System prompt, System tools そして Autocompact buffer でコンテキストウィンドウの約 25% をシステムで使用していることになる。

また、真ん中にある Memory files であるが、これは先ほどの CLAUDE.md を読むことで使用した token である。 色々なタイプのメモリを紹介したが、複数の CLAUDE.md やそこから参照されているドキュメントを読むほど Memory files が増えていくことになる。

コンテキストウィンドウを管理する

コンテキストウィンドウが逼迫してくると、Claude Code は会話の焦点がズレていき、精度が下がってきてしまう。これを回避するためにもコンテキストの管理が重要となる。

ここからはコンテキストを管理する方法を見ていく。

効果的な CLAUDE.md を書く

上述したように CLAUDE.md はセッション開始時に Memory files として必ず読み込まれる。常に必要な情報はここに記載すべきだが、いつか必要になるかもしれない情報を毎度読み込むのはコンテキストウィンドウを無駄に消費することに繋がる。

常に必要な情報かどうかを意識して CLAUDE.md を書くとよい。

subagent を使う

Claude Code には subagent という仕組みが存在する。 subagent は独立したコンテキストウィンドウで動作する。一部のタスクを subagent に任せ、その結果だけ返してもらうことで、メインのコンテキストウィンドウを汚染せずに済む。

subagent は個別に使用可能なツールを制限できたり、モデルの指定もできるため、用途にあわせて柔軟に設定できる点もよい。

/agents コマンドから作成することができる。

あくまで参考だが、自作した code-review skill を使って Pull Request のレビューを依頼したときに subagent を使うパターンと使わないパターンを比較したところ、Message の使用量が大きく異なっていた。subagent の有用さが垣間見える。

subagent なし subagent あり

話が変わるときは /clear コマンドを実行

同一セッションで話が変わる場合は、今までのやり取りを一度すべて忘れてもらったほうが都合がよい。セッションを終了してもよいが /clear を実行することでコンテキストをリセットできる。

まとめ

Claude Code を使いこなすうえで、コンテキストウィンドウの仕組みを知っておくことは大事だと感じた。

CLAUDE.md の内容を定期的に見直したり、subagent を作ったりと常に環境を更新していきたい。

参考文献

FormGroup#setValue() で引数 Object のプロパティ順が挙動に影響する話

はじめに

Angular の FormGroup で値を更新する際、setValue() を使うことが多い。setValue() は対象となる form の raw value と同じ型の object を引数で渡す。

この setValue() にドキュメントには記載されていない暗黙的な仕様があることがわかったので、今回はそれについて説明する。

setValue() は引数で受け取った object のプロパティを順次更新する

例えば species と name を持つ FormGroup があるとする。

form = new FormGroup({
  species: new FormControl("", { nonNullable: true }),
  name: new FormControl("", { nonNullable: true }),
});

この form に対して、以下のように setValue() をするメソッドを用意する。

selectCat(): void {
  this.form.setValue({ name: 'Tama', species: 'Cat' });
}

この selectCat() が実行されたとき、form の raw value は { species: '', name: '' }{ species: 'Cat', name: 'Tama' } のように一度に更新されず、以下の順に更新される。

  1. { species: '', name: '' }(初期値)
  2. { species: '', name: 'Tama' }(name が先に更新)
  3. { species: 'Cat', name: 'Tama' }(species が次に更新)

FormGroup#setValue のコードを確認すると、確かに FormGroup が持つひとつひとつの FormControl を setValue() の引数として渡された object のプロパティの順に更新しているのがわかる。 angular/packages/forms/src/model/form_group.ts at v21.1.6 · angular/angular · GitHub

setValue() の振る舞いを把握していないと困ること

先ほどのコードを更新して、以下のようなコンポーネントを用意する。

@Component({
  selector: "app-root",
  template: `
    <div>Greeting: {{ greetingWords() }}</div>
    <button (click)="selectCat()">Cat</button>
    <button (click)="selectDog()">Dog</button>
  `,
})
export class App implements OnInit {
  greetingWords = signal("");
  form = new FormGroup({
    species: new FormControl("", { nonNullable: true }),
    name: new FormControl("", { nonNullable: true }),
  });

  ngOnInit(): void {
    this.form.controls.species.valueChanges.subscribe((species) => {
      const name = this.form.getRawValue().name;
      this.greetingWords.set(`[${species}] Hi, ${name}`);
    });
  }

  selectCat(): void {
    this.form.setValue({ name: "Tama", species: "Cat" });
  }

  selectDog(): void {
    this.form.setValue({ species: "Dog", name: "Pochi" });
  }
}

form の species に変更があったときに greetingWords を書き換えている処理がある。 ここで期待していることは Cat ボタンを押したときに Greeting: [Cat] Hi, Tama が表示され Dog ボタンを押したときに Greeting: [Dog] Hi, Pochi が表示されることである。

しかし、実際にこのコードを動かすと Cat ボタンを押したときは Greeting: [Cat] Hi, Tama が表示されるが、続けて Dog ボタンを押すと Greeting: [Dog] Hi, Tama が表示される。

注目すべきは setValue() に渡している object のプロパティの順番である。 selectDog() では name よりも先に species を指定している。つまり setValue() では species を先に更新する。 すると name を更新するよりも先に valueChanges が動くことで、greetingWords には期待とは異なる値がセットされるということである。

つまり軽い気持ちで setValue() の引数のプロパティの順番を変えただけで、今まで動いていたものが動かなくなる可能性がある。

まとめ

何年 Angular を使ってても ReactiveForms は難しい…

参考

2025 年振り返り & 2026 年の抱負

晦日なので今年も 1 年の振り返りをする。

2025 年振り返り

仕事

現職で丸 3 年が経った。時間が経つのは早い。

引き続きフロントエンドの開発や運用検討などがメイン業務ではあったが、今年はそれに追加してチームビルディングについて考えることも増えてきた。 といっても実行できたことは少なく、社外向けに書けることは特にない。ただそういう変化もあって、インプットした内容はチームビルディングに関することが多かったように思う。

プライベート

読書

毎年読書を目標に置いているが、ここ 5 年間では 1 番多く 14 冊読んだ。

年初に Habitify というアプリを知ったのだが、それのおかげもあり、毎日 10 分読むというのを継続した。本当に 365 日読み続けたわけではないが、それでもモチベーションの維持には大いに役立った。

Habitify で習慣づくりをする

読んだ本は

良い習慣なので来年も維持したい。

健康

chocoZAP に通っていたが、ちょっとした運動では年齢には勝てず、体重が増加してしまった。年間の平均体重が去年より 3kg くらい増えている…

そんなわけで健康を維持するために 9 月からパーソナルトレーニングに通い始めた。まだ主だった成果はないが、自分の消費カロリーとか食事で摂取する栄養バランスとかを気にするようになった。 運動をしに行ったのに結局食事管理なのかと現実の残酷さを噛み締めている…

勉強会

今年は勉強会やカンファレンスにも足を運んだ。オフライン勉強会への感度が下がり気味だったので、忙しいなりにも行けるものは行ってみた。

  • 2/27 EMConf JP 2025
  • 3/12 EM ゆるミートアップ〜EMConf expansion〜
  • 7/8 Claude Code Meetup Japan 1
  • 7/18 Startup Angular / ng-japan OnAir
  • 11/22 DevFest Tokyo 2025
  • 11/26 STORES Tech Conf 2025
  • 12/10 Angular 入門ハンズオン ~ Learn with AI ~ (DevFest Tokyo)

いざ行ってみるとやはり刺激を受けるのでいいものである。

日報管理

7 月にブログにも書いたが Obsidian と Claude Code で日報を管理する - とんかつ時々あんどーなつ の通り Obsidian を使い始めた。 特別凝った使い方はしていないので Obsidian の機能は活かしきれていないが、そのデータが Markdown としてマシン上にあるため Claude Code と相性が良く、個人的にはかなり重宝した。

ちなみにこのブログを書く際にも何をいつ始めたのかとか聞くと答えが返ってくるので便利だった。

潜在能力はまだまだありそうなので、引き続き使いこなしていきたい。

ブログ

今年の 1 月にこの とんかつ時々あんどーなつ も 100 記事を超えた。こんな訳のわからない名前なのに読者になってくれている人もいて嬉しい限りである。

また、今年からインプットの内容が変わったこともあり、自分の考えを整理するもうひとつのブログも作った。 まだあんまり記事は多くないが、来年は こちら でもアウトプットしていきたい。

趣味とか

今年もライブにたくさん行った。 ライブ観戦の記録 2025|kasaharu

いわゆる遠征みたいなこともしてちょっと足を伸ばしたりもした。

ゲームもちょいちょいやった。昔は Final Fantasy が好きだったのにいつの間にかゲーム自体をあまりやらなくなっていた。が、ふと Nintendo Store で安く買えることを知ったのでナンバリングを一気に買った。

今年は 4 と 8 をクリアした。改めてやると当時はわからなかった話もわかって面白い。まだまだ積まれているので遊ぶには困らない。

2026 年抱負

今年 40 歳になってしまったので、来年は本厄である。まずは健康に過ごしたい。 いつもなら「運動」を目標にあげるが「健康」を目標にする。

あとは例年通り「読書」を続けたい。今年はコンスタントに読めたと思うので、同じ感じでいければと思う。

最後に「アウトプット」を目標にする。「四十にして惑わず」ということはなく惑い続けているので、来年もたくさんインプットし、考えてアウトプットしたい。

それでは良いお年を

AI Tutor で学ぶ最新の Angular アプリケーション開発

はじめに

2025 年、アプリケーション開発に AI を使うということがだいぶ浸透した年だったのではないでしょうか?

そんな中 Angular は AI と学ぶことができるチュートリアルが公式から発表されました。今回の記事ではそんな AI Tutor について紹介します。

これは Angular Advent Calendar 2025 1 日目の記事です。

Angular CLI MCP Server

Angular CLI は今年、MCP Server の提供を始めた。この MCP Server は開発者向けに複数のツールが内包されている。

利用可能なツールについては公式ドキュメントから確認することができる。 https://angular.jp/ai/mcp

今回はこの中の ai_tutor について紹介する。

ai_tutor

ai_tutor は最新の Angular アプリケーションをゼロから学習する手伝いをしてくれるチューターである。レシピ作成・管理ツール「スマートレシピボックス」を作りながら Angular v20 の最新パターンとベストプラクティスを学ぶことのできるチュートリアルとなっている。

ai_tutor の始め方

GitHub リポジトリhttps://github.com/angular/ai-tutor である。 ここに記載の通り Firebase Studio で始めることができる。しかし、必ずしも Firebase Studio を使わなければいけないわけではない。使い慣れたエディターや使い慣れた AI Agent で開始することもできる。

今回はローカルマシンで ai_tutor を始める方法を説明する。

チュートリアル用アプリケーションの用意

まずは Angular CLI を使ってアプリケーションを用意する。

アプリケーションの作成

npm init @angular@latest ng-sample

上記コマンドを叩くといくつかの設定を対話形式でおこなうことになる。そのうちのひとつに AI ツールの設定について尋ねるものがある。今回は Claude Code を使って進めるため、Claude Code を使うための設定ファイルを配置するようにした。

AI ツールの設定については以前 Angular アプリケーションを新規作成するときに AI を設定してくれる話 でも記事にしている。

ng-sample はプロジェクト名なので、好きな名前をつけてよい。

アプリケーションの作成が終わったら MCP Server を追加する。

MCP Server の追加

先に書いた通り、今回 AI ツールには Claude Code を使っているので Claude Code に Angular CLIMCP Server を追加する。 local スコープで動作できるように以下のコマンドを実行し、このプロジェクト内で使用できるようにする。

claude mcp add -s local angular-cli -- npx -y @angular/cli@latest mcp

以下のコマンドを実行することで、登録済みの MCP Server の一覧を確認することができる。

claude mcp list
Checking MCP server health...

angular-cli: npx -y @angular/cli@latest mcp - ✓ Connected

Angular CLI が接続済みであることがわかる。 これで準備が整ったので、チュートリアルを始めていく。

チュートリアルの開始

チュートリアルMCP Server で提供される ai_tutor を動かすことで始めることができる。

チュートリアル開始のプロンプトは https://github.com/angular/angular-cli/blob/21.0.0/packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts を参考にした。

今回は次のプロンプトで起動した。

Angular を教えて下さい!やりとりはすべて日本語でお願いします。

返事は次のとおりである。

⏺ こんにちは!私はあなたの専門的なAngularチューターです。まずは、プロジェクトファイルを分析して、どこから始めるべきかを確認しますね。少々お待ちください...

⏺ Read(src/app/app.ts)
  ⎿  Read 13 lines

⏺ Read(src/app/app.html)
  ⎿  Read 343 lines

⏺ Read(src/app/models.ts)
  ⎿  Error reading file

⏺ Search(pattern: "src/app/**/*.ts")
  ⎿  Found 4 files (ctrl+o to expand)

⏺ 分析が完了しました。これは新しく作成されたプロジェクトですね。素晴らしい!これからスマートレシピボックスアプリケーションを一緒に構築していきましょう。最初から始めていきます。

  始める前に、あなたのAngularの経験レベルを教えていただけますか?これによって、説明の深さやヒントの出し方を調整します。

  1から10の間で評価してください:
  - 1-3: 初心者(Web開発やAngularの基礎から学びたい)
  - 4-7: 中級者(Web開発の知識はあるが、Angularは初めてまたは経験が浅い)
  - 8-10: 経験者(他のフレームワークの経験があり、Angularの特徴を素早く学びたい)

  あなたの経験レベルはどのくらいですか?

無事起動できたことがわかる。レベルを問われているので、自分が始めたいレベルを指定することで開始できる。

レベルは1です。よろしくお願いします。

これだけで Phase 1 の Module 1 からチュートリアルを始めることができる。例えば、Phase 1 の Module 1 だけを実施し、一度離れた場合も再び同じプロンプトを入力するだけで、再開することができる。

> Angular を教えてください。やりとりはすべて日本語でお願いします。

⏺ Angular学習のチュートリアルを開始しますね。まず、Angular AI Tutorをロードします。
  ⎿  ⚠ Large MCP response (~16.2k tokens), this can fill up context quickly
  ⎿ # `airules.md` - Modern Angular Tutor 🧑‍🏫

    Your primary role is to act as an expert, friendly, and patient **Angular tutor**. You will guide users step-by-step throu
    … +961 lines (ctrl+o to expand)


⏺ こんにちは!私はあなたの専属Angularチューターです。始める前に、プロジェクトファイルを簡単に分析して、どこから始めるべきかを確
  認しますね。少々お待ちください...

⏺ Read(src/app/app.html)
  ⎿  Read 2 lines

⏺ Read(src/app/app.ts)
  ⎿  Read 13 lines

⏺ Search(pattern: "src/app/**/*.ts")
  ⎿  Found 4 files (ctrl+o to expand)

⏺ 分析が完了しました。素晴らしいですね!モジュール1:はじめにのステップは完璧に完了しています。プロジェクトは学習パスに沿って順調に進んでいますよ。

  前回の続きから始めましょう。次の概念に進む前に、あなたの経験レベルを教えていただけますか?

  あなたのAngular/Web開発の経験レベルを1〜10で評価してください:
  - 1-3: 初心者(プログラミングやWeb開発が初めて)
  - 4-7: 中級者(他のフレームワークやWeb開発の経験あり)
  - 8-10: 上級者(他のフレームワークに精通しており、すぐにAngular特有の部分を知りたい)

細かい指示出しの方法や学習のステップについては https://angular.jp/ai/ai-tutor にまとまっているので、こちらも参考にするとよい。

余談だが、チューターはめちゃくちゃ褒めてくれる。

> できました!

⏺ 素晴らしい!では、あなたのコードを確認させてください。

⏺ Read(src/app/app.html)
  ⎿  Read 2 lines

⏺ 完璧です!素晴らしい仕事ですね!🎉

また基本的には最新のベストプラクティスに沿っているかを見ているので、ちょっと違うコードを書いたときも優しく教えてくれる。

小さなアドバイス:
  今のコードは完璧に動作しますが、Angularのベストプラクティスとして、テンプレートで使用するメソッドにはprotectedという修飾子をつけることが推奨されています。これは、そのメソッドが「コンポーネントの外部からは使わない」という意図を明確にするためです。

今日はここまで、と思ったときも総括してくれて本当のチューターのように振る舞ってくれるのもよい。

> 今日はここまでにします。

⏺ お疲れ様でした!今日は素晴らしい進歩がありましたね!🎉

  今日の学習内容のまとめ:

  ✅ フェーズ1:Angularの基礎 (完了)
  - Module 2: テンプレート補間 - {{ }}でシグナルの値を表示
  - Module 3: イベントリスナー - (click)でユーザー操作に反応

  ✅ フェーズ2:状態管理とシグナル (完了)
  - Module 4: .set()メソッド - 状態を新しい値に置き換える
  - Module 5: .update()メソッド - 現在の値を基に状態を更新
  - Module 6: computed() - 他のシグナルから自動計算される値を作成

  📍 現在の位置:
  - Module 7: テンプレートバインディング(セットアップ完了、演習は次回)

  次回の予定:
  次回は、[src]プロパティバインディングを使ってレシピの画像を表示する演習から始めます。データ構造の更新(models.tsとmock-recipes
  .ts)は完了しているので、すぐに演習に取り掛かれますよ!

  初心者レベルからのスタートでしたが、シグナルや算出シグナルといった重要な概念をしっかり理解できていますね。素晴らしいペースで
  す!

  また次回お会いしましょう。お疲れ様でした!👋

まとめ

ai_tutor の登場でただドキュメントを見ながら写経するだけでなく、チューターに聞きながら学ぶという新しい学習スタイルを体験できるようになりました。

答えを写すのではなく、チューターが提示したサンプルを見ながら自分で考えて書くという体験に変わるため、初学者には少し難しく感じるかもしれません。一方で、学習という意味ではより意味のあるものになると思いました。

これを機に新しい Angular に触れてみてはいかがでしょうか?

明日は ringtail003 さんです。

参考リンク

CSS Grid Layout は Claude Code に図示してもらうと楽になる

はじめに

Claude Code を使って CSS Grid Layout を組む体験が想像以上によかったので備忘として残す。

Claude Code によるレイアウトの図示

Grid Layout は行と列を定義する二次元レイアウトである。これを図示することで Claude Code とのやり取りの齟齬を減らす。

例えばすでに Grid Layout で表現されている CSS ファイルがあるとする。まずは現在のレイアウトを図示してもらうためのプロンプトを投げる。

> プロンプト
xxx.html と xxx.scss があります。
まず現時点の grid layout がどのように並んでいるかを Markdown のテーブルを使って図示した上で説明してください。

上のような感じである。すると、こんなレスポンスが返ってくる。

| Row | Col 1             | Col 2       | Col 3                |
|-----|-------------------|-------------|----------------------|
| 1   | .logo (rowspan=2) | .title      | .actions (rowspan=2) |
| 2   |                   | .annotation |                      |

わかりやすい。

この状態から例えば Col 1 と Col 2 の間に列を追加して、.logo, .title, .annotation の見せ方を変えるときは次のように指示をする。

> プロンプト
Col 1 と Col 2 の間に追加で Col 1.5(仮) を挿入したいです。
Row1 は `.logo` が Col 1 と Col 1.5 に配置され `.title` が Col 2 に配置されることを期待します。
Row2 は `.annotation` が Col 1.5 と Col 2 になるように配置します。

それ以外はいまと同じレイアウトを維持するように変更してください。

先に変更内容の確認をするために次の一文も追加しておく。

> プロンプト
コード修正の前に Markdown のテーブルで表現してください。

すると次のようなレスポンスが返ってくる。

| Row | Col 1             | Col 1.5                 | Col 2  | Col 3                |
| --- | ----------------- | ----------------------- | ------ | -------------------- |
| 1   | .logo (colspan=2) |                         | .title | .actions (rowspan=2) |
| 2   |                   | .annotation (colspan=2) |        |                      |

Claude Code と認識があったところでコードを修正してもらえば、かなりスムーズに意図したレイアウトを組むことができる。

列を追加するときに grid-template-columns をひとつ増やし、影響を受けるすべての要素の grid-area をひとつずつ丁寧に変更する必要がなくなったのはとても嬉しい。

まとめ

CSS を書くときの Claude Code のうまい使い方を探っていたが、Grid Layout ととても相性がいいことに気づいた。

コードの変更より先に Markdown のテーブルで図示してもらうことで Claude Code がどう変更しようとしているのか、より直感的に知ることができるのがポイントだと感じた。

他にもいいテクニックがあったら知りたい。

Angular Signal based Form (experimental)

はじめに

Angular に Signal API が登場して以降、いろいろな API が signal に対応するようになっている。

この流れの中 signal based な form が v21 で experimental API として提供されようとしているため、next バージョンを使って少し試してみた。

サンプルアプリケーションの用意

今回は v21 で提供予定の API を使用するため 21.0.0-next.3 を使ってアプリケーションを用意した。

作成するサンプルは SaaS に signup するときのフォームをイメージしており、ユーザー名 / メールアドレス / パスワード / パスワード(確認用) の 4 つの input を配置するものとする。

フォームの作成

今回使用する新しい APIform() という名前である。この API を使って Field 型を作っていく。

Reactive Forms と異なり Signal Form はデータを管理しないという点で今までと考え方を変える必要がある。つまり、データの管理は開発者自身がおこなう。 model をWritableSignal 型で用意し、これを form() の第一引数に渡すことで、フォームでの更新を model に反映させることができる。

というわけで、まずは signup のための interface とその model を定義する。

export interface SignupUser {
  userName: string;
  email: string;
  password: string;
  confirmPassword: string;
}

@Component({ ... })
export class App {
  signupUserModel = signal<SignupUser>({
    userName: '',
    email: '',
    password: '',
    confirmPassword: '',
  });

  form = form(this.signupUserModel);
}

まずはミニマムの準備ができた。これを HTML で input にバインドするためには、新しく提供される Control という class が必要になる。@Component の imports に登録した上で次のように書くとよい。

<form>
  <mat-form-field>
    <mat-label>ユーザー名</mat-label>
    <input [control]="form.userName" matInput />
  </mat-form-field>
</form>

これでユーザー名のフォームに値を入力すると signupUserModel の値が更新されることがわかる。

さて、フォームに重要なのはバリデーションである。次はバリデーションの設定の仕方を見ていくことにする。

バリデーションの設定は form() の第二引数に schema として渡せば良い。そのためには schema を準備する必要がある。 ここでも Signal Form 向けに提供される新しい API を使用する。使う APIschema() となる。model を作ったときと同じ型をジェネリクスとして渡すことで、各プロパティのバリデーションを設定できる。

まず、すべてのプロパティは必須であることを期待するため、schema を次のようにする。

const signupUserSchema = schema<SignupUser>((form) => {
  required(form.userName);
  required(form.email);
  required(form.password);
  required(form.confirmPassword);
});

@Component({ ... })
export class App {
  signupUserModel = signal<SignupUser>({
    userName: '',
    email: '',
    password: '',
    confirmPassword: '',
  });

  form = form(this.signupUserModel, signupUserSchema);
}

エラーメッセージを表示する場合は errors() の種類を確認して、適切な文字列を表示するように HTML を書く。例えば必須エラーは次のようになる。

  <mat-form-field>
    <mat-label>ユーザー名</mat-label>
    <input [control]="form.userName" matInput />
    @if (!form.userName().valid() && form.userName().errors()[0].kind === 'required') {
      <mat-error>ユーザー名は必須です</mat-error>
    }
  </mat-form-field>

上の例ではエラーメッセージをテンプレート内に書いている。 しかし、実際のアプリケーションを作っていると同じバリデーションエラーでもいくつかの場所で表記揺れが起きてくるといった課題もあるのではないだろうか? schema ではこの課題も解決してくれる。それが required() の第二引数の config である。

const signupUserSchema = schema<SignupUser>((form) => {
  required(form.userName, { message: 'userName is required' });
  ...
});
  <mat-form-field>
    <mat-label>ユーザー名</mat-label>
    <input [control]="form.userName" matInput />
    @if (!form.userName().valid() && form.userName().errors()[0].kind === 'required') {
      <mat-error>{{ form.userName().errors()[0].message }}</mat-error>
    }
  </mat-form-field>

かゆいところに手が届く、といった印象を受ける。

最後にカスタムバリデーターについても紹介しておく。 ここではパスワードとパスワード(確認用)が一致しているかどうかをチェックする。

const signupUserSchema = schema<SignupUser>((form) => {
  ...
  validate(form, ({ value }) => {
    if (value().password === value().confirmPassword) {
      return [];
    }
    return customError({
      kind: 'unmatch',
      message: 'パスワードとパスワード(確認用)が一致しません',
    });
  });
});

validate() という API を使うことでクロスフィールドバリデーションも簡単に作ることができる。

このようにシンプルなフォームの実装では Signal Form で問題なく実装することができる。 今回の例では required しか使っていないが、min, max や minLength, maxLength, email, pattern など ReactiveForms で使えていた組み込みバリデーターは v21 で提供される。

まとめ

ここまで見てきたように Signal based な Form が着々と使えるようになってきている。

最初に書いたように experimental であるため、仕事で開発しているアプリケーションに使うことは推奨しない。が、Reactive Forms などで作っているアプリケーションがあれば、どこまで再現可能なのか確認してみるのはよいと思う。

まだまだ試せていない API もあるが、next が終わったときにでもまた触ってみたい。

参考