diff --git a/src/main/webapp/integration/angularNgModel.js b/src/main/webapp/integration/angularNgModel.js deleted file mode 100644 index eae13b3..0000000 --- a/src/main/webapp/integration/angularNgModel.js +++ /dev/null @@ -1,26 +0,0 @@ -(function ($, angular) { - var mod = angular.module('ng'); - /* - * Angular does not use $.prop or $.attr for setting the attributes "selected" (radio/check boxes) and "checked" - * (select boxes). By this, we need to trigger the requestrefresh event ourselves. - * TODO use iAttrs.$observe for selected and checked and trigger a refresh on the widgets. - */ - mod.directive("ngModel", function () { - return { - restrict:'A', - require:'ngModel', - compile:function () { - return { - post:function (scope, iElement, iAttrs, ctrl) { - var _$render = ctrl.$render; - ctrl.$render = function () { - var res = _$render.apply(this, arguments); - iElement.jqmChanged(true); - return res; - }; - } - } - } - } - }); -})(window.jQuery, window.angular); \ No newline at end of file diff --git a/src/main/webapp/integration/angularNgOptions.js b/src/main/webapp/integration/angularNgOptions.js new file mode 100644 index 0000000..87370e3 --- /dev/null +++ b/src/main/webapp/integration/angularNgOptions.js @@ -0,0 +1,142 @@ +(function ($, angular) { + + function watchJQueryDomChangesInSubtree(element, callback) { + for (var fnName in jqFnWatchers) { + jqFnWatchers[fnName](element, fnName, callback); + } + } + + var jqFnWatchers = { + append: watchDomAddingFn, + after: watchAttributeChangingFn, + text: watchAttributeChangingFn, + val: watchAttributeChangingFn, + prop: watchAttributeChangingFn, + attr: watchAttributeChangingFn, + remove: watchAttributeChangingFn + }; + + function watchDomAddingFn(node, fnName, callback) { + var _old = node[fnName]; + node[fnName] = function (otherNode) { + watchJQueryDomChangesInSubtree(otherNode, callback); + var res = _old.apply(this, arguments); + callback(); + return res; + }; + } + + function watchAttributeChangingFn(node, fnName, callback) { + var _old = node[fnName]; + node[fnName] = function (otherNode) { + var res = _old.apply(this, arguments); + callback(); + return res; + }; + } + + + /** + * Modify the original repeat: Make sure that all elements are added under the same parent. + * This is important, as some jquery mobile widgets wrap the elements into new elements, + * and angular just uses element.after(). + * See angular issue 831 + */ + function instrumentNodeForNgRepeat(scope, parent, node, fnName) { + var _old = node[fnName]; + node[fnName] = function (otherNode) { + var target = this; + while (target.parent()[0] !== parent) { + target = target.parent(); + if (target.length === 0) { + throw new Error("Could not find the expected parent in the node path", this, parent); + } + } + instrumentNodeForNgRepeat(scope, parent, otherNode, fnName); + var res = _old.call(target, otherNode); + scope.$emit("$childrenChanged"); + return res; + }; + } + + var mod = angular.module('ng'); + mod.directive('ngRepeat', function () { + return { + priority:1000, // same as original repeat + compile:function (element, attr, linker) { + return { + pre:function (scope, iterStartElement, attr) { + instrumentNodeForNgRepeat(scope, iterStartElement.parent()[0], iterStartElement, 'after'); + } + }; + } + }; + }); + + function sortedKeys(obj) { + var keys = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + keys.push(key); + } + } + return keys.sort(); + } + + var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/; + mod.directive('ngOptions', ['$parse', function ($parse) { + return { + require: ['select', '?ngModel'], + link:function (scope, element, attr, ctrls) { + // if ngModel is not defined, we don't need to do anything + if (!ctrls[1]) return; + + var match; + var optionsExp = attr.ngOptions; + + if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { + throw Error( + "Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + + " but got '" + optionsExp + "'."); + } + + var displayFn = $parse(match[2] || match[1]), + valueName = match[4] || match[6], + keyName = match[5], + groupByFn = $parse(match[3] || ''), + valueFn = $parse(match[2] ? match[1] : valueName), + valuesFn = $parse(match[7]); + + scope.$watch(optionsModel, function() { + scope.$emit("$childrenChanged"); + console.log("now"); + }, true); + + function optionsModel() { + var optionGroups = [], // Temporary location for the option groups before we render them + optionGroupName, + values = valuesFn(scope) || [], + keys = keyName ? sortedKeys(values) : values, + length, + index, + locals = {}; + + // We now build up the list of options we need (we merge later) + for (index = 0; length = keys.length, index < length; index++) { + var value = values[index]; + locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index]; + optionGroupName = groupByFn(scope, locals); + optionGroups.push({ + id: keyName ? keys[index] : index, // either the index into array or key from object + label: displayFn(scope, locals), // what will be seen by the user + optionGroup: optionGroupName + }); + } + return optionGroups; + } + } + }; + }]); + + +})(window.jQuery, window.angular); \ No newline at end of file diff --git a/src/main/webapp/integration/jqmAngularWidgets.js b/src/main/webapp/integration/jqmAngularWidgets.js new file mode 100644 index 0000000..93e3d51 --- /dev/null +++ b/src/main/webapp/integration/jqmAngularWidgets.js @@ -0,0 +1,113 @@ +(function(angular) { + // TODO refactor this: Create a config for every jqm widget... + + var ng = angular.module("ng"); + ng.config(["$compileProvider", function($compileProvider) { + $compileProvider.parseSelectorAndRegisterJqmWidget('button', function (scope, iElement, iAttrs) { + disabledHandling('button', scope, iElement, iAttrs); + + }); + $compileProvider.parseSelectorAndRegisterJqmWidget('collapsible', function (scope, iElement, iAttrs) { + disabledHandling('collapsible', scope, iElement, iAttrs); + + }); + + $compileProvider.parseSelectorAndRegisterJqmWidget('textinput', function (scope, iElement, iAttrs) { + disabledHandling('textinput', scope, iElement, iAttrs); + + }); + + $compileProvider.parseSelectorAndRegisterJqmWidget('checkboxradio', function (scope, iElement, iAttrs, ctrls) { + disabledHandling('checkboxradio', scope, iElement, iAttrs); + refreshOnNgModelRender('checkboxradio', iElement, ctrls); + + }); + $compileProvider.parseSelectorAndRegisterJqmWidget('slider', function (scope, iElement, iAttrs, ctrls) { + disabledHandling('slider', scope, iElement, iAttrs); + refreshOnNgModelRender('slider', iElement, ctrls); + + }); + + $compileProvider.parseSelectorAndRegisterJqmWidget('listview', function (scope, iElement, iAttrs, ctrls) { + refreshOnChildrenChange('listview', scope, iElement); + }); + + $compileProvider.parseSelectorAndRegisterJqmWidget('collapsibleset', function (scope, iElement, iAttrs, ctrls) { + refreshOnChildrenChange('collapsibleset', scope, iElement); + }); + + $compileProvider.parseSelectorAndRegisterJqmWidget('selectmenu', function (scope, iElement, iAttrs, ctrls) { + disabledHandling('selectmenu', scope, iElement, iAttrs); + refreshOnNgModelRender('selectmenu', iElement, ctrls); + refreshOnChildrenChange('selectmenu', scope, iElement); + }); + }]); + + function disabledHandling(widget, scope, iElement, iAttrs) { + iAttrs.$observe("disabled", function (value) { + if (value) { + iElement[widget]("disable"); + } else { + iElement[widget]("enable"); + } + }); + } + + function addCtrlFunctionListener(ctrl, ctrlFnName, fn) { + var listenersName = "_listeners"+ctrlFnName; + if (!ctrl[listenersName]) { + ctrl[listenersName] = []; + var oldFn = ctrl[ctrlFnName]; + ctrl[ctrlFnName] = function() { + var res = oldFn.apply(this, arguments); + for (var i=0; i'); + expect($.fn.listview.callCount).toBe(0); + var scope = c.page.scope(); + scope.list = [1,2]; + scope.$root.$digest(); + expect($.fn.listview.callCount).toBe(2); + }); + + it("should refresh only once when child entries are added by angular", function() { + var d = testutils.compileInPage( + ''); + var list = d.element; + var lis = list.children("li"); + expect(lis.length).toBe(0); + var scope = d.element.scope(); + var listview = list.data("listview"); + spyOn(listview, 'refresh').andCallThrough(); + scope.list = [1,2]; + scope.$digest(); + expect(listview.refresh.callCount).toBe(1); + var lis = list.children("li"); + expect(lis.length).toBe(2); + expect(lis.eq(0).hasClass("ui-corner-top")).toBe(true); + expect(lis.eq(0).hasClass("ui-corner-bottom")).toBe(false); + expect(lis.eq(1).hasClass("ui-corner-top")).toBe(false); + expect(lis.eq(1).hasClass("ui-corner-bottom")).toBe(true); + }); + + it("should refresh only once when child entries are removed by angular", function() { + var d = testutils.compileInPage( + ''); + // d.element.scope().$digest(); + var list = d.element; + var lis = list.children("li"); + expect(lis.length).toBe(3); + var scope = d.element.scope(); + scope.list = [1]; + var listview = list.data("listview"); + spyOn(listview, 'refresh').andCallThrough(); + scope.$digest(); + expect(listview.refresh.callCount).toBe(1); + var lis = list.children("li"); + expect(lis.length).toBe(1); + expect(lis.eq(0).hasClass("ui-corner-top")).toBe(true); + expect(lis.eq(0).hasClass("ui-corner-bottom")).toBe(true); + }); + + it('should be removable when subpages are used', function () { + var d = testutils.compileInPage('
' + + '' + + '
'); + var container = d.element; + var list = container.children('ul'); + // ui select creates sub pages for nested uls. + expect($(":jqmData(role='page')").length).toEqual(2); + list.remove(); + expect($(":jqmData(role='page')").length).toEqual(1); + }); +}); diff --git a/src/test/webapp/unit/integration/disabledHandlingSpec.js b/src/test/webapp/unit/integration/disabledHandlingSpec.js deleted file mode 100644 index 9a86047..0000000 --- a/src/test/webapp/unit/integration/disabledHandlingSpec.js +++ /dev/null @@ -1,19 +0,0 @@ -describe("disabledHandling", function () { - function execTest(attribute) { - var d = testutils.compileInPage(''); - var page = d.page; - var input = d.element; - var scope = input.scope(); - var parentDiv = input.parent(); - scope.disabled = false; - scope.$root.$digest(); - expect(parentDiv.hasClass('ui-disabled')).toBeFalsy(); - scope.disabled = true; - scope.$root.$digest(); - expect(parentDiv.hasClass('ui-disabled')).toBeTruthy(); - } - - it('should work with {{ }}', function () { - execTest('ng-disabled="disabled"'); - }); -}); diff --git a/src/test/webapp/unit/integration/scopeEventsSpec.js b/src/test/webapp/unit/integration/scopeEventsSpec.js new file mode 100644 index 0000000..e69de29 diff --git a/src/test/webapp/unit/integration/selectmenuSpec.js b/src/test/webapp/unit/integration/selectmenuSpec.js new file mode 100644 index 0000000..e8b3dfa --- /dev/null +++ b/src/test/webapp/unit/integration/selectmenuSpec.js @@ -0,0 +1,187 @@ +describe("selectmenu", function () { + it('should save the ui value into the model when using non native menus and popups', function () { + var scope, dialogOpen; + loadHtml('/jqmng/ui/test-fixture.html', function (frame) { + var page = frame.$('#start'); + page.append( + '
' + + '' + + '
'); + }); + runs(function () { + var $ = testframe().$; + var page = $("#start"); + var select = page.find("#mysel"); + expect(select[0].value).toEqual("v1"); + scope = select.scope(); + expect(scope.mysel).toEqual("v1"); + dialogOpen = function () { + return select.data('selectmenu').isOpen; + }; + expect(dialogOpen()).toBeFalsy(); + // find the menu and click on the second entry + var oldHeight = testframe().$.fn.height; + testframe().$.fn.height = function () { + if (this[0].window == testframe()) { + return 10; + } + return oldHeight.apply(this, arguments); + }; + select.selectmenu('open'); + }); + waitsFor(function () { + return dialogOpen(); + }); + runs(function () { + var $ = testframe().$; + var dialog = $(".ui-dialog"); + $(dialog.find('li a')[1]).trigger('click') + expect(scope.mysel).toEqual("v2"); + }); + waitsFor(function () { + return !dialogOpen(); + }); + }); + + it('should save the ui value into the model when using non native menus', function () { + loadHtml('/jqmng/ui/test-fixture.html', function (frame) { + var page = frame.$('#start'); + page.append( + '
' + + '' + + '
'); + }); + runs(function () { + var page = testframe().$("#start"); + var select = page.find("#mysel"); + expect(select[0].value).toEqual("v1"); + var scope = select.scope(); + expect(scope.mysel).toEqual("v1"); + + // find the menu and click on the second entry + select.selectmenu('open'); + var popup = page.find(".ui-selectmenu"); + var options = popup.find("li"); + var option = testframe().$(options[1]); + option.trigger('click'); + select.selectmenu('close'); + expect(scope.mysel).toEqual("v2"); + }); + }); + + it('should save the model value into the ui when using non native menus', function () { + loadHtml('/jqmng/ui/test-fixture.html', function (frame) { + var page = frame.$('#start'); + page.append( + '
' + + '' + + '
'); + }); + runs(function () { + var page = testframe().$("#start"); + var select = page.find("#mysel"); + var scope = select.scope(); + expect(select[0].value).toEqual("v1"); + // jquery mobile creates a new span + // that displays the actual value of the select box. + var valueSpan = select.parent().find(".ui-btn-text"); + expect(valueSpan.text()).toEqual("v1"); + scope.mysel = "v2"; + scope.$apply(); + expect(select[0].value).toEqual("v2"); + expect(valueSpan.text()).toEqual("v2"); + }); + }); + + it('should use the disabled attribute', function () { + loadHtml('/jqmng/ui/test-fixture.html', function (frame) { + var page = frame.$('#start'); + page.append( + '
' + + '' + + '
'); + }); + runs(function () { + var page = testframe().$("#start"); + var select = page.find("#mysel"); + var scope = select.scope(); + scope.disabled = false; + scope.$root.$digest(); + var disabled = select.selectmenu('option', 'disabled'); + expect(disabled).toEqual(false); + scope.disabled = true; + scope.$root.$digest(); + var disabled = select.selectmenu('option', 'disabled'); + console.log(select.parent().html()); + expect(disabled).toEqual(true); + }); + }); + + it('should be removable', function () { + loadHtml('/jqmng/ui/test-fixture.html', function (frame) { + var page = frame.$('#start'); + page.append( + '
' + + '' + + '
'); + }); + runs(function () { + var page = testframe().$("#start"); + var scope = page.scope(); + // ui select creates a new parent for itself + var content = page.find(":jqmData(role='content')"); + expect(content.children('div').length).toEqual(1); + // select creates a parent div. This should be removed when the select is removed. + content.find('select').eq(0).remove(); + expect(content.children('div').length).toEqual(0); + }); + }); + + it('should refresh when the dialog opens', function () { + loadHtml('/jqmng/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( + '
' + + '' + + '
'); + }); + runs(function () { + var page = testframe().$("#start"); + var select = page.find("#mysel"); + var scope = select.scope(); + scope.options = [1, 2]; + scope.mysel = 1; + scope.$apply(); + select.selectmenu('open'); + expect(page.find(".ui-selectmenu li").length).toEqual(2); + }); + }); + + it('should be able to display the label of a new entry when the options grow in a native menu', function () { + loadHtml('/jqmng/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( + '
' + + '' + + '
'); + }); + runs(function () { + var page = testframe().$("#start"); + var select = page.find("#mysel"); + var scope = select.scope(); + expect(scope.myval).toBeFalsy(); + scope.list = [ + {value:'value1'} + ]; + scope.myval = scope.list[0]; + scope.$root.$apply(); + }); + waitsForAsync(); + runs(function () { + var page = testframe().$("#start"); + expect(page.find(".ui-select .ui-btn-text").text()).toEqual("value1"); + }); + }); +});