Reputation: 3416
How can parent component recognise type of let-content
which comes from ngTemplateOutletContext
? Now {{content.type}}
works correctly, but IDE says:
unresolved variable type
How can I type it as Video
?
parent.component.ts:
export interface Video {
id: number;
duration: number;
type: string;
}
public videos: Video = [{id: 1, duration: 30, type: 'documentary'}];
parent.component.html:
<ul>
<li *ngFor="let video of videos">
<tile [bodyTemplate]="tileTemplate" [content]="video"></app-card>
</li>
</ul>
<ng-template #tileTemplate let-content>
<h5 class="tile__type">{{content.type}}</h5>
</ng-template>
tile.component.ts:
@Component({
selector: 'tile',
templateUrl: './tile.component.html',
styleUrls: ['./tile.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CardComponent {
@Input() tileTemplate: TemplateRef<any>;
@Input() content: Video;
}
tile.component.html:
<div
...
<ng-container
[ngTemplateOutlet]="tileTemplate"
[ngTemplateOutletContext]="{ $implicit: content }">
</ng-container>
...
</div>
Upvotes: 63
Views: 41663
Reputation: 6409
A solutión without the creation of a new directive, this is somehow another workaround very similar to @Reacangular's.
This can be solved by wrapping your variable inside another ng-template
, but I liked a lot more than the other solutions because it just adds 2 more lines of code in the HTML, of course if you're using your variable only 1 or 2 times @Reactangular answer is better. My answer:
Instead of this:
<ng-template *ngTemplateOutlet="foo; context: {$implicit: {fooProp: 'Hello!'}}"></ng-template>
<ng-template #foo let-args>
This is untyped: {{ args.fooProp }}<br>
</ng-template>
Do this:
<ng-template *ngTemplateOutlet="foo; context: {$implicit: {fooProp: 'Hello!'}}"></ng-template>
<ng-template #foo let-untypedArgs>
<ng-container *ngIf="identity(untypedArgs) as args">
This is typed: {{ args.fooProp }}<br>
</ng-container>
</ng-template>
identity(foo: Foo): Foo {
return foo;
}
<ng-template #foo let-untypedArgs>
@if (identity(untypedArgs); as args) {
This is typed: {{ args.fooProp }}<br>
}
</ng-template>
<ng-template #foo let-untypedArgs>
@let args = identity(untypedArgs)
This is typed: {{ args.fooProp }}<br>
</ng-template>
The type assertion is noticed by the IDE when *ngFor
or *ngIf
is in use. The downside with this solution is that the inner <ng-container>
is rendered later because of the *ngIf
.
With this, now if you add an invalid property to your context, you'll get the following compilation error which is great, here's a stackblitz demo:
Property 'newFooProp' does not exist on type 'Foo'.
As stated in comments, just like the accepted answer; this solution has the downward of calling ngZone
every lifecycle, It might be recommended only when using together with ChangeDetectionStrategy.OnPush
.
Upvotes: 4
Reputation: 3419
I have another workaround that works since Angular 18 using the @let
statement.
In Typescript:
asPerson(person: Person) : Person {
return person;
}
In your Angular Template
<ng-template let-param="untypedPerson">
@let person = asPerson(untypedPerson);
{{person.name}}
</ng-template>
Upvotes: 3
Reputation: 6401
You type any template variable using an *ngIf and a function to type it
<ng-container *ngIf="asMyType(anyType) as myType">
<!-- myType is typed here -->
</ng-container>
const asMyType = (something: unknown) => something as myType;
So you can apply this same method inside a ng-template to type a variable
<ng-template let-my-type="my-type">
<ng-container *ngIf="asMyType(my-type) as myType">
<!-- myType is typed here -->
</ng-container>
</ng-template>
Upvotes: 3
Reputation: 541
I created a helper directive to solve this.
import { Directive, Input, TemplateRef } from '@angular/core';
@Directive({selector: 'ng-template[typedTemplate]'})
export class TypedTemplateDirective<TypeToken> {
// how you tell the directive what the type should be
@Input('typedTemplate')
typeToken: TypeToken;
// the directive gets the template from Angular
constructor(private contentTemplate: TemplateRef<TypeToken>) {
}
// this magic is how we tell Angular the context type for this directive, which then propagates down to the type of the template
static ngTemplateContextGuard<TypeToken>(dir: TypedTemplateDirective<TypeToken>, ctx: unknown): ctx is TypeToken{ return true; }
}
Use it like this
<!-- typedTemplate is the directive, typeToken is an object on our component -->
<ng-template #someTemplate [typedTemplate]="typeToken" let-param="param">
{{param}}
</ng-template>
And in the component
// here we create typeToken. the value doesn't matter as it's never used, but the type of this variable defines the types of all template parameters.
typeToken: { param: string };
Upvotes: 43
Reputation: 1435
There is an easy workaround to get the IDE to play along and also have code completion using a user-defined type guard:
Create a function in your class which takes the variable as an argument and returns the same variable:
export class CardComponent {
...
public video = (item: Video) => item;
}
Now simply wrap the variable in your template with the function:
<h5 class="tile__type">{{video(content).type}}</h5>
Upvotes: 8
Reputation: 54741
There is no type inference for let-*
variables. The let-
context is part of the micro syntax parser for Angular, and an IDE can not infer the type as there is no clear origin.
https://gist.github.com/mhevery/d3530294cff2e4a1b3fe15ff75d08855
You can try to silence the IDE warning using $any()
https://angular.io/guide/template-syntax#the-any-type-cast-function
<ng-template #tileTemplate let-content>
<h5 class="tile__type">{{$any(content).type}}</h5>
</ng-template>
You can force type inference by using a function
<ng-template #tileTemplate let-content>
<h5 class="tile__type">{{toVideo(content).type}}</h5>
</ng-template>
public toVideo(value: any): Video { return value as Video; }
Upvotes: 37