🅰️
Angular19で無限スクロールを作ってみる
今年のAngularは盛りだくさんでしたね!
これはAngularアドベントカレンダー 17日目の記事です!
はじめに
今回はAngular signalsを使った無限スクロールを実装します。
実装コードは末尾にレポジトリを置いておきます。
サーバーサイドは以下のポケモンAPIを使わせていただきます。
無限スクロールの追加読み込み実装
ポケモンAPIからデータ取得を行います。
画面遷移後も取得したデータを保持したいのでservice側のsignalsで状態管理します。
api.service.ts
export class ApiService {
#http = inject(HttpClient);
items = signal<ItemDetail[]>([]);
count = signal(0);
lastIndex = signal(0);
next = signal('');
getItems(next?: string) {
const limit = 15;
const offset = 0;
const url = this.next() ? this.next() : `https://pokeapi.co/api/v2/pokemon/?limit=${limit}&offset=${offset}`
this.#http.get<ItemDetail>(url).subscribe((res) => {
this.count.update(() => res.count);
this.lastIndex.update(lastIndex => lastIndex + res.details.length);
this.next.update(() => res.next ? res.next : '');
this.items.update(i => {
return [...i, ...res.details];
});
});
}
}
取得したデータをTimeline Componentで表示します。
また子コンポーネントのapp-item
から(loaded)
されたitemのidを受け取るようにします。
読み込んだデータの末尾から5個前のitemがviewportに入ったら追加の読み込みが走ります。
timeline.component.ts
@Component({
selector: 'app-timeline',
imports: [RouterLink, ItemComponent],
template: `
@for (item of items(); track item.id) { @defer (on viewport) {
<app-item [item]="item" (loaded)="loaded(item.id)" [id]="item.id" />
} @placeholder (minimum 500ms) {
// ~ loading animation ~
} }
`,
})
export class TimelineComponent implements OnInit {
apiService = inject(ApiService);
items = computed(() => this.apiService.items());
count = computed(() => this.apiService.count());
lastIndex = computed(() => this.apiService.lastIndex());
ngOnInit() {
// ① データ読み込み
this.apiService.getItems();
}
// ② データの追加読み込み
loaded(id: number) {
if ( this.lastIndex() < this.count() && this.items().at(-5)?.id === id) {
this.getItems();
}
}
}
Item Componentでは以下のようにeffect()
内でthis.item()
を呼ぶことでitem()
が更新される度に(スクロールされitemがviewportに入る度に)親コンポーネントのloaded()
が呼ばれます。
item.component.ts
export class ItemComponent {
readonly item = input.required<ItemDetail>();
loaded = output<number>();
constructor() {
effect(() => {
this.loaded.emit(this.item().id);
});
}
}
スクロール位置の復元実装
無限スクロールでは画面遷移後にスクロール位置を復元したいことがあります。
これもservice側のsignalsで状態管理することで簡単に実現できます。
api.service.ts
@Injectable({
providedIn: 'root'
})
export class ApiService {
scrollY = signal(0);
}
コンポーネント側で以下のようにscrollY
を更新/復元することができます。
timeline.component.ts
@Component({
selector: 'app-timeline',
})
export class TimelineComponent implements OnInit, AfterViewInit {
scrollY = computed(() => this.apiService.scrollY());
@HostListener('window:scroll', ['$event'])
onScroll() {
// スクロール位置を更新
this.apiService.scrollY.update(() => window.scrollY);
}
ngOnInit() {
// 初回読み込みは y = 0
if (this.items().length == 0) {
this.apiService.scrollY.update(() => 0);
}
}
// スクロール位置を復元
ngAfterViewInit(): void {
this.viewportScroller.scrollToPosition([0, this.apiService.scrollY()]);
}
}
まとめ
Angular signalsを使った無限スクロールを実装しました。
コードはこちら。
Discussion