I've created a directive and initiated some models in link: method, then bind a click event on <a> tag of template. Where scope models switches it's values to each-other.
When I see model values in console, seems it's working fine. But does not reflect on UI.
Here's my Fiddle: http://jsfiddle.net/gp32g7sr/7/
Need to $apply on a scope to run digest cycle. Changing scope variable from an events doesn't run angular digest cycle we need to manually run it.
Link Fn
link: function (scope, element, attrs) {
scope.activeContinent = 'Asia';
scope.altContinent = 'America';
element.find('a').on('click', function (e) {
e.preventDefault();
var x = scope.altContinent;
scope.altContinent = scope.activeContinent;
scope.activeContinent = x;
document.querySelector('#log')
.innerHTML = scope.activeContinent + ' ' + scope.altContinent;
//$compile(document.querySelector('#log'))(scope);
scope.$apply();
});
}
Working Fiddle
Related
I had to add a $destroy event listener to the element object in a directive from what I found with this answer Why isn't $destroy triggered when I call element.remove?
Resulting in a link function made with a scope/element....
controller: "MyCtrl",
link: function(scope, element) {
element.on("$destroy", function() {
scope.func();
});
}
Where func is a function defined in MyCtrl.
This works for what I want...but I'm having trouble testing the element.on("$destroy" event.
After injecting/mocking in my directive test, I create the element such like...
this.$compile = $injector.get("$compile");
this.$rootScope = $injector.get("$rootScope");
this.$scope = this.$rootScope.$new();
this.template = "<my-dir></my-dir>";
this.initElement = function() {
this.element = this.$compile(this.template)(this.$scope);
return this.element;
};
Trying to write a unit test, with destroying the scope. The element destroy event isn't triggered...and my this.element does not have a $destroy function it to call. So I'm not sure exactly how I trigger the element's $destroy event.
it("when element destroyed, call scope.func", function() {
this.$httpBackend.whenGET("app/my-dir.tpl.html").respond(200);
this.$scope.unsubscribeToMapMoveEvents = jasmine.createSpy("func");
this.initElement();
this.$scope.$destroy();
expect(this.$scope.func).toHaveBeenCalled();
});
I think the problem I'm facing in this unit test is the same reason why I moved this logic from the ctrl to the directives link function
Any help on how I can test this element on destroy workflow?
My solution was the directive that is defined using this controller is using a $rootScope passed in, and that was breaking it to where the destroy wouldn't kick off.
I instead changed the directive to be initialized with it's own empty scope like....
controller: "MyCtrl",
scope: {}
I'm building a directive that decorates a table header item and sort the data upon the click event. I cannot apply the changes on the data model to the parent scope using the directive scope.
vm.data is an array on the parent scope that contains the data I want to sort in the directive.
After the click the data object in the directive has changed but the parent is still in the same order.
I dont want to access the parent scope using $parent, What I'm missing ??
<th sortable="browser" data="vm.data">Browser</th>
directive code:
angular
.module("app")
.directive("sortable", ['lodash', sortableDirective]);
function sortableDirective(lodash) {
return {
restrict: "A",
scope:{
data:"="
},
controller:function($scope){
},
link: function (scope, element, attributes) {
var sorted = undefined;
var col = attributes['sortable'];
var oldClass = 'sorting'
attributes.$$element.addClass(oldClass);
$(element).on("click", sort);
function changeClass(){
if(sorted=='asc'){
attributes.$$element.removeClass(oldClass);
attributes.$$element.addClass('sorting_asc');
oldClass = 'sorting_asc';
}
else if(sorted=='desc'){
attributes.$$element.removeClass(oldClass);
attributes.$$element.addClass('sorting_desc');
oldClass='sorting_desc';
}
}
function sort() {
if (sorted == 'asc') {
sorted = 'desc';
}
else {
sorted = 'asc';
}
scope.data = lodash.sortBy(scope.data, function (o) {
return o[col];
});
if (sorted == 'desc') {
lodash.reverse(scope.data);
}
changeClass();
}
}
};
}
This is because you are using jQuery to listen to change on the element. So just change this line:
$(element).on("click", sort);
to
element.on("click", sort);
The 2nd attribute i.e. element is already an instance of jQlite if jQuery is not available and will be an instance of jQuery if jQuery is available.
In any case, there is a method available .on which will be executed on the value change. Since you again wrapped it to $(), the Angular was not getting notified of the change in the data.
Edit:
On the 2nd walk through of your code, I see the actual problem. You are reassigning the complete scope.data in the sort() method which is breaking the pass by reference behavior of Javascript (or in any OOPS programming).
The pass by reference will only work if you continue to modify your SAME reference variable. Noticed the word, SAME?? By writing scope.data = lodash.sortBy(scope.data, function (o) {}) you removed the reference of the actual data passed to the directive. Hence the values are not updated.
So to fix this problem, you have a few options:
Change your sorting code to not reassign the complete scope.data variable RECOMMENDED (use inbuilt sort method)
Pass the modified data to the parent scope using scope.$emit()
Or use the $parent property which you don't want to use
The bidirectional binding will update the parent on each digest cycle but the click handler needs to invoke that digest cycle with $apply:
link: function (scope, element, attributes) {
var sorted = undefined;
var col = attributes['sortable'];
var oldClass = 'sorting'
element.addClass(oldClass);
//REPLACE this
//$(element).on("click", sort);
//
//WITH this
element.on("click", function (e) {
sort();
scope.$apply();
});
function changeClass(){
if(sorted=='asc'){
element.removeClass(oldClass);
element.addClass('sorting_asc');
oldClass = 'sorting_asc';
}
else if(sorted=='desc'){
element.removeClass(oldClass);
element.addClass('sorting_desc');
oldClass='sorting_desc';
}
}
The ng-click directive automatically invokes $apply but when the click event is handled by AngularJS jqLite, the code needs to notify the AngularJS framework.
From the Docs:
$apply([exp]);
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
-- AngularJS $rootScope.scope API Reference -- $apply
I have a directive which does something like this in its link function
angular.module('myApp')
.directive('barFoo', function() {
return {
restrict: 'E',
link: function (scope, element) {
element.on('click', ....);
}
};
});
Now I would like to verify in my unit test that it calls on correctly
element = angular.element('<bar-foo></bar-foo>');
$compile(element)(scope);
spyOn(element, 'on');
...
expect(element.on).toHaveBeenCalled();
It tells me the spy is not called. From what I've found on the web, angular/jquery creates a new wrapper around the DOM element every time. Meaning the the element inside my directive is not the same as the element in my spec file. Most likely (not verified) the element[0] probably are the same. I've also tried to spy on angular.element
var mockEl = { on: angular.noop };
spyOn(angular, 'element').andReturn(mockEl);
spyOn(mockEl, 'on');
but that seems to break more than it fixes (I also need functions like isloateScope for example).
Anyway, is there some easy way I can spy on the on function of the element used inside a directive?
Link function can be tested separately
element = angular.element('<bar-foo');
spyOn(element, 'on');
BarFooDirective[0].link(scope, element);
expect(element.on).toHaveBeenCalled();
if it is simple enough (stays away from attrs, required controllers, dependencies), otherwise the spec will cause more problems than it can solve.
Otherwise, it can be tested like it is done by the framework:
element = angular.element('<bar-foo');
expect(angular.element._data(element[0]).events.click).toBeDefined();
For real-world directive which may have more than one click listener defined in either itself or child directives it is not enough to make sure that listeners exist. Generally you may want to encapsulate internal functions, but anonymous click handler can also be exposed to scope for the purpose of testing:
element = angular.element('<bar-foo');
expect(angular.element._data(element[0]).events.click).toContain(scope.onClick);
Well, it's not necessary to test on method. You can test event bindings with the help of triggerHandler()
link: function (scope, element) {
element.on('click', 'something to heppen');
}
Test:
element = angular.element('<bar-foo></bar-foo>');
$compile(element)(scope);
$(element[0]).triggerHandler('click');
expect('something binded to click event to happen');
If you've compiled your element in in a beforeEach like this:
myEl = '<div my-el>MY EL</div>';
scope = $rootScope.$new();
directiveElement = $compile(myEl)(scope);
scope.$digest();
Try replacing mousedown with the event you are testing like this:
expect(directiveElement.on.mousedown).toBeDefined;
Any thoughts on why this directive is triggering an infinite digest error?
http://jsfiddle.net/smithkl42/cwrgLd0L/13/
var App = angular.module('prettifyTest', []);
App.controller('myCtrl', function ($scope) {
$scope.message = 'Hello, world!';
})
App.directive('prettify', ['$compile', function ($compile) {
var template;
return {
restrict: 'E',
link: function (scope, element, attrs) {
if (!template) {
template = element.html();
}
scope.$watch(function () {
var compiled = $compile(template)(scope);
element.html('');
element.append(compiled);
var html = element.html();
var prettified = prettyPrintOne(html);
element.html(prettified);
});
}
};
}]);
It seems to be the very first line in the scope.$watch() function that's triggering the model update, as when I remove that line, it doesn't trigger the error.
var compiled = $compile(template)(scope);
I'm a little confused as to why that line is triggering another $digest - it doesn't seem to be updating anything directly in the scope.
Is there a better way to accomplish what I'm trying to do, e.g., some other way to check to see if the key values in the scope have actually changed so I can recompile the template? (And is there a better way of grabbing the template?)
When you use scope.$watch() with just a function and no watch expression, it registers a watcher that gets triggered on every digest cycle. Since you're calling $compile within that watcher, that's effectively triggering another digest cycle each time since it needs to process the watchers created by your template. This effectively creates your infinite digest cycle.
To use the same code, you should probably just be compiling once in your postLink function, but I don't think you even need to do that - you should be able just use the template property. Then your $watch() statement should include an expression targeting the property you want to watch for changes - in this case, just 'message', and update the HTML accordingly.
I'm using the AngularUI bootstrap library in my project. In particular, I'm using the accordion and a custom template to display the accordion groups etc. On the template I have an ng-click event which seems to be working only when I don't have the parameter on it. Inside my directive I have a scope variable that produces a unique identifier which I have included as a parameter on the ng-click event. What am I missing to get this working? I'm using Angular 1.0.8.
thanks in advance
!-- template
<a callback="accordion_group_opened($index)" ng-click="callSubmit(currentRow)">
!-- scope variable incremented each time the directive is called
countRow = generateUnique.getNextIdStartingAtOne();
scope.currentRow = countRow;
Edit:
Added the compilation still not getting the value from the scope into my ng-click param. Any ideas on a work around?
compile:function (scope, element, attrs) {
var countRow = generateUnique.getNextIdStartingAtOne();
scope.currentRow = countRow;
return function(scope, element, attr) {
$timeout(function() {
var fn = $parse(attr["ngClick"]);
element[0].on("click", function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
})
};
}