Karma Testing Visual Response of Kendo UI - javascript

So i've looked around for someone who has tried to attempt what I am and have had no luck... So here we go. In response to the Kendo Lab's post *Disclaimer*: While Angular Kendo UI is not supported under the formal Kendo UI support agreement, this is an active community project, and we would LOVE for you to open an issue, or fork the repo and submit a pull request. StackOverflow is also a great place to get assistance. I have no choice but to come to StackOverflow. Here's my situation.
I have angular kendo setup in my web app and it runs GREAT! (Bit of a learning curve but that's how things go). I want to know test that directives i've written work properly and would LOVE to be able to test the with the kendo directives i'm looking for.
Now for the code
resize-directive.js
app.directive("ibsResizeGraphDirective", ['$window', function ($window) {
return function (scope, element) {
//Bind to window resize so that we can have the elements respond.
//There is no element.resize event and watching for width change doesn't work
angular.element($window).bind('resize', function () {
scope.$apply();
});
//Watch for element.width() change
scope.$watch(function () {
return $(element).width();
}, function (newValue, oldValue) {
if (newValue != oldValue) {
scope.graph.options.chartArea.width = $(element).width();
// Turn off transitions so that the graphs don't redraw everytime the window changes
if (oldValue != 0 && scope.graph.options.transitions) {
scope.graph.options.transitions = false;
}
scope.graph.refresh();
}
})
//...
};
}]);
as you can see i'm trying to basically check the size of the chart's element and set the chartArea.width accordingly.
The biggest problem i'm having is getting the chart to even show up. To help make things easier on our end we decided to wrap up our chart declaration into a directive!
chart.js
app.directive('ibsChart', [ "ibsMainService", function (ibsMainService) {
return {
// Restrict E for element
restrict: 'E',
// Here we setup the template for the code we want to replace our directive
template:"<div ibs-resize-graph-directive \n\
ibs-owner-warehouse-listener-directive \n\
ibs-graph-culture-caption \n\
kendo-chart='graph' \n\
k-options='chartOptions' \n\
k-data-source='dataSource' \n\
class='col-lg-4'/>",
replace: true,
scope: {
//...
},
controller: function($scope){
//...
},
link: function (scope, element, attrs) {
//...
};
}]);
and finally my tests...which I can't even get my chart to render properly...so how am I supposed to even check that the width changed?!
resize-test.js
//Setup items before each test
beforeEach(module('dynamanMain', 'kendo.directives'));
//Initialization Tests
it('should render the chart', angular.mock.inject(function ($compile, $rootScope, $timeout) {
var scope = $rootScope.$new();
ele = $compile('<ibs-chart-directive></ibs-chart-directive>')(scope);
scope.$apply();
//$timeout.flush();
//Test that the kendo grid was created over the div element
console.log(ele.find('div')); //This basically outputs nothing
}));
And here is a screenie of the result
There is not chart rendered (which I know it's because i'm not appending it to the body)
There is no element in the body after my scripts (which when doing the angular-kendo tests they appear)
I get the prototype returned from my element.find('div')
This is definitely a longer post but I wanted to be as thorough as I could to get a good answer. Anyone have any thoughts?

I'm a bonehead. I was not targeting the directive correctly.
ele = $compile('<ibs-chart-directive></ibs-chart-directive>')(scope);
should be
ele = $compile('<ibs-chart></ibs-chart>')(scope);
because my directive is defined as
app.directive('ibsChart', [ "ibsMainService", function (ibsMainService) {
as a result of making these changes I was also able to complete the test and test the width of the chart and whether or not the resize function was being called....I LOVE finding small things like that after searching for days.....

Related

Angularjs $watchCollection on $rootScope not triggered when values updated from directive

I'm running into a bit of an issue solving a problem with some Angularjs functionality I'm working on.
The basic idea is that I have a system where certain criteria must be met before the user is allowed to advance to the next section of the app. One example of this is that a user must both add a comment, and click a link (in the real app, this is a file download) in order for them to advance.
You can take a look at the full example here: https://jsfiddle.net/d81xxweu/10/
I'll assume that the HTML is pretty self explanatory and move on to what I'm doing with my Angular module. My app declaration and initialization are as follows:
var myApp = angular.module('myApp', ['ngRoute']);
myApp.run(function ($rootScope) {
// Both of these must be met in order for the user to proceed with 'special-button'
$rootScope.criteria = {
criteria1: false,
criteria2: false
};
});
This is pretty simple. I'm attaching an object called criteria to the root scope of the application in order to make it accessible to my directives and controllers. I have a directive that renders the link which allows the user to advance once the criteria are met. In this example the text of the link changes from "Waiting..." to "Click to proceed" to indicate we may advance.
myApp.directive('specialButton', function ($rootScope) {
return {
scope: true,
template: "<a href='#'>{{ linkText }}</a>",
replace: true,
link: function (scope, el, attrs) {
scope.linkText = 'Waiting...';
var setLinkState = function(currentCriteria) {
var criteriaMet = true;
for(var k in $rootScope.criteria) {
if($rootScope.criteria[k] == false) {
criteriaMet = false;
}
}
if(criteriaMet) {
scope.linkText = 'Click to proceed';
}
};
// Watch for changes to this object at the root scope level
$rootScope.$watchCollection('criteria', function(newValues) {
setLinkState(newValues);
});
}
};
});
So in order to trigger the watch statement we've set on this directive I can add a comment as allowed by this controller:
myApp.controller('comments', function ($scope, $rootScope) {
$scope.commentText = '';
$scope.comments = [];
$scope.addComment = function () {
$scope.comments.push({ commentText: $scope.commentText });
$scope.commentText = ''
// When the user adds a comment they have met the first criteria
$rootScope.criteria.criteria1 = true;
};
});
The previous is my controller for displaying/adding comments. I set criteria1 to true here to indicate the user has added a comment. This actually works fine, and the $watchCollection in the specialButton directive is called as expected.
The problem arises when I try to perform the same action from the link that must be clicked in order to advance. This is rendered with a directive as it is my understanding that in a case such as this a directive makes more sense than a controller, unlike the comment list/form.
myApp.directive('requiredLink', function($rootScope) {
return {
scope: true,
template: "<a href='#'>Click me!</a>",
replace: true,
link: function(scope, el, attrs) {
el.bind('click', function(evt) {
evt.preventDefault();
// When the user clicks this link they have met the second criteria
$rootScope.criteria.criteria2 = true;
});
}
};
});
As you can see here I pass in $rootScope just as in the controller. However when I set criteria2 to true the $watchCollection is not triggered.
So what ends up happening is if I add a comment first, then click the other button, I do not see specialButton update its text because the second change never triggers the watch. If, however, I click the link first, then add a comment, specialButton updates as expected. The click of requiredLink IS updating the data, but not triggering the watch. So when I then add a comment and the $watch is triggered it sees that BOTH have been set to true.
Thanks in advance for any help you can offer in resolving this issue; I appreciate your time.
Your actual problem is you are update $rootScope from the event which is outside the angular context, so its obivious that angular binding will not update because digest cycle doesn't get fired in that case. You need to fire it manually by using $apply() method of $rootScope
el.bind('click', function(evt) {
evt.preventDefault();
// When the user clicks this link they have met the second criteria
$rootScope.criteria.criteria2 = true;
$rootScope.$apply(); //this will run digest cycle & will fire `watchCollection` `$watcher`
});
Demo Plunkr
Though this solution work but I'll suggest you to use service instead
of using $rootScope
For implementation using service you need to follow below things that would help you.
Your service should be using criteria variable in object form, should follow the dot rule so that the respective reference will update using JavaScript prototypal
Sevice
app.service('dataService', function(){
this.criteria = {
criteria1: false,
criteria2: false
};
//...here would be other sharable data.
})
Whenever you want to use it any where you need to inject it in function of controller, directive, filter wherever you want.
And while putting watch on service variable from directive you need to do something like below.
Directive
myApp.directive('specialButton', function (dataService) {
return {
scope: true,
template: "<a href='#'>{{ linkText }}</a>",
replace: true,
link: function (scope, el, attrs) {
//.. other code
// deep watch will watch on whole object making last param true
scope.$watch(function(){
return dataService.criteria //this will get get evaluated on criteria change
}, function(newValues) {
setLinkState(newValues);
}, true);
}
};
});

run angular compile only when a certian section of a page has changed

So I am working on a application that has many many sections that are loaded via jquery ajax, some when it starts and some in response to other sections, and some are even nested. What I wan't to be able to do is say "Oh, the content in mainContainer changed?" Well, go compile the code in that section.
The reason is that I would like to be able to put ui-sref's into parts of the code we don't otherwise want to mess with yet. I came up with a solution but it isn't working the way I thought, I added a $watch to a custom directive but it fires a bazillion times. Here is the code.
Hopefully some one can explain how to get it to run once only when the content in X <div> is changed via one of the many ajax calls that happen, that way I don't need to immediately turn all of the old code into angular and can link to new code that is using angular.
app.directive('ngMainContainer', function ($compile, $timeout) {
console.log("main container directive");
return function (scope, elem, attrs) {
scope.$watch(function () {
return elem;
}, function (val) {
console.log("Something changed. " + val);
$timeout(function () {
$compile(val)(scope);
}, 0);
});
};
});
again, I thought putting a watch on the element passed into the directive was the answer however this runs even when I move the mouse and click on the page somewhere outside of the container that was loaded via jquery ajax.
Here is a solution that may work for you. I place directive ngMainContainer on our element we want to observe the html contents of.
In our controller I append <p>{{ content }}</p> which I first $compile, then update the scope var afterwards like $scope.html = angular.element(myDiv).html(). This is where the our directive $watch fires.
Here is the full working example. The <button> is appending html, but this should emulate the resolution of an ajax call request complete.
JSFiddle Link
app.directive('ngMainContainer', [function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
scope.$watch('html', function(n, o) {
if(n !== o) {
console.log(elem.contents())
// html has changed, do something!
}
});
}
}
}]);
app.controller('ctrl', ['$scope', '$compile', function($scope, $compile) {
$scope.content = "scope content";
var myDiv = document.getElementById('main');
$scope.appendContent = function() { // Replicate Ajax Callback
var dynamic = $compile('<p ng-cloak>{{ content }}</p>')($scope); // Compile content
angular.element(myDiv).append(dynamic);
$scope.html = angular.element(myDiv).html()
}
}]);
Edit
Here is a way to do this while appending content to the <div> with no notion of angular on the code that is performing the appending
Updated JSFiddle
<button id="noAngular" onclick="noAngularAppend()">Append Content - No Angular</button>
function noAngularAppend() {
angular.element(document.getElementById('main')).scope().externallyAppend('<p ng-cloak>{{ content }}</p>');
}
Directive Function (inject $compile)
scope.externallyAppend = function(element) {
var node = $compile(element)(scope);
elem.append(node);
scope.$apply(scope.html = elem.contents());
}
Sal, I up voted your answer because it ultimately did help me figure this out but in the end it wasn't quite what I was going for.
This is what I ended up with. In the controller for the main application there is function with only two lines in it.
$scope.doReshing = function (element) {
$compile(angular.element(element))($scope);
$scope.$apply();
}
Then in which ever partial that has an ng directive in it right at the top of the document.ready() I put the following.
var removeableContentContainer = $('#removableContentContainer');
angular.element(removeableContentContainer).scope().doReshing(removeableContentContainer);
This does exactly what I want. It lets me take an existing application change virtually nothing about how it works and lets me add ui-srefs in the "old" code and not have to worry about how or when it loads, when it does load, the document.ready() fires and it will tell angular that it needs to compile. This was my goal, you can pull bits of code in at any point in time and have them essentially act as though they were parts of that page with it's controller.

How do you trigger a reload in angular-masonry?

I got Masonry to work in my AngularJS app using the angular-masonry directive but I want to be able to call a function or method in my controller that will trigger a reload of items in the container. I see in the source code (lines 101-104) there is a reload method but I'm not sure how to invoke that. Any ideas?
Thanks!
In case it's of use to someone in the future, Passy watches for an event called masonry.reload.
So, you can issue this event and Passy should call 'layout' on the masonry element, e.g. call
$rootScope.$broadcast('masonry.reload');
In my case, I had some third-party javascript decorating my bricks, so I needed to repaint after that was done. For reason (I was not able to figure out why), I needed to wrap the event broadcast in a timeout, I think the Passy scheduler was eating up the event and not repainting. E.g. I did:
$timeout(function () {
$rootScope.$broadcast('masonry.reload');
}, 5000);
This way you don't have to modify Passy directly.
I don't think you can trigger the reload() method directly, since it is part of the controller which is only visible within the directives.
You might either ...
A) trigger a reload directly of masonry via the element, jQuery-style
(see http://jsfiddle.net/riemersebastian/rUS9n/10/):
$scope.doReloadRelatively = function(e) {
var $parent = $(e.currentTarget).parent().parent();
$parent.masonry();
}
$scope.doReloadViaID = function() {
$('#container').masonry();
}
B) or extend the directives yourself, i.e. add necessary watches on masonry-brick and call the reload() method within (depends on what use-case you have).
.directive('masonryBrick', function masonryBrickDirective() {
return {
restrict: 'AC',
require: '^masonry',
scope: true,
link: {
pre: function preLink(scope, element, attrs, ctrl) {
var id = scope.$id, index;
ctrl.appendBrick(element, id);
element.on('$destroy', function () {
console.log("masonryBrick > destroy")
ctrl.removeBrick(id, element);
});
scope.$watch(function () {
return element.height();
},
function (newValue, oldValue) {
if (newValue != oldValue) {
console.log("ctrl.scheduleMasonryOnce('layout');");
ctrl.scheduleMasonryOnce('layout');
}
}
);
...
In the code block above, I've simply added a watch on the element with the class 'masonry-brick' in order to trigger a reload of masonry whenever the elements' height changes.
I've created a jsFiddle for testing passys' angular-masonry myself, feel free to check it out!
http://jsfiddle.net/riemersebastian/rUS9n/10/
EDIT:
Just found a similar post on stackoverflow which could be another solution to this problem:
AngularJS Masonry for Dynamically changing heights
The answer from #SebastianRiemer helped me.
But for future persons that need help try instead use the following
scope.$watch(function () {
return element.height();
},
function (newValue, oldValue) {
if (newValue != oldValue) {
ctrl.scheduleMasonryOnce('reloadItems');
ctrl.scheduleMasonryOnce('layout');
}
});

AngularJS directive for storing DOM elements and redirecting on form submit

I am getting into AngularJS, and I've been trying to understand directives because they are pretty much mandatory if you want to work with the DOM (when using AngularJS, correct me if I'm wrong). So here is the scenario, I am trying to create a simple login system (I am actually using the MEAN stack - MongoDB, ExpressJS, AngularJS, NodeJS). I'm not too worried about security (or otherwise less than perfect code) because I am just trying to learn how to use the frameworks. Here is the relevant code:
MemberModule.js:
var MemberModule = angular.module('MemberModule', ['ui.bootstrap']);
MemberModule.controller('MemberListController', function ($scope, $html)) {
$scope.members = [];
$scope.newMember = {
done : false
};
$scope.doneFilter = { done : true };
$scope.notDoneFilter = { done : false };
//various methods...
});
MemberModule.directive('usernameDir', ['$interval', function($interval) {
function link(scope, element, attrs) {
var newMember,
timeoutId;
function updateUsername() {
element.text(scope.newMember.username);
}
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
element.on('$destroy', function() {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function() {
UpdateTime(); // update DOM
}, 1000);
}
return {
link: link
};
});
MemberModule.directive('passwordDir', function () {
// The above name 'myDirective' will be parsed out as 'my-directive'
// for in-markup uses.
return {
restrict: 'E',
transclude: true,
scope: {
'sub' : '&ngSubmit'
},
template: 'home'
}
});
As you can see above, I created the main angular.module and called it MemberModule - which gets referenced in my HTML (I am using jade templates - so by HTML I mean layout.jade). After that I created the controller with its various methods that I need. Finally, I created the directives which is what I need help with. I am trying to assign a DOM input element (in a form) to an object attribute, and then redirect (or render) a jade template (home.jade).
The relevant form HTML ('index.jade'):
extends layout
block content
div.container(ng-controller="MemberListController", ng-init="setMembers( #{JSON.stringify(members)} )")
h1 Welcome
h2 Sign Up
form(novalidate, ng-submit="addNewMember()")
input( type="text", username-dir info="userdir")
br
input( type="password", password-dir info="passdir")
br
input( type="password" )
br
button.btn.btn-primary(class="sub", type="submit") Submit
h2 Adding...
span(username dir)
span(password dir)
I am just pasting what I have so far so you can see where I am at in terms of progress. I am fully aware that my code is not functional as is - I am just looking for some help in pointing out what needs to go where to accomplish my goal. I realize that the two directives (while trying to attain the same goal) are not using the same style of directive code - this is just because of where I am at in terms of trying things. Again, my goal is (specifically for the username and password):
I am trying to assign a DOM input element (in a form) to an object attribute, and then redirect (or render) a jade template (home.jade).
Thanks.
Big ups to Julian Hollmann (check comments):
"You don't need both directives at all. Just use ng-model (docs.angularjs.org/api/ng/directive/ngModel) to bind your scope data to the input elements. Then use ng-submit to call a function in the controller."
Bingo - thanks!

Is $timeout the only/recommended way to avoid jQuery plugin rendering problems in AngularJS directives?

I'm porting a jQuery webapp to AngularJS (<- beginner!).
To integrate bxSlider along with some templating stuff, I wrote following directive:
[Edit] better have a look at jsFiddle jsfiddle.net/Q5AcH/2/ [/Edit].
angular.module('myApp')
.directive('docListWrapper', ['$timeout', function ($timeout) {
return {
restrict: 'C',
templateUrl: 'partials/doc-list-wrapper.html',
scope: { docs: '=docs'},
link: function (scope, element, attrs) {
$timeout(function () {
element
.children('.doc-list')
.not('.ng-hide')
.bxSlider(); // <-- jQuery plugin doing heavy DOM manipulation
}, 100); // <-------------- timeout in millis
}
};
}]);
Without $timeout there is the problem that bxSlider cannot calculate sizes of the freshly created elements or doesn't find them at all.
I'm a bit concerned that using a long timeout-value might cause flickering while using a short value could cause problems on slow machines.
In my real application (of course with more data and more sections than in the jsFiddle) I observed something strange:
When I play around with the timeout value, using 10 or more milliseconds is enough so the jQuery plugin bxSlider finds a complete DOM. With less time waiting (9 millis or less), the plugin is not able to wrap the <ul> as it should.
But the problem of a very nasty flickering is still present.
In the fiddle, probably due to a smaller DOM, the flickering is not visible in Chrome + Firefox, only with Internet Explorer 10.
I don't want to rely on empiric values for $timeout which could be highly dependent on machine, os, rendering engine, angular version, blood preasure, ...
Is there a robust workaround?
I've found some examples with event listeners ($on, $emit) and with some magic done with ng-repeat $scope.$last. If I can remove flickering, I'd accept some coupling between components, even this does not fit nice with AngularJS' ambition.
Your problem is a racing condition problem, so you can't just remove the $timeout. Pretty much what happens is:
Angular compiles both elements;
Angular links you bx-slider element;
bx-slider looks for <li> elements (none at this time) and create the list;
Angular links the ng-repeat and build the <li> list and resolve the bindings.
So, to solve the first aspect of racing condition (build the component only after all <li> are ready), you should expose a update method at bxSlider directive and create a sub-directive that would call a update function in the bxSlider controller, using the $scope.$last trick:
.directive('bxSlider', function () {
var BX_SLIDER_OPTIONS = {
minSlides: 2,
maxSlides: 7,
slideWidth: 120
};
return {
restrict: 'A',
require: 'bxSlider',
priority: 0,
controller: function() {},
link: function (scope, element, attrs, ctrl) {
var slider;
ctrl.update = function() {
slider && slider.destroySlider();
slider = element.bxSlider(BX_SLIDER_OPTIONS);
};
}
}
}])
.directive('bxSliderItem', function($timeout) {
return {
require: '^bxSlider',
link: function(scope, elm, attr, bxSliderCtrl) {
if (scope.$last) {
bxSliderCtrl.update();
}
}
}
})
This solution would even give you the ability to add new itens to the model, for everytime you have a new $last item, the bxSlider would be built. But again, you would run into another racing condition. During step 3, the slider component duplicates the last element, in order to show it just before the first, to create a 'continuity' impression (take a look at the fiddle to understand what I mean). So now your flow is like:
Angular compiles both elements;
Angular links you bx-slider element;
Angular links the ng-repeat and build the <li> list;
Your code calls the parent update function, that invokes your component building process, that duplicates the last element;
Angular resolves the bindings.
So now, your problem is that the duplications made by the slider, carries only the templates of the elements, as Angular hadn't yet resolved it bindings. So, whenever you loop the list, you gonna see a broken content. To solve it, simply adding a $timeout of 1 millisecond is enough, because you gonna swap the order of steps 4 and 5, as Angular binding resolution happens in the same stack as the $digest cycle, so you should have no problem with it:
.directive('bxSliderItem', function($timeout) {
return {
require: '^bxSlider',
link: function(scope, elm, attr, bxSliderCtrl) {
if (scope.$last) {
$timeout(bxSliderCtrl.update, 1);
}
}
}
})
But you have a new problem with that, as the Slider duplicates the boundaries elements, these duplications are not overviewed by AngularJs digest cycle, so you lost the capability of model binding inside these components.
After all of this, what I suggest you is to use a already-adapted-angularjs-only slide solution.
So, summarizing:
you can use 1 millisecond delay in your solution, because Angular digest cycle is synchronous; - but you lost the capability to add new items to your list
you can use a $scope.$last trick with the $timeout as well - but you lost Angular bindings and if this components have any instance (selected, hover), you gonna have problem
you could use an already written solution (like the one I suggested).
you could write your own AngularJs native solution.
My answer seems round-about but might remove your need for $timeout. Try making another directive and attaching it to the li element. Something like the following pseudo code:
angular.module('myApp').directive('pdfClick', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
$element.bxSlider().delegate('a', 'click', pdfClicked);
}
}
});
<li class="doc-thumbnail" ng-repeat="doc in docs" pdfClick>
It should attach the click event to every list item's anchor generated by ng repeat.
Data hasn't yet arrived at scope at rendering time!
It turned out the problem was that the data has not been present at the time the directive was executed (linked).
In the fiddle, data was accessible in the scope very fast. In my application it took more time since it was loaded via $http. This is the reason why a $timeout of < 10ms was not enough in most cases.
So the solution in my case was, instead of
angular.module('myApp')
.directive('docListWrapper', ['$timeout', function ($timeout) {
return {
restrict: 'C',
templateUrl: 'partials/doc-list-wrapper.html',
scope: { docs: '=docs'},
link: function (scope, element, attrs) {
$timeout(function () { // <-------------------- $timeout
element
.children('.doc-list')
.not('.ng-hide')
.bxSlider();
}, 10);
}
};
}]);
I now have this:
angular.module('myApp')
.directive('docListWrapper', [function () {
return {
restrict: 'C',
templateUrl: 'partials/doc-list-wrapper.html',
scope: { docs: '=docs'},
link: function (scope, element, attrs) {
scope.$watch('docs', function () { // <---------- $watch
element
.children('.doc-list')
.not('.ng-hide')
.bxSlider();
});
}
};
}]);
Maybe there is a more elegant solution for this problem, but for now I'm happy that it works.
I hope this helps other AngularJS beginners.

Categories

Resources