aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Angular.js105
-rw-r--r--src/Browser.js10
-rw-r--r--src/Scope.js3
-rw-r--r--src/angular-bootstrap.js3
-rw-r--r--src/apis.js173
-rw-r--r--src/directives.js86
-rw-r--r--src/filters.js113
-rw-r--r--src/formatters.js202
-rw-r--r--src/jqLite.js15
-rw-r--r--src/markups.js24
-rw-r--r--src/parser.js43
-rw-r--r--src/scenario/Scenario.js2
-rw-r--r--src/scenario/dsl.js22
-rw-r--r--src/service/formFactory.js394
-rw-r--r--src/service/invalidWidgets.js69
-rw-r--r--src/service/log.js3
-rw-r--r--src/service/resource.js3
-rw-r--r--src/service/route.js6
-rw-r--r--src/service/window.js2
-rw-r--r--src/service/xhr.js5
-rw-r--r--src/validators.js482
-rw-r--r--src/widget/form.js81
-rw-r--r--src/widget/input.js773
-rw-r--r--src/widget/select.js427
-rw-r--r--src/widgets.js1033
25 files changed, 2104 insertions, 1975 deletions
diff --git a/src/Angular.js b/src/Angular.js
index caa51a06..7c218c6e 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -55,7 +55,6 @@ function fromCharCode(code) { return String.fromCharCode(code); }
var _undefined = undefined,
_null = null,
$$scope = '$scope',
- $$validate = '$validate',
$angular = 'angular',
$array = 'array',
$boolean = 'boolean',
@@ -93,12 +92,10 @@ var _undefined = undefined,
angularDirective = extensionMap(angular, 'directive'),
/** @name angular.widget */
angularWidget = extensionMap(angular, 'widget', lowercase),
- /** @name angular.validator */
- angularValidator = extensionMap(angular, 'validator'),
- /** @name angular.fileter */
+ /** @name angular.filter */
angularFilter = extensionMap(angular, 'filter'),
- /** @name angular.formatter */
- angularFormatter = extensionMap(angular, 'formatter'),
+ /** @name angular.service */
+ angularInputType = extensionMap(angular, 'inputType', lowercase),
/** @name angular.service */
angularService = extensionMap(angular, 'service'),
angularCallbacks = extensionMap(angular, 'callbacks'),
@@ -156,10 +153,18 @@ function forEach(obj, iterator, context) {
return obj;
}
-function forEachSorted(obj, iterator, context) {
+function sortedKeys(obj) {
var keys = [];
- for (var key in obj) keys.push(key);
- keys.sort();
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ keys.push(key);
+ }
+ }
+ return keys.sort();
+}
+
+function forEachSorted(obj, iterator, context) {
+ var keys = sortedKeys(obj)
for ( var i = 0; i < keys.length; i++) {
iterator.call(context, obj[keys[i]], keys[i]);
}
@@ -180,7 +185,6 @@ function formatError(arg) {
}
/**
- * @description
* A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric
* characters such as '012ABC'. The reason why we are not using simply a number counter is that
* the number string gets longer over time, and it can also overflow, where as the the nextId
@@ -599,20 +603,33 @@ function isLeafNode (node) {
* @example
* <doc:example>
* <doc:source>
- Salutation: <input type="text" name="master.salutation" value="Hello" /><br/>
- Name: <input type="text" name="master.name" value="world"/><br/>
- <button ng:click="form = master.$copy()">copy</button>
- <hr/>
-
- The master object is <span ng:hide="master.$equals(form)">NOT</span> equal to the form object.
-
- <pre>master={{master}}</pre>
- <pre>form={{form}}</pre>
+ <script>
+ function Ctrl(){
+ this.master = {
+ salutation: 'Hello',
+ name: 'world'
+ };
+ this.copy = function (){
+ this.form = angular.copy(this.master);
+ }
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Salutation: <input type="text" ng:model="master.salutation" ><br/>
+ Name: <input type="text" ng:model="master.name"><br/>
+ <button ng:click="copy()">copy</button>
+ <hr/>
+
+ The master object is <span ng:hide="master.$equals(form)">NOT</span> equal to the form object.
+
+ <pre>master={{master}}</pre>
+ <pre>form={{form}}</pre>
+ </div>
* </doc:source>
* <doc:scenario>
it('should print that initialy the form object is NOT equal to master', function() {
- expect(element('.doc-example-live input[name="master.salutation"]').val()).toBe('Hello');
- expect(element('.doc-example-live input[name="master.name"]').val()).toBe('world');
+ expect(element('.doc-example-live input[ng\\:model="master.salutation"]').val()).toBe('Hello');
+ expect(element('.doc-example-live input[ng\\:model="master.name"]').val()).toBe('world');
expect(element('.doc-example-live span').css('display')).toBe('inline');
});
@@ -691,20 +708,31 @@ function copy(source, destination){
* @example
* <doc:example>
* <doc:source>
- Salutation: <input type="text" name="greeting.salutation" value="Hello" /><br/>
- Name: <input type="text" name="greeting.name" value="world"/><br/>
- <hr/>
-
- The <code>greeting</code> object is
- <span ng:hide="greeting.$equals({salutation:'Hello', name:'world'})">NOT</span> equal to
- <code>{salutation:'Hello', name:'world'}</code>.
-
- <pre>greeting={{greeting}}</pre>
+ <script>
+ function Ctrl(){
+ this.master = {
+ salutation: 'Hello',
+ name: 'world'
+ };
+ this.greeting = angular.copy(this.master);
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Salutation: <input type="text" ng:model="greeting.salutation"><br/>
+ Name: <input type="text" ng:model="greeting.name"><br/>
+ <hr/>
+
+ The <code>greeting</code> object is
+ <span ng:hide="greeting.$equals(master)">NOT</span> equal to
+ <code>{salutation:'Hello', name:'world'}</code>.
+
+ <pre>greeting={{greeting}}</pre>
+ </div>
* </doc:source>
* <doc:scenario>
it('should print that initialy greeting is equal to the hardcoded value object', function() {
- expect(element('.doc-example-live input[name="greeting.salutation"]').val()).toBe('Hello');
- expect(element('.doc-example-live input[name="greeting.name"]').val()).toBe('world');
+ expect(element('.doc-example-live input[ng\\:model="greeting.salutation"]').val()).toBe('Hello');
+ expect(element('.doc-example-live input[ng\\:model="greeting.name"]').val()).toBe('world');
expect(element('.doc-example-live span').css('display')).toBe('none');
});
@@ -915,24 +943,19 @@ function angularInit(config, document){
if (config.css)
$browser.addCss(config.base_url + config.css);
- else if(msie<8)
- $browser.addJs(config.ie_compat, config.ie_compat_id);
scope.$apply();
}
}
-function angularJsConfig(document, config) {
+function angularJsConfig(document) {
bindJQuery();
var scripts = document.getElementsByTagName("script"),
+ config = {},
match;
- config = extend({
- ie_compat_id: 'ng-ie-compat'
- }, config);
for(var j = 0; j < scripts.length; j++) {
match = (scripts[j].src || "").match(rngScript);
if (match) {
config.base_url = match[1];
- config.ie_compat = match[1] + 'angular-ie-compat' + (match[2] || '') + '.js';
extend(config, parseKeyValue(match[6]));
eachAttribute(jqLite(scripts[j]), function(value, name){
if (/^ng:/.exec(name)) {
@@ -974,11 +997,13 @@ function assertArg(arg, name, reason) {
(reason || "required"));
throw error;
}
+ return arg;
}
function assertArgFn(arg, name) {
- assertArg(isFunction(arg), name, 'not a function, got ' +
+ assertArg(isFunction(arg), name, 'not a function, got ' +
(typeof arg == 'object' ? arg.constructor.name : typeof arg));
+ return arg;
}
diff --git a/src/Browser.js b/src/Browser.js
index ed12441a..77d1c684 100644
--- a/src/Browser.js
+++ b/src/Browser.js
@@ -105,7 +105,7 @@ function Browser(window, document, body, XHR, $log, $sniffer) {
window[callbackId].data = data;
};
- var script = self.addJs(url.replace('JSON_CALLBACK', callbackId), null, function() {
+ var script = self.addJs(url.replace('JSON_CALLBACK', callbackId), function() {
if (window[callbackId].data) {
completeOutstandingRequest(callback, 200, window[callbackId].data);
} else {
@@ -442,24 +442,18 @@ function Browser(window, document, body, XHR, $log, $sniffer) {
* @methodOf angular.service.$browser
*
* @param {string} url Url to js file
- * @param {string=} domId Optional id for the script tag
*
* @description
* Adds a script tag to the head.
*/
- self.addJs = function(url, domId, done) {
+ self.addJs = function(url, done) {
// we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.:
// - fetches local scripts via XHR and evals them
// - adds and immediately removes script elements from the document
- //
- // We need addJs to be able to add angular-ie-compat.js which is very special and must remain
- // part of the DOM so that the embedded images can reference it. jQuery's append implementation
- // (v1.4.2) fubars it.
var script = rawDocument.createElement('script');
script.type = 'text/javascript';
script.src = url;
- if (domId) script.id = domId;
if (msie) {
script.onreadystatechange = function() {
diff --git a/src/Scope.js b/src/Scope.js
index c5f5bf1b..e4fc0622 100644
--- a/src/Scope.js
+++ b/src/Scope.js
@@ -354,7 +354,8 @@ Scope.prototype = {
// circuit it with === operator, only when === fails do we use .equals
if ((value = watch.get(current)) !== (last = watch.last) && !equals(value, last)) {
dirty = true;
- watch.fn(current, watch.last = copy(value), last);
+ watch.last = copy(value);
+ watch.fn(current, value, last);
}
} catch (e) {
current.$service('$exceptionHandler')(e);
diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js
index f9d9643d..7a1752d2 100644
--- a/src/angular-bootstrap.js
+++ b/src/angular-bootstrap.js
@@ -101,9 +101,6 @@
var config = angularJsConfig(document);
- // angular-ie-compat.js needs to be pregenerated for development with IE<8
- config.ie_compat = serverPath + '../build/angular-ie-compat.js';
-
angularInit(config, document);
}
diff --git a/src/apis.js b/src/apis.js
index bec54b8e..6a5bf6c4 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -103,9 +103,16 @@ var angularArray = {
* @example
<doc:example>
<doc:source>
- <div ng:init="books = ['Moby Dick', 'Great Gatsby', 'Romeo and Juliet']"></div>
- <input name='bookName' value='Romeo and Juliet'> <br>
- Index of '{{bookName}}' in the list {{books}} is <em>{{books.$indexOf(bookName)}}</em>.
+ <script>
+ function Ctrl(){
+ this.books = ['Moby Dick', 'Great Gatsby', 'Romeo and Juliet'];
+ this.bookName = 'Romeo and Juliet';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <input ng:model='bookName'> <br>
+ Index of '{{bookName}}' in the list {{books}} is <em>{{books.$indexOf(bookName)}}</em>.
+ </div>
</doc:source>
<doc:scenario>
it('should correctly calculate the initial index', function() {
@@ -146,17 +153,29 @@ var angularArray = {
* @example
<doc:example>
<doc:source>
- <table ng:init="invoice= {items:[{qty:10, description:'gadget', cost:9.95}]}">
+ <script>
+ function Ctrl(){
+ this.invoice = {
+ items:[ {
+ qty:10,
+ description:'gadget',
+ cost:9.95
+ }
+ ]
+ };
+ }
+ </script>
+ <table class="invoice" ng:controller="Ctrl">
<tr><th>Qty</th><th>Description</th><th>Cost</th><th>Total</th><th></th></tr>
<tr ng:repeat="item in invoice.items">
- <td><input name="item.qty" value="1" size="4" ng:required ng:validate="integer"></td>
- <td><input name="item.description"></td>
- <td><input name="item.cost" value="0.00" ng:required ng:validate="number" size="6"></td>
+ <td><input type="integer" ng:model="item.qty" size="4" required></td>
+ <td><input type="text" ng:model="item.description"></td>
+ <td><input type="number" ng:model="item.cost" required size="6"></td>
<td>{{item.qty * item.cost | currency}}</td>
<td>[<a href ng:click="invoice.items.$remove(item)">X</a>]</td>
</tr>
<tr>
- <td><a href ng:click="invoice.items.$add()">add item</a></td>
+ <td><a href ng:click="invoice.items.$add({qty:1, cost:0})">add item</a></td>
<td></td>
<td>Total:</td>
<td>{{invoice.items.$sum('qty*cost') | currency}}</td>
@@ -166,8 +185,8 @@ var angularArray = {
<doc:scenario>
//TODO: these specs are lame because I had to work around issues #164 and #167
it('should initialize and calculate the totals', function() {
- expect(repeater('.doc-example-live table tr', 'item in invoice.items').count()).toBe(3);
- expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(1)).
+ expect(repeater('table.invoice tr', 'item in invoice.items').count()).toBe(3);
+ expect(repeater('table.invoice tr', 'item in invoice.items').row(1)).
toEqual(['$99.50']);
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
@@ -178,7 +197,7 @@ var angularArray = {
using('.doc-example-live tr:nth-child(3)').input('item.qty').enter('20');
using('.doc-example-live tr:nth-child(3)').input('item.cost').enter('100');
- expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(2)).
+ expect(repeater('table.invoice tr', 'item in invoice.items').row(2)).
toEqual(['$2,000.00']);
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$2,099.50');
});
@@ -297,7 +316,7 @@ var angularArray = {
{name:'Adam', phone:'555-5678'},
{name:'Julie', phone:'555-8765'}]"></div>
- Search: <input name="searchText"/>
+ Search: <input ng:model="searchText"/>
<table id="searchTextResults">
<tr><th>Name</th><th>Phone</th><tr>
<tr ng:repeat="friend in friends.$filter(searchText)">
@@ -306,9 +325,9 @@ var angularArray = {
<tr>
</table>
<hr>
- Any: <input name="search.$"/> <br>
- Name only <input name="search.name"/><br>
- Phone only <input name="search.phone"/><br>
+ Any: <input ng:model="search.$"/> <br>
+ Name only <input ng:model="search.name"/><br>
+ Phone only <input ng:model="search.phone"/><br>
<table id="searchObjResults">
<tr><th>Name</th><th>Phone</th><tr>
<tr ng:repeat="friend in friends.$filter(search)">
@@ -442,22 +461,29 @@ var angularArray = {
* with objects created from user input.
<doc:example>
<doc:source>
- [<a href="" ng:click="people.$add()">add empty</a>]
- [<a href="" ng:click="people.$add({name:'John', sex:'male'})">add 'John'</a>]
- [<a href="" ng:click="people.$add({name:'Mary', sex:'female'})">add 'Mary'</a>]
-
- <ul ng:init="people=[]">
- <li ng:repeat="person in people">
- <input name="person.name">
- <select name="person.sex">
- <option value="">--chose one--</option>
- <option>male</option>
- <option>female</option>
- </select>
- [<a href="" ng:click="people.$remove(person)">X</a>]
- </li>
- </ul>
- <pre>people = {{people}}</pre>
+ <script>
+ function Ctrl(){
+ this.people = [];
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ [<a href="" ng:click="people.$add()">add empty</a>]
+ [<a href="" ng:click="people.$add({name:'John', sex:'male'})">add 'John'</a>]
+ [<a href="" ng:click="people.$add({name:'Mary', sex:'female'})">add 'Mary'</a>]
+
+ <ul>
+ <li ng:repeat="person in people">
+ <input ng:model="person.name">
+ <select ng:model="person.sex">
+ <option value="">--chose one--</option>
+ <option>male</option>
+ <option>female</option>
+ </select>
+ [<a href="" ng:click="people.$remove(person)">X</a>]
+ </li>
+ </ul>
+ <pre>people = {{people}}</pre>
+ </div>
</doc:source>
<doc:scenario>
beforeEach(function() {
@@ -466,7 +492,7 @@ var angularArray = {
it('should create an empty record when "add empty" is clicked', function() {
element('.doc-example-live a:contains("add empty")').click();
- expect(binding('people')).toBe('people = [{\n "name":"",\n "sex":null}]');
+ expect(binding('people')).toBe('people = [{\n }]');
});
it('should create a "John" record when "add \'John\'" is clicked', function() {
@@ -521,7 +547,7 @@ var angularArray = {
<ul>
<li ng:repeat="item in items">
{{item.name}}: points=
- <input type="text" name="item.points"/> <!-- id="item{{$index}} -->
+ <input type="text" ng:model="item.points"/> <!-- id="item{{$index}} -->
</li>
</ul>
<p>Number of items which have one point: <em>{{ items.$count('points==1') }}</em></p>
@@ -585,49 +611,56 @@ var angularArray = {
* @example
<doc:example>
<doc:source>
- <div ng:init="friends = [{name:'John', phone:'555-1212', age:10},
- {name:'Mary', phone:'555-9876', age:19},
- {name:'Mike', phone:'555-4321', age:21},
- {name:'Adam', phone:'555-5678', age:35},
- {name:'Julie', phone:'555-8765', age:29}]"></div>
-
- <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
- <hr/>
- [ <a href="" ng:click="predicate=''">unsorted</a> ]
- <table ng:init="predicate='-age'">
- <tr>
- <th><a href="" ng:click="predicate = 'name'; reverse=false">Name</a>
- (<a href ng:click="predicate = '-name'; reverse=false">^</a>)</th>
- <th><a href="" ng:click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th>
- <th><a href="" ng:click="predicate = 'age'; reverse=!reverse">Age</a></th>
- <tr>
- <tr ng:repeat="friend in friends.$orderBy(predicate, reverse)">
- <td>{{friend.name}}</td>
- <td>{{friend.phone}}</td>
- <td>{{friend.age}}</td>
- <tr>
- </table>
+ <script>
+ function Ctrl(){
+ this.friends =
+ [{name:'John', phone:'555-1212', age:10},
+ {name:'Mary', phone:'555-9876', age:19},
+ {name:'Mike', phone:'555-4321', age:21},
+ {name:'Adam', phone:'555-5678', age:35},
+ {name:'Julie', phone:'555-8765', age:29}]
+ this.predicate = '-age';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
+ <hr/>
+ [ <a href="" ng:click="predicate=''">unsorted</a> ]
+ <table class="friend">
+ <tr>
+ <th><a href="" ng:click="predicate = 'name'; reverse=false">Name</a>
+ (<a href ng:click="predicate = '-name'; reverse=false">^</a>)</th>
+ <th><a href="" ng:click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th>
+ <th><a href="" ng:click="predicate = 'age'; reverse=!reverse">Age</a></th>
+ <tr>
+ <tr ng:repeat="friend in friends.$orderBy(predicate, reverse)">
+ <td>{{friend.name}}</td>
+ <td>{{friend.phone}}</td>
+ <td>{{friend.age}}</td>
+ <tr>
+ </table>
+ </div>
</doc:source>
<doc:scenario>
it('should be reverse ordered by aged', function() {
expect(binding('predicate')).toBe('Sorting predicate = -age; reverse = ');
- expect(repeater('.doc-example-live table', 'friend in friends').column('friend.age')).
+ expect(repeater('table.friend', 'friend in friends').column('friend.age')).
toEqual(['35', '29', '21', '19', '10']);
- expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')).
+ expect(repeater('table.friend', 'friend in friends').column('friend.name')).
toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']);
});
it('should reorder the table when user selects different predicate', function() {
element('.doc-example-live a:contains("Name")').click();
- expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')).
+ expect(repeater('table.friend', 'friend in friends').column('friend.name')).
toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']);
- expect(repeater('.doc-example-live table', 'friend in friends').column('friend.age')).
+ expect(repeater('table.friend', 'friend in friends').column('friend.age')).
toEqual(['35', '10', '29', '19', '21']);
element('.doc-example-live a:contains("Phone")').click();
- expect(repeater('.doc-example-live table', 'friend in friends').column('friend.phone')).
+ expect(repeater('table.friend', 'friend in friends').column('friend.phone')).
toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']);
- expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')).
+ expect(repeater('table.friend', 'friend in friends').column('friend.name')).
toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']);
});
</doc:scenario>
@@ -704,14 +737,20 @@ var angularArray = {
* @example
<doc:example>
<doc:source>
- <div ng:init="numbers = [1,2,3,4,5,6,7,8,9]">
- Limit [1,2,3,4,5,6,7,8,9] to: <input name="limit" value="3"/>
+ <script>
+ function Ctrl(){
+ this.numbers = [1,2,3,4,5,6,7,8,9];
+ this.limit = 3;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Limit {{numbers}} to: <input type="integer" ng:model="limit"/>
<p>Output: {{ numbers.$limitTo(limit) | json }}</p>
</div>
</doc:source>
<doc:scenario>
it('should limit the numer array to first three items', function() {
- expect(element('.doc-example-live input[name=limit]').val()).toBe('3');
+ expect(element('.doc-example-live input[ng\\:model=limit]').val()).toBe('3');
expect(binding('numbers.$limitTo(limit) | json')).toEqual('[1,2,3]');
});
@@ -840,7 +879,7 @@ var angularFunction = {
* Hash of a:
* string is string
* number is number as string
- * object is either result of calling $$hashKey function on the object or uniquely generated id,
+ * object is either result of calling $$hashKey function on the object or uniquely generated id,
* that is also assigned to the $$hashKey property of the object.
*
* @param obj
@@ -864,7 +903,9 @@ function hashKey(obj) {
/**
* HashMap which can use objects as keys
*/
-function HashMap(){}
+function HashMap(array){
+ forEach(array, this.put, this);
+}
HashMap.prototype = {
/**
* Store key value pair
diff --git a/src/directives.js b/src/directives.js
index dd67ddc7..852d04cd 100644
--- a/src/directives.js
+++ b/src/directives.js
@@ -19,8 +19,6 @@
* to `ng:bind`, but uses JSON key / value pairs to do so.
* * {@link angular.directive.ng:bind-template ng:bind-template} - Replaces the text value of an
* element with a specified template.
- * * {@link angular.directive.ng:change ng:change} - Executes an expression when the value of an
- * input widget changes.
* * {@link angular.directive.ng:class ng:class} - Conditionally set a CSS class on an element.
* * {@link angular.directive.ng:class-even ng:class-even} - Like `ng:class`, but works in
* conjunction with {@link angular.widget.@ng:repeat} to affect even rows in a collection.
@@ -133,16 +131,16 @@ angularDirective("ng:init", function(expression){
};
</script>
<div ng:controller="SettingsController">
- Name: <input type="text" name="name"/>
+ Name: <input type="text" ng:model="name"/>
[ <a href="" ng:click="greet()">greet</a> ]<br/>
Contact:
<ul>
<li ng:repeat="contact in contacts">
- <select name="contact.type">
+ <select ng:model="contact.type">
<option>phone</option>
<option>email</option>
</select>
- <input type="text" name="contact.value"/>
+ <input type="text" ng:model="contact.value"/>
[ <a href="" ng:click="clearContact(contact)">clear</a>
| <a href="" ng:click="removeContact(contact)">X</a> ]
</li>
@@ -153,16 +151,16 @@ angularDirective("ng:init", function(expression){
<doc:scenario>
it('should check controller', function(){
expect(element('.doc-example-live div>:input').val()).toBe('John Smith');
- expect(element('.doc-example-live li[ng\\:repeat-index="0"] input').val())
+ expect(element('.doc-example-live li:nth-child(1) input').val())
.toBe('408 555 1212');
- expect(element('.doc-example-live li[ng\\:repeat-index="1"] input').val())
+ expect(element('.doc-example-live li:nth-child(2) input').val())
.toBe('john.smith@example.org');
element('.doc-example-live li:first a:contains("clear")').click();
expect(element('.doc-example-live li:first input').val()).toBe('');
element('.doc-example-live li:last a:contains("add")').click();
- expect(element('.doc-example-live li[ng\\:repeat-index="2"] input').val())
+ expect(element('.doc-example-live li:nth-child(3) input').val())
.toBe('yourname@example.org');
});
</doc:scenario>
@@ -200,8 +198,15 @@ angularDirective("ng:controller", function(expression){
* Enter a name in the Live Preview text box; the greeting below the text box changes instantly.
<doc:example>
<doc:source>
- Enter name: <input type="text" name="name" value="Whirled"> <br>
- Hello <span ng:bind="name"></span>!
+ <script>
+ function Ctrl(){
+ this.name = 'Whirled';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Enter name: <input type="text" ng:model="name"> <br/>
+ Hello <span ng:bind="name"></span>!
+ </div>
</doc:source>
<doc:scenario>
it('should check ng:bind', function(){
@@ -320,9 +325,17 @@ function compileBindTemplate(template){
* Try it here: enter text in text box and watch the greeting change.
<doc:example>
<doc:source>
- Salutation: <input type="text" name="salutation" value="Hello"><br/>
- Name: <input type="text" name="name" value="World"><br/>
- <pre ng:bind-template="{{salutation}} {{name}}!"></pre>
+ <script>
+ function Ctrl(){
+ this.salutation = 'Hello';
+ this.name = 'World';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Salutation: <input type="text" ng:model="salutation"><br/>
+ Name: <input type="text" ng:model="name"><br/>
+ <pre ng:bind-template="{{salutation}} {{name}}!"></pre>
+ </div>
</doc:source>
<doc:scenario>
it('should check ng:bind', function(){
@@ -351,13 +364,6 @@ angularDirective("ng:bind-template", function(expression, element){
};
});
-var REMOVE_ATTRIBUTES = {
- 'disabled':'disabled',
- 'readonly':'readOnly',
- 'checked':'checked',
- 'selected':'selected',
- 'multiple':'multiple'
-};
/**
* @ngdoc directive
* @name angular.directive.ng:bind-attr
@@ -395,9 +401,16 @@ var REMOVE_ATTRIBUTES = {
* Enter a search string in the Live Preview text box and then click "Google". The search executes instantly.
<doc:example>
<doc:source>
- Google for:
- <input type="text" name="query" value="AngularJS"/>
- <a href="http://www.google.com/search?q={{query}}">Google</a>
+ <script>
+ function Ctrl(){
+ this.query = 'AngularJS';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Google for:
+ <input type="text" ng:model="query"/>
+ <a href="http://www.google.com/search?q={{query}}">Google</a>
+ </div>
</doc:source>
<doc:scenario>
it('should check ng:bind-attr', function(){
@@ -417,18 +430,15 @@ angularDirective("ng:bind-attr", function(expression){
var values = scope.$eval(expression);
for(var key in values) {
var value = compileBindTemplate(values[key])(scope, element),
- specialName = REMOVE_ATTRIBUTES[lowercase(key)];
+ specialName = BOOLEAN_ATTR[lowercase(key)];
if (lastValue[key] !== value) {
lastValue[key] = value;
if (specialName) {
if (toBoolean(value)) {
element.attr(specialName, specialName);
- element.attr('ng-' + specialName, value);
} else {
element.removeAttr(specialName);
- element.removeAttr('ng-' + specialName);
}
- (element.data($$validate)||noop)();
} else {
element.attr(key, value);
}
@@ -505,12 +515,22 @@ angularDirective("ng:click", function(expression, element){
* @example
<doc:example>
<doc:source>
- <form ng:submit="list.push(text);text='';" ng:init="list=[]">
+ <script>
+ function Ctrl(){
+ this.list = [];
+ this.text = 'hello';
+ this.submit = function(){
+ this.list.push(this.text);
+ this.text = '';
+ };
+ }
+ </script>
+ <form ng:submit="submit()" ng:controller="Ctrl">
Enter text and hit enter:
- <input type="text" name="text" value="hello"/>
+ <input type="text" ng:model="text"/>
<input type="submit" id="submit" value="Submit" />
+ <pre>list={{list}}</pre>
</form>
- <pre>list={{list}}</pre>
</doc:source>
<doc:scenario>
it('should check ng:submit', function(){
@@ -537,7 +557,7 @@ function ngClass(selector) {
return function(element) {
this.$watch(expression, function(scope, newVal, oldVal) {
if (selector(scope.$index)) {
- element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal)
+ element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal);
element.addClass(isArray(newVal) ? newVal.join(' ') : newVal);
}
});
@@ -689,7 +709,7 @@ angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;}));
* @example
<doc:example>
<doc:source>
- Click me: <input type="checkbox" name="checked"><br/>
+ Click me: <input type="checkbox" ng:model="checked"><br/>
Show: <span ng:show="checked">I show up when your checkbox is checked.</span> <br/>
Hide: <span ng:hide="checked">I hide when your checkbox is checked.</span>
</doc:source>
@@ -730,7 +750,7 @@ angularDirective("ng:show", function(expression, element){
* @example
<doc:example>
<doc:source>
- Click me: <input type="checkbox" name="checked"><br/>
+ Click me: <input type="checkbox" ng:model="checked"><br/>
Show: <span ng:show="checked">I show up when you checkbox is checked?</span> <br/>
Hide: <span ng:hide="checked">I hide when you checkbox is checked?</span>
</doc:source>
diff --git a/src/filters.js b/src/filters.js
index c5d886ea..0fcd442b 100644
--- a/src/filters.js
+++ b/src/filters.js
@@ -48,9 +48,16 @@
* @example
<doc:example>
<doc:source>
- <input type="text" name="amount" value="1234.56"/> <br/>
- default currency symbol ($): {{amount | currency}}<br/>
- custom currency identifier (USD$): {{amount | currency:"USD$"}}
+ <script>
+ function Ctrl(){
+ this.amount = 1234.56;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <input type="number" ng:model="amount"/> <br/>
+ default currency symbol ($): {{amount | currency}}<br/>
+ custom currency identifier (USD$): {{amount | currency:"USD$"}}
+ </div>
</doc:source>
<doc:scenario>
it('should init with 1234.56', function(){
@@ -93,10 +100,17 @@ angularFilter.currency = function(amount, currencySymbol){
* @example
<doc:example>
<doc:source>
- Enter number: <input name='val' value='1234.56789' /><br/>
- Default formatting: {{val | number}}<br/>
- No fractions: {{val | number:0}}<br/>
- Negative number: {{-val | number:4}}
+ <script>
+ function Ctrl(){
+ this.val = 1234.56789;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Enter number: <input ng:model='val'><br/>
+ Default formatting: {{val | number}}<br/>
+ No fractions: {{val | number:0}}<br/>
+ Negative number: {{-val | number:4}}
+ </div>
</doc:source>
<doc:scenario>
it('should format numbers', function(){
@@ -462,36 +476,43 @@ angularFilter.uppercase = uppercase;
* @example
<doc:example>
<doc:source>
- Snippet: <textarea name="snippet" cols="60" rows="3">
- &lt;p style="color:blue"&gt;an html
- &lt;em onmouseover="this.textContent='PWN3D!'"&gt;click here&lt;/em&gt;
- snippet&lt;/p&gt;</textarea>
- <table>
- <tr>
- <td>Filter</td>
- <td>Source</td>
- <td>Rendered</td>
- </tr>
- <tr id="html-filter">
- <td>html filter</td>
- <td>
- <pre>&lt;div ng:bind="snippet | html"&gt;<br/>&lt;/div&gt;</pre>
- </td>
- <td>
- <div ng:bind="snippet | html"></div>
- </td>
- </tr>
- <tr id="escaped-html">
- <td>no filter</td>
- <td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
- <td><div ng:bind="snippet"></div></td>
- </tr>
- <tr id="html-unsafe-filter">
- <td>unsafe html filter</td>
- <td><pre>&lt;div ng:bind="snippet | html:'unsafe'"&gt;<br/>&lt;/div&gt;</pre></td>
- <td><div ng:bind="snippet | html:'unsafe'"></div></td>
- </tr>
- </table>
+ <script>
+ function Ctrl(){
+ this.snippet =
+ '<p style="color:blue">an html\n' +
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
+ 'snippet</p>';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
+ <table>
+ <tr>
+ <td>Filter</td>
+ <td>Source</td>
+ <td>Rendered</td>
+ </tr>
+ <tr id="html-filter">
+ <td>html filter</td>
+ <td>
+ <pre>&lt;div ng:bind="snippet | html"&gt;<br/>&lt;/div&gt;</pre>
+ </td>
+ <td>
+ <div ng:bind="snippet | html"></div>
+ </td>
+ </tr>
+ <tr id="escaped-html">
+ <td>no filter</td>
+ <td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
+ <td><div ng:bind="snippet"></div></td>
+ </tr>
+ <tr id="html-unsafe-filter">
+ <td>unsafe html filter</td>
+ <td><pre>&lt;div ng:bind="snippet | html:'unsafe'"&gt;<br/>&lt;/div&gt;</pre></td>
+ <td><div ng:bind="snippet | html:'unsafe'"></div></td>
+ </tr>
+ </table>
+ </div>
</doc:source>
<doc:scenario>
it('should sanitize the html snippet ', function(){
@@ -543,12 +564,18 @@ angularFilter.html = function(html, option){
* @example
<doc:example>
<doc:source>
- Snippet: <textarea name="snippet" cols="60" rows="3">
- Pretty text with some links:
- http://angularjs.org/,
- mailto:us@somewhere.org,
- another@somewhere.org,
- and one more: ftp://127.0.0.1/.</textarea>
+ <script>
+ function Ctrl(){
+ this.snippet =
+ 'Pretty text with some links:\n'+
+ 'http://angularjs.org/,\n'+
+ 'mailto:us@somewhere.org,\n'+
+ 'another@somewhere.org,\n'+
+ 'and one more: ftp://127.0.0.1/.';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
<td>Filter</td>
diff --git a/src/formatters.js b/src/formatters.js
deleted file mode 100644
index 2fadc9d7..00000000
--- a/src/formatters.js
+++ /dev/null
@@ -1,202 +0,0 @@
-'use strict';
-
-/**
- * @workInProgress
- * @ngdoc overview
- * @name angular.formatter
- * @description
- *
- * Formatters are used for translating data formats between those used for display and those used
- * for storage.
- *
- * Following is the list of built-in angular formatters:
- *
- * * {@link angular.formatter.boolean boolean} - Formats user input in boolean format
- * * {@link angular.formatter.json json} - Formats user input in JSON format
- * * {@link angular.formatter.list list} - Formats user input string as an array
- * * {@link angular.formatter.number number} - Formats user input strings as a number
- * * {@link angular.formatter.trim trim} - Trims extras spaces from end of user input
- *
- * For more information about how angular formatters work, and how to create your own formatters,
- * see {@link guide/dev_guide.templates.formatters Understanding Angular Formatters} in the angular
- * Developer Guide.
- */
-
-function formatter(format, parse) {return {'format':format, 'parse':parse || format};}
-function toString(obj) {
- return (isDefined(obj) && obj !== null) ? "" + obj : obj;
-}
-
-var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/;
-
-angularFormatter.noop = formatter(identity, identity);
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.json
- *
- * @description
- * Formats the user input as JSON text.
- *
- * @returns {?string} A JSON string representation of the model.
- *
- * @example
- <doc:example>
- <doc:source>
- <div ng:init="data={name:'misko', project:'angular'}">
- <input type="text" size='50' name="data" ng:format="json"/>
- <pre>data={{data}}</pre>
- </div>
- </doc:source>
- <doc:scenario>
- it('should format json', function(){
- expect(binding('data')).toEqual('data={\n \"name\":\"misko\",\n \"project\":\"angular\"}');
- input('data').enter('{}');
- expect(binding('data')).toEqual('data={\n }');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter.json = formatter(toJson, function(value){
- return fromJson(value || 'null');
-});
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.boolean
- *
- * @description
- * Use boolean formatter if you wish to store the data as boolean.
- *
- * @returns {boolean} Converts to `true` unless user enters (blank), `f`, `false`, `0`, `no`, `[]`.
- *
- * @example
- <doc:example>
- <doc:source>
- Enter truthy text:
- <input type="text" name="value" ng:format="boolean" value="no"/>
- <input type="checkbox" name="value"/>
- <pre>value={{value}}</pre>
- </doc:source>
- <doc:scenario>
- it('should format boolean', function(){
- expect(binding('value')).toEqual('value=false');
- input('value').enter('truthy');
- expect(binding('value')).toEqual('value=true');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter['boolean'] = formatter(toString, toBoolean);
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.number
- *
- * @description
- * Use number formatter if you wish to convert the user entered string to a number.
- *
- * @returns {number} Number from the parsed string.
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid number:
- <input type="text" name="value" ng:format="number" value="1234"/>
- <pre>value={{value}}</pre>
- </doc:source>
- <doc:scenario>
- it('should format numbers', function(){
- expect(binding('value')).toEqual('value=1234');
- input('value').enter('5678');
- expect(binding('value')).toEqual('value=5678');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter.number = formatter(toString, function(obj){
- if (obj == null || NUMBER.exec(obj)) {
- return obj===null || obj === '' ? null : 1*obj;
- } else {
- throw "Not a number";
- }
-});
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.list
- *
- * @description
- * Use list formatter if you wish to convert the user entered string to an array.
- *
- * @returns {Array} Array parsed from the entered string.
- *
- * @example
- <doc:example>
- <doc:source>
- Enter a list of items:
- <input type="text" name="value" ng:format="list" value=" chair ,, table"/>
- <input type="text" name="value" ng:format="list"/>
- <pre>value={{value}}</pre>
- </doc:source>
- <doc:scenario>
- it('should format lists', function(){
- expect(binding('value')).toEqual('value=["chair","table"]');
- this.addFutureAction('change to XYZ', function($window, $document, done){
- $document.elements('.doc-example-live :input:last').val(',,a,b,').trigger('change');
- done();
- });
- expect(binding('value')).toEqual('value=["a","b"]');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter.list = formatter(
- function(obj) { return obj ? obj.join(", ") : obj; },
- function(value) {
- var list = [];
- forEach((value || '').split(','), function(item){
- item = trim(item);
- if (item) list.push(item);
- });
- return list;
- }
-);
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.trim
- *
- * @description
- * Use trim formatter if you wish to trim extra spaces in user text.
- *
- * @returns {String} Trim excess leading and trailing space.
- *
- * @example
- <doc:example>
- <doc:source>
- Enter text with leading/trailing spaces:
- <input type="text" name="value" ng:format="trim" value=" book "/>
- <input type="text" name="value" ng:format="trim"/>
- <pre>value={{value|json}}</pre>
- </doc:source>
- <doc:scenario>
- it('should format trim', function(){
- expect(binding('value')).toEqual('value="book"');
- this.addFutureAction('change to XYZ', function($window, $document, done){
- $document.elements('.doc-example-live :input:last').val(' text ').trigger('change');
- done();
- });
- expect(binding('value')).toEqual('value="text"');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter.trim = formatter(
- function(obj) { return obj ? trim("" + obj) : ""; }
-);
diff --git a/src/jqLite.js b/src/jqLite.js
index 5f761f92..0052055c 100644
--- a/src/jqLite.js
+++ b/src/jqLite.js
@@ -100,6 +100,10 @@ function camelCase(name) {
/////////////////////////////////////////////
// jQuery mutation patch
+//
+// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a
+// $destroy event on all DOM nodes being removed.
+//
/////////////////////////////////////////////
function JQLitePatchJQueryRemove(name, dispatchThis) {
@@ -129,7 +133,9 @@ function JQLitePatchJQueryRemove(name, dispatchThis) {
} else {
fireEvent = !fireEvent;
}
- for(childIndex = 0, childLength = (children = element.children()).length; childIndex < childLength; childIndex++) {
+ for(childIndex = 0, childLength = (children = element.children()).length;
+ childIndex < childLength;
+ childIndex++) {
list.push(jQuery(children[childIndex]));
}
}
@@ -283,7 +289,10 @@ var JQLitePrototype = JQLite.prototype = {
// these functions return self on setter and
// value on get.
//////////////////////////////////////////
-var SPECIAL_ATTR = makeMap("multiple,selected,checked,disabled,readonly,required");
+var BOOLEAN_ATTR = {};
+forEach('multiple,selected,checked,disabled,readOnly,required'.split(','), function(value, key) {
+ BOOLEAN_ATTR[lowercase(value)] = value;
+});
forEach({
data: JQLiteData,
@@ -331,7 +340,7 @@ forEach({
},
attr: function(element, name, value){
- if (SPECIAL_ATTR[name]) {
+ if (BOOLEAN_ATTR[name]) {
if (isDefined(value)) {
if (!!value) {
element[name] = true;
diff --git a/src/markups.js b/src/markups.js
index 1adad3e0..40f4322b 100644
--- a/src/markups.js
+++ b/src/markups.js
@@ -163,7 +163,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* This example uses `link` variable inside `href` attribute:
<doc:example>
<doc:source>
- <input name="value" /><br />
+ <input ng:model="value" /><br />
<a id="link-1" href ng:click="value = 1">link 1</a> (link, don't reload)<br />
<a id="link-2" href="" ng:click="value = 2">link 2</a> (link, don't reload)<br />
<a id="link-3" ng:href="/{{'123'}}" ng:ext-link>link 3</a> (link, reload!)<br />
@@ -262,8 +262,8 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Click me to toggle: <input type="checkbox" name="checked"><br/>
- <button name="button" ng:disabled="{{checked}}">Button</button>
+ Click me to toggle: <input type="checkbox" ng:model="checked"><br/>
+ <button ng:model="button" ng:disabled="{{checked}}">Button</button>
</doc:source>
<doc:scenario>
it('should toggle button', function() {
@@ -292,7 +292,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Check me to check both: <input type="checkbox" name="master"><br/>
+ Check me to check both: <input type="checkbox" ng:model="master"><br/>
<input id="checkSlave" type="checkbox" ng:checked="{{master}}">
</doc:source>
<doc:scenario>
@@ -323,7 +323,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Check me check multiple: <input type="checkbox" name="checked"><br/>
+ Check me check multiple: <input type="checkbox" ng:model="checked"><br/>
<select id="select" ng:multiple="{{checked}}">
<option>Misko</option>
<option>Igor</option>
@@ -358,7 +358,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Check me to make text readonly: <input type="checkbox" name="checked"><br/>
+ Check me to make text readonly: <input type="checkbox" ng:model="checked"><br/>
<input type="text" ng:readonly="{{checked}}" value="I'm Angular"/>
</doc:source>
<doc:scenario>
@@ -388,7 +388,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Check me to select: <input type="checkbox" name="checked"><br/>
+ Check me to select: <input type="checkbox" ng:model="checked"><br/>
<select>
<option>Hello!</option>
<option id="greet" ng:selected="{{checked}}">Greetings!</option>
@@ -408,10 +408,10 @@ angularTextMarkup('option', function(text, textNode, parentElement){
var NG_BIND_ATTR = 'ng:bind-attr';
-var SPECIAL_ATTRS = {};
+var SIDE_EFFECT_ATTRS = {};
-forEach('src,href,checked,disabled,multiple,readonly,selected'.split(','), function(name) {
- SPECIAL_ATTRS['ng:' + name] = name;
+forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) {
+ SIDE_EFFECT_ATTRS['ng:' + name] = name;
});
angularAttrMarkup('{{}}', function(value, name, element){
@@ -421,10 +421,10 @@ angularAttrMarkup('{{}}', function(value, name, element){
value = decodeURI(value);
var bindings = parseBindings(value),
bindAttr;
- if (hasBindings(bindings) || SPECIAL_ATTRS[name]) {
+ if (hasBindings(bindings) || SIDE_EFFECT_ATTRS[name]) {
element.removeAttr(name);
bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}");
- bindAttr[SPECIAL_ATTRS[name] || name] = value;
+ bindAttr[SIDE_EFFECT_ATTRS[name] || name] = value;
element.attr(NG_BIND_ATTR, toJson(bindAttr));
}
});
diff --git a/src/parser.js b/src/parser.js
index f8978a0b..4934b9e6 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -247,8 +247,6 @@ function parser(text, json){
assignable: assertConsumed(assignable),
primary: assertConsumed(primary),
statements: assertConsumed(statements),
- validator: assertConsumed(validator),
- formatter: assertConsumed(formatter),
filter: assertConsumed(filter)
};
@@ -361,36 +359,6 @@ function parser(text, json){
return pipeFunction(angularFilter);
}
- function validator(){
- return pipeFunction(angularValidator);
- }
-
- function formatter(){
- var token = expect();
- var formatter = angularFormatter[token.text];
- var argFns = [];
- if (!formatter) throwError('is not a valid formatter.', token);
- while(true) {
- if ((token = expect(':'))) {
- argFns.push(expression());
- } else {
- return valueFn({
- format:invokeFn(formatter.format),
- parse:invokeFn(formatter.parse)
- });
- }
- }
- function invokeFn(fn){
- return function(self, input){
- var args = [input];
- for ( var i = 0; i < argFns.length; i++) {
- args.push(argFns[i](self));
- }
- return fn.apply(self, args);
- };
- }
- }
-
function _pipeFunction(fnScope){
var fn = functionIdent(fnScope);
var argsFn = [];
@@ -735,16 +703,19 @@ function getterFn(path) {
code += 'if(!s) return s;\n' +
'l=s;\n' +
's=s' + key + ';\n' +
- 'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l' +
- key + '.apply(l, arguments); };\n';
+ 'if(typeof s=="function" && !(s instanceof RegExp)) {\n' +
+ ' fn=function(){ return l' + key + '.apply(l, arguments); };\n' +
+ ' fn.$unboundFn=s;\n' +
+ ' s=fn;\n' +
+ '}\n';
if (key.charAt(1) == '$') {
// special code for super-imposed functions
var name = key.substr(2);
code += 'if(!s) {\n' +
' t = angular.Global.typeOf(l);\n' +
' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' +
- ' if (fn) s = function(){ return fn.apply(l, ' +
- '[l].concat(Array.prototype.slice.call(arguments, 0))); };\n' +
+ ' if (fn) ' +
+ 's = function(){ return fn.apply(l, [l].concat(Array.prototype.slice.call(arguments, 0))); };\n' +
'}\n';
}
});
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
index 5ee7bde4..53be6c67 100644
--- a/src/scenario/Scenario.js
+++ b/src/scenario/Scenario.js
@@ -247,7 +247,7 @@ function browserTrigger(element, type) {
'radio': 'click',
'select-one': 'change',
'select-multiple': 'change'
- }[element.type] || 'click';
+ }[lowercase(element.type)] || 'click';
}
if (lowercase(nodeName_(element)) == 'option') {
element.parentNode.value = element.value;
diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js
index 8bc4030e..e0af0c8c 100644
--- a/src/scenario/dsl.js
+++ b/src/scenario/dsl.js
@@ -180,7 +180,7 @@ angular.scenario.dsl('input', function() {
chain.enter = function(value) {
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function($window, $document, done) {
- var input = $document.elements(':input[name="$1"]', this.name);
+ var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
input.val(value);
input.trigger('keydown');
done();
@@ -189,7 +189,7 @@ angular.scenario.dsl('input', function() {
chain.check = function() {
return this.addFutureAction("checkbox '" + this.name + "' toggle", function($window, $document, done) {
- var input = $document.elements(':checkbox[name="$1"]', this.name);
+ var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':checkbox');
input.trigger('click');
done();
});
@@ -198,7 +198,7 @@ angular.scenario.dsl('input', function() {
chain.select = function(value) {
return this.addFutureAction("radio button '" + this.name + "' toggle '" + value + "'", function($window, $document, done) {
var input = $document.
- elements(':radio[name$="@$1"][value="$2"]', this.name, value);
+ elements('[ng\\:model="$1"][value="$2"]', this.name, value).filter(':radio');
input.trigger('click');
done();
});
@@ -206,7 +206,7 @@ angular.scenario.dsl('input', function() {
chain.val = function() {
return this.addFutureAction("return input val", function($window, $document, done) {
- var input = $document.elements(':input[name="$1"]', this.name);
+ var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
done(null,input.val());
});
};
@@ -268,8 +268,16 @@ angular.scenario.dsl('select', function() {
chain.option = function(value) {
return this.addFutureAction("select '" + this.name + "' option '" + value + "'", function($window, $document, done) {
- var select = $document.elements('select[name="$1"]', this.name);
- select.val(value);
+ var select = $document.elements('select[ng\\:model="$1"]', this.name);
+ var option = select.find('option[value="' + value + '"]');
+ if (option.length) {
+ select.val(value);
+ } else {
+ option = select.find('option:contains("' + value + '")');
+ if (option.length) {
+ select.val(option.val());
+ }
+ }
select.trigger('change');
done();
});
@@ -278,7 +286,7 @@ angular.scenario.dsl('select', function() {
chain.options = function() {
var values = arguments;
return this.addFutureAction("select '" + this.name + "' options '" + values + "'", function($window, $document, done) {
- var select = $document.elements('select[multiple][name="$1"]', this.name);
+ var select = $document.elements('select[multiple][ng\\:model="$1"]', this.name);
select.val(values);
select.trigger('change');
done();
diff --git a/src/service/formFactory.js b/src/service/formFactory.js
new file mode 100644
index 00000000..4fc53935
--- /dev/null
+++ b/src/service/formFactory.js
@@ -0,0 +1,394 @@
+'use strict';
+
+/**
+ * @ngdoc service
+ * @name angular.service.$formFactory
+ *
+ * @description
+ * Use `$formFactory` to create a new instance of a {@link guide/dev_guide.forms form}
+ * controller or to find the nearest form instance for a given DOM element.
+ *
+ * The form instance is a collection of widgets, and is responsible for life cycle and validation
+ * of widget.
+ *
+ * Keep in mind that both form and widget instances are {@link api/angular.scope scopes}.
+ *
+ * @param {Form=} parentForm The form which should be the parent form of the new form controller.
+ * If none specified default to the `rootForm`.
+ * @returns {Form} A new <a href="#form">form</a> instance.
+ *
+ * @example
+ *
+ * This example shows how one could write a widget which would enable data-binding on
+ * `contenteditable` feature of HTML.
+ *
+ <doc:example>
+ <doc:source>
+ <script>
+ function EditorCntl(){
+ this.html = '<b>Hello</b> <i>World</i>!';
+ }
+
+ function HTMLEditorWidget(element) {
+ var self = this;
+ var htmlFilter = angular.filter('html');
+
+ this.$parseModel = function(){
+ // need to protect for script injection
+ try {
+ this.$viewValue = htmlFilter(this.$modelValue || '').get();
+ if (this.$error.HTML) {
+ // we were invalid, but now we are OK.
+ this.$emit('$valid', 'HTML');
+ }
+ } catch (e) {
+ // if HTML not parsable invalidate form.
+ this.$emit('$invalid', 'HTML');
+ }
+ }
+
+ this.$render = function(){
+ element.html(this.$viewValue);
+ }
+
+ element.bind('keyup', function(){
+ self.$apply(function(){
+ self.$emit('$viewChange', element.html());
+ });
+ });
+ }
+
+ angular.directive('ng:contenteditable', function(){
+ function linkFn($formFactory, element) {
+ var exp = element.attr('ng:contenteditable'),
+ form = $formFactory.forElement(element),
+ widget;
+ element.attr('contentEditable', true);
+ widget = form.$createWidget({
+ scope: this,
+ model: exp,
+ controller: HTMLEditorWidget,
+ controllerArgs: [element]});
+ // if the element is destroyed, then we need to notify the form.
+ element.bind('$destroy', function(){
+ widget.$destroy();
+ });
+ }
+ linkFn.$inject = ['$formFactory'];
+ return linkFn;
+ });
+ </script>
+ <form name='editorForm' ng:controller="EditorCntl">
+ <div ng:contenteditable="html"></div>
+ <hr/>
+ HTML: <br/>
+ <textarea ng:model="html" cols=80></textarea>
+ <hr/>
+ <pre>editorForm = {{editorForm}}</pre>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should enter invalid HTML', function(){
+ expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
+ input('html').enter('<');
+ expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularServiceInject('$formFactory', function(){
+
+
+ /**
+ * @ngdoc proprety
+ * @name rootForm
+ * @propertyOf angular.service.$formFactory
+ * @description
+ * Static property on `$formFactory`
+ *
+ * Each application ({@link guide/dev_guide.scopes.internals root scope}) gets a root form which
+ * is the top-level parent of all forms.
+ */
+ formFactory.rootForm = formFactory(this);
+
+
+ /**
+ * @ngdoc method
+ * @name forElement
+ * @methodOf angular.service.$formFactory
+ * @description
+ * Static method on `$formFactory` service.
+ *
+ * Retrieve the closest form for a given element or defaults to the `root` form. Used by the
+ * {@link angular.widget.form form} element.
+ * @param {Element} element The element where the search for form should initiate.
+ */
+ formFactory.forElement = function (element) {
+ return element.inheritedData('$form') || formFactory.rootForm;
+ };
+ return formFactory;
+
+ function formFactory(parent) {
+ return (parent || formFactory.rootForm).$new(FormController);
+ }
+
+});
+
+function propertiesUpdate(widget) {
+ widget.$valid = !(widget.$invalid =
+ !(widget.$readonly || widget.$disabled || equals(widget.$error, {})));
+}
+
+/**
+ * @ngdoc property
+ * @name $error
+ * @propertyOf angular.service.$formFactory
+ * @description
+ * Property of the form and widget instance.
+ *
+ * Summary of all of the errors on the page. If a widget emits `$invalid` with `REQUIRED` key,
+ * then the `$error` object will have a `REQUIRED` key with an array of widgets which have
+ * emitted this key. `form.$error.REQUIRED == [ widget ]`.
+ */
+
+/**
+ * @workInProgress
+ * @ngdoc property
+ * @name $invalid
+ * @propertyOf angular.service.$formFactory
+ * @description
+ * Property of the form and widget instance.
+ *
+ * True if any of the widgets of the form are invalid.
+ */
+
+/**
+ * @workInProgress
+ * @ngdoc property
+ * @name $valid
+ * @propertyOf angular.service.$formFactory
+ * @description
+ * Property of the form and widget instance.
+ *
+ * True if all of the widgets of the form are valid.
+ */
+
+/**
+ * @ngdoc event
+ * @name angular.service.$formFactory#$valid
+ * @eventOf angular.service.$formFactory
+ * @eventType listen on form
+ * @description
+ * Upon receiving the `$valid` event from the widget update the `$error`, `$valid` and `$invalid`
+ * properties of both the widget as well as the from.
+ *
+ * @param {String} validationKey The validation key to be used when updating the `$error` object.
+ * The validation key is what will allow the template to bind to a specific validation error
+ * such as `<div ng:show="form.$error.KEY">error for key</div>`.
+ */
+
+/**
+ * @ngdoc event
+ * @name angular.service.$formFactory#$invalid
+ * @eventOf angular.service.$formFactory
+ * @eventType listen on form
+ * @description
+ * Upon receiving the `$invalid` event from the widget update the `$error`, `$valid` and `$invalid`
+ * properties of both the widget as well as the from.
+ *
+ * @param {String} validationKey The validation key to be used when updating the `$error` object.
+ * The validation key is what will allow the template to bind to a specific validation error
+ * such as `<div ng:show="form.$error.KEY">error for key</div>`.
+ */
+
+/**
+ * @ngdoc event
+ * @name angular.service.$formFactory#$validate
+ * @eventOf angular.service.$formFactory
+ * @eventType emit on widget
+ * @description
+ * Emit the `$validate` event on the widget, giving a widget a chance to emit a
+ * `$valid` / `$invalid` event base on its state. The `$validate` event is triggered when the
+ * model or the view changes.
+ */
+
+/**
+ * @ngdoc event
+ * @name angular.service.$formFactory#$viewChange
+ * @eventOf angular.service.$formFactory
+ * @eventType listen on widget
+ * @description
+ * A widget is responsible for emitting this event whenever the view changes do to user interaction.
+ * The event takes a `$viewValue` parameter, which is the new value of the view. This
+ * event triggers a call to `$parseView()` as well as `$validate` event on widget.
+ *
+ * @param {*} viewValue The new value for the view which will be assigned to `widget.$viewValue`.
+ */
+
+function FormController(){
+ var form = this,
+ $error = form.$error = {};
+
+ form.$on('$destroy', function(event){
+ var widget = event.targetScope;
+ if (widget.$widgetId) {
+ delete form[widget.$widgetId];
+ }
+ forEach($error, removeWidget, widget);
+ });
+
+ form.$on('$valid', function(event, error){
+ var widget = event.targetScope;
+ delete widget.$error[error];
+ propertiesUpdate(widget);
+ removeWidget($error[error], error, widget);
+ });
+
+ form.$on('$invalid', function(event, error){
+ var widget = event.targetScope;
+ addWidget(error, widget);
+ widget.$error[error] = true;
+ propertiesUpdate(widget);
+ });
+
+ propertiesUpdate(form);
+
+ function removeWidget(queue, errorKey, widget) {
+ if (queue) {
+ widget = widget || this; // so that we can be used in forEach;
+ for (var i = 0, length = queue.length; i < length; i++) {
+ if (queue[i] === widget) {
+ queue.splice(i, 1);
+ if (!queue.length) {
+ delete $error[errorKey];
+ }
+ }
+ }
+ propertiesUpdate(form);
+ }
+ }
+
+ function addWidget(errorKey, widget) {
+ var queue = $error[errorKey];
+ if (queue) {
+ for (var i = 0, length = queue.length; i < length; i++) {
+ if (queue[i] === widget) {
+ return;
+ }
+ }
+ } else {
+ $error[errorKey] = queue = [];
+ }
+ queue.push(widget);
+ propertiesUpdate(form);
+ }
+}
+
+
+/**
+ * @ngdoc method
+ * @name $createWidget
+ * @methodOf angular.service.$formFactory
+ * @description
+ *
+ * Use form's `$createWidget` instance method to create new widgets. The widgets can be created
+ * using an alias which makes the accessible from the form and available for data-binding,
+ * useful for displaying validation error messages.
+ *
+ * The creation of a widget sets up:
+ *
+ * - `$watch` of `expression` on `model` scope. This code path syncs the model to the view.
+ * The `$watch` listener will:
+ *
+ * - assign the new model value of `expression` to `widget.$modelValue`.
+ * - call `widget.$parseModel` method if present. The `$parseModel` is responsible for copying
+ * the `widget.$modelValue` to `widget.$viewValue` and optionally converting the data.
+ * (For example to convert a number into string)
+ * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid`
+ * event.
+ * - call `widget.$render()` method on widget. The `$render` method is responsible for
+ * reading the `widget.$viewValue` and updating the DOM.
+ *
+ * - Listen on `$viewChange` event from the `widget`. This code path syncs the view to the model.
+ * The `$viewChange` listener will:
+ *
+ * - assign the value to `widget.$viewValue`.
+ * - call `widget.$parseView` method if present. The `$parseView` is responsible for copying
+ * the `widget.$viewValue` to `widget.$modelValue` and optionally converting the data.
+ * (For example to convert a string into number)
+ * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid`
+ * event.
+ * - Assign the `widget.$modelValue` to the `expression` on the `model` scope.
+ *
+ * - Creates these set of properties on the `widget` which are updated as a response to the
+ * `$valid` / `$invalid` events:
+ *
+ * - `$error` - object - validation errors will be published as keys on this object.
+ * Data-binding to this property is useful for displaying the validation errors.
+ * - `$valid` - boolean - true if there are no validation errors
+ * - `$invalid` - boolean - opposite of `$valid`.
+ * @param {Object} params Named parameters:
+ *
+ * - `scope` - `{Scope}` - The scope to which the model for this widget is attached.
+ * - `model` - `{string}` - The name of the model property on model scope.
+ * - `controller` - {WidgetController} - The controller constructor function.
+ * The controller constructor should create these instance methods.
+ * - `$parseView()`: optional method responsible for copying `$viewVale` to `$modelValue`.
+ * The method may fire `$valid`/`$invalid` events.
+ * - `$parseModel()`: optional method responsible for copying `$modelVale` to `$viewValue`.
+ * The method may fire `$valid`/`$invalid` events.
+ * - `$render()`: required method which needs to update the DOM of the widget to match the
+ * `$viewValue`.
+ *
+ * - `controllerArgs` - `{Array}` (Optional) - Any extra arguments will be curried to the
+ * WidgetController constructor.
+ * - `onChange` - `{(string|function())}` (Optional) - Expression to execute when user changes the
+ * value.
+ * - `alias` - `{string}` (Optional) - The name of the form property under which the widget
+ * instance should be published. The name should be unique for each form.
+ * @returns {Widget} Instance of a widget scope.
+ */
+FormController.prototype.$createWidget = function(params) {
+ var form = this,
+ modelScope = params.scope,
+ onChange = params.onChange,
+ alias = params.alias,
+ scopeGet = parser(params.model).assignable(),
+ scopeSet = scopeGet.assign,
+ widget = this.$new(params.controller, params.controllerArgs);
+
+ widget.$error = {};
+ // Set the state to something we know will change to get the process going.
+ widget.$modelValue = Number.NaN;
+ // watch for scope changes and update the view appropriately
+ modelScope.$watch(scopeGet, function (scope, value) {
+ if (!equals(widget.$modelValue, value)) {
+ widget.$modelValue = value;
+ widget.$parseModel ? widget.$parseModel() : (widget.$viewValue = value);
+ widget.$emit('$validate');
+ widget.$render && widget.$render();
+ }
+ });
+
+ widget.$on('$viewChange', function(event, viewValue){
+ if (!equals(widget.$viewValue, viewValue)) {
+ widget.$viewValue = viewValue;
+ widget.$parseView ? widget.$parseView() : (widget.$modelValue = widget.$viewValue);
+ scopeSet(modelScope, widget.$modelValue);
+ if (onChange) modelScope.$eval(onChange);
+ widget.$emit('$validate');
+ }
+ });
+
+ propertiesUpdate(widget);
+
+ // assign the widgetModel to the form
+ if (alias && !form.hasOwnProperty(alias)) {
+ form[alias] = widget;
+ widget.$widgetId = alias;
+ } else {
+ alias = null;
+ }
+
+ return widget;
+};
diff --git a/src/service/invalidWidgets.js b/src/service/invalidWidgets.js
deleted file mode 100644
index 7c1b2a9f..00000000
--- a/src/service/invalidWidgets.js
+++ /dev/null
@@ -1,69 +0,0 @@
-'use strict';
-
-/**
- * @workInProgress
- * @ngdoc service
- * @name angular.service.$invalidWidgets
- *
- * @description
- * Keeps references to all invalid widgets found during validation.
- * Can be queried to find whether there are any invalid widgets currently displayed.
- *
- * @example
- */
-angularServiceInject("$invalidWidgets", function(){
- var invalidWidgets = [];
-
-
- /** Remove an element from the array of invalid widgets */
- invalidWidgets.markValid = function(element){
- var index = indexOf(invalidWidgets, element);
- if (index != -1)
- invalidWidgets.splice(index, 1);
- };
-
-
- /** Add an element to the array of invalid widgets */
- invalidWidgets.markInvalid = function(element){
- var index = indexOf(invalidWidgets, element);
- if (index === -1)
- invalidWidgets.push(element);
- };
-
-
- /** Return count of all invalid widgets that are currently visible */
- invalidWidgets.visible = function() {
- var count = 0;
- forEach(invalidWidgets, function(widget){
- count = count + (isVisible(widget) ? 1 : 0);
- });
- return count;
- };
-
-
- /* At the end of each eval removes all invalid widgets that are not part of the current DOM. */
- this.$watch(function() {
- for(var i = 0; i < invalidWidgets.length;) {
- var widget = invalidWidgets[i];
- if (isOrphan(widget[0])) {
- invalidWidgets.splice(i, 1);
- if (widget.dealoc) widget.dealoc();
- } else {
- i++;
- }
- }
- });
-
-
- /**
- * Traverses DOM element's (widget's) parents and considers the element to be an orphan if one of
- * it's parents isn't the current window.document.
- */
- function isOrphan(widget) {
- if (widget == window.document) return false;
- var parent = widget.parentNode;
- return !parent || isOrphan(parent);
- }
-
- return invalidWidgets;
-});
diff --git a/src/service/log.js b/src/service/log.js
index 09945732..3dacd117 100644
--- a/src/service/log.js
+++ b/src/service/log.js
@@ -18,12 +18,13 @@
<script>
function LogCtrl($log) {
this.$log = $log;
+ this.message = 'Hello World!';
}
</script>
<div ng:controller="LogCtrl">
<p>Reload this page with open console, enter text and hit the log button...</p>
Message:
- <input type="text" name="message" value="Hello World!"/>
+ <input type="text" ng:model="message"/>
<button ng:click="$log.log(message)">log</button>
<button ng:click="$log.warn(message)">warn</button>
<button ng:click="$log.info(message)">info</button>
diff --git a/src/service/resource.js b/src/service/resource.js
index f6e0be18..915f2d92 100644
--- a/src/service/resource.js
+++ b/src/service/resource.js
@@ -160,6 +160,7 @@
<doc:source jsfiddle="false">
<script>
function BuzzController($resource) {
+ this.userId = 'googlebuzz';
this.Activity = $resource(
'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
{alt:'json', callback:'JSON_CALLBACK'},
@@ -179,7 +180,7 @@
</script>
<div ng:controller="BuzzController">
- <input name="userId" value="googlebuzz"/>
+ <input ng:model="userId"/>
<button ng:click="fetch()">fetch</button>
<hr/>
<div ng:repeat="item in activities.data.items">
diff --git a/src/service/route.js b/src/service/route.js
index 73c73b04..b78cca91 100644
--- a/src/service/route.js
+++ b/src/service/route.js
@@ -260,7 +260,8 @@ angularServiceInject('$route', function($location, $routeParams) {
function updateRoute() {
var next = parseRoute(),
- last = $route.current;
+ last = $route.current,
+ Controller;
if (next && last && next.$route === last.$route
&& equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) {
@@ -283,7 +284,8 @@ angularServiceInject('$route', function($location, $routeParams) {
}
} else {
copy(next.params, $routeParams);
- next.scope = parentScope.$new(next.controller);
+ (Controller = next.controller) && inferInjectionArgs(Controller);
+ next.scope = parentScope.$new(Controller);
}
}
rootScope.$broadcast('$afterRouteChange', next, last);
diff --git a/src/service/window.js b/src/service/window.js
index 2f3f677a..9795e4fc 100644
--- a/src/service/window.js
+++ b/src/service/window.js
@@ -17,7 +17,7 @@
* @example
<doc:example>
<doc:source>
- <input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" name="greeting" />
+ <input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" ng:model="greeting" />
<button ng:click="$window.alert(greeting)">ALERT</button>
</doc:source>
<doc:scenario>
diff --git a/src/service/xhr.js b/src/service/xhr.js
index 09e7d070..4981c078 100644
--- a/src/service/xhr.js
+++ b/src/service/xhr.js
@@ -111,6 +111,7 @@
<script>
function FetchCntl($xhr) {
var self = this;
+ this.url = 'index.html';
this.fetch = function() {
self.code = null;
@@ -133,11 +134,11 @@
FetchCntl.$inject = ['$xhr'];
</script>
<div ng:controller="FetchCntl">
- <select name="method">
+ <select ng:model="method">
<option>GET</option>
<option>JSON</option>
</select>
- <input type="text" name="url" value="index.html" size="80"/>
+ <input type="text" ng:model="url" size="80"/>
<button ng:click="fetch()">fetch</button><br>
<button ng:click="updateModel('GET', 'index.html')">Sample GET</button>
<button ng:click="updateModel('JSON', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button>
diff --git a/src/validators.js b/src/validators.js
deleted file mode 100644
index 72a995fc..00000000
--- a/src/validators.js
+++ /dev/null
@@ -1,482 +0,0 @@
-'use strict';
-
-/**
- * @workInProgress
- * @ngdoc overview
- * @name angular.validator
- * @description
- *
- * Most of the built-in angular validators are used to check user input against defined types or
- * patterns. You can easily create your own custom validators as well.
- *
- * Following is the list of built-in angular validators:
- *
- * * {@link angular.validator.asynchronous asynchronous()} - Provides asynchronous validation via a
- * callback function.
- * * {@link angular.validator.date date()} - Checks user input against default date format:
- * "MM/DD/YYYY"
- * * {@link angular.validator.email email()} - Validates that user input is a well-formed email
- * address.
- * * {@link angular.validator.integer integer()} - Validates that user input is an integer
- * * {@link angular.validator.json json()} - Validates that user input is valid JSON
- * * {@link angular.validator.number number()} - Validates that user input is a number
- * * {@link angular.validator.phone phone()} - Validates that user input matches the pattern
- * "1(123)123-1234"
- * * {@link angular.validator.regexp regexp()} - Restricts valid input to a specified regular
- * expression pattern
- * * {@link angular.validator.url url()} - Validates that user input is a well-formed URL.
- *
- * For more information about how angular validators work, and how to create your own validators,
- * see {@link guide/dev_guide.templates.validators Understanding Angular Validators} in the angular
- * Developer Guide.
- */
-
-extend(angularValidator, {
- 'noop': function() { return null; },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.regexp
- * @description
- * Use regexp validator to restrict the input to any Regular Expression.
- *
- * @param {string} value value to validate
- * @param {string|regexp} expression regular expression.
- * @param {string=} msg error message to display.
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- <script> function Cntl(){
- this.ssnRegExp = /^\d\d\d-\d\d-\d\d\d\d$/;
- }
- </script>
- Enter valid SSN:
- <div ng:controller="Cntl">
- <input name="ssn" value="123-45-6789" ng:validate="regexp:ssnRegExp" >
- </div>
- </doc:source>
- <doc:scenario>
- it('should invalidate non ssn', function(){
- var textBox = element('.doc-example-live :input');
- expect(textBox.prop('className')).not().toMatch(/ng-validation-error/);
- expect(textBox.val()).toEqual('123-45-6789');
- input('ssn').enter('123-45-67890');
- expect(textBox.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'regexp': function(value, regexp, msg) {
- if (!value.match(regexp)) {
- return msg ||
- "Value does not match expected format " + regexp + ".";
- } else {
- return null;
- }
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.number
- * @description
- * Use number validator to restrict the input to numbers with an
- * optional range. (See integer for whole numbers validator).
- *
- * @param {string} value value to validate
- * @param {int=} [min=MIN_INT] minimum value.
- * @param {int=} [max=MAX_INT] maximum value.
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter number: <input name="n1" ng:validate="number" > <br>
- Enter number greater than 10: <input name="n2" ng:validate="number:10" > <br>
- Enter number between 100 and 200: <input name="n3" ng:validate="number:100:200" > <br>
- </doc:source>
- <doc:scenario>
- it('should invalidate number', function(){
- var n1 = element('.doc-example-live :input[name=n1]');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('n1').enter('1.x');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- var n2 = element('.doc-example-live :input[name=n2]');
- expect(n2.prop('className')).not().toMatch(/ng-validation-error/);
- input('n2').enter('9');
- expect(n2.prop('className')).toMatch(/ng-validation-error/);
- var n3 = element('.doc-example-live :input[name=n3]');
- expect(n3.prop('className')).not().toMatch(/ng-validation-error/);
- input('n3').enter('201');
- expect(n3.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'number': function(value, min, max) {
- var num = 1 * value;
- if (num == value) {
- if (typeof min != $undefined && num < min) {
- return "Value can not be less than " + min + ".";
- }
- if (typeof min != $undefined && num > max) {
- return "Value can not be greater than " + max + ".";
- }
- return null;
- } else {
- return "Not a number";
- }
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.integer
- * @description
- * Use number validator to restrict the input to integers with an
- * optional range. (See integer for whole numbers validator).
- *
- * @param {string} value value to validate
- * @param {int=} [min=MIN_INT] minimum value.
- * @param {int=} [max=MAX_INT] maximum value.
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter integer: <input name="n1" ng:validate="integer" > <br>
- Enter integer equal or greater than 10: <input name="n2" ng:validate="integer:10" > <br>
- Enter integer between 100 and 200 (inclusive): <input name="n3" ng:validate="integer:100:200" > <br>
- </doc:source>
- <doc:scenario>
- it('should invalidate integer', function(){
- var n1 = element('.doc-example-live :input[name=n1]');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('n1').enter('1.1');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- var n2 = element('.doc-example-live :input[name=n2]');
- expect(n2.prop('className')).not().toMatch(/ng-validation-error/);
- input('n2').enter('10.1');
- expect(n2.prop('className')).toMatch(/ng-validation-error/);
- var n3 = element('.doc-example-live :input[name=n3]');
- expect(n3.prop('className')).not().toMatch(/ng-validation-error/);
- input('n3').enter('100.1');
- expect(n3.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- */
- 'integer': function(value, min, max) {
- var numberError = angularValidator['number'](value, min, max);
- if (numberError) return numberError;
- if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) {
- return "Not a whole number";
- }
- return null;
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.date
- * @description
- * Use date validator to restrict the user input to a valid date
- * in format in format MM/DD/YYYY.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid date:
- <input name="text" value="1/1/2009" ng:validate="date" >
- </doc:source>
- <doc:scenario>
- it('should invalidate date', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('123/123/123');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'date': function(value) {
- var fields = /^(\d\d?)\/(\d\d?)\/(\d\d\d\d)$/.exec(value);
- var date = fields ? new Date(fields[3], fields[1]-1, fields[2]) : 0;
- return (date &&
- date.getFullYear() == fields[3] &&
- date.getMonth() == fields[1]-1 &&
- date.getDate() == fields[2])
- ? null
- : "Value is not a date. (Expecting format: 12/31/2009).";
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.email
- * @description
- * Use email validator if you wist to restrict the user input to a valid email.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid email:
- <input name="text" ng:validate="email" value="me@example.com">
- </doc:source>
- <doc:scenario>
- it('should invalidate email', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('a@b.c');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'email': function(value) {
- if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) {
- return null;
- }
- return "Email needs to be in username@host.com format.";
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.phone
- * @description
- * Use phone validator to restrict the input phone numbers.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid phone number:
- <input name="text" value="1(234)567-8901" ng:validate="phone" >
- </doc:source>
- <doc:scenario>
- it('should invalidate phone', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('+12345678');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'phone': function(value) {
- if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) {
- return null;
- }
- if (value.match(/^\+\d{2,3} (\(\d{1,5}\))?[\d ]+\d$/)) {
- return null;
- }
- return "Phone number needs to be in 1(987)654-3210 format in North America " +
- "or +999 (123) 45678 906 internationally.";
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.url
- * @description
- * Use phone validator to restrict the input URLs.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid URL:
- <input name="text" value="http://example.com/abc.html" size="40" ng:validate="url" >
- </doc:source>
- <doc:scenario>
- it('should invalidate url', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('abc://server/path');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'url': function(value) {
- if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) {
- return null;
- }
- return "URL needs to be in http://server[:port]/path format.";
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.json
- * @description
- * Use json validator if you wish to restrict the user input to a valid JSON.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- <textarea name="json" cols="60" rows="5" ng:validate="json">
- {name:'abc'}
- </textarea>
- </doc:source>
- <doc:scenario>
- it('should invalidate json', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('json').enter('{name}');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'json': function(value) {
- try {
- fromJson(value);
- return null;
- } catch (e) {
- return e.toString();
- }
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.asynchronous
- * @description
- * Use asynchronous validator if the validation can not be computed
- * immediately, but is provided through a callback. The widget
- * automatically shows a spinning indicator while the validity of
- * the widget is computed. This validator caches the result.
- *
- * @param {string} value value to validate
- * @param {function(inputToValidate,validationDone)} validate function to call to validate the state
- * of the input.
- * @param {function(data)=} [update=noop] function to call when state of the
- * validator changes
- *
- * @paramDescription
- * The `validate` function (specified by you) is called as
- * `validate(inputToValidate, validationDone)`:
- *
- * * `inputToValidate`: value of the input box.
- * * `validationDone`: `function(error, data){...}`
- * * `error`: error text to display if validation fails
- * * `data`: data object to pass to update function
- *
- * The `update` function is optionally specified by you and is
- * called by <angular/> on input change. Since the
- * asynchronous validator caches the results, the update
- * function can be called without a call to `validate`
- * function. The function is called as `update(data)`:
- *
- * * `data`: data object as passed from validate function
- *
- * @css ng-input-indicator-wait, ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- <script>
- function MyCntl(){
- this.myValidator = function (inputToValidate, validationDone) {
- setTimeout(function(){
- validationDone(inputToValidate.length % 2);
- }, 500);
- }
- }
- </script>
- This input is validated asynchronously:
- <div ng:controller="MyCntl">
- <input name="text" ng:validate="asynchronous:myValidator">
- </div>
- </doc:source>
- <doc:scenario>
- it('should change color in delayed way', function(){
- var textBox = element('.doc-example-live :input');
- expect(textBox.prop('className')).not().toMatch(/ng-input-indicator-wait/);
- expect(textBox.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('X');
- expect(textBox.prop('className')).toMatch(/ng-input-indicator-wait/);
- sleep(.6);
- expect(textBox.prop('className')).not().toMatch(/ng-input-indicator-wait/);
- expect(textBox.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- /*
- * cache is attached to the element
- * cache: {
- * inputs : {
- * 'user input': {
- * response: server response,
- * error: validation error
- * },
- * current: 'current input'
- * }
- * }
- *
- */
- 'asynchronous': function(input, asynchronousFn, updateFn) {
- if (!input) return;
- var scope = this;
- var element = scope.$element;
- var cache = element.data('$asyncValidator');
- if (!cache) {
- element.data('$asyncValidator', cache = {inputs:{}});
- }
-
- cache.current = input;
-
- var inputState = cache.inputs[input],
- $invalidWidgets = scope.$service('$invalidWidgets');
-
- if (!inputState) {
- cache.inputs[input] = inputState = { inFlight: true };
- $invalidWidgets.markInvalid(scope.$element);
- element.addClass('ng-input-indicator-wait');
- asynchronousFn(input, function(error, data) {
- inputState.response = data;
- inputState.error = error;
- inputState.inFlight = false;
- if (cache.current == input) {
- element.removeClass('ng-input-indicator-wait');
- $invalidWidgets.markValid(element);
- }
- element.data($$validate)();
- });
- } else if (inputState.inFlight) {
- // request in flight, mark widget invalid, but don't show it to user
- $invalidWidgets.markInvalid(scope.$element);
- } else {
- (updateFn||noop)(inputState.response);
- }
- return inputState.error;
- }
-
-});
diff --git a/src/widget/form.js b/src/widget/form.js
new file mode 100644
index 00000000..bc34bf0d
--- /dev/null
+++ b/src/widget/form.js
@@ -0,0 +1,81 @@
+'use strict';
+
+/**
+ * @workInProgress
+ * @ngdoc widget
+ * @name angular.widget.form
+ *
+ * @description
+ * Angular widget that creates a form scope using the
+ * {@link angular.service.$formFactory $formFactory} API. The resulting form scope instance is
+ * attached to the DOM element using the jQuery `.data()` method under the `$form` key.
+ * See {@link guide/dev_guide.forms forms} on detailed discussion of forms and widgets.
+ *
+ *
+ * # Alias: `ng:form`
+ *
+ * In angular forms can be nested. This means that the outer form is valid when all of the child
+ * forms are valid as well. However browsers do not allow nesting of `<form>` elements, for this
+ * reason angular provides `<ng:form>` alias which behaves identical to `<form>` but allows
+ * element nesting.
+ *
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'guest';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ text: <input type="text" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.text.$error.REQUIRED">Required!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function(){
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function(){
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularWidget('form', function(form){
+ this.descend(true);
+ this.directives(true);
+ return annotate('$formFactory', function($formFactory, formElement) {
+ var name = formElement.attr('name'),
+ parentForm = $formFactory.forElement(formElement),
+ form = $formFactory(parentForm);
+ formElement.data('$form', form);
+ formElement.bind('submit', function(event){
+ event.preventDefault();
+ });
+ if (name) {
+ this[name] = form;
+ }
+ watch('valid');
+ watch('invalid');
+ function watch(name) {
+ form.$watch('$' + name, function(scope, value) {
+ formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ });
+ }
+ });
+});
+
+angularWidget('ng:form', angularWidget('form'));
diff --git a/src/widget/input.js b/src/widget/input.js
new file mode 100644
index 00000000..f82027f4
--- /dev/null
+++ b/src/widget/input.js
@@ -0,0 +1,773 @@
+'use strict';
+
+
+var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
+var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
+var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
+var INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.text
+ *
+ * @description
+ * Standard HTML text input with angular data binding.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'guest';
+ this.word = /^\w*$/;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Single word: <input type="text" name="input" ng:model="text"
+ ng:pattern="word" required>
+ <span class="error" ng:show="myForm.input.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.$error.PATTERN">
+ Single word only!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if multi word', function() {
+ input('text').enter('hello world');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.email
+ *
+ * @description
+ * Text input with email validation. Sets the `EMAIL` validation error key if not a valid email
+ * address.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'me@example.com';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Email: <input type="email" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.$error.EMAIL">
+ Not valid email!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ <tt>myForm.$error.EMAIL = {{!!myForm.$error.EMAIL}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('me@example.com');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if not email', function() {
+ input('text').enter('xxx');
+ expect(binding('text')).toEqual('xxx');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('email', function() {
+ var widget = this;
+ this.$on('$validate', function(event){
+ var value = widget.$viewValue;
+ widget.$emit(!value || value.match(EMAIL_REGEXP) ? "$valid" : "$invalid", "EMAIL");
+ });
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.url
+ *
+ * @description
+ * Text input with URL validation. Sets the `URL` validation error key if the content is not a
+ * valid URL.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'http://google.com';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ URL: <input type="url" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.$error.url">
+ Not valid url!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ <tt>myForm.$error.url = {{!!myForm.$error.url}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('http://google.com');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if not url', function() {
+ input('text').enter('xxx');
+ expect(binding('text')).toEqual('xxx');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('url', function() {
+ var widget = this;
+ this.$on('$validate', function(event){
+ var value = widget.$viewValue;
+ widget.$emit(!value || value.match(URL_REGEXP) ? "$valid" : "$invalid", "URL");
+ });
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.list
+ *
+ * @description
+ * Text input that converts between comma-seperated string into an array of strings.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.names = ['igor', 'misko', 'vojta'];
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ List: <input type="list" name="input" ng:model="names" required>
+ <span class="error" ng:show="myForm.list.$error.REQUIRED">
+ Required!</span>
+ </form>
+ <tt>names = {{names}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('names')).toEqual('["igor","misko","vojta"]');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('names').enter('');
+ expect(binding('names')).toEqual('[]');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('list', function() {
+ function parse(viewValue) {
+ var list = [];
+ forEach(viewValue.split(/\s*,\s*/), function(value){
+ if (value) list.push(trim(value));
+ });
+ return list;
+ }
+ this.$parseView = function() {
+ isString(this.$viewValue) && (this.$modelValue = parse(this.$viewValue));
+ };
+ this.$parseModel = function() {
+ var modelValue = this.$modelValue;
+ if (isArray(modelValue)
+ && (!isString(this.$viewValue) || !equals(parse(this.$viewValue), modelValue))) {
+ this.$viewValue = modelValue.join(', ');
+ }
+ };
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.number
+ *
+ * @description
+ * Text input with number validation and transformation. Sets the `NUMBER` validation
+ * error if not a valid number.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} min Sets the `MIN` validation error key if the value entered is less then `min`.
+ * @param {string=} max Sets the `MAX` validation error key if the value entered is greater then `min`.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.value = 12;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Number: <input type="number" name="input" ng:model="value"
+ min="0" max="99" required>
+ <span class="error" ng:show="myForm.list.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.list.$error.NUMBER">
+ Not valid number!</span>
+ </form>
+ <tt>value = {{value}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('value')).toEqual('12');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('value').enter('');
+ expect(binding('value')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if over max', function() {
+ input('value').enter('123');
+ expect(binding('value')).toEqual('123');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('number', numericRegexpInputType(NUMBER_REGEXP, 'NUMBER'));
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.integer
+ *
+ * @description
+ * Text input with integer validation and transformation. Sets the `INTEGER`
+ * validation error key if not a valid integer.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} min Sets the `MIN` validation error key if the value entered is less then `min`.
+ * @param {string=} max Sets the `MAX` validation error key if the value entered is greater then `min`.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.value = 12;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Integer: <input type="integer" name="input" ng:model="value"
+ min="0" max="99" required>
+ <span class="error" ng:show="myForm.list.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.list.$error.INTEGER">
+ Not valid integer!</span>
+ </form>
+ <tt>value = {{value}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('value')).toEqual('12');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('value').enter('1.2');
+ expect(binding('value')).toEqual('12');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if over max', function() {
+ input('value').enter('123');
+ expect(binding('value')).toEqual('123');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.checkbox
+ *
+ * @description
+ * HTML checkbox.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} true-value The value to which the expression should be set when selected.
+ * @param {string=} false-value The value to which the expression should be set when not selected.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.value1 = true;
+ this.value2 = 'YES'
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Value1: <input type="checkbox" ng:model="value1"> <br/>
+ Value2: <input type="checkbox" ng:model="value2"
+ true-value="YES" false-value="NO"> <br/>
+ </form>
+ <tt>value1 = {{value1}}</tt><br/>
+ <tt>value2 = {{value2}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should change state', function() {
+ expect(binding('value1')).toEqual('true');
+ expect(binding('value2')).toEqual('YES');
+
+ input('value1').check();
+ input('value2').check();
+ expect(binding('value1')).toEqual('false');
+ expect(binding('value2')).toEqual('NO');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('checkbox', function (inputElement) {
+ var widget = this,
+ trueValue = inputElement.attr('true-value'),
+ falseValue = inputElement.attr('false-value');
+
+ if (!isString(trueValue)) trueValue = true;
+ if (!isString(falseValue)) falseValue = false;
+
+ inputElement.bind('click', function() {
+ widget.$apply(function() {
+ widget.$emit('$viewChange', inputElement[0].checked);
+ });
+ });
+
+ widget.$render = function() {
+ inputElement[0].checked = widget.$viewValue;
+ };
+
+ widget.$parseModel = function() {
+ widget.$viewValue = this.$modelValue === trueValue;
+ };
+
+ widget.$parseView = function() {
+ widget.$modelValue = widget.$viewValue ? trueValue : falseValue;
+ };
+
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.radio
+ *
+ * @description
+ * HTML radio.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string} value The value to which the expression should be set when selected.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.color = 'blue';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ <input type="radio" ng:model="color" value="red"> Red <br/>
+ <input type="radio" ng:model="color" value="green"> Green <br/>
+ <input type="radio" ng:model="color" value="blue"> Blue <br/>
+ </form>
+ <tt>color = {{color}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should change state', function() {
+ expect(binding('color')).toEqual('blue');
+
+ input('color').select('red');
+ expect(binding('color')).toEqual('red');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('radio', function(inputElement) {
+ var widget = this,
+ value = inputElement.attr('value');
+
+ //correct the name
+ inputElement.attr('name', widget.$id + '@' + inputElement.attr('name'));
+ inputElement.bind('click', function() {
+ widget.$apply(function() {
+ if (inputElement[0].checked) {
+ widget.$emit('$viewChange', value);
+ }
+ });
+ });
+
+ widget.$render = function() {
+ inputElement[0].checked = value == widget.$viewValue;
+ };
+
+ if (inputElement[0].checked) {
+ widget.$viewValue = value;
+ }
+});
+
+
+function numericRegexpInputType(regexp, error) {
+ return function(inputElement) {
+ var widget = this,
+ min = 1 * (inputElement.attr('min') || Number.MIN_VALUE),
+ max = 1 * (inputElement.attr('max') || Number.MAX_VALUE);
+
+ widget.$on('$validate', function(event){
+ var value = widget.$viewValue,
+ filled = value && trim(value) != '',
+ valid = isString(value) && value.match(regexp);
+
+ widget.$emit(!filled || valid ? "$valid" : "$invalid", error);
+ filled && (value = 1 * value);
+ widget.$emit(valid && value < min ? "$invalid" : "$valid", "MIN");
+ widget.$emit(valid && value > max ? "$invalid" : "$valid", "MAX");
+ });
+
+ widget.$parseView = function() {
+ if (widget.$viewValue.match(regexp)) {
+ widget.$modelValue = 1 * widget.$viewValue;
+ } else if (widget.$viewValue == '') {
+ widget.$modelValue = null;
+ }
+ };
+
+ widget.$parseModel = function() {
+ if (isNumber(widget.$modelValue)) {
+ widget.$viewValue = '' + widget.$modelValue;
+ }
+ };
+ };
+}
+
+
+var HTML5_INPUTS_TYPES = makeMap(
+ "search,tel,url,email,datetime,date,month,week,time,datetime-local,number,range,color," +
+ "radio,checkbox,text,button,submit,reset,hidden");
+
+
+/**
+ * @workInProgress
+ * @ngdoc widget
+ * @name angular.widget.input
+ *
+ * @description
+ * HTML input element widget with angular data-binding. Input widget follows HTML5 input types
+ * and polyfills the HTML5 validation behavior for older browsers.
+ *
+ * The {@link angular.inputType custom angular.inputType}s provides a short hand for declaring new
+ * inputs. This is a shart hand for text-box based inputs, and there is no need to go through the
+ * full {@link angular.service.$formFactory $formFactory} widget lifecycle.
+ *
+ *
+ * @param {string} type Widget types as defined by {@link angular.inputType}. If the
+ * type is in the format of `@ScopeType` then `ScopeType` is loaded from the
+ * current scope, allowing quick definition of type.
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'guest';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ text: <input type="text" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.$error.REQUIRED">
+ Required!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularWidget('input', function (inputElement){
+ this.directives(true);
+ this.descend(true);
+ var modelExp = inputElement.attr('ng:model');
+ return modelExp &&
+ annotate('$defer', '$formFactory', function($defer, $formFactory, inputElement){
+ var form = $formFactory.forElement(inputElement),
+ // We have to use .getAttribute, since jQuery tries to be smart and use the
+ // type property. Trouble is some browser change unknown to text.
+ type = inputElement[0].getAttribute('type') || 'text',
+ TypeController,
+ modelScope = this,
+ patternMatch, widget,
+ pattern = trim(inputElement.attr('ng:pattern')),
+ loadFromScope = type.match(/^\s*\@\s*(.*)/);
+
+
+ if (!pattern) {
+ patternMatch = valueFn(true);
+ } else {
+ if (pattern.match(/^\/(.*)\/$/)) {
+ pattern = new RegExp(pattern.substring(1, pattern.length - 2));
+ patternMatch = function(value) {
+ return pattern.test(value);
+ }
+ } else {
+ patternMatch = function(value) {
+ var patternObj = modelScope.$eval(pattern);
+ if (!patternObj || !patternObj.test) {
+ throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj);
+ }
+ return patternObj.test(value);
+ }
+ }
+ }
+
+ type = lowercase(type);
+ TypeController = (loadFromScope
+ ? (assertArgFn(this.$eval(loadFromScope[1]), loadFromScope[1])).$unboundFn
+ : angularInputType(type)) || noop;
+
+ if (!HTML5_INPUTS_TYPES[type]) {
+ try {
+ // jquery will not let you so we have to go to bare metal
+ inputElement[0].setAttribute('type', 'text');
+ } catch(e){
+ // also turns out that ie8 will not allow changing of types, but since it is not
+ // html5 anyway we can ignore the error.
+ }
+ }
+
+ !TypeController.$inject && (TypeController.$inject = []);
+ widget = form.$createWidget({
+ scope: modelScope,
+ model: modelExp,
+ onChange: inputElement.attr('ng:change'),
+ alias: inputElement.attr('name'),
+ controller: TypeController,
+ controllerArgs: [inputElement]});
+
+ widget.$pattern =
+ watchElementProperty(this, widget, 'required', inputElement);
+ watchElementProperty(this, widget, 'readonly', inputElement);
+ watchElementProperty(this, widget, 'disabled', inputElement);
+
+
+ widget.$pristine = !(widget.$dirty = false);
+
+ widget.$on('$validate', function(event) {
+ var $viewValue = trim(widget.$viewValue);
+ var inValid = widget.$required && !$viewValue;
+ var missMatch = $viewValue && !patternMatch($viewValue);
+ if (widget.$error.REQUIRED != inValid){
+ widget.$emit(inValid ? '$invalid' : '$valid', 'REQUIRED');
+ }
+ if (widget.$error.PATTERN != missMatch){
+ widget.$emit(missMatch ? '$invalid' : '$valid', 'PATTERN');
+ }
+ });
+
+ forEach(['valid', 'invalid', 'pristine', 'dirty'], function (name) {
+ widget.$watch('$' + name, function(scope, value) {
+ inputElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ }
+ );
+ });
+
+ inputElement.bind('$destroy', function() {
+ widget.$destroy();
+ });
+
+ if (type != 'checkbox' && type != 'radio') {
+ // TODO (misko): checkbox / radio does not really belong here, but until we can do
+ // widget registration with CSS, we are hacking it this way.
+ widget.$render = function() {
+ inputElement.val(widget.$viewValue || '');
+ };
+
+ inputElement.bind('keydown change', function(event){
+ var key = event.keyCode;
+ if (/*command*/ key != 91 &&
+ /*modifiers*/ !(15 < key && key < 19) &&
+ /*arrow*/ !(37 < key && key < 40)) {
+ $defer(function() {
+ widget.$dirty = !(widget.$pristine = false);
+ var value = trim(inputElement.val());
+ if (widget.$viewValue !== value ) {
+ widget.$emit('$viewChange', value);
+ }
+ });
+ }
+ });
+ }
+ });
+
+});
+
+angularWidget('textarea', angularWidget('input'));
+
+
+function watchElementProperty(modelScope, widget, name, element) {
+ var bindAttr = fromJson(element.attr('ng:bind-attr') || '{}'),
+ match = /\s*{{(.*)}}\s*/.exec(bindAttr[name]);
+ widget['$' + name] =
+ // some browsers return true some '' when required is set without value.
+ isString(element.prop(name)) || !!element.prop(name) ||
+ // this is needed for ie9, since it will treat boolean attributes as false
+ !!element[0].attributes[name];
+ if (bindAttr[name] && match) {
+ modelScope.$watch(match[1], function(scope, value){
+ widget['$' + name] = !!value;
+ widget.$emit('$validate');
+ });
+ }
+}
+
diff --git a/src/widget/select.js b/src/widget/select.js
new file mode 100644
index 00000000..f397180e
--- /dev/null
+++ b/src/widget/select.js
@@ -0,0 +1,427 @@
+'use strict';
+
+/**
+ * @workInProgress
+ * @ngdoc widget
+ * @name angular.widget.select
+ *
+ * @description
+ * HTML `SELECT` element with angular data-binding.
+ *
+ * # `ng:options`
+ *
+ * Optionally `ng:options` attribute can be used to dynamically generate a list of `<option>`
+ * elements for a `<select>` element using an array or an object obtained by evaluating the
+ * `ng:options` expression.
+ *
+ * When an item in the select menu is select, the value of array element or object property
+ * represented by the selected option will be bound to the model identified by the `name` attribute
+ * of the parent select element.
+ *
+ * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
+ * be nested into the `<select>` element. This element will then represent `null` or "not selected"
+ * option. See example below for demonstration.
+ *
+ * Note: `ng:options` provides iterator facility for `<option>` element which must be used instead
+ * of {@link angular.widget.@ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with
+ * `<option>` element because of the following reasons:
+ *
+ * * value attribute of the option element that we need to bind to requires a string, but the
+ * source of data for the iteration might be in a form of array containing objects instead of
+ * strings
+ * * {@link angular.widget.@ng:repeat ng:repeat} unrolls after the select binds causing
+ * incorect rendering on most browsers.
+ * * binding to a value not in list confuses most browsers.
+ *
+ * @param {string} name assignable expression to data-bind to.
+ * @param {string=} required The widget is considered valid only if value is entered.
+ * @param {comprehension_expression=} ng:options in one of the following forms:
+ *
+ * * for array data sources:
+ * * `label` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * for object data sources:
+ * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`group by`** `group`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ *
+ * Where:
+ *
+ * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
+ * * `value`: local variable which will refer to each item in the `array` or each property value
+ * of `object` during iteration.
+ * * `key`: local variable which will refer to a property name in `object` during iteration.
+ * * `label`: The result of this expression will be the label for `<option>` element. The
+ * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
+ * * `select`: The result of this expression will be bound to the model of the parent `<select>`
+ * element. If not specified, `select` expression will default to `value`.
+ * * `group`: The result of this expression will be used to group options using the `<optgroup>`
+ * DOM element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function MyCntrl(){
+ this.colors = [
+ {name:'black', shade:'dark'},
+ {name:'white', shade:'light'},
+ {name:'red', shade:'dark'},
+ {name:'blue', shade:'dark'},
+ {name:'yellow', shade:'light'}
+ ];
+ this.color = this.colors[2]; // red
+ }
+ </script>
+ <div ng:controller="MyCntrl">
+ <ul>
+ <li ng:repeat="color in colors">
+ Name: <input ng:model="color.name">
+ [<a href ng:click="colors.$remove(color)">X</a>]
+ </li>
+ <li>
+ [<a href ng:click="colors.push({})">add</a>]
+ </li>
+ </ul>
+ <hr/>
+ Color (null not allowed):
+ <select ng:model="color" ng:options="c.name for c in colors"></select><br>
+
+ Color (null allowed):
+ <div class="nullable">
+ <select ng:model="color" ng:options="c.name for c in colors">
+ <option value="">-- chose color --</option>
+ </select>
+ </div><br/>
+
+ Color grouped by shade:
+ <select ng:model="color" ng:options="c.name group by c.shade for c in colors">
+ </select><br/>
+
+
+ Select <a href ng:click="color={name:'not in list'}">bogus</a>.<br>
+ <hr/>
+ Currently selected: {{ {selected_color:color} }}
+ <div style="border:solid 1px black; height:20px"
+ ng:style="{'background-color':color.name}">
+ </div>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:options', function(){
+ expect(binding('color')).toMatch('red');
+ select('color').option('0');
+ expect(binding('color')).toMatch('black');
+ using('.nullable').select('color').option('');
+ expect(binding('color')).toMatch('null');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+
+ //00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
+var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;
+
+
+angularWidget('select', function (element){
+ this.directives(true);
+ this.descend(true);
+ return element.attr('ng:model') && annotate('$formFactory', function($formFactory, selectElement){
+ var modelScope = this,
+ match,
+ form = $formFactory.forElement(selectElement),
+ multiple = selectElement.attr('multiple'),
+ optionsExp = selectElement.attr('ng:options'),
+ modelExp = selectElement.attr('ng:model'),
+ widget = form.$createWidget({
+ scope: this,
+ model: modelExp,
+ onChange: selectElement.attr('ng:change'),
+ alias: selectElement.attr('name'),
+ controller: optionsExp ? Options : (multiple ? Multiple : Single)});
+
+ selectElement.bind('$destroy', function(){ widget.$destroy(); });
+
+ widget.$pristine = !(widget.$dirty = false);
+
+ watchElementProperty(modelScope, widget, 'required', selectElement);
+ watchElementProperty(modelScope, widget, 'readonly', selectElement);
+ watchElementProperty(modelScope, widget, 'disabled', selectElement);
+
+ widget.$on('$validate', function(){
+ var valid = !widget.$required || !!widget.$modelValue;
+ if (valid && multiple && widget.$required) valid = !!widget.$modelValue.length;
+ if (valid !== !widget.$error.REQUIRED) {
+ widget.$emit(valid ? '$valid' : '$invalid', 'REQUIRED');
+ }
+ });
+
+ widget.$on('$viewChange', function(){
+ widget.$pristine = !(widget.$dirty = true);
+ });
+
+ forEach(['valid', 'invalid', 'pristine', 'dirty'], function (name) {
+ widget.$watch('$' + name, function(scope, value) {
+ selectElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ });
+ });
+
+ ////////////////////////////
+
+ function Multiple(){
+ var widget = this;
+
+ this.$render = function(){
+ var items = new HashMap(this.$viewValue);
+ forEach(selectElement.children(), function(option){
+ option.selected = isDefined(items.get(option.value));
+ });
+ };
+
+ selectElement.bind('change', function (){
+ widget.$apply(function(){
+ var array = [];
+ forEach(selectElement.children(), function(option){
+ if (option.selected) {
+ array.push(option.value);
+ }
+ });
+ widget.$emit('$viewChange', array);
+ });
+ });
+
+ }
+
+ function Single(){
+ var widget = this;
+
+ widget.$render = function(){
+ selectElement.val(widget.$viewValue);
+ };
+
+ selectElement.bind('change', function(){
+ widget.$apply(function(){
+ widget.$emit('$viewChange', selectElement.val());
+ });
+ });
+
+ widget.$viewValue = selectElement.val();
+ }
+
+ function Options(){
+ var widget = this,
+ match;
+
+ if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
+ throw Error(
+ "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
+ " but got '" + optionsExp + "'.");
+ }
+
+ var widgetScope = this,
+ displayFn = expressionCompile(match[2] || match[1]),
+ valueName = match[4] || match[6],
+ keyName = match[5],
+ groupByFn = expressionCompile(match[3] || ''),
+ valueFn = expressionCompile(match[2] ? match[1] : valueName),
+ valuesFn = expressionCompile(match[7]),
+ // we can't just jqLite('<option>') since jqLite is not smart enough
+ // to create it in <select> and IE barfs otherwise.
+ optionTemplate = jqLite(document.createElement('option')),
+ optGroupTemplate = jqLite(document.createElement('optgroup')),
+ nullOption = false, // if false then user will not be able to select it
+ // This is an array of array of existing option groups in DOM. We try to reuse these if possible
+ // optionGroupsCache[0] is the options with no option group
+ // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
+ optionGroupsCache = [[{element: selectElement, label:''}]],
+ inChangeEvent;
+
+ // find existing special options
+ forEach(selectElement.children(), function(option){
+ if (option.value == '')
+ // User is allowed to select the null.
+ nullOption = {label:jqLite(option).text(), id:''};
+ });
+ selectElement.html(''); // clear contents
+
+ selectElement.bind('change', function(){
+ widgetScope.$apply(function(){
+ var optionGroup,
+ collection = valuesFn(modelScope) || [],
+ key = selectElement.val(),
+ tempScope = inherit(modelScope),
+ value, optionElement, index, groupIndex, length, groupLength;
+
+ if (multiple) {
+ value = [];
+ for (groupIndex = 0, groupLength = optionGroupsCache.length;
+ groupIndex < groupLength;
+ groupIndex++) {
+ // list of options for that group. (first item has the parent)
+ optionGroup = optionGroupsCache[groupIndex];
+
+ for(index = 1, length = optionGroup.length; index < length; index++) {
+ if ((optionElement = optionGroup[index].element)[0].selected) {
+ if (keyName) tempScope[keyName] = key;
+ tempScope[valueName] = collection[optionElement.val()];
+ value.push(valueFn(tempScope));
+ }
+ }
+ }
+ } else {
+ if (key == '?') {
+ value = undefined;
+ } else if (key == ''){
+ value = null;
+ } else {
+ tempScope[valueName] = collection[key];
+ if (keyName) tempScope[keyName] = key;
+ value = valueFn(tempScope);
+ }
+ }
+ if (isDefined(value) && modelScope.$viewVal !== value) {
+ widgetScope.$emit('$viewChange', value);
+ }
+ });
+ });
+
+ widgetScope.$watch(render);
+ widgetScope.$render = render;
+
+ function render() {
+ var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
+ optionGroupNames = [''],
+ optionGroupName,
+ optionGroup,
+ option,
+ existingParent, existingOptions, existingOption,
+ modelValue = widget.$modelValue,
+ values = valuesFn(modelScope) || [],
+ keys = keyName ? sortedKeys(values) : values,
+ groupLength, length,
+ groupIndex, index,
+ optionScope = inherit(modelScope),
+ selected,
+ selectedSet = false, // nothing is selected yet
+ lastElement,
+ element;
+
+ if (multiple) {
+ selectedSet = new HashMap(modelValue);
+ } else if (modelValue === null || nullOption) {
+ // if we are not multiselect, and we are null then we have to add the nullOption
+ optionGroups[''].push(extend({selected:modelValue === null, id:'', label:''}, nullOption));
+ selectedSet = true;
+ }
+
+ // We now build up the list of options we need (we merge later)
+ for (index = 0; length = keys.length, index < length; index++) {
+ optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index];
+ optionGroupName = groupByFn(optionScope) || '';
+ if (!(optionGroup = optionGroups[optionGroupName])) {
+ optionGroup = optionGroups[optionGroupName] = [];
+ optionGroupNames.push(optionGroupName);
+ }
+ if (multiple) {
+ selected = selectedSet.remove(valueFn(optionScope)) != undefined;
+ } else {
+ selected = modelValue === valueFn(optionScope);
+ selectedSet = selectedSet || selected; // see if at least one item is selected
+ }
+ optionGroup.push({
+ id: keyName ? keys[index] : index, // either the index into array or key from object
+ label: displayFn(optionScope) || '', // what will be seen by the user
+ selected: selected // determine if we should be selected
+ });
+ }
+ if (!multiple && !selectedSet) {
+ // nothing was selected, we have to insert the undefined item
+ optionGroups[''].unshift({id:'?', label:'', selected:true});
+ }
+
+ // Now we need to update the list of DOM nodes to match the optionGroups we computed above
+ for (groupIndex = 0, groupLength = optionGroupNames.length;
+ groupIndex < groupLength;
+ groupIndex++) {
+ // current option group name or '' if no group
+ optionGroupName = optionGroupNames[groupIndex];
+
+ // list of options for that group. (first item has the parent)
+ optionGroup = optionGroups[optionGroupName];
+
+ if (optionGroupsCache.length <= groupIndex) {
+ // we need to grow the optionGroups
+ optionGroupsCache.push(
+ existingOptions = [existingParent = {
+ element: optGroupTemplate.clone().attr('label', optionGroupName),
+ label: optionGroup.label
+ }]
+ );
+ selectElement.append(existingParent.element);
+ } else {
+ existingOptions = optionGroupsCache[groupIndex];
+ existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
+
+ // update the OPTGROUP label if not the same.
+ if (existingParent.label != optionGroupName) {
+ existingParent.element.attr('label', existingParent.label = optionGroupName);
+ }
+ }
+
+ lastElement = null; // start at the begining
+ for(index = 0, length = optionGroup.length; index < length; index++) {
+ option = optionGroup[index];
+ if ((existingOption = existingOptions[index+1])) {
+ // reuse elements
+ lastElement = existingOption.element;
+ if (existingOption.label !== option.label) {
+ lastElement.text(existingOption.label = option.label);
+ }
+ if (existingOption.id !== option.id) {
+ lastElement.val(existingOption.id = option.id);
+ }
+ if (existingOption.selected !== option.selected) {
+ lastElement.prop('selected', (existingOption.selected = option.selected));
+ }
+ } else {
+ // grow elements
+ // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
+ // in this version of jQuery on some browser the .text() returns a string
+ // rather then the element.
+ (element = optionTemplate.clone())
+ .val(option.id)
+ .attr('selected', option.selected)
+ .text(option.label);
+ existingOptions.push(existingOption = {
+ element: element,
+ label: option.label,
+ id: option.id,
+ selected: option.selected
+ });
+ if (lastElement) {
+ lastElement.after(element);
+ } else {
+ existingParent.element.append(element);
+ }
+ lastElement = element;
+ }
+ }
+ // remove any excessive OPTIONs in a group
+ index++; // increment since the existingOptions[0] is parent element not OPTION
+ while(existingOptions.length > index) {
+ existingOptions.pop().element.remove();
+ }
+ }
+ // remove any excessive OPTGROUPs from select
+ while(optionGroupsCache.length > groupIndex) {
+ optionGroupsCache.pop()[0].element.remove();
+ }
+ };
+ }
+ });
+});
diff --git a/src/widgets.js b/src/widgets.js
index 1047c3ce..e3c6906f 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -14,14 +14,11 @@
*
* Following is the list of built-in angular widgets:
*
- * * {@link angular.widget.@ng:format ng:format} - Formats data for display to user and for storage.
* * {@link angular.widget.@ng:non-bindable ng:non-bindable} - Blocks angular from processing an
* HTML element.
* * {@link angular.widget.@ng:repeat ng:repeat} - Creates and manages a collection of cloned HTML
* elements.
- * * {@link angular.widget.@ng:required ng:required} - Verifies presence of user input.
- * * {@link angular.widget.@ng:validate ng:validate} - Validates content of user input.
- * * {@link angular.widget.HTML HTML input elements} - Standard HTML input elements data-bound by
+ * * {@link angular.inputType HTML input elements} - Standard HTML input elements data-bound by
* angular.
* * {@link angular.widget.ng:view ng:view} - Works with $route to "include" partial templates
* * {@link angular.widget.ng:switch ng:switch} - Conditionally changes DOM structure
@@ -34,915 +31,6 @@
/**
* @workInProgress
* @ngdoc widget
- * @name angular.widget.HTML
- *
- * @description
- * The most common widgets you will use will be in the form of the
- * standard HTML set. These widgets are bound using the `name` attribute
- * to an expression. In addition, they can have `ng:validate`, `ng:required`,
- * `ng:format`, `ng:change` attribute to further control their behavior.
- *
- * @usageContent
- * see example below for usage
- *
- * <input type="text|checkbox|..." ... />
- * <textarea ... />
- * <select ...>
- * <option>...</option>
- * </select>
- *
- * @example
- <doc:example>
- <doc:source>
- <table style="font-size:.9em;">
- <tr>
- <th>Name</th>
- <th>Format</th>
- <th>HTML</th>
- <th>UI</th>
- <th ng:non-bindable>{{input#}}</th>
- </tr>
- <tr>
- <th>text</th>
- <td>String</td>
- <td><tt>&lt;input type="text" name="input1"&gt;</tt></td>
- <td><input type="text" name="input1" size="4"></td>
- <td><tt>{{input1|json}}</tt></td>
- </tr>
- <tr>
- <th>textarea</th>
- <td>String</td>
- <td><tt>&lt;textarea name="input2"&gt;&lt;/textarea&gt;</tt></td>
- <td><textarea name="input2" cols='6'></textarea></td>
- <td><tt>{{input2|json}}</tt></td>
- </tr>
- <tr>
- <th>radio</th>
- <td>String</td>
- <td><tt>
- &lt;input type="radio" name="input3" value="A"&gt;<br>
- &lt;input type="radio" name="input3" value="B"&gt;
- </tt></td>
- <td>
- <input type="radio" name="input3" value="A">
- <input type="radio" name="input3" value="B">
- </td>
- <td><tt>{{input3|json}}</tt></td>
- </tr>
- <tr>
- <th>checkbox</th>
- <td>Boolean</td>
- <td><tt>&lt;input type="checkbox" name="input4" value="checked"&gt;</tt></td>
- <td><input type="checkbox" name="input4" value="checked"></td>
- <td><tt>{{input4|json}}</tt></td>
- </tr>
- <tr>
- <th>pulldown</th>
- <td>String</td>
- <td><tt>
- &lt;select name="input5"&gt;<br>
- &nbsp;&nbsp;&lt;option value="c"&gt;C&lt;/option&gt;<br>
- &nbsp;&nbsp;&lt;option value="d"&gt;D&lt;/option&gt;<br>
- &lt;/select&gt;<br>
- </tt></td>
- <td>
- <select name="input5">
- <option value="c">C</option>
- <option value="d">D</option>
- </select>
- </td>
- <td><tt>{{input5|json}}</tt></td>
- </tr>
- <tr>
- <th>multiselect</th>
- <td>Array</td>
- <td><tt>
- &lt;select name="input6" multiple size="4"&gt;<br>
- &nbsp;&nbsp;&lt;option value="e"&gt;E&lt;/option&gt;<br>
- &nbsp;&nbsp;&lt;option value="f"&gt;F&lt;/option&gt;<br>
- &lt;/select&gt;<br>
- </tt></td>
- <td>
- <select name="input6" multiple size="4">
- <option value="e">E</option>
- <option value="f">F</option>
- </select>
- </td>
- <td><tt>{{input6|json}}</tt></td>
- </tr>
- </table>
- </doc:source>
- <doc:scenario>
-
- it('should exercise text', function(){
- input('input1').enter('Carlos');
- expect(binding('input1')).toEqual('"Carlos"');
- });
- it('should exercise textarea', function(){
- input('input2').enter('Carlos');
- expect(binding('input2')).toEqual('"Carlos"');
- });
- it('should exercise radio', function(){
- expect(binding('input3')).toEqual('null');
- input('input3').select('A');
- expect(binding('input3')).toEqual('"A"');
- input('input3').select('B');
- expect(binding('input3')).toEqual('"B"');
- });
- it('should exercise checkbox', function(){
- expect(binding('input4')).toEqual('false');
- input('input4').check();
- expect(binding('input4')).toEqual('true');
- });
- it('should exercise pulldown', function(){
- expect(binding('input5')).toEqual('"c"');
- select('input5').option('d');
- expect(binding('input5')).toEqual('"d"');
- });
- it('should exercise multiselect', function(){
- expect(binding('input6')).toEqual('[]');
- select('input6').options('e');
- expect(binding('input6')).toEqual('["e"]');
- select('input6').options('e', 'f');
- expect(binding('input6')).toEqual('["e","f"]');
- });
- </doc:scenario>
- </doc:example>
- */
-
-function modelAccessor(scope, element) {
- var expr = element.attr('name');
- var exprFn, assignFn;
- if (expr) {
- exprFn = parser(expr).assignable();
- assignFn = exprFn.assign;
- if (!assignFn) throw new Error("Expression '" + expr + "' is not assignable.");
- return {
- get: function() {
- return exprFn(scope);
- },
- set: function(value) {
- if (value !== undefined) {
- assignFn(scope, value);
- }
- }
- };
- }
-}
-
-function modelFormattedAccessor(scope, element) {
- var accessor = modelAccessor(scope, element),
- formatterName = element.attr('ng:format') || NOOP,
- formatter = compileFormatter(formatterName);
- if (accessor) {
- return {
- get: function() {
- return formatter.format(scope, accessor.get());
- },
- set: function(value) {
- return accessor.set(formatter.parse(scope, value));
- }
- };
- }
-}
-
-function compileValidator(expr) {
- return parser(expr).validator()();
-}
-
-function compileFormatter(expr) {
- return parser(expr).formatter()();
-}
-
-/**
- * @workInProgress
- * @ngdoc widget
- * @name angular.widget.@ng:validate
- *
- * @description
- * The `ng:validate` attribute widget validates the user input. If the input does not pass
- * validation, the `ng-validation-error` CSS class and the `ng:error` attribute are set on the input
- * element. Check out {@link angular.validator validators} to find out more.
- *
- * @param {string} validator The name of a built-in or custom {@link angular.validator validator} to
- * to be used.
- *
- * @element INPUT
- * @css ng-validation-error
- *
- * @example
- * This example shows how the input element becomes red when it contains invalid input. Correct
- * the input to make the error disappear.
- *
- <doc:example>
- <doc:source>
- I don't validate:
- <input type="text" name="value" value="NotANumber"><br/>
-
- I need an integer or nothing:
- <input type="text" name="value" ng:validate="integer"><br/>
- </doc:source>
- <doc:scenario>
- it('should check ng:validate', function(){
- expect(element('.doc-example-live :input:last').prop('className')).
- toMatch(/ng-validation-error/);
-
- input('value').enter('123');
- expect(element('.doc-example-live :input:last').prop('className')).
- not().toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- */
-/**
- * @workInProgress
- * @ngdoc widget
- * @name angular.widget.@ng:required
- *
- * @description
- * The `ng:required` attribute widget validates that the user input is present. It is a special case
- * of the {@link angular.widget.@ng:validate ng:validate} attribute widget.
- *
- * @element INPUT
- * @css ng-validation-error
- *
- * @example
- * This example shows how the input element becomes red when it contains invalid input. Correct
- * the input to make the error disappear.
- *
- <doc:example>
- <doc:source>
- I cannot be blank: <input type="text" name="value" ng:required><br/>
- </doc:source>
- <doc:scenario>
- it('should check ng:required', function(){
- expect(element('.doc-example-live :input').prop('className')).
- toMatch(/ng-validation-error/);
- input('value').enter('123');
- expect(element('.doc-example-live :input').prop('className')).
- not().toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- */
-/**
- * @workInProgress
- * @ngdoc widget
- * @name angular.widget.@ng:format
- *
- * @description
- * The `ng:format` attribute widget formats stored data to user-readable text and parses the text
- * back to the stored form. You might find this useful, for example, if you collect user input in a
- * text field but need to store the data in the model as a list. Check out
- * {@link angular.formatter formatters} to learn more.
- *
- * @param {string} formatter The name of the built-in or custom {@link angular.formatter formatter}
- * to be used.
- *
- * @element INPUT
- *
- * @example
- * This example shows how the user input is converted from a string and internally represented as an
- * array.
- *
- <doc:example>
- <doc:source>
- Enter a comma separated list of items:
- <input type="text" name="list" ng:format="list" value="table, chairs, plate">
- <pre>list={{list}}</pre>
- </doc:source>
- <doc:scenario>
- it('should check ng:format', function(){
- expect(binding('list')).toBe('list=["table","chairs","plate"]');
- input('list').enter(',,, a ,,,');
- expect(binding('list')).toBe('list=["a"]');
- });
- </doc:scenario>
- </doc:example>
- */
-function valueAccessor(scope, element) {
- var validatorName = element.attr('ng:validate') || NOOP,
- validator = compileValidator(validatorName),
- requiredExpr = element.attr('ng:required'),
- formatterName = element.attr('ng:format') || NOOP,
- formatter = compileFormatter(formatterName),
- format, parse, lastError, required,
- invalidWidgets = scope.$service('$invalidWidgets') || {markValid:noop, markInvalid:noop};
- if (!validator) throw "Validator named '" + validatorName + "' not found.";
- format = formatter.format;
- parse = formatter.parse;
- if (requiredExpr) {
- scope.$watch(requiredExpr, function(scope, newValue) {
- required = newValue;
- validate();
- });
- } else {
- required = requiredExpr === '';
- }
-
- element.data($$validate, validate);
- return {
- get: function(){
- if (lastError)
- elementError(element, NG_VALIDATION_ERROR, null);
- try {
- var value = parse(scope, element.val());
- validate();
- return value;
- } catch (e) {
- lastError = e;
- elementError(element, NG_VALIDATION_ERROR, e);
- }
- },
- set: function(value) {
- var oldValue = element.val(),
- newValue = format(scope, value);
- if (oldValue != newValue) {
- element.val(newValue || ''); // needed for ie
- }
- validate();
- }
- };
-
- function validate() {
- var value = trim(element.val());
- if (element[0].disabled || element[0].readOnly) {
- elementError(element, NG_VALIDATION_ERROR, null);
- invalidWidgets.markValid(element);
- } else {
- var error, validateScope = inherit(scope, {$element:element});
- error = required && !value
- ? 'Required'
- : (value ? validator(validateScope, value) : null);
- elementError(element, NG_VALIDATION_ERROR, error);
- lastError = error;
- if (error) {
- invalidWidgets.markInvalid(element);
- } else {
- invalidWidgets.markValid(element);
- }
- }
- }
-}
-
-function checkedAccessor(scope, element) {
- var domElement = element[0], elementValue = domElement.value;
- return {
- get: function(){
- return !!domElement.checked;
- },
- set: function(value){
- domElement.checked = toBoolean(value);
- }
- };
-}
-
-function radioAccessor(scope, element) {
- var domElement = element[0];
- return {
- get: function(){
- return domElement.checked ? domElement.value : null;
- },
- set: function(value){
- domElement.checked = value == domElement.value;
- }
- };
-}
-
-function optionsAccessor(scope, element) {
- var formatterName = element.attr('ng:format') || NOOP,
- formatter = compileFormatter(formatterName);
- return {
- get: function(){
- var values = [];
- forEach(element[0].options, function(option){
- if (option.selected) values.push(formatter.parse(scope, option.value));
- });
- return values;
- },
- set: function(values){
- var keys = {};
- forEach(values, function(value){
- keys[formatter.format(scope, value)] = true;
- });
- forEach(element[0].options, function(option){
- option.selected = keys[option.value];
- });
- }
- };
-}
-
-function noopAccessor() { return { get: noop, set: noop }; }
-
-/*
- * TODO: refactor
- *
- * The table below is not quite right. In some cases the formatter is on the model side
- * and in some cases it is on the view side. This is a historical artifact
- *
- * The concept of model/view accessor is useful for anyone who is trying to develop UI, and
- * so it should be exposed to others. There should be a form object which keeps track of the
- * accessors and also acts as their factory. It should expose it as an object and allow
- * the validator to publish errors to it, so that the the error messages can be bound to it.
- *
- */
-var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, initWidgetValue(), true),
- INPUT_TYPE = {
- 'text': textWidget,
- 'textarea': textWidget,
- 'hidden': textWidget,
- 'password': textWidget,
- 'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)),
- 'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit),
- 'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(null)),
- 'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([]))
-// 'file': fileWidget???
- };
-
-
-function initWidgetValue(initValue) {
- return function (model, view) {
- var value = view.get();
- if (!value && isDefined(initValue)) {
- value = copy(initValue);
- }
- if (isUndefined(model.get()) && isDefined(value)) {
- model.set(value);
- }
- };
-}
-
-function radioInit(model, view, element) {
- var modelValue = model.get(), viewValue = view.get(), input = element[0];
- input.checked = false;
- input.name = this.$id + '@' + input.name;
- if (isUndefined(modelValue)) {
- model.set(modelValue = null);
- }
- if (modelValue == null && viewValue !== null) {
- model.set(viewValue);
- }
- view.set(modelValue);
-}
-
-/**
- * @workInProgress
- * @ngdoc directive
- * @name angular.directive.ng:change
- *
- * @description
- * The directive executes an expression whenever the input widget changes.
- *
- * @element INPUT
- * @param {expression} expression to execute.
- *
- * @example
- * @example
- <doc:example>
- <doc:source>
- <div ng:init="checkboxCount=0; textCount=0"></div>
- <input type="text" name="text" ng:change="textCount = 1 + textCount">
- changeCount {{textCount}}<br/>
- <input type="checkbox" name="checkbox" ng:change="checkboxCount = 1 + checkboxCount">
- changeCount {{checkboxCount}}<br/>
- </doc:source>
- <doc:scenario>
- it('should check ng:change', function(){
- expect(binding('textCount')).toBe('0');
- expect(binding('checkboxCount')).toBe('0');
-
- using('.doc-example-live').input('text').enter('abc');
- expect(binding('textCount')).toBe('1');
- expect(binding('checkboxCount')).toBe('0');
-
-
- using('.doc-example-live').input('checkbox').check();
- expect(binding('textCount')).toBe('1');
- expect(binding('checkboxCount')).toBe('1');
- });
- </doc:scenario>
- </doc:example>
- */
-function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) {
- return annotate('$defer', function($defer, element) {
- var scope = this,
- model = modelAccessor(scope, element),
- view = viewAccessor(scope, element),
- ngChange = element.attr('ng:change') || noop,
- lastValue;
- if (model) {
- initFn.call(scope, model, view, element);
- scope.$eval(element.attr('ng:init') || noop);
- element.bind(events, function(event){
- function handler(){
- var value = view.get();
- if (!textBox || value != lastValue) {
- model.set(value);
- lastValue = model.get();
- scope.$eval(ngChange);
- }
- }
- event.type == 'keydown' ? $defer(handler) : scope.$apply(handler);
- });
- scope.$watch(model.get, function(scope, value) {
- if (!equals(lastValue, value)) {
- view.set(lastValue = value);
- }
- });
- }
- });
-}
-
-function inputWidgetSelector(element){
- this.directives(true);
- this.descend(true);
- return INPUT_TYPE[lowercase(element[0].type)] || noop;
-}
-
-angularWidget('input', inputWidgetSelector);
-angularWidget('textarea', inputWidgetSelector);
-
-
-/**
- * @workInProgress
- * @ngdoc directive
- * @name angular.directive.ng:options
- *
- * @description
- * Dynamically generate a list of `<option>` elements for a `<select>` element using an array or
- * an object obtained by evaluating the `ng:options` expression.
- *
- * When an item in the select menu is select, the value of array element or object property
- * represented by the selected option will be bound to the model identified by the `name` attribute
- * of the parent select element.
- *
- * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
- * be nested into the `<select>` element. This element will then represent `null` or "not selected"
- * option. See example below for demonstration.
- *
- * Note: `ng:options` provides iterator facility for `<option>` element which must be used instead
- * of {@link angular.widget.@ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with
- * `<option>` element because of the following reasons:
- *
- * * value attribute of the option element that we need to bind to requires a string, but the
- * source of data for the iteration might be in a form of array containing objects instead of
- * strings
- * * {@link angular.widget.@ng:repeat ng:repeat} unrolls after the select binds causing
- * incorect rendering on most browsers.
- * * binding to a value not in list confuses most browsers.
- *
- * @element select
- * @param {comprehension_expression} comprehension in one of the following forms:
- *
- * * for array data sources:
- * * `label` **`for`** `value` **`in`** `array`
- * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
- * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
- * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
- * * for object data sources:
- * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
- * * `select` **`as`** `label` **`group by`** `group`
- * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
- *
- * Where:
- *
- * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
- * * `value`: local variable which will refer to each item in the `array` or each property value
- * of `object` during iteration.
- * * `key`: local variable which will refer to a property name in `object` during iteration.
- * * `label`: The result of this expression will be the label for `<option>` element. The
- * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
- * * `select`: The result of this expression will be bound to the model of the parent `<select>`
- * element. If not specified, `select` expression will default to `value`.
- * * `group`: The result of this expression will be used to group options using the `<optgroup>`
- * DOM element.
- *
- * @example
- <doc:example>
- <doc:source>
- <script>
- function MyCntrl(){
- this.colors = [
- {name:'black', shade:'dark'},
- {name:'white', shade:'light'},
- {name:'red', shade:'dark'},
- {name:'blue', shade:'dark'},
- {name:'yellow', shade:'light'}
- ];
- this.color = this.colors[2]; // red
- }
- </script>
- <div ng:controller="MyCntrl">
- <ul>
- <li ng:repeat="color in colors">
- Name: <input name="color.name">
- [<a href ng:click="colors.$remove(color)">X</a>]
- </li>
- <li>
- [<a href ng:click="colors.push({})">add</a>]
- </li>
- </ul>
- <hr/>
- Color (null not allowed):
- <select name="color" ng:options="c.name for c in colors"></select><br>
-
- Color (null allowed):
- <div class="nullable">
- <select name="color" ng:options="c.name for c in colors">
- <option value="">-- chose color --</option>
- </select>
- </div><br/>
-
- Color grouped by shade:
- <select name="color" ng:options="c.name group by c.shade for c in colors">
- </select><br/>
-
-
- Select <a href ng:click="color={name:'not in list'}">bogus</a>.<br>
- <hr/>
- Currently selected: {{ {selected_color:color} }}
- <div style="border:solid 1px black; height:20px"
- ng:style="{'background-color':color.name}">
- </div>
- </div>
- </doc:source>
- <doc:scenario>
- it('should check ng:options', function(){
- expect(binding('color')).toMatch('red');
- select('color').option('0');
- expect(binding('color')).toMatch('black');
- using('.nullable').select('color').option('');
- expect(binding('color')).toMatch('null');
- });
- </doc:scenario>
- </doc:example>
- */
-// 00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
-var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;
-angularWidget('select', function(element){
- this.descend(true);
- this.directives(true);
-
- var isMultiselect = element.attr('multiple'),
- expression = element.attr('ng:options'),
- onChange = expressionCompile(element.attr('ng:change') || ""),
- match;
-
- if (!expression) {
- return inputWidgetSelector.call(this, element);
- }
- if (! (match = expression.match(NG_OPTIONS_REGEXP))) {
- throw Error(
- "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
- " but got '" + expression + "'.");
- }
-
- var displayFn = expressionCompile(match[2] || match[1]),
- valueName = match[4] || match[6],
- keyName = match[5],
- groupByFn = expressionCompile(match[3] || ''),
- valueFn = expressionCompile(match[2] ? match[1] : valueName),
- valuesFn = expressionCompile(match[7]),
- // we can't just jqLite('<option>') since jqLite is not smart enough
- // to create it in <select> and IE barfs otherwise.
- optionTemplate = jqLite(document.createElement('option')),
- optGroupTemplate = jqLite(document.createElement('optgroup')),
- nullOption = false; // if false then user will not be able to select it
-
- return function(selectElement){
-
- // This is an array of array of existing option groups in DOM. We try to reuse these if possible
- // optionGroupsCache[0] is the options with no option group
- // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
- var optionGroupsCache = [[{element: selectElement, label:''}]],
- scope = this,
- model = modelAccessor(scope, element),
- inChangeEvent;
-
- // find existing special options
- forEach(selectElement.children(), function(option){
- if (option.value == '')
- // User is allowed to select the null.
- nullOption = {label:jqLite(option).text(), id:''};
- });
- selectElement.html(''); // clear contents
-
- selectElement.bind('change', function(){
- var optionGroup,
- collection = valuesFn(scope) || [],
- key = selectElement.val(),
- tempScope = scope.$new(),
- value, optionElement, index, groupIndex, length, groupLength;
-
- // let's set a flag that the current model change is due to a change event.
- // the default action of option selection will cause the appropriate option element to be
- // deselected and another one to be selected - there is no need for us to be updating the DOM
- // in this case.
- inChangeEvent = true;
-
- try {
- if (isMultiselect) {
- value = [];
- for (groupIndex = 0, groupLength = optionGroupsCache.length;
- groupIndex < groupLength;
- groupIndex++) {
- // list of options for that group. (first item has the parent)
- optionGroup = optionGroupsCache[groupIndex];
-
- for(index = 1, length = optionGroup.length; index < length; index++) {
- if ((optionElement = optionGroup[index].element)[0].selected) {
- if (keyName) tempScope[keyName] = key;
- tempScope[valueName] = collection[optionElement.val()];
- value.push(valueFn(tempScope));
- }
- }
- }
- } else {
- if (key == '?') {
- value = undefined;
- } else if (key == ''){
- value = null;
- } else {
- tempScope[valueName] = collection[key];
- if (keyName) tempScope[keyName] = key;
- value = valueFn(tempScope);
- }
- }
- if (isDefined(value) && model.get() !== value) {
- model.set(value);
- onChange(scope);
- }
- scope.$root.$apply();
- } finally {
- tempScope = null; // TODO(misko): needs to be $destroy
- inChangeEvent = false;
- }
- });
-
- scope.$watch(function(scope) {
- var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
- optionGroupNames = [''],
- optionGroupName,
- optionGroup,
- option,
- existingParent, existingOptions, existingOption,
- values = valuesFn(scope) || [],
- keys = values,
- key,
- groupLength, length,
- fragment,
- groupIndex, index,
- optionElement,
- optionScope = scope.$new(),
- modelValue = model.get(),
- selected,
- selectedSet = false, // nothing is selected yet
- isMulti = isMultiselect,
- lastElement,
- element;
-
- try {
- if (isMulti) {
- selectedSet = new HashMap();
- if (modelValue && isNumber(length = modelValue.length)) {
- for (index = 0; index < length; index++) {
- selectedSet.put(modelValue[index], true);
- }
- }
- } else if (modelValue === null || nullOption) {
- // if we are not multiselect, and we are null then we have to add the nullOption
- optionGroups[''].push(extend({selected:modelValue === null, id:'', label:''}, nullOption));
- selectedSet = true;
- }
-
- // If we have a keyName then we are iterating over on object. Grab the keys and sort them.
- if(keyName) {
- keys = [];
- for (key in values) {
- if (values.hasOwnProperty(key))
- keys.push(key);
- }
- keys.sort();
- }
-
- // We now build up the list of options we need (we merge later)
- for (index = 0; length = keys.length, index < length; index++) {
- optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index];
- optionGroupName = groupByFn(optionScope) || '';
- if (!(optionGroup = optionGroups[optionGroupName])) {
- optionGroup = optionGroups[optionGroupName] = [];
- optionGroupNames.push(optionGroupName);
- }
- if (isMulti) {
- selected = !!selectedSet.remove(valueFn(optionScope));
- } else {
- selected = modelValue === valueFn(optionScope);
- selectedSet = selectedSet || selected; // see if at least one item is selected
- }
- optionGroup.push({
- id: keyName ? keys[index] : index, // either the index into array or key from object
- label: displayFn(optionScope) || '', // what will be seen by the user
- selected: selected // determine if we should be selected
- });
- }
- optionGroupNames.sort();
- if (!isMulti && !selectedSet) {
- // nothing was selected, we have to insert the undefined item
- optionGroups[''].unshift({id:'?', label:'', selected:true});
- }
-
- // Now we need to update the list of DOM nodes to match the optionGroups we computed above
- for (groupIndex = 0, groupLength = optionGroupNames.length;
- groupIndex < groupLength;
- groupIndex++) {
- // current option group name or '' if no group
- optionGroupName = optionGroupNames[groupIndex];
-
- // list of options for that group. (first item has the parent)
- optionGroup = optionGroups[optionGroupName];
-
- if (optionGroupsCache.length <= groupIndex) {
- // we need to grow the optionGroups
- optionGroupsCache.push(
- existingOptions = [
- existingParent = {
- element: optGroupTemplate.clone().attr('label', optionGroupName),
- label: optionGroup.label
- }
- ]
- );
- selectElement.append(existingParent.element);
- } else {
- existingOptions = optionGroupsCache[groupIndex];
- existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
-
- // update the OPTGROUP label if not the same.
- if (existingParent.label != optionGroupName) {
- existingParent.element.attr('label', existingParent.label = optionGroupName);
- }
- }
-
- lastElement = null; // start at the begining
- for(index = 0, length = optionGroup.length; index < length; index++) {
- option = optionGroup[index];
- if ((existingOption = existingOptions[index+1])) {
- // reuse elements
- lastElement = existingOption.element;
- if (existingOption.label !== option.label) {
- lastElement.text(existingOption.label = option.label);
- }
- if (existingOption.id !== option.id) {
- lastElement.val(existingOption.id = option.id);
- }
- if (!inChangeEvent && existingOption.selected !== option.selected) {
- lastElement.prop('selected', (existingOption.selected = option.selected));
- }
- } else {
- // grow elements
- // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
- // in this version of jQuery on some browser the .text() returns a string
- // rather then the element.
- (element = optionTemplate.clone())
- .val(option.id)
- .attr('selected', option.selected)
- .text(option.label);
- existingOptions.push(existingOption = {
- element: element,
- label: option.label,
- id: option.id,
- selected: option.selected
- });
- if (lastElement) {
- lastElement.after(element);
- } else {
- existingParent.element.append(element);
- }
- lastElement = element;
- }
- }
- // remove any excessive OPTIONs in a group
- index++; // increment since the existingOptions[0] is parent element not OPTION
- while(existingOptions.length > index) {
- existingOptions.pop().element.remove();
- }
- }
- // remove any excessive OPTGROUPs from select
- while(optionGroupsCache.length > groupIndex) {
- optionGroupsCache.pop()[0].element.remove();
- }
- } finally {
- optionScope.$destroy();
- }
- });
- };
-});
-
-
-/**
- * @workInProgress
- * @ngdoc widget
* @name angular.widget.ng:include
*
* @description
@@ -960,28 +48,36 @@ angularWidget('select', function(element){
* @example
<doc:example>
<doc:source jsfiddle="false">
- <select name="url">
- <option value="examples/ng-include/template1.html">template1.html</option>
- <option value="examples/ng-include/template2.html">template2.html</option>
- <option value="">(blank)</option>
- </select>
- url of the template: <tt><a href="{{url}}">{{url}}</a></tt>
- <hr/>
- <ng:include src="url"></ng:include>
+ <script>
+ function Ctrl(){
+ this.templates =
+ [ { name: 'template1.html', url: 'examples/ng-include/template1.html'}
+ , { name: 'template2.html', url: 'examples/ng-include/template2.html'} ];
+ this.template = this.templates[0];
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <select ng:model="template" ng:options="t.name for t in templates">
+ <option value="">(blank)</option>
+ </select>
+ url of the template: <tt><a href="{{template.url}}">{{template.url}}</a></tt>
+ <hr/>
+ <ng:include src="template.url"></ng:include>
+ </div>
</doc:source>
<doc:scenario>
it('should load template1.html', function(){
- expect(element('.doc-example-live ng\\:include').text()).
+ expect(element('.doc-example-live .ng-include').text()).
toBe('Content of template1.html\n');
});
it('should load template2.html', function(){
- select('url').option('examples/ng-include/template2.html');
- expect(element('.doc-example-live ng\\:include').text()).
+ select('template').option('1');
+ expect(element('.doc-example-live .ng-include').text()).
toBe('Content of template2.html\n');
});
it('should change to blank', function(){
- select('url').option('');
- expect(element('.doc-example-live ng\\:include').text()).toEqual('');
+ select('template').option('');
+ expect(element('.doc-example-live .ng-include').text()).toEqual('');
});
</doc:scenario>
</doc:example>
@@ -1064,30 +160,34 @@ angularWidget('ng:include', function(element){
* @example
<doc:example>
<doc:source>
- <select name="switch">
- <option>settings</option>
- <option>home</option>
- <option>other</option>
- </select>
- <tt>switch={{switch}}</tt>
- </hr>
- <ng:switch on="switch" >
- <div ng:switch-when="settings">Settings Div</div>
- <span ng:switch-when="home">Home Span</span>
- <span ng:switch-default>default</span>
- </ng:switch>
- </code>
+ <script>
+ function Ctrl(){
+ this.items = ['settings', 'home', 'other'];
+ this.selection = this.items[0];
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <select ng:model="selection" ng:options="item for item in items">
+ </select>
+ <tt>selection={{selection}}</tt>
+ <hr/>
+ <ng:switch on="selection" >
+ <div ng:switch-when="settings">Settings Div</div>
+ <span ng:switch-when="home">Home Span</span>
+ <span ng:switch-default>default</span>
+ </ng:switch>
+ </div>
</doc:source>
<doc:scenario>
it('should start in settings', function(){
expect(element('.doc-example-live ng\\:switch').text()).toEqual('Settings Div');
});
it('should change to home', function(){
- select('switch').option('home');
+ select('selection').option('home');
expect(element('.doc-example-live ng\\:switch').text()).toEqual('Home Span');
});
it('should select deafault', function(){
- select('switch').option('other');
+ select('selection').option('other');
expect(element('.doc-example-live ng\\:switch').text()).toEqual('default');
});
</doc:scenario>
@@ -1568,27 +668,36 @@ angularWidget('ng:view', function(element) {
* @example
<doc:example>
<doc:source>
- Person 1:<input type="text" name="person1" value="Igor" /><br/>
- Person 2:<input type="text" name="person2" value="Misko" /><br/>
- Number of People:<input type="text" name="personCount" value="1" /><br/>
-
- <!--- Example with simple pluralization rules for en locale --->
- Without Offset:
- <ng:pluralize count="personCount"
- when="{'0': 'Nobody is viewing.',
- 'one': '1 person is viewing.',
- 'other': '{} people are viewing.'}">
- </ng:pluralize><br>
-
- <!--- Example with offset --->
- With Offset(2):
- <ng:pluralize count="personCount" offset=2
- when="{'0': 'Nobody is viewing.',
- '1': '{{person1}} is viewing.',
- '2': '{{person1}} and {{person2}} are viewing.',
- 'one': '{{person1}}, {{person2}} and one other person are viewing.',
- 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
- </ng:pluralize>
+ <script>
+ function Ctrl(){
+ this.person1 = 'Igor';
+ this.person2 = 'Misko';
+ this.personCount = 1;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Person 1:<input type="text" ng:model="person1" value="Igor" /><br/>
+ Person 2:<input type="text" ng:model="person2" value="Misko" /><br/>
+ Number of People:<input type="text" ng:model="personCount" value="1" /><br/>
+
+ <!--- Example with simple pluralization rules for en locale --->
+ Without Offset:
+ <ng:pluralize count="personCount"
+ when="{'0': 'Nobody is viewing.',
+ 'one': '1 person is viewing.',
+ 'other': '{} people are viewing.'}">
+ </ng:pluralize><br>
+
+ <!--- Example with offset --->
+ With Offset(2):
+ <ng:pluralize count="personCount" offset=2
+ when="{'0': 'Nobody is viewing.',
+ '1': '{{person1}} is viewing.',
+ '2': '{{person1}} and {{person2}} are viewing.',
+ 'one': '{{person1}}, {{person2}} and one other person are viewing.',
+ 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
+ </ng:pluralize>
+ </div>
</doc:source>
<doc:scenario>
it('should show correct pluralized string', function(){