How to unit test an AngularJS directive with shared scope of parent - angularjs

I have a parent view that includes an error handling directive like so:
<service-error the-errors="vm.serviceErrors"></service-error>
The service errors are shared with the directive and then displayed if present:
class ServiceError implements ng.IDirective{
public restrict: string;
public template: string;
public controller: string;
public controllerAs: string;
public bindToController: boolean;
public scope: Object;
constructor(...deps: Array<any>) {
this.restrict = 'E';
this.template = require('./service.error.directive.html');
this.controller = 'ServiceErrorController';
this.controllerAs = 'vm';
this.bindToController = true;
this.scope = {
theErrors: '='
};
}
static factory(...deps: Array<any>) {
return new ServiceError(...deps);
}
}
// Dependency Injection
ServiceError.factory.$inject = [];
export { ServiceError };
<ul class="list-unstyled service-error">
<li class="bg-danger" ng-repeat="error in vm.theErrors track by $index">{{error.message}}</li>
</ul>
I want to test that when vm.serviceErrors from the parent has data that the HTML of the service error is compiled correctly but am not sure how to set this up.
I have created a basic test so far:
describe('Directive: Service Error', function() {
var element,
scope,
template;
beforeEach(inject(function(_$rootScope_, $compile) {
element = angular.element('<service-error the-errors="vm.serviceErrors"></service-error>');
scope = _$rootScope_.$new();
template = $compile(element)(scope);
scope.$digest();
}));
it('should compile error list', function(){
var el = element.find('.service-error');
assert.isDefined(el);
});
});

It is
element = angular.element('<service-error the-errors="serviceErrors"></service-error>');
scope = _$rootScope_.$new();
scope.serviceErrors = ...;
template = $compile(element)(scope);
scope.$digest();

Related

Angular dynamic creation of Directives

I have page setup where I want to dynamically create directives and add them to the page. I have gotten this work, but in the Directives that I'm adding are not getting compiled correctly because no interpolation is occurring and the Dependency in the constructor is not getting loaded.
Here is the code that I'm using.
page.html
<dashboard-dynamic-component ng-repeat="item in vm.parts" dashboard-part="item.part.implementationComponent" dashboard-part-id="item.id"</dashboard-dynamic-component>
dashboardDynamicComponent
function dynamicComponent($compile: ng.ICompileService, $timeout: ng.ITimeoutService) {
return {
restrict: 'E',
replace: true,
templateUrl: 'app/main/dashboard/dynamicComponent.html',
scope: {
dashboardPart: '=',
dashboardPartId: '='
},
link: (scope, element) => {
var newElement = $compile(`<${scope.dashboardPart} dashboard-part-id="${scope.dashboardPartId}" />`)(scope);
element.append(newElement);
$timeout(() => {});
}
}
}
dynamicComponent.$inject = ['$compile', '$timeout'];
angular.module('Cranalytics')
.directive('dashboardDynamicComponent', dynamicComponent);
This creating the component that I expect it to, but then in the next part is were I see the problems.
titleComponent.html - Which is what is dynamically being created above
<h3 class="title">****{{vm.title}}****</h3>
titleComponent.ts
export class TitleComponent implements ng.IDirective {
restrict = 'E';
replace = true;
templateUrl = 'app/main/dashboard/titleComponent/titleComponent.html';
scope = {
dashboardPartId: '='
};
controller = TitleController;
controllerAs = 'vm';
bindToController = true;
static create(): ng.IDirectiveFactory {
const directive = () => new TitleComponent();
return directive;
}
static instance(): ng.IDirective { return new TitleComponent(); }
}
export class TitleController {
_dashboardPartId: number;
get dashboardPartId(): number {
return this._dashboardPartId;
}
set dashboardPartId(value: number) {
this._dashboardPartId = value;
if (!value) return;
this.dataService.loadById(value)
.then((result: Models.Dashboard.dashboardPart) => {
var options = JSON.parse(result.options);
this.title = options.Title;
});
}
title: string;
static $inject = ['Cranalytics.Dashboard.dashboardPartDataService'];
constructor(private dataService: Dashboard.dashboardPartDataService) {}
}
angular.module('Cranalytics')
.directive('dashboardTitleComponent', TitleComponent.create());
So I think I'm just missing one little piece because I am getting the {{vm.title}} on the page, but the interpolation is not displaying and also in the constructor in the above code the dependency for Cranalytics.Dashboard.dashboardPartDataService is returning undefined
The problem that I had was that the property setter for dashboardPartId was being called before the constructor was being called, which resulted in the who component erroring out because this.dataService was undefined. Once I refactored the loading out of the set and checked for the dataService and added that same call into the constructor everything worked.

Angular directive receiving object as attribute using Typescript

I am developing using Angular 1.5.8 and Typescript
I have a directive which is used under the scope of another directive (and another controller of course). Let's say Directive1, Controller1 and Directive2, Controller2.
Given the Controller1 already has the user information, I would like to pass this user information to the Controller2 through the Directive2, to prevent from fetching the information again from the backend.
I am not sure if this can be done, but it would be nice if that's the case :)
Below is the code to help my explanation:
Directive1 HTML:
<div>
...
<directive2 user="{{ctrl.loggedUser}}"></directive2>
...
</div>
loggedUser is loaded in Controller1 constructor through a call to the backend.
Directive2 and Directive2Ctrl Typescript code:
class Directive2 implements ng.IDirective {
controller = "Directive2Ctrl";
controllerAs = "d2Ctrl";
bindToController = {
user: "#"
};
restrict = "E";
templateUrl = "directive2.html";
static factory(): ng.IDirectiveFactory {
const directive = () => new Directive2();
return directive;
}
}
angular
.module("app")
.controller("Directive2Ctrl", Directive2Ctrl)
.directive("directive2", Directive2.factory());
class Directive2Ctrl implements IDirective2Ctrl {
public user: User;
constructor(user: User) {
// user is undefined
}
$onInit(user: User): void {
// user is undefined
}
}
I couldn't find a way of passing the user object to the Directive2Ctrl (not even sure if it is possible).
Use "scope" property instead of "bindToController" property, and replace your "#" with "=".
Then I use an interface for my specific scope to get autocompletion.
export interface IMyDirectiveScope extends ng.IScope {
user: any;
}
export class Myirective {
public restrict: string = 'E';
public templateUrl = "/mytemplate.html";
public link: (scope: IMyDirectiveScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ngModel: ng.INgModelController) => void;
public scope = {
user: "="
};
constructor() {
var context = this;
context.link = (scope: IMyDirectiveScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ngModel: ng.INgModelController) => {
//INSERT YOUR CODE
//console.log(scope.user);
};
}
public static Factory() {
var directive = () => {
return new MyDirective();
};
return directive;
}
}
In your html, remove your curly braces.
<div>
...
<directive2 user="ctrl.loggedUser"></directive2>
...
</div>
If you want to share data between different locations in your application, just put it in a service and use DI wherever you need the data.
That is, fetch the data, store it in a service and use DI to make the data available in different locations. There is no need to pass data through bindings over several layers, much easier to use a service.
var mod = angular.module('testApp', ['ngRoute']);
mod.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/intern', {
template: '<div class="outer" ng-controller="InternController">{{User.firstName}} <div class="nested" ng-controller="NestedController">{{NestedUser.lastName}}<test-dir></test-dir></div></div>',
resolve: {
userResolve: function($q, $timeout, User) {
var deferred = $q.defer();
// mock server call, which returns server data
$timeout(function() {
var mockUserResp = {
firstName: 'John',
lastName: 'Rambo'
};
User.setUser(mockUserResp);
deferred.resolve();
}, 1000);
return deferred.promise;
}
}
}).
otherwise({
redirectTo: '/intern'
});
}]);
mod.factory('User', function() {
var _user = null;
return {
setUser: function(user) {
_user = user;
},
getUser: function() {
return _user;
}
}
});
mod.controller('InternController', function($scope, User) {
$scope.User = User.getUser();
});
mod.controller('NestedController', function($scope, User) {
$scope.NestedUser = User.getUser();
});
mod.directive('testDir', function(User) {
return {
restrict: 'EA',
scope: {},
template: '<div class="dir">{{firstName}} is a cool guy.</div>',
link: function(scope) {
scope.firstName = User.getUser().firstName;
}
};
});
.outer {
border: 1px solid green;
}
.nested {
border: 1px solid blue;
}
.dir {
border: 1px solid orange;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.12/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.12/angular-route.min.js"></script>
<div ng-app="testApp">
<div ng-view></div>
</div>
It should be like this. But if it's still not working for you, you can create a simple plunker and I will fix it there. Cheers!
<div>
...
<directive2 user="ctrl.loggedUser"></directive2>
...
</div>
`
class Directive2 implements ng.IDirective {
controller = "Directive2Ctrl";
controllerAs = "d2Ctrl";
scope = {},
bindToController = {
user: "="
};
restrict = "E";
templateUrl = "directive2.html";
static factory(): ng.IDirectiveFactory {
return () => new Directive2();
}
}
angular
.module("app")
.controller("Directive2Ctrl", Directive2Ctrl)
.directive("directive2", Directive2.factory());
class Directive2Ctrl implements IDirective2Ctrl {
public user: User;
$onInit(user: User): void {
// user should be available
console.log(this.user);
}
}
Sorry, I wonder if you still need to set scope = {}
To do what I asked in the first place, the scope needs to be correctly used.
Here is another question which is nicely explained which uses the scope correctly:
How can I define my controller using TypeScript?

How to get the 'this' context of the Class in a typescript angular 1.4 directive

I recently started using bindToController in my angular directives and I had an issue with 'this'. In my controller method, how to I access properties of the class MultiSelect. 'this' in that context is referring to the $scope due to the controllerAs syntax which is expected, but now how do I access my searchService service?
/// <reference path="../../../../definitions/app.d.ts" />
module App.directives
{
'use strict';
class MultiSelect implements ng.IDirective
{
restrict = 'E';
templateUrl = 'directives/multi-select/multi-select.directive.html';
scope = {};
bindToController = {
value: '='
};
controllerAs = 'multiSelect';
constructor(private searchService: App.ISearchService) {
}
controller()
{
console.log(this)
// prints {value: undefined}
// which matches bindToController
this.searchService.get();
// TypeError: Cannot read property 'get' of undefined
}
link = ($scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
}
static factory(): ng.IDirectiveFactory
{
const directive = (searchService: App.ISearchService) => new MultiSelect(searchService);
return directive;
}
}
angular.module('App').directive('multiSelect', ['searchService', MultiSelect.factory()]);
}
I recently started using bindToController in my angular directives
Don't. That changes the semantics from this to be different from what TypeScript infers.
This is how it is done according to Dan Wahlin. From the Angular in 20 Typescript project.
///<reference path="../../../tools/typings/tsd.d.ts" />
///<reference path="../../../tools/typings/typescriptApp.d.ts" />
module demoApp.directives {
class FilterTextbox implements ng.IDirective {
static instance() : ng.IDirective {
return new FilterTextbox();
}
template = 'Search: <input type="text" ng-model="vm.filter" /> {{ vm.message }}';
restrict = 'E';
scope = {
filter: '='
};
controller: ($scope: ng.IScope) => void;
controllerAs = 'vm';
bindToController = true;
constructor() {
this.controller = function ($scope: ng.IScope) {
var vm = this;
vm.message = 'Hello';
$scope.$watch('vm.filter', (newVal, oldVal) => {
if (oldVal !== '' && newVal === '') {
vm.message = 'Please enter a value';
} else {
vm.message = '';
}
});
};
}
}
angular.module('demoApp').directive('filterTextbox', FilterTextbox.instance);
}

Angularjs+Typescript directive implementing $compile

I am having issues injecting $compile in the following directive.
export class Element {
public link(scope:dirScopeInterface, element:any, attrs:ng.IAttributes, formCtrl:ng.IFormController) {
var attr = this.arrayJoiner(scope.standard, scope.attrs || {}, scope.ignore || {});
element.html(this.compiler(attr));
$compile(element.contents())(scope);
}
}
At the moment it is throwing an $compile is undefined error. I have tried using
static $inject = ['$compile'];
But it disappears from the transpiled script for some reason.
Here is the full code used.
Include the static $inject and a constructor:
export class Element {
// $compile can then be used as this.$compile
constructor(private $compile: ng.ICompileService){};
public link(scope:dirScopeInterface, element:any, attrs:ng.IAttributes, formCtrl:ng.IFormController) {
var attr = this.arrayJoiner(scope.standard, scope.attrs || {}, scope.ignore || {});
element.html(this.compiler(attr));
this.$compile(element.contents())(scope);
}
}
EDIT
To register this directive with angular, this is what I always do (there are multiple solutions):
export class Element implements angular.IDirective {
public static ID = "element";
// This can then be removed:
// static $inject = ["$compile"];
// ..
/**
* The factory function that creates the directive
*/
static factory(): angular.IDirectiveFactory {
const directive = ($compile) => new Element($compile);
directive.$inject = ["$compile"];
return directive;
}
}
and to register:
angular.module("myModule" [])
.directive(Element.ID, Element.factory());
So I found a way to get it to work but it is not as elegant as I would have liked.
angular.module('formDirectives', [], function($compileProvider){
$compileProvider.directive('catElement', ($compile) => {
return new Element($compile);
});
})
my implementation for jQuery Wizard Steps with AngularJS + Typescripts
It should work well for other $compile function too.
AppDirective.ts
export class stepwizard implements ng.IDirective {
constructor() {
console.log("construtor step wizard directive");
}
link($scope: ng.IScope, $element: JQuery, attributes: ng.IAttributes, ngModel: ng.INgModelController, ctrl: any) {
console.log("start step wizard link function");
$element.wrapInner('<div class="steps-wrapper">');
const steps = $element.children(".steps-wrapper").steps({
headerTag: "h3",
bodyTag: "section",
transitionEffect: "slideLeft",
autoFocus: true
});
const compiled = ctrl.$compile(steps);
}
public static Factory() {
var directive = () => {
return new stepwizard();
};
directive.$inject = ['$compile'];
console.log("initial step wizard");
return directive;
}
}
AppController.ts
export class pageController{
// your declaraction
static $inject: Array<string> = [
$compile',
];
constructor(
$compile: ng.ICompileService,
) {
// your constructor declaraction
}
HTML
// sample take from official website
<div stepwizard>
<h3>Keyboard</h3>
<section>
<p>Try the keyboard navigation by clicking arrow left or right!</p>
</section>
<h3>Effects</h3>
<section>
<p>Wonderful transition effects.</p>
</section>
<h3>Pager</h3>
<section>
<p>The next and previous buttons help you to navigate through your content.</p>
</section>
</div>
directives.stepwizard = <namespace>.directives.stepwizard.Factory();
app.directive(directives);

AngularJS Typescript Directive

Having problems creating nested directive using Typescript. I can do it in simple AngularJs:
http://plnkr.co/edit/UruTqEdMnqNT5wjxcQNC?p=preview ,
but using TypeScript it gives me "No controller" error message.
/// <reference path="../../Scripts/AngularJs/Typings/angular.d.ts" />
export class directiveA {
public static $inject: Array<string> = [];
constructor() {
var directive: ng.IDirective = {};
directive.priority = 0;
directive.restrict = "A";
directive.scope = {};
directive.transclude = true;
directive.templateUrl = "otherTemplate.html";
directive.replace = true;
directive.controller = function ($scope, $element) {
this.flip = function () {
$element.toggleClass("flipped");
}
}
directive.replace = true;
return directive;
}
}
export class directiveB{
public static $inject: Array<string> = [];
constructor() {
var directive: ng.IDirective = {};
directive.require = "^directiveA";
directive.priority = 1;
directive.restrict = "A";
directive.scope = {
simplrSide : "#"
};
directive.transclude = true;
directive.templateUrl = "templeUrl.html";
directive.link = function (scope, iElement, iAttrs, simplrEditable) {
scope.flip = function () {
simplrEditable.flip();
}
}
directive.replace = true;
return directive;
}
}
I dont know if its relevant, but i am using AMD Require.JS for script load
Assuming you are registering these as :
import mod = require('yourfile')
youmodule.directive('directiveA',mod.directiveA);
youmodule.directive('directiveB',mod.directiveB);
That should work as long as your html looks like:
<div directiveA>
<div directiveB>
</div>
</div>
A couple of notes beyond that:
Use functions for your directive definitions.
This is because directives (unlike controllers) are called without the new operator. So if you have something like:
class Test{
foo = "EAC";
constructor(){
var directive:any = {};
directive.restrict = this.foo;
}
}
It compiles to incorrect javascript. As the function Test is called without the new operator and that means that this refers to window and not an instance of the class. So you can't use anything defined outside the constructor anyways. I recommend something like:
function foo():ng.IDirective{
return {
restrict: 'EAC';
}
}
This way typescript will help you write correct javascript for angular instead of point you in the wrong way. I will make a video about this at some point
Use classes for your controller
Controllers inside of directives are also called with the new operator. Same as controllers outside : http://www.youtube.com/watch?v=WdtVn_8K17E Again let typescript help you with the meaning of this inside the controller definition. Plus you can use the type for the controller in the child directive something like (for typesafety and inference):
link: function (scope, iElement, iAttrs, simplrEditable:YourControllerClass)
For injection into directive functions
I still use $inject. I have the following interface definition :
interface Function{
$inject:string[]
}
This means you can do :
foo.$inject = ['$compile']; // e.g
The issue is not related to Typescript, but to AngularJS Directives.
Changing templateUrl to template and using inline code, helps to resolve the errors. It's AngularJS Issue, more on that: https://github.com/angular/angular.js/issues/1903
Hope they will fix this in the future!
export class directiveA {
public static $inject: Array<string> = [];
constructor() {
var directive: ng.IDirective = {};
directive.priority = 0;
directive.restrict = "A";
directive.scope = {};
directive.transclude = true;
directive.template = "<div>Your content</div>";
directive.replace = true;
directive.controller = function ($scope, $element) {
this.flip = function () {
//Some func
}
}
directive.replace = true;
return directive;
}
}
With my solution you can both use a TS class and not having to worry about a factory and repeat what you need injected.
module YourApp.Common.Directives {
class SearchInputController {
public query: string;
constructor(private $location: ng.ILocationService) {
}
doSearch(): void {
this.$location.url(`/search?q=${this.query}`);
this.query = '';
}
}
export function SearchInputDirective($location: ng.ILocationService): ng.IDirective {
return {
restrict: 'E',
templateUrl: 'common/directives/searchInput/SearchInputDirective.html',
replace: true,
scope: true,
controllerAs: 'SearchInputController',
bindToController: {
'query': '#'
},
controller: (): any => new SearchInputController($location)
};
}
SearchInputDirective.$inject = ['$location'];
}
To register:
angular.module('common', [])
.directive('searchInput', YourApp.Common.Directives.SearchInputDirective);
And the HTML to see the whole picture (templateUrl):
<form ng-submit="SearchInputController.doSearch()">
<input type="search" ng-model="SearchInputController.query">
</form>
a simple way to write directive in ts
I think can work with nested directive as well
class D{
static foo(){
return {
restrict:'A',
template:'<div>Here I am to save the day</div>',
replace: true
}
}
}
/// <reference path="./angular.d.ts"/>
/// <reference path="./directive.ts"/>
class MyApp{
public app:AngularModule;
constructor(){
this.app = angular.module('myApp', []);
this.app.directive ('superman',() => {
return D.foo();
}
);
}
}
new MyApp();
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Directive</title>
</head>
<body>
<div data-ng-app="myApp">
<div data-superman></div>
</div>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
<script src="./directive.js"></script>
<script src="./appDirective.js"></script>
</body>
</html>

Resources