Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10644432ca | |||
| a0bfdd0d60 | |||
| 3624e3800f | |||
| b1ee5386d5 | |||
| ab80cd9066 | |||
| 8199f4dbde | |||
| 313d7956e4 | |||
| b9479ee73b | |||
| 769a00dc86 | |||
| 6593c2371e | |||
| 8b54524c07 | |||
| 66bb5aa41c | |||
| b186709003 | |||
| a27d827c22 | |||
| a1648a76c0 | |||
| 2bcd02dc1a | |||
| b0033a44bd | |||
| 76b755f3cb | |||
| 6303c3dcf6 | |||
| cd2cfafcab | |||
| 86d33c5f9d | |||
| eb935e6be0 | |||
| b119251827 | |||
| 5572b40b15 | |||
| 4a6c7cf8ce | |||
| 27d12340d9 | |||
| fb0c77f0b6 | |||
| 6417a3e9eb | |||
| 07e3abc7dd | |||
| b9df121655 | |||
| b5bb4a986a | |||
| 8202c4dcea | |||
| 2c8b464852 | |||
| a192c41ddc | |||
| a8fe2cc345 | |||
| e522c25fd4 | |||
| 5dbc2d65f3 | |||
| 27300072d1 | |||
| 7ffc247d0f | |||
| 8ab673d430 | |||
| b9e899c8b2 | |||
| 92f05e5a59 | |||
| e81ae1464d |
+1
-1
@@ -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
@@ -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
@@ -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)
|
||||
];
|
||||
|
||||
@@ -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, '&').
|
||||
replace(/\</g, '<').
|
||||
replace(/\>/g, '>').
|
||||
replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -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
@@ -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('!');
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -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
@@ -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>'
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/)
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Generated
+796
-342
File diff suppressed because it is too large
Load Diff
+7
-6
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user