-
Notifications
You must be signed in to change notification settings - Fork 25.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Define generic of ng-template #28731
Comments
Oh, this is an interesting one ! @waterplea did you consider any options for "Provide a way to define the generic in place without extra overhead"? Somehow we would have to define context type in a template but my first knee-jerk reaction is that it would be... I don't know, like writing TS code in a template. Anyway, interested in other people thoughts... |
Well, since it digs in the source code files statically anyway it could try to find the uses of this template. If you have it with context, you most certainly have a template variable for it. So there's a chance to find it in components'
and
Which makes it even more difficult. |
I think having some Yes it would be some level of typing in the template, but usually you'd probably just bind a specific type rather than define full interfaces there. Also @waterplea outlined issues with inferring it automatically, and at the end of the day having type information is better than not having type information. Trying to infer types when not defined explicitly is a separate effort that could be done on top of providing the expected type information. The language service can also analyze that ngTemplateOutletContext is set according to this type in the templates. |
With the Lack of type safety for ng-template is one of the big places in Angular at the moment where type safety really suffers. Contrary to my half-baked proposal above I would instead suggest something like this, which closely follows the implementation of
This also avoids the issue of introducing types directly into the template and would still leave automatic discovery of the proper type to be an option in the future. Ideally using |
Is there update? |
Would be a great addition. |
There's repeated calls for better documentation on templates, for example #39349, and promoting template usage is slightly dangerous with the lack of type safety we currently have in the framework. So I'd like to shine light on this issue and my proposal above again as well as ask whether this issue qualifies for the cross-cutting types label. |
Is there any workaround to add type context for let-variables? Even without language service, just for compiler? |
@Lonli-Lokli
Then you need a variable with your desired context type, and you can use it like follows
After that you get a type error with the new ivy language services: |
@kentkwee thanks, looks promising! |
@Lonli-Lokli |
@kentkwee my goal is [appTemplateContext]="item" but it's seems like not allowed now, unfortunately. |
Okay. I understand.
|
@kentkwee it does not work, you can check it there https://github.com/Lonli-Lokli/templateContextGuard |
@Lonli-Lokli You still need to actually pass the |
Disclaimer! I'm the author of ng-as Angular library with pipe and directive for type casting template variables. Casting with directirve eg.: import { Component } from '@angular/core';
// your interface, but also work with any typescript type (class, type, etc.)
interface Person {
name: string;
}
@Component({
selector: 'app-root',
template: `
<ng-container *ngTemplateOutlet="personTemplate; context: {$implicit: person}"></ng-container>
<ng-template #personTemplate [ngAs]="Person" let-person>
<span>Hello {{ person.name }}!</span>
</ng-template>
`,
})
export class AppComponent {
// NOTE: If you have "strictPropertyInitialization" enabled,
// you will need to add a non-null assertion (!)
public Person!: Person; // publish your interface into html template
person: Person = { name: 'Simone' }; // the data
} Casting with pipe eg.: import { Component } from '@angular/core';
// your interface, but also work with any typescript type (class, type, etc.)
interface Person {
name: string;
}
@Component({
selector: 'app-root',
template: `
<ng-container *ngTemplateOutlet="personTemplate; context: {$implicit: person}"></ng-container>
<ng-template #personTemplate let-person>
<span>Hello {{ (person | as: Person).name }}!</span>
</ng-template>
`,
})
export class AppComponent {
// NOTE: If you have "strictPropertyInitialization" enabled,
// you will need to add a non-null assertion (!)
public Person!: Person; // publish your interface into html template
person: Person = { name: 'Simone' }; // the data
} source of pipe: import { Pipe, PipeTransform } from "@angular/core";
@Pipe({ name: 'as', pure: true })
export class NgAsPipe implements PipeTransform {
// eslint-disable-next-line no-unused-vars
transform<T>(input: unknown, baseItem: T | undefined): T {
return input as unknown as T;
}
} source of directive: import { Directive, Input } from "@angular/core";
interface NgAsContext<T> {
ngLet: T;
$implicit: T;
}
@Directive({ selector: '[ngAs]' })
export class NgAsDirective<T> {
@Input() ngAs!: T;
static ngTemplateGuard_ngLet: 'binding';
static ngTemplateContextGuard<T>(dir: NgAsDirective<T>, ctx: any): ctx is NgAsContext<Exclude<T, false | 0 | '' | null | undefined>> {
return true;
}
} That's all!More info on: https://www.npmjs.com/package/ng-as |
Would be really nice to have a build in solution without workarounds and duplication. We use ng-template and ngTemplateOutlet a lot. |
@pburgmer since it doesn't seem to be likely that they will implement this any time soon, I would recommend to try out ng-polymorpheus which not only allows you to type template context but also has some other cool features. |
@hakimio Thanks for the hint. Looks very promising. Will try it. |
as parrallel info, here is an interesting solution to strong type variable in template, here for mat-table of angular material : https://nartc.me/blog/typed-mat-cell-def/ |
So is there any progress on this? |
Simplest Workaround:Every time I try to research this to see if something new has come along I invariably revert back to the simplicity of just creating a new typed scoped using *ngIf.
Where Notice this is NOT a typeguard and there's no new directive or pipe involved.
Keeping consistent naming means that if generic templates are ever introduced I can search for |
That's a great workaround I hadn't thought of, thanks for sharing :) |
it would be nice if template accepts the typing like: <ng-template #productTemplate let-product="product as Product"> |
If you use
import {Directive, Input} from '@angular/core';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'ng-template[typedTemplate]',
standalone: true
})
export class TypedTemplateDirective<TypeToken> {
@Input('typedTemplate')
typeToken: TypeToken;
static ngTemplateContextGuard<TypeToken>(
_dir: TypedTemplateDirective<TypeToken>,
_ctx: unknown
): _ctx is TypeToken {
return true;
}
}
<ng-template let-myNumber="myNumber" let-myText="myText" [typedTemplate]="templateType">
<h2>{{ myText.toUpperCase() }}</h2>
<p>{{ myNumber.toFixed(2) }}</p>
</ng-template>
templateType: {
myText: string;
myNumber: number;
}; Tested in "WebStorm", should also work in "VS Code". |
One possible solution can be to use a directive for the context consumer and a pipe for the context provider.
import { Directive, Input } from '@angular/core';
@Directive({ selector: 'ng-template[templateContextType]' })
export class TemplateContextTypeDirective<T> {
@Input() protected templateContextType!: T;
public static ngTemplateContextGuard<T>(
dir: TemplateContextTypeDirective<T>,
ctx: unknown
): ctx is T {
return true;
}
}
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'typify' })
export class TypifyPipe implements PipeTransform {
public transform<T>(data: T, type: T): T {
return data;
}
}
import { Component, Input } from '@angular/core';
@Component({
selector: 'products-list',
templateUrl: './products-list.component.html',
})
export class ProductsList {
protected contextType!: {
products: Array<string>;
noDataMessage: string;
loadingMessage: string;
};
@Input() public products!: Array<string> | null;
}
<h2>Products list</h2>
<ng-container
[ngTemplateOutlet]="
products ? (products.length ? productsListTmpl : noDataTmpl) : loadingTmpl
"
[ngTemplateOutletContext]="
{
products: products,
noDataMessage: 'There are no products so far',
loadingMessage: 'Loading products...'
} | typify: contextType
"
></ng-container>
<ng-template
#productsListTmpl
let-products="products"
[templateContextType]="contextType"
>
<p *ngFor="let product of products">{{ product }}</p>
</ng-template>
<ng-template
#loadingTmpl
let-loadingMessage="loadingMessage"
[templateContextType]="contextType"
>
<p>{{ loadingMessage }}</p>
</ng-template>
<ng-template
#noDataTmpl
let-noDataMessage="noDataMessage"
[templateContextType]="contextType"
>
<p>{{ noDataMessage }}</p>
</ng-template> Here is a simple demo. |
What about defining templates outside of the component's template in a type-safe way? @Component({
...
template: `<mc-user-list [actions]="actionsTemplate"/>`
})
class MyCmp {
// Type is inferred to `TemplateRef<{user: User}>`
actionsTemplate = createTemplate<{user: User}>(
`<mc-button (click)="removeUser(user)">REMOVE</mc-button>`, {
context: this, // or `context: { removeUser(user: User) { this.removeUser(user); }}`
imports: [ButtonCmp]
})
removeUser(user: User) {
...
}
} |
A few examples above use Important note: Currently this same approach does not work with a signal
It's not immediately clear why, my best guess is it's something related to the generated code from a decorator that wouldn't exist for signal inputs. |
Does the workaround with ngTemplateContextGuard work if you have a built and npm released library that uses input signals? I have a library that uses input signals and the ngTemplateContextGuard workaround works if the library is not built. But once i build it, the type of the $implicit object is unknown. No idea if it worked before with @input or if it is another problem. This issue really needs to be looked at, because the developer experience is quite bad. Would be nice if it became a priority. |
This is also exactly my problem. A generic component published in our custom library is not correctly type-inferred when used inside a template. |
I think Svelte 5 has a very elegant solution with their "snippets" that could be the source for inspiration how to evolve Angular here: https://svelte.dev/docs/svelte/snippet |
Umh... made a proposal some time ago #56056 (mostly DX driven without tech details on how to implement it). How about using arrow functions / inputs / template vars? Idea: in a regular parent-child components relationship, an It's a different perspective where a component can accept arbitrary inputs (primitives, objects / functions, templates) and those inputs can be defined in templates (same story as Here is a pseudo-example (needs beautification): // ----------------------------------------
// consumer-of-my-tree.component.ts
@Component({
selector: 'consumer-component',
// ...
template: `
@ref customNode = (node) => {
<ng-template>
@if (node.desc) {
{{ node.desc }}
}
// manageNodeClick is defined in ConsumerComponent
<span [class]="node.style" (click)="manageNodeClick(node)">{{ node.label }}</span>
</ng-template>
}
<my-tree-component [data]="dataStructure" [customNode]="customNode"></my-tree-component>
`,
})
export class ConsumerComponent {...}
// ----------------------------------------
// my-tree.component.ts
export interface Node {
id: string;
desc?: string;
label: string;
style: string;
}
@Component({
selector: 'my-tree-component',
// ...
template: `
<!-- Of course the usage of ngTemplateOutlet below
is just to provide ideas: requires further investigation / adaptations
-->
for (node of nodes(); track node.id) {
@let temp = customNode();
<ng-container [ngTemplateOutlet]="temp(node)"></ng-container>
}
`,
})
export class MyTreeComponent {
data = input.required<...>();
// This is the place where the type is defined (I hope something like this is enough)
customNode = input<(node: Node) => TemplateRef<any>>();
nodes = signal<Node[]>([]); // populated started from data
} |
My approach to this problem (early solution, I haven't tested any performance yet): pipe:
component:
template:
|
@MarekMlodzianowski the directive workaround is just as simple and requires much less code to type the |
@hakimio I tested the directive workaround with Angular v19 / TypeScript 5.7.2 / VS Code 1.96.2, and it doesn't work for me. The |
@MarekMlodzianowski I am using it with Here it's working with Have you enabled "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
} |
I had the same config as you. After restarting the TypeScript service and VSCode a few times, the directive started working. Since my Angular app is wrapped in an NX monorepo, I suspect some aggressive caching might have been causing the issue. 🎈 @hakimio Thank you so much for your help! 😊 Edit: After refactoring the view (previously, I focused only on static types as shown in the screenshots), the directive is still not working (binding prints empty / undefined value). It seems the IDE correctly recognizes the type, but when the app is running, the let-value is missing. That’s strange, and I’ll investigate further to figure out what’s going on. ng-template without it works fine... 🎢 Edit-2: Now it works :) The problem was
|
Thank you so much guys |
🚀 feature request
Relevant Package
This feature request is for @angular/language-serviceDescription
I quite often use ng-templates in my application to later include them in the *ngTemplateOutlet and pass them the context. However when I define the template itself, it doesn't know the interface the context will have, so say this:
Gives me an error
ng: The template context does not defined a member called 'index'.
(note also a typo here)Under ng-template there's
TemplateRef<T>
, but there is no way to define that T.Describe the solution you'd like
Provide a way to define the generic in place without extra overhead
Describe alternatives you've considered
At least disable this error as it has no purpose if you have any complex template usage
The text was updated successfully, but these errors were encountered: