diff options
| author | Misko Hevery | 2011-09-08 13:56:29 -0700 | 
|---|---|---|
| committer | Igor Minar | 2011-10-11 11:01:45 -0700 | 
| commit | 4f78fd692c0ec51241476e6be9a4df06cd62fdd6 (patch) | |
| tree | 91f70bb89b9c095126fbc093f51cedbac5cb0c78 | |
| parent | df6d2ba3266de405ad6c2f270f24569355706e76 (diff) | |
| download | angular.js-4f78fd692c0ec51241476e6be9a4df06cd62fdd6.tar.bz2 | |
feat(forms): new and improved forms
104 files changed, 7051 insertions, 3970 deletions
| @@ -77,66 +77,8 @@ task :compile_jstd_scenario_adapter => :init do  end -desc 'Generate IE css js patch' -task :generate_ie_compat => :init do -  css = File.open('css/angular.css', 'r') {|f| f.read } - -  # finds all css rules that contain backround images and extracts the rule name(s), content type of -  # the image and base64 encoded image data -  r = /\n([^\{\n]+)\s*\{[^\}]*background-image:\s*url\("data:([^;]+);base64,([^"]+)"\);[^\}]*\}/ - -  images = css.scan(r) - -  # create a js file with multipart header containing the extracted images. the entire file *must* -  # be CRLF (\r\n) delimited -  File.open(path_to('angular-ie-compat.js'), 'w') do |f| -    f.write("/*\r\n" + -            "Content-Type: multipart/related; boundary=\"_\"\r\n" + -            "\r\n") - -    images.each_index do |idx| -      f.write("--_\r\n" + -              "Content-Location:img#{idx}\r\n" + -              "Content-Transfer-Encoding:base64\r\n" + -              "\r\n" + -              images[idx][2] + "\r\n") -    end - -    f.write("--_--\r\n" + -            "*/\r\n") - -    # generate a css string containing *background-image rules for IE that point to the mime type -    # images in the header -    cssString = '' -    images.each_index do |idx| -      cssString += "#{images[idx][0]}{*background-image:url(\"mhtml:' + jsUri + '!img#{idx}\")}" -    end - -    # generate a javascript closure that contains a function which will append the generated css -    # string as a stylesheet to the current html document -    jsString = "(function(){ \r\n" + -               "  var jsUri = document.location.href.replace(/\\/[^\\\/]+(#.*)?$/, '/') + \r\n" + -               "              document.getElementById('ng-ie-compat').src,\r\n" + -               "      css = '#{cssString}',\r\n" + -               "      s = document.createElement('style'); \r\n" + -               "\r\n" + -               "  s.setAttribute('type', 'text/css'); \r\n" + -               "\r\n" + -               "  if (s.styleSheet) { \r\n" + -               "    s.styleSheet.cssText = css; \r\n" + -               "  } else { \r\n" + -               "    s.appendChild(document.createTextNode(css)); \r\n" + -               "  } \r\n" + -               "  document.getElementsByTagName('head')[0].appendChild(s); \r\n" + -               "})();\r\n" - -    f.write(jsString) -  end -end - -  desc 'Compile JavaScript' -task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter, :generate_ie_compat] do +task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter] do    deps = [        'src/angular.prefix', @@ -193,7 +135,6 @@ task :package => [:clean, :compile, :docs] do    ['src/angular-mocks.js',      path_to('angular.js'),      path_to('angular.min.js'), -    path_to('angular-ie-compat.js'),      path_to('angular-scenario.js'),      path_to('jstd-scenario-adapter.js'),      path_to('jstd-scenario-adapter-config.js'), diff --git a/angularFiles.js b/angularFiles.js index 8e52731b..906c3f98 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -12,14 +12,12 @@ angularFiles = {      'src/jqLite.js',      'src/apis.js',      'src/filters.js', -    'src/formatters.js', -    'src/validators.js',      'src/service/cookieStore.js',      'src/service/cookies.js',      'src/service/defer.js',      'src/service/document.js',      'src/service/exceptionHandler.js', -    'src/service/invalidWidgets.js', +    'src/service/formFactory.js',      'src/service/location.js',      'src/service/log.js',      'src/service/resource.js', @@ -35,6 +33,9 @@ angularFiles = {      'src/directives.js',      'src/markups.js',      'src/widgets.js', +    'src/widget/form.js', +    'src/widget/input.js', +    'src/widget/select.js',      'src/AngularPublic.js',    ], @@ -74,6 +75,7 @@ angularFiles = {      'test/jstd-scenario-adapter/*.js',      'test/*.js',      'test/service/*.js', +    'test/widget/*.js',      'example/personalLog/test/*.js'    ], diff --git a/css/angular.css b/css/angular.css index 89519da6..d1146215 100644 --- a/css/angular.css +++ b/css/angular.css @@ -7,12 +7,3 @@  .ng-format-negative {    color: red;  } - -/***************** - * indicators - *****************/ -.ng-input-indicator-wait { -  background-image: url("data:image/png;base64,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"); -  background-position: right; -  background-repeat: no-repeat; -} diff --git a/docs/content/api/angular.inputType.ngdoc b/docs/content/api/angular.inputType.ngdoc new file mode 100644 index 00000000..434fe6c2 --- /dev/null +++ b/docs/content/api/angular.inputType.ngdoc @@ -0,0 +1,92 @@ +@ngdoc overview +@name angular.inputType +@description + +Angular {@link guide/dev_guide.forms forms} allow you to build complex widgets. However for +simple widget which are based on HTML input text element a simpler way of providing the validation +and parsing is also provided. `angular.inputType` is a short hand for creating a widget which +already has the DOM listeners and `$render` method supplied. The only thing which needs to +be provided by the developer are the optional `$validate` listener and +`$parseModel` or `$parseModel` methods. + +All `inputType` widgets support: + +  - CSS classes: +    - **`ng-valid`**: when widget is valid. +    - **`ng-invalid`**: when widget is invalid. +    - **`ng-pristine`**: when widget has not been modified by user action. +    - **`ng-dirty`**: when has been modified do to user action. + +  - Widget properties: +    - **`$valid`**: When widget is valid. +    - **`$invalid`**: When widget is invalid. +    - **`$pristine`**: When widget has not been modified by user interaction. +    - **`$dirty`**: When user has been modified do to user interaction. +    - **`$required`**: When the `<input>` element has `required` attribute. This means that the +       widget will have `REQUIRED` validation error if empty. +    - **`$disabled`**: When the `<input>` element has `disabled` attribute. +    - **`$readonly`**: When the `<input>` element has `readonly` attribute. + +  - Widget Attribute Validators: +    - **`required`**: Sets `REQUIRED` validation error key if the input is empty +    - **`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> +     angular.inputType('json', function(){ +       this.$parseView = function(){ +         try { +           this.$modelValue = angular.fromJson(this.$viewValue); +           if (this.$error.JSON) { +             this.$emit('$valid', 'JSON'); +           } +         } catch (e) { +             this.$emit('$invalid', 'JSON'); +         } +       } + +       this.$parseModel = function(){ +         this.$viewValue = angular.toJson(this.$modelValue); +       } +     }); + +     function Ctrl(){ +       this.data = { +         framework:'angular', +         codenames:'supper-powers' +       } +       this.required = false; +       this.disabled = false; +       this.readonly = false; +     } +   </script> +   <div ng:controller="Ctrl"> +     <form name="myForm"> +       <input type="json" ng:model="data" size="80" +              ng:required="{{required}}" ng:disabled="{{disabled}}" +              ng:readonly="{{readonly}}"/><br/> +       Required: <input type="checkbox" ng:model="required"> <br/> +       Disabled: <input type="checkbox" ng:model="disabled"> <br/> +       Readonly: <input type="checkbox" ng:model="readonly"> <br/> +      <pre>data={{data}}</pre> +       <pre>myForm={{myForm}}</pre> +     </form> +   </div> +</doc:source> +<doc:scenario> +  it('should invalidate on wrong input', function(){ +    expect(element('form[name=myForm]').prop('className')).toMatch('ng-valid'); +    input('data').enter('{}'); +    expect(binding('data')).toEqual('data={\n  }'); +    input('data').enter('{'); +    expect(element('form[name=myForm]').prop('className')).toMatch('ng-invalid'); +  }); +</doc:scenario> +</doc:example> diff --git a/docs/content/api/angular.service.ngdoc b/docs/content/api/angular.service.ngdoc index 874fe4bb..50fe1560 100644 --- a/docs/content/api/angular.service.ngdoc +++ b/docs/content/api/angular.service.ngdoc @@ -14,8 +14,6 @@ session cookies  * {@link angular.service.$document $document } - Provides reference to `window.document` element  * {@link angular.service.$exceptionHandler $exceptionHandler } - Receives uncaught angular  exceptions -* {@link angular.service.$hover $hover } - -* {@link angular.service.$invalidWidgets $invalidWidgets } - Holds references to invalid widgets  * {@link angular.service.$location $location } - Parses the browser location URL  * {@link angular.service.$log $log } - Provides logging service  * {@link angular.service.$resource $resource } - Creates objects for interacting with RESTful diff --git a/docs/content/api/index.ngdoc b/docs/content/api/index.ngdoc index 05928ab4..2ec86346 100644 --- a/docs/content/api/index.ngdoc +++ b/docs/content/api/index.ngdoc @@ -8,8 +8,6 @@  * {@link angular.directive Directives} - Angular DOM element attributes  * {@link angular.markup Markup} and {@link angular.attrMarkup Attribute Markup}  * {@link angular.filter Filters} - Angular output filters -* {@link angular.formatter Formatters} - Angular converters for form elements -* {@link angular.validator Validators} - Angular input validators  * {@link angular.compile angular.compile()} - Template compiler  ## Angular Scope API diff --git a/docs/content/cookbook/advancedform.ngdoc b/docs/content/cookbook/advancedform.ngdoc index 585c66a6..d38008f2 100644 --- a/docs/content/cookbook/advancedform.ngdoc +++ b/docs/content/cookbook/advancedform.ngdoc @@ -9,9 +9,7 @@ detection, and preventing invalid form submission.  <doc:example>  <doc:source>     <script> -   UserForm.$inject = ['$invalidWidgets']; -   function UserForm($invalidWidgets){ -     this.$invalidWidgets = $invalidWidgets; +   function UserForm(){       this.state = /^\w\w$/;       this.zip = /^\d\d\d\d\d$/;       this.master = { @@ -42,31 +40,34 @@ detection, and preventing invalid form submission.     </script>     <div ng:controller="UserForm"> -     <label>Name:</label><br/> -     <input type="text" name="form.name" ng:required/> <br/><br/> +     <form name="myForm"> -     <label>Address:</label><br/> -     <input type="text" name="form.address.line1" size="33" ng:required/> <br/> -     <input type="text" name="form.address.city" size="12" ng:required/>, -     <input type="text" name="form.address.state" size="2" ng:required ng:validate="regexp:state"/> -     <input type="text" name="form.address.zip" size="5" ng:required -ng:validate="regexp:zip"/><br/><br/> +       <label>Name:</label><br/> +       <input type="text" ng:model="form.name" required/> <br/><br/> -     <label>Contacts:</label> -     [ <a href="" ng:click="form.contacts.$add()">add</a> ] -     <div ng:repeat="contact in form.contacts"> -       <select name="contact.type"> -         <option>email</option> -         <option>phone</option> -         <option>pager</option> -         <option>IM</option> -       </select> -       <input type="text" name="contact.value" ng:required/> -        [ <a href="" ng:click="form.contacts.$remove(contact)">X</a> ] -     </div> -   <button ng:click="cancel()" ng:disabled="{{master.$equals(form)}}">Cancel</button> -   <button ng:click="save()" ng:disabled="{{$invalidWidgets.visible() || -master.$equals(form)}}">Save</button> +       <label>Address:</label> <br/> +       <input type="text" ng:model="form.address.line1" size="33" required/> <br/> +       <input type="text" ng:model="form.address.city" size="12" required/>, +       <input type="text" ng:model="form.address.state" size="2" +              ng:pattern="state" required/> +       <input type="text" ng:model="form.address.zip" size="5" +              ng:pattern="zip" required/><br/><br/> + +       <label>Contacts:</label> +       [ <a href="" ng:click="form.contacts.$add()">add</a> ] +       <div ng:repeat="contact in form.contacts"> +         <select ng:model="contact.type"> +           <option>email</option> +           <option>phone</option> +           <option>pager</option> +           <option>IM</option> +         </select> +         <input type="text" ng:model="contact.value" required/> +          [ <a href="" ng:click="form.contacts.$remove(contact)">X</a> ] +       </div> +     <button ng:click="cancel()" ng:disabled="{{master.$equals(form)}}">Cancel</button> +     <button ng:click="save()" ng:disabled="{{myForm.$invalid || master.$equals(form)}}">Save</button> +   </form>     <hr/>     Debug View: @@ -90,7 +91,7 @@ master.$equals(form)}}">Save</button>      expect(element(':button:contains(Cancel)').attr('disabled')).toBeFalsy();      element(':button:contains(Cancel)').click();      expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy(); -    expect(element(':input[name="form.name"]').val()).toEqual('John Smith'); +    expect(element(':input[ng\\:model="form.name"]').val()).toEqual('John Smith');    });  </doc:scenario>  </doc:example> diff --git a/docs/content/cookbook/buzz.ngdoc b/docs/content/cookbook/buzz.ngdoc index a1e4a8b2..fad4c1ff 100644 --- a/docs/content/cookbook/buzz.ngdoc +++ b/docs/content/cookbook/buzz.ngdoc @@ -15,6 +15,7 @@ to retrieve Buzz activity and comments.      <script>      BuzzController.$inject = ['$resource'];      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'}, @@ -32,7 +33,7 @@ to retrieve Buzz activity and comments.      };      </script>      <div ng:controller="BuzzController"> -     <input name="userId" value="googlebuzz"/> +     <input ng:model="userId"/>       <button ng:click="fetch()">fetch</button>       <hr/>       <div class="buzz" ng:repeat="item in activities.data.items"> diff --git a/docs/content/cookbook/form.ngdoc b/docs/content/cookbook/form.ngdoc index 2aeafc4d..c74b203b 100644 --- a/docs/content/cookbook/form.ngdoc +++ b/docs/content/cookbook/form.ngdoc @@ -24,25 +24,26 @@ allow a user to enter data.    <div ng:controller="FormController" class="example">      <label>Name:</label><br/> -    <input type="text" name="user.name" ng:required/> <br/><br/> +    <input type="text" ng:model="user.name" required/> <br/><br/>      <label>Address:</label><br/> -    <input type="text" name="user.address.line1" size="33" ng:required/> <br/> -    <input type="text" name="user.address.city" size="12" ng:required/>, -    <input type="text" name="user.address.state" size="2" ng:required ng:validate="regexp:state"/> -    <input type="text" name="user.address.zip" size="5" ng:required -ng:validate="regexp:zip"/><br/><br/> +    <input type="text" ng:model="user.address.line1" size="33" required> <br/> +    <input type="text" ng:model="user.address.city" size="12" required>, +    <input type="text" ng:model="user.address.state" size="2" +           ng:pattern="state" required> +    <input type="text" ng:model="user.address.zip" size="5" +           ng:pattern="zip"  required><br/><br/>      <label>Phone:</label>      [ <a href="" ng:click="user.contacts.$add()">add</a> ]      <div ng:repeat="contact in user.contacts"> -      <select name="contact.type"> +      <select ng:model="contact.type">          <option>email</option>          <option>phone</option>          <option>pager</option>          <option>IM</option>        </select> -      <input type="text" name="contact.value" ng:required/> +      <input type="text" ng:model="contact.value" required/>         [ <a href="" ng:click="user.contacts.$remove(contact)">X</a> ]      </div>      <hr/> @@ -68,19 +69,21 @@ ng:validate="regexp:zip"/><br/><br/>    });    it('should validate zip', function(){ -    expect(using('.example').element(':input[name="user.address.zip"]').prop('className')) -      .not().toMatch(/ng-validation-error/); +    expect(using('.example'). +      element(':input[ng\\:model="user.address.zip"]'). +      prop('className')).not().toMatch(/ng-invalid/);      using('.example').input('user.address.zip').enter('abc'); -    expect(using('.example').element(':input[name="user.address.zip"]').prop('className')) -      .toMatch(/ng-validation-error/); +    expect(using('.example'). +      element(':input[ng\\:model="user.address.zip"]'). +      prop('className')).toMatch(/ng-invalid/);    });    it('should validate state', function(){ -    expect(using('.example').element(':input[name="user.address.state"]').prop('className')) -      .not().toMatch(/ng-validation-error/); +    expect(using('.example').element(':input[ng\\:model="user.address.state"]').prop('className')) +      .not().toMatch(/ng-invalid/);      using('.example').input('user.address.state').enter('XXX'); -    expect(using('.example').element(':input[name="user.address.state"]').prop('className')) -      .toMatch(/ng-validation-error/); +    expect(using('.example').element(':input[ng\\:model="user.address.state"]').prop('className')) +      .toMatch(/ng-invalid/);    });   </doc:scenario>  </doc:example> @@ -94,7 +97,7 @@ available in  * For debugging purposes we have included a debug view of the model to better understand what    is going on.  * The {@link api/angular.widget.HTML input widgets} simply refer to the model and are auto bound. -* The inputs {@link api/angular.validator validate}. (Try leaving them blank or entering non digits +* The inputs {@link guide/dev_guide.forms validate}. (Try leaving them blank or entering non digits    in the zip field)  * In your application you can simply read from or write to the model and the form will be updated.  * By clicking the 'add' link you are adding new items into the `user.contacts` array which are then diff --git a/docs/content/cookbook/helloworld.ngdoc b/docs/content/cookbook/helloworld.ngdoc index 8018a399..9562aaff 100644 --- a/docs/content/cookbook/helloworld.ngdoc +++ b/docs/content/cookbook/helloworld.ngdoc @@ -5,9 +5,16 @@  <doc:example>   <doc:source> -  Your name: <input type="text" name="name" value="World"/> -  <hr/> -  Hello {{name}}! +  <script> +    function HelloCntl(){ +      this.name = 'World'; +    } +  </script> +  <div ng:controller="HelloCntl"> +    Your name: <input type="text" ng:model="name" value="World"/> +    <hr/> +    Hello {{name}}! +  </div>   </doc:source>   <doc:scenario>     it('should change the binding when user enters text', function(){ diff --git a/docs/content/guide/dev_guide.compiler.directives.ngdoc b/docs/content/guide/dev_guide.compiler.directives.ngdoc index 0f99e46b..3b233551 100644 --- a/docs/content/guide/dev_guide.compiler.directives.ngdoc +++ b/docs/content/guide/dev_guide.compiler.directives.ngdoc @@ -16,7 +16,7 @@ directives per element.  You add angular directives to a standard HTML tag as in the following example, in which we have  added the {@link api/angular.directive.ng:click ng:click} directive to a button tag: -        <button name="button1" ng:click="foo()">Click This</button> +        <button ng:model="button1" ng:click="foo()">Click This</button>  In the example above, `name` is the standard HTML attribute, and `ng:click` is the angular  directive. The `ng:click` directive lets you implement custom behavior in an associated controller diff --git a/docs/content/guide/dev_guide.expressions.ngdoc b/docs/content/guide/dev_guide.expressions.ngdoc index 177a5e87..ab5a897b 100644 --- a/docs/content/guide/dev_guide.expressions.ngdoc +++ b/docs/content/guide/dev_guide.expressions.ngdoc @@ -51,9 +51,15 @@ You can try evaluating different expressions here:  <doc:example>  <doc:source> - <div ng:init="exprs=[]" class="expressions"> + <script> +   function Cntl2(){ +     this.exprs = []; +     this.expr = '3*10|currency'; +   } + </script> + <div ng:controller="Cntl2" class="expressions">     Expression: -   <input type='text' name="expr" value="3*10|currency" size="80"/> +   <input type='text' ng:model="expr" size="80"/>     <button ng:click="exprs.$add(expr)">Evaluate</button>     <ul>      <li ng:repeat="expr in exprs"> @@ -84,9 +90,18 @@ the global state (a common source of subtle bugs).  <doc:example>  <doc:source> - <div class="example2" ng:init="$window = $service('$window')"> -   Name: <input name="name" type="text" value="World"/> -   <button ng:click="($window.mockWindow || $window).alert('Hello ' + name)">Greet</button> + <script> +   function Cntl1($window){ +     this.name = 'World'; + +     this.greet = function() { +       ($window.mockWindow || $window).alert('Hello ' + this.name); +     } +   } + </script> + <div class="example2" ng:controller="Cntl1"> +   Name: <input ng:model="name" type="text"/> +   <button ng:click="greet()">Greet</button>   </div>  </doc:source>  <doc:scenario> @@ -158,7 +173,7 @@ Extensions: You can further extend the expression vocabulary by adding new metho     {name:'Mike', phone:'555-4321'},     {name:'Adam', phone:'555-5678'},     {name:'Julie', phone:'555-8765'}]"></div> - Search: <input name="searchText"/> + Search: <input ng:model="searchText"/>   <table class="example3">     <tr><th>Name</th><th>Phone</th><tr>     <tr ng:repeat="friend in friends.$filter(searchText)"> diff --git a/docs/content/guide/dev_guide.forms.ngdoc b/docs/content/guide/dev_guide.forms.ngdoc new file mode 100644 index 00000000..6849ff4e --- /dev/null +++ b/docs/content/guide/dev_guide.forms.ngdoc @@ -0,0 +1,610 @@ +@ngdoc overview +@name Developer Guide: Forms +@description + +# Overview + +Forms allow users to enter data into your application. Forms represent the bidirectional data +bindings in Angular. + +Forms consist of all of the following: + +  - the individual widgets with which users interact +  - the validation rules for widgets +  - the form, a collection of widgets that contains aggregated validation information + + +# Form + +A form groups a set of widgets together into a single logical data-set. A form is created using +the {@link api/angular.widget.form <form>} element that calls the +{@link api/angular.service.$formFactory $formFactory} service. The form is responsible for managing +the widgets and for tracking validation information. + +A form is: + +- The collection which contains widgets or other forms. +- Responsible for marshaling data from the model into a widget. This is +  triggered by {@link api/angular.scope.$watch $watch} of the model expression. +- Responsible for marshaling data from the widget into the model. This is +  triggered by the widget emitting the `$viewChange` event. +- Responsible for updating the validation state of the widget, when the widget emits +  `$valid` / `$invalid` event. The validation state is useful for controlling the validation +   errors shown to the user in it consist of: + +  - `$valid` / `$invalid`: Complementary set of booleans which show if a widget is valid / invalid. +  - `$error`: an object which has a property for each validation key emited by the widget. +              The value of the key is always true. If widget is valid, then the `$error` +              object has no properties. For example if the widget emits +              `$invalid` event with `REQUIRED` key. The internal state of the `$error` would be +              updated to `$error.REQUIRED == true`. + +- Responsible for aggregating widget validation information into the form. + +  - `$valid` / `$invalid`: Complementary set of booleans which show if all the child widgets +              (or forms) are valid or if any are invalid. +  - `$error`: an object which has a property for each validation key emited by the +              child widget. The value of the key is an array of widgets which fired the invalid +              event. If all child widgets are valid then, then the `$error` object has no +              properties. For example if a child widget emits +              `$invalid` event with `REQUIRED` key. The internal state of the `$error` would be +              updated to `$error.REQUIRED == [ widgetWhichEmitedInvalid ]`. + + +# Widgets + +In Angular, a widget is the term used for the UI with which the user input. Examples of +bult-in Angular widgets are {@link api/angular.widget.input input} and +{@link api/angular.widget.select select}. Widgets provide the rendering and the user +interaction logic. Widgets should be declared inside a form, if no form is provided an implicit +form {@link api/angular.service.$formFactory $formFactory.rootForm} form is used. + +Widgets are implemented as Angular controllers. A widget controller: + +-  implements methods: + +  - `$render` - Updates the DOM from the internal state as represented by `$viewValue`. +  - `$parseView` - Translate `$viewValue` to `$modelValue`. (`$modelValue` will be assigned to +     the model scope by the form) +  - `$parseModel` - Translate `$modelValue` to `$viewValue`. (`$viewValue` will be assigned to +     the DOM inside the `$render` method) + +- responds to events: + +  - `$validate` - Emitted by the form when the form determines that the widget needs to validate +    itself. There may be more then one listener on the `$validate` event. The widget responds +    by emitting `$valid` / `$invalid` event of its own. + +- emits events: + +  - `$viewChange` - Emitted when the user interacts with the widget and it is necessary to update +     the model. +  - `$valid` - Emitted when the widget determines that it is valid (usually as a response to +    `$validate` event or inside `$parseView()` or `$parseModel()` method). +  - `$invalid` - Emitted when the widget determines that it is invalid (usually as a response to +    `$validate` event or inside `$parseView()` or `$parseModel()` method). +  - `$destroy` - Emitted when the widget element is removed from the DOM. + + +# CSS + +Angular-defined widgets and forms set `ng-valid` and `ng-invalid` classes on themselves to allow +the web-designer a way to style them. If you write your own widgets, then their `$render()` +methods must set the appropriate CSS classes to allow styling. +(See {@link dev_guide.templates.css-styling CSS}) + + +# Example + +The following example demonstrates: + +  - How an error is displayed when a required field is empty. +  - Error highlighting. +  - How form submission is disabled when the form is invalid. +  - The internal state of the widget and form in the the 'Debug View' area. + + +<doc:example> +<doc:source> +   <style> +     .ng-invalid { border: solid 1px red; } +     .ng-form {display: block;} +   </style> +   <script> +   function UserFormCntl(){ +     this.state = /^\w\w$/; +     this.zip = /^\d\d\d\d\d$/; +     this.master = { +       customer: 'John Smith', +       address:{ +         line1: '123 Main St.', +         city:'Anytown', +         state:'AA', +         zip:'12345' +       } +     }; +     this.cancel(); +   } + +   UserFormCntl.prototype = { +     cancel: function(){ +       this.form = angular.copy(this.master); +     }, + +     save: function(){ +       this.master = this.form; +       this.cancel(); +     } +   }; +   </script> +   <div ng:controller="UserFormCntl"> + +     <form name="userForm"> + +       <label>Name:</label><br/> +       <input type="text" name="customer" ng:model="form.customer" required/> +       <span class="error" ng:show="userForm.customer.$error.REQUIRED"> +         Customer name is required!</span> +       <br/><br/> + +       <ng:form name="addressForm"> +         <label>Address:</label> <br/> +         <input type="text" name="line1" size="33" required +                ng:model="form.address.line1"/> <br/> +         <input type="text" name="city" size="12" required +                ng:model="form.address.city"/>, +         <input type="text" name="state" ng:pattern="state" size="2" required +                ng:model="form.address.state"/> +         <input type="text" name="zip" ng:pattern="zip" size="5" required +                ng:model="form.address.zip"/><br/><br/> + +         <span class="error" ng:show="addressForm.$invalid"> +           Incomplete address: +           <div class="error" ng:show="addressForm.state.$error.REQUIRED"> +             Missing state!</span> +           <div class="error" ng:show="addressForm.state.$error.PATTERN"> +             Invalid state!</span> +           <div class="error" ng:show="addressForm.zip.$error.REQUIRED"> +             Missing zip!</span> +           <div class="error" ng:show="addressForm.zip.$error.PATTERN"> +             Invalid zip!</span> +         </span> +       </ng:form> + +       <button ng:click="cancel()" +               ng:disabled="{{master.$equals(form)}}">Cancel</button> +       <button ng:click="save()" +               ng:disabled="{{userForm.$invalid || master.$equals(form)}}"> +          Save</button> +     </form> + +     <hr/> +     Debug View: +     <pre>form={{form}}</pre> +     <pre>master={{master}}</pre> +     <pre>userForm={{userForm}}</pre> +     <pre>addressForm={{addressForm}}</pre> +   </div> +</doc:source> +<doc:scenario> +  it('should enable save button', function(){ +    expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy(); +    input('form.customer').enter(''); +    expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy(); +    input('form.customer').enter('change'); +    expect(element(':button:contains(Save)').attr('disabled')).toBeFalsy(); +    element(':button:contains(Save)').click(); +    expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy(); +  }); +  it('should enable cancel button', function(){ +    expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy(); +    input('form.customer').enter('change'); +    expect(element(':button:contains(Cancel)').attr('disabled')).toBeFalsy(); +    element(':button:contains(Cancel)').click(); +    expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy(); +    expect(element(':input[ng\\:model="form.customer"]').val()).toEqual('John Smith'); +  }); +</doc:scenario> +</doc:example> + +# Life-cycle + +- The `<form>` element triggers creation of a new form {@link dev_guide.scopes scope} using the +  {@link api/angular.service.$formFactory $formfactory}. The new form scope is added to the +  `<form>` element using the jQuery `.data()` method for later retrieval under the key `$form`. +  The form also sets up these listeners: + +  - `$destroy` - This event is emitted by nested widget when it is removed from the view. It gives +     the form a chance to clean up any validation references to the destroyed widget. +  - `$valid` / `$invalid` - This event is emitted by the widget on validation state change. + +- `<input>` element triggers the creation of the widget using the +  {@link api/angular.service.$formFactory $formfactory.$createWidget()} method. The `$createWidget()` +  creates new widget instance by calling the current scope {@link api/angular.scope.$new .$new()} and +  registers these listeners: + +  - `$watch` on the model scope. +  - `$viewChange` event on the widget scope. +  - `$validate` event on the widget scope. +  - Element `change` event when the user enters data. + +<img class="center" src="img/form_data_flow.png" border="1" /> + + +- When the user interacts with the widget: + +  1. The DOM element fires the `change` event which the widget intercepts. Widget then emits +     a `$viewChange` event which includes the new user-entered value. (Remember that the DOM events +     are outside of the Angular environment so the widget must emit its event within the +     {@link api/angular.scope.$apply $apply} method). +  2. The form's `$viewChange` listener copies the user-entered value to the widget's `$viewValue` +     property. Since the `$viewValue` is the raw value as entered by user, it may need to be +     translated to a different format/type (for example, translating a string to a number). +     If you need your widget to translate between the internal `$viewValue` and the external +     `$modelValue` state, you must declare a `$parseView()` method. The `$parseView()` method +     will copy `$viewValue` to `$modelValue` and perform any necessary translations. +  3. The `$modelValue` is written into the application model. +  4. The form then emits a `$validate` event, giving the widget's validators chance to validate the +     input. There can be any number of validators registered. Each validator may in turn +     emit a `$valid` / `$invalid` event with the validator's validation key. For example `REQUIRED`. +  5. Form listens to `$valid`/`$invalid` events and updates both the form as well as the widget +     scope with the validation state. The validation updates the `$valid` and `$invalid`, property +     as well as `$error` object. The widget's `$error` object is updated with the validation key +     such that `$error.REQUIRED == true` when the validation emits `$invalid` with `REQUIRED` +     validation key. Similarly the form's `$error` object gets updated, but instead of boolean +     `true` it contains an array of invalid widgets (widgets which fired `$invalid` event with +     `REQUIRED` validation key). + +- When the model is updated: + +  1. The model `$watch` listener assigns the model value to `$modelValue` on the widget. +  2. The form then calls `$parseModel` method on widget if present. The method converts the +     value to renderable format and assigns it to `$viewValue` (for example converting number to a +     string.) +  3. The form then emits a `$validate` which behaves as described above. +  4. The form then calls `$render` method on the widget to update the DOM structure from the +     `$viewValue`. + + + +# Writing Your Own Widget + +This example shows how to implement a custom HTML editor widget in Angular. + +    <doc:example> +      <doc:source> +        <script> +          function EditorCntl(){ +            this.htmlContent = '<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:html-editor-model', function(){ +            function linkFn($formFactory, element) { +              var exp = element.attr('ng:html-editor-model'), +                  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:html-editor-model="htmlContent"></div> +          <hr/> +          HTML: <br/> +          <textarea ng:model="htmlContent" 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('htmlContent').enter('<'); +          expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/); +        }); +      </doc:scenario> +    </doc:example> + + + +# HTML Inputs + +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 `required` attribute to further control their +validation. +<doc:example> +  <doc:source> +     <script> +       function Ctrl(){ +         this.input1 = ''; +         this.input2 = ''; +         this.input3 = 'A'; +         this.input4 = false; +         this.input5 = 'c'; +         this.input6 = []; +       } +     </script> +    <table style="font-size:.9em;" ng:controller="Ctrl"> +      <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" ng:model="input1"></tt></td> +        <td><input type="text" ng:model="input1" size="4"></td> +        <td><tt>{{input1|json}}</tt></td> +      </tr> +      <tr> +        <th>textarea</th> +        <td>String</td> +        <td><tt><textarea ng:model="input2"></textarea></tt></td> +        <td><textarea ng:model="input2" cols='6'></textarea></td> +        <td><tt>{{input2|json}}</tt></td> +      </tr> +      <tr> +        <th>radio</th> +        <td>String</td> +        <td><tt> +          <input type="radio" ng:model="input3" value="A"><br> +          <input type="radio" ng:model="input3" value="B"> +        </tt></td> +        <td> +          <input type="radio" ng:model="input3" value="A"> +          <input type="radio" ng:model="input3" value="B"> +        </td> +        <td><tt>{{input3|json}}</tt></td> +      </tr> +      <tr> +        <th>checkbox</th> +        <td>Boolean</td> +        <td><tt><input type="checkbox" ng:model="input4"></tt></td> +        <td><input type="checkbox" ng:model="input4"></td> +        <td><tt>{{input4|json}}</tt></td> +      </tr> +      <tr> +        <th>pulldown</th> +        <td>String</td> +        <td><tt> +          <select ng:model="input5"><br> +            <option value="c">C</option><br> +            <option value="d">D</option><br> +          </select><br> +        </tt></td> +        <td> +          <select ng:model="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 ng:model="input6" multiple size="4"><br> +            <option value="e">E</option><br> +            <option value="f">F</option><br> +          </select><br> +        </tt></td> +        <td> +          <select ng:model="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('"A"'); +     input('input3').select('B'); +     expect(binding('input3')).toEqual('"B"'); +     input('input3').select('A'); +     expect(binding('input3')).toEqual('"A"'); +    }); +    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> + +#Testing + +When unit-testing a controller it may be desirable to have a reference to form and to simulate +different form validation states. + +This example demonstrates a login form, where the login button is enabled only when the form is +properly filled out. +<pre> +  <div ng:controller="LoginController"> +    <form name="loginForm"> +      <input type="text" ng:model="username" required/> +      <input type="password" ng:model="password" required/> +      <button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login> +    </form> +  </div> +</pre> + +In the unit tests we do not have access to the DOM, and therefore the `loginForm` reference does +not get set on the controller. This example shows how it can be unit-tested, by creating a mock +form. +<pre> +function LoginController() { +  this.disableLogin = function() { +    return this.loginForm.$invalid; +  }; +} + +describe('LoginController', function() { +  it('should disable login button when form is invalid', function() { +    var scope = angular.scope(); +    var loginController = scope.$new(LoginController); + +    // In production the 'loginForm' form instance gets set from the view, +    // but in unit-test we have to set it manually. +    loginController.loginForm = scope.$service('$formFactory')(); + +    expect(loginController.disableLogin()).toBe(false); + +    // Now simulate an invalid form +    loginController.loginForm.$emit('$invalid', 'MyReason'); +    expect(loginController.disableLogin()).toBe(true); + +    // Now simulate a valid form +    loginController.loginForm.$emit('$valid', 'MyReason'); +    expect(loginController.disableLogin()).toBe(false); +  }); +}); +</pre> + +## Custom widgets + +This example demonstrates a login form, where the password has custom validation rules. +<pre> +  <div ng:controller="LoginController"> +    <form name="loginForm"> +      <input type="text" ng:model="username" required/> +      <input type="@StrongPassword" ng:model="password" required/> +      <button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login> +    </form> +  </div> +</pre> + +In the unit tests we do not have access to the DOM, and therefore the `loginForm` and custom +input type reference does not get set on the controller. This example shows how it can be +unit-tested, by creating a mock form and a mock custom input type. +<pre> +function LoginController(){ +  this.disableLogin = function() { +    return this.loginForm.$invalid; +  }; + +  this.StrongPassword = function(element) { +    var widget = this; +    element.attr('type', 'password'); // act as password. +    this.$on('$validate', function(){ +      widget.$emit(widget.$viewValue.length > 5 ? '$valid' : '$invalid', 'PASSWORD'); +    }); +  }; +} + +describe('LoginController', function() { +  it('should disable login button when form is invalid', function() { +    var scope = angular.scope(); +    var loginController = scope.$new(LoginController); +    var input = angular.element('<input>'); + +    // In production the 'loginForm' form instance gets set from the view, +    // but in unit-test we have to set it manually. +    loginController.loginForm = scope.$service('$formFactory')(); + +    // now instantiate a custom input type +    loginController.loginForm.$createWidget({ +      scope: loginController, +      model: 'password', +      alias: 'password', +      controller: loginController.StrongPassword, +      controllerArgs: [input] +    }); + +    // Verify that the custom password input type sets the input type to password +    expect(input.attr('type')).toEqual('password'); + +    expect(loginController.disableLogin()).toBe(false); + +    // Now simulate an invalid form +    loginController.loginForm.password.$emit('$invalid', 'PASSWORD'); +    expect(loginController.disableLogin()).toBe(true); + +    // Now simulate a valid form +    loginController.loginForm.password.$emit('$valid', 'PASSWORD'); +    expect(loginController.disableLogin()).toBe(false); + +    // Changing model state, should also influence the form validity +    loginController.password = 'abc'; // too short so it should be invalid +    scope.$digest(); +    expect(loginController.loginForm.password.$invalid).toBe(true); + +    // Changeing model state, should also influence the form validity +    loginController.password = 'abcdef'; // should be valid +    scope.$digest(); +    expect(loginController.loginForm.password.$valid).toBe(true); +  }); +}); +</pre> + + diff --git a/docs/content/guide/dev_guide.mvc.understanding_controller.ngdoc b/docs/content/guide/dev_guide.mvc.understanding_controller.ngdoc index 15ae3b34..7a6653e9 100644 --- a/docs/content/guide/dev_guide.mvc.understanding_controller.ngdoc +++ b/docs/content/guide/dev_guide.mvc.understanding_controller.ngdoc @@ -68,7 +68,7 @@ Putting any presentation logic into controllers significantly affects testabilit  logic. Angular offers {@link dev_guide.templates.databinding} for automatic DOM manipulation. If  you have to perform your own manual DOM manipulation, encapsulate the presentation logic in {@link  dev_guide.compiler.widgets widgets} and {@link dev_guide.compiler.directives directives}. -- Input formatting — Use {@link dev_guide.templates.formatters angular formatters} instead. +- Input formatting — Use {@link dev_guide.forms angular form widgets} instead.  - Output filtering — Use {@link dev_guide.templates.filters angular filters} instead.  - Run stateless or stateful code shared across controllers — Use {@link dev_guide.services angular  services} instead. @@ -139,7 +139,7 @@ previous example.  <pre>  <body ng:controller="SpicyCtrl"> - <input name="customSpice" value="wasabi"> + <input ng:model="customSpice" value="wasabi">   <button ng:click="spicy('chili')">Chili</button>   <button ng:click="spicy(customSpice)">Custom spice</button>   <p>The food is {{spice}} spicy!</p> diff --git a/docs/content/guide/dev_guide.mvc.understanding_model.ngdoc b/docs/content/guide/dev_guide.mvc.understanding_model.ngdoc index a35541d0..b4659b0c 100644 --- a/docs/content/guide/dev_guide.mvc.understanding_model.ngdoc +++ b/docs/content/guide/dev_guide.mvc.understanding_model.ngdoc @@ -41,7 +41,7 @@ when processing the following template constructs:  * Form input, select, textarea and other form elements: -         <input name="query" value="fluffy cloud"> +         <input ng:model="query" value="fluffy cloud">     The code above creates a model called "query" on the current scope with the value set to "fluffy  cloud". diff --git a/docs/content/guide/dev_guide.overview.ngdoc b/docs/content/guide/dev_guide.overview.ngdoc index f5db7f94..fcf15044 100644 --- a/docs/content/guide/dev_guide.overview.ngdoc +++ b/docs/content/guide/dev_guide.overview.ngdoc @@ -42,19 +42,27 @@ easier a web developer's life can if they're using angular:  <doc:example>  <doc:source> - <b>Invoice:</b> - <br /> - <br /> - <table> -  <tr><td> </td><td> </td> -  <tr><td>Quantity</td><td>Cost</td></tr> -  <tr> -    <td><input name="qty" value="1" ng:validate="integer:0" ng:required /></td> -    <td><input name="cost" value="19.95" ng:validate="number" ng:required /></td> -  </tr> - </table> - <hr /> - <b>Total:</b> {{qty * cost | currency}} + <script> +   function InvoiceCntl(){ +     this.qty = 1; +     this.cost = 19.95; +   } + </script> + <div ng:controller="InvoiceCntl"> +   <b>Invoice:</b> +   <br /> +   <br /> +   <table> +    <tr><td> </td><td> </td> +    <tr><td>Quantity</td><td>Cost</td></tr> +    <tr> +      <td><input type="integer" min="0" ng:model="qty" required ></td> +      <td><input type="number" ng:model="cost" required ></td> +    </tr> +   </table> +   <hr /> +   <b>Total:</b> {{qty * cost | currency}} + </div>  </doc:source>  <!--  <doc:scenario> @@ -89,18 +97,18 @@ In the `<script>` tag we do two angular setup tasks:  From the `name` attribute of the `<input>` tags, angular automatically sets up two-way data  binding, and we also demonstrate some easy input validation: -        Quantity: <input name="qty" value="1" ng:validate="integer:0" ng:required/> -        Cost: <input name="cost" value="199.95" ng:validate="number" ng:required/> +        Quantity: <input type="integer" min="0" ng:model="qty" required > +        Cost: <input type="number" ng:model="cost" required >  These input widgets look normal enough, but consider these points:  * When this page loaded, angular bound the names of the input widgets (`qty` and `cost`) to  variables of the same name. Think of those variables as the "Model" component of the  Model-View-Controller design pattern. -* Note the angular directives, {@link api/angular.widget.@ng:validate ng:validate} and {@link -api/angular.widget.@ng:required ng:required}. You may have noticed that when you enter invalid data +* Note the angular/HTML widget, {@link api/angular.widget.input input}. +You may have noticed that when you enter invalid data  or leave the the input fields blank, the borders turn red color, and the display value disappears. -These `ng:` directives make it easier to implement field validators than coding them in JavaScript, +These widgets make it easier to implement field validation than coding them in JavaScript,  no?  Yes.  And finally, the mysterious `{{ double curly braces }}`: diff --git a/docs/content/guide/dev_guide.services.$location.ngdoc b/docs/content/guide/dev_guide.services.$location.ngdoc index 4e0e8548..c0f35c96 100644 --- a/docs/content/guide/dev_guide.services.$location.ngdoc +++ b/docs/content/guide/dev_guide.services.$location.ngdoc @@ -612,7 +612,7 @@ https://github.com/angular/angular.js/issues/404 issue}).  If you should require  you will need to specify an extra property that has two watchers. For example:  <pre>  <!-- html --> -<input type="text" name="locationPath" /> +<input type="text" ng:model="locationPath" />  </pre>  <pre>  // js - controller diff --git a/docs/content/guide/dev_guide.services.injecting_controllers.ngdoc b/docs/content/guide/dev_guide.services.injecting_controllers.ngdoc index 0046dd7f..44206f7c 100644 --- a/docs/content/guide/dev_guide.services.injecting_controllers.ngdoc +++ b/docs/content/guide/dev_guide.services.injecting_controllers.ngdoc @@ -54,13 +54,13 @@ myController.$inject = ['notify'];  <div ng:controller="myController">  <p>Let's try this simple notify service, injected into the controller...</p> -<input ng:init="message='test'" type="text" name="message" /> +<input ng:init="message='test'" type="text" ng:model="message" />  <button ng:click="callNotify(message);">NOTIFY</button>  </div>  </doc:source>  <doc:scenario>  it('should test service', function(){ -  expect(element(':input[name=message]').val()).toEqual('test'); +  expect(element(':input[ng\\:model="message"]').val()).toEqual('test');  });  </doc:scenario>  </doc:example> diff --git a/docs/content/guide/dev_guide.templates.css-styling.ngdoc b/docs/content/guide/dev_guide.templates.css-styling.ngdoc index 4a4b2d65..4bd3f1b2 100644 --- a/docs/content/guide/dev_guide.templates.css-styling.ngdoc +++ b/docs/content/guide/dev_guide.templates.css-styling.ngdoc @@ -4,48 +4,32 @@  @description -Angular includes built-in CSS classes, which in turn have predefined CSS styles. +Angular sets these CSS classes. It is up to your application to provide useful styling. -# Built-in CSS classes +# CSS classes used by angular -* `ng-exception` +* `ng-invalid`, `ng-valid` +  - **Usage:** angular applies this class to an input widget element if that element's input does +    notpass validation. (see {@link api/angular.widget.input input} widget). -**Usage:** angular applies this class to a DOM element if that element contains an Expression that -threw an exception when evaluated. +* `ng-pristine`, `ng-dirty` +  - **Usage:** angular {@link api/angular.widget.input input} widget applies `ng-pristine` class +    to a new input widget element which did not have user interaction. Once the user interacts with +    the input widget the class is changed to `ng-dirty`. -**Styling:** The built-in styling of the ng-exception class displays an error message surrounded -by a solid red border, for example: +# Marking CSS classes - <div class="ng-exception">Error message</div> +* `ng-widget`, `ng-directive` +  -  **Usage:** angular sets these class on elements where {@link api/angular.widget widget} or +     {@link api/angular.directive directive} has bound to. -You can try to evaluate malformed expressions in {@link dev_guide.expressions expressions} to see -the `ng-exception` class' styling. - -* `ng-validation-error` - -**Usage:** angular applies this class to an input widget element if that element's input does not -pass validation. Note that you set the validation criteria on the input widget element using the -Ng:validate or Ng:required directives. - -**Styling:** The built-in styling of the ng-validation-error class turns the border of the input -box red and includes a hovering UI element that includes more details of the validation error. You -can see an example in {@link api/angular.widget.@ng:validate ng:validate example}. - -## Overriding Styles for Angular CSS Classes - -To override the styles for angular's built-in CSS classes, you can do any of the following: - -* Download the source code, edit angular.css, and host the source on your own server. -* Create a local CSS file, overriding any styles that you'd like, and link to it from your HTML file -as you normally would: - -<pre> -<link href="yourfile.css" rel="stylesheet" type="text/css"> -</pre> +* Old browser support +  - Pre v9, IE browsers could not select `ng:include` elements in CSS, because of the `:` +    character. For this reason angular also sets `ng-include` class on any element which has `:` +    character in the name by replacing `:` with `-`.  ## Related Topics  * {@link dev_guide.templates Angular Templates} -* {@link dev_guide.templates.formatters Angular Formatters} -* {@link dev_guide.templates.formatters.creating_formatters Creating Angular Formatters} +* {@link dev_guide.forms Angular Forms} diff --git a/docs/content/guide/dev_guide.templates.filters.creating_filters.ngdoc b/docs/content/guide/dev_guide.templates.filters.creating_filters.ngdoc index ebb7d923..27daec9f 100644 --- a/docs/content/guide/dev_guide.templates.filters.creating_filters.ngdoc +++ b/docs/content/guide/dev_guide.templates.filters.creating_filters.ngdoc @@ -35,20 +35,26 @@ text upper-case and assigns color.     }     return out;   }); + + function Ctrl(){ +   this.greeting = 'hello'; + }  </script> -<input name="text" type="text" value="hello" /><br> -No filter: {{text}}<br> -Reverse: {{text|reverse}}<br> -Reverse + uppercase: {{text|reverse:true}}<br> -Reverse + uppercase + blue:  {{text|reverse:true:"blue"}} +<div ng:controller="Ctrl"> +  <input ng:model="greeting" type="greeting"><br> +  No filter: {{greeting}}<br> +  Reverse: {{greeting|reverse}}<br> +  Reverse + uppercase: {{greeting|reverse:true}}<br> +  Reverse + uppercase + blue:  {{greeting|reverse:true:"blue"}} +</div>  </doc:source>  <doc:scenario> -it('should reverse text', function(){ -expect(binding('text|reverse')).toEqual('olleh'); -input('text').enter('ABC'); -expect(binding('text|reverse')).toEqual('CBA'); -}); +  it('should reverse greeting', function(){ +    expect(binding('greeting|reverse')).toEqual('olleh'); +    input('greeting').enter('ABC'); +    expect(binding('greeting|reverse')).toEqual('CBA'); +  });  </doc:scenario>  </doc:example> diff --git a/docs/content/guide/dev_guide.templates.formatters.creating_formatters.ngdoc b/docs/content/guide/dev_guide.templates.formatters.creating_formatters.ngdoc deleted file mode 100644 index 2ecd8f19..00000000 --- a/docs/content/guide/dev_guide.templates.formatters.creating_formatters.ngdoc +++ /dev/null @@ -1,55 +0,0 @@ -@workInProgress -@ngdoc overview -@name Developer Guide: Templates: Angular Formatters: Creating Angular Formatters -@description - -To create your own formatter, you can simply register a pair of JavaScript functions with -`angular.formatter`. One of your functions is used to parse text from the input widget into the -data storage format; the other function is used to format stored data into user-readable text. - -The following example demonstrates a "reverse" formatter. Data is stored in uppercase and in -reverse, but it is displayed in lower case and non-reversed. When a user edits the data model via -the input widget, the input is automatically parsed into the internal data storage format, and when -the data changes in the model, it is automatically formatted to the user-readable form for display -in the view. - -<pre> -function reverse(text) { -var reversed = []; -for (var i = 0; i < text.length; i++) { -reversed.unshift(text.charAt(i)); -} -return reversed.join(''); -} - -angular.formatter('reverse', { -parse: function(value){ -return reverse(value||'').toUpperCase(); -}, -format: function(value){ -return reverse(value||'').toLowerCase(); -} -}); -</pre> - -<doc:example> -<doc:source> -<script type="text/javascript"> -function reverse(text) { -var reversed = []; -for (var i = 0; i < text.length; i++) { -  reversed.unshift(text.charAt(i)); -} -return reversed.join(''); -} - -angular.formatter('reverse', { -parse: function(value){ -  return reverse(value||'').toUpperCase(); -}, -format: function(value){ -  return reverse(value||'').toLowerCase(); -} -}); -</script> - diff --git a/docs/content/guide/dev_guide.templates.formatters.ngdoc b/docs/content/guide/dev_guide.templates.formatters.ngdoc deleted file mode 100644 index 82a14fb4..00000000 --- a/docs/content/guide/dev_guide.templates.formatters.ngdoc +++ /dev/null @@ -1,20 +0,0 @@ -@workInProgress -@ngdoc overview -@name Developer Guide: Templates: Angular Formatters -@description - -In angular, formatters are responsible for translating user-readable text entered in an {@link -api/angular.widget.HTML input widget} to a JavaScript object in the data model that the application -can manipulate. - -You can use formatters in a template, and also in JavaScript.  Angular provides built-in -formatters, and of course you can create your own formatters. - -## Related Topics - -* {@link dev_guide.templates.formatters.using_formatters Using Angular Formatters} -* {@link dev_guide.templates.formatters.creating_formatters Creating Angular Formatters} - -## Related API - -* {@link api/angular.formatter Angular Formatter API} diff --git a/docs/content/guide/dev_guide.templates.formatters.using_formatters.ngdoc b/docs/content/guide/dev_guide.templates.formatters.using_formatters.ngdoc deleted file mode 100644 index bf983cd5..00000000 --- a/docs/content/guide/dev_guide.templates.formatters.using_formatters.ngdoc +++ /dev/null @@ -1,9 +0,0 @@ -@workInProgress -@ngdoc overview -@name Developer Guide: Templates: Angular Formatters: Using Angular Formatters -@description - -The following snippet shows how to use a formatter in a template. The formatter below is -`ng:format="reverse"`, added as an attribute to an `<input>` tag. - -<pre> diff --git a/docs/content/guide/dev_guide.templates.ngdoc b/docs/content/guide/dev_guide.templates.ngdoc index ca0ca99a..32514eb9 100644 --- a/docs/content/guide/dev_guide.templates.ngdoc +++ b/docs/content/guide/dev_guide.templates.ngdoc @@ -18,9 +18,7 @@ is {@link api/angular.widget.@ng:repeat ng:repeat}.  * {@link dev_guide.compiler.markup  Markup} — Shorthand for a widget or a directive. The double  curly brace notation `{{ }}` to bind expressions to elements is built-in angular markup.  * {@link dev_guide.templates.filters Filter} — Formats your data for display to the user. -* {@link dev_guide.templates.validators Validator} — Lets you validate user input. -* {@link dev_guide.templates.formatters Formatter} — Lets you format the input object into a user -readable view. +* {@link dev_guide.forms Form widgets} — Lets you validate user input.  Note:  In addition to declaring the elements above in templates, you can also access these elements  in JavaScript code. @@ -33,7 +31,7 @@ and {@link dev_guide.expressions expressions}:  <html>   <!-- Body tag augmented with ng:controller directive  -->   <body ng:controller="MyController"> -   <input name="foo" value="bar"> +   <input ng:model="foo" value="bar">     <!-- Button tag with ng:click directive, and            string expression 'buttonText'            wrapped in "{{ }}" markup --> @@ -55,8 +53,7 @@ eight.  ## Related Topics  * {@link dev_guide.templates.filters Angular Filters} -* {@link dev_guide.templates.formatters Angular Formatters} -* {@link dev_guide.templates.validators Angular Validators} +* {@link dev_guide.forms Angular Forms}  ## Related API diff --git a/docs/content/guide/dev_guide.templates.validators.creating_validators.ngdoc b/docs/content/guide/dev_guide.templates.validators.creating_validators.ngdoc deleted file mode 100644 index 835b0b51..00000000 --- a/docs/content/guide/dev_guide.templates.validators.creating_validators.ngdoc +++ /dev/null @@ -1,82 +0,0 @@ -@workInProgress -@ngdoc overview -@name Developer Guide: Validators: Creating Angular Validators -@description - - -To create a custom validator, you simply add your validator code as a method onto the -`angular.validator` object and provide input(s) for the validator function. Each input provided is -treated as an argument to the validator function.  Any additional inputs should be separated by -commas. - -The following bit of pseudo-code shows how to set up a custom validator: - -<pre> -angular.validator('your_validator', function(input [,additional params]) { -        [your validation code]; -        if ( [validation succeeds] ) { -                return false; -        } else { -                return true; // No error message specified -                         } -} -</pre> - -Note that this validator returns "true" when the user's input is incorrect, as in "Yes, it's true, -there was a problem with that input". If you prefer to provide more information when a validator -detects a problem with input, you can specify an error message in the validator that angular will -display when the user hovers over the input widget. - -To specify an error message, replace "`return true;`" with an error string, for example: - -     return "Must be a value between 1 and 5!"; - -Following is a sample UPS Tracking Number validator: - -<doc:example> -<doc:source> -<script> -angular.validator('upsTrackingNo', function(input, format) { - var regexp = new RegExp("^" + format.replace(/9/g, '\\d') + "$"); - return input.match(regexp)?"":"The format must match " + format; -}); -</script> -<input type="text" name="trackNo" size="40" -    ng:validate="upsTrackingNo:'1Z 999 999 99 9999 999 9'" -    value="1Z 123 456 78 9012 345 6"/> -</doc:source> -<doc:scenario> -it('should validate correct UPS tracking number', function() { -expect(element('input[name=trackNo]').attr('class')). -   not().toMatch(/ng-validation-error/); -}); - -it('should not validate in correct UPS tracking number', function() { -input('trackNo').enter('foo'); -expect(element('input[name=trackNo]').attr('class')). -   toMatch(/ng-validation-error/); -}); -</doc:scenario> -</doc:example> - -In this sample validator, we specify a regular expression against which to test the user's input. -Note that when the user's input matches `regexp`, the function returns "false" (""); otherwise it -returns the specified error message ("true"). - -Note: you can also access the current angular scope and DOM element objects in your validator -functions as follows: - -* `this` ===  The current angular scope. -* `this.$element` ===  The DOM element that contains the binding. This allows the filter to -manipulate the DOM in addition to transforming the input. - - -## Related Topics - -* {@link dev_guide.templates Angular Templates} -* {@link dev_guide.templates.filters Angular Filters} -* {@link dev_guide.templates.formatters Angular Formatters} - -## Related API - -* {@link api/angular.validator API Validator Reference} diff --git a/docs/content/guide/dev_guide.templates.validators.ngdoc b/docs/content/guide/dev_guide.templates.validators.ngdoc deleted file mode 100644 index 76df92b5..00000000 --- a/docs/content/guide/dev_guide.templates.validators.ngdoc +++ /dev/null @@ -1,131 +0,0 @@ -@workInProgress -@ngdoc overview -@name Developer Guide: Templates: Understanding Angular Validators -@description - -Angular validators are attributes that test the validity of different types of user input. Angular -provides a set of built-in input validators: - -* {@link api/angular.validator.phone phone number} -* {@link api/angular.validator.number number} -* {@link api/angular.validator.integer integer} -* {@link api/angular.validator.date date} -* {@link api/angular.validator.email email address} -* {@link api/angular.validator.json JSON} -* {@link api/angular.validator.regexp regular expressions} -* {@link api/angular.validator.url URLs} -* {@link api/angular.validator.asynchronous asynchronous} - -You can also create your own custom validators. - -# Using Angular Validators - -You can use angular validators in HTML template bindings, and in JavaScript: - -* Validators in HTML Template Bindings - -<pre> -<input ng:validator="validator_type:parameters" [...]> -</pre> - -* Validators in JavaScript - -<pre> -angular.validator.[validator_type](parameters) -</pre> - -The following example shows how to use the built-in angular integer validator: - -<doc:example> -<doc:source> - Change me: <input type="text" name="number" ng:validate="integer" value="123"> -</doc:source> -<doc:scenario> - it('should validate the default number string', function() { -   expect(element('input[name=number]').attr('class')). -      not().toMatch(/ng-validation-error/); - }); - it('should not validate "foo"', function() { -   input('number').enter('foo'); -   expect(element('input[name=number]').attr('class')). -      toMatch(/ng-validation-error/); - }); -</doc:scenario> -</doc:example> - -# Creating an Angular Validator - -To create a custom validator, you simply add your validator code as a method onto the -`angular.validator` object and provide input(s) for the validator function. Each input provided is -treated as an argument to the validator function.  Any additional inputs should be separated by -commas. - -The following bit of pseudo-code shows how to set up a custom validator: - -<pre> -angular.validator('your_validator', function(input [,additional params]) { -        [your validation code]; -        if ( [validation succeeds] ) { -                return false; -        } else { -                return true; // No error message specified -                          } -} -</pre> - -Note that this validator returns "true" when the user's input is incorrect, as in "Yes, it's true, -there was a problem with that input". If you prefer to provide more information when a validator -detects a problem with input, you can specify an error message in the validator that angular will -display when the user hovers over the input widget. - -To specify an error message, replace "`return true;`" with an error string, for example: - -      return "Must be a value between 1 and 5!"; - -Following is a sample UPS Tracking Number validator: - -<doc:example> -<doc:source> -<script> -angular.validator('upsTrackingNo', function(input, format) { -  var regexp = new RegExp("^" + format.replace(/9/g, '\\d') + "$"); -  return input.match(regexp)?"":"The format must match " + format; -}); -</script> -<input type="text" name="trackNo" size="40" -     ng:validate="upsTrackingNo:'1Z 999 999 99 9999 999 9'" -     value="1Z 123 456 78 9012 345 6"/> -</doc:source> -<doc:scenario> -it('should validate correct UPS tracking number', function() { - expect(element('input[name=trackNo]').attr('class')). -    not().toMatch(/ng-validation-error/); -}); - -it('should not validate in correct UPS tracking number', function() { - input('trackNo').enter('foo'); - expect(element('input[name=trackNo]').attr('class')). -    toMatch(/ng-validation-error/); -}); -</doc:scenario> -</doc:example> - -In this sample validator, we specify a regular expression against which to test the user's input. -Note that when the user's input matches `regexp`, the function returns "false" (""); otherwise it -returns the specified error message ("true"). - -Note: you can also access the current angular scope and DOM element objects in your validator -functions as follows: - -* `this` ===  The current angular scope. -* `this.$element` ===  The DOM element that contains the binding. This allows the filter to -manipulate the DOM in addition to transforming the input. - - -## Related Topics - -* {@link dev_guide.templates Angular Templates} - -## Related API - -* {@link api/angular.validator Validator API} diff --git a/docs/content/guide/index.ngdoc b/docs/content/guide/index.ngdoc index b2aab161..8d609afa 100644 --- a/docs/content/guide/index.ngdoc +++ b/docs/content/guide/index.ngdoc @@ -42,8 +42,7 @@ of the following documents before returning here to the Developer Guide:  ## {@link dev_guide.templates Angular Templates}  * {@link dev_guide.templates.filters Understanding Angular Filters} -* {@link dev_guide.templates.formatters Understanding Angular Formatters} -* {@link dev_guide.templates.validators Understanding Angular Validators} +* {@link dev_guide.forms Understanding Angular Forms}  ## {@link dev_guide.services Angular Services} diff --git a/docs/content/misc/started.ngdoc b/docs/content/misc/started.ngdoc index 3bf71cf1..591fb859 100644 --- a/docs/content/misc/started.ngdoc +++ b/docs/content/misc/started.ngdoc @@ -67,7 +67,7 @@ This example demonstrates angular's two-way data binding:  <doc:example>   <doc:source> -  Your name: <input type="text" name="yourname" value="World"/> +  Your name: <input type="text" ng:model="yourname" value="World"/>    <hr/>    Hello {{yourname}}!   </doc:source> diff --git a/docs/content/tutorial/step_03.ngdoc b/docs/content/tutorial/step_03.ngdoc index ec546956..89a1b0cb 100644 --- a/docs/content/tutorial/step_03.ngdoc +++ b/docs/content/tutorial/step_03.ngdoc @@ -32,7 +32,7 @@ We made no changes to the controller.  __`app/index.html`:__  <pre>  ... -   Fulltext Search: <input name="query"/> +   Fulltext Search: <input ng:model="query"/>    <ul class="phones">      <li ng:repeat="phone in phones.$filter(query)"> diff --git a/docs/content/tutorial/step_04.ngdoc b/docs/content/tutorial/step_04.ngdoc index 72aa26c9..d05a8e7c 100644 --- a/docs/content/tutorial/step_04.ngdoc +++ b/docs/content/tutorial/step_04.ngdoc @@ -27,11 +27,11 @@ __`app/index.html`:__  ...    <ul class="controls">      <li> -      Search: <input type="text" name="query"/> +      Search: <input type="text" ng:model="query"/>      </li>      <li>        Sort by: -      <select name="orderProp"> +      <select ng:model="orderProp">          <option value="name">Alphabetical</option>          <option value="age">Newest</option>        </select> diff --git a/docs/content/tutorial/step_07.ngdoc b/docs/content/tutorial/step_07.ngdoc index fa0c1e1f..eaf7f4ab 100644 --- a/docs/content/tutorial/step_07.ngdoc +++ b/docs/content/tutorial/step_07.ngdoc @@ -122,11 +122,11 @@ __`app/partials/phone-list.html`:__  <pre>  <ul class="predicates">    <li> -    Search: <input type="text" name="query"/> +    Search: <input type="text" ng:model="query"/>    </li>    <li>      Sort by: -    <select name="orderProp"> +    <select ng:model="orderProp">        <option value="name">Alphabetical</option>        <option value="age">Newest</option>      </select> diff --git a/docs/content/tutorial/step_09.ngdoc b/docs/content/tutorial/step_09.ngdoc index 80b10f65..7d8e3430 100644 --- a/docs/content/tutorial/step_09.ngdoc +++ b/docs/content/tutorial/step_09.ngdoc @@ -109,7 +109,7 @@ following bindings to `index.html`:  *  We can also create a model with an input element, and combine it with a filtered binding. Add  the following to index.html: -        <input name="userInput"> Uppercased: {{ userInput | uppercase }} +        <input ng:model="userInput"> Uppercased: {{ userInput | uppercase }}  # Summary diff --git a/docs/examples/settings.html b/docs/examples/settings.html index 2fa5dca8..74500b35 100644 --- a/docs/examples/settings.html +++ b/docs/examples/settings.html @@ -1,13 +1,13 @@  <label>Name:</label> -<input type="text" name="form.name" ng:required> +<input type="text" ng:model="form.name" required>  <div ng:repeat="contact in form.contacts"> -  <select name="contact.type"> +  <select ng:model="contact.type">      <option>url</option>      <option>email</option>      <option>phone</option>    </select> -  <input type="text" name="contact.url"> +  <input type="text" ng:model="contact.url">    [ <a href="" ng:click="form.contacts.$remove(contact)">X</a> ]  </div>  <div> @@ -15,4 +15,4 @@  </div>  <button ng:click="cancel()">Cancel</button> -<button ng:click="save()">Save</button>
\ No newline at end of file +<button ng:click="save()">Save</button> diff --git a/docs/img/form_data_flow.png b/docs/img/form_data_flow.pngBinary files differ new file mode 100644 index 00000000..60e947a5 --- /dev/null +++ b/docs/img/form_data_flow.png diff --git a/docs/spec/ngdocSpec.js b/docs/spec/ngdocSpec.js index 106fd22b..2afcc3d4 100644 --- a/docs/spec/ngdocSpec.js +++ b/docs/spec/ngdocSpec.js @@ -194,12 +194,12 @@ describe('ngdoc', function(){      it('should ignore nested doc widgets', function() {        expect(new Doc().markdown(          'before<doc:tutorial-instructions>\n' + -          '<doc:tutorial-instruction id="git-mac" name="Git on Mac/Linux">' + +          '<doc:tutorial-instruction id="git-mac" ng:model="Git on Mac/Linux">' +            '\ngit bla bla\n</doc:tutorial-instruction>\n' +          '</doc:tutorial-instructions>')).toEqual(          '<p>before</p><doc:tutorial-instructions>\n' + -          '<doc:tutorial-instruction id="git-mac" name="Git on Mac/Linux">\n' + +          '<doc:tutorial-instruction id="git-mac" ng:model="Git on Mac/Linux">\n' +            'git bla bla\n' +            '</doc:tutorial-instruction>\n' +          '</doc:tutorial-instructions>'); @@ -543,38 +543,6 @@ describe('ngdoc', function(){        });      }); -    describe('validator', function(){ -      it('should format', function(){ -        var doc = new Doc({ -          ngdoc:'validator', -          shortName:'myValidator', -          param: [ -            {name:'a'}, -            {name:'b'} -          ] -        }); -        doc.html_usage_validator(dom); -        expect(dom).toContain('ng:validate="myValidator:b"'); -        expect(dom).toContain('angular.validator.myValidator(a, b)'); -      }); -    }); - -    describe('formatter', function(){ -      it('should format', function(){ -        var doc = new Doc({ -          ngdoc:'formatter', -          shortName:'myFormatter', -          param: [ -            {name:'a'}, -          ] -        }); -        doc.html_usage_formatter(dom); -        expect(dom).toContain('ng:format="myFormatter:a"'); -        expect(dom).toContain('var userInputString = angular.formatter.myFormatter.format(modelValue, a);'); -        expect(dom).toContain('var modelValue = angular.formatter.myFormatter.parse(userInputString, a);'); -      }); -    }); -      describe('property', function(){        it('should format', function(){          var doc = new Doc({ diff --git a/docs/src/ngdoc.js b/docs/src/ngdoc.js index 8a20e64a..1a4f5d25 100644 --- a/docs/src/ngdoc.js +++ b/docs/src/ngdoc.js @@ -13,6 +13,11 @@ exports.scenarios = scenarios;  exports.merge = merge;  exports.Doc = Doc; +var BOOLEAN_ATTR = {}; +['multiple', 'selected', 'checked', 'disabled', 'readOnly', 'required'].forEach(function(value, key) { +  BOOLEAN_ATTR[value] = true; +}); +  //////////////////////////////////////////////////////////  function Doc(text, file, line) {    if (typeof text == 'object') { @@ -385,69 +390,21 @@ Doc.prototype = {      });    }, -  html_usage_formatter: function(dom){ -    var self = this; -    dom.h('Usage', function(){ -      dom.h('In HTML Template Binding', function(){ -        dom.code(function(){ -          if (self.inputType=='select') -            dom.text('<select name="bindExpression"'); -          else -            dom.text('<input type="text" name="bindExpression"'); -          dom.text(' ng:format="'); -          dom.text(self.shortName); -          self.parameters(dom, ':', false, true); -          dom.text('">'); -        }); -      }); - -      dom.h('In JavaScript', function(){ -        dom.code(function(){ -          dom.text('var userInputString = angular.formatter.'); -          dom.text(self.shortName); -          dom.text('.format(modelValue'); -          self.parameters(dom, ', ', false, true); -          dom.text(');'); -          dom.text('\n'); -          dom.text('var modelValue = angular.formatter.'); -          dom.text(self.shortName); -          dom.text('.parse(userInputString'); -          self.parameters(dom, ', ', false, true); -          dom.text(');'); -        }); -      }); - -      self.html_usage_parameters(dom); -      self.html_usage_this(dom); -      self.html_usage_returns(dom); -    }); -  }, - -  html_usage_validator: function(dom){ +  html_usage_inputType: function(dom){      var self = this;      dom.h('Usage', function(){ -      dom.h('In HTML Template Binding', function(){ -        dom.code(function(){ -          dom.text('<input type="text" ng:validate="'); -          dom.text(self.shortName); -          self.parameters(dom, ':', true); -          dom.text('"/>'); -        }); -      }); - -      dom.h('In JavaScript', function(){ -        dom.code(function(){ -          dom.text('angular.validator.'); -          dom.text(self.shortName); -          dom.text('('); -          self.parameters(dom, ', '); -          dom.text(')'); +      dom.code(function(){ +        dom.text('<input type="' + self.shortName + '"'); +        (self.param||[]).forEach(function(param){ +          dom.text('\n      '); +          dom.text(param.optional ? ' [' : ' '); +          dom.text(param.name); +          dom.text(BOOLEAN_ATTR[param.name] ? '' : '="..."'); +          dom.text(param.optional ? ']' : '');          }); +        dom.text('>');        }); -        self.html_usage_parameters(dom); -      self.html_usage_this(dom); -      self.html_usage_returns(dom);      });    }, @@ -473,11 +430,11 @@ Doc.prototype = {              dom.text('<');              dom.text(self.shortName);              (self.param||[]).forEach(function(param){ -              if (param.optional) { -                dom.text(' [' + param.name + '="..."]'); -              } else { -                dom.text(' ' + param.name + '="..."'); -              } +              dom.text('\n      '); +              dom.text(param.optional ? ' [' : ' '); +              dom.text(param.name); +              dom.text(BOOLEAN_ATTR[param.name] ? '' : '="..."'); +              dom.text(param.optional ? ']' : '');              });              dom.text('></');              dom.text(self.shortName); @@ -533,12 +490,18 @@ Doc.prototype = {      dom.h('Events', this.events, function(event){      dom.h(event.shortName, event, function(){          dom.html(event.description); -        dom.tag('div', {class:'inline'}, function(){ -          dom.h('Type:', event.type); -        }); -        dom.tag('div', {class:'inline'}, function(){ -          dom.h('Target:', event.target); -        }); +        if (event.type == 'listen') { +          dom.tag('div', {class:'inline'}, function(){ +            dom.h('Listen on:', event.target); +          }); +        } else { +          dom.tag('div', {class:'inline'}, function(){ +            dom.h('Type:', event.type); +          }); +          dom.tag('div', {class:'inline'}, function(){ +            dom.h('Target:', event.target); +          }); +        }          event.html_usage_parameters(dom);          self.html_usage_this(dom); @@ -632,10 +595,9 @@ var KEYWORD_PRIORITY = {    '.angular.Object': 7,    '.angular.directive': 7,    '.angular.filter': 7, -  '.angular.formatter': 7,    '.angular.scope': 7,    '.angular.service': 7, -  '.angular.validator': 7, +  '.angular.inputType': 7,    '.angular.widget': 7,    '.angular.mock': 8,    '.dev_guide.overview': 1, diff --git a/docs/src/templates/doc_widgets.js b/docs/src/templates/doc_widgets.js index 17284a1d..72f59f74 100644 --- a/docs/src/templates/doc_widgets.js +++ b/docs/src/templates/doc_widgets.js @@ -81,14 +81,16 @@            fiddleSrc = fiddleSrc.replace(new RegExp('^\\s{' + stripIndent + '}', 'gm'), '');            return '<form class="jsfiddle" method="post" action="' + fiddleUrl + '" target="_blank">' + -                    '<textarea name="css">' + +                    '<textarea ng:model="css">' + +                      '.ng-invalid { border: 1px solid red; } \n' +                        'body { font-family: Arial,Helvetica,sans-serif; }\n' +                        'body, td, th { font-size: 14px; margin: 0; }\n' +                        'table { border-collapse: separate; border-spacing: 2px; display: table; margin-bottom: 0; margin-top: 0; -moz-box-sizing: border-box; text-indent: 0; }\n' +                        'a:link, a:visited, a:hover { color: #5D6DB6; text-decoration: none; }\n' + +                      '.error { color: red; }\n' +                      '</textarea>' + -                    '<input type="text" name="title" value="AngularJS Live Example">' + -                    '<textarea name="html">' + +                    '<input type="text" ng:model="title" value="AngularJS Live Example">' + +                    '<textarea ng:model="html">' +                        '<script src="' + angularJsUrl + '" ng:autobind></script>\n\n' +                        '<!-- AngularJS Example Code: -->\n\n' +                        fiddleSrc + diff --git a/docs/src/templates/docs.css b/docs/src/templates/docs.css index 99ea7454..c38252ff 100644 --- a/docs/src/templates/docs.css +++ b/docs/src/templates/docs.css @@ -49,6 +49,10 @@ li {    margin: 0.3em 0 0.3em 0;  } +.ng-invalid { +  border: 1px solid red; +} +  /*----- Upgrade IE Prompt -----*/ @@ -426,7 +430,7 @@ li {  }  table { -	border-collapse: collapse; +  border-collapse: collapse;  }  td { @@ -448,7 +452,7 @@ td.empty-corner-lt {  .html5-hashbang-example {    height: 255px;    margin-left: -40px; -  padding-left: 30px;  +  padding-left: 30px;  }  .html5-hashbang-example div { @@ -459,3 +463,7 @@ td.empty-corner-lt {  .html5-hashbang-example div input {    width: 360px;  } + +.error { +   color: red; +} diff --git a/docs/src/templates/index.html b/docs/src/templates/index.html index a2def7a6..87c27ac0 100644 --- a/docs/src/templates/index.html +++ b/docs/src/templates/index.html @@ -99,7 +99,7 @@      </ul>      <div id="sidebar"> -      <input type="text" name="search" id="search-box" placeholder="search the docs" +      <input type="text" ng:model="search" id="search-box" placeholder="search the docs"               tabindex="1" accesskey="s">        <ul id="content-list" ng:class="sectionId" ng:cloak> diff --git a/example/buzz/buzz.html b/example/buzz/buzz.html index 8e2f4f83..f2bf21ac 100644 --- a/example/buzz/buzz.html +++ b/example/buzz/buzz.html @@ -12,11 +12,11 @@      <span><angular/> Buzz</span>      <span>        filter: -      <input type="text" name="filterText"/> +      <input type="text" ng:model="filterText"/>      </span>      <span>        user: -      <input type="text" name="userId" ng:required/> +      <input type="text" ng:model="userId" required/>        <button ng:click="$location.hashPath = userId">fetch</button>      </span>    </div> diff --git a/example/personalLog/personalLog.html b/example/personalLog/personalLog.html index 9d3af77d..bc76b263 100644 --- a/example/personalLog/personalLog.html +++ b/example/personalLog/personalLog.html @@ -12,7 +12,7 @@    <body ng:controller="example.personalLog.LogCtrl">      <form action="" ng:submit="addLog(newMsg)"> -      <input type="text" name="newMsg" /> +      <input type="text" ng:model="newMsg" />        <input type="submit" value="add" />        <input type="button" value="remove all" ng:click="rmLogs()" />      </form> diff --git a/example/tweeter/tweeter_addressbook.html b/example/tweeter/tweeter_addressbook.html index fb8f5b4b..5ffa6f74 100644 --- a/example/tweeter/tweeter_addressbook.html +++ b/example/tweeter/tweeter_addressbook.html @@ -11,7 +11,7 @@    <body ng:class="status" ng:init="mute={}" ng:watch="$anchor.user: tweets = fetchTweets($anchor.user)">      <div class="addressbook box">        <h1>Address Book</h1> -      [ Filter: <input type="text" name="userFilter"/>] +      [ Filter: <input type="text" ng:model="userFilter"/>]        <ul>          <li ng:repeat="user in users.$filter(userFilter).$orderBy('screen_name')" ng:class-even="'even'" ng:class-odd="'odd'">            <a href="" ng:click="$anchor.user=user.screen_name"><img src="{{user.profile_image_url}}"/></a> @@ -29,13 +29,13 @@        <div ng:show="$anchor.edituser" ng:eval="user = users.$find({:$.screen_name == $anchor.edituser})">          <div class="editor">            <label>Username:</label> -          <input type="text" name="user.screen_name" disabled="disabled"/> +          <input type="text" ng:model="user.screen_name" disabled="disabled"/>            <label>Name:</label> -          <input type="text" name="user.name"/> +          <input type="text" ng:model="user.name"/>            <label>Image:</label> -          <input type="text" name="user.profile_image_url"/> +          <input type="text" ng:model="user.profile_image_url"/>            <label>Notes:</label> -          <textarea type="text" name="user.notes"></textarea> +          <textarea type="text" ng:model="user.notes"></textarea>            <input type="button" ng:click="$anchor.edituser=undefined" value="Close"/>          </div> @@ -57,7 +57,7 @@ tweets={{tweets}}      </div>      <div class="tweeter box">        <h1>Tweets: {{$anchor.user}}</h1> -      [ Filter: <input type="text" name="tweetFilter"/> +      [ Filter: <input type="text" ng:model="tweetFilter"/>          <span ng:show="$anchor.user">| <a href="#user="><< All</a></span>        ]        <div class="loading">Loading...</div> diff --git a/example/tweeter/tweeter_demo.html b/example/tweeter/tweeter_demo.html index 0df794f4..6966192a 100644 --- a/example/tweeter/tweeter_demo.html +++ b/example/tweeter/tweeter_demo.html @@ -12,7 +12,7 @@       (TODO: I should fetch current tweets)      <div class="tweeter box">        <h1>Tweets: {{$anchor.user}}</h1> -      [ Filter: <input type="text" name="tweetFilter"/> (TODO: this should act as search box) +      [ Filter: <input type="text" ng:model="tweetFilter"/> (TODO: this should act as search box)          <span ng:show="$anchor.user">| <a href="#user="><< All</a></span>        ]        <div class="loading">Loading...</div> diff --git a/gen_docs.sh b/gen_docs.sh index 0df9fbb4..3c74339e 100755 --- a/gen_docs.sh +++ b/gen_docs.sh @@ -1,4 +1,4 @@  #!/bin/bash  if [ ! -e gen_docs.disable ]; then -  /usr/bin/env jasmine-node docs/spec -i docs/src -i lib --noColor && node docs/src/gen-docs.js +  jasmine-node docs/spec -i docs/src -i lib --noColor && node docs/src/gen-docs.js  fi diff --git a/i18n/e2e/localeTest_cs.html b/i18n/e2e/localeTest_cs.html index a2e1966e..2d8845a2 100644 --- a/i18n/e2e/localeTest_cs.html +++ b/i18n/e2e/localeTest_cs.html @@ -5,9 +5,14 @@      <title>locale test</title>      <script src="../../build/angular.js" ng:autobind></script>      <script src="../../build/i18n/angular-locale_cs.js"></script> +    <script> +      function AppCntl(){ +        this.input = 234234443432; +      } +    </script>    </head> -  <body> -    <input type="text" name="input" value="234234443432"><br> +  <body ng:controller="AppCntl"> +    <input type="text" ng:model="input"><br>      date: {{input | date:"medium"}}<br>      date: {{input | date:"longDate"}}<br>      number: {{input | number}}<br> diff --git a/i18n/e2e/localeTest_de.html b/i18n/e2e/localeTest_de.html index 931c56dd..8618c44d 100644 --- a/i18n/e2e/localeTest_de.html +++ b/i18n/e2e/localeTest_de.html @@ -5,9 +5,14 @@      <title>locale test</title>      <script src="../../build/angular.js" ng:autobind></script>      <script src="../../build/i18n/angular-locale_de.js"></script> +    <script> +      function AppCntl(){ +        this.input = 234234443432; +      } +    </script>    </head> -  <body> -    <input type="text" name="input" value="234234443432"><br> +  <body ng:controller="AppCntl"> +    <input type="text" ng:model="input"><br>      date: {{input | date:"medium"}}<br>      date: {{input | date:"longDate"}}<br>      number: {{input | number}}<br> diff --git a/i18n/e2e/localeTest_en.html b/i18n/e2e/localeTest_en.html index ca151c30..de77681b 100644 --- a/i18n/e2e/localeTest_en.html +++ b/i18n/e2e/localeTest_en.html @@ -7,17 +7,26 @@      <!-- not needed, already bundled in angular.js        <script src="../../build/i18n/angular-locale_en.js"></script>      --> +    <script> +      function AppCntl(){ +        this.input = 234234443432; +        this.plInput = 1; +        this.person1 = "Shanjian"; +        this.person2 = "Di"; +        this.plInput2 = 1; +      } +    </script>    </head> -  <body> +  <body ng:controller="AppCntl">      <h3>Datetime/Number/Currency filters demo:</h3> -    <input type="text" name="input" value="234234443432"><br> +    <input type="text" ng:model="input" value="234234443432"><br>      date(medium): {{input | date:"medium"}}<br>      date(longDate): {{input | date:"longDate"}}<br>      number: {{input | number}}<br>      currency: {{input | currency }}      <hr/>      <h3>Pluralization demo:</h3> -    <input type="text" name="plInput" value="1"><br> +    <input type="text" ng:model="plInput"><br>      <ng:pluralize count="plInput"                    when= "{ '0': 'You have no email!',                             'one': 'You have one email!', @@ -25,9 +34,9 @@      </ng:pluralize>      <hr/>      <h3>Pluralization demo with offsets:</h3> -    Name of person1:<input type="text" name="person1" value="Shanjian"/><br/> -    Name of person2:<input type="text" name="person2" value="Di"/><br/> -    <input type="text" name="plInput2" value="1"><br> +    Name of person1:<input type="text" ng:model="person1"/><br/> +    Name of person2:<input type="text" ng:model="person2"/><br/> +    <input type="text" ng:model="plInput2"><br>      <ng:pluralize count="plInput2" offset=2                    when= "{'0':'Nobody is viewing!',                            '1': '{{person1}} is viewing!', diff --git a/i18n/e2e/localeTest_es.html b/i18n/e2e/localeTest_es.html index bb426923..50c8c076 100644 --- a/i18n/e2e/localeTest_es.html +++ b/i18n/e2e/localeTest_es.html @@ -5,9 +5,14 @@      <title>locale test</title>      <script src="../../build/angular.js" ng:autobind></script>      <script src="../../build/i18n/angular-locale_es.js"></script> +    <script> +      function AppCntl(){ +        this.input = 234234443432; +      } +    </script>    </head> -  <body> -    <input type="text" name="input" value="234234443432"><br> +  <body ng:controller="AppCntl"> +    <input type="text" ng:model="input" value="234234443432"><br>      date: {{input | date:"medium"}}<br>      date: {{input | date:"longDate"}}<br>      number: {{input | number}}<br> diff --git a/i18n/e2e/localeTest_sk.html b/i18n/e2e/localeTest_sk.html index f9ae87f7..ab0bb2a5 100644 --- a/i18n/e2e/localeTest_sk.html +++ b/i18n/e2e/localeTest_sk.html @@ -5,15 +5,21 @@      <title>locale test</title>      <script src="../../build/angular.js" ng:autobind></script>      <script src="../../build/i18n/angular-locale_sk-sk.js"></script> +    <script> +      function AppCntl(){ +        this.input = 234234443432; +        this.plInput = 1; +      } +    </script>    </head> -  <body> -    <input type="text" name="input" value="234234443432"><br> +  <body ng:controller="AppCntl"> +    <input type="text" ng:model="input" value="234234443432"><br>      date: {{input | date:"medium"}}<br>      date: {{input | date:"longDate"}}<br>      number: {{input | number}}<br>      currency: {{input | currency }}      <hr/> -    <input type="text" name="plInput" value="1"><br> +    <input type="text" ng:model="plInput"><br>      <ng:pluralize count="plInput"                    when= "{ 'one': 'Mas jeden email!',                             'few': 'Mas {} emaily!', diff --git a/i18n/e2e/localeTest_zh.html b/i18n/e2e/localeTest_zh.html index 7e2feec5..3ccad5ed 100644 --- a/i18n/e2e/localeTest_zh.html +++ b/i18n/e2e/localeTest_zh.html @@ -5,25 +5,34 @@      <title>locale test</title>      <script src="../../build/angular.js" ng:autobind></script>      <script src="../../build/i18n/angular-locale_zh-cn.js"></script> +    <script> +      function AppCntl(){ +        this.input = 234234443432; +        this.plInput = 1; +        this.person1 = "Shanjian"; +        this.person2 = "Di"; +        this.plInput2 = 1; +      } +    </script>    </head> -  <body> +  <body ng:controller="AppCntl">      <h3>Datetime/Number/Currency filters demo:</h3> -    <input type="text" name="input" value="234234443432"><br> +    <input type="text" ng:model="input"><br>      date(medium): {{input | date:"medium"}}<br>      date(longDate): {{input | date:"longDate"}}<br>      number: {{input | number}}<br>      currency: {{input | currency }}      <hr/>      <h3>Pluralization demo:</h3> -    <input type="text" name="plInput" value="1"><br> +    <input type="text" ng:model="plInput"><br>      <ng:pluralize count="plInput"                    when= "{'other':'{}人在浏览该文件!'}">      </ng:pluralize>      <hr/>      <h3>Pluralization demo with offsets:</h3> -    Name of person1:<input type="text" name="person1" value="Shanjian"/><br/> -    Name of person2:<input type="text" name="person2" value="Di"/><br/> -    <input type="text" name="plInput2" value="1"><br> +    Name of person1:<input type="text" ng:model="person1"/><br/> +    Name of person2:<input type="text" ng:model="person2"/><br/> +    <input type="text" ng:model="plInput2"><br>      <ng:pluralize count="plInput2" offset=2                    when= "{'0':'没有人在浏览该文件!',                            '1': '{{person1}} 在浏览该文件!', diff --git a/images/docs/guide/form_data_flow.graffle b/images/docs/guide/form_data_flow.graffle new file mode 100644 index 00000000..f47217fd --- /dev/null +++ b/images/docs/guide/form_data_flow.graffle @@ -0,0 +1,2301 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> +	<key>ActiveLayerIndex</key> +	<integer>0</integer> +	<key>ApplicationVersion</key> +	<array> +		<string>com.omnigroup.OmniGrafflePro</string> +		<string>138.30.0.155892</string> +	</array> +	<key>AutoAdjust</key> +	<true/> +	<key>BackgroundGraphic</key> +	<dict> +		<key>Bounds</key> +		<string>{{0, 0}, {576, 733}}</string> +		<key>Class</key> +		<string>SolidGraphic</string> +		<key>ID</key> +		<integer>2</integer> +		<key>Style</key> +		<dict> +			<key>shadow</key> +			<dict> +				<key>Draws</key> +				<string>NO</string> +			</dict> +			<key>stroke</key> +			<dict> +				<key>Draws</key> +				<string>NO</string> +			</dict> +		</dict> +	</dict> +	<key>CanvasOrigin</key> +	<string>{0, 0}</string> +	<key>ColumnAlign</key> +	<integer>1</integer> +	<key>ColumnSpacing</key> +	<real>36</real> +	<key>CreationDate</key> +	<string>2011-10-05 20:45:08 -0700</string> +	<key>Creator</key> +	<string>Miško Hevery</string> +	<key>DisplayScale</key> +	<string>1 0/72 in = 1 0/72 in</string> +	<key>GraphDocumentVersion</key> +	<integer>6</integer> +	<key>GraphicsList</key> +	<array> +		<dict> +			<key>Bounds</key> +			<string>{{107, 265.5}, {65, 14}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>FitText</key> +			<string>YES</string> +			<key>Flow</key> +			<string>Resize</string> +			<key>ID</key> +			<integer>28</integer> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Align</key> +				<integer>0</integer> +				<key>Pad</key> +				<integer>0</integer> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fmodern\fcharset0 CourierNewPS-BoldMT;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural + +\f0\b\fs24 \cf0 $validate}</string> +				<key>VerticalPad</key> +				<integer>0</integer> +			</dict> +			<key>Wrap</key> +			<string>NO</string> +		</dict> +		<dict> +			<key>Class</key> +			<string>LineGraphic</string> +			<key>ControlPoints</key> +			<array> +				<string>{0, 0}</string> +				<string>{0, 29}</string> +				<string>{4.57764e-05, -29.0001}</string> +				<string>{0, 0}</string> +				<string>{0, 0}</string> +				<string>{0, 0}</string> +			</array> +			<key>Head</key> +			<dict> +				<key>ID</key> +				<integer>5</integer> +				<key>Info</key> +				<integer>8</integer> +			</dict> +			<key>ID</key> +			<integer>29</integer> +			<key>Points</key> +			<array> +				<string>{223, 272.5}</string> +				<string>{179, 270}</string> +				<string>{223, 273}</string> +				<string>{223, 272.5}</string> +			</array> +			<key>Style</key> +			<dict> +				<key>stroke</key> +				<dict> +					<key>Bezier</key> +					<true/> +					<key>HeadArrow</key> +					<string>FilledArrow</string> +					<key>LineType</key> +					<integer>1</integer> +					<key>TailArrow</key> +					<string>0</string> +				</dict> +			</dict> +			<key>Tail</key> +			<dict> +				<key>ID</key> +				<integer>5</integer> +				<key>Info</key> +				<integer>8</integer> +			</dict> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{334, 405.5}, {136, 44}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>FitText</key> +			<string>Vertical</string> +			<key>Flow</key> +			<string>Resize</string> +			<key>ID</key> +			<integer>22</integer> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Align</key> +				<integer>0</integer> +				<key>Pad</key> +				<integer>0</integer> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fmodern\fcharset0 CourierNewPS-BoldMT;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs24 \cf0 copy  +\f1\b $modelValue +\f0\b0  \ +to model  +\f1\b property\ +$validate}</string> +				<key>VerticalPad</key> +				<integer>0</integer> +			</dict> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{330, 189.25}, {124, 66.5}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>ID</key> +			<integer>21</integer> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Align</key> +				<integer>0</integer> +				<key>Pad</key> +				<integer>0</integer> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fmodern\fcharset0 CourierNewPS-BoldMT;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs24 \cf0 DOM Event\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f1\b \cf0 $emit(\ +  '$viewChange', \ +  value)}</string> +				<key>VerticalPad</key> +				<integer>0</integer> +			</dict> +			<key>Wrap</key> +			<string>NO</string> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{151, 215.5}, {65, 14}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>FitText</key> +			<string>YES</string> +			<key>Flow</key> +			<string>Resize</string> +			<key>ID</key> +			<integer>19</integer> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Pad</key> +				<integer>0</integer> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fmodern\fcharset0 CourierNewPS-BoldMT;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural + +\f0\b\fs24 \cf0 $render()}</string> +				<key>VerticalPad</key> +				<integer>0</integer> +			</dict> +			<key>Wrap</key> +			<string>NO</string> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{330, 315}, {87, 14}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>FitText</key> +			<string>YES</string> +			<key>Flow</key> +			<string>Resize</string> +			<key>ID</key> +			<integer>17</integer> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Pad</key> +				<integer>0</integer> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fmodern\fcharset0 CourierNewPS-BoldMT;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural + +\f0\b\fs24 \cf0 $parseView()}</string> +				<key>VerticalPad</key> +				<integer>0</integer> +			</dict> +			<key>Wrap</key> +			<string>NO</string> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{121, 315}, {94, 14}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>FitText</key> +			<string>YES</string> +			<key>Flow</key> +			<string>Resize</string> +			<key>ID</key> +			<integer>16</integer> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Pad</key> +				<integer>0</integer> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fmodern\fcharset0 CourierNewPS-BoldMT;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural + +\f0\b\fs24 \cf0 $parseModel()}</string> +				<key>VerticalPad</key> +				<integer>0</integer> +			</dict> +			<key>Wrap</key> +			<string>NO</string> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{164, 414.5}, {51, 28}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>FitText</key> +			<string>YES</string> +			<key>Flow</key> +			<string>Resize</string> +			<key>ID</key> +			<integer>15</integer> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>Draws</key> +					<string>NO</string> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Align</key> +				<integer>0</integer> +				<key>Pad</key> +				<integer>0</integer> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fmodern\fcharset0 CourierNewPS-BoldMT;\f1\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs24 \cf0 $watch  +\f1\b0 \ +callback}</string> +				<key>VerticalPad</key> +				<integer>0</integer> +			</dict> +			<key>Wrap</key> +			<string>NO</string> +		</dict> +		<dict> +			<key>Class</key> +			<string>LineGraphic</string> +			<key>Head</key> +			<dict> +				<key>ID</key> +				<integer>8</integer> +				<key>Info</key> +				<integer>4</integer> +			</dict> +			<key>ID</key> +			<integer>14</integer> +			<key>Points</key> +			<array> +				<string>{229.332, 257.285}</string> +				<string>{216, 222}</string> +				<string>{229.332, 186.715}</string> +			</array> +			<key>Style</key> +			<dict> +				<key>stroke</key> +				<dict> +					<key>HeadArrow</key> +					<string>FilledArrow</string> +					<key>LineType</key> +					<integer>1</integer> +					<key>TailArrow</key> +					<string>0</string> +				</dict> +			</dict> +			<key>Tail</key> +			<dict> +				<key>ID</key> +				<integer>5</integer> +				<key>Info</key> +				<integer>3</integer> +			</dict> +		</dict> +		<dict> +			<key>Class</key> +			<string>LineGraphic</string> +			<key>Head</key> +			<dict> +				<key>ID</key> +				<integer>5</integer> +				<key>Info</key> +				<integer>4</integer> +			</dict> +			<key>ID</key> +			<integer>13</integer> +			<key>Points</key> +			<array> +				<string>{229.332, 350.285}</string> +				<string>{219, 320}</string> +				<string>{229.332, 287.715}</string> +			</array> +			<key>Style</key> +			<dict> +				<key>stroke</key> +				<dict> +					<key>HeadArrow</key> +					<string>FilledArrow</string> +					<key>LineType</key> +					<integer>1</integer> +					<key>TailArrow</key> +					<string>0</string> +				</dict> +			</dict> +			<key>Tail</key> +			<dict> +				<key>ID</key> +				<integer>4</integer> +				<key>Info</key> +				<integer>3</integer> +			</dict> +		</dict> +		<dict> +			<key>Class</key> +			<string>LineGraphic</string> +			<key>Head</key> +			<dict> +				<key>ID</key> +				<integer>4</integer> +				<key>Info</key> +				<integer>4</integer> +			</dict> +			<key>ID</key> +			<integer>12</integer> +			<key>Points</key> +			<array> +				<string>{229.332, 479.49}</string> +				<string>{214, 425.705}</string> +				<string>{229.332, 380.715}</string> +			</array> +			<key>Style</key> +			<dict> +				<key>stroke</key> +				<dict> +					<key>HeadArrow</key> +					<string>FilledArrow</string> +					<key>LineType</key> +					<integer>1</integer> +					<key>TailArrow</key> +					<string>0</string> +				</dict> +			</dict> +			<key>Tail</key> +			<dict> +				<key>ID</key> +				<integer>3</integer> +				<key>Info</key> +				<integer>3</integer> +			</dict> +		</dict> +		<dict> +			<key>Class</key> +			<string>LineGraphic</string> +			<key>Head</key> +			<dict> +				<key>ID</key> +				<integer>3</integer> +				<key>Info</key> +				<integer>2</integer> +			</dict> +			<key>ID</key> +			<integer>11</integer> +			<key>Points</key> +			<array> +				<string>{313.668, 380.715}</string> +				<string>{329, 418.705}</string> +				<string>{313.668, 479.49}</string> +			</array> +			<key>Style</key> +			<dict> +				<key>stroke</key> +				<dict> +					<key>HeadArrow</key> +					<string>FilledArrow</string> +					<key>LineType</key> +					<integer>1</integer> +					<key>TailArrow</key> +					<string>0</string> +				</dict> +			</dict> +			<key>Tail</key> +			<dict> +				<key>ID</key> +				<integer>4</integer> +				<key>Info</key> +				<integer>1</integer> +			</dict> +		</dict> +		<dict> +			<key>Class</key> +			<string>LineGraphic</string> +			<key>Head</key> +			<dict> +				<key>ID</key> +				<integer>4</integer> +			</dict> +			<key>ID</key> +			<integer>10</integer> +			<key>Points</key> +			<array> +				<string>{313.668, 287.715}</string> +				<string>{325, 321}</string> +				<string>{313.668, 350.285}</string> +			</array> +			<key>Style</key> +			<dict> +				<key>stroke</key> +				<dict> +					<key>HeadArrow</key> +					<string>FilledArrow</string> +					<key>LineType</key> +					<integer>1</integer> +					<key>TailArrow</key> +					<string>0</string> +				</dict> +			</dict> +			<key>Tail</key> +			<dict> +				<key>ID</key> +				<integer>5</integer> +			</dict> +		</dict> +		<dict> +			<key>Class</key> +			<string>LineGraphic</string> +			<key>Head</key> +			<dict> +				<key>ID</key> +				<integer>5</integer> +				<key>Info</key> +				<integer>2</integer> +			</dict> +			<key>ID</key> +			<integer>9</integer> +			<key>Points</key> +			<array> +				<string>{313.668, 186.715}</string> +				<string>{325, 218}</string> +				<string>{313.668, 257.285}</string> +			</array> +			<key>Style</key> +			<dict> +				<key>stroke</key> +				<dict> +					<key>HeadArrow</key> +					<string>FilledArrow</string> +					<key>LineType</key> +					<integer>1</integer> +					<key>TailArrow</key> +					<string>0</string> +				</dict> +			</dict> +			<key>Tail</key> +			<dict> +				<key>ID</key> +				<integer>8</integer> +				<key>Info</key> +				<integer>1</integer> +			</dict> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{223, 154}, {97, 35}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>ID</key> +			<integer>8</integer> +			<key>Magnets</key> +			<array> +				<string>{1, 1}</string> +				<string>{1, -1}</string> +				<string>{-1, -1}</string> +				<string>{-1, 1}</string> +				<string>{0, 1}</string> +				<string>{0, -1}</string> +				<string>{1, 0}</string> +				<string>{-1, 0}</string> +			</array> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Color</key> +					<dict> +						<key>b</key> +						<string>0.302239</string> +						<key>g</key> +						<string>0.746867</string> +						<key>r</key> +						<string>0.964157</string> +					</dict> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Beneath</key> +					<string>YES</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>CornerRadius</key> +					<real>14</real> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural + +\f0\fs24 \cf0 DOM}</string> +			</dict> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{223, 255}, {97, 35}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>ID</key> +			<integer>5</integer> +			<key>Magnets</key> +			<array> +				<string>{1, 1}</string> +				<string>{1, -1}</string> +				<string>{-1, -1}</string> +				<string>{-1, 1}</string> +				<string>{0, 1}</string> +				<string>{0, -1}</string> +				<string>{1, 0}</string> +				<string>{-1, 0}</string> +			</array> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Color</key> +					<dict> +						<key>b</key> +						<string>0.59983</string> +						<key>g</key> +						<string>0.937216</string> +						<key>r</key> +						<string>0.609412</string> +					</dict> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Beneath</key> +					<string>YES</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>CornerRadius</key> +					<real>14</real> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fmodern\fcharset0 CourierNewPS-BoldMT;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural + +\f0\b\fs24 \cf0 $viewValue}</string> +			</dict> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{223, 348}, {97, 35}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>ID</key> +			<integer>4</integer> +			<key>Magnets</key> +			<array> +				<string>{1, 1}</string> +				<string>{1, -1}</string> +				<string>{-1, -1}</string> +				<string>{-1, 1}</string> +				<string>{0, 1}</string> +				<string>{0, -1}</string> +				<string>{1, 0}</string> +				<string>{-1, 0}</string> +			</array> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Color</key> +					<dict> +						<key>b</key> +						<string>0.59983</string> +						<key>g</key> +						<string>0.937216</string> +						<key>r</key> +						<string>0.609412</string> +					</dict> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Beneath</key> +					<string>YES</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>CornerRadius</key> +					<real>14</real> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fmodern\fcharset0 CourierNewPS-BoldMT;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural + +\f0\b\fs24 \cf0 $modelValue}</string> +			</dict> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{223, 477.205}, {97, 35}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>ID</key> +			<integer>3</integer> +			<key>Magnets</key> +			<array> +				<string>{1, 1}</string> +				<string>{1, -1}</string> +				<string>{-1, -1}</string> +				<string>{-1, 1}</string> +				<string>{0, 1}</string> +				<string>{0, -1}</string> +				<string>{1, 0}</string> +				<string>{-1, 0}</string> +			</array> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Color</key> +					<dict> +						<key>b</key> +						<string>0.59983</string> +						<key>g</key> +						<string>0.937216</string> +						<key>r</key> +						<string>0.609412</string> +					</dict> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Beneath</key> +					<string>YES</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>CornerRadius</key> +					<real>14</real> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fmodern\fcharset0 CourierNewPS-BoldMT;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural + +\f0\b\fs24 \cf0 property}</string> +			</dict> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{94, 142}, {365, 259}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>ID</key> +			<integer>6</integer> +			<key>Magnets</key> +			<array> +				<string>{1, 1}</string> +				<string>{1, -1}</string> +				<string>{-1, -1}</string> +				<string>{-1, 1}</string> +				<string>{0, 1}</string> +				<string>{0, -1}</string> +				<string>{1, 0}</string> +				<string>{-1, 0}</string> +			</array> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Color</key> +					<dict> +						<key>b</key> +						<string>1</string> +						<key>g</key> +						<string>0.928021</string> +						<key>r</key> +						<string>0.860007</string> +					</dict> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Beneath</key> +					<string>YES</string> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>CornerRadius</key> +					<real>14</real> +					<key>Pattern</key> +					<integer>1</integer> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Align</key> +				<integer>0</integer> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs28 \cf0 Widget (scope)}</string> +			</dict> +			<key>TextPlacement</key> +			<integer>0</integer> +		</dict> +		<dict> +			<key>Bounds</key> +			<string>{{94, 454}, {365, 87.7054}}</string> +			<key>Class</key> +			<string>ShapedGraphic</string> +			<key>ID</key> +			<integer>7</integer> +			<key>Magnets</key> +			<array> +				<string>{1, 1}</string> +				<string>{1, -1}</string> +				<string>{-1, -1}</string> +				<string>{-1, 1}</string> +				<string>{0, 1}</string> +				<string>{0, -1}</string> +				<string>{1, 0}</string> +				<string>{-1, 0}</string> +			</array> +			<key>Shape</key> +			<string>Rectangle</string> +			<key>Style</key> +			<dict> +				<key>fill</key> +				<dict> +					<key>Color</key> +					<dict> +						<key>b</key> +						<string>1</string> +						<key>g</key> +						<string>0.930219</string> +						<key>r</key> +						<string>0.859335</string> +					</dict> +				</dict> +				<key>shadow</key> +				<dict> +					<key>Beneath</key> +					<string>YES</string> +					<key>Draws</key> +					<string>NO</string> +				</dict> +				<key>stroke</key> +				<dict> +					<key>CornerRadius</key> +					<real>14</real> +					<key>Pattern</key> +					<integer>1</integer> +				</dict> +			</dict> +			<key>Text</key> +			<dict> +				<key>Align</key> +				<integer>0</integer> +				<key>Text</key> +				<string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs28 \cf0 Controller (scope)}</string> +			</dict> +			<key>TextPlacement</key> +			<integer>0</integer> +		</dict> +	</array> +	<key>GridInfo</key> +	<dict/> +	<key>GuidesLocked</key> +	<string>NO</string> +	<key>GuidesVisible</key> +	<string>YES</string> +	<key>HPages</key> +	<integer>1</integer> +	<key>ImageCounter</key> +	<integer>1</integer> +	<key>KeepToScale</key> +	<false/> +	<key>Layers</key> +	<array> +		<dict> +			<key>Lock</key> +			<string>NO</string> +			<key>Name</key> +			<string>Layer 1</string> +			<key>Print</key> +			<string>YES</string> +			<key>View</key> +			<string>YES</string> +		</dict> +	</array> +	<key>LayoutInfo</key> +	<dict> +		<key>Animate</key> +		<string>NO</string> +		<key>circoMinDist</key> +		<real>18</real> +		<key>circoSeparation</key> +		<real>0.0</real> +		<key>layoutEngine</key> +		<string>dot</string> +		<key>neatoSeparation</key> +		<real>0.0</real> +		<key>twopiSeparation</key> +		<real>0.0</real> +	</dict> +	<key>LinksVisible</key> +	<string>NO</string> +	<key>MagnetsVisible</key> +	<string>NO</string> +	<key>MasterSheets</key> +	<array/> +	<key>ModificationDate</key> +	<string>2011-10-05 21:16:40 -0700</string> +	<key>Modifier</key> +	<string>Miško Hevery</string> +	<key>NotesVisible</key> +	<string>NO</string> +	<key>Orientation</key> +	<integer>2</integer> +	<key>OriginVisible</key> +	<string>NO</string> +	<key>PageBreaks</key> +	<string>YES</string> +	<key>PrintInfo</key> +	<dict> +		<key>NSBottomMargin</key> +		<array> +			<string>float</string> +			<string>41</string> +		</array> +		<key>NSLeftMargin</key> +		<array> +			<string>float</string> +			<string>18</string> +		</array> +		<key>NSPaperSize</key> +		<array> +			<string>coded</string> +			<string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAx7X05TU2l6ZT1mZn2WgWQCgRgDhg==</string> +		</array> +		<key>NSRightMargin</key> +		<array> +			<string>float</string> +			<string>18</string> +		</array> +		<key>NSTopMargin</key> +		<array> +			<string>float</string> +			<string>18</string> +		</array> +	</dict> +	<key>PrintOnePage</key> +	<false/> +	<key>QuickLookPreview</key> +	<data> +	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 +	eeKbBcY+wZcSYxsNHlewj+ixyY5xwJsW3sf8hELZMt6/rArIJ2EwXjkIMQy8xcMY1pKJ +	nSHe63w/wpXD0sGAsL4wCnTAbFgYFmBcPVCZDS4AAbTwepitdH7kqoBQKyfyybOHrwrA +	Bo9n/uCe8Mf53eYjcu9HEv9HR9/Js3FMBPly92hRELL9m/P6VIYHtvwm1wr/rTWtzeZ/ +	7dUMSfzBFiJGLeYNHhOu/d57EGKGdY4Ifnr2AQ+LeBU8eXZzfvtl+5opiHcnfJ47sk/o +	Z0YRJuzhacC8OUJHD69bMs3PuHPdO4vBRVYeXaDle2el9gJM+EL4MxC2bwcOQrZzu/30 +	fnt77ySIJbSMXwLY8bTgNPiGFvmSuhe44she1WP5sbzQvK4PMt993T5X+RDxoJoLd+3E +	4CSGE2vzP4hjX8khH7dXF3fvTo6fU93c0wY38z7Ztf5G7sWnH88//br95h/HaX98D/tn +	1PZgyy2g3721/sps1z1sYzyUs414BKgXjZ1zQlhZzqm80O2oFh6f8xnc1MKf2AdfnZW8 +	Og+nsTvVg6ghf+fAUMc3PJBDvfy+7+dYBm889KPTDDvC2Cv+omTh2Hu2gs/sRzTysRFf +	IB+ZMSoEGhECN+X1vwFViXzVavAKmTFsz4/FPMm7f3/B/3tYIrxdoL/w1fsEPHx+ePIM +	agC0bx83w4PEz/nlxfvzu55shenIf7YyzNWfwWrSmX8Hw5i23b9zhWGWAg+c8hsZ60gJ +	OU7cHOl7Ea0jrEEcjUOekTtktInrCONtXLokXD9+D0Uki6sQ/rKF0Sz+lkQiH0YBIAnE +	ydicQUFElQKI6RHSp0XupvSTh50fc8ulByIEOGOYQOw1T4elIhyJEuPwmVimwxUZcY/c +	TYfpaufH/DIoL7b+CSk8wn7oT2BB9k7xp78AQQi6fAplbmRzdHJlYW0KZW5kb2JqCjYg +	MCBvYmoKMzIxNwplbmRvYmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDQg +	MCBSIC9SZXNvdXJjZXMgNyAwIFIgL0NvbnRlbnRzIDUgMCBSIC9NZWRpYUJveCBbMCAw +	IDU3NiA3MzNdCj4+CmVuZG9iago3IDAgb2JqCjw8IC9Qcm9jU2V0IFsgL1BERiAvVGV4 +	dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSSBdIC9Db2xvclNwYWNlIDw8IC9DczIgOSAw +	IFIKL0NzMyAxMCAwIFIgL0NzMSA4IDAgUiA+PiAvRm9udCA8PCAvRjMuMCAxNyAwIFIg +	L0YyLjAgMTQgMCBSIC9GMS4wIDExIDAgUgo+PiAvWE9iamVjdCA8PCAvSW0xIDEyIDAg +	UiAvSW0yIDE1IDAgUiA+PiA+PgplbmRvYmoKMTIgMCBvYmoKPDwgL0xlbmd0aCAxMyAw +	IFIgL1R5cGUgL1hPYmplY3QgL1N1YnR5cGUgL0ltYWdlIC9XaWR0aCAyMzggL0hlaWdo +	dCAxMTQgL0ludGVycG9sYXRlCnRydWUgL0NvbG9yU3BhY2UgMTggMCBSIC9JbnRlbnQg +	L1BlcmNlcHR1YWwgL1NNYXNrIDE5IDAgUiAvQml0c1BlckNvbXBvbmVudAo4IC9GaWx0 +	ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae3QMQEAAADCoPVPbQwfiEBhwIABAwYM +	GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB +	AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg +	wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM +	GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB +	AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg +	wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM +	GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwMBzYD4DAAEKZW5k +	c3RyZWFtCmVuZG9iagoxMyAwIG9iagozNzgKZW5kb2JqCjE1IDAgb2JqCjw8IC9MZW5n +	dGggMTYgMCBSIC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggMjM4 +	IC9IZWlnaHQgMTE0IC9JbnRlcnBvbGF0ZQp0cnVlIC9Db2xvclNwYWNlIDE4IDAgUiAv +	SW50ZW50IC9QZXJjZXB0dWFsIC9TTWFzayAyMSAwIFIgL0JpdHNQZXJDb21wb25lbnQK +	OCAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAHt0DEBAAAAwqD1T20MH4hA +	YcCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG +	DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA +	AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw +	YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG +	DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA +	AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw +	YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMDAc2A+ +	AwABCmVuZHN0cmVhbQplbmRvYmoKMTYgMCBvYmoKMzc4CmVuZG9iagoyMSAwIG9iago8 +	PCAvTGVuZ3RoIDIyIDAgUiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1hZ2UgL1dp +	ZHRoIDIzOCAvSGVpZ2h0IDExNCAvQ29sb3JTcGFjZQovRGV2aWNlR3JheSAvSW50ZXJw +	b2xhdGUgdHJ1ZSAvQml0c1BlckNvbXBvbmVudCA4IC9GaWx0ZXIgL0ZsYXRlRGVjb2Rl +	ID4+CnN0cmVhbQp4Ae2c+Ttb+RfHVY2tliC2JLbQEMtE1Iillkp5LDFqN0ztrS2G0uBh +	RKla4rGXGlWqdhr73s4z/9r3nM9NULTVmX5ddZ1f+rT15HNe933O+3xu3PvR0bmO6ytw +	fQWur8D1Ffhhr8CNSxT/t4uIjLpHcZPmOMpEFzP7ntgazps39fT0frpUAQndvEnIvw8x +	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 +	ULlC2dE3Apa8srGzp2nbcyxL049QtB/2djZWwJRH+jqUinIygkQeLhxr3Cmf1JbQcl2E +	vkGR8Wn5cgU1gJaAVmtSNKGcY1kN7T7QLuEI6mxWyPPT4iODfGGn/BlaE5Y1bC7EQZGy +	dDJucd+4tHJkyedYlqYfOaTdRNrR/k4cuOmyyCAxbC+sWbC9OK0ttZXCm9uMAnndM7JL +	1oxb/DiaSM61LOZHRtAqoe16VicvyMBbXO1m6kxaHmiLtIUVFK12c/ED0c6CtkBbUaih +	5ZGt49m0rp/QHm6lLrm0ZEul0RY2U8dpXa9pdeBbKehbZlUyejJTXIpREwj2UgzaXTBp +	56jHqLsCht3xMexunknf1DDrWzhGfcN6g1HfnjPsNyMM+60Xk36jqcOo31brMOtJBGY9 +	ZXIFnyByP3zs4sSvb3V0mPV0GNW45KFOBjz5B+Iy6alOZj2xq8Oop7GxlMnj2Mx40h5L +	Gd8GYsZbFFpxP31DpvRHekOm9PxvyJCRi2/xMePtJ4LLmDfbgBaNiilvLR7WMrxYffXf +	SEVxGfS2MVXLmtfmr/yb5HgrBK3LlFMCyJ0fOBVDToDQYdbpHke4TDi5hbQuHEH0uVN5 +	wi/TqTzh//lUHsCF+wPGnLhEcMGZqdO04OSwH+c0Leq4pW87TQueXMZtBsrLhJPSKFyQ +	lyGn4FHVDPJSvFf9hEOUlypn4L36p1fiawdaXiacTKrlRb/SnMV6pU+dpXihoHWZcaIw +	8lIVTSFf9dOiKV4kxi4+DJoPAsezkrWBmWmz/O5/4odflvjucNcfeH0Frq/A9RW4vgIX +	dwX+B0Voq7YKZW5kc3RyZWFtCmVuZG9iagoyMiAwIG9iagozMzYzCmVuZG9iagoxOSAw +	IG9iago8PCAvTGVuZ3RoIDIwIDAgUiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1h +	Z2UgL1dpZHRoIDIzOCAvSGVpZ2h0IDExNCAvQ29sb3JTcGFjZQovRGV2aWNlR3JheSAv +	SW50ZXJwb2xhdGUgdHJ1ZSAvQml0c1BlckNvbXBvbmVudCA4IC9GaWx0ZXIgL0ZsYXRl +	RGVjb2RlID4+CnN0cmVhbQp4Ae2c+T9b6RfHVY2tRRBbEluiIZaJqBFLLRW8LDFqN0yt +	bUPF0GrwYkSpWuJFLKVGW6p2Gvvadl7zr33PeW5SirY606+rrvNLX8XrPs/7fs75nOcm +	93mMjC7i4g5c3IGLO3BxB37YO3DpDMX/7SYio/F+XKY59mdijDP7nth6zsuXTUxMfjpT +	ARO6fJmQfx9iSlIARUpTUzMzc0NY0BaGGZibmZma4rwo5P+sMVGVkJoCpoWF5RWMqxBW +	tAbOgEzF0sLC3NwMkU1Q4/+iMLIiKiEF0KtW1tY2NiyW7ZkIFsvGxtraCqgt9cQI/G95 +	DawgqiWC2gClnT2b7eDgCOFEa+AMHBzYbHs7W1uWDSJbgsSUwP+Gd5/VAlFZtnZsB0B0 +	4XA4XC4PwpXGwPG5XJiKi5OTowPbzpaFwBb/mhe7DeSwmTmwAqo92xFAgdDdw8PTk88X +	0B58vqenh4e7qysPkB3Z9gBs4MX6/aaWRBWsKbBaWYOqjs5A6u7BF3gJhd4+IghfKvxO +	PfQD4xx8vIVCLwEfkHkcZ0dQ2NoK9DUlfvUNuJjFJuhNqKsd28mF6wqkQm+Rr3/Az2Jx +	oAQjiMYgEwgUi38O8PcVeQuB2JXr4sS2Q33RoU2+wa0AFpzYFLyJsHJ47p4CocgvQCwJ +	Cg4OkUpDw8LCD0TEqcWBQcPDwkKl0pDg4CCJOMBPJBR4uvM4hBf8ish7QrOispgIawu6 +	8tz513z8AgKDgqVh4Tcio2JibsbKSMTRFNTosTdjYqIib4SHSYODAgP8fK7x3Xmgry2R +	98TZTClrBhXLsnNw5rrxr4n8xUG/hIZHxsTK4hMSk5JT5HJ5amrqr7QFDA5TSElOSkyI +	l8XGRIaH/hIk9hdd47txnR3sWFC9Zoh7AnU/wlrb2jtyXD28fPzF16WAKktISklNS8/I +	zM7Oyc3Nozlyc3OyszMz0tNSU5ISZAAsvS729/HycOU42ttanxiX1Cx4sTVkMdddIPQD +	1ohoWUJy6q3MnLz83wsLi4tLSssw7tAUZPDSkuLiwsLf8/NyMm+lJifIoiOA108ocOdi +	NoM3E3W/0oiIG0MaA6wzz8NLFCAJiYiJS5TfysorKCwuu3OvvOJ+pVJZ9QdENU2BY1cp +	lZX3K8rv3SkrLizIy7olT4yLiQiRBIi8PHjOBBeS+Wt99yCsq6fQVxwcFiVLTM3IyS8s +	uau4r6yueVirUtXV1zfQHPX1dSpV7cOaauV9xd2SwvycjNREWVRYsNhX6Ol6Qly0458o +	ZV0A1h+FTZCn5xQUlSkqqx88qmtobGpWq1set9Iej1vU6uamxoa6Rw+qKxVlRQU56fIE +	lNcfcF306uJTwueTGYoW+ixJY1e+t79EGilLSsvKLyqrUD5QNTQ1t7Q+ae/o7Ozq7tbQ +	HN3dXZ2dHe1PWluamxpUD5QVZUX5WWlJskipxN+bT6kLfReM+bO0JI/NLa1YbGdUNig0 +	Ki45Pfd2qUL5UNXY3Nre0aXp6e3Tavv7BwYGaY2Bgf5+rbavt0fT1dHe2tyoeqhUlN7O +	TU+OiwoNQnWd2SwrS3CqL4iL0ppZXLWxd+J5IGx0vDwjr+huZY2qUd3W0d3Tpx0cejY8 +	8nwU4y8ag0zg+cjws6FBbV9Pd0ebulFVU3m3KC9DHh+NuB48J3ubq9B2Py+uIY/tHDkA +	KwmNTpBn5hcrqmob1G2dmt7+oeHno2MvXr4aHx+fgHhNW+DoMIlXL1+MjT4fHurv1XS2 +	qRtqqxTF+ZnyhOhQqF0PjqOd9ZUv5fIleBTAonXguHn5SqRR8fKsgpLyalVT61NN3+Dw +	6NjL8YnJyamp6enpGYhZ2gJHh0lMTU1OToy/HBsdHuzTPG1tUlWXlxRkyeOjpBJfLzeO +	A7RdzOXjK9eQx1C0ApE4JDJOnllQWlFT19zW1TswPPpi/PWb6Zm5ufmFhUWMJRqDTGBh +	YX5ubmb6zevxF6PDA71dbc11NRWlBZnyuMgQsUgApfulXNZLa+fI9RAGBEfIUjLyAbZe +	3a7RDgHr5PTs3MLikk6nW15eWVlZpTVgAsvLMJWlxYW52elJ4B3SatrV9YCbn5EiiwgO +	EHpwSS5/TlxjY7QoFtvFzctPEhaTlJ5XTMH2D48B69zCkm4ZINfW1zfORKyvr62urizr +	lhbmgHdsuJ/CLc5LT4oJk/h5ubmwWWhUxsbHNCFMZKhaOyce30ccEpWQllukqK5Tt/cM +	jLyYmJpbeKsD0I3Nza1tfezQFoYZbG1ubgCy7u3C3NTEi5GBnnZ1XbWiKDctISpE7MPn +	OaFRHW/LkMimKC3H/Zr/9XCZPOv23SpVc7sGYCen55d0q4C6vb2zs7t3ZmJ3Z2d7G4BX +	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 +	W7CXsQPYYew7HA7HwBniHHB+uBhcMm41rgS3D9eMu4Drww3hJvF4vCreFO+CD8Fz8GJ8 +	Ib4Kfxx/Ht+PH8a/J5AJWgRrgg8hliAkbCRUEBoI5wj9hBHCNFGBqE90IoYQecRcYimx +	jthBvEkcJk6TFEmGJBdSJCmZtIFUSWoiXSY9Jr0hk8k6ZEdyGFlAXk+uJJ8gXyUPkj9Q +	lCgmFE9KHEVC2U45SrlAeUB5Q6VSDahu1FiqmLqdWk+9RH1KfS9HkzOX85fjya2Tq5Fr +	leuXeyVPlNeXd5dfLp8nXyF/Sv6m/LgCUcFAwVOBo7BWoUbhtMI9hUlFmqKVYohimmKJ +	YoPiNcVRJbySgZK3Ek+pQOmw0iWlIRpC06V50ri0TbQ62mXaMB1HN6T705PpxfQf6L30 +	CWUlZVvlKOUc5Rrls8pSBsIwYPgzUhmljJOMu4yP8zTmuc/jz9s2r2le/7wplfkqbip8 +	lSKVZpUBlY+qTFVv1RTVnaptqk/UMGomamFq2Wr71S6rjc+nz3eez51fNP/k/IfqsLqJ +	erj6avXD6j3qkxqaGr4aGRpVGpc0xjUZmm6ayZrlmuc0x7RoWgu1BFrlWue1XjCVme7M +	VGYls4s5oa2u7act0T6k3as9rWOos1hno06zzhNdki5bN0G3XLdTd0JPSy9YL1+vUe+h +	PlGfrZ+kv0e/W3/KwNAg2mCLQZvBqKGKob9hnmGj4WMjqpGr0SqjWqM7xjhjtnGK8T7j +	WyawiZ1JkkmNyU1T2NTeVGC6z7TPDGvmaCY0qzW7x6Kw3FlZrEbWoDnDPMh8o3mb+SsL +	PYtYi50W3RZfLO0sUy3rLB9ZKVkFWG206rD6w9rEmmtdY33HhmrjY7POpt3mta2pLd92 +	v+19O5pdsN0Wu067z/YO9iL7JvsxBz2HeIe9DvfYdHYou4R91RHr6OG4zvGM4wcneyex +	00mn351ZzinODc6jCwwX8BfULRhy0XHhuBxykS5kLoxfeHCh1FXbleNa6/rMTdeN53bE +	bcTd2D3Z/bj7Kw9LD5FHi8eUp5PnGs8LXoiXr1eRV6+3kvdi72rvpz46Pok+jT4Tvna+ +	q30v+GH9Av12+t3z1/Dn+tf7TwQ4BKwJ6AqkBEYEVgc+CzIJEgV1BMPBAcG7gh8v0l8k +	XNQWAkL8Q3aFPAk1DF0V+nMYLiw0rCbsebhVeH54dwQtYkVEQ8S7SI/I0shHi40WSxZ3 +	RslHxUXVR01Fe0WXRUuXWCxZs+RGjFqMIKY9Fh8bFXskdnKp99LdS4fj7OIK4+4uM1yW +	s+zacrXlqcvPrpBfwVlxKh4bHx3fEP+JE8Kp5Uyu9F+5d+UE15O7h/uS58Yr543xXfhl +	/JEEl4SyhNFEl8RdiWNJrkkVSeMCT0G14HWyX/KB5KmUkJSjKTOp0anNaYS0+LTTQiVh +	irArXTM9J70vwzSjMEO6ymnV7lUTokDRkUwoc1lmu5iO/kz1SIwkmyWDWQuzarLeZ0dl +	n8pRzBHm9OSa5G7LHcnzyft+NWY1d3Vnvnb+hvzBNe5rDq2F1q5c27lOd13BuuH1vuuP +	bSBtSNnwy0bLjWUb326K3tRRoFGwvmBos+/mxkK5QlHhvS3OWw5sxWwVbO3dZrOtatuX +	Il7R9WLL4oriTyXckuvfWX1X+d3M9oTtvaX2pft34HYId9zd6brzWJliWV7Z0K7gXa3l +	zPKi8re7V+y+VmFbcWAPaY9kj7QyqLK9Sq9qR9Wn6qTqgRqPmua96nu37Z3ax9vXv99t +	f9MBjQPFBz4eFBy8f8j3UGutQW3FYdzhrMPP66Lqur9nf19/RO1I8ZHPR4VHpcfCj3XV +	O9TXN6g3lDbCjZLGseNxx2/94PVDexOr6VAzo7n4BDghOfHix/gf754MPNl5in2q6Sf9 +	n/a20FqKWqHW3NaJtqQ2aXtMe9/pgNOdHc4dLT+b/3z0jPaZmrPKZ0vPkc4VnJs5n3d+ +	8kLGhfGLiReHOld0Prq05NKdrrCu3suBl69e8blyqdu9+/xVl6tnrjldO32dfb3thv2N +	1h67npZf7H5p6bXvbb3pcLP9luOtjr4Ffef6Xfsv3va6feWO/50bA4sG+u4uvnv/Xtw9 +	6X3e/dEHqQ9eP8x6OP1o/WPs46InCk8qnqo/rf3V+Ndmqb307KDXYM+ziGePhrhDL/+V +	+a9PwwXPqc8rRrRG6ketR8+M+YzderH0xfDLjJfT44W/Kf6295XRq59+d/u9Z2LJxPBr +	0euZP0reqL45+tb2bedk6OTTd2nvpqeK3qu+P/aB/aH7Y/THkensT/hPlZ+NP3d8Cfzy +	eCZtZubf94Tz+wplbmRzdHJlYW0KZW5kb2JqCjI0IDAgb2JqCjI2MTIKZW5kb2JqCjkg +	MCBvYmoKWyAvSUNDQmFzZWQgMjMgMCBSIF0KZW5kb2JqCjI1IDAgb2JqCjw8IC9MZW5n +	dGggMjYgMCBSIC9OIDEgL0FsdGVybmF0ZSAvRGV2aWNlR3JheSAvRmlsdGVyIC9GbGF0 +	ZURlY29kZSA+PgpzdHJlYW0KeAGFUk9IFFEc/s02EoSIQYV4iHcKCZUprKyg2nZ1WZVt +	W5XSohhn37qjszPTm9k1xZMEXaI8dQ+iY3Ts0KGbl6LArEvXIKkgCDx16PvN7OoohG95 +	O9/7/f1+33tEbZ2m7zspQVRzQ5UrpaduTk2Lgx8pRR3UTlimFfjpYnGMseu5kr+719Zn +	0tiy3se1dvv2PbWVZWAh6i22txD6IZFmAB+ZnyhlgLPAHZav2D4BPFgOrBrwI6IDD5q5 +	MNPRnHSlsi2RU+aiKCqvYjtJrvv5uca+i7WJg/5cj2bWjr2z6qrRTNS090ShvA+uRBnP +	X1T2bDUUpw3jnEhDGinyrtXfK0zHEZErEEoGUjVkuZ9qTp114HUYu126k+P49hClPslg +	qIm16bKZHYV9AHYqy+wQ8AXo8bJiD+eBe2H/W1HDk8AnYT9kh3nWrR/2F65T4HuEPTXg +	zhSuxfHaih9eLQFD91QjaIxzTcTT1zlzpIjvMdQZmPdGOaYLMXeWqhM3gDthH1mqZgqx +	Xfuu6iXuewJ30+M70Zs5C1ygHElysRXZFNA8CVgUfYuwSQ48Ps4eVeB3qJjAHLmJ3M0o +	9x7VERtno1KBVnqNV8ZP47nxxfhlbBjPgH6sdtd7fP/p4xV117Y+PPmNetw5rr2dG1Vh +	VnFlC93/xzKEj9knOabB06FZWGvYduQPmsxMsAwoxH8FPpf6khNV3NXu7bhFEsxQPixs +	JbpLVG4p1Oo9g0qsHCvYAHZwksQsWhy4U2u6OXh32CJ6bflNV7Lrhv769nr72vIebcqo +	KSgTzbNEZpSxW6Pk3Xjb/WaREZ84Or7nvYpayf5JRRA/hTlaKvIUVfRWUNbEb2cOfhu2 +	flw/pef1Qf08CT2tn9Gv6KMRvgx0Sc/Cc1Efo0nwsGkh4hKgioMz1E5UY40D4inx8rRb +	ZJH9D0AZ/WYKZW5kc3RyZWFtCmVuZG9iagoyNiAwIG9iago3MDQKZW5kb2JqCjEwIDAg +	b2JqClsgL0lDQ0Jhc2VkIDI1IDAgUiBdCmVuZG9iagoyNyAwIG9iago8PCAvTGVuZ3Ro +	IDI4IDAgUiAvTiAzIC9BbHRlcm5hdGUgL0RldmljZVJHQiAvRmlsdGVyIC9GbGF0ZURl +	Y29kZSA+PgpzdHJlYW0KeAGFVM9rE0EU/jZuqdAiCFprDrJ4kCJJWatoRdQ2/RFiawzb +	H7ZFkGQzSdZuNuvuJrWliOTi0SreRe2hB/+AHnrwZC9KhVpFKN6rKGKhFy3xzW5MtqXq +	wM5+8943731vdt8ADXLSNPWABOQNx1KiEWlsfEJq/IgAjqIJQTQlVdvsTiQGQYNz+Xvn +	2HoPgVtWw3v7d7J3rZrStpoHhP1A4Eea2Sqw7xdxClkSAog836Epx3QI3+PY8uyPOU55 +	eMG1Dys9xFkifEA1Lc5/TbhTzSXTQINIOJT1cVI+nNeLlNcdB2luZsbIEL1PkKa7zO6r +	YqGcTvYOkL2d9H5Os94+wiHCCxmtP0a4jZ71jNU/4mHhpObEhj0cGDX0+GAVtxqp+DXC +	FF8QTSeiVHHZLg3xmK79VvJKgnCQOMpkYYBzWkhP10xu+LqHBX0m1xOv4ndWUeF5jxNn +	3tTd70XaAq8wDh0MGgyaDUhQEEUEYZiwUECGPBoxNLJyPyOrBhuTezJ1JGq7dGJEsUF7 +	Ntw9t1Gk3Tz+KCJxlEO1CJL8Qf4qr8lP5Xn5y1yw2Fb3lK2bmrry4DvF5Zm5Gh7X08jj +	c01efJXUdpNXR5aseXq8muwaP+xXlzHmgjWPxHOw+/EtX5XMlymMFMXjVfPqS4R1WjE3 +	359sfzs94i7PLrXWc62JizdWm5dn/WpI++6qvJPmVflPXvXx/GfNxGPiKTEmdornIYmX +	xS7xkthLqwviYG3HCJ2VhinSbZH6JNVgYJq89S9dP1t4vUZ/DPVRlBnM0lSJ93/CKmQ0 +	nbkOb/qP28f8F+T3iuefKAIvbODImbptU3HvEKFlpW5zrgIXv9F98LZua6N+OPwEWDyr +	Fq1SNZ8gvAEcdod6HugpmNOWls05Uocsn5O66cpiUsxQ20NSUtcl12VLFrOZVWLpdtiZ +	0x1uHKE5QvfEp0plk/qv8RGw/bBS+fmsUtl+ThrWgZf6b8C8/UUKZW5kc3RyZWFtCmVu +	ZG9iagoyOCAwIG9iago3MzcKZW5kb2JqCjggMCBvYmoKWyAvSUNDQmFzZWQgMjcgMCBS +	IF0KZW5kb2JqCjI5IDAgb2JqCjw8IC9MZW5ndGggMzAgMCBSIC9OIDMgL0FsdGVybmF0 +	ZSAvRGV2aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4AdWWZ1hT +	yRrH55yTXigJXUrovXeQXkOXDjZCQocYQkdUVMQVWFFEREARdEFEwVUpshZEEBVEsPcN +	siio62JBVFTuCVxcn+fe/Xa/3DfPzPzynzfvmczMeZ4/AJReFo+XDIsBkMJN5wd6ODPC +	IyIZ+IeAAKQBDegCORY7jecUEOAD/jE+3AGQcPKmvrDWP6b99wlxTkwaGwAoAJ2O5qSx +	U1A+hTbA5vHTAYBRBsNZ6TyUkQKUJfjoAlGuFHLcAh8VcvQCd8/nBAe6oDm3ACBQWCx+ +	HABkAaozMtlxaB0KisCIy0ngomyEsj07nsVBmYeyXkrKGiHXoKwV/UOduB+YxYr+XpPF +	ivvOC/8F/SX6YNeENF4yK2f+y/+yS0nOQPdrPmhoT+Em+/mgowzaJjgsV+9F5iXPn9m8 +	HsMNCVrUudF+/oscy3cPXGReuvMPHBC8qOfGu/gtckya2/c6iSwv4ZnN1+dnBIYsclpm +	kNsi58YHhy0yJ8b1ux6b4M5c1BPSmd+flbTG+/sagCtwAz7ohwFMgBkwAubAHQSAsPSY +	bPQMAXBZw8vhJ8TFpzOc0FsXo8dgctkGegwTI2Nj4fT/TQjft4XFvrs+/x5BMsKr/G8t +	Fb3H1g/Qu1z/txbVB0B7BQBSZ//W1K4BILoDgM5r7Ax+5kI9jHDAAhIQBRJAFigCVaAF +	9NHdtAC2wBHdXS/gD4JBBFgF2CAepAA+yAJ5YCMoBMVgB9gNqkAtOAgOg2PgBOgAZ8AF +	cAkMgGFwGzwEAjAOXoIp8AHMQhCEh6gQHZKFlCB1SBcygawge8gN8oECoQgoCoqDuFAG +	lAdthoqhMqgKqoOaoF+h09AF6Ao0At2HRqFJ6C30GUZgCiwBK8AasCFsBTvB3nAwvBKO +	g1PhXLgA3g5XwvXwUbgdvgAPwLdhAfwSnkYAQkakEGVEH7FCXBB/JBKJRfjIeqQIqUDq +	kRakC+lHbiIC5BXyCYPD0DEMjD7GFuOJCcGwMamY9ZgSTBXmMKYd04u5iRnFTGG+YalY +	eawu1gbLxIZj47BZ2EJsBbYB24btw97GjmM/4HA4KZwmzhLniYvAJeLW4kpw+3CtuG7c +	CG4MN43H42Xxung7vD+ehU/HF+L34o/iz+Nv4MfxHwlkghLBhOBOiCRwCZsIFYQjhHOE +	G4TnhFmiGFGdaEP0J3KIOcRS4iFiF/E6cZw4SxInaZLsSMGkRNJGUiWphdRHekR6RyaT +	VcjW5GXkBHI+uZJ8nHyZPEr+RKFRdCgulBWUDMp2SiOlm3Kf8o5KpWpQHamR1HTqdmoT +	9SL1CfWjCF3EQIQpwhHZIFIt0i5yQ+S1KFFUXdRJdJVormiF6EnR66KvxIhiGmIuYiyx +	9WLVYqfF7opNi9PFjcX9xVPES8SPiF8Rn6DhaRo0NxqHVkA7SLtIG6MjdFW6C51N30w/ +	RO+jj0vgJDQlmBKJEsUSxySGJKYkaZJmkqGS2ZLVkmclBVKIlIYUUypZqlTqhNQdqc/S +	CtJO0jHS26RbpG9Iz8gskXGUiZEpkmmVuS3zWZYh6yabJLtTtkP2sRxGTkdumVyW3H65 +	PrlXSySW2C5hLylacmLJA3lYXkc+UH6t/EH5QflpBUUFDwWewl6FiwqvFKUUHRUTFcsV +	zylOKtGV7JUSlMqVziu9YEgynBjJjEpGL2NKWV7ZUzlDuU55SHlWRVMlRGWTSqvKY1WS +	qpVqrGq5ao/qlJqSmq9anlqz2gN1orqVerz6HvV+9RkNTY0wja0aHRoTmjKaTM1czWbN +	R1pULQetVK16rVvaOG0r7STtfdrDOrCOuU68TrXOdV1Y10I3QXef7ogeVs9aj6tXr3dX +	n6LvpJ+p36w/aiBl4GOwyaDD4LWhmmGk4U7DfsNvRuZGyUaHjB4a04y9jDcZdxm/NdEx +	YZtUm9wypZq6m24w7TR9Y6ZrFmO23+yeOd3c13yreY/5VwtLC75Fi8WkpZpllGWN5V0r +	CasAqxKry9ZYa2frDdZnrD/ZWNik25yw+ctW3zbJ9ojtxFLNpTFLDy0ds1OxY9nV2Qns +	GfZR9gfsBQ7KDiyHeoenjqqOHMcGx+dO2k6JTkedXjsbOfOd25xnXGxc1rl0uyKuHq5F +	rkNuNLcQtyq3J+4q7nHuze5THuYeaz26PbGe3p47Pe8yFZhsZhNzysvSa51XrzfFO8i7 +	yvupj44P36fLF/b18t3l+8hP3Y/r1+EP/Jn+u/wfB2gGpAb8tgy3LGBZ9bJngcaBeYH9 +	QfSg1UFHgj4EOweXBj8M0QrJCOkJFQ1dEdoUOhPmGlYWJgg3DF8XPhAhF5EQ0RmJjwyN +	bIicXu62fPfy8RXmKwpX3FmpuTJ75ZVVcquSV51dLbqatfpkFDYqLOpI1BeWP6ueNR3N +	jK6JnmK7sPewX3IcOeWcyRi7mLKY57F2sWWxE3F2cbviJuMd4iviXyW4JFQlvEn0TKxN +	nEnyT2pMmksOS25NIaREpZzm0rhJ3N41imuy14zwdHmFPEGqTeru1Cm+N78hDUpbmdaZ +	LoEam8EMrYwtGaOZ9pnVmR+zQrNOZotnc7MHc3RytuU8z3XP/WUtZi17bU+ect7GvNF1 +	Tuvq1kPro9f3bFDdULBhPN8j//BG0sakjdc2GW0q2/R+c9jmrgKFgvyCsS0eW5oLRQr5 +	hXe32m6t/QnzU8JPQ9tMt+3d9q2IU3S12Ki4ovhLCbvk6s/GP1f+PLc9dvtQqUXp/h24 +	Hdwdd3Y67DxcJl6WWza2y3dXezmjvKj8/e7Vu69UmFXU7iHtydgjqPSp7NyrtnfH3i9V +	8VW3q52rW2vka7bVzOzj7Lux33F/S61CbXHt5wMJB+7VedS112vUVxzEHcw8+OxQ6KH+ +	X6x+aWqQayhu+NrIbRQcDjzc22TZ1HRE/khpM9yc0Tx5dMXR4WOuxzpb9FvqWqVai4+D +	4xnHX/wa9eudE94nek5anWw5pX6qpo3eVtQOtee0T3XEdwg6IzpHTnud7umy7Wr7zeC3 +	xjPKZ6rPSp4tPUc6V3Bu7nzu+eluXverC3EXxnpW9zy8GH7xVu+y3qE+777Ll9wvXex3 +	6j9/2e7ymSs2V05ftbraMWAx0D5oPth2zfxa25DFUPt1y+udw9bDXSNLR87dcLhx4abr +	zUu3mLcGbvvdHrkTcufe3RV3Bfc49ybuJ99/8yDzwezD/EfYR0WPxR5XPJF/Uv+79u+t +	AgvB2VHX0cGnQU8fjrHHXv6R9seX8YJn1GcVz5WeN02YTJyZdJ8cfrH8xfhL3svZV4V/ +	iv9Z81rr9am/HP8anAqfGn/DfzP3tuSd7LvG92bve6YDpp98SPkwO1P0Ufbj4U9Wn/o/ +	h31+Ppv1Bf+l8qv2165v3t8ezaXMzfFYfNa8F0DQHo6NBeBtIwDUCADowwCQRBb88HwG +	tODhURZ6+Xk//5+84Jnn8y0AONgNQHA+AD7oWI2OGo6oB0Gb0Bailg42Nf3eUEUYabGm +	JvMAUeRQa9I9N/d2DgB8FABfh+bmZivn5r6ivgZ5D8B5vwUfLswWQ/39ATEjH6+gc3kD +	+ULlx/gXSb7pbwplbmRzdHJlYW0KZW5kb2JqCjMwIDAgb2JqCjI2NjkKZW5kb2JqCjE4 +	IDAgb2JqClsgL0lDQ0Jhc2VkIDI5IDAgUiBdCmVuZG9iago0IDAgb2JqCjw8IC9UeXBl +	IC9QYWdlcyAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvQ291bnQgMSAvS2lkcyBbIDMg +	MCBSIF0gPj4KZW5kb2JqCjMxIDAgb2JqCjw8IC9UeXBlIC9DYXRhbG9nIC9PdXRsaW5l +	cyAyIDAgUiAvUGFnZXMgNCAwIFIgL1ZlcnNpb24gLzEuNCA+PgplbmRvYmoKMiAwIG9i +	ago8PCAvTGFzdCAzMiAwIFIgL0ZpcnN0IDMzIDAgUiA+PgplbmRvYmoKMzMgMCBvYmoK +	PDwgL1BhcmVudCAzNCAwIFIgL0NvdW50IDAgL0Rlc3QgWyAzIDAgUiAvWFlaIDAgNzMz +	IDAgXSAvVGl0bGUgKENhbnZhcyAxKQo+PgplbmRvYmoKMzQgMCBvYmoKPDwgPj4KZW5k +	b2JqCjMyIDAgb2JqCjw8IC9QYXJlbnQgMzQgMCBSIC9Db3VudCAwIC9EZXN0IFsgMyAw +	IFIgL1hZWiAwIDczMyAwIF0gL1RpdGxlIChDYW52YXMgMSkKPj4KZW5kb2JqCjM1IDAg +	b2JqCjw8IC9MZW5ndGggMzYgMCBSIC9MZW5ndGgxIDEwMDQ4IC9GaWx0ZXIgL0ZsYXRl +	RGVjb2RlID4+CnN0cmVhbQp4Ab1aeXiTVdY/975rlqZJmr1Jk5AmadrSlaWllYbSQim0 +	ForQIsW2UCgIWrFWUeGriiJVGZFV8FNxoSxiQ+lAgIEPGRScRdFxZdTRsTqOYx9nvg8d +	B0jynfdNqZTH8fEPn3nf3OXc9ZzfPffc5U378ttaIAE6gYGaOU1tC0F+vC8AkI3zlzW1 +	xekkFsPfze9od8VpLg2AWbqwbdGyOC1uBFA6Fi1dMVg/6RKALtja0rQgng9Iw5hWTIjT +	ZBSGqa3L2u+I0/o+DGuW3jx/MD/pLNLpy5ruGOwfPkDadVPTspZ4ee+DGKa23Xxr+yBd +	jOG0tuUtg+VJHfL3OhBMNcDNoIAbQQAKWnwbAIQvlA5gMVfKx2dBpmrDDYnF34BOlOkb +	qn4hh6+4f/XOdy2X/Kr14r8wQXG5vBTygWgAQE0wf0C1fihHroeeIQy1GWGYgq4E3Wh0 +	GRkTLNBJdsKj6J5Gx8Bi8hCsQLcW3ePo2KHYbqQOk4d6WTF4hKwAG6kMqljnTIPVaVGq +	nG+GCd/3pPN9y6dHiRVH7xNi7U0AxQQleZo8BQvASZ4HL7kTKiCNbDsQWOpsxKzd0Iau +	Ex0j+4Ts7k3Jcx4nmeBlCdbxQQpLDjr/kjvS+VlumJJe50l/mMXgpRSkgonOE44nnf/j +	WOQ8jm5vPGtPAEscdO52LHVuSAmTbb3OxxxhgnXWx4PbHFj1oHNZYLNzQa6cP21zmO7t +	dRZi/qygyjmmwO0c7eh3ZvvDIkF6pGOaMz33985UrIjFXNioN6hz2h0bnOMwK8VR7h+H +	7ijZQ7ZDOtne6610HsEointgSqBgc5jcdaAiLdcbJncGx1SkbQ5U+L2BaU5vYJLfj/FZ +	Z4TVwvXCBCFPyBDSBJ/gFpIFg6gXtaJGVItKURSFMHmht8TJHyV7oQRh2XtA5EUuTF7E +	RPYo2Scn7jsksiIVQTSEYx+j8hIccbK3TyvFMHKQl2N8mOw7EE/aF3TiHCLAyhlaKsXR +	Qx8oESlUQog8EubhflNHiaVEP15XOKns33mNcs5lP+PfPxbiCG2eWlsX2uOoD+VJkZij +	/nJxy+XIvw3bb8OsltKMjKkzVhzoaFuysLzFU97oKW9B1xh6qKPVEupsdrn2L2mTMlwh +	xtfYPL9VCptaQm2elrLQEk+Za3+HXO+q7IVSdoenbD8sLJ9Zt39hsKWstyPYUe5pKqs/ +	0Fy6vGFYX2uH+lpe+gN9lUqNLZf6apbrXdVXg5TdLPXVIPXVIPXVHGyW+5KEL19cW3pr +	O2qnq3zxVFcorTY0ZfqcupCrqb4sTHZiYtltwJ0ALXcM0rhOsLHZ4ASIvY/unBRGr4t9 +	zp0GbXRZ7B9MEQ7qYcnRaEkxnIBHYDv0AA+7MJ4G82ArvEqW4NyeC33wDkmBLLS9LIRh +	GvyOxGJvwEJ4Dsu3w0nYBPtBjXWWgRFz1xFv7E6kgxhvhtWxZyAVCuABOAaF2Oo6GIjt +	jh3A3BlwHeyBvVj/t8RD97NJsRdj/SDCdGxzNea8EZsW6wE9ZEIp1GDqajhOvMy5WCtY +	oAi5ewKegh3wEnxF7iV9sdZYR+xs7BNUVQvYoRbflaSPfML0sA/Enoh9GYsiEmmQjr02 +	wgZ4FtvvwfcEmtZyciNpJxvIJhqk99I+9n7OHI0gDgGYjG8FWuUHEYHDcAr+F/5FvqYW +	Rsu0My/HRsf+D1QwFaWUJGmBDnzX4LsOZTpKeJJDJpIaspJsJJvIH2g6vY7W0dvpHfRz +	ppqZy6xg/sDeyvZyD3NbeVX0m9jR2OnY22AGB1wPy2EVSncSzsJ5uEAYbMtOvKSIlJJ5 +	+HaS7fQw2UEO0xpygpyle8ifyKfka3KRclRNjTSDttMNdC89SV9jFjObmMeZPzHfsOM5 +	yu3gPuO9wh+jzdG10ddiRbFPYt+hiRXBjSNTCtVwAzShtG0wCv4LpdiHbw+O2il4GV6V +	30+JHQbgO0QBiJ7YSB6pwreaXEsWksXkSXIE3+MyL99SHAiqoDpqpnZaS5vpMtpJ36ad +	TDKTzlQyc5gefM8w7zAXmYssxyaxRnYyOwUeZpex2/Ddye5ie9nXuUJuPFfNzeI6ubXc +	w8x87g3uHX4Vv47v5b/m/45mcZpws/Awjs6rqLMvoS5//7AkFbnPg5tgPikjzbAZR2MH +	aYIu1K4F5EHEqw3SYg3MKmYyzUFtOA53obZug5WwlpkLO2LvMXvgXdSUpdhkJ3SzpeDg +	tuDo3As5qEWDbzCQHkjz+7ypnhFuF5p8e7LNajGbjIYkvU6boFYpFaLAcyxDCWSWeyY1 +	ukK+xhDr81RUjJRoTxMmNF2R0IhT2RWaNLxMyCXVa8KsYSWDWHLhVSWD8ZLBoZJE6yqG +	4pGZrnKPK/T7Mo8rTOZMr8P4I2WeeldoQI5XyfFH5XgCxt1urOAqt7SWuUKk0VUemtTR +	2lXeWDYykxwOIhzKkZmS4QiCSmo4BBObVqKBhYlSifKQzVNWHrJ6MI55jLe8aUGoZnpd +	eVmy212PaZg0ow77GJm5OIR8wkPqBZ4FD4WD0NwoxZrm1oWYpvoQbZTa0mWEzJ6ykPnO +	zyzfk5dj5Q9fkRmi3klNLV2TQsHGhxBciWyUqKaHkZpa68Jm6f31dSFy/yATEo9LkFOJ +	3fia4G1c4gopPKWe1q4ljQguzKjrtQVtsvENQU1drzVolYmRmYctq4rcKP3hkRNGTpDC +	IrdlVTz8y33x9DdPSKFl1amPMZw6YwgAIiHgmYJ8hlzz5U48yGyB5LUUQNf8AsQJn3qC +	Yi5GfiaGKOoM4w1x3ilNoc7ay2y0lsWZa1xS1quw2uRFqLQeyzd2acfhSGF5rcfV9Q2u +	1o2ega+GpzQNpvBe7TcgZUoDPaQrIdJ0Od4hLZZelLrV4mmVxrdDHlOkPZbyKxKQlqCR +	eA4ZcAGvqXOHXPWYgLvJzKlhUNTU7SdkXX2YxO4PQ5njMO5RmRvmYXampGqLy7B/JEZm +	YkK6G2NZma5J2PMkSVdcXa6uKQu6XJNcrahMrFcOMaOlqz4bEaytQ5xgJvYYrE8eirbU +	14/DdrKldrAKFu+qxxaWDLaAoZyUHcFCOZm4mDK+mrrpdaHOsuRQsKweRwHV90RNXegE +	am59PZbKHeIUOV652DLIcx7ynJuO+fnxVnDv0olN1Hd1SW3W1nncoRNdXcld0nyL02EC +	VycEBxPCIBWRIA+Tzhqsi4HHnSyPgdvjRrbqJUxHoUpf1ijcs/84wmOG+MaaY5HbMTLC +	BT8TwoU/BeFxPwnhoiFOhyFcjDwXSQhf859DePwwhEt+HOHgEN/I5ATkNigjXPozITzx +	pyBc9pMQLh/idBjCk5Dncgnhyf85hCuGITzlxxGuHOIbmZyK3FbKCE/7mRCu+ikIV/8k +	hK8d4nQYwjXI87USwtP/cwjPGIZw7Y8jPHOIb2TyOuR2pozwrJ8J4dk/BeG6n4Rw/RCn +	wxCegzzXSwhfP4RwMDkEV9rhzqvMLvzshnnuFZDjTonTQyktxPA0PM/Ngh5+D2zhC6GG +	vRVmoOvAg3YRhgXoKjDeSU7DWiyzGmnJSXkdWN+MeSp0Rmzy8l2QGk8ox5F2wRzpaP6z +	PBRPA8Mf7BS44Uk/SPF4ayXimq68IleFZ8AE0EAi3mXFHx0GejmaJPuj8KSxGc7h6aGf +	vshYmG0sz57lglwz9xE/ju8VUoV2vOh4GstSPJ8A5h1DDgUoid9Zidm4sUAnasMAZ9FJ +	NMaZD8LAogOMCx/AEawBMCvjCLbCYZiTm69z6/zoStl14Ut/5o5dmBhmqy7i/QeWeB4F +	no/9JOA5clHQuUa3WU/zRFVKIoUUsyjmJtlsCV6N1Wp7x92xFm8mqs9XRaq131YNQEmk +	JJKbM3FF0EdMOq/RxwucwAqMQAWOV2rFPEJM6Cn0qjwiGPBkkpFBMjLSMzLuafDmjR0j +	vaO11OPWMW6X2aQzCDRA6NmWCe2VRbbE9/8RfeoMrSXZ3ZvqtkcfiPTsMfpvrn+odjLR +	kayLW7mkd09G3/jyWLRXlqEHsRpAGaQRqA6mCiksq2JS8BpHIaYoVaKaqtUU+MW0SGHT +	MKIXrAmaMFEdcG+6LFCxJNH5fp2+MBtKSoojxSXFAxhH8ZLcRrdu0JEeNvvSBibj0tvM +	3RdPUid3rC9auieq6cGu8SGwBW8fzUgkwW+D9WVkKkN5omBMxMq8S7gkYmcMqmT1bFLH +	vEX+yLyl+qNaySrZhHL6AGWn0y2UBpRpCQXKgoTJdDbtoIJ3QYKSMnqGUJVaz/Ci0Wy2 +	sSxeeG0PJiidjIqPqAmNJDj1mHIwCayGjjZLRrX2fHFVpN96vrAQf5b+CMpW3lL2OZSY +	USq9uRCvjPYnqMNkTx8lVKnCSC+lzBquKuvOCLvy1BouHubmQMPyW8jyhluS3Ari1nl0 +	o8aMJh5iNJiMOs8W4iA7ybPEdoyNNrwcncMd545d9LHnLkxk5o88e/vFAPvuyDEfjrr0 +	34gL3l/H3ua+4D7DmZGMdxVdwcw1aCBOk1/TM+KrSn6iaByXyCSPExR2arer9LmMLcWS +	q7I6Ut5zL1kYV7kBWeXiwzNQMiCrXR7YEnzEq/ByPpPGkof3yfo8YhMxpuUxZlYb80gS +	Rc+qTM4DHYuedKeEShh/7sEbZ1Q8rUDdLr9Ppx2rd4N+tBY8I0Bn0LsZdvvRx7pPRTdF +	953ct/E4Xpkk/y36j7/1Rz/+JzFquM8u/Dp6NnroXAw+fo9UkvS3iPbCM2TFN3h9URw9 +	HX39fHQ/Nw9lnxH7QD7pJ+IdTjF8GCxIzyFKLeqB3Z9foV2sWKIVCkW9WsEk5wmpCodW +	7SjKoFmBokNFtCgv3avXCpxo948w28OkK+gxO5yC35Gloo7RqmKhuNhuEALpu1Jt45MD +	9spEf4H1mvG/IlvwguMw2QyD0J2XweuPnBqCr2QAtVuHutCAGp81kDVAMNSZC2VY08aM +	NY4AYvWSMYlusKQku8HkMriJewSMpW6wOcxuYnSjJ6FJtMUSmPfcg2CShlRTPs7sa4iG +	JBJe4I1EmuOjfJ4RAi94xpP8PLwq0BmwEHahIZ4Rfp9fCnyjR40Zm0Q0y6tvqN/sbs1b +	1pxbS/rGG9X33flIkVu5i/vns8c6bjN71Sm69ExfQ7pJMfa1uzcdO7Kl6/U5mVN2rjfa +	eU2CPXsRWSpmWkbOrZ2WXvvK9oqKrZEt9hEMc7+aL/UEK5b88sFNzyWRfsnmdcQ+Yr3c +	SdBBCrQFs3YK3fZ37cwIMTGFovE3OzhBp0xxqFQGv2hz2bK0WSQAOqvTtcZ9rEEGVZph +	/YNWcKCkZKBEV6iLo2fRm3iliTf4iF6JnlEw+0iSIsUXt34STEn5OgkKvc5AZQSMntQ4 +	SLzRYDbld/QUPdd45l/fnrtzZl7hTrpw/fpH7jrsm3ySOxn5W9X06ED0fDQaKvJUrV35 +	xfHdHx18Y8u8/bINxNst5ixbDTacY93B7G4r2WrZJe6xMJWibruBYQy8wyYkONACCcnJ +	Zq1fTxg/1dkcSr/Zascrf+GAe/nK7ydbcdVAYaFkA9Ee4oTDiDY+60aBVfSqjUofaJK0 +	KKUuUStYkeKAcRNCWUZlSvBBoh49hYX3EZbwbnnKoapIyhL3M2R9AZPZk4UKgKoS14p8 +	SR0ozsF8gb7zqblHu3zVC5U5Dz7Wdp+1J+XvR9+8QPRv2dnq0Lvz79u17OkdH6y9/e2X +	Sf7neDU3jsNxLYidYwZwXFXggNuDeWM1kzWzNd3s7mTOKxpookMLosMhJCmpw6zispKy +	tAGd3uZU+W3WFOca9/LSK8XHAQaU/MqxtVnsCiUQYlGhbHb0wEp9oEwWfSgg/uRZoJfU +	W1Z63ojmxazL13lGS2LB6FH6/G8f27Fyx847H9xNumpzrtn3TMkLNx+IXvj6I3LDF+++ +	+ttfn/0NHTsqZSp1XBi/aX4dGXnhSzIbbUhF7Bxrw9tCO94se4k6uGKL+Lit28lwGprI +	GYwafaLREFQHDWLARqaqDjKnySvM6eT3xPcV7zjf83xh/sKjOq07radzRc6dmrjN5Egt +	5AXB5HbYBaXDpPIKW+zd9kM4B1ivKdFr56xKtaDT+BMdfs7mT80S/Farz/+We2dc+VH3 +	ZdV/K1KoL0QzUohBdsOQnuDqqR3AVNmYTAIPyzF4FUs4lneikdVrk7QGLcurvSOSU324 +	m3P4SIpDYRZ8oDJqfCRB47G5MYlDT7SgXiVo0Ysb7rj6oLlJz0i/h9zSALc0NKAK4Wt0 +	p+CUwi0FKhDaGl4y4ahExOdH48MLhPa9UzBGr730Nffolkdm5hj2C9fmzlgxYcaZ6JfE +	8mfiVKVV7rt7F0c87OQbr5u+tPKZZ19uGDO5aH1WjV2Lax5PKCmN+m6bdO+BLiJ98MS1 +	vhMXtu/kPUd3cEE9JeNEYqXYuZmfzS3iVvB3CGu4w8yrzDlGyXE8frxSMHQ13Uifowwt +	1CsULIcXpfwyvSBgHl6ZcrxC5HD64I6SZXilwCt5W4KCKgOgsqoTet3Nh4kpbtHR+BQX +	W6u1n1ugpBjX9BLJkhN0a6qyMsSV2pfYNVmWjAZupfaEViwWi3NziATVcjQ8JF+B4gg6 +	T+c+8trn0YVk/+fR3i37uGOX9pLT0ZsjzdTeFb1Jlm8tCnkNysdAIIj7cJQCdx+EBoCx +	stwed3N8ski8VMe3FyW4WUJdX9vXJ20w5TZW437Iy04GH9wfLBJEQcMnmkWzxpzoF/2o +	XhXWWapFKrXHq7Q5PFYlZc1et8PsSOAF4JPtXiZJmYZ96gL4AY/02gLSd8sgzr8sb8AH +	Vn9amCQcuIKPfu35gfORQWZwr1OCJgz1EKFBZZTUUTK9RlyRpGXIfHk1wm2NpCK4pdGN +	kpUFY6t7g6Pqb+mszkwtfqblver0ozdWLXn8kC3QtrC7j83eem3qNSWpk2bVPjFzXWQs +	/eLGmnU7I+vp0WV5U598PXJGWl9QbmYA56sVrfG8YO4h/jRPWd7A+w0dfLvAGdTUYNHi +	KgO8RaW0CTYbqAMKm51kWQJWsCbjUs8Pk+yKzQ/KNaArxOGWBSKSSFeIIkmA+q8hKAVZ +	vXfantb+msxDjpxVwUBlwcjkPtKN/M+b8dTsZyLT6bPNxQsSTKWjb1kceR2ZxZEuir3P +	unENUeNZwAqPBvO3ipu1j5ueZ3eJO7W7TWHxjPgu+5nmrwb1OJF3WAS1Q6+yClarkfoT +	bckKv9FqSw4TBa4kg5bih7ZtmXi886mSFDirddRHBDPGuASMKQ1qHxAteqIJFw5Gg560 +	uZA9acFI1Y8eHCNcLfQ4wynuSeKLxcf350w78vzmzc/ih7tL0X9+GL1E9H/h20nizs3z +	Nl7q3dvPnIt+hUtnJPoiybiEG5SgtF50RK9jvSi6BkZAezBzt9htpmmiy67T8A6jkMhr +	HHbVCA31W2ypStwFuAMjEq2e1B/cBchLhU7WMzwR2U3JwNl8rA+SUTDOhB6xanzAmGWZ +	ZImkvYC08sfHTF77yaB+4scUyYbh9kjnoa90eycdOVruRT+a1TMmeP1dB6OH2retmJFT +	1LfiD292zt1/dMG2u2fvZPavm5JWHP0ryvjM5htGp0yJfCjZKXPsa6rg5uCIzvhlQpby +	hIaESUnQy5oKzQyvUepsOMXwi1sAjBpjIuNkKHPJhKe9S+5Fg7uBSEPhqWzJqMenVrY0 +	sSLFA9pIvzzh840enbRvubyn843G9S5/18G9e33G3IQUg3Oif9Wc9eu5OdG3N0TKC5JU +	hK5TiPcsoi9vkG2EChXvSzYb0MYGs0rJy4TCImilrcwifg37INcNu6iIXyVpOVvJPcCu +	5U6zZzhxStqtaYIoq9qiVbhq45kmHGvrw0XGxYbJfYcYZpkeTzd4VLovmMKjlUUkOJ5l +	COEowzOAplcpSoL30CMEZytZfYD08FZr9XlLVeTjjyNWWVbJvuKpST9oQQQ0r9rq/ioh +	HmRMnb4i6KUBPcOwENDzPK5xwxpHY97DwfftFhZGCguvapkTtBn4QxONyxketRQkHw30 +	BySFZLwcXXoiehueOLcyrRffQIQoGKNTmC9QX6XZ+ZvgTV3GBy3dFkZacwr0Ffo6/SLh +	duZ24WHDVtjCbTVuMW0x74JdJm0FTDVONr9qZMu4Vzi6htsJO0k3t8vMpaZxFqPZRIA3 +	qlWJDlEjTWZTMgIj8W02WnrUvzDhnH7LLaOM8FT1W1CI7+WIbxCrIoV51mwLrkYIViHB +	0QjqjUYwmZbpzWYLR4g0AJY1iNvKU3IgYkgabsnNuQWXpgaSzzNUoKjxPv9oaSEfM3Y8 +	GYtIMIz7tO++5tInOp/wBVKy07V52VpuvCba/jviJGz2ouj66FcvRhf28eJzCbzbIm5M +	ZasRrnslGyw/sRb8Tv1DjwETGfDjF/IcvJkog3KYJH/7roZr5W/vM/B7+myoh7lyZYK3 +	OPE7J16625kwtbRyRmVGRcvSjpb2xfObsEw8VyqM/2/C//kAfnUFaR2AjeieQ4d/RcEv +	ywBvoetHdx4rsegM6FLRjUJXhm4mugXo2tGtjg0+WB6G4gRcV9FYb1h++VV01VX0tVfR +	kgRXtt98FT3/Khr5G1ZexvgK/m68Kl/6pnxl+/J/064oL+04rsy/+Sq67SoasRlWvuMq +	eoVE/z9X+zr7CmVuZHN0cmVhbQplbmRvYmoKMzYgMCBvYmoKNjQ2OQplbmRvYmoKMzcg +	MCBvYmoKPDwgL1R5cGUgL0ZvbnREZXNjcmlwdG9yIC9Bc2NlbnQgNzcwIC9DYXBIZWln +	aHQgNzM3IC9EZXNjZW50IC0yMzAgL0ZsYWdzIDMyCi9Gb250QkJveCBbLTk1MSAtNDgx +	IDE0NDUgMTEyMl0gL0ZvbnROYW1lIC9BS0JKUkorSGVsdmV0aWNhIC9JdGFsaWNBbmds +	ZSAwCi9TdGVtViAwIC9NYXhXaWR0aCAxNTAwIC9YSGVpZ2h0IDYzNyAvRm9udEZpbGUy +	IDM1IDAgUiA+PgplbmRvYmoKMzggMCBvYmoKWyAyNzggMCAwIDAgMCAwIDAgMCAwIDAg +	MCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMAow +	IDcyMiA2NjcgMCAwIDAgMCAwIDAgMCA4MzMgMCA3NzggMCAwIDAgMCAwIDAgMCAwIDAg +	MCAwIDAgMCAwIDAgMCAwIDU1NiA1NTYKNTAwIDU1NiA1NTYgMCAwIDAgMCAwIDUwMCAy +	MjIgODMzIDU1NiA1NTYgNTU2IDAgMCAwIDI3OCAwIDUwMCAwIDAgNTAwIF0KZW5kb2Jq +	CjE3IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UcnVlVHlwZSAvQmFzZUZv +	bnQgL0FLQkpSSitIZWx2ZXRpY2EgL0ZvbnREZXNjcmlwdG9yCjM3IDAgUiAvV2lkdGhz +	IDM4IDAgUiAvRmlyc3RDaGFyIDMyIC9MYXN0Q2hhciAxMjEgL0VuY29kaW5nIC9NYWNS +	b21hbkVuY29kaW5nCj4+CmVuZG9iagozOSAwIG9iago8PCAvTGVuZ3RoIDQwIDAgUiAv +	TGVuZ3RoMSAyMzcyOCAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAGtfAl8 +	lNXV973PM3uSmWf2JcyemSyTZCbJJJAQmCcbIWFJ2CRBYoKAgKIJiLhLrAuKWnCv2r5g +	61qtThLEgAv4aq21Umm1Ctoa2vK61FLQF22tZOb73zsTFtv3W36/bybn7s8597n3nnPP +	OfdONqy/bCXJJYNEJPLyi5cNEP4Jz0b01vKNG3yZvG0tIRr9BQOrLs7kPdcQou5etfbK +	CzL5woWExH+7euWyFZk8OYm4ZjUKMnkaR1yw+uINV2TyoYOIN6ztX56tL3QgX3/xsiuy +	9MnvkfddsuzilZn2S29EXDrQf+mGbL4I8dKB9Suz7WkXIVbT2XnxbULRyk2+JPXkRqIk +	ApFIlHTjTdYq3yEK5Fm9khDhZx8v6jXUf6XRajj6nyzqb2CJn+fsXp66/J/zlD/S/hxt +	tbw9q8BzqifHHyBEsT91eTqm/NGpGlbLPu6hxd6GaQpKooAYQOSpBFIdgF5AH+BtwBjg +	GEBDfAhZ262A7QBWoyReMU2igBhAJAmEvYCxU7mtSG0H7AAcByiJLKZGcvSV3oZWMYVH +	U2QAsB2gwKOnc8d4ydZs3Q7EIjEolHiXKMIEYCvgGEBBfOJJlEvit6QfsAO5wwAFsP8T +	XWLwLelA3MfhW8QnyV6UHQAcB+jS+8R/jMxbUEka6sVvgOgb9PIb0gkYAAwCkoDDAIwD +	wqg4jjf+BojHeas+pLcB9iK/D/EBAGudAzysxThIjpNnAQzPRCvW4jhAC/LfDE+9r3I3 +	T+QZeeKrkbr6ygMNFvErvNs2HhoQRgEJQAdgK+BZgApkTgxrc/lzJ4Zr6yob2CudwNKq +	Sg8iXoAY+ZF58zHuHhQkAB0AVnkAoATeE+jkCVA6gVc4gdEzINwK2A44xkqA4svhmjpO +	5cvhuQsrG+ayIvIux/4leScb78rGP87GN2fjm7LxJdl4dTY+JxtnevklmZ7NT8vG7C0Y +	ncpsXJGNQ9k4kI192djL4y+GF1RtaygWv8Dw9YmfYSY/w+t+hmXUifDMkm3I7wAkAfsA +	BwBask2hIDS9DyH6Jf5dWEwWES/6cZzjzRePc7yfAu+nwPspx/sp8J4u2Yb0DkASsA9w +	QPx0WGvyNcjijVg9N2LSbkRfbsRQ94kPAs+DwPMgJuBBlBCEEsAHiAFkQCdAhZr3UfM+ +	BMRh8R2sn3eQIgglgA8QA8gA5Vk5UXxV6CUrwK+PCD3DK7xRLINhLINhLINh9P2w+C5w +	vctxvQtc7+Lpd4HrXeB6l+M6nRPFJcPiCu+o+J/DTSx6ZcS/wmtoqBCbgL4JK6kJL9SE +	l/CJjRikfQgPAwSsqEbUNgJlI1o04pUbiVJsFSMkjCfrhXNINeKpyLO4TizlcW02niJG +	hqtBJyDGgCWGtRljMkEsRK4QuUKeK0CuALkCIooxhAXAVIi4CnGBGGR5TKJv2Ozk69g3 +	7A9lE+WVlS+JfmERmcqb+EdaWiv7GnLESejnJPS+UMwn7wMEPJ8/XFHJH8sfntGaTUB+ +	NBhFu7CW07IKXxEvaFoQFyM2Z2PvsKfRu5s2CF2YBdKQL+ZitHMxVLkY7VwMTS7mORfD +	kwuy2PoA2wA7AEnAPsABMXdEbzLJo8Ivhwuqtu8R3iDHhDfkRYLPT7crjymF7YpjCmG7 +	eEwUtgvHBGGvaq9a8KoSql5Vv2qrSulVJ9S96n71VrUyISTEDqFDVPg8voCv0Ffqa1VK +	HskvBaRCqVRqVfU2rBEuwiT2Cr8nVPi90I9NyEsGhQ9R5hMOIYwhlAEC6UM4wFODCLfx +	1A6ESZ7ahzDzDKvFdodQ5inW8gDgMEDk5axEEA4Jazk1n3AQVA6i9UEiCgeFJ3ipJLyP +	HjA+YGEMIAM6AQrhfeFB3uYJ4T0yCjgIEIX3hIvAWF7hd8Nxg7dhXPidcA7PvyW8JfwK +	3zfx/SW+b2BADRze5G/1S7JP+CVJA7DDobwPMADYBtgHUGJ03sS77RDeQhhFKAP6AKz9 +	m2QrYC8AuyxaR5FKcFy9CCnZJFxDrhKGQGmTcAXgSsBVgKvBQJuEDYDLABsBl/OSAaTW +	AdYDLuUla5G6GHAJoJ+XrEZqDeBCwEUo6QeNlZxGP2j0g0Y/aPRzGv2g0Q8a/aDRz2n0 +	CwNIrQOsBzAa/VjU/aDRDxr9nEa/sBqpNYALAYxGO2hQhFcArgRcBWDv0A787cDfDvzt +	HH878LcDfzvwt3P87cDfDvztwN/O8bcDfzvwtwN/O8dfx/HXAX8d8NcBfx3HXwf8dcBf +	B/x1HH8d8NcBfx3w13H8dcBfB/x1wF8n9A8p6hrSIFAHAnUgUMcJRDmBKAhEQSAKAlFO +	IAoCURCIgkCUE4iCQBQEoiAQ5QSiIBAFgSgIRPkLRIE/CvxR4I9y/GMc/xjwjwH/GPCP +	cfxjwD8G/GPAP8bxjwH/GPCPAf8Yxz8G/GPAPwb8Yxz/GPCPAf8Y8I9x/JuEVVhITwGe +	wVLbJCwHrACsBFyAidiEDWCT0AdYBjifl5yL1FJAD+A8XrIYqS5AN2AJL1mA1ELAIsA5 +	KOkHnQtBZyWn0w86/aDTDzr9nE4/6PSDTj/o9HM6/cK5SC0F9AAYnX5sp/2g0w86/ZxO +	v7AAqYWARQBGpxd0eoUnyRLQEpFaDlgBWAlg79MLOr2g0ws6vZxOL+j0gk4v6PRyOr2g +	0ws6vaDTy+n0gk6vsLABiioo9XJKHaDUAUrtnFIHKHWAUgcodXBKHaDUAUodoNTBKXWA +	UgcodYBSB6fUAUodoNQBSh2cUgcodeCNOkCng9NJgE4daAgQAMsBKwArAextEqCRAI0E +	aCQ4jQRoJEAjARoJTiMBGgnQSIBGgtNIgEYCNBKgkeA0oqBRwmlEQSMKGlHQiHIaUdCI +	gkYUNKKcRhQ0oqARBY0opxEFjShoREEjymlEQSMKGlHQiHIaY6DxAacxBhpjoDEGGmOc +	xhhojIHGGGiMcRpjoDEGGmOgMcZpjIHGGGiMgcYYpzEGGmOgMQYaY4yGcA19TLiausAl +	34Jb/gmueRi8sQM8sh28sgI8sxic0QoOaQKn1INjYuCLMvBHKfikEPwSAlcEwB1+cIkP +	3OIRVgHnBcC5knzbEESv/4neP4w+7kBft6PPK9D3xehhK3rahB7Xo+cx9K8M/SxFfwvR +	7xB6F0Av/eitT1ggOz33/WOF91bAesA6QAWgHDBKXXI1NKNvATsArYB6QAxQCAgBAgAf +	wAMgNhtsM5NRIzfYhWkC9ACSR1/i4VYefp+Hl/NwNg9beVgn2zvzXurM29KZ19+Z19uZ +	192ZN6Mzr64z7wWaItcByyey+7q8e6/Lu/m6vKXX5bVfl9d4XV7DdXm11+XVXJcXRdpH +	/0rr0fDHPLyPh3eykHzLw3/w8DAPz+NhPQ99PPTQ+uE8oh2lXw37p+G9Twz7OxAdHfaf +	j+jJYX/c+yJ9jPhhMXrpI8P+81D6k2H/fESrhv3ViC4Y9lcgahz2NyFq2OmPef/pH1VQ +	2eD9o3+997f+dm/SX+t9mJUNe7fzqhzven/Eu9Jf4l2RKV6ciZpYtMs7zf+UtyxTUpop +	WWTWmrXbRuluuUq97RfqbX3qbTH1toh6W4l6W1i9rUC9zave5lZbNCaNpNFrcjU6jUaj +	0ig0goZoLKPpw3Ips64tKolFKugOlCh4WoIKTsHWLCQC1QiknfTtEaZBTZg2JExOmsVZ +	wqwFjXRWct9yMut8X/LrBcFRqpu3JKkMNtKkaRaZtbAxcqljVtK5YFZywbwlXaPCtORg +	8ywfPknnfJ7d19ydDPPkKCVIV2bTMtJ12fQg0q3ZNNp3JydHZo2q0/OTUyKzktrOc7uG +	KP1+N3JJ4RZgWdg1StOs6Kb8pKmpazeh1HvTHfksTt90R3c3sW1MOBKm6cbaGc3/Jujj +	hX3NkdMfx+kko915pZzrfUbtbVF7q9TeoJrVzlqAwm3PqLe1qLdhIjKFDnfyvlkLupJp +	N14sm5iFeVzgW9q1W0gI01qadwvTWdTdtdu5Q0i0zGflzh14yVPtwJwJtANvIuLtSIi1 +	I6HvtAsI01m7QhZl2gV4u8BZ7YZa/S3NQ34EmTatvE3r2W12nN1mB2+zI9tG5P3nKCbw +	mKcQP2/jN0/hfT+zTSBD63/bpvDftjk97N9JrWz8TsG/z9LdZD4dG5q6sWVlsKUv2LIS +	0Je8beNqR3LwfJ9vN5lKx1iVLymG+85fvprFy1aO0rHgyubk1GCzb2g+f/Ts+uRGVj0/ +	2DxENrYs7BraKK9sHp4vz28JLmvuHulYlVh7FrlbJ8gNJVb9K7HkKoYswWh18Oe+Q2st +	q+5gtNYyWmsZrQ65g9NqWcO4r7NrSEMau5uWZuIRIUeHVd+X7+9utEkD0zkLTPU7rsvf +	A9P/CZIT6U7mBhuTeQDGHWUNZQ2sCozPqvQoNmSrHNdN9efvoU9kqyQUG4ONBCzwL5+W +	5v//3w38c+n/xef/piXZkEW0wdGypvnMv0iEvdGGyKX4i1wGXJmGyF26YQMB8IINl0YI +	xljO7SvsK+1rFfs8fX7h0ku7WeFLsKyY1cPsK4oyuoFEIjQ7SHgw+wHeTIoA86VoApIb +	LkU7FuHDUO2B2+M6IOmml264DC0uQwdY/G8+ExWZmIUAIJ5IXBaBt/QTwF0kH7FHPJ94 +	CEmPZeFPqet4vTU1DvH+PsT8/iwgwucCsp8Woox97yM/RdgN2Ew205upk5feTZ5EfBU8 +	vfewlyebmDEIv/DTpBjlh0iEnEPux/cb5EzkddTvT39BGuFSW8jbF6HsfuRfo9cKbsGL +	rWa/IkTeoWnF59QkPko20k30v8Ve4L8fGFLC3nQbmU9uIj/UlKafIWEik4vJNeRO8iNq +	oIH0JelDcCTZQLsl/Wj6DbIMtUNklP5M7FRcm96OJxeQS8hdZCctV/Qpfjn+59QN6f70 +	b+GJv5U8RnOoX0AHlCXpxWQSmUISZCn5FajiS32K4vF06g/pIeCPkAZg2gSqd5L/JAfI +	F7SZvqMIK0mKpr3pX6U/IGq4+paSe6mIr0QDdAZ9SrCLb8NLqyQO0oqnl5KVZBXpJ+vJ +	4/g+jV4eo3FaTZuFZqFHuEW4V3hVvFtxreI6zMwm8gIlVEFLqExn0QX0Kfpb+luM1pXi +	tSm4xIkP79tEWshs0oP33YqZeoP3+hAZpxQ9uID202vpQ3QH3U//KLwmLlTMVHyeviB9 +	I15WwKzYiJ8UkmnAsBDz+wwZIbvx/B9B0Ym+V9EE3u97wmxhoxgXO8VzxWvEbeKj4ruK +	xYpnUvHU39I3pR9Ov5h+L/1h+ijwGUmAlJFZGOmFpItcjZm7k/wYWF8h75MvaZA20kvo +	9+g90Mh+Rp+hL9L3aErIE54Sa8S7xV0KqpAV9ypeTxlTP0mNpo6lW9Ld6ZN4v/PJDeQW +	cjf5CXkMK24nsI3RVjqbzqNLaB8w3kxvpY/TV+lfBYWwVHhODIvrxKvEq8V7xa8UIcVV +	it8pN6Z6Unendqdj6UvR41vSf0FfDcRJJkOlWUjOI2uwMgbIRnIF+nwNxvx76PlN/HsH +	3uBnoPk8eQHjcpj8lXxFtTSP6qmbxvCdQqfjrbroBno7fYA+Qv9EP6F/Fyh6EhFqhLnC +	Ksznw8JrwjvCH8WF4tPii+I74jsKm2KOYhFW4eOKZ5REaVRN07z17aGTz47/YPzBlJAq +	TvWk1en89KR0a/rZ9KvpQ+m/gXN9pBTrci546hqyDatmFDP1K6zAA+C0/yKfYA0psd6M +	tICG6Ry6lF6Pkb4ZY/1D+hN8n8TKeZaO4vsivvvoz+kBjP779DD9L/otxeIVwkIUPV4q +	XCBcLTwhvCS8KqTEHDFfDGI868WVGNNrxc3iY3iH34pfiH9X6BVmRVgxVbFScZfiKcUr +	ikOKb5WtyjnKy1VG1e2qrXwVMv4540NbhDjwC7Qb/A9XIHlOeF0oA0dwPvv/HN5K/07e +	oI3kv+g4Vvmt+F5PPgUfLRaa6MdYST+mk+ld9GFBhOV0K91HdpCHxafpe8IN5HZwfzn5 +	HCEVVtNyeoswCdLwTmGE/BkrYz/45QuhFen9mGkH2S/upwPkH/RLegc5hnfpE6xkFf0t +	mUJvoc1krVBMgmQD3Y8Vho9SVlDluZC3q5jsVdwr/EW4lx6Dbbadv/3tdBnZQYux3vbT +	c8mzwpiiRvESVukMcKkLrecLKnol1uYPBQV5XHgda3cIfDYXXHE/uHcH+KQBvS4iG0gT +	nQd99+9US4z0Vqz288CZt6I/T5Gn6DjOnfaTGek9HD4VYljp95IfoHu7SQH5afr75GV6 +	Pvh4J9WRH5I/ktniCYUVu8ZxhVvZkhZS55OD6XnkTUgsSfyIzCQf0tsgN2aSD6iNPJRe +	m45jNe5Pd6OfN5LVZJGyQemBNF4G6/UV9Q7VR6p6VYWKKq9SrlDOV85SNiknKyuUxUq/ +	0qk0KHXw8v5BcUDxsuIRxffAu+UKqyJX/Ajyc0h8QLxN7BfniAmxHGvSLSqEb4S/CZ/B +	gXtQ2Cc8KWyiSfTyw/Qb6QfSnelp6clpcyqV+ir1auqZ1EOpe1PfTw2mBlJ946+d/MPJ +	d04OnXyUfj1+EPLrFfpm6lvsAZell6Rnp78Gv1nSd6enpd6nW/GOITIO/noLcvVuzMsj +	GNsuSDhZmEklkiJfkaMYofdQv5s8gTV2Oekj56jgH8F8h8GZN2RX9UrI2seREzFXJuwA +	CYz4bMzJUlhWIi3ETvsaeTr9sLgIOIY4yzwuvE19qZ+QQkiZS7A/zSJ/ptPJX/DdSXaO +	PwhqT6geB9XdqifJV6of4cTvXuRuE1qURkUUa35c6Kd3pM9NnQuZdjXZrfgvHPUQeXbX +	4nMWLVwwf15nx9z2tsT0afVT62qnTK6OV1VWxKLlZaWRkuKiwnCoIBjw+7we96R8l9Nh +	t1ktZpNRMujzcnN0Wo1apVSIAiWlLcEZfb5kuC+pCAdnzixj+eAyFCw7o6Av6UPRjLPb +	JH3suWWoOquljJYXfKelnGkpn2pJJV89qS8r9bUEfcn9zUHfKF0yrwvpO5qD3b7kUZ6e +	w9OKMM/kIeP34wlfi2N1sy9J+3wtyRkbV29p6WsuK6VDObqmYNNKXVkpGdLlIJmDVNIe +	HBii9umUJwR7S92QQDR5eMekK9jcknQG8SjQiKGWZSuSnfO6Wprz/f7ustIkbVoePD9J +	mCId4U1IEyeTVDUl1ZyMb00Sr0Nu8w2V7tty+6hEzu+L5K4Irli2tCspLgOOlqQxArrN +	SftVRxyns0AOlX3zmbX54pYWxxofa7xly2Zfcse8rjOezfczDN3dwIFnhdCMvi0zQPp2 +	TBV1RNE51n32KpmXyphCob4LfUltsDG4esuFfZgQ15YkmX+lf9jlknenDxNXi2/Lwq6g +	P5nID3Yva540ZCFb5l854pR9zrNrykqHJGNmNIf0hmwiN+/MxEqMdKaOp3hzlpo1/9Rw +	UtbHYFtSxjpa7kNPuoJ4kSksWDmFbFk+BaOOTzfFU8kVmIY1SW1T3xapjpVjKGlSGZKC +	vi1fEUx78Ohfzy5Zli1RhSRwMirZ4ji1wJJ02UQ6CSuhpIStC3UTJhJ9nM7z1WWlG0eF +	/cEByYcIliTp7MJj3XVRjLnfz2b1tlGZnI9McnBeVybvI+fnDxM5CntL6GM1+yZqrItY +	zeBEzanH+4JYvjuxa+LSRVITPvVnkGzmltV1SWr731SvzNTPWhCcBSeMr2VLX3apzlp4 +	Vi5TzwYU44a6bCppbuoS8wW2tJES8kVei5W4dMmpJsh05SYVIfyp+EpeMarWYCnyEuqb +	kZT6ZmbCbp3fn2WU/9NDo+nj7CkenX4s+xrJuki2o5luJ6eelT+re7lbxFkLIWiEWQuX +	bNmiO6suOTeSzA0ltSGsk2ReKKnnaXNo2KZfFPEl9X0hSBbDqZAlqbSo613Y174uX3Jh +	CSRLveN49Hh9shPsnswJYb2yEOiAy8BD4AUBayhpDzmoVH+yvnZa1HH4OGumCzHyaIZQ +	E0pKoaSRp22hYaeR9cDIaZtOhUkkyb/0gHVAqv8/9wGE8GcPJZ0hB5HqNSdJti9cPiRp +	ZsY64T9YBlmKN8GfMrSoK6niwwueQsPMeOHt0H90GH8ZtAvBt8mOCP7Apd3XMw7kHwzR +	mR9gEMNUaptaVhpEivCULxzEH0rYovT1gQ1DW6bkB/3do+k0eITlMRFCXwij7uvb0odk +	MLmghNWGffkQB33hbjwmou0M7EpbtswI+mZs6duybDQ9eH7QJwW37BZtom3LQAv2kwyT +	jqb33JafnHF7N1bnaloHUSSQxqEgvWXekExvWbCkazf8oL5bFnYNQ6Fv6mvsZiwgNC3s +	yi5Bzh/8JbvLwJiK/WQVgMWPKWBtKfYLbqS/RPphxLJif/oDwG+Q7gasB0wHNGbBhXgQ +	cD3afIS4CnAO0scQrwXcD7gH4AGARvokYlj4kAlMKhDo0yryMmIfWZItgZrJa0Q4d3Dn +	Ch9ctPh/+Kj/TVucQH/no/1OPpPVIcpBn/IQ62FrYSTxMUL3MRMLgTYJndwOe9hJXCQf +	uUmsmlSSSrocltSYgJMJ0axsVrWoK9V/0fxR+xfdP3Jezp2dO6R/yXCXFDB2m+ZZ7Jan +	rWttlzseda3BmzKNehVeU4QFPlX2qNTHoaEoFcdFolMpj4ui4NKqFccpcWpmXe2IzJVO +	1M8Zr58rfV0/RxqvJ4n68XoGFbEqo98Y8hv9qxTkpE/cd1JWkm9xT2wfG+nH0gS2/lYy +	jw7KDVPbZrUJhfmr8q9o+cHMJ72jM1XqfLvTlD+pxdo24Pkd/aD4CP2K6gzG3MlV97m2 +	NwsbXBsahOYGl11hrCNVtGqPsJWUUv3zJbLNES/5omCP8H1Sl94naw2WhFTnqxPqRulj +	z/uKcmWnO547Sr99TslaKvdQPbuvsQtp4Qsyd1TokHOMMh7yGqNGwYiHZBNJtNP2wqKi +	9vZZfp+PkHnTR4Xznnd/sdWw3SAY9gibcRXhDtmkMWi92g7tgHa7dkyr2qSl2tH0vhGb +	M45zj41y3iyPYV50ntA7b/s8Yd4eupFYhPPkPNLqaxVavxicvG2yMJmhKhFWyUbcX9ka +	3REVB6LJqBCL0uiLwu0wh+6mvcQRkb5ev66n/ujR9SfWHR3vWTc+HunJZI9KEFsTXxJF +	4brIEelE5ETkaCTKM0ciJ472GE322p71plqjqbYiRnpozzpqs9fUVFXarFaLKhgIF4aD +	AZXVYrfZ8Qd1VaXOlFdXhwtZfXV1nLVGna2qcnJNdbwwXIi/MIqh/HJEwv7WZsmtL6+t +	LSuuVc5cu26Nz3fBLU/POn/ksdrystrOzkioora2PFRps3iublg/uyoQuPjBn8ye/cit +	rFrxNkZSm2hvbE/UxGvnVYQ9Hqs7OKP7rsFf+J5pTyTanzEWFExvv749UbTA6otMLZxe +	5ffZvL5Fiy7fOOqY0pZItGGFwUdzm/CZ4giu5BXKkvIgrIMnhQrNk1EsPqd2D1XRJRhN +	LN6eOSfGj5DE0YoY9YsqPgL0j1TToPTnO/1KxZHx4yVebzEwCu4Urn8pg/AgNMg5Vymo +	2mg36SwnDWyinYaEYVR4SjYSiI0YHGp9MMpVxOlduJt+NEFoPHIC/JEApR4an1wzOTPs +	FrVKsFpMfKyDATaUhWHBXRdZ0jA9XFrvWLt8+VpHfWloUlFjb3Ay/fLZkRt/vKG6vsRd +	NJR6c/uO1JtDhZ6SekfwqqFLYbRR8mVqn/Ay72W1nGc3qk0W3UmDPNG/3CgMpl6yF4LM +	BXW/ZegONghf98w53Tf0R83HoRp9NFXHBT63VZVsOdhtwsv/c8923vjwhpr6Ek/hEK3Z +	sZ3WDBW50bPA1UPrYYhR+nDqeXEmteDyXESWcDZ2kNCXiXAXeZGoqXqPsI3o6MtD32dL +	vOfIUekoiY5LkCLUT/myi08WaqgpdchV6AqpqWX83YqA1eBibyyn/6S4RfEhrPt3ZP8V +	k6k3AA5eS9bQFYUrii+cfDW93Lqh8IrJu5273DnRALyATNrTaXKeubBa1P2nKOQXRrS4 +	CNQtG1RRfULfoe/V9+s36VX6F3CyqyJq4YaRkKvW/jJyFcTBwxikhsEU98JzNUo/HZly +	yWO835F6dBzjOedE/dEevELiKPsekbC6ZiUNOO4KlMYtrvJoWVRQWUNVYVepo4RY4vYS +	4ozmlxBbpbkEXmzu8C65/nraEwGHnsGcmcXBGFQFbuX8d3oRqTFMWDngT1MBGFR8wlzm +	KmMs6HTmqq3Fd7cvfXDj+3vXd5THfQX24ukl0/qu/+GuuzY+di/V3NP9kOIWl2t6O/jL +	bk+U2MtqOndec9M9r3tN1T7z9JKS2Iyimln1VHzgth3Ueh/jBvhniWIcPpVK8a3niENy +	CA7GB0Wl8crR9Ke76qorHHXVSO6SH8Laq2ALcHHlG5UHK0WlPcdptec7FS671VliDzkV +	pphcVBcnLIjJgRBSCGKyy4cUAgOhDkNMcvgcsuOAQ72VbI3dVnFb5XayPfZAxQOVz5Bn +	Yk9XPF25l+yNHXYcd0grKy6svAkN7q54sPInFT+tfK/iUKXuXfvvHR86P6gYq1QSjVaX +	k5unNxikPcKDwkPCD+U8TxtxuvInuT1en88/UWr1tMlaWSfrZYOisKi4JFJaVh6N7c08 +	AzHQlt5H9Nhmcg3CaVN+4mGGcsLcl7BPnCY04RXw+yZKtZ42Z6yitYJW4Bh5pKgyjnjf +	SLwuEycWIBY+lCc5nBaHw2knlTMraaUPzSpltKmU0aCSNah02NHA7qyIVdppTF5QvR3L +	k7AYXGKvqNQYHF7Mk0Njj9virrhTqMBjX8o5tFRTVFio1Wo0mMPDw33VPFqQiToz0YxM +	VM+jkcamOGsjT55SG1c4LI4VjnscOx1HHCccaoujwLHQcSMveM3xrkNT4IijgLVgWbUD +	cxllr2fJTbBY1ppyEtFoIipER4VzZItv0H/ALxC/5Pf5Y36FXy6u9oPFZClOfHDEy3iM +	yniEstZOySA3tsQNcklpfKuBeg1R7MnOql+CG5loXxeJQDeJRHrqpa8jPevqx3tQ4DTV +	RrGHZs+BiCOR2TXHv65PnDhxxFgb7THV1ppq10fwR3mydnM5a71u87WvbS7nR9gsmT3L +	HlIxRXY3ceBV8nLysW4REB7gmW4IsHU9hPWEfWDc8ZYjwRkJDOCgrPc3JGIyAsICFO2T +	9WV2FCEgLMgUFVlQhAA8YuFFIwZnpsqdy6oQEBZUOnJMSCFAN0yZFnmQhoQpNTEWZPpx +	dtiNLq5nRSTSQ41MFYAm4MdWj+3cGOaRnVI7L1cbz6quMopBGm5n23Lqv9sT0xtqJifa +	/0jt1PTn9sSU6obpKP+SbcmzPh8WY+M/Yhs8AzGgnFJSXkvH7xcupFPKS6YoT35UV5ap +	E5aPH2cS5jeQMCchYQL07t1wR34kNxoxKFCAcbHX6EG6xlwTWC2tMt8i3Wx+3vQG/UXg +	M6rLoVqjIuAw1tJaqcZYZ9LIWppheIMkGRuks7g3FxyM+/XEBrACZMbNdlVWEvgDJMuh +	I5AEuZicnTrZJlv5LFllu+o01790Jlo5V0sz3j6L2WzKotgFFFQyGkeFi2QdwTZIaMBs +	MrFsGwkgGwiYzNQI9V5ryo9r4y6XRsBtHyLFpAFpUEpKh6XjktonUelV82BltcGcMG8y +	bzXvNR8zp83qqJmaX/UR3rPKagUsgzgZJElYK6NC20hw2xRHxAmGiLiOYuUD2LLHwj9y +	BHogBWDBs1WeXeGIXHx5Tyxukn5vOAccyNdzD9uWIpQyZRArRMkWBDYfnEph/zlVogoG +	cZuqOMqWBjW3J2L21tS5P08tnWmPocAKj2l7jEaE12rLsQymlE/2SSeN4jHJO4Vny2uZ +	zdUN++0H4uOkmEzGfl1doqSxcqqssdUEaxIliUiidFrZxfpr9Fqlz+q7X/Oq6pe+d1VH +	VF/XwLY65XudkLEWT5vRHCuZHCD05mJaXDI5nmvSsfGKenxxSdepE2TdoE7Q+XtLaUcp +	LS0ttshllXHLSpPk96iLdYNxGvcrcvIwoIt3+nsDNMAeZsIrAN1gc2xUWCSb1LJdn/Cq +	feqYWlQ7pyR2ZeRQZM440wogh2ApQSlYl0gchSiQDZJctiQhyQYPCzhzdh+NQD6tW390 +	3fqMtMBPNUbQiHV1BO14LOVnYyuPhyf4uptNo7FW+uvElEaYjr8O7O2vzqp0UA3s1XGo +	EExdYLM1oYZO8L3I65gBUFVZM1n0ztkz+9FfU/WnPVd19J97Z42nuNZSUDv7P+S97wTZ +	1B6/evW1S6bkVy5uf6EtVlz87IXX/8FSUV5XkDe13BW2S1bno1tTSxjH037HtMIit8lf +	V4ndaD1m9jbMbIQck+ccMr5jeb/gUOFnpo8tHxd8Vvit5dugTmPRBoUa00rjKtNK6wVF +	3+aqcnKpqc00p7Db9AfLoYLPLZ8VqF3OvFyiVJmd+bbcPEkr5dP8UerfGSBXFetHhX/u +	lPzFahhg7bJWUNn8gRzVXA+bNclZPeA57BE6PQc8gsdVZuaTNxCmJOwLx8IDYUXYWfrr +	a7KbyBzMXWp9T2QOMxPwHT8iQaODJYWx5txj56YUm1BJI+fZEjoWaFmQD2rDWBJMuDIp +	S9ZBnztlaxWGoepza4urcrwCnOTnbESYDvdYqLAU8rLEbXWUz7vuzmefeHVwXuycYMm0 +	ni2pr4/dtJMWfL7oLnFVMNF2Y/t0h6k/P/bT711xm0uaM72kedq5y2/65EPqhc0qkOnp +	McVnSiOUlhI6IEd1ORq90iqe0FMpx2v1+qQSX07UGvX5Sj4Mf1jyefjzkpPGcd/JAoNP +	1uXES/hKR8IHizrBcw4k8mVzIF8uygtqyP8oCG2UnDr6mOBEh6dNd7PGZvaQgN+s1uiK +	/Hk5sCa9WnCWbCD9dIAK+3CeKlBXaYjNjMsrdUi9Uj8E4Zh0TEpLmr0Qg85I21Y+Q+t6 +	2Ax9DTsY3IXNnM0S/hgfRCIZCfacTob4srHZ4GKMybGmK2VHsFBvCppCXlKoR1BgDHhp +	2FDkhYXAlPDrrydtC6+UpeKSnNySnKKgojjXG6Q5OjgesVVO1Pr8FqvPGggq/RbUWm2n +	atmkw8Am607tqUHOhaQ6zibXXp0VoGbGjjC5GcMppgXbZHDVjAfmf5L6mBb9rvPBWZzP +	gsPXDw7vuOvOHyuN377I+KmyuITm/OIADVdUpGvLymtPPrgpmbxm3Z13YrYbMdsXgrvc +	pIA+JOtGTaOW5/N/ka/IYzpb2yRPfIWw1vIL1fuqg5aDzk9Un1o+df638JXqv00nLf/w +	fhM01KhaVYJpjWWN40LXhd4LgvcI273bgk97Hwn+05njVivFHHOBh2ownCMldXEWy7nO +	QHxQc0AjHNeggtqeM3lkdzXnNoMbAtZDZc+gR9jqoZ5R6pCriWwKQi3xIzGp2kuoAQd9 +	bxP8BBD2XK4BP7mE5gdxh8CS8PttaoVfyvGMCn3D5PIcKE5MgeLxrDCLQT9YED+cQ3Nc +	4YLLoRr2yRazHKz2mgfMglnOM8TNzlDbWr5cIuDk8SNswWB+5pzgRhp0RCh+UBGZGnSU +	R2Dn5zyy059g7zBiKcrE6DTPB008HoY6xqY50v03rmxK9ZC8TBzQHjy/G86ld2UtNBVv +	KYLgaPrdYcSsORMEWBz+jBOFq1xqlSIYmFgbsLStEA9YF2rFmpPP+35y2/qX5nqKp3iK +	Um9u/Tp1iCYOXPubqplR35+jP1iz+gcxel7n+RWWutKiSaEmavvVQWroqmq/ePaKjV2L +	F3exFZFaqHhF/CkJkQp6h1yrLxQqBHWuPddvqjA1mka9o/43vG/4vyn8piJHyveGfPnR +	0H25J7zf+v9Z+G3kRNlXFTmFjPeZXSJrkSgchDhAblDOQ8IhB4vz5ag7mFkKbkoFUaFU +	qaF3TTC939NmsYdNFpsr6raVGor9QTV+ikBVUb87x6APX06d4P5hKEZMCAR127XPavdq +	39YqBrT7tIe1olcbhXdN1LqqOk19JsH0eikTDBavr8PX6+v3DfiUe33U56xsWzUhEsY/ +	7sEcr8tIBTgmIbwhFOqPJGChQ2xn1Z4J+eBi8oGpT2fLh5JyT6AkUOol5R4EEX+xl5Z5 +	o9+VD7GK/EkV+dGgIjYpHITmd0oCMOmhLygqDBUHlUUFqAuRbB27s5YVDtV89qEuGS3Y +	o7MyAaKg+kxhcaaMEGceZztv8NwXN37KEn86b3XzHe2/gbhw/WbuHYnHL7vscQZixzQm +	J8Ztax/ewATEBXNXlJZS+/5fU3tZqm3dY4+tW//oo9BC4TcmiqexN0whf5WdY1qqUtlU +	hSqRmb6CZDSZLVY7zM49+NnUhKmcsYWjsVjFd/ReGxRYdratgzE5MfPMEM5oYwE/M3lP +	oyFTmP+QnZdn2+70tE0hcEs9KRvoV26/Ql1cVGQ0Sjqng60KSdOhpQNYGFgPWlcd8bNC +	fSw2WEG9MJ2dtR2ZyYehxzzRjMOR4i5p5hdNHD1xNMOsGccnNXLPis1qhGuLWTx86CfX +	2I1waTLG+5fy7NzQu+Ubmi587OK5jtj09r+0JWLOOQXRpc1rujvsFYn2z9oTFY65XGYr +	jalZ4dDMhzamNhm8tcy4meKVKL20wxep7koNnlEmXs6mCnw6iLmYhbkQ4cd/FRetIXpy +	8qYLjO2sSHRqqZwn6wXmvchMzdneiDwmCW3C6csFZ06C4tQVhIlSnactitvTHbi6s0dY +	DOkLE1Zfm8CFOzJJhVlUvoBSCxzci4fpFQo23Aar1WeJWfososXpXvJIRkNiw32Ceexg +	UyTWQX5ibTMRyHZRa7D61PaXNSmrjNmBFJInqPQ1W8LjrSz8+lG29ymNhw6lrh5vYCPC +	gH7Jx4bi1hFRzMbYOMh1cs4pJ45knHgbttBOKyJnlpJTFy8mSuFxcaBxVt64jBJzKUjM +	ueCDwdUn7ZAUktM58YLcjcCONtj6wZtN6Mr/8kL7/83bZLZr9g7ZWabpj3B/iYpPkEZx +	mWwbM9DHVE+5nyp9wb3b80LpfvevSjUmNdt0XME4ZTPvtwbjpn5vf/km76byrd6t5du9 +	28vHvGPlugrN2JQx/EqTtdbq41NYazMSJhkBiVfXwEFTN3Vq/UvCdsa8GZ8VVgjFguf3 +	UAz6Bv2ZddwSNsISNgEktpoMyok7LF7Pv2lryLQjeEbONSknrr+Ul+3JoOWuNdLYILMb +	MvVTs6XDnjbDbjR4UJ7kKSuppupGl19X4ldcrlM3qqrj8VDIqsMSx4p7zm6TK6uZ5ibn +	h6ttsqcu/rZtzHbMlrYp+mwDtkHbNpvCZBulx2Wjx+eNeQUvm0Uvm08vnt9VaAoDQZgh +	KAhXhzmC8Fj4WDgdVvRB1R8Mb4O6z54Js2fCwDRMysrx6HC9NJU95ghXb59KDVN3TB2b +	enjq8anKt3lC5JWlS8oSU+VpifhUuaExPnWwqRWpmbOQmr0Aqc6FCM7tiU91NiWy6geT +	/PgwFxROeUf6p9Kpu4UUaYJ53s012K+xMzAWYhpIWLbloU+YXpubx3IeCIaZOzXMbE2O +	qrse+ktP/dfsARt7gA+XHU/YWEMba2hjb2hjb8gfiXRzEjinY8uai8cEY2Ejt2lqW5rR +	NTZB21uSRbiMz59hbumeCY+AmkmmfL4+h5nDi7u6oFL7HG5N3qRQbihf63ETt0ejdubY +	3dStcblFR57LTblqzQgCG3Nxcz0pwYYZ9lJgEEEFCwKywTS9ggVscQ8jzvYbz/NHWAfQ +	jjEIi8EwH41Y8nl+GDFrDS0LJy1WLubtLIScz7qvgsaMDTzhzsrkmcw/vRXA+ZXxcNCS +	ynnVZV0l1RfXrm89T54+vf3VQDDgDlXzZDBYMKNChvjazfxfzOMl3l5XESotLY1M6/xe +	qpq5tYTN0QKTsyW1PJMpD5U1ZdIZAcdK2U5cBQnH/B3V1Crns234frc4ph3zCnwvzkp8 +	HL2duYmGuWe6rLw8+i978cQ1M63mX6oyF9N8Xq/nTFzQPvnttWh5Vk6CT6GuPykb6Vf5 +	2I6rVdBJJcmgs9sYa2q0UPHZOadsgkGd2ZgNOPl0TfYQxnuyvrx8MEq9OMJ01py9ObPd +	mflgz9yf2QbNJ3lijWH3y6wt5kXFdpKdyzNmMeuj/Pcb9qnpu3/K5o5dFy5kU8PnqCB6 +	XutF8yd265ijIzNpLbHYuiV3pW5gc8Ln5YYmb9Hk7tQNBk9dZvM20C/5VAnkHFhYV2Om +	DNin/1M+73lhVPWe7pD+A9PvrO85fuf8IP/gpE/0fxe+UeW97nw9XzAdNR+xfuz8PF/x +	geO9SZ8Jn6g+1n2u/8ykXuG4cNKjyse1j+X8NO8Jg3qNcIFqpe4i/YWmFTaVxZ+rdsHl +	JDFVF9cAJBwtHsbB3Qv4XwEeYhcWPe/VxDQDGlGzGyVuqDtHGXAnBdt+8QGnwBMdMGgT +	JhbAc/nRCGIn4mHEGSaBjUotzPiAfIQ/qKpSYVNnRjRrk159Q2r8jtvT5OZb0rfdTsUb +	97cu+4/b9rx465YX6XMb/3DD9R9defXRW277/NrlCwaGL+t7/HGYPcdgb9yL8QmTOD0o +	R8e9JwLjxeNlJ2In4ipVvi4s7PK/7j9Y/H7Zp8Ufl6m8+VI4mu8LK0xlzK6IMbvCgQRM +	yEi+XFlQil/YZc9sJOPZGg+8t3Ku7n84fzl94XJi33fhpMVBbi6I+N2ur5xXuNUOVaW/ +	ADcx9YVslIMxn+zr9InEJ+GndYd9iiSsCldN/lUul9NJwl/iYic3UJxGIjFF4e2sQ0K9 +	nTkkqpdnXEY4oGeeiPqPmTdCYqYmzJA5XD2CZ+IEnEfSX7n8yzon4LCX4i4m5s50Tniq +	4kUlnmBxOBDGkaqXVgURFHkjXvghKydMECZDs16KWEUIvqvKoKIiFA1i4M+yQkyl5fmT +	ykLl+ZGgsnQS6iesFObGyEpUnGmO6IwJjP4+2Q4br0xGrpwFZcz2K2cB509sHsyt0RP6 +	n/SgKi5zCXcLB8I0zgxZddbDQVdfzXS91L3tCbktmPEhznxw3m9p0bN3Pd/xoGBpvqP3 +	gSXTnr3+e8+sSyU5L8J4Ef+DpWZUxFJ/Hv3VjZeU0+9Hbuq+tKNt/kMPQm7iPzzx1VZM +	r3geh0Q/UFETV5xCrmqd1C4Jz0rPGqEXKPT8OC9HzpXzlBPHff5AQ+6ZznuSA42GnQtk +	dGnF6Vu7E2uI6Zn/6l92etpy8zQmo68sGjfKDa0I/KG4Ue/iO1Sskm+VI54wj3dZnHFa +	rM8ZpW7Zr2eGjsrl1BGNDwzdqekDU6u2wY/iiuCw2ogzgsWyMUD4gVinv88/4Ff5nSWj +	lA5lXZR8uUmQqR+vx4/BoYTPOcpsXExWJuTuYOTOEqzmjDufu7hMBkkQJUEfVBpEY5BI +	RoFi+Uy4ueDaYILEIjFtwsgCrikbmUqRFSHrMttsxpAKZMwn/3dOiia0/vrr721YcZ48 +	LRJe6I88NUgLuAWA9ZBov4qfJIm3D/ZMb6+Ml06bvXZt6len5HHGEMB83w/pWw/p0iq8 +	IldrTapqp8lWfUHs5th9sUfKd5a/Wv6e9l3dexUfaz+pOJH7ddSoo2qlWquuKYrVRFuL +	Z0Q1BWyND+QY2FmZIaEjBqoJTibTi2cQVZQEC4qqozOirZsr7q/4hqTpP4I6kzJHzNVG +	c2P2HEuu2+F1umKmuptybov9NufDqP7j2j/WfRMVfTh0LbCLVeW5OqKIqAv8tlxnTCj3 +	Ye5jLMAB0uGR8sq4LhuzA6XhumpkWcRra2oztYhZ7UjnApbnMa9v78jUI+ZPt7Kn9wzz +	6LCc01QdA3FFIWmpy9Jgsax1Fcbr6sVcnW5UWCu3xMotsVi56J+MH95uajnWIhpaOloE +	bwttkYOheItcU93y3rRp9Sq7nF8Wt18hYb0d9ovEn/AL/vdcukK/JUfGKUjfcMPcCBOa +	RnY+tQ0nVPtwRqWSXG3qF4RF0GUK4IXL8bjneqt8VbEqsQovImv9wXiVc2ZHxnMbweWb +	7I0JyMcTR7nrFqdTRyLQSaELQEdOHN2sL49cK70GPsCChuPGztb1mZ/1OOtjXjf8rePn +	HWyxuoisy0s0sWAGC1pY0MwCdm4zgpitARb7srGf9w8FMaZCs2GXtVCcowEctcJxio3T +	yVx4XEZmcBgT7BkmMxkO2Y4ECBoTrSwAVSOjaszKzDN7jHQ3bl7hwBVuwAnhmLlfkzlL +	wzWszHkNO06Dq/iMu1Y4MmAXc1gpV2xxEYQf5YjNlZfUXz7DW+Lrf6tzzfplt33UfX/C +	EDDFoMuEKvXRG8+5Y26ouvqxvy9Y0HPdW6031Jv9+pIpkm9yaIrwQ6+30IgTH8kwaVLo +	rnmXtF/k9eTpE+0t7YniyqLiUpujyOUyudrbLrqkbUX+JD2qKpsc5eVMZ70HvLhH8Wv8 +	purpYacGR/XDsjeEH8OFQyG3SvuV0m/MGXBSp9NSVlxMB3IP5wps5cparHhXeagg4/AL +	uz1WYmGehU74FgYsScs+y2HLcYtOQiErGLQoLc6yPbgAUJ25TYU9tj6zy86V/hbpgfcW +	6yMK6QffLjNoxrHxch+/ZDILChGqBXUTwax0k4xnn7L3ZRfYTvtew9UTjomqzLBbrVkR +	JsTzS9b+6KYqd9FUX0VqbPnevVxOtXMtkW9nwt7Uykarv8lVHylyRzsevYK+yiqhbkLn +	ZCmMlAcj9YB4OymmGjmgC+jNOCNCoNO79Gt0a3xf+5TF+in668Jj9KDhU4OKjRIs9Ynf +	iNCJs2i4xdtkNz29L52t3Afy2BG1bJHNskk2ynbZIU+S3bIh98wtK+ODg/vH7yl2qtQ6 +	TNuTsk77lcefm6MJBOBZ68NJzAB+6HaYioMYdVfE/4KghaswX4BabzQOmqjXRE3OkrPV +	+o+5rsPmhR/DwOGWqOf7UGbn2amFuJ04RcYZcoRO2Fvc18mW9qkpMPLzkMkTeYWLKemv +	du2YufKeiDejNiQapm9dk90xxhuYZh4tKlo0q2Ye5UM+/h8N0ypk+mM+/Bh/fBS/w/gH +	6NO7DBIxCyZ+Xm7J0ccPEAqXSfaeAHOtnT2mOM/PXAnwB9iVgMzosWmAscPnB9sleSlT +	PuFjmbgCwBwtWc0B7k30gu3mOUbJgp94GqVAJm+Cl9WEPpiJKd/lgvNURTAjO00mbF1I +	7JI7cfgvwBBQyzqfcfNxDP1mEmBN5F6c/LNyydeJn7fCW9d56uB/x0hwEwQtO/hf5xrv +	cTlg4PccdTl5kpn98IezXd5Uu1lTHlFCyCJ28MR3Tv+zmimzwyBcn8M/3vBwOSrrkcCd +	HNjdsr0sW4SE5IBqQFlghOR8fsL+OCUDYZlz9wYZRBWbhRE8xGMctLD4lAlPe0Lm7LWC +	LDOyiwZZVYKXQPoJq/8w316JawVOxm+B1JPvpZ50syQ1wz3rWPANfV3v5TYc/OENwl62 +	ImrLaz24WYbfxRLFb7AqwuKf5Iu2mbZZBa1gEFUhp+AVbaH7zT+wHBIOmt63vhf6i/Cp +	6RPrxyHpQXqfcJ/5AcsDofvCKtM+0z7rGDlgOmA9Rg6bDlvxnyxNX1pzyGBdL+4BYUcl +	g7hhQAYnVZNBV7VFdlWbATDFBkdae+OmbMwulYzgCeR5zPPOTF6+AQl2QDZoGsySUnuJ +	ZJKsvaTT1GndTli/tSVCKFQr1ITahBmhpcYFtu+Zb7e8TfFPy0yvmd+yvG79eWhf+Bua +	Nlrg4he0IVXYST2CMWQLT6VV4XbaHD6HXkb1B+iY+YBljAnMgBVdxQuEcUTHtNjnJ1Xb +	PM66OOb0TyOIQ4ifRyywQtTLuQ6aMdNwzwWL/tFTBwbsXp0/ECwoCO0VHjntiWR3b5j1 +	Zsq6bZkbmldzr2Hm4Bh7nzWLCvxTEMKJ8FpZZ7VZrAASDo8Kh2St1YKsRRQEXmkyWsA5 +	ZkJhqx2SixhvmcwFoSJL2CraiCgUmk1UZDforWLYQswSjgjNgtYySi+UJY/H7dbptCps +	X/h5rc62R/iAGIUPZL8MxhrgrHWYHCdqZowzTtuGSzb4MUBR4a934+ehYDa2DUGDOTJx +	sSzDYhK7aBNdVw8dhesq3M9mx12NM27dMP4D+2Xv4CB3BhM+R2StO84XBWK2SIYRw3Lj +	N3Im+Iqb+zaH3p4wO/KMCfxTmjdhtHkS5hoEyH3EL5eZA7hXxp2CmEhwnj6BifzoeZMj +	ITgQTCADauhfZH326lq2mGQcdlasXpDhqxcx7xj2Mr56EfM8esDziHkerM3ziHkefeJ5 +	xMgPjqBXyPOY56Fv8Txi3t7M8/tGEPM8+sqfR8zywzm1LDuU8139kPe8m8I0Wk/Zj7CZ +	l6Mqu+0HzeYq84SA4WUqtQh58v1Xnp5Sm2jf254oyO+Yu2nXYOdsB+4tvdKemFzzxMv0 +	qtTNwl6xNsLESLnPkXqBzkmN0JbsiU5xrWK8gelGEXhBnoBsKRMF+cZGw1pyqeFmg8LA +	7hXhcl9l/Bz3Rs/Nhps897qVhkFWeB8CwyC7AnsfggHD3YYfG3aTnYbXDSqFx+LZrL9P +	/wuPMkrL9cXSfe57PI94drl/RV53f+zRmXDJ02eoMDQY5hvWGp4lPzMcI8cM2qCh2rCJ +	bDLcZfg9URkwPvLlOdWlUthdr59hWGxYIi32XETW6Fd5riJX6Z8kTxo+J38xfEOM+ZLZ +	gwtd+imGGYZmz0HyOw/+N6nWp/Pl+HJ9eVFT1By1RK3aqDGBWU6YE5aEtcPcYdF0GDtM +	HdZeY6+p19xr6bVqDAY9hqKsTGK0c3MpOxRmt2/1hu/ssbn4IYKDW+VuOCUz12WN2GNx +	M0oIZbycpWWnlCBmhP+LavQc25Jx34FTEpkxyg45JEOD7iwLH3d2Cc4t5FxJnDjb8Lgn +	SGJbL/O44TqlanVYKisbFX4vw5VtgWDAjWGWc7Pbe1AgaLYd0YQhpDToTTikh476JP6X +	2A9kyS3hF8MX4Uxv8w4DNaT0TMXKGXBTyb3JfdgtukeF+3dtgt4A7epF+hB+J5+g70OA +	sFNL3EAYd57ocRztwS84uN8os0/bT2/UEBQZo8hem72IqpY09Rp2YT7j99tN3OlPR+xe +	dk/h3WzMPeRuxuzYsaWAoVaP9JAhyzDd4PYIboJCiuzm46PH06Qd7GwYZAEbVrbfkyPw +	ABh4gCJmFqHqo11AaZDPYL7MfSfGfBS3Ksj6ECydU5ynAuNlnLdZpqPZQ9iJIzV6IvWj +	ynii/QVs481fHm/EZv5KW6KmOlUyhzFiqoFt72+p4XedUoQfmmBDTxXT9zMbO/jv5PkZ +	XoTrCGqfwHQ/QlJB/Iac8eV3P24UsF876Qj7pZUBzrMYqSXN+G8Brfg1fjt+pT4bu28n +	mYef4SzAr7/PIYvx2/Vu/D5sKccH1Y2jVLGz9PlNXZ1z50ea+i9bv2bl+rkrL+9cUNbY +	v3bFnIX/C1uK8MIKZW5kc3RyZWFtCmVuZG9iago0MCAwIG9iagoxNjAzMQplbmRvYmoK +	NDEgMCBvYmoKPDwgL1R5cGUgL0ZvbnREZXNjcmlwdG9yIC9Bc2NlbnQgODMzIC9DYXBI +	ZWlnaHQgNjI1IC9EZXNjZW50IC0zMDAgL0ZsYWdzIDMyCi9Gb250QkJveCBbLTE5MiAt +	NzEwIDcwMiAxMjIyXSAvRm9udE5hbWUgL1JDWFBOUitDb3VyaWVyTmV3UFMtQm9sZE1U +	IC9JdGFsaWNBbmdsZQowIC9TdGVtViAwIC9NYXhXaWR0aCA2MDAgL1hIZWlnaHQgNTQ5 +	IC9Gb250RmlsZTIgMzkgMCBSID4+CmVuZG9iago0MiAwIG9iagpbIDYwMCAwIDAgMCA2 +	MDAgMCAwIDYwMCA2MDAgNjAwIDAgMCA2MDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAg +	MCAwIDAgMCAwCjAgMCAwIDAgMCA2MDAgMCAwIDAgMCAwIDAgMCAwIDAgNjAwIDAgMCAw +	IDAgMCAwIDAgMCA2MDAgMCAwIDAgMCAwIDAgMCAwIDAKMCA2MDAgMCA2MDAgNjAwIDYw +	MCAwIDYwMCA2MDAgNjAwIDAgMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDAgNjAwIDYwMCA2 +	MDAgNjAwCjYwMCA2MDAgMCA2MDAgXQplbmRvYmoKMTQgMCBvYmoKPDwgL1R5cGUgL0Zv +	bnQgL1N1YnR5cGUgL1RydWVUeXBlIC9CYXNlRm9udCAvUkNYUE5SK0NvdXJpZXJOZXdQ +	Uy1Cb2xkTVQgL0ZvbnREZXNjcmlwdG9yCjQxIDAgUiAvV2lkdGhzIDQyIDAgUiAvRmly +	c3RDaGFyIDMyIC9MYXN0Q2hhciAxMjEgL0VuY29kaW5nIC9NYWNSb21hbkVuY29kaW5n +	Cj4+CmVuZG9iago0MyAwIG9iago8PCAvTGVuZ3RoIDQ0IDAgUiAvTGVuZ3RoMSA4ODQ4 +	IC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ab1aC3hU1bVe+zznlck8knkl +	kzOTmbwn72RCIJBDmAmBEAgJgQQTzRCCgQIGxAgiFAEJBG3ttQpSWxSppajtJCAOUr18 +	lPqo8tU3FfFRRXxgpNoICszMXedMiISv14/7Xb+ek3XWXvu5zr/WXnufPVmx/JZOiIN1 +	QEP93ED3ApAvpxuALO5YEuiOycZk5Ec6elY4YjKbCUBvWtB945KYrAgCqNw3Ll413D5B +	AaB5vKszMD9WDpeQe7swIyaTEuTuriUrVsZkwxvIaxbf1DFcbvwA5fQlgZXD48NJlB1L +	A0s6kUtJqb/M7ptuXiGL4NiPvKZ7eedwfdKM+v0NCOYaYBEoYTEogAId3q0A/KcqNzBY +	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 +	2VUNvqqcms7FPZ0rFnYEcqtuWjxfwuDyVY8J/A8C/H8CwG8qgLuQHkR6HOlPSC8jnUT6 +	AukSNtQgJSN5kCqQpiG1RocvrAMjaYIzd7SM/2Mxqhz/32CULLvqFe3lN7pC7riqPr7I +	qPYydlfUv/Gq8oVXyYuvkpdeJd90ldx9lbz8Kvnmq2T5fzn+BwhxZOwKZW5kc3RyZWFt +	CmVuZG9iago0NCAwIG9iago1NjE5CmVuZG9iago0NSAwIG9iago8PCAvVHlwZSAvRm9u +	dERlc2NyaXB0b3IgL0FzY2VudCA3NzAgL0NhcEhlaWdodCA3MjAgL0Rlc2NlbnQgLTIz +	MCAvRmxhZ3MgMzIKL0ZvbnRCQm94IFstMTAxOCAtNDgxIDE0MzYgMTE1OV0gL0ZvbnRO +	YW1lIC9CVkJSREIrSGVsdmV0aWNhLUJvbGQgL0l0YWxpY0FuZ2xlCjAgL1N0ZW1WIDAg +	L01heFdpZHRoIDE1MDAgL1hIZWlnaHQgNjQ0IC9Gb250RmlsZTIgNDMgMCBSID4+CmVu +	ZG9iago0NiAwIG9iagpbIDI3OCAwIDAgMCAwIDAgMCAwIDMzMyAzMzMgMCAwIDAgMCAw +	IDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwCjAgMCA3MjIgMCAwIDAg +	MCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCA5NDQgMCAwIDAgMCAwIDAgMCAw +	IDAgMCAwIDU1Ngo2MTEgNTU2IDAgNjExIDAgMjc4IDAgMCAyNzggMCA2MTEgNjExIDYx +	MSAwIDM4OSA1NTYgMzMzIF0KZW5kb2JqCjExIDAgb2JqCjw8IC9UeXBlIC9Gb250IC9T +	dWJ0eXBlIC9UcnVlVHlwZSAvQmFzZUZvbnQgL0JWQlJEQitIZWx2ZXRpY2EtQm9sZCAv +	Rm9udERlc2NyaXB0b3IKNDUgMCBSIC9XaWR0aHMgNDYgMCBSIC9GaXJzdENoYXIgMzIg +	L0xhc3RDaGFyIDExNiAvRW5jb2RpbmcgL01hY1JvbWFuRW5jb2RpbmcKPj4KZW5kb2Jq +	CjQ3IDAgb2JqCihNYWMgT1MgWCAxMC42LjggUXVhcnR6IFBERkNvbnRleHQpCmVuZG9i +	ago0OCAwIG9iagooRDoyMDExMTAwNjA0MTkyOVowMCcwMCcpCmVuZG9iagoxIDAgb2Jq +	Cjw8IC9Qcm9kdWNlciA0NyAwIFIgL0NyZWF0aW9uRGF0ZSA0OCAwIFIgL01vZERhdGUg +	NDggMCBSID4+CmVuZG9iagp4cmVmCjAgNDkKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAw +	MDUwMzMwIDAwMDAwIG4gCjAwMDAwMTk1NzggMDAwMDAgbiAKMDAwMDAwMzMzMyAwMDAw +	MCBuIAowMDAwMDE5NDE1IDAwMDAwIG4gCjAwMDAwMDAwMjIgMDAwMDAgbiAKMDAwMDAw +	MzMxMyAwMDAwMCBuIAowMDAwMDAzNDM3IDAwMDAwIG4gCjAwMDAwMTY1NDkgMDAwMDAg +	biAKMDAwMDAxNDc4OCAwMDAwMCBuIAowMDAwMDE1NjUyIDAwMDAwIG4gCjAwMDAwNTAw +	NTYgMDAwMDAgbiAKMDAwMDAwMzY0OCAwMDAwMCBuIAowMDAwMDA0MjQ3IDAwMDAwIG4g +	CjAwMDAwNDM2ODYgMDAwMDAgbiAKMDAwMDAwNDI2NyAwMDAwMCBuIAowMDAwMDA0ODY2 +	IDAwMDAwIG4gCjAwMDAwMjY4NzggMDAwMDAgbiAKMDAwMDAxOTM3OCAwMDAwMCBuIAow +	MDAwMDA4NDYyIDAwMDAwIG4gCjAwMDAwMTIwMzEgMDAwMDAgbiAKMDAwMDAwNDg4NiAw +	MDAwMCBuIAowMDAwMDA4NDQxIDAwMDAwIG4gCjAwMDAwMTIwNTIgMDAwMDAgbiAKMDAw +	MDAxNDc2NyAwMDAwMCBuIAowMDAwMDE0ODI0IDAwMDAwIG4gCjAwMDAwMTU2MzIgMDAw +	MDAgbiAKMDAwMDAxNTY4OSAwMDAwMCBuIAowMDAwMDE2NTI5IDAwMDAwIG4gCjAwMDAw +	MTY1ODUgMDAwMDAgbiAKMDAwMDAxOTM1NyAwMDAwMCBuIAowMDAwMDE5NDk4IDAwMDAw +	IG4gCjAwMDAwMTk3NDEgMDAwMDAgbiAKMDAwMDAxOTYyNiAwMDAwMCBuIAowMDAwMDE5 +	NzE5IDAwMDAwIG4gCjAwMDAwMTk4MzQgMDAwMDAgbiAKMDAwMDAyNjM5NCAwMDAwMCBu +	IAowMDAwMDI2NDE1IDAwMDAwIG4gCjAwMDAwMjY2NDAgMDAwMDAgbiAKMDAwMDAyNzA1 +	MyAwMDAwMCBuIAowMDAwMDQzMTc1IDAwMDAwIG4gCjAwMDAwNDMxOTcgMDAwMDAgbiAK +	MDAwMDA0MzQzMCAwMDAwMCBuIAowMDAwMDQzODcxIDAwMDAwIG4gCjAwMDAwNDk1ODAg +	MDAwMDAgbiAKMDAwMDA0OTYwMSAwMDAwMCBuIAowMDAwMDQ5ODMyIDAwMDAwIG4gCjAw +	MDAwNTAyMzYgMDAwMDAgbiAKMDAwMDA1MDI4OCAwMDAwMCBuIAp0cmFpbGVyCjw8IC9T +	aXplIDQ5IC9Sb290IDMxIDAgUiAvSW5mbyAxIDAgUiAvSUQgWyA8M2M5NzBjNTI4YTY0 +	NDFlNTJkMmUxNzY2M2MwMzFlM2Y+CjwzYzk3MGM1MjhhNjQ0MWU1MmQyZTE3NjYzYzAz +	MWUzZj4gXSA+PgpzdGFydHhyZWYKNTA0MDUKJSVFT0YKMSAwIG9iago8PC9BdXRob3Ig +	KE1pXDIzNWtvIEhldmVyeSkvQ3JlYXRpb25EYXRlIChEOjIwMTExMDA2MDM0NTAwWikv +	Q3JlYXRvciAoT21uaUdyYWZmbGUgUHJvZmVzc2lvbmFsIDUuMy40KS9Nb2REYXRlIChE +	OjIwMTExMDA2MDQxNjAwWikvUHJvZHVjZXIgNDcgMCBSID4+CmVuZG9iagp4cmVmCjEg +	MQowMDAwMDUxNTQzIDAwMDAwIG4gCnRyYWlsZXIKPDwvSUQgWzwzYzk3MGM1MjhhNjQ0 +	MWU1MmQyZTE3NjYzYzAzMWUzZj4gPDNjOTcwYzUyOGE2NDQxZTUyZDJlMTc2NjNjMDMx +	ZTNmPl0gL0luZm8gMSAwIFIgL1ByZXYgNTA0MDUgL1Jvb3QgMzEgMCBSIC9TaXplIDQ5 +	Pj4Kc3RhcnR4cmVmCjUxNzA3CiUlRU9GCg== +	</data> +	<key>QuickLookThumbnail</key> +	<data> +	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 +	</data> +	<key>ReadOnly</key> +	<string>NO</string> +	<key>RowAlign</key> +	<integer>1</integer> +	<key>RowSpacing</key> +	<real>36</real> +	<key>SheetTitle</key> +	<string>Canvas 1</string> +	<key>SmartAlignmentGuidesActive</key> +	<string>YES</string> +	<key>SmartDistanceGuidesActive</key> +	<string>YES</string> +	<key>UniqueID</key> +	<integer>1</integer> +	<key>UseEntirePage</key> +	<false/> +	<key>VPages</key> +	<integer>1</integer> +	<key>WindowInfo</key> +	<dict> +		<key>CurrentSheet</key> +		<integer>0</integer> +		<key>ExpandedCanvases</key> +		<array> +			<dict> +				<key>name</key> +				<string>Canvas 1</string> +			</dict> +		</array> +		<key>Frame</key> +		<string>{{218, 122}, {831, 885}}</string> +		<key>ListView</key> +		<true/> +		<key>OutlineWidth</key> +		<integer>142</integer> +		<key>RightSidebar</key> +		<false/> +		<key>ShowRuler</key> +		<true/> +		<key>Sidebar</key> +		<true/> +		<key>SidebarWidth</key> +		<integer>120</integer> +		<key>VisibleRegion</key> +		<string>{{-53, 0}, {682, 731}}</string> +		<key>Zoom</key> +		<real>1</real> +		<key>ZoomValues</key> +		<array> +			<array> +				<string>Canvas 1</string> +				<real>1</real> +				<real>1</real> +			</array> +		</array> +	</dict> +	<key>saveQuickLookFiles</key> +	<string>YES</string> +</dict> +</plist> diff --git a/regression/filter_repeater.html b/regression/filter_repeater.html index 4160fc6a..0ff6111a 100644 --- a/regression/filter_repeater.html +++ b/regression/filter_repeater.html @@ -14,7 +14,7 @@    </ol>    <p>Why doesn't the data goes back to the original?</p>    <hr> -  Input: <input type="text" name="filterName" id="filterInputField"/> +  Input: <input type="text" ng:model="filterName" id="filterInputField"/>    <br/>    <table ng:eval="filtered_data = data.$filter(filterName)" style="border: 1px solid black">    <tr> diff --git a/regression/issue-169.html b/regression/issue-169.html index e18c4f2e..80902dc2 100644 --- a/regression/issue-169.html +++ b/regression/issue-169.html @@ -3,8 +3,8 @@    <script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>    <body>      <span ng:init='x = {d:3}; x1 = {bar:[x,5]}; x1.bar[0].d = 4'> -        <input name="x1.bar[0].d" type="text"></input> -        <input name="x.d" type="text"></input> +        <input ng:model="x1.bar[0].d" type="text"></input> +        <input ng:model="x.d" type="text"></input>      <span> {{x1}} -- {{x1.bar[0].d}}</span>    </body>  </html>
\ No newline at end of file diff --git a/regression/issue-352.html b/regression/issue-352.html index 3f061e1b..c93d4aa4 100644 --- a/regression/issue-352.html +++ b/regression/issue-352.html @@ -2,12 +2,12 @@  <html xmlns:ng="http://angularjs.org">    <script type="text/javascript" src="../build/angular.js" ng:autobind></script>    <body ng:init="scope = { itemId: 12345 }"> -	<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:click="value = 3">link 3</a> (link, reload!)<br /> -	<a id="link-4" href="" name="xx" ng:click="value = 4">anchor</a> (link, don't reload)<br /> -	<a id="link-5" name="xxx" ng:click="value = 5">anchor</a> (no link)<br /> +	<a id="link-4" href="" ng:model="xx" ng:click="value = 4">anchor</a> (link, don't reload)<br /> +	<a id="link-5" ng:model="xxx" ng:click="value = 5">anchor</a> (no link)<br />  	<a id="link-6" ng:href="#/{{value}}">link</a> (link, change hash)    </body>  </html> diff --git a/regression/issue-353.html b/regression/issue-353.html index 8410adf4..e3569197 100644 --- a/regression/issue-353.html +++ b/regression/issue-353.html @@ -11,7 +11,7 @@      }      Cntl.$inject = ['$route'];    </script> -  <body ng:controller="Cntl"> +  <body ng:controller="Ctrl">  	<a href="#/item1">test</a>  	<a href="#/item2">test</a>    </body> diff --git a/regression/sanitizer.html b/regression/sanitizer.html index 775a6009..b44ae5ed 100644 --- a/regression/sanitizer.html +++ b/regression/sanitizer.html @@ -2,7 +2,7 @@  <html xmlns:ng="http://angularjs.org">    <script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>    <body> -   <textarea name="html" rows="10" cols="100"></textarea> +   <textarea ng:model="html" rows="10" cols="100"></textarea>     <div>{{html|html}}</div>    </body>  </html>
\ No newline at end of file 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(){ diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 9a1a20c7..0332c01b 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -112,7 +112,6 @@ describe('angular', function(){      });    }); -    describe('size', function() {      it('should return the number of items in an array', function() {        expect(size([])).toBe(0); @@ -170,6 +169,12 @@ describe('angular', function(){      });    }); +  describe('sortedKeys', function(){ +    it('should collect keys from object', function(){ +      expect(sortedKeys({c:0, b:0, a:0})).toEqual(['a', 'b', 'c']); +    }); +  }); +    describe('encodeUriSegment', function() {      it('should correctly encode uri segment and not encode chars defined as pchar set in rfc3986', @@ -322,9 +327,7 @@ describe('angular', function(){        }        }; -      expect(angularJsConfig(doc)).toEqual({base_url: '', -        ie_compat: 'angular-ie-compat.js', -        ie_compat_id: 'ng-ie-compat'}); +      expect(angularJsConfig(doc)).toEqual({base_url: ''});      }); @@ -335,16 +338,12 @@ describe('angular', function(){          return [{nodeName: 'SCRIPT',            src: 'angularjs/angular.js',            attributes: [{name: 'ng:autobind', value:'elementIdToCompile'}, -                       {name: 'ng:css', value: 'css/my_custom_angular.css'}, -                       {name: 'ng:ie-compat', value: 'myjs/angular-ie-compat.js'}, -                       {name: 'ng:ie-compat-id', value: 'ngcompat'}] }]; +                       {name: 'ng:css', value: 'css/my_custom_angular.css'}] }];        }};        expect(angularJsConfig(doc)).toEqual({base_url: 'angularjs/',          autobind: 'elementIdToCompile', -        css: 'css/my_custom_angular.css', -        ie_compat: 'myjs/angular-ie-compat.js', -        ie_compat_id: 'ngcompat'}); +        css: 'css/my_custom_angular.css'});      }); @@ -357,9 +356,7 @@ describe('angular', function(){        }};        expect(angularJsConfig(doc)).toEqual({autobind: true, -                                            base_url: 'angularjs/', -                                            ie_compat_id: 'ng-ie-compat', -                                            ie_compat: 'angularjs/angular-ie-compat.js'}); +                                            base_url: 'angularjs/'});      }); @@ -371,9 +368,7 @@ describe('angular', function(){        }};        expect(angularJsConfig(doc)).toEqual({base_url: 'angularjs/', -        autobind: true, -        ie_compat: 'angularjs/angular-ie-compat.js', -        ie_compat_id: 'ng-ie-compat'}); +        autobind: true});      }); @@ -385,9 +380,7 @@ describe('angular', function(){        }};        expect(angularJsConfig(doc)).toEqual({base_url: 'angularjs/', -        autobind: 'foo', -        ie_compat: 'angularjs/angular-ie-compat.js', -        ie_compat_id: 'ng-ie-compat'}); +        autobind: 'foo'});      }); @@ -398,9 +391,7 @@ describe('angular', function(){            src: 'js/angular-0.9.0.js'}];        }}; -      expect(angularJsConfig(doc)).toEqual({base_url: 'js/', -        ie_compat: 'js/angular-ie-compat-0.9.0.js', -        ie_compat_id: 'ng-ie-compat'}); +      expect(angularJsConfig(doc)).toEqual({base_url: 'js/'});      }); @@ -411,9 +402,7 @@ describe('angular', function(){            src: 'js/angular-0.9.0-cba23f00.min.js'}];        }}; -      expect(angularJsConfig(doc)).toEqual({base_url: 'js/', -        ie_compat: 'js/angular-ie-compat-0.9.0-cba23f00.js', -        ie_compat_id: 'ng-ie-compat'}); +      expect(angularJsConfig(doc)).toEqual({base_url: 'js/'});      });    }); diff --git a/test/ApiSpecs.js b/test/ApiSpecs.js index 9683a7b7..bd77d734 100644 --- a/test/ApiSpecs.js +++ b/test/ApiSpecs.js @@ -15,6 +15,13 @@ describe('api', function() {        expect(map.remove(key)).toBe(value2);        expect(map.get(key)).toBe(undefined);      }); + +    it('should init from an array', function(){ +      var map = new HashMap(['a','b']); +      expect(map.get('a')).toBe(0); +      expect(map.get('b')).toBe(1); +      expect(map.get('c')).toBe(undefined); +    });    }); diff --git a/test/BinderSpec.js b/test/BinderSpec.js index 224c449f..fa7fde60 100644 --- a/test/BinderSpec.js +++ b/test/BinderSpec.js @@ -28,56 +28,12 @@ describe('Binder', function(){      }    }); - -  it('text-field should default to value attribute', function(){ -    var scope = this.compile('<input type="text" name="model.price" value="abc">'); -    scope.$apply(); -    assertEquals('abc', scope.model.price); -  }); - -  it('ChangingTextareaUpdatesModel', function(){ -    var scope = this.compile('<textarea name="model.note">abc</textarea>'); -    scope.$apply(); -    assertEquals(scope.model.note, 'abc'); -  }); - -  it('ChangingRadioUpdatesModel', function(){ -    var scope = this.compile('<div><input type="radio" name="model.price" value="A" checked>' + -          '<input type="radio" name="model.price" value="B"></div>'); -    scope.$apply(); -    assertEquals(scope.model.price, 'A'); -  }); - -  it('ChangingCheckboxUpdatesModel', function(){ -    var scope = this.compile('<input type="checkbox" name="model.price" value="true" checked ng:format="boolean"/>'); -    assertEquals(true, scope.model.price); -  }); -    it('BindUpdate', function(){      var scope = this.compile('<div ng:init="a=123"/>');      scope.$digest();      assertEquals(123, scope.a);    }); -  it('ChangingSelectNonSelectedUpdatesModel', function(){ -    var scope = this.compile('<select name="model.price"><option value="A">A</option><option value="B">B</option></select>'); -    assertEquals('A', scope.model.price); -  }); - -  it('ChangingMultiselectUpdatesModel', function(){ -    var scope = this.compile('<select name="Invoice.options" multiple="multiple">' + -            '<option value="A" selected>Gift wrap</option>' + -            '<option value="B" selected>Extra padding</option>' + -            '<option value="C">Expedite</option>' + -            '</select>'); -    assertJsonEquals(["A", "B"], scope.Invoice.options); -  }); - -  it('ChangingSelectSelectedUpdatesModel', function(){ -    var scope = this.compile('<select name="model.price"><option>A</option><option selected value="b">B</option></select>'); -    assertEquals(scope.model.price, 'b'); -  }); -    it('ExecuteInitialization', function(){      var scope = this.compile('<div ng:init="a=123">');      assertEquals(scope.a, 123); @@ -236,14 +192,13 @@ describe('Binder', function(){    });    it('RepeaterAdd', function(){ -    var scope = this.compile('<div><input type="text" name="item.x" ng:repeat="item in items"></div>'); +    var scope = this.compile('<div><input type="text" ng:model="item.x" ng:repeat="item in items"></div>');      scope.items = [{x:'a'}, {x:'b'}];      scope.$apply();      var first = childNode(scope.$element, 1);      var second = childNode(scope.$element, 2);      expect(first.val()).toEqual('a');      expect(second.val()).toEqual('b'); -    return      first.val('ABC');      browserTrigger(first, 'keydown'); @@ -440,15 +395,6 @@ describe('Binder', function(){      assertEquals('123{{a}}{{b}}{{c}}', scope.$element.text());    }); -  it('RepeaterShouldBindInputsDefaults', function () { -    var scope = this.compile('<div><input value="123" name="item.name" ng:repeat="item in items"></div>'); -    scope.items = [{}, {name:'misko'}]; -    scope.$apply(); - -    expect(scope.$eval('items[0].name')).toEqual("123"); -    expect(scope.$eval('items[1].name')).toEqual("misko"); -  }); -    it('ShouldTemplateBindPreElements', function () {      var scope = this.compile('<pre>Hello {{name}}!</pre>');      scope.name = "World"; @@ -459,7 +405,11 @@ describe('Binder', function(){    it('FillInOptionValueWhenMissing', function(){      var scope = this.compile( -        '<select name="foo"><option selected="true">{{a}}</option><option value="">{{b}}</option><option>C</option></select>'); +        '<select ng:model="foo">' + +          '<option selected="true">{{a}}</option>' + +          '<option value="">{{b}}</option>' + +          '<option>C</option>' + +        '</select>');      scope.a = 'A';      scope.b = 'B';      scope.$apply(); @@ -477,52 +427,14 @@ describe('Binder', function(){      expect(optionC.text()).toEqual('C');    }); -  it('ValidateForm', function(){ -    var scope = this.compile('<div id="test"><input name="name" ng:required>' + -            '<input ng:repeat="item in items" name="item.name" ng:required/></div>', -            jqLite(document.body)); -    var items = [{}, {}]; -    scope.items = items; -    scope.$apply(); -    assertEquals(3, scope.$service('$invalidWidgets').length); - -    scope.name = ''; -    scope.$apply(); -    assertEquals(3, scope.$service('$invalidWidgets').length); - -    scope.name = ' '; -    scope.$apply(); -    assertEquals(3, scope.$service('$invalidWidgets').length); - -    scope.name = 'abc'; -    scope.$apply(); -    assertEquals(2, scope.$service('$invalidWidgets').length); - -    items[0].name = 'abc'; -    scope.$apply(); -    assertEquals(1, scope.$service('$invalidWidgets').length); - -    items[1].name = 'abc'; -    scope.$apply(); -    assertEquals(0, scope.$service('$invalidWidgets').length); -  }); - -  it('ValidateOnlyVisibleItems', function(){ -    var scope = this.compile('<div><input name="name" ng:required><input ng:show="show" name="name" ng:required></div>', jqLite(document.body)); -    scope.show = true; -    scope.$apply(); -    assertEquals(2, scope.$service('$invalidWidgets').length); - -    scope.show = false; -    scope.$apply(); -    assertEquals(1, scope.$service('$invalidWidgets').visible()); -  }); -    it('DeleteAttributeIfEvaluatesFalse', function(){      var scope = this.compile('<div>' + -        '<input name="a0" ng:bind-attr="{disabled:\'{{true}}\'}"><input name="a1" ng:bind-attr="{disabled:\'{{false}}\'}">' + -        '<input name="b0" ng:bind-attr="{disabled:\'{{1}}\'}"><input name="b1" ng:bind-attr="{disabled:\'{{0}}\'}">' + -        '<input name="c0" ng:bind-attr="{disabled:\'{{[0]}}\'}"><input name="c1" ng:bind-attr="{disabled:\'{{[]}}\'}"></div>'); +        '<input ng:model="a0" ng:bind-attr="{disabled:\'{{true}}\'}">' + +        '<input ng:model="a1" ng:bind-attr="{disabled:\'{{false}}\'}">' + +        '<input ng:model="b0" ng:bind-attr="{disabled:\'{{1}}\'}">' + +        '<input ng:model="b1" ng:bind-attr="{disabled:\'{{0}}\'}">' + +        '<input ng:model="c0" ng:bind-attr="{disabled:\'{{[0]}}\'}">' + +        '<input ng:model="c1" ng:bind-attr="{disabled:\'{{[]}}\'}"></div>');      scope.$apply();      function assertChild(index, disabled) {        var child = childNode(scope.$element, index); @@ -556,8 +468,8 @@ describe('Binder', function(){    it('ItShouldSelectTheCorrectRadioBox', function(){      var scope = this.compile('<div>' + -        '<input type="radio" name="sex" value="female"/>' + -        '<input type="radio" name="sex" value="male"/></div>'); +        '<input type="radio" ng:model="sex" value="female">' + +        '<input type="radio" ng:model="sex" value="male"></div>');      var female = jqLite(scope.$element[0].childNodes[0]);      var male = jqLite(scope.$element[0].childNodes[1]); @@ -603,23 +515,4 @@ describe('Binder', function(){      assertEquals("3", scope.$element.text());    }); -  it('ItBindHiddenInputFields', function(){ -    var scope = this.compile('<input type="hidden" name="myName" value="abc" />'); -    scope.$apply(); -    assertEquals("abc", scope.myName); -  }); - -  it('ItShouldUseFormaterForText', function(){ -    var scope = this.compile('<input name="a" ng:format="list" value="a,b">'); -    scope.$apply(); -    assertEquals(['a','b'], scope.a); -    var input = scope.$element; -    input[0].value = ' x,,yz'; -    browserTrigger(input, 'change'); -    assertEquals(['x','yz'], scope.a); -    scope.a = [1 ,2, 3]; -    scope.$apply(); -    assertEquals('1, 2, 3', input[0].value); -  }); -  }); diff --git a/test/BrowserSpecs.js b/test/BrowserSpecs.js index de4354a0..692bc5ae 100644 --- a/test/BrowserSpecs.js +++ b/test/BrowserSpecs.js @@ -669,7 +669,6 @@ describe('browser', function(){    });    describe('addJs', function() { -      it('should append a script tag to body', function() {        browser.addJs('http://localhost/bar.js');        expect(scripts.length).toBe(1); @@ -677,15 +676,6 @@ describe('browser', function(){        expect(scripts[0].id).toBe('');      }); - -    it('should append a script with an id to body', function() { -      browser.addJs('http://localhost/bar.js', 'foo-id'); -      expect(scripts.length).toBe(1); -      expect(scripts[0].src).toBe('http://localhost/bar.js'); -      expect(scripts[0].id).toBe('foo-id'); -    }); - -      it('should return the appended script element', function() {        var script = browser.addJs('http://localhost/bar.js');        expect(script).toBe(scripts[0]); diff --git a/test/FormattersSpec.js b/test/FormattersSpec.js deleted file mode 100644 index 8f438671..00000000 --- a/test/FormattersSpec.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -describe("formatter", function(){ -  it('should noop', function(){ -    assertEquals("abc", angular.formatter.noop.format("abc")); -    assertEquals("xyz", angular.formatter.noop.parse("xyz")); -    assertEquals(null, angular.formatter.noop.parse(null)); -  }); - -  it('should List', function() { -    assertEquals('a, b', angular.formatter.list.format(['a', 'b'])); -    assertEquals('', angular.formatter.list.format([])); -    assertEquals(['abc', 'c'], angular.formatter.list.parse("  , abc , c ,,")); -    assertEquals([], angular.formatter.list.parse("")); -    assertEquals([], angular.formatter.list.parse(null)); -  }); - -  it('should Boolean', function() { -    assertEquals('true', angular.formatter['boolean'].format(true)); -    assertEquals('false', angular.formatter['boolean'].format(false)); -    assertEquals(true, angular.formatter['boolean'].parse("true")); -    assertEquals(false, angular.formatter['boolean'].parse("")); -    assertEquals(false, angular.formatter['boolean'].parse("false")); -    assertEquals(false, angular.formatter['boolean'].parse(null)); -  }); - -  it('should Number', function() { -    assertEquals('1', angular.formatter.number.format(1)); -    assertEquals(1, angular.formatter.number.format('1')); -  }); - -  it('should Trim', function() { -    assertEquals('', angular.formatter.trim.format(null)); -    assertEquals('', angular.formatter.trim.format("")); -    assertEquals('a', angular.formatter.trim.format(" a ")); -    assertEquals('a', angular.formatter.trim.parse(' a ')); -  }); - -  describe('json', function(){ -    it('should treat empty string as null', function(){ -      expect(angular.formatter.json.parse('')).toEqual(null); -    }); -  }); - -}); diff --git a/test/JsonSpec.js b/test/JsonSpec.js index b0bb15bc..2bd7241f 100644 --- a/test/JsonSpec.js +++ b/test/JsonSpec.js @@ -15,6 +15,10 @@ describe('json', function(){      expect(toJson({$$some:'value', 'this':1, '$parent':1}, false)).toEqual('{}');    }); +  it('should not serialize this or $parent', function(){ +    expect(toJson({'this':'value', $parent:'abc'}, false)).toEqual('{}'); +  }); +    it('should serialize strings with escaped characters', function() {      expect(toJson("7\\\"7")).toEqual("\"7\\\\\\\"7\"");    }); diff --git a/test/ParserSpec.js b/test/ParserSpec.js index a5e1901c..980a673c 100644 --- a/test/ParserSpec.js +++ b/test/ParserSpec.js @@ -415,24 +415,6 @@ describe('parser', function() {      expect(scope.$eval('true || run()')).toBe(true);    }); -  describe('formatter', function() { -    it('should return no argument function', function() { -      var noop = parser('noop').formatter()(); -      expect(noop.format(null, 'abc')).toEqual('abc'); -      expect(noop.parse(null, '123')).toEqual('123'); -    }); - -    it('should delegate arguments', function() { -      angularFormatter.myArgs = { -        parse: function(a, b){ return [a, b]; }, -        format: function(a, b){ return [a, b]; } -      }; -      var myArgs = parser('myArgs:objs').formatter()(); -      expect(myArgs.format({objs:'B'}, 'A')).toEqual(['A', 'B']); -      expect(myArgs.parse({objs:'D'}, 'C')).toEqual(['C', 'D']); -      delete angularFormatter.myArgs; -    }); -  });    describe('assignable', function(){      it('should expose assignment function', function(){ @@ -443,5 +425,4 @@ describe('parser', function() {        expect(scope).toEqual({a:123});      });    }); -  }); diff --git a/test/ScopeSpec.js b/test/ScopeSpec.js index 492396c5..fa41e5a9 100644 --- a/test/ScopeSpec.js +++ b/test/ScopeSpec.js @@ -1,7 +1,7 @@  'use strict'; -describe('Scope', function() { -  var root, mockHandler; +describe('Scope', function(){ +  var root = null, mockHandler = null;    beforeEach(function() {      root = createScope(angular.service, { @@ -245,8 +245,14 @@ describe('Scope', function() {        var log = '';        root.a = [];        root.b = {}; -      root.$watch('a', function() { log +='.';}); -      root.$watch('b', function() { log +='!';}); +      root.$watch('a', function(scope, value){ +        log +='.'; +        expect(value).toBe(root.a); +      }); +      root.$watch('b', function(scope, value){ +        log +='!'; +        expect(value).toBe(root.b); +      });        root.$digest();        log = ''; @@ -296,8 +302,8 @@ describe('Scope', function() {    }); -  describe('$destroy', function() { -    var first, middle, last, log; +  describe('$destroy', function(){ +    var first = null, middle = null, last = null, log = null;      beforeEach(function() {        log = ''; @@ -531,7 +537,6 @@ describe('Scope', function() {          greatGrandChild.$on('myEvent', logger);        }); -        it('should bubble event up to the root scope', function() {          grandChild.$emit('myEvent');          expect(log).toEqual('2>1>0>'); diff --git a/test/ValidatorsSpec.js b/test/ValidatorsSpec.js deleted file mode 100644 index f44a9a59..00000000 --- a/test/ValidatorsSpec.js +++ /dev/null @@ -1,172 +0,0 @@ -'use strict'; - -describe('Validator', function(){ - -  it('ShouldHaveThisSet', function() { -    var validator = {}; -    angular.validator.myValidator = function(first, last){ -      validator.first = first; -      validator.last = last; -      validator._this = this; -    }; -    var scope = compile('<input name="name" ng:validate="myValidator:\'hevery\'"/>')(); -    scope.name = 'misko'; -    scope.$digest(); -    assertEquals('misko', validator.first); -    assertEquals('hevery', validator.last); -    expect(validator._this.$id).toEqual(scope.$id); -    delete angular.validator.myValidator; -    scope.$element.remove(); -  }); - -  it('Regexp', function() { -    assertEquals(angular.validator.regexp("abc", /x/, "E1"), "E1"); -    assertEquals(angular.validator.regexp("abc", '/x/'), -        "Value does not match expected format /x/."); -    assertEquals(angular.validator.regexp("ab", '^ab$'), null); -    assertEquals(angular.validator.regexp("ab", '^axb$', "E3"), "E3"); -  }); - -  it('Number', function() { -    assertEquals(angular.validator.number("ab"), "Not a number"); -    assertEquals(angular.validator.number("-0.1",0), "Value can not be less than 0."); -    assertEquals(angular.validator.number("10.1",0,10), "Value can not be greater than 10."); -    assertEquals(angular.validator.number("1.2"), null); -    assertEquals(angular.validator.number(" 1 ", 1, 1), null); -  }); - -  it('Integer', function() { -    assertEquals(angular.validator.integer("ab"), "Not a number"); -    assertEquals(angular.validator.integer("1.1"), "Not a whole number"); -    assertEquals(angular.validator.integer("1.0"), "Not a whole number"); -    assertEquals(angular.validator.integer("1."), "Not a whole number"); -    assertEquals(angular.validator.integer("-1",0), "Value can not be less than 0."); -    assertEquals(angular.validator.integer("11",0,10), "Value can not be greater than 10."); -    assertEquals(angular.validator.integer("1"), null); -    assertEquals(angular.validator.integer(" 1 ", 1, 1), null); -  }); - -  it('Date', function() { -    var error = "Value is not a date. (Expecting format: 12/31/2009)."; -    expect(angular.validator.date("ab")).toEqual(error); -    expect(angular.validator.date("12/31/2009")).toEqual(null); -    expect(angular.validator.date("1/1/1000")).toEqual(null); -    expect(angular.validator.date("12/31/9999")).toEqual(null); -    expect(angular.validator.date("2/29/2004")).toEqual(null); -    expect(angular.validator.date("2/29/2000")).toEqual(null); -    expect(angular.validator.date("2/29/2100")).toEqual(error); -    expect(angular.validator.date("2/29/2003")).toEqual(error); -    expect(angular.validator.date("41/1/2009")).toEqual(error); -    expect(angular.validator.date("13/1/2009")).toEqual(error); -    expect(angular.validator.date("1/1/209")).toEqual(error); -    expect(angular.validator.date("1/32/2010")).toEqual(error); -    expect(angular.validator.date("001/031/2009")).toEqual(error); -  }); - -  it('Phone', function() { -    var error = "Phone number needs to be in 1(987)654-3210 format in North America " + -                "or +999 (123) 45678 906 internationally."; -    assertEquals(angular.validator.phone("ab"), error); -    assertEquals(null, angular.validator.phone("1(408)757-3023")); -    assertEquals(null, angular.validator.phone("+421 (0905) 933 297")); -    assertEquals(null, angular.validator.phone("+421 0905 933 297")); -  }); - -  it('URL', function() { -    var error = "URL needs to be in http://server[:port]/path format."; -    assertEquals(angular.validator.url("ab"), error); -    assertEquals(angular.validator.url("http://server:123/path"), null); -  }); - -  it('Email', function() { -    var error = "Email needs to be in username@host.com format."; -    assertEquals(error, angular.validator.email("ab")); -    assertEquals(null, angular.validator.email("misko@hevery.com")); -  }); - -  it('Json', function() { -    assertNotNull(angular.validator.json("'")); -    assertNotNull(angular.validator.json("''X")); -    assertNull(angular.validator.json("{}")); -  }); - -  describe('asynchronous', function(){ -    var asynchronous = angular.validator.asynchronous; -    var self; -    var value, fn; - -    beforeEach(function(){ -      value = null; -      fn = null; -      self = angular.compile('<input />')(); -      jqLite(document.body).append(self.$element); -      self.$element.data('$validate', noop); -      self.$root = self; -    }); - -    afterEach(function(){ -      if (self.$element) self.$element.remove(); -    }); - -    it('should make a request and show spinner', function(){ -      var value, fn; -      var scope = angular.compile( -          '<input type="text" name="name" ng:validate="asynchronous:asyncFn"/>')(); -      jqLite(document.body).append(scope.$element); -      var input = scope.$element; -      scope.asyncFn = function(v,f){ -        value=v; fn=f; -      }; -      scope.name = "misko"; -      scope.$digest(); -      expect(value).toEqual('misko'); -      expect(input.hasClass('ng-input-indicator-wait')).toBeTruthy(); -      fn("myError"); -      expect(input.hasClass('ng-input-indicator-wait')).toBeFalsy(); -      expect(input.attr(NG_VALIDATION_ERROR)).toEqual("myError"); -      scope.$element.remove(); -    }); - -    it("should not make second request to same value", function(){ -      asynchronous.call(self, "kai", function(v,f){value=v; fn=f;}); -      expect(value).toEqual('kai'); -      expect(self.$service('$invalidWidgets')[0]).toEqual(self.$element); - -      var spy = jasmine.createSpy(); -      asynchronous.call(self, "kai", spy); -      expect(spy).not.toHaveBeenCalled(); - -      asynchronous.call(self, "misko", spy); -      expect(spy).toHaveBeenCalled(); -    }); - -    it("should ignore old callbacks, and not remove spinner", function(){ -      var firstCb, secondCb; -      asynchronous.call(self, "first", function(v,f){value=v; firstCb=f;}); -      asynchronous.call(self, "second", function(v,f){value=v; secondCb=f;}); - -      firstCb(); -      expect(self.$element.hasClass('ng-input-indicator-wait')).toBeTruthy(); - -      secondCb(); -      expect(self.$element.hasClass('ng-input-indicator-wait')).toBeFalsy(); -    }); - -    it("should handle update function", function(){ -      var scope = angular.compile( -          '<input name="name" ng:validate="asynchronous:asyncFn:updateFn"/>')(); -      scope.asyncFn = jasmine.createSpy(); -      scope.updateFn = jasmine.createSpy(); -      scope.name = 'misko'; -      scope.$digest(); -      expect(scope.asyncFn).toHaveBeenCalledWith('misko', scope.asyncFn.mostRecentCall.args[1]); -      assertTrue(scope.$element.hasClass('ng-input-indicator-wait')); -      scope.asyncFn.mostRecentCall.args[1]('myError', {id: 1234, data:'data'}); -      assertFalse(scope.$element.hasClass('ng-input-indicator-wait')); -      assertEquals('myError', scope.$element.attr('ng-validation-error')); -      expect(scope.updateFn.mostRecentCall.args[0]).toEqual({id: 1234, data:'data'}); -      scope.$element.remove(); -    }); - -  }); -}); diff --git a/test/directivesSpec.js b/test/directivesSpec.js index c925bdb5..1cbb92b0 100644 --- a/test/directivesSpec.js +++ b/test/directivesSpec.js @@ -80,6 +80,11 @@ describe("directive", function() {        expect(scope.$element.text()).toEqual('-0false');      }); +    it('should render object as JSON ignore $$', function(){ +      var scope = compile('<div>{{ {key:"value", $$key:"hide"}  }}</div>'); +      scope.$digest(); +      expect(fromJson(scope.$element.text())).toEqual({key:'value'}); +    });    });    describe('ng:bind-template', function() { @@ -103,6 +108,12 @@ describe("directive", function() {        expect(innerText).toEqual('INNER');      }); +    it('should render object as JSON ignore $$', function(){ +      var scope = compile('<pre>{{ {key:"value", $$key:"hide"}  }}</pre>'); +      scope.$digest(); +      expect(fromJson(scope.$element.text())).toEqual({key:'value'}); +    }); +    });    describe('ng:bind-attr', function() { diff --git a/test/jQueryPatchSpec.js b/test/jQueryPatchSpec.js new file mode 100644 index 00000000..0953bdac --- /dev/null +++ b/test/jQueryPatchSpec.js @@ -0,0 +1,57 @@ +'use strict'; + +if (window.jQuery) { + +  describe('jQuery patch', function(){ + +    var doc = null; +    var divSpy = null; +    var spy1 = null; +    var spy2 = null; + +    beforeEach(function(){ +      divSpy = jasmine.createSpy('div.$destroy'); +      spy1 = jasmine.createSpy('span1.$destroy'); +      spy2 = jasmine.createSpy('span2.$destroy'); +      doc = $('<div><span class=first>abc</span><span class=second>xyz</span></div>'); +      doc.find('span.first').bind('$destroy', spy1); +      doc.find('span.second').bind('$destroy', spy2); +    }); + +    afterEach(function(){ +      expect(divSpy).not.toHaveBeenCalled(); + +      expect(spy1).toHaveBeenCalled(); +      expect(spy1.callCount).toEqual(1); +      expect(spy2).toHaveBeenCalled(); +      expect(spy2.callCount).toEqual(1); +    }); + +    describe('$detach event', function(){ + +      it('should fire on detach()', function(){ +        doc.find('span').detach(); +      }); + +      it('should fire on remove()', function(){ +        doc.find('span').remove(); +      }); + +      it('should fire on replaceWith()', function(){ +        doc.find('span').replaceWith('<b>bla</b>'); +      }); + +      it('should fire on replaceAll()', function(){ +        $('<b>bla</b>').replaceAll(doc.find('span')); +      }); + +      it('should fire on empty()', function(){ +        doc.empty(); +      }); + +      it('should fire on html()', function(){ +        doc.html('abc'); +      }); +    }); +  }); +} diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index bb00ca25..28cc7b90 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -110,6 +110,7 @@ describe('jqLite', function(){      });    }); +    describe('scope', function() {      it('should retrieve scope attached to the current element', function() {        var element = jqLite('<i>foo</i>'); @@ -138,7 +139,7 @@ describe('jqLite', function(){    describe('data', function(){ -    it('should set and get ande remove data', function(){ +    it('should set and get and remove data', function(){        var selected = jqLite([a, b, c]);        expect(selected.data('prop', 'value')).toEqual(selected); @@ -158,6 +159,14 @@ describe('jqLite', function(){        expect(jqLite(b).data('prop')).toEqual(undefined);        expect(jqLite(c).data('prop')).toEqual(undefined);      }); + +    it('should call $destroy function if element removed', function(){ +      var log = ''; +      var element = jqLite(a); +      element.bind('$destroy', function(){log+= 'destroy;';}); +      element.remove(); +      expect(log).toEqual('destroy;'); +    });    }); @@ -242,6 +251,21 @@ describe('jqLite', function(){          var selector = jqLite([a, b]);          expect(selector.hasClass('abc')).toEqual(false);        }); + + +      it('should make sure that partial class is not checked as a subset', function(){ +        var selector = jqLite([a, b]); +        selector.addClass('a'); +        selector.addClass('b'); +        selector.addClass('c'); +        expect(selector.addClass('abc')).toEqual(selector); +        expect(selector.removeClass('abc')).toEqual(selector); +        expect(jqLite(a).hasClass('abc')).toEqual(false); +        expect(jqLite(b).hasClass('abc')).toEqual(false); +        expect(jqLite(a).hasClass('a')).toEqual(true); +        expect(jqLite(a).hasClass('b')).toEqual(true); +        expect(jqLite(a).hasClass('c')).toEqual(true); +      });      }); @@ -318,16 +342,10 @@ describe('jqLite', function(){      describe('removeClass', function(){        it('should allow removal of class', function(){          var selector = jqLite([a, b]); -        selector.addClass('a'); -        selector.addClass('b'); -        selector.addClass('c');          expect(selector.addClass('abc')).toEqual(selector);          expect(selector.removeClass('abc')).toEqual(selector);          expect(jqLite(a).hasClass('abc')).toEqual(false);          expect(jqLite(b).hasClass('abc')).toEqual(false); -        expect(jqLite(a).hasClass('a')).toEqual(true); -        expect(jqLite(a).hasClass('b')).toEqual(true); -        expect(jqLite(a).hasClass('c')).toEqual(true);        }); diff --git a/test/markupSpec.js b/test/markupSpec.js index 2704e0dc..bd77c058 100644 --- a/test/markupSpec.js +++ b/test/markupSpec.js @@ -26,12 +26,18 @@ describe("markups", function(){    });    it('should translate {{}} in terminal nodes', function(){ -    compile('<select name="x"><option value="">Greet {{name}}!</option></select>'); +    compile('<select ng:model="x"><option value="">Greet {{name}}!</option></select>');      scope.$digest(); -    expect(sortedHtml(element).replace(' selected="true"', '')).toEqual('<select name="x"><option ng:bind-template="Greet {{name}}!">Greet !</option></select>'); +    expect(sortedHtml(element).replace(' selected="true"', '')). +      toEqual('<select ng:model="x">' + +                '<option ng:bind-template="Greet {{name}}!">Greet !</option>' + +              '</select>');      scope.name = 'Misko';      scope.$digest(); -    expect(sortedHtml(element).replace(' selected="true"', '')).toEqual('<select name="x"><option ng:bind-template="Greet {{name}}!">Greet Misko!</option></select>'); +    expect(sortedHtml(element).replace(' selected="true"', '')). +      toEqual('<select ng:model="x">' + +                '<option ng:bind-template="Greet {{name}}!">Greet Misko!</option>' + +              '</select>');    });    it('should translate {{}} in attributes', function(){ @@ -69,24 +75,24 @@ describe("markups", function(){      it('should populate value attribute on OPTION', function(){ -      compile('<select name="x"><option>abc</option></select>'); +      compile('<select ng:model="x"><option>abc</option></select>');        expect(element).toHaveValue('abc');      });      it('should ignore value if already exists', function(){ -      compile('<select name="x"><option value="abc">xyz</option></select>'); +      compile('<select ng:model="x"><option value="abc">xyz</option></select>');        expect(element).toHaveValue('abc');      });      it('should set value even if newlines present', function(){ -      compile('<select name="x"><option attr="\ntext\n" \n>\nabc\n</option></select>'); +      compile('<select ng:model="x"><option attr="\ntext\n" \n>\nabc\n</option></select>');        expect(element).toHaveValue('\nabc\n');      });      it('should set value even if self closing HTML', function(){        // IE removes the \n from option, which makes this test pointless        if (msie) return; -      compile('<select name="x"><option>\n</option></select>'); +      compile('<select ng:model="x"><option>\n</option></select>');        expect(element).toHaveValue('\n');      }); diff --git a/test/scenario/dslSpec.js b/test/scenario/dslSpec.js index c5d0a29d..3fc69c14 100644 --- a/test/scenario/dslSpec.js +++ b/test/scenario/dslSpec.js @@ -203,29 +203,40 @@ describe("angular.scenario.dsl", function() {      describe('Select', function() {        it('should select single option', function() {          doc.append( -          '<select name="test">' + -          '  <option>A</option>' + -          '  <option selected>B</option>' + +          '<select ng:model="test">' + +          '  <option value=A>one</option>' + +          '  <option value=B selected>two</option>' +            '</select>'          );          $root.dsl.select('test').option('A'); -        expect(_jQuery('[name="test"]').val()).toEqual('A'); +        expect(_jQuery('[ng\\:model="test"]').val()).toEqual('A'); +      }); + +      it('should select option by name', function(){ +        doc.append( +            '<select ng:model="test">' + +            '  <option value=A>one</option>' + +            '  <option value=B selected>two</option>' + +            '</select>' +          ); +          $root.dsl.select('test').option('one'); +          expect(_jQuery('[ng\\:model="test"]').val()).toEqual('A');        });        it('should select multiple options', function() {          doc.append( -          '<select name="test" multiple>' + +          '<select ng:model="test" multiple>' +            '  <option>A</option>' +            '  <option selected>B</option>' +            '  <option>C</option>' +            '</select>'          );          $root.dsl.select('test').options('A', 'B'); -        expect(_jQuery('[name="test"]').val()).toEqual(['A','B']); +        expect(_jQuery('[ng\\:model="test"]').val()).toEqual(['A','B']);        });        it('should fail to select multiple options on non-multiple select', function() { -        doc.append('<select name="test"></select>'); +        doc.append('<select ng:model="test"></select>');          $root.dsl.select('test').options('A', 'B');          expect($root.futureError).toMatch(/did not match/);        }); @@ -477,12 +488,12 @@ describe("angular.scenario.dsl", function() {        it('should prefix selector in $document.elements()', function() {          var chain;          doc.append( -          '<div id="test1"><input name="test.input" value="something"></div>' + -          '<div id="test2"><input name="test.input" value="something"></div>' +          '<div id="test1"><input ng:model="test.input" value="something"></div>' + +          '<div id="test2"><input ng:model="test.input" value="something"></div>'          );          chain = $root.dsl.using('div#test2');          chain.input('test.input').enter('foo'); -        var inputs = _jQuery('input[name="test.input"]'); +        var inputs = _jQuery('input[ng\\:model="test.input"]');          expect(inputs.first().val()).toEqual('something');          expect(inputs.last().val()).toEqual('foo');        }); @@ -501,10 +512,10 @@ describe("angular.scenario.dsl", function() {      describe('Input', function() {        it('should change value in text input', function() { -        doc.append('<input name="test.input" value="something">'); +        doc.append('<input ng:model="test.input" value="something">');          var chain = $root.dsl.input('test.input');          chain.enter('foo'); -        expect(_jQuery('input[name="test.input"]').val()).toEqual('foo'); +        expect(_jQuery('input[ng\\:model="test.input"]').val()).toEqual('foo');        });        it('should return error if no input exists', function() { @@ -514,16 +525,16 @@ describe("angular.scenario.dsl", function() {        });        it('should toggle checkbox state', function() { -        doc.append('<input type="checkbox" name="test.input" checked>'); -        expect(_jQuery('input[name="test.input"]'). +        doc.append('<input type="checkbox" ng:model="test.input" checked>'); +        expect(_jQuery('input[ng\\:model="test.input"]').            prop('checked')).toBe(true);          var chain = $root.dsl.input('test.input');          chain.check(); -        expect(_jQuery('input[name="test.input"]'). +        expect(_jQuery('input[ng\\:model="test.input"]').            prop('checked')).toBe(false);          $window.angular.reset();          chain.check(); -        expect(_jQuery('input[name="test.input"]'). +        expect(_jQuery('input[ng\\:model="test.input"]').            prop('checked')).toBe(true);        }); @@ -535,20 +546,20 @@ describe("angular.scenario.dsl", function() {        it('should select option from radio group', function() {          doc.append( -          '<input type="radio" name="0@test.input" value="foo">' + -          '<input type="radio" name="0@test.input" value="bar" checked="checked">' +          '<input type="radio" name="r" ng:model="test.input" value="foo">' + +          '<input type="radio" name="r" ng:model="test.input" value="bar" checked="checked">'          );          // HACK! We don't know why this is sometimes false on chrome -        _jQuery('input[name="0@test.input"][value="bar"]').prop('checked', true); -        expect(_jQuery('input[name="0@test.input"][value="bar"]'). +        _jQuery('input[ng\\:model="test.input"][value="bar"]').prop('checked', true); +        expect(_jQuery('input[ng\\:model="test.input"][value="bar"]').            prop('checked')).toBe(true); -        expect(_jQuery('input[name="0@test.input"][value="foo"]'). +        expect(_jQuery('input[ng\\:model="test.input"][value="foo"]').            prop('checked')).toBe(false);          var chain = $root.dsl.input('test.input');          chain.select('foo'); -        expect(_jQuery('input[name="0@test.input"][value="bar"]'). +        expect(_jQuery('input[ng\\:model="test.input"][value="bar"]').            prop('checked')).toBe(false); -        expect(_jQuery('input[name="0@test.input"][value="foo"]'). +        expect(_jQuery('input[ng\\:model="test.input"][value="foo"]').            prop('checked')).toBe(true);        }); @@ -560,7 +571,7 @@ describe("angular.scenario.dsl", function() {        describe('val', function() {          it('should return value in text input', function() { -          doc.append('<input name="test.input" value="something">'); +          doc.append('<input ng:model="test.input" value="something">');            $root.dsl.input('test.input').val();            expect($root.futureResult).toEqual("something");          }); @@ -570,10 +581,10 @@ describe("angular.scenario.dsl", function() {      describe('Textarea', function() {        it('should change value in textarea', function() { -        doc.append('<textarea name="test.textarea">something</textarea>'); +        doc.append('<textarea ng:model="test.textarea">something</textarea>');          var chain = $root.dsl.input('test.textarea');          chain.enter('foo'); -        expect(_jQuery('textarea[name="test.textarea"]').val()).toEqual('foo'); +        expect(_jQuery('textarea[ng\\:model="test.textarea"]').val()).toEqual('foo');        });        it('should return error if no textarea exists', function() { diff --git a/test/scenario/e2e/widgets.html b/test/scenario/e2e/widgets.html index e19a33f4..fb27f72e 100644 --- a/test/scenario/e2e/widgets.html +++ b/test/scenario/e2e/widgets.html @@ -15,34 +15,34 @@        <tr>          <td>basic</td>          <td id="text-basic-box"> -          <input type="text" name="text.basic"/> +          <input type="text" ng:model="text.basic"/>          </td>          <td>text.basic={{text.basic}}</td>        </tr>        <tr>          <td>password</td> -        <td><input type="password" name="text.password" /></td> +        <td><input type="password" ng:model="text.password" /></td>          <td>text.password={{text.password}}</td>        </tr>        <tr>          <td>hidden</td> -        <td><input type="hidden" name="text.hidden" value="hiddenValue" /></td> +        <td><input type="hidden" ng:model="text.hidden" value="hiddenValue" /></td>          <td>text.hidden={{text.hidden}}</td>        </tr>        <tr><th colspan="3">Input selection field</th></tr>        <tr id="gender-box">          <td>radio</td>          <td> -         <input type="radio" name="gender" value="female"/> Female <br/> -         <input type="radio" name="gender" value="male" checked="checked"/> Male +         <input type="radio" ng:model="gender" value="female"/> Female <br/> +         <input type="radio" ng:model="gender" value="male" checked="checked"/> Male          </td>          <td>gender={{gender}}</td>        </tr>        <tr>          <td>checkbox</td>          <td> -         <input type="checkbox" name="checkbox.tea" checked value="on"/> Tea<br/> -         <input type="checkbox" name="checkbox.coffee" value="on"/> Coffe +         <input type="checkbox" ng:model="checkbox.tea" checked value="on"/> Tea<br/> +         <input type="checkbox" ng:model="checkbox.coffee" value="on"/> Coffe          </td>          <td>            <pre>checkbox={{checkbox}}</pre> @@ -51,7 +51,7 @@        <tr>          <td>select</td>          <td> -          <select name="select"> +          <select ng:model="select">              <option>A</option>              <option>B</option>              <option>C</option> @@ -62,7 +62,7 @@        <tr>          <td>multiselect</td>          <td> -          <select name="multiselect" multiple> +          <select ng:model="multiselect" multiple>              <option>A</option>              <option>B</option>              <option>C</option> diff --git a/test/service/formFactorySpec.js b/test/service/formFactorySpec.js new file mode 100644 index 00000000..5223cede --- /dev/null +++ b/test/service/formFactorySpec.js @@ -0,0 +1,218 @@ +'use strict'; + +describe('$formFactory', function(){ + +  var rootScope; +  var formFactory; + +  beforeEach(function(){ +    rootScope = angular.scope(); +    formFactory = rootScope.$service('$formFactory'); +  }); + + +  it('should have global form', function(){ +    expect(formFactory.rootForm).toBeTruthy(); +    expect(formFactory.rootForm.$createWidget).toBeTruthy(); +  }); + + +  describe('new form', function(){ +    var form; +    var scope; +    var log; + +    function WidgetCtrl($formFactory){ +      this.$formFactory = $formFactory; +      log += '<init>'; +      this.$render = function(){ +        log += '$render();'; +      }; +      this.$on('$validate', function(e){ +        log += '$validate();'; +      }); +    } + +    WidgetCtrl.$inject = ['$formFactory']; + +    WidgetCtrl.prototype = { +        getFormFactory: function() { +          return this.$formFactory; +        } +    }; + +    beforeEach(function(){ +      log = ''; +      scope = rootScope.$new(); +      form = formFactory(scope); +    }); + +    describe('$createWidget', function(){ +      var widget; + +      beforeEach(function() { +        widget = form.$createWidget({ +          scope:scope, +          model:'text', +          alias:'text', +          controller:WidgetCtrl}); +      }); + + +      describe('data flow', function(){ +        it('should have status properties', function(){ +          expect(widget.$error).toEqual({}); +          expect(widget.$valid).toBe(true); +          expect(widget.$invalid).toBe(false); +        }); + + +        it('should update view when model changes', function(){ +          scope.text = 'abc'; +          scope.$digest(); +          expect(log).toEqual('<init>$validate();$render();'); +          expect(widget.$modelValue).toEqual('abc'); + +          scope.text = 'xyz'; +          scope.$digest(); +          expect(widget.$modelValue).toEqual('xyz'); + +        }); + + +        it('should have controller prototype methods', function(){ +          expect(widget.getFormFactory()).toEqual(formFactory); +        }); +      }); + + +      describe('validation', function(){ +        it('should update state on error', function(){ +          widget.$emit('$invalid', 'E'); +          expect(widget.$valid).toEqual(false); +          expect(widget.$invalid).toEqual(true); + +          widget.$emit('$valid', 'E'); +          expect(widget.$valid).toEqual(true); +          expect(widget.$invalid).toEqual(false); +        }); + + +        it('should have called the model setter before the validation', function(){ +          var modelValue; +          widget.$on('$validate', function(){ +            modelValue = scope.text; +          }); +          widget.$emit('$viewChange', 'abc'); +          expect(modelValue).toEqual('abc'); +        }); + + +        describe('form', function(){ +          it('should invalidate form when widget is invalid', function(){ +            expect(form.$error).toEqual({}); +            expect(form.$valid).toEqual(true); +            expect(form.$invalid).toEqual(false); + +            widget.$emit('$invalid', 'REASON'); + +            expect(form.$error.REASON).toEqual([widget]); +            expect(form.$valid).toEqual(false); +            expect(form.$invalid).toEqual(true); + +            var widget2 = form.$createWidget({ +              scope:scope, model:'text', +              alias:'text', +              controller:WidgetCtrl +            }); +            widget2.$emit('$invalid', 'REASON'); + +            expect(form.$error.REASON).toEqual([widget, widget2]); +            expect(form.$valid).toEqual(false); +            expect(form.$invalid).toEqual(true); + +            widget.$emit('$valid', 'REASON'); + +            expect(form.$error.REASON).toEqual([widget2]); +            expect(form.$valid).toEqual(false); +            expect(form.$invalid).toEqual(true); + +            widget2.$emit('$valid', 'REASON'); + +            expect(form.$error).toEqual({}); +            expect(form.$valid).toEqual(true); +            expect(form.$invalid).toEqual(false); +          }); +        }); + +      }); + +      describe('id assignment', function(){ +        it('should default to name expression', function(){ +          expect(form.text).toEqual(widget); +        }); + + +        it('should use ng:id', function() { +          widget = form.$createWidget({ +            scope:scope, +            model:'text', +            alias:'my.id', +            controller:WidgetCtrl +          }); +          expect(form['my.id']).toEqual(widget); +        }); + + +        it('should not override existing names', function() { +          var widget2 = form.$createWidget({ +            scope:scope, +            model:'text', +            alias:'text', +            controller:WidgetCtrl +          }); +          expect(form.text).toEqual(widget); +          expect(widget2).not.toEqual(widget); +        }); +      }); + +      describe('dealocation', function() { +        it('should dealocate', function() { +          var widget2 = form.$createWidget({ +            scope:scope, +            model:'text', +            alias:'myId', +            controller:WidgetCtrl +          }); +          expect(form.myId).toEqual(widget2); +          var widget3 = form.$createWidget({ +            scope:scope, +            model:'text', +            alias:'myId', +            controller:WidgetCtrl +          }); +          expect(form.myId).toEqual(widget2); + +          widget3.$destroy(); +          expect(form.myId).toEqual(widget2); + +          widget2.$destroy(); +          expect(form.myId).toBeUndefined(); +        }); + + +        it('should remove invalid fields from errors, when child widget removed', function(){ +          widget.$emit('$invalid', 'MyError'); + +          expect(form.$error.MyError).toEqual([widget]); +          expect(form.$invalid).toEqual(true); + +          widget.$destroy(); + +          expect(form.$error.MyError).toBeUndefined(); +          expect(form.$invalid).toEqual(false); +        }); +      }); +    }); +  }); +}); diff --git a/test/service/invalidWidgetsSpec.js b/test/service/invalidWidgetsSpec.js deleted file mode 100644 index fe7efe38..00000000 --- a/test/service/invalidWidgetsSpec.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -describe('$invalidWidgets', function() { -  var scope; - -  beforeEach(function(){ -    scope = angular.scope(); -  }); - - -  afterEach(function(){ -    dealoc(scope); -  }); - - -  it("should count number of invalid widgets", function(){ -    var element = jqLite('<input name="price" ng:required ng:validate="number">'); -    jqLite(document.body).append(element); -    scope = compile(element)(); -    var $invalidWidgets = scope.$service('$invalidWidgets'); -    expect($invalidWidgets.length).toEqual(1); - -    scope.price = 123; -    scope.$digest(); -    expect($invalidWidgets.length).toEqual(0); - -    scope.$element.remove(); -    scope.price = 'abc'; -    scope.$digest(); -    expect($invalidWidgets.length).toEqual(0); - -    jqLite(document.body).append(scope.$element); -    scope.price = 'abcd'; //force revalidation, maybe this should be done automatically? -    scope.$digest(); -    expect($invalidWidgets.length).toEqual(1); - -    jqLite(document.body).html(''); -    scope.$digest(); -    expect($invalidWidgets.length).toEqual(0); -  }); -}); diff --git a/test/service/routeSpec.js b/test/service/routeSpec.js index c8c8cbeb..5aba2a1f 100644 --- a/test/service/routeSpec.js +++ b/test/service/routeSpec.js @@ -152,18 +152,18 @@ describe('$route', function() {      $location.path('/foo');      scope.$digest(); -    expect(scope.$$childHead).toBeTruthy(); -    expect(scope.$$childHead).toEqual(scope.$$childTail); +    expect(scope.$$childHead.$id).toBeTruthy(); +    expect(scope.$$childHead.$id).toEqual(scope.$$childTail.$id);      $location.path('/bar');      scope.$digest(); -    expect(scope.$$childHead).toBeTruthy(); -    expect(scope.$$childHead).toEqual(scope.$$childTail); +    expect(scope.$$childHead.$id).toBeTruthy(); +    expect(scope.$$childHead.$id).toEqual(scope.$$childTail.$id);      $location.path('/baz');      scope.$digest(); -    expect(scope.$$childHead).toBeTruthy(); -    expect(scope.$$childHead).toEqual(scope.$$childTail); +    expect(scope.$$childHead.$id).toBeTruthy(); +    expect(scope.$$childHead.$id).toEqual(scope.$$childTail.$id);      $location.path('/');      scope.$digest(); @@ -172,6 +172,14 @@ describe('$route', function() {    }); +  it('should infer arguments in injection', function() { +    $route.when('/test', {controller: function($route){ this.$route = $route; }}); +    $location.path('/test'); +    scope.$digest(); +    expect($route.current.scope.$route).toBe($route); +  }); + +    describe('redirection', function() {      it('should support redirection via redirectTo property by updating $location', function() {        var onChangeSpy = jasmine.createSpy('onChange'); diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index 3b9d9208..41a6455c 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -11,13 +11,17 @@ _jQuery.event.special.change = undefined;  if (window.jstestdriver) {    window.jstd = jstestdriver; -  window.dump = function(){ +  window.dump = function dump(){      var args = [];      forEach(arguments, function(arg){        if (isElement(arg)) {          arg = sortedHtml(arg);        } else if (isObject(arg)) { -        arg = toJson(arg, true); +        if (arg.$eval == Scope.prototype.$eval) { +          arg = dumpScope(arg); +        } else { +          arg = toJson(arg, true); +        }        }        args.push(arg);      }); @@ -25,6 +29,23 @@ if (window.jstestdriver) {    };  } +function dumpScope(scope, offset) { +  offset = offset ||  '  '; +  var log = [offset + 'Scope(' + scope.$id + '): {']; +  for ( var key in scope ) { +    if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { +      log.push('  ' + key + ': ' + toJson(scope[key])); +    } +  } +  var child = scope.$$childHead; +  while(child) { +    log.push(dumpScope(child, offset + '  ')); +    child = child.$$nextSibling; +  } +  log.push('}'); +  return log.join('\n' + offset); +} +  beforeEach(function(){    // This is to reset parsers global cache of expressions.    compileCache = {}; @@ -36,30 +57,41 @@ beforeEach(function(){      jQuery = _jQuery;    } +  // This resets global id counter; +  uid = ['0', '0', '0']; +    // reset to jQuery or default to us.    bindJQuery();    jqLite(document.body).html(''); -  this.addMatchers({ -    toBeInvalid: function(){ -      var element = jqLite(this.actual); -      var hasClass = element.hasClass('ng-validation-error'); -      var validationError = element.attr('ng-validation-error'); -      this.message = function(){ -        if (!hasClass) -          return "Expected class 'ng-validation-error' not found."; -        return "Expected an error message, but none was found."; -      }; -      return hasClass && validationError; -    }, -    toBeValid: function(){ +  function cssMatcher(presentClasses, absentClasses) { +    return function(){        var element = jqLite(this.actual); -      var hasClass = element.hasClass('ng-validation-error'); +      var present = true; +      var absent = false; + +      forEach(presentClasses.split(' '), function(className){ +        present = present && element.hasClass(className); +      }); + +      forEach(absentClasses.split(' '), function(className){ +        absent = absent || element.hasClass(className); +      }); +        this.message = function(){ -        return "Expected to not have class 'ng-validation-error' but found."; +        return "Expected to have " + presentClasses + +          (absentClasses ? (" and not have " + absentClasses + "" ) : "") + +          " but had " + element[0].className + ".";        }; -      return !hasClass; -    }, +      return present && !absent; +    }; +  } + +  this.addMatchers({ +    toBeInvalid: cssMatcher('ng-invalid', 'ng-valid'), +    toBeValid: cssMatcher('ng-valid', 'ng-invalid'), +    toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'), +    toBePristine: cssMatcher('ng-pristine', 'ng-dirty'),      toEqualData: function(expected) {        return equals(this.actual, expected); diff --git a/test/widget/formSpec.js b/test/widget/formSpec.js new file mode 100644 index 00000000..7c575c33 --- /dev/null +++ b/test/widget/formSpec.js @@ -0,0 +1,97 @@ +'use strict'; + +describe('form', function(){ +  var doc; + +  afterEach(function(){ +    dealoc(doc); +  }); + + +  it('should attach form to DOM', function(){ +    doc = angular.element('<form>'); +    var scope = angular.compile(doc)(); +    expect(doc.data('$form')).toBeTruthy(); +  }); + + +  it('should prevent form submission', function(){ +    var startingUrl = '' + window.location; +    doc = angular.element('<form name="myForm"><input type=submit val=submit>'); +    var scope = angular.compile(doc)(); +    browserTrigger(doc.find('input')); +    waitsFor( +        function(){ return true; }, +        'let browser breath, so that the form submision can manifest itself', 10); +    runs(function(){ +      expect('' + window.location).toEqual(startingUrl); +    }); +  }); + + +  it('should publish form to scope', function(){ +    doc = angular.element('<form name="myForm">'); +    var scope = angular.compile(doc)(); +    expect(scope.myForm).toBeTruthy(); +    expect(doc.data('$form')).toBeTruthy(); +    expect(doc.data('$form')).toEqual(scope.myForm); +  }); + + +  it('should have ng-valide/ng-invalid style', function(){ +    doc = angular.element('<form name="myForm"><input type=text ng:model=text required>'); +    var scope = angular.compile(doc)(); +    scope.text = 'misko'; +    scope.$digest(); + +    expect(doc.hasClass('ng-valid')).toBe(true); +    expect(doc.hasClass('ng-invalid')).toBe(false); + +    scope.text = ''; +    scope.$digest(); +    expect(doc.hasClass('ng-valid')).toBe(false); +    expect(doc.hasClass('ng-invalid')).toBe(true); +  }); + + +  it('should chain nested forms', function(){ +    doc = angular.element('<ng:form name=parent><ng:form name=child><input type=text ng:model=text name=text>'); +    var scope = angular.compile(doc)(); +    var parent = scope.parent; +    var child = scope.child; +    var input = child.text; + +    input.$emit('$invalid', 'MyError'); +    expect(parent.$error.MyError).toEqual([input]); +    expect(child.$error.MyError).toEqual([input]); + +    input.$emit('$valid', 'MyError'); +    expect(parent.$error.MyError).toBeUndefined(); +    expect(child.$error.MyError).toBeUndefined(); +  }); + + +  it('should chain nested forms in repeater', function(){ +    doc = angular.element('<ng:form name=parent>' + +        '<ng:form ng:repeat="f in forms" name=child><input type=text ng:model=text name=text>'); +    var scope = angular.compile(doc)(); +    scope.forms = [1]; +    scope.$digest(); + +    var parent = scope.parent; +    var child = doc.find('input').scope().child; +    var input = child.text; +    expect(parent).toBeDefined(); +    expect(child).toBeDefined(); +    expect(input).toBeDefined(); + +    input.$emit('$invalid', 'myRule'); +    expect(input.$error.myRule).toEqual(true); +    expect(child.$error.myRule).toEqual([input]); +    expect(parent.$error.myRule).toEqual([input]); + +    input.$emit('$valid', 'myRule'); +    expect(parent.$error.myRule).toBeUndefined(); +    expect(child.$error.myRule).toBeUndefined(); +  }); +}); diff --git a/test/widget/inputSpec.js b/test/widget/inputSpec.js new file mode 100644 index 00000000..31f8c59c --- /dev/null +++ b/test/widget/inputSpec.js @@ -0,0 +1,547 @@ +'use strict'; + +describe('widget: input', function(){ +  var compile = null, element = null, scope = null, defer = null; +  var doc = null; + +  beforeEach(function() { +    scope = null; +    element = null; +    compile = function(html, parent) { +      if (parent) { +        parent.html(html); +        element = parent.children(); +      } else { +        element = jqLite(html); +      } +      scope = angular.compile(element)(); +      scope.$apply(); +      defer = scope.$service('$browser').defer; +      return scope; +    }; +  }); + +  afterEach(function(){ +    dealoc(element); +    dealoc(doc); +  }); + + +  describe('text', function(){ +    var scope = null, +        form = null, +        formElement = null, +        inputElement = null; + +    function createInput(flags){ +      var prefix = ''; +      forEach(flags, function(value, key){ +        prefix += key + '="' + value + '" '; +      }); +      formElement = doc = angular.element('<form name="form"><input ' + prefix + +          'type="text" ng:model="name" name="name" ng:change="change()"></form>'); +      inputElement = formElement.find('input'); +      scope = angular.compile(doc)(); +      form = formElement.inheritedData('$form'); +    }; + + +    it('should bind update scope from model', function(){ +      createInput(); +      expect(scope.form.name.$required).toBe(false); +      scope.name = 'misko'; +      scope.$digest(); +      expect(inputElement.val()).toEqual('misko'); +    }); + + +    it('should require', function(){ +      createInput({required:''}); +      expect(scope.form.name.$required).toBe(true); +      scope.$digest(); +      expect(scope.form.name.$valid).toBe(false); +      scope.name = 'misko'; +      scope.$digest(); +      expect(scope.form.name.$valid).toBe(true); +    }); + + +    it('should call $destroy on element remove', function(){ +      createInput(); +      var log = ''; +      form.$on('$destroy', function(){ +        log += 'destroy;'; +      }); +      inputElement.remove(); +      expect(log).toEqual('destroy;'); +    }); + + +    it('should update the model and trim input', function(){ +      createInput(); +      var log = ''; +      scope.change = function(){ +        log += 'change();'; +      }; +      inputElement.val(' a '); +      browserTrigger(inputElement); +      scope.$service('$browser').defer.flush(); +      expect(scope.name).toEqual('a'); +      expect(log).toEqual('change();'); +    }); + + +    it('should change non-html5 types to text', function(){ +      doc = angular.element('<form name="form"><input type="abc" ng:model="name"></form>'); +      scope = angular.compile(doc)(); +      expect(doc.find('input').attr('type')).toEqual('text'); +    }); + + +    it('should not change html5 types to text', function(){ +      doc = angular.element('<form name="form"><input type="number" ng:model="name"></form>'); +      scope = angular.compile(doc)(); +      expect(doc.find('input')[0].getAttribute('type')).toEqual('number'); +    }); +  }); + + +  describe("input", function(){ + +    describe("text", function(){ +      it('should input-text auto init and handle keydown/change events', function(){ +        compile('<input type="text" ng:model="name"/>'); + +        scope.name = 'Adam'; +        scope.$digest(); +        expect(element.val()).toEqual("Adam"); + +        element.val('Shyam'); +        browserTrigger(element, 'keydown'); +        // keydown event must be deferred +        expect(scope.name).toEqual('Adam'); +        defer.flush(); +        expect(scope.name).toEqual('Shyam'); + +        element.val('Kai'); +        browserTrigger(element, 'change'); +        scope.$service('$browser').defer.flush(); +        expect(scope.name).toEqual('Kai'); +      }); + + +      it('should not trigger eval if value does not change', function(){ +        compile('<input type="text" ng:model="name" ng:change="count = count + 1" ng:init="count=0"/>'); +        scope.name = 'Misko'; +        scope.$digest(); +        expect(scope.name).toEqual("Misko"); +        expect(scope.count).toEqual(0); +        browserTrigger(element, 'keydown'); +        scope.$service('$browser').defer.flush(); +        expect(scope.name).toEqual("Misko"); +        expect(scope.count).toEqual(0); +      }); + + +      it('should allow complex reference binding', function(){ +        compile('<div>'+ +                  '<input type="text" ng:model="obj[\'abc\'].name"/>'+ +                '</div>'); +        scope.obj = { abc: { name: 'Misko'} }; +        scope.$digest(); +        expect(scope.$element.find('input').val()).toEqual('Misko'); +      }); + + +      describe("ng:format", function(){ +        it("should format text", function(){ +          compile('<input type="list" ng:model="list"/>'); + +          scope.list = ['x', 'y', 'z']; +          scope.$digest(); +          expect(element.val()).toEqual("x, y, z"); + +          element.val('1, 2, 3'); +          browserTrigger(element); +          scope.$service('$browser').defer.flush(); +          expect(scope.list).toEqual(['1', '2', '3']); +        }); + + +        it("should render as blank if null", function(){ +          compile('<input type="text" ng:model="age" ng:format="number" ng:init="age=null"/>'); +          expect(scope.age).toBeNull(); +          expect(scope.$element[0].value).toEqual(''); +        }); + + +        it("should show incorrect text while number does not parse", function(){ +          compile('<input type="number" ng:model="age"/>'); +          scope.age = 123; +          scope.$digest(); +          expect(scope.$element.val()).toEqual('123'); +          try { +            // to allow non-number values, we have to change type so that +            // the browser which have number validation will not interfere with +            // this test. IE8 won't allow it hence the catch. +            scope.$element[0].setAttribute('type', 'text'); +          } catch (e){} +          scope.$element.val('123X'); +          browserTrigger(scope.$element, 'change'); +          scope.$service('$browser').defer.flush(); +          expect(scope.$element.val()).toEqual('123X'); +          expect(scope.age).toEqual(123); +          expect(scope.$element).toBeInvalid(); +        }); + + +        it("should not clobber text if model changes due to itself", function(){ +          // When the user types 'a,b' the 'a,' stage parses to ['a'] but if the +          // $parseModel function runs it will change to 'a', in essence preventing +          // the user from ever typying ','. +          compile('<input type="list" ng:model="list"/>'); + +          scope.$element.val('a '); +          browserTrigger(scope.$element, 'change'); +          scope.$service('$browser').defer.flush(); +          expect(scope.$element.val()).toEqual('a '); +          expect(scope.list).toEqual(['a']); + +          scope.$element.val('a ,'); +          browserTrigger(scope.$element, 'change'); +          scope.$service('$browser').defer.flush(); +          expect(scope.$element.val()).toEqual('a ,'); +          expect(scope.list).toEqual(['a']); + +          scope.$element.val('a , '); +          browserTrigger(scope.$element, 'change'); +          scope.$service('$browser').defer.flush(); +          expect(scope.$element.val()).toEqual('a , '); +          expect(scope.list).toEqual(['a']); + +          scope.$element.val('a , b'); +          browserTrigger(scope.$element, 'change'); +          scope.$service('$browser').defer.flush(); +          expect(scope.$element.val()).toEqual('a , b'); +          expect(scope.list).toEqual(['a', 'b']); +        }); + + +        it("should come up blank when no value specified", function(){ +          compile('<input type="number" ng:model="age"/>'); +          scope.$digest(); +          expect(scope.$element.val()).toEqual(''); +          expect(scope.age).toEqual(null); +        }); +      }); + + +      describe("checkbox", function(){ +        it("should format booleans", function(){ +          compile('<input type="checkbox" ng:model="name" ng:init="name=false"/>'); +          expect(scope.name).toBe(false); +          expect(scope.$element[0].checked).toBe(false); +        }); + + +        it('should support type="checkbox" with non-standard capitalization', function(){ +          compile('<input type="checkBox" ng:model="checkbox"/>'); + +          browserTrigger(element); +          expect(scope.checkbox).toBe(true); + +          browserTrigger(element); +          expect(scope.checkbox).toBe(false); +        }); + + +        it('should allow custom enumeration', function(){ +          compile('<input type="checkbox" ng:model="name" true-value="ano" false-value="nie"/>'); + +          scope.name='ano'; +          scope.$digest(); +          expect(scope.$element[0].checked).toBe(true); + +          scope.name='nie'; +          scope.$digest(); +          expect(scope.$element[0].checked).toBe(false); + +          scope.name='abc'; +          scope.$digest(); +          expect(scope.$element[0].checked).toBe(false); + +          browserTrigger(element); +          expect(scope.name).toEqual('ano'); + +          browserTrigger(element); +          expect(scope.name).toEqual('nie'); +        }); +      }); +    }); + + +    it("should process required", function(){ +      compile('<input type="text" ng:model="price" name="p" required/>', jqLite(document.body)); +      expect(scope.$service('$formFactory').rootForm.p.$required).toBe(true); +      expect(element.hasClass('ng-invalid')).toBeTruthy(); + +      scope.price = 'xxx'; +      scope.$digest(); +      expect(element.hasClass('ng-invalid')).toBeFalsy(); + +      element.val(''); +      browserTrigger(element); +      scope.$service('$browser').defer.flush(); +      expect(element.hasClass('ng-invalid')).toBeTruthy(); +    }); + + +    it('should allow bindings on ng:required', function() { +      compile('<input type="text" ng:model="price" ng:required="{{required}}"/>', +              jqLite(document.body)); +      scope.price = ''; +      scope.required = false; +      scope.$digest(); +      expect(element).toBeValid(); + +      scope.price = 'xxx'; +      scope.$digest(); +      expect(element).toBeValid(); + +      scope.price = ''; +      scope.required =  true; +      scope.$digest(); +      expect(element).toBeInvalid(); + +      element.val('abc'); +      browserTrigger(element); +      scope.$service('$browser').defer.flush(); +      expect(element).toBeValid(); +    }); + + +    describe('textarea', function(){ +      it("should process textarea", function() { +        compile('<textarea ng:model="name"></textarea>'); + +        scope.name = 'Adam'; +        scope.$digest(); +        expect(element.val()).toEqual("Adam"); + +        element.val('Shyam'); +        browserTrigger(element); +        defer.flush(); +        expect(scope.name).toEqual('Shyam'); + +        element.val('Kai'); +        browserTrigger(element); +        defer.flush(); +        expect(scope.name).toEqual('Kai'); +      }); +    }); + + +    describe('radio', function(){ +      it('should support type="radio"', function(){ +        compile('<div>' + +            '<input type="radio" name="r" ng:model="chose" value="A"/>' + +            '<input type="radio" name="r" ng:model="chose" value="B"/>' + +            '<input type="radio" name="r" ng:model="chose" value="C"/>' + +        '</div>'); +        var a = element[0].childNodes[0]; +        var b = element[0].childNodes[1]; +        expect(b.name.split('@')[1]).toEqual('r'); +        scope.chose = 'A'; +        scope.$digest(); +        expect(a.checked).toBe(true); + +        scope.chose = 'B'; +        scope.$digest(); +        expect(a.checked).toBe(false); +        expect(b.checked).toBe(true); +        expect(scope.clicked).not.toBeDefined(); + +        browserTrigger(a); +        expect(scope.chose).toEqual('A'); +      }); + + +      it('should honor model over html checked keyword after', function(){ +        compile('<div ng:init="choose=\'C\'">' + +            '<input type="radio" ng:model="choose" value="A""/>' + +            '<input type="radio" ng:model="choose" value="B" checked/>' + +            '<input type="radio" ng:model="choose" value="C"/>' + +        '</div>'); + +        expect(scope.choose).toEqual('C'); +        var inputs = scope.$element.find('input'); +        expect(inputs[1].checked).toBe(false); +        expect(inputs[2].checked).toBe(true); +      }); + + +      it('should honor model over html checked keyword before', function(){ +        compile('<div ng:init="choose=\'A\'">' + +            '<input type="radio" ng:model="choose" value="A""/>' + +            '<input type="radio" ng:model="choose" value="B" checked/>' + +            '<input type="radio" ng:model="choose" value="C"/>' + +        '</div>'); + +        expect(scope.choose).toEqual('A'); +        var inputs = scope.$element.find('input'); +        expect(inputs[0].checked).toBe(true); +        expect(inputs[1].checked).toBe(false); +      }); +    }); + + +    it('should ignore text widget which have no name', function(){ +      compile('<input type="text"/>'); +      expect(scope.$element.attr('ng-exception')).toBeFalsy(); +      expect(scope.$element.hasClass('ng-exception')).toBeFalsy(); +    }); + + +    it('should ignore checkbox widget which have no name', function(){ +      compile('<input type="checkbox"/>'); +      expect(scope.$element.attr('ng-exception')).toBeFalsy(); +      expect(scope.$element.hasClass('ng-exception')).toBeFalsy(); +    }); + + +    it('should report error on assignment error', function(){ +      expect(function(){ +        compile('<input type="text" ng:model="throw \'\'">'); +      }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at ['']."); +      $logMock.error.logs.shift(); +    }); +  }); + + +  describe('scope declaration', function(){ +    it('should read the declaration from scope', function(){ +      var input, $formFactory; +      element = angular.element('<input type="@MyType" ng:model="abc">'); +      scope = angular.scope(); +      scope.MyType = function($f, i) { +        input = i; +        $formFactory = $f; +      }; +      scope.MyType.$inject = ['$formFactory']; + +      angular.compile(element)(scope); + +      expect($formFactory).toBe(scope.$service('$formFactory')); +      expect(input[0]).toBe(element[0]); +    }); + +    it('should throw an error of Cntoroller not declared in scope', function() { +      var input, $formFactory; +      element = angular.element('<input type="@DontExist" ng:model="abc">'); +      var error; +      try { +        scope = angular.scope(); +        angular.compile(element)(scope); +        error = 'no error thrown'; +      } catch (e) { +        error = e; +      } +      expect(error.message).toEqual("Argument 'DontExist' is not a function, got undefined"); +    }); +  }); + + +  describe('text subtypes', function(){ + +    function itShouldVerify(type, validList, invalidList, params, fn) { +      describe(type, function(){ +        forEach(validList, function(value){ +          it('should validate "' + value + '"', function(){ +            setup(value); +            expect(scope.$element).toBeValid(); +          }); +        }); +        forEach(invalidList, function(value){ +          it('should NOT validate "' + value + '"', function(){ +            setup(value); +            expect(scope.$element).toBeInvalid(); +          }); +        }); + +        function setup(value){ +          var html = ['<input type="', type.split(' ')[0], '" ']; +          forEach(params||{}, function(value, key){ +            html.push(key + '="' + value + '" '); +          }); +          html.push('ng:model="value">'); +          compile(html.join('')); +          (fn||noop)(scope); +          scope.value = null; +          try { +            // to allow non-number values, we have to change type so that +            // the browser which have number validation will not interfere with +            // this test. IE8 won't allow it hence the catch. +            scope.$element[0].setAttribute('type', 'text'); +          } catch (e){} +          if (value != undefined) { +            scope.$element.val(value); +            browserTrigger(element, 'keydown'); +            scope.$service('$browser').defer.flush(); +          } +          scope.$digest(); +        } +      }); +    } + + +    itShouldVerify('email', ['a@b.com'], ['a@B.c']); + + +    itShouldVerify('url', ['http://server:123/path'], ['a@b.c']); + + +    itShouldVerify('number', +        ['', '1', '12.34', '-4', '+13', '.1'], +        ['x', '12b', '-6', '101'], +        {min:-5, max:100}); + + +    itShouldVerify('integer', +        [null, '', '1', '12', '-4', '+13'], +        ['x', '12b', '-6', '101', '1.', '1.2'], +        {min:-5, max:100}); + + +    itShouldVerify('integer', +        [null, '', '0', '1'], +        ['-1', '2'], +        {min:0, max:1}); + + +    itShouldVerify('text with inlined pattern contraint', +        ['', '000-00-0000', '123-45-6789'], +        ['x000-00-0000x', 'x'], +        {'ng:pattern':'/^\\d\\d\\d-\\d\\d-\\d\\d\\d\\d$/'}); + + +    itShouldVerify('text with pattern constraint on scope', +        ['', '000-00-0000', '123-45-6789'], +        ['x000-00-0000x', 'x'], +        {'ng:pattern':'regexp'}, function(scope){ +          scope.regexp = /^\d\d\d-\d\d-\d\d\d\d$/; +        }); + + +    it('should throw an error when scope pattern can\'t be found', function() { +      var el = jqLite('<input ng:model="foo" ng:pattern="fooRegexp">'), +          scope = angular.compile(el)(); + +      el.val('xx'); +      browserTrigger(el, 'keydown'); +      expect(function() { scope.$service('$browser').defer.flush(); }). +        toThrow('Expected fooRegexp to be a RegExp but was undefined'); + +      dealoc(el); +    }); +  }); +}); diff --git a/test/widget/selectSpec.js b/test/widget/selectSpec.js new file mode 100644 index 00000000..6adf8b93 --- /dev/null +++ b/test/widget/selectSpec.js @@ -0,0 +1,510 @@ +'use strict'; + +describe('select', function(){ +  var compile = null, element = null, scope = null, $formFactory = null; + +  beforeEach(function() { +    scope = null; +    element = null; +    compile = function(html, parent) { +      if (parent) { +        parent.html(html); +        element = parent.children(); +      } else { +        element = jqLite(html); +      } +      scope = angular.compile(element)(); +      scope.$apply(); +      $formFactory = scope.$service('$formFactory'); +      return scope; +    }; +  }); + +  afterEach(function(){ +    dealoc(element); +  }); + + +  describe('select-one', function(){ + +    it('should compile children of a select without a name, but not create a model for it', +        function() { +      compile('<select>' + +                '<option selected="true">{{a}}</option>' + +                '<option value="">{{b}}</option>' + +                '<option>C</option>' + +              '</select>'); +      scope.a = 'foo'; +      scope.b = 'bar'; +      scope.$digest(); + +      expect(scope.$element.text()).toBe('foobarC'); +    }); + +    it('should require', function(){ +      compile('<select name="select" ng:model="selection" required ng:change="log=log+\'change;\'">' + +          '<option value=""></option>' + +          '<option value="c">C</option>' + +        '</select>'); +      scope.log = ''; +      scope.selection = 'c'; +      scope.$digest(); +      expect($formFactory.forElement(element).select.$error.REQUIRED).toEqual(undefined); +      expect(element).toBeValid(); +      expect(element).toBePristine(); + +      scope.selection = ''; +      scope.$digest(); +      expect($formFactory.forElement(element).select.$error.REQUIRED).toEqual(true); +      expect(element).toBeInvalid(); +      expect(element).toBePristine(); +      expect(scope.log).toEqual(''); + +      element[0].value = 'c'; +      browserTrigger(element, 'change'); +      expect(element).toBeValid(); +      expect(element).toBeDirty(); +      expect(scope.log).toEqual('change;'); +    }); + +    it('should not be invalid if no require', function(){ +      compile('<select name="select" ng:model="selection">' + +          '<option value=""></option>' + +          '<option value="c">C</option>' + +        '</select>'); + +      expect(element).toBeValid(); +      expect(element).toBePristine(); +    }); + +  }); + + +  describe('select-multiple', function(){ +    it('should support type="select-multiple"', function(){ +      compile('<select ng:model="selection" multiple>' + +                '<option>A</option>' + +                '<option>B</option>' + +              '</select>'); +      scope.selection = ['A']; +      scope.$digest(); +      expect(element[0].childNodes[0].selected).toEqual(true); +    }); + +    it('should require', function(){ +      compile('<select name="select" ng:model="selection" multiple required>' + +          '<option>A</option>' + +          '<option>B</option>' + +        '</select>'); + +      scope.selection = []; +      scope.$digest(); +      expect($formFactory.forElement(element).select.$error.REQUIRED).toEqual(true); +      expect(element).toBeInvalid(); +      expect(element).toBePristine(); + +      scope.selection = ['A']; +      scope.$digest(); +      expect(element).toBeValid(); +      expect(element).toBePristine(); + +      element[0].value = 'B'; +      browserTrigger(element, 'change'); +      expect(element).toBeValid(); +      expect(element).toBeDirty(); +    }); + +  }); + + +  describe('ng:options', function(){ +    var select, scope; + +    function createSelect(attrs, blank, unknown){ +      var html = '<select'; +      forEach(attrs, function(value, key){ +        if (isBoolean(value)) { +          if (value) html += ' ' + key; +        } else { +          html += ' ' + key + '="' + value + '"'; +        } +      }); +      html += '>' + +        (blank ? '<option value="">blank</option>' : '') + +        (unknown ? '<option value="?">unknown</option>' : '') + +      '</select>'; +      select = jqLite(html); +      scope = compile(select); +    } + +    function createSingleSelect(blank, unknown){ +      createSelect({ +        'ng:model':'selected', +        'ng:options':'value.name for value in values' +      }, blank, unknown); +    } + +    function createMultiSelect(blank, unknown){ +      createSelect({ +        'ng:model':'selected', +        'multiple':true, +        'ng:options':'value.name for value in values' +      }, blank, unknown); +    } + +    afterEach(function(){ +      dealoc(select); +      dealoc(scope); +    }); + +    it('should throw when not formated "? for ? in ?"', function(){ +      expect(function(){ +        compile('<select ng:model="selected" ng:options="i dont parse"></select>'); +      }).toThrow("Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in" + +                 " _collection_' but got 'i dont parse'."); +    }); + +    it('should render a list', function(){ +      createSingleSelect(); +      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; +      scope.selected = scope.values[0]; +      scope.$digest(); +      var options = select.find('option'); +      expect(options.length).toEqual(3); +      expect(sortedHtml(options[0])).toEqual('<option value="0">A</option>'); +      expect(sortedHtml(options[1])).toEqual('<option value="1">B</option>'); +      expect(sortedHtml(options[2])).toEqual('<option value="2">C</option>'); +    }); + +    it('should render an object', function(){ +      createSelect({ +        'ng:model':'selected', +        'ng:options': 'value as key for (key, value) in object' +      }); +      scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; +      scope.selected = scope.object.red; +      scope.$digest(); +      var options = select.find('option'); +      expect(options.length).toEqual(3); +      expect(sortedHtml(options[0])).toEqual('<option value="blue">blue</option>'); +      expect(sortedHtml(options[1])).toEqual('<option value="green">green</option>'); +      expect(sortedHtml(options[2])).toEqual('<option value="red">red</option>'); +      expect(options[2].selected).toEqual(true); + +      scope.object.azur = '8888FF'; +      scope.$digest(); +      options = select.find('option'); +      expect(options[3].selected).toEqual(true); +    }); + +    it('should grow list', function(){ +      createSingleSelect(); +      scope.values = []; +      scope.$digest(); +      expect(select.find('option').length).toEqual(1); // because we add special empty option +      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="?"></option>'); + +      scope.values.push({name:'A'}); +      scope.selected = scope.values[0]; +      scope.$digest(); +      expect(select.find('option').length).toEqual(1); +      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); + +      scope.values.push({name:'B'}); +      scope.$digest(); +      expect(select.find('option').length).toEqual(2); +      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); +      expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>'); +    }); + +    it('should shrink list', function(){ +      createSingleSelect(); +      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; +      scope.selected = scope.values[0]; +      scope.$digest(); +      expect(select.find('option').length).toEqual(3); + +      scope.values.pop(); +      scope.$digest(); +      expect(select.find('option').length).toEqual(2); +      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); +      expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>'); + +      scope.values.pop(); +      scope.$digest(); +      expect(select.find('option').length).toEqual(1); +      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); + +      scope.values.pop(); +      scope.selected = null; +      scope.$digest(); +      expect(select.find('option').length).toEqual(1); // we add back the special empty option +    }); + +    it('should shrink and then grow list', function(){ +      createSingleSelect(); +      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; +      scope.selected = scope.values[0]; +      scope.$digest(); +      expect(select.find('option').length).toEqual(3); + +      scope.values = [{name:'1'}, {name:'2'}]; +      scope.selected = scope.values[0]; +      scope.$digest(); +      expect(select.find('option').length).toEqual(2); + +      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; +      scope.selected = scope.values[0]; +      scope.$digest(); +      expect(select.find('option').length).toEqual(3); +    }); + +    it('should update list', function(){ +      createSingleSelect(); +      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; +      scope.selected = scope.values[0]; +      scope.$digest(); + +      scope.values = [{name:'B'}, {name:'C'}, {name:'D'}]; +      scope.selected = scope.values[0]; +      scope.$digest(); +      var options = select.find('option'); +      expect(options.length).toEqual(3); +      expect(sortedHtml(options[0])).toEqual('<option value="0">B</option>'); +      expect(sortedHtml(options[1])).toEqual('<option value="1">C</option>'); +      expect(sortedHtml(options[2])).toEqual('<option value="2">D</option>'); +    }); + +    it('should preserve existing options', function(){ +      createSingleSelect(true); + +      scope.values = []; +      scope.$digest(); +      expect(select.find('option').length).toEqual(1); + +      scope.values = [{name:'A'}]; +      scope.selected = scope.values[0]; +      scope.$digest(); +      expect(select.find('option').length).toEqual(2); +      expect(jqLite(select.find('option')[0]).text()).toEqual('blank'); +      expect(jqLite(select.find('option')[1]).text()).toEqual('A'); + +      scope.values = []; +      scope.selected = null; +      scope.$digest(); +      expect(select.find('option').length).toEqual(1); +      expect(jqLite(select.find('option')[0]).text()).toEqual('blank'); +    }); + +    describe('binding', function(){ +      it('should bind to scope value', function(){ +        createSingleSelect(); +        scope.values = [{name:'A'}, {name:'B'}]; +        scope.selected = scope.values[0]; +        scope.$digest(); +        expect(select.val()).toEqual('0'); + +        scope.selected = scope.values[1]; +        scope.$digest(); +        expect(select.val()).toEqual('1'); +      }); + +      it('should bind to scope value and group', function(){ +        createSelect({ +          'ng:model':'selected', +          'ng:options':'item.name group by item.group for item in values' +        }); +        scope.values = [{name:'A'}, +                        {name:'B', group:'first'}, +                        {name:'C', group:'second'}, +                        {name:'D', group:'first'}, +                        {name:'E', group:'second'}]; +        scope.selected = scope.values[3]; +        scope.$digest(); +        expect(select.val()).toEqual('3'); + +        var first = jqLite(select.find('optgroup')[0]); +        var b = jqLite(first.find('option')[0]); +        var d = jqLite(first.find('option')[1]); +        expect(first.attr('label')).toEqual('first'); +        expect(b.text()).toEqual('B'); +        expect(d.text()).toEqual('D'); + +        var second = jqLite(select.find('optgroup')[1]); +        var c = jqLite(second.find('option')[0]); +        var e = jqLite(second.find('option')[1]); +        expect(second.attr('label')).toEqual('second'); +        expect(c.text()).toEqual('C'); +        expect(e.text()).toEqual('E'); + +        scope.selected = scope.values[0]; +        scope.$digest(); +        expect(select.val()).toEqual('0'); +      }); + +      it('should bind to scope value through experession', function(){ +        createSelect({'ng:model':'selected', 'ng:options':'item.id as item.name for item in values'}); +        scope.values = [{id:10, name:'A'}, {id:20, name:'B'}]; +        scope.selected = scope.values[0].id; +        scope.$digest(); +        expect(select.val()).toEqual('0'); + +        scope.selected = scope.values[1].id; +        scope.$digest(); +        expect(select.val()).toEqual('1'); +      }); + +      it('should bind to object key', function(){ +        createSelect({ +          'ng:model':'selected', +          'ng:options':'key as value for (key, value) in object' +        }); +        scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; +        scope.selected = 'green'; +        scope.$digest(); +        expect(select.val()).toEqual('green'); + +        scope.selected = 'blue'; +        scope.$digest(); +        expect(select.val()).toEqual('blue'); +      }); + +      it('should bind to object value', function(){ +        createSelect({ +          'ng:model':'selected', +          'ng:options':'value as key for (key, value) in object' +        }); +        scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; +        scope.selected = '00FF00'; +        scope.$digest(); +        expect(select.val()).toEqual('green'); + +        scope.selected = '0000FF'; +        scope.$digest(); +        expect(select.val()).toEqual('blue'); +      }); + +      it('should insert a blank option if bound to null', function(){ +        createSingleSelect(); +        scope.values = [{name:'A'}]; +        scope.selected = null; +        scope.$digest(); +        expect(select.find('option').length).toEqual(2); +        expect(select.val()).toEqual(''); +        expect(jqLite(select.find('option')[0]).val()).toEqual(''); + +        scope.selected = scope.values[0]; +        scope.$digest(); +        expect(select.val()).toEqual('0'); +        expect(select.find('option').length).toEqual(1); +      }); + +      it('should reuse blank option if bound to null', function(){ +        createSingleSelect(true); +        scope.values = [{name:'A'}]; +        scope.selected = null; +        scope.$digest(); +        expect(select.find('option').length).toEqual(2); +        expect(select.val()).toEqual(''); +        expect(jqLite(select.find('option')[0]).val()).toEqual(''); + +        scope.selected = scope.values[0]; +        scope.$digest(); +        expect(select.val()).toEqual('0'); +        expect(select.find('option').length).toEqual(2); +      }); + +      it('should insert a unknown option if bound to something not in the list', function(){ +        createSingleSelect(); +        scope.values = [{name:'A'}]; +        scope.selected = {}; +        scope.$digest(); +        expect(select.find('option').length).toEqual(2); +        expect(select.val()).toEqual('?'); +        expect(jqLite(select.find('option')[0]).val()).toEqual('?'); + +        scope.selected = scope.values[0]; +        scope.$digest(); +        expect(select.val()).toEqual('0'); +        expect(select.find('option').length).toEqual(1); +      }); +    }); + +    describe('on change', function(){ +      it('should update model on change', function(){ +        createSingleSelect(); +        scope.values = [{name:'A'}, {name:'B'}]; +        scope.selected = scope.values[0]; +        scope.$digest(); +        expect(select.val()).toEqual('0'); + +        select.val('1'); +        browserTrigger(select, 'change'); +        expect(scope.selected).toEqual(scope.values[1]); +      }); + +      it('should update model on change through expression', function(){ +        createSelect({'ng:model':'selected', 'ng:options':'item.id as item.name for item in values'}); +        scope.values = [{id:10, name:'A'}, {id:20, name:'B'}]; +        scope.selected = scope.values[0].id; +        scope.$digest(); +        expect(select.val()).toEqual('0'); + +        select.val('1'); +        browserTrigger(select, 'change'); +        expect(scope.selected).toEqual(scope.values[1].id); +      }); + +      it('should update model to null on change', function(){ +        createSingleSelect(true); +        scope.values = [{name:'A'}, {name:'B'}]; +        scope.selected = scope.values[0]; +        select.val('0'); +        scope.$digest(); + +        select.val(''); +        browserTrigger(select, 'change'); +        expect(scope.selected).toEqual(null); +      }); +    }); + +    describe('select-many', function(){ +      it('should read multiple selection', function(){ +        createMultiSelect(); +        scope.values = [{name:'A'}, {name:'B'}]; + +        scope.selected = []; +        scope.$digest(); +        expect(select.find('option').length).toEqual(2); +        expect(jqLite(select.find('option')[0]).attr('selected')).toBeFalsy(); +        expect(jqLite(select.find('option')[1]).attr('selected')).toBeFalsy(); + +        scope.selected.push(scope.values[1]); +        scope.$digest(); +        expect(select.find('option').length).toEqual(2); +        expect(select.find('option')[0].selected).toEqual(false); +        expect(select.find('option')[1].selected).toEqual(true); + +        scope.selected.push(scope.values[0]); +        scope.$digest(); +        expect(select.find('option').length).toEqual(2); +        expect(select.find('option')[0].selected).toEqual(true); +        expect(select.find('option')[1].selected).toEqual(true); +      }); + +      it('should update model on change', function(){ +        createMultiSelect(); +        scope.values = [{name:'A'}, {name:'B'}]; + +        scope.selected = []; +        scope.$digest(); +        select.find('option')[0].selected = true; + +        browserTrigger(select, 'change'); +        expect(scope.selected).toEqual([scope.values[0]]); +      }); +    }); + +  }); + +}); diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 02d0ef71..9361d28d 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -1,7 +1,7 @@  'use strict'; -describe("widget", function() { -  var compile, element, scope; +describe("widget", function(){ +  var compile = null, element = null, scope = null;    beforeEach(function() {      scope = null; @@ -24,397 +24,8 @@ describe("widget", function() {    }); -  describe("input", function() { - -    describe("text", function() { -      it('should input-text auto init and handle keydown/change events', function() { -        compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>'); -        expect(scope.name).toEqual("Misko"); -        expect(scope.count).toEqual(0); - -        scope.name = 'Adam'; -        scope.$digest(); -        expect(element.val()).toEqual("Adam"); - -        element.val('Shyam'); -        browserTrigger(element, 'keydown'); -        // keydown event must be deferred -        expect(scope.name).toEqual('Adam'); -        scope.$service('$browser').defer.flush(); -        expect(scope.name).toEqual('Shyam'); -        expect(scope.count).toEqual(1); - -        element.val('Kai'); -        browserTrigger(element, 'change'); -        expect(scope.name).toEqual('Kai'); -        expect(scope.count).toEqual(2); -      }); - -      it('should not trigger eval if value does not change', function() { -        compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>'); -        expect(scope.name).toEqual("Misko"); -        expect(scope.count).toEqual(0); -        browserTrigger(element, 'keydown'); -        expect(scope.name).toEqual("Misko"); -        expect(scope.count).toEqual(0); -      }); - -      it('should allow complex refernce binding', function() { -        compile('<div ng:init="obj={abc:{}}">'+ -                  '<input type="Text" name="obj[\'abc\'].name" value="Misko""/>'+ -                '</div>'); -        expect(scope.obj['abc'].name).toEqual('Misko'); -      }); - - -      describe("ng:format", function() { -        it("should format text", function() { -          compile('<input type="Text" name="list" value="a,b,c" ng:format="list"/>'); -          expect(scope.list).toEqual(['a', 'b', 'c']); - -          scope.list = ['x', 'y', 'z']; -          scope.$digest(); -          expect(element.val()).toEqual("x, y, z"); - -          element.val('1, 2, 3'); -          browserTrigger(element); -          expect(scope.list).toEqual(['1', '2', '3']); -        }); - -        it("should come up blank if null", function() { -          compile('<input type="text" name="age" ng:format="number" ng:init="age=null"/>'); -          expect(scope.age).toBeNull(); -          expect(scope.$element[0].value).toEqual(''); -        }); - -        it("should show incorect text while number does not parse", function() { -          compile('<input type="text" name="age" ng:format="number"/>'); -          scope.age = 123; -          scope.$digest(); -          scope.$element.val('123X'); -          browserTrigger(scope.$element, 'change'); -          expect(scope.$element.val()).toEqual('123X'); -          expect(scope.age).toEqual(123); -          expect(scope.$element).toBeInvalid(); -        }); - -        it("should clober incorect text if model changes", function() { -          compile('<input type="text" name="age" ng:format="number" value="123X"/>'); -          scope.age = 456; -          scope.$digest(); -          expect(scope.$element.val()).toEqual('456'); -        }); - -        it("should not clober text if model changes due to itself", function() { -          compile('<input type="text" name="list" ng:format="list" value="a"/>'); - -          scope.$element.val('a '); -          browserTrigger(scope.$element, 'change'); -          expect(scope.$element.val()).toEqual('a '); -          expect(scope.list).toEqual(['a']); - -          scope.$element.val('a ,'); -          browserTrigger(scope.$element, 'change'); -          expect(scope.$element.val()).toEqual('a ,'); -          expect(scope.list).toEqual(['a']); - -          scope.$element.val('a , '); -          browserTrigger(scope.$element, 'change'); -          expect(scope.$element.val()).toEqual('a , '); -          expect(scope.list).toEqual(['a']); - -          scope.$element.val('a , b'); -          browserTrigger(scope.$element, 'change'); -          expect(scope.$element.val()).toEqual('a , b'); -          expect(scope.list).toEqual(['a', 'b']); -        }); - -        it("should come up blank when no value specifiend", function() { -          compile('<input type="text" name="age" ng:format="number"/>'); -          scope.$digest(); -          expect(scope.$element.val()).toEqual(''); -          expect(scope.age).toEqual(null); -        }); -      }); - - -      describe("checkbox", function() { -        it("should format booleans", function() { -          compile('<input type="checkbox" name="name" ng:init="name=false"/>'); -          expect(scope.name).toEqual(false); -          expect(scope.$element[0].checked).toEqual(false); -        }); - -        it('should support type="checkbox"', function() { -          compile('<input type="checkBox" name="checkbox" checked ng:change="action = true"/>'); -          expect(scope.checkbox).toEqual(true); -          browserTrigger(element); -          expect(scope.checkbox).toEqual(false); -          expect(scope.action).toEqual(true); -          browserTrigger(element); -          expect(scope.checkbox).toEqual(true); -        }); - -        it("should use ng:format", function() { -          angularFormatter('testFormat', { -            parse: function(value) { -              return value ? "Worked" : "Failed"; -            }, - -            format: function(value) { -              if (value == undefined) return value; -              return value == "Worked"; -            } - -          }); -          compile('<input type="checkbox" name="state" ng:format="testFormat" checked/>'); -          expect(scope.state).toEqual("Worked"); -          expect(scope.$element[0].checked).toEqual(true); - -          browserTrigger(scope.$element); -          expect(scope.state).toEqual("Failed"); -          expect(scope.$element[0].checked).toEqual(false); - -          scope.state = "Worked"; -          scope.$digest(); -          expect(scope.state).toEqual("Worked"); -          expect(scope.$element[0].checked).toEqual(true); -        }); -      }); - - -      describe("ng:validate", function() { -        it("should process ng:validate", function() { -          compile('<input type="text" name="price" value="abc" ng:validate="number"/>', -                  jqLite(document.body)); -          expect(element.hasClass('ng-validation-error')).toBeTruthy(); -          expect(element.attr('ng-validation-error')).toEqual('Not a number'); - -          scope.price =  '123'; -          scope.$digest(); -          expect(element.hasClass('ng-validation-error')).toBeFalsy(); -          expect(element.attr('ng-validation-error')).toBeFalsy(); - -          element.val('x'); -          browserTrigger(element); -          expect(element.hasClass('ng-validation-error')).toBeTruthy(); -          expect(element.attr('ng-validation-error')).toEqual('Not a number'); -        }); - -        it('should not blow up for validation with bound attributes', function() { -          compile('<input type="text" name="price" boo="{{abc}}" ng:required/>'); -          expect(element.hasClass('ng-validation-error')).toBeTruthy(); -          expect(element.attr('ng-validation-error')).toEqual('Required'); - -          scope.price =  '123'; -          scope.$digest(); -          expect(element.hasClass('ng-validation-error')).toBeFalsy(); -          expect(element.attr('ng-validation-error')).toBeFalsy(); -        }); - -        it("should not call validator if undefined/empty", function() { -          var lastValue = "NOT_CALLED"; -          angularValidator.myValidator = function(value) {lastValue = value;}; -          compile('<input type="text" name="url" ng:validate="myValidator"/>'); -          expect(lastValue).toEqual("NOT_CALLED"); - -          scope.url = 'http://server'; -          scope.$digest(); -          expect(lastValue).toEqual("http://server"); - -          delete angularValidator.myValidator; -        }); -      }); -    }); - - -    it("should ignore disabled widgets", function() { -      compile('<input type="text" name="price" ng:required disabled/>'); -      expect(element.hasClass('ng-validation-error')).toBeFalsy(); -      expect(element.attr('ng-validation-error')).toBeFalsy(); -    }); - -    it("should ignore readonly widgets", function() { -      compile('<input type="text" name="price" ng:required readonly/>'); -      expect(element.hasClass('ng-validation-error')).toBeFalsy(); -      expect(element.attr('ng-validation-error')).toBeFalsy(); -    }); - -    it("should process ng:required", function() { -      compile('<input type="text" name="price" ng:required/>', jqLite(document.body)); -      expect(element.hasClass('ng-validation-error')).toBeTruthy(); -      expect(element.attr('ng-validation-error')).toEqual('Required'); - -      scope.price =  'xxx'; -      scope.$digest(); -      expect(element.hasClass('ng-validation-error')).toBeFalsy(); -      expect(element.attr('ng-validation-error')).toBeFalsy(); - -      element.val(''); -      browserTrigger(element); -      expect(element.hasClass('ng-validation-error')).toBeTruthy(); -      expect(element.attr('ng-validation-error')).toEqual('Required'); -    }); - -    it('should allow conditions on ng:required', function() { -      compile('<input type="text" name="price" ng:required="ineedz"/>', -              jqLite(document.body)); -      scope.ineedz =  false; -      scope.$digest(); -      expect(element.hasClass('ng-validation-error')).toBeFalsy(); -      expect(element.attr('ng-validation-error')).toBeFalsy(); - -      scope.price =  'xxx'; -      scope.$digest(); -      expect(element.hasClass('ng-validation-error')).toBeFalsy(); -      expect(element.attr('ng-validation-error')).toBeFalsy(); - -      scope.price =  ''; -      scope.ineedz =  true; -      scope.$digest(); -      expect(element.hasClass('ng-validation-error')).toBeTruthy(); -      expect(element.attr('ng-validation-error')).toEqual('Required'); - -      element.val('abc'); -      browserTrigger(element); -      expect(element.hasClass('ng-validation-error')).toBeFalsy(); -      expect(element.attr('ng-validation-error')).toBeFalsy(); -    }); - -    it("should process ng:required2", function() { -      compile('<textarea name="name">Misko</textarea>'); -      expect(scope.name).toEqual("Misko"); - -      scope.name =  'Adam'; -      scope.$digest(); -      expect(element.val()).toEqual("Adam"); - -      element.val('Shyam'); -      browserTrigger(element); -      expect(scope.name).toEqual('Shyam'); - -      element.val('Kai'); -      browserTrigger(element); -      expect(scope.name).toEqual('Kai'); -    }); - - -    describe('radio', function() { -      it('should support type="radio"', function() { -        compile('<div>' + -            '<input type="radio" name="chose" value="A" ng:change="clicked = 1"/>' + -            '<input type="radio" name="chose" value="B" checked ng:change="clicked = 2"/>' + -            '<input type="radio" name="chose" value="C" ng:change="clicked = 3"/>' + -        '</div>'); -        var a = element[0].childNodes[0]; -        var b = element[0].childNodes[1]; -        expect(b.name.split('@')[1]).toEqual('chose'); -        expect(scope.chose).toEqual('B'); -        scope.chose = 'A'; -        scope.$digest(); -        expect(a.checked).toEqual(true); - -        scope.chose = 'B'; -        scope.$digest(); -        expect(a.checked).toEqual(false); -        expect(b.checked).toEqual(true); -        expect(scope.clicked).not.toBeDefined(); - -        browserTrigger(a); -        expect(scope.chose).toEqual('A'); -        expect(scope.clicked).toEqual(1); -      }); - -      it('should honor model over html checked keyword after', function() { -        compile('<div ng:init="choose=\'C\'">' + -            '<input type="radio" name="choose" value="A""/>' + -            '<input type="radio" name="choose" value="B" checked/>' + -            '<input type="radio" name="choose" value="C"/>' + -        '</div>'); - -        expect(scope.choose).toEqual('C'); -      }); - -      it('should honor model over html checked keyword before', function() { -        compile('<div ng:init="choose=\'A\'">' + -            '<input type="radio" name="choose" value="A""/>' + -            '<input type="radio" name="choose" value="B" checked/>' + -            '<input type="radio" name="choose" value="C"/>' + -        '</div>'); - -        expect(scope.choose).toEqual('A'); -      }); - -    }); - - -    describe('select-one', function() { -      it('should initialize to selected', function() { -        compile( -            '<select name="selection">' + -                '<option>A</option>' + -                '<option selected>B</option>' + -            '</select>'); -        expect(scope.selection).toEqual('B'); -        scope.selection = 'A'; -        scope.$digest(); -        expect(scope.selection).toEqual('A'); -        expect(element[0].childNodes[0].selected).toEqual(true); -      }); - -      it('should compile children of a select without a name, but not create a model for it', -          function() { -        compile('<select>' + -                  '<option selected="true">{{a}}</option>' + -                  '<option value="">{{b}}</option>' + -                  '<option>C</option>' + -                '</select>'); -        scope.a = 'foo'; -        scope.b = 'bar'; -        scope.$digest(); - -        expect(scope.$element.text()).toBe('foobarC'); -      }); -    }); - - -    describe('select-multiple', function() { -      it('should support type="select-multiple"', function() { -        compile('<select name="selection" multiple>' + -                  '<option>A</option>' + -                  '<option selected>B</option>' + -                '</select>'); -        expect(scope.selection).toEqual(['B']); -        scope.selection = ['A']; -        scope.$digest(); -        expect(element[0].childNodes[0].selected).toEqual(true); -      }); -    }); - - -    it('should ignore text widget which have no name', function() { -      compile('<input type="text"/>'); -      expect(scope.$element.attr('ng-exception')).toBeFalsy(); -      expect(scope.$element.hasClass('ng-exception')).toBeFalsy(); -    }); - -    it('should ignore checkbox widget which have no name', function() { -      compile('<input type="checkbox"/>'); -      expect(scope.$element.attr('ng-exception')).toBeFalsy(); -      expect(scope.$element.hasClass('ng-exception')).toBeFalsy(); -    }); - -    it('should report error on assignment error', function() { -      expect(function() { -        compile('<input type="text" name="throw \'\'" value="x"/>'); -      }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at ['']."); -      $logMock.error.logs.shift(); -    }); -  }); - - -  describe('ng:switch', function() { -    it('should switch on value change', function() { +  describe('ng:switch', function(){ +    it('should switch on value change', function(){        compile('<ng:switch on="select">' +            '<div ng:switch-when="1">first:{{name}}</div>' +            '<div ng:switch-when="2">second:{{name}}</div>' + @@ -458,6 +69,7 @@ describe("widget", function() {        expect(scope.$element.text()).toEqual('works');        dealoc(scope);      }); +    }); @@ -577,428 +189,6 @@ describe("widget", function() {    }); -  describe('ng:options', function() { -    var select, scope; - -    function createSelect(attrs, blank, unknown) { -      var html = '<select'; -      forEach(attrs, function(value, key) { -        if (isBoolean(value)) { -          if (value) html += ' ' + key; -        } else { -          html+= ' ' + key + '="' + value + '"'; -        } -      }); -      html += '>' + -        (blank ? '<option value="">blank</option>' : '') + -        (unknown ? '<option value="?">unknown</option>' : '') + -      '</select>'; -      select = jqLite(html); -      scope = compile(select); -    } - -    function createSingleSelect(blank, unknown) { -      createSelect({ -        'name':'selected', -        'ng:options':'value.name for value in values' -      }, blank, unknown); -    } - -    function createMultiSelect(blank, unknown) { -      createSelect({ -        'name':'selected', -        'multiple':true, -        'ng:options':'value.name for value in values' -      }, blank, unknown); -    } - -    afterEach(function() { -      dealoc(select); -      dealoc(scope); -    }); - -    it('should throw when not formated "? for ? in ?"', function() { -      expect(function() { -        compile('<select name="selected" ng:options="i dont parse"></select>'); -      }).toThrow("Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in" + -                 " _collection_' but got 'i dont parse'."); -    }); - -    it('should render a list', function() { -      createSingleSelect(); -      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; -      scope.selected = scope.values[0]; -      scope.$digest(); -      var options = select.find('option'); -      expect(options.length).toEqual(3); -      expect(sortedHtml(options[0])).toEqual('<option value="0">A</option>'); -      expect(sortedHtml(options[1])).toEqual('<option value="1">B</option>'); -      expect(sortedHtml(options[2])).toEqual('<option value="2">C</option>'); -    }); - -    it('should render an object', function() { -      createSelect({ -        'name':'selected', -        'ng:options': 'value as key for (key, value) in object' -      }); -      scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; -      scope.selected = scope.object.red; -      scope.$digest(); -      var options = select.find('option'); -      expect(options.length).toEqual(3); -      expect(sortedHtml(options[0])).toEqual('<option value="blue">blue</option>'); -      expect(sortedHtml(options[1])).toEqual('<option value="green">green</option>'); -      expect(sortedHtml(options[2])).toEqual('<option value="red">red</option>'); -      expect(options[2].selected).toEqual(true); - -      scope.object.azur = '8888FF'; -      scope.$digest(); -      options = select.find('option'); -      expect(options[3].selected).toEqual(true); -    }); - -    it('should grow list', function() { -      createSingleSelect(); -      scope.values = []; -      scope.$digest(); -      expect(select.find('option').length).toEqual(1); // because we add special empty option -      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="?"></option>'); - -      scope.values.push({name:'A'}); -      scope.selected = scope.values[0]; -      scope.$digest(); -      expect(select.find('option').length).toEqual(1); -      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); - -      scope.values.push({name:'B'}); -      scope.$digest(); -      expect(select.find('option').length).toEqual(2); -      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); -      expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>'); -    }); - -    it('should shrink list', function() { -      createSingleSelect(); -      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; -      scope.selected = scope.values[0]; -      scope.$digest(); -      expect(select.find('option').length).toEqual(3); - -      scope.values.pop(); -      scope.$digest(); -      expect(select.find('option').length).toEqual(2); -      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); -      expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>'); - -      scope.values.pop(); -      scope.$digest(); -      expect(select.find('option').length).toEqual(1); -      expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>'); - -      scope.values.pop(); -      scope.selected = null; -      scope.$digest(); -      expect(select.find('option').length).toEqual(1); // we add back the special empty option -    }); - -    it('should shrink and then grow list', function() { -      createSingleSelect(); -      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; -      scope.selected = scope.values[0]; -      scope.$digest(); -      expect(select.find('option').length).toEqual(3); - -      scope.values = [{name:'1'}, {name:'2'}]; -      scope.selected = scope.values[0]; -      scope.$digest(); -      expect(select.find('option').length).toEqual(2); - -      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; -      scope.selected = scope.values[0]; -      scope.$digest(); -      expect(select.find('option').length).toEqual(3); -    }); - -    it('should update list', function() { -      createSingleSelect(); -      scope.values = [{name:'A'}, {name:'B'}, {name:'C'}]; -      scope.selected = scope.values[0]; -      scope.$digest(); - -      scope.values = [{name:'B'}, {name:'C'}, {name:'D'}]; -      scope.selected = scope.values[0]; -      scope.$digest(); -      var options = select.find('option'); -      expect(options.length).toEqual(3); -      expect(sortedHtml(options[0])).toEqual('<option value="0">B</option>'); -      expect(sortedHtml(options[1])).toEqual('<option value="1">C</option>'); -      expect(sortedHtml(options[2])).toEqual('<option value="2">D</option>'); -    }); - -    it('should preserve existing options', function() { -      createSingleSelect(true); - -      scope.$digest(); -      expect(select.find('option').length).toEqual(1); - -      scope.values = [{name:'A'}]; -      scope.selected = scope.values[0]; -      scope.$digest(); -      expect(select.find('option').length).toEqual(2); -      expect(jqLite(select.find('option')[0]).text()).toEqual('blank'); -      expect(jqLite(select.find('option')[1]).text()).toEqual('A'); - -      scope.values = []; -      scope.selected = null; -      scope.$digest(); -      expect(select.find('option').length).toEqual(1); -      expect(jqLite(select.find('option')[0]).text()).toEqual('blank'); -    }); - - -    describe('binding', function() { -      it('should bind to scope value', function() { -        createSingleSelect(); -        scope.values = [{name:'A'}, {name:'B'}]; -        scope.selected = scope.values[0]; -        scope.$digest(); -        expect(select.val()).toEqual('0'); - -        scope.selected = scope.values[1]; -        scope.$digest(); -        expect(select.val()).toEqual('1'); -      }); - - -      it('should bind to scope value and group', function() { -        createSelect({ -          'name':'selected', -          'ng:options':'item.name group by item.group for item in values' -        }); -        scope.values = [{name:'A'}, -                        {name:'B', group:'first'}, -                        {name:'C', group:'second'}, -                        {name:'D', group:'first'}, -                        {name:'E', group:'second'}]; -        scope.selected = scope.values[3]; -        scope.$digest(); -        expect(select.val()).toEqual('3'); - -        var first = jqLite(select.find('optgroup')[0]); -        var b = jqLite(first.find('option')[0]); -        var d = jqLite(first.find('option')[1]); -        expect(first.attr('label')).toEqual('first'); -        expect(b.text()).toEqual('B'); -        expect(d.text()).toEqual('D'); - -        var second = jqLite(select.find('optgroup')[1]); -        var c = jqLite(second.find('option')[0]); -        var e = jqLite(second.find('option')[1]); -        expect(second.attr('label')).toEqual('second'); -        expect(c.text()).toEqual('C'); -        expect(e.text()).toEqual('E'); - -        scope.selected = scope.values[0]; -        scope.$digest(); -        expect(select.val()).toEqual('0'); -      }); - -      it('should bind to scope value through experession', function() { -        createSelect({'name':'selected', 'ng:options':'item.id as item.name for item in values'}); -        scope.values = [{id:10, name:'A'}, {id:20, name:'B'}]; -        scope.selected = scope.values[0].id; -        scope.$digest(); -        expect(select.val()).toEqual('0'); - -        scope.selected = scope.values[1].id; -        scope.$digest(); -        expect(select.val()).toEqual('1'); -      }); - -      it('should bind to object key', function() { -        createSelect({ -          'name':'selected', -          'ng:options':'key as value for (key, value) in object' -        }); -        scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; -        scope.selected = 'green'; -        scope.$digest(); -        expect(select.val()).toEqual('green'); - -        scope.selected = 'blue'; -        scope.$digest(); -        expect(select.val()).toEqual('blue'); -      }); - -      it('should bind to object value', function() { -        createSelect({ -          name:'selected', -          'ng:options':'value as key for (key, value) in object' -        }); -        scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; -        scope.selected = '00FF00'; -        scope.$digest(); -        expect(select.val()).toEqual('green'); - -        scope.selected = '0000FF'; -        scope.$digest(); -        expect(select.val()).toEqual('blue'); -      }); - -      it('should insert a blank option if bound to null', function() { -        createSingleSelect(); -        scope.values = [{name:'A'}]; -        scope.selected = null; -        scope.$digest(); -        expect(select.find('option').length).toEqual(2); -        expect(select.val()).toEqual(''); -        expect(jqLite(select.find('option')[0]).val()).toEqual(''); - -        scope.selected = scope.values[0]; -        scope.$digest(); -        expect(select.val()).toEqual('0'); -        expect(select.find('option').length).toEqual(1); -      }); - -      it('should reuse blank option if bound to null', function() { -        createSingleSelect(true); -        scope.values = [{name:'A'}]; -        scope.selected = null; -        scope.$digest(); -        expect(select.find('option').length).toEqual(2); -        expect(select.val()).toEqual(''); -        expect(jqLite(select.find('option')[0]).val()).toEqual(''); - -        scope.selected = scope.values[0]; -        scope.$digest(); -        expect(select.val()).toEqual('0'); -        expect(select.find('option').length).toEqual(2); -      }); - -      it('should insert a unknown option if bound to something not in the list', function() { -        createSingleSelect(); -        scope.values = [{name:'A'}]; -        scope.selected = {}; -        scope.$digest(); -        expect(select.find('option').length).toEqual(2); -        expect(select.val()).toEqual('?'); -        expect(jqLite(select.find('option')[0]).val()).toEqual('?'); - -        scope.selected = scope.values[0]; -        scope.$digest(); -        expect(select.val()).toEqual('0'); -        expect(select.find('option').length).toEqual(1); -      }); -    }); - - -    describe('on change', function() { -      it('should update model on change', function() { -        createSingleSelect(); -        scope.values = [{name:'A'}, {name:'B'}]; -        scope.selected = scope.values[0]; -        scope.$digest(); -        expect(select.val()).toEqual('0'); - -        select.val('1'); -        browserTrigger(select, 'change'); -        expect(scope.selected).toEqual(scope.values[1]); -      }); - -      it('should fire ng:change if present', function() { -        createSelect({ -          name:'selected', -          'ng:options':'value for value in values', -          'ng:change':'log = log + selected.name' -        }); -        scope.values = [{name:'A'}, {name:'B'}]; -        scope.selected = scope.values[0]; -        scope.log = ''; -        scope.$digest(); -        expect(scope.log).toEqual(''); - -        select.val('1'); -        browserTrigger(select, 'change'); -        expect(scope.log).toEqual('B'); -        expect(scope.selected).toEqual(scope.values[1]); - -        // ignore change event when the model doesn't change -        browserTrigger(select, 'change'); -        expect(scope.log).toEqual('B'); -        expect(scope.selected).toEqual(scope.values[1]); - -        select.val('0'); -        browserTrigger(select, 'change'); -        expect(scope.log).toEqual('BA'); -        expect(scope.selected).toEqual(scope.values[0]); -      }); - -      it('should update model on change through expression', function() { -        createSelect({name:'selected', 'ng:options':'item.id as item.name for item in values'}); -        scope.values = [{id:10, name:'A'}, {id:20, name:'B'}]; -        scope.selected = scope.values[0].id; -        scope.$digest(); -        expect(select.val()).toEqual('0'); - -        select.val('1'); -        browserTrigger(select, 'change'); -        expect(scope.selected).toEqual(scope.values[1].id); -      }); - -      it('should update model to null on change', function() { -        createSingleSelect(true); -        scope.values = [{name:'A'}, {name:'B'}]; -        scope.selected = scope.values[0]; -        select.val('0'); -        scope.$digest(); - -        select.val(''); -        browserTrigger(select, 'change'); -        expect(scope.selected).toEqual(null); -      }); -    }); - - -    describe('select-many', function() { -      it('should read multiple selection', function() { -        createMultiSelect(); -        scope.values = [{name:'A'}, {name:'B'}]; - -        scope.selected = []; -        scope.$digest(); -        expect(select.find('option').length).toEqual(2); -        expect(select.find('option')[0].selected).toBe(false); -        expect(select.find('option')[1].selected).toBe(false); - -        scope.selected.push(scope.values[1]); -        scope.$digest(); -        expect(select.find('option').length).toEqual(2); -        expect(select.find('option')[0].selected).toEqual(false); -        expect(select.find('option')[1].selected).toEqual(true); - -        scope.selected.push(scope.values[0]); -        scope.$digest(); -        expect(select.find('option').length).toEqual(2); -        expect(select.find('option')[0].selected).toEqual(true); -        expect(select.find('option')[1].selected).toEqual(true); -      }); - -      it('should update model on change', function() { -        createMultiSelect(); -        scope.values = [{name:'A'}, {name:'B'}]; - -        scope.selected = []; -        scope.$digest(); -        select.find('option')[0].selected = true; - -        browserTrigger(select, 'change'); -        expect(scope.selected).toEqual([scope.values[0]]); -      }); -    }); - -  }); - -    describe('@ng:repeat', function() {      it('should ng:repeat over array', function() {        var scope = compile('<ul><li ng:repeat="item in items" ng:init="suffix = \';\'" ng:bind="item + suffix"></li></ul>'); | 
