Compare commits

...

43 Commits

Author SHA1 Message Date
Tobias Bosch 10644432ca fix(input): register builtin parsers/formatters before anyone else
Previously, builtin parsers/formatters for e.g. `input[date]`
or `input[number]` were added in the post linking phase to `ngModelController`,
which in most cases was after a custom formatter/parser was registered.

This commit registers builtin parsers/formatters already
in the pre linking phase. With that builtin
parsers run first, and builtin formatters run last.

Closes #9218
Closes #9358
2014-10-01 17:37:40 -07:00
Tobias Bosch a0bfdd0d60 fix(input): correctly handle invalid model values for input[date/time/…]
Similar to `input[number]` Angular will throw if the model value
for a `input[date]` is not a `Date` object.
For `Invalid Date`s (dates whose `getTime()` is `NaN`) `input[date]`
will render an empty string.

Closes #8949
Closes #9375
2014-10-01 16:12:05 -07:00
Igor Minar 3624e3800f fix(ngView): use animation promises ensure that only one leave animation occurs at a time
the tracking depended on a local flag variable, which was susceptible to corruption due to
race conditions.

using promises ensures that the previousLeaveAnimation is nulled out only if it hasn't been
canceled yet.

Closes #9355
Closes #7606
Closes #9374
2014-10-01 15:19:29 -07:00
Jason Bedard b1ee5386d5 perf(ngForm,ngModel): move initial addClass to the compile phase
Closes #8268
2014-09-30 21:45:41 -07:00
ltrillaud ab80cd9066 fix(compile): sanitize srcset attribute
Applies similar sanitization as is applie to img[src] to img[srcset],
while adapting to the different semantics and syntax of srcset.
2014-09-30 16:32:58 -07:00
Maxi Ferreira 8199f4dbde docs(guide/forms): improve example
When explaining ng-model-options, there's no print of `user.data` to show
the difference between the default behaviour and updateOn: 'blur'
2014-09-30 14:46:22 -07:00
Brian Feister 313d7956e4 docs(minerr/unpr): note that ctrls cant depend on other ctrls 2014-09-30 14:36:08 -07:00
Tobias Bosch b9479ee73b chore(ngCsp): add e2e tests
Also changes `connect:devserver` and `connect:testserver` to conditionally serve files with csp headers when the path contains `.csp` somewhere.

Closes #9136
Closes #9059
2014-09-30 14:10:19 -07:00
active-low 769a00dc86 docs(guide/concepts): improve readability 2014-09-30 12:56:45 -07:00
Adam Humphrey 6593c2371e docs(readme): fix formatting 2014-09-30 12:35:44 -07:00
thorn0 8b54524c07 docs($compile): fix a broken link 2014-09-30 12:33:01 -07:00
Justin Walsh 66bb5aa41c docs(guide/compiler): change span to block element in draggable example
The draggable example does not work as expected in Chrome (37.0.2062.124 m).
The span disappears when dragged beyond what appears to be a small area.
Changing the span to a block element (with a width of 65px) resolves this issue.
An alternative solution would be to change the span to a div.
2014-09-30 12:29:18 -07:00
thorn0 b186709003 docs($compile): add header to example 2014-09-30 11:40:48 -07:00
Caitlin Potter a27d827c22 fix($compile): get $$observe listeners array as own property
Prevent accidentally treating a builtin function from Object.prototype as the binding object, and thus
preventing the compiler from throwing when using attribute binding names which match a property of the
Object prototype.

Closes #9343
Closes #9345
2014-09-30 13:05:09 -04:00
Caitlin Potter a1648a76c0 docs(CHANGELOG.md): put <base> in codeblock
Prevent the tag from being processed (and not rendered). Thanks @davidlehn.

Closes #9331
2014-09-29 17:16:11 -04:00
Brian Ford 2bcd02dc1a fix(select): make ctrl.hasOption method consistent
Prior to this fix, options added to a select by ngOptions would not cause
`selectCtrl.hasOption` to return `true`

Closes #8761
2014-09-29 13:58:03 -07:00
Peter Bacon Darwin b0033a44bd chore(npm-shrinkwrap): update to dgeni-packages 0.10.0 2014-09-29 21:56:22 +01:00
Julie Ralph 76b755f3cb chore(e2e): bump protractor to version 1.3.1 2014-09-29 10:15:49 -07:00
Lucas Galfaso 6303c3dcf6 fix($compile): Resolve leak with asynchronous compilation
Stop an asynchronous compilation when this is performed on an
already destroyed scope

Closes #9199
Closes #9079
Closes #8504
Closes #9197
2014-09-29 12:47:21 +01:00
Lucas Galfaso cd2cfafcab refactor($scope): prevent multiple calls to listener on $destroy
Prevent isolated scopes from having listeners that get called
multiple times when on `$destroy`
2014-09-29 12:41:36 +01:00
Richard Littauer 86d33c5f9d docs(CONTRIBUTING.md): Added a not about type
It's important that we let people use the GitHub editing interface without being 100% strict about how to name the commit changes. Otherwise, it is basically a barrier to entry and highly discouraging for new people who may just be trying to fix a spelling error. Since it is possible for contributors to edit the commit message before merging it into master, for people who are new to the commit styling system, we should be lenient about minor infractions like forgetting to put docs: in front of a message. 

CF: https://github.com/angular-ui/bootstrap/pull/2635#issuecomment-57117579
2014-09-29 02:50:49 -07:00
Georgios Kalpakas eb935e6be0 test($http): fix typo in spec name
'applyAapply' -> 'applyAsync'.

Closes #9323
2014-09-28 20:33:05 -04:00
Jason Bedard b119251827 perf($rootScope): moving internal queues out of the Scope instances
Closes #9071
2014-09-27 08:19:15 -07:00
Jason Bedard 5572b40b15 refactor($parse): change 'this' to a $parse keyword instead of scope field
BREAKING CHANGE:
- $scope['this'] no longer exits on the $scope object
- $parse-ed expressions no longer allow chaining 'this' such as this['this'] or $parent['this']
- 'this' in $parse-ed expressions can no longer be overriden, if a variable named 'this' is put on the scope it must be accessed using this['this']

Closes #9105
2014-09-27 08:13:14 -07:00
Brian Iversen 4a6c7cf8ce docs(guide): update compiler guide with minor grammatical fixes
Minor changes to grammar. Changed sentence "But the declarative language
is also limited, since it does not allow you to teach the browser new syntax."
to now read "However, the declarative language is also limited, as it does not
allow you to teach the browser new syntax."
However is a less informal start to a sentence, and replacing "since"
correctly references extent/degree rather than comparison of time.
2014-09-26 17:16:23 -07:00
Victor Queiroz 27d12340d9 docs(guide): update directive guide to not imply ngView is part of core 2014-09-26 17:04:40 -07:00
Peter Bacon Darwin fb0c77f0b6 fix($compile): connect transclude scopes to their containing scope to prevent memory leaks
Transcluded scopes are now connected to the scope in which they are created
via their `$parent` property. This means that they will be automatically destroyed
when their "containing" scope is destroyed, without having to resort to listening
for a `$destroy` event on various DOM elements or other scopes.

Previously, transclude scope not only inherited prototypically from the scope from
which they were transcluded but they were also still owned by that "outer" scope.
This meant that there were scenarios where the "real" container scope/element was
destroyed but the transclude scope was not, leading to memory leaks.

The original strategy for dealing with this was to attach a `$destroy` event handler
to the DOM elements in the transcluded content, so that if the elements were removed
from the DOM then their associated transcluded scope would be destroyed.

This didn't work for transclude contents that didn't contain any elements - most
importantly in the case of the transclude content containing an element transclude
directive at its root, since the compiler swaps out this element for a comment
before a destroy handler could be attached.

BREAKING CHANGE:

`$transclude` functions no longer attach `$destroy` event handlers to the
transcluded content, and so the associated transclude scope will not automatically
be destroyed if you remove a transcluded element from the DOM using direct DOM
manipulation such as the jquery `remove()` method.

If you want to explicitly remove DOM elements inside your directive that have
been compiled, and so potentially contain child (and transcluded) scopes, then
it is your responsibility to get hold of the scope and destroy it at the same time.

The suggested approach is to create a new child scope of your own around any DOM
elements that you wish to manipulate in this way and destroy those scopes if you
remove their contents - any child scopes will then be destroyed and cleaned up
automatically.

Note that all the built-in directives that manipulate the DOM (ngIf, ngRepeat,
ngSwitch, etc) already follow this best practice, so if you only use these for
manipulating the DOM then you do not have to worry about this change.

Closes #9095
Closes #9281
2014-09-26 21:38:17 +01:00
Peter Bacon Darwin 6417a3e9eb feat(Scope): allow the parent of a new scope to be specified on creation
This enables us to place transclude scopes more accurately in the scope hierarchy.
2014-09-26 21:38:02 +01:00
Caitlin Potter 07e3abc7dd feat($compile): optionally get controllers from ancestors only
Implement option to strengthen require '^' operator, by adding another '^'.

When a second '^' is used, the controller will only search parent nodes for the
matching controller, and will throw or return null if not found, depending on
whether or not the requirement is optional.

Closes #4518
Closes #4540
Closes #8240
Closes #8511
2014-09-26 16:32:26 -04:00
Peter Bacon Darwin b9df121655 chore(docs): fix links to github
Closes https://github.com/angular/code.angularjs.org/issues/13
2014-09-26 20:51:55 +01:00
Brian Ford b5bb4a986a docs(guide/accessibility): explain ngAria 2014-09-26 12:02:02 -07:00
Michał Gołębiowski 8202c4dcea chore(Angular): drop support for Opera < 15
Closes #8589
2014-09-26 11:32:11 -07:00
Leonardo Zizzamia 2c8b464852 perf(benchmark): add ngBindOnce benchmarks to largetable-bp 2014-09-26 10:04:29 -07:00
Sandeep Panda a192c41ddc docs(guide/index): add book AngularJS: Novice to Ninja
I wrote a book on AngularJS (AngularJS: Novice to Ninja).

Closes #9293
2014-09-26 12:27:52 -04:00
Georgios Kalpakas a8fe2cc345 test(input): test that number validates with unspecified viewValue
Adds an additional test verifying that a number which is not required will validate successfully
when ngModelCtrl.$validate() is called. Before 92f05e5 landed, this would have failed because of
a parse error.

Closes #9193
2014-09-25 09:53:45 -04:00
Peter Bacon Darwin e522c25fd4 chore(docs): remove unused code 2014-09-25 05:43:20 +01:00
Peter Bacon Darwin 5dbc2d65f3 chore(docs): improve logo rendering performance 2014-09-25 05:32:12 +01:00
Peter Bacon Darwin 27300072d1 chore(protractor): annotate $animate to allow tests to run under strict-di 2014-09-25 05:32:12 +01:00
Peter Bacon Darwin 7ffc247d0f chore(docs): minify javascript 2014-09-25 05:32:12 +01:00
Peter Bacon Darwin 8ab673d430 chore(docs): ensure DI annotations are in place 2014-09-25 05:31:14 +01:00
Caitlin Potter b9e899c8b2 test(ngModel): rename test to better reflect what is being tested
I meant to do this in before 92f05e5a59 landed, sorry u_u
2014-09-24 18:04:37 -04:00
Caitlin Potter 92f05e5a59 fix(ngModel): do not parse undefined viewValue when validating
Previously, if a viewValue had not yet been set on the element, it could incorrectly produce a
parse error.

This change prevents the parsers from running if a view value has not yet been committed.

Closes #9106
Closes #9260
2014-09-24 18:00:20 -04:00
Peter Bacon Darwin e81ae1464d chore(docs): show error 404 without partial failing
We can move the test back into the main describe as it no longer causes an
error message to be logged
2014-09-24 07:32:04 +01:00
49 changed files with 1998 additions and 959 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
## Features
- **$location:** add ability to opt-out of <base/> tag requirement in html5Mode
- **$location:** add ability to opt-out of `<base>` tag requirement in html5Mode
([dc3de7fb](https://github.com/angular/angular.js/commit/dc3de7fb7a14c38b5c3dc7decfafb0b51d422dd1),
[#8934](https://github.com/angular/angular.js/issues/8934))
- **formController:** add $setUntouched to propagate untouched state
+4 -4
View File
@@ -54,7 +54,7 @@ For large fixes, please build and test the documentation before submitting the P
accidentally introduced any layout or formatting issues. You should also make sure that your commit message
is labeled "docs:" and follows the **Git Commit Guidelines** outlined below.
If you're just making a small change, don't worry about filing an issue first. Use the friendly blue "Improve this doc" button at the top right of the doc page to fork the repository in-place and make a quick change on the fly.
If you're just making a small change, don't worry about filing an issue first. Use the friendly blue "Improve this doc" button at the top right of the doc page to fork the repository in-place and make a quick change on the fly. When naming the commit, it is advised to still label it according to the commit guidelines below, by starting the commit message with **docs** and referencing the filename. Since this is not obvious and some changes are made on the fly, this is not strictly necessary and we will understand if this isn't done the first few times.
## <a name="submit"></a> Submission Guidelines
@@ -66,13 +66,13 @@ Help us to maximize the effort we can spend fixing issues and adding new
features, by not reporting duplicate issues. Providing the following information will increase the
chances of your issue being dealt with quickly:
* **Overview of the issue** - if an error is being thrown a non-minified stack trace helps
* **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
* **Motivation for or Use Case** - explain why this is a bug for you
* **Angular Version(s)** - is it a regression?
* **Browsers and Operating System** - is this a problem with all browsers or only IE8?
* **Reproduce the error** - provide a live example (using [Plunker][plunker] or
* **Reproduce the Error** - provide a live example (using [Plunker][plunker] or
[JSFiddle][jsfiddle]) or a unambiguous set of steps.
* **Related issues** - has a similar issue been reported before?
* **Related Issues** - has a similar issue been reported before?
* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
causing the problem (line of code or commit)
+2 -2
View File
@@ -47,8 +47,7 @@ module.exports = function(grunt) {
keepalive: true,
middleware: function(connect, options){
return [
//uncomment to enable CSP
// util.csp(),
util.conditionalCsp(),
util.rewrite(),
connect.favicon('images/favicon.ico'),
connect.static(options.base),
@@ -74,6 +73,7 @@ module.exports = function(grunt) {
next();
},
util.conditionalCsp(),
connect.favicon('images/favicon.ico'),
connect.static(options.base)
];
+21 -10
View File
@@ -8,15 +8,16 @@
Large table rendered with AngularJS
</p>
<div>none: <input type=radio ng-model="benchmarkType" value="none"></div>
<div>baseline binding: <input type=radio ng-model="benchmarkType" value="baselineBinding"></div>
<div>baseline interpolation: <input type=radio ng-model="benchmarkType" value="baselineInterpolation"></div>
<div>ngBind: <input type=radio ng-model="benchmarkType" value="ngBind"></div>
<div>interpolation: <input type=radio ng-model="benchmarkType" value="interpolation"></div>
<div>ngBind + fnInvocation: <input type=radio ng-model="benchmarkType" value="ngBindFn"></div>
<div>interpolation + fnInvocation: <input type=radio ng-model="benchmarkType" value="interpolationFn"></div>
<div>ngBind + filter: <input type=radio ng-model="benchmarkType" value="ngBindFilter"></div>
<div>interpolation + filter: <input type=radio ng-model="benchmarkType" value="interpolationFilter"></div>
<div>none: <input type="radio" ng-model="benchmarkType" value="none"></div>
<div>baseline binding: <input type="radio" ng-model="benchmarkType" value="baselineBinding"></div>
<div>baseline interpolation: <input type="radio" ng-model="benchmarkType" value="baselineInterpolation"></div>
<div>ngBind: <input type="radio" ng-model="benchmarkType" value="ngBind"></div>
<div>ngBindOnce: <input type="radio" ng-model="benchmarkType" value="ngBindOnce"></div>
<div>interpolation: <input type="radio" ng-model="benchmarkType" value="interpolation"></div>
<div>ngBind + fnInvocation: <input type="radio" ng-model="benchmarkType" value="ngBindFn"></div>
<div>interpolation + fnInvocation: <input type="radio" ng-model="benchmarkType" value="interpolationFn"></div>
<div>ngBind + filter: <input type="radio" ng-model="benchmarkType" value="ngBindFilter"></div>
<div>interpolation + filter: <input type="radio" ng-model="benchmarkType" value="interpolationFilter"></div>
<ng-switch on="benchmarkType">
<baseline-binding-table ng-switch-when="baselineBinding">
@@ -26,7 +27,17 @@
<div ng-switch-when="ngBind">
<h2>baseline binding</h2>
<div ng-repeat="row in data">
<span ng-repeat="column in row"><span ng-bind="column.i"></span>:<span ng-bind="column.j"></span>|</span>
<span ng-repeat="column in row">
<span ng-bind="column.i"></span>:<span ng-bind="column.j"></span>|
</span>
</div>
</div>
<div ng-switch-when="ngBindOnce">
<h2>baseline binding once</h2>
<div ng-repeat="row in data">
<span ng-repeat="column in ::row">
<span ng-bind="::column.i"></span>:<span ng-bind="::column.j"></span>|
</span>
</div>
</div>
<div ng-switch-when="interpolation">
@@ -1,284 +0,0 @@
'use strict';
var directive = {};
var service = { value: {} };
var DEPENDENCIES = {
'angular.js': 'http://code.angularjs.org/' + angular.version.full + '/angular.min.js',
'angular-resource.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-resource.min.js',
'angular-route.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-route.min.js',
'angular-animate.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-animate.min.js',
'angular-sanitize.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-sanitize.min.js',
'angular-cookies.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-cookies.min.js'
};
function escape(text) {
return text.
replace(/\&/g, '&amp;').
replace(/\</g, '&lt;').
replace(/\>/g, '&gt;').
replace(/"/g, '&quot;');
}
/**
* http://stackoverflow.com/questions/451486/pre-tag-loses-line-breaks-when-setting-innerhtml-in-ie
* http://stackoverflow.com/questions/195363/inserting-a-newline-into-a-pre-tag-ie-javascript
*/
function setHtmlIe8SafeWay(element, html) {
var newElement = angular.element('<pre>' + html + '</pre>');
element.empty();
element.append(newElement.contents());
return element;
}
directive.jsFiddle = function(getEmbeddedTemplate, escape, script) {
return {
terminal: true,
link: function(scope, element, attr) {
var name = '',
stylesheet = '<link rel="stylesheet" href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css">\n',
fields = {
html: '',
css: '',
js: ''
};
angular.forEach(attr.jsFiddle.split(' '), function(file, index) {
var fileType = file.split('.')[1];
if (fileType == 'html') {
if (index == 0) {
fields[fileType] +=
'<div ng-app' + (attr.module ? '="' + attr.module + '"' : '') + '>\n' +
getEmbeddedTemplate(file, 2);
} else {
fields[fileType] += '\n\n\n <!-- CACHE FILE: ' + file + ' -->\n' +
' <script type="text/ng-template" id="' + file + '">\n' +
getEmbeddedTemplate(file, 4) +
' </script>\n';
}
} else {
fields[fileType] += getEmbeddedTemplate(file) + '\n';
}
});
fields.html += '</div>\n';
setHtmlIe8SafeWay(element,
'<form class="jsfiddle" method="post" action="http://jsfiddle.net/api/post/library/pure/" target="_blank">' +
hiddenField('title', 'AngularJS Example: ' + name) +
hiddenField('css', '</style> <!-- Ugly Hack due to jsFiddle issue: http://goo.gl/BUfGZ --> \n' +
stylesheet +
script.angular +
(attr.resource ? script.resource : '') +
'<style>\n' +
fields.css) +
hiddenField('html', fields.html) +
hiddenField('js', fields.js) +
'<button class="btn btn-primary"><i class="icon-white icon-pencil"></i> Edit Me</button>' +
'</form>');
function hiddenField(name, value) {
return '<input type="hidden" name="' + name + '" value="' + escape(value) + '">';
}
}
}
};
directive.ngSetText = ['getEmbeddedTemplate', function(getEmbeddedTemplate) {
return {
restrict: 'CA',
priority: 10,
compile: function(element, attr) {
setHtmlIe8SafeWay(element, escape(getEmbeddedTemplate(attr.ngSetText)));
}
}
}]
directive.ngHtmlWrap = ['reindentCode', 'templateMerge', function(reindentCode, templateMerge) {
return {
compile: function(element, attr) {
var properties = {
head: '',
module: '',
body: element.text()
},
html = "<!doctype html>\n<html ng-app{{module}}>\n <head>\n{{head:4}} </head>\n <body>\n{{body:4}} </body>\n</html>";
angular.forEach((attr.ngHtmlWrap || '').split(' '), function(dep) {
if (!dep) return;
dep = DEPENDENCIES[dep] || dep;
var ext = dep.split(/\./).pop();
if (ext == 'css') {
properties.head += '<link rel="stylesheet" href="' + dep + '" type="text/css">\n';
} else if(ext == 'js') {
properties.head += '<script src="' + dep + '"></script>\n';
} else {
properties.module = '="' + dep + '"';
}
});
setHtmlIe8SafeWay(element, escape(templateMerge(html, properties)));
}
}
}];
directive.ngSetHtml = ['getEmbeddedTemplate', function(getEmbeddedTemplate) {
return {
restrict: 'CA',
priority: 10,
compile: function(element, attr) {
setHtmlIe8SafeWay(element, getEmbeddedTemplate(attr.ngSetHtml));
}
}
}];
directive.ngEvalJavascript = ['getEmbeddedTemplate', function(getEmbeddedTemplate) {
return {
compile: function (element, attr) {
var fileNames = attr.ngEvalJavascript.split(' ');
angular.forEach(fileNames, function(fileName) {
var script = getEmbeddedTemplate(fileName);
try {
if (window.execScript) { // IE
window.execScript(script || '""'); // IE complains when evaling empty string
} else {
window.eval(script + '//@ sourceURL=' + fileName);
}
} catch (e) {
if (window.console) {
window.console.log(script, '\n', e);
} else {
window.alert(e);
}
}
});
}
};
}];
directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location', '$sniffer', '$animate',
function($templateCache, $browser, docsRootScope, $location, $sniffer, $animate) {
return {
terminal: true,
link: function(scope, element, attrs) {
var modules = ['ngAnimate'],
embedRootScope,
deregisterEmbedRootScope;
modules.push(['$provide', function($provide) {
$provide.value('$templateCache', $templateCache);
$provide.value('$anchorScroll', angular.noop);
$provide.value('$browser', $browser);
$provide.value('$sniffer', $sniffer);
$provide.value('$animate', $animate);
$provide.provider('$location', function() {
this.$get = ['$rootScope', function($rootScope) {
docsRootScope.$on('$locationChangeSuccess', function(event, oldUrl, newUrl) {
$rootScope.$broadcast('$locationChangeSuccess', oldUrl, newUrl);
});
return $location;
}];
this.html5Mode = angular.noop;
});
$provide.decorator('$rootScope', ['$delegate', function($delegate) {
embedRootScope = $delegate;
// Since we are teleporting the $animate service, which relies on the $$postDigestQueue
// we need the embedded scope to use the same $$postDigestQueue as the outer scope
embedRootScope.$$postDigestQueue = docsRootScope.$$postDigestQueue;
deregisterEmbedRootScope = docsRootScope.$watch(function embedRootScopeDigestWatch() {
embedRootScope.$digest();
});
return embedRootScope;
}]);
}]);
if (attrs.ngEmbedApp) modules.push(attrs.ngEmbedApp);
element.on('click', function(event) {
if (event.target.attributes.getNamedItem('ng-click')) {
event.preventDefault();
}
});
element.on('$destroy', function() {
deregisterEmbedRootScope();
embedRootScope.$destroy();
});
element.data('$injector', null);
angular.bootstrap(element, modules);
}
};
}];
service.reindentCode = function() {
return function (text, spaces) {
if (!text) return text;
var lines = text.split(/\r?\n/);
var prefix = ' '.substr(0, spaces || 0);
var i;
// remove any leading blank lines
while (lines.length && lines[0].match(/^\s*$/)) lines.shift();
// remove any trailing blank lines
while (lines.length && lines[lines.length - 1].match(/^\s*$/)) lines.pop();
var minIndent = 999;
for (i = 0; i < lines.length; i++) {
var line = lines[0];
var reindentCode = line.match(/^\s*/)[0];
if (reindentCode !== line && reindentCode.length < minIndent) {
minIndent = reindentCode.length;
}
}
for (i = 0; i < lines.length; i++) {
lines[i] = prefix + lines[i].substring(minIndent);
}
lines.push('');
return lines.join('\n');
}
};
service.templateMerge = ['reindentCode', function(indentCode) {
return function(template, properties) {
return template.replace(/\{\{(\w+)(?:\:(\d+))?\}\}/g, function(_, key, indent) {
var value = properties[key];
if (indent) {
value = indentCode(value, indent);
}
return value == undefined ? '' : value;
});
};
}];
service.getEmbeddedTemplate = ['reindentCode', function(reindentCode) {
return function (id) {
var element = document.getElementById(id);
if (!element) {
return null;
}
return reindentCode(angular.element(element).html(), 0);
}
}];
angular.module('bootstrapPrettify', []).directive(directive).factory(service);
+4 -7
View File
@@ -76,14 +76,11 @@ describe('docs.angularjs.org', function () {
expect(element(by.css('.minerr-errmsg')).getText()).toEqual("Argument 'Missing' is not a function, got undefined");
});
it("should display an error if the page does not exist", function() {
browser.get('index-debug.html#!/api/does/not/exist');
expect(element(by.css('h1')).getText()).toBe('Oops!');
});
});
});
describe('Error Handling', function() {
it("should display an error if the page does not exist", function() {
browser.get('index-debug.html#!/api/does/not/exist');
expect(element(by.css('h1')).getText()).toBe('Oops!');
});
});
+2 -3
View File
@@ -14,11 +14,10 @@ angular.module('docsApp', [
'tutorials',
'versions',
'bootstrap',
'bootstrapPrettify',
'ui.bootstrap.dropdown'
])
.config(function($locationProvider) {
.config(['$locationProvider', function($locationProvider) {
$locationProvider.html5Mode(true).hashPrefix('!');
});
}]);
+2 -7
View File
@@ -24,20 +24,14 @@ angular.module('DocsController', [])
$window._gaq.push(['_trackPageview', pagePath]);
});
$scope.$on('$includeContentError', function() {
$scope.partialPath = 'Error404.html';
});
$scope.$watch(function docsPathWatch() {return $location.path(); }, function docsPathWatchAction(path) {
path = path.replace(/^\/?(.+?)(\/index)?\/?$/, '$1');
$scope.partialPath = 'partials/' + path + '.html';
currentPage = $scope.currentPage = NG_PAGES[path];
if ( currentPage ) {
$scope.partialPath = 'partials/' + path + '.html';
$scope.currentArea = NG_NAVIGATION[currentPage.area];
var pathParts = currentPage.path.split('/');
var breadcrumb = $scope.breadcrumb = [];
@@ -50,6 +44,7 @@ angular.module('DocsController', [])
} else {
$scope.currentArea = NG_NAVIGATION['api'];
$scope.breadcrumb = [];
$scope.partialPath = 'Error404.html';
}
});
+15 -16
View File
@@ -1,6 +1,6 @@
angular.module('tutorials', [])
.directive('docTutorialNav', function(templateMerge) {
.directive('docTutorialNav', function() {
var pages = [
'',
'step_00', 'step_01', 'step_02', 'step_03', 'step_04',
@@ -8,23 +8,22 @@ angular.module('tutorials', [])
'step_10', 'step_11', 'step_12', 'the_end'
];
return {
compile: function(element, attrs) {
var seq = 1 * attrs.docTutorialNav,
props = {
seq: seq,
prev: pages[seq],
next: pages[2 + seq],
diffLo: seq ? (seq - 1): '0~1',
diffHi: seq
};
scope: {},
template:
'<a ng-href="tutorial/{{prev}}"><li class="btn btn-primary"><i class="glyphicon glyphicon-step-backward"></i> Previous</li></a>\n' +
'<a ng-href="http://angular.github.io/angular-phonecat/step-{{seq}}/app"><li class="btn btn-primary"><i class="glyphicon glyphicon-play"></i> Live Demo</li></a>\n' +
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{diffLo}}...step-{{diffHi}}"><li class="btn btn-primary"><i class="glyphicon glyphicon-search"></i> Code Diff</li></a>\n' +
'<a ng-href="tutorial/{{next}}"><li class="btn btn-primary">Next <i class="glyphicon glyphicon-step-forward"></i></li></a>',
link: function(scope, element, attrs) {
var seq = 1 * attrs.docTutorialNav;
scope.seq = seq;
scope.prev = pages[seq];
scope.next = pages[2 + seq];
scope.diffLo = seq ? (seq - 1): '0~1';
scope.diffHi = seq;
element.addClass('btn-group');
element.addClass('tutorial-nav');
element.append(templateMerge(
'<a href="tutorial/{{prev}}"><li class="btn btn-primary"><i class="glyphicon glyphicon-step-backward"></i> Previous</li></a>\n' +
'<a href="http://angular.github.io/angular-phonecat/step-{{seq}}/app"><li class="btn btn-primary"><i class="glyphicon glyphicon-play"></i> Live Demo</li></a>\n' +
'<a href="https://github.com/angular/angular-phonecat/compare/step-{{diffLo}}...step-{{diffHi}}"><li class="btn btn-primary"><i class="glyphicon glyphicon-search"></i> Code Diff</li></a>\n' +
'<a href="tutorial/{{next}}"><li class="btn btn-primary">Next <i class="glyphicon glyphicon-step-forward"></i></li></a>', props));
}
};
})
@@ -47,4 +46,4 @@ angular.module('tutorials', [])
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{step ? (step - 1): \'0~1\'}}...step-{{step}}">GitHub</a>\n' +
'</p>'
};
});
});
-2
View File
@@ -1,5 +1,3 @@
"use strict";
angular.module('versions', [])
.controller('DocsVersionsCtrl', ['$scope', '$location', '$window', 'NG_VERSIONS', function($scope, $location, $window, NG_VERSIONS) {
@@ -19,7 +19,6 @@ module.exports = function debugDeployment(getVersion) {
'../angular-animate.js',
'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js',
'js/angular-bootstrap/bootstrap.js',
'js/angular-bootstrap/bootstrap-prettify.js',
'js/angular-bootstrap/dropdown-toggle.js',
'components/lunr.js-' + getVersion('lunr.js') + '/lunr.js',
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
+3 -4
View File
@@ -18,16 +18,15 @@ module.exports = function defaultDeployment(getVersion) {
'../angular-touch.min.js',
'../angular-animate.min.js',
'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js',
'js/angular-bootstrap/bootstrap.js',
'js/angular-bootstrap/bootstrap-prettify.js',
'js/angular-bootstrap/dropdown-toggle.js',
'js/angular-bootstrap/bootstrap.min.js',
'js/angular-bootstrap/dropdown-toggle.min.js',
'components/lunr.js-' + getVersion('lunr.js') + '/lunr.min.js',
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/lang-css.js',
'js/versions-data.js',
'js/pages-data.js',
'js/nav-data.js',
'js/docs.js'
'js/docs.min.js'
],
stylesheets: [
'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.min.css',
+3 -4
View File
@@ -22,16 +22,15 @@ module.exports = function jqueryDeployment(getVersion) {
'../angular-touch.min.js',
'../angular-animate.min.js',
'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js',
'js/angular-bootstrap/bootstrap.js',
'js/angular-bootstrap/bootstrap-prettify.js',
'js/angular-bootstrap/dropdown-toggle.js',
'js/angular-bootstrap/bootstrap.min.js',
'js/angular-bootstrap/dropdown-toggle.min.js',
'components/lunr.js-' + getVersion('lunr.js') + '/lunr.min.js',
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/lang-css.js',
'js/versions-data.js',
'js/pages-data.js',
'js/nav-data.js',
'js/docs.js'
'js/docs.min.js'
],
stylesheets: [
'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.min.css',
@@ -21,16 +21,15 @@ module.exports = function productionDeployment(getVersion) {
cdnUrl + '/angular-touch.min.js',
cdnUrl + '/angular-animate.min.js',
'components/marked-' + getVersion('marked', 'node_modules', 'package.json') + '/lib/marked.js',
'js/angular-bootstrap/bootstrap.js',
'js/angular-bootstrap/bootstrap-prettify.js',
'js/angular-bootstrap/dropdown-toggle.js',
'js/angular-bootstrap/bootstrap.min.js',
'js/angular-bootstrap/dropdown-toggle.min.js',
'components/lunr.js-' + getVersion('lunr.js') + '/lunr.min.js',
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/lang-css.js',
'js/versions-data.js',
'js/pages-data.js',
'js/nav-data.js',
'js/docs.js'
'js/docs.min.js'
],
stylesheets: [
'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.min.css',
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" ng-app="docsApp" ng-controller="DocsController">
<html lang="en" ng-app="docsApp" ng-strict-di ng-controller="DocsController">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
@@ -76,7 +76,7 @@
<div class="row">
<div class="col-md-9 header-branding">
<a class="brand navbar-brand" href="http://angularjs.org">
<img class="logo" src="img/angularjs-for-header-only.svg">
<img width="117" height="30" class="logo" ng-src="img/angularjs-for-header-only.svg">
</a>
<ul class="nav navbar-nav">
<li class="divider-vertical"></li>
+15 -1
View File
@@ -54,4 +54,18 @@ angular.module('myModule')
.directive('myDirective', ['myCoolService', function (myCoolService) {
// This directive definition does not throw unknown provider.
}]);
```
```
Attempting to inject one controller into another will also throw an `Unknown provider` error:
```
angular.module('myModule', [])
.controller('MyFirstController', function() { /* ... */ });
.controller('MySecondController', ['MyFirstController', function(MyFirstController) {
// This controller throws an unknown provider error because
// MyFirstController cannot be injected.
}]);
```
Use the `$controller` service if you want to instantiate controllers yourself.
+11
View File
@@ -0,0 +1,11 @@
@ngdoc error
@name ngModel:datefmt
@fullName Model is not a date object
@description
All date-related inputs like `<input type="date">` require the model to be a `Date` object.
If the model is something else, this error will be thrown.
Angular does not set validation errors on the `<input>` in this case
as those errors are shown to the user, but the erroneous state was
caused by incorrect application logic and not by the user.
+40
View File
@@ -0,0 +1,40 @@
@ngdoc overview
@name Accessibility
@description
# Accessibility with ngAria
You can use the `ngAria` module to have certain ARIA attributes automatically applied when you
use certain directives.
```js
angular.module('myApp', ['ngAria'])...
```
Elements using `ng-model` with `required` or `ngRequired` directives will automatically have
`aria-required` attributes with the proper corresponding values.
```html
<material-input ng-model="val" required>
```
Becomes:
```html
<material-input ng-model="val" required aria-required="true">
```
ngAria is just a starting point. You'll have to manually choose how to implement some
accessibility features.
For instance, you may want to add `ng-keypress` bindings alongside `ng-click` to make keyboard
navigation easier.
## Additional Resources
Accessibility best practices that apply to web apps in general also apply to Angular.
* [WebAim](http://webaim.org/)
* [Using WAI-ARIA in HTML](http://www.w3.org/TR/2014/WD-aria-in-html-20140626/)
+5 -3
View File
@@ -25,8 +25,8 @@ browser how the window size needs to be divided in half so that the center is fo
center needs to be aligned with the text's center. Simply add an `align="center"` attribute to any
element to achieve the desired behavior. Such is the power of declarative language.
But the declarative language is also limited, since it does not allow you to teach the browser new
syntax. For example there is no easy way to get the browser to align the text at 1/3 the position
However, the declarative language is also limited, as it does not allow you to teach the browser new
syntax. For example, there is no easy way to get the browser to align the text at 1/3 the position
instead of 1/2. What is needed is a way to teach the browser new HTML syntax.
Angular comes pre-bundled with common directives which are useful for building any app. We also
@@ -85,7 +85,9 @@ Here is a directive which makes any element draggable. Notice the `draggable` at
position: 'relative',
border: '1px solid red',
backgroundColor: 'lightgrey',
cursor: 'pointer'
cursor: 'pointer',
display: 'block',
width: '65px'
});
element.on('mousedown', function(event) {
// Prevent default dragging of selected content
+2 -2
View File
@@ -67,8 +67,8 @@ element that adds extra behavior to the element. The {@link ng.directive:ngModel
stores/updates the value of the input field into/from a variable.
<div class="alert alert-info">
**Custom directives to access the DOM**: In Angular, the only place where an application touches the DOM is
within directives. This is good as artifacts that access the DOM are hard to test.
**Custom directives to access the DOM**: In Angular, the only place where an application should access the DOM is
within directives. This is important because artifacts that access the DOM are hard to test.
If you need to access the DOM directly you should write a custom directive for this. The
{@link directive directives guide} explains how to do this.
</div>
+1 -1
View File
@@ -22,7 +22,7 @@ At a high level, directives are markers on a DOM element (such as an attribute,
name, comment or CSS class) that tell AngularJS's **HTML compiler** ({@link ng.$compile `$compile`}) to
attach a specified behavior to that DOM element or even transform the DOM element and its children.
Angular comes with a set of these directives built-in, like `ngBind`, `ngModel`, and `ngView`.
Angular comes with a set of these directives built-in, like `ngBind`, `ngModel`, and `ngClass`.
Much like you create controllers and services, you can create your own directives for Angular to use.
When Angular {@link guide/bootstrap bootstraps} your application, the
{@link guide/compiler HTML compiler} traverses the DOM matching directives against the DOM elements.
+1
View File
@@ -213,6 +213,7 @@ only when the control loses focus (blur event).
<input type="text" ng-model="user.data" /><br />
</form>
<pre>username = "{{user.name}}"</pre>
<pre>userdata = "{{user.data}}"</pre>
</div>
</file>
<file name="script.js">
+1
View File
@@ -105,6 +105,7 @@ This is a short list of libraries with specific support and documentation for wo
* [Recipes With AngularJS](http://www.amazon.co.uk/Recipes-Angular-js-Frederik-Dietz-ebook/dp/B00DK95V48) by Frederik Dietz
* [Developing an AngularJS Edge](http://www.amazon.com/Developing-AngularJS-Edge-Christopher-Hiller-ebook/dp/B00CJLFF8K) by Christopher Hiller
* [ng-book: The Complete Book on AngularJS](http://ng-book.com/) by Ari Lerner
* [AngularJS : Novice to Ninja](http://www.amazon.in/AngularJS-Novice-Ninja-Sandeep-Panda/dp/0992279453) by Sandeep Panda
###Videos:
* [egghead.io](http://egghead.io/)
+32 -5
View File
@@ -8,7 +8,10 @@ var bower = require('bower');
var Dgeni = require('dgeni');
var merge = require('event-stream').merge;
var path = require('canonical-path');
var foreach = require('gulp-foreach');
var uglify = require('gulp-uglify');
var sourcemaps = require('gulp-sourcemaps');
var rename = require('gulp-rename');
// We indicate to gulp that tasks are async by returning the stream.
// Gulp can then wait for the stream to close before starting dependent tasks.
@@ -20,6 +23,7 @@ var bowerFolder = 'bower_components';
var src = 'app/src/**/*.js';
var assets = 'app/assets/**/*';
var copyComponent = function(component, pattern, sourceFolder, packageFile) {
pattern = pattern || '/**/*';
sourceFolder = sourceFolder || bowerFolder;
@@ -42,14 +46,37 @@ gulp.task('bower', function() {
});
gulp.task('build-app', function() {
gulp.src(src)
.pipe(concat('docs.js'))
.pipe(gulp.dest(outputFolder + '/js/'));
var file = 'docs.js';
var minFile = 'docs.min.js';
var folder = outputFolder + '/js/';
return gulp.src(src)
.pipe(sourcemaps.init())
.pipe(concat(file))
.pipe(gulp.dest(folder))
.pipe(rename(minFile))
.pipe(uglify())
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(folder));
});
gulp.task('assets', ['bower'], function() {
var JS_EXT = /\.js$/;
return merge(
gulp.src([assets]).pipe(gulp.dest(outputFolder)),
gulp.src([assets])
.pipe(gulp.dest(outputFolder)),
gulp.src([assets])
.pipe(foreach(function(stream, file) {
if (JS_EXT.test(file.relative)) {
var minFile = file.relative.replace(JS_EXT, '.min.js');
return stream
.pipe(sourcemaps.init())
.pipe(concat(minFile))
.pipe(uglify())
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(outputFolder));
}
})),
copyComponent('bootstrap', '/dist/**/*'),
copyComponent('open-sans-fontface'),
copyComponent('lunr.js','/*.js'),
+8 -4
View File
@@ -285,11 +285,15 @@ module.exports = {
//csp connect middleware
csp: function(){
conditionalCsp: function(){
return function(req, res, next){
res.setHeader("X-WebKit-CSP", "default-src 'self';");
res.setHeader("X-Content-Security-Policy", "default-src 'self'");
res.setHeader("Content-Security-Policy", "default-src 'self'");
var CSP = /\.csp\W/;
if (CSP.test(req.url)) {
res.setHeader("X-WebKit-CSP", "default-src 'self';");
res.setHeader("X-Content-Security-Policy", "default-src 'self'");
res.setHeader("Content-Security-Policy", "default-src 'self'");
}
next();
};
},
+2
View File
@@ -83,6 +83,7 @@ var getTaggedVersion = function() {
if ( version && semver.satisfies(version, currentPackage.branchVersion)) {
version.codeName = getCodeName(tag);
version.full = version.version;
version.branch = 'v' + currentPackage.branchVersion.replace('*', 'x');
return version;
}
}
@@ -197,6 +198,7 @@ var getSnapshotVersion = function() {
version.isSnapshot = true;
version.format();
version.full = version.version + '+' + version.build;
version.branch = 'master';
return version;
};
+796 -342
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -13,7 +13,6 @@
"canonical-path": "0.0.2",
"dgeni": "^0.4.0",
"dgeni-packages": "^0.10.0",
"es6-shim": "^0.14.0",
"event-stream": "~3.1.0",
"grunt": "~0.4.2",
"grunt-bump": "~0.0.13",
@@ -29,8 +28,12 @@
"grunt-parallel": "~0.3.1",
"grunt-shell": "~0.4.0",
"gulp": "~3.8.0",
"gulp-concat": "~2.1.7",
"gulp-concat": "^2.4.1",
"gulp-foreach": "0.0.1",
"gulp-jshint": "~1.4.2",
"gulp-rename": "^1.2.0",
"gulp-sourcemaps": "^1.2.2",
"gulp-uglify": "^1.0.1",
"gulp-util": "^3.0.1",
"jasmine-node": "~1.11.0",
"jasmine-reporters": "~0.2.1",
@@ -49,7 +52,7 @@
"marked": "~0.3.0",
"node-html-encoder": "0.0.2",
"promises-aplus-tests": "~2.0.4",
"protractor": "1.2.0",
"protractor": "1.3.1",
"q": "~1.0.0",
"q-io": "^1.10.9",
"qq": "^0.3.5",
@@ -57,9 +60,7 @@
"semver": "~2.1.0",
"shelljs": "~0.2.6",
"sorted-object": "^1.0.0",
"stringmap": "^0.2.2",
"winston": "~0.7.2",
"yaml-js": "~0.0.8"
"stringmap": "^0.2.2"
},
"licenses": [
{
+2 -2
View File
@@ -21,9 +21,9 @@ exports.config = {
// Disable animations so e2e tests run more quickly
var disableNgAnimate = function() {
angular.module('disableNgAnimate', []).run(function($animate) {
angular.module('disableNgAnimate', []).run(['$animate', function($animate) {
$animate.enabled(false);
});
}]);
};
browser.addMockModule('disableNgAnimate', disableNgAnimate);
+2 -2
View File
@@ -12,9 +12,9 @@ exports.config = {
// Disable animations so e2e tests run more quickly
var disableNgAnimate = function() {
angular.module('disableNgAnimate', []).run(function($animate) {
angular.module('disableNgAnimate', []).run(['$animate', function($animate) {
$animate.enabled(false);
});
}]);
};
browser.addMockModule('disableNgAnimate', disableNgAnimate);
-2
View File
@@ -158,8 +158,6 @@ function Browser(window, document, $log, $sniffer) {
if (replace) history.replaceState(null, '', url);
else {
history.pushState(null, '', url);
// Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462
baseElement.attr('href', baseElement.attr('href'));
}
} else {
newLocation = url;
+82 -29
View File
@@ -212,8 +212,11 @@
* * (no prefix) - Locate the required controller on the current element. Throw an error if not found.
* * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found.
* * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found.
* * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found.
* * `?^` - Attempt to locate the required controller by searching the element and its parents or pass
* `null` to the `link` fn if not found.
* * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass
* `null` to the `link` fn if not found.
*
*
* #### `controllerAs`
@@ -294,13 +297,20 @@
* compile the content of the element and make it available to the directive.
* Typically used with {@link ng.directive:ngTransclude
* ngTransclude}. The advantage of transclusion is that the linking function receives a
* transclusion function which is pre-bound to the correct scope. In a typical setup the widget
* creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate`
* scope. This makes it possible for the widget to have private state, and the transclusion to
* be bound to the parent (pre-`isolate`) scope.
* transclusion function which is pre-bound to the scope of the position in the DOM from where
* it was taken.
*
* * `true` - transclude the content of the directive.
* * `'element'` - transclude the whole element including any directives defined at lower priority.
* In a typical setup the widget creates an `isolate` scope, but the transcluded
* content has its own **transclusion scope**. While the **transclusion scope** is owned as a child,
* by the **isolate scope**, it prototypically inherits from the original scope from where the
* transcluded content was taken.
*
* This makes it possible for the widget to have private state, and the transclusion to
* be bound to the original (pre-`isolate`) scope.
*
* * `true` - transclude the content (i.e. the child nodes) of the directive's element.
* * `'element'` - transclude the whole of the directive's element including any directives on this
* element that defined at a lower priority than this directive.
*
* <div class="alert alert-warning">
* **Note:** When testing an element transclude directive you must not place the directive at the root of the
@@ -404,7 +414,6 @@
* It is safe to do DOM transformation in the post-linking function on elements that are not waiting
* for their async templates to be resolved.
*
* <a name="Attributes"></a>
* ### Attributes
*
* The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the
@@ -442,7 +451,7 @@
* }
* ```
*
* Below is an example using `$compileProvider`.
* ## Example
*
* <div class="alert alert-warning">
* **Note**: Typically directives are registered with `module.directive`. The example below is
@@ -567,7 +576,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
Suffix = 'Directive',
COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/,
CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/,
ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset');
ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'),
REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/;
// Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes
// The assumption is that future DOM event attribute names will begin with
@@ -872,10 +882,44 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeName = nodeName_(this.$$element);
// sanitize a[href] and img[src] values
if ((nodeName === 'a' && key === 'href') ||
(nodeName === 'img' && key === 'src')) {
// sanitize a[href] and img[src] values
this[key] = value = $$sanitizeUri(value, key === 'src');
} else if (nodeName === 'img' && key === 'srcset') {
// sanitize img[srcset] values
var result = "";
// first check if there are spaces because it's not the same pattern
var trimmedSrcset = trim(value);
// ( 999x ,| 999w ,| ,|, )
var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;
// split srcset into tuple of uri and descriptor except for the last item
var rawUris = trimmedSrcset.split(pattern);
// for each tuples
var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
for (var i=0; i<nbrUrisWith2parts; i++) {
var innerIdx = i*2;
// sanitize the uri
result += $$sanitizeUri(trim( rawUris[innerIdx]), true);
// add the descriptor
result += ( " " + trim(rawUris[innerIdx+1]));
}
// split the last item into uri and descriptor
var lastTuple = trim(rawUris[i*2]).split(/\s/);
// sanitize the last uri
result += $$sanitizeUri(trim(lastTuple[0]), true);
// and add the last descriptor if any
if( lastTuple.length === 2) {
result += (" " + trim(lastTuple[1]));
}
this[key] = value = result;
}
if (writeAttr !== false) {
@@ -913,12 +957,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @param {string} key Normalized key. (ie ngAttribute) .
* @param {function(interpolatedValue)} fn Function that will be called whenever
the interpolated value of the attribute changes.
* See the {@link guide/directive#Attributes Directives} guide for more info.
* See {@link ng.$compile#attributes $compile} for more info.
* @returns {function()} Returns a deregistration function for this observer.
*/
$observe: function(key, fn) {
var attrs = this,
$$observers = (attrs.$$observers || (attrs.$$observers = {})),
$$observers = (attrs.$$observers || (attrs.$$observers = Object.create(null))),
listeners = ($$observers[key] || ($$observers[key] = []));
listeners.push(fn);
@@ -1166,20 +1210,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) {
var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement) {
var scopeCreated = false;
var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
if (!transcludedScope) {
transcludedScope = scope.$new();
transcludedScope = scope.$new(false, containingScope);
transcludedScope.$$transcluded = true;
scopeCreated = true;
}
var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement);
if (scopeCreated && !elementTransclusion) {
clone.on('$destroy', function() { transcludedScope.$destroy(); });
}
return clone;
return transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement);
};
return boundTranscludeFn;
@@ -1589,14 +1627,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function getControllers(directiveName, require, $element, elementControllers) {
var value, retrievalMethod = 'data', optional = false;
var $searchElement = $element;
var match;
if (isString(require)) {
while((value = require.charAt(0)) == '^' || value == '?') {
require = require.substr(1);
if (value == '^') {
retrievalMethod = 'inheritedData';
}
optional = optional || value == '?';
match = require.match(REQUIRE_PREFIX_REGEXP);
require = require.substring(match[0].length);
if (match[3]) {
if (match[1]) match[3] = null;
else match[1] = match[3];
}
if (match[1] === '^') {
retrievalMethod = 'inheritedData';
} else if (match[1] === '^^') {
retrievalMethod = 'inheritedData';
$searchElement = $element.parent();
}
if (match[2] === '?') {
optional = true;
}
value = null;
if (elementControllers && retrievalMethod === 'data') {
@@ -1604,7 +1654,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
value = value.instance;
}
}
value = value || $element[retrievalMethod]('$' + require + 'Controller');
value = value || $searchElement[retrievalMethod]('$' + require + 'Controller');
if (!value && !optional) {
throw $compileMinErr('ctreq',
@@ -1810,7 +1860,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!futureParentElement) {
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
}
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement);
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
}
}
}
@@ -1994,6 +2044,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
boundTranscludeFn = linkQueue.shift(),
linkNode = $compileNode[0];
if (scope.$$destroyed) continue;
if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
var oldClasses = beforeTemplateLinkNode.className;
@@ -2020,6 +2072,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) {
var childBoundTranscludeFn = boundTranscludeFn;
if (scope.$$destroyed) return;
if (linkQueue) {
linkQueue.push(scope);
linkQueue.push(node);
+5 -5
View File
@@ -76,9 +76,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
parentForm.$addControl(form);
// Setup initial state of the control
element.addClass(PRISTINE_CLASS);
/**
* @ngdoc method
* @name form.FormController#$rollbackViewValue
@@ -451,9 +448,12 @@ var formDirectiveFactory = function(isNgForm) {
name: 'form',
restrict: isNgForm ? 'EAC' : 'E',
controller: FormController,
compile: function() {
compile: function ngFormCompile(formElement) {
// Setup initial state of the control
formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS);
return {
pre: function(scope, formElement, attr, controller) {
pre: function ngFormPreLink(scope, formElement, attr, controller) {
if (!attr.action) {
// we can't use jq events because if a form is destroyed during submission the default
// action is not prevented. see #1238
+77 -53
View File
@@ -1091,16 +1091,15 @@ function createDateInputType(type, regexp, parseDate, format) {
badInputChecker(scope, element, attr, ctrl);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
var previousDate;
ctrl.$$parserName = type;
ctrl.$parsers.push(function(value) {
if (ctrl.$isEmpty(value)) return null;
if (regexp.test(value)) {
var previousDate = ctrl.$modelValue;
if (previousDate && timezone === 'UTC') {
var timezoneOffset = 60000 * previousDate.getTimezoneOffset();
previousDate = new Date(previousDate.getTime() + timezoneOffset);
}
// Note: We cannot read ctrl.$modelValue, as there might be a different
// parser/formatter in the processing chain so that the model
// contains some different data format!
var parsedDate = parseDate(value, previousDate);
if (timezone === 'UTC') {
parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset());
@@ -1111,8 +1110,18 @@ function createDateInputType(type, regexp, parseDate, format) {
});
ctrl.$formatters.push(function(value) {
if (isDate(value)) {
if (!ctrl.$isEmpty(value)) {
if (!isDate(value)) {
throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value);
}
previousDate = value;
if (previousDate && timezone === 'UTC') {
var timezoneOffset = 60000 * previousDate.getTimezoneOffset();
previousDate = new Date(previousDate.getTime() + timezoneOffset);
}
return $filter('date')(value, format, timezone);
} else {
previousDate = null;
}
return '';
});
@@ -1138,6 +1147,11 @@ function createDateInputType(type, regexp, parseDate, format) {
ctrl.$validate();
});
}
// Override the standard $isEmpty to detect invalid dates as well
ctrl.$isEmpty = function(value) {
// Invalid Date: getTime() returns NaN
return !value || (value.getTime && value.getTime() !== value.getTime());
};
function parseObservedDateValue(val) {
return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined;
@@ -1452,10 +1466,12 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
return {
restrict: 'E',
require: ['?ngModel'],
link: function(scope, element, attr, ctrls) {
if (ctrls[0]) {
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer,
$browser, $filter, $parse);
link: {
pre: function(scope, element, attr, ctrls) {
if (ctrls[0]) {
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer,
$browser, $filter, $parse);
}
}
}
};
@@ -1756,11 +1772,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
currentValidationRunId = 0;
// Setup initial state of the control
$element
.addClass(PRISTINE_CLASS)
.addClass(UNTOUCHED_CLASS);
/**
* @ngdoc method
* @name ngModel.NgModelController#$setValidity
@@ -2053,14 +2064,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
};
this.$$parseAndValidate = function() {
var parserValid = true,
viewValue = ctrl.$$lastCommittedViewValue,
modelValue = viewValue;
for(var i = 0; i < ctrl.$parsers.length; i++) {
modelValue = ctrl.$parsers[i](modelValue);
if (isUndefined(modelValue)) {
parserValid = false;
break;
var viewValue = ctrl.$$lastCommittedViewValue;
var modelValue = viewValue;
var parserValid = isUndefined(modelValue) ? undefined : true;
if (parserValid) {
for(var i = 0; i < ctrl.$parsers.length; i++) {
modelValue = ctrl.$parsers[i](modelValue);
if (isUndefined(modelValue)) {
parserValid = false;
break;
}
}
}
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
@@ -2377,42 +2391,51 @@ var ngModelDirective = function() {
restrict: 'A',
require: ['ngModel', '^?form', '^?ngModelOptions'],
controller: NgModelController,
link: {
pre: function(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0],
formCtrl = ctrls[1] || nullFormCtrl;
// Prelink needs to run before any input directive
// so that we can set the NgModelOptions in NgModelController
// before anyone else uses it.
priority: 1,
compile: function ngModelCompile(element) {
// Setup initial state of the control
element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS);
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
return {
pre: function ngModelPreLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0],
formCtrl = ctrls[1] || nullFormCtrl;
// notify others, especially parent forms
formCtrl.$addControl(modelCtrl);
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
attr.$observe('name', function(newValue) {
if (modelCtrl.$name !== newValue) {
formCtrl.$$renameControl(modelCtrl, newValue);
// notify others, especially parent forms
formCtrl.$addControl(modelCtrl);
attr.$observe('name', function(newValue) {
if (modelCtrl.$name !== newValue) {
formCtrl.$$renameControl(modelCtrl, newValue);
}
});
scope.$on('$destroy', function() {
formCtrl.$removeControl(modelCtrl);
});
},
post: function ngModelPostLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0];
if (modelCtrl.$options && modelCtrl.$options.updateOn) {
element.on(modelCtrl.$options.updateOn, function(ev) {
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
});
}
});
scope.$on('$destroy', function() {
formCtrl.$removeControl(modelCtrl);
});
},
post: function(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0];
if (modelCtrl.$options && modelCtrl.$options.updateOn) {
element.on(modelCtrl.$options.updateOn, function(ev) {
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
element.on('blur', function(ev) {
if (modelCtrl.$touched) return;
scope.$apply(function() {
modelCtrl.$setTouched();
});
});
}
element.on('blur', function(ev) {
if (modelCtrl.$touched) return;
scope.$apply(function() {
modelCtrl.$setTouched();
});
});
}
};
}
};
};
@@ -2967,8 +2990,9 @@ function addSetValidityMethod(context) {
parentForm = context.parentForm,
$animate = context.$animate;
classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
ctrl.$setValidity = setValidity;
toggleValidationCss('', true);
function setValidity(validationErrorKey, state, options) {
if (state === undefined) {
+119 -1
View File
@@ -49,7 +49,125 @@
...
</html>
```
*/
* @example
// Note: the suffix `.csp` in the example name triggers
// csp mode in our http server!
<example name="example.csp" module="cspExample" ng-csp="true">
<file name="index.html">
<div ng-controller="MainController as ctrl">
<div>
<button ng-click="ctrl.inc()" id="inc">Increment</button>
<span id="counter">
{{ctrl.counter}}
</span>
</div>
<div>
<button ng-click="ctrl.evil()" id="evil">Evil</button>
<span id="evilError">
{{ctrl.evilError}}
</span>
</div>
</div>
</file>
<file name="script.js">
angular.module('cspExample', [])
.controller('MainController', function() {
this.counter = 0;
this.inc = function() {
this.counter++;
};
this.evil = function() {
// jshint evil:true
try {
eval('1+2');
} catch (e) {
this.evilError = e.message;
}
};
});
</file>
<file name="protractor.js" type="protractor">
var util, webdriver;
var incBtn = element(by.id('inc'));
var counter = element(by.id('counter'));
var evilBtn = element(by.id('evil'));
var evilError = element(by.id('evilError'));
function getAndClearSevereErrors() {
return browser.manage().logs().get('browser').then(function(browserLog) {
return browserLog.filter(function(logEntry) {
return logEntry.level.value > webdriver.logging.Level.WARNING.value;
});
});
}
function clearErrors() {
getAndClearSevereErrors();
}
function expectNoErrors() {
getAndClearSevereErrors().then(function(filteredLog) {
expect(filteredLog.length).toEqual(0);
if (filteredLog.length) {
console.log('browser console errors: ' + util.inspect(filteredLog));
}
});
}
function expectError(regex) {
getAndClearSevereErrors().then(function(filteredLog) {
var found = false;
filteredLog.forEach(function(log) {
if (log.message.match(regex)) {
found = true;
}
});
if (!found) {
throw new Error('expected an error that matches ' + regex);
}
});
}
beforeEach(function() {
util = require('util');
webdriver = require('protractor/node_modules/selenium-webdriver');
});
// For now, we only test on Chrome,
// as Safari does not load the page with Protractor's injected scripts,
// and Firefox webdriver always disables content security policy (#6358)
if (browser.params.browser !== 'chrome') {
return;
}
it('should not report errors when the page is loaded', function() {
// clear errors so we are not dependent on previous tests
clearErrors();
// Need to reload the page as the page is already loaded when
// we come here
browser.driver.getCurrentUrl().then(function(url) {
browser.get(url);
});
expectNoErrors();
});
it('should evaluate expressions', function() {
expect(counter.getText()).toEqual('0');
incBtn.click();
expect(counter.getText()).toEqual('1');
expectNoErrors();
});
it('should throw and report an error when using "eval"', function() {
evilBtn.click();
expect(evilError.getText()).toMatch(/Content Security Policy/);
expectError(/Content Security Policy/);
});
</file>
</example>
*/
// ngCsp is not implemented as a proper directive any more, because we need it be processed while we
// bootstrap the system (before $parse is instantiated), for this reason we just have
+5 -6
View File
@@ -611,6 +611,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
id: option.id,
selected: option.selected
});
selectCtrl.addOption(option.label, element);
if (lastElement) {
lastElement.after(element);
} else {
@@ -622,7 +623,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
// remove any excessive OPTIONs in a group
index++; // increment since the existingOptions[0] is parent element not OPTION
while(existingOptions.length > index) {
existingOptions.pop().element.remove();
option = existingOptions.pop();
selectCtrl.removeOption(option.label);
option.element.remove();
}
}
// remove any excessive OPTGROUPs from select
@@ -658,11 +661,7 @@ var optionDirective = ['$interpolate', function($interpolate) {
selectCtrl = parent.data(selectCtrlName) ||
parent.parent().data(selectCtrlName); // in case we are in optgroup
if (selectCtrl && selectCtrl.databound) {
// For some reason Opera defaults to true and if not overridden this messes up the repeater.
// We don't want the view to drive the initialization of the model anyway.
element.prop('selected', false);
} else {
if (!selectCtrl || !selectCtrl.databound) {
selectCtrl = nullSelectCtrl;
}
+5
View File
@@ -92,6 +92,11 @@ forEach({
CONSTANTS[name] = constantGetter;
});
//Not quite a constant, but can be lex/parsed the same
CONSTANTS['this'] = function(self) { return self; };
CONSTANTS['this'].sharedGetter = true;
//Operators - will be wrapped by binaryFn/unaryFn/assignment/filter
var OPERATORS = extend(createMap(), {
/* jshint bitwise : false */
+45 -34
View File
@@ -128,14 +128,11 @@ function $RootScopeProvider(){
this.$$phase = this.$parent = this.$$watchers =
this.$$nextSibling = this.$$prevSibling =
this.$$childHead = this.$$childTail = null;
this['this'] = this.$root = this;
this.$root = this;
this.$$destroyed = false;
this.$$asyncQueue = [];
this.$$postDigestQueue = [];
this.$$listeners = {};
this.$$listenerCount = {};
this.$$isolateBindings = null;
this.$$applyAsyncQueue = [];
}
/**
@@ -184,18 +181,23 @@ function $RootScopeProvider(){
* When creating widgets, it is useful for the widget to not accidentally read parent
* state.
*
* @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent`
* of the newly created scope. Defaults to `this` scope if not provided.
* This is used when creating a transclude scope to correctly place it
* in the scope hierarchy while maintaining the correct prototypical
* inheritance.
*
* @returns {Object} The newly created child scope.
*
*/
$new: function(isolate) {
$new: function(isolate, parent) {
var child;
parent = parent || this;
if (isolate) {
child = new Scope();
child.$root = this.$root;
// ensure that there is just one async queue per $rootScope and its children
child.$$asyncQueue = this.$$asyncQueue;
child.$$postDigestQueue = this.$$postDigestQueue;
} else {
// Only create a child scope class if somebody asks for one,
// but cache it to allow the VM to optimize lookups.
@@ -212,16 +214,27 @@ function $RootScopeProvider(){
}
child = new this.$$ChildScope();
}
child['this'] = child;
child.$parent = this;
child.$$prevSibling = this.$$childTail;
if (this.$$childHead) {
this.$$childTail.$$nextSibling = child;
this.$$childTail = child;
child.$parent = parent;
child.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) {
parent.$$childTail.$$nextSibling = child;
parent.$$childTail = child;
} else {
this.$$childHead = this.$$childTail = child;
parent.$$childHead = parent.$$childTail = child;
}
// When the new scope is not isolated or we inherit from `this`, and
// the parent scope is destroyed, the property `$$destroyed` is inherited
// prototypically. In all other cases, this property needs to be set
// when the parent scope is destroyed.
// The listener needs to be added after the parent is set
if (isolate || parent != this) child.$on('$destroy', destroyChild);
return child;
function destroyChild() {
child.$$destroyed = true;
}
},
/**
@@ -697,8 +710,6 @@ function $RootScopeProvider(){
$digest: function() {
var watch, value, last,
watchers,
asyncQueue = this.$$asyncQueue,
postDigestQueue = this.$$postDigestQueue,
length,
dirty, ttl = TTL,
next, current, target = this,
@@ -863,6 +874,10 @@ function $RootScopeProvider(){
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
// Disable listeners, watchers and apply/digest methods
this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop;
this.$on = this.$watch = this.$watchGroup = function() { return noop; };
this.$$listeners = {};
// All of the code below is bogus code that works around V8's memory leak via optimized code
// and inline caches.
@@ -873,15 +888,7 @@ function $RootScopeProvider(){
// - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
this.$$childTail = this.$root = null;
// don't reset these to null in case some async task tries to register a listener/watch/task
this.$$listeners = {};
this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = [];
// prevent NPEs since these methods have references to properties we nulled out
this.$destroy = this.$digest = this.$apply = noop;
this.$on = this.$watch = this.$watchGroup = function() { return noop; };
this.$$childTail = this.$root = this.$$watchers = null;
},
/**
@@ -948,19 +955,19 @@ function $RootScopeProvider(){
$evalAsync: function(expr) {
// if we are outside of an $digest loop and this is the first time we are scheduling async
// task also schedule async auto-flush
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
if (!$rootScope.$$phase && !asyncQueue.length) {
$browser.defer(function() {
if ($rootScope.$$asyncQueue.length) {
if (asyncQueue.length) {
$rootScope.$digest();
}
});
}
this.$$asyncQueue.push({scope: this, expression: expr});
asyncQueue.push({scope: this, expression: expr});
},
$$postDigest : function(fn) {
this.$$postDigestQueue.push(fn);
postDigestQueue.push(fn);
},
/**
@@ -1044,7 +1051,7 @@ function $RootScopeProvider(){
*/
$applyAsync: function(expr) {
var scope = this;
expr && $rootScope.$$applyAsyncQueue.push($applyAsyncExpression);
expr && applyAsyncQueue.push($applyAsyncExpression);
scheduleApplyAsync();
function $applyAsyncExpression() {
@@ -1253,6 +1260,11 @@ function $RootScopeProvider(){
var $rootScope = new Scope();
//The internal queues. Expose them on the $rootScope for debugging/testing purposes.
var asyncQueue = $rootScope.$$asyncQueue = [];
var postDigestQueue = $rootScope.$$postDigestQueue = [];
var applyAsyncQueue = $rootScope.$$applyAsyncQueue = [];
return $rootScope;
@@ -1286,10 +1298,9 @@ function $RootScopeProvider(){
function initWatchVal() {}
function flushApplyAsync() {
var queue = $rootScope.$$applyAsyncQueue;
while (queue.length) {
while (applyAsyncQueue.length) {
try {
queue.shift()();
applyAsyncQueue.shift()();
} catch(e) {
$exceptionHandler(e);
}
+8 -7
View File
@@ -188,7 +188,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) {
link: function(scope, $element, attr, ctrl, $transclude) {
var currentScope,
currentElement,
previousElement,
previousLeaveAnimation,
autoScrollExp = attr.autoscroll,
onloadExp = attr.onload || '';
@@ -196,19 +196,20 @@ function ngViewFactory( $route, $anchorScroll, $animate) {
update();
function cleanupLastView() {
if(previousElement) {
previousElement.remove();
previousElement = null;
if(previousLeaveAnimation) {
$animate.cancel(previousLeaveAnimation);
previousLeaveAnimation = null;
}
if(currentScope) {
currentScope.$destroy();
currentScope = null;
}
if(currentElement) {
$animate.leave(currentElement).then(function() {
previousElement = null;
previousLeaveAnimation = $animate.leave(currentElement);
previousLeaveAnimation.then(function() {
previousLeaveAnimation = null;
});
previousElement = currentElement;
currentElement = null;
}
}
+375 -46
View File
@@ -3124,6 +3124,35 @@ describe('$compile', function() {
}));
it('should be able to bind attribute names which are present in Object.prototype', function() {
module(function() {
directive('inProtoAttr', valueFn({
scope: {
'constructor': '@',
'toString': '&',
// Spidermonkey extension, may be obsolete in the future
'watch': '=',
}
}));
});
inject(function($rootScope) {
expect(function() {
compile('<div in-proto-attr constructor="hello, world" watch="[]" ' +
'to-string="value = !value"></div>');
}).not.toThrow();
var isolateScope = element.isolateScope();
expect(typeof isolateScope.constructor).toBe('string');
expect(isArray(isolateScope.watch)).toBe(true);
expect(typeof isolateScope.toString).toBe('function');
expect($rootScope.value).toBeUndefined();
isolateScope.toString();
expect($rootScope.value).toBe(true);
});
});
describe('bind-once', function () {
function countWatches(scope) {
@@ -3672,6 +3701,43 @@ describe('$compile', function() {
});
it('should get required parent controller', function() {
module(function() {
directive('nested', function(log) {
return {
require: '^^?nested',
controller: function($scope) {},
link: function(scope, element, attrs, controller) {
log(!!controller);
}
};
});
});
inject(function(log, $compile, $rootScope) {
element = $compile('<div nested><div nested></div></div>')($rootScope);
expect(log).toEqual('true; false');
});
});
it('should throw if required parent is not found', function() {
module(function() {
directive('nested', function() {
return {
require: '^^nested',
controller: function($scope) {},
link: function(scope, element, attrs, controller) {}
};
});
});
inject(function($compile, $rootScope) {
expect(function() {
element = $compile('<div nested></div>')($rootScope);
}).toThrowMinErr('$compile', 'ctreq', "Controller 'nested', required by directive 'nested', can't be found!");
});
});
it('should get required controller via linkingFn (template)', function() {
module(function() {
directive('dirA', function() {
@@ -4331,17 +4397,21 @@ describe('$compile', function() {
return {
transclude: 'content',
replace: true,
scope: true,
template: '<ul><li>W:{{$parent.$id}}-{{$id}};</li><li ng-transclude></li></ul>'
scope: {},
link: function(scope) {
scope.x='iso';
},
template: '<ul><li>W:{{x}}-{{$parent.$id}}-{{$id}};</li><li ng-transclude></li></ul>'
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('<div><div trans>T:{{$parent.$id}}-{{$id}}<span>;</span></div></div>')
element = $compile('<div><div trans>T:{{x}}-{{$parent.$id}}-{{$id}}<span>;</span></div></div>')
($rootScope);
$rootScope.x = 'root';
$rootScope.$apply();
expect(element.text()).toEqual('W:1-2;T:1-3;');
expect(jqLite(element.find('span')[0]).text()).toEqual('T:1-3');
expect(element.text()).toEqual('W:iso-1-2;T:root-2-3;');
expect(jqLite(element.find('span')[0]).text()).toEqual('T:root-2-3');
expect(jqLite(element.find('span')[1]).text()).toEqual(';');
});
});
@@ -4496,6 +4566,65 @@ describe('$compile', function() {
});
});
it('should not leak when continuing the compilation of elements on a scope that was destroyed', function() {
if (jQuery) {
// jQuery 2.x doesn't expose the cache storage.
return;
}
var linkFn = jasmine.createSpy('linkFn');
module(function($controllerProvider, $compileProvider) {
$controllerProvider.register('Leak', function ($scope, $timeout) {
$scope.code = 'red';
$timeout(function () {
$scope.code = 'blue';
});
});
$compileProvider.directive('isolateRed', function() {
return {
restrict: 'A',
scope: {},
template: '<div red></div>'
};
});
$compileProvider.directive('red', function() {
return {
restrict: 'A',
templateUrl: 'red.html',
scope: {},
link: linkFn
};
});
});
inject(function($compile, $rootScope, $httpBackend, $timeout, $templateCache) {
$httpBackend.whenGET('red.html').respond('<p>red.html</p>');
var template = $compile(
'<div ng-controller="Leak">' +
'<div ng-switch="code">' +
'<div ng-switch-when="red">' +
'<div isolate-red></div>' +
'</div>' +
'</div>' +
'</div>');
element = template($rootScope);
$rootScope.$digest();
$timeout.flush();
$httpBackend.flush();
expect(linkFn).not.toHaveBeenCalled();
expect(jqLiteCacheSize()).toEqual(2);
$templateCache.removeAll();
var destroyedScope = $rootScope.$new();
destroyedScope.$destroy();
var clone = template(destroyedScope);
$rootScope.$digest();
$timeout.flush();
expect(linkFn).not.toHaveBeenCalled();
});
});
if (jQuery) {
describe('cleaning up after a replaced element', function () {
var $compile, xs;
@@ -4551,47 +4680,6 @@ describe('$compile', function() {
}
it('should remove transclusion scope, when the DOM is destroyed', function() {
module(function() {
directive('box', valueFn({
transclude: true,
scope: { name: '=', show: '=' },
template: '<div><h1>Hello: {{name}}!</h1><div ng-transclude></div></div>',
link: function(scope, element) {
scope.$watch(
'show',
function(show) {
if (!show) {
element.find('div').find('div').remove();
}
}
);
}
}));
});
inject(function($compile, $rootScope) {
$rootScope.username = 'Misko';
$rootScope.select = true;
element = $compile(
'<div><div box name="username" show="select">user: {{username}}</div></div>')
($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('Hello: Misko!user: Misko');
var widgetScope = $rootScope.$$childHead;
var transcludeScope = widgetScope.$$nextSibling;
expect(widgetScope.name).toEqual('Misko');
expect(widgetScope.$parent).toEqual($rootScope);
expect(transcludeScope.$parent).toEqual($rootScope);
$rootScope.select = false;
$rootScope.$apply();
expect(element.text()).toEqual('Hello: Misko!');
expect(widgetScope.$$nextSibling).toEqual(null);
});
});
it('should add a $$transcluded property onto the transcluded scope', function() {
module(function() {
directive('trans', function() {
@@ -4964,6 +5052,179 @@ describe('$compile', function() {
});
// see issue https://github.com/angular/angular.js/issues/9095
describe('removing a transcluded element', function() {
function countScopes($rootScope) {
return [$rootScope].concat(
getChildScopes($rootScope)
).length;
}
function getChildScopes(scope) {
var children = [];
if (!scope.$$childHead) { return children; }
var childScope = scope.$$childHead;
do {
children.push(childScope);
children = children.concat(getChildScopes(childScope));
} while ((childScope = childScope.$$nextSibling));
return children;
}
beforeEach(module(function() {
directive('toggle', function() {
return {
transclude: true,
template: '<div ng:if="t"><div ng:transclude></div></div>'
};
});
}));
it('should not leak the transclude scope when the transcluded content is an element transclusion directive',
inject(function($compile, $rootScope) {
element = $compile(
'<div toggle>' +
'<div ng:repeat="msg in [\'msg-1\']">{{ msg }}</div>' +
'</div>'
)($rootScope);
$rootScope.$apply('t = true');
expect(element.text()).toContain('msg-1');
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat
expect(countScopes($rootScope)).toEqual(4);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('msg-1');
// Expected scopes: $rootScope
expect(countScopes($rootScope)).toEqual(1);
$rootScope.$apply('t = true');
expect(element.text()).toContain('msg-1');
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat
expect(countScopes($rootScope)).toEqual(4);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('msg-1');
// Expected scopes: $rootScope
expect(countScopes($rootScope)).toEqual(1);
}));
it('should not leak the transclude scope when the transcluded content is an multi-element transclusion directive',
inject(function($compile, $rootScope) {
element = $compile(
'<div toggle>' +
'<div ng:repeat-start="msg in [\'msg-1\']">{{ msg }}</div>' +
'<div ng:repeat-end>{{ msg }}</div>' +
'</div>'
)($rootScope);
$rootScope.$apply('t = true');
expect(element.text()).toContain('msg-1msg-1');
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat
expect(countScopes($rootScope)).toEqual(4);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('msg-1msg-1');
// Expected scopes: $rootScope
expect(countScopes($rootScope)).toEqual(1);
$rootScope.$apply('t = true');
expect(element.text()).toContain('msg-1msg-1');
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat
expect(countScopes($rootScope)).toEqual(4);
$rootScope.$apply('t = false');
expect(element.text()).not.toContain('msg-1msg-1');
// Expected scopes: $rootScope
expect(countScopes($rootScope)).toEqual(1);
}));
it('should not leak the transclude scope if the transcluded contains only comments',
inject(function($compile, $rootScope) {
element = $compile(
'<div toggle>' +
'<!-- some comment -->' +
'</div>'
)($rootScope);
$rootScope.$apply('t = true');
expect(element.html()).toContain('some comment');
// Expected scopes: $rootScope, ngIf, transclusion
expect(countScopes($rootScope)).toEqual(3);
$rootScope.$apply('t = false');
expect(element.html()).not.toContain('some comment');
// Expected scopes: $rootScope
expect(countScopes($rootScope)).toEqual(1);
$rootScope.$apply('t = true');
expect(element.html()).toContain('some comment');
// Expected scopes: $rootScope, ngIf, transclusion
expect(countScopes($rootScope)).toEqual(3);
$rootScope.$apply('t = false');
expect(element.html()).not.toContain('some comment');
// Expected scopes: $rootScope
expect(countScopes($rootScope)).toEqual(1);
}));
it('should not leak the transclude scope if the transcluded contains only text nodes',
inject(function($compile, $rootScope) {
element = $compile(
'<div toggle>' +
'some text' +
'</div>'
)($rootScope);
$rootScope.$apply('t = true');
expect(element.html()).toContain('some text');
// Expected scopes: $rootScope, ngIf, transclusion
expect(countScopes($rootScope)).toEqual(3);
$rootScope.$apply('t = false');
expect(element.html()).not.toContain('some text');
// Expected scopes: $rootScope
expect(countScopes($rootScope)).toEqual(1);
$rootScope.$apply('t = true');
expect(element.html()).toContain('some text');
// Expected scopes: $rootScope, ngIf, transclusion
expect(countScopes($rootScope)).toEqual(3);
$rootScope.$apply('t = false');
expect(element.html()).not.toContain('some text');
// Expected scopes: $rootScope
expect(countScopes($rootScope)).toEqual(1);
}));
it('should mark as destroyed all sub scopes of the scope being destroyed',
inject(function($compile, $rootScope) {
element = $compile(
'<div toggle>' +
'<div ng:repeat="msg in [\'msg-1\']">{{ msg }}</div>' +
'</div>'
)($rootScope);
$rootScope.$apply('t = true');
var childScopes = getChildScopes($rootScope);
$rootScope.$apply('t = false');
for (var i = 0; i < childScopes.length; ++i) {
expect(childScopes[i].$$destroyed).toBe(true);
}
}));
});
describe('nested transcludes', function() {
beforeEach(module(function($compileProvider) {
@@ -5609,6 +5870,74 @@ describe('$compile', function() {
});
});
describe('img[srcset] sanitization', function() {
it('should NOT require trusted values for img srcset', inject(function($rootScope, $compile, $sce) {
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope);
$rootScope.testUrl = 'http://example.com/image.png';
$rootScope.$digest();
expect(element.attr('srcset')).toEqual('http://example.com/image.png');
// But it should accept trusted values anyway.
$rootScope.testUrl = $sce.trustAsUrl('http://example.com/image2.png');
$rootScope.$digest();
expect(element.attr('srcset')).toEqual('http://example.com/image2.png');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope);
$rootScope.testUrl = "someUrl";
$$sanitizeUri.andReturn('someSanitizedUrl');
$rootScope.$apply();
expect(element.attr('srcset')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
});
});
it('should sanitize all uris in srcset', inject(function($rootScope, $compile) {
/*jshint scripturl:true*/
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope);
var testSet = {
'http://example.com/image.png':'http://example.com/image.png',
' http://example.com/image.png':'http://example.com/image.png',
'http://example.com/image.png ':'http://example.com/image.png',
'http://example.com/image.png 128w':'http://example.com/image.png 128w',
'http://example.com/image.png 2x':'http://example.com/image.png 2x',
'http://example.com/image.png 1.5x':'http://example.com/image.png 1.5x',
'http://example.com/image1.png 1x,http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x ,http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x, http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x , http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
'http://example.com/image1.png 48w,http://example.com/image2.png 64w':'http://example.com/image1.png 48w,http://example.com/image2.png 64w',
//Test regex to make sure doesn't mistake parts of url for width descriptors
'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w':'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w',
'http://example.com/image1.png 1x,http://example.com/image2.png 64w':'http://example.com/image1.png 1x,http://example.com/image2.png 64w',
'http://example.com/image1.png,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png ,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png, http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png , http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png 1x, http://example.com/image2.png 2x, http://example.com/image3.png 3x':
'http://example.com/image1.png 1x,http://example.com/image2.png 2x,http://example.com/image3.png 3x',
'javascript:doEvilStuff() 2x': 'unsafe:javascript:doEvilStuff() 2x',
'http://example.com/image1.png 1x,javascript:doEvilStuff() 2x':'http://example.com/image1.png 1x,unsafe:javascript:doEvilStuff() 2x',
'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x',
//Test regex to make sure doesn't mistake parts of url for pixel density descriptors
'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x'
};
forEach( testSet, function( ref, url) {
$rootScope.testUrl = url;
$rootScope.$digest();
expect(element.attr('srcset')).toEqual(ref);
});
}));
});
describe('a[href] sanitization', function() {
+122 -27
View File
@@ -946,6 +946,69 @@ describe('ngModel', function() {
dealoc(element);
}));
describe('custom formatter and parser that are added by a directive in post linking', function() {
var inputElm, scope;
beforeEach(module(function($compileProvider) {
$compileProvider.directive('customFormat', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$formatters.push(function(value) {
return value.part;
});
ngModelCtrl.$parsers.push(function(value) {
return {part: value};
});
}
};
});
}));
afterEach(function() {
dealoc(inputElm);
});
function createInput(type) {
inject(function($compile, $rootScope) {
scope = $rootScope;
inputElm = $compile('<input type="'+type+'" ng-model="val" custom-format/>')($rootScope);
});
}
it('should use them after the builtin ones for text inputs', function() {
createInput('text');
scope.$apply('val = {part: "a"}');
expect(inputElm.val()).toBe('a');
inputElm.val('b');
browserTrigger(inputElm, 'change');
expect(scope.val).toEqual({part: 'b'});
});
it('should use them after the builtin ones for number inputs', function() {
createInput('number');
scope.$apply('val = {part: 1}');
expect(inputElm.val()).toBe('1');
inputElm.val('2');
browserTrigger(inputElm, 'change');
expect(scope.val).toEqual({part: 2});
});
it('should use them after the builtin ones for date inputs', function() {
createInput('date');
scope.$apply(function() {
scope.val = {part: new Date(2000, 10, 8)};
});
expect(inputElm.val()).toBe('2000-11-08');
inputElm.val('2001-12-09');
browserTrigger(inputElm, 'change');
expect(scope.val).toEqual({part: new Date(2001, 11, 9)});
});
});
it('should always format the viewValue as a string for a blank input type when the value is present',
inject(function($compile, $rootScope, $sniffer) {
@@ -2251,14 +2314,14 @@ describe('input', function() {
// INPUT TYPES
describe('month', function (){
it('should render blank if model is not a Date object', function() {
it('should throw if model is not a Date object', function() {
compileInput('<input type="month" ng-model="january"/>');
scope.$apply(function(){
scope.january = '2013-01';
});
expect(inputElm.val()).toBe('');
expect(function() {
scope.$apply(function(){
scope.january = '2013-01';
});
}).toThrowMinErr('ngModel', 'datefmt', 'Expected `2013-01` to be a date');
});
it('should set the view if the model is a valid Date object', function (){
@@ -2433,14 +2496,14 @@ describe('input', function() {
});
describe('week', function (){
it('should set render blank if model is not a Date object', function() {
it('should throw if model is not a Date object', function() {
compileInput('<input type="week" ng-model="secondWeek"/>');
scope.$apply(function(){
scope.secondWeek = '2013-W02';
});
expect(inputElm.val()).toBe('');
expect(function() {
scope.$apply(function(){
scope.secondWeek = '2013-W02';
});
}).toThrowMinErr('ngModel', 'datefmt', 'Expected `2013-W02` to be a date');
});
it('should set the view if the model is a valid Date object', function (){
@@ -2615,14 +2678,14 @@ describe('input', function() {
});
describe('datetime-local', function () {
it('should render blank if model is not a Date object', function() {
it('should throw if model is not a Date object', function() {
compileInput('<input type="datetime-local" ng-model="lunchtime"/>');
scope.$apply(function(){
scope.lunchtime = '2013-12-16T11:30:00';
});
expect(inputElm.val()).toBe('');
expect(function() {
scope.$apply(function(){
scope.lunchtime = '2013-12-16T11:30:00';
});
}).toThrowMinErr('ngModel', 'datefmt', 'Expected `2013-12-16T11:30:00` to be a date');
});
it('should set the view if the model if a valid Date object.', function(){
@@ -2890,14 +2953,14 @@ describe('input', function() {
});
describe('time', function () {
it('should render blank if model is not a Date object', function() {
it('should throw if model is not a Date object', function() {
compileInput('<input type="time" ng-model="lunchtime"/>');
scope.$apply(function(){
scope.lunchtime = '11:30:00';
});
expect(inputElm.val()).toBe('');
expect(function() {
scope.$apply(function(){
scope.lunchtime = '11:30:00';
});
}).toThrowMinErr('ngModel', 'datefmt', 'Expected `11:30:00` to be a date');
});
it('should set the view if the model if a valid Date object.', function(){
@@ -3141,11 +3204,24 @@ describe('input', function() {
});
describe('date', function () {
it('should render blank if model is not a Date object.', function() {
it('should throw if model is not a Date object.', function() {
compileInput('<input type="date" ng-model="birthday"/>');
scope.$apply(function(){
scope.birthday = '1977-10-22';
expect(function() {
scope.$apply(function(){
scope.birthday = '1977-10-22';
});
}).toThrowMinErr('ngModel', 'datefmt', 'Expected `1977-10-22` to be a date');
});
it('should set the view to empty when the model is an InvalidDate', function() {
compileInput('<input type="date" ng-model="val"/>');
// reset the element type to text otherwise newer browsers
// would always set the input.value to empty for invalid dates...
inputElm.attr('type', 'text');
scope.$apply(function (){
scope.val = new Date('a');
});
expect(inputElm.val()).toBe('');
@@ -3514,6 +3590,17 @@ describe('input', function() {
expect(inputElm).toBeValid();
});
it('should validate with undefined viewValue when $validate() called', function() {
compileInput('<input type="number" name="alias" ng-model="value" />');
scope.form.alias.$validate();
expect(inputElm).toBeValid();
expect(scope.form.alias.$error.number).toBeUndefined();
});
it('should throw if the model value is not a number', function() {
expect(function() {
scope.value = 'one';
@@ -3717,6 +3804,14 @@ describe('input', function() {
expect(inputElm).toBeInvalid();
expect(scope.form.alias.$error.required).toBeTruthy();
});
it('should not invalidate number if ng-required=false and viewValue has not been committed', function() {
compileInput('<input type="number" ng-model="value" name="alias" ng-required="required">');
scope.$apply("required = false");
expect(inputElm).toBeValid();
});
});
describe('minlength', function() {
+2 -2
View File
@@ -160,13 +160,13 @@ describe('ngBind*', function() {
it('should NOT set html for untrusted values', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind-html="html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
expect($rootScope.$digest).toThrow();
expect(function() { $rootScope.$digest(); }).toThrow();
}));
it('should NOT set html for wrongly typed values', inject(function($rootScope, $compile, $sce) {
element = $compile('<div ng-bind-html="html"></div>')($rootScope);
$rootScope.html = $sce.trustAsCss('<div onclick="">hello</div>');
expect($rootScope.$digest).toThrow();
expect(function() { $rootScope.$digest(); }).toThrow();
}));
it('should set html for trusted values', inject(function($rootScope, $compile, $sce) {
+16 -7
View File
@@ -8,13 +8,22 @@ describe('ngSrc', function() {
dealoc(element);
});
it('should not result empty string in img src', inject(function($rootScope, $compile) {
$rootScope.image = {};
element = $compile('<img ng-src="{{image.url}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('src')).not.toBe('');
expect(element.attr('src')).toBe(undefined);
}));
describe('img[ng-src]', function() {
it('should not result empty string in img src', inject(function($rootScope, $compile) {
$rootScope.image = {};
element = $compile('<img ng-src="{{image.url}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('src')).not.toBe('');
expect(element.attr('src')).toBe(undefined);
}));
it('should sanitize url', inject(function($rootScope, $compile) {
$rootScope.imageUrl = 'javascript:alert(1);';
element = $compile('<img ng-src="{{imageUrl}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('src')).toBe('unsafe:javascript:alert(1);');
}));
});
describe('iframe[ng-src]', function() {
it('should pass through src attributes for the same domain', inject(function($compile, $rootScope) {
+16
View File
@@ -1,3 +1,4 @@
/*jshint scripturl:true*/
'use strict';
describe('ngSrcset', function() {
@@ -13,4 +14,19 @@ describe('ngSrcset', function() {
$rootScope.$digest();
expect(element.attr('srcset')).toBeUndefined();
}));
it('should sanitize good urls', inject(function($rootScope, $compile) {
$rootScope.imageUrl = 'http://example.com/image1.png 1x, http://example.com/image2.png 2x';
element = $compile('<img ng-srcset="{{imageUrl}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('srcset')).toBe('http://example.com/image1.png 1x,http://example.com/image2.png 2x');
}));
it('should sanitize evil url', inject(function($rootScope, $compile) {
$rootScope.imageUrl = 'http://example.com/image1.png 1x, javascript:doEvilStuff() 2x';
element = $compile('<img ng-srcset="{{imageUrl}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('srcset')).toBe('http://example.com/image1.png 1x,unsafe:javascript:doEvilStuff() 2x');
}));
});
+66
View File
@@ -405,6 +405,39 @@ describe('select', function() {
expect(element).toEqualSelect(['? string:r2d2 ?']);
expect(scope.robot).toBe('r2d2');
});
describe('selectController.hasOption', function() {
it('should return true for options added via ngOptions', function() {
scope.robots = [
{key: 1, value: 'c3p0'},
{key: 2, value: 'r2d2'}
];
scope.robot = 'r2d2';
compile('<select ng-model="robot" ' +
'ng-options="item.key as item.value for item in robots">' +
'</select>');
var selectCtrl = element.data().$selectController;
expect(selectCtrl.hasOption('c3p0')).toBe(true);
expect(selectCtrl.hasOption('r2d2')).toBe(true);
scope.$apply(function() {
scope.robots.pop();
});
expect(selectCtrl.hasOption('c3p0')).toBe(true);
expect(selectCtrl.hasOption('r2d2')).toBe(false);
scope.$apply(function() {
scope.robots.push({key: 2, value: 'r2d2'});
});
expect(selectCtrl.hasOption('c3p0')).toBe(true);
expect(selectCtrl.hasOption('r2d2')).toBe(true);
});
});
});
});
});
@@ -481,6 +514,39 @@ describe('select', function() {
expect(element).toBeValid();
expect(element).toBeDirty();
});
describe('selectController.hasOption', function() {
it('should return true for options added via ngOptions', function() {
scope.robots = [
{key: 1, value: 'c3p0'},
{key: 2, value: 'r2d2'}
];
scope.robot = 'r2d2';
compile('<select ng-model="robot" multiple ' +
'ng-options="item.key as item.value for item in robots">' +
'</select>');
var selectCtrl = element.data().$selectController;
expect(selectCtrl.hasOption('c3p0')).toBe(true);
expect(selectCtrl.hasOption('r2d2')).toBe(true);
scope.$apply(function() {
scope.robots.pop();
});
expect(selectCtrl.hasOption('c3p0')).toBe(true);
expect(selectCtrl.hasOption('r2d2')).toBe(false);
scope.$apply(function() {
scope.robots.push({key: 2, value: 'r2d2'});
});
expect(selectCtrl.hasOption('c3p0')).toBe(true);
expect(selectCtrl.hasOption('r2d2')).toBe(true);
});
});
});
+1 -1
View File
@@ -1528,7 +1528,7 @@ describe('$http', function() {
});
describe('$http with $applyAapply', function() {
describe('$http with $applyAsync', function() {
var $http, $httpBackend, $rootScope, $browser, log;
beforeEach(module(function($httpProvider) {
$httpProvider.useApplyAsync(true);
+63 -3
View File
@@ -51,8 +51,25 @@ describe('Scope', function() {
describe('this', function() {
it('should have a \'this\'', inject(function($rootScope) {
expect($rootScope['this']).toEqual($rootScope);
it('should evaluate \'this\' to be the scope', inject(function($rootScope) {
var child = $rootScope.$new();
expect($rootScope.$eval('this')).toEqual($rootScope);
expect(child.$eval('this')).toEqual(child);
}));
it('\'this\' should not be recursive', inject(function($rootScope) {
expect($rootScope.$eval('this.this')).toBeUndefined();
expect($rootScope.$eval('$parent.this')).toBeUndefined();
}));
it('should not be able to overwrite the \'this\' keyword', inject(function($rootScope) {
$rootScope['this'] = 123;
expect($rootScope.$eval('this')).toEqual($rootScope);
}));
it('should be able to access a variable named \'this\'', inject(function($rootScope) {
$rootScope['this'] = 42;
expect($rootScope.$eval('this[\'this\']')).toBe(42);
}));
});
@@ -72,6 +89,15 @@ describe('Scope', function() {
expect(child.$new).toBe($rootScope.$new);
expect(child.$root).toBe($rootScope);
}));
it("should attach the child scope to a specified parent", inject(function($rootScope) {
var isolated = $rootScope.$new(true);
var trans = $rootScope.$new(false, isolated);
$rootScope.a = 123;
expect(isolated.a).toBeUndefined();
expect(trans.a).toEqual(123);
expect(trans.$parent).toBe(isolated);
}));
});
@@ -994,6 +1020,13 @@ describe('Scope', function() {
expect(log).toBe('123');
}));
it('should broadcast the $destroy only once', inject(function($rootScope, log) {
var isolateScope = first.$new(true);
isolateScope.$on('$destroy', log.fn('event'));
first.$destroy();
isolateScope.$destroy();
expect(log).toEqual('event');
}));
it('should decrement ancestor $$listenerCount entries', inject(function($rootScope) {
var EVENT = 'fooEvent',
@@ -1038,6 +1071,33 @@ describe('Scope', function() {
expect(fn).toBe(noop);
}));
it("should do nothing when $apply()ing after parent's destruction", inject(function($rootScope) {
var parent = $rootScope.$new(),
child = parent.$new();
parent.$destroy();
var called = false;
function applyFunc() { called = true; }
child.$apply(applyFunc);
expect(called).toBe(false);
}));
it("should do nothing when $evalAsync()ing after parent's destruction", inject(function($rootScope, $timeout) {
var parent = $rootScope.$new(),
child = parent.$new();
parent.$destroy();
var called = false;
function applyFunc() { called = true; }
child.$evalAsync(applyFunc);
$timeout.verifyNoPendingTasks();
expect(called).toBe(false);
}));
it("should preserve all (own and inherited) model properties on a destroyed scope",
inject(function($rootScope) {
@@ -1204,7 +1264,7 @@ describe('Scope', function() {
isolateScope.$evalAsync('isolateExpression');
expect(childScope.$$asyncQueue).toBe($rootScope.$$asyncQueue);
expect(isolateScope.$$asyncQueue).toBe($rootScope.$$asyncQueue);
expect(isolateScope.$$asyncQueue).toBeUndefined();
expect($rootScope.$$asyncQueue).toEqual([
{scope: $rootScope, expression: 'rootExpression'},
{scope: childScope, expression: 'childExpression'},
-3
View File
@@ -107,9 +107,6 @@ describe('$sniffer', function() {
else if(/ie/i.test(ua) || /trident/i.test(ua)) {
expectedPrefix = 'Ms';
}
else if(/opera/i.test(ua)) {
expectedPrefix = 'O';
}
expect($sniffer.vendorPrefix).toBe(expectedPrefix);
});
});
+4 -14
View File
@@ -845,18 +845,8 @@ describe('ngView animations', function() {
});
});
it('should destroy the previous leave animation if a new one takes place', function() {
module(function($provide) {
$provide.decorator('$animate', function($delegate, $$q) {
var emptyPromise = $$q.defer().promise;
$delegate.leave = function() {
return emptyPromise;
};
return $delegate;
});
});
inject(function ($compile, $rootScope, $animate, $location) {
var item;
it('should destroy the previous leave animation if a new one takes place',
inject(function ($compile, $rootScope, $animate, $location, $timeout) {
var $scope = $rootScope.$new();
element = $compile(html(
'<div>' +
@@ -884,8 +874,8 @@ describe('ngView animations', function() {
$rootScope.$digest();
expect(destroyed).toBe(true);
});
});
})
);
});