(Im using Babel to be able to use ES6)
When I call addConfigurationToCart() I get:
ReferenceError: Order is not defined.
But in the constructor I don't. Why is that? I get the same error if I add Order as a parameter to addConfigurationToCart
class ConfigCtrl {
constructor($state, api, Order) {
this.current = Order.current;
}
addConfigurationToCart() {
Order.saveConfiguration();
$state.go('order');
}
}
constructor and addConfigurationToCart functions have different scopes (in JS sense), and sure, the variable from one scope isn't available in another, unless the variable is assigned to either this property or the variable from parent scope.
Private variables are still aren't there in ES2015+, but there are some workarounds to do that.
The most obvious way is using local variables:
let $state, api, Order;
class ConfigCtrl {
static $inject = ['$state', 'api', 'Order'];
constructor(...args) {
[$state, api, Order] = [...args];
// ...
}
addConfigurationToCart() {
Order.saveConfiguration();
// ...
}
}
And more idiomatic approach that successfully provides private variables within class:
const [$state, api, Order] = [Symbol(), Symbol(), Symbol()];
class ConfigCtrl {
static $inject = ['$state', 'api', 'Order'];
constructor(...args) {
[$state, api, Order].forEach((v, i) => this[v] = args[i]);
// ...
}
addConfigurationToCart() {
this[Order].saveConfiguration();
// ...
}
}
You have to make the Service public to the rest of your class.
class ConfigCtrl {
constructor($state, api, Order) {
...
this.Order = Order;
}
addConfigurationToCart() {
this.Order.saveConfiguration();
...
}
}
controller: class {
constructor($http, Restangular, $state) {
Object.assign(this, {$http, Restangular, $state});
}
doIt() {
// use this.$http, this.Restangular & this.$state freely here
}
}
Related
Consider the following component
import DoSomethingSpecial from '....';
export default MyComponentController {
constructor() {
this.result = new DoSomethingSpecial().getData();
}
...
}
So, the MyComponentController is of course a controller for a component and the DoSomethingSpecial class is an ES215 class.
The problem I have is as follows: Inside DoSomethingSpecial I would like to use some angular services, like $http and/or $q, etc. How can I inject these into this class, for example:
export default class DoSomethingSpecial {
constructor() {
}
getData() {
let $q = angular.injector().get('$q');
....
}
}
This however doesn't work. Is there an other way, or should I change this class to a service ?
UPDATE: A possible use case: Suppose you download in a service a list of objects, which you convert into instances of MyRecordItems as follows
import MyRecordItem from '...';
export default class MyRecordsService {
constructor($http) {
$http.get('url', { options}).then((response) => {
this.records = [];
response.data.forEach((item) => {
this.records.push(new MyRecordItem(item));
});
});
}
}
Next, somewhere in you app this list is used -> modified -> and saved
let list = myRecordService.getList();
list[0].count = 10;
list[0].save();
How does this record persist itself ? It could be as follows
export default class MyRecordItem {
constructor(data) { this.data = data; }
save() {
let url = ....;
return angular.$injector.get('$http').post(url, this.data)
}
}
Inject the dependencies that your ES6 class needs to the controller's constructor ($q in this case). After you've injected $q to the controller, create an instance of the class, and pass $q as a parameter to the constructor. Assign the instance to this.
import DoSomethingSpecial from '....';
class MyComponentController {
constructor($q) {
'ngInject';
this.doSomethingSpecialInstance = new DoSomethingSpecial($q); // instantiate the service, and pass the dependencies
}
getData() {
this.result = doSomethingSpecialInstance().getData(); // use the instance
}
}
export default MyComponentController;
And the class definition:
export default class DoSomethingSpecial {
constructor($q) {
this.$q = $q;
}
getData() {
// do something with this.$q
}
}
I am trying to use typescript to build an angularjs (1.5) application.
I want to make the equivalent of a factory that calls a model (which can take parameters).
I could do the following if I were to do it in pure JS
angular.module('test', [])
.factory('Talker', [$q, function($q){
return Talker()
function Talker(name) {
this.name = name || 'Bobuel Johnson';
}
Talker.prototype.deferredHello = function(){
$q.when('Hi, I\'m ' + this.name);
}
}])
Now I want to do this in typescript such that I can have the Talker class but I want to be able to inject (in this case) $q into the model class.
Please, can you help me figure out how to get this set up as the typescript equivalent?
I would recommend using a service recipe instead of a factory, if only to prepare for angular2. A TypeScript rewrite of that js code would look something like this.
class Talker {
public static $inject = ['$q']; // configure angular di
private name: string = 'Some talker';
constructor(private $q) {}
public deferredHello() {
this.$q.when('Hi, I\'m ' + this.name);
}
}
angular.module('test').service('Talker', Talker);
You can find the translation to js here.
If a factory is absolutely needed you should be able to do that as well like this:
class Talker {
private name: string = 'Some talker';
constructor(private $q) {}
public deferredHello() {
this.$q.when('Hi, I\'m ' + this.name);
}
}
angular.module('test').service('Talker', function($q) {
return new Talker($q);
});
You can check out the generated javascript here.
If you need to create multiple instances of Talker you can do it this way.
class Talker {
private name: string = 'Some talker';
constructor(private $q, money: number) {}
public deferredHello() {
this.$q.when('Hi, I\'m ' + this.name);
}
}
angular.module('test').service('Talker', function($q) {
// we can ask for more parameters if needed
return function talkerFactory(money) { // return a factory instead of a new talker
return new Talker($q, money);
};
});
angular.module('yatest').service('Organiser', function(Talker) {
let talker = Talker(42); // will call talkerFactory and return a new instance of talker
})
The example for that is here.
Ext.define('App.View.ClassDemo', {
privates: {
runFactory: function () {
this.factory('paresh');
}
},
factory: function (brand) {
alert(brand);
}
});
this class contain privates block and its contain runFactory method how i call this method without creating object outside
With ExtJS you can do something like this:
Ext.define('Computer', {
statics: {
factory: function(brand) {
// 'this' in static methods refer to the class itself
return new this(brand);
}
},
constructor: function() { ... }
});
var dellComputer = Computer.factory('Dell');
The "factory" method is static and can be used without a Computer instance.
So after taking a look at some of the examples of angularjs directives in typescript, it seems most people agree to use functions instead of classes when implementing them.
I would prefer to have them as a class and attempted to implement them as follows:
module directives
{
export class search implements ng.IDirective
{
public restrict: string;
public templateUrl: string;
constructor()
{
this.restrict = 'AE';
this.templateUrl = 'directives/search.html';
}
public link($scope: ng.IScope, element: JQuery, attributes: ng.IAttributes)
{
element.text("Hello world");
}
}
}
Now this works fine. However, I need to have an isolated scope with some attributes and I'm struggling to find out how to include that in the class itself.
logic dictates that since I can have
public restrict: string;
public templateUrl: string;
I should be able to have something like:
public scope;
But I'm not sure if this is correct or how to carry on from there (i.e how to add the attributes to the scope).
Anybody know how to solve this? (hopefully, without having to revert to a function if possible)
Thanks,
Creating directives as classes can be problematic since you still need to involve a factory function to wrap its instantiation. For example:
export class SomeDirective implements ng.IDirective {
public link = () => {
}
constructor() {}
}
What Doesn't Work
myModule.directive('someDirective', SomeDirective);
Since directives are not invoked using 'new' but are just called as factory functions. This will cause problems on what your constructor function actually returns.
What Does (with Caveats)
myModule.directive(() => new SomeDirective());
This works fine provided you don't have any IoC involved, but once you start introducing injectables, you have to maintain duplicate parameter lists for your factory function and your directive contstructor.
export class SomeDirective implements ng.IDirective {
...
constructor(someService: any) {}
}
myModule.directive('someDirective', ['someService', (someService) => new SomeDirective(someService)]);
Still an option if that is what you prefer, but is important to understand how the directive registration is actually consumed.
An alternative approach
The thing that is actually expected by angular is a directive factory function, so something like:
export var SomeDirectiveFactory = (someService: any): ng.IDirective => {
return {
link: () => {...}
};
}
SomeDirectiveFactory.$inject = ['someService']; //including $inject annotations
myModule.directive('someDirective', SomeDirectiveFactory);
This has the benefit of allowing the use of $inject annotations since angular needs it to be on the factory function in this case.
You could always return an instance of your class from the factory function as well:
export var SomeDirectiveFactory = (someService: any): ng.IDirective => {
return new SomeDirective(someService);
}
SomeDirectiveFactory.$inject = ['someService']; //including $inject annotations
But really depends on your use case, how much duplication of parameter lists you are okay with, etc.
Assuming that what you have works without an islolated scope, the following should work with an isolated scope:
module directives
{
export class search implements ng.IDirective
{
public restrict = 'AE';
public templateUrl = 'directives/search.html';
public scope = {
foo:'=',
bar:'#',
bas:'&'
};
public link($scope: ng.IScope, element: JQuery, attributes: ng.IAttributes)
{
element.text("Hello world");
}
}
}
Here is my proposal:
Directive:
import {directive} from '../../decorators/directive';
#directive('$location', '$rootScope')
export class StoryBoxDirective implements ng.IDirective {
public templateUrl:string = 'src/module/story/view/story-box.html';
public restrict:string = 'EA';
public scope:Object = {
story: '='
};
public link:Function = (scope:ng.IScope, element:ng.IAugmentedJQuery, attrs:ng.IAttributes):void => {
// console.info(scope, element, attrs, this.$location);
scope.$watch('test', () => {
return null;
});
};
constructor(private $location:ng.ILocationService, private $rootScope:ng.IScope) {
// console.log('Dependency injection', $location, $rootScope);
}
}
Module (registers directive...):
import {App} from '../../App';
import {StoryBoxDirective} from './../story/StoryBoxDirective';
import {StoryService} from './../story/StoryService';
const module:ng.IModule = App.module('app.story', []);
module.service('storyService', StoryService);
module.directive('storyBox', <any>StoryBoxDirective);
Decorator (adds inject and produce directive object):
export function directive(...values:string[]):any {
return (target:Function) => {
const directive:Function = (...args:any[]):Object => {
return ((classConstructor:Function, args:any[], ctor:any):Object => {
ctor.prototype = classConstructor.prototype;
const child:Object = new ctor;
const result:Object = classConstructor.apply(child, args);
return typeof result === 'object' ? result : child;
})(target, args, () => {
return null;
});
};
directive.$inject = values;
return directive;
};
}
I thinking about moving module.directive(...), module.service(...) to classes files e.g. StoryBoxDirective.ts but didn't make decision and refactor yet ;)
You can check full working example here: https://github.com/b091/ts-skeleton
Directive is here: https://github.com/b091/ts-skeleton/blob/master/src/module/story/StoryBoxDirective.ts
Here finally i got working a directive as class plus inheritance. In derived directive I extend the scope plus define the templateUrl.
You can override any methods from base directive
.
The key was to return from constructor the instance of directive.Angularjs calls constructor without new keyword. In this case this is of type window
I wrapped few lines to check the instance type of this and in case of window I create a new instance of directive. (See Activator class from sample below)
module Realty.directives {
export class BaseElementWithLabel implements ng.IDirective {
public restrict = 'E';
public replace = true;
public scope = {
label: '#',
model: '=',
icon: '#',
readonlyElement: '=',
remark: '#'
}
constructor(extendedScope) {
if (!(this instanceof Window)) {
extendedScope = extendedScope || {};
this.scope = angular.extend(this.scope, extendedScope);
}
}
link(scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes, controller, transclude) {
scope['vm'] = {};
}
}
}
module Realty.directives {
export class textareaWithLabel extends Realty.directives.BaseElementWithLabel implements ng.IDirective {
templateUrl = 'directives/element-form/textarea-with-label-template.html';
constructor() {
super({
rows: '#'
});
return Realty.Activator.Activate(this, textareaWithLabel, arguments);
}
};
};
module Realty {
export class Activator {
public static Activate(instance: any, type, arguments) {
if (instance instanceof type) {
return instance;
}
return new(type.bind.apply(type, Array.prototype.concat.apply([null], arguments)));
}
}
}
In a AngularJS 1.2.5 using TypeScript 0.9.1 app, we are seeing that when we change routes, the private methods on a controller class remain in the heap and leave detached DOM trees in chromes profiler.
If we navigate /#/view1 to /#/view2 and back to /3/view1, we end up with view1 controller class in the heap twice and view2 controller class in the heap as well.
Our workaround has been to not use private methods anymore.
The code generally looks like:
module views {
app.controller("view1Ctrl", function($scope, $routeParams) {
return new view1Ctrl($scope, $routeParams);
});
interface Scope extends ng.IScope {
TrackingTab: any;
}
class view1Ctrl {
constructor(private $scope: Scope, $routeParams: any) {
$scope.TrackingTab = $routeParams["tab"];
$scope.$watch("showTab", (newValue: TrackingTab): void => {
if (newValue === undefined) return;
});
}
private changeTabToNew(): void {
this.$scope.TrackingTab = "new"
}
}
}
we have to change to something along the lines of:
module views {
app.controller("view1Ctrl", function($scope, $routeParams) {
return new view1Ctrl($scope, $routeParams);
});
interface Scope extends ng.IScope {
TrackingTab: any;
}
class view1Ctrl {
constructor(private $scope: Scope, $routeParams: any) {
$scope.TrackingTab = $routeParams["tab"];
$scope.$watch("showTab", (newValue: TrackingTab): void => {
if (newValue === undefined) return;
});
$scope.changeTabToNew(): void {
this.$scope.TrackingTab = "new"
};
}
}
Thanks in advance
If you want to make functions private in javascript, please refer to:
http://javascript.crockford.com/private.html
From the above code I think that the code:
private changeTabToNew(): void {
this.$scope.TrackingTab = "new"
}
is simply creating a function changeTabToNew() on the global (or root) scope (the private keyword is not having the effect you are expecting btw). Since this is not part of the scope that exists in the controller, you are creating a reference to your 'TrackingTab' in global scope and thus the controller cannot be garbage collected.