Angular開発メモ

調べたことを記録しておきます。

【Angular】ページ遷移してもスクロール位置が変わらない理由と解決方法

Angularで画面遷移を実装していると、

「別のページへ遷移したのに、前のページのスクロール位置がそのまま残っている」

という現象に遭遇することがあります。

これはAngularの仕様であり、withInMemoryScrolling を利用することで簡単に改善できます。

原因

AngularはSPA(Single Page Application)です。

通常のWebサイトでは、

  • 新しいページへ移動 → 一番上から表示
  • 戻るボタン → 前回のスクロール位置を復元

というブラウザの標準動作になります。

しかしAngularでは画面を書き換えているだけなので、ブラウザから見ると同じページです。

そのため、何もしないとスクロール位置はそのままになります。

例えば

/home (500pxまでスクロール)

↓

/about

と遷移しても、/about が500px付近から表示されてしまいます。

解決方法

app.config.tsprovideRouter()withInMemoryScrolling() を追加します。

import { provideRouter, withInMemoryScrolling } from '@angular/router';

provideRouter(
  routes,
  withInMemoryScrolling({
    scrollPositionRestoration: 'top',
  })
);

これだけで、ページ遷移時に常にページ先頭から表示されるようになります。

anchorScrollingとは?

withInMemoryScrolling にはアンカーリンク用の機能も用意されています。

provideRouter(
  routes,
  withInMemoryScrolling({
    scrollPositionRestoration: 'top',
    anchorScrolling: 'enabled',
  })
);

anchorScrolling を有効にすると、

/#section1

のようなURLへアクセスした際、

<div id="section1">

まで自動でスクロールしてくれます。

固定ヘッダーがある場合

固定ヘッダーを採用しているサイトでは、アンカーリンクで移動すると見出しがヘッダーの裏に隠れてしまうことがあります。

そのような場合は、スクロール位置をJavaScriptで計算して制御する方法もあります。

const element = document.getElementById(id);

const headerOffset = 56;

const y =
  element.getBoundingClientRect().top +
  window.scrollY -
  headerOffset;

window.scrollTo({
  top: y,
  behavior: 'smooth',
});

この方法なら、ヘッダーの高さに応じて自由にスクロール位置を調整できます。

まとめ

  • AngularはSPAのため、ページ遷移時にスクロール位置は自動でリセットされない。
  • withInMemoryScrolling() を利用するとスクロール位置を管理できる。
  • scrollPositionRestoration: 'top' を指定するとページ先頭から表示できる。
  • アンカーリンクは anchorScrolling が利用できる。
  • 固定ヘッダーがある場合は、自前でスクロール位置を計算すると柔軟に制御できる。

【Angular】複数の子コンポーネントをまたぐフォームはどう管理する?

Angularでフォームを作っていると、こんな構成にしたくなることがあります。

  • 親コンポーネント
    • 名前入力コンポーネント
    • 住所入力コンポーネント
    • 確認ボタンコンポーネント

つまり、

「1つのフォームを、複数の子コンポーネントで分割して使いたい」

というケースです。

このとき、

  • 親から子へフォームを渡す?
  • フォーム専用サービスを作る?

どちらがよいのでしょうか。

今回はAngularでよくあるフォーム管理パターンを整理します。


結論:まずは「親がFormGroupを持つ」が基本

Angularでは、まずはこれが定番です。

親コンポーネント

import { Component } from '@angular/core';
import {
  FormControl,
  FormGroup,
  ReactiveFormsModule,
} from '@angular/forms';

@Component({
  selector: 'app-parent',
  imports: [ReactiveFormsModule],
  templateUrl: './parent.component.html',
})
export class ParentComponent {
  form = new FormGroup({
    name: new FormControl(''),
    email: new FormControl(''),
    address: new FormControl(''),
  });
}

子コンポーネントへフォームを渡す

親HTML

<app-name-form [form]="form"></app-name-form>

<app-address-form [form]="form"></app-address-form>

子コンポーネント側

TypeScript

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

@Component({
  selector: 'app-name-form',
  imports: [ReactiveFormsModule],
  templateUrl: './name-form.component.html',
})
export class NameFormComponent {
  @Input() form!: FormGroup;
}

HTML

<div [formGroup]="form">
  <input formControlName="name">
</div>

イメージとしてはこう

親コンポーネント
└─ フォーム全体を管理

子コンポーネント
└─ 入力欄の一部だけ担当

つまり、

= フォームの責任者

= 入力パーツ

という役割分担です。


なぜこれがわかりやすいのか

Angularでは、

状態を持つ場所を明確にする

のがとても重要です。

もし子コンポーネントごとに勝手にフォームを持ち始めると、

  • バリデーション
  • submit
  • 入力値取得
  • エラー表示

などが複雑になります。

なので、

フォーム本体は親が管理

に寄せたほうがシンプルです。


では、フォーム用サービスは不要?

不要ではありません。

ただし、

最初からサービス化しないほうがよい

ことが多いです。


サービス化が向いているケース

例えばこんな場合です。

  • 親子関係ではない画面同士で共有したい
  • 入力途中を保持したい
  • 画面遷移しても値を残したい
  • フォーム生成ロジックが巨大
  • 同じフォームを複数画面で再利用したい

この場合は、

FormService

を作ることがあります。


例えばこんな感じ

@Injectable({
  providedIn: 'root',
})
export class UserFormService {
  form = new FormGroup({
    name: new FormControl(''),
    email: new FormControl(''),
  });
}

コンポーネント側。

constructor(public userFormService: UserFormService) {}

ただし初心者のうちは混乱しやすい

サービス化すると、

これは画面の状態なのか?

アプリ全体の状態なのか?

が曖昧になりやすいです。

なので学習初期は、

親がFormGroupを持つ
↓
子へInputで渡す

をまずしっかり理解するのがおすすめです。


Angularっぽい考え方

Angularは、

画面 = Component
データやロジック = Service

という役割分担をかなり重視しています。

フォームについても、

  • 画面ローカルな状態 → 親コンポーネント

  • アプリ全体で共有したい状態 → Service

と考えると整理しやすいです。


まとめ

複数の子コンポーネントで1つのフォームを扱う場合は、

まずはこれがおすすめです。

  • 親がFormGroupを持つ
  • 子へInputで渡す
  • 子は入力欄だけ担当する

フォーム用サービスは、

  • 大規模化
  • 状態共有
  • 再利用

が必要になってから検討すると理解しやすいです。

【Angular】`@for`で`track`が必須なのはなぜ? `*ngFor`との違いも解説

Angular 17以降では、新しい制御フロー構文として @for@if が追加されました。

例えば一覧表示はこんな感じで書けます。

@for (food of foods; track food) {
  <li>{{ food }}</li>
}

初めて見ると、

なんで `track food` が必要なの?

と思う方も多いはず。

実際、これを消すとエラーになります。

今回は、

  • track とは何か
  • なぜ必須なのか
  • track $index との違い
  • *ngFor との違い

を初心者向けに整理します。


従来の *ngFor

Angularでは長年、一覧表示はこの書き方でした。

<li *ngFor="let food of foods">
  {{ food }}
</li>

これは今でも大量に使われています。


Angular 17以降の @for

新しい書き方はこちら。

@for (food of foods; track food) {
  <li>{{ food }}</li>
}

JavaScriptの for に近く、かなり読みやすくなっています。


track を消すとエラーになる

例えばこう書くと…

@for (food of foods) {
  <li>{{ food }}</li>
}

Angularはエラーを出します。


track は何をしている?

簡単に言うと、

「どのデータがどのHTMLか」

をAngularに教えるためのものです。


例えばこんな配列

foods = ['ラーメン', 'カレー', '寿司'];

を表示しているとします。

その後、

foods = ['ラーメン', 'うどん', 'カレー', '寿司'];

に変わった場合、

Angularは画面を書き換える必要があります。


track がないと何が困る?

Angularが、

「どの要素が同じで、どれが新しいのか」

判断しづらくなります。

結果として、

  • 全部描画し直す
  • DOM更新が増える
  • パフォーマンス悪化

につながります。


track food の意味

@for (food of foods; track food)

これは、

food の値そのものを識別IDとして使う

という意味。

つまり:

'ラーメン'
'カレー'

などの文字列をそのまま目印にしています。


実務で多いのは id

実際の開発ではオブジェクト配列が多いです。

users = [
  { id: 1, name: '田中' },
  { id: 2, name: '佐藤' }
];

この場合は:

@for (user of users; track user.id) {
  <li>{{ user.name }}</li>
}

のように書きます。

これが実務で最もよく見る形です。


track $index も使える

単純な練習なら、

@for (food of foods; track $index) {
  <li>{{ food }}</li>
}

でもOKです。


track foodtrack $index の違い

track food

値そのもので識別。

['ラーメン', 'カレー']

のような単純配列向け。


track $index

配列の順番で識別。

0番目
1番目
2番目

という番号管理。

初心者の練習ではかなり使いやすいです。


なぜAngularは必須にしたの?

昔の *ngFor には trackBy がありました。

<li *ngFor="let user of users; trackBy: trackById">

ただ、実際には省略されることが多く、

気づかないうちにパフォーマンスが悪化

するケースがありました。

そのためAngular 17以降の @for では、

最初から track を書いてもらう

設計になっています。


Angular学習としてはどうするべき?

おすすめはこの順番。

  • まず *ngFor / *ngIf を理解
  • その後 @for / @if を覚える

理由は、実務ではまだ *ngFor が大量に残っているからです。

ただ、Angularの未来としては @for 系がかなり重要になっていくと思われます。


まとめ

track は、

「このデータを識別するための目印」

です。

Angularに名札をつけてあげるイメージ。

最初は難しく感じますが、

track item.id

を見たら、

「ああ、このIDで管理してるんだな」

と思えればOKです。

【Angular】declarationsとprovidersの違いを本質から理解する

Angularのモジュールで必ず出てくるこの2つ👇

@NgModule({
  declarations: [],
  providers: []
})

なんとなく使い分けているけど

  • 何が違うの?
  • なぜ分かれているの?

この記事では 本質からシンプルに理解する


結論(まずここだけ)

  • declarations 👉 テンプレートで使うUI部品

  • providers 👉 DIで使うサービス(ロジック)

👉 「見た目」と「ロジック」で分かれている


declarationsとは(宣言の世界)

まず英語から👇

  • declaration = 宣言

つまり

👉 「このモジュールで使うUIを宣言する場所」

対象

  • コンポーネント
  • ディレクティブ
  • パイプ

例👇

@NgModule({
  declarations: [UserComponent]
})

これは

👉「UserComponentを使います」と宣言している


declarationsが必要な理由

Angularはコンパイル時に

👉「このタグは何?」 を解決する必要がある

例えば👇

<app-user></app-user>

これを見たAngularは

👉「app-userって何?」

となる

そこで

declarations: [UserComponent]

👉「それはUserComponentだよ」と教えている


providersとは(サービスの世界)

providers は👇

👉 サービスをDI(依存性注入)するための設定

対象

  • Serviceクラス
  • DI対象のクラス

例👇

@Injectable()
export class UserService {}
@NgModule({
  providers: [UserService]
})

👉 Angularがインスタンスを管理してくれる


providersが必要な理由

Angularは

👉「必要なときにサービスを注入する」

仕組み(DI)を持っている

constructor(private userService: UserService) {}

このとき

👉 providersに登録されていないと使えない


なぜ分かれているのか(ここが本質)

一言でいうと👇

👉 コンパイルと実行の役割が違うから


declarations
  • コンパイル時に使う
  • テンプレート解析
  • UIの定義

providers
  • 実行時に使う
  • インスタンス生成
  • ロジックの供給

もし一緒だったらどうなる?

  • コンポーネントをDI対象にする?
  • サービスをテンプレートで使う?
  • 役割がごちゃ混ぜ

👉 カオスになる


イメージで理解

  • declarations 👉 部品リスト(UI)

  • providers 👉 工場(インスタンス生成)

👉 まったく別の責務


最近のAngular事情

最近は👇

@Injectable({ providedIn: 'root' })

これでprovidersを書かないことも多い

👉 でも内部的には同じ(DI)


まとめ

  • declarations

    • UI(コンポーネントなど)
    • コンパイル時に必要
  • providers

    • サービス(ロジック)
    • 実行時に必要

👉 見た目とロジックを分離するための設計

CSS Gridで「行ごとに下線」を引く方法(ハマりポイント解説)

CSS Gridで「ラベル + 値」のレイアウトを作ったとき、

名前   田中
年齢   30

👉 これに「行ごとに下線を引きたい」と思うことがある。

でもここ、ちょっとハマりポイントがある。


前提(よくあるGridレイアウト)

.kv {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 8px 16px;
}
<div class="kv">
  <div>名前</div>
  <div>田中</div>

  <div>年齢</div>
  <div>30</div>
</div>

❌ NGパターン(よくやるミス)

.kv > div {
  border-bottom: 1px solid #ddd;
}

👉 結果

名前────
田中────
年齢────
30─────

👉 1セルごとに線が引かれてしまう


✔️ 方法①:value側だけに線を引く(シンプル)

.value {
  border-bottom: 1px solid #ddd;
  padding-bottom: 4px;
}
<div class="kv">
  <div class="label">名前</div>
  <div class="value">田中</div>

  <div class="label">年齢</div>
  <div class="value">30</div>
</div>

👉 見た目

名前   田中────
年齢   30─────

メリット

  • シンプル
  • そのままGridが使える

デメリット

  • 左側(ラベル)には線がない

✔️ 方法②:行ごとに線を引く(ちゃんとやる)

👉 行のラッパーを作る

HTML

<div class="kv">
  <div class="row">
    <div class="label">名前</div>
    <div class="value">田中</div>
  </div>

  <div class="row">
    <div class="label">年齢</div>
    <div class="value">30</div>
  </div>
</div>

CSS

.kv {
  display: flex;
  flex-direction: column;
}

.row {
  display: grid;
  grid-template-columns: 120px 1fr;
  border-bottom: 1px solid #ddd;
  padding: 8px 0;
}

👉 見た目

名前   田中
───────
年齢   30
───────

メリット

  • 行全体に線が引ける
  • UIとして綺麗

デメリット

  • HTMLが少し増える

✔️ なぜこうなるのか

👉 Gridには「行」というDOM構造がない

  • Gridは「セルの集合」
  • 行は見た目上の概念

👉 だから

  • セル単位のスタイル → 簡単
  • 行単位のスタイル → 工夫が必要

✔️ どっちを使うべき?

  • シンプルなUI → 方法①
  • しっかり区切りたい → 方法②

✔️ まとめ

  • Gridは便利だけど「行スタイル」は弱い
  • 行単位で装飾したいならラッパーを作る
  • NGパターンに注意

👉 「行を扱いたいならdivを1枚かませる」

CSS Gridの「fr」とは?割合でレイアウトを組む基本

CSS Gridを触っているとよく出てくるのが fr という単位。

最初は「これ何?」となりがちだけど、理解するとレイアウトが一気に楽になる。

frとは何か

frfraction(フラクション)=割合 の略。

👉 残りスペースを分割する単位

これがポイント。


基本の使い方

左固定 + 右可変

grid-template-columns: 120px 1fr;

これはこういう意味:

  • 左:120px固定
  • 右:残り全部

よくある「ラベル + 値」のレイアウトに最適。


均等割り

grid-template-columns: 1fr 1fr;
  • 残りスペースを半分ずつ

比率を変える

grid-template-columns: 1fr 2fr;
  • 左:1
  • 右:2(2倍の幅)

👉 「割合で分ける」イメージ


pxとfrの違い

  • px → 固定サイズ
  • fr → 可変サイズ(残りの分配)

組み合わせると強い:

grid-template-columns: 200px 1fr 1fr;
  • 左:200px固定
  • 残りを2つで分割

昔のやり方との違い

昔はこうしていた:

width: calc(100% - 200px);
  • 計算が必要
  • レイアウト崩れやすい

今は:

grid-template-columns: 200px 1fr;

👉 シンプルで壊れにくい


よくある実用パターン

ラベルと値を揃える

.kv {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 8px 16px;
}
<div class="kv">
  <div>名前</div>
  <div>田中</div>

  <div>年齢</div>
  <div>30</div>
</div>

👉 縦位置がピシッと揃う


6分割したいときの注意点

ここはよく間違えるポイント。

❌ 間違い

grid-template-columns: 6fr;

👉 これは「1列」しかできない (幅が大きいだけ)


✅ 正しい書き方

grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;

👉 6列に均等分割される


✅ 実務でよく使う書き方

grid-template-columns: repeat(6, 1fr);

👉 同じ指定をまとめて書ける


frの正しい理解

  • fr は「数」ではない
  • 比率(重み)

例えば:

grid-template-columns: 1fr 6fr;
  • 左:1
  • 右:6(6倍広い)

まとめ

  • fr は fraction(割合)
  • 残りスペースを分配する単位
  • pxと組み合わせると強い
  • 分割したいときは repeat() が便利

👉 「固定 + 可変」を簡単に書けるのが最大のメリット

【Angular】定番SEO対策(SSG対応版)まとめ

AngularでWebサイトを作ると「SEOに弱いのでは?」と思われがちですが、 SSR / SSGを導入すれば十分にSEOに強い構成にできます。

この記事では、Angular(特にSSG構成)で行うべき 定番のSEO対策を実装ベースで整理します。


前提(この記事の対象)

  • Angular(v17以降)
  • @angular/ssr を導入済み
  • SSG(Prerender)を使用

全体像(SEOは3つに分かれる)

SEO対策は大きく以下の3つに分類されます。

  • 技術SEO(クロール・インデックス対策)
  • コンテンツSEO(記事やページ内容)
  • 外部SEO(被リンク)

Angular開発者がまずやるべきは 👉 技術SEO(最優先)


1. title / meta description の設定

検索結果に直接影響する最重要項目です。

Angularでは TitleMeta を利用します。

import { Component, OnInit } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';

@Component({
  selector: 'app-page',
  templateUrl: './page.component.html',
})
export class PageComponent implements OnInit {
  constructor(
    private title: Title,
    private meta: Meta
  ) {}

  ngOnInit(): void {
    this.title.setTitle('ページタイトル');

    this.meta.updateTag({
      name: 'description',
      content: 'ページの説明文をここに記述します'
    });
  }
}

ポイント

  • ページごとに必ず設定する
  • descriptionは検索結果の説明文になる

2. canonicalタグ(重複コンテンツ対策)

同じ内容のページが複数URLで存在すると評価が分散します。

<link rel="canonical" href="https://example.com/page" />

ポイント

  • SSR / SSG時に出力するのが理想
  • URLは正規URLを指定する

3. sitemap.xml の作成

検索エンジンにページ一覧を伝えるファイルです。

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
  </url>
  <url>
    <loc>https://example.com/page</loc>
  </url>
</urlset>

配置場所

  • 静的ホスティングの場合は公開ディレクトリに配置

4. robots.txt の設定

クローラーの挙動を制御します。

User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml

5. OGP設定(SNS対策)

SNSでシェアされたときの表示に影響します。

<meta property="og:title" content="ページタイトル" />
<meta property="og:description" content="ページの説明文" />
<meta property="og:image" content="https://example.com/ogp.png" />
<meta property="og:url" content="https://example.com/page" />

6. 構造化データ(JSON-LD)

検索結果のリッチ表示に関係します。

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "サイト名",
  "url": "https://example.com"
}
</script>

7. 表示速度の最適化(Core Web Vitals)

SSGを使っていても以下は確認します。

  • 画像の最適化(WebPなど)
  • 不要なJavaScriptの削減
  • 初期表示の軽量化

👉 Lighthouseで計測するのがおすすめ


8. 見出し構造(h1〜h3)

検索エンジンはページ構造を見ています。

<h1>ページの主題</h1>
<h2>セクション</h2>
<h3>詳細</h3>

9. 内部リンクの設計

ページ同士を適切につなぐことで評価が上がります。

  • 関連ページへのリンク
  • トップページへの導線

10. AngularでSEOを共通化する

毎回 TitleMeta を設定するのは手間なので、 サービスとして共通化するのがおすすめです。

import { Injectable } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';

@Injectable({ providedIn: 'root' })
export class SeoService {
  constructor(
    private title: Title,
    private meta: Meta
  ) {}

  update(titleText: string, description: string): void {
    this.title.setTitle(titleText);

    this.meta.updateTag({
      name: 'description',
      content: description,
    });
  }
}

使用例:

constructor(private seo: SeoService) {}

ngOnInit(): void {
  this.seo.update(
    'ページタイトル',
    'ページの説明文'
  );
}

まとめ(優先順位)

まずは以下の順で対応すると効果的です。

  • title / description
  • sitemap.xml
  • robots.txt
  • OGP
  • 構造化データ
  • コンテンツ拡充

おわりに

AngularでもSSR / SSGを活用すれば、 SEOに強いWebサイトを構築できます。

特に

  • 静的生成による高速表示
  • コンポーネント設計による保守性

といったメリットを活かしつつ、 基本的なSEO対策をしっかり行うことが重要です。