aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--CHANGELOG.md7
-rw-r--r--docs/collect.js3
-rw-r--r--docs/docs.js8
-rw-r--r--docs/service.template6
-rw-r--r--perf/noangular.html19
-rw-r--r--src/Browser.js15
-rw-r--r--src/Compiler.js1
-rw-r--r--src/Injector.js10
-rw-r--r--src/directives.js12
-rw-r--r--src/scenario/Scenario.js2
-rw-r--r--src/services.js62
-rw-r--r--src/validators.js2
-rw-r--r--src/widgets.js22
-rw-r--r--test/BinderSpec.js3
-rw-r--r--test/servicesSpec.js57
-rw-r--r--test/testabilityPatch.js2
-rw-r--r--test/widgetsSpec.js21
18 files changed, 204 insertions, 51 deletions
diff --git a/.gitignore b/.gitignore
index ee16a0c8..1d447a4a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@ build/
angularjs.netrc
jstd.log
.DS_Store
-regression/temp.html
+regression/temp*.html
+performance/temp*.html
.idea/workspace.xml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f04983b..a72281dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@
not needed.
- $location service now listens for `onhashchange` events (if supported by browser) instead of
constant polling.
+- input widgets known listens on keydown events instead of keyup which improves perceived
+ performance
+
+### API
+
+- new service $updateView which should be used in favor of $root.$eval() to run a complete eval on
+ the entire document. This service bulks and throttles DOM updates to improve performance.
### Breaking changes
- API for accessing registered services — `scope.$inject` — was renamed to
diff --git a/docs/collect.js b/docs/collect.js
index e1af14ef..eb540051 100644
--- a/docs/collect.js
+++ b/docs/collect.js
@@ -244,7 +244,7 @@ var TAG = {
name: function(doc, name, value) {
var parts = value.split(/\./);
doc.name = value;
- doc.shortName = parts.pop();
+ doc.shortName = parts.pop().replace('#', '.');
doc.depth = parts.length;
},
param: function(doc, name, value){
@@ -378,6 +378,7 @@ function processNgDoc(documentation, doc) {
if (doc.methodOf) {
if (parent = documentation.byName[doc.methodOf]) {
(parent.method = parent.method || []).push(doc);
+ parent.method.sort(keywordSort);
} else {
throw 'Owner "' + doc.methodOf + '" is not defined.';
}
diff --git a/docs/docs.js b/docs/docs.js
index 7e6c2ac4..6bf86ed3 100644
--- a/docs/docs.js
+++ b/docs/docs.js
@@ -1,5 +1,3 @@
-SyntaxHighlighter['defaults'].toolbar = false;
-
DocsController.$inject = ['$location', '$browser', '$window'];
function DocsController($location, $browser, $window) {
this.pages = NG_PAGES;
@@ -38,10 +36,12 @@ function DocsController($location, $browser, $window) {
return "mailto:angular@googlegroups.com?" +
"subject=" + escape("Feedback on " + $location.href) + "&" +
"body=" + escape("Hi there,\n\nI read " + $location.href + " and wanted to ask ....");
- }
+ };
}
angular.filter('short', function(name){
return (name||'').split(/\./).pop();
-}); \ No newline at end of file
+});
+
+SyntaxHighlighter['defaults'].toolbar = false;
diff --git a/docs/service.template b/docs/service.template
index ee3a284e..c28bddc9 100644
--- a/docs/service.template
+++ b/docs/service.template
@@ -25,19 +25,23 @@
{{/requires}}
</ul>
+{{#method.length}}
<h2>Methods</h2>
<ul>
{{#method}}
- <li><tt>{{shortName}}</tt>: {{{description}}}</li>
+ <li><tt>{{shortName}}()</tt>: {{{description}}}</li>
{{/method}}
</ul>
+{{/method.length}}
+{{#property.length}}
<h2>Properties</h2>
<ul>
{{#property}}
<li><tt>{{name}}:{{#type}}{{type}}{{/type}}</tt>{{#description}}: {{{description}}}{{/description}}</li>
{{/property}}
</ul>
+{{/property.length}}
{{#example}}
<h2>Example</h2>
diff --git a/perf/noangular.html b/perf/noangular.html
new file mode 100644
index 00000000..8e0db311
--- /dev/null
+++ b/perf/noangular.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html xmlns:ng="http://angularjs.org">
+ <head>
+ <script>
+ function el(id) {
+ return document.getElementById(id);
+ }
+ function update() {
+ el("output").innerHTML = el("input").value;
+ }
+ </script>
+ </head>
+ <body>
+ Your name: <input id="input" type="text" value="World"
+ onkeydown="setTimeout(update,0)"/>
+ <hr/>
+ Hello <span id="output">{{yourname}}</span>!
+ </body>
+</html> \ No newline at end of file
diff --git a/src/Browser.js b/src/Browser.js
index c93f115c..377c740c 100644
--- a/src/Browser.js
+++ b/src/Browser.js
@@ -136,9 +136,7 @@ function Browser(window, document, body, XHR, $log) {
* @methodOf angular.service.$browser
*/
self.poll = function() {
- foreach(pollFns, function(pollFn){
- pollFn();
- });
+ foreach(pollFns, function(pollFn){ pollFn(); });
};
/**
@@ -319,22 +317,23 @@ function Browser(window, document, body, XHR, $log) {
/**
* @workInProgress
- * @ngdoc
+ * @ngdoc method
* @name angular.service.$browser#defer
* @methodOf angular.service.$browser
+ * @param {function()} fn A function, who's execution should be defered.
+ * @param {int=} [delay=0] of milliseconds to defer the function execution.
*
* @description
- * Executes a fn asynchroniously via `setTimeout(fn, 0)`.
+ * Executes a fn asynchroniously via `setTimeout(fn, delay)`.
*
* Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using
* `setTimeout` in tests, the fns are queued in an array, which can be programaticaly flushed via
* `$browser.defer.flush()`.
*
- * @param {function()} fn A function, who's execution should be defered.
*/
- self.defer = function(fn) {
+ self.defer = function(fn, delay) {
outstandingRequestCount++;
- setTimeout(function() { completeOutstandingRequest(fn); }, 0);
+ setTimeout(function() { completeOutstandingRequest(fn); }, delay || 0);
};
//////////////////////////////////////////////////////////////
diff --git a/src/Compiler.js b/src/Compiler.js
index eea36263..a18341f4 100644
--- a/src/Compiler.js
+++ b/src/Compiler.js
@@ -74,6 +74,7 @@ Template.prototype = {
*/
function retrieveScope(element) {
var scope;
+ element = jqLite(element);
while (element && !(scope = element.data($$scope))) {
element = element.parent();
}
diff --git a/src/Injector.js b/src/Injector.js
index 78858e86..f6cb897f 100644
--- a/src/Injector.js
+++ b/src/Injector.js
@@ -67,4 +67,12 @@ function createInjector(providerScope, providers, cache) {
}
return returnValue;
};
-} \ No newline at end of file
+}
+
+function injectService(services, fn) {
+ return extend(fn, {$inject:services});;
+}
+
+function injectUpdateView(fn) {
+ return injectService(['$updateView'], fn);
+}
diff --git a/src/directives.js b/src/directives.js
index d40d6120..bf215cc8 100644
--- a/src/directives.js
+++ b/src/directives.js
@@ -423,14 +423,14 @@ angularDirective("ng:bind-attr", function(expression){
* TODO: maybe we should consider allowing users to control event propagation in the future.
*/
angularDirective("ng:click", function(expression, element){
- return function(element){
+ return injectUpdateView(function($updateView, element){
var self = this;
element.bind('click', function(event){
self.$tryEval(expression, element);
- self.$root.$eval();
+ $updateView();
event.stopPropagation();
});
- };
+ });
});
@@ -471,14 +471,14 @@ angularDirective("ng:click", function(expression, element){
* server and reloading the current page).
*/
angularDirective("ng:submit", function(expression, element) {
- return function(element) {
+ return injectUpdateView(function($updateView, element) {
var self = this;
element.bind('submit', function(event) {
self.$tryEval(expression, element);
- self.$root.$eval();
+ $updateView();
event.preventDefault();
});
- };
+ });
});
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
index 1723412e..a2926b5b 100644
--- a/src/scenario/Scenario.js
+++ b/src/scenario/Scenario.js
@@ -285,7 +285,7 @@ function browserTrigger(element, type) {
(function(fn){
var parentTrigger = fn.trigger;
fn.trigger = function(type) {
- if (/(click|change|keyup)/.test(type)) {
+ if (/(click|change|keydown)/.test(type)) {
return this.each(function(index, node) {
browserTrigger(node, type);
});
diff --git a/src/services.js b/src/services.js
index 91bd226d..1a2aada6 100644
--- a/src/services.js
+++ b/src/services.js
@@ -408,6 +408,62 @@ angularServiceInject('$exceptionHandler', function($log){
/**
* @workInProgress
* @ngdoc service
+ * @name angular.service.$updateView
+ * @requires $browser
+ *
+ * @description
+ * Calling `$updateView` enqueues the eventual update of the view. (Update the DOM to reflect the
+ * model). The update is eventual, since there are often multiple updates to the model which may
+ * be deferred. The default update delayed is 25 ms. This means that the view lags the model by
+ * that time. (25ms is small enough that it is perceived as instantaneous by the user). The delay
+ * can be adjusted by setting the delay property of the service.
+ *
+ * <pre>angular.service('$updateView').delay = 10</pre>
+ *
+ * The delay is there so that multiple updates to the model which occur sufficiently close
+ * together can be merged into a single update.
+ *
+ * You don't usually call '$updateView' directly since angular does it for you in most cases,
+ * but there are some cases when you need to call it.
+ *
+ * - `$updateView()` called automatically by angular:
+ * - Your Application Controllers: Your controller code is called by angular and hence
+ * angular is aware that you may have changed the model.
+ * - Your Services: Your service is usually called by your controller code, hence same rules
+ * apply.
+ * - May need to call `$updateView()` manually:
+ * - Widgets / Directives: If you listen to any DOM events or events on any third party
+ * libraries, then angular is not aware that you may have changed state state of the
+ * model, and hence you need to call '$updateView()' manually.
+ * - 'setTimeout'/'XHR': If you call 'setTimeout' (instead of {@link angular.service.$defer})
+ * or 'XHR' (instead of {@link angular.service.$xhr}) then you may be changing the model
+ * without angular knowledge and you may need to call '$updateView()' directly.
+ *
+ * NOTE: if you wish to update the view immediately (without delay), you can do so by calling
+ * {@link scope.$eval} at any time from your code:
+ * <pre>scope.$root.$eval()</pre>
+ *
+ * In unit-test mode the update is instantaneous and synchronous to simplify writing tests.
+ *
+ */
+angularServiceInject('$updateView', extend(function factory($browser){
+ var rootScope = this;
+ var scheduled;
+ function update(){
+ scheduled = false;
+ rootScope.$eval();
+ }
+ return $browser.isMock ? update : function(){
+ if (!scheduled) {
+ scheduled = true;
+ $browser.defer(update, factory.delay);
+ }
+ };
+}, {delay:25}), ['$browser']);
+
+/**
+ * @workInProgress
+ * @ngdoc service
* @name angular.service.$hover
* @requires $browser
* @requires $document
@@ -815,7 +871,7 @@ angularServiceInject('$xhr.bulk', function($xhr, $error, $log){
*
* @param {function()} fn A function, who's execution should be deferred.
*/
-angularServiceInject('$defer', function($browser, $exceptionHandler) {
+angularServiceInject('$defer', function($browser, $exceptionHandler, $updateView) {
var scope = this;
return function(fn) {
@@ -825,11 +881,11 @@ angularServiceInject('$defer', function($browser, $exceptionHandler) {
} catch(e) {
$exceptionHandler(e);
} finally {
- scope.$eval();
+ $updateView();
}
});
};
-}, ['$browser', '$exceptionHandler']);
+}, ['$browser', '$exceptionHandler', '$updateView']);
/**
diff --git a/src/validators.js b/src/validators.js
index 3de98d61..8030c0d0 100644
--- a/src/validators.js
+++ b/src/validators.js
@@ -398,7 +398,7 @@ extend(angularValidator, {
$invalidWidgets.markValid(element);
}
element.data($$validate)();
- scope.$root.$eval();
+ scope.$service('$updateView')();
});
} else if (inputState.inFlight) {
// request in flight, mark widget invalid, but don't show it to user
diff --git a/src/widgets.js b/src/widgets.js
index 05979281..5ff4a28f 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -376,7 +376,7 @@ function optionsAccessor(scope, element) {
function noopAccessor() { return { get: noop, set: noop }; }
-var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue(), true),
+var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, initWidgetValue(), true),
buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop),
INPUT_TYPE = {
'text': textWidget,
@@ -454,8 +454,8 @@ function radioInit(model, view, element) {
expect(binding('checkboxCount')).toBe('1');
});
*/
-function inputWidget(events, modelAccessor, viewAccessor, initFn, dirtyChecking) {
- return function(element) {
+function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) {
+ return injectService(['$updateView', '$defer'], function($updateView, $defer, element) {
var scope = this,
model = modelAccessor(scope, element),
view = viewAccessor(scope, element),
@@ -464,25 +464,25 @@ function inputWidget(events, modelAccessor, viewAccessor, initFn, dirtyChecking)
if (model) {
initFn.call(scope, model, view, element);
this.$eval(element.attr('ng:init')||'');
- // Don't register a handler if we are a button (noopAccessor) and there is no action
- if (action || modelAccessor !== noopAccessor) {
- element.bind(events, function (){
+ element.bind(events, function(event){
+ function handler(){
var value = view.get();
- if (!dirtyChecking || value != lastValue) {
+ if (!textBox || value != lastValue) {
model.set(value);
lastValue = model.get();
scope.$tryEval(action, element);
- scope.$root.$eval();
+ $updateView();
}
- });
- }
+ }
+ event.type == 'keydown' ? $defer(handler) : handler();
+ });
scope.$watch(model.get, function(value){
if (lastValue !== value) {
view.set(lastValue = value);
}
});
}
- };
+ });
}
function inputWidgetSelector(element){
diff --git a/test/BinderSpec.js b/test/BinderSpec.js
index 9515accb..33bcb091 100644
--- a/test/BinderSpec.js
+++ b/test/BinderSpec.js
@@ -248,7 +248,8 @@ describe('Binder', function(){
assertEquals('b', second.val());
first.val('ABC');
- browserTrigger(first, 'keyup');
+ browserTrigger(first, 'keydown');
+ c.scope.$service('$browser').defer.flush();
assertEquals(c.scope.items[0].x, 'ABC');
});
diff --git a/test/servicesSpec.js b/test/servicesSpec.js
index 6df83beb..8918f415 100644
--- a/test/servicesSpec.js
+++ b/test/servicesSpec.js
@@ -394,7 +394,7 @@ describe("service", function(){
it('should call eval even if an exception is thrown in callback', function() {
var eval = this.spyOn(scope, '$eval').andCallThrough();
- $defer(function() {throw "Test Error"});
+ $defer(function() {throw "Test Error";});
expect(eval).wasNotCalled();
$browser.defer.flush();
@@ -594,7 +594,7 @@ describe("service", function(){
$browser.defer.flush();
expect(eval).wasCalled();
- })
+ });
});
});
@@ -777,4 +777,57 @@ describe("service", function(){
expect(match[10]).toEqual('?book=moby');
});
});
+
+ describe('$updateView', function(){
+ var scope, browser, evalCount, $updateView;
+
+ beforeEach(function(){
+ browser = new MockBrowser();
+ // Pretend that you are real Browser so that we see the delays
+ browser.isMock = false;
+ browser.defer = jasmine.createSpy('defer');
+
+ scope = angular.scope(null, null, {$browser:browser});
+ $updateView = scope.$service('$updateView');
+ scope.$onEval(function(){ evalCount++; });
+ evalCount = 0;
+ });
+
+ it('should eval root scope after a delay', function(){
+ $updateView();
+ expect(evalCount).toEqual(0);
+ expect(browser.defer).toHaveBeenCalled();
+ expect(browser.defer.mostRecentCall.args[1]).toEqual(25);
+ browser.defer.mostRecentCall.args[0]();
+ expect(evalCount).toEqual(1);
+ });
+
+ it('should allow changing of delay time', function(){
+ var oldValue = angular.service('$updateView').delay;
+ angular.service('$updateView').delay = 50;
+ $updateView();
+ expect(evalCount).toEqual(0);
+ expect(browser.defer).toHaveBeenCalled();
+ expect(browser.defer.mostRecentCall.args[1]).toEqual(50);
+ angular.service('$updateView').delay = oldValue;
+ });
+
+ it('should ignore multiple requests for update', function(){
+ $updateView();
+ $updateView();
+ expect(evalCount).toEqual(0);
+ expect(browser.defer).toHaveBeenCalled();
+ expect(browser.defer.callCount).toEqual(1);
+ browser.defer.mostRecentCall.args[0]();
+ expect(evalCount).toEqual(1);
+ });
+
+ it('should update immediatelly in test/mock mode', function(){
+ scope = angular.scope();
+ scope.$onEval(function(){ evalCount++; });
+ expect(evalCount).toEqual(0);
+ scope.$service('$updateView')();
+ expect(evalCount).toEqual(1);
+ });
+ });
});
diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js
index d389ae19..6cbf91e9 100644
--- a/test/testabilityPatch.js
+++ b/test/testabilityPatch.js
@@ -1,7 +1,7 @@
/**
* Here is the problem: http://bugs.jquery.com/ticket/7292
* basically jQuery treats change event on some browsers (IE) as a
- * special event and changes it form 'change' to 'click/keyup' and
+ * special event and changes it form 'change' to 'click/keydown' and
* few others. This horrible hack removes the special treatment
*/
_jQuery.event.special.change = undefined;
diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js
index e56e895b..8dab4630 100644
--- a/test/widgetsSpec.js
+++ b/test/widgetsSpec.js
@@ -22,7 +22,7 @@ describe("widget", function(){
describe("input", function(){
describe("text", function(){
- it('should input-text auto init and handle keyup/change events', function(){
+ it('should input-text auto init and handle keydown/change events', function(){
compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>');
expect(scope.$get('name')).toEqual("Misko");
expect(scope.$get('count')).toEqual(0);
@@ -32,7 +32,10 @@ describe("widget", function(){
expect(element.val()).toEqual("Adam");
element.val('Shyam');
- browserTrigger(element, 'keyup');
+ browserTrigger(element, 'keydown');
+ // keydown event must be deferred
+ expect(scope.$get('name')).toEqual('Adam');
+ scope.$service('$browser').defer.flush();
expect(scope.$get('name')).toEqual('Shyam');
expect(scope.$get('count')).toEqual(1);
@@ -46,7 +49,7 @@ describe("widget", function(){
compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>');
expect(scope.name).toEqual("Misko");
expect(scope.count).toEqual(0);
- browserTrigger(element, 'keyup');
+ browserTrigger(element, 'keydown');
expect(scope.name).toEqual("Misko");
expect(scope.count).toEqual(0);
});
@@ -69,7 +72,7 @@ describe("widget", function(){
expect(element.val()).toEqual("x, y, z");
element.val('1, 2, 3');
- browserTrigger(element, 'keyup');
+ browserTrigger(element);
expect(scope.$get('list')).toEqual(['1', '2', '3']);
});
@@ -191,7 +194,7 @@ describe("widget", function(){
expect(element.attr('ng-validation-error')).toBeFalsy();
element.val('x');
- browserTrigger(element, 'keyup');
+ browserTrigger(element);
expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-validation-error')).toEqual('Not a number');
});
@@ -245,7 +248,7 @@ describe("widget", function(){
expect(element.attr('ng-validation-error')).toBeFalsy();
element.val('');
- browserTrigger(element, 'keyup');
+ browserTrigger(element);
expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-validation-error')).toEqual('Required');
});
@@ -270,7 +273,7 @@ describe("widget", function(){
expect(element.attr('ng-validation-error')).toEqual('Required');
element.val('abc');
- browserTrigger(element, 'keyup');
+ browserTrigger(element);
expect(element.hasClass('ng-validation-error')).toBeFalsy();
expect(element.attr('ng-validation-error')).toBeFalsy();
});
@@ -284,11 +287,11 @@ describe("widget", function(){
expect(element.val()).toEqual("Adam");
element.val('Shyam');
- browserTrigger(element, 'keyup');
+ browserTrigger(element);
expect(scope.$get('name')).toEqual('Shyam');
element.val('Kai');
- browserTrigger(element, 'change');
+ browserTrigger(element);
expect(scope.$get('name')).toEqual('Kai');
});