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?

Dor Cohen
  • 16,769
  • 23
  • 93
  • 161
kpg
  • 7,644
  • 6
  • 34
  • 67
  • By not elegant do you mean that you want the code cleaned up? – Katana24 Feb 17 '16 at 09:13
  • @Katana24 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? – kpg Feb 17 '16 at 09:37
  • 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... – Katana24 Feb 17 '16 at 10:13

7 Answers7

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 });
};
scarlz
  • 2,512
  • 1
  • 17
  • 20
  • 1
    And how would you access `prop` in MyComponent with Typescript? – gerasalus May 19 '16 at 08:31
  • 2
    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? – Dominik Palo Sep 29 '16 at 08:34
  • 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? – Joy George Kunjikkuru Jan 03 '17 at 00:32
  • I don't think this will work with any modern version of TypeScript since whatever the decorator returns must be compatible with the Controller class – Zar Shardan May 17 '20 at 14:11
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

Ali Malekpour
  • 691
  • 7
  • 8
  • 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 Ng1Component – Ali Malekpour Mar 02 '16 at 16:10
  • 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 & pasted – Aral Roca Apr 20 '16 at 14:22
  • 2
    Adopting from [scarlz](http://stackoverflow.com/a/36634697/4068027) you could replace your options type with `ng.IComponentOptions` – Aides Apr 28 '16 at 13:09
  • 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. – icfantv Aug 05 '16 at 17:27
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());
Joe Skeen
  • 1,727
  • 16
  • 18
romiem
  • 8,360
  • 7
  • 29
  • 36
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());

}
Almero Steyn
  • 116
  • 3
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: "&"
        }
    });
Dor Cohen
  • 16,769
  • 23
  • 93
  • 161
  • 2
    The `Function` type is certainly one of the things I was missing. I don't even see where that is documented! – kpg Feb 17 '16 at 17:21
  • 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? – Shamshiel Aug 26 '16 at 07:19
  • @Shamshiel 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. – abalos Apr 11 '17 at 12:18
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)

bersling
  • 17,851
  • 9
  • 60
  • 74
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.

Francesco Borzi
  • 56,083
  • 47
  • 179
  • 252