From 325908d3233442084693561aabde842866010d17 Mon Sep 17 00:00:00 2001 From: Siddique Hameed Date: Wed, 22 May 2013 13:47:17 -0500 Subject: [PATCH 01/23] docs(guide/unit-testing): add expression example * Improved developer guide, directive unit testing documentation code with scope expression * Removed documentation block with nothing on it --- .../guide/dev_guide.unit-testing.ngdoc | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/docs/content/guide/dev_guide.unit-testing.ngdoc b/docs/content/guide/dev_guide.unit-testing.ngdoc index 175af471ff22..c27180fea84a 100644 --- a/docs/content/guide/dev_guide.unit-testing.ngdoc +++ b/docs/content/guide/dev_guide.unit-testing.ngdoc @@ -294,14 +294,14 @@ app.directive('aGreatEye', function () { return { restrict: 'E', replace: true, - template: '

lidless, wreathed in flame

' + template: '

lidless, wreathed in flame, {{1 + 1}} times

' }; }); This directive is used as a tag ``. It replaces the entire tag with the -template `

lidless, wreathed in flame

`. Now we are going to write a jasmine unit test to -verify this functionality. +template `

lidless, wreathed in flame, {{1 + 1}} times

`. Now we are going to write a jasmine unit test to +verify this functionality. Note that the expression `{{1 + 1}}` times will also be evaluated in the rendered content.
 describe('Unit testing great quotes', function() {
@@ -322,30 +322,18 @@ describe('Unit testing great quotes', function() {
     it('Replaces the element with the appropriate content', function() {
         // Compile a piece of HTML containing the directive
         var element = $compile("")($rootScope);
+        // fire all the watches, so the scope expression {{1 + 1}} will be evaluated
+        $rootScope.$digest();
         // Check that the compiled element contains the templated content
-        expect(element.html()).toContain("lidless, wreathed in flame");
+        expect(element.html()).toContain("lidless, wreathed in flame, 2 times");
     });
 });
 
We inject the $compile service and $rootScope before each jasmine test. The $compile service is used to render the aGreatEye directive. After rendering the directive we ensure that the directive has -replaced the content and "lidless, wreathed in flame" is present. +replaced the content and "lidless, wreathed in flame, 2 times" is present. -## Mocks -oue - -## Global State Isolation -oue - -# Preferred way of Testing -uo - -## JavaScriptTestDriver -ou - -## Jasmine -ou ## Sample project See the {@link https://github.com/angular/angular-seed angular-seed} project for an example. From 508d845d81e300c98a2498a9358c2b3c98372c87 Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Fri, 24 May 2013 15:55:11 -0700 Subject: [PATCH 02/23] test(matchers): update toThrow matcher --- test/matchers.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/matchers.js b/test/matchers.js index 3630c3b18c98..8e4be118e122 100644 --- a/test/matchers.js +++ b/test/matchers.js @@ -151,3 +151,38 @@ beforeEach(function() { }); }); + + +// TODO(vojta): remove this once Jasmine in Karma gets updated +// https://github.com/pivotal/jasmine/blob/c40b64a24c607596fa7488f2a0ddb98d063c872a/src/core/Matchers.js#L217-L246 +// This toThrow supports RegExps. +jasmine.Matchers.prototype.toThrow = function(expected) { + var result = false; + var exception, exceptionMessage; + if (typeof this.actual != 'function') { + throw new Error('Actual is not a function'); + } + try { + this.actual(); + } catch (e) { + exception = e; + } + + if (exception) { + exceptionMessage = exception.message || exception; + result = (isUndefined(expected) || this.env.equals_(exceptionMessage, expected.message || expected) || (jasmine.isA_("RegExp", expected) && expected.test(exceptionMessage))); + } + + var not = this.isNot ? "not " : ""; + var regexMatch = jasmine.isA_("RegExp", expected) ? " an exception matching" : ""; + + this.message = function() { + if (exception) { + return ["Expected function " + not + "to throw" + regexMatch, expected ? expected.message || expected : "an exception", ", but it threw", exceptionMessage].join(' '); + } else { + return "Expected function to throw an exception."; + } + }; + + return result; +}; From 64672501f07c7e8be36b43bce84d3b6ff93655ea Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 24 May 2013 11:00:14 -0700 Subject: [PATCH 03/23] feat(ngError): add error message compression and better error messages - add toThrowNg matcher --- angularFiles.js | 1 + src/Angular.js | 11 +++-- src/auto/injector.js | 12 ++--- src/jqLite.js | 14 +++--- src/loader.js | 2 +- src/ng/cacheFactory.js | 2 +- src/ng/compile.js | 33 +++++-------- src/ng/controller.js | 5 +- src/ng/directive/input.js | 7 +-- src/ng/directive/ngRepeat.js | 12 ++--- src/ng/directive/select.js | 8 ++-- src/ng/httpBackend.js | 2 +- src/ng/interpolate.js | 2 +- src/ng/location.js | 6 +-- src/ng/parse.js | 23 +++++---- src/ng/rootScope.js | 11 ++--- src/ngError.js | 47 ++++++++++++++++++ src/ngMock/angular-mocks.js | 19 ++++---- test/AngularSpec.js | 16 ++++--- test/BinderSpec.js | 2 +- test/auto/injectorSpec.js | 31 ++++++------ test/loaderSpec.js | 2 +- test/matchers.js | 6 ++- test/ng/animatorSpec.js | 2 +- test/ng/cacheFactorySpec.js | 2 +- test/ng/compileSpec.js | 41 +++++++++++----- test/ng/controllerSpec.js | 10 ++++ test/ng/directive/inputSpec.js | 14 +++--- test/ng/directive/ngRepeatSpec.js | 14 +++--- test/ng/directive/selectSpec.js | 4 +- test/ng/interpolateSpec.js | 4 +- test/ng/locationSpec.js | 8 ++-- test/ng/parseSpec.js | 10 ++-- test/ng/rootScopeSpec.js | 12 ++--- test/ngErrorSpec.js | 80 +++++++++++++++++++++++++++++++ 35 files changed, 315 insertions(+), 160 deletions(-) create mode 100644 src/ngError.js create mode 100644 test/ngErrorSpec.js diff --git a/angularFiles.js b/angularFiles.js index 1ffe3310b336..39ccec907d0a 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -5,6 +5,7 @@ angularFiles = { 'src/AngularPublic.js', 'src/jqLite.js', 'src/apis.js', + 'src/ngError.js', 'src/auto/injector.js', diff --git a/src/Angular.js b/src/Angular.js index aefe7ad5f5bc..22fb005e0663 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -215,7 +215,7 @@ function nextUid() { /** * Set or clear the hashkey for an object. - * @param obj object + * @param obj object * @param h the hashkey (!truthy to delete the hashkey) */ function setHashKey(obj, h) { @@ -590,7 +590,10 @@ function isLeafNode (node) { * @returns {*} The copy or updated `destination`, if `destination` was specified. */ function copy(source, destination){ - if (isWindow(source) || isScope(source)) throw Error("Can't copy Window or Scope"); + if (isWindow(source) || isScope(source)) { + throw ngError(43, "Can't copy! Making copies of Window or Scope instances is not supported."); + } + if (!destination) { destination = source; if (source) { @@ -603,7 +606,7 @@ function copy(source, destination){ } } } else { - if (source === destination) throw Error("Can't copy equivalent objects or arrays"); + if (source === destination) throw ngError(44, "Can't copy! Source and destination are identical."); if (isArray(source)) { destination.length = 0; for ( var i = 0; i < source.length; i++) { @@ -1055,7 +1058,7 @@ function bindJQuery() { */ function assertArg(arg, name, reason) { if (!arg) { - throw new Error("Argument '" + (name || '?') + "' is " + (reason || "required")); + throw ngError(45, "Argument '{0}' is {1}", (name || '?'), (reason || "required")); } return arg; } diff --git a/src/auto/injector.js b/src/auto/injector.js index 9c492dc54bd6..1bd56acdf639 100644 --- a/src/auto/injector.js +++ b/src/auto/injector.js @@ -422,7 +422,7 @@ function createInjector(modulesToLoad) { }, providerInjector = (providerCache.$injector = createInternalInjector(providerCache, function() { - throw Error("Unknown provider: " + path.join(' <- ')); + throw ngError(1, "Unknown provider: {0}", path.join(' <- ')); })), instanceCache = {}, instanceInjector = (instanceCache.$injector = @@ -455,7 +455,7 @@ function createInjector(modulesToLoad) { provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { - throw Error('Provider ' + name + ' must define $get factory method.'); + throw ngError(2, "Provider '{0}' must define $get factory method.", name); } return providerCache[name + providerSuffix] = provider_; } @@ -536,12 +536,9 @@ function createInjector(modulesToLoad) { function createInternalInjector(cache, factory) { function getService(serviceName) { - if (typeof serviceName !== 'string') { - throw Error('Service name expected'); - } if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { - throw Error('Circular dependency: ' + path.join(' <- ')); + throw ngError(4, 'Circular dependency found: {0}', path.join(' <- ')); } return cache[serviceName]; } else { @@ -563,6 +560,9 @@ function createInjector(modulesToLoad) { for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; + if (typeof key !== 'string') { + throw ngError(3, 'Incorrect injection token! Expected service name as string, got {0}', key); + } args.push( locals && locals.hasOwnProperty(key) ? locals[key] diff --git a/src/jqLite.js b/src/jqLite.js index 218b9683378b..cf9d1fa168e8 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -153,7 +153,7 @@ function JQLite(element) { } if (!(this instanceof JQLite)) { if (isString(element) && element.charAt(0) != '<') { - throw Error('selectors not implemented'); + throw ngError(46, 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); } return new JQLite(element); } @@ -627,22 +627,22 @@ forEach({ } } return false; - }; + }; events[type] = []; - - // Refer to jQuery's implementation of mouseenter & mouseleave + + // Refer to jQuery's implementation of mouseenter & mouseleave // Read about mouseenter and mouseleave: // http://www.quirksmode.org/js/events_mouse.html#link8 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"} + var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; + bindFn(element, eventmap[type], function(event) { var ret, target = this, related = event.relatedTarget; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || (related !== target && !contains(target, related)) ){ handle(event, type); - } - + } }); } else { diff --git a/src/loader.js b/src/loader.js index 5b74a4f3ac95..7c96a20d3604 100644 --- a/src/loader.js +++ b/src/loader.js @@ -70,7 +70,7 @@ function setupModuleLoader(window) { } return ensure(modules, name, function() { if (!requires) { - throw Error('No module: ' + name); + throw ngError(47, "Module '{0}' is not available! You either misspelled the module name or forgot to load it.", name); } /** @type {!Array.>} */ diff --git a/src/ng/cacheFactory.js b/src/ng/cacheFactory.js index ce690ebfe2a4..1c23e06339eb 100644 --- a/src/ng/cacheFactory.js +++ b/src/ng/cacheFactory.js @@ -28,7 +28,7 @@ function $CacheFactoryProvider() { function cacheFactory(cacheId, options) { if (cacheId in caches) { - throw Error('cacheId ' + cacheId + ' taken'); + throw ngError(10, "CacheId '{0}' is already taken!", cacheId); } var size = 0, diff --git a/src/ng/compile.js b/src/ng/compile.js index 96529d3cdda9..be22482b033b 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -18,9 +18,6 @@ */ -var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: '; - - /** * @ngdoc function * @name ng.$compile @@ -155,7 +152,6 @@ function $CompileProvider($provide) { Suffix = 'Directive', COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, - MULTI_ROOT_TEMPLATE_ERROR = 'Template must have exactly one root element. was: ', urlSanitizationWhitelist = /^\s*(https?|ftp|mailto|file):/; @@ -392,10 +388,6 @@ function $CompileProvider($provide) { }; } - function wrongMode(localName, mode) { - throw Error("Unsupported '" + mode + "' for '" + localName + "'."); - } - function safeAddClass($element, className) { try { $element.addClass(className); @@ -669,7 +661,7 @@ function $CompileProvider($provide) { compileNode = $template[0]; if ($template.length != 1 || compileNode.nodeType !== 1) { - throw new Error(MULTI_ROOT_TEMPLATE_ERROR + directiveValue); + throw ngError(12, "Template for directive '{0}' must have exactly one root element.", directiveName); } replaceWith(jqCollection, $compileNode, compileNode); @@ -755,7 +747,7 @@ function $CompileProvider($provide) { } value = $element[retrievalMethod]('$' + require + 'Controller'); if (!value && !optional) { - throw Error("No controller: " + require); + throw ngError(13, "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName); } return value; } else if (isArray(require)) { @@ -783,8 +775,8 @@ function $CompileProvider($provide) { var parentScope = scope.$parent || scope; - forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) { - var match = definiton.match(LOCAL_REGEXP) || [], + forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { + var match = definition.match(LOCAL_REGEXP) || [], attrName = match[3] || scopeName, optional = (match[2] == '?'), mode = match[1], // @, =, or & @@ -815,8 +807,8 @@ function $CompileProvider($provide) { parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest lastValue = scope[scopeName] = parentGet(parentScope); - throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] + - ' (directive: ' + newIsolateScopeDirective.name + ')'); + throw ngError(14, "Expression '{0}' used with directive '{1}' is non-assignable!", + attrs[attrName], newIsolateScopeDirective.name); }; lastValue = scope[scopeName] = parentGet(parentScope); scope.$watch(function parentValueWatch() { @@ -846,8 +838,8 @@ function $CompileProvider($provide) { } default: { - throw Error('Invalid isolate scope definition for directive ' + - newIsolateScopeDirective.name + ': ' + definiton); + throw ngError(15, "Invalid isolate scope definition for directive '{0}'. Definition: {... {1}: '{2}' ...}", + newIsolateScopeDirective.name, scopeName, definition); } } }); @@ -1000,7 +992,8 @@ function $CompileProvider($provide) { compileNode = $template[0]; if ($template.length != 1 || compileNode.nodeType !== 1) { - throw new Error(MULTI_ROOT_TEMPLATE_ERROR + content); + throw ngError(16, "Template for directive '{0}' must have exactly one root element. Template: {1}", + origAsyncDirective.name, templateUrl); } tempTemplateAttrs = {$attr: {}}; @@ -1037,7 +1030,7 @@ function $CompileProvider($provide) { linkQueue = null; }). error(function(response, code, headers, config) { - throw Error('Failed to load template: ' + config.url); + throw ngError(17, 'Failed to load template: {0}', config.url); }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) { @@ -1065,8 +1058,8 @@ function $CompileProvider($provide) { function assertNoDuplicate(what, previousDirective, directive, element) { if (previousDirective) { - throw Error('Multiple directives [' + previousDirective.name + ', ' + - directive.name + '] asking for ' + what + ' on: ' + startingTag(element)); + throw ngError(18, 'Multiple directives [{0}, {1}] asking for {2} on: {3}', + previousDirective.name, directive.name, what, startingTag(element)); } } diff --git a/src/ng/controller.js b/src/ng/controller.js index 5c19cf887265..2df0bde9519c 100644 --- a/src/ng/controller.js +++ b/src/ng/controller.js @@ -74,9 +74,8 @@ function $ControllerProvider() { instance = $injector.instantiate(expression, locals); if (identifier) { - if (typeof locals.$scope !== 'object') { - throw new Error('Can not export controller as "' + identifier + '". ' + - 'No scope object provided!'); + if (!(locals && typeof locals.$scope == 'object')) { + throw ngError(47, "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", constructor || expression.name, identifier); } locals.$scope[identifier] = instance; diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 543ed36733a1..610396a566d8 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -475,7 +475,8 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { var patternObj = scope.$eval(pattern); if (!patternObj || !patternObj.test) { - throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj); + throw ngError(5, 'ngPattern error! Expected {0} to be a RegExp but was {1}. Element: {2}', + pattern, patternObj, startingTag(element)); } return validate(patternObj, value); }; @@ -918,8 +919,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ngModelSet = ngModelGet.assign; if (!ngModelSet) { - throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + $attr.ngModel + - ' (' + startingTag($element) + ')'); + throw ngError(6, "ngModel error! Expression '{0}' is non-assignable. Element: {1}", $attr.ngModel, + startingTag($element)); } /** diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 330f6abb18cc..34d32f59adb2 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -157,8 +157,8 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { hashFnLocals = {$id: hashKey}; if (!match) { - throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" + - expression + "'."); + throw ngError(7, "ngRepeat error! Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + expression); } lhs = match[1]; @@ -182,8 +182,8 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); if (!match) { - throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" + - lhs + "'."); + throw ngError(8, "ngRepeat error! '_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", + lhs); } valueIdentifier = match[3] || match[1]; keyIdentifier = match[2]; @@ -244,8 +244,8 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { if (block && block.element) lastBlockMap[block.id] = block; }); // This is a duplicate and we need to throw an error - throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression + - ' key: ' + trackById); + throw ngError(50, "ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", + expression, trackById); } else { // new never before seen block nextBlockOrder[index] = { id: trackById }; diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index 7a1cab5337ec..6dda33f5d086 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -300,9 +300,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { var match; if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { - throw Error( - "Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_ (track by _expr_)?'" + - " but got '" + optionsExp + "'."); + throw ngError(9, + "ngOptions error! Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}", + optionsExp, startingTag(selectElement)); } var displayFn = $parse(match[2] || match[1]), @@ -357,7 +357,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) { locals[valueName] = collection[trackIndex]; if (trackFn(scope, locals) == key) break; - } + } } else { locals[valueName] = collection[key]; } diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index ed8404f96cfb..5b9657057e66 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -2,7 +2,7 @@ var XHR = window.XMLHttpRequest || function() { try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} - throw new Error("This browser does not support XMLHttpRequest."); + throw ngError(19, "This browser does not support XMLHttpRequest."); }; diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index dcf05d778f3e..42218fd383d8 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -139,7 +139,7 @@ function $InterpolateProvider() { return concat.join(''); } catch(err) { - var newErr = new Error('Error while interpolating: ' + text + '\n' + err.toString()); + var newErr = ngError(48, "$interpolate error! Can't interpolate: {0}\n{1}", text, err.toString()); $exceptionHandler(newErr); } }; diff --git a/src/ng/location.js b/src/ng/location.js index 4efa019c706f..7b011abe9b57 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -95,7 +95,7 @@ function LocationHtml5Url(appBase, basePrefix) { matchUrl(url, parsed); var pathUrl = beginsWith(appBaseNoFile, url); if (!isString(pathUrl)) { - throw Error('Invalid url "' + url + '", missing path prefix "' + appBaseNoFile + '".'); + throw ngError(21, '$location error! Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile); } matchAppUrl(pathUrl, parsed); extend(this, parsed); @@ -157,11 +157,11 @@ function LocationHashbangUrl(appBase, hashPrefix) { matchUrl(url, this); var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); if (!isString(withoutBaseUrl)) { - throw new Error('Invalid url "' + url + '", does not start with "' + appBase + '".'); + throw ngError(22, '$location error! Invalid url "{0}", does not start with "{1}".', url, appBase); } var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' ? beginsWith(hashPrefix, withoutBaseUrl) : withoutBaseUrl; if (!isString(withoutHashUrl)) { - throw new Error('Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '".'); + throw ngError(49, '$location error! Invalid url "{0}", missing hash prefix "{1}".', url, hashPrefix); } matchAppUrl(withoutHashUrl, this); this.$$compose(); diff --git a/src/ng/parse.js b/src/ng/parse.js index 4616d15d267b..5af52f600d6f 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -123,11 +123,11 @@ function lex(text, csp){ function throwError(error, start, end) { end = end || index; - throw Error("Lexer Error: " + error + " at column" + - (isDefined(start) - ? "s " + start + "-" + index + " [" + text.substring(start, end) + "]" - : " " + end) + - " in expression [" + text + "]."); + var colStr = (isDefined(start) ? + "s " + start + "-" + index + " [" + text.substring(start, end) + "]" + : " " + end); + throw ngError(23, "Lexer Error: {0} at column{1} in expression [{2}].", + error, colStr, text); } function readNumber() { @@ -309,15 +309,14 @@ function parser(text, json, $filter, csp){ /////////////////////////////////// function throwError(msg, token) { - throw Error("Syntax Error: Token '" + token.text + - "' " + msg + " at column " + - (token.index + 1) + " of the expression [" + - text + "] starting at [" + text.substring(token.index) + "]."); + throw ngError(24, + "Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].", + token.text, msg, (token.index + 1), text, text.substring(token.index)); } function peekToken() { if (tokens.length === 0) - throw Error("Unexpected end of expression: " + text); + throw ngError(25, "Unexpected end of expression: {0}", text); return tokens[0]; } @@ -366,7 +365,7 @@ function parser(text, json, $filter, csp){ constant: left.constant && middle.constant && right.constant }); } - + function binaryFn(left, fn, right) { return extend(function(self, locals) { return fn(self, locals, left, right); @@ -471,7 +470,7 @@ function parser(text, json, $filter, csp){ return left; } } - + function logicalOR() { var left = logicalAND(); var token; diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 904f24d4c6d5..6648655133f1 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -161,10 +161,6 @@ function $RootScopeProvider(){ var Child, child; - if (isFunction(isolate)) { - // TODO: remove at some point - throw Error('API-CHANGE: Use $controller to instantiate controllers.'); - } if (isolate) { child = new Scope(); child.$root = this.$root; @@ -560,8 +556,9 @@ function $RootScopeProvider(){ if(dirty && !(ttl--)) { clearPhase(); - throw Error(TTL + ' $digest() iterations reached. Aborting!\n' + - 'Watchers fired in the last 5 iterations: ' + toJson(watchLog)); + throw ngError(27, + '{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}', + TTL, toJson(watchLog)); } } while (dirty || asyncQueue.length); @@ -923,7 +920,7 @@ function $RootScopeProvider(){ function beginPhase(phase) { if ($rootScope.$$phase) { - throw Error($rootScope.$$phase + ' already in progress'); + throw ngError(28, '{0} already in progress', $rootScope.$$phase); } $rootScope.$$phase = phase; diff --git a/src/ngError.js b/src/ngError.js new file mode 100644 index 000000000000..d054336cc7c3 --- /dev/null +++ b/src/ngError.js @@ -0,0 +1,47 @@ +'use strict'; + +/** + * @description + * + * This object extends the error class and provides interpolation capability + * to make it easier to write and read Error messages within Angular. It can + * be called as follows: + * + * throw ngError(13, 'This {0} is {1}', foo, bar); + * + * The above will replace {0} with the value of foo, and {1} with the value of + * bar. The object is not restricted in the number of arguments it can take. + * + * If fewer arguments are specified than necessary for interpolation, the extra + * interpolation markers will be preserved in the final string. + * + * @param {...} arguments The first argument to this object is the error + * number, the second argument the message with templated points for + * Interpolation (of the for {0} for the first, {1} for the second and + * so on). The second argument onwards are interpolated into the error + * message string in order. + */ +function ngError() { + var message = '[NgErr' + arguments[0] + '] ' + arguments[1], + i = 0, + l = arguments.length - 2, + curlyRegexp, arg; + + for (; i < l; i++) { + curlyRegexp = new RegExp("\\{" + i + "\\}", "gm"); + arg = arguments[i + 2]; + + if (isFunction(arg)) { + arg = arg.toString().replace(/ \{[\s\S]*$/, ''); + } else if (!isString(arg)) { + arg = toJson(arg); + } + + message = message.replace(curlyRegexp, arg); + } + + // even if we are called as constructor we can bypass the new ngError instance and return + // an instance of a real Error that contains correct stack info + extra frame for ngError call + // TODO(i): can we rewrite the stack string to remove ngError frame? + return new Error(message); +} diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index be71e326bd3b..647c0be626ee 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -972,13 +972,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { if (expectation && expectation.match(method, url)) { if (!expectation.matchData(data)) - throw Error('Expected ' + expectation + ' with different data\n' + + throw new Error('Expected ' + expectation + ' with different data\n' + 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); if (!expectation.matchHeaders(headers)) - throw Error('Expected ' + expectation + ' with different headers\n' + - 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + - prettyPrint(headers)); + throw new Error('Expected ' + expectation + ' with different headers\n' + + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers)); expectations.shift(); @@ -1002,9 +1001,9 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { } } throw wasExpected ? - Error('No response defined !') : - Error('Unexpected request: ' + method + ' ' + url + '\n' + - (expectation ? 'Expected ' + expectation : 'No more request expected')); + new Error('No response defined !') : + new Error('Unexpected request: ' + method + ' ' + url + '\n' + + (expectation ? 'Expected ' + expectation : 'No more request expected')); } /** @@ -1299,7 +1298,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { $httpBackend.verifyNoOutstandingExpectation = function() { $rootScope.$digest(); if (expectations.length) { - throw Error('Unsatisfied requests: ' + expectations.join(', ')); + throw new Error('Unsatisfied requests: ' + expectations.join(', ')); } }; @@ -1451,7 +1450,7 @@ function MockXhr() { * * This service is just a simple decorator for {@link ng.$timeout $timeout} service * that adds a "flush" and "verifyNoPendingTasks" methods. - */ + */ angular.mock.$TimeoutDecorator = function($delegate, $browser) { @@ -1477,7 +1476,7 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) { */ $delegate.verifyNoPendingTasks = function() { if ($browser.deferredFns.length) { - throw Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + + throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + formatPendingTasksAsString($browser.deferredFns)); } }; diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 0e5017ad25d9..de0296239231 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -84,19 +84,21 @@ describe('angular', function() { }); it('should throw an exception if a Scope is being copied', inject(function($rootScope) { - expect(function() { copy($rootScope.$new()); }).toThrow("Can't copy Window or Scope"); + expect(function() { copy($rootScope.$new()); }). + toThrow("[NgErr43] Can't copy! Making copies of Window or Scope instances is not supported."); })); it('should throw an exception if a Window is being copied', function() { - expect(function() { copy(window); }).toThrow("Can't copy Window or Scope"); + expect(function() { copy(window); }). + toThrow("[NgErr43] Can't copy! Making copies of Window or Scope instances is not supported."); }); it('should throw an exception when source and destination are equivalent', function() { var src, dst; src = dst = {key: 'value'}; - expect(function() { copy(src, dst); }).toThrow("Can't copy equivalent objects or arrays"); + expect(function() { copy(src, dst); }).toThrow("[NgErr44] Can't copy! Source and destination are identical."); src = dst = [2, 4]; - expect(function() { copy(src, dst); }).toThrow("Can't copy equivalent objects or arrays"); + expect(function() { copy(src, dst); }).toThrow("[NgErr44] Can't copy! Source and destination are identical."); }); it('should not copy the private $$hashKey', function() { @@ -580,7 +582,7 @@ describe('angular', function() { expect(function() { angularInit(appElement, bootstrap); - }).toThrow('No module: doesntexist'); + }).toThrow("[NgErr47] Module 'doesntexist' is not available! You either misspelled the module name or forgot to load it."); }); }); @@ -724,7 +726,7 @@ describe('angular', function() { expect(function() { angular.bootstrap(element, ['doesntexist']); - }).toThrow('No module: doesntexist'); + }).toThrow("[NgErr47] Module 'doesntexist' is not available! You either misspelled the module name or forgot to load it."); expect(element.html()).toBe('{{1+2}}'); dealoc(element); @@ -783,7 +785,7 @@ describe('angular', function() { expect(function() { element.injector().get('foo'); - }).toThrow('Unknown provider: fooProvider <- foo'); + }).toThrow('[NgErr1] Unknown provider: fooProvider <- foo'); expect(element.injector().get('$http')).toBeDefined(); }); diff --git a/test/BinderSpec.js b/test/BinderSpec.js index b6a7a00d4dbe..2a1b205d1df8 100644 --- a/test/BinderSpec.js +++ b/test/BinderSpec.js @@ -175,7 +175,7 @@ describe('Binder', function() { $rootScope.error['throw'] = function() {throw 'MyError';}; errorLogs.length = 0; $rootScope.$apply(); - expect(errorLogs.shift().message).toBe('Error while interpolating: {{error.throw()}}\nMyError'); + expect(errorLogs.shift().message).toBe("[NgErr48] $interpolate error! Can't interpolate: {{error.throw()}}\nMyError"); $rootScope.error['throw'] = function() {return 'ok';}; $rootScope.$apply(); diff --git a/test/auto/injectorSpec.js b/test/auto/injectorSpec.js index 8fd03be48cbb..b59a344f0466 100644 --- a/test/auto/injectorSpec.js +++ b/test/auto/injectorSpec.js @@ -70,7 +70,7 @@ describe('injector', function() { it('should provide useful message if no provider', function() { expect(function() { injector.get('idontexist'); - }).toThrow("Unknown provider: idontexistProvider <- idontexist"); + }).toThrow("[NgErr1] Unknown provider: idontexistProvider <- idontexist"); }); @@ -79,7 +79,7 @@ describe('injector', function() { providers('b', function(a) {return 2;}); expect(function() { injector.get('b'); - }).toThrow("Unknown provider: idontexistProvider <- idontexist <- a <- b"); + }).toThrow("[NgErr1] Unknown provider: idontexistProvider <- idontexist <- a <- b"); }); @@ -127,10 +127,10 @@ describe('injector', function() { it('should fail with errors if not function or array', function() { expect(function() { injector.invoke({}); - }).toThrow("Argument 'fn' is not a function, got Object"); + }).toThrow("[NgErr45] Argument 'fn' is not a function, got Object"); expect(function() { injector.invoke(['a', 123], {}); - }).toThrow("Argument 'fn' is not a function, got number"); + }).toThrow("[NgErr45] Argument 'fn' is not a function, got number"); }); }); @@ -268,7 +268,8 @@ describe('injector', function() { it('should error on invalid module name', function() { expect(function() { createInjector(['IDontExist'], {}); - }).toThrow("No module: IDontExist"); + }).toThrow("[NgErr47] Module 'IDontExist' is not available! You either misspelled the module name or forgot to load it."); + }); @@ -551,7 +552,7 @@ describe('injector', function() { createInjector([ {} ], {}); - }).toThrow("Argument 'module' is not a function, got Object"); + }).toThrow("[NgErr45] Argument 'module' is not a function, got Object"); }); @@ -568,7 +569,7 @@ describe('injector', function() { angular.module('TestModule', [], function(xyzzy) {}); expect(function() { createInjector(['TestModule']); - }).toThrow('Unknown provider: xyzzy from TestModule'); + }).toThrow('[NgErr1] Unknown provider: xyzzy from TestModule'); }); @@ -576,7 +577,7 @@ describe('injector', function() { function myModule(xyzzy){} expect(function() { createInjector([myModule]); - }).toThrow('Unknown provider: xyzzy from ' + myModule); + }).toThrow('[NgErr1] Unknown provider: xyzzy from ' + myModule); }); @@ -584,7 +585,7 @@ describe('injector', function() { function myModule(xyzzy){} expect(function() { createInjector([['xyzzy', myModule]]); - }).toThrow('Unknown provider: xyzzy from ' + myModule); + }).toThrow('[NgErr1] Unknown provider: xyzzy from ' + myModule); }); @@ -594,7 +595,7 @@ describe('injector', function() { $provide.factory('service', function(service){}); return function(service) {} }]) - }).toThrow('Circular dependency: service'); + }).toThrow("[NgErr4] Circular dependency found: service"); }); @@ -605,7 +606,7 @@ describe('injector', function() { $provide.factory('b', function(a){}); return function(a) {} }]) - }).toThrow('Circular dependency: b <- a'); + }).toThrow('[NgErr4] Circular dependency found: b <- a'); }); }); }); @@ -695,7 +696,7 @@ describe('injector', function() { it('should throw usefull error on wrong argument type]', function() { expect(function() { $injector.invoke({}); - }).toThrow("Argument 'fn' is not a function, got Object"); + }).toThrow("[NgErr45] Argument 'fn' is not a function, got Object"); }); }); @@ -782,7 +783,7 @@ describe('injector', function() { }]); expect(function() { $injector.get('nameProvider'); - }).toThrow("Unknown provider: nameProviderProvider <- nameProvider"); + }).toThrow("[NgErr1] Unknown provider: nameProviderProvider <- nameProvider"); }); @@ -790,7 +791,7 @@ describe('injector', function() { var $injector = createInjector([]); expect(function() { $injector.get('$provide').value('a', 'b'); - }).toThrow("Unknown provider: $provideProvider <- $provide"); + }).toThrow("[NgErr1] Unknown provider: $provideProvider <- $provide"); }); @@ -800,7 +801,7 @@ describe('injector', function() { createInjector([function($provide) { $provide.value('name', 'angular') }, instanceLookupInModule]); - }).toThrow('Unknown provider: name from ' + String(instanceLookupInModule)); + }).toThrow('[NgErr1] Unknown provider: name from ' + String(instanceLookupInModule)); }); }); }); diff --git a/test/loaderSpec.js b/test/loaderSpec.js index b2341a71ae38..802d5c1dccb8 100644 --- a/test/loaderSpec.js +++ b/test/loaderSpec.js @@ -68,6 +68,6 @@ describe('module loader', function() { it('should complain of no module', function() { expect(function() { window.angular.module('dontExist'); - }).toThrow('No module: dontExist'); + }).toThrow("[NgErr47] Module 'dontExist' is not available! You either misspelled the module name or forgot to load it."); }); }); diff --git a/test/matchers.js b/test/matchers.js index 8e4be118e122..13d284e6452e 100644 --- a/test/matchers.js +++ b/test/matchers.js @@ -147,8 +147,12 @@ beforeEach(function() { return this.actual.hasClass ? this.actual.hasClass(clazz) : angular.element(this.actual).hasClass(clazz); - } + }, + toThrowNg: function(expected) { + return jasmine.Matchers.prototype.toThrow.call(this, new RegExp('\\[NgErr\\d*\\] ' + + expected.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"))); + } }); }); diff --git a/test/ng/animatorSpec.js b/test/ng/animatorSpec.js index d4d7c0ece02b..cf5667d291f4 100644 --- a/test/ng/animatorSpec.js +++ b/test/ng/animatorSpec.js @@ -719,6 +719,6 @@ describe("$animator", function() { expect(function() { var animate = $animator($rootScope, { ngAnimate: ':' }); animate.enter(); - }).toThrow("Syntax Error: Token ':' not a primary expression at column 1 of the expression [:] starting at [:]."); + }).toThrow("[NgErr24] Syntax Error: Token ':' not a primary expression at column 1 of the expression [:] starting at [:]."); })); }); diff --git a/test/ng/cacheFactorySpec.js b/test/ng/cacheFactorySpec.js index ddfadbbc3efb..4b15fd7b8ac8 100644 --- a/test/ng/cacheFactorySpec.js +++ b/test/ng/cacheFactorySpec.js @@ -15,7 +15,7 @@ describe('$cacheFactory', function() { it('should complain if the cache id is being reused', inject(function($cacheFactory) { $cacheFactory('cache1'); expect(function() { $cacheFactory('cache1'); }). - toThrow('cacheId cache1 taken'); + toThrow("[NgErr10] CacheId 'cache1' is already taken!"); })); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 26f61357bee3..bf3d0b777c50 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -632,11 +632,11 @@ describe('$compile', function() { inject(function($compile) { expect(function() { $compile('

'); - }).toThrow('Template must have exactly one root element. was: dada'); + }).toThrow("[NgErr12] Template for directive 'noRootElem' must have exactly one root element."); expect(function() { $compile('

'); - }).toThrow('Template must have exactly one root element. was:
'); + }).toThrow("[NgErr12] Template for directive 'multiRootElem' must have exactly one root element."); // ws is ok expect(function() { @@ -985,7 +985,7 @@ describe('$compile', function() { expect(function() { $httpBackend.flush(); - }).toThrow('Failed to load template: hello.html'); + }).toThrow('[NgErr17] Failed to load template: hello.html'); expect(sortedHtml(element)).toBe('
'); } )); @@ -1005,7 +1005,7 @@ describe('$compile', function() { inject(function($compile){ expect(function() { $compile('
'); - }).toThrow('Multiple directives [sync, async] asking for template on: '+ + }).toThrow('[NgErr18] Multiple directives [sync, async] asking for template on: '+ '
'); }); }); @@ -1189,14 +1189,14 @@ describe('$compile', function() { $compile('

'); $rootScope.$digest(); expect($exceptionHandler.errors.pop().message). - toBe('Template must have exactly one root element. was: dada'); + toBe("[NgErr16] Template for directive 'template' must have exactly one root element. Template: template.html"); // multi root $templateCache.put('template.html', '
'); $compile('

'); $rootScope.$digest(); expect($exceptionHandler.errors.pop().message). - toBe('Template must have exactly one root element. was:
'); + toBe("[NgErr16] Template for directive 'template' must have exactly one root element. Template: template.html"); // ws is ok $templateCache.put('template.html', '
\n'); @@ -1456,7 +1456,7 @@ describe('$compile', function() { function($rootScope, $compile) { expect(function(){ $compile('
'); - }).toThrow('Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' + + }).toThrow('[NgErr18] Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' + '
'); }) ); @@ -1466,7 +1466,7 @@ describe('$compile', function() { function($rootScope, $compile) { expect(function(){ $compile('
'); - }).toThrow('Multiple directives [iscopeA, iscopeB] asking for isolated scope on: ' + + }).toThrow('[NgErr18] Multiple directives [iscopeA, iscopeB] asking for isolated scope on: ' + '
'); }) ); @@ -2074,7 +2074,7 @@ describe('$compile', function() { componentScope.ref = 'ignore me'; expect($rootScope.$apply). - toThrow("Non-assignable model expression: 'hello ' + name (directive: myComponent)"); + toThrow("[NgErr14] Expression ''hello ' + name' used with directive 'myComponent' is non-assignable!"); expect(componentScope.ref).toBe('hello world'); // reset since the exception was rethrown which prevented phase clearing $rootScope.$$phase = null; @@ -2150,7 +2150,7 @@ describe('$compile', function() { it('should throw on unknown definition', inject(function() { expect(function() { compile('
'); - }).toThrow('Invalid isolate scope definition for directive badDeclaration: xxx'); + }).toThrow("[NgErr15] Invalid isolate scope definition for directive 'badDeclaration'. Definition: {... attr: 'xxx' ...}"); })); it('should expose a $$isolateBindings property onto the scope', inject(function() { @@ -2233,6 +2233,25 @@ describe('$compile', function() { }); + it("should throw an error if required controller can't be found",function() { + module(function() { + directive('dep', function(log) { + return { + require: '^main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller.name); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + expect(function() { + $compile('
')($rootScope); + }).toThrow("[NgErr13] Controller 'main', required by directive 'dep', can't be found!"); + }); + }); + + it('should have optional controller on current element', function() { module(function() { directive('dep', function(log) { @@ -2415,7 +2434,7 @@ describe('$compile', function() { inject(function($compile) { expect(function() { $compile('
'); - }).toThrow('Multiple directives [first, second] asking for transclusion on: ' + + }).toThrow('[NgErr18] Multiple directives [first, second] asking for transclusion on: ' + '
'); }); }); diff --git a/test/ng/controllerSpec.js b/test/ng/controllerSpec.js index f0dcb407cae8..b041dec71607 100644 --- a/test/ng/controllerSpec.js +++ b/test/ng/controllerSpec.js @@ -124,5 +124,15 @@ describe('$controller', function() { expect(scope.foo).toBe(foo); expect(scope.foo.mark).toBe('foo'); }); + + + it('should throw an error if $scope is not provided', function() { + $controllerProvider.register('a.b.FooCtrl', function() { this.mark = 'foo'; }); + + expect(function() { + $controller('a.b.FooCtrl as foo'); + }).toThrow("[NgErr47] Cannot export controller 'a.b.FooCtrl' as 'foo'! No $scope object provided via `locals`."); + + }); }); }); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index f8898074f13e..68caf1f56ad8 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -43,7 +43,7 @@ describe('NgModelController', function() { } expect(exception.message). - toMatch(/Non-assignable model expression: 1\+2 \(\)/); + toMatch(/^\[NgErr6\] ngModel error! Expression '1\+2' is non\-assignable\. Element: $/); })); @@ -457,7 +457,7 @@ describe('input', function() { expect(function() { compileInput(''); scope.$digest(); - }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at ['']."); + }).toThrow("[NgErr24] Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at ['']."); }); @@ -548,11 +548,11 @@ describe('input', function() { }); - xit('should throw an error when scope pattern can\'t be found', function() { - compileInput(''); - - expect(function() { changeInputValueTo('xx'); }). - toThrow('Expected fooRegexp to be a RegExp but was undefined'); + it('should throw an error when scope pattern can\'t be found', function() { + expect(function() { + compileInput(''); + scope.$apply(); + }).toThrowNg('ngPattern error! Expected fooRegexp to be a RegExp but was undefined.'); }); }); diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index e7e9af3549cc..ac6ceb830f2e 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -269,7 +269,7 @@ describe('ngRepeat', function() { element = jqLite('
'); $compile(element)(scope); expect($exceptionHandler.errors.shift()[0].message). - toBe("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got 'i dont parse'."); + toBe("[NgErr7] ngRepeat error! Expected expression in form of '_item_ in _collection_[ track by _id_]' but got 'i dont parse'."); }); @@ -277,7 +277,7 @@ describe('ngRepeat', function() { element = jqLite('
'); $compile(element)(scope); expect($exceptionHandler.errors.shift()[0].message). - toBe("'item' in 'item in collection' should be identifier or (key, value) but got 'i dont parse'."); + toBe("[NgErr8] ngRepeat error! '_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got 'i dont parse'."); }); @@ -481,7 +481,7 @@ describe('ngRepeat', function() { scope.items = [a, a, a]; scope.$digest(); expect($exceptionHandler.errors.shift().message). - toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:003'); + toEqual("[NgErr50] ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: item in items, Duplicate key: object:003"); // recover scope.items = [a]; @@ -501,7 +501,7 @@ describe('ngRepeat', function() { scope.items = [d, d, d]; scope.$digest(); expect($exceptionHandler.errors.shift().message). - toEqual('Duplicates in a repeater are not allowed. Repeater: item in items key: object:009'); + toEqual("[NgErr50] ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: item in items, Duplicate key: object:009"); // recover scope.items = [a]; @@ -563,7 +563,7 @@ describe('ngRepeat ngAnimate', function() { } function applyCSS(element, cssProp, cssValue) { - element.css(cssProp, cssValue); + element.css(cssProp, cssValue); element.css(vendorPrefix + cssProp, cssValue); } @@ -592,7 +592,7 @@ describe('ngRepeat ngAnimate', function() { '
' + - '{{ item }}' + + '{{ item }}' + '
' ))($rootScope); @@ -635,7 +635,7 @@ describe('ngRepeat ngAnimate', function() { '
' + - '{{ item }}' + + '{{ item }}' + '
' ))($rootScope); diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js index 0cb2a81222d5..5a168f53de88 100644 --- a/test/ng/directive/selectSpec.js +++ b/test/ng/directive/selectSpec.js @@ -494,8 +494,8 @@ describe('select', function() { it('should throw when not formated "? for ? in ?"', function() { expect(function() { compile(''); - }).toThrow("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in" + - " _collection_ (track by _expr_)?' but got 'i dont parse'."); + }).toThrowNg("ngOptions error! Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in" + + " _collection_' but got 'i dont parse'."); }); diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 2877f92e83e3..0af38506ae46 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -32,7 +32,7 @@ describe('$interpolate', function() { }; expect(function () { $interpolate('{{err()}}')($rootScope); - }).toThrow('Error while interpolating: {{err()}}\nError: oops'); + }).toThrow("[NgErr48] $interpolate error! Can't interpolate: {{err()}}\nError: oops"); })); it('should stop interpolation when encountering an exception', inject(function($interpolate, $compile, $rootScope) { @@ -43,7 +43,7 @@ describe('$interpolate', function() { $compile(dom)($rootScope); expect(function () { $rootScope.$apply(); - }).toThrow('Error while interpolating: {{err()}}\nError: oops'); + }).toThrow("[NgErr48] $interpolate error! Can't interpolate: {{err()}}\nError: oops"); expect(dom[0].innerHTML).toEqual('2'); expect(dom[1].innerHTML).toEqual('{{err()}}'); expect(dom[2].innerHTML).toEqual('{{1 + 2}}'); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index c4a88fd93eb0..4aaa4d51520c 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -203,7 +203,7 @@ describe('$location', function() { expect(function() { url.$$parse('http://other.server.org/path#/path'); - }).toThrow('Invalid url "http://other.server.org/path#/path", missing path prefix "http://server.org/base/".'); + }).toThrow('[NgErr21] $location error! Invalid url "http://other.server.org/path#/path", missing path prefix "http://server.org/base/".'); }); @@ -212,7 +212,7 @@ describe('$location', function() { expect(function() { url.$$parse('http://server.org/path#/path'); - }).toThrow('Invalid url "http://server.org/path#/path", missing path prefix "http://server.org/base/".'); + }).toThrow('[NgErr21] $location error! Invalid url "http://server.org/path#/path", missing path prefix "http://server.org/base/".'); }); @@ -325,14 +325,14 @@ describe('$location', function() { it('should throw error when invalid server url given', function() { expect(function() { url.$$parse('http://server.org/path#/path'); - }).toThrow('Invalid url "http://server.org/path#/path", does not start with "http://www.server.org:1234/base".'); + }).toThrow('[NgErr22] $location error! Invalid url "http://server.org/path#/path", does not start with "http://www.server.org:1234/base".'); }); it('should throw error when invalid hashbang prefix given', function() { expect(function() { url.$$parse('http://www.server.org:1234/base#/path'); - }).toThrow('Invalid url "http://www.server.org:1234/base#/path", missing hash prefix "#!".'); + }).toThrow('[NgErr49] $location error! Invalid url "http://www.server.org:1234/base#/path", missing hash prefix "#!".'); }); diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 7d3315b3cea4..c3cb0ce11f4a 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -156,11 +156,11 @@ describe('parser', function() { it('should throws exception for invalid exponent', function() { expect(function() { lex("0.5E-"); - }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-].')); + }).toThrow(new Error('[NgErr23] Lexer Error: Invalid exponent at column 4 in expression [0.5E-].')); expect(function() { lex("0.5E-A"); - }).toThrow(new Error('Lexer Error: Invalid exponent at column 4 in expression [0.5E-A].')); + }).toThrow(new Error('[NgErr23] Lexer Error: Invalid exponent at column 4 in expression [0.5E-A].')); }); it('should tokenize number starting with a dot', function() { @@ -171,7 +171,7 @@ describe('parser', function() { it('should throw error on invalid unicode', function() { expect(function() { lex("'\\u1''bla'"); - }).toThrow(new Error("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla'].")); + }).toThrow(new Error("[NgErr23] Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla'].")); }); }); @@ -304,7 +304,7 @@ describe('parser', function() { expect(function() { scope.$eval("1|nonexistent"); - }).toThrow(new Error("Unknown provider: nonexistentFilterProvider <- nonexistentFilter")); + }).toThrow(new Error("[NgErr1] Unknown provider: nonexistentFilterProvider <- nonexistentFilter")); scope.offset = 3; expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); @@ -492,7 +492,7 @@ describe('parser', function() { it('should throw exception on non-closed bracket', function() { expect(function() { scope.$eval('[].count('); - }).toThrow('Unexpected end of expression: [].count('); + }).toThrow('[NgErr25] Unexpected end of expression: [].count('); }); it('should evaluate double negation', function() { diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index cac7c16017dc..0b258c83a74f 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -215,7 +215,7 @@ describe('Scope', function() { expect(function() { $rootScope.$digest(); - }).toThrow('100 $digest() iterations reached. Aborting!\n'+ + }).toThrow('[NgErr27] 100 $digest() iterations reached. Aborting!\n'+ 'Watchers fired in the last 5 iterations: ' + '[["a; newVal: 96; oldVal: 95","b; newVal: 97; oldVal: 96"],' + '["a; newVal: 97; oldVal: 96","b; newVal: 98; oldVal: 97"],' + @@ -299,7 +299,7 @@ describe('Scope', function() { $rootScope.$watch('name', function() { expect(function() { $rootScope.$digest(); - }).toThrow('$digest already in progress'); + }).toThrow('[NgErr28] $digest already in progress'); callCount++; }); $rootScope.name = 'a'; @@ -759,7 +759,7 @@ describe('Scope', function() { $rootScope.$apply(function() { $rootScope.$apply(); }); - }).toThrow('$apply already in progress'); + }).toThrow('[NgErr28] $apply already in progress'); })); @@ -771,7 +771,7 @@ describe('Scope', function() { $rootScope.$apply(); }); }); - }).toThrow('$digest already in progress'); + }).toThrow('[NgErr28] $digest already in progress'); })); @@ -781,7 +781,7 @@ describe('Scope', function() { childScope1.$watch('x', function() { childScope1.$apply(); }); - expect(function() { childScope1.$apply(); }).toThrow('$digest already in progress'); + expect(function() { childScope1.$apply(); }).toThrow('[NgErr28] $digest already in progress'); })); @@ -798,7 +798,7 @@ describe('Scope', function() { expect(function() { childScope2.$apply(function() { childScope2.x = 'something'; - }); }).toThrow('$digest already in progress'); + }); }).toThrow('[NgErr28] $digest already in progress'); })); }); }); diff --git a/test/ngErrorSpec.js b/test/ngErrorSpec.js new file mode 100644 index 000000000000..81773322841c --- /dev/null +++ b/test/ngErrorSpec.js @@ -0,0 +1,80 @@ +'use strict'; + +describe('ngError', function() { + + var supportStackTraces = function() { + var e = new Error(); + return isDefined(e.stack); + }; + + it('should return an Error instance', function() { + var myError = ngError(); + expect(myError instanceof Error).toBe(true); + }); + + + it('should generate stack trace at the frame where ngError was called', function() { + var myError; + + function someFn() { + function nestedFn() { + myError = ngError(0, "I fail!"); + } + nestedFn(); + } + + someFn(); + + // only Chrome, Firefox have stack + if (!supportStackTraces()) return; + + expect(myError.stack).toMatch(/^[.\s\S]+nestedFn[.\s\S]+someFn.+/); + }); + + + it('should interpolate string arguments without quotes', function() { + var myError = ngError(26, 'This {0} is "{1}"', 'foo', 'bar'); + expect(myError.message).toBe('[NgErr26] This foo is "bar"'); + }); + + + it('should interpolate non-string arguments', function() { + var arr = [1, 2, 3], + obj = {a: 123, b: 'baar'}, + anonFn = function(something) { return something; }, + namedFn = function foo(something) { return something; }, + myError; + + myError = ngError(26, 'arr: {0}; obj: {1}; anonFn: {2}; namedFn: {3}', + arr, obj, anonFn, namedFn); + + expect(myError.message).toContain('[NgErr26] arr: [1,2,3]; obj: {"a":123,"b":"baar"};'); + // IE does not add space after "function" + expect(myError.message).toMatch(/anonFn: function\s?\(something\);/); + expect(myError.message).toContain('namedFn: function foo(something)'); + }); + + + it('should not suppress falsy objects', function() { + var myError = ngError(26, 'false: {0}; zero: {1}; null: {2}; undefined: {3}; emptyStr: {4}', + false, 0, null, undefined, ''); + expect(myError.message). + toBe('[NgErr26] false: false; zero: 0; null: null; undefined: undefined; emptyStr: '); + }); + + + it('should preserve interpolation markers when fewer arguments than needed are provided', function() { + // this way we can easily see if we are passing fewer args than needed + + var foo = 'Fooooo', + myError = ngError(26, 'This {0} is {1} on {2}', foo); + + expect(myError.message).toBe('[NgErr26] This Fooooo is {1} on {2}'); + }); + + + it('should pass through the message if no interpolation is needed', function() { + var myError = ngError(26, 'Something horrible happened!'); + expect(myError.message).toBe('[NgErr26] Something horrible happened!'); + }); +}); From f2472174dee2824ec11b6d879b48e33ae313a356 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 24 May 2013 12:41:38 -0700 Subject: [PATCH 04/23] feat($compile): support multi-element directive By appending directive-start and directive-end to a directive it is now possible to have the directive act on a group of elements. It is now possible to iterate over multiple elements like so: I get repeatedI also get repeated
--- src/jqLite.js | 50 ++++++++------ src/ng/animator.js | 15 +++-- src/ng/compile.js | 123 ++++++++++++++++++++++++++++------ src/ng/directive/ngRepeat.js | 2 +- test/jqLiteSpec.js | 12 ++-- test/ng/compileSpec.js | 125 +++++++++++++++++++++++++++++++++++ 6 files changed, 276 insertions(+), 51 deletions(-) diff --git a/src/jqLite.js b/src/jqLite.js index cf9d1fa168e8..4959a2ed36b9 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -165,7 +165,8 @@ function JQLite(element) { div.innerHTML = '
 
' + element; // IE insanity to make NoScope elements work! div.removeChild(div.firstChild); // remove the superfluous div JQLiteAddNodes(this, div.childNodes); - this.remove(); // detach the elements from the temporary DOM div. + var fragment = jqLite(document.createDocumentFragment()); + fragment.append(this); // detach the elements from the temporary DOM div. } else { JQLiteAddNodes(this, element); } @@ -456,24 +457,26 @@ forEach({ } }, - text: extend((msie < 9) - ? function(element, value) { - if (element.nodeType == 1 /** Element */) { - if (isUndefined(value)) - return element.innerText; - element.innerText = value; - } else { - if (isUndefined(value)) - return element.nodeValue; - element.nodeValue = value; - } + text: (function() { + var NODE_TYPE_TEXT_PROPERTY = []; + if (msie < 9) { + NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ + } else { + NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ + NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ + } + getText.$dv = ''; + return getText; + + function getText(element, value) { + var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType] + if (isUndefined(value)) { + return textProp ? element[textProp] : ''; } - : function(element, value) { - if (isUndefined(value)) { - return element.textContent; - } - element.textContent = value; - }, {$dv:''}), + element[textProp] = value; + } + })(), val: function(element, value) { if (isUndefined(value)) { @@ -518,8 +521,14 @@ forEach({ return this; } else { // we are a read, so read the first child. - if (this.length) - return fn(this[0], arg1, arg2); + var value = fn.$dv; + // Only if we have $dv do we iterate over all, otherwise it is just the first element. + var jj = value == undefined ? Math.min(this.length, 1) : this.length; + for (var j = 0; j < jj; j++) { + var nodeValue = fn(this[j], arg1, arg2); + value = value ? value + nodeValue : nodeValue; + } + return value; } } else { // we are a write, so apply to all children @@ -529,7 +538,6 @@ forEach({ // return self for chaining return this; } - return fn.$dv; }; }); diff --git a/src/ng/animator.js b/src/ng/animator.js index 2965717bdc3d..2b399813264b 100644 --- a/src/ng/animator.js +++ b/src/ng/animator.js @@ -395,11 +395,16 @@ var $AnimatorProvider = function() { } function insert(element, parent, after) { - if (after) { - after.after(element); - } else { - parent.append(element); - } + var afterNode = after && after[after.length - 1]; + var parentNode = parent && parent[0] || afterNode && afterNode.parentNode; + var afterNextSibling = afterNode && afterNode.nextSibling; + forEach(element, function(node) { + if (afterNextSibling) { + parentNode.insertBefore(node, afterNextSibling); + } else { + parentNode.appendChild(node); + } + }); } function remove(element) { diff --git a/src/ng/compile.js b/src/ng/compile.js index be22482b033b..2dddf82dcbff 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -358,11 +358,12 @@ function $CompileProvider($provide) { // jquery always rewraps, whereas we need to preserve the original selector so that we can modify it. $compileNodes = jqLite($compileNodes); } + var tempParent = document.createDocumentFragment(); // We can not compile top level text elements since text nodes can be merged and we will // not be able to attach scope data to them, so we will wrap them in forEach($compileNodes, function(node, index){ if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = jqLite(node).wrap('').parent()[0]; + $compileNodes[index] = node = jqLite(node).wrap('').parent()[0]; } }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority); @@ -420,7 +421,7 @@ function $CompileProvider($provide) { attrs = new Attributes(); // we must always refer to nodeList[i] since the nodes can be replaced underneath us. - directives = collectDirectives(nodeList[i], [], attrs, maxPriority); + directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined); nodeLinkFn = (directives.length) ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement) @@ -509,6 +510,10 @@ function $CompileProvider($provide) { // iterate over the attributes for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { + var attrStartName; + var attrEndName; + var index; + attr = nAttrs[j]; if (attr.specified) { name = attr.name; @@ -517,6 +522,11 @@ function $CompileProvider($provide) { if (NG_ATTR_BINDING.test(ngAttrName)) { name = ngAttrName.substr(6).toLowerCase(); } + if ((index = ngAttrName.lastIndexOf('Start')) != -1 && index == ngAttrName.length - 5) { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); + } nName = directiveNormalize(name.toLowerCase()); attrsMap[nName] = name; attrs[nName] = value = trim((msie && name == 'href') @@ -526,7 +536,7 @@ function $CompileProvider($provide) { attrs[nName] = true; // presence means true } addAttrInterpolateDirective(node, directives, value, nName); - addDirective(directives, nName, 'A', maxPriority); + addDirective(directives, nName, 'A', maxPriority, attrStartName, attrEndName); } } @@ -565,6 +575,47 @@ function $CompileProvider($provide) { return directives; } + /** + * Given a node with an directive-start it collects all of the siblings until it find directive-end. + * @param node + * @param attrStart + * @param attrEnd + * @returns {*} + */ + function groupScan(node, attrStart, attrEnd) { + var nodes = []; + var depth = 0; + if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { + var startNode = node; + do { + if (!node) { + throw ngError(51, "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd); + } + if (node.hasAttribute(attrStart)) depth++; + if (node.hasAttribute(attrEnd)) depth--; + nodes.push(node); + node = node.nextSibling; + } while (depth > 0); + } else { + nodes.push(node); + } + return jqLite(nodes); + } + + /** + * Wrapper for linking function which converts normal linking function into a grouped + * linking function. + * @param linkFn + * @param attrStart + * @param attrEnd + * @returns {Function} + */ + function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { + return function(scope, element, attrs, controllers) { + element = groupScan(element[0], attrStart, attrEnd); + return linkFn(scope, element, attrs, controllers); + } + } /** * Once the directives have been collected, their compile functions are executed. This method @@ -601,6 +652,13 @@ function $CompileProvider($provide) { // executes all directives on the current element for(var i = 0, ii = directives.length; i < ii; i++) { directive = directives[i]; + var attrStart = directive.$$start; + var attrEnd = directive.$$end; + + // collect multiblock sections + if (attrStart) { + $compileNode = groupScan(compileNode, attrStart, attrEnd) + } $template = undefined; if (terminalPriority > directive.priority) { @@ -631,11 +689,11 @@ function $CompileProvider($provide) { transcludeDirective = directive; terminalPriority = directive.priority; if (directiveValue == 'element') { - $template = jqLite(compileNode); + $template = groupScan(compileNode, attrStart, attrEnd) $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite($template[0]), compileNode); + replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority); } else { $template = jqLite(JQLiteClone(compileNode)).contents(); @@ -699,9 +757,9 @@ function $CompileProvider($provide) { try { linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); if (isFunction(linkFn)) { - addLinkFns(null, linkFn); + addLinkFns(null, linkFn, attrStart, attrEnd); } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post); + addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); } } catch (e) { $exceptionHandler(e, startingTag($compileNode)); @@ -723,12 +781,14 @@ function $CompileProvider($provide) { //////////////////// - function addLinkFns(pre, post) { + function addLinkFns(pre, post, attrStart, attrEnd) { if (pre) { + if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); pre.require = directive.require; preLinkFns.push(pre); } if (post) { + if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); post.require = directive.require; postLinkFns.push(post); } @@ -907,8 +967,8 @@ function $CompileProvider($provide) { * * `M`: comment * @returns true if directive was added. */ - function addDirective(tDirectives, name, location, maxPriority) { - var match = false; + function addDirective(tDirectives, name, location, maxPriority, startAttrName, endAttrName) { + var match = null; if (hasDirectives.hasOwnProperty(name)) { for(var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i directive.priority) && directive.restrict.indexOf(location) != -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + } tDirectives.push(directive); - match = true; + match = directive; } } catch(e) { $exceptionHandler(e); } } @@ -1120,30 +1183,50 @@ function $CompileProvider($provide) { * * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes * in the root of the tree. - * @param {JqLite} $element The jqLite element which we are going to replace. We keep the shell, + * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep the shell, * but replace its DOM node reference. * @param {Node} newNode The new DOM node. */ - function replaceWith($rootElement, $element, newNode) { - var oldNode = $element[0], - parent = oldNode.parentNode, + function replaceWith($rootElement, elementsToRemove, newNode) { + var firstElementToRemove = elementsToRemove[0], + removeCount = elementsToRemove.length, + parent = firstElementToRemove.parentNode, i, ii; if ($rootElement) { for(i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == oldNode) { - $rootElement[i] = newNode; + if ($rootElement[i] == firstElementToRemove) { + $rootElement[i++] = newNode; + for (var j = i, j2 = j + removeCount - 1, + jj = $rootElement.length; + j < jj; j++, j2++) { + if (j2 < jj) { + $rootElement[j] = $rootElement[j2]; + } else { + delete $rootElement[j]; + } + } + $rootElement.length -= removeCount - 1; break; } } } if (parent) { - parent.replaceChild(newNode, oldNode); + parent.replaceChild(newNode, firstElementToRemove); + } + var fragment = document.createDocumentFragment(); + fragment.appendChild(firstElementToRemove); + newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; + for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { + var element = elementsToRemove[k]; + jqLite(element).remove(); // must do this way to clean up expando + fragment.appendChild(element); + delete elementsToRemove[k]; } - newNode[jqLite.expando] = oldNode[jqLite.expando]; - $element[0] = newNode; + elementsToRemove[0] = newNode; + elementsToRemove.length = 1 } }]; } diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 34d32f59adb2..6c2da071956d 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -258,7 +258,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { if (lastBlockMap.hasOwnProperty(key)) { block = lastBlockMap[key]; animate.leave(block.element); - block.element[0][NG_REMOVED] = true; + forEach(block.element, function(element) { element[NG_REMOVED] = true}); block.scope.$destroy(); } } diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index 1ebe6ad48943..70c18d35600f 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -56,6 +56,9 @@ describe('jqLite', function() { it('should allow construction with html', function() { var nodes = jqLite('
1
2'); + expect(nodes[0].parentNode).toBeDefined(); + expect(nodes[0].parentNode.nodeType).toBe(11); /** Document Fragment **/; + expect(nodes[0].parentNode).toBe(nodes[1].parentNode); expect(nodes.length).toEqual(2); expect(nodes[0].innerHTML).toEqual('1'); expect(nodes[1].innerHTML).toEqual('2'); @@ -644,12 +647,13 @@ describe('jqLite', function() { it('should read/write value', function() { - var element = jqLite('
abc
'); - expect(element.length).toEqual(1); - expect(element[0].innerHTML).toEqual('abc'); + var element = jqLite('
ab
c'); + expect(element.length).toEqual(2); + expect(element[0].innerHTML).toEqual('ab'); + expect(element[1].innerHTML).toEqual('c'); expect(element.text()).toEqual('abc'); expect(element.text('xyz') == element).toBeTruthy(); - expect(element.text()).toEqual('xyz'); + expect(element.text()).toEqual('xyzxyz'); }); }); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index bf3d0b777c50..95b2ab72d93f 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -2718,4 +2718,129 @@ describe('$compile', function() { expect(element.attr('test4')).toBe('Misko'); })); }); + + + describe('multi-element directive', function() { + it('should group on link function', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '' + + '' + + '
')($rootScope); + $rootScope.$digest(); + var spans = element.find('span'); + expect(spans.eq(0).css('display')).toBe('none'); + expect(spans.eq(1).css('display')).toBe('none'); + })); + + + it('should group on compile function', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '{{i}}A' + + '{{i}}B;' + + '
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('1A1B;2A2B;'); + })); + + + it('should group on $root compile function', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '{{i}}A' + + '{{i}}B;' + + '
')($rootScope); + $rootScope.$digest(); + element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level. + expect(element.text()).toEqual('1A1B;2A2B;'); + })); + + + it('should group on nested groups', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '
{{i}}A
' + + '' + + '' + + '
{{i}}B;
' + + '
')($rootScope); + $rootScope.$digest(); + element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level. + expect(element.text()).toEqual('1A..1B;2A..2B;'); + })); + + + it('should group on nested groups', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '
{{i}}(
' + + '{{j}}-' + + '{{j}}' + + '
){{i}};
' + + '
')($rootScope); + $rootScope.$digest(); + element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level. + expect(element.text()).toEqual('1(2-23-3)1;2(2-23-3)2;'); + })); + + + it('should throw error if unterminated', function () { + module(function($compileProvider) { + $compileProvider.directive('foo', function() { + return { + }; + }); + }); + inject(function($compile, $rootScope) { + expect(function() { + element = $compile( + '
' + + '' + + '
'); + }).toThrow("[NgErr51] Unterminated attribute, found 'foo-start' but no matching 'foo-end' found."); + }); + }); + + + it('should throw error if unterminated', function () { + module(function($compileProvider) { + $compileProvider.directive('foo', function() { + return { + }; + }); + }); + inject(function($compile, $rootScope) { + expect(function() { + element = $compile( + '
' + + '' + + '
'); + }).toThrow("[NgErr51] Unterminated attribute, found 'foo-start' but no matching 'foo-end' found."); + }); + }); + + + it('should support data- and x- prefix', inject(function($compile, $rootScope) { + $rootScope.show = false; + element = $compile( + '
' + + '' + + '' + + '' + + '' + + '
')($rootScope); + $rootScope.$digest(); + var spans = element.find('span'); + expect(spans.eq(0).css('display')).toBe('none'); + expect(spans.eq(1).css('display')).toBe('none'); + expect(spans.eq(2).css('display')).toBe('none'); + expect(spans.eq(3).css('display')).toBe('none'); + })); + }); }); From 513e79a1e63cbde1b70325b569bd299a22c6cc95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 20 May 2013 21:00:12 -0400 Subject: [PATCH 05/23] feat(ngdocs): provide support for inline variable hinting --- docs/spec/ngdocSpec.js | 5 +++++ docs/src/ngdoc.js | 4 ++++ src/ng/directive/ngRepeat.js | 10 ++++++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/spec/ngdocSpec.js b/docs/spec/ngdocSpec.js index 3cd9834b9423..9eed24ca28db 100644 --- a/docs/spec/ngdocSpec.js +++ b/docs/spec/ngdocSpec.js @@ -150,6 +150,11 @@ describe('ngdoc', function() { toMatch('\n\n

One

\n\n/); + }); + it('should ignore nested doc widgets', function() { expect(new Doc().markdown( 'before
\n' + diff --git a/docs/src/ngdoc.js b/docs/src/ngdoc.js index 7c07f00ffdcf..d66f33e64b0c 100644 --- a/docs/src/ngdoc.js +++ b/docs/src/ngdoc.js @@ -214,6 +214,10 @@ Doc.prototype = { (title || url).replace(/^#/g, '').replace(/\n/g, ' ') + (isAngular ? '' : '') + ''; + }). + replace(/{@type\s+(\S+)(?:\s+(\S+))?}/g, function(_, type, url) { + url = url || '#'; + return '' + type + ''; }); }); text = parts.join(''); diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 6c2da071956d..89b402c87b20 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -11,10 +11,12 @@ * * Special properties are exposed on the local scope of each template instance, including: * - * * `$index` – `{number}` – iterator offset of the repeated element (0..length-1) - * * `$first` – `{boolean}` – true if the repeated element is first in the iterator. - * * `$middle` – `{boolean}` – true if the repeated element is between the first and last in the iterator. - * * `$last` – `{boolean}` – true if the repeated element is last in the iterator. + * | Variable | Type | Details | + * |===========|=================|=============================================================================| + * | `$index` | {@type number} | iterator offset of the repeated element (0..length-1) | + * | `$first` | {@type boolean} | true if the repeated element is first in the iterator. | + * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. | + * | `$last` | {@type boolean} | true if the repeated element is last in the iterator. | * * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**, * **leave** and **move** effects. From f004b872a583c0221a4f947153d5c2fc794ed10c Mon Sep 17 00:00:00 2001 From: Gias Kay Lee Date: Thu, 23 May 2013 14:02:52 +0800 Subject: [PATCH 06/23] docs(): Rectify animator.animate documentation --- src/ng/animator.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ng/animator.js b/src/ng/animator.js index 2b399813264b..b588de2445bb 100644 --- a/src/ng/animator.js +++ b/src/ng/animator.js @@ -262,7 +262,8 @@ var $AnimatorProvider = function() { * @description * Triggers a custom animation event to be executed on the given element * - * @param {jQuery/jqLite element} element that will be animated + * @param {string} event the name of the custom event + * @param {jQuery/jqLite element} element the element that will be animated */ animator.animate = function(event, element) { animateActionFactory(event, noop, noop)(element); From afc325c7aceff8ac685a78d5a0b13c79834e73ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Sun, 26 May 2013 20:28:47 -0400 Subject: [PATCH 07/23] fix($animator): ensure $animator calculates the highest duration + delay for and transitions and animations together --- src/ng/animator.js | 37 +++++++++++++++++++------------------ test/ng/animatorSpec.js | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/ng/animator.js b/src/ng/animator.js index b588de2445bb..a9ec161611db 100644 --- a/src/ng/animator.js +++ b/src/ng/animator.js @@ -345,28 +345,29 @@ var $AnimatorProvider = function() { var ELEMENT_NODE = 1; forEach(element, function(element) { if (element.nodeType == ELEMENT_NODE) { - var w3cProp = w3cTransitionProp, - vendorProp = vendorTransitionProp, - iterations = 1, - elementStyles = $window.getComputedStyle(element) || {}; + var elementStyles = $window.getComputedStyle(element) || {}; - //use CSS Animations over CSS Transitions - if(parseFloat(elementStyles[w3cAnimationProp + durationKey]) > 0 || - parseFloat(elementStyles[vendorAnimationProp + durationKey]) > 0) { - w3cProp = w3cAnimationProp; - vendorProp = vendorAnimationProp; - iterations = Math.max(parseInt(elementStyles[w3cProp + animationIterationCountKey]) || 0, - parseInt(elementStyles[vendorProp + animationIterationCountKey]) || 0, - iterations); - } + var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]), + parseMaxTime(elementStyles[vendorTransitionProp + delayKey])); + + var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]), + parseMaxTime(elementStyles[vendorAnimationProp + delayKey])); - var parsedDelay = Math.max(parseMaxTime(elementStyles[w3cProp + delayKey]), - parseMaxTime(elementStyles[vendorProp + delayKey])); + var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), + parseMaxTime(elementStyles[vendorTransitionProp + durationKey])); - var parsedDuration = Math.max(parseMaxTime(elementStyles[w3cProp + durationKey]), - parseMaxTime(elementStyles[vendorProp + durationKey])); + var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]), + parseMaxTime(elementStyles[vendorAnimationProp + durationKey])); + + if(animationDuration > 0) { + animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0, + parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0, + 1); + } - duration = Math.max(parsedDelay + (iterations * parsedDuration), duration); + duration = Math.max(animationDelay + animationDuration, + transitionDelay + transitionDuration, + duration); } }); $window.setTimeout(done, duration * 1000); diff --git a/test/ng/animatorSpec.js b/test/ng/animatorSpec.js index cf5667d291f4..439836091587 100644 --- a/test/ng/animatorSpec.js +++ b/test/ng/animatorSpec.js @@ -666,6 +666,33 @@ describe("$animator", function() { expect(element[0].style.display).toBe(''); })); + it("should select the highest duration and delay", + inject(function($animator, $rootScope, $compile, $sniffer) { + var styles = 'transition:1s linear all 2s;' + + vendorPrefix + 'transition:1s linear all 2s;' + + 'animation:my_ani 10s 1s;' + + vendorPrefix + 'animation:my_ani 10s 1s;'; + + element = $compile(html('
foo
'))($rootScope); + + var animator = $animator($rootScope, { + ngAnimate : '{show: \'inline-show\'}' + }); + + element.css('display','none'); + expect(element.css('display')).toBe('none'); + + animator.show(element); + if ($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(11000).process(); + } + else { + expect(window.setTimeout.queue.length).toBe(0); + } + expect(element[0].style.display).toBe(''); + })); + it("should finish the previous transition when a new animation is started", inject(function($animator, $rootScope, $compile, $sniffer) { var style = 'transition: 1s linear all;' + From 44946f9fb3a60fe8bda2860dc52c387e82f0650e Mon Sep 17 00:00:00 2001 From: Jeff Cross Date: Wed, 29 May 2013 13:44:59 -0700 Subject: [PATCH 08/23] fix(jqLite): Added optional name arg in removeData jQuery's API for removeData allows a second 'name' argument to just remove the property by that name from an element's data. The absence of this argument was causing some features not to work correctly when combining multiple directives, such as ng-click, ng-show, and ng-animate. --- src/jqLite.js | 7 ++++++- test/jqLiteSpec.js | 19 +++++++++++++++++++ test/ng/animatorSpec.js | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/jqLite.js b/src/jqLite.js index 4959a2ed36b9..4c68cdef325f 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -204,11 +204,16 @@ function JQLiteUnbind(element, type, fn) { } } -function JQLiteRemoveData(element) { +function JQLiteRemoveData(element, name) { var expandoId = element[jqName], expandoStore = jqCache[expandoId]; if (expandoStore) { + if (name) { + delete jqCache[expandoId].data[name]; + return; + } + if (expandoStore.handle) { expandoStore.events.$destroy && expandoStore.handle({}, '$destroy'); JQLiteUnbind(element); diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index 70c18d35600f..f121e1a055e0 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -246,6 +246,25 @@ describe('jqLite', function() { expect(jqLite(c).data('prop')).toBeUndefined(); }); + it('should only remove the specified value when providing a property name to removeData', function () { + var selected = jqLite(a); + + expect(selected.data('prop1')).toBeUndefined(); + + selected.data('prop1', 'value'); + selected.data('prop2', 'doublevalue'); + + expect(selected.data('prop1')).toBe('value'); + expect(selected.data('prop2')).toBe('doublevalue'); + + selected.removeData('prop1'); + + expect(selected.data('prop1')).toBeUndefined(); + expect(selected.data('prop2')).toBe('doublevalue'); + + selected.removeData('prop2'); + }); + it('should emit $destroy event if element removed via remove()', function() { var log = ''; var element = jqLite(a); diff --git a/test/ng/animatorSpec.js b/test/ng/animatorSpec.js index 439836091587..63fcf5c3ea02 100644 --- a/test/ng/animatorSpec.js +++ b/test/ng/animatorSpec.js @@ -345,11 +345,33 @@ describe("$animator", function() { }); child.css('display','none'); + element.data('foo', 'bar'); animator.show(element); window.setTimeout.expect(1).process(); + animator.hide(element); expect(element.hasClass('animation-cancelled')).toBe(true); + expect(element.data('foo')).toEqual('bar'); + })); + + it("should NOT clobber all data on an element when animation is finished", + inject(function($animator, $rootScope) { + $animator.enabled(true); + + animator = $animator($rootScope, { + ngAnimate : '{hide: \'custom-delay\', show: \'custom-delay\'}' + }); + + child.css('display','none'); + element.data('foo', 'bar'); + + animator.show(element); + window.setTimeout.expect(1).process(); + + animator.hide(element); + + expect(element.data('foo')).toEqual('bar'); })); From cd3410ab668ba369be1291516febf427b9da61d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Galfas=C3=B3?= Date: Fri, 24 May 2013 22:43:09 -0300 Subject: [PATCH 09/23] chore(docs): add reference to the blog Add a reference to the blog at the documentation. --- docs/src/templates/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/templates/index.html b/docs/src/templates/index.html index 6054f81344e0..a3edba30f0e9 100644 --- a/docs/src/templates/index.html +++ b/docs/src/templates/index.html @@ -155,6 +155,7 @@ Discuss