What is best practice to create an AngularJS 1.5 component in Typescript?
Asked Answered
L

7

36

I am experimenting with the .component() syntax in Angular 1.5.

It seems that the latest fashion is to code the controller in-line in the component rather that in a separate file, and I can see the advantage of that given that the component boilerplate is minimal.

The problem is that I having been coding my controllers as typescript classes and would like to continue doing so because that seems to be consistent with Angular2.

My best effort is something like this:

export let myComponent = {
  template: ($element, $attrs) => {
    return [
      `<my-html>Bla</my-html>`
    ].join('')
  },
  controller: MyController
};
class MyController {

}

It works, but it's not elegant. Is there a better way?

Leroylerwick answered 17/2, 2016 at 8:42 Comment(3)
By not elegant do you mean that you want the code cleaned up?Widener
@Widener I suppose you could put it that way :). I haven't been able to find an example of a 1.5 component in Typescript so I was wondering if the way I have done it is best practice. e.g. as per the heading, can I define the whole thing as a class?Leroylerwick
To be honest if it works great, but it isn't the style to write angular 1 stuff in Typescript and your post is the first I've seen. Generally I think you should write Angular 1 in pure javascript following the conventions recommended. I know this doesn't really answer your question though...Widener
C
36

If you wanted to completely adopt an Angular 2 approach, you could use:

module.ts

import { MyComponent } from './MyComponent';

angular.module('myModule', [])
  .component('myComponent', MyComponent);

MyComponent.ts

import { Component } from './decorators';

@Component({
  bindings: {
    prop: '<'
  },
  template: '<p>{{$ctrl.prop}}</p>'
})
export class MyComponent {

   prop: string;

   constructor(private $q: ng.IQService) {}

   $onInit() {
     // do something with this.prop or this.$q upon initialization
   }
}

decorators.ts

/// <reference path="../typings/angularjs/angular.d.ts" />

export const Component = (options: ng.IComponentOptions) => {
  return controller => angular.extend(options, { controller });
};
Circum answered 14/4, 2016 at 21:55 Comment(4)
And how would you access prop in MyComponent with Typescript?Alisander
It works perfectly in Chrome, Safari and Edge, but Firefox fails with error Error: class constructors must be invoked with |new| on the $controllerInit command. Any ideas how to fix it?Straka
Should I use any module loader to get this ng2 style works in ng1? How angular knows about this component only via annotation? Do you have any sample repo?Tomkins
I don't think this will work with any modern version of TypeScript since whatever the decorator returns must be compatible with the Controller classLowder
S
34

I am using a simple Typescript decorator to create the component

function Component(moduleOrName: string | ng.IModule, selector: string, options: {
  controllerAs?: string,
  template?: string,
  templateUrl?: string
}) {
  return (controller: Function) => {
    var module = typeof moduleOrName === "string"
      ? angular.module(moduleOrName)
      : moduleOrName;
    module.component(selector, angular.extend(options, { controller: controller }));
  }
}

so I can use it like this

@Component(app, 'testComponent', {
  controllerAs: 'ct',
  template: `
    <pre>{{ct}}</pre>
    <div>
      <input type="text" ng-model="ct.count">
      <button type="button" ng-click="ct.decrement();">-</button>
      <button type="button" ng-click="ct.increment();">+</button>
    </div>
  `
})
class CounterTest {
  count = 0;
  increment() {
    this.count++;
  }
  decrement() {
    this.count--;
  }
}

You can try a working jsbin here http://jsbin.com/jipacoxeki/edit?html,js,output

Schleswig answered 2/3, 2016 at 16:8 Comment(4)
BTW this is almost identical to Angular 2 component, so less pain to upgrades. In case of using both versions decorator can be renamed to Ng1ComponentSchleswig
in this Typescript decorator line controllerAs?: string, and the next 2 lines there are this error in my case: TS1005: ";" expected. Why? :/ Thanks. Just I copied & pastedKoenig
Adopting from scarlz you could replace your options type with ng.IComponentOptionsEspagnole
While I love this idea, one downside is that it requires a reference to your angular module, which, depending on your application structure, may not be easily had or even exist yet. Especially if you're using functional silos with ES6 module loading. In this case, you would wind up having all your components in your index.js file or some other anti-pattern.Indwell
M
14

This is the pattern I use:

ZippyComponent.ts

import {ZippyController} from './ZippyController';

export class ZippyComponent implements ng.IComponentOptions {

    public bindings: {
        bungle: '<',
        george: '<'
    };
    public transclude: boolean = false;
    public controller: Function = ZippyController;
    public controllerAs: string = 'vm'; 
    public template: string = require('./Zippy.html');
}

ZippyController.ts

export class ZippyController {

    bungle: string;
    george: Array<number>;

    static $inject = ['$timeout'];

    constructor (private $timeout: ng.ITimeoutService) {
    }
}

Zippy.html

<div class="zippy">
    {{vm.bungle}}
    <span ng-repeat="item in vm.george">{{item}}</span>
</div>

main.ts

import {ZippyComponent} from './components/Zippy/ZippyComponent';

angular.module('my.app', [])
    .component('myZippy', new ZippyComponent());
Mouth answered 2/3, 2016 at 12:53 Comment(2)
Since you use require, do you build this with babel?Etana
I have been building with webpack.Mouth
D
9

I was struggling with the same question and put my solution in this article:

http://almerosteyn.github.io/2016/02/angular15-component-typescript

module app.directives {

  interface ISomeComponentBindings {
    textBinding: string;
    dataBinding: number;
    functionBinding: () => any;
  }

  interface ISomeComponentController extends ISomeComponentBindings {
    add(): void;
  }

  class SomeComponentController implements ISomeComponentController {

    public textBinding: string;
    public dataBinding: number;
    public functionBinding: () => any;

    constructor() {
      this.textBinding = '';
      this.dataBinding = 0;
    }

    add(): void {
      this.functionBinding();
    }

  }

  class SomeComponent implements ng.IComponentOptions {

    public bindings: any;
    public controller: any;
    public templateUrl: string;

    constructor() {
      this.bindings = {
        textBinding: '@',
        dataBinding: '<',
        functionBinding: '&'
      };
      this.controller = SomeComponentController;
      this.templateUrl = 'some-component.html';
    }

  }

  angular.module('appModule').component('someComponent', new SomeComponent());

}
Doyledoyley answered 18/2, 2016 at 20:51 Comment(2)
You should put the important parts of your solution ("answer") in your answer.Hesta
Yeah will edit it. Posted this from my phone blush.Doyledoyley
I
7

I'm using the following pattern to use angular 1.5 component with typescript

class MyComponent {
    model: string;
    onModelChange: Function;

    /* @ngInject */
    constructor() {
    }

    modelChanged() {
        this.onModelChange(this.model);
    }
}

angular.module('myApp')
    .component('myComponent', {
        templateUrl: 'model.html',
        //template: `<div></div>`,
        controller: MyComponent,
        controllerAs: 'ctrl',
        bindings: {
            model: '<',
            onModelChange: "&"
        }
    });
Inodorous answered 17/2, 2016 at 15:56 Comment(3)
The Function type is certainly one of the things I was missing. I don't even see where that is documented!Leroylerwick
I tried your code but if I use it I get the following error "class constructors must be invoked with |new|". Do you know why?Sundown
@Sundown this is super late, but that can happen sometimes when Angular can't detect that an object is an ES6 class. I believe Firefox is particularly vulnerable to this issue when declaring component controllers.Sind
E
1

I'd suggest not to use custom made solutions, but to use the ng-metadata library instead. You can find it at https://github.com/ngParty/ng-metadata. Like this your code is the most compatible with Angular 2 possible. And as stated in the readme it's

No hacks. No overrides. Production ready.

I just switched after using a custom made solution from the answers here, but it's easier if you use this library right away. Otherwise you’ll have to migrate all the small syntax changes. One example would be that the other solutions here use the syntax

@Component('moduleName', 'selectorName', {...})

while Angular 2 uses

@Component({
  selector: ...,
  ...
})

So if you're not using ng-metadata right away, you'll considerably increase the effort of migrating your codebase later on.

A full example for the best practice to write a component would be the following:

// hero.component.ts
import { Component, Inject, Input, Output, EventEmitter } from 'ng-metadata/core';

@Component({
  selector: 'hero',
  moduleId: module.id,
  templateUrl: './hero.html'
})
export class HeroComponent {

  @Input() name: string;
  @Output() onCall = new EventEmitter<void>();

  constructor(@Inject('$log') private $log: ng.ILogService){}

}

(copied from ng-metadata recipies)

Excrescent answered 8/2, 2017 at 20:51 Comment(0)
H
1

I believe one good approach is to use angular-ts-decorators. With it you can define Components in AngularJS like this:

import { Component, Input, Output } from 'angular-ts-decorators';

@Component({
  selector: 'myComponent',
  templateUrl: 'my-component.html
})
export class MyComponent {
    @Input() todo;
    @Output() onAddTodo;

    $onChanges(changes) {
      if (changes.todo) {
        this.todo = {...this.todo};
      }
    }
    onSubmit() {
      if (!this.todo.title) return;
      this.onAddTodo({
        $event: {
          todo: this.todo
        }
      });
    }
}

and then register them in your module using:

import { NgModule } from 'angular-ts-decorators';
import { MyComponent } from './my-component';

@NgModule({
  declarations: [MyComponent]
})
export class MyModule {}

If you want to check an example of a real application using it, you can check this one.

Hoenack answered 28/4, 2017 at 13:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.