AngularJS Pagination Directive w/ Isolate Scope - angularjs

I have the following pagination directive written by a member of my team:
myApp.directive('pagination', function($timeout) {
return {
restrict: 'A',
controller: function($scope, $element, $attrs, $rootScope) {
var halfDisplayed = 1.5,
displayedPages = 3,
edges = 2;
$scope.getInterval = function() {
return {
start: Math.ceil($scope.currentPage > halfDisplayed ? Math.max(Math.min($scope.currentPage - halfDisplayed, ($scope.pages - displayedPages)), 0) : 0),
end: Math.ceil($scope.currentPage > halfDisplayed ? Math.min($scope.currentPage + halfDisplayed, $scope.pages) : Math.min(displayedPages, $scope.pages))
};
};
$scope.selectPage = function(pageIndex) {
$scope.currentPage = pageIndex;
$scope.$apply();
$scope.draw();
$scope.paginationUpdate();
};
$scope.appendItem = function(pageIndex, opts) {
var options, link;
pageIndex = pageIndex < 0 ? 0 : (pageIndex < $scope.pages ? pageIndex : $scope.pages - 1);
options = $.extend({
text: pageIndex + 1,
classes: ''
}, opts || {});
if (pageIndex === $scope.currentPage) {
link = $('<span class="current">' + (options.text) + '</span>');
} else {
link = $('' + (options.text) + '');
link.bind('click', function() {
$scope.selectPage(pageIndex);
});
}
if (options.classes) {
link.addClass(options.classes);
}
$element.append(link);
};
$rootScope.draw = function() {
$($element).empty();
var interval = $scope.getInterval(),
i;
// Generate Prev link
if (true) {
$scope.appendItem($scope.currentPage - 1, {
text: 'Prev',
classes: 'prev'
});
}
// Generate start edges
if (interval.start > 0 && edges > 0) {
var end = Math.min(edges, interval.start);
for (i = 0; i < end; i++) {
$scope.appendItem(i);
}
if (edges < interval.start) {
$element.append('<span class="ellipse">...</span>');
}
}
// Generate interval links
for (i = interval.start; i < interval.end; i++) {
$scope.appendItem(i);
}
// Generate end edges
if (interval.end < $scope.pages && edges > 0) {
if ($scope.pages - edges > interval.end) {
$element.append('<span class="ellipse">...</span>');
}
var begin = Math.max($scope.pages - edges, interval.end);
for (i = begin; i < $scope.pages; i++) {
$scope.appendItem(i);
}
}
// Generate Next link
if (true) {
$scope.appendItem($scope.currentPage + 1, {
text: 'Next',
classes: 'next'
});
}
};
},
link: function(scope, element, attrs, controller) {
$timeout(function() {
scope.draw();
}, 2000);
scope.$watch(scope.paginatePages, function() {
scope.draw();
});
},
template: '<div class="pagination-holder dark-theme">' + '</div>',
replace: true
};
});
Unfortunately, when he wrote it, the directive refered to controller variables and functions:
function PatientListModalConfigCtrl($scope, $rootScope, myService) {
$scope.currentPage = 0;
$scope.paginationUpdate = function() {
var list = myService.search($scope.currentPage + 1);
list.then(function(data) {
$rootScope.List = data[1];
$rootScope.pages = data[0];
});
};
};
I was able to replace the function call with $scope[](); using the attr parameter, but as soon as I try to add:
scope: {
currentPage: '=',
totalPages: '='
}
into the directive and isolate the directive from the controller, the pagination stops displaying altogether. Can I use the attr parameter for these two variables as well? I would prefer to use the scope in the directive because the variables will be changing, but my attempts have failed. I would appreciate any feedback.

Unfortunately, I have to say the whole directive looks a bit poorly designed. The dependancy on $rootScope is a big code smell, so is all the injecting of HTML in the controller.
To get on topic, you are two-way binding to primitives (integers). I am not entirely sure that's your entire problem, but it won't work as you expect. It's very logical once you think about it, how could AngularJS ever put any data back into (i.e update) a primitive?
By the look of it, you don't actually want two-way binding, you just need to send in some values. For such simple data you can define them as attributes ('#'), and then use attrs.$observe() to listen to the changes. As for the HTML, you can then use current-page="{{currentPage}}" to pass in the value (please note, it will be as a string, which you can then parse back).
Another option could be to pass in an object (using two-way binding). Example:
page = {
currentPage: 3,
totalPages: 14
}

Related

Why does only the last of these duplicated directives work as intended?

I have a directive that needs to display certain numbers of images based on the width of the screen, however I have noticed that when repeating this directive using an ng-repeat only the last implementation of the directive works as intended. I think this may be something to do with calling $apply() but window re-sizes do not trigger digests so I need to in order to see the changes reflected on the page.
I'm not getting any errors in the console. Is there any way to solve this?
Here's the directive.
(function () {
'use strict';
angular.module('xxxx')
.directive('xxx', xxx);
function xxxxx ($window, $timeout) {
return {
restrict: 'AE',
templateUrl: 'xxxx',
scope :{
data : "="
},
link: function (scope, elem, attrs) {
scope.componentName = 'xxxxx';
var pageWidth = window.innerWidth;
scope.resultLimit = scope.data.conferences.length;
scope.setResultLimit = function(){
console.log('triggered');
pageWidth = window.innerWidth;
if(pageWidth < 768){
if(scope.data.conferences.length % 2 !== 0){
console.log('Less than 768');
scope.resultLimit = scope.data.conferences.length - 1;
} else{
scope.resultLimit = scope.data.conferences.length;
}
}
if(pageWidth >= 768 && pageWidth < 1200){
if(scope.data.conferences.length % 3 !== 0){
console.log('Greater than 768 and less than 1200');
scope.resultLimit = scope.data.conferences.length - (scope.data.conferences.length % 3);
} else{
scope.resultLimit = scope.data.conferences.length;
}
}
if(pageWidth >= 1200){
console.log('greater than 1200');
if(scope.data.conferences.length % 5 !== 0){
scope.resultLimit = scope.data.conferences.length - (scope.data.conferences.length % 5);
} else{
scope.resultLimit = scope.data.conferences.length;
}
}
console.log(scope.resultLimit);
//scope.$apply();
};
window.onresize = function(event){
scope.$apply(function(){
scope.setResultLimit();
})
};
scope.setResultLimit();
}
};
}
})();
You are overwriting onresize event handler (window.onresize property) in every next directive initialization. You need to bind several events with addEventListener method. So it will be:
window.addEventListener('resize', function() {
scope.$apply(function() {
scope.setResultLimit();
});
});

Change Text in directive

How can I change something (for instance the color) in the directive?
I have a controller which gets after every 3 seconds new data (eye Positions)
myApp.controller('MainController', function ($scope, $interval, externalService, eyeTrackerService) {
$rootScope.gazePoints = [];
var size = 4;
var analyze = function () {
if ($rootScope.gazePoints.length > size) {
$scope.gazeArea = eyeTrackerService.getGazePoints($scope.gazePoints);
//reduce data in array
$scope.gazePoints.splice(0, 3);
}
};
var eyeTrackerData = function () {
externalService.getData().then(function (eyeTrackerData) {
var eyetracker = eyeTrackerData.data.EyeTracker;
var gaze_X = (eyetracker.X_left + eyetracker.X_right) * 0.5 * screen.availWidth;
var gaze_Y = (eyetracker.Y_left + eyetracker.Y_right) * 0.5 * screen.availHeight;
$scope.gazePoints.push({ x: gaze_X, y: gaze_Y });
analyze();
});
};
$interval(eyeTrackerData, 3000)
});
The service gets an array of gazePoints and have to analyse if the user is looking at the panel:
myApp.service('eyeTrackerService', function ($rootScope) {
this.getGazePoints = function (gazePoints) {
var counter = 0;
var date = $rootScope.rect;
for (var i = 0; i < gazePoints.length; i++) {
var x = gazePoints[i].x;
var y = gazePoints[i].y;
if (x >= date.left && x <= date.right && y >= date.top && y < date.bottom) {
counter =+ 1;
console.log("is watching");
if(counter == 5){
///Here it should call the directive and change the color
}
}
else {
console.log("is not watching")
}
}
}
});
my directive:
myApp.directive('dateInfo', function ($rootScope, $interval) {
return {
restrict: 'A',
scope: {
},
templateUrl: '123/Scripts/directives/html/dateInfo.html',
link: function (scope, element, attrs) {
$interval(function () {
$rootScope.rect = element[0].getBoundingClientRect();
//Here i want to change the color
//for example a scope.changetext(); but how??
}, 3000);
}
};
});
my html-file:
<div class="panel panel-primary">
<div class="panel-heading">MyPanel</div>
<div class="panel-body">
<input id="test" name="test">
</div>
</div>
I know it´s a lot of code. But as you can see the directive are also called after every second to get the current position of the panel.
Now I want to change the color in the panel, after the counter is == 5
What is the best solution in that case? I heard it not good to change the text in the controller.
Inside the interval, use
$interval(function () {
$rootScope.rect = element[0].getBoundingClientRect();
element.addClass('someClassThatSetsColor');
}, 3000);
You can set the class based on a parameter of your choice, say if it's not looking you'll set a class that changes the color to red, or whatever.

Angularjs responsive directive live updating issue (possibly due to ng-repeating the directive)

I am creating a post feed by ng-repeating JSON files from the cloud. I tried to make the posts responsive by using angular directives that update the template url with the screen size.
The problem is that only the last post in the ng-repeat responds and changes templates (with or without the reverse filter) when I resize the page. The other posts just remain the template that it was when originally loaded.
Here's the ng-repeat in the page
<div ng-show="post_loaded" ng-repeat="post in posts | reverse | filter:searchText ">
<feed-post>
</feed-post>
</div>
Here's the directive javascript file
app.directive('feedPost', function ($window) {
return {
restrict: 'E',
template: '<div ng-include="templateUrl"></div>',
link: function(scope) {
$window.onresize = function() {
changeTemplate();
scope.$apply();
};
changeTemplate();
function changeTemplate() {
var screenWidth = $window.innerWidth;
if (screenWidth < 768) {
scope.templateUrl = 'directives/post_mobile.html';
} else if (screenWidth >= 768) {
scope.templateUrl = 'directives/post_desktop.html';
}
}
}
};});
This happens because you re-assigning the .onresize in each directive and it stays effective only for the last linked directive.
I'd suggest to use it in a more angular way. You don't actually need a custom directive
In the controller that manages list of posts add reference to $window in $scope
$scope.window = $window;
Then in template make use of it
<div ng-include="directives/post_mobile.html" ng-if="window.innerWidth < 768"></div>
<div ng-include="directives/post_desktop.html" ng-if="window.innerWidth >= 768"></div>
To avoid extra wrappers for posts feed you might want to use ng-repeat-start, ng-repeat-end directives
this is a directive i wrote based on bootstrap sizes and ngIf directive :
mainApp.directive("responsive", function($window, $animate) {
return {
restrict: "A",
transclude: 'element',
terminal: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
//var val = $attr["responsive"];
var block, childScope;
$scope.$watch(function(){ return $window.innerWidth; }, function (width) {
if (width < 768) {
var s = "xs";
} else if (width < 992) {
var s = "sm";
} else if (width < 1200) {
var s = "md";
} else {
var s = "lg";
}
console.log("responsive ok?", $attr.responsive == s);
if ($attr.responsive == s) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = document.createComment(' end responsive: ' + $attr.responsive + ' ');
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
block.clone.remove();
block.clone = null;
block = null;
}
}
});
}
};
});

Angular UI Boostrap Carousel setting active slide after making new slides

I have a simple Carousel example. The example on Plnkr shows what I do in my application. I have to change the slides in my application. When I set the active slide after I change slides it goes to that slide and then slides out into oblivion or it goes to the first slide. How can I solve this problem? So that after making new slides I can go to the right slide?
http://plnkr.co/edit/PJg9U4HZ1k5aSTSvbl3k?p=preview
angular.module('plunker', ['ui.bootstrap', 'ngTouch']);
function CarouselDemoCtrl($scope) {
$scope.genderPerson = "men";
$scope.myInterval = -1;
$scope.slides = [];
$scope.$watch("genderPerson", function( newValue, oldValue ) {
$scope.MakeSlides();
});
$scope.MakeSlides = function() {
var newSlides = [];
for ( var i = 0; i < 10; i++ ) {
newSlides[i] = { image: 'http://api.randomuser.me/portraits/' + $scope.genderPerson + '/' + i + '.jpg' };
}
$scope.slides = newSlides;
if ( $scope.slides[6] ) {
$scope.slides[6].active=true;
}
}
}
Looks like there is a race condition, if I wrap the active slide set in a timeout with a delay it seems to work:
$timeout(function () {
$scope.slides[6].active=true;
}, 100);
I just struggled with this problem. Here's my overly technical hack totally legit solution:
1. Create a new directive that overrides the addSlide method
.directive('onCarouselChange', function ($animate, $parse) {
return {
require: 'carousel',
link: function (scope, element, attrs, carouselCtrl) {
var origAdd = carouselCtrl.addSlide;
carouselCtrl.addSlide = function(slide, element) {
origAdd.apply(this, arguments);
$animate.on('enter', element, function (elem, phase) {
if (phase === 'close') {
scope.$emit('carouselEntered');
}
});
};
}
};
})
This will emit an event when the carousel's ngRepeat has finished parsing its new elements.
2. Add the new directive to the carousel element
<carousel interval="myInterval" on-carousel-change>
3. Set the active slide on an event listener
Add an event listener to the function where you add the elements, and set the active slide on its callback
$scope.MakeSlides = function() {
var newSlides = [];
for ( var i = 0; i < 10; i++ ) {
newSlides[i] = { image: 'http://api.randomuser.me/portraits/' + $scope.genderPerson + '/' + i + '.jpg' };
}
$scope.slides = newSlides;
var dereg = $scope.$on('carouselEntered', function(event, data) {
if ($scope.slides[6]) {
$timeout(function () {
$scope.slides[6].active=true;
});
dereg();
}
});
}
All of this is possible thanks to the magic of $animate's events.

Angularjs, select2 with dynamic tags and onclick

I use angularjs with "ui_select2" directive. Select2 draws new tags with formatting function, there are "" elements with "ng-click" attribute. How to tell angularjs about new DOM elements? Otherwise new "ng-clicks" wont work.
HTML:
<input type="text" name="contact_ids" ui-select2="unit.participantsOptions" ng-model="unit.contactIds" />
JS (angular controller):
anyFunction = function(id) {
console.log(id)
}
formatContactSelection = function(state) {
return "<a class=\"select2-search-choice-favorite\" tabindex=\"-1\" href=\"\" ng-click=\"anyFunction(state.id)\"></a>"
}
return $scope.unit.participantsOptions = {
tags: [],
formatSelection: formatContactSelection,
escapeMarkup: function(m) {
return m
},
ajax: {
url: '/contacts/search',
quietMillis: 100,
data: function(term, page) {
return {
term: term,
limit: 20,
page: page
}
},
results: function(data, page) {
return {
results: data,
more: (page * 10) < data.total
}
}
}
}
The problem is that select2 creates DOM elements, that not yet discovered by angularjs, I read that new DOM elements need to be appended to some element with using angularjs $compile function, but I cannot use it in controller.
I found a solution - I created the directive, that watches for changes in ngModel and apply it on the element, that has already ui_select2 directive. "uiRequiredTags" implements custom behavior I need for my select2 tag choices. The solution is to watch changes in ngModel attribute.
angular.module("myAppModule", []).directive("uiRequiredTags", function() {
return {
restrict: 'A',
require: "ngModel",
link: function(scope, el, attrs) {
var opts;
opts = scope.$eval("{" + attrs.uiRequiredTags + "}");
return scope.$watch(attrs.ngModel, function(val) {
var $requireLink;
$requireLink = el.parent().find(opts.path);
$requireLink.off('click');
$requireLink.on('click', function() {
var id, n, tagIds;
id = "" + ($(this).data('requiredTagId'));
if (opts.removeLinkPath && opts.innerContainer) {
$(this).parents(opts.innerContainer).find(opts.removeLinkPath).data('requiredTagId', id);
}
tagIds = scope.$eval(opts.model).split(',');
n = tagIds.indexOf(id);
if (n < 0) {
tagIds.push(id);
} else {
tagIds.splice(n, 1);
}
scope.$eval("" + opts.model + " = '" + tagIds + "'");
scope.$apply();
return $(this).toggleClass('active');
});
});
}
};

Resources