diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Angular.js | 105 | ||||
| -rw-r--r-- | src/Browser.js | 10 | ||||
| -rw-r--r-- | src/Scope.js | 3 | ||||
| -rw-r--r-- | src/angular-bootstrap.js | 3 | ||||
| -rw-r--r-- | src/apis.js | 173 | ||||
| -rw-r--r-- | src/directives.js | 86 | ||||
| -rw-r--r-- | src/filters.js | 113 | ||||
| -rw-r--r-- | src/formatters.js | 202 | ||||
| -rw-r--r-- | src/jqLite.js | 15 | ||||
| -rw-r--r-- | src/markups.js | 24 | ||||
| -rw-r--r-- | src/parser.js | 43 | ||||
| -rw-r--r-- | src/scenario/Scenario.js | 2 | ||||
| -rw-r--r-- | src/scenario/dsl.js | 22 | ||||
| -rw-r--r-- | src/service/formFactory.js | 394 | ||||
| -rw-r--r-- | src/service/invalidWidgets.js | 69 | ||||
| -rw-r--r-- | src/service/log.js | 3 | ||||
| -rw-r--r-- | src/service/resource.js | 3 | ||||
| -rw-r--r-- | src/service/route.js | 6 | ||||
| -rw-r--r-- | src/service/window.js | 2 | ||||
| -rw-r--r-- | src/service/xhr.js | 5 | ||||
| -rw-r--r-- | src/validators.js | 482 | ||||
| -rw-r--r-- | src/widget/form.js | 81 | ||||
| -rw-r--r-- | src/widget/input.js | 773 | ||||
| -rw-r--r-- | src/widget/select.js | 427 | ||||
| -rw-r--r-- | src/widgets.js | 1033 | 
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"> -     <p style="color:blue">an html -     <em onmouseover="this.textContent='PWN3D!'">click here</em> -     snippet</p></textarea> -       <table> -         <tr> -           <td>Filter</td> -           <td>Source</td> -           <td>Rendered</td> -         </tr> -         <tr id="html-filter"> -           <td>html filter</td> -           <td> -             <pre><div ng:bind="snippet | html"><br/></div></pre> -           </td> -           <td> -             <div ng:bind="snippet | html"></div> -           </td> -         </tr> -         <tr id="escaped-html"> -           <td>no filter</td> -           <td><pre><div ng:bind="snippet"><br/></div></pre></td> -           <td><div ng:bind="snippet"></div></td> -         </tr> -         <tr id="html-unsafe-filter"> -           <td>unsafe html filter</td> -           <td><pre><div ng:bind="snippet | html:'unsafe'"><br/></div></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><div ng:bind="snippet | html"><br/></div></pre> +               </td> +               <td> +                 <div ng:bind="snippet | html"></div> +               </td> +             </tr> +             <tr id="escaped-html"> +               <td>no filter</td> +               <td><pre><div ng:bind="snippet"><br/></div></pre></td> +               <td><div ng:bind="snippet"></div></td> +             </tr> +             <tr id="html-unsafe-filter"> +               <td>unsafe html filter</td> +               <td><pre><div ng:bind="snippet | html:'unsafe'"><br/></div></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><input type="text" name="input1"></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><textarea name="input2"></textarea></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> -              <input type="radio" name="input3" value="A"><br> -              <input type="radio" name="input3" value="B"> -            </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><input type="checkbox" name="input4" value="checked"></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> -              <select name="input5"><br> -                <option value="c">C</option><br> -                <option value="d">D</option><br> -              </select><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> -              <select name="input6" multiple size="4"><br> -                <option value="e">E</option><br> -                <option value="f">F</option><br> -              </select><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(){ | 
