Compare commits

...

44 Commits

Author SHA1 Message Date
Jeff Cross de38899f74 docs(changelog): add release notes for 1.3.0-rc.3 2014-09-23 18:47:24 -07:00
Caitlin Potter 729c238e19 feat(input): support dynamic element validation
Interpolates the form and form control attribute name, so that dynamic form controls (such as those
rendered in an ngRepeat) will always have their expected interpolated name.

The control will be present in its parent form controller with the interpolated property name, and
this name can change when the interpolated value changes.

Closes #4791
Closes #1404
2014-09-23 16:03:53 -04:00
Jeff Cross dc3de7fb7a feat($location): add ability to opt-out of <base/> tag requirement in html5Mode
This feature allows disabling Angular's requirement of using a <base/> tag
when using location in html5Mode, for applications that do not require
using $location in html5Mode in IE9. To accomplish this, the $locationProvider.html5Mode 
method has been changed to accept a definition object which can optionally set a 
requireBase property to false, removing the requirement of a <base> tag being present
when html5Mode is enabled.

BREAKING CHANGE: The $location.html5Mode API has changed to allow enabling html5Mode by
    passing an object (as well as still supporting passing a boolean). Symmetrically, the
    method now returns an object instead of a boolean value.

    To migrate, follow the code example below:

    Before:

    var mode = $locationProvider.html5Mode();

    After:

    var mode = $locationProvider.html5Mode().enabled;

Fixes #8934
2014-09-23 11:34:24 -07:00
Peter Bacon Darwin ace40d5526 chore(docs): refactor the docs app search for better bootup time
This commit refactors how the search index is built. The docsSearch service
is now defined by a provider, which returns a different implementation of
the service depending upon whether the current browser supports WebWorkers
or now.

* **WebWorker supported**: The index is then built and stored in a new worker.
The service posts and receives messages to and from this worker to make
queries on the search index.

* **WebWorker no supported**: The index is built locally but with a 500ms
delay so that the initial page can render before the browser is blocked as
the index is built.

Also the way that the current app is identified has been modified so we can
slim down the js data files (pages-data.js) to again improve startup time.

Closes #9204
Closes #9203
2014-09-23 18:58:45 +01:00
Shahar Talmi fd8997551f feat(formController): add $setUntouched to propagate untouched state
Closes #9050
2014-09-23 13:48:39 -04:00
Brian Ford d8c8b2ebb7 chore(bower): add ngAria module to script 2014-09-22 15:27:00 -07:00
Andrew Delikat f5bb34ab4a docs(tutorial/step_05): fix typo 2014-09-22 14:52:20 -07:00
Shahar Talmi 4b83f6ca2c fix(ngModel): support milliseconds in time and datetime
Closes #8874
2014-09-22 14:50:08 -07:00
Rouven Weßling a591e8b8d3 perf(map): use Array.prototype.map
Replace helper functions with the native ES5 method
2014-09-22 14:09:48 -07:00
William Chen 6b05105c08 docs(triaging): fix formatting 2014-09-22 13:14:49 -07:00
Bocharsky Victor 728832ec85 docs(guide/$location): fix broken link 2014-09-22 13:12:27 -07:00
Christopher Rains f1a75a445c docs(tutorial/step_02): fix formatting 2014-09-22 13:07:19 -07:00
James Ferguson c59bee5d21 docs(readme): improve readability 2014-09-22 11:40:30 -07:00
Maarten Stolte 17ecf84b90 docs(ngAria): fix wording 2014-09-22 11:35:10 -07:00
Ariel Mashraki df8d9507aa docs(route): remove irrelevant note
Closes #9196
Closes #9200
2014-09-22 13:27:43 -04:00
krusty 6e7fbe77c9 docs(guide/directive): remove note about default restrict value
The text said a directive wouldn't work out of the box as an element, but the note immediatelly
below says that by default the directives restrict to elements or attributes.

11f5aee made the removed comments invalid.

Closes #9205
2014-09-21 20:59:20 -04:00
Jeff Cross 3686f45398 chore($http): disable flaky JSONP test
See #9185
2014-09-19 17:19:57 -07:00
Brian Ford ad28baaa6c refactor(ngAria): bind to ngModel rather than form types 2014-09-19 15:31:48 -07:00
Peter Bacon Darwin 8f9c4daca5 docs(limitTo): restore the missing * to make comment a jsdoc block 2014-09-19 22:53:48 +01:00
Peter Bacon Darwin f0c94ea292 docs(limitTo): exclude the e2e test that fails on safari
Safari and doesn't like the minus key to be sent to it via Protractor.
Commenting out rather than using xit so that it passes the build!
2014-09-19 22:45:14 +01:00
Peter Bacon Darwin 729129b461 docs(limitTo): exclude the e2e test that fails on safari
Safari and doesn't like the minus key to be sent to it via Protractor.
Commenting out rather than using xit so that it passes the build!
2014-09-19 22:42:48 +01:00
Peter Bacon Darwin bf2c55ea29 docs($aria): add basic missing docs for the $aria service
The individual service methods should be documented too.

cc: @arbus
2014-09-19 22:40:05 +01:00
Peter Bacon Darwin deafb5e545 docs(limitTo): exclude the e2e test that fails on safari
Safari and doesn't like the minus key to be sent to it via Protractor.
2014-09-19 22:35:51 +01:00
Ciro Nunes 0702aef7ee docs(guide/unit-testing): clarify the use of underscore notation
Closes #9024
2014-09-19 22:24:48 +01:00
Ciro Nunes f0ee335311 test(injector): allow service names with a single underscore
Closes #9024
2014-09-19 22:24:48 +01:00
Sekib Omazic 02169d4957 docs(angular.extend) actually only copies own enumerable properties
Closes #9007
2014-09-19 21:55:31 +01:00
Zahid Mahir 25d0eff3e6 docs(tutorial/step-3): correct slight grammar issue
Closes #8996
2014-09-19 21:36:08 +01:00
jimmywarting bd41bd594c docs(limitTo): fix input type in examples
Closes #8989
2014-09-19 21:24:04 +01:00
Georgios Kalpakas 373d7c95d9 docs(ngResource): fix error in one of the code examples
Closes #8948
Closes #9069
2014-09-19 19:36:43 +01:00
Sercan Eraslan 4c5c762378 docs(navigation): side navigation footer overlap problem fix
Closes #8923
2014-09-19 19:26:06 +01:00
Subra d1434c999a feat(ngAria): add an ngAria module to make a11y easier
Conditionally adds various aria attributes to the built in directives.
This module currently hooks into ng-show/hide, input, textarea and
button as a basic level of support for a11y.

Closes #5486 and #1600
2014-09-18 16:17:14 -07:00
Luke Schoen 8b8f6f5124 docs(guide/directive): fix grammar 2014-09-18 16:12:00 -07:00
Matt Kim 25082b3439 docs(misc/faq): fix typo 2014-09-18 16:09:56 -07:00
Greg Fedirko 27b3ea4d32 docs(guide/$location): improve readability 2014-09-18 16:08:27 -07:00
DeK ffc32b4e42 docs(guide/migration): fix typo 2014-09-18 16:02:26 -07:00
Luke Schoen 80b0909927 docs(tutorial): improve readability 2014-09-18 16:00:25 -07:00
Christopher Rains efbb365533 docs(tutorial): fix formatting
- proper case "jQuery" vs "JQuery"
- wrap ng-view in markdown code `ng-view`
2014-09-18 15:55:51 -07:00
Brian 38e0ab9bd8 docs(guide/filter): fix label in example 2014-09-18 15:53:56 -07:00
jeffavis 3c53b28cc2 docs(guide/bootstrap): fix missing ngController in example 2014-09-18 15:51:07 -07:00
Rahul Doshi 0ba864184b docs(guide): add angular-localization module to internationalization section
Closes #9158
2014-09-18 17:59:54 -04:00
Sebastian Müller 4f9dc44f88 refactor(ngMessages): remove unused $scope
Closes #9150
2014-09-18 16:04:47 -04:00
Georgios Kalpakas f3884df0a9 docs(ngController): Fix priority value mentioned in the docs
The `@priority 500` part was missing from the ngDoc comment, thus the docs mentioned a priority of 0
(instead of the correct 500).

Closes #9070
2014-09-18 11:43:17 -04:00
Peter Bacon Darwin f7a4a70c28 chore(npm-shrinkwrap): update to dgeni-packages v0.10.0-rc.6
Closes https://github.com/angular/dgeni-packages/pull/70
2014-09-17 19:11:24 +01:00
Jose Martinez 8428a0adef docs(error/$controller/noscp): fix example
Fix the "correct" example to have the proper syntax for creating the locals
object and provide a more explicit explanation as to how the scope object
should be provided.
2014-09-17 10:56:21 -07:00
62 changed files with 1570 additions and 341 deletions
+53
View File
@@ -1,3 +1,56 @@
<a name="1.3.0-rc.3"></a>
# 1.3.0-rc.3 aggressive-pacification (2014-09-23)
## Bug Fixes
- **ngModel:** support milliseconds in time and datetime
([4b83f6ca](https://github.com/angular/angular.js/commit/4b83f6ca2c15bd65fe2b3894a02c04f9967fbff4),
[#8874](https://github.com/angular/angular.js/issues/8874))
## Features
- **$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
([fd899755](https://github.com/angular/angular.js/commit/fd8997551f9ed4431f5e99d61f637139485076b9),
[#9050](https://github.com/angular/angular.js/issues/9050))
- **input:** support dynamic element validation
([729c238e](https://github.com/angular/angular.js/commit/729c238e19ab27deff01448d79342ea53721bfed),
[#4791](https://github.com/angular/angular.js/issues/4791), [#1404](https://github.com/angular/angular.js/issues/1404))
- **ngAria:** add an ngAria module to make a11y easier
([d1434c99](https://github.com/angular/angular.js/commit/d1434c999a66c6bb915ee1a8b091e497d288d940),
[#5486](https://github.com/angular/angular.js/issues/5486))
## Performance Improvements
- **map:** use Array.prototype.map
([a591e8b8](https://github.com/angular/angular.js/commit/a591e8b8d302efefd67bf0d5c4bad300a5f3aded))
## Breaking Changes
- **$location:** due to [dc3de7fb](https://github.com/angular/angular.js/commit/dc3de7fb7a14c38b5c3dc7decfafb0b51d422dd1),
The $location.html5Mode API has changed to allow enabling html5Mode by
passing an object (as well as still supporting passing a boolean). Symmetrically, the
method now returns an object instead of a boolean value.
To migrate, follow the code example below:
Before:
var mode = $locationProvider.html5Mode();
After:
var mode = $locationProvider.html5Mode().enabled;
Fixes #8934
<a name="1.2.25"></a>
# 1.2.25 hypnotic-gesticulation (2014-09-16)
+9 -1
View File
@@ -153,6 +153,9 @@ module.exports = function(grunt) {
},
ngTouch: {
files: { src: 'src/ngTouch/**/*.js' },
},
ngAria: {
files: {src: 'src/ngAria/**/*.js'},
}
},
@@ -220,6 +223,10 @@ module.exports = function(grunt) {
dest: 'build/angular-cookies.js',
src: util.wrap(files['angularModules']['ngCookies'], 'module')
},
aria: {
dest: 'build/angular-aria.js',
src: util.wrap(files['angularModules']['ngAria'], 'module')
},
"promises-aplus-adapter": {
dest:'tmp/promises-aplus-adapter++.js',
src:['src/ng/q.js','lib/promises-aplus/promises-aplus-test-adapter.js']
@@ -236,7 +243,8 @@ module.exports = function(grunt) {
touch: 'build/angular-touch.js',
resource: 'build/angular-resource.js',
route: 'build/angular-route.js',
sanitize: 'build/angular-sanitize.js'
sanitize: 'build/angular-sanitize.js',
aria: 'build/angular-aria.js'
},
+5 -4
View File
@@ -6,10 +6,11 @@ use good old HTML (or HAML, Jade and friends!) as your template language and let
syntax to express your applications components clearly and succinctly. It automatically
synchronizes data from your UI (view) with your JavaScript objects (model) through 2-way data
binding. To help you structure your application better and make it easy to test, AngularJS teaches
the browser how to do dependency injection and inversion of control. Oh yeah and it also helps with
server-side communication, taming async callbacks with promises and deferreds; and makes client-side
navigation and deeplinking with hashbang urls or HTML5 pushState a piece of cake. The best of all:
it makes development fun!
the browser how to do dependency injection and inversion of control.
Oh yeah and it helps with server-side communication, taming async callbacks with promises and
deferreds. It also makes client-side navigation and deeplinking with hashbang urls or HTML5 pushState a
piece of cake. The best of all: it makes development fun!
* Web site: http://angularjs.org
* Tutorial: http://docs.angularjs.org/tutorial
-3
View File
@@ -26,7 +26,6 @@ This process based on the idea of minimizing user pain
* You can triage older issues as well
* Triage to your heart's content
1. Assign yourself: Pick an issue that is not assigned to anyone and assign it to you
1. Understandable? - verify if the description of the request is clear.
* If not, [close it][] according to the instructions below and go to the last step.
1. Duplicate?
@@ -36,7 +35,6 @@ This process based on the idea of minimizing user pain
* Label `Type: Bug`
* Reproducible? - Steps to reproduce the bug are clear. If they are not, ask for a clarification. If there's no reply after a week, [close it][].
* Reproducible on master? - <http://code.angularjs.org/snapshot/>
1. Non bugs:
* Label `Type: Feature`, `Type: Chore`, or `Type: Perf`
* Belongs in core? Often new features should be implemented as a third-party module rather than an addition to the core.
@@ -59,7 +57,6 @@ This process based on the idea of minimizing user pain
* In rare cases, it's ok to have multiple components.
1. Label `PRs plz!` - These issues are good targets for PRs from the open source community. Apply to issues where the problem and solution are well defined in the comments, and it's not too complex.
1. Label `origin: google` for issues from Google
1. Assign a milestone:
* Backlog - triaged fixes and features, should be the default choice
* Current 1.x.y milestone (e.g. 1.3.0-beta-2) - regressions and urgent bugs only
+9 -3
View File
@@ -108,6 +108,9 @@ var angularFiles = {
'src/ngTouch/directive/ngClick.js',
'src/ngTouch/directive/ngSwipe.js'
],
'ngAria': [
'src/ngAria/aria.js'
]
},
'angularScenario': [
@@ -141,7 +144,8 @@ var angularFiles = {
'test/ngRoute/**/*.js',
'test/ngSanitize/**/*.js',
'test/ngMock/*.js',
'test/ngTouch/**/*.js'
'test/ngTouch/**/*.js',
'test/ngAria/*.js'
],
'karma': [
@@ -175,7 +179,8 @@ var angularFiles = {
'test/ngRoute/**/*.js',
'test/ngResource/*.js',
'test/ngSanitize/**/*.js',
'test/ngTouch/**/*.js'
'test/ngTouch/**/*.js',
'test/ngAria/*.js'
],
'karmaJquery': [
@@ -203,7 +208,8 @@ angularFiles['angularSrcModules'] = [].concat(
angularFiles['angularModules']['ngRoute'],
angularFiles['angularModules']['ngSanitize'],
angularFiles['angularModules']['ngMock'],
angularFiles['angularModules']['ngTouch']
angularFiles['angularModules']['ngTouch'],
angularFiles['angularModules']['ngAria']
);
if (exports) {
+4 -4
View File
@@ -316,10 +316,10 @@ iframe.example {
}
.search-results-group.col-group-api { width:30%; }
.search-results-group.col-group-guide { width:30%; }
.search-results-group.col-group-tutorial { width:25%; }
.search-results-group.col-group-guide,
.search-results-group.col-group-tutorial { width:20%; }
.search-results-group.col-group-misc,
.search-results-group.col-group-error { float:right; clear:both; width:15% }
.search-results-group.col-group-error { width:15%; float: right; }
.search-results-group.col-group-api .search-result {
@@ -391,7 +391,6 @@ iframe.example {
position:fixed;
top:120px;
bottom:0;
padding-bottom:120px;
overflow:auto;
}
@@ -412,6 +411,7 @@ iframe.example {
.main-body-grid .side-navigation {
position:relative;
padding-bottom:120px;
}
.main-body-grid .side-navigation.ng-hide {
+44
View File
@@ -0,0 +1,44 @@
"use strict";
/* jshint browser: true */
/* global importScripts, onmessage: true, postMessage, lunr */
// Load up the lunr library
importScripts('../components/lunr.js-0.4.2/lunr.min.js');
// Create the lunr index - the docs should be an array of object, each object containing
// the path and search terms for a page
var index = lunr(function() {
this.ref('path');
this.field('titleWords', {boost: 50});
this.field('members', { boost: 40});
this.field('keywords', { boost : 20 });
});
// Retrieve the searchData which contains the information about each page to be indexed
var searchData = {};
var searchDataRequest = new XMLHttpRequest();
searchDataRequest.onload = function() {
// Store the pages data to be used in mapping query results back to pages
searchData = JSON.parse(this.responseText);
// Add search terms from each page to the search index
searchData.forEach(function(page) {
index.add(page);
});
postMessage({ e: 'index-ready' });
};
searchDataRequest.open('GET', 'search-data.json');
searchDataRequest.send();
// The worker receives a message everytime the web app wants to query the index
onmessage = function(oEvent) {
var q = oEvent.data.q;
var hits = index.search(q);
var results = [];
// Only return the array of paths to pages
hits.forEach(function(hit) {
results.push(hit.ref);
});
// The results of the query are sent back to the web app via a new message
postMessage({ e: 'query-ready', q: q, d: results });
};
+7 -4
View File
@@ -77,10 +77,13 @@ describe('docs.angularjs.org', 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!');
});
});
});
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!');
});
});
+1
View File
@@ -6,6 +6,7 @@ angular.module('docsApp', [
'DocsController',
'versionsData',
'pagesData',
'navData',
'directives',
'errors',
'examples',
+11 -71
View File
@@ -6,31 +6,10 @@ angular.module('DocsController', [])
function($scope, $rootScope, $location, $window, $cookies, openPlunkr,
NG_PAGES, NG_NAVIGATION, NG_VERSION) {
$scope.openPlunkr = openPlunkr;
$scope.docsVersion = NG_VERSION.isSnapshot ? 'snapshot' : NG_VERSION.version;
$scope.fold = function(url) {
if(url) {
$scope.docs_fold = '/notes/' + url;
if(/\/build/.test($window.location.href)) {
$scope.docs_fold = '/build/docs' + $scope.docs_fold;
}
window.scrollTo(0,0);
}
else {
$scope.docs_fold = null;
}
};
var OFFLINE_COOKIE_NAME = 'ng-offline',
INDEX_PATH = /^(\/|\/index[^\.]*.html)$/;
/**********************************
Publish methods
***********************************/
$scope.navClass = function(navItem) {
return {
active: navItem.href && this.currentPage && this.currentPage.path,
@@ -38,55 +17,28 @@ angular.module('DocsController', [])
};
};
$scope.afterPartialLoaded = function() {
$scope.$on('$includeContentLoaded', function() {
var pagePath = $scope.currentPage ? $scope.currentPage.path : $location.path();
$window._gaq.push(['_trackPageview', pagePath]);
};
});
/** stores a cookie that is used by apache to decide which manifest ot send */
$scope.enableOffline = function() {
//The cookie will be good for one year!
var date = new Date();
date.setTime(date.getTime()+(365*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
var value = angular.version.full;
document.cookie = OFFLINE_COOKIE_NAME + "="+value+expires+"; path=" + $location.path;
//force the page to reload so server can serve new manifest file
window.location.reload(true);
};
/**********************************
Watches
***********************************/
$scope.$on('$includeContentError', function() {
$scope.partialPath = 'Error404.html';
});
$scope.$watch(function docsPathWatch() {return $location.path(); }, function docsPathWatchAction(path) {
var currentPage = $scope.currentPage = NG_PAGES[path];
if ( !currentPage && path.charAt(0)==='/' ) {
// Strip off leading slash
path = path.substr(1);
}
path = path.replace(/^\/?(.+?)(\/index)?\/?$/, '$1');
currentPage = $scope.currentPage = NG_PAGES[path];
if ( !currentPage && path.charAt(path.length-1) === '/' && path.length > 1 ) {
// Strip off trailing slash
path = path.substr(0, path.length-1);
}
currentPage = $scope.currentPage = NG_PAGES[path];
if ( !currentPage && /\/index$/.test(path) ) {
// Strip off index from the end
path = path.substr(0, path.length - 6);
}
$scope.partialPath = 'partials/' + path + '.html';
currentPage = $scope.currentPage = NG_PAGES[path];
if ( currentPage ) {
$scope.currentArea = currentPage && NG_NAVIGATION[currentPage.area];
$scope.currentArea = NG_NAVIGATION[currentPage.area];
var pathParts = currentPage.path.split('/');
var breadcrumb = $scope.breadcrumb = [];
var breadcrumbPath = '';
@@ -107,24 +59,12 @@ angular.module('DocsController', [])
$scope.versionNumber = angular.version.full;
$scope.version = angular.version.full + " " + angular.version.codeName;
$scope.subpage = false;
$scope.offlineEnabled = ($cookies[OFFLINE_COOKIE_NAME] == angular.version.full);
$scope.futurePartialTitle = null;
$scope.loading = 0;
$scope.$cookies = $cookies;
$cookies.platformPreference = $cookies.platformPreference || 'gitUnix';
var INDEX_PATH = /^(\/|\/index[^\.]*.html)$/;
if (!$location.path() || INDEX_PATH.test($location.path())) {
$location.path('/api').replace();
}
// bind escape to hash reset callback
angular.element(window).on('keydown', function(e) {
if (e.keyCode === 27) {
$scope.$apply(function() {
$scope.subpage = false;
});
}
});
}]);
-24
View File
@@ -1,24 +0,0 @@
angular.module('docsApp.navigationService', [])
.factory('navigationService', function($window) {
var service = {
currentPage: null,
currentVersion: null,
changePage: function(newPage) {
},
changeVersion: function(newVersion) {
//TODO =========
// var currentPagePath = '';
// // preserve URL path when switching between doc versions
// if (angular.isObject($rootScope.currentPage) && $rootScope.currentPage.section && $rootScope.currentPage.id) {
// currentPagePath = '/' + $rootScope.currentPage.section + '/' + $rootScope.currentPage.id;
// }
// $window.location = version.url + currentPagePath;
}
};
});
+123 -64
View File
@@ -10,22 +10,35 @@ angular.module('search', [])
$scope.search = function(q) {
var MIN_SEARCH_LENGTH = 2;
if(q.length >= MIN_SEARCH_LENGTH) {
var results = docsSearch(q);
var totalAreas = 0;
for(var i in results) {
++totalAreas;
}
if(totalAreas > 0) {
$scope.colClassName = 'cols-' + totalAreas;
}
$scope.hasResults = totalAreas > 0;
$scope.results = results;
docsSearch(q).then(function(hits) {
var results = {};
angular.forEach(hits, function(hit) {
var area = hit.area;
var limit = (area == 'api') ? 40 : 14;
results[area] = results[area] || [];
if(results[area].length < limit) {
results[area].push(hit);
}
});
var totalAreas = 0;
for(var i in results) {
++totalAreas;
}
if(totalAreas > 0) {
$scope.colClassName = 'cols-' + totalAreas;
}
$scope.hasResults = totalAreas > 0;
$scope.results = results;
});
}
else {
clearResults();
}
if(!$scope.$$phase) $scope.$apply();
};
$scope.submit = function() {
var result;
for(var i in $scope.results) {
@@ -39,78 +52,124 @@ angular.module('search', [])
$scope.hideResults();
}
};
$scope.hideResults = function() {
clearResults();
$scope.q = '';
};
}])
.controller('Error404SearchCtrl', ['$scope', '$location', 'docsSearch', function($scope, $location, docsSearch) {
$scope.results = docsSearch($location.path().split(/[\/\.:]/).pop());
.controller('Error404SearchCtrl', ['$scope', '$location', 'docsSearch',
function($scope, $location, docsSearch) {
docsSearch($location.path().split(/[\/\.:]/).pop()).then(function(results) {
$scope.results = {};
angular.forEach(results, function(result) {
var area = $scope.results[result.area] || [];
area.push(result);
$scope.results[result.area] = area;
});
});
}])
.factory('lunrSearch', function() {
return function(properties) {
if (window.RUNNING_IN_NG_TEST_RUNNER) return null;
var engine = lunr(properties);
return {
store : function(values) {
engine.add(values);
},
search : function(q) {
return engine.search(q);
}
};
};
})
.provider('docsSearch', function() {
.factory('docsSearch', ['$rootScope','lunrSearch', 'NG_PAGES',
function($rootScope, lunrSearch, NG_PAGES) {
if (window.RUNNING_IN_NG_TEST_RUNNER) {
return null;
}
// This version of the service builds the index in the current thread,
// which blocks rendering and other browser activities.
// It should only be used where the browser does not support WebWorkers
function localSearchFactory($http, $timeout, NG_PAGES) {
var index = lunrSearch(function() {
this.ref('id');
this.field('title', {boost: 50});
this.field('members', { boost: 40});
this.field('keywords', { boost : 20 });
});
console.log('Using Local Search Index');
angular.forEach(NG_PAGES, function(page, key) {
if(page.searchTerms) {
index.store({
id : key,
title : page.searchTerms.titleWords,
keywords : page.searchTerms.keywords,
members : page.searchTerms.members
// Create the lunr index
var index = lunr(function() {
this.ref('path');
this.field('titleWords', {boost: 50});
this.field('members', { boost: 40});
this.field('keywords', { boost : 20 });
});
// Delay building the index by loading the data asynchronously
var indexReadyPromise = $http.get('js/search-data.json').then(function(response) {
var searchData = response.data;
// Delay building the index for 500ms to allow the page to render
return $timeout(function() {
// load the page data into the index
angular.forEach(searchData, function(page) {
index.add(page);
});
}, 500);
});
// The actual service is a function that takes a query string and
// returns a promise to the search results
// (In this case we just resolve the promise immediately as it is not
// inherently an async process)
return function(q) {
return indexReadyPromise.then(function() {
var hits = index.search(q);
var results = [];
angular.forEach(hits, function(hit) {
results.push(NG_PAGES[hit.ref]);
});
return results;
});
};
});
}
localSearchFactory.$inject = ['$http', '$timeout', 'NG_PAGES'];
return function(q) {
var results = {
api : [],
tutorial : [],
guide : [],
error : [],
misc : []
// This version of the service builds the index in a WebWorker,
// which does not block rendering and other browser activities.
// It should only be used where the browser does support WebWorkers
function webWorkerSearchFactory($q, $rootScope, NG_PAGES) {
console.log('Using WebWorker Search Index')
var searchIndex = $q.defer();
var results;
var worker = new Worker('js/search-worker.js');
// The worker will send us a message in two situations:
// - when the index has been built, ready to run a query
// - when it has completed a search query and the results are available
worker.onmessage = function(oEvent) {
$rootScope.$apply(function() {
switch(oEvent.data.e) {
case 'index-ready':
searchIndex.resolve();
break;
case 'query-ready':
var pages = oEvent.data.d.map(function(path) {
return NG_PAGES[path];
});
results.resolve(pages);
break;
}
});
};
angular.forEach(index.search(q), function(result) {
var key = result.ref;
var item = NG_PAGES[key];
var area = item.area;
item.path = key;
var limit = area == 'api' ? 40 : 14;
if(results[area].length < limit) {
results[area].push(item);
}
});
return results;
// The actual service is a function that takes a query string and
// returns a promise to the search results
return function(q) {
// We only run the query once the index is ready
return searchIndex.promise.then(function() {
results = $q.defer();
worker.postMessage({ q: q });
return results.promise;
});
};
}
webWorkerSearchFactory.$inject = ['$q', '$rootScope', 'NG_PAGES'];
return {
$get: window.Worker ? webWorkerSearchFactory : localSearchFactory
};
}])
})
.directive('focused', function($timeout) {
return function(scope, element, attrs) {
+2 -2
View File
@@ -19,7 +19,7 @@ describe("DocsController", function() {
it("should update the Google Analytics with currentPage path if currentPage exists", inject(function($window) {
$window._gaq = [];
$scope.currentPage = { path: 'a/b/c' };
$scope.afterPartialLoaded();
$scope.$broadcast('$includeContentLoaded');
expect($window._gaq.pop()).toEqual(['_trackPageview', 'a/b/c']);
}));
@@ -27,7 +27,7 @@ describe("DocsController", function() {
it("should update the Google Analytics with $location.path if currentPage is missing", inject(function($window, $location) {
$window._gaq = [];
spyOn($location, 'path').andReturn('x/y/z');
$scope.afterPartialLoaded();
$scope.$broadcast('$includeContentLoaded');
expect($window._gaq.pop()).toEqual(['_trackPageview', 'x/y/z']);
}));
});
+11 -4
View File
@@ -92,10 +92,7 @@ module.exports = new Package('angularjs', [
}
return docPath;
},
getOutputPath: function(doc) {
return 'partials/' + doc.path +
( doc.fileInfo.baseName === 'index' ? '/index.html' : '.html');
}
outputPathTemplate: 'partials/${path}.html'
});
computePathsProcessor.pathTemplates.push({
@@ -110,6 +107,16 @@ module.exports = new Package('angularjs', [
outputPathTemplate: '${id}.html'
});
computePathsProcessor.pathTemplates.push({
docTypes: ['module' ],
pathTemplate: '${area}/${name}',
outputPathTemplate: 'partials/${area}/${name}.html'
});
computePathsProcessor.pathTemplates.push({
docTypes: ['componentGroup' ],
pathTemplate: '${area}/${moduleName}/${groupType}',
outputPathTemplate: 'partials/${area}/${moduleName}/${groupType}.html'
});
computeIdsProcessor.idTemplates.push({
docTypes: ['overview', 'tutorial', 'e2e-test', 'indexPage'],
+40 -26
View File
@@ -147,24 +147,18 @@ module.exports = function generatePagesDataProcessor(log) {
};
return {
$runAfter: ['paths-computed'],
$runAfter: ['paths-computed', 'generateKeywordsProcessor'],
$runBefore: ['rendering-docs'],
$process: function(docs) {
_(docs)
.filter(function(doc) { return doc.area === 'api' && doc.docType === 'module'; })
.forEach(function(doc) { if ( !doc.path ) {
log.warn('Missing path property for ', doc.id);
}})
.map(function(doc) { return _.pick(doc, ['id', 'module', 'docType', 'area']); })
.tap(function(docs) {
log.debug(docs);
// We are only interested in docs that are in an area
var pages = _.filter(docs, function(doc) {
return doc.area;
});
// We are only interested in docs that are in an area and are not landing pages
var navPages = _.filter(docs, function(page) {
return page.area && page.docType != 'componentGroup';
// We are only interested in pages that are not landing pages
var navPages = _.filter(pages, function(page) {
return page.docType != 'componentGroup';
});
// Generate an object collection of pages that is grouped by area e.g.
@@ -198,28 +192,48 @@ module.exports = function generatePagesDataProcessor(log) {
area.navGroups = navGroupMapper(pages, area);
});
docs.push({
docType: 'nav-data',
id: 'nav-data',
template: 'nav-data.template.js',
outputPath: 'js/nav-data.js',
areas: areas
});
var searchData = _(pages)
.filter(function(page) {
return page.searchTerms;
})
.map(function(page) {
return _.extend({ path: page.path }, page.searchTerms);
})
.value();
docs.push({
docType: 'json-doc',
id: 'search-data-json',
template: 'json-doc.template.json',
outputPath: 'js/search-data.json',
data: searchData
});
// Extract a list of basic page information for mapping paths to partials and for client side searching
var pages = _(docs)
var pageData = _(docs)
.map(function(doc) {
var page = _.pick(doc, [
'docType', 'id', 'name', 'area', 'outputPath', 'path', 'searchTerms'
]);
return page;
return _.pick(doc, ['name', 'area', 'path']);
})
.indexBy('path')
.value();
var docData = {
docs.push({
docType: 'pages-data',
id: 'pages-data',
template: 'pages-data.template.js',
outputPath: 'js/pages-data.js',
areas: areas,
pages: pages
};
docs.push(docData);
pages: pageData
});
}
}
};
};
@@ -26,6 +26,7 @@ module.exports = function debugDeployment(getVersion) {
'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'
],
stylesheets: [
@@ -26,6 +26,7 @@ module.exports = function defaultDeployment(getVersion) {
'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'
],
stylesheets: [
+1
View File
@@ -30,6 +30,7 @@ module.exports = function jqueryDeployment(getVersion) {
'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'
],
stylesheets: [
@@ -29,6 +29,7 @@ module.exports = function productionDeployment(getVersion) {
'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'
],
stylesheets: [
+1 -10
View File
@@ -56,15 +56,6 @@
}
})();
// force page reload when new update is available
window.applicationCache && window.applicationCache.addEventListener('updateready', function(e) {
if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
window.applicationCache.swapCache();
window.location.reload();
}
}, false);
// GA asynchronous tracker
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-8594346-3']);
@@ -219,7 +210,7 @@
</div>
<div class="grid-right">
<div id="loading" ng-show="loading">Loading...</div>
<div ng-hide="loading" ng-include="currentPage.outputPath || 'Error404.html'" onload="afterPartialLoaded()" autoscroll></div>
<div ng-hide="loading" ng-include="partialPath" autoscroll></div>
</div>
</div>
</section>
@@ -0,0 +1 @@
{$ doc.data | json $}
@@ -0,0 +1,3 @@
// Meta data used by the AngularJS docs app
angular.module('navData', [])
.value('NG_NAVIGATION', {$ doc.areas | json $});
+1 -2
View File
@@ -1,4 +1,3 @@
// Meta data used by the AngularJS docs app
angular.module('pagesData', [])
.value('NG_PAGES', {$ doc.pages | json $})
.value('NG_NAVIGATION', {$ doc.areas | json $});
.value('NG_PAGES', {$ doc.pages | json $});
+2 -2
View File
@@ -12,10 +12,10 @@ $controller(MyController);
$controller(MyController, {scope: newScope});
```
To fix the example above please provide a scope to the $controller call:
To fix the example above please provide a scope (using the `$scope` property in the locals object) to the $controller call:
```
$controller(MyController, {$scope, newScope});
$controller(MyController, {$scope: newScope});
```
Please consult the {@link ng.$controller $controller} service api docs to learn more.
+13 -1
View File
@@ -4,7 +4,19 @@
@description
If you configure {@link ng.$location `$location`} to use
{@link api/ng.provider.$locationProvider `html5Mode`} (`history.pushState`), you need to specify the base URL for the application with a [`<base href="">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) tag.
{@link api/ng.provider.$locationProvider `html5Mode`} (`history.pushState`), you need to specify the base URL for the application with a [`<base href="">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) tag or configure
`$locationProvider` to not require a base tag by passing a definition object with
`requireBase:false` to `$locationProvider.html5Mode()`:
```javascript
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
```
Note that removing the requirement for a <base> tag will have adverse side effects when resolving
relative paths with `$location` in IE9.
The base URL is then used to resolve all relative URLs throughout the application regardless of the
entry point into the app.
+12 -9
View File
@@ -91,10 +91,11 @@ To configure the `$location` service, retrieve the
{@link ng.$locationProvider $locationProvider} and set the parameters as follows:
- **html5Mode(mode)**: {boolean}<br />
`true` - see HTML5 mode<br />
`false` - see Hashbang mode<br />
default: `false`
- **html5Mode(mode)**: {boolean|Object}<br />
`true` or `enabled:true` - see HTML5 mode<br />
`false` or `enabled:false` - see Hashbang mode<br />
`requireBase:true` - see Relative links<br />
default: `enabled:false`
- **hashPrefix(prefix)**: {string}<br />
prefix used for Hashbang URLs (used in Hashbang mode or in legacy browser in Html5 mode)<br />
@@ -164,7 +165,7 @@ encoded.
`$location` service has two configuration modes which control the format of the URL in the browser
address bar: **Hashbang mode** (the default) and the **HTML5 mode** which is based on using the
HTML5 [History API](http://www.w3.org/TR/html5/history.html). Applications use the same API in
HTML5 [History API](http://www.w3.org/TR/html5/introduction.html#history-0). Applications use the same API in
both modes and the `$location` service will work with appropriate URL segments and browser APIs to
facilitate the browser URL change and history management.
@@ -245,7 +246,7 @@ it('should show example', inject(
## HTML5 mode
In HTML5 mode, the `$location` service getters and setters interact with the browser URL address
through the HTML5 history API, which allows for use of regular URL path and search segments,
through the HTML5 history API. This allows for use of regular URL path and search segments,
instead of their hashbang equivalents. If the HTML5 History API is not supported by a browser, the
`$location` service will fall back to using the hashbang URLs automatically. This frees you from
having to worry about whether the browser displaying your app supports the history API or not; the
@@ -328,9 +329,11 @@ reload to the original link.
### Relative links
Be sure to check all relative links, images, scripts etc. Angular requires you to specify the url base in
the head of your main html file (`<base href="/my-base">`). With that, relative urls will
always be resolved to this base url, event if the initial url of the document was different.
Be sure to check all relative links, images, scripts etc. Angular requires you to specify the url
base in the head of your main html file (`<base href="/my-base">`) unless `html5Mode.requireBase` is
set to `false` in the html5Mode definition object passed to `$locationProvider.html5Mode()`. With
that, relative urls will always be resolved to this base url, event if the initial url of the
document was different.
There is one exception: Links that only contain a hash fragment (e.g. `<a href="#target">`)
will only change `$location.hash()` and not modify the url otherwise. This is useful for scrolling
+3 -1
View File
@@ -87,7 +87,9 @@ Here is an example of manually initializing Angular:
<!doctype html>
<html>
<body>
Hello {{greetMe}}!
<div ng-controller="MyController">
Hello {{greetMe}}!
</div>
<script src="http://code.angularjs.org/snapshot/angular.js"></script>
<script>
+1 -4
View File
@@ -160,7 +160,7 @@ restrictions, you cannot simply write `cx="{{cx}}"`.
With `ng-attr-cx` you can work around this problem.
If an attribute with a binding is prefixed with the `ngAttr` prefix (denormalized as `ng-attr-`)
then during the binding will be applied to the corresponding unprefixed attribute. This allows
then during the binding it will be applied to the corresponding unprefixed attribute. This allows
you to bind to attributes that would otherwise be eagerly processed by browsers
(e.g. an SVG element's `circle[cx]` attributes). When using `ngAttr`, the `allOrNothing` flag of
{@link ng.$interpolate $interpolate} is used, so if any expression in the interpolated string
@@ -282,9 +282,6 @@ using `templateUrl` instead:
</file>
</example>
Great! But what if we wanted to have our directive match the tag name `<my-customer>` instead?
If we simply put a `<my-customer>` element into the HTML, it doesn't work.
<div class="alert alert-warning">
**Note:** When you create a directive, it is restricted to attribute and elements only by default. In order to
create directives that are triggered by class name, you need to use the `restrict` option.
+1 -1
View File
@@ -143,7 +143,7 @@ means that it will be executed one or more times during the each `$digest` cycle
Input: <input ng-model="greeting" type="text"><br>
Decoration: <input ng-model="decoration.symbol" type="text"><br>
No filter: {{greeting}}<br>
Reverse: {{greeting | decorate}}<br>
Decorated: {{greeting | decorate}}<br>
</div>
</file>
+1 -1
View File
@@ -70,7 +70,7 @@ In Angular applications, you move the job of filling page templates with data fr
This is a short list of libraries with specific support and documentation for working with Angular. You can find a full list of all known Angular external libraries at [ngmodules.org](http://ngmodules.org/).
* **Internationalization:** [angular-translate](http://angular-translate.github.io), [angular-gettext](http://angular-gettext.rocketeer.be/)
* **Internationalization:** [angular-translate](http://angular-translate.github.io), [angular-gettext](http://angular-gettext.rocketeer.be/), [angular-localization](http://doshprompt.github.io/angular-localization/)
* **RESTful services:** [Restangular](https://github.com/mgonto/restangular)
* **SQL and NoSQL backends:** [BreezeJS](http://www.breezejs.com/), [AngularFire](http://angularfire.com/)
* **UI Widgets: **[KendoUI](http://kendo-labs.github.io/angular-kendo/#/), [UI Bootstrap](http://angular-ui.github.io/bootstrap/), [Wijmo](http://wijmo.com/tag/angularjs-2/), [ngTagsInput](https://github.com/mbenford/ngTagsInput)
+1 -1
View File
@@ -96,7 +96,7 @@ this limitation, use a regular expression object as the value for the expression
//after
$scope.exp = /abc/i;
- **Scope:** due to [8c6a8171](https://github.com/angular/angular.js/commit/8c6a8171f9bdaa5cdabc0cc3f7d3ce10af7b434d),
Scope#$id is now of time number rather than string. Since the
Scope#$id is now of type number rather than string. Since the
id is primarily being used for debugging purposes this change should not affect
anyone.
- **forEach:** due to [55991e33](https://github.com/angular/angular.js/commit/55991e33af6fece07ea347a059da061b76fc95f5),
+10
View File
@@ -338,6 +338,16 @@ We inject the $compile service and $rootScope before each jasmine test. The $com
to render the aGreatEye directive. After rendering the directive we ensure that the directive has
replaced the content and "lidless, wreathed in flame, 2 times" is present.
<div class="alert alert-info">
**Underscore notation**:
The use of the underscore notation (e.g.: `_$rootScope_`) is a convention wide spread in AngularJS
community to keep the variable names clean in your tests. That's why the
{@link $injector} strips out the leading and the trailing underscores when
matching the parameters. The underscore rule applies ***only*** if the name starts **and** ends with
exactly one underscore, otherwise no replacing happens.
</div>
### Testing Transclusion Directives
Directives that use transclusion are treated specially by the compiler. Before their compile
+1 -1
View File
@@ -205,7 +205,7 @@ If you want to apply a directive to each inner piece of the repeat, put it on a
### `$rootScope` exists, but it can be used for evil
Scopes in Angular form a hierarchy, prototypically inheriting from a root scope at the top of the tree.
Scopes in Angular form a hierarchy, prototypally inheriting from a root scope at the top of the tree.
Usually this can be ignored, since most views have a controller, and therefore a scope, of their own.
Occasionally there are pieces of data that you want to make global to the whole app.
+2 -2
View File
@@ -233,7 +233,7 @@ browser is limited, which results in your karma tests running extremely slow.
Refresh your browser and verify that it says "Hello, World!".
* Update the unit test for the controller in ./test/unit/controllersSpec.js to reflect the previous change. For example by adding:
* Update the unit test for the controller in `./test/unit/controllersSpec.js` to reflect the previous change. For example by adding:
expect(scope.name).toBe('World');
@@ -251,7 +251,7 @@ browser is limited, which results in your karma tests running extremely slow.
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
</table>
Extra points: try and make an 8x8 table using an additional ng-repeat.
Extra points: try and make an 8x8 table using an additional `ng-repeat`.
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use `toBe(4)`.
+2 -2
View File
@@ -95,7 +95,7 @@ describe('PhoneCat App', function() {
});
it('should filter the phone list as user types into the search box', function() {
it('should filter the phone list as a user types into the search box', function() {
var phoneList = element.all(by.repeater('phone in phones'));
var query = element(by.model('query'));
@@ -159,7 +159,7 @@ Let's see how we can get the current value of the `query` model to appear in the
var phoneList = element.all(by.repeater('phone in phones'));
var query = element(by.model('query'));
it('should filter the phone list as user types into the search box', function() {
it('should filter the phone list as a user types into the search box', function() {
expect(phoneList.count()).toBe(3);
query.sendKeys('nexus');
+1 -1
View File
@@ -11,7 +11,7 @@ from our server using one of Angular's built-in {@link guide/services services}
ng.$http $http}. We will use Angular's {@link guide/di dependency
injection (DI)} to provide the service to the `PhoneListCtrl` controller.
* There are now a list of 20 phones, loaded from the server.
* There is now a list of 20 phones, loaded from the server.
<div doc-tutorial-reset="5"></div>
+7 -7
View File
@@ -21,7 +21,7 @@ animations on top of the template code we created before.
## Dependencies
The animation functionality is provided by Angular in the `ngAnimate` module, which is distributed
separately from the core Angular framework. In addition we will use `JQuery` in this project to do
separately from the core Angular framework. In addition we will use `jQuery` in this project to do
extra JavaScript animations.
We are using [Bower][bower] to install client side dependencies. This step updates the
@@ -49,8 +49,8 @@ We are using [Bower][bower] to install client side dependencies. This step upda
* `"angular-animate": "~1.2.x"` tells bower to install a version of the
angular-animate component that is compatible with version 1.2.x.
* `"jquery": "1.10.2"` tells bower to install the 1.10.2 version of JQuery. Note that this is not an
Angular library, it is the standard JQuery library. We can use bower to install a wide range of 3rd
* `"jquery": "1.10.2"` tells bower to install the 1.10.2 version of jQuery. Note that this is not an
Angular library, it is the standard jQuery library. We can use bower to install a wide range of 3rd
party libraries.
We must ask bower to download and install this dependency. We can do this by running:
@@ -255,7 +255,7 @@ which are described in detail below.
Next let's add an animation for transitions between route changes in {@link api/ngRoute.directive:ngView `ngView`}.
To start, let's add a new CSS class to our HTML like we did in the example above.
This time, instead of the `ng-repeat` element, let's add it to the element containing the ng-view directive.
This time, instead of the `ng-repeat` element, let's add it to the element containing the `ng-view` directive.
In order to do this, we'll have to make some small changes to the HTML code so that we can have more control over our
animations between view changes.
@@ -340,13 +340,13 @@ a cross fade animation in between. So as the previous page is just about to be r
while the new page fades in right on top of it.
Once the leave animation is over then element is removed and once the enter animation is complete
then the `ng-enter` and `ng-enter-active` CSS classes are removed from the element rendering it to
be position itself with its default CSS code (so no more absolute positioning once the animation is
then the `ng-enter` and `ng-enter-active` CSS classes are removed from the element, causing it to rerender and
reposition itself with its default CSS code (so no more absolute positioning once the animation is
over). This works fluidly so that pages flow naturally between route changes without anything
jumping around.
The CSS classes applied (the start and end classes) are much the same as with `ng-repeat`. Each time
a new page is loaded the ng-view directive will create a copy of itself, download the template and
a new page is loaded the `ng-view` directive will create a copy of itself, download the template and
append the contents. This ensures that all views are contained within a single HTML element which
allows for easy animation control.
+7 -2
View File
@@ -17,6 +17,8 @@ var path = require('canonical-path');
var outputFolder = '../build/docs';
var bowerFolder = 'bower_components';
var src = 'app/src/**/*.js';
var assets = 'app/assets/**/*';
var copyComponent = function(component, pattern, sourceFolder, packageFile) {
pattern = pattern || '/**/*';
@@ -40,14 +42,14 @@ gulp.task('bower', function() {
});
gulp.task('build-app', function() {
gulp.src('app/src/**/*.js')
gulp.src(src)
.pipe(concat('docs.js'))
.pipe(gulp.dest(outputFolder + '/js/'));
});
gulp.task('assets', ['bower'], function() {
return merge(
gulp.src(['app/assets/**/*']).pipe(gulp.dest(outputFolder)),
gulp.src([assets]).pipe(gulp.dest(outputFolder)),
copyComponent('bootstrap', '/dist/**/*'),
copyComponent('open-sans-fontface'),
copyComponent('lunr.js','/*.js'),
@@ -77,3 +79,6 @@ gulp.task('jshint', ['doc-gen'], function() {
// The default task that will be run if no task is supplied
gulp.task('default', ['assets', 'doc-gen', 'build-app', 'jshint']);
gulp.task('watch', function() {
gulp.watch([src, assets], ['assets', 'build-app']);
});
+1 -1
View File
@@ -1142,7 +1142,7 @@
}
},
"dgeni-packages": {
"version": "0.10.0-rc.5",
"version": "0.10.0-rc.6",
"dependencies": {
"catharsis": {
"version": "0.7.1"
+1
View File
@@ -17,6 +17,7 @@ function init {
REPOS=(
angular
angular-animate
angular-aria
angular-cookies
angular-i18n
angular-loader
-1
View File
@@ -55,7 +55,6 @@
"trim": false,
"isElement": false,
"makeMap": false,
"map": false,
"size": false,
"includes": false,
"arrayRemove": false,
+1 -11
View File
@@ -50,7 +50,6 @@
trim: true,
isElement: true,
makeMap: true,
map: true,
size: true,
includes: true,
arrayRemove: true,
@@ -327,7 +326,7 @@ function setHashKey(obj, h) {
* @kind function
*
* @description
* Extends the destination object `dst` by copying all of the properties from the `src` object(s)
* Extends the destination object `dst` by copying own enumerable properties from the `src` object(s)
* to `dst`. You can specify multiple `src` objects.
*
* @param {Object} dst Destination object.
@@ -617,15 +616,6 @@ function nodeName_(element) {
}
function map(obj, iterator, context) {
var results = [];
forEach(obj, function(value, index, list) {
results.push(iterator.call(context, value, index, list));
});
return results;
}
/**
* @description
* Determines the number of elements in an array, the number of properties an object has, or
+50 -7
View File
@@ -4,6 +4,7 @@
*/
var nullFormCtrl = {
$addControl: noop,
$$renameControl: nullFormRenameControl,
$removeControl: noop,
$setValidity: noop,
$$setPending: noop,
@@ -14,6 +15,10 @@ var nullFormCtrl = {
},
SUBMITTED_CLASS = 'ng-submitted';
function nullFormRenameControl(control, name) {
control.$name = name;
}
/**
* @ngdoc type
* @name form.FormController
@@ -51,17 +56,18 @@ SUBMITTED_CLASS = 'ng-submitted';
*
*/
//asks for $scope to fool the BC controller module
FormController.$inject = ['$element', '$attrs', '$scope', '$animate'];
function FormController(element, attrs, $scope, $animate) {
FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
function FormController(element, attrs, $scope, $animate, $interpolate) {
var form = this,
parentForm = element.parent().controller('form') || nullFormCtrl,
controls = [];
var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl;
// init state
form.$error = {};
form.$$success = {};
form.$pending = undefined;
form.$name = attrs.name || attrs.ngForm;
form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope);
form.$dirty = false;
form.$pristine = true;
form.$valid = true;
@@ -127,6 +133,17 @@ function FormController(element, attrs, $scope, $animate) {
}
};
// Private API: rename a form control
form.$$renameControl = function(control, newName) {
var oldName = control.$name;
if (form[oldName] === control) {
delete form[oldName];
}
form[newName] = control;
control.$name = newName;
};
/**
* @ngdoc method
* @name form.FormController#$removeControl
@@ -230,6 +247,25 @@ function FormController(element, attrs, $scope, $animate) {
});
};
/**
* @ngdoc method
* @name form.FormController#$setUntouched
*
* @description
* Sets the form to its untouched state.
*
* This method can be called to remove the 'ng-touched' class and set the form controls to their
* untouched state (ng-untouched class).
*
* Setting a form controls back to their untouched state is often useful when setting the form
* back to its pristine state.
*/
form.$setUntouched = function () {
forEach(controls, function(control) {
control.$setUntouched();
});
};
/**
* @ngdoc method
* @name form.FormController#$setSubmitted
@@ -447,13 +483,20 @@ var formDirectiveFactory = function(isNgForm) {
});
}
var parentFormCtrl = formElement.parent().controller('form'),
alias = attr.name || attr.ngForm;
var parentFormCtrl = controller.$$parentForm,
alias = controller.$name;
if (alias) {
setter(scope, alias, controller, alias);
attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) {
if (alias === newValue) return;
setter(scope, alias, undefined, alias);
alias = newValue;
setter(scope, alias, controller, alias);
parentFormCtrl.$$renameControl(controller, alias);
});
}
if (parentFormCtrl) {
if (parentFormCtrl !== nullFormCtrl) {
formElement.on('$destroy', function() {
parentFormCtrl.$removeControl(controller);
if (alias) {
+17 -11
View File
@@ -14,10 +14,10 @@ var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d))?$/;
var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d))?$/;
var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
var $ngModelMinErr = new minErr('ngModel');
@@ -281,8 +281,8 @@ var inputType = {
</example>
*/
'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP,
createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss']),
'yyyy-MM-ddTHH:mm:ss'),
createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']),
'yyyy-MM-ddTHH:mm:ss.sss'),
/**
* @ngdoc input
@@ -370,8 +370,8 @@ var inputType = {
</example>
*/
'time': createDateInputType('time', TIME_REGEXP,
createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss']),
'HH:mm:ss'),
createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']),
'HH:mm:ss.sss'),
/**
* @ngdoc input
@@ -1067,7 +1067,7 @@ function createDateParser(regexp, mapping) {
HH: date.getHours(),
mm: date.getMinutes(),
ss: date.getSeconds(),
sss: date.getMilliseconds()
sss: date.getMilliseconds() / 1000
};
} else {
map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 };
@@ -1078,7 +1078,7 @@ function createDateParser(regexp, mapping) {
map[mapping[index]] = +part;
}
});
return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss || 0);
return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0);
}
}
@@ -1657,8 +1657,8 @@ var VALID_CLASS = 'ng-valid',
*
*
*/
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q',
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q) {
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$validators = {};
@@ -1675,7 +1675,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
this.$error = {}; // keep invalid keys here
this.$$success = {}; // keep valid keys here
this.$pending = undefined; // keep pending keys here
this.$name = $attr.name;
this.$name = $interpolate($attr.name || '', false)($scope);
var parsedNgModel = $parse($attr.ngModel),
@@ -2387,6 +2387,12 @@ var ngModelDirective = function() {
// 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);
});
+1
View File
@@ -23,6 +23,7 @@
*
* @element ANY
* @scope
* @priority 500
* @param {expression} ngController Name of a constructor function registered with the current
* {@link ng.$controllerProvider $controllerProvider} or an {@link guide/expression expression}
* that on the current scope evaluates to a constructor function.
+15 -14
View File
@@ -34,9 +34,9 @@
}]);
</script>
<div ng-controller="ExampleController">
Limit {{numbers}} to: <input type="integer" ng-model="numLimit">
Limit {{numbers}} to: <input type="number" step="1" ng-model="numLimit">
<p>Output numbers: {{ numbers | limitTo:numLimit }}</p>
Limit {{letters}} to: <input type="integer" ng-model="letterLimit">
Limit {{letters}} to: <input type="number" step="1" ng-model="letterLimit">
<p>Output letters: {{ letters | limitTo:letterLimit }}</p>
Limit {{longNumber}} to: <input type="integer" ng-model="longNumberLimit">
<p>Output long number: {{ longNumber | limitTo:longNumberLimit }}</p>
@@ -59,17 +59,18 @@
expect(limitedLongNumber.getText()).toEqual('Output long number: 234');
});
it('should update the output when -3 is entered', function() {
numLimitInput.clear();
numLimitInput.sendKeys('-3');
letterLimitInput.clear();
letterLimitInput.sendKeys('-3');
longNumberLimitInput.clear();
longNumberLimitInput.sendKeys('-3');
expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]');
expect(limitedLetters.getText()).toEqual('Output letters: ghi');
expect(limitedLongNumber.getText()).toEqual('Output long number: 342');
});
// There is a bug in safari and protractor that doesn't like the minus key
// it('should update the output when -3 is entered', function() {
// numLimitInput.clear();
// numLimitInput.sendKeys('-3');
// letterLimitInput.clear();
// letterLimitInput.sendKeys('-3');
// longNumberLimitInput.clear();
// longNumberLimitInput.sendKeys('-3');
// expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]');
// expect(limitedLetters.getText()).toEqual('Output letters: ghi');
// expect(limitedLongNumber.getText()).toEqual('Output long number: 342');
// });
it('should not exceed the maximum size of input array', function() {
numLimitInput.clear();
@@ -84,7 +85,7 @@
});
</file>
</example>
*/
*/
function limitToFilter(){
return function(input, limit) {
if (isNumber(input)) input = input.toString();
+1 -1
View File
@@ -118,7 +118,7 @@ function orderByFilter($parse){
if (!(isArrayLike(array))) return array;
if (!sortPredicate) return array;
sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate];
sortPredicate = map(sortPredicate, function(predicate){
sortPredicate = sortPredicate.map(function(predicate){
var descending = false, get = predicate || identity;
if (isString(predicate)) {
if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
+7 -6
View File
@@ -662,12 +662,13 @@ function $HttpProvider() {
expect(data.getText()).toMatch(/Hello, \$http!/);
});
it('should make a JSONP request to angularjs.org', function() {
sampleJsonpBtn.click();
fetchBtn.click();
expect(status.getText()).toMatch('200');
expect(data.getText()).toMatch(/Super Hero!/);
});
// Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185
// it('should make a JSONP request to angularjs.org', function() {
// sampleJsonpBtn.click();
// fetchBtn.click();
// expect(status.getText()).toMatch('200');
// expect(data.getText()).toMatch(/Super Hero!/);
// });
it('should make JSONP request to invalid URL and invoke the error handler',
function() {
+28 -7
View File
@@ -584,7 +584,10 @@ function locationGetterSetter(property, preprocess) {
*/
function $LocationProvider(){
var hashPrefix = '',
html5Mode = false;
html5Mode = {
enabled: false,
requireBase: true
};
/**
* @ngdoc method
@@ -606,12 +609,30 @@ function $LocationProvider(){
* @ngdoc method
* @name $locationProvider#html5Mode
* @description
* @param {boolean=} mode Use HTML5 strategy if available.
* @returns {*} current value if used as getter or itself (chaining) if used as setter
* @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value.
* If object, sets `enabled` and `requireBase` to respective values.
* - **enabled** `{boolean}` Sets `html5Mode.enabled`. If true, will rely on
* `history.pushState` to change urls where supported. Will fall back to hash-prefixed paths
* in browsers that do not support `pushState`.
* - **requireBase** - `{boolean}` - Sets `html5Mode.requireBase` (default: `true`). When
* html5Mode is enabled, specifies whether or not a <base> tag is required to be present. If
* `enabled` and `requireBase` are true, and a base tag is not present, an error will be
* thrown when `$location` is injected. See the
* {@link guide/$location $location guide for more information}
*
* @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
*/
this.html5Mode = function(mode) {
if (isDefined(mode)) {
html5Mode = mode;
if (isBoolean(mode)) {
html5Mode.enabled = mode;
return this;
} else if (isObject(mode)) {
html5Mode.enabled = isBoolean(mode.enabled) ?
mode.enabled :
html5Mode.enabled;
html5Mode.requireBase = isBoolean(mode.requireBase) ?
mode.requireBase :
html5Mode.requireBase;
return this;
} else {
return html5Mode;
@@ -653,8 +674,8 @@ function $LocationProvider(){
initialUrl = $browser.url(),
appBase;
if (html5Mode) {
if (!baseHref) {
if (html5Mode.enabled) {
if (!baseHref && html5Mode.requireBase) {
throw $locationMinErr('nobase',
"$location in HTML5 mode requires a <base> tag to be present!");
}
+242
View File
@@ -0,0 +1,242 @@
'use strict';
/**
* @ngdoc module
* @name ngAria
* @description
*
* The `ngAria` module provides support for adding aria tags that convey state or semantic information
* about the application in order to allow assistive technologies to convey appropriate information to
* persons with disabilities.
*
* <div doc-module-components="ngAria"></div>
*
* # Usage
* To enable the addition of the aria tags, just require the module into your application and the tags will
* hook into your ng-show/ng-hide, input, textarea, button, select and ng-required directives and adds the
* appropriate aria-tags.
*
* Currently, the following aria tags are implemented:
*
* + aria-hidden
* + aria-checked
* + aria-disabled
* + aria-required
* + aria-invalid
* + aria-multiline
* + aria-valuenow
* + aria-valuemin
* + aria-valuemax
* + tabindex
*
* You can disable individual aria tags by using the {@link ngAria.$ariaProvider#config config} method.
*/
/* global -ngAriaModule */
var ngAriaModule = angular.module('ngAria', ['ng']).
provider('$aria', $AriaProvider);
/**
* @ngdoc provider
* @name $ariaProvider
*
* @description
*
* Used for configuring aria attributes.
*
* ## Dependencies
* Requires the {@link ngAria} module to be installed.
*/
function $AriaProvider() {
var config = {
ariaHidden : true,
ariaChecked: true,
ariaDisabled: true,
ariaRequired: true,
ariaInvalid: true,
ariaMultiline: true,
ariaValue: true,
tabindex: true
};
/**
* @ngdoc method
* @name $ariaProvider#config
*
* @param {object} config object to enable/disable specific aria tags
*
* - **ariaHidden** `{boolean}` Enables/disables aria-hidden tags
* - **ariaChecked** `{boolean}` Enables/disables aria-checked tags
* - **ariaDisabled** `{boolean}` Enables/disables aria-disabled tags
* - **ariaRequired** `{boolean}` Enables/disables aria-required tags
* - **ariaInvalid** `{boolean}` Enables/disables aria-invalid tags
* - **ariaMultiline** `{boolean}` Enables/disables aria-multiline tags
* - **ariaValue** `{boolean}` Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags
* - **tabindex** `{boolean}` Enables/disables tabindex tags
*
* @description
* Enables/disables various aria tags
*/
this.config = function(newConfig) {
config = angular.extend(config, newConfig);
};
function camelCase(input) {
return input.replace(/-./g, function(letter, pos) {
return letter[1].toUpperCase();
});
}
function watchExpr(attrName, ariaAttr, negate) {
var ariaCamelName = camelCase(ariaAttr);
return function(scope, elem, attr) {
if (config[ariaCamelName] && !attr[ariaCamelName]) {
scope.$watch(attr[attrName], function(boolVal) {
if (negate) {
boolVal = !boolVal;
}
elem.attr(ariaAttr, boolVal);
});
}
};
}
/**
* @ngdoc service
* @name $aria
*
* @description
*
* Contains helper methods for applying aria tags to HTML
*
* ## Dependencies
* Requires the {@link ngAria} module to be installed.
*/
this.$get = function() {
return {
config: function (key) {
return config[camelCase(key)];
},
$$watchExpr: watchExpr
};
};
}
var ngAriaTabindex = ['$aria', function($aria) {
return function(scope, elem, attr) {
if ($aria.config('tabindex') && !elem.attr('tabindex')) {
elem.attr('tabindex', 0);
}
};
}];
ngAriaModule.directive('ngShow', ['$aria', function($aria) {
return $aria.$$watchExpr('ngShow', 'aria-hidden', true);
}])
.directive('ngHide', ['$aria', function($aria) {
return $aria.$$watchExpr('ngHide', 'aria-hidden', false);
}])
.directive('ngModel', ['$aria', function($aria) {
function shouldAttachAttr (attr, elem) {
return $aria.config(attr) && !elem.attr(attr);
}
function getShape (attr, elem) {
var type = attr.type,
role = attr.role;
return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' :
((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' :
(type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' :
(type || role) === 'textbox' || elem[0].nodeName === 'TEXTAREA' ? 'multiline' : '';
}
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elem, attr, ngModel) {
var shape = getShape(attr, elem);
var needsTabIndex = shouldAttachAttr('tabindex', elem);
function ngAriaWatchModelValue() {
return ngModel.$modelValue;
}
function getRadioReaction() {
if (needsTabIndex) {
needsTabIndex = false;
return function ngAriaRadioReaction(newVal) {
var boolVal = newVal === attr.value;
elem.attr('aria-checked', boolVal);
elem.attr('tabindex', 0 - !boolVal);
};
} else {
return function ngAriaRadioReaction(newVal) {
elem.attr('aria-checked', newVal === attr.value);
};
}
}
function ngAriaCheckboxReaction(newVal) {
elem.attr('aria-checked', !!newVal);
}
switch (shape) {
case 'radio':
case 'checkbox':
if (shouldAttachAttr('aria-checked', elem)) {
scope.$watch(ngAriaWatchModelValue, shape === 'radio' ?
getRadioReaction() : ngAriaCheckboxReaction);
}
break;
case 'range':
if ($aria.config('ariaValue')) {
if (attr.min && !elem.attr('aria-valuemin')) {
elem.attr('aria-valuemin', attr.min);
}
if (attr.max && !elem.attr('aria-valuemax')) {
elem.attr('aria-valuemax', attr.max);
}
if (!elem.attr('aria-valuenow')) {
scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) {
elem.attr('aria-valuenow', newVal);
});
}
}
break;
case 'multiline':
if (shouldAttachAttr('aria-multiline', elem)) {
elem.attr('aria-multiline', true);
}
break;
}
if (needsTabIndex) {
elem.attr('tabindex', 0);
}
if (ngModel.$validators.required && shouldAttachAttr('aria-required', elem)) {
scope.$watch(function ngAriaRequiredWatch() {
return ngModel.$error.required;
}, function ngAriaRequiredReaction(newVal) {
elem.attr('aria-required', !!newVal);
});
}
if (shouldAttachAttr('aria-invalid', elem)) {
scope.$watch(function ngAriaInvalidWatch() {
return ngModel.$invalid;
}, function ngAriaInvalidReaction(newVal) {
elem.attr('aria-invalid', !!newVal);
});
}
}
};
}])
.directive('ngDisabled', ['$aria', function($aria) {
return $aria.$$watchExpr('ngDisabled', 'aria-disabled');
}])
.directive('ngClick', ngAriaTabindex)
.directive('ngDblclick', ngAriaTabindex);
+2 -2
View File
@@ -235,7 +235,7 @@ angular.module('ngMessages', [])
return {
restrict: 'AE',
controller: ['$scope', function($scope) {
controller: function() {
this.$renderNgMessageClasses = angular.noop;
var messages = [];
@@ -276,7 +276,7 @@ angular.module('ngMessages', [])
return value !== null && value !== false && value;
}
};
}],
},
require: 'ngMessages',
link: function($scope, element, $attrs, ctrl) {
ctrl.renderElementClasses = function(bool) {
+1 -1
View File
@@ -187,7 +187,7 @@ function shallowClearAndCopy(src, dst) {
* read, update, delete) on server-side data like this:
* ```js
* var User = $resource('/user/:userId', {userId:'@id'});
* var user = User.get({id:123}, function() {
* var user = User.get({userId:123}, function() {
* user.abc = true;
* user.$save();
* });
-3
View File
@@ -268,9 +268,6 @@ function $RouteProvider(){
* This example shows how changing the URL hash causes the `$route` to match a route against the
* URL, and the `ngView` pulls in the partial.
*
* Note that this example is using {@link ng.directive:script inlined templates}
* to get it working on jsfiddle as well.
*
* <example name="$route-service" module="ngRouteExample"
* deps="angular-route.js" fixBase="true">
* <file name="index.html">
+4
View File
@@ -182,6 +182,10 @@ describe('injector', function() {
expect(annotate(beforeEachFn)).toEqual(['foo']);
});
it('should not strip service names with a single underscore', function() {
function beforeEachFn(_) { /* _ = _ */ }
expect(annotate(beforeEachFn)).toEqual(['_']);
});
it('should handle no arg functions', function() {
function $f_n0() {}
+1 -1
View File
@@ -335,7 +335,7 @@ var karmaDump = window.dump || function() {
};
window.dump = function () {
karmaDump.apply(undefined, map(arguments, function(arg) {
karmaDump.apply(undefined, Array.prototype.map.call(arguments, function(arg) {
return angular.mock.dump(arg);
}));
};
+83
View File
@@ -750,6 +750,89 @@ describe('form', function() {
});
});
describe('$setUntouched', function () {
it('should trigger setUntouched on form controls', function() {
var form = $compile(
'<form name="myForm">' +
'<input name="alias" type="text" ng-model="name" />' +
'</form>')(scope);
scope.$digest();
scope.myForm.alias.$setTouched();
expect(scope.myForm.alias.$touched).toBe(true);
scope.myForm.$setUntouched();
expect(scope.myForm.alias.$touched).toBe(false);
dealoc(form);
});
it('should trigger setUntouched on form controls with nested forms', function() {
var form = $compile(
'<form name="myForm">' +
'<div class="ng-form" name="childForm">' +
'<input name="alias" type="text" ng-model="name" />' +
'</div>' +
'</form>')(scope);
scope.$digest();
scope.myForm.childForm.alias.$setTouched();
expect(scope.myForm.childForm.alias.$touched).toBe(true);
scope.myForm.$setUntouched();
expect(scope.myForm.childForm.alias.$touched).toBe(false);
dealoc(form);
});
});
it('should rename nested form controls when interpolated name changes', function() {
scope.idA = 'A';
scope.idB = 'X';
doc = $compile(
'<form name="form">' +
'<div ng-form="nested{{idA}}">' +
'<div ng-form name="nested{{idB}}"' +
'</div>' +
'</div>' +
'</form'
)(scope);
scope.$digest();
var formA = scope.form.nestedA;
expect(formA).toBeDefined();
expect(formA.$name).toBe('nestedA');
var formX = formA.nestedX;
expect(formX).toBeDefined();
expect(formX.$name).toBe('nestedX');
scope.idA = 'B';
scope.idB = 'Y';
scope.$digest();
expect(scope.form.nestedA).toBeUndefined();
expect(scope.form.nestedB).toBe(formA);
expect(formA.nestedX).toBeUndefined();
expect(formA.nestedY).toBe(formX);
});
it('should rename forms with no parent when interpolated name changes', function() {
var element = $compile('<form name="name{{nameID}}"></form>')(scope);
var element2 = $compile('<div ng-form="name{{nameID}}"></div>')(scope);
scope.nameID = "A";
scope.$digest();
var form = element.controller('form');
var form2 = element2.controller('form');
expect(form.$name).toBe('nameA');
expect(form2.$name).toBe('nameA');
scope.nameID = "B";
scope.$digest();
expect(form.$name).toBe('nameB');
expect(form2.$name).toBe('nameB');
});
describe('$setSubmitted', function() {
beforeEach(function() {
doc = $compile(
+79 -15
View File
@@ -1289,6 +1289,42 @@ describe('input', function() {
}
}));
it('should interpolate input names', function() {
scope.nameID = '47';
compileInput('<input type="text" ng-model="name" name="name{{nameID}}" />');
expect(scope.form.name47.$pristine).toBeTruthy();
changeInputValueTo('caitp');
expect(scope.form.name47.$dirty).toBeTruthy();
});
it('should rename form controls in form when interpolated name changes', function() {
scope.nameID = "A";
compileInput('<input type="text" ng-model="name" name="name{{nameID}}" />');
expect(scope.form.nameA.$name).toBe('nameA');
var oldModel = scope.form.nameA;
scope.nameID = "B";
scope.$digest();
expect(scope.form.nameA).toBeUndefined();
expect(scope.form.nameB).toBe(oldModel);
expect(scope.form.nameB.$name).toBe('nameB');
});
it('should rename form controls in null form when interpolated name changes', function() {
var element = $compile('<input type="text" ng-model="name" name="name{{nameID}}" />')(scope);
scope.nameID = "A";
scope.$digest();
var model = element.controller('ngModel');
expect(model.$name).toBe('nameA');
scope.nameID = "B";
scope.$digest();
expect(model.$name).toBe('nameB');
});
describe('"change" event', function() {
function assertBrowserSupportsChangeEvent(inputEventSupported) {
// Force browser to report a lack of an 'input' event
@@ -2590,13 +2626,13 @@ describe('input', function() {
});
it('should set the view if the model if a valid Date object.', function(){
compileInput('<input type="datetime-local" ng-model="tenSecondsToNextYear"/>');
compileInput('<input type="datetime-local" ng-model="halfSecondToNextYear"/>');
scope.$apply(function (){
scope.tenSecondsToNextYear = new Date(2013, 11, 31, 23, 59, 0);
scope.halfSecondToNextYear = new Date(2013, 11, 31, 23, 59, 59, 500);
});
expect(inputElm.val()).toBe('2013-12-31T23:59:00');
expect(inputElm.val()).toBe('2013-12-31T23:59:59.500');
});
it('should set the model undefined if the view is invalid', function (){
@@ -2606,7 +2642,7 @@ describe('input', function() {
scope.breakMe = new Date(2009, 0, 6, 16, 25, 0);
});
expect(inputElm.val()).toBe('2009-01-06T16:25:00');
expect(inputElm.val()).toBe('2009-01-06T16:25:00.000');
try {
//set to text for browsers with datetime-local validation.
@@ -2663,7 +2699,21 @@ describe('input', function() {
scope.$apply(function() {
scope.value = new Date(Date.UTC(2001, 0, 1, 1, 2, 0));
});
expect(inputElm.val()).toBe('2001-01-01T01:02:00');
expect(inputElm.val()).toBe('2001-01-01T01:02:00.000');
});
it('should allow to specify the milliseconds', function() {
compileInput('<input type="datetime-local" ng-model="value"" />');
changeInputValueTo('2000-01-01T01:02:03.500');
expect(+scope.value).toBe(+new Date(2000, 0, 1, 1, 2, 3, 500));
});
it('should allow to specify single digit milliseconds', function() {
compileInput('<input type="datetime-local" ng-model="value"" />');
changeInputValueTo('2000-01-01T01:02:03.4');
expect(+scope.value).toBe(+new Date(2000, 0, 1, 1, 2, 3, 400));
});
it('should allow to specify the seconds', function() {
@@ -2675,7 +2725,7 @@ describe('input', function() {
scope.$apply(function() {
scope.value = new Date(2001, 0, 1, 1, 2, 3);
});
expect(inputElm.val()).toBe('2001-01-01T01:02:03');
expect(inputElm.val()).toBe('2001-01-01T01:02:03.000');
});
it('should allow to skip the seconds', function() {
@@ -2854,10 +2904,10 @@ describe('input', function() {
compileInput('<input type="time" ng-model="threeFortyOnePm"/>');
scope.$apply(function (){
scope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0);
scope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('15:41:00');
expect(inputElm.val()).toBe('15:41:00.500');
});
it('should set the model undefined if the view is invalid', function (){
@@ -2867,7 +2917,7 @@ describe('input', function() {
scope.breakMe = new Date(1970, 0, 1, 16, 25, 0);
});
expect(inputElm.val()).toBe('16:25:00');
expect(inputElm.val()).toBe('16:25:00.000');
try {
//set to text for browsers with time validation.
@@ -2924,7 +2974,21 @@ describe('input', function() {
scope.$apply(function() {
scope.value = new Date(Date.UTC(1971, 0, 1, 23, 2, 0));
});
expect(inputElm.val()).toBe('23:02:00');
expect(inputElm.val()).toBe('23:02:00.000');
});
it('should allow to specify the milliseconds', function() {
compileInput('<input type="time" ng-model="value"" />');
changeInputValueTo('01:02:03.500');
expect(+scope.value).toBe(+new Date(1970, 0, 1, 1, 2, 3, 500));
});
it('should allow to specify single digit milliseconds', function() {
compileInput('<input type="time" ng-model="value"" />');
changeInputValueTo('01:02:03.4');
expect(+scope.value).toBe(+new Date(1970, 0, 1, 1, 2, 3, 400));
});
it('should allow to specify the seconds', function() {
@@ -2936,7 +3000,7 @@ describe('input', function() {
scope.$apply(function() {
scope.value = new Date(1970, 0, 1, 1, 2, 3);
});
expect(inputElm.val()).toBe('01:02:03');
expect(inputElm.val()).toBe('01:02:03.000');
});
it('should allow to skip the seconds', function() {
@@ -3192,13 +3256,13 @@ describe('input', function() {
scope.val = new Date(2013, 1, 2, 3, 4, 5, 6);
});
expect(timeElm.val()).toBe('03:04:05');
expect(timeElm.val()).toBe('03:04:05.006');
expect(monthElm.val()).toBe('2013-02');
expect(weekElm.val()).toBe('2013-W05');
changeGivenInputTo(monthElm, '2012-02');
expect(monthElm.val()).toBe('2012-02');
expect(timeElm.val()).toBe('03:04:05');
expect(timeElm.val()).toBe('03:04:05.006');
expect(weekElm.val()).toBe('2012-W05');
changeGivenInputTo(timeElm, '04:05:06');
@@ -3208,10 +3272,10 @@ describe('input', function() {
changeGivenInputTo(weekElm, '2014-W01');
expect(monthElm.val()).toBe('2014-01');
expect(timeElm.val()).toBe('04:05:06');
expect(timeElm.val()).toBe('04:05:06.000');
expect(weekElm.val()).toBe('2014-W01');
expect(+scope.val).toBe(+new Date(2014, 0, 2, 4, 5, 6, 6));
expect(+scope.val).toBe(+new Date(2014, 0, 2, 4, 5, 6, 0));
function changeGivenInputTo(inputElm, value) {
inputElm.val(value);
+27
View File
@@ -148,6 +148,33 @@ describe('select', function() {
});
it('should interpolate select names', function() {
scope.robots = ['c3p0', 'r2d2'];
scope.name = 'r2d2';
scope.nameID = 47;
compile('<select ng-model="name" name="name{{nameID}}">' +
'<option ng-repeat="r in robots">{{r}}</option>' +
'</select>');
expect(scope.form.name47.$pristine).toBeTruthy();
browserTrigger(element.find('option').eq(0));
expect(scope.form.name47.$dirty).toBeTruthy();
expect(scope.name).toBe('c3p0');
});
it('should rename select controls in form when interpolated name changes', function() {
scope.nameID = "A";
compile('<select ng-model="name" name="name{{nameID}}"></select>');
expect(scope.form.nameA.$name).toBe('nameA');
var oldModel = scope.form.nameA;
scope.nameID = "B";
scope.$digest();
expect(scope.form.nameA).toBeUndefined();
expect(scope.form.nameB).toBe(oldModel);
expect(scope.form.nameB.$name).toBe('nameB');
});
describe('empty option', function() {
it('should select the empty option when model is undefined', function() {
+104
View File
@@ -1639,6 +1639,77 @@ describe('$location', function() {
return undefined;
}
describe('html5Mode', function() {
it('should set enabled and requireBase when called with object', function() {
module(function($locationProvider) {
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
it('should only overwrite existing properties if values are boolean', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({
enabled: 'duh',
requireBase: 'probably'
});
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
it('should not set unknown input properties to html5Mode object', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({
someProp: 'foo'
});
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
it('should default to enabled:false and requireBase:true', function() {
module(function($locationProvider) {
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
it('should return html5Mode object when called without value', function() {
module(function($locationProvider) {
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
});
describe('LocationHtml5Url', function() {
var location, locationIndex;
@@ -1661,6 +1732,39 @@ describe('$location', function() {
// Note: relies on the previous state!
expect(parseLinkAndReturn(location, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/otherPath#test');
});
it('should complain if no base tag present', function() {
module(function($locationProvider) {
$locationProvider.html5Mode(true);
});
inject(function($browser, $injector) {
$browser.$$baseHref = undefined;
expect(function() {
$injector.get('$location');
}).toThrowMinErr('$location', 'nobase',
"$location in HTML5 mode requires a <base> tag to be present!");
});
});
it('should not complain if baseOptOut set to true in html5Mode', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
});
inject(function($browser, $injector) {
$browser.$$baseHref = undefined;
expect(function() {
$injector.get('$location');
}).not.toThrowMinErr('$location', 'nobase',
"$location in HTML5 mode requires a <base> tag to be present!");
});
});
});
+2 -2
View File
@@ -147,12 +147,12 @@ describe('parser', function() {
it('should tokenize function invocation', function() {
var tokens = lex("a()");
expect(map(tokens, function(t) { return t.text;})).toEqual(['a', '(', ')']);
expect(tokens.map(function(t) { return t.text;})).toEqual(['a', '(', ')']);
});
it('should tokenize method invocation', function() {
var tokens = lex("a.b.c (d) - e.f()");
expect(map(tokens, function(t) { return t.text;})).
expect(tokens.map(function(t) { return t.text;})).
toEqual(['a.b', '.', 'c', '(', 'd', ')', '-', 'e', '.', 'f', '(', ')']);
});
+1 -1
View File
@@ -41,7 +41,7 @@ describe('q', function() {
}
function _argumentsToString(args) {
return map(sliceArgs(args), _argToString).join(',');
return sliceArgs(args).map(_argToString).join(',');
}
// Help log invocation of success(), finally(), progress() and error()
+509
View File
@@ -0,0 +1,509 @@
'use strict';
describe('$aria', function() {
var scope, $compile, element;
beforeEach(module('ngAria'));
function injectScopeAndCompiler() {
return inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
scope = _$rootScope_;
});
}
function compileInput(inputHtml) {
element = $compile(inputHtml)(scope);
scope.$digest();
}
describe('aria-hidden', function() {
beforeEach(injectScopeAndCompiler);
it('should attach aria-hidden to ng-show', function() {
compileInput('<div ng-show="val"></div>');
scope.$apply('val = false');
expect(element.attr('aria-hidden')).toBe('true');
scope.$apply('val = true');
expect(element.attr('aria-hidden')).toBe('false');
});
it('should attach aria-hidden to ng-hide', function() {
compileInput('<div ng-hide="val"></div>');
scope.$apply('val = false');
expect(element.attr('aria-hidden')).toBe('false');
scope.$apply('val = true');
expect(element.attr('aria-hidden')).toBe('true');
});
it('should not change aria-hidden if it is already present on ng-show', function() {
compileInput('<div ng-show="val" aria-hidden="userSetValue"></div>');
expect(element.attr('aria-hidden')).toBe('userSetValue');
scope.$apply('val = true');
expect(element.attr('aria-hidden')).toBe('userSetValue');
});
it('should not change aria-hidden if it is already present on ng-hide', function() {
compileInput('<div ng-hide="val" aria-hidden="userSetValue"></div>');
expect(element.attr('aria-hidden')).toBe('userSetValue');
scope.$apply('val = true');
expect(element.attr('aria-hidden')).toBe('userSetValue');
});
});
describe('aria-hidden when disabled', function() {
beforeEach(configAriaProvider({
ariaHidden: false
}));
beforeEach(injectScopeAndCompiler);
it('should not attach aria-hidden', function() {
scope.$apply('val = false');
compileInput('<div ng-show="val"></div>');
expect(element.attr('aria-hidden')).toBeUndefined();
compileInput('<div ng-hide="val"></div>');
expect(element.attr('aria-hidden')).toBeUndefined();
});
});
describe('aria-checked', function() {
beforeEach(injectScopeAndCompiler);
it('should attach itself to input type="checkbox"', function() {
compileInput('<input type="checkbox" ng-model="val">');
scope.$apply('val = true');
expect(element.attr('aria-checked')).toBe('true');
scope.$apply('val = false');
expect(element.attr('aria-checked')).toBe('false');
});
it('should attach itself to input type="radio"', function() {
var element = $compile('<input type="radio" ng-model="val" value="one">' +
'<input type="radio" ng-model="val" value="two">')(scope);
scope.$apply("val='one'");
expect(element.eq(0).attr('aria-checked')).toBe('true');
expect(element.eq(1).attr('aria-checked')).toBe('false');
scope.$apply("val='two'");
expect(element.eq(0).attr('aria-checked')).toBe('false');
expect(element.eq(1).attr('aria-checked')).toBe('true');
});
it('should attach itself to role="radio"', function() {
scope.$apply("val = 'one'");
compileInput('<div role="radio" ng-model="val" value="{{val}}"></div>');
expect(element.attr('aria-checked')).toBe('true');
});
it('should attach itself to role="checkbox"', function() {
scope.val = true;
compileInput('<div role="checkbox" ng-model="val"></div>');
expect(element.attr('aria-checked')).toBe('true');
});
it('should attach itself to role="menuitemradio"', function() {
scope.val = 'one';
compileInput('<div role="menuitemradio" ng-model="val" value="{{val}}"></div>');
expect(element.attr('aria-checked')).toBe('true');
});
it('should attach itself to role="menuitemcheckbox"', function() {
scope.val = true;
compileInput('<div role="menuitemcheckbox" ng-model="val"></div>');
expect(element.attr('aria-checked')).toBe('true');
});
it('should not attach itself if an aria-checked value is already present', function() {
var element = [
$compile("<input type='checkbox' ng-model='val1' aria-checked='userSetValue'>")(scope),
$compile("<input type='radio' ng-model='val2' value='one' aria-checked='userSetValue'><input type='radio' ng-model='val2' value='two'>")(scope),
$compile("<div role='radio' ng-model='val' value='{{val3}}' aria-checked='userSetValue'></div>")(scope),
$compile("<div role='menuitemradio' ng-model='val' value='{{val3}}' aria-checked='userSetValue'></div>")(scope),
$compile("<div role='checkbox' checked='checked' aria-checked='userSetValue'></div>")(scope),
$compile("<div role='menuitemcheckbox' checked='checked' aria-checked='userSetValue'></div>")(scope)
];
scope.$apply("val1=true;val2='one';val3='1'");
expectAriaAttrOnEachElement(element, 'aria-checked', 'userSetValue');
});
});
describe('aria-checked when disabled', function() {
beforeEach(configAriaProvider({
ariaChecked: false
}));
beforeEach(injectScopeAndCompiler);
it('should not attach aria-checked', function() {
compileInput("<div role='radio' ng-model='val' value='{{val}}'></div>");
expect(element.attr('aria-checked')).toBeUndefined();
compileInput("<div role='menuitemradio' ng-model='val' value='{{val}}'></div>");
expect(element.attr('aria-checked')).toBeUndefined();
compileInput("<div role='checkbox' checked='checked'></div>");
expect(element.attr('aria-checked')).toBeUndefined();
compileInput("<div role='menuitemcheckbox' checked='checked'></div>");
expect(element.attr('aria-checked')).toBeUndefined();
});
});
describe('aria-disabled', function() {
beforeEach(injectScopeAndCompiler);
it('should attach itself to input elements', function() {
scope.$apply('val = false');
compileInput("<input ng-disabled='val'>");
expect(element.attr('aria-disabled')).toBe('false');
scope.$apply('val = true');
expect(element.attr('aria-disabled')).toBe('true');
});
it('should attach itself to textarea elements', function() {
scope.$apply('val = false');
compileInput('<textarea ng-disabled="val"></textarea>');
expect(element.attr('aria-disabled')).toBe('false');
scope.$apply('val = true');
expect(element.attr('aria-disabled')).toBe('true');
});
it('should attach itself to button elements', function() {
scope.$apply('val = false');
compileInput('<button ng-disabled="val"></button>');
expect(element.attr('aria-disabled')).toBe('false');
scope.$apply('val = true');
expect(element.attr('aria-disabled')).toBe('true');
});
it('should attach itself to select elements', function() {
scope.$apply('val = false');
compileInput('<select ng-disabled="val"></select>');
expect(element.attr('aria-disabled')).toBe('false');
scope.$apply('val = true');
expect(element.attr('aria-disabled')).toBe('true');
});
it('should not attach itself if an aria-disabled attribute is already present', function() {
var element = [
$compile("<input aria-disabled='userSetValue' ng-disabled='val'>")(scope),
$compile("<textarea aria-disabled='userSetValue' ng-disabled='val'></textarea>")(scope),
$compile("<button aria-disabled='userSetValue' ng-disabled='val'></button>")(scope),
$compile("<select aria-disabled='userSetValue' ng-disabled='val'></select>")(scope)
];
scope.$apply('val = true');
expectAriaAttrOnEachElement(element, 'aria-disabled', 'userSetValue');
});
});
describe('aria-disabled when disabled', function() {
beforeEach(configAriaProvider({
ariaDisabled: false
}));
beforeEach(injectScopeAndCompiler);
it('should not attach aria-disabled', function() {
var element = [
$compile("<input ng-disabled='val'>")(scope),
$compile("<textarea ng-disabled='val'></textarea>")(scope),
$compile("<button ng-disabled='val'></button>")(scope),
$compile("<select ng-disabled='val'></select>")(scope)
];
scope.$apply('val = false');
expectAriaAttrOnEachElement(element, 'aria-disabled', undefined);
});
});
describe('aria-invalid', function() {
beforeEach(injectScopeAndCompiler);
it('should attach aria-invalid to input', function() {
compileInput('<input ng-model="txtInput" ng-minlength="10">');
scope.$apply("txtInput='LTten'");
expect(element.attr('aria-invalid')).toBe('true');
scope.$apply("txtInput='morethantencharacters'");
expect(element.attr('aria-invalid')).toBe('false');
});
it('should not attach itself if aria-invalid is already present', function() {
compileInput('<input ng-model="txtInput" ng-minlength="10" aria-invalid="userSetValue">');
scope.$apply("txtInput='LTten'");
expect(element.attr('aria-invalid')).toBe('userSetValue');
});
});
describe('aria-invalid when disabled', function() {
beforeEach(configAriaProvider({
ariaInvalid: false
}));
beforeEach(injectScopeAndCompiler);
it('should not attach aria-invalid if the option is disabled', function() {
scope.$apply("txtInput='LTten'");
compileInput('<input ng-model="txtInput" ng-minlength="10">');
expect(element.attr('aria-invalid')).toBeUndefined();
});
});
describe('aria-required', function() {
beforeEach(injectScopeAndCompiler);
it('should attach aria-required to input', function() {
compileInput('<input ng-model="val" required>');
expect(element.attr('aria-required')).toBe('true');
scope.$apply("val='input is valid now'");
expect(element.attr('aria-required')).toBe('false');
});
it('should attach aria-required to textarea', function() {
compileInput('<textarea ng-model="val" required></textarea>');
expect(element.attr('aria-required')).toBe('true');
scope.$apply("val='input is valid now'");
expect(element.attr('aria-required')).toBe('false');
});
it('should attach aria-required to select', function() {
compileInput('<select ng-model="val" required></select>');
expect(element.attr('aria-required')).toBe('true');
scope.$apply("val='input is valid now'");
expect(element.attr('aria-required')).toBe('false');
});
it('should attach aria-required to ngRequired', function() {
compileInput('<input ng-model="val" ng-required="true">');
expect(element.attr('aria-required')).toBe('true');
scope.$apply("val='input is valid now'");
expect(element.attr('aria-required')).toBe('false');
});
it('should not attach itself if aria-required is already present', function() {
compileInput("<input ng-model='val' required aria-required='userSetValue'>");
expect(element.attr('aria-required')).toBe('userSetValue');
compileInput("<textarea ng-model='val' required aria-required='userSetValue'></textarea>");
expect(element.attr('aria-required')).toBe('userSetValue');
compileInput("<select ng-model='val' required aria-required='userSetValue'></select>");
expect(element.attr('aria-required')).toBe('userSetValue');
compileInput("<input ng-model='val' ng-required='true' aria-required='userSetValue'>");
expect(element.attr('aria-required')).toBe('userSetValue');
});
});
describe('aria-required when disabled', function() {
beforeEach(configAriaProvider({
ariaRequired: false
}));
beforeEach(injectScopeAndCompiler);
it('should not add the aria-required attribute', function() {
compileInput("<input ng-model='val' required>");
expect(element.attr('aria-required')).toBeUndefined();
compileInput("<textarea ng-model='val' required></textarea>");
expect(element.attr('aria-required')).toBeUndefined();
compileInput("<select ng-model='val' required></select>");
expect(element.attr('aria-required')).toBeUndefined();
});
});
describe('aria-multiline', function() {
beforeEach(injectScopeAndCompiler);
it('should attach itself to textarea', function() {
compileInput('<textarea ng-model="val"></textarea>');
expect(element.attr('aria-multiline')).toBe('true');
});
it('should attach itself role="textbox"', function() {
compileInput('<div role="textbox" ng-model="val"></div>');
expect(element.attr('aria-multiline')).toBe('true');
});
it('should not attach itself if aria-multiline is already present', function() {
compileInput('<textarea aria-multiline="userSetValue"></textarea>');
expect(element.attr('aria-multiline')).toBe('userSetValue');
compileInput('<div role="textbox" aria-multiline="userSetValue"></div>');
expect(element.attr('aria-multiline')).toBe('userSetValue');
});
});
describe('aria-multiline when disabled', function() {
beforeEach(configAriaProvider({
ariaMultiline: false
}));
beforeEach(injectScopeAndCompiler);
it('should not attach itself to textarea', function() {
compileInput('<textarea></textarea>');
expect(element.attr('aria-multiline')).toBeUndefined();
});
it('should not attach itself role="textbox"', function() {
compileInput('<div role="textbox"></div>');
expect(element.attr('aria-multiline')).toBeUndefined();
});
});
describe('aria-value', function() {
beforeEach(injectScopeAndCompiler);
it('should attach to input type="range"', function() {
var element = [
$compile('<input type="range" ng-model="val" min="0" max="100">')(scope),
$compile('<div role="progressbar" min="0" max="100" ng-model="val">')(scope),
$compile('<div role="slider" min="0" max="100" ng-model="val">')(scope)
];
scope.$apply('val = 50');
expectAriaAttrOnEachElement(element, 'aria-valuenow', "50");
expectAriaAttrOnEachElement(element, 'aria-valuemin', "0");
expectAriaAttrOnEachElement(element, 'aria-valuemax', "100");
scope.$apply('val = 90');
expectAriaAttrOnEachElement(element, 'aria-valuenow', "90");
});
it('should not attach if aria-value* is already present', function() {
var element = [
$compile('<input type="range" ng-model="val" min="0" max="100" aria-valuenow="userSetValue1" aria-valuemin="userSetValue2" aria-valuemax="userSetValue3">')(scope),
$compile('<div role="progressbar" min="0" max="100" ng-model="val" aria-valuenow="userSetValue1" aria-valuemin="userSetValue2" aria-valuemax="userSetValue3">')(scope),
$compile('<div role="slider" min="0" max="100" ng-model="val" aria-valuenow="userSetValue1" aria-valuemin="userSetValue2" aria-valuemax="userSetValue3">')(scope)
];
scope.$apply('val = 50');
expectAriaAttrOnEachElement(element, 'aria-valuenow', 'userSetValue1');
expectAriaAttrOnEachElement(element, 'aria-valuemin', 'userSetValue2');
expectAriaAttrOnEachElement(element, 'aria-valuemax', 'userSetValue3');
});
});
describe('aria-value when disabled', function() {
beforeEach(configAriaProvider({
ariaValue: false
}));
beforeEach(injectScopeAndCompiler);
it('should not attach itself', function() {
scope.$apply('val = 50');
compileInput('<input type="range" ng-model="val" min="0" max="100">');
expect(element.attr('aria-valuenow')).toBeUndefined();
expect(element.attr('aria-valuemin')).toBeUndefined();
expect(element.attr('aria-valuemax')).toBeUndefined();
compileInput('<div role="progressbar" min="0" max="100" ng-model="val">');
expect(element.attr('aria-valuenow')).toBeUndefined();
expect(element.attr('aria-valuemin')).toBeUndefined();
expect(element.attr('aria-valuemax')).toBeUndefined();
});
});
describe('tabindex', function() {
beforeEach(injectScopeAndCompiler);
it('should attach tabindex to role="checkbox", ng-click, and ng-dblclick', function() {
compileInput('<div role="checkbox" ng-model="val"></div>');
expect(element.attr('tabindex')).toBe('0');
compileInput('<div ng-click="someAction()"></div>');
expect(element.attr('tabindex')).toBe('0');
compileInput('<div ng-dblclick="someAction()"></div>');
expect(element.attr('tabindex')).toBe('0');
});
it('should not attach tabindex if it is already on an element', function() {
compileInput('<div role="button" tabindex="userSetValue"></div>');
expect(element.attr('tabindex')).toBe('userSetValue');
compileInput('<div role="checkbox" tabindex="userSetValue"></div>');
expect(element.attr('tabindex')).toBe('userSetValue');
compileInput('<div ng-click="someAction()" tabindex="userSetValue"></div>');
expect(element.attr('tabindex')).toBe('userSetValue');
compileInput('<div ng-dblclick="someAction()" tabindex="userSetValue"></div>');
expect(element.attr('tabindex')).toBe('userSetValue');
});
it('should set proper tabindex values for radiogroup', function() {
compileInput('<div role="radiogroup">' +
'<div role="radio" ng-model="val" value="one">1</div>' +
'<div role="radio" ng-model="val" value="two">2</div>' +
'</div>');
var one = element.contents().eq(0);
var two = element.contents().eq(1);
scope.$apply("val = 'one'");
expect(one.attr('tabindex')).toBe('0');
expect(two.attr('tabindex')).toBe('-1');
scope.$apply("val = 'two'");
expect(one.attr('tabindex')).toBe('-1');
expect(two.attr('tabindex')).toBe('0');
dealoc(element);
});
});
describe('tabindex when disabled', function() {
beforeEach(configAriaProvider({
tabindex: false
}));
beforeEach(injectScopeAndCompiler);
it('should not add a tabindex attribute', function() {
compileInput('<div role="button"></div>');
expect(element.attr('tabindex')).toBeUndefined();
compileInput('<div role="checkbox"></div>');
expect(element.attr('tabindex')).toBeUndefined();
compileInput('<div ng-click="someAction()"></div>');
expect(element.attr('tabindex')).toBeUndefined();
compileInput('<div ng-dblclick="someAction()"></div>');
expect(element.attr('tabindex')).toBeUndefined();
});
});
});
function expectAriaAttrOnEachElement(elem, ariaAttr, expected) {
angular.forEach(elem, function(val) {
expect(angular.element(val).attr(ariaAttr)).toBe(expected);
});
}
function configAriaProvider (config) {
return function() {
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider) {
$ariaProvider.config(config);
});
module('ariaTest');
};
}