diff --git a/.gitignore b/.gitignore
index e2a8d56..e099741 100644
--- a/.gitignore
+++ b/.gitignore
@@ -218,3 +218,11 @@ pip-log.txt
.idea/
node_modules/
vendor/
+
+
+#############
+## Sublime Text
+#############
+
+*.sublime-project
+*.sublime-workspace
diff --git a/Gruntfile.js b/Gruntfile.js
index 9d39d2b..d0e3701 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -85,6 +85,12 @@ module.exports = function(grunt) {
'src/directives/uiBreadcrumbs/uiBreadcrumbs.tpl.html'],
dest: '../angularUtils-dist/angularUtils-uiBreadcrumbs/'
+ },
+ dirDisqus: {
+ expand: true,
+ flatten: true,
+ src: ['src/directives/disqus/dirDisqus.js'],
+ dest: '../angularUtils-dist/angularUtils-disqus/'
}
}
});
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..37c5a1a
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,28 @@
+Hi, thanks for contributing!
+
+This project is maintained in my spare time, so in order to help me address your issue as quickly as
+possible, please provide as much of the following information as you can:
+
+- Title: Please indicate the module in question, e.g. "dirPagination: X is broken when Y"
+- If reporting on dirPagination, please include the version you are using (can be found in the package.json / bower.json file)
+- If you are able to reproduce your issue on Plunker, this will vastly increase the chances of a rapid response from me.
+
+-- Michael
+
+=======
+(Delete the above. Fill in the rest as applicable)
+
+**Description of issue**:
+
+**Steps to reproduce**:
+
+**Expected result**:
+
+**Actual result**:
+
+**Demo**: (for dirPagination, fork and modify this Plunk: http://plnkr.co/edit/b37IdFFJUokaeSummETX?p=preview)
+
+Any relevant code:
+```
+
+```
diff --git a/README.md b/README.md
index b8338d9..ac0d61e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,10 @@
# Angular Utilities
+### No longer maintained
+(20/04/2017) - I am no longer actively maintaining this project. I no longer use AngularJS in my own projects and do not have the time to dedicate to maintiaining this project as well as my other active open source projects. Thank you for your understanding.
+
+---
+
I am working on a large-scale AngularJS-based project and I'll be extracting
any useful re-usable components that I make and putting them here.
@@ -33,4 +38,4 @@ This code is made available under the MIT license, so feel free to use any of th
## License
-MIT
\ No newline at end of file
+MIT
diff --git a/bower.json b/bower.json
index 54ccd11..f7567f1 100644
--- a/bower.json
+++ b/bower.json
@@ -17,10 +17,10 @@
"tests"
],
"dependencies": {
- "angular-ui-router": "~0.2.10",
- "angular": "~1.2.24"
+ "angular-ui-router": "~0.2.13",
+ "angular": "~1.4.0"
},
"devDependencies": {
- "angular-mocks": "~1.2.24"
+ "angular-mocks": "~1.4.0"
}
}
diff --git a/package.json b/package.json
index dc38ec5..4c78ee3 100644
--- a/package.json
+++ b/package.json
@@ -6,22 +6,18 @@
"author": "Michael Bromley",
"license": "MIT",
"devDependencies": {
+ "karma": "~0.12.0",
+ "bower": "^1.3.1",
"grunt": "~0.4.2",
- "karma-script-launcher": "~0.1.0",
- "karma-chrome-launcher": "~0.1.2",
- "karma-firefox-launcher": "~0.1.3",
- "karma-html2js-preprocessor": "~0.1.0",
- "karma-jasmine": "^0.2.1",
- "karma-coffee-preprocessor": "~0.1.2",
- "requirejs": "~2.1.10",
- "karma-requirejs": "~0.2.1",
- "karma-phantomjs-launcher": "~0.1.1",
- "karma": "~0.10.9",
- "grunt-karma": "~0.6.2",
+ "grunt-contrib-copy": "^0.5.0",
"grunt-contrib-jshint": "~0.8.0",
- "bower": "^1.3.1",
"grunt-contrib-watch": "^0.6.1",
"grunt-html2js": "^0.2.4",
- "grunt-contrib-copy": "^0.5.0"
+ "grunt-karma": "~0.9.0",
+ "karma-chrome-launcher": "^0.1.5",
+ "karma-firefox-launcher": "^0.1.3",
+ "karma-jasmine": "^0.2.2",
+ "karma-phantomjs-launcher": "^0.1.4",
+ "requirejs": "~2.1.10"
}
}
diff --git a/src/directives/disqus/README.md b/src/directives/disqus/README.md
index de9d4cd..e386a83 100644
--- a/src/directives/disqus/README.md
+++ b/src/directives/disqus/README.md
@@ -1,5 +1,11 @@
# Disqus Directive
+### No longer maintained
+(20/04/2017) - I am no longer actively maintaining this project. I no longer use AngularJS in my own projects and do not have the time to dedicate to maintiaining this project as well as my other active open source projects. Thank you for your understanding.
+
+---
+
+
A directive to embed a Disqus comments widget on your AngularJS page.
## Prerequisites
@@ -13,61 +19,68 @@ Setting up as above will ensure that Disqus is able to correctly distinguish bet
By default, Angular does not use html5 mode, and also has no hashPrefix set, so you'll have to do one of the above set-up actions in order to use this directive. As far as I know, there is no way to get it to work with
the default hash-only (no `!`) urls that Angular uses.
+## Installation
+
+1. Download the file `dirDisqus.js` or:
+ * via Bower: `bower install angular-utils-disqus`
+ * via npm: `npm install angular-utils-disqus`
+2. Include the JavaScript file in your index.html page.
+2. Add a reference to the module `angularUtils.directives.dirDisqus` to your app.
+
## Usage
First, put the directive code in your app, wherever you store your directives.
Wherever you want the Disqus comments to appear, add the following to your template:
-```html
-
-
+```
+
+```
+
+And in your controller:
+
+```
+$scope.disqusConfig = {
+ disqus_shortname: 'Your disqus shortname',
+ disqus_identifier: 'Comments identifier',
+ disqus_url: 'Comments url'
+};
```
The attributes given above are all required. The inclusion of the identifier and URL ensure that identifier conflicts will not occur. See http://help.disqus.com/customer/portal/articles/662547-why-are-the-same-comments-showing-up-on-multiple-pages-
-If the identifier and URL and not included as attributes, the directive will throw an exception.
+If the identifier and URL and not included as attributes, the directive will not appear.
+
+## Full API
You can optionally specify the other configuration variables by including the as attributes
on the directive's element tag. For more information on the available config vars, see the
[Disqus docs](http://help.disqus.com/customer/portal/articles/472098-javascript-configuration-variables).
-Note that in the tag, the config attribute names are separated with a hyphen rather
-than an underscore (to make it look more HTML-like).
-
-
-
-## `ready-to-bind` attribute
+```
+$scope.disqusConfig = {
+ disqus_shortname: 'Your disqus shortname',
+ disqus_identifier: 'Comments identifier',
+ disqus_url: 'Comments url',
+ disqus_title: 'Comments title',
+ disqus_category_id: 'Comments category id }}',
+ disqus_disable_mobile: 'false',
+ disqus_config_language: 'Comments language',
+ disqus_remote_auth_s3: 'remote_auth_s3',
+ disqus_api_key: 'public_api_key',
+ disqus_on_ready: ready()
+};
+```
-If you are loading the page asynchronously, the model data (`$scope.article` in the above example) used to populate the config variables above
-will probably not be defined at the time the page is loaded. This will result in your config settings
-being all undefined. To get around this, you can specify a scope variable that should be set to (or evaluate to) `false`
-until your data is loaded, at which point you can set it to `true`. The directive watches this property and once it changes
-to `true`, any config attributes which are bound to your model should be available and used to load up the Disqus widget.
+If using the `disqus-config-language` setting, please see [this Disqus article on multi-lingual websites](https://help.disqus.com/customer/portal/articles/466249-multi-lingual-websites)
+for which languages are supported.
-For example:
+## `disqus-remote-auth-s3 and disqus-api-key` attributes for SSO
+If using the `disqus-remote-auth-s3 and disqus-api-key` setting, please see [Integrating Single Sign-On](https://help.disqus.com/customer/portal/articles/236206#sso-script)
+to know how to generate a remote_auth_s3 and public_api_key.
-```JavaScript
-// simple example of controller loading async data
-function myController($scope, $http) {
- $scope.contentLoaded = false;
+Note: Single Sign-on (SSO) allows users to sign into a site and be able to use Disqus Comments without having to re-authenticate Disqus. SSO will create a site-specific user profile on Disqus, in a way that will prevent conflict with existing Disqus users.
- $http.get('api/article/1').then(function(result) {
- $scope.article = result.article;
- $scope.contentLoaded = true; // this tells the directive that it should load the Disqus widget now
- })
-}
-```
-```html
-// in your view code
-
-
-```
+## `disqus-on-ready` attribute
- If you omit the `ready-to-bind` attribute, the Disqus widget will be created immediately. This is okay so long as
- rely on interpolated data which is not available on page load.
\ No newline at end of file
+If Disqus is rendered, `disqus-on-ready` function will be called. Callback is registered to disqus by similar technique as explained in [this post](https://help.disqus.com/customer/portal/articles/466258-capturing-disqus-commenting-activity-via-callbacks).
diff --git a/src/directives/disqus/dirDisqus.js b/src/directives/disqus/dirDisqus.js
index 535b66f..ff0f182 100644
--- a/src/directives/disqus/dirDisqus.js
+++ b/src/directives/disqus/dirDisqus.js
@@ -1,68 +1,88 @@
-/**
+/**
* A directive to embed a Disqus comments widget on your AngularJS page.
*
- * For documentation, see the README.md file in this directory
- *
* Created by Michael on 22/01/14.
+ * Modified by Serkan "coni2k" Holat on 24/02/16.
* Copyright Michael Bromley 2014
* Available under the MIT license.
*/
-angular.module('angularUtils.directives.dirDisqus', [])
- .directive('dirDisqus', ['$window', function($window) {
+(function () {
+
+ /**
+ * Config
+ */
+ var moduleName = 'angularUtils.directives.dirDisqus';
+
+ /**
+ * Module
+ */
+ var module;
+ try {
+ module = angular.module(moduleName);
+ } catch (err) {
+ // named module does not exist, so create one
+ module = angular.module(moduleName, []);
+ }
+
+ module.directive('dirDisqus', ['$window', function ($window) {
return {
restrict: 'E',
scope: {
- disqus_shortname: '@disqusShortname',
- disqus_identifier: '@disqusIdentifier',
- disqus_title: '@disqusTitle',
- disqus_url: '@disqusUrl',
- disqus_category_id: '@disqusCategoryId',
- disqus_disable_mobile: '@disqusDisableMobile',
- readyToBind: "@"
+ config: '='
},
- template: '
comments powered by ',
- link: function(scope) {
+ template: '',
+ link: function (scope) {
- // ensure that the disqus_identifier and disqus_url are both set, otherwise we will run in to identifier conflicts when using URLs with "#" in them
- // see http://help.disqus.com/customer/portal/articles/662547-why-are-the-same-comments-showing-up-on-multiple-pages-
- if (typeof scope.disqus_identifier === 'undefined' || typeof scope.disqus_url === 'undefined') {
- throw "Please ensure that the `disqus-identifier` and `disqus-url` attributes are both set.";
- }
+ scope.$watch('config', configChanged, true);
- scope.$watch("readyToBind", function(isReady) {
+ function configChanged() {
- // If the directive has been called without the 'ready-to-bind' attribute, we
- // set the default to "true" so that Disqus will be loaded straight away.
- if ( !angular.isDefined( isReady ) ) {
- isReady = "true";
+ // Ensure that the disqus_identifier and disqus_url are both set, otherwise we will run in to identifier conflicts when using URLs with "#" in them
+ // see http://help.disqus.com/customer/portal/articles/662547-why-are-the-same-comments-showing-up-on-multiple-pages-
+ if (!scope.config.disqus_shortname ||
+ !scope.config.disqus_identifier ||
+ !scope.config.disqus_url) {
+ return;
}
- if (scope.$eval(isReady)) {
- // put the config variables into separate global vars so that the Disqus script can see them
- $window.disqus_shortname = scope.disqus_shortname;
- $window.disqus_identifier = scope.disqus_identifier;
- $window.disqus_title = scope.disqus_title;
- $window.disqus_url = scope.disqus_url;
- $window.disqus_category_id = scope.disqus_category_id;
- $window.disqus_disable_mobile = scope.disqus_disable_mobile;
- // get the remote Disqus script and insert it into the DOM, but only if it not already loaded (as that will cause warnings)
- if (!$window.DISQUS) {
- var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
- dsq.src = '//' + scope.disqus_shortname + '.disqus.com/embed.js';
- (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
- } else {
- $window.DISQUS.reset({
- reload: true,
- config: function () {
- this.page.identifier = scope.disqus_identifier;
- this.page.url = scope.disqus_url;
- this.page.title = scope.disqus_title;
- }
- });
+ $window.disqus_shortname = scope.config.disqus_shortname;
+ $window.disqus_identifier = scope.config.disqus_identifier;
+ $window.disqus_url = scope.config.disqus_url;
+ $window.disqus_title = scope.config.disqus_title;
+ $window.disqus_category_id = scope.config.disqus_category_id;
+ $window.disqus_disable_mobile = scope.config.disqus_disable_mobile;
+ $window.disqus_config = function () {
+ this.language = scope.config.disqus_config_language;
+ this.page.remote_auth_s3 = scope.config.disqus_remote_auth_s3;
+ this.page.api_key = scope.config.disqus_api_key;
+ if (scope.config.disqus_on_ready) {
+ this.callbacks.onReady = [function () {
+ scope.config.disqus_on_ready();
+ }];
}
+ };
+
+ // Get the remote Disqus script and insert it into the DOM, but only if it not already loaded (as that will cause warnings)
+ if (!$window.DISQUS) {
+ var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
+ dsq.src = '//' + scope.config.disqus_shortname + '.disqus.com/embed.js';
+ (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
+ } else {
+ $window.DISQUS.reset({
+ reload: true,
+ config: function () {
+ this.page.identifier = scope.config.disqus_identifier;
+ this.page.url = scope.config.disqus_url;
+ this.page.title = scope.config.disqus_title;
+ this.language = scope.config.disqus_config_language;
+ this.page.remote_auth_s3 = scope.config.disqus_remote_auth_s3;
+ this.page.api_key = scope.config.disqus_api_key;
+ }
+ });
}
- });
+ }
}
};
}]);
+})();
diff --git a/src/directives/disqus/dirDisqus.spec.js b/src/directives/disqus/dirDisqus.spec.js
index 8acc174..b6ce512 100644
--- a/src/directives/disqus/dirDisqus.spec.js
+++ b/src/directives/disqus/dirDisqus.spec.js
@@ -1,4 +1,9 @@
-xdescribe('dirDiqus directive', function() {
+/**
+ * For some reason, when these tests are run along with all the others in this project, I get a "script error". Running
+ * them on their own using `ddescribe` works okay. Therefore this test is ignored in general unless specifically testing
+ * this directive, in which case change `xdescribe` to `ddescribe`.
+ */
+xdescribe('dirDisqus directive', function() {
var scope,
elem,
compiled,
@@ -8,13 +13,15 @@ xdescribe('dirDiqus directive', function() {
beforeEach(function (){
//set our view html.
html = '' +
- '';
+ 'disqus-identifier="{{ post.ID }}"' +
+ 'disqus-title="{{ post.title }}"' +
+ 'disqus-url="{{ post.link }}"' +
+ 'disqus-category-id="{{ post.catId }}"' +
+ 'disqus-disable-mobile="false"' +
+ 'disqus-config-language="{{ post.lang }}"' +
+ 'disqus-on-ready="ready()"' +
+ 'ready-to-bind="{{ loaded }}">' +
+ '';
inject(function($compile, $rootScope) {
//create a scope and populate it
@@ -23,9 +30,14 @@ xdescribe('dirDiqus directive', function() {
ID: 123,
title: 'test title',
link: 'http://www.test.com/',
- catId: 999
+ catId: 999,
+ lang: 'en'
};
scope.loaded = false;
+ scope.readyCalled = false;
+ scope.ready = function() {
+ scope.readyCalled = true;
+ };
//get the jqLite or jQuery element
elem = angular.element(html);
@@ -34,14 +46,20 @@ xdescribe('dirDiqus directive', function() {
compiled = $compile(elem);
//run the compiled view.
- compiled(scope);
+ var element = compiled(scope);
+
+ var div = document.createElement("div");
+ div.innerHTML = element.html();
- //call digest on the scope!
- scope.$digest();
+ // Just add disqus to document - it is needed to work embed.js properly
+ document.getElementsByTagName('body')[0].appendChild(div);
});
});
it('should not do anything when ready to bind is false', function() {
+ //call digest on the scope!
+ scope.$digest();
+
expect(elem.find("#disqus_thread")).toBeTruthy();
expect($("script[src='http://shortname.disqus.com/embed.js']").length).toEqual(0);
expect(window.disqus_shortname).toBeFalsy();
@@ -50,6 +68,8 @@ xdescribe('dirDiqus directive', function() {
expect(window.disqus_url).toBeFalsy();
expect(window.disqus_category_id).toBeFalsy();
expect(window.disqus_disable_mobile).toBeFalsy();
+ expect(scope.readyCalled).toBeFalsy();
+ expect(window.language).toBeFalsy();
});
it('should activate when ready to bind is true', function() {
@@ -62,5 +82,16 @@ xdescribe('dirDiqus directive', function() {
expect(window.disqus_url).toEqual('http://www.test.com/');
expect(window.disqus_category_id).toEqual('999');
expect(window.disqus_disable_mobile).toEqual('false');
+
+ window.page = {};
+ window.callbacks = {};
+ window.disqus_config();
+
+ expect(window.language).toEqual('en');
+ expect(window.callbacks.onReady).toBeDefined();
+ expect(window.callbacks.onReady.length).toEqual(1);
+ window.callbacks.onReady[0]();
+ expect(scope.readyCalled).toBeTruthy();
+
});
});
\ No newline at end of file
diff --git a/src/directives/pagination/README.md b/src/directives/pagination/README.md
index 034bd35..4fd209f 100644
--- a/src/directives/pagination/README.md
+++ b/src/directives/pagination/README.md
@@ -1,17 +1,46 @@
# Pagination Directive
+### No longer maintained
+(20/04/2017) - I am no longer actively maintaining this project. I no longer use AngularJS in my own projects and do not have the time to dedicate to maintiaining this project as well as my other active open source projects. Thank you for your understanding.
+
+---
+
## Another one?
Yes, there are quite a few pagination solutions for Angular out there already, but what I wanted to do was make
something that would be truly plug-n-play - no need to do any set-up or logic in your controller. Just add
an attribute, drop in your navigation wherever you like, and boom - instant, full-featured pagination.
+(**Looking for the Angular2 version? [Right here!](https://github.com/michaelbromley/ng2-pagination)**)
+
## Demo
[Here is a working demo on Plunker](http://plnkr.co/edit/Wtkv71LIqUR4OhzhgpqL?p=preview) which demonstrates some cool features such as live-binding the "itemsPerPage" and
filtering of the collection.
-## Example
+# Table of Contents
+
+- [Basic Example](#basic-example)
+- [Installation](#installation)
+- [Usage](#usage)
+ - [Specifying The Template](#customising--specifying-the-template)
+- [Directives API](#directives-api)
+ - [dir-paginate](#dir-paginate)
+ - [dir-pagination-controls](#dir-pagination-controls)
+- [Writing A Custom Pagination-Controls Template](#writing-a-custom-pagination-controls-template)
+- [Special Repeat Start and End Points](#special-repeat-start-and-end-points)
+- [Multiple Pagination Instances on One Page](#multiple-pagination-instances-on-one-page)
+ - [Multiple Instances With ngRepeat](#multiple-instances-with-ngrepeat)
+ - [Demo](#demo-1)
+- [Working With Asynchronous Data](#working-with-asynchronous-data)
+ - [Example Asynchronous Setup](#example-asynchronous-setup)
+- [Styling](#styling)
+- [FAQ](#frequently-asked-questions)
+- [Contribution](#contribution)
+- [Changelog](#changelog)
+- [Credits](#credits)
+
+## Basic Example
Let's say you have a collection of items on your controller's `$scope`. Often you want to display them with
the `ng-repeat` directive and then paginate the results if there are too many to fit on one page. This is what this
@@ -35,7 +64,11 @@ You can install with Bower:
`bower install angular-utils-pagination`
-Alternatively just download the files `dirPagination.js` and `dirPagination.tpl.html`. Using bower has the advantage of making version management easier.
+or npm:
+
+`npm install angular-utils-pagination`
+
+Alternatively just download the files `dirPagination.js` and `dirPagination.tpl.html`. Using Bower or npm has the advantage of making version management easier.
## Usage
@@ -67,10 +100,64 @@ And finally include the pagination itself.
[boundary-links=""]
[on-page-change=""]
[pagination-id=""]
- [template-url=""]>
+ [template-url=""]
+ [auto-hide=""]>
```
+### Customising & Specifying The Template
+
+By default, the pagination controls will use a built-in template which uses the exact same markup as is found in the
+dirPagination.tpl.html file (which conforms to Bootstrap's pagination markup). Therefore, it is not necessary to specify a template.
+
+However, you may not want to use the default embedded template - for example if you use a another CSS framework that
+expects pagination lists to have a particular structure different from the default.
+
+If you plan to use a custom template, take a look at the default as demonstrated in dirPagination.tpl.html to get
+an idea of how it interacts with the directive.
+
+There are three ways to specify the template of the pagination controls directive:
+
+**1. Use the `paginationTemplateProvider` in your app's config block to set a global templateUrl for your app:**
+
+```JavaScript
+myApp.config(function(paginationTemplateProvider) {
+ paginationTemplateProvider.setPath('path/to/dirPagination.tpl.html');
+});
+```
+
+**2. Use the `paginationTemplateProvider` in your app's config block to set a global template string for your app:**
+
+```JavaScript
+myApp.config(function(paginationTemplateProvider) {
+ paginationTemplateProvider.setString('...
');
+
+ // or with e.g. Webpack you might do
+ paginationTemplateProvider.setString(require('/path/to/customPagination.tpl.html'));
+});
+```
+
+**3. Use the `template-url` attribute on each pagination controls directive:**
+
+```HTML
+
+```
+
+#### Template Priority
+
+If you use more than one method for specifying the template, the actual template to use will be decided based on
+the following order of precedence (highest priority first):
+
+1. `paginationTemplate.getString()`
+2. `template-url`
+3. `paginationTemplate.getPath()`
+4. (default built-in template)
+
+## Directives API
+
+The following attributes form the API for the pagination and pagination-controls directives. Optional attributes are marked as such,
+otherwise they are required.
+
### `dir-paginate`
* **`expression`** Under the hood, this directive delegates to the `ng-repeat` directive, so the syntax for the
@@ -83,7 +170,7 @@ The optional third argument `paginationId` is used when you need more than one i
on setting up multiple instances.
* **`current-page`** (optional) Specify a property on your controller's $scope that will be bound to the current
-page of the pagination. If this is not specified, the directive will automatically create a property named `__currentPage` and use
+page of the pagination. If this is not specified, the directive will automatically create a property named `__default__currentPage` and use
that instead.
* **`pagination-id`** (optional) Used to group together the dir-paginate directive with a corresponding dir-pagination-controls when you need more than
@@ -106,17 +193,44 @@ pagination.
pagination.
* **`on-page-change`** (optional, default = null) Specify a callback method to run each time one of the pagination links is clicked. The method will be passed the
-argument `newPageNumber`, which is an integer equal to the page number that has just been navigated to. **Note** you must use that exact argument name in your view,
-i.e. ``, and the method you specify must be defined on your controller $scope.
+optional arguments `newPageNumber` and `oldPageNumber`, which are integers equal to the page number that has just been navigated to, and the one just left, respectively. **Note** you must use that exact argument name in your view,
+i.e. ``, and the method you specify must be defined on your controller $scope.
* **`pagination-id`** (optional) Used to group together the dir-pagination-controls with a corresponding dir-paginate when you need more than
one pagination instance per page. See the section below on setting up multiple instances.
-* **`template-url`** (optional, default = `directives/pagination/dirPagination.tpl.html`) Specifies the template to use.
+* **`template-url`** (optional, default = `directives/pagination/dirPagination.tpl.html`) Specifies the template to use.
+
+* **`auto-hide`** (optional, default = true) Specify whether the dir-pagination-controls should be hidden when there's not enough elements to paginate over.
Note: you cannot use the `dir-pagination-controls` directive without `dir-paginate`. Attempting to do so will result in an
exception.
+## Writing A Custom Pagination-Controls Template
+
+The default template ([dirPagination.tpl.html](dirPagination.tpl.html)) is based on the [Bootstrap pagination markup](http://getbootstrap.com/components/#pagination). If you wish to modify the template or write your own,
+there are a few useful values exposed by the directive which you can use:
+
+- `pages` The array of page numbers, typically used in an `ng-repeat` to generate the individual page links.
+- `{{ pagination.current }}` The current page.
+- `{{ pagination.last }}` The number of the last page in the collection.
+- `{{ range.lower }}` The ordinal number of the first item on the current page. E.g. assuming 10 items per page, when on page 2 this will equal 11.
+- `{{ range.upper }}` The ordinal number of the last item on the current page. E.g. assuming 10 items per page, when on page 2 this will equal 20.
+- `{{ range.total }}` The total number of items in the collection.
+
+The three `range` values can be used to generate a label like *"Displaying 16-20 of 53 items"*.
+
+Here is an example of a custom template which uses the range values along with "previous" and "next" arrow links, but no page links:
+
+```HTML
+Displaying {{ range.lower }} - {{ range.upper }} of {{ range.total }}
+
+‹
+›
+```
+
+To use a custom template in your app, see the section on [specifying the template](#specifying-the-template).
+
## Special Repeat Start and End Points
As with the [ngRepeat directive](https://docs.angularjs.org/api/ng/directive/ngRepeat#special-repeat-start-and-end-points), you can use the `-start` and `-end` suffix on the `dir-paginate` directive to
@@ -134,41 +248,70 @@ repeat a series of elements instead of just one parent element:
```
-
## Multiple Pagination Instances on One Page
-Multiple instances of the directives may be included on a single page by specifying a `pagination-id`. This property **must** be specified in **3** places
+Multiple instances of the directives may be included on a single page by specifying a `pagination-id`. This property **must** be specified in **2** places
for this to work:
1. Specify the `pagination-id` attribute on the `dir-paginate` directive.
-2. Specify the third parameter of the `itemsPerPage` filter.
3. Specify the `pagination-id` attribute on the `dir-paginations-controls` directive.
+**Note:** Prior to version 0.5.0, there was an additional requirement to add the ID as a second parameter of the `itemsPerPage` filter. This is now no longer required, as the
+directive will add this parameter automatically. Old code that *does* explicitly declare the ID in the filter will still work.
+
An example of two independent paginations on one page would look like this:
```HTML
- - {{ customer.name }}
+ - {{ customer.name }}
- - {{ customer.name }}
+ - {{ customer.name }}
```
-The pagination-ids above are set to "cust" in the first instance and "branch" in the second. The pagination-ids can be anything you like,
-the important thing is to make sure the exact same id is used in all 3 places. If the 3 ids don't match, you should see a helpful
+The pagination-ids above are set to "cust" in the first instance and "branch" in the second. The pagination-ids can be any [valid JavaScript identifier](https://mathiasbynens.be/notes/javascript-identifiers) (i.e. no hyphens, cannot begin with a number etc. [further discussion here](http://www.michaelbromley.co.uk/blog/410/a-note-on-angular-expressions-and-javascript-identifiers)),
+the important thing is to make sure the exact same id is used on both the pagination and the controls directives. If the 2 ids don't match, you should see a helpful
exception in the console.
+### Multiple Instances With ngRepeat
+
+You can use the pagination-id feature to dynamically create pagination instances, for example inside an `ng-repeat` block. Here is a bare-bones example to
+demonstrate how that would work:
+
+```JavaScript
+// in the controller
+$scope.lists = [
+ {
+ id: 'list1',
+ collection: [1, 2, 3, 4, 5]
+ },
+ {
+ id: 'list2',
+ collection: ['a', 'b', 'c', 'd', 'e']
+ }];
+```
+
+```HTML
+
+
+
+ - ID: {{ list.id }}, item: {{ item }}
+
+
+
+```
+
### Demo
-Here is a working demo featuring two instances on one page: [http://plnkr.co/edit/Pm4L53UYAieF808v8wxL?p=preview](http://plnkr.co/edit/Pm4L53UYAieF808v8wxL?p=preview)
+Here is a working demo featuring two instances on one page: [http://plnkr.co/edit/xmjmIId0c9Glh5QH97xz?p=preview](http://plnkr.co/edit/xmjmIId0c9Glh5QH97xz?p=preview)
## Working With Asynchronous Data
@@ -233,7 +376,7 @@ potential advantage of being triggered whenever the current-page changes, rather
```HTML
-
+
| {{ user.name }} |
{{ user.email }} |
@@ -248,14 +391,40 @@ potential advantage of being triggered whenever the current-page changes, rather
I've based the pagination navigation on the Bootstrap 3 component, so if you use Bootstrap in your project,
you'll get some nice styling for free. If you don't use Bootstrap, it's simple to style the links with css.
+## Frequently Asked Questions
+
+### Why does my sort / filter only affect the current page?
+This is a common problem and is usually due to the `itemsPerPage` filter not being at the end of the expression. For example, consider the following:
+
+```HTML
+...
+```
+
+In this case, the collection is first truncated to 10 items by the `itemsPerPage` filter, and then *those 10 items only* are filtered. The solution is to ensure the `itemsPerPage` filter comes after any sorting / filtering:
+
+```HTML
+...
+```
+
+### What is the `paginationService` and why is it not documented?
+
+The [`paginationService`](https://github.com/michaelbromley/angularUtils/blob/6055d260be44c0ba221a8c9bea015ac97e836a10/src/directives/pagination/dirPagination.js#L466-L521) is used internally to facilitate communication between the instances of the `dir-pagination` and `dir-pagination-controls` directives. Due to the way Angular's dependency injection system works, the service will be exposed in your app, meaning you can inject it directly into your controllers etc.
+
+However, since the `paginationService` is intended as an internal service, I cannot make any guarantees about the API, so it is dangerous to rely on using it directly in your code. Therefore I am not documenting it currently, so as not to encourage its general use. If you have a case that you feel can only be solved by direct use of this API, please open an issue and we can discuss it.
+
## Contribution
Pull requests are welcome. If you are adding a new feature or fixing an as-yet-untested use case, please consider
-writing unit tests to cover your change(s). All unit tests are contained in the `dirPagination.spec.js` file, and
+writing unit tests to cover your change(s). All unit tests are contained in the `dirPagination.spec.js` file, and
Karma is set up if you run `grunt watch` as you make changes.
At a minimum, make sure that all the tests still pass. Thanks!
+## Changelog
+
+Please see the [releases page of the package repo](https://github.com/michaelbromley/angularUtils-pagination/releases) for details
+of each released version.
+
## Credits
I did quite a bit of research before I figured I needed to make my own directive, and I picked up a lot of good ideas
@@ -269,3 +438,9 @@ from their pagination directive.
* StackOverflow: http://stackoverflow.com/questions/10816073/how-to-do-paging-in-angularjs. Picked up a lot of ideas
from the various contributors to this thread.
+
+* Massive credit is due to all the [contributors](https://github.com/michaelbromley/angularUtils/graphs/contributors) to this project - they have brought improvements that I would not have the time or insight to figure out myself.
+
+## License
+
+MIT
diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js
index 2c53b2e..d5103ff 100644
--- a/src/directives/pagination/dirPagination.js
+++ b/src/directives/pagination/dirPagination.js
@@ -20,92 +20,386 @@
* Config
*/
var moduleName = 'angularUtils.directives.dirPagination';
- var templatePath = 'directives/pagination/dirPagination.tpl.html';
+ var DEFAULT_ID = '__default';
/**
* Module
*/
- var module;
- try {
- module = angular.module(moduleName);
- } catch(err) {
- // named module does not exist, so create one
- module = angular.module(moduleName, []);
- }
+ angular.module(moduleName, [])
+ .directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective])
+ .directive('dirPaginateNoCompile', noCompileDirective)
+ .directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective])
+ .filter('itemsPerPage', ['paginationService', itemsPerPageFilter])
+ .service('paginationService', paginationService)
+ .provider('paginationTemplate', paginationTemplateProvider)
+ .run(['$templateCache',dirPaginationControlsTemplateInstaller]);
+
+ function dirPaginateDirective($compile, $parse, paginationService) {
- module.directive('dirPaginate', ['$compile', '$parse', '$timeout', 'paginationService', function($compile, $parse, $timeout, paginationService) {
return {
terminal: true,
multiElement: true,
- priority: 5000, // This setting is used in conjunction with the later call to $compile() to prevent infinite recursion of compilation
- compile: function dirPaginationCompileFn(tElement, tAttrs){
-
- // Add ng-repeat to the dom element
- if (tElement[0].hasAttribute('dir-paginate-start') || tElement[0].hasAttribute('data-dir-paginate-start')) {
- // using multiElement mode (dir-paginate-start, dir-paginate-end)
- tAttrs.$set('ngRepeatStart', tAttrs.dirPaginate);
- tElement.eq(tElement.length - 1).attr('ng-repeat-end', true);
+ priority: 100,
+ compile: dirPaginationCompileFn
+ };
+
+ function dirPaginationCompileFn(tElement, tAttrs){
+
+ var expression = tAttrs.dirPaginate;
+ // regex taken directly from https://github.com/angular/angular.js/blob/v1.4.x/src/ng/directive/ngRepeat.js#L339
+ var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
+
+ var filterPattern = /\|\s*itemsPerPage\s*:\s*(.*\(\s*\w*\)|([^\)]*?(?=\s+as\s+))|[^\)]*)/;
+ if (match[2].match(filterPattern) === null) {
+ throw 'pagination directive: the \'itemsPerPage\' filter must be set.';
+ }
+ var itemsPerPageFilterRemoved = match[2].replace(filterPattern, '');
+ var collectionGetter = $parse(itemsPerPageFilterRemoved);
+
+ addNoCompileAttributes(tElement);
+
+ // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any
+ // dir-pagination-controls directives that may be looking for this ID.
+ var rawId = tAttrs.paginationId || DEFAULT_ID;
+ paginationService.registerInstance(rawId);
+
+ return function dirPaginationLinkFn(scope, element, attrs){
+
+ // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and
+ // potentially register a new ID if it evaluates to a different value than the rawId.
+ var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID;
+
+ // (TODO: this seems sound, but I'm reverting as many bug reports followed it's introduction in 0.11.0.
+ // Needs more investigation.)
+ // In case rawId != paginationId we deregister using rawId for the sake of general cleanliness
+ // before registering using paginationId
+ // paginationService.deregisterInstance(rawId);
+ paginationService.registerInstance(paginationId);
+
+ var repeatExpression = getRepeatExpression(expression, paginationId);
+ addNgRepeatToElement(element, attrs, repeatExpression);
+
+ removeTemporaryAttributes(element);
+ var compiled = $compile(element);
+
+ var currentPageGetter = makeCurrentPageGetterFn(scope, attrs, paginationId);
+ paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope);
+
+ if (typeof attrs.totalItems !== 'undefined') {
+ paginationService.setAsyncModeTrue(paginationId);
+ scope.$watch(function() {
+ return $parse(attrs.totalItems)(scope);
+ }, function (result) {
+ if (0 <= result) {
+ paginationService.setCollectionLength(paginationId, result);
+ }
+ });
} else {
- tAttrs.$set('ngRepeat', tAttrs.dirPaginate);
+ paginationService.setAsyncModeFalse(paginationId);
+ scope.$watchCollection(function() {
+ return collectionGetter(scope);
+ }, function(collection) {
+ if (collection) {
+ var collectionLength = (collection instanceof Array) ? collection.length : Object.keys(collection).length;
+ paginationService.setCollectionLength(paginationId, collectionLength);
+ }
+ });
}
- var expression = tAttrs.dirPaginate;
- // regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211
- var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
+ // Delegate to the link function returned by the new compilation of the ng-repeat
+ compiled(scope);
+
+ // (TODO: Reverting this due to many bug reports in v 0.11.0. Needs investigation as the
+ // principle is sound)
+ // When the scope is destroyed, we make sure to remove the reference to it in paginationService
+ // so that it can be properly garbage collected
+ // scope.$on('$destroy', function destroyDirPagination() {
+ // paginationService.deregisterInstance(paginationId);
+ // });
+ };
+ }
+
+ /**
+ * If a pagination id has been specified, we need to check that it is present as the second argument passed to
+ * the itemsPerPage filter. If it is not there, we add it and return the modified expression.
+ *
+ * @param expression
+ * @param paginationId
+ * @returns {*}
+ */
+ function getRepeatExpression(expression, paginationId) {
+ var repeatExpression,
+ idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/);
+
+ if (paginationId !== DEFAULT_ID && !idDefinedInFilter) {
+ repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:\s*[^|\s]*)/, "$1 : '" + paginationId + "'");
+ } else {
+ repeatExpression = expression;
+ }
+
+ return repeatExpression;
+ }
+
+ /**
+ * Adds the ng-repeat directive to the element. In the case of multi-element (-start, -end) it adds the
+ * appropriate multi-element ng-repeat to the first and last element in the range.
+ * @param element
+ * @param attrs
+ * @param repeatExpression
+ */
+ function addNgRepeatToElement(element, attrs, repeatExpression) {
+ if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) {
+ // using multiElement mode (dir-paginate-start, dir-paginate-end)
+ attrs.$set('ngRepeatStart', repeatExpression);
+ element.eq(element.length - 1).attr('ng-repeat-end', true);
+ } else {
+ attrs.$set('ngRepeat', repeatExpression);
+ }
+ }
- var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/;
- if (match[2].match(filterPattern) === null) {
- throw 'pagination directive: the \'itemsPerPage\' filter must be set.';
+ /**
+ * Adds the dir-paginate-no-compile directive to each element in the tElement range.
+ * @param tElement
+ */
+ function addNoCompileAttributes(tElement) {
+ angular.forEach(tElement, function(el) {
+ if (el.nodeType === 1) {
+ angular.element(el).attr('dir-paginate-no-compile', true);
}
- var itemsPerPageFilterRemoved = match[2].replace(filterPattern, '');
- var collectionGetter = $parse(itemsPerPageFilterRemoved);
+ });
+ }
- return function dirPaginationLinkFn(scope, element, attrs){
- var paginationId;
- var compiled = $compile(element, false, 5000); // we manually compile the element again, as we have now added ng-repeat. Priority less than 5000 prevents infinite recursion of compiling dirPaginate
+ /**
+ * Removes the variations on dir-paginate (data-, -start, -end) and the dir-paginate-no-compile directives.
+ * @param element
+ */
+ function removeTemporaryAttributes(element) {
+ angular.forEach(element, function(el) {
+ if (el.nodeType === 1) {
+ angular.element(el).removeAttr('dir-paginate-no-compile');
+ }
+ });
+ element.eq(0).removeAttr('dir-paginate-start').removeAttr('dir-paginate').removeAttr('data-dir-paginate-start').removeAttr('data-dir-paginate');
+ element.eq(element.length - 1).removeAttr('dir-paginate-end').removeAttr('data-dir-paginate-end');
+ }
- paginationId = attrs.paginationId || '__default';
- paginationService.registerInstance(paginationId);
+ /**
+ * Creates a getter function for the current-page attribute, using the expression provided or a default value if
+ * no current-page expression was specified.
+ *
+ * @param scope
+ * @param attrs
+ * @param paginationId
+ * @returns {*}
+ */
+ function makeCurrentPageGetterFn(scope, attrs, paginationId) {
+ var currentPageGetter;
+ if (attrs.currentPage) {
+ currentPageGetter = $parse(attrs.currentPage);
+ } else {
+ // If the current-page attribute was not set, we'll make our own.
+ // Replace any non-alphanumeric characters which might confuse
+ // the $parse service and give unexpected results.
+ // See https://github.com/michaelbromley/angularUtils/issues/233
+ // Adding the '_' as a prefix resolves an issue where paginationId might be have a digit as its first char
+ // See https://github.com/michaelbromley/angularUtils/issues/400
+ var defaultCurrentPage = '_' + (paginationId + '__currentPage').replace(/\W/g, '_');
+ scope[defaultCurrentPage] = 1;
+ currentPageGetter = $parse(defaultCurrentPage);
+ }
+ return currentPageGetter;
+ }
+ }
- var currentPageGetter;
- if (attrs.currentPage) {
- currentPageGetter = $parse(attrs.currentPage);
- } else {
- // if the current-page attribute was not set, we'll make our own
- var defaultCurrentPage = paginationId + '__currentPage';
- scope[defaultCurrentPage] = 1;
- currentPageGetter = $parse(defaultCurrentPage);
- }
- paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope);
-
- if (typeof attrs.totalItems !== 'undefined') {
- paginationService.setAsyncModeTrue(paginationId);
- scope.$watch(function() {
- return $parse(attrs.totalItems)(scope);
- }, function (result) {
- if (0 <= result) {
- paginationService.setCollectionLength(paginationId, result);
- }
+ /**
+ * This is a helper directive that allows correct compilation when in multi-element mode (ie dir-paginate-start, dir-paginate-end).
+ * It is dynamically added to all elements in the dir-paginate compile function, and it prevents further compilation of
+ * any inner directives. It is then removed in the link function, and all inner directives are then manually compiled.
+ */
+ function noCompileDirective() {
+ return {
+ priority: 5000,
+ terminal: true
+ };
+ }
+
+ function dirPaginationControlsTemplateInstaller($templateCache) {
+ $templateCache.put('angularUtils.directives.dirPagination.template', '');
+ }
+
+ function dirPaginationControlsDirective(paginationService, paginationTemplate) {
+
+ var numberRegex = /^\d+$/;
+
+ var DDO = {
+ restrict: 'AE',
+ scope: {
+ maxSize: '=?',
+ onPageChange: '&?',
+ paginationId: '=?',
+ autoHide: '=?'
+ },
+ link: dirPaginationControlsLinkFn
+ };
+
+ // We need to check the paginationTemplate service to see whether a template path or
+ // string has been specified, and add the `template` or `templateUrl` property to
+ // the DDO as appropriate. The order of priority to decide which template to use is
+ // (highest priority first):
+ // 1. paginationTemplate.getString()
+ // 2. attrs.templateUrl
+ // 3. paginationTemplate.getPath()
+ var templateString = paginationTemplate.getString();
+ if (templateString !== undefined) {
+ DDO.template = templateString;
+ } else {
+ DDO.templateUrl = function(elem, attrs) {
+ return attrs.templateUrl || paginationTemplate.getPath();
+ };
+ }
+ return DDO;
+
+ function dirPaginationControlsLinkFn(scope, element, attrs) {
+
+ // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has
+ // not yet been linked (e.g. if it is inside an ng-if block), and in that case it prevents this controls directive from assuming that there is
+ // no corresponding dir-paginate directive and wrongly throwing an exception.
+ var rawId = attrs.paginationId || DEFAULT_ID;
+ var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID;
+
+ if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) {
+ var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' ';
+ if (window.console) {
+ console.warn('Pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive, which was not found at link time.');
+ }
+ }
+
+ if (!scope.maxSize) { scope.maxSize = 9; }
+ scope.autoHide = scope.autoHide === undefined ? true : scope.autoHide;
+ scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true;
+ scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false;
+
+ var paginationRange = Math.max(scope.maxSize, 5);
+ scope.pages = [];
+ scope.pagination = {
+ last: 1,
+ current: 1
+ };
+ scope.range = {
+ lower: 1,
+ upper: 1,
+ total: 1
+ };
+
+ scope.$watch('maxSize', function(val) {
+ if (val) {
+ paginationRange = Math.max(scope.maxSize, 5);
+ generatePagination();
+ }
+ });
+
+ scope.$watch(function() {
+ if (paginationService.isRegistered(paginationId)) {
+ return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId);
+ }
+ }, function(length) {
+ if (0 < length) {
+ generatePagination();
+ }
+ });
+
+ scope.$watch(function() {
+ if (paginationService.isRegistered(paginationId)) {
+ return (paginationService.getItemsPerPage(paginationId));
+ }
+ }, function(current, previous) {
+ if (current != previous && typeof previous !== 'undefined') {
+ goToPage(scope.pagination.current);
+ }
+ });
+
+ scope.$watch(function() {
+ if (paginationService.isRegistered(paginationId)) {
+ return paginationService.getCurrentPage(paginationId);
+ }
+ }, function(currentPage, previousPage) {
+ if (currentPage != previousPage) {
+ goToPage(currentPage);
+ }
+ });
+
+ scope.setCurrent = function(num) {
+ if (paginationService.isRegistered(paginationId) && isValidPageNumber(num)) {
+ num = parseInt(num, 10);
+ paginationService.setCurrentPage(paginationId, num);
+ }
+ };
+
+ /**
+ * Custom "track by" function which allows for duplicate "..." entries on long lists,
+ * yet fixes the problem of wrongly-highlighted links which happens when using
+ * "track by $index" - see https://github.com/michaelbromley/angularUtils/issues/153
+ * @param id
+ * @param index
+ * @returns {string}
+ */
+ scope.tracker = function(id, index) {
+ return id + '_' + index;
+ };
+
+ function goToPage(num) {
+ if (paginationService.isRegistered(paginationId) && isValidPageNumber(num)) {
+ var oldPageNumber = scope.pagination.current;
+
+ scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
+ scope.pagination.current = num;
+ updateRangeValues();
+
+ // if a callback has been set, then call it with the page number as the first argument
+ // and the previous page number as a second argument
+ if (scope.onPageChange) {
+ scope.onPageChange({
+ newPageNumber : num,
+ oldPageNumber : oldPageNumber
});
+ }
+ }
+ }
+
+ function generatePagination() {
+ if (paginationService.isRegistered(paginationId)) {
+ var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1;
+ scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
+ scope.pagination.current = page;
+ scope.pagination.last = scope.pages[scope.pages.length - 1];
+ if (scope.pagination.last < scope.pagination.current) {
+ scope.setCurrent(scope.pagination.last);
} else {
- scope.$watchCollection(function() {
- return collectionGetter(scope);
- }, function(collection) {
- if (collection) {
- paginationService.setCollectionLength(paginationId, collection.length);
- }
- });
+ updateRangeValues();
}
+ }
+ }
- // Delegate to the link function returned by the new compilation of the ng-repeat
- compiled(scope);
- };
+ /**
+ * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination
+ * template to display the current page range, e.g. "showing 21 - 40 of 144 results";
+ */
+ function updateRangeValues() {
+ if (paginationService.isRegistered(paginationId)) {
+ var currentPage = paginationService.getCurrentPage(paginationId),
+ itemsPerPage = paginationService.getItemsPerPage(paginationId),
+ totalItems = paginationService.getCollectionLength(paginationId);
+
+ scope.range.lower = (currentPage - 1) * itemsPerPage + 1;
+ scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems);
+ scope.range.total = totalItems;
+ }
+ }
+ function isValidPageNumber(num) {
+ return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last));
}
- };
- }]);
+ }
- module.directive('dirPaginationControls', ['paginationService', function(paginationService) {
- var numberRegex = /^\d+$/;
/**
* Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the
* links used in pagination
@@ -174,96 +468,25 @@
return i;
}
}
+ }
- return {
- restrict: 'AE',
- templateUrl: function(elem, attrs) {
- return attrs.templateUrl || templatePath;
- },
- scope: {
- maxSize: '=?',
- onPageChange: '&?'
- },
- link: function(scope, element, attrs) {
- var paginationId;
- paginationId = attrs.paginationId || '__default';
- if (!scope.maxSize) { scope.maxSize = 9; }
- scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true;
- scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false;
-
- if (!paginationService.isRegistered(paginationId)) {
- var idMessage = (paginationId !== '__default') ? ' (id: ' + paginationId + ') ' : ' ';
- throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.';
- }
-
- var paginationRange = Math.max(scope.maxSize, 5);
- scope.pages = [];
- scope.pagination = {
- last: 1,
- current: 1
- };
-
- scope.$watch(function() {
- return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId);
- }, function(length) {
- if (0 < length) {
- generatePagination();
- }
- });
-
- scope.$watch(function() {
- return paginationService.getCurrentPage(paginationId);
- }, function(currentPage, previousPage) {
- if (currentPage != previousPage) {
- goToPage(currentPage);
- }
- });
-
- scope.setCurrent = function(num) {
- if (isValidPageNumber(num)) {
- paginationService.setCurrentPage(paginationId, num);
- }
- };
-
- function goToPage(num) {
- if (isValidPageNumber(num)) {
- scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
- scope.pagination.current = num;
-
- // if a callback has been set, then call it with the page number as an argument
- if (scope.onPageChange) {
- scope.onPageChange({ newPageNumber : num });
- }
- }
- }
-
- function generatePagination() {
- scope.pages = generatePagesArray(1, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
- scope.pagination.current = parseInt(paginationService.getCurrentPage(paginationId));
- scope.pagination.last = scope.pages[scope.pages.length - 1];
- if (scope.pagination.last < scope.pagination.current) {
- scope.setCurrent(scope.pagination.last);
- }
- }
-
- function isValidPageNumber(num) {
- return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last));
- }
- }
- };
- }]);
+ /**
+ * This filter slices the collection into pages based on the current page number and number of items per page.
+ * @param paginationService
+ * @returns {Function}
+ */
+ function itemsPerPageFilter(paginationService) {
- module.filter('itemsPerPage', ['paginationService', function(paginationService) {
return function(collection, itemsPerPage, paginationId) {
if (typeof (paginationId) === 'undefined') {
- paginationId = '__default';
+ paginationId = DEFAULT_ID;
}
if (!paginationService.isRegistered(paginationId)) {
throw 'pagination directive: the itemsPerPage id argument (id: ' + paginationId + ') does not match a registered pagination-id.';
}
var end;
var start;
- if (collection instanceof Array) {
+ if (angular.isObject(collection)) {
itemsPerPage = parseInt(itemsPerPage) || 9999999999;
if (paginationService.isAsyncMode(paginationId)) {
start = 0;
@@ -273,14 +496,48 @@
end = start + itemsPerPage;
paginationService.setItemsPerPage(paginationId, itemsPerPage);
- return collection.slice(start, end);
+ if (collection instanceof Array) {
+ // the array just needs to be sliced
+ return collection.slice(start, end);
+ } else {
+ // in the case of an object, we need to get an array of keys, slice that, then map back to
+ // the original object.
+ var slicedObject = {};
+ angular.forEach(keys(collection).slice(start, end), function(key) {
+ slicedObject[key] = collection[key];
+ });
+ return slicedObject;
+ }
} else {
return collection;
}
};
- }]);
+ }
+
+ /**
+ * Shim for the Object.keys() method which does not exist in IE < 9
+ * @param obj
+ * @returns {Array}
+ */
+ function keys(obj) {
+ if (!Object.keys) {
+ var objKeys = [];
+ for (var i in obj) {
+ if (obj.hasOwnProperty(i)) {
+ objKeys.push(i);
+ }
+ }
+ return objKeys;
+ } else {
+ return Object.keys(obj);
+ }
+ }
+
+ /**
+ * This service allows the various parts of the module to communicate and stay in sync.
+ */
+ function paginationService() {
- module.service('paginationService', function() {
var instances = {};
var lastRegisteredInstance;
@@ -293,6 +550,10 @@
}
};
+ this.deregisterInstance = function(instanceId) {
+ delete instances[instanceId];
+ };
+
this.isRegistered = function(instanceId) {
return (typeof instances[instanceId] !== 'undefined');
};
@@ -309,7 +570,8 @@
instances[instanceId].currentPageParser.assign(instances[instanceId].context, val);
};
this.getCurrentPage = function(instanceId) {
- return instances[instanceId].currentPageParser(instances[instanceId].context);
+ var parser = instances[instanceId].currentPageParser;
+ return parser ? parser(instances[instanceId].context) : 1;
};
this.setItemsPerPage = function(instanceId, val) {
@@ -330,8 +592,50 @@
instances[instanceId].asyncMode = true;
};
+ this.setAsyncModeFalse = function(instanceId) {
+ instances[instanceId].asyncMode = false;
+ };
+
this.isAsyncMode = function(instanceId) {
return instances[instanceId].asyncMode;
};
- });
+ }
+
+ /**
+ * This provider allows global configuration of the template path used by the dir-pagination-controls directive.
+ */
+ function paginationTemplateProvider() {
+
+ var templatePath = 'angularUtils.directives.dirPagination.template';
+ var templateString;
+
+ /**
+ * Set a templateUrl to be used by all instances of
+ * @param {String} path
+ */
+ this.setPath = function(path) {
+ templatePath = path;
+ };
+
+ /**
+ * Set a string of HTML to be used as a template by all instances
+ * of . If both a path *and* a string have been set,
+ * the string takes precedence.
+ * @param {String} str
+ */
+ this.setString = function(str) {
+ templateString = str;
+ };
+
+ this.$get = function() {
+ return {
+ getPath: function() {
+ return templatePath;
+ },
+ getString: function() {
+ return templateString;
+ }
+ };
+ };
+ }
})();
diff --git a/src/directives/pagination/dirPagination.spec.js b/src/directives/pagination/dirPagination.spec.js
index 767d081..07a7e45 100644
--- a/src/directives/pagination/dirPagination.spec.js
+++ b/src/directives/pagination/dirPagination.spec.js
@@ -6,15 +6,25 @@ describe('dirPagination directive', function() {
var $compile;
var $scope;
+ var $timeout;
var containingElement;
var myCollection;
+ var myObjectCollection;
beforeEach(module('angularUtils.directives.dirPagination'));
beforeEach(module('templates-main'));
- beforeEach(inject(function($rootScope, _$compile_) {
+ // used to test the paginationTemplateProvider (see end of file)
+ var templateProvider;
+ angular.module('customTemplateTestApp', []);
+ beforeEach(module('customTemplateTestApp', function(paginationTemplateProvider) {
+ templateProvider = paginationTemplateProvider;
+ }));
+
+ beforeEach(inject(function($rootScope, _$compile_, _$timeout_) {
$compile = _$compile_;
+ $timeout = _$timeout_;
$scope = $rootScope.$new();
containingElement = angular.element('');
@@ -62,14 +72,6 @@ describe('dirPagination directive', function() {
expect(compile).toThrow("pagination directive: the 'itemsPerPage' filter must be set.");
});
- it('should allow a space after itemsPerPage and before the colon', function() {
- function compile() {
- var customExpression = "item in collection | itemsPerPage : 10";
- compileElement(myCollection, 5, 1, customExpression);
- }
- expect(compile).not.toThrow();
- });
-
it('should repeat the items like ng-repeat', function() {
compileElement(myCollection);
var listItems = getListItems();
@@ -122,9 +124,9 @@ describe('dirPagination directive', function() {
it('should work inside a transcluded directive (ng-if)', function() {
$scope.collection = myCollection;
var html = '';
+ ' ' +
+ '' +
+ '';
containingElement.append($compile(html)($scope));
$scope.$apply();
@@ -168,6 +170,196 @@ describe('dirPagination directive', function() {
expect(listItems).toEqual(['item 1', 'item 2']);
});
+ // see https://github.com/michaelbromley/angularUtils/issues/233
+ function testPaginationId(paginationIdString) {
+ $scope.collection = myCollection;
+ var html = ' ' +
+ '';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+
+ var listItems = getListItems();
+ expect(listItems.length).toEqual(2);
+ expect(listItems).toEqual(['item 1', 'item 2']);
+ }
+
+ it('should work when the pagination-id evaluates to a string containing a period.', function() {
+ testPaginationId('some.string');
+ });
+
+ it('should work when the pagination-id evaluates to a string containing a hyphen.', function() {
+ testPaginationId('some-string');
+ });
+
+ });
+
+ describe('paginating over an object', function() {
+ beforeEach(function() {
+ myObjectCollection = {};
+ for(var i = 1; i <= 100; i++) {
+ myObjectCollection['key_' + i] = 'item ' + i;
+ }
+ });
+
+ it('should not throw an exception when the collection is an object', function() {
+ function compile() {
+ compileElement(myObjectCollection, 10, 1, "(key, item) in collection | itemsPerPage: itemsPerPage");
+ }
+ expect(compile).not.toThrow();
+ });
+
+ it('should correctly paginate with simple syntax', function() {
+ compileElement(myObjectCollection, 5, 1, "item in collection | itemsPerPage: itemsPerPage");
+ expect(getListItems()).toEqual(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
+ });
+
+ it('should correctly paginate with (key, value) syntax', function() {
+ compileElement(myObjectCollection, 5, 1, "(key, item) in collection | itemsPerPage: itemsPerPage");
+ expect(getListItems()).toEqual(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
+ });
+
+ it('should show the correct items for the currentPage', function() {
+ compileElement(myObjectCollection, 5, 4, "(key, item) in collection | itemsPerPage: itemsPerPage");
+ expect(getListItems()).toEqual(['item 16', 'item 17', 'item 18', 'item 19', 'item 20']);
+ });
+
+ it('should display the correct pagination links', function() {
+ compileElement(myObjectCollection, 20, 1, "item in collection | itemsPerPage: itemsPerPage");
+ var paginationLinks = getPageLinksArray();
+
+ expect(paginationLinks).toEqual(['‹','1', '2', '3', '4', '5', '›']);
+ });
+
+ });
+
+ describe('valid expressions', function() {
+
+ beforeEach(function() {
+ $scope.getPageSize = function() {
+ return 10;
+ };
+ });
+
+ it('should allow a space after itemsPerPage and before the colon', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage : 10");
+ }
+ expect(compile).not.toThrow();
+ });
+
+ it('should allow parentheses around the itemsPerPage filter', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in filtered = (collection | filter: '1' | itemsPerPage: itemsPerPage)");
+ }
+
+ expect(compile).not.toThrow();
+ expect(getListItems()).toEqual(['item 1', 'item 10', 'item 11', 'item 12', 'item 13']);
+ expect($scope.filtered.length).toEqual(5);
+ });
+
+ it('should allow the itemsPerPage to be a scope method 1', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: getPageSize()");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow the itemsPerPage to be a scope method 2', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: getPageSize(myvar)");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow the itemsPerPage to be a scope method 3', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in filtered = (collection | itemsPerPage: getPageSize(_myvar))");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow alias syntax', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: 10 as myAlias");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ expect($scope.myAlias.length).toEqual(10);
+ });
+
+ it('should allow alias syntax 2', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: getPageSize() as myAlias");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ expect($scope.myAlias.length).toEqual(10);
+ });
+
+ it('should allow alias syntax 3', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in alias1 = (collection | itemsPerPage: getPageSize()) as alias2");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ expect($scope.alias1.length).toEqual(10);
+ expect($scope.alias2.length).toEqual(10);
+ });
+
+ it('should allow dot and bracket notation', function() {
+ function compile() {
+ $scope.foo = {
+ name: 'myCollection',
+ myCollection: myCollection,
+ perPage: 10
+ };
+ compileElement(myCollection, 5, 1, "item in foo[foo.name] | itemsPerPage:foo.perPage");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ // see https://github.com/michaelbromley/angularUtils/issues/241
+ // the actual issue was caused by the work "sharedTasksFilters", which was
+ // being matched by the part of the regex that looks for "as" alias syntax.
+ it('should allow deeply nested, long-winded object for itemsPerPage', function() {
+ function compile() {
+ $scope.eventsCtrl = { eventsFilters: { sharedTasksFilters: { eventsPerPage: 10 } } };
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage:eventsCtrl.eventsFilters.sharedTasksFilters.eventsPerPage");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+
+ it('should allow track by syntax 1', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: 10 track by $index");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow track by syntax 2', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: 10 track by item");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow track by with dot in itemsPerPage', function() {
+ function compile() {
+ $scope.foo = { perPage : 10 };
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: foo.perPage track by item");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
});
describe('if currentPage attribute is not set', function() {
@@ -198,14 +390,21 @@ describe('dirPagination directive', function() {
describe('pagination controls', function() {
- it('should throw an exception if the dir-paginate directive has not been set up', function() {
+ beforeEach(function(){
+ spyOn(console, 'warn');
+ });
+
+ it('should throw a warning if the dir-paginate directive has not been set up', function() {
+
function compile() {
var html = '';
containingElement.append($compile(html)($scope));
$scope.$apply();
}
- expect(compile).toThrow("pagination directive: the pagination controls cannot be used without the corresponding pagination directive.");
+ compile();
+
+ expect(console.warn).toHaveBeenCalledWith('Pagination directive: the pagination controls cannot be used without the corresponding pagination directive, which was not found at link time.');
});
it('should not display pagination if all rows fit on one page', function() {
@@ -274,6 +473,20 @@ describe('dirPagination directive', function() {
expect(pageLinks).toEqual(['‹','1', '...', '94', '95', '96', '97', '98', '99', '100', '›']);
});
+ it('should show the correct pagination links after item removed from cllection', function() {
+ compileElement(myCollection, 1);
+ $scope.$apply(function() {
+ $scope.currentPage = 98;
+ });
+
+ $scope.$apply(function() {
+ $scope.collection.pop();
+ });
+ var pageLinks = getPageLinksArray();
+
+ expect(pageLinks).toEqual(['‹','1', '...', '93', '94', '95', '96', '97', '98', '99', '›']);
+ });
+
it('should calculate pages based off collection after all filters are applied', function() {
$scope.filterBy = '2';
var customExpression = "item in collection | filter: filterBy | itemsPerPage: itemsPerPage";
@@ -297,12 +510,39 @@ describe('dirPagination directive', function() {
expect(activeLink.html()).toContain(1);
});
+ /**
+ * Issue raised here https://github.com/michaelbromley/angularUtils/issues/78
+ * Where the dir-paginate directive is inside an ngSwitch block (which is initially hidden), so the linking function is not immediately executed.
+ * The dir-pagination-controls directive is *outside* the switch block, so it gets both compiled *and* linked on page load.
+ */
+ it('should allow paginate directive to be defined in a deferred-linking situation without error', function() {
+ function compile() {
+ var html;
+ $scope.collection = myCollection;
+ $scope.showList = false;
+ html = '' +
+ '';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+ }
+
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(0);
+
+ $scope.$apply(function() {
+ $scope.showList = true;
+ });
+ expect(getListItems().length).toEqual(10);
+ });
+
describe('optional attributes', function() {
function compileWithAttributes(attributes) {
$scope.collection = myCollection;
$scope.currentPage = 1;
- html = ' ' +
+ var html = ' ' +
'';
containingElement.append($compile(html)($scope));
$scope.$apply();
@@ -317,6 +557,23 @@ describe('dirPagination directive', function() {
expect(pageLinks).toEqual(['‹','1', '2', '3', '...', '10', '›']);
});
+ it('should alter links array when value of max-size changes', function() {
+ $scope.maxSize = 5;
+ compileWithAttributes(' max-size="maxSize" ');
+
+ var pageLinks = getPageLinksArray();
+
+ expect(pageLinks).toEqual(['‹','1', '2', '3', '...', '10', '›']);
+
+ $scope.$apply(function() {
+ $scope.maxSize = 9;
+ });
+
+ pageLinks = getPageLinksArray();
+
+ expect(pageLinks).toEqual(['‹','1', '2', '3', '4', '5', '6', '7', '...', '10', '›']);
+ });
+
it('should impose a minimum max-size of 5', function() {
compileWithAttributes(' max-size="2" ');
@@ -363,11 +620,37 @@ describe('dirPagination directive', function() {
$scope.myCallback = function(currentPage) {
return "The current page is " + currentPage;
};
- spyOn($scope, 'myCallback').and.callThrough();
- compileWithAttributes(' on-page-change="myCallback(newPageNumber)" ');
+ spyOn($scope, 'myCallback');
});
it('should call the callback once when page link clicked', function() {
+ compileWithAttributes(' on-page-change="myCallback(newPageNumber)" ');
+ var pagination = containingElement.find('ul.pagination');
+
+ expect($scope.myCallback.calls.count()).toEqual(0);
+ pagination.children().eq(2).find('a').triggerHandler('click');
+ $scope.$apply();
+ expect($scope.myCallback).toHaveBeenCalled();
+ expect($scope.myCallback.calls.count()).toEqual(1);
+ });
+
+ it('should not call the callback on loading first page, even with controls appearing above the pagination', function() {
+ function compileWithControlsFirst(attributes) {
+ $scope.currentPage = 1;
+ var html = '' +
+ '';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+ }
+
+ compileWithControlsFirst(' on-page-change="myCallback(newPageNumber)" ');
+
+ $scope.$apply(function() {
+ $scope.collection = myCollection;
+ });
+
var pagination = containingElement.find('ul.pagination');
expect($scope.myCallback.calls.count()).toEqual(0);
@@ -378,12 +661,22 @@ describe('dirPagination directive', function() {
});
it('should pass the current page number to the callback', function() {
+ compileWithAttributes(' on-page-change="myCallback(newPageNumber)" ');
var pagination = containingElement.find('ul.pagination');
pagination.children().eq(2).find('a').triggerHandler('click');
$scope.$apply();
expect($scope.myCallback).toHaveBeenCalledWith(2);
});
+
+ it('should pass the previous page number to the callback', function() {
+ compileWithAttributes(' on-page-change="myCallback(oldPageNumber)" ');
+ var pagination = containingElement.find('ul.pagination');
+
+ pagination.children().eq(3).find('a').triggerHandler('click');
+ $scope.$apply();
+ expect($scope.myCallback).toHaveBeenCalledWith(1);
+ });
});
describe('total-items attribute', function() {
@@ -408,6 +701,56 @@ describe('dirPagination directive', function() {
expect(listItems.length).toEqual(100);
});
});
+
+ describe('auto-hide attribute', function () {
+ function compileWithAttributesAndItemsPerPage(attributes, itemsPerPage) {
+ $scope.collection = myCollection;
+ $scope.currentPage = 1;
+ var html = ' ' +
+ '';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+ }
+
+ it('when not set, should not generate pagination controls, with not enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('', 100);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(0);
+ });
+
+ it('when not set, should generate pagination controls, with enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('', 99);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(1);
+ });
+
+ it('when set to false, should generate pagination controls, with not enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('auto-hide="false"', 100);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(1);
+ var pageLinks = containingElement.find('ul.pagination li.disabled');
+ expect(pageLinks.length).toEqual(3);
+ });
+
+ it('when set to false, should generate pagination controls, with enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('auto-hide="false"', 99);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(1);
+ });
+
+ it('when set to true, should generate pagination controls, with enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('auto-hide="true"', 100);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(0);
+ });
+
+ it('when set to true, should generate pagination controls, with not enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('auto-hide="true"', 99);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(1);
+ });
+ });
});
});
@@ -423,6 +766,7 @@ describe('dirPagination directive', function() {
collection1.push('c1:' + i);
collection2.push('c2:' + i);
}
+ spyOn(console, 'warn');
});
/**
@@ -448,7 +792,7 @@ describe('dirPagination directive', function() {
$scope.collection[paginationId] = collection;
$scope.itemsPerPage[paginationId] = itemsPerPage;
$scope.currentPage[paginationId] = currentPage || 1;
- expression = customExpression || "item in collection." + paginationId + " | itemsPerPage: itemsPerPage." + paginationId + ": '" + paginationId + "'";
+ expression = customExpression || "item in collection." + paginationId + " | itemsPerPage: itemsPerPage." + paginationId;
html = ' ' +
'';
containingElement.append($compile(html)($scope));
@@ -534,7 +878,7 @@ describe('dirPagination directive', function() {
expect(getMultiListItems("c2")).toEqual(['c2:6', 'c2:7']);
});
- it('should throw an exception if a non-existant paginationId is set in the pagination-controls', function() {
+ it('should print a warning if a non-existant paginationId is set in the pagination-controls', function() {
$scope.collection = [1,2,3,4,5];
function compile() {
@@ -544,8 +888,8 @@ describe('dirPagination directive', function() {
containingElement.append($compile(html)($scope));
$scope.$apply();
}
-
- expect(compile).toThrow("pagination directive: the pagination controls (id: id2) cannot be used without the corresponding pagination directive.");
+ compile();
+ expect(console.warn).toHaveBeenCalledWith('Pagination directive: the pagination controls (id: id2) cannot be used without the corresponding pagination directive, which was not found at link time.');
});
it('should throw an exception if a non-existant paginationId is set in the itemsPerPage filter', function() {
@@ -562,6 +906,110 @@ describe('dirPagination directive', function() {
expect(compile).toThrow("pagination directive: the itemsPerPage id argument (id: id2) does not match a registered pagination-id.");
});
+ describe('valid expressions with pagination id', function() {
+
+ it('should allow track by syntax', function() {
+ function compile() {
+ compileMultipleInstance(collection1, 10, 1, "c1", "item in collection.c1 | itemsPerPage: itemsPerPage.c1 track by $index");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow track by with other filter syntax', function() {
+ function compile() {
+ compileMultipleInstance(collection1, 10, 1, "c1", "item in collection.c1 | orderBy: reverse | itemsPerPage: itemsPerPage.c1 track by $index");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+ });
+
+ describe('dymanic pagination-id', function() {
+
+ function compileWithDynamicId(paginationId, customExpression) {
+ var html;
+ html = ' ' +
+ '';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+ }
+
+ it('should allow object reference', function() {
+ function compile() {
+ $scope.myId = {
+ foo: 'foo'
+ };
+ $scope.collection = myCollection;
+ compileWithDynamicId('myId.foo', 'item in collection | itemsPerPage: 10');
+ }
+
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+ });
+
+ });
+
+ describe('pagination controls template API', function() {
+ function compile() {
+ var html = ' ' +
+ '';
+ $scope.collection = [1,2,3,4,5,6,7];
+ $scope.currentPage = 1;
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+ }
+
+ it('should provide correct values for current page', function() {
+ compile();
+
+ expect(containingElement.find('#tt-pagination-current').html()).toEqual('1');
+ $scope.$apply(function() {
+ $scope.currentPage = 2;
+ });
+ expect(containingElement.find('#tt-pagination-current').html()).toEqual('2');
+ });
+
+ it('should provide correct value for last page', function() {
+ compile();
+ expect(containingElement.find('#tt-pagination-last').html()).toEqual('3');
+ });
+
+ it('should provide correct value for range.lower', function() {
+ compile();
+ expect(containingElement.find('#tt-range-lower').html()).toEqual('1');
+ $scope.$apply(function() {
+ $scope.currentPage = 2;
+ });
+ expect(containingElement.find('#tt-range-lower').html()).toEqual('4');
+ $scope.$apply(function() {
+ $scope.currentPage = 3;
+ });
+ expect(containingElement.find('#tt-range-lower').html()).toEqual('7');
+ });
+
+ it('should provide correct value for range.upper', function() {
+ compile();
+ expect(containingElement.find('#tt-range-upper').html()).toEqual('3');
+ $scope.$apply(function() {
+ $scope.currentPage = 2;
+ });
+ expect(containingElement.find('#tt-range-upper').html()).toEqual('6');
+ $scope.$apply(function() {
+ $scope.currentPage = 3;
+ });
+ expect(containingElement.find('#tt-range-upper').html()).toEqual('7');
+ });
+
+ it('should provide correct value for range.total', function() {
+ compile();
+ expect(containingElement.find('#tt-range-total').html()).toEqual('7');
+ $scope.$apply(function() {
+ $scope.currentPage = 2;
+ });
+ expect(containingElement.find('#tt-range-total').html()).toEqual('7');
+ });
});
describe('multi element functionality', function() {
@@ -572,10 +1020,10 @@ describe('dirPagination directive', function() {
$scope.itemsPerPage = itemsPerPage || 10;
$scope.currentPage = currentPage || 1;
html = '' +
- '
header
' +
- '
{{ item }}
' +
- '
footer
' +
- '
';
+ 'header
' +
+ '{{ item }}
' +
+ 'footer
' +
+ ' ';
containingElement.append($compile(html)($scope));
$scope.$apply();
}
@@ -619,5 +1067,133 @@ describe('dirPagination directive', function() {
expect(containingElement.find('h1').length).toEqual(3);
expect(containingElement.find('p').length).toEqual(3);
});
+
+ /**
+ * See https://github.com/michaelbromley/angularUtils/issues/92
+ */
+ it('should correctly compile an inner ng-repeat', function() {
+ function compile() {
+ var html = '' +
+ '
{{ item }}
' +
+ '
yo
- {{ option }} : {{ item }}
' +
+ '
';
+ $scope.options = ['option1', 'option2', 'option3'];
+ $scope.collection = myCollection;
+ $scope.currentPage = 1;
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+ }
+
+ compile();
+
+ var options = containingElement.find('.options').eq(0).find('li');
+ expect(options.length).toEqual(3);
+ expect(options.eq(0).text()).toEqual('option1 : item 1');
+ });
+ });
+
+ /**
+ * THis suite tests out the ability to handle dynamically-generated pagination ids. The main use case is when
+ * there are multiple dir-pagination instances being generated by an ng-repeat, and the pagination id is
+ * only known at run-time.
+ */
+ describe('dynamic pagination ids', function() {
+ function compile() {
+ var html = '';
+
+ $scope.lists = [
+ {
+ id: 'list1',
+ collection: [1, 2, 3, 4, 5]
+ },
+ {
+ id: 'list2',
+ collection: ['a', 'b', 'c', 'd', 'e']
+ }
+ ];
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+ }
+
+ function getListItems($list) {
+ return $list.find('li').map(function() {
+ return $(this).text().trim();
+ }).get();
+ }
+
+ it('should not throw an exception', function() {
+ expect(compile).not.toThrow();
+ });
+
+ it('should allow independent pagination', function() {
+ compile();
+
+ var $list1 = containingElement.find('ul.list').eq(0);
+ var $list2 = containingElement.find('ul.list').eq(1);
+
+ expect(getListItems($list1)).toEqual([ '1', '2', '3' ]);
+ expect(getListItems($list2)).toEqual([ 'a', 'b', 'c' ]);
+
+ // click the "page 2" link on the first set of pagination
+ var pagination1 = $list1.parent().find('ul.pagination');
+ pagination1.children().eq(2).find('a').triggerHandler('click');
+ $scope.$apply();
+
+ // ensure only the first set of pagination changes
+ expect(getListItems($list1)).toEqual([ '4', '5' ]);
+ expect(getListItems($list2)).toEqual([ 'a', 'b', 'c' ]);
+ });
+
+ });
+
+ describe('paginationTemplateProvider', function() {
+
+ beforeEach(inject(function($templateCache) {
+ $templateCache.put('setPath_template', 'Test Template{{ pages.length }}
');
+ $templateCache.put('templateUrl_template', 'Test TemplateUrl Template{{ pages.length }}
');
+ }));
+
+ it('should use the custom template specified by setPath()', function() {
+ templateProvider.setPath('setPath_template');
+ compileElement(myCollection, 10);
+
+ expect(containingElement.find('.set-path-template').html()).toContain('Test Template');
+ });
+
+ it('should use the custom template specified by setString()', function() {
+ templateProvider.setString('Test Template String{{ pages.length }}
');
+ compileElement(myCollection, 10);
+
+ expect(containingElement.find('.set-string-template').html()).toContain('Test Template String');
+ });
+
+ it('should prioritize setString() if both path and string have been set', function() {
+ templateProvider.setString('Test Template String{{ pages.length }}
');
+ templateProvider.setPath('setPath_template');
+ compileElement(myCollection, 10);
+
+ expect(containingElement.find('.set-path-template').html()).toBeUndefined();
+ expect(containingElement.find('.set-string-template').html()).toContain('Test Template String');
+ });
+
+ it('should prioritize setString() over path and template-url attribute.', function() {
+ templateProvider.setString('Test Template String{{ pages.length }}
');
+ templateProvider.setPath('setPath_template');
+
+ var html = ' ' +
+ '';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+
+ expect(containingElement.find('.set-path-template').html()).toBeUndefined();
+ expect(containingElement.find('.template-url-template').html()).toBeUndefined();
+ expect(containingElement.find('.set-string-template').html()).toContain('Test Template String');
+ });
+
});
+
});
diff --git a/src/directives/pagination/dirPagination.tpl.html b/src/directives/pagination/dirPagination.tpl.html
index 558aa20..db98d4c 100644
--- a/src/directives/pagination/dirPagination.tpl.html
+++ b/src/directives/pagination/dirPagination.tpl.html
@@ -1,11 +1,11 @@
-