aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Rakefile61
-rw-r--r--angularFiles.js8
-rw-r--r--css/angular.css9
-rw-r--r--docs/content/api/angular.inputType.ngdoc92
-rw-r--r--docs/content/api/angular.service.ngdoc2
-rw-r--r--docs/content/api/index.ngdoc2
-rw-r--r--docs/content/cookbook/advancedform.ngdoc55
-rw-r--r--docs/content/cookbook/buzz.ngdoc3
-rw-r--r--docs/content/cookbook/form.ngdoc37
-rw-r--r--docs/content/cookbook/helloworld.ngdoc13
-rw-r--r--docs/content/guide/dev_guide.compiler.directives.ngdoc2
-rw-r--r--docs/content/guide/dev_guide.expressions.ngdoc27
-rw-r--r--docs/content/guide/dev_guide.forms.ngdoc610
-rw-r--r--docs/content/guide/dev_guide.mvc.understanding_controller.ngdoc4
-rw-r--r--docs/content/guide/dev_guide.mvc.understanding_model.ngdoc2
-rw-r--r--docs/content/guide/dev_guide.overview.ngdoc44
-rw-r--r--docs/content/guide/dev_guide.services.$location.ngdoc2
-rw-r--r--docs/content/guide/dev_guide.services.injecting_controllers.ngdoc4
-rw-r--r--docs/content/guide/dev_guide.templates.css-styling.ngdoc52
-rw-r--r--docs/content/guide/dev_guide.templates.filters.creating_filters.ngdoc26
-rw-r--r--docs/content/guide/dev_guide.templates.formatters.creating_formatters.ngdoc55
-rw-r--r--docs/content/guide/dev_guide.templates.formatters.ngdoc20
-rw-r--r--docs/content/guide/dev_guide.templates.formatters.using_formatters.ngdoc9
-rw-r--r--docs/content/guide/dev_guide.templates.ngdoc9
-rw-r--r--docs/content/guide/dev_guide.templates.validators.creating_validators.ngdoc82
-rw-r--r--docs/content/guide/dev_guide.templates.validators.ngdoc131
-rw-r--r--docs/content/guide/index.ngdoc3
-rw-r--r--docs/content/misc/started.ngdoc2
-rw-r--r--docs/content/tutorial/step_03.ngdoc2
-rw-r--r--docs/content/tutorial/step_04.ngdoc4
-rw-r--r--docs/content/tutorial/step_07.ngdoc4
-rw-r--r--docs/content/tutorial/step_09.ngdoc2
-rw-r--r--docs/examples/settings.html8
-rw-r--r--docs/img/form_data_flow.pngbin0 -> 55400 bytes
-rw-r--r--docs/spec/ngdocSpec.js36
-rw-r--r--docs/src/ngdoc.js104
-rw-r--r--docs/src/templates/doc_widgets.js8
-rw-r--r--docs/src/templates/docs.css12
-rw-r--r--docs/src/templates/index.html2
-rw-r--r--example/buzz/buzz.html4
-rw-r--r--example/personalLog/personalLog.html2
-rw-r--r--example/tweeter/tweeter_addressbook.html12
-rw-r--r--example/tweeter/tweeter_demo.html2
-rwxr-xr-xgen_docs.sh2
-rw-r--r--i18n/e2e/localeTest_cs.html9
-rw-r--r--i18n/e2e/localeTest_de.html9
-rw-r--r--i18n/e2e/localeTest_en.html21
-rw-r--r--i18n/e2e/localeTest_es.html9
-rw-r--r--i18n/e2e/localeTest_sk.html12
-rw-r--r--i18n/e2e/localeTest_zh.html21
-rw-r--r--images/docs/guide/form_data_flow.graffle2301
-rw-r--r--regression/filter_repeater.html2
-rw-r--r--regression/issue-169.html4
-rw-r--r--regression/issue-352.html6
-rw-r--r--regression/issue-353.html2
-rw-r--r--regression/sanitizer.html2
-rw-r--r--src/Angular.js105
-rw-r--r--src/Browser.js10
-rw-r--r--src/Scope.js3
-rw-r--r--src/angular-bootstrap.js3
-rw-r--r--src/apis.js173
-rw-r--r--src/directives.js86
-rw-r--r--src/filters.js113
-rw-r--r--src/formatters.js202
-rw-r--r--src/jqLite.js15
-rw-r--r--src/markups.js24
-rw-r--r--src/parser.js43
-rw-r--r--src/scenario/Scenario.js2
-rw-r--r--src/scenario/dsl.js22
-rw-r--r--src/service/formFactory.js394
-rw-r--r--src/service/invalidWidgets.js69
-rw-r--r--src/service/log.js3
-rw-r--r--src/service/resource.js3
-rw-r--r--src/service/route.js6
-rw-r--r--src/service/window.js2
-rw-r--r--src/service/xhr.js5
-rw-r--r--src/validators.js482
-rw-r--r--src/widget/form.js81
-rw-r--r--src/widget/input.js773
-rw-r--r--src/widget/select.js427
-rw-r--r--src/widgets.js1033
-rw-r--r--test/AngularSpec.js39
-rw-r--r--test/ApiSpecs.js7
-rw-r--r--test/BinderSpec.js135
-rw-r--r--test/BrowserSpecs.js10
-rw-r--r--test/FormattersSpec.js45
-rw-r--r--test/JsonSpec.js4
-rw-r--r--test/ParserSpec.js19
-rw-r--r--test/ScopeSpec.js19
-rw-r--r--test/ValidatorsSpec.js172
-rw-r--r--test/directivesSpec.js11
-rw-r--r--test/jQueryPatchSpec.js57
-rw-r--r--test/jqLiteSpec.js32
-rw-r--r--test/markupSpec.js20
-rw-r--r--test/scenario/dslSpec.js63
-rw-r--r--test/scenario/e2e/widgets.html18
-rw-r--r--test/service/formFactorySpec.js218
-rw-r--r--test/service/invalidWidgetsSpec.js41
-rw-r--r--test/service/routeSpec.js20
-rw-r--r--test/testabilityPatch.js70
-rw-r--r--test/widget/formSpec.js97
-rw-r--r--test/widget/inputSpec.js547
-rw-r--r--test/widget/selectSpec.js510
-rw-r--r--test/widgetsSpec.js820
104 files changed, 7051 insertions, 3970 deletions
diff --git a/Rakefile b/Rakefile
index 0c9efcf9..e71fd0cf 100644
--- a/Rakefile
+++ b/Rakefile
@@ -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 &lt;form&gt;} 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>&lt;input type="text" ng:model="input1"&gt;</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>&lt;textarea ng:model="input2"&gt;&lt;/textarea&gt;</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>
+ &lt;input type="radio" ng:model="input3" value="A"&gt;<br>
+ &lt;input type="radio" ng:model="input3" value="B"&gt;
+ </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>&lt;input type="checkbox" ng:model="input4"&gt;</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>
+ &lt;select ng:model="input5"&gt;<br>
+ &nbsp;&nbsp;&lt;option value="c"&gt;C&lt;/option&gt;<br>
+ &nbsp;&nbsp;&lt;option value="d"&gt;D&lt;/option&gt;<br>
+ &lt;/select&gt;<br>
+ </tt></td>
+ <td>
+ <select 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>
+ &lt;select ng:model="input6" multiple size="4"&gt;<br>
+ &nbsp;&nbsp;&lt;option value="e"&gt;E&lt;/option&gt;<br>
+ &nbsp;&nbsp;&lt;option value="f"&gt;F&lt;/option&gt;<br>
+ &lt;/select&gt;<br>
+ </tt></td>
+ <td>
+ <select 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.png
new file mode 100644
index 00000000..60e947a5
--- /dev/null
+++ b/docs/img/form_data_flow.png
Binary files differ
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>&lt;angular/&gt; 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=">&lt;&lt; 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=">&lt;&lt; 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
+ dUvz05OAq2lvVlXdvZ0ll4Vf97/mzkFxTY/1qX1pRWJpdGL6b6WVtU1tmn6AnVnQraxt
+ bG0j6bt37zE+0BxkEu/e7e3t7mxvbayt6BZmALdf09ZUW1n6W3pitPSguEdSmUpkG6ha
+ kDZCJs8pUtQ0PO7UDgPsom4VdEXUA5R/0xj7t/o9AoO+q7pFwB3Wdj5uqFEU5chlESAu
+ VK7N8alMEtnK1pHr6UOkLShTqtRPe4f+mpheANitHQPrEch/Ti2ODI3UyLuzBbgL0xN/
+ DfU+VauUZQWUuJ5cR1urY1P5kjF6lL2Tq8BXAlWbXVT+sLFNMzA6PjUPsNs7el0PDnhq
+ lIcGOjgHPe/ONuDOT42PDmjaGh+UF2VD5Up8Ba5O9uhTRxcYpGzBo7DXRibcykdpO/og
+ j+eWVlBZksQHx/n770OTOLX/fjoLSl5Qd2VpDnK5rwPFzb+VEIk9F3yKpPLhwkVHJokM
+ HhWTnFWoeADSDo6OQx6vbR4De2psxw50EFiPu7kGuTw+OojiKgqzkmOkYtHHVD5EC2Vr
+ ZgnN1lXgFxQuS80rqXzU3NGL0r6FPN49rOyxUzjVHx7gJbi7kMtvUdzejuZHlSV5qbLw
+ ID9IZTtrS7MjPQjK1uyKNdsFmi0mcsGdPxpau/spaSGP0YsN1z9VqC8OZpgR4oJTUeL2
+ d7c2/HGnAFMZWq4L2/oKLKcOPRkQk8Ky9QZHTsqERG560jM49np2cWVje+/dmYT9558D
+ uO/2tjdWFmdfjw32PGmCVM5MgpbrrS/cI7SwbLwK/YcvCgy9mZJdfJ8k8ss3c7pVqFqQ
+ 1nDhL97sU/+lYVZE3M1V3dyblySV7xdnp9wMDRTxoQddhcXjIW2NL6NJ6ftPal6psq6l
+ S/scPWp9axekNVz21Hm+MqBhXh8+vNvdWkefeq7tbKlTlmLhkh6EHffyoQcDpEWT8vK/
+ fiM+Lf9OdUMrOPLEDEnkMyvtwVx+T1J5ZgJcubWh+k5+WvyN6/5eaFPH0KIl2zvDspGY
+ 1L2aRizbydklcGRIZP0t/MqNpuXX+qmBT4ErL81OYuE21tzT25Sbsz0x5U9bEDQgYsnC
+ gJCoxIzb5Q+b2nufkf4DZWtIZFpovjoohfvhw97OJulBz3rbmx6W385IjAoJEFKmbHI4
+ k4HWhrLkGLDkito/n/YOv3wzr1vbL9uvjkvTHxBcUrhrunm0qad/1laAKcP6Ak3Z5go0
+ 3MPamhNaeCSAlRRYsrqzb+TVFCykkJZcjyaWEwxL4YJNQcedejXS16l+dL+YrKZ8CK35
+ EVp4JmA5cOABiDSgSpW6UzsClry8vr2rL9sTDEvTn1C073e315fBlEe0nWpVJWlBYh9P
+ jgOulA9rS2i5nqLAsNiU7BKlimpAi0BrMCmaUE4wrJ52D2gXsQV1taiUJdkpsWGBsFL+
+ DO1VlgMsLiRhsfIc0m5x3bi4vG/JJxiWpj/5SLuBtKP9Xdhwc+SxYRJYXjiwYHlxVFtq
+ KYUPt7mlyvrHZJWsb7d4OZpITjQszo+0oBVC2/24Xlmai4+4hsXUsbQ80BZpy6ooWsPi
+ 4geinQFtgbaqTE/LI0vH42kFn9B+XEqdcWnJkkqvLSymDtIKLmiN4FMpqFtmZTJ6MlNc
+ ilEdCNZSDFpdMGnlaMKopwKGPfEx7GmeSZ/UMOtTOEZ9wnqJUZ+eM+ybEYZ968WkbzSN
+ GPVttRGz3kRg1lsm5/ANIu+Pr10c+vrWyIhZb4dRhUte6mTAm38gLpPe6mTWG7tGjHob
+ G1OZvI7NjDftMZVxNxAzdlEYxP10h0zFj7RDpuLkO2RIy8VdfMzY/URwGbOzDWjRqI7u
+ Wiw/l7sWP+YybKw+/ztSUVwG7Tamclm/bf7c7yTHRyEoXaacEkCe/MCpGHIChBGzTvfY
+ x2XCyS2kdOEIos+dyhN9lk7lif7Pp/IALjwfMObEJYILzkydpgUnh/04p2lRxy1922la
+ 8OYyLjNQXiaclEbhgrzkFDw88O98n4JHZTPIS536d95POER5qXQG3vN/eiVuOzDwMuFk
+ UgMv+pX+LNZzfeosxQsJbcyME4WRl8poCvm8nxZN8SIxVvHHoPkgcDwr2RA4M8Msv/u/
+ ePGzEt8d7uKCF3fg4g5c3IGLO3B6d+B/bC6ruAplbmRzdHJlYW0KZW5kb2JqCjIwIDAg
+ b2JqCjMzNzcKZW5kb2JqCjIzIDAgb2JqCjw8IC9MZW5ndGggMjQgMCBSIC9OIDMgL0Fs
+ dGVybmF0ZSAvRGV2aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4
+ AZ2Wd1RT2RaHz703vdASIiAl9Bp6CSDSO0gVBFGJSYBQAoaEJnZEBUYUESlWZFTAAUeH
+ ImNFFAuDgmLXCfIQUMbBUURF5d2MawnvrTXz3pr9x1nf2ee319ln733XugBQ/IIEwnRY
+ AYA0oVgU7uvBXBITy8T3AhgQAQ5YAcDhZmYER/hEAtT8vT2ZmahIxrP27i6AZLvbLL9Q
+ JnPW/3+RIjdDJAYACkXVNjx+JhflApRTs8UZMv8EyvSVKTKGMTIWoQmirCLjxK9s9qfm
+ K7vJmJcm5KEaWc4ZvDSejLtQ3pol4aOMBKFcmCXgZ6N8B2W9VEmaAOX3KNPT+JxMADAU
+ mV/M5yahbIkyRRQZ7onyAgAIlMQ5vHIOi/k5aJ4AeKZn5IoEiUliphHXmGnl6Mhm+vGz
+ U/liMSuUw03hiHhMz/S0DI4wF4Cvb5ZFASVZbZloke2tHO3tWdbmaPm/2d8eflP9Pch6
+ +1XxJuzPnkGMnlnfbOysL70WAPYkWpsds76VVQC0bQZA5eGsT+8gAPIFALTenPMehmxe
+ ksTiDCcLi+zsbHMBn2suK+g3+5+Cb8q/hjn3mcvu+1Y7phc/gSNJFTNlReWmp6ZLRMzM
+ DA6Xz2T99xD/48A5ac3Jwyycn8AX8YXoVVHolAmEiWi7hTyBWJAuZAqEf9Xhfxg2JwcZ
+ fp1rFGh1XwB9hTlQuEkHyG89AEMjAyRuP3oCfetbEDEKyL68aK2Rr3OPMnr+5/ofC1yK
+ buFMQSJT5vYMj2RyJaIsGaPfhGzBAhKQB3SgCjSBLjACLGANHIAzcAPeIACEgEgQA5YD
+ LkgCaUAEskE+2AAKQTHYAXaDanAA1IF60AROgjZwBlwEV8ANcAsMgEdACobBSzAB3oFp
+ CILwEBWiQaqQFqQPmULWEBtaCHlDQVA4FAPFQ4mQEJJA+dAmqBgqg6qhQ1A99CN0GroI
+ XYP6oAfQIDQG/QF9hBGYAtNhDdgAtoDZsDscCEfCy+BEeBWcBxfA2+FKuBY+DrfCF+Eb
+ 8AAshV/CkwhAyAgD0UZYCBvxREKQWCQBESFrkSKkAqlFmpAOpBu5jUiRceQDBoehYZgY
+ FsYZ44dZjOFiVmHWYkow1ZhjmFZMF+Y2ZhAzgfmCpWLVsaZYJ6w/dgk2EZuNLcRWYI9g
+ 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
+ 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
+ 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">
- &lt;p style="color:blue"&gt;an html
- &lt;em onmouseover="this.textContent='PWN3D!'"&gt;click here&lt;/em&gt;
- snippet&lt;/p&gt;</textarea>
- <table>
- <tr>
- <td>Filter</td>
- <td>Source</td>
- <td>Rendered</td>
- </tr>
- <tr id="html-filter">
- <td>html filter</td>
- <td>
- <pre>&lt;div ng:bind="snippet | html"&gt;<br/>&lt;/div&gt;</pre>
- </td>
- <td>
- <div ng:bind="snippet | html"></div>
- </td>
- </tr>
- <tr id="escaped-html">
- <td>no filter</td>
- <td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
- <td><div ng:bind="snippet"></div></td>
- </tr>
- <tr id="html-unsafe-filter">
- <td>unsafe html filter</td>
- <td><pre>&lt;div ng:bind="snippet | html:'unsafe'"&gt;<br/>&lt;/div&gt;</pre></td>
- <td><div ng:bind="snippet | html:'unsafe'"></div></td>
- </tr>
- </table>
+ <script>
+ function Ctrl(){
+ this.snippet =
+ '<p style="color:blue">an html\n' +
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
+ 'snippet</p>';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
+ <table>
+ <tr>
+ <td>Filter</td>
+ <td>Source</td>
+ <td>Rendered</td>
+ </tr>
+ <tr id="html-filter">
+ <td>html filter</td>
+ <td>
+ <pre>&lt;div ng:bind="snippet | html"&gt;<br/>&lt;/div&gt;</pre>
+ </td>
+ <td>
+ <div ng:bind="snippet | html"></div>
+ </td>
+ </tr>
+ <tr id="escaped-html">
+ <td>no filter</td>
+ <td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
+ <td><div ng:bind="snippet"></div></td>
+ </tr>
+ <tr id="html-unsafe-filter">
+ <td>unsafe html filter</td>
+ <td><pre>&lt;div ng:bind="snippet | html:'unsafe'"&gt;<br/>&lt;/div&gt;</pre></td>
+ <td><div ng:bind="snippet | html:'unsafe'"></div></td>
+ </tr>
+ </table>
+ </div>
</doc:source>
<doc:scenario>
it('should sanitize the html snippet ', function(){
@@ -543,12 +564,18 @@ angularFilter.html = function(html, option){
* @example
<doc:example>
<doc:source>
- Snippet: <textarea name="snippet" cols="60" rows="3">
- Pretty text with some links:
- http://angularjs.org/,
- mailto:us@somewhere.org,
- another@somewhere.org,
- and one more: ftp://127.0.0.1/.</textarea>
+ <script>
+ function Ctrl(){
+ this.snippet =
+ 'Pretty text with some links:\n'+
+ 'http://angularjs.org/,\n'+
+ 'mailto:us@somewhere.org,\n'+
+ 'another@somewhere.org,\n'+
+ 'and one more: ftp://127.0.0.1/.';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
<td>Filter</td>
diff --git a/src/formatters.js b/src/formatters.js
deleted file mode 100644
index 2fadc9d7..00000000
--- a/src/formatters.js
+++ /dev/null
@@ -1,202 +0,0 @@
-'use strict';
-
-/**
- * @workInProgress
- * @ngdoc overview
- * @name angular.formatter
- * @description
- *
- * Formatters are used for translating data formats between those used for display and those used
- * for storage.
- *
- * Following is the list of built-in angular formatters:
- *
- * * {@link angular.formatter.boolean boolean} - Formats user input in boolean format
- * * {@link angular.formatter.json json} - Formats user input in JSON format
- * * {@link angular.formatter.list list} - Formats user input string as an array
- * * {@link angular.formatter.number number} - Formats user input strings as a number
- * * {@link angular.formatter.trim trim} - Trims extras spaces from end of user input
- *
- * For more information about how angular formatters work, and how to create your own formatters,
- * see {@link guide/dev_guide.templates.formatters Understanding Angular Formatters} in the angular
- * Developer Guide.
- */
-
-function formatter(format, parse) {return {'format':format, 'parse':parse || format};}
-function toString(obj) {
- return (isDefined(obj) && obj !== null) ? "" + obj : obj;
-}
-
-var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/;
-
-angularFormatter.noop = formatter(identity, identity);
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.json
- *
- * @description
- * Formats the user input as JSON text.
- *
- * @returns {?string} A JSON string representation of the model.
- *
- * @example
- <doc:example>
- <doc:source>
- <div ng:init="data={name:'misko', project:'angular'}">
- <input type="text" size='50' name="data" ng:format="json"/>
- <pre>data={{data}}</pre>
- </div>
- </doc:source>
- <doc:scenario>
- it('should format json', function(){
- expect(binding('data')).toEqual('data={\n \"name\":\"misko\",\n \"project\":\"angular\"}');
- input('data').enter('{}');
- expect(binding('data')).toEqual('data={\n }');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter.json = formatter(toJson, function(value){
- return fromJson(value || 'null');
-});
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.boolean
- *
- * @description
- * Use boolean formatter if you wish to store the data as boolean.
- *
- * @returns {boolean} Converts to `true` unless user enters (blank), `f`, `false`, `0`, `no`, `[]`.
- *
- * @example
- <doc:example>
- <doc:source>
- Enter truthy text:
- <input type="text" name="value" ng:format="boolean" value="no"/>
- <input type="checkbox" name="value"/>
- <pre>value={{value}}</pre>
- </doc:source>
- <doc:scenario>
- it('should format boolean', function(){
- expect(binding('value')).toEqual('value=false');
- input('value').enter('truthy');
- expect(binding('value')).toEqual('value=true');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter['boolean'] = formatter(toString, toBoolean);
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.number
- *
- * @description
- * Use number formatter if you wish to convert the user entered string to a number.
- *
- * @returns {number} Number from the parsed string.
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid number:
- <input type="text" name="value" ng:format="number" value="1234"/>
- <pre>value={{value}}</pre>
- </doc:source>
- <doc:scenario>
- it('should format numbers', function(){
- expect(binding('value')).toEqual('value=1234');
- input('value').enter('5678');
- expect(binding('value')).toEqual('value=5678');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter.number = formatter(toString, function(obj){
- if (obj == null || NUMBER.exec(obj)) {
- return obj===null || obj === '' ? null : 1*obj;
- } else {
- throw "Not a number";
- }
-});
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.list
- *
- * @description
- * Use list formatter if you wish to convert the user entered string to an array.
- *
- * @returns {Array} Array parsed from the entered string.
- *
- * @example
- <doc:example>
- <doc:source>
- Enter a list of items:
- <input type="text" name="value" ng:format="list" value=" chair ,, table"/>
- <input type="text" name="value" ng:format="list"/>
- <pre>value={{value}}</pre>
- </doc:source>
- <doc:scenario>
- it('should format lists', function(){
- expect(binding('value')).toEqual('value=["chair","table"]');
- this.addFutureAction('change to XYZ', function($window, $document, done){
- $document.elements('.doc-example-live :input:last').val(',,a,b,').trigger('change');
- done();
- });
- expect(binding('value')).toEqual('value=["a","b"]');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter.list = formatter(
- function(obj) { return obj ? obj.join(", ") : obj; },
- function(value) {
- var list = [];
- forEach((value || '').split(','), function(item){
- item = trim(item);
- if (item) list.push(item);
- });
- return list;
- }
-);
-
-/**
- * @workInProgress
- * @ngdoc formatter
- * @name angular.formatter.trim
- *
- * @description
- * Use trim formatter if you wish to trim extra spaces in user text.
- *
- * @returns {String} Trim excess leading and trailing space.
- *
- * @example
- <doc:example>
- <doc:source>
- Enter text with leading/trailing spaces:
- <input type="text" name="value" ng:format="trim" value=" book "/>
- <input type="text" name="value" ng:format="trim"/>
- <pre>value={{value|json}}</pre>
- </doc:source>
- <doc:scenario>
- it('should format trim', function(){
- expect(binding('value')).toEqual('value="book"');
- this.addFutureAction('change to XYZ', function($window, $document, done){
- $document.elements('.doc-example-live :input:last').val(' text ').trigger('change');
- done();
- });
- expect(binding('value')).toEqual('value="text"');
- });
- </doc:scenario>
- </doc:example>
- */
-angularFormatter.trim = formatter(
- function(obj) { return obj ? trim("" + obj) : ""; }
-);
diff --git a/src/jqLite.js b/src/jqLite.js
index 5f761f92..0052055c 100644
--- a/src/jqLite.js
+++ b/src/jqLite.js
@@ -100,6 +100,10 @@ function camelCase(name) {
/////////////////////////////////////////////
// jQuery mutation patch
+//
+// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a
+// $destroy event on all DOM nodes being removed.
+//
/////////////////////////////////////////////
function JQLitePatchJQueryRemove(name, dispatchThis) {
@@ -129,7 +133,9 @@ function JQLitePatchJQueryRemove(name, dispatchThis) {
} else {
fireEvent = !fireEvent;
}
- for(childIndex = 0, childLength = (children = element.children()).length; childIndex < childLength; childIndex++) {
+ for(childIndex = 0, childLength = (children = element.children()).length;
+ childIndex < childLength;
+ childIndex++) {
list.push(jQuery(children[childIndex]));
}
}
@@ -283,7 +289,10 @@ var JQLitePrototype = JQLite.prototype = {
// these functions return self on setter and
// value on get.
//////////////////////////////////////////
-var SPECIAL_ATTR = makeMap("multiple,selected,checked,disabled,readonly,required");
+var BOOLEAN_ATTR = {};
+forEach('multiple,selected,checked,disabled,readOnly,required'.split(','), function(value, key) {
+ BOOLEAN_ATTR[lowercase(value)] = value;
+});
forEach({
data: JQLiteData,
@@ -331,7 +340,7 @@ forEach({
},
attr: function(element, name, value){
- if (SPECIAL_ATTR[name]) {
+ if (BOOLEAN_ATTR[name]) {
if (isDefined(value)) {
if (!!value) {
element[name] = true;
diff --git a/src/markups.js b/src/markups.js
index 1adad3e0..40f4322b 100644
--- a/src/markups.js
+++ b/src/markups.js
@@ -163,7 +163,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* This example uses `link` variable inside `href` attribute:
<doc:example>
<doc:source>
- <input name="value" /><br />
+ <input ng:model="value" /><br />
<a id="link-1" href ng:click="value = 1">link 1</a> (link, don't reload)<br />
<a id="link-2" href="" ng:click="value = 2">link 2</a> (link, don't reload)<br />
<a id="link-3" ng:href="/{{'123'}}" ng:ext-link>link 3</a> (link, reload!)<br />
@@ -262,8 +262,8 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Click me to toggle: <input type="checkbox" name="checked"><br/>
- <button name="button" ng:disabled="{{checked}}">Button</button>
+ Click me to toggle: <input type="checkbox" ng:model="checked"><br/>
+ <button ng:model="button" ng:disabled="{{checked}}">Button</button>
</doc:source>
<doc:scenario>
it('should toggle button', function() {
@@ -292,7 +292,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Check me to check both: <input type="checkbox" name="master"><br/>
+ Check me to check both: <input type="checkbox" ng:model="master"><br/>
<input id="checkSlave" type="checkbox" ng:checked="{{master}}">
</doc:source>
<doc:scenario>
@@ -323,7 +323,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Check me check multiple: <input type="checkbox" name="checked"><br/>
+ Check me check multiple: <input type="checkbox" ng:model="checked"><br/>
<select id="select" ng:multiple="{{checked}}">
<option>Misko</option>
<option>Igor</option>
@@ -358,7 +358,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Check me to make text readonly: <input type="checkbox" name="checked"><br/>
+ Check me to make text readonly: <input type="checkbox" ng:model="checked"><br/>
<input type="text" ng:readonly="{{checked}}" value="I'm Angular"/>
</doc:source>
<doc:scenario>
@@ -388,7 +388,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
- Check me to select: <input type="checkbox" name="checked"><br/>
+ Check me to select: <input type="checkbox" ng:model="checked"><br/>
<select>
<option>Hello!</option>
<option id="greet" ng:selected="{{checked}}">Greetings!</option>
@@ -408,10 +408,10 @@ angularTextMarkup('option', function(text, textNode, parentElement){
var NG_BIND_ATTR = 'ng:bind-attr';
-var SPECIAL_ATTRS = {};
+var SIDE_EFFECT_ATTRS = {};
-forEach('src,href,checked,disabled,multiple,readonly,selected'.split(','), function(name) {
- SPECIAL_ATTRS['ng:' + name] = name;
+forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) {
+ SIDE_EFFECT_ATTRS['ng:' + name] = name;
});
angularAttrMarkup('{{}}', function(value, name, element){
@@ -421,10 +421,10 @@ angularAttrMarkup('{{}}', function(value, name, element){
value = decodeURI(value);
var bindings = parseBindings(value),
bindAttr;
- if (hasBindings(bindings) || SPECIAL_ATTRS[name]) {
+ if (hasBindings(bindings) || SIDE_EFFECT_ATTRS[name]) {
element.removeAttr(name);
bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}");
- bindAttr[SPECIAL_ATTRS[name] || name] = value;
+ bindAttr[SIDE_EFFECT_ATTRS[name] || name] = value;
element.attr(NG_BIND_ATTR, toJson(bindAttr));
}
});
diff --git a/src/parser.js b/src/parser.js
index f8978a0b..4934b9e6 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -247,8 +247,6 @@ function parser(text, json){
assignable: assertConsumed(assignable),
primary: assertConsumed(primary),
statements: assertConsumed(statements),
- validator: assertConsumed(validator),
- formatter: assertConsumed(formatter),
filter: assertConsumed(filter)
};
@@ -361,36 +359,6 @@ function parser(text, json){
return pipeFunction(angularFilter);
}
- function validator(){
- return pipeFunction(angularValidator);
- }
-
- function formatter(){
- var token = expect();
- var formatter = angularFormatter[token.text];
- var argFns = [];
- if (!formatter) throwError('is not a valid formatter.', token);
- while(true) {
- if ((token = expect(':'))) {
- argFns.push(expression());
- } else {
- return valueFn({
- format:invokeFn(formatter.format),
- parse:invokeFn(formatter.parse)
- });
- }
- }
- function invokeFn(fn){
- return function(self, input){
- var args = [input];
- for ( var i = 0; i < argFns.length; i++) {
- args.push(argFns[i](self));
- }
- return fn.apply(self, args);
- };
- }
- }
-
function _pipeFunction(fnScope){
var fn = functionIdent(fnScope);
var argsFn = [];
@@ -735,16 +703,19 @@ function getterFn(path) {
code += 'if(!s) return s;\n' +
'l=s;\n' +
's=s' + key + ';\n' +
- 'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l' +
- key + '.apply(l, arguments); };\n';
+ 'if(typeof s=="function" && !(s instanceof RegExp)) {\n' +
+ ' fn=function(){ return l' + key + '.apply(l, arguments); };\n' +
+ ' fn.$unboundFn=s;\n' +
+ ' s=fn;\n' +
+ '}\n';
if (key.charAt(1) == '$') {
// special code for super-imposed functions
var name = key.substr(2);
code += 'if(!s) {\n' +
' t = angular.Global.typeOf(l);\n' +
' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' +
- ' if (fn) s = function(){ return fn.apply(l, ' +
- '[l].concat(Array.prototype.slice.call(arguments, 0))); };\n' +
+ ' if (fn) ' +
+ 's = function(){ return fn.apply(l, [l].concat(Array.prototype.slice.call(arguments, 0))); };\n' +
'}\n';
}
});
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
index 5ee7bde4..53be6c67 100644
--- a/src/scenario/Scenario.js
+++ b/src/scenario/Scenario.js
@@ -247,7 +247,7 @@ function browserTrigger(element, type) {
'radio': 'click',
'select-one': 'change',
'select-multiple': 'change'
- }[element.type] || 'click';
+ }[lowercase(element.type)] || 'click';
}
if (lowercase(nodeName_(element)) == 'option') {
element.parentNode.value = element.value;
diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js
index 8bc4030e..e0af0c8c 100644
--- a/src/scenario/dsl.js
+++ b/src/scenario/dsl.js
@@ -180,7 +180,7 @@ angular.scenario.dsl('input', function() {
chain.enter = function(value) {
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function($window, $document, done) {
- var input = $document.elements(':input[name="$1"]', this.name);
+ var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
input.val(value);
input.trigger('keydown');
done();
@@ -189,7 +189,7 @@ angular.scenario.dsl('input', function() {
chain.check = function() {
return this.addFutureAction("checkbox '" + this.name + "' toggle", function($window, $document, done) {
- var input = $document.elements(':checkbox[name="$1"]', this.name);
+ var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':checkbox');
input.trigger('click');
done();
});
@@ -198,7 +198,7 @@ angular.scenario.dsl('input', function() {
chain.select = function(value) {
return this.addFutureAction("radio button '" + this.name + "' toggle '" + value + "'", function($window, $document, done) {
var input = $document.
- elements(':radio[name$="@$1"][value="$2"]', this.name, value);
+ elements('[ng\\:model="$1"][value="$2"]', this.name, value).filter(':radio');
input.trigger('click');
done();
});
@@ -206,7 +206,7 @@ angular.scenario.dsl('input', function() {
chain.val = function() {
return this.addFutureAction("return input val", function($window, $document, done) {
- var input = $document.elements(':input[name="$1"]', this.name);
+ var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
done(null,input.val());
});
};
@@ -268,8 +268,16 @@ angular.scenario.dsl('select', function() {
chain.option = function(value) {
return this.addFutureAction("select '" + this.name + "' option '" + value + "'", function($window, $document, done) {
- var select = $document.elements('select[name="$1"]', this.name);
- select.val(value);
+ var select = $document.elements('select[ng\\:model="$1"]', this.name);
+ var option = select.find('option[value="' + value + '"]');
+ if (option.length) {
+ select.val(value);
+ } else {
+ option = select.find('option:contains("' + value + '")');
+ if (option.length) {
+ select.val(option.val());
+ }
+ }
select.trigger('change');
done();
});
@@ -278,7 +286,7 @@ angular.scenario.dsl('select', function() {
chain.options = function() {
var values = arguments;
return this.addFutureAction("select '" + this.name + "' options '" + values + "'", function($window, $document, done) {
- var select = $document.elements('select[multiple][name="$1"]', this.name);
+ var select = $document.elements('select[multiple][ng\\:model="$1"]', this.name);
select.val(values);
select.trigger('change');
done();
diff --git a/src/service/formFactory.js b/src/service/formFactory.js
new file mode 100644
index 00000000..4fc53935
--- /dev/null
+++ b/src/service/formFactory.js
@@ -0,0 +1,394 @@
+'use strict';
+
+/**
+ * @ngdoc service
+ * @name angular.service.$formFactory
+ *
+ * @description
+ * Use `$formFactory` to create a new instance of a {@link guide/dev_guide.forms form}
+ * controller or to find the nearest form instance for a given DOM element.
+ *
+ * The form instance is a collection of widgets, and is responsible for life cycle and validation
+ * of widget.
+ *
+ * Keep in mind that both form and widget instances are {@link api/angular.scope scopes}.
+ *
+ * @param {Form=} parentForm The form which should be the parent form of the new form controller.
+ * If none specified default to the `rootForm`.
+ * @returns {Form} A new <a href="#form">form</a> instance.
+ *
+ * @example
+ *
+ * This example shows how one could write a widget which would enable data-binding on
+ * `contenteditable` feature of HTML.
+ *
+ <doc:example>
+ <doc:source>
+ <script>
+ function EditorCntl(){
+ this.html = '<b>Hello</b> <i>World</i>!';
+ }
+
+ function HTMLEditorWidget(element) {
+ var self = this;
+ var htmlFilter = angular.filter('html');
+
+ this.$parseModel = function(){
+ // need to protect for script injection
+ try {
+ this.$viewValue = htmlFilter(this.$modelValue || '').get();
+ if (this.$error.HTML) {
+ // we were invalid, but now we are OK.
+ this.$emit('$valid', 'HTML');
+ }
+ } catch (e) {
+ // if HTML not parsable invalidate form.
+ this.$emit('$invalid', 'HTML');
+ }
+ }
+
+ this.$render = function(){
+ element.html(this.$viewValue);
+ }
+
+ element.bind('keyup', function(){
+ self.$apply(function(){
+ self.$emit('$viewChange', element.html());
+ });
+ });
+ }
+
+ angular.directive('ng:contenteditable', function(){
+ function linkFn($formFactory, element) {
+ var exp = element.attr('ng:contenteditable'),
+ form = $formFactory.forElement(element),
+ widget;
+ element.attr('contentEditable', true);
+ widget = form.$createWidget({
+ scope: this,
+ model: exp,
+ controller: HTMLEditorWidget,
+ controllerArgs: [element]});
+ // if the element is destroyed, then we need to notify the form.
+ element.bind('$destroy', function(){
+ widget.$destroy();
+ });
+ }
+ linkFn.$inject = ['$formFactory'];
+ return linkFn;
+ });
+ </script>
+ <form name='editorForm' ng:controller="EditorCntl">
+ <div ng:contenteditable="html"></div>
+ <hr/>
+ HTML: <br/>
+ <textarea ng:model="html" cols=80></textarea>
+ <hr/>
+ <pre>editorForm = {{editorForm}}</pre>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should enter invalid HTML', function(){
+ expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
+ input('html').enter('<');
+ expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularServiceInject('$formFactory', function(){
+
+
+ /**
+ * @ngdoc proprety
+ * @name rootForm
+ * @propertyOf angular.service.$formFactory
+ * @description
+ * Static property on `$formFactory`
+ *
+ * Each application ({@link guide/dev_guide.scopes.internals root scope}) gets a root form which
+ * is the top-level parent of all forms.
+ */
+ formFactory.rootForm = formFactory(this);
+
+
+ /**
+ * @ngdoc method
+ * @name forElement
+ * @methodOf angular.service.$formFactory
+ * @description
+ * Static method on `$formFactory` service.
+ *
+ * Retrieve the closest form for a given element or defaults to the `root` form. Used by the
+ * {@link angular.widget.form form} element.
+ * @param {Element} element The element where the search for form should initiate.
+ */
+ formFactory.forElement = function (element) {
+ return element.inheritedData('$form') || formFactory.rootForm;
+ };
+ return formFactory;
+
+ function formFactory(parent) {
+ return (parent || formFactory.rootForm).$new(FormController);
+ }
+
+});
+
+function propertiesUpdate(widget) {
+ widget.$valid = !(widget.$invalid =
+ !(widget.$readonly || widget.$disabled || equals(widget.$error, {})));
+}
+
+/**
+ * @ngdoc property
+ * @name $error
+ * @propertyOf angular.service.$formFactory
+ * @description
+ * Property of the form and widget instance.
+ *
+ * Summary of all of the errors on the page. If a widget emits `$invalid` with `REQUIRED` key,
+ * then the `$error` object will have a `REQUIRED` key with an array of widgets which have
+ * emitted this key. `form.$error.REQUIRED == [ widget ]`.
+ */
+
+/**
+ * @workInProgress
+ * @ngdoc property
+ * @name $invalid
+ * @propertyOf angular.service.$formFactory
+ * @description
+ * Property of the form and widget instance.
+ *
+ * True if any of the widgets of the form are invalid.
+ */
+
+/**
+ * @workInProgress
+ * @ngdoc property
+ * @name $valid
+ * @propertyOf angular.service.$formFactory
+ * @description
+ * Property of the form and widget instance.
+ *
+ * True if all of the widgets of the form are valid.
+ */
+
+/**
+ * @ngdoc event
+ * @name angular.service.$formFactory#$valid
+ * @eventOf angular.service.$formFactory
+ * @eventType listen on form
+ * @description
+ * Upon receiving the `$valid` event from the widget update the `$error`, `$valid` and `$invalid`
+ * properties of both the widget as well as the from.
+ *
+ * @param {String} validationKey The validation key to be used when updating the `$error` object.
+ * The validation key is what will allow the template to bind to a specific validation error
+ * such as `<div ng:show="form.$error.KEY">error for key</div>`.
+ */
+
+/**
+ * @ngdoc event
+ * @name angular.service.$formFactory#$invalid
+ * @eventOf angular.service.$formFactory
+ * @eventType listen on form
+ * @description
+ * Upon receiving the `$invalid` event from the widget update the `$error`, `$valid` and `$invalid`
+ * properties of both the widget as well as the from.
+ *
+ * @param {String} validationKey The validation key to be used when updating the `$error` object.
+ * The validation key is what will allow the template to bind to a specific validation error
+ * such as `<div ng:show="form.$error.KEY">error for key</div>`.
+ */
+
+/**
+ * @ngdoc event
+ * @name angular.service.$formFactory#$validate
+ * @eventOf angular.service.$formFactory
+ * @eventType emit on widget
+ * @description
+ * Emit the `$validate` event on the widget, giving a widget a chance to emit a
+ * `$valid` / `$invalid` event base on its state. The `$validate` event is triggered when the
+ * model or the view changes.
+ */
+
+/**
+ * @ngdoc event
+ * @name angular.service.$formFactory#$viewChange
+ * @eventOf angular.service.$formFactory
+ * @eventType listen on widget
+ * @description
+ * A widget is responsible for emitting this event whenever the view changes do to user interaction.
+ * The event takes a `$viewValue` parameter, which is the new value of the view. This
+ * event triggers a call to `$parseView()` as well as `$validate` event on widget.
+ *
+ * @param {*} viewValue The new value for the view which will be assigned to `widget.$viewValue`.
+ */
+
+function FormController(){
+ var form = this,
+ $error = form.$error = {};
+
+ form.$on('$destroy', function(event){
+ var widget = event.targetScope;
+ if (widget.$widgetId) {
+ delete form[widget.$widgetId];
+ }
+ forEach($error, removeWidget, widget);
+ });
+
+ form.$on('$valid', function(event, error){
+ var widget = event.targetScope;
+ delete widget.$error[error];
+ propertiesUpdate(widget);
+ removeWidget($error[error], error, widget);
+ });
+
+ form.$on('$invalid', function(event, error){
+ var widget = event.targetScope;
+ addWidget(error, widget);
+ widget.$error[error] = true;
+ propertiesUpdate(widget);
+ });
+
+ propertiesUpdate(form);
+
+ function removeWidget(queue, errorKey, widget) {
+ if (queue) {
+ widget = widget || this; // so that we can be used in forEach;
+ for (var i = 0, length = queue.length; i < length; i++) {
+ if (queue[i] === widget) {
+ queue.splice(i, 1);
+ if (!queue.length) {
+ delete $error[errorKey];
+ }
+ }
+ }
+ propertiesUpdate(form);
+ }
+ }
+
+ function addWidget(errorKey, widget) {
+ var queue = $error[errorKey];
+ if (queue) {
+ for (var i = 0, length = queue.length; i < length; i++) {
+ if (queue[i] === widget) {
+ return;
+ }
+ }
+ } else {
+ $error[errorKey] = queue = [];
+ }
+ queue.push(widget);
+ propertiesUpdate(form);
+ }
+}
+
+
+/**
+ * @ngdoc method
+ * @name $createWidget
+ * @methodOf angular.service.$formFactory
+ * @description
+ *
+ * Use form's `$createWidget` instance method to create new widgets. The widgets can be created
+ * using an alias which makes the accessible from the form and available for data-binding,
+ * useful for displaying validation error messages.
+ *
+ * The creation of a widget sets up:
+ *
+ * - `$watch` of `expression` on `model` scope. This code path syncs the model to the view.
+ * The `$watch` listener will:
+ *
+ * - assign the new model value of `expression` to `widget.$modelValue`.
+ * - call `widget.$parseModel` method if present. The `$parseModel` is responsible for copying
+ * the `widget.$modelValue` to `widget.$viewValue` and optionally converting the data.
+ * (For example to convert a number into string)
+ * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid`
+ * event.
+ * - call `widget.$render()` method on widget. The `$render` method is responsible for
+ * reading the `widget.$viewValue` and updating the DOM.
+ *
+ * - Listen on `$viewChange` event from the `widget`. This code path syncs the view to the model.
+ * The `$viewChange` listener will:
+ *
+ * - assign the value to `widget.$viewValue`.
+ * - call `widget.$parseView` method if present. The `$parseView` is responsible for copying
+ * the `widget.$viewValue` to `widget.$modelValue` and optionally converting the data.
+ * (For example to convert a string into number)
+ * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid`
+ * event.
+ * - Assign the `widget.$modelValue` to the `expression` on the `model` scope.
+ *
+ * - Creates these set of properties on the `widget` which are updated as a response to the
+ * `$valid` / `$invalid` events:
+ *
+ * - `$error` - object - validation errors will be published as keys on this object.
+ * Data-binding to this property is useful for displaying the validation errors.
+ * - `$valid` - boolean - true if there are no validation errors
+ * - `$invalid` - boolean - opposite of `$valid`.
+ * @param {Object} params Named parameters:
+ *
+ * - `scope` - `{Scope}` - The scope to which the model for this widget is attached.
+ * - `model` - `{string}` - The name of the model property on model scope.
+ * - `controller` - {WidgetController} - The controller constructor function.
+ * The controller constructor should create these instance methods.
+ * - `$parseView()`: optional method responsible for copying `$viewVale` to `$modelValue`.
+ * The method may fire `$valid`/`$invalid` events.
+ * - `$parseModel()`: optional method responsible for copying `$modelVale` to `$viewValue`.
+ * The method may fire `$valid`/`$invalid` events.
+ * - `$render()`: required method which needs to update the DOM of the widget to match the
+ * `$viewValue`.
+ *
+ * - `controllerArgs` - `{Array}` (Optional) - Any extra arguments will be curried to the
+ * WidgetController constructor.
+ * - `onChange` - `{(string|function())}` (Optional) - Expression to execute when user changes the
+ * value.
+ * - `alias` - `{string}` (Optional) - The name of the form property under which the widget
+ * instance should be published. The name should be unique for each form.
+ * @returns {Widget} Instance of a widget scope.
+ */
+FormController.prototype.$createWidget = function(params) {
+ var form = this,
+ modelScope = params.scope,
+ onChange = params.onChange,
+ alias = params.alias,
+ scopeGet = parser(params.model).assignable(),
+ scopeSet = scopeGet.assign,
+ widget = this.$new(params.controller, params.controllerArgs);
+
+ widget.$error = {};
+ // Set the state to something we know will change to get the process going.
+ widget.$modelValue = Number.NaN;
+ // watch for scope changes and update the view appropriately
+ modelScope.$watch(scopeGet, function (scope, value) {
+ if (!equals(widget.$modelValue, value)) {
+ widget.$modelValue = value;
+ widget.$parseModel ? widget.$parseModel() : (widget.$viewValue = value);
+ widget.$emit('$validate');
+ widget.$render && widget.$render();
+ }
+ });
+
+ widget.$on('$viewChange', function(event, viewValue){
+ if (!equals(widget.$viewValue, viewValue)) {
+ widget.$viewValue = viewValue;
+ widget.$parseView ? widget.$parseView() : (widget.$modelValue = widget.$viewValue);
+ scopeSet(modelScope, widget.$modelValue);
+ if (onChange) modelScope.$eval(onChange);
+ widget.$emit('$validate');
+ }
+ });
+
+ propertiesUpdate(widget);
+
+ // assign the widgetModel to the form
+ if (alias && !form.hasOwnProperty(alias)) {
+ form[alias] = widget;
+ widget.$widgetId = alias;
+ } else {
+ alias = null;
+ }
+
+ return widget;
+};
diff --git a/src/service/invalidWidgets.js b/src/service/invalidWidgets.js
deleted file mode 100644
index 7c1b2a9f..00000000
--- a/src/service/invalidWidgets.js
+++ /dev/null
@@ -1,69 +0,0 @@
-'use strict';
-
-/**
- * @workInProgress
- * @ngdoc service
- * @name angular.service.$invalidWidgets
- *
- * @description
- * Keeps references to all invalid widgets found during validation.
- * Can be queried to find whether there are any invalid widgets currently displayed.
- *
- * @example
- */
-angularServiceInject("$invalidWidgets", function(){
- var invalidWidgets = [];
-
-
- /** Remove an element from the array of invalid widgets */
- invalidWidgets.markValid = function(element){
- var index = indexOf(invalidWidgets, element);
- if (index != -1)
- invalidWidgets.splice(index, 1);
- };
-
-
- /** Add an element to the array of invalid widgets */
- invalidWidgets.markInvalid = function(element){
- var index = indexOf(invalidWidgets, element);
- if (index === -1)
- invalidWidgets.push(element);
- };
-
-
- /** Return count of all invalid widgets that are currently visible */
- invalidWidgets.visible = function() {
- var count = 0;
- forEach(invalidWidgets, function(widget){
- count = count + (isVisible(widget) ? 1 : 0);
- });
- return count;
- };
-
-
- /* At the end of each eval removes all invalid widgets that are not part of the current DOM. */
- this.$watch(function() {
- for(var i = 0; i < invalidWidgets.length;) {
- var widget = invalidWidgets[i];
- if (isOrphan(widget[0])) {
- invalidWidgets.splice(i, 1);
- if (widget.dealoc) widget.dealoc();
- } else {
- i++;
- }
- }
- });
-
-
- /**
- * Traverses DOM element's (widget's) parents and considers the element to be an orphan if one of
- * it's parents isn't the current window.document.
- */
- function isOrphan(widget) {
- if (widget == window.document) return false;
- var parent = widget.parentNode;
- return !parent || isOrphan(parent);
- }
-
- return invalidWidgets;
-});
diff --git a/src/service/log.js b/src/service/log.js
index 09945732..3dacd117 100644
--- a/src/service/log.js
+++ b/src/service/log.js
@@ -18,12 +18,13 @@
<script>
function LogCtrl($log) {
this.$log = $log;
+ this.message = 'Hello World!';
}
</script>
<div ng:controller="LogCtrl">
<p>Reload this page with open console, enter text and hit the log button...</p>
Message:
- <input type="text" name="message" value="Hello World!"/>
+ <input type="text" ng:model="message"/>
<button ng:click="$log.log(message)">log</button>
<button ng:click="$log.warn(message)">warn</button>
<button ng:click="$log.info(message)">info</button>
diff --git a/src/service/resource.js b/src/service/resource.js
index f6e0be18..915f2d92 100644
--- a/src/service/resource.js
+++ b/src/service/resource.js
@@ -160,6 +160,7 @@
<doc:source jsfiddle="false">
<script>
function BuzzController($resource) {
+ this.userId = 'googlebuzz';
this.Activity = $resource(
'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
{alt:'json', callback:'JSON_CALLBACK'},
@@ -179,7 +180,7 @@
</script>
<div ng:controller="BuzzController">
- <input name="userId" value="googlebuzz"/>
+ <input ng:model="userId"/>
<button ng:click="fetch()">fetch</button>
<hr/>
<div ng:repeat="item in activities.data.items">
diff --git a/src/service/route.js b/src/service/route.js
index 73c73b04..b78cca91 100644
--- a/src/service/route.js
+++ b/src/service/route.js
@@ -260,7 +260,8 @@ angularServiceInject('$route', function($location, $routeParams) {
function updateRoute() {
var next = parseRoute(),
- last = $route.current;
+ last = $route.current,
+ Controller;
if (next && last && next.$route === last.$route
&& equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) {
@@ -283,7 +284,8 @@ angularServiceInject('$route', function($location, $routeParams) {
}
} else {
copy(next.params, $routeParams);
- next.scope = parentScope.$new(next.controller);
+ (Controller = next.controller) && inferInjectionArgs(Controller);
+ next.scope = parentScope.$new(Controller);
}
}
rootScope.$broadcast('$afterRouteChange', next, last);
diff --git a/src/service/window.js b/src/service/window.js
index 2f3f677a..9795e4fc 100644
--- a/src/service/window.js
+++ b/src/service/window.js
@@ -17,7 +17,7 @@
* @example
<doc:example>
<doc:source>
- <input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" name="greeting" />
+ <input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" ng:model="greeting" />
<button ng:click="$window.alert(greeting)">ALERT</button>
</doc:source>
<doc:scenario>
diff --git a/src/service/xhr.js b/src/service/xhr.js
index 09e7d070..4981c078 100644
--- a/src/service/xhr.js
+++ b/src/service/xhr.js
@@ -111,6 +111,7 @@
<script>
function FetchCntl($xhr) {
var self = this;
+ this.url = 'index.html';
this.fetch = function() {
self.code = null;
@@ -133,11 +134,11 @@
FetchCntl.$inject = ['$xhr'];
</script>
<div ng:controller="FetchCntl">
- <select name="method">
+ <select ng:model="method">
<option>GET</option>
<option>JSON</option>
</select>
- <input type="text" name="url" value="index.html" size="80"/>
+ <input type="text" ng:model="url" size="80"/>
<button ng:click="fetch()">fetch</button><br>
<button ng:click="updateModel('GET', 'index.html')">Sample GET</button>
<button ng:click="updateModel('JSON', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button>
diff --git a/src/validators.js b/src/validators.js
deleted file mode 100644
index 72a995fc..00000000
--- a/src/validators.js
+++ /dev/null
@@ -1,482 +0,0 @@
-'use strict';
-
-/**
- * @workInProgress
- * @ngdoc overview
- * @name angular.validator
- * @description
- *
- * Most of the built-in angular validators are used to check user input against defined types or
- * patterns. You can easily create your own custom validators as well.
- *
- * Following is the list of built-in angular validators:
- *
- * * {@link angular.validator.asynchronous asynchronous()} - Provides asynchronous validation via a
- * callback function.
- * * {@link angular.validator.date date()} - Checks user input against default date format:
- * "MM/DD/YYYY"
- * * {@link angular.validator.email email()} - Validates that user input is a well-formed email
- * address.
- * * {@link angular.validator.integer integer()} - Validates that user input is an integer
- * * {@link angular.validator.json json()} - Validates that user input is valid JSON
- * * {@link angular.validator.number number()} - Validates that user input is a number
- * * {@link angular.validator.phone phone()} - Validates that user input matches the pattern
- * "1(123)123-1234"
- * * {@link angular.validator.regexp regexp()} - Restricts valid input to a specified regular
- * expression pattern
- * * {@link angular.validator.url url()} - Validates that user input is a well-formed URL.
- *
- * For more information about how angular validators work, and how to create your own validators,
- * see {@link guide/dev_guide.templates.validators Understanding Angular Validators} in the angular
- * Developer Guide.
- */
-
-extend(angularValidator, {
- 'noop': function() { return null; },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.regexp
- * @description
- * Use regexp validator to restrict the input to any Regular Expression.
- *
- * @param {string} value value to validate
- * @param {string|regexp} expression regular expression.
- * @param {string=} msg error message to display.
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- <script> function Cntl(){
- this.ssnRegExp = /^\d\d\d-\d\d-\d\d\d\d$/;
- }
- </script>
- Enter valid SSN:
- <div ng:controller="Cntl">
- <input name="ssn" value="123-45-6789" ng:validate="regexp:ssnRegExp" >
- </div>
- </doc:source>
- <doc:scenario>
- it('should invalidate non ssn', function(){
- var textBox = element('.doc-example-live :input');
- expect(textBox.prop('className')).not().toMatch(/ng-validation-error/);
- expect(textBox.val()).toEqual('123-45-6789');
- input('ssn').enter('123-45-67890');
- expect(textBox.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'regexp': function(value, regexp, msg) {
- if (!value.match(regexp)) {
- return msg ||
- "Value does not match expected format " + regexp + ".";
- } else {
- return null;
- }
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.number
- * @description
- * Use number validator to restrict the input to numbers with an
- * optional range. (See integer for whole numbers validator).
- *
- * @param {string} value value to validate
- * @param {int=} [min=MIN_INT] minimum value.
- * @param {int=} [max=MAX_INT] maximum value.
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter number: <input name="n1" ng:validate="number" > <br>
- Enter number greater than 10: <input name="n2" ng:validate="number:10" > <br>
- Enter number between 100 and 200: <input name="n3" ng:validate="number:100:200" > <br>
- </doc:source>
- <doc:scenario>
- it('should invalidate number', function(){
- var n1 = element('.doc-example-live :input[name=n1]');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('n1').enter('1.x');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- var n2 = element('.doc-example-live :input[name=n2]');
- expect(n2.prop('className')).not().toMatch(/ng-validation-error/);
- input('n2').enter('9');
- expect(n2.prop('className')).toMatch(/ng-validation-error/);
- var n3 = element('.doc-example-live :input[name=n3]');
- expect(n3.prop('className')).not().toMatch(/ng-validation-error/);
- input('n3').enter('201');
- expect(n3.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'number': function(value, min, max) {
- var num = 1 * value;
- if (num == value) {
- if (typeof min != $undefined && num < min) {
- return "Value can not be less than " + min + ".";
- }
- if (typeof min != $undefined && num > max) {
- return "Value can not be greater than " + max + ".";
- }
- return null;
- } else {
- return "Not a number";
- }
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.integer
- * @description
- * Use number validator to restrict the input to integers with an
- * optional range. (See integer for whole numbers validator).
- *
- * @param {string} value value to validate
- * @param {int=} [min=MIN_INT] minimum value.
- * @param {int=} [max=MAX_INT] maximum value.
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter integer: <input name="n1" ng:validate="integer" > <br>
- Enter integer equal or greater than 10: <input name="n2" ng:validate="integer:10" > <br>
- Enter integer between 100 and 200 (inclusive): <input name="n3" ng:validate="integer:100:200" > <br>
- </doc:source>
- <doc:scenario>
- it('should invalidate integer', function(){
- var n1 = element('.doc-example-live :input[name=n1]');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('n1').enter('1.1');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- var n2 = element('.doc-example-live :input[name=n2]');
- expect(n2.prop('className')).not().toMatch(/ng-validation-error/);
- input('n2').enter('10.1');
- expect(n2.prop('className')).toMatch(/ng-validation-error/);
- var n3 = element('.doc-example-live :input[name=n3]');
- expect(n3.prop('className')).not().toMatch(/ng-validation-error/);
- input('n3').enter('100.1');
- expect(n3.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- */
- 'integer': function(value, min, max) {
- var numberError = angularValidator['number'](value, min, max);
- if (numberError) return numberError;
- if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) {
- return "Not a whole number";
- }
- return null;
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.date
- * @description
- * Use date validator to restrict the user input to a valid date
- * in format in format MM/DD/YYYY.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid date:
- <input name="text" value="1/1/2009" ng:validate="date" >
- </doc:source>
- <doc:scenario>
- it('should invalidate date', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('123/123/123');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'date': function(value) {
- var fields = /^(\d\d?)\/(\d\d?)\/(\d\d\d\d)$/.exec(value);
- var date = fields ? new Date(fields[3], fields[1]-1, fields[2]) : 0;
- return (date &&
- date.getFullYear() == fields[3] &&
- date.getMonth() == fields[1]-1 &&
- date.getDate() == fields[2])
- ? null
- : "Value is not a date. (Expecting format: 12/31/2009).";
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.email
- * @description
- * Use email validator if you wist to restrict the user input to a valid email.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid email:
- <input name="text" ng:validate="email" value="me@example.com">
- </doc:source>
- <doc:scenario>
- it('should invalidate email', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('a@b.c');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'email': function(value) {
- if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) {
- return null;
- }
- return "Email needs to be in username@host.com format.";
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.phone
- * @description
- * Use phone validator to restrict the input phone numbers.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid phone number:
- <input name="text" value="1(234)567-8901" ng:validate="phone" >
- </doc:source>
- <doc:scenario>
- it('should invalidate phone', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('+12345678');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'phone': function(value) {
- if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) {
- return null;
- }
- if (value.match(/^\+\d{2,3} (\(\d{1,5}\))?[\d ]+\d$/)) {
- return null;
- }
- return "Phone number needs to be in 1(987)654-3210 format in North America " +
- "or +999 (123) 45678 906 internationally.";
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.url
- * @description
- * Use phone validator to restrict the input URLs.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- Enter valid URL:
- <input name="text" value="http://example.com/abc.html" size="40" ng:validate="url" >
- </doc:source>
- <doc:scenario>
- it('should invalidate url', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('abc://server/path');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'url': function(value) {
- if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) {
- return null;
- }
- return "URL needs to be in http://server[:port]/path format.";
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.json
- * @description
- * Use json validator if you wish to restrict the user input to a valid JSON.
- *
- * @param {string} value value to validate
- * @css ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- <textarea name="json" cols="60" rows="5" ng:validate="json">
- {name:'abc'}
- </textarea>
- </doc:source>
- <doc:scenario>
- it('should invalidate json', function(){
- var n1 = element('.doc-example-live :input');
- expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
- input('json').enter('{name}');
- expect(n1.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- 'json': function(value) {
- try {
- fromJson(value);
- return null;
- } catch (e) {
- return e.toString();
- }
- },
-
- /**
- * @workInProgress
- * @ngdoc validator
- * @name angular.validator.asynchronous
- * @description
- * Use asynchronous validator if the validation can not be computed
- * immediately, but is provided through a callback. The widget
- * automatically shows a spinning indicator while the validity of
- * the widget is computed. This validator caches the result.
- *
- * @param {string} value value to validate
- * @param {function(inputToValidate,validationDone)} validate function to call to validate the state
- * of the input.
- * @param {function(data)=} [update=noop] function to call when state of the
- * validator changes
- *
- * @paramDescription
- * The `validate` function (specified by you) is called as
- * `validate(inputToValidate, validationDone)`:
- *
- * * `inputToValidate`: value of the input box.
- * * `validationDone`: `function(error, data){...}`
- * * `error`: error text to display if validation fails
- * * `data`: data object to pass to update function
- *
- * The `update` function is optionally specified by you and is
- * called by <angular/> on input change. Since the
- * asynchronous validator caches the results, the update
- * function can be called without a call to `validate`
- * function. The function is called as `update(data)`:
- *
- * * `data`: data object as passed from validate function
- *
- * @css ng-input-indicator-wait, ng-validation-error
- *
- * @example
- <doc:example>
- <doc:source>
- <script>
- function MyCntl(){
- this.myValidator = function (inputToValidate, validationDone) {
- setTimeout(function(){
- validationDone(inputToValidate.length % 2);
- }, 500);
- }
- }
- </script>
- This input is validated asynchronously:
- <div ng:controller="MyCntl">
- <input name="text" ng:validate="asynchronous:myValidator">
- </div>
- </doc:source>
- <doc:scenario>
- it('should change color in delayed way', function(){
- var textBox = element('.doc-example-live :input');
- expect(textBox.prop('className')).not().toMatch(/ng-input-indicator-wait/);
- expect(textBox.prop('className')).not().toMatch(/ng-validation-error/);
- input('text').enter('X');
- expect(textBox.prop('className')).toMatch(/ng-input-indicator-wait/);
- sleep(.6);
- expect(textBox.prop('className')).not().toMatch(/ng-input-indicator-wait/);
- expect(textBox.prop('className')).toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- *
- */
- /*
- * cache is attached to the element
- * cache: {
- * inputs : {
- * 'user input': {
- * response: server response,
- * error: validation error
- * },
- * current: 'current input'
- * }
- * }
- *
- */
- 'asynchronous': function(input, asynchronousFn, updateFn) {
- if (!input) return;
- var scope = this;
- var element = scope.$element;
- var cache = element.data('$asyncValidator');
- if (!cache) {
- element.data('$asyncValidator', cache = {inputs:{}});
- }
-
- cache.current = input;
-
- var inputState = cache.inputs[input],
- $invalidWidgets = scope.$service('$invalidWidgets');
-
- if (!inputState) {
- cache.inputs[input] = inputState = { inFlight: true };
- $invalidWidgets.markInvalid(scope.$element);
- element.addClass('ng-input-indicator-wait');
- asynchronousFn(input, function(error, data) {
- inputState.response = data;
- inputState.error = error;
- inputState.inFlight = false;
- if (cache.current == input) {
- element.removeClass('ng-input-indicator-wait');
- $invalidWidgets.markValid(element);
- }
- element.data($$validate)();
- });
- } else if (inputState.inFlight) {
- // request in flight, mark widget invalid, but don't show it to user
- $invalidWidgets.markInvalid(scope.$element);
- } else {
- (updateFn||noop)(inputState.response);
- }
- return inputState.error;
- }
-
-});
diff --git a/src/widget/form.js b/src/widget/form.js
new file mode 100644
index 00000000..bc34bf0d
--- /dev/null
+++ b/src/widget/form.js
@@ -0,0 +1,81 @@
+'use strict';
+
+/**
+ * @workInProgress
+ * @ngdoc widget
+ * @name angular.widget.form
+ *
+ * @description
+ * Angular widget that creates a form scope using the
+ * {@link angular.service.$formFactory $formFactory} API. The resulting form scope instance is
+ * attached to the DOM element using the jQuery `.data()` method under the `$form` key.
+ * See {@link guide/dev_guide.forms forms} on detailed discussion of forms and widgets.
+ *
+ *
+ * # Alias: `ng:form`
+ *
+ * In angular forms can be nested. This means that the outer form is valid when all of the child
+ * forms are valid as well. However browsers do not allow nesting of `<form>` elements, for this
+ * reason angular provides `<ng:form>` alias which behaves identical to `<form>` but allows
+ * element nesting.
+ *
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'guest';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ text: <input type="text" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.text.$error.REQUIRED">Required!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function(){
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function(){
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularWidget('form', function(form){
+ this.descend(true);
+ this.directives(true);
+ return annotate('$formFactory', function($formFactory, formElement) {
+ var name = formElement.attr('name'),
+ parentForm = $formFactory.forElement(formElement),
+ form = $formFactory(parentForm);
+ formElement.data('$form', form);
+ formElement.bind('submit', function(event){
+ event.preventDefault();
+ });
+ if (name) {
+ this[name] = form;
+ }
+ watch('valid');
+ watch('invalid');
+ function watch(name) {
+ form.$watch('$' + name, function(scope, value) {
+ formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ });
+ }
+ });
+});
+
+angularWidget('ng:form', angularWidget('form'));
diff --git a/src/widget/input.js b/src/widget/input.js
new file mode 100644
index 00000000..f82027f4
--- /dev/null
+++ b/src/widget/input.js
@@ -0,0 +1,773 @@
+'use strict';
+
+
+var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
+var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
+var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
+var INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.text
+ *
+ * @description
+ * Standard HTML text input with angular data binding.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'guest';
+ this.word = /^\w*$/;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Single word: <input type="text" name="input" ng:model="text"
+ ng:pattern="word" required>
+ <span class="error" ng:show="myForm.input.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.$error.PATTERN">
+ Single word only!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if multi word', function() {
+ input('text').enter('hello world');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.email
+ *
+ * @description
+ * Text input with email validation. Sets the `EMAIL` validation error key if not a valid email
+ * address.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'me@example.com';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Email: <input type="email" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.$error.EMAIL">
+ Not valid email!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ <tt>myForm.$error.EMAIL = {{!!myForm.$error.EMAIL}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('me@example.com');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if not email', function() {
+ input('text').enter('xxx');
+ expect(binding('text')).toEqual('xxx');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('email', function() {
+ var widget = this;
+ this.$on('$validate', function(event){
+ var value = widget.$viewValue;
+ widget.$emit(!value || value.match(EMAIL_REGEXP) ? "$valid" : "$invalid", "EMAIL");
+ });
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.url
+ *
+ * @description
+ * Text input with URL validation. Sets the `URL` validation error key if the content is not a
+ * valid URL.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'http://google.com';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ URL: <input type="url" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.$error.url">
+ Not valid url!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ <tt>myForm.$error.url = {{!!myForm.$error.url}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('http://google.com');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if not url', function() {
+ input('text').enter('xxx');
+ expect(binding('text')).toEqual('xxx');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('url', function() {
+ var widget = this;
+ this.$on('$validate', function(event){
+ var value = widget.$viewValue;
+ widget.$emit(!value || value.match(URL_REGEXP) ? "$valid" : "$invalid", "URL");
+ });
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.list
+ *
+ * @description
+ * Text input that converts between comma-seperated string into an array of strings.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.names = ['igor', 'misko', 'vojta'];
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ List: <input type="list" name="input" ng:model="names" required>
+ <span class="error" ng:show="myForm.list.$error.REQUIRED">
+ Required!</span>
+ </form>
+ <tt>names = {{names}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('names')).toEqual('["igor","misko","vojta"]');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('names').enter('');
+ expect(binding('names')).toEqual('[]');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('list', function() {
+ function parse(viewValue) {
+ var list = [];
+ forEach(viewValue.split(/\s*,\s*/), function(value){
+ if (value) list.push(trim(value));
+ });
+ return list;
+ }
+ this.$parseView = function() {
+ isString(this.$viewValue) && (this.$modelValue = parse(this.$viewValue));
+ };
+ this.$parseModel = function() {
+ var modelValue = this.$modelValue;
+ if (isArray(modelValue)
+ && (!isString(this.$viewValue) || !equals(parse(this.$viewValue), modelValue))) {
+ this.$viewValue = modelValue.join(', ');
+ }
+ };
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.number
+ *
+ * @description
+ * Text input with number validation and transformation. Sets the `NUMBER` validation
+ * error if not a valid number.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} min Sets the `MIN` validation error key if the value entered is less then `min`.
+ * @param {string=} max Sets the `MAX` validation error key if the value entered is greater then `min`.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.value = 12;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Number: <input type="number" name="input" ng:model="value"
+ min="0" max="99" required>
+ <span class="error" ng:show="myForm.list.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.list.$error.NUMBER">
+ Not valid number!</span>
+ </form>
+ <tt>value = {{value}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('value')).toEqual('12');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('value').enter('');
+ expect(binding('value')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if over max', function() {
+ input('value').enter('123');
+ expect(binding('value')).toEqual('123');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('number', numericRegexpInputType(NUMBER_REGEXP, 'NUMBER'));
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.integer
+ *
+ * @description
+ * Text input with integer validation and transformation. Sets the `INTEGER`
+ * validation error key if not a valid integer.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} min Sets the `MIN` validation error key if the value entered is less then `min`.
+ * @param {string=} max Sets the `MAX` validation error key if the value entered is greater then `min`.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.value = 12;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Integer: <input type="integer" name="input" ng:model="value"
+ min="0" max="99" required>
+ <span class="error" ng:show="myForm.list.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.list.$error.INTEGER">
+ Not valid integer!</span>
+ </form>
+ <tt>value = {{value}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('value')).toEqual('12');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('value').enter('1.2');
+ expect(binding('value')).toEqual('12');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if over max', function() {
+ input('value').enter('123');
+ expect(binding('value')).toEqual('123');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.checkbox
+ *
+ * @description
+ * HTML checkbox.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} true-value The value to which the expression should be set when selected.
+ * @param {string=} false-value The value to which the expression should be set when not selected.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.value1 = true;
+ this.value2 = 'YES'
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Value1: <input type="checkbox" ng:model="value1"> <br/>
+ Value2: <input type="checkbox" ng:model="value2"
+ true-value="YES" false-value="NO"> <br/>
+ </form>
+ <tt>value1 = {{value1}}</tt><br/>
+ <tt>value2 = {{value2}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should change state', function() {
+ expect(binding('value1')).toEqual('true');
+ expect(binding('value2')).toEqual('YES');
+
+ input('value1').check();
+ input('value2').check();
+ expect(binding('value1')).toEqual('false');
+ expect(binding('value2')).toEqual('NO');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('checkbox', function (inputElement) {
+ var widget = this,
+ trueValue = inputElement.attr('true-value'),
+ falseValue = inputElement.attr('false-value');
+
+ if (!isString(trueValue)) trueValue = true;
+ if (!isString(falseValue)) falseValue = false;
+
+ inputElement.bind('click', function() {
+ widget.$apply(function() {
+ widget.$emit('$viewChange', inputElement[0].checked);
+ });
+ });
+
+ widget.$render = function() {
+ inputElement[0].checked = widget.$viewValue;
+ };
+
+ widget.$parseModel = function() {
+ widget.$viewValue = this.$modelValue === trueValue;
+ };
+
+ widget.$parseView = function() {
+ widget.$modelValue = widget.$viewValue ? trueValue : falseValue;
+ };
+
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.radio
+ *
+ * @description
+ * HTML radio.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string} value The value to which the expression should be set when selected.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.color = 'blue';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ <input type="radio" ng:model="color" value="red"> Red <br/>
+ <input type="radio" ng:model="color" value="green"> Green <br/>
+ <input type="radio" ng:model="color" value="blue"> Blue <br/>
+ </form>
+ <tt>color = {{color}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should change state', function() {
+ expect(binding('color')).toEqual('blue');
+
+ input('color').select('red');
+ expect(binding('color')).toEqual('red');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('radio', function(inputElement) {
+ var widget = this,
+ value = inputElement.attr('value');
+
+ //correct the name
+ inputElement.attr('name', widget.$id + '@' + inputElement.attr('name'));
+ inputElement.bind('click', function() {
+ widget.$apply(function() {
+ if (inputElement[0].checked) {
+ widget.$emit('$viewChange', value);
+ }
+ });
+ });
+
+ widget.$render = function() {
+ inputElement[0].checked = value == widget.$viewValue;
+ };
+
+ if (inputElement[0].checked) {
+ widget.$viewValue = value;
+ }
+});
+
+
+function numericRegexpInputType(regexp, error) {
+ return function(inputElement) {
+ var widget = this,
+ min = 1 * (inputElement.attr('min') || Number.MIN_VALUE),
+ max = 1 * (inputElement.attr('max') || Number.MAX_VALUE);
+
+ widget.$on('$validate', function(event){
+ var value = widget.$viewValue,
+ filled = value && trim(value) != '',
+ valid = isString(value) && value.match(regexp);
+
+ widget.$emit(!filled || valid ? "$valid" : "$invalid", error);
+ filled && (value = 1 * value);
+ widget.$emit(valid && value < min ? "$invalid" : "$valid", "MIN");
+ widget.$emit(valid && value > max ? "$invalid" : "$valid", "MAX");
+ });
+
+ widget.$parseView = function() {
+ if (widget.$viewValue.match(regexp)) {
+ widget.$modelValue = 1 * widget.$viewValue;
+ } else if (widget.$viewValue == '') {
+ widget.$modelValue = null;
+ }
+ };
+
+ widget.$parseModel = function() {
+ if (isNumber(widget.$modelValue)) {
+ widget.$viewValue = '' + widget.$modelValue;
+ }
+ };
+ };
+}
+
+
+var HTML5_INPUTS_TYPES = makeMap(
+ "search,tel,url,email,datetime,date,month,week,time,datetime-local,number,range,color," +
+ "radio,checkbox,text,button,submit,reset,hidden");
+
+
+/**
+ * @workInProgress
+ * @ngdoc widget
+ * @name angular.widget.input
+ *
+ * @description
+ * HTML input element widget with angular data-binding. Input widget follows HTML5 input types
+ * and polyfills the HTML5 validation behavior for older browsers.
+ *
+ * The {@link angular.inputType custom angular.inputType}s provides a short hand for declaring new
+ * inputs. This is a shart hand for text-box based inputs, and there is no need to go through the
+ * full {@link angular.service.$formFactory $formFactory} widget lifecycle.
+ *
+ *
+ * @param {string} type Widget types as defined by {@link angular.inputType}. If the
+ * type is in the format of `@ScopeType` then `ScopeType` is loaded from the
+ * current scope, allowing quick definition of type.
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'guest';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ text: <input type="text" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.$error.REQUIRED">
+ Required!</span>
+ </form>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
+ <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularWidget('input', function (inputElement){
+ this.directives(true);
+ this.descend(true);
+ var modelExp = inputElement.attr('ng:model');
+ return modelExp &&
+ annotate('$defer', '$formFactory', function($defer, $formFactory, inputElement){
+ var form = $formFactory.forElement(inputElement),
+ // We have to use .getAttribute, since jQuery tries to be smart and use the
+ // type property. Trouble is some browser change unknown to text.
+ type = inputElement[0].getAttribute('type') || 'text',
+ TypeController,
+ modelScope = this,
+ patternMatch, widget,
+ pattern = trim(inputElement.attr('ng:pattern')),
+ loadFromScope = type.match(/^\s*\@\s*(.*)/);
+
+
+ if (!pattern) {
+ patternMatch = valueFn(true);
+ } else {
+ if (pattern.match(/^\/(.*)\/$/)) {
+ pattern = new RegExp(pattern.substring(1, pattern.length - 2));
+ patternMatch = function(value) {
+ return pattern.test(value);
+ }
+ } else {
+ patternMatch = function(value) {
+ var patternObj = modelScope.$eval(pattern);
+ if (!patternObj || !patternObj.test) {
+ throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj);
+ }
+ return patternObj.test(value);
+ }
+ }
+ }
+
+ type = lowercase(type);
+ TypeController = (loadFromScope
+ ? (assertArgFn(this.$eval(loadFromScope[1]), loadFromScope[1])).$unboundFn
+ : angularInputType(type)) || noop;
+
+ if (!HTML5_INPUTS_TYPES[type]) {
+ try {
+ // jquery will not let you so we have to go to bare metal
+ inputElement[0].setAttribute('type', 'text');
+ } catch(e){
+ // also turns out that ie8 will not allow changing of types, but since it is not
+ // html5 anyway we can ignore the error.
+ }
+ }
+
+ !TypeController.$inject && (TypeController.$inject = []);
+ widget = form.$createWidget({
+ scope: modelScope,
+ model: modelExp,
+ onChange: inputElement.attr('ng:change'),
+ alias: inputElement.attr('name'),
+ controller: TypeController,
+ controllerArgs: [inputElement]});
+
+ widget.$pattern =
+ watchElementProperty(this, widget, 'required', inputElement);
+ watchElementProperty(this, widget, 'readonly', inputElement);
+ watchElementProperty(this, widget, 'disabled', inputElement);
+
+
+ widget.$pristine = !(widget.$dirty = false);
+
+ widget.$on('$validate', function(event) {
+ var $viewValue = trim(widget.$viewValue);
+ var inValid = widget.$required && !$viewValue;
+ var missMatch = $viewValue && !patternMatch($viewValue);
+ if (widget.$error.REQUIRED != inValid){
+ widget.$emit(inValid ? '$invalid' : '$valid', 'REQUIRED');
+ }
+ if (widget.$error.PATTERN != missMatch){
+ widget.$emit(missMatch ? '$invalid' : '$valid', 'PATTERN');
+ }
+ });
+
+ forEach(['valid', 'invalid', 'pristine', 'dirty'], function (name) {
+ widget.$watch('$' + name, function(scope, value) {
+ inputElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ }
+ );
+ });
+
+ inputElement.bind('$destroy', function() {
+ widget.$destroy();
+ });
+
+ if (type != 'checkbox' && type != 'radio') {
+ // TODO (misko): checkbox / radio does not really belong here, but until we can do
+ // widget registration with CSS, we are hacking it this way.
+ widget.$render = function() {
+ inputElement.val(widget.$viewValue || '');
+ };
+
+ inputElement.bind('keydown change', function(event){
+ var key = event.keyCode;
+ if (/*command*/ key != 91 &&
+ /*modifiers*/ !(15 < key && key < 19) &&
+ /*arrow*/ !(37 < key && key < 40)) {
+ $defer(function() {
+ widget.$dirty = !(widget.$pristine = false);
+ var value = trim(inputElement.val());
+ if (widget.$viewValue !== value ) {
+ widget.$emit('$viewChange', value);
+ }
+ });
+ }
+ });
+ }
+ });
+
+});
+
+angularWidget('textarea', angularWidget('input'));
+
+
+function watchElementProperty(modelScope, widget, name, element) {
+ var bindAttr = fromJson(element.attr('ng:bind-attr') || '{}'),
+ match = /\s*{{(.*)}}\s*/.exec(bindAttr[name]);
+ widget['$' + name] =
+ // some browsers return true some '' when required is set without value.
+ isString(element.prop(name)) || !!element.prop(name) ||
+ // this is needed for ie9, since it will treat boolean attributes as false
+ !!element[0].attributes[name];
+ if (bindAttr[name] && match) {
+ modelScope.$watch(match[1], function(scope, value){
+ widget['$' + name] = !!value;
+ widget.$emit('$validate');
+ });
+ }
+}
+
diff --git a/src/widget/select.js b/src/widget/select.js
new file mode 100644
index 00000000..f397180e
--- /dev/null
+++ b/src/widget/select.js
@@ -0,0 +1,427 @@
+'use strict';
+
+/**
+ * @workInProgress
+ * @ngdoc widget
+ * @name angular.widget.select
+ *
+ * @description
+ * HTML `SELECT` element with angular data-binding.
+ *
+ * # `ng:options`
+ *
+ * Optionally `ng:options` attribute can be used to dynamically generate a list of `<option>`
+ * elements for a `<select>` element using an array or an object obtained by evaluating the
+ * `ng:options` expression.
+ *
+ * When an item in the select menu is select, the value of array element or object property
+ * represented by the selected option will be bound to the model identified by the `name` attribute
+ * of the parent select element.
+ *
+ * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
+ * be nested into the `<select>` element. This element will then represent `null` or "not selected"
+ * option. See example below for demonstration.
+ *
+ * Note: `ng:options` provides iterator facility for `<option>` element which must be used instead
+ * of {@link angular.widget.@ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with
+ * `<option>` element because of the following reasons:
+ *
+ * * value attribute of the option element that we need to bind to requires a string, but the
+ * source of data for the iteration might be in a form of array containing objects instead of
+ * strings
+ * * {@link angular.widget.@ng:repeat ng:repeat} unrolls after the select binds causing
+ * incorect rendering on most browsers.
+ * * binding to a value not in list confuses most browsers.
+ *
+ * @param {string} name assignable expression to data-bind to.
+ * @param {string=} required The widget is considered valid only if value is entered.
+ * @param {comprehension_expression=} ng:options in one of the following forms:
+ *
+ * * for array data sources:
+ * * `label` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * for object data sources:
+ * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`group by`** `group`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ *
+ * Where:
+ *
+ * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
+ * * `value`: local variable which will refer to each item in the `array` or each property value
+ * of `object` during iteration.
+ * * `key`: local variable which will refer to a property name in `object` during iteration.
+ * * `label`: The result of this expression will be the label for `<option>` element. The
+ * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
+ * * `select`: The result of this expression will be bound to the model of the parent `<select>`
+ * element. If not specified, `select` expression will default to `value`.
+ * * `group`: The result of this expression will be used to group options using the `<optgroup>`
+ * DOM element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function MyCntrl(){
+ this.colors = [
+ {name:'black', shade:'dark'},
+ {name:'white', shade:'light'},
+ {name:'red', shade:'dark'},
+ {name:'blue', shade:'dark'},
+ {name:'yellow', shade:'light'}
+ ];
+ this.color = this.colors[2]; // red
+ }
+ </script>
+ <div ng:controller="MyCntrl">
+ <ul>
+ <li ng:repeat="color in colors">
+ Name: <input ng:model="color.name">
+ [<a href ng:click="colors.$remove(color)">X</a>]
+ </li>
+ <li>
+ [<a href ng:click="colors.push({})">add</a>]
+ </li>
+ </ul>
+ <hr/>
+ Color (null not allowed):
+ <select ng:model="color" ng:options="c.name for c in colors"></select><br>
+
+ Color (null allowed):
+ <div class="nullable">
+ <select ng:model="color" ng:options="c.name for c in colors">
+ <option value="">-- chose color --</option>
+ </select>
+ </div><br/>
+
+ Color grouped by shade:
+ <select ng:model="color" ng:options="c.name group by c.shade for c in colors">
+ </select><br/>
+
+
+ Select <a href ng:click="color={name:'not in list'}">bogus</a>.<br>
+ <hr/>
+ Currently selected: {{ {selected_color:color} }}
+ <div style="border:solid 1px black; height:20px"
+ ng:style="{'background-color':color.name}">
+ </div>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:options', function(){
+ expect(binding('color')).toMatch('red');
+ select('color').option('0');
+ expect(binding('color')).toMatch('black');
+ using('.nullable').select('color').option('');
+ expect(binding('color')).toMatch('null');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+
+ //00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
+var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;
+
+
+angularWidget('select', function (element){
+ this.directives(true);
+ this.descend(true);
+ return element.attr('ng:model') && annotate('$formFactory', function($formFactory, selectElement){
+ var modelScope = this,
+ match,
+ form = $formFactory.forElement(selectElement),
+ multiple = selectElement.attr('multiple'),
+ optionsExp = selectElement.attr('ng:options'),
+ modelExp = selectElement.attr('ng:model'),
+ widget = form.$createWidget({
+ scope: this,
+ model: modelExp,
+ onChange: selectElement.attr('ng:change'),
+ alias: selectElement.attr('name'),
+ controller: optionsExp ? Options : (multiple ? Multiple : Single)});
+
+ selectElement.bind('$destroy', function(){ widget.$destroy(); });
+
+ widget.$pristine = !(widget.$dirty = false);
+
+ watchElementProperty(modelScope, widget, 'required', selectElement);
+ watchElementProperty(modelScope, widget, 'readonly', selectElement);
+ watchElementProperty(modelScope, widget, 'disabled', selectElement);
+
+ widget.$on('$validate', function(){
+ var valid = !widget.$required || !!widget.$modelValue;
+ if (valid && multiple && widget.$required) valid = !!widget.$modelValue.length;
+ if (valid !== !widget.$error.REQUIRED) {
+ widget.$emit(valid ? '$valid' : '$invalid', 'REQUIRED');
+ }
+ });
+
+ widget.$on('$viewChange', function(){
+ widget.$pristine = !(widget.$dirty = true);
+ });
+
+ forEach(['valid', 'invalid', 'pristine', 'dirty'], function (name) {
+ widget.$watch('$' + name, function(scope, value) {
+ selectElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ });
+ });
+
+ ////////////////////////////
+
+ function Multiple(){
+ var widget = this;
+
+ this.$render = function(){
+ var items = new HashMap(this.$viewValue);
+ forEach(selectElement.children(), function(option){
+ option.selected = isDefined(items.get(option.value));
+ });
+ };
+
+ selectElement.bind('change', function (){
+ widget.$apply(function(){
+ var array = [];
+ forEach(selectElement.children(), function(option){
+ if (option.selected) {
+ array.push(option.value);
+ }
+ });
+ widget.$emit('$viewChange', array);
+ });
+ });
+
+ }
+
+ function Single(){
+ var widget = this;
+
+ widget.$render = function(){
+ selectElement.val(widget.$viewValue);
+ };
+
+ selectElement.bind('change', function(){
+ widget.$apply(function(){
+ widget.$emit('$viewChange', selectElement.val());
+ });
+ });
+
+ widget.$viewValue = selectElement.val();
+ }
+
+ function Options(){
+ var widget = this,
+ match;
+
+ if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
+ throw Error(
+ "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
+ " but got '" + optionsExp + "'.");
+ }
+
+ var widgetScope = this,
+ displayFn = expressionCompile(match[2] || match[1]),
+ valueName = match[4] || match[6],
+ keyName = match[5],
+ groupByFn = expressionCompile(match[3] || ''),
+ valueFn = expressionCompile(match[2] ? match[1] : valueName),
+ valuesFn = expressionCompile(match[7]),
+ // we can't just jqLite('<option>') since jqLite is not smart enough
+ // to create it in <select> and IE barfs otherwise.
+ optionTemplate = jqLite(document.createElement('option')),
+ optGroupTemplate = jqLite(document.createElement('optgroup')),
+ nullOption = false, // if false then user will not be able to select it
+ // This is an array of array of existing option groups in DOM. We try to reuse these if possible
+ // optionGroupsCache[0] is the options with no option group
+ // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
+ optionGroupsCache = [[{element: selectElement, label:''}]],
+ inChangeEvent;
+
+ // find existing special options
+ forEach(selectElement.children(), function(option){
+ if (option.value == '')
+ // User is allowed to select the null.
+ nullOption = {label:jqLite(option).text(), id:''};
+ });
+ selectElement.html(''); // clear contents
+
+ selectElement.bind('change', function(){
+ widgetScope.$apply(function(){
+ var optionGroup,
+ collection = valuesFn(modelScope) || [],
+ key = selectElement.val(),
+ tempScope = inherit(modelScope),
+ value, optionElement, index, groupIndex, length, groupLength;
+
+ if (multiple) {
+ value = [];
+ for (groupIndex = 0, groupLength = optionGroupsCache.length;
+ groupIndex < groupLength;
+ groupIndex++) {
+ // list of options for that group. (first item has the parent)
+ optionGroup = optionGroupsCache[groupIndex];
+
+ for(index = 1, length = optionGroup.length; index < length; index++) {
+ if ((optionElement = optionGroup[index].element)[0].selected) {
+ if (keyName) tempScope[keyName] = key;
+ tempScope[valueName] = collection[optionElement.val()];
+ value.push(valueFn(tempScope));
+ }
+ }
+ }
+ } else {
+ if (key == '?') {
+ value = undefined;
+ } else if (key == ''){
+ value = null;
+ } else {
+ tempScope[valueName] = collection[key];
+ if (keyName) tempScope[keyName] = key;
+ value = valueFn(tempScope);
+ }
+ }
+ if (isDefined(value) && modelScope.$viewVal !== value) {
+ widgetScope.$emit('$viewChange', value);
+ }
+ });
+ });
+
+ widgetScope.$watch(render);
+ widgetScope.$render = render;
+
+ function render() {
+ var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
+ optionGroupNames = [''],
+ optionGroupName,
+ optionGroup,
+ option,
+ existingParent, existingOptions, existingOption,
+ modelValue = widget.$modelValue,
+ values = valuesFn(modelScope) || [],
+ keys = keyName ? sortedKeys(values) : values,
+ groupLength, length,
+ groupIndex, index,
+ optionScope = inherit(modelScope),
+ selected,
+ selectedSet = false, // nothing is selected yet
+ lastElement,
+ element;
+
+ if (multiple) {
+ selectedSet = new HashMap(modelValue);
+ } else if (modelValue === null || nullOption) {
+ // if we are not multiselect, and we are null then we have to add the nullOption
+ optionGroups[''].push(extend({selected:modelValue === null, id:'', label:''}, nullOption));
+ selectedSet = true;
+ }
+
+ // We now build up the list of options we need (we merge later)
+ for (index = 0; length = keys.length, index < length; index++) {
+ optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index];
+ optionGroupName = groupByFn(optionScope) || '';
+ if (!(optionGroup = optionGroups[optionGroupName])) {
+ optionGroup = optionGroups[optionGroupName] = [];
+ optionGroupNames.push(optionGroupName);
+ }
+ if (multiple) {
+ selected = selectedSet.remove(valueFn(optionScope)) != undefined;
+ } else {
+ selected = modelValue === valueFn(optionScope);
+ selectedSet = selectedSet || selected; // see if at least one item is selected
+ }
+ optionGroup.push({
+ id: keyName ? keys[index] : index, // either the index into array or key from object
+ label: displayFn(optionScope) || '', // what will be seen by the user
+ selected: selected // determine if we should be selected
+ });
+ }
+ if (!multiple && !selectedSet) {
+ // nothing was selected, we have to insert the undefined item
+ optionGroups[''].unshift({id:'?', label:'', selected:true});
+ }
+
+ // Now we need to update the list of DOM nodes to match the optionGroups we computed above
+ for (groupIndex = 0, groupLength = optionGroupNames.length;
+ groupIndex < groupLength;
+ groupIndex++) {
+ // current option group name or '' if no group
+ optionGroupName = optionGroupNames[groupIndex];
+
+ // list of options for that group. (first item has the parent)
+ optionGroup = optionGroups[optionGroupName];
+
+ if (optionGroupsCache.length <= groupIndex) {
+ // we need to grow the optionGroups
+ optionGroupsCache.push(
+ existingOptions = [existingParent = {
+ element: optGroupTemplate.clone().attr('label', optionGroupName),
+ label: optionGroup.label
+ }]
+ );
+ selectElement.append(existingParent.element);
+ } else {
+ existingOptions = optionGroupsCache[groupIndex];
+ existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
+
+ // update the OPTGROUP label if not the same.
+ if (existingParent.label != optionGroupName) {
+ existingParent.element.attr('label', existingParent.label = optionGroupName);
+ }
+ }
+
+ lastElement = null; // start at the begining
+ for(index = 0, length = optionGroup.length; index < length; index++) {
+ option = optionGroup[index];
+ if ((existingOption = existingOptions[index+1])) {
+ // reuse elements
+ lastElement = existingOption.element;
+ if (existingOption.label !== option.label) {
+ lastElement.text(existingOption.label = option.label);
+ }
+ if (existingOption.id !== option.id) {
+ lastElement.val(existingOption.id = option.id);
+ }
+ if (existingOption.selected !== option.selected) {
+ lastElement.prop('selected', (existingOption.selected = option.selected));
+ }
+ } else {
+ // grow elements
+ // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
+ // in this version of jQuery on some browser the .text() returns a string
+ // rather then the element.
+ (element = optionTemplate.clone())
+ .val(option.id)
+ .attr('selected', option.selected)
+ .text(option.label);
+ existingOptions.push(existingOption = {
+ element: element,
+ label: option.label,
+ id: option.id,
+ selected: option.selected
+ });
+ if (lastElement) {
+ lastElement.after(element);
+ } else {
+ existingParent.element.append(element);
+ }
+ lastElement = element;
+ }
+ }
+ // remove any excessive OPTIONs in a group
+ index++; // increment since the existingOptions[0] is parent element not OPTION
+ while(existingOptions.length > index) {
+ existingOptions.pop().element.remove();
+ }
+ }
+ // remove any excessive OPTGROUPs from select
+ while(optionGroupsCache.length > groupIndex) {
+ optionGroupsCache.pop()[0].element.remove();
+ }
+ };
+ }
+ });
+});
diff --git a/src/widgets.js b/src/widgets.js
index 1047c3ce..e3c6906f 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -14,14 +14,11 @@
*
* Following is the list of built-in angular widgets:
*
- * * {@link angular.widget.@ng:format ng:format} - Formats data for display to user and for storage.
* * {@link angular.widget.@ng:non-bindable ng:non-bindable} - Blocks angular from processing an
* HTML element.
* * {@link angular.widget.@ng:repeat ng:repeat} - Creates and manages a collection of cloned HTML
* elements.
- * * {@link angular.widget.@ng:required ng:required} - Verifies presence of user input.
- * * {@link angular.widget.@ng:validate ng:validate} - Validates content of user input.
- * * {@link angular.widget.HTML HTML input elements} - Standard HTML input elements data-bound by
+ * * {@link angular.inputType HTML input elements} - Standard HTML input elements data-bound by
* angular.
* * {@link angular.widget.ng:view ng:view} - Works with $route to "include" partial templates
* * {@link angular.widget.ng:switch ng:switch} - Conditionally changes DOM structure
@@ -34,915 +31,6 @@
/**
* @workInProgress
* @ngdoc widget
- * @name angular.widget.HTML
- *
- * @description
- * The most common widgets you will use will be in the form of the
- * standard HTML set. These widgets are bound using the `name` attribute
- * to an expression. In addition, they can have `ng:validate`, `ng:required`,
- * `ng:format`, `ng:change` attribute to further control their behavior.
- *
- * @usageContent
- * see example below for usage
- *
- * <input type="text|checkbox|..." ... />
- * <textarea ... />
- * <select ...>
- * <option>...</option>
- * </select>
- *
- * @example
- <doc:example>
- <doc:source>
- <table style="font-size:.9em;">
- <tr>
- <th>Name</th>
- <th>Format</th>
- <th>HTML</th>
- <th>UI</th>
- <th ng:non-bindable>{{input#}}</th>
- </tr>
- <tr>
- <th>text</th>
- <td>String</td>
- <td><tt>&lt;input type="text" name="input1"&gt;</tt></td>
- <td><input type="text" name="input1" size="4"></td>
- <td><tt>{{input1|json}}</tt></td>
- </tr>
- <tr>
- <th>textarea</th>
- <td>String</td>
- <td><tt>&lt;textarea name="input2"&gt;&lt;/textarea&gt;</tt></td>
- <td><textarea name="input2" cols='6'></textarea></td>
- <td><tt>{{input2|json}}</tt></td>
- </tr>
- <tr>
- <th>radio</th>
- <td>String</td>
- <td><tt>
- &lt;input type="radio" name="input3" value="A"&gt;<br>
- &lt;input type="radio" name="input3" value="B"&gt;
- </tt></td>
- <td>
- <input type="radio" name="input3" value="A">
- <input type="radio" name="input3" value="B">
- </td>
- <td><tt>{{input3|json}}</tt></td>
- </tr>
- <tr>
- <th>checkbox</th>
- <td>Boolean</td>
- <td><tt>&lt;input type="checkbox" name="input4" value="checked"&gt;</tt></td>
- <td><input type="checkbox" name="input4" value="checked"></td>
- <td><tt>{{input4|json}}</tt></td>
- </tr>
- <tr>
- <th>pulldown</th>
- <td>String</td>
- <td><tt>
- &lt;select name="input5"&gt;<br>
- &nbsp;&nbsp;&lt;option value="c"&gt;C&lt;/option&gt;<br>
- &nbsp;&nbsp;&lt;option value="d"&gt;D&lt;/option&gt;<br>
- &lt;/select&gt;<br>
- </tt></td>
- <td>
- <select name="input5">
- <option value="c">C</option>
- <option value="d">D</option>
- </select>
- </td>
- <td><tt>{{input5|json}}</tt></td>
- </tr>
- <tr>
- <th>multiselect</th>
- <td>Array</td>
- <td><tt>
- &lt;select name="input6" multiple size="4"&gt;<br>
- &nbsp;&nbsp;&lt;option value="e"&gt;E&lt;/option&gt;<br>
- &nbsp;&nbsp;&lt;option value="f"&gt;F&lt;/option&gt;<br>
- &lt;/select&gt;<br>
- </tt></td>
- <td>
- <select name="input6" multiple size="4">
- <option value="e">E</option>
- <option value="f">F</option>
- </select>
- </td>
- <td><tt>{{input6|json}}</tt></td>
- </tr>
- </table>
- </doc:source>
- <doc:scenario>
-
- it('should exercise text', function(){
- input('input1').enter('Carlos');
- expect(binding('input1')).toEqual('"Carlos"');
- });
- it('should exercise textarea', function(){
- input('input2').enter('Carlos');
- expect(binding('input2')).toEqual('"Carlos"');
- });
- it('should exercise radio', function(){
- expect(binding('input3')).toEqual('null');
- input('input3').select('A');
- expect(binding('input3')).toEqual('"A"');
- input('input3').select('B');
- expect(binding('input3')).toEqual('"B"');
- });
- it('should exercise checkbox', function(){
- expect(binding('input4')).toEqual('false');
- input('input4').check();
- expect(binding('input4')).toEqual('true');
- });
- it('should exercise pulldown', function(){
- expect(binding('input5')).toEqual('"c"');
- select('input5').option('d');
- expect(binding('input5')).toEqual('"d"');
- });
- it('should exercise multiselect', function(){
- expect(binding('input6')).toEqual('[]');
- select('input6').options('e');
- expect(binding('input6')).toEqual('["e"]');
- select('input6').options('e', 'f');
- expect(binding('input6')).toEqual('["e","f"]');
- });
- </doc:scenario>
- </doc:example>
- */
-
-function modelAccessor(scope, element) {
- var expr = element.attr('name');
- var exprFn, assignFn;
- if (expr) {
- exprFn = parser(expr).assignable();
- assignFn = exprFn.assign;
- if (!assignFn) throw new Error("Expression '" + expr + "' is not assignable.");
- return {
- get: function() {
- return exprFn(scope);
- },
- set: function(value) {
- if (value !== undefined) {
- assignFn(scope, value);
- }
- }
- };
- }
-}
-
-function modelFormattedAccessor(scope, element) {
- var accessor = modelAccessor(scope, element),
- formatterName = element.attr('ng:format') || NOOP,
- formatter = compileFormatter(formatterName);
- if (accessor) {
- return {
- get: function() {
- return formatter.format(scope, accessor.get());
- },
- set: function(value) {
- return accessor.set(formatter.parse(scope, value));
- }
- };
- }
-}
-
-function compileValidator(expr) {
- return parser(expr).validator()();
-}
-
-function compileFormatter(expr) {
- return parser(expr).formatter()();
-}
-
-/**
- * @workInProgress
- * @ngdoc widget
- * @name angular.widget.@ng:validate
- *
- * @description
- * The `ng:validate` attribute widget validates the user input. If the input does not pass
- * validation, the `ng-validation-error` CSS class and the `ng:error` attribute are set on the input
- * element. Check out {@link angular.validator validators} to find out more.
- *
- * @param {string} validator The name of a built-in or custom {@link angular.validator validator} to
- * to be used.
- *
- * @element INPUT
- * @css ng-validation-error
- *
- * @example
- * This example shows how the input element becomes red when it contains invalid input. Correct
- * the input to make the error disappear.
- *
- <doc:example>
- <doc:source>
- I don't validate:
- <input type="text" name="value" value="NotANumber"><br/>
-
- I need an integer or nothing:
- <input type="text" name="value" ng:validate="integer"><br/>
- </doc:source>
- <doc:scenario>
- it('should check ng:validate', function(){
- expect(element('.doc-example-live :input:last').prop('className')).
- toMatch(/ng-validation-error/);
-
- input('value').enter('123');
- expect(element('.doc-example-live :input:last').prop('className')).
- not().toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- */
-/**
- * @workInProgress
- * @ngdoc widget
- * @name angular.widget.@ng:required
- *
- * @description
- * The `ng:required` attribute widget validates that the user input is present. It is a special case
- * of the {@link angular.widget.@ng:validate ng:validate} attribute widget.
- *
- * @element INPUT
- * @css ng-validation-error
- *
- * @example
- * This example shows how the input element becomes red when it contains invalid input. Correct
- * the input to make the error disappear.
- *
- <doc:example>
- <doc:source>
- I cannot be blank: <input type="text" name="value" ng:required><br/>
- </doc:source>
- <doc:scenario>
- it('should check ng:required', function(){
- expect(element('.doc-example-live :input').prop('className')).
- toMatch(/ng-validation-error/);
- input('value').enter('123');
- expect(element('.doc-example-live :input').prop('className')).
- not().toMatch(/ng-validation-error/);
- });
- </doc:scenario>
- </doc:example>
- */
-/**
- * @workInProgress
- * @ngdoc widget
- * @name angular.widget.@ng:format
- *
- * @description
- * The `ng:format` attribute widget formats stored data to user-readable text and parses the text
- * back to the stored form. You might find this useful, for example, if you collect user input in a
- * text field but need to store the data in the model as a list. Check out
- * {@link angular.formatter formatters} to learn more.
- *
- * @param {string} formatter The name of the built-in or custom {@link angular.formatter formatter}
- * to be used.
- *
- * @element INPUT
- *
- * @example
- * This example shows how the user input is converted from a string and internally represented as an
- * array.
- *
- <doc:example>
- <doc:source>
- Enter a comma separated list of items:
- <input type="text" name="list" ng:format="list" value="table, chairs, plate">
- <pre>list={{list}}</pre>
- </doc:source>
- <doc:scenario>
- it('should check ng:format', function(){
- expect(binding('list')).toBe('list=["table","chairs","plate"]');
- input('list').enter(',,, a ,,,');
- expect(binding('list')).toBe('list=["a"]');
- });
- </doc:scenario>
- </doc:example>
- */
-function valueAccessor(scope, element) {
- var validatorName = element.attr('ng:validate') || NOOP,
- validator = compileValidator(validatorName),
- requiredExpr = element.attr('ng:required'),
- formatterName = element.attr('ng:format') || NOOP,
- formatter = compileFormatter(formatterName),
- format, parse, lastError, required,
- invalidWidgets = scope.$service('$invalidWidgets') || {markValid:noop, markInvalid:noop};
- if (!validator) throw "Validator named '" + validatorName + "' not found.";
- format = formatter.format;
- parse = formatter.parse;
- if (requiredExpr) {
- scope.$watch(requiredExpr, function(scope, newValue) {
- required = newValue;
- validate();
- });
- } else {
- required = requiredExpr === '';
- }
-
- element.data($$validate, validate);
- return {
- get: function(){
- if (lastError)
- elementError(element, NG_VALIDATION_ERROR, null);
- try {
- var value = parse(scope, element.val());
- validate();
- return value;
- } catch (e) {
- lastError = e;
- elementError(element, NG_VALIDATION_ERROR, e);
- }
- },
- set: function(value) {
- var oldValue = element.val(),
- newValue = format(scope, value);
- if (oldValue != newValue) {
- element.val(newValue || ''); // needed for ie
- }
- validate();
- }
- };
-
- function validate() {
- var value = trim(element.val());
- if (element[0].disabled || element[0].readOnly) {
- elementError(element, NG_VALIDATION_ERROR, null);
- invalidWidgets.markValid(element);
- } else {
- var error, validateScope = inherit(scope, {$element:element});
- error = required && !value
- ? 'Required'
- : (value ? validator(validateScope, value) : null);
- elementError(element, NG_VALIDATION_ERROR, error);
- lastError = error;
- if (error) {
- invalidWidgets.markInvalid(element);
- } else {
- invalidWidgets.markValid(element);
- }
- }
- }
-}
-
-function checkedAccessor(scope, element) {
- var domElement = element[0], elementValue = domElement.value;
- return {
- get: function(){
- return !!domElement.checked;
- },
- set: function(value){
- domElement.checked = toBoolean(value);
- }
- };
-}
-
-function radioAccessor(scope, element) {
- var domElement = element[0];
- return {
- get: function(){
- return domElement.checked ? domElement.value : null;
- },
- set: function(value){
- domElement.checked = value == domElement.value;
- }
- };
-}
-
-function optionsAccessor(scope, element) {
- var formatterName = element.attr('ng:format') || NOOP,
- formatter = compileFormatter(formatterName);
- return {
- get: function(){
- var values = [];
- forEach(element[0].options, function(option){
- if (option.selected) values.push(formatter.parse(scope, option.value));
- });
- return values;
- },
- set: function(values){
- var keys = {};
- forEach(values, function(value){
- keys[formatter.format(scope, value)] = true;
- });
- forEach(element[0].options, function(option){
- option.selected = keys[option.value];
- });
- }
- };
-}
-
-function noopAccessor() { return { get: noop, set: noop }; }
-
-/*
- * TODO: refactor
- *
- * The table below is not quite right. In some cases the formatter is on the model side
- * and in some cases it is on the view side. This is a historical artifact
- *
- * The concept of model/view accessor is useful for anyone who is trying to develop UI, and
- * so it should be exposed to others. There should be a form object which keeps track of the
- * accessors and also acts as their factory. It should expose it as an object and allow
- * the validator to publish errors to it, so that the the error messages can be bound to it.
- *
- */
-var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, initWidgetValue(), true),
- INPUT_TYPE = {
- 'text': textWidget,
- 'textarea': textWidget,
- 'hidden': textWidget,
- 'password': textWidget,
- 'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)),
- 'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit),
- 'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(null)),
- 'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([]))
-// 'file': fileWidget???
- };
-
-
-function initWidgetValue(initValue) {
- return function (model, view) {
- var value = view.get();
- if (!value && isDefined(initValue)) {
- value = copy(initValue);
- }
- if (isUndefined(model.get()) && isDefined(value)) {
- model.set(value);
- }
- };
-}
-
-function radioInit(model, view, element) {
- var modelValue = model.get(), viewValue = view.get(), input = element[0];
- input.checked = false;
- input.name = this.$id + '@' + input.name;
- if (isUndefined(modelValue)) {
- model.set(modelValue = null);
- }
- if (modelValue == null && viewValue !== null) {
- model.set(viewValue);
- }
- view.set(modelValue);
-}
-
-/**
- * @workInProgress
- * @ngdoc directive
- * @name angular.directive.ng:change
- *
- * @description
- * The directive executes an expression whenever the input widget changes.
- *
- * @element INPUT
- * @param {expression} expression to execute.
- *
- * @example
- * @example
- <doc:example>
- <doc:source>
- <div ng:init="checkboxCount=0; textCount=0"></div>
- <input type="text" name="text" ng:change="textCount = 1 + textCount">
- changeCount {{textCount}}<br/>
- <input type="checkbox" name="checkbox" ng:change="checkboxCount = 1 + checkboxCount">
- changeCount {{checkboxCount}}<br/>
- </doc:source>
- <doc:scenario>
- it('should check ng:change', function(){
- expect(binding('textCount')).toBe('0');
- expect(binding('checkboxCount')).toBe('0');
-
- using('.doc-example-live').input('text').enter('abc');
- expect(binding('textCount')).toBe('1');
- expect(binding('checkboxCount')).toBe('0');
-
-
- using('.doc-example-live').input('checkbox').check();
- expect(binding('textCount')).toBe('1');
- expect(binding('checkboxCount')).toBe('1');
- });
- </doc:scenario>
- </doc:example>
- */
-function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) {
- return annotate('$defer', function($defer, element) {
- var scope = this,
- model = modelAccessor(scope, element),
- view = viewAccessor(scope, element),
- ngChange = element.attr('ng:change') || noop,
- lastValue;
- if (model) {
- initFn.call(scope, model, view, element);
- scope.$eval(element.attr('ng:init') || noop);
- element.bind(events, function(event){
- function handler(){
- var value = view.get();
- if (!textBox || value != lastValue) {
- model.set(value);
- lastValue = model.get();
- scope.$eval(ngChange);
- }
- }
- event.type == 'keydown' ? $defer(handler) : scope.$apply(handler);
- });
- scope.$watch(model.get, function(scope, value) {
- if (!equals(lastValue, value)) {
- view.set(lastValue = value);
- }
- });
- }
- });
-}
-
-function inputWidgetSelector(element){
- this.directives(true);
- this.descend(true);
- return INPUT_TYPE[lowercase(element[0].type)] || noop;
-}
-
-angularWidget('input', inputWidgetSelector);
-angularWidget('textarea', inputWidgetSelector);
-
-
-/**
- * @workInProgress
- * @ngdoc directive
- * @name angular.directive.ng:options
- *
- * @description
- * Dynamically generate a list of `<option>` elements for a `<select>` element using an array or
- * an object obtained by evaluating the `ng:options` expression.
- *
- * When an item in the select menu is select, the value of array element or object property
- * represented by the selected option will be bound to the model identified by the `name` attribute
- * of the parent select element.
- *
- * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
- * be nested into the `<select>` element. This element will then represent `null` or "not selected"
- * option. See example below for demonstration.
- *
- * Note: `ng:options` provides iterator facility for `<option>` element which must be used instead
- * of {@link angular.widget.@ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with
- * `<option>` element because of the following reasons:
- *
- * * value attribute of the option element that we need to bind to requires a string, but the
- * source of data for the iteration might be in a form of array containing objects instead of
- * strings
- * * {@link angular.widget.@ng:repeat ng:repeat} unrolls after the select binds causing
- * incorect rendering on most browsers.
- * * binding to a value not in list confuses most browsers.
- *
- * @element select
- * @param {comprehension_expression} comprehension in one of the following forms:
- *
- * * for array data sources:
- * * `label` **`for`** `value` **`in`** `array`
- * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
- * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
- * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
- * * for object data sources:
- * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
- * * `select` **`as`** `label` **`group by`** `group`
- * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
- *
- * Where:
- *
- * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
- * * `value`: local variable which will refer to each item in the `array` or each property value
- * of `object` during iteration.
- * * `key`: local variable which will refer to a property name in `object` during iteration.
- * * `label`: The result of this expression will be the label for `<option>` element. The
- * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
- * * `select`: The result of this expression will be bound to the model of the parent `<select>`
- * element. If not specified, `select` expression will default to `value`.
- * * `group`: The result of this expression will be used to group options using the `<optgroup>`
- * DOM element.
- *
- * @example
- <doc:example>
- <doc:source>
- <script>
- function MyCntrl(){
- this.colors = [
- {name:'black', shade:'dark'},
- {name:'white', shade:'light'},
- {name:'red', shade:'dark'},
- {name:'blue', shade:'dark'},
- {name:'yellow', shade:'light'}
- ];
- this.color = this.colors[2]; // red
- }
- </script>
- <div ng:controller="MyCntrl">
- <ul>
- <li ng:repeat="color in colors">
- Name: <input name="color.name">
- [<a href ng:click="colors.$remove(color)">X</a>]
- </li>
- <li>
- [<a href ng:click="colors.push({})">add</a>]
- </li>
- </ul>
- <hr/>
- Color (null not allowed):
- <select name="color" ng:options="c.name for c in colors"></select><br>
-
- Color (null allowed):
- <div class="nullable">
- <select name="color" ng:options="c.name for c in colors">
- <option value="">-- chose color --</option>
- </select>
- </div><br/>
-
- Color grouped by shade:
- <select name="color" ng:options="c.name group by c.shade for c in colors">
- </select><br/>
-
-
- Select <a href ng:click="color={name:'not in list'}">bogus</a>.<br>
- <hr/>
- Currently selected: {{ {selected_color:color} }}
- <div style="border:solid 1px black; height:20px"
- ng:style="{'background-color':color.name}">
- </div>
- </div>
- </doc:source>
- <doc:scenario>
- it('should check ng:options', function(){
- expect(binding('color')).toMatch('red');
- select('color').option('0');
- expect(binding('color')).toMatch('black');
- using('.nullable').select('color').option('');
- expect(binding('color')).toMatch('null');
- });
- </doc:scenario>
- </doc:example>
- */
-// 00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
-var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;
-angularWidget('select', function(element){
- this.descend(true);
- this.directives(true);
-
- var isMultiselect = element.attr('multiple'),
- expression = element.attr('ng:options'),
- onChange = expressionCompile(element.attr('ng:change') || ""),
- match;
-
- if (!expression) {
- return inputWidgetSelector.call(this, element);
- }
- if (! (match = expression.match(NG_OPTIONS_REGEXP))) {
- throw Error(
- "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
- " but got '" + expression + "'.");
- }
-
- var displayFn = expressionCompile(match[2] || match[1]),
- valueName = match[4] || match[6],
- keyName = match[5],
- groupByFn = expressionCompile(match[3] || ''),
- valueFn = expressionCompile(match[2] ? match[1] : valueName),
- valuesFn = expressionCompile(match[7]),
- // we can't just jqLite('<option>') since jqLite is not smart enough
- // to create it in <select> and IE barfs otherwise.
- optionTemplate = jqLite(document.createElement('option')),
- optGroupTemplate = jqLite(document.createElement('optgroup')),
- nullOption = false; // if false then user will not be able to select it
-
- return function(selectElement){
-
- // This is an array of array of existing option groups in DOM. We try to reuse these if possible
- // optionGroupsCache[0] is the options with no option group
- // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
- var optionGroupsCache = [[{element: selectElement, label:''}]],
- scope = this,
- model = modelAccessor(scope, element),
- inChangeEvent;
-
- // find existing special options
- forEach(selectElement.children(), function(option){
- if (option.value == '')
- // User is allowed to select the null.
- nullOption = {label:jqLite(option).text(), id:''};
- });
- selectElement.html(''); // clear contents
-
- selectElement.bind('change', function(){
- var optionGroup,
- collection = valuesFn(scope) || [],
- key = selectElement.val(),
- tempScope = scope.$new(),
- value, optionElement, index, groupIndex, length, groupLength;
-
- // let's set a flag that the current model change is due to a change event.
- // the default action of option selection will cause the appropriate option element to be
- // deselected and another one to be selected - there is no need for us to be updating the DOM
- // in this case.
- inChangeEvent = true;
-
- try {
- if (isMultiselect) {
- value = [];
- for (groupIndex = 0, groupLength = optionGroupsCache.length;
- groupIndex < groupLength;
- groupIndex++) {
- // list of options for that group. (first item has the parent)
- optionGroup = optionGroupsCache[groupIndex];
-
- for(index = 1, length = optionGroup.length; index < length; index++) {
- if ((optionElement = optionGroup[index].element)[0].selected) {
- if (keyName) tempScope[keyName] = key;
- tempScope[valueName] = collection[optionElement.val()];
- value.push(valueFn(tempScope));
- }
- }
- }
- } else {
- if (key == '?') {
- value = undefined;
- } else if (key == ''){
- value = null;
- } else {
- tempScope[valueName] = collection[key];
- if (keyName) tempScope[keyName] = key;
- value = valueFn(tempScope);
- }
- }
- if (isDefined(value) && model.get() !== value) {
- model.set(value);
- onChange(scope);
- }
- scope.$root.$apply();
- } finally {
- tempScope = null; // TODO(misko): needs to be $destroy
- inChangeEvent = false;
- }
- });
-
- scope.$watch(function(scope) {
- var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
- optionGroupNames = [''],
- optionGroupName,
- optionGroup,
- option,
- existingParent, existingOptions, existingOption,
- values = valuesFn(scope) || [],
- keys = values,
- key,
- groupLength, length,
- fragment,
- groupIndex, index,
- optionElement,
- optionScope = scope.$new(),
- modelValue = model.get(),
- selected,
- selectedSet = false, // nothing is selected yet
- isMulti = isMultiselect,
- lastElement,
- element;
-
- try {
- if (isMulti) {
- selectedSet = new HashMap();
- if (modelValue && isNumber(length = modelValue.length)) {
- for (index = 0; index < length; index++) {
- selectedSet.put(modelValue[index], true);
- }
- }
- } else if (modelValue === null || nullOption) {
- // if we are not multiselect, and we are null then we have to add the nullOption
- optionGroups[''].push(extend({selected:modelValue === null, id:'', label:''}, nullOption));
- selectedSet = true;
- }
-
- // If we have a keyName then we are iterating over on object. Grab the keys and sort them.
- if(keyName) {
- keys = [];
- for (key in values) {
- if (values.hasOwnProperty(key))
- keys.push(key);
- }
- keys.sort();
- }
-
- // We now build up the list of options we need (we merge later)
- for (index = 0; length = keys.length, index < length; index++) {
- optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index];
- optionGroupName = groupByFn(optionScope) || '';
- if (!(optionGroup = optionGroups[optionGroupName])) {
- optionGroup = optionGroups[optionGroupName] = [];
- optionGroupNames.push(optionGroupName);
- }
- if (isMulti) {
- selected = !!selectedSet.remove(valueFn(optionScope));
- } else {
- selected = modelValue === valueFn(optionScope);
- selectedSet = selectedSet || selected; // see if at least one item is selected
- }
- optionGroup.push({
- id: keyName ? keys[index] : index, // either the index into array or key from object
- label: displayFn(optionScope) || '', // what will be seen by the user
- selected: selected // determine if we should be selected
- });
- }
- optionGroupNames.sort();
- if (!isMulti && !selectedSet) {
- // nothing was selected, we have to insert the undefined item
- optionGroups[''].unshift({id:'?', label:'', selected:true});
- }
-
- // Now we need to update the list of DOM nodes to match the optionGroups we computed above
- for (groupIndex = 0, groupLength = optionGroupNames.length;
- groupIndex < groupLength;
- groupIndex++) {
- // current option group name or '' if no group
- optionGroupName = optionGroupNames[groupIndex];
-
- // list of options for that group. (first item has the parent)
- optionGroup = optionGroups[optionGroupName];
-
- if (optionGroupsCache.length <= groupIndex) {
- // we need to grow the optionGroups
- optionGroupsCache.push(
- existingOptions = [
- existingParent = {
- element: optGroupTemplate.clone().attr('label', optionGroupName),
- label: optionGroup.label
- }
- ]
- );
- selectElement.append(existingParent.element);
- } else {
- existingOptions = optionGroupsCache[groupIndex];
- existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
-
- // update the OPTGROUP label if not the same.
- if (existingParent.label != optionGroupName) {
- existingParent.element.attr('label', existingParent.label = optionGroupName);
- }
- }
-
- lastElement = null; // start at the begining
- for(index = 0, length = optionGroup.length; index < length; index++) {
- option = optionGroup[index];
- if ((existingOption = existingOptions[index+1])) {
- // reuse elements
- lastElement = existingOption.element;
- if (existingOption.label !== option.label) {
- lastElement.text(existingOption.label = option.label);
- }
- if (existingOption.id !== option.id) {
- lastElement.val(existingOption.id = option.id);
- }
- if (!inChangeEvent && existingOption.selected !== option.selected) {
- lastElement.prop('selected', (existingOption.selected = option.selected));
- }
- } else {
- // grow elements
- // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
- // in this version of jQuery on some browser the .text() returns a string
- // rather then the element.
- (element = optionTemplate.clone())
- .val(option.id)
- .attr('selected', option.selected)
- .text(option.label);
- existingOptions.push(existingOption = {
- element: element,
- label: option.label,
- id: option.id,
- selected: option.selected
- });
- if (lastElement) {
- lastElement.after(element);
- } else {
- existingParent.element.append(element);
- }
- lastElement = element;
- }
- }
- // remove any excessive OPTIONs in a group
- index++; // increment since the existingOptions[0] is parent element not OPTION
- while(existingOptions.length > index) {
- existingOptions.pop().element.remove();
- }
- }
- // remove any excessive OPTGROUPs from select
- while(optionGroupsCache.length > groupIndex) {
- optionGroupsCache.pop()[0].element.remove();
- }
- } finally {
- optionScope.$destroy();
- }
- });
- };
-});
-
-
-/**
- * @workInProgress
- * @ngdoc widget
* @name angular.widget.ng:include
*
* @description
@@ -960,28 +48,36 @@ angularWidget('select', function(element){
* @example
<doc:example>
<doc:source jsfiddle="false">
- <select name="url">
- <option value="examples/ng-include/template1.html">template1.html</option>
- <option value="examples/ng-include/template2.html">template2.html</option>
- <option value="">(blank)</option>
- </select>
- url of the template: <tt><a href="{{url}}">{{url}}</a></tt>
- <hr/>
- <ng:include src="url"></ng:include>
+ <script>
+ function Ctrl(){
+ this.templates =
+ [ { name: 'template1.html', url: 'examples/ng-include/template1.html'}
+ , { name: 'template2.html', url: 'examples/ng-include/template2.html'} ];
+ this.template = this.templates[0];
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <select ng:model="template" ng:options="t.name for t in templates">
+ <option value="">(blank)</option>
+ </select>
+ url of the template: <tt><a href="{{template.url}}">{{template.url}}</a></tt>
+ <hr/>
+ <ng:include src="template.url"></ng:include>
+ </div>
</doc:source>
<doc:scenario>
it('should load template1.html', function(){
- expect(element('.doc-example-live ng\\:include').text()).
+ expect(element('.doc-example-live .ng-include').text()).
toBe('Content of template1.html\n');
});
it('should load template2.html', function(){
- select('url').option('examples/ng-include/template2.html');
- expect(element('.doc-example-live ng\\:include').text()).
+ select('template').option('1');
+ expect(element('.doc-example-live .ng-include').text()).
toBe('Content of template2.html\n');
});
it('should change to blank', function(){
- select('url').option('');
- expect(element('.doc-example-live ng\\:include').text()).toEqual('');
+ select('template').option('');
+ expect(element('.doc-example-live .ng-include').text()).toEqual('');
});
</doc:scenario>
</doc:example>
@@ -1064,30 +160,34 @@ angularWidget('ng:include', function(element){
* @example
<doc:example>
<doc:source>
- <select name="switch">
- <option>settings</option>
- <option>home</option>
- <option>other</option>
- </select>
- <tt>switch={{switch}}</tt>
- </hr>
- <ng:switch on="switch" >
- <div ng:switch-when="settings">Settings Div</div>
- <span ng:switch-when="home">Home Span</span>
- <span ng:switch-default>default</span>
- </ng:switch>
- </code>
+ <script>
+ function Ctrl(){
+ this.items = ['settings', 'home', 'other'];
+ this.selection = this.items[0];
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <select ng:model="selection" ng:options="item for item in items">
+ </select>
+ <tt>selection={{selection}}</tt>
+ <hr/>
+ <ng:switch on="selection" >
+ <div ng:switch-when="settings">Settings Div</div>
+ <span ng:switch-when="home">Home Span</span>
+ <span ng:switch-default>default</span>
+ </ng:switch>
+ </div>
</doc:source>
<doc:scenario>
it('should start in settings', function(){
expect(element('.doc-example-live ng\\:switch').text()).toEqual('Settings Div');
});
it('should change to home', function(){
- select('switch').option('home');
+ select('selection').option('home');
expect(element('.doc-example-live ng\\:switch').text()).toEqual('Home Span');
});
it('should select deafault', function(){
- select('switch').option('other');
+ select('selection').option('other');
expect(element('.doc-example-live ng\\:switch').text()).toEqual('default');
});
</doc:scenario>
@@ -1568,27 +668,36 @@ angularWidget('ng:view', function(element) {
* @example
<doc:example>
<doc:source>
- Person 1:<input type="text" name="person1" value="Igor" /><br/>
- Person 2:<input type="text" name="person2" value="Misko" /><br/>
- Number of People:<input type="text" name="personCount" value="1" /><br/>
-
- <!--- Example with simple pluralization rules for en locale --->
- Without Offset:
- <ng:pluralize count="personCount"
- when="{'0': 'Nobody is viewing.',
- 'one': '1 person is viewing.',
- 'other': '{} people are viewing.'}">
- </ng:pluralize><br>
-
- <!--- Example with offset --->
- With Offset(2):
- <ng:pluralize count="personCount" offset=2
- when="{'0': 'Nobody is viewing.',
- '1': '{{person1}} is viewing.',
- '2': '{{person1}} and {{person2}} are viewing.',
- 'one': '{{person1}}, {{person2}} and one other person are viewing.',
- 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
- </ng:pluralize>
+ <script>
+ function Ctrl(){
+ this.person1 = 'Igor';
+ this.person2 = 'Misko';
+ this.personCount = 1;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Person 1:<input type="text" ng:model="person1" value="Igor" /><br/>
+ Person 2:<input type="text" ng:model="person2" value="Misko" /><br/>
+ Number of People:<input type="text" ng:model="personCount" value="1" /><br/>
+
+ <!--- Example with simple pluralization rules for en locale --->
+ Without Offset:
+ <ng:pluralize count="personCount"
+ when="{'0': 'Nobody is viewing.',
+ 'one': '1 person is viewing.',
+ 'other': '{} people are viewing.'}">
+ </ng:pluralize><br>
+
+ <!--- Example with offset --->
+ With Offset(2):
+ <ng:pluralize count="personCount" offset=2
+ when="{'0': 'Nobody is viewing.',
+ '1': '{{person1}} is viewing.',
+ '2': '{{person1}} and {{person2}} are viewing.',
+ 'one': '{{person1}}, {{person2}} and one other person are viewing.',
+ 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
+ </ng:pluralize>
+ </div>
</doc:source>
<doc:scenario>
it('should show correct pluralized string', function(){
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>');