Skip to content

Angular Examples

Illustrative only. The Angular + PrimeNG Standards page states the rules; this page shows the three that a reader cannot infer from the rule alone.

The store itself is unremarkable. The lifetime is the point — no providedIn, listed in the routed page’s providers, so it dies with the page.

features/route-optimization/data-access/route-optimization-store.ts
@Injectable() // ← no providedIn: 'root'
export class RouteOptimizationStore {
private readonly api = inject(RouteOptimizationApi);
private readonly _branches = signal<Branch[]>([]);
private readonly _selectedId = signal<string | null>(null);
private readonly _loading = signal(false);
readonly branches = this._branches.asReadonly();
readonly loading = this._loading.asReadonly();
readonly selected = computed(() =>
this._branches().find(b => b.id === this._selectedId()) ?? null);
async load(): Promise<void> {
this._loading.set(true);
const result = await this.api.getBranches();
if (result.kind === 'ok') this._branches.set(result.branches);
this._loading.set(false);
}
}
features/route-optimization/pages/route-optimization-page.ts
@Component({
selector: 'app-route-optimization-page',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [RouteOptimizationStore], // ← created and destroyed with this page
templateUrl: './route-optimization-page.html',
})
export class RouteOptimizationPage {
protected readonly store = inject(RouteOptimizationStore);
}

Make it providedIn: 'root' and the second visit to the page shows the first visit’s data before the reload lands. That bug looks like a caching feature until a user notices stale numbers.

Transport failure becomes a value at the boundary, so the store handles it with a switch, not a try.

export type BranchesResult =
| { kind: 'ok'; branches: Branch[] }
| { kind: 'unavailable' };
@Injectable({ providedIn: 'root' })
export class RouteOptimizationApi {
private readonly http = inject(HttpClient);
getBranches(): Promise<BranchesResult> {
return firstValueFrom(
this.http.get<BranchDto[]>('/api/branches').pipe(
map(dtos => ({ kind: 'ok', branches: dtos.map(toBranch) }) satisfies BranchesResult),
catchError(() => of({ kind: 'unavailable' } satisfies BranchesResult)),
),
);
}
}

satisfies on both arms means adding a third result variant is a compile error at every call site, instead of a silent fall-through.

Per-request opt-out uses a context token, never the URL:

export const SKIP_AUTH = new HttpContextToken(() => false);
// caller
this.http.get('/api/public/config', { context: new HttpContext().set(SKIP_AUTH, true) });
// interceptor
export const authInterceptor: HttpInterceptorFn = (req, next) =>
req.context.get(SKIP_AUTH) ? next(req) : next(withBearerToken(req));

Sniffing req.url.includes('token') works until an unrelated endpoint contains that substring.

// app.config.ts — client branding as a preset. One place, follows dark mode, survives upgrades.
providePrimeNG({
theme: {
preset: definePreset(Aura, {
semantic: {
primary: { 500: '{sky.500}', 600: '{sky.600}' },
},
}),
},
});
<!-- A single instance that must differ: the scoped dt property. -->
<p-button label="Save" [dt]="{ root: { borderRadius: '0' } }" />
/* Banned. Wins by specificity today, breaks on the next library upgrade,
and the override loads in lazy-chunk order — so it applies in one feature
and not another, for reasons nobody can see from the component. */
::ng-deep .p-button { border-radius: 0; }

There is no Karma config and no Jest config. The runner is wired in two places:

angular.json
"test": { "builder": "@angular/build:unit-test" }
tsconfig.spec.json
{ "compilerOptions": { "types": ["vitest/globals"] } }

Then npm testng test.

Back to Angular + PrimeNG Standards.

👤 Owner: Kenana Reda🗓 Last reviewed: 2026-07-25

اقرأ هذه الصفحة بالعربية