Reputation: 9622
I'm working on an Angular project (Angular 4.0.0) and I'm having trouble binding a property of an abstract class to ngModel because I first need to cast it as the concrete class it actually is in order to access the property.
i.e. I have an AbstractEvent class this has a a concrete implementation Event which has a boolean property 'acknowledged' which I need a two way binding via ngModel to set with a checkbox.
I currently have this element in my DOM:
<input type="checkbox" *ngIf="event.end" [(ngModel)]="(event as Event).acknowledged"
[disabled]="(event as Event).acknowledged">
Unfortunately this is throwing the following error:
Uncaught Error: Template parse errors: Parser Error: Missing expected ) at column 8 in [(event as Event).acknowledged]
Googling around seemed to suggest this might be because using 'as' is not supported when using it inside a template? Although I'm not certain about this.
I also can't work out how to just write a function for it in my typescript file driving the template because this would break the two way binding on ngModel that I require.
If anyone has any way to get around this or perform type casting in angular templates correctly I would be very appreciative!
Upvotes: 92
Views: 80343
Reputation: 3538
If you don't care about type control.
In Angular 8 and higher versions
[(ngModel)]="$any(event).acknowledged"
From Offical Document: https://angular.dev/tools/cli/template-typecheck
New Document: https://angular.dev/tools/cli/template-typecheck
@Component({
selector: 'my-component',
template: '{{$any(person).addresss.street}}'
})
class MyComponent {
person?: Person;
}
Upvotes: 101
Reputation: 24531
This pipe can be used to take the type from various inputs. It works pretty well with classes, named types / interfaces and primitives.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'as',
pure: true,
})
export class AsPipe implements PipeTransform {
transform<T>(value: any, _type: (new (...args: any[]) => T) | T): T {
return value as T;
}
}
_type
argument is unused, but is serving the main goal: the type gets inferred from the constructor / variable.
Could be used as:
class ClassEvent {
prop: string;
}
interface InterfaceEvent {
prop: string;
}
export class MyComponent {
MyClass = ClassEvent; // class constructor
MyInterface: InterfaceEvent; // typed property
propString: any; // primitive, string
propNumber: any; // primitive, number
}
<td mat-cell *matCellDef="let row">
Type from class constructor: {{ (row | as : MyClass).prop }}
Type from interface: {{ (row | as : MyInterface).prop }}
Type from primitive, string: {{ (propString | as : '').substr(1) }}
Type from primitive, number: {{ (propNumber | as : 123).toFixed(2) }}
</td>
Requires strict templates and Ivy.
Upvotes: 42
Reputation: 7804
This is still not supported by Angular. You can create a custom pipe or a function to typecast.
Or you can cast it to 'any' using the following syntax: $any() .
Example:
{{$any(person).address.street}}
Reference doc: https://angular.io/guide/template-typecheck
Upvotes: 0
Reputation: 4887
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 ngTemplateContextGuard<T>(dir: NgAsDirective<T>, ctx: any): ctx is NgAsContext<Exclude<T, false | 0 | '' | null | undefined>> {
return true;
}
}
More info on: https://www.npmjs.com/package/ng-as
Upvotes: 1
Reputation: 8119
Using my TypeSafe generics answer:
And inspired from smnbbrv answer pass type explicitly as an optional argument when there is nowhere to infer the type from.
import { Pipe, PipeTransform } from '@angular/core';
/**
* Cast super type into type using generics
* Return Type obtained by optional @param type OR assignment type.
*/
@Pipe({ name: 'cast' })
export class CastPipe implements PipeTransform {
/**
* Cast (S: SuperType) into (T: Type) using @Generics.
* @param value (S: SuperType) obtained from input type.
* @optional @param type (T CastingType)
* type?: { new (): T }
* type?: new () => T
*/
transform<S, T extends S>(value: S, type?: new () => T): T {
return <T>value;
}
}
Usage:
template.html
<input
type="checkbox"
*ngIf="event.end"
[(ngModel)]="(event | cast: Event).acknowledged"
[disabled]="(event | cast: Event).acknowledged"
/>
component.ts
export abstract class AbstractEvent {
end: boolean;
}
export class Event extends AbstractEvent {
acknowledged: boolean;
}
export class MyComponent{
event: AbstractEvent;
Event = Event;
}
Upvotes: 9
Reputation: 3840
You can also create a function that returns a Type Predicate.
app.component.html
<some-component *ngIf="isFoo(foo)" [foo]="foo"></some-component>
app.component.ts
isFoo(value: Foo | Bar): value is Foo {
return value === 'Foo';
}
This will cast the template variable foo
to type Foo
and will silence any strictTemplate errors regarding union types.
Upvotes: 6
Reputation: 79
To expand on the answer by @smnbbrv, you can use a similar syntax with interfaces as follows:
@Pipe({ name: 'as', pure: true })
export class AsPipe implements PipeTransform {
transform<T>(input: unknown, baseItem: T | undefined): T {
return (input as unknown) as T;
}
}
This requires us to provide a "baseItem" of the correct type. However, we do not need to actually create the item, we only need to declare it (since the item can be undefined). That means we can create a variable of the suggested type in our class as follows:
export interface Person{
name: string;
age: number;
}
export class MyComponent {
Person: Person;
}
Take note, we're not assigning any value to the baseItem
, we're simply specifying its type. If you have strictPropertyInitialization
enabled, you will need to add a non-null assertion to your baseItem
export class MyComponent {
Person!: Person;
}
This can then be used in your template as follows:
<td mat-cell *matCellDef="let row">
{{ (row | as : Person).name }}
</td>
Upvotes: 6
Reputation: 7456
As mentioned, using a barebone method call will have performance impact.
A better approach is to use a pipe, and you have best of both worlds. Just define a Cast pipe:
@Pipe({
name: 'cast',
pure: true
})
export class CastPipe implements PipeTransform {
transform(value: any, args?: any): Event {
return value;
}
}
and then in your template, use event | cast
when you need the cast.
That way, change detection stays efficient, and typing is safe (given the requested type change is sound of course).
Unfortunately, I don't see a way to have this generic because of the name
attribute, so you'd have to define a new pipe for each type.
Upvotes: 29
Reputation: 657078
That's not possible because Event
can't be referenced from within the template.
(as
is also not supported in template binding expressions)
You need to make it available first:
class MyComponent {
EventType = Event;
then this should work
[(ngModel)]="(event as EventType).acknowledged"
update
class MyComponent {
asEvent(val) : Event { return val; }
then use it as
[(ngModel)]="asEvent(event).acknowledged"
Upvotes: 50