I have this simple directive:
...
var readyData = {
caption: ctrl.caption,
leftValue: ctrl.leftValue,
rightValue: ctrl.rightValue,
};
$scope.$emit($attrs.id + ".ready", readyData); //$scope is directive's scope and not $rootScope
}
and I have the following test:
describe("Directive: summaryItem", function () {
// load the directive"s module
beforeEach(module("summaryItem"));
it("Should emit the ready event", inject(function ($compile) {
element = angular.element("<summary-item id=\"summaryItem\"></summary-item>");
element = $compile(element)(scope);
scope.$digest();
var dscope = element.scope();
spyOn(dscope, "$emit");
//run code to test
expect(dscope.$emit).toHaveBeenCalledWith("summaryItem.ready");
}));
I am getting the following error:
Expected spy $emit to have been called with [ 'summaryItem.ready' ] but it was never called.
How can I solve this one? Thanks!
Update
For #themyth92 request here is the full Directive's code:
"use strict";
(function (angular) {
/**
* #ngdoc directive
* #name summaryItemApp.directive:summaryItem
* #description
* # summaryItem
*/
angular.module("summaryItem")
.directive("summaryItem", function () {
return {
templateUrl: "views/summary-item.html",
restrict: "E",
transclude: true,
controller: SummaryItemCtrl,
controllerAs: 'ctrl',
bindToController: true,
scope: {
options: "=",
caption: "="
}
};
});
function SummaryItemCtrl($scope, $attrs) {
var ctrl = this;
ctrl.caption = this.caption;
if(this.options) {
ctrl.leftValue = this.options.leftValue;
ctrl.rightValue = this.options.rightValue;
}
var readyData = {
caption: ctrl.caption,
leftValue: ctrl.leftValue,
rightValue: ctrl.rightValue
};
$scope.$emit($attrs.id + ".ready", readyData);
}
}(angular));
There are two problems in your test. First of all, the event will be triggered at the first $scope.$digest() call. In your test, you mock the $emit function after the digest, so this will not work.
Furthermore, because your directive uses an isolate scope, element.scope() does not do what you expect it to do. In this case, element.scope() will return the original scope of the element; element.isolateScope() will return the isolate scope introduced by the directive.
However, there is another way to test this. Because $emit-ted events bubble up to their parent scopes, you could also test that one of the parent scopes received the correct event.
Untested code:
it("Should emit the ready event", inject(function ($compile) {
var emitted = false;
scope.$on('summaryItem.ready', function() {
emitted = true;
});
element = angular.element("<summary-item id=\"summaryItem\"></summary-item>");
element = $compile(element)(scope);
scope.$digest();
expect(emitted).toBe(true);
}));
As an improvement, you can also store the event instead of just true, which allows you to do all kinds of expects on the emitted events.
Related
I have a directive for users to like (or "fave") posts in my application. Throughout my controllers I use $rootScope.$emit('name-of-function', some-id) to update user data when they like a new post, as this is reflected throughout my application. But I can't seem to use $rootScope.$emit in the directive. I receive an error
$rootScope.$emit is not a function
Presumably the $rootScope.$on event which corresponds with this command has not been called yet, so this function does not yet exist? What can be done about this? Is there a better way to arrange this?
var module = angular.module('directives.module');
module.directive('postFave', function (contentService, $rootScope) {
return {
restrict: 'E',
templateUrl: 'directives/post-fave.html',
scope: {
contentId: '#',
slug: '#'
},
link: function ($scope, $rootScope, element) {
$scope.contentFavToggle = function (ev) {
contentId = $scope.contentId;
contentService.contentFavToggle(contentId, ev).then(function (response) {
$rootScope.$emit('dataUpdated', $scope.slug);
if (response) {
$scope.favourite[contentId] = response;
} else {
$scope.favourite[contentId] = null;
}
});
};
console.log("track fave directive called");
}
};
});
from controller:
var dataUpdatedListener = $rootScope.$on('dataUpdated', function (event, slug) {
dataService.clearData(slug);
dataControllerInit();
});
How can I access this rootscope function from within the directive? Thanks.
FYI - "link" has been used in the directive because this is related to an instance of an HTML element which will be used a number of times on the page
link has the following signature, there is no need to add $rootScope injection into link function:
function link(scope, element, attrs, controller, transcludeFn) { ... }
Remove it from link and it will work.
So I have a directive with isolate scope and a controllerAs pattern.
var directive = {
restrict: 'E',
scope: {
something: '='
},
templateUrl: './App/directiveTemplate.html',
controller: directiveController,
controllerAs: 'vm',
bindToController: true
}
and in the controller I init with a call to a REST service using $http that returns a promise.
function directiveController(someService) {
var vm = this;
// Here vm.something is defined and bound to the appropriate model set where the directive is used
init()
function init() {
return someService.getProducts()
.then(productsReady);
function productsReady(response) {
vm.products = response;
//find product using vm.something
// here vm.something is undefined
return vm.products;
}
}
The problem is that if I breakpoint before the init() method vm.something is defined like it should be but in the productsReady function it is undefined.
Is that a normal behaviour? Is the promise resolving code in a different scope?
Use the $onInit Life-Cycle Hook to guarantee the timing of bindings:
function directiveController(someService) {
var vm = this;
̶i̶n̶i̶t̶(̶)̶
this.$onInit = init;
function init() {
return someService.getProducts()
.then(productsReady);
function productsReady(data) {
vm.products = data;
return vm.products;
}
}
From the Docs:
Initialization logic that relies on bindings being present should be put in the controller's $onInit() method, which is guaranteed to always be called after the bindings have been assigned.
.component('myComponent', {
bindings: {value: '<'},
controller: function() {
this.$onInit = function() {
// `this.value` will always be initialized,
// regardless of the value of `preAssignBindingsEnabled`.
this.doubleValue = this.value * 2;
};
}
})
— AngularJS Developer Guide - Migrating to V1.6 - $compile
I will explain what exactly I'm trying to do before explaining the issue. I have a Directive which holds a form, and I need to access that form from the parent element (where the Directive is used) when clicking on a submit button to check fi the form is valid.
To do this, I am trying to use $scope.$parent[$attrs.directiveName] = this; and then binding some methods to the the Directive such as this.isValid which will be exposed and executable in the parent.
This works fine when running locally, but when minifying and building my code (Yeoman angular-fullstack) I will get an error for aProvider being unknown which I traced back to a $scopeProvider error in the Controller.
I've had similar issues in the past, and my first thought was that I need to specifically say $inject for $scope so that the name isn't lost. But alas.....no luck.
Is something glaringly obvious that I am doing wrong?
Any help appreciated.
(function() {
'use strict';
angular
.module('myApp')
.directive('formDirective', formDirective);
function formDirective() {
var directive = {
templateUrl: 'path/to/template.html',
restrict: 'EA',
scope: {
user: '='
},
controller: controller
};
return directive;
controller.$inject = ['$scope', '$attrs', 'myService'];
function controller($scope, $attrs, myService) {
$scope.myService = myService;
// Exposes the Directive Controller on the parent Scope with name Directive's name
$scope.$parent[$attrs.directiveName] = this;
this.isValid = function() {
return $scope.myForm.$valid;
};
this.setDirty = function() {
Object.keys($scope.myForm).forEach(function(key) {
if (!key.match(/\$/)) {
$scope.myForm[key].$setDirty();
$scope.myForm[key].$setTouched();
}
});
$scope.myForm.$setDirty();
};
}
}
})();
Change the directive to a component and implement a clear interface.
Parent Container (parent.html):
<form-component some-input="importantInfo" on-update="someFunction(data)">
</form-component>
Parent controller (parent.js):
//...
$scope.importantInfo = {data: 'data...'};
$scope.someFunction = function (data) {
//do stuff with the data
}
//..
form-component.js:
angular.module('app')
.component('formComponent', {
template:'<template-etc>',
controller: Controller,
controllerAs: 'ctrl',
bindings: {
onUpdate: '&',
someInput: '<'
}
});
function Controller() {
var ctrl = this;
ctrl.someFormThing = function (value) {
ctrl.onUpdate({data: value})
}
}
So if an event in your form triggers the function ctrl.someFormThing(data). This can be passed up to the parent by calling ctrl.onUpdate().
I wanted to use a directive to have some click-to-edit functionality in my front end.
This is the directive I am using for that: http://icelab.com.au/articles/levelling-up-with-angularjs-building-a-reusable-click-to-edit-directive/
'use strict';
angular.module('jayMapApp')
.directive('clickToEdit', function () {
return {
templateUrl: 'directives/clickToEdit/clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I added a second attribute to the directive to call a method after when the user changed the value and then update the database etc. The method (´$onSave´ here) is called fine, but it seems the parent scope is not yet updated when I call the method at the end of the directive.
Is there a way to call the method but have the parent scope updated for sure?
Thanks in advance,
Michael
I believe you are supposed to create the functions to attach inside the linking function:
Take a look at this code:
http://plnkr.co/edit/ZTx0xrOoQF3i93buJ279?p=preview
app.directive('clickToEdit', function () {
return {
templateUrl: 'clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
link: function(scope, element, attrs){
scope.save = function(){
console.log('save in link fired');
}
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
console.log('save in controller fired');
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I haven't declared the functions inside the controller before, but I don't see why it wouldn't work.
Though this question/answer explain it Link vs compile vs controller
From my understanding:
The controller is used to share data between directive instances, not to "link" functions which would be run as callbacks.
The method is being called but angular doesn't realise it needs to run the digest cycle to update the controller scope. Luckily you can still trigger the digest from inside your isolate scope just wrap the call to the method:
$scope.$apply($scope.method());
I have to test a directive depending on a parent scope function for its initialisation:
.directive('droppedSnippet', function () {
return {
templateUrl: 'views/dropped-snippet.html',
restrict: 'E',
scope: {
id: '#',
get: '&'
},
link: function postLink(scope, element, attrs) {
var s = scope.get({id: attrs.id});
element.find('.title').text(s.title);
}
};
});
Context, skip if in a hurry: In order to make it easier to imagine (and to discuss the whole idea if you want), on a drop event this directive is added to the document. The directive represents an embed code. During linking the directive, knowing only its id, should fetch its content from a controller and fill its markup.
In order to mock the parent scope created by the controller, i set up the following mock:
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
scope.foo = function() {
return {
title: 'test title',
code: 'test <code>'
};
};
spyOn(scope, 'foo').andCallThrough();
element = angular.element('<dropped-snippet id="3" get="foo(id)"></dropped-snippet>');
element = $compile(element)(scope);
}));
it('calls the scope function', function() {
expect(scope.foo).toHaveBeenCalledWith(3);
});
The test fails, scope.foo is not called. The code works on the server though. I can not find similar examples around. Is this the right way to mock a function in the parent scope?
Try expect(scope.foo).toHaveBeenCalledWith("3");
Or cast it as a number, the # treats it as a String