Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Refactored ng:if, better refresh handling in selectmenu, refactoring …

…paging in lists
  • Loading branch information...
commit 9df6d2f9cf9b149eb9add127e632a9254513ea75 1 parent 40d24ba
Tobias Bosch authored
View
9 README.md
@@ -135,6 +135,11 @@ Parameters (see $.mobile.changePage)
Usage: E.g. `$activePage('page2')`
+### Function angular.Object.iff / $iff
+Every expression can now use the function `$iff` as a ternary operator:
+`$iff(test, trueCase, falseCase)` will return the `trueCase` if the `test` is truthy and the `falseCase` otherwise.
+
+
### Paging for lists
Lists can be paged in the sense that more entries can be additionally loaded. By "loading" we mean the
display of a sublist of a list that is already fully loaded in JavaScript. This is useful, as the main performance
@@ -142,7 +147,7 @@ problems result from DOM operations, which can be reduced with this paging mecha
To implement this paging mechaism, we extend the angular array type with the folling functions:
-- `angular.Array.loadedPages(array)`: Returns the subarray of the given array with the loaded pages.
+- `angular.Array.paged(array)`: Returns the subarray of the given array with the loaded pages.
- `angular.Array.hasMorePages(array)`: Returns a boolean indicating if there are more pages that can be loaded.
- `angular.Array.loadNextPage(array)`: Loads the next page from the given array.
@@ -155,7 +160,7 @@ The following example shows an example for a paged list for the data in the vari
<ul data-role="listview">
- <li ng:repeat="item in myList">
+ <li ng:repeat="item in myList.$paged()">
{{item}}
</li>
<li ng:if="mylist.$hasMorePages()">
View
190 src/jquery-mobile-angular-adapter.js
@@ -23,6 +23,8 @@
*/
+
+
/**
* Global scope
*/
@@ -353,9 +355,30 @@
pageElements.detach();
var instance = element.data().selectmenu;
var oldOpen = instance.open;
+ var oldRefresh = instance.refresh;
+ instance.refresh = function() {
+ var page = element.closest('.ui-page');
+ if (page.length>0) {
+ var needsAttach = pageElements.parent().length==0;
+ if (needsAttach) {
+ page.append(pageElements);
+ }
+ try {
+ return oldRefresh.apply(this, arguments);
+ } finally {
+ if (needsAttach) {
+ pageElements.detach();
+ }
+ }
+ }
+
+ };
instance.open = function() {
var page = element.closest('.ui-page');
page.append(pageElements);
+ // always refresh the menu when opening.
+ // By this we do not have to watch for changes to the options.
+ oldRefresh.call(instance, true);
return oldOpen.apply(this, arguments);
};
var oldClose = instance.close;
@@ -364,42 +387,21 @@
pageElements.detach();
return res;
};
- var oldRefresh = instance.refresh;
- instance.refresh = function() {
- var page = element.closest('.ui-page');
- page.append(pageElements);
- try {
- return oldRefresh.apply(this, arguments);
- } finally {
- pageElements.detach();
- }
-
- };
});
scope.$watch(name, function(value) {
- var page = element.closest('.ui-page');
- if (page.length > 0) {
- element.selectmenu('refresh', true);
- }
+ element.selectmenu('refresh', true);
});
- // Watch the options elements. If they change, refresh the component.
- var oldIds;
- scope.$onEval(99999, function() {
- var page = element.closest('.ui-page');
- if (page.length == 0) {
- return;
- }
- var options = element.children('option');
- var newIds = '';
- for (var i=0; i<options.length; i++) {
- var opt = $(options[i]);
- newIds += ':'+opt.prop('value');
- }
- if (oldIds!=newIds) {
- oldIds = newIds;
- element.selectmenu('refresh', true);
+ // update the value when the number of options change.
+ // needed if the default values changes.
+ var oldCount;
+ scope.$onEval(999999, function() {
+ var newCount = element[0].childNodes.length;
+ if (oldCount!==newCount) {
+ oldCount = newCount;
+ element.trigger('change');
}
});
+
return res;
}
});
@@ -566,6 +568,10 @@
var oldSize;
scope.$onEval(function() {
var collection = scope.$tryEval(rhs, element);
+ // for the listview widget, we only need to
+ // check for changes to the list size, as ng:repeat
+ // reuses the same dom element for an index position,
+ // even if the object at that index changes.
var size = angular.Object.size(collection);
if (size != oldSize) {
oldSize = size;
@@ -661,63 +667,21 @@
/*
* Defines the ng:if tag. This is useful if jquery mobile does not allow
* an ng:switch element in the dom, e.g. between ul and li.
+ * Uses ng:repeat and angular.Object.iff under the hood.
*/
(function(angular) {
- angular.widget('@ng:if', function(expression, element) {
- var isListView = false;
- if (element.attr('jqmwidget') == 'listviewchild') {
- isListView = true;
+ angular.Object.iff = function(self, test, trueCase, falseCase) {
+ if (test) {
+ return trueCase;
+ } else {
+ return falseCase;
}
+ }
+ angular.widget('@ng:if', function(expression, element) {
+ var newExpr = 'ngif in $iff(' + expression + ",[1],[])";
element.removeAttr('ng:if');
- element.replaceWith(angular.element('<!-- ng:if: ' + expression + ' --!>'));
- var linker = this.compile(element);
- // See ng:repeat: For options we cannot use
- // fragments, as some parts of angular rely on the fact
- // that options always have a parent.
- var useFragment = (element[0].nodeName != 'OPTION');
- return function(element) {
- var child = null, currentScope = this;
- this.$onEval(function() {
- var result = this.$tryEval(expression, element);
- var changed = false;
- if (result) {
- if (!child) {
- changed = true;
- // The element should be added to the dom
- // create the element
- var fragment = useFragment?document.createDocumentFragment():null;
- // Attention: As we do not create a new scope
- // the linker changes the $element of the scope,
- // so we save it and restore it later.
- var oldScopeElement = currentScope.$element;
- linker(currentScope, function(clone) {
- child = clone;
- if (fragment) {
- fragment.appendChild(clone[0]);
- } else {
- element.after(clone);
- }
- });
- currentScope.$element = oldScopeElement;
- if (fragment) {
- element.after(angular.element(fragment));
- }
- }
- } else if (child) {
- changed = true;
- // remove the element from the dom
- child.remove();
- child = null;
- }
- if (changed && isListView) {
- // If the ng:if is embedded in a listview,
- // refresh the listview if the element changes!
- var list = element.parent();
- list.listview('refresh');
- }
- }, element);
- };
+ return angular.widget('@ng:repeat').call(this, newExpr, element);
});
})(angular);
@@ -759,10 +723,11 @@
angular.service("$browser", function() {
var res = oldBrowser.apply(this, arguments);
res.onHashChange = function(handler) {
- $(window).bind( 'hashchange', handler );
+ $(window).bind('hashchange', handler);
return handler;
};
- res.setUrl = function() {};
+ res.setUrl = function() {
+ };
return res;
}, {$inject:['$log']});
})(angular);
@@ -773,7 +738,7 @@
* includes taps, mousedowns, ...
*/
angular.directive("ngm:click", function(expression, element) {
- return angular.directive('ng:event')('vclick:'+expression, element);
+ return angular.directive('ng:event')('vclick:' + expression, element);
});
})(angular);
@@ -784,7 +749,7 @@
var pattern = /(.*?):(.*)/;
var match = pattern.exec(expression);
if (!match) {
- throw "Expression "+expression+" needs to have the syntax <event>:<handler>";
+ throw "Expression " + expression + " needs to have the syntax <event>:<handler>";
}
var event = match[1];
var handler = match[2];
@@ -808,8 +773,8 @@
var linkFn = function($updateView, element) {
var self = this;
element.bind('keypress', function(e) {
- var key=e.keyCode || e.which;
- if (key==13){
+ var key = e.keyCode || e.which;
+ if (key == 13) {
var res = self.$tryEval(expression, element);
$updateView();
}
@@ -823,17 +788,19 @@
/**
* Paging Support for lists:
* Usage:
- <li ng:repeat="l in list.$loadedPages()">{{l}}</li>
- <li ng:if="list.$hasMorePages()">
- <a href="#" ngm:click="list.$loadNextPage()">Load more</a>
- </li>
+ <li ng:repeat="l in list.$loadedPages()">{{l}}</li>
+ <li ng:if="list.$hasMorePages()">
+ <a href="#" ngm:click="list.$loadNextPage()">Load more</a>
+ </li>
*/
(function(angular) {
/**
* The default page size for all lists.
* Can be overwritten using array.pageSize.
*/
- $.mobile.defaultListPageSize = 10;
+ if (!$.mobile.defaultListPageSize) {
+ $.mobile.defaultListPageSize = 10;
+ }
function getPageSize(list) {
if (list.pageSize) {
@@ -843,10 +810,21 @@
}
function getLoadedEntryCount(list) {
+ var res;
if (!list.loadedCount) {
- return getPageSize(list);
+ res = getPageSize(list);
} else {
- return list.loadedCount;
+ res = list.loadedCount;
+ }
+ return Math.min(list.length, res);
+ }
+
+ function shrinkLoadedEntryCountIfNeeded(list) {
+ if (list.loadedCount && list.loadedCount > list.length) {
+ list.loadedCount = list.length;
+ if (list.loadedCount < getPageSize(list)) {
+ delete list.loadedCount;
+ }
}
}
@@ -858,10 +836,24 @@
}
/**
- * Returns the already loaded pages
+ * Returns the already loaded pages.
+ * Also includes filtering (second argument) and ordering (third argument),
+ * as the standard angular way does not work with paging.
*/
- angular.Array.loadedPages = function(list) {
- return list.slice(0, getLoadedEntryCount(list));
+ angular.Array.paged = function(list, filterExpr, orderExpr) {
+ var origList = list;
+ if (filterExpr) {
+ list = angular.Array.filter(list, filterExpr);
+ }
+ if (orderExpr) {
+ list = angular.Array.orderBy(list, orderExpr);
+ }
+ shrinkLoadedEntryCountIfNeeded(origList);
+
+ // Use the loaded count of the unchanged list.
+ // This is the only place where we can store
+ // the paging state!
+ return list.slice(0, getLoadedEntryCount(origList));
}
/**
View
2  test/UiSpecRunner.html
@@ -84,10 +84,10 @@
<script src="ui/activatePassivateSpec.js"></script>
<script src="ui/radioSpec.js"></script>
<script src="ui/checkBoxSpec.js"></script>
- <script src="ui/selectmenuSpec.js"></script>
<script src="ui/inputButtonSpec.js"></script>
<script src="ui/selectmenuSpec.js"></script>
<script src="ui/ngEventSpec.js"></script>
+ <script src="ui/selectmenuSpec.js"></script>
-->
View
38 test/ui/selectmenuSpec.js
@@ -154,8 +154,7 @@ describe("selectmenu", function() {
expect(content.children('div').length).toEqual(1);
});
});
-
- it('should refresh when the options change via ng:if', function() {
+ it('should refresh the default value when the number of options changes', function() {
loadHtml('/jqmng/test/ui/test-fixture.html', function(frame) {
var page = frame.$('#start');
// Note: Be sure to use ng:repeat, as this is the most problematic case!
@@ -167,51 +166,34 @@ describe("selectmenu", function() {
runs(function() {
var page = testframe().$("#start");
var select = page.find("#mysel");
- var refreshSpy = spyOn(select.data().selectmenu, 'refresh').andCallThrough();
expect(select.children('option').length).toEqual(0);
var scope = select.scope();
- scope.option1 = true;
- scope.option2 = true;
- expect(refreshSpy.callCount).toEqual(0);
- scope.$eval();
- expect(refreshSpy.callCount).toEqual(1);
- expect(select.children('option').length).toEqual(2);
- scope.$eval();
- expect(refreshSpy.callCount).toEqual(1);
scope.option1 = false;
+ scope.option2 = true;
+ expect(scope.mysel).toBeFalsy();
scope.$eval();
- expect(refreshSpy.callCount).toEqual(2);
- expect(select.children('option').length).toEqual(1);
+ expect(scope.mysel).toEqual('v2');
});
});
- it('should refresh when the options change via ng:repeat', function() {
+ it('should refresh when the dialog opens', function() {
loadHtml('/jqmng/test/ui/test-fixture.html', function(frame) {
var page = frame.$('#start');
// Note: Be sure to use ng:repeat, as this is the most problematic case!
page.append(
'<div data-role="content">' +
- '<select ng:repeat="item in [1]" name="mysel" id="mysel" data-native-menu="false"><option ng:repeat="o in opts" value="o">{{o}}</option></select>' +
+ '<select ng:repeat="item in [1]" name="mysel" id="mysel" data-native-menu="false"><option value="v1" ng:if="option1" default="true">v1</option><option value="v2" ng:if="option2">v2</option></select>' +
'</div>');
});
runs(function() {
var page = testframe().$("#start");
var select = page.find("#mysel");
- var refreshSpy = spyOn(select.data().selectmenu, 'refresh').andCallThrough();
- expect(select.children('option').length).toEqual(0);
var scope = select.scope();
- scope.opts = ['v1', 'v2'];
- expect(refreshSpy.callCount).toEqual(0);
- scope.$eval();
- expect(refreshSpy.callCount).toEqual(1);
- expect(select.children('option').length).toEqual(2);
- scope.$eval();
- expect(refreshSpy.callCount).toEqual(1);
- scope.opts = ['v2'];
+ scope.option1 = false;
+ scope.option2 = true;
scope.$eval();
- expect(refreshSpy.callCount).toEqual(2);
- expect(select.children('option').length).toEqual(1);
+ select.selectmenu('open');
+ expect(page.find(".ui-selectmenu li").length).toEqual(1);
});
});
-
});
View
61 test/unit/arrayPagingSpec.js
@@ -4,7 +4,7 @@ describe("arrayPaging", function() {
for (var i=0; i<$.mobile.defaultListPageSize; i++) {
l.push(i);
}
- var pagedList = angular.Array.loadedPages(l);
+ var pagedList = angular.Array.paged(l);
expect(pagedList.length).toEqual($.mobile.defaultListPageSize);
expect(pagedList).toEqual(l.slice(0, $.mobile.defaultListPageSize));
});
@@ -12,7 +12,7 @@ describe("arrayPaging", function() {
it('should use the given page size if defined', function() {
var l = [1,2,3,4];
l.pageSize = 2;
- var pagedList = angular.Array.loadedPages(l);
+ var pagedList = angular.Array.paged(l);
expect(pagedList.length).toEqual(l.pageSize);
expect(pagedList).toEqual(l.slice(0, l.pageSize));
});
@@ -20,7 +20,7 @@ describe("arrayPaging", function() {
it('should show the first page by default', function() {
var l = [1,2,3,4];
l.pageSize = 2;
- var pagedList = angular.Array.loadedPages(l);
+ var pagedList = angular.Array.paged(l);
expect(pagedList).toEqual(l.slice(0, l.pageSize));
});
@@ -28,27 +28,27 @@ describe("arrayPaging", function() {
var l = [1,2,3,4,5];
l.pageSize = 2;
angular.Array.loadNextPage(l);
- var pagedList = angular.Array.loadedPages(l);
+ var pagedList = angular.Array.paged(l);
expect(pagedList).toEqual(l.slice(0, 4));
});
it('should load an incomplete last page', function() {
var l = [1,2,3];
l.pageSize = 2;
- var pagedList = angular.Array.loadedPages(l);
+ var pagedList = angular.Array.paged(l);
expect(pagedList).toEqual(l.slice(0, 2));
angular.Array.loadNextPage(l);
- var pagedList = angular.Array.loadedPages(l);
+ var pagedList = angular.Array.paged(l);
expect(pagedList).toEqual(l.slice(0, 3));
});
it('should load a complete last page', function() {
var l = [1,2,3,4];
l.pageSize = 2;
- var pagedList = angular.Array.loadedPages(l);
+ var pagedList = angular.Array.paged(l);
expect(pagedList).toEqual(l.slice(0, 2));
angular.Array.loadNextPage(l);
- var pagedList = angular.Array.loadedPages(l);
+ var pagedList = angular.Array.paged(l);
expect(pagedList).toEqual(l.slice(0, 4));
});
@@ -56,7 +56,7 @@ describe("arrayPaging", function() {
var l = [1,2];
l.pageSize = 2;
angular.Array.loadNextPage(l);
- var pagedList = angular.Array.loadedPages(l);
+ var pagedList = angular.Array.paged(l);
expect(pagedList).toEqual(l.slice(0, 2));
});
@@ -77,5 +77,48 @@ describe("arrayPaging", function() {
l.pageSize = 2;
expect(angular.Array.hasMorePages(l)).toBeTruthy();
});
+
+ it('should reduce the entry count permanently when the page shrinks', function() {
+ var l = [1,2,3];
+ l.pageSize = 2;
+ angular.Array.loadNextPage(l);
+ expect(angular.Array.paged(l)).toEqual([1,2,3]);
+ l.pop();
+ expect(angular.Array.paged(l)).toEqual([1,2]);
+ l.push(2);
+ expect(angular.Array.paged(l)).toEqual([1,2]);
+ });
+
+ it('should reduce the entry count permanently to the page size when the page shrinks lower than the page size', function() {
+ var l = [1,2,3];
+ l.pageSize = 2;
+ angular.Array.loadNextPage(l);
+ l.splice(0, l.length);
+ expect(angular.Array.paged(l)).toEqual([]);
+ l.push(1);
+ expect(angular.Array.paged(l)).toEqual([1]);
+ l.push(2);
+ expect(angular.Array.paged(l)).toEqual([1,2]);
+ l.push(3);
+ expect(angular.Array.paged(l)).toEqual([1,2]);
+ });
+
+ it('should page and filter with the second argument', function() {
+ var l = [1,2,2,2,2];
+ l.pageSize = 2;
+ expect(angular.Array.paged(l, '2')).toEqual([2,2]);
+ angular.Array.loadNextPage(l);
+ expect(angular.Array.paged(l, '2')).toEqual([2,2,2,2]);
+ });
+
+ it('should page and sort with the second and third argument', function() {
+ var l = [1,2,3,4];
+ l.pageSize = 2;
+ expect(angular.Array.paged(l, function() { return true }, function(v) { return -v })).toEqual([4,3]);
+ angular.Array.loadNextPage(l);
+ console.log(l.loadedCount);
+ expect(angular.Array.paged(l, function() { return true }, function(v) { return -v })).toEqual([4,3,2,1]);
+ });
+
});
View
6 test/unit/ifSpec.js
@@ -22,10 +22,10 @@ describe("ng:if", function() {
expect(element.children('span').length).toEqual(0);
});
- it('should use the same scope', function() {
+ it('should use an own scope', function() {
compile('<div><span ng:if="true"><span ng:init="test = true"></span></span></div>');
- expect(scope.test).toBeTruthy();
- expect(scope.$element).toEqual(element);
+ expect(scope.test).toBeFalsy();
+ expect(element.children('span').scope().test).toBeTruthy();
});
it('should work with select options', function() {
View
10 test/unit/iffSpec.js
@@ -0,0 +1,10 @@
+describe("iff", function() {
+ it('should return the second argument if the first is truthy', function() {
+ expect(angular.Object.iff(null, true, 1, 2)).toEqual(1);
+ });
+
+ it('should return the third argument if the first is falsy', function() {
+ expect(angular.Object.iff(null, false, 1, 2)).toEqual(2);
+ });
+});
+
Please sign in to comment.
Something went wrong with that request. Please try again.