feat(fp): WP-06 — kill $any() in templates (18x)
Make AsyncLoadedDirective generic with a static ngTemplateContextGuard for AsyncComponent's own internal typing. That can't propagate to consumer `<ng-template appAsyncLoaded let-p>` sites though -- Angular only infers a structural directive's type parameter from an input bound on that same node, not from a sibling input on the parent component -- so the ~9 root-cause consumers (dashboard, registration-detail, aanvraag-detail, registratie-wizard) instead unwrap the RemoteData Success value via a typed computed() and narrow it locally with `@if (x(); as p)`. The remaining union-narrowing casts (registration-summary, showcase concepts page) are replaced with a stable @let binding and a direct resource read, respectively. Documented as a deviation in WP-06's backlog file.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
@@ -30,24 +30,26 @@ import { detailRows } from '@registratie/domain/aanvraag-view';
|
||||
backLink="/dashboard"
|
||||
>
|
||||
<app-async [data]="store.applications()">
|
||||
<ng-template appAsyncLoaded let-list>
|
||||
@let a = find($any(list));
|
||||
@if (a) {
|
||||
<app-data-block
|
||||
i18n-ariaLabel="@@aanvraagDetail.ariaLabel"
|
||||
ariaLabel="Aanvraaggegevens"
|
||||
>
|
||||
@for (row of rows(a); track row.key) {
|
||||
<div app-data-row [key]="row.key" [value]="row.value"></div>
|
||||
}
|
||||
</app-data-block>
|
||||
<app-alert class="app-section" type="info" i18n="@@aanvraagDetail.stub">
|
||||
De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC.
|
||||
</app-alert>
|
||||
} @else {
|
||||
<app-alert type="warning" i18n="@@aanvraagDetail.nietGevonden"
|
||||
>Deze aanvraag is niet gevonden.</app-alert
|
||||
>
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (applications(); as list) {
|
||||
@let a = find(list);
|
||||
@if (a) {
|
||||
<app-data-block
|
||||
i18n-ariaLabel="@@aanvraagDetail.ariaLabel"
|
||||
ariaLabel="Aanvraaggegevens"
|
||||
>
|
||||
@for (row of rows(a); track row.key) {
|
||||
<div app-data-row [key]="row.key" [value]="row.value"></div>
|
||||
}
|
||||
</app-data-block>
|
||||
<app-alert class="app-section" type="info" i18n="@@aanvraagDetail.stub">
|
||||
De volledige afhandeling van deze aanvraag is nog niet beschikbaar in deze POC.
|
||||
</app-alert>
|
||||
} @else {
|
||||
<app-alert type="warning" i18n="@@aanvraagDetail.nietGevonden"
|
||||
>Deze aanvraag is niet gevonden.</app-alert
|
||||
>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
@@ -63,4 +65,10 @@ export class AanvraagDetailPage {
|
||||
|
||||
protected find = (list: Aanvraag[]): Aanvraag | undefined => list.find((a) => a.id === this.id);
|
||||
protected rows = detailRows;
|
||||
|
||||
/** See DashboardPage's `profile` for why this narrows via a computed instead of `let-`. */
|
||||
protected readonly applications = computed(() => {
|
||||
const rd = this.store.applications();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,59 +87,61 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
}
|
||||
|
||||
<app-async [data]="store.profile()">
|
||||
<ng-template appAsyncLoaded let-p>
|
||||
@let tasks = tasksFor($any(p).registration);
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (profile(); as p) {
|
||||
@let tasks = tasksFor(p.registration);
|
||||
|
||||
<section>
|
||||
@if (tasks.length) {
|
||||
<app-task-list
|
||||
class="app-section"
|
||||
i18n-listHeading="@@dashboard.watMoetIkRegelen"
|
||||
listHeading="Wat moet ik regelen"
|
||||
[tasks]="tasks"
|
||||
/>
|
||||
} @else {
|
||||
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen"
|
||||
>Wat moet ik regelen</app-heading
|
||||
<section>
|
||||
@if (tasks.length) {
|
||||
<app-task-list
|
||||
class="app-section"
|
||||
i18n-listHeading="@@dashboard.watMoetIkRegelen"
|
||||
listHeading="Wat moet ik regelen"
|
||||
[tasks]="tasks"
|
||||
/>
|
||||
} @else {
|
||||
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen"
|
||||
>Wat moet ik regelen</app-heading
|
||||
>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">
|
||||
U heeft op dit moment niets openstaan.
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie"
|
||||
>Mijn registratie</app-heading
|
||||
>
|
||||
<p class="app-text-subtle" i18n="@@dashboard.nietsOpenstaan">
|
||||
U heeft op dit moment niets openstaan.
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie"
|
||||
>Mijn registratie</app-heading
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-registration-summary [reg]="$any(p).registration" />
|
||||
</div>
|
||||
<app-data-block
|
||||
class="app-section"
|
||||
i18n-heading="@@dashboard.persoonsgegevens"
|
||||
heading="Persoonsgegevens (BRP)"
|
||||
>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.straat"
|
||||
key="Straat"
|
||||
[value]="$any(p).person.adres.straat"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.postcode"
|
||||
key="Postcode"
|
||||
[value]="$any(p).person.adres.postcode"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.woonplaats"
|
||||
key="Woonplaats"
|
||||
[value]="$any(p).person.adres.woonplaats"
|
||||
></div>
|
||||
</app-data-block>
|
||||
</section>
|
||||
<div class="app-section">
|
||||
<app-registration-summary [reg]="p.registration" />
|
||||
</div>
|
||||
<app-data-block
|
||||
class="app-section"
|
||||
i18n-heading="@@dashboard.persoonsgegevens"
|
||||
heading="Persoonsgegevens (BRP)"
|
||||
>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.straat"
|
||||
key="Straat"
|
||||
[value]="p.person.adres.straat"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.postcode"
|
||||
key="Postcode"
|
||||
[value]="p.person.adres.postcode"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@dashboard.woonplaats"
|
||||
key="Woonplaats"
|
||||
[value]="p.person.adres.woonplaats"
|
||||
></div>
|
||||
</app-data-block>
|
||||
</section>
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="6" />
|
||||
@@ -152,8 +154,10 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
||||
>
|
||||
<div class="app-section">
|
||||
<app-async [data]="store.aantekeningen()">
|
||||
<ng-template appAsyncLoaded let-r>
|
||||
<app-registration-table [rows]="$any(r)" />
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (aantekeningen(); as r) {
|
||||
<app-registration-table [rows]="r" />
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="3" />
|
||||
@@ -239,6 +243,19 @@ export class DashboardPage {
|
||||
return tasksFromProfile(reg, this.eligible());
|
||||
}
|
||||
|
||||
/** Typed narrowing for the `<app-async>` loaded slot — `<ng-template>`'s own
|
||||
context can't inherit a generic from a sibling host input (Angular only infers
|
||||
a structural directive's type parameter from an input on that same node), so
|
||||
the Success value is unwrapped here instead of through `let-`. */
|
||||
protected readonly profile = computed(() => {
|
||||
const rd = this.store.profile();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
protected readonly aantekeningen = computed(() => {
|
||||
const rd = this.store.aantekeningen();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
|
||||
/** Primary transactional actions, as an "aanvragen" list (see CIBG's
|
||||
componenten/aanvragen). The core portal sections live in the header nav now;
|
||||
the teaching pages (concepts/brief) are only reachable from here. */
|
||||
|
||||
@@ -169,83 +169,85 @@ const NL_TAALVAARDIGHEID_VRAAG = 'nl-taalvaardigheid';
|
||||
}
|
||||
@case ('beroep') {
|
||||
<app-async [data]="lookupRd()">
|
||||
<ng-template appAsyncLoaded let-data>
|
||||
<app-form-field
|
||||
i18n-label="@@regWizard.diplomaLabel"
|
||||
label="Kies het diploma waarmee u zich wilt registreren"
|
||||
fieldId="diploma"
|
||||
required
|
||||
[error]="err('diploma')"
|
||||
>
|
||||
<app-radio-group
|
||||
name="diploma"
|
||||
[options]="diplomaOptions($any(data))"
|
||||
[invalid]="!!err('diploma')"
|
||||
[ngModel]="diplomaKeuze()"
|
||||
(ngModelChange)="onDiplomaKeuze($any(data), $event)"
|
||||
/>
|
||||
</app-form-field>
|
||||
|
||||
@if (handmatigActief()) {
|
||||
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing"
|
||||
>Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw
|
||||
beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig
|
||||
beoordeeld.</app-alert
|
||||
>
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (duoData(); as data) {
|
||||
<app-form-field
|
||||
i18n-label="@@regWizard.beroepLabel"
|
||||
label="Voor welk beroep wilt u zich registreren?"
|
||||
fieldId="hm-beroep"
|
||||
i18n-label="@@regWizard.diplomaLabel"
|
||||
label="Kies het diploma waarmee u zich wilt registreren"
|
||||
fieldId="diploma"
|
||||
required
|
||||
[error]="err('diploma')"
|
||||
>
|
||||
<app-radio-group
|
||||
name="hm-beroep"
|
||||
[options]="beroepOptions($any(data))"
|
||||
name="diploma"
|
||||
[options]="diplomaOptions(data)"
|
||||
[invalid]="!!err('diploma')"
|
||||
[ngModel]="draft().beroep ?? ''"
|
||||
(ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })"
|
||||
[ngModel]="diplomaKeuze()"
|
||||
(ngModelChange)="onDiplomaKeuze(data, $event)"
|
||||
/>
|
||||
</app-form-field>
|
||||
} @else if (draft().beroep) {
|
||||
<dl class="mb-0 app-section">
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@regWizard.beroepAfgeleid"
|
||||
key="Beroep (afgeleid uit diploma)"
|
||||
[value]="draft().beroep ?? ''"
|
||||
></div>
|
||||
</dl>
|
||||
}
|
||||
|
||||
@for (q of actieveVragen($any(data)); track q.id) {
|
||||
<app-form-field
|
||||
[label]="q.vraag"
|
||||
[fieldId]="'vraag-' + q.id"
|
||||
[error]="vraagErr(q.id)"
|
||||
>
|
||||
@if (q.type === 'ja-nee') {
|
||||
@if (handmatigActief()) {
|
||||
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing"
|
||||
>Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies
|
||||
uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna
|
||||
handmatig beoordeeld.</app-alert
|
||||
>
|
||||
<app-form-field
|
||||
i18n-label="@@regWizard.beroepLabel"
|
||||
label="Voor welk beroep wilt u zich registreren?"
|
||||
fieldId="hm-beroep"
|
||||
[error]="err('diploma')"
|
||||
>
|
||||
<app-radio-group
|
||||
[name]="'vraag-' + q.id"
|
||||
[options]="jaNee"
|
||||
[invalid]="!!vraagErr(q.id)"
|
||||
[ngModel]="antwoord(q.id)"
|
||||
(ngModelChange)="
|
||||
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
|
||||
"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
name="hm-beroep"
|
||||
[options]="beroepOptions(data)"
|
||||
[invalid]="!!err('diploma')"
|
||||
[ngModel]="draft().beroep ?? ''"
|
||||
(ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })"
|
||||
/>
|
||||
} @else {
|
||||
<app-text-input
|
||||
[inputId]="'vraag-' + q.id"
|
||||
[invalid]="!!vraagErr(q.id)"
|
||||
[ngModel]="antwoord(q.id)"
|
||||
(ngModelChange)="
|
||||
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
|
||||
"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
}
|
||||
</app-form-field>
|
||||
</app-form-field>
|
||||
} @else if (draft().beroep) {
|
||||
<dl class="mb-0 app-section">
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@regWizard.beroepAfgeleid"
|
||||
key="Beroep (afgeleid uit diploma)"
|
||||
[value]="draft().beroep ?? ''"
|
||||
></div>
|
||||
</dl>
|
||||
}
|
||||
|
||||
@for (q of actieveVragen(data); track q.id) {
|
||||
<app-form-field
|
||||
[label]="q.vraag"
|
||||
[fieldId]="'vraag-' + q.id"
|
||||
[error]="vraagErr(q.id)"
|
||||
>
|
||||
@if (q.type === 'ja-nee') {
|
||||
<app-radio-group
|
||||
[name]="'vraag-' + q.id"
|
||||
[options]="jaNee"
|
||||
[invalid]="!!vraagErr(q.id)"
|
||||
[ngModel]="antwoord(q.id)"
|
||||
(ngModelChange)="
|
||||
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
|
||||
"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
} @else {
|
||||
<app-text-input
|
||||
[inputId]="'vraag-' + q.id"
|
||||
[invalid]="!!vraagErr(q.id)"
|
||||
[ngModel]="antwoord(q.id)"
|
||||
(ngModelChange)="
|
||||
dispatch({ tag: 'SetAntwoord', vraagId: q.id, value: $event })
|
||||
"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
}
|
||||
</app-form-field>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
@@ -485,8 +487,11 @@ export class RegistratieWizardComponent {
|
||||
protected lookupRd: () => RemoteData<Error | undefined, DuoLookupDto> = this.lookup.duoLookup;
|
||||
|
||||
/** Parsed lookup as a plain value (or null) — used outside the beroep step (the
|
||||
controle summary) where the <app-async> template variable isn't in scope. */
|
||||
private duoData = computed<DuoLookupDto | null>(() => {
|
||||
controle summary) where the <app-async> template variable isn't in scope, and
|
||||
inside it too: `<ng-template appAsyncLoaded>`'s own context can't inherit a
|
||||
generic from the sibling [data] input (Angular only infers a structural
|
||||
directive's type parameter from an input on that same node). */
|
||||
protected duoData = computed<DuoLookupDto | null>(() => {
|
||||
const rd = this.lookupRd();
|
||||
return rd.tag === 'Success' ? rd.value : null;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
||||
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
|
||||
import { ASYNC } from '@shared/ui/async/async.component';
|
||||
@@ -22,8 +22,10 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
backLink="/dashboard"
|
||||
>
|
||||
<app-async [data]="store.profile()">
|
||||
<ng-template appAsyncLoaded let-p>
|
||||
<app-registration-summary [reg]="$any(p).registration" />
|
||||
<ng-template appAsyncLoaded>
|
||||
@if (profile(); as p) {
|
||||
<app-registration-summary [reg]="p.registration" />
|
||||
}
|
||||
</ng-template>
|
||||
<ng-template appAsyncLoading>
|
||||
<app-skeleton height="2.5rem" [count]="6" />
|
||||
@@ -38,4 +40,10 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
|
||||
})
|
||||
export class RegistrationDetailPage {
|
||||
protected store = inject(BigProfileStore);
|
||||
|
||||
/** See DashboardPage's `profile` for why this narrows via a computed instead of `let-`. */
|
||||
protected readonly profile = computed(() => {
|
||||
const rd = this.store.profile();
|
||||
return rd.tag === 'Success' ? rd.value : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,14 +30,17 @@ import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
key="Registratiedatum"
|
||||
[value]="reg().registratiedatum | date: 'longDate'"
|
||||
></div>
|
||||
<!-- Each status variant renders only the row its own data supports. -->
|
||||
@switch (reg().status.tag) {
|
||||
<!-- Each status variant renders only the row its own data supports. A single
|
||||
@let binds status once so the @switch narrows its union by tag -- calling
|
||||
reg().status again per case would give the checker a fresh, unnarrowed call. -->
|
||||
@let status = reg().status;
|
||||
@switch (status.tag) {
|
||||
@case ('Geregistreerd') {
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.uiterste"
|
||||
key="Uiterste herregistratie"
|
||||
[value]="$any(reg().status).herregistratieDatum | date: 'longDate'"
|
||||
[value]="status.herregistratieDatum | date: 'longDate'"
|
||||
></div>
|
||||
}
|
||||
@case ('Geschorst') {
|
||||
@@ -45,28 +48,18 @@ import { DataBlockComponent } from '@shared/ui/data-block/data-block.component';
|
||||
app-data-row
|
||||
i18n-key="@@summary.geschorstTot"
|
||||
key="Geschorst tot"
|
||||
[value]="$any(reg().status).geschorstTot | date: 'longDate'"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.reden"
|
||||
key="Reden"
|
||||
[value]="$any(reg().status).reden"
|
||||
[value]="status.geschorstTot | date: 'longDate'"
|
||||
></div>
|
||||
<div app-data-row i18n-key="@@summary.reden" key="Reden" [value]="status.reden"></div>
|
||||
}
|
||||
@case ('Doorgehaald') {
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.doorgehaaldOp"
|
||||
key="Doorgehaald op"
|
||||
[value]="$any(reg().status).doorgehaaldOp | date: 'longDate'"
|
||||
></div>
|
||||
<div
|
||||
app-data-row
|
||||
i18n-key="@@summary.reden"
|
||||
key="Reden"
|
||||
[value]="$any(reg().status).reden"
|
||||
[value]="status.doorgehaaldOp | date: 'longDate'"
|
||||
></div>
|
||||
<div app-data-row i18n-key="@@summary.reden" key="Reden" [value]="status.reden"></div>
|
||||
}
|
||||
}
|
||||
</app-data-block>
|
||||
|
||||
Reference in New Issue
Block a user