Call static function from angular2 template

I'm trying to build 'utility' services (classes) for an angular project. The utility classes have static functions (so we don't have to instantiate needless objects). One example is this:

import { Injectable } from '@angular/core';

@Injectable()
export class DateService {
   constructor() {
   }

   public static parseDate(dateString: string): Date {
       if (dateString) {
           return new Date(dateString);
       } else {
           return null;
       }
   }
}

In my component class file, I then import it like so:

import { DateService } from '../utilities/date.service';

and within the class code like this works:

ngOnInit():void {
  let temp = DateService.parseDate("2012/07/30");
  console.log(temp);  // Mon Jul 30 2012 00:00:00 GMT-0500 (Central Daylight Time)
 }

However, I would like to be able to use these utility functions within the angular html template, like so:

<label for="date">Date</label>
          <input type="date" class="form-control" id="date" required
            [value]="event.date | date: 'yyyy-MM-dd'" (input)="event.date=DateService.parseDate($event.target.value)" name="date">

Unfortunately, that does not work; giving a "Cannot read property 'parseDate' of undefined" error.

Now, I can move the 'parseDate' function to the component class, and that works fine (with the required change in the template, of course)... however, if I have a bunch of components, they'd all need to have their own 'parseDate' function and I think we all know that's a bad idea that doesn't scale well. (please ignore the trivial nature of the parseDate function)

Further, even though I don't really want to instantiate an object just to run a static function, I try it with actual dependency injection. Adding it to the providers array, and building a instance in the constructor - like so:

constructor(private _dateService: DateService) { }

and then changing my template to:

label for="date">Date</label>
          <input type="date" class="form-control" id="date" required
            [value]="event.date | date: 'yyyy-MM-dd'" (input)="event.date=_dateService.parseDate($event.target.value)" name="date">

This also fails, this time with a with a "self.context._dateService.parseDate is not a function" error. Removing the 'static' from the function fixes the problem and I could move on, but I'm still left needing to instantiate something just to run what should be just a static function. Surely there is a better way.

Thoughts?


Solution 1:

Only instance members of the components class can be called from the view.

If you want to call static members, you need to provide a getter in the component.

export class MyComponent {
  parseDate = DateService.parseDate;
}

then you can use it like

(input)="event.date=parseDate($event.target.value)"

Solution 2:

Gunter's answer is perfectly valid and is the way I've done it most of the time.

If you are using typescript, you additionally have the option of creating a custom decorator to provide functions to your view so your component remains uncluttered.

Example:

Define a decorator:

import {StaticClassFunctions} from "./static-class-functions"

export function CustomDecorator(): Function {
    return (target: Function): Function => {
        target.prototype.yourStaticMethod = (param1) => {
            return StaticClassFunctions.yourStaticMethod(param1);
        }
    }
}

Apply the decorator to your component:

@Component{ ... }
@CustomDecorator()
export class YourComponent { ... }

Now your have access to those static functions in your view without having to declare them in your Component! Very useful for repetitive "utility" functions to support view formatting and such (like enum casting!)

<span>{{yourStaticMethod(yourInput)}}</span>

You won't have access in your component though unless you declare the function at the top so it can compile.

Solution 3:

You can declare a field in your component that will make the class accessible for your template.

export class YourComponent {
    public DateService= DateService;
}

Solution 4:

There is already a pattern for doing this in Angular called pipes. In Angular 1 this was called filters.

In Angular you create your custom pipe class and use the | in the template to pass values to. There is a built in one for dates, it is used like:

{{ "2017-01-24" | parseDate }}

Of course if this pipe does not do what you want, you can create your own:

@Pipe({
  name: 'parseDate'
})
export class ParseDate implements PipeTransform {

  transform(value: string): string {
    return DateService.parseDate(value);
  }
}

For more info please see: https://angular.io/docs/ts/latest/guide/pipes.html

Solution 5:

This is how I did it once -

public get DateService(): typeof DateService {
    return DateService;
}

and used it from the template as -

(input)="event.date=DateService.parseDate($event.target.value)"

It is basically @Sergey's version, but in form of a more "TypeScripty" getter which clarified that I'm returning the type of the class and thus it's static members would be exposed at the template.