50
votes

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!

4

4 Answers

16
votes

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.

41
votes

If you don't care about type control.

In Angular 8 and higher versions

[(ngModel)]="$any(event).acknowledged"

From Offical Document: https://angular.io/guide/template-typecheck#disabling-type-checking-using-any

@Component({
  selector: 'my-component',
  template: '{{$any(person).addresss.street}}'
})
class MyComponent {
  person?: Person;
}
39
votes

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"
12
votes

If you use class (not interface!) you can pass the class to extract the type from it.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'as',
  pure: true,
})
export class AsPipe implements PipeTransform {

  transform<T>(value: any, clss: new (...args: any[]) => T): T {
    return value as T;
  }

}

clss argument is unused, but is serving the main goal: the type gets inferred from the constructor.

Could be used as:

class Event {
  prop: string;
}

export class MyComponent {

  MyClass = MyClass; // export class as value, that is visible in the template

}
<td mat-cell *matCellDef="let row">
  {{ (row | as : MyClass).prop }}
</td>

This will not work with interface because the interface cannot be passed into the template (at the moment of writing).

To use the interface, you can probably wrap it into the class:

class MyClass implements Partial<MyInterface> {}

Tested with Angular 11.1 and latest Ivy Language Service enabled.