Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions docs/modernization-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,19 @@ Remaining:

## Phase 6: Further Signal Adoption

274 `BehaviorSubject` instances across 52 files. Some already converted to signals.

Priority conversions:
- [ ] `gear-filter-state.service.ts` (45 BehaviorSubjects)
- [ ] `uber-list-state.service.ts` (28 BehaviorSubjects)
- [ ] `clan-state.service.ts` (18 BehaviorSubjects)
- [ ] Evaluate Angular 19 signal APIs (`resource()`, `linkedSignal()`) after Phase 2
- [ ] Convert incrementally, one service per PR
Started at 177 `BehaviorSubject` instances across 52 files.

Completed (PR #1):
- [x] `clan-state.service.ts` — converted 17 BehaviorSubjects to signals, updated 16 consumer components/templates
- Service: `.next()` → `.set()`, `.getValue()` → `()`
- Templates: removed `| async` pipe, use direct signal call `()`
- Components: replaced `.subscribe()` with `effect()`, converted local BehaviorSubjects to signals

Remaining:
- [ ] `gear-filter-state.service.ts` (23 BehaviorSubjects)
- [ ] `uber-list-state.service.ts` (17 BehaviorSubjects)
- [ ] Evaluate Angular 19 signal APIs (`resource()`, `linkedSignal()`)
- [ ] Convert remaining services incrementally, one per PR

**Done when:** BehaviorSubject count under 150. The three largest files are converted.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "d2-checklist",
"version": "30.1.0",
"version": "30.2.0",
"manifest": "242999.26.03.25.2000-1-bnet.64463",
"license": "MIT",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ng-container *ngIf="state.badges|async as badges">
<ng-container *ngIf="state.badges() as badges">
<div class="badge-list">
<ng-container *ngFor="let badge of badges">

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { ClanBadge, ClanStateService } from '@app/clan/clan-state.service';
import { ChildComponent } from '@app/shared/child.component';
import { ClanCollectionBadgeDialogComponent } from '../clan-collection-badge-dialog/clan-collection-badge-dialog.component';
import { NgIf, NgFor, AsyncPipe } from '@angular/common';
import { NgIf, NgFor } from '@angular/common';
import { MatButton } from '@angular/material/button';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'd2c-clan-badges',
templateUrl: './clan-badges.component.html',
styleUrls: ['./clan-badges.component.scss'],
imports: [NgIf, NgFor, MatButton, AsyncPipe]
imports: [NgIf, NgFor, MatButton]
})
export class ClanBadgesComponent extends ChildComponent {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div *ngIf="filteredCollection|async as c" class="clan-collection-search">
<div *ngIf="filteredCollection() as c" class="clan-collection-search">
<div class="left" class="medium-margin">
<mat-form-field class="searchField">
<mat-label>Wildcard Search Collections</mat-label>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ClanSearchableTriumph, ClanStateService } from '@app/clan/clan-state.service';
import { ChangeDetectionStrategy, Component, effect, OnInit, signal } from '@angular/core';
import { ClanSearchableCollection, ClanStateService } from '@app/clan/clan-state.service';
import { ChildComponent } from '@app/shared/child.component';
import { BehaviorSubject, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgIf, NgFor, AsyncPipe } from '@angular/common';
import { NgIf, NgFor } from '@angular/common';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
Expand All @@ -15,55 +15,55 @@ import { ClanCollectionItemComponent } from '../clan-collection-item/clan-collec
selector: 'd2c-clan-collection-search',
templateUrl: './clan-collection-search.component.html',
styleUrls: ['./clan-collection-search.component.scss'],
imports: [NgIf, MatFormField, MatLabel, MatInput, FormsModule, NgFor, ClanCollectionItemComponent, AsyncPipe]
imports: [NgIf, MatFormField, MatLabel, MatInput, FormsModule, NgFor, ClanCollectionItemComponent]
})
export class ClanCollectionSearchComponent extends ChildComponent implements OnInit {
private collectionSearchSubject: Subject<void> = new Subject<void>();
public collectionFilterText: string | null = null;
public filteredCollection: BehaviorSubject<ClanSearchableTriumph[]> = new BehaviorSubject<ClanSearchableTriumph[]>([]);
public filteredCollection = signal<ClanSearchableCollection[]>([]);

constructor(public state: ClanStateService) {
super();
this.collectionFilterText = localStorage.getItem('collection-filter');
effect(() => {
const _collections = this.state.searchableCollection();
this.filterCollections();
});
}

ngOnInit() {
this.state.searchableTriumphs.pipe(
takeUntilDestroyed(this.destroyRef)).subscribe(p => {
this.filterTriumphs();
});
this.collectionSearchSubject.pipe(
takeUntilDestroyed(this.destroyRef),
debounceTime(50))
.subscribe(() => {
const saveMe = this.collectionFilterText == null ? null : this.collectionFilterText.toLowerCase();
localStorage.setItem('collection-filter', saveMe!);
this.filterTriumphs();
this.filterCollections();
});
}
collectionSearchChange() {
this.collectionSearchSubject.next();
}

private filterTriumphs() {
const searchableCollection = this.state.searchableCollection.getValue();
private filterCollections() {
const searchableCollection = this.state.searchableCollection();
if (this.collectionFilterText == null || this.collectionFilterText.trim().length == 0) {
this.filteredCollection.next([]);
this.filteredCollection.set([]);
return;
}
if (searchableCollection == null) {
this.filteredCollection.next([]);
this.filteredCollection.set([]);
return;
}
const temp = [];
const temp: ClanSearchableCollection[] = [];
const filterText = this.collectionFilterText.toLowerCase();
for (const t of searchableCollection) {
if (temp.length > 20) { break; }
if (t.data.searchText.indexOf(filterText) >= 0) {
temp.push(t);
}
}
this.filteredCollection.next(temp as any);
this.filteredCollection.set(temp);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</a>
</nav>
<mat-tab-nav-panel #tabPanel>
<ng-container *ngIf="state.allLoaded|async; else notLoaded">
<ng-container *ngIf="state.allLoaded(); else notLoaded">
<router-outlet></router-outlet>
</ng-container>
</mat-tab-nav-panel>
Expand Down
4 changes: 2 additions & 2 deletions src/app/clan/clan-collections/clan-collections.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { ClanStateService } from '../clan-state.service';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { NgIf, AsyncPipe } from '@angular/common';
import { NgIf } from '@angular/common';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'd2c-clan-collections',
templateUrl: './clan-collections.component.html',
styleUrls: ['./clan-collections.component.scss'],
imports: [MatTabNav, MatTabLink, RouterLink, RouterLinkActive, FaIconComponent, MatTabNavPanel, NgIf, RouterOutlet, AsyncPipe]
imports: [MatTabNav, MatTabLink, RouterLink, RouterLinkActive, FaIconComponent, MatTabNavPanel, NgIf, RouterOutlet]
})
export class ClanCollectionsComponent extends ChildComponent {

Expand Down
10 changes: 5 additions & 5 deletions src/app/clan/clan-info/clan-info.component.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<div class="body" *ngIf="state.info|async as info">
<div class="body" *ngIf="state.info() as info">

<h3>Progress</h3>
<div class="section" *ngIf="info.primaryProgression!=null">

<div class="row">
<div class="col-sm-12 col-md-6">
<div class="clan-progress-row" *ngIf="state.modelPlayer|async as modelPlayer ; else loadingModel">
<div class="clan-progress-row" *ngIf="state.modelPlayer() as modelPlayer ; else loadingModel">
<span style="margin-right: 0.5em" *ngFor="let clanMs of modelPlayer.characters[0].clanMilestones">
{{clanMs.name}}
<fa-icon *ngIf="clanMs.earned==true" class="accent-text mat-option.mat-selected" [icon]="iconService.fasCheckSquare"></fa-icon>
Expand All @@ -20,8 +20,8 @@ <h3>Progress</h3>
</div>
<div class="col-sm-12 col-md-6">
<div class="clan-progress-row">
<button mat-stroked-button [disabled]="(state.allLoaded|async) === false" (click)="state.downloadCsvReport()">
<fa-icon [hidden]="state.allLoaded|async" [icon]="iconService.farSpinner" animation="spin-pulse" [fixedWidth]="true"></fa-icon>
<button mat-stroked-button [disabled]="state.allLoaded() === false" (click)="state.downloadCsvReport()">
<fa-icon [hidden]="state.allLoaded()" [icon]="iconService.farSpinner" animation="spin-pulse" [fixedWidth]="true"></fa-icon>
<fa-icon [icon]="iconService.falDownload"></fa-icon> Download CSV Report</button>
</div>
</div>
Expand Down Expand Up @@ -70,7 +70,7 @@ <h3>Membership Status</h3>
</th>
</thead>
<tbody>
<tr *ngFor="let member of members|async">
<tr *ngFor="let member of members()">
<td>
<a [routerLink]="['/',member.destinyUserInfo.membershipType, member.destinyUserInfo.membershipId]">
{{member.destinyUserInfo.displayName}} {{member.destinyUserInfo.platformName}}
Expand Down
25 changes: 10 additions & 15 deletions src/app/clan/clan-info/clan-info.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, effect, signal } from '@angular/core';
import { IconService } from '@app/service/icon.service';
import { BungieGroupMember, Sort } from '@app/service/model';
import { ChildComponent } from '@app/shared/child.component';
import { BehaviorSubject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ClanStateService } from '../clan-state.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgIf, NgFor, AsyncPipe, DecimalPipe, DatePipe } from '@angular/common';
import { NgIf, NgFor, DecimalPipe, DatePipe } from '@angular/common';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatButton } from '@angular/material/button';
import { RouterLink } from '@angular/router';
Expand All @@ -18,15 +15,15 @@ import { AgoHumanizedPipe, DateFormatPipe } from '../../shared/pipe/timing.pipe'
selector: 'd2c-clan-info',
templateUrl: './clan-info.component.html',
styleUrls: ['./clan-info.component.scss'],
imports: [NgIf, NgFor, FaIconComponent, MatButton, RouterLink, MatTooltip, AgoHumanizedPipe, DateFormatPipe, AsyncPipe, DecimalPipe, DatePipe]
imports: [NgIf, NgFor, FaIconComponent, MatButton, RouterLink, MatTooltip, AgoHumanizedPipe, DateFormatPipe, DecimalPipe, DatePipe]
})
export class ClanInfoComponent extends ChildComponent {
sort: Sort = {
name: 'name',
ascending: true
};

public members: BehaviorSubject<BungieGroupMember[]> = new BehaviorSubject<BungieGroupMember[]>([]);
public members = signal<BungieGroupMember[]>([]);

private static sortMembers(members: BungieGroupMember[], sort: Sort) {
const modifier = sort.ascending ? 1 : -1;
Expand Down Expand Up @@ -74,24 +71,22 @@ export class ClanInfoComponent extends ChildComponent {
this.sort.ascending = true;
this.sort.name = field;
}
this.applySort(this.members.getValue());

this.applySort(this.members().slice(0));
}


private applySort(m: BungieGroupMember[]) {
ClanInfoComponent.sortMembers(m, this.sort);
this.members.next(m);
this.members.set(m);
}

constructor(public iconService: IconService,
public state: ClanStateService) {
super();
this.state.rawMembers.pipe(
takeUntilDestroyed(this.destroyRef))
.subscribe((rawMembers) => {
this.applySort(rawMembers.slice(0));
});
effect(() => {
const rawMembers = this.state.rawMembers();
this.applySort(rawMembers.slice(0));
});
}

}
10 changes: 5 additions & 5 deletions src/app/clan/clan-lifetime/clan-lifetime.component.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<ng-container *ngIf="(state.aggHistoryAllLoaded|async) === false">
<p>Loading lifetime data for clan members. <ng-container *ngIf="state.sweepMsg|async as msg">{{msg}}</ng-container>
<span *ngIf="state.aggHistoryLoadCount|async as cnt">{{cnt}} / {{(state.sortedMembers|async)!.length}}</span>
<ng-container *ngIf="state.aggHistoryAllLoaded() === false">
<p>Loading lifetime data for clan members. <ng-container *ngIf="state.sweepMsg() as msg">{{msg}}</ng-container>
<span *ngIf="state.aggHistoryLoadCount() as cnt">{{cnt}} / {{state.sortedMembers()!.length}}</span>
</p>
<mat-progress-bar mode="determinate" [value]="(state.aggHistoryLoaded|async)!*100"></mat-progress-bar>
<mat-progress-bar mode="determinate" [value]="state.aggHistoryLoaded()!*100"></mat-progress-bar>
<div class="center-spinner">
<mat-spinner class="loading" class="center-spinner" *ngIf="(state.loading())===true">
</mat-spinner>
</div>
</ng-container>


<div class="body" *ngIf="state.aggHistory|async as history">
<div class="body" *ngIf="state.aggHistory() as history">
<mat-tab-group>
<mat-tab label="Raids">
<ng-container *ngTemplateOutlet="aggTable; context: {type: 'raid', history: history}">
Expand Down
28 changes: 12 additions & 16 deletions src/app/clan/clan-lifetime/clan-lifetime.component.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Component, ChangeDetectionStrategy, effect } from '@angular/core';
import { ChildComponent } from '@app/shared/child.component';
import { ClanStateService, ClanAggHistoryEntry } from '../clan-state.service';
import { distinctUntilChanged, filter, takeUntil } from 'rxjs/operators';
import { MatDialogConfig, MatDialog } from '@angular/material/dialog';
import { ClanLifetimeDialogComponent } from './clan-lifetime-dialog/clan-lifetime-dialog.component';
import { ClanUserListDialogComponent } from '../clan-settings/clan-user-list-dialog/clan-user-list-dialog.component';
import { IconService } from '@app/service/icon.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgIf, NgTemplateOutlet, NgFor, AsyncPipe, DecimalPipe } from '@angular/common';
import { NgIf, NgTemplateOutlet, NgFor, DecimalPipe } from '@angular/common';
import { MatProgressBar } from '@angular/material/progress-bar';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatTabGroup, MatTab } from '@angular/material/tabs';
Expand All @@ -22,26 +20,24 @@ import { TimingPipe } from '../../shared/pipe/timing.pipe';
selector: 'd2c-clan-lifetime',
templateUrl: './clan-lifetime.component.html',
styleUrls: ['./clan-lifetime.component.scss'],
imports: [NgIf, MatProgressBar, MatProgressSpinner, MatTabGroup, MatTab, NgTemplateOutlet, NgFor, MatButton, MatTooltip, FaIconComponent, RouterLink, TimingPipe, AsyncPipe, DecimalPipe]
imports: [NgIf, MatProgressBar, MatProgressSpinner, MatTabGroup, MatTab, NgTemplateOutlet, NgFor, MatButton, MatTooltip, FaIconComponent, RouterLink, TimingPipe, DecimalPipe]
})
export class ClanLifetimeComponent extends ChildComponent implements OnInit {
export class ClanLifetimeComponent extends ChildComponent {
private aggHistoryTriggered = false;

constructor(public iconService: IconService,
public state: ClanStateService,
public dialog: MatDialog) {
super();
effect(() => {
const allLoaded = this.state.allLoaded();
if (allLoaded && !this.aggHistoryTriggered) {
this.aggHistoryTriggered = true;
this.state.loadAggHistory();
}
});
}

ngOnInit() {
this.state.allLoaded.pipe(
takeUntilDestroyed(this.destroyRef),
distinctUntilChanged(),
filter(x => x)
)
.subscribe((done: boolean) => {
this.state.loadAggHistory();
});
}



Expand Down
8 changes: 4 additions & 4 deletions src/app/clan/clan-members/clan-members.component.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<ng-container *ngIf="state.modelPlayer|async as modelPlayer; else loadingModel">
<h4 *ngIf="(state.sortedMembers|async)!.length==0" class="left warn-text">
<ng-container *ngIf="state.modelPlayer() as modelPlayer; else loadingModel">
<h4 *ngIf="state.sortedMembers()!.length==0" class="left warn-text">
All members filtered out, check your <a routerLink="../settings">Settings</a>
</h4>
<table class="tidy-auto-table clan-table" *ngIf="(state.sortedMembers|async)!.length>0">
<table class="tidy-auto-table clan-table" *ngIf="state.sortedMembers()!.length>0">
<thead>
<tr>
<ng-container *ngTemplateOutlet="sortHeader"></ng-container>
</tr>
</thead>
<tbody>
<tr *ngFor="let member of state.sortedMembers|async">
<tr *ngFor="let member of state.sortedMembers()">
<td class="lead">
<a [routerLink]="['/',member.destinyUserInfo.membershipType, member.destinyUserInfo.membershipId]">
{{member.destinyUserInfo.displayName}} {{member.destinyUserInfo.platformName}}
Expand Down
4 changes: 2 additions & 2 deletions src/app/clan/clan-milestones/clan-milestones.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ng-container *ngIf="state.modelPlayer|async as modelPlayer; else loadingModel">
<ng-container *ngIf="state.modelPlayer() as modelPlayer; else loadingModel">

<div class="mobile-button-row">
<button mat-button (click)="showAllClanMilestones()" *ngIf="hiddenClanMilestones()!.length>0">
Expand Down Expand Up @@ -42,7 +42,7 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let member of filteredMembers|async">
<tr *ngFor="let member of filteredMembers()">
<td class="lead">
<a [routerLink]="['/',member.destinyUserInfo.membershipType, member.destinyUserInfo.membershipId]">
{{member.destinyUserInfo.displayName}} {{member.destinyUserInfo.platformName}}
Expand Down
Loading
Loading