How to inject a service into a ES6 helper class - angularjs

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
}
}

Related

Unit test angular 1.x components with Jasmine (using Typescript, Webpack)

I am writing an app using angular 1.6, typescript, webpack, karma and jasmine. I was able to create unit tests for angular services, but now I am having troubles for testing components. On SO(1) and (2) and on the net I found different examples (like this), but not a clear guide explaining how to test angular 1 components with the above technology set.
My component (HeaderComponent.ts):
import {IWeatherforecast} from '../models/weather-forecast';
import WeatherSearchService from '../search/weather-search.service';
import WeatherMapperService from '../common/mapping/weatherMapper.service';
export default class HeaderComponent implements ng.IComponentOptions {
public bindings: any;
public controller: any;
public controllerAs: string = 'vm';
public templateUrl: string;
public transclude: boolean = false;
constructor() {
this.bindings = {
};
this.controller = HeaderComponentController;
this.templateUrl = 'src/header/header.html';
}
}
export class HeaderComponentController {
public searchText:string
private weatherData : IWeatherforecast;
static $inject: Array<string> = ['weatherSearchService',
'$rootScope',
'weatherMapperService'];
constructor(private weatherSearchService: WeatherSearchService,
private $rootScope: ng.IRootScopeService,
private weatherMapperService: WeatherMapperService) {
}
public $onInit = () => {
this.searchText = '';
}
public searchCity = (searchName: string) : void => {
this.weatherSearchService.getWeatherForecast(searchName)
.then((weatherData : ng.IHttpPromiseCallbackArg<IWeatherforecast>) => {
let mappedData = this.weatherMapperService.ConvertSingleWeatherForecastToDto(weatherData.data);
sessionStorage.setItem('currentCityWeather', JSON.stringify(mappedData));
this.$rootScope.$broadcast('weatherDataFetched', mappedData);
})
.catch((error:any) => console.error('An error occurred: ' + JSON.stringify(error)));
}
}
The unit test:
import * as angular from 'angular';
import 'angular-mocks';
import HeaderComponent from '../../../src/header/header.component';
describe('Header Component', () => {
let $compile: ng.ICompileService;
let scope: ng.IRootScopeService;
let element: ng.IAugmentedJQuery;
beforeEach(angular.mock.module('weather'));
beforeEach(angular.mock.inject(function (_$compile_: ng.ICompileService, _$rootScope_: ng.IRootScopeService) {
$compile = _$compile_;
scope = _$rootScope_;
}));
beforeEach(() => {
element = $compile('<header-weather></header-weather>')(scope);
scope.$digest();
});
To me is not clear how to access the controller class, in order to test the component business logic. I tried injecting $componentController, but i keep getting the error "Uncaught TypeError: Cannot set property 'mock' of undefined", I think this is related to angular-mocks not properly injected.
Can anyone suggest an approach of solution or a site where to find further details about unit testing angular 1 components with typescript and webpack?
I was able to found a solution for my question. I post the edited code below, so others can benefit from it and compare the starting point (the question above) with the final code for the unit test(below, splitted in sections for the sake of explanation).
Test the component template :
import * as angular from 'angular';
import 'angular-mocks/angular-mocks';
import weatherModule from '../../../src/app/app.module';
import HeaderComponent, { HeaderComponentController } from '../../../src/header/header.component';
import WeatherSearchService from '../../../src/search/weather-search.service';
import WeatherMapper from '../../../src/common/mapping/weatherMapper.service';
describe('Header Component', () => {
let $rootScope: ng.IRootScopeService;
let compiledElement: any;
beforeEach(angular.mock.module(weatherModule));
beforeEach(angular.mock.module('templates'));
beforeEach(angular.mock.inject(($compile: ng.ICompileService,
_$rootScope_: ng.IRootScopeService) => {
$rootScope = _$rootScope_.$new();
let element = angular.element('<header-weather></header-weather>');
compiledElement = $compile(element)($rootScope)[0];
$rootScope.$digest();
}));
As for directives, also for components we need to compile the relative template and trigger a digest loop.
After this step, we can test the generated template code:
describe('WHEN the template is compiled', () => {
it('THEN the info label text should be displayed.', () => {
expect(compiledElement).toBeDefined();
let expectedLabelText = 'Here the text you want to test';
let targetLabel = angular.element(compiledElement.querySelector('.label-test'));
expect(targetLabel).toBeDefined();
expect(targetLabel.text()).toBe(expectedLabelText);
});
});
Test the component controller :
I created two mocked objects with jasmine.createSpyObj. In this way it is possible to create an instance of our controller and pass the mocked objects with the desired methods.
As the mocked method in my case was returning a promise, we need to use the callFake method from the jasmine.SpyAnd namespace and return a resolved promise.
describe('WHEN searchCity function is called', () => {
let searchMock: any;
let mapperMock: any;
let mockedExternalWeatherData: any;
beforeEach(() => {
searchMock = jasmine.createSpyObj('SearchServiceMock', ['getWeatherForecast']);
mapperMock = jasmine.createSpyObj('WeatherMapperMock', ['convertSingleWeatherForecastToDto']);
mockedExternalWeatherData = {}; //Here I pass a mocked POCO entity (removed for sake of clarity)
});
it('WITH proper city name THEN the search method should be invoked.', angular.mock.inject((_$q_: any) => {
//Arrange
let $q = _$q_;
let citySearchString = 'Roma';
searchMock.getWeatherForecast.and.callFake(() => $q.when(mockedExternalWeatherData));
mapperMock.convertSingleWeatherForecastToDto.and.callFake(() => $q.when(mockedExternalWeatherData));
let headerCtrl = new HeaderComponentController(searchMock, $rootScope, mapperMock);
//Act
headerCtrl.searchCity(citySearchString);
//Assert
expect(searchMock.getWeatherForecast).toHaveBeenCalledWith(citySearchString);
}));
});
});
Thanks for this post! I worked at the same time at the same problem and also found a solution. But this hero example doesn't require compiling the component (also no digest required) but uses the $componentController where also bindings can be defined.
The my-components module - my-components.module.ts:
import {IModule, module, ILogService} from 'angular';
import 'angular-material';
export let myComponents: IModule = module('my-components', ['ngMaterial']);
myComponents.run(function ($log: ILogService) {
'ngInject';
$log.debug('[my-components] module');
});
The hero component - my-hero.component.ts
import {myComponents} from './my-components.module';
import IController = angular.IController;
export default class MyHeroController implements IController {
public hero: string;
constructor() {
'ngInject';
}
}
myComponents.component('hero', {
template: `<span>Hero: {{$ctrl.hero}}</span>`,
controller: MyHeroController,
bindings: {
hero: '='
}
});
The hero spec file - my-hero.component.spec.ts
import MyHeroController from './my-hero.component';
import * as angular from 'angular';
import 'angular-mocks';
describe('Hero', function() {
let $componentController: any;
let createController: Function;
beforeEach(function() {
angular.mock.module('my-components');
angular.mock.inject(function(_$componentController_: any) {
$componentController = _$componentController_;
});
});
it('should expose a hero object', function() {
let bindings: any = {hero: 'Wolverine'};
let ctrl: any = $componentController('hero', null, bindings);
expect(ctrl.hero).toBe('Wolverine');
})
});
Note: It took some time to fix an error in testing the binding:
$compileProvider doesn't have method 'preAssignBindingsEnabled'
The reason was a version difference between angular and angular-mock. The solution was provide by: Ng-mock: $compileProvider doesn't have method 'preAssignBindingsEnabled`

angularJS ES6 Directive

I am trying to develop an application in angular es6 . I have a problem with directve.
Here is my code
export default class RoleDirective {
constructor() {
this.template="";
this.restrict = 'A';
this.scope = {
role :"#rolePermission"
};
this.controller = RoleDirectiveController;
this.controllerAs = 'ctrl';
this.bindToController = true;
}
// Directive compile function
compile(element,attrs,ctrl) {
console.log("df",this)
}
// Directive link function
link(scope,element,attrs,ctrl) {
console.log("dsf",ctrl.role)
}
}
// Directive's controller
class RoleDirectiveController {
constructor () {
console.log(this.role)
//console.log("role", commonService.userModule().getUserPermission("change_corsmodel"));
//$($element[0]).css('visibility', 'hidden');
}
}
export default angular
.module('common.directive', [])
.directive('rolePermission',[() => new RoleDirective()]);
The problem is i couldn't get the role value inside constructor.
here is my html implementation
<a ui-sref="event" class="button text-uppercase button-md" role-permission="dfsd" detail="sdfdsfsfdssd">Create event</a>
If i console this it will get the controller object. But it will not get any result while use this.role.
Ok, so I managed to find out how this works.
Basically, the scope values cannot be initialized on the controller's constructor (because this is the first thing executed on a new object) and there is also binding to be considered.
There is a hook that you can implement in your controller that can help you with your use case: $onInit:
class RoleDirectiveController {
constructor () {
// data not available yet on 'this' - they couldn't be
}
$onInit() {
console.log(this.role)
}
}
This should work. Note that this is angular1.5+ way of doing things when not relying on $scope to hold the model anymore. Because if you use the scope, you could have it in the controller's constructor (injected).

how i call private block method without create object outside in ext Js?

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.

Calling angular service from ES6 class method

(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
}
}

Implementing angularjs directives as classes in Typescript

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)));
}
}
}

Resources