82

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?

2
  • You could create a custom pipe to avoid the need of calling the static function in the template Commented Jan 25, 2017 at 16:47
  • If you are going to inject service then why are you using static keyword? what's wrong in dealing with a single instance of the service? Commented Jan 25, 2017 at 17:05

6 Answers 6

117

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)"
Sign up to request clarification or add additional context in comments.

3 Comments

Could you also create a custom decorator to add the static function to the prototype? That would also make it available in the template, but not in the component body.
Sorry, I don't know about custom decorators. I'm not using TS myself.
I don't see a "real" getter in a TypeScript way: public get parseDate(){ return DateService.parseDate;}
34

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

export class YourComponent {
    public DateService= DateService;
}

3 Comments

This is much easier than using a decorator, +1 for simplicity
Probably not the "right" way of doing this, but certainly seems like the simplest of all of these options and works just as you'd expect!
Make sure you put DateService= DateService and not DateService: DateService. Hope this saves you 5 minutes
32

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.

6 Comments

If I have multiple static functions, do I need to write custom decorator for each of the functions ?
Nope! You can put as many functions as you want in that decorator. The decorator is returning the function it decorates and takes it as an argument, so you can modify it in any way you want.
I copy pasted this exact thing. I am getting TS2355: A function whose declared type is neither 'void' nor 'any' must return a value.
I tried adding a return for target.prototype.yourStaticMethod. But that did not work. It was throwing compile error for parameters.
Works very well, but how to do you get typing support in the decorated function?
|
11

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

Comments

3

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.

Comments

2

This is how I usually did it:

public readonly DateService: typeof DateService = DateService;

and in the template:

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

This way, you don't have to create a getter, but just a public variable that will be accesible in the template.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.