I have been trying and experimenting with a load of different approaches, but I cannot seem to get it working that my template gets updated.
Currently the situation: I have an Angular 19 app, completely zoneless (so without zone.js and using provideExperimentalZonelessChangeDetection
in my app.config.ts.
I was also having the same issue before, in Angular 18.
I have a dashboard, that you can only reach after logging in. That part works fine.
On that dashboard I want to display a small widget, showing the number of tickets sold in the last week and percentage of increase or decrease compared to the week before.
Creating the widget works fine, it even adds more if I change editions (at the top, in my header, I can select an edition and it then tries to retrieve the sales data for that edition). It does create an extra widget when I do that. And the sales.service.ts
shows getting the correct numbers from the API.
It's just not updating the template/website that the user is watching and my creative thinking is exhausted. I hope someone here can set me on the right track again.
Here are the relevant files:
- sales.service.ts:
import { Injectable, inject, DestroyRef, Signal, signal, WritableSignal } from '@angular/core';
import { Observable, of } from 'rxjs';
import { ApisService } from './apis.service';
import { Edition } from './interfaces/api-interfaces';
import { UsersService } from './users.service';
import { ToastsService } from './toasts.service';
@Injectable({
providedIn: 'root'
})
export class SalesService {
// First injections
private destroyRef: DestroyRef = inject(DestroyRef);
private toasts: ToastsService = inject(ToastsService);
private apis: ApisService = inject(ApisService);
private users: UsersService = inject(UsersService);
// Then other declarations
private _editions: WritableSignal<Edition[]> = signal([]);
constructor() { }
get editions (): Signal<Edition[]> {
return this._editions;
}
get currentActiveEdition (): Signal<Edition> {
return signal(this._editions().find(edition => edition.current_active === true) ?? { edition_id: 0, name: '', event_date: 0, default: 0, current_active: true });
}
getCurrentEditions (): void {
if (this.users.userToken !== undefined) {
const ticketSubscription = this.apis.ticketsEditions(this.users.userToken().userToken)
.subscribe({
next: (resData) => {
if (resData.length > 0) {
this._editions.set(resData);
this.setCurrentActiveEdition(resData.find(edition => edition.default === 1)!.edition_id);
} else {
this.users.userServiceError.set(true);
this.users.errorMsg.set({ status: 'invalid request', message: 'No editions found' });
this.toasts.show({ id: this.toasts.toastCount + 1, type: 'error', header: this.users.errorMsg().status, body: this.users.errorMsg().message, delay: 10000 });
}
}, error: (err) => {
this.users.userServiceError.set(true);
this.users.errorMsg.set(err.error);
this.toasts.show({ id: this.toasts.toastCount + 1, type: 'error', header: this.users.errorMsg().status, body: this.users.errorMsg().message, delay: 10000 });
}
});
this.destroyRef.onDestroy(() => {
ticketSubscription.unsubscribe();
});
} else {
console.error(`No logged in user!! (sales.service)`);
}
}
setCurrentActiveEdition (newActiveEdition: number): void {
this._editions.update(editions => {
return editions.map(edition => {
return edition.edition_id === newActiveEdition ? { ...edition, current_active: true } : { ...edition, current_active: false };
});
});
}
ticketsSoldPerPeriod (start: number, end: number, editionId: number): Observable<number | null> {
console.info(`Start date from timestamp: ${ start }`);
console.info(`End date from timestamp: ${ end }`);
if (this.users.userToken !== undefined) {
const ticketSubscription = this.apis.ticketsSoldPerPeriod(start, end, editionId, this.users.userToken().userToken)
.subscribe({
next: (resData) => {
console.log(resData);
return of(resData);
}, error: (err) => {
this.users.userServiceError.set(true);
this.users.errorMsg.set(err.error);
this.toasts.show({ id: this.toasts.toastCount + 1, type: 'error', header: this.users.errorMsg().status, body: this.users.errorMsg().message, delay: 10000 });
return of(null);
}
});
this.destroyRef.onDestroy(() => {
ticketSubscription.unsubscribe();
});
return of(null);
} else {
console.error(`No logged in user!! (sales.service)`);
return of(null);
}
}
}
- dashboard.component.ts:
import { Component, effect, inject, OnInit, WritableSignal, Signal, signal, computed } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { bootstrapCart } from '@ng-icons/bootstrap-icons';
import { Breadcrumb, BreadcrumbsComponent } from 'src/app/core/header/breadcrumbs/breadcrumbs.component';
import { UsersService } from 'src/app/core/users.service';
import { SalesService } from 'src/app/core/sales.service';
import { WidgetData, DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.component';
@Component({
selector: 'app-dashboard',
imports: [BreadcrumbsComponent, NgbModule, DashboardWidgetComponent, NgIconComponent],
providers: [provideIcons({ bootstrapCart })],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss'
})
export class DashboardComponent implements OnInit {
// First injections
private users: UsersService = inject(UsersService);
private sales: SalesService = inject(SalesService);
// Then other declarations
protected breadcrumbs: Breadcrumb[] = [{ linkId: 1, linkable: false, text: 'Dashboard' }];
protected widgetInputData: WritableSignal<WidgetData[]> = signal([]);
protected ticketsLastWeek: WritableSignal<number | null> = signal(null);
protected ticketsPreviousWeek: WritableSignal<number | null> = signal(null);
private currentActiveEditionId: number = 0;
constructor() {
effect(() => {
if (this.currentActiveEditionId = this.sales.currentActiveEdition().edition_id) {
console.warn(`Current active edition: ${ this.currentActiveEditionId }`);
this.sales.ticketsSoldPerPeriod(Math.floor((Date.now() / 1000)) - 7 * (24 * 3600), Math.floor(Date.now() / 1000), this.currentActiveEditionId).subscribe({
next: (quantity) => {
this.ticketsLastWeek.set(quantity);
this.sales.ticketsSoldPerPeriod(Math.floor((Date.now() / 1000)) - 14 * (24 * 3600), Math.floor(Date.now() / 1000) - 7 * (24 * 3600), this.currentActiveEditionId).subscribe({
next: (quantity) => {
this.ticketsPreviousWeek.set(quantity);
console.warn(`hier dan?`);
this.widgetInputData.update(widgetData => this.updateWidgetData(widgetData));
}
});
}
});
}
});
}
ngOnInit (): void { }
private updateWidgetData (widgetData: WidgetData[]): WidgetData[] {
console.warn(this.ticketsLastWeek());
console.warn(this.ticketsPreviousWeek());
let widgetDataNew = [...widgetData, {
widgetId: widgetData.length + 1,
header: 'Tickets sold',
subTitle: 'Sold in the last week',
iconName: 'bootstrapCart',
data: this.ticketsLastWeek()?.toString() + ' tickets',
deviationPercentage: (((this.ticketsLastWeek() ?? 1) / (this.ticketsPreviousWeek() ?? 1)) - 1)
}];
console.log(widgetDataNew);
return widgetDataNew;
}
};
- dashboard.component.html:
<app-breadcrumbs [breadcrumbs]="breadcrumbs"></app-breadcrumbs>
<div class="container">
<div class="row justify-content-center">
@for (widget of widgetInputData(); track widget.widgetId; let idx = $index) {
<div class="col-xxl-3 col-md-3">
<div class="card border-info mb-3">
<div class="card-header bg-info text-white">
<h3>{{ widget.header }}</h3>
</div>
<div class="card-body">
<h6 class="card-subtitle text-muted">{{ widget.subTitle }}</h6>
</div>
<div class="d-flex align-items-center card-body">
<div class="card-icon rounded-circle d-flex align-items-center justify-content-center dashboard-widget">
<ng-icon name="bootstrapCart" size="1em"></ng-icon>
</div>
<div class="ps-3 card-text">
<h6>{{ widget.data }}</h6>
@if (widget.deviationPercentage) {
@if (widget.deviationPercentage === 0) {
<span class="text-info small pt-1 fw-bold"> {{ widget.deviationPercentage }}%
</span>
<span class="text-muted small pt-2 ps-1"> steady </span>
} @else if (widget.deviationPercentage < 0) { <span class="text-danger small pt-1 fw-bold">
{{ widget.deviationPercentage }}%
</span>
<span class="text-muted small pt-2 ps-1"> decrease </span>
} @else if (widget.deviationPercentage > 0) {
<span class="text-success small pt-1 fw-bold">
{{ widget.deviationPercentage }}% </span>
<span class="text-muted small pt-2 ps-1"> increase </span>
}
}
<!-- @for (edition of editions(); track edition.edition_id) {
<span class="text-warning fw-bold">{{ edition.name }}</span>
} @empty {
<span class="text-warning fw-bold">No editions found!</span>
} -->
</div>
</div>
</div>
</div>
}
</div>
</div>
this.currentActiveEditionId = this.sales.currentActiveEdition().edition_id
? Should be==
or===
. Other than that, there are multiple issues in this code (subscribe within subscribe), so it's bit difficult to pinpoint the cause of the issuethis.currentActiveEditionId
and then theif
-statement checks ifthis.currentActiveEditionId
equalstrue
.return resData
(as it's now in my code) withinticketsSoldPerPeriod
) comes after I have updated the widgetdata. And then it no longer updates the widget.sales.service.ts
. (I see in my console logging that it arrives later than me updating the data for any widget)