From 4f78fd692c0ec51241476e6be9a4df06cd62fdd6 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 8 Sep 2011 13:56:29 -0700 Subject: [PATCH] feat(forms): new and improved forms --- Rakefile | 61 +- angularFiles.js | 8 +- css/angular.css | 9 - docs/content/api/angular.inputType.ngdoc | 92 + docs/content/api/angular.service.ngdoc | 2 - docs/content/api/index.ngdoc | 2 - docs/content/cookbook/advancedform.ngdoc | 55 +- docs/content/cookbook/buzz.ngdoc | 3 +- docs/content/cookbook/form.ngdoc | 37 +- docs/content/cookbook/helloworld.ngdoc | 13 +- .../guide/dev_guide.compiler.directives.ngdoc | 2 +- .../content/guide/dev_guide.expressions.ngdoc | 27 +- docs/content/guide/dev_guide.forms.ngdoc | 610 +++++ ...v_guide.mvc.understanding_controller.ngdoc | 4 +- .../dev_guide.mvc.understanding_model.ngdoc | 2 +- docs/content/guide/dev_guide.overview.ngdoc | 44 +- .../guide/dev_guide.services.$location.ngdoc | 2 +- ...guide.services.injecting_controllers.ngdoc | 4 +- .../dev_guide.templates.css-styling.ngdoc | 52 +- ...e.templates.filters.creating_filters.ngdoc | 26 +- ...lates.formatters.creating_formatters.ngdoc | 55 - .../dev_guide.templates.formatters.ngdoc | 20 - ...emplates.formatters.using_formatters.ngdoc | 9 - docs/content/guide/dev_guide.templates.ngdoc | 9 +- ...lates.validators.creating_validators.ngdoc | 82 - .../dev_guide.templates.validators.ngdoc | 131 - docs/content/guide/index.ngdoc | 3 +- docs/content/misc/started.ngdoc | 2 +- docs/content/tutorial/step_03.ngdoc | 2 +- docs/content/tutorial/step_04.ngdoc | 4 +- docs/content/tutorial/step_07.ngdoc | 4 +- docs/content/tutorial/step_09.ngdoc | 2 +- docs/examples/settings.html | 8 +- docs/img/form_data_flow.png | Bin 0 -> 55400 bytes docs/spec/ngdocSpec.js | 36 +- docs/src/ngdoc.js | 104 +- docs/src/templates/doc_widgets.js | 8 +- docs/src/templates/docs.css | 12 +- docs/src/templates/index.html | 2 +- example/buzz/buzz.html | 4 +- example/personalLog/personalLog.html | 2 +- example/tweeter/tweeter_addressbook.html | 12 +- example/tweeter/tweeter_demo.html | 2 +- gen_docs.sh | 2 +- i18n/e2e/localeTest_cs.html | 9 +- i18n/e2e/localeTest_de.html | 9 +- i18n/e2e/localeTest_en.html | 21 +- i18n/e2e/localeTest_es.html | 9 +- i18n/e2e/localeTest_sk.html | 12 +- i18n/e2e/localeTest_zh.html | 21 +- images/docs/guide/form_data_flow.graffle | 2301 +++++++++++++++++ regression/filter_repeater.html | 2 +- regression/issue-169.html | 4 +- regression/issue-352.html | 6 +- regression/issue-353.html | 2 +- regression/sanitizer.html | 2 +- src/Angular.js | 97 +- src/Browser.js | 10 +- src/Scope.js | 3 +- src/angular-bootstrap.js | 3 - src/apis.js | 171 +- src/directives.js | 86 +- src/filters.js | 113 +- src/formatters.js | 202 -- src/jqLite.js | 15 +- src/markups.js | 24 +- src/parser.js | 43 +- src/scenario/Scenario.js | 2 +- src/scenario/dsl.js | 22 +- src/service/formFactory.js | 394 +++ src/service/invalidWidgets.js | 69 - src/service/log.js | 3 +- src/service/resource.js | 3 +- src/service/route.js | 6 +- src/service/window.js | 2 +- src/service/xhr.js | 5 +- src/validators.js | 482 ---- src/widget/form.js | 81 + src/widget/input.js | 773 ++++++ src/widget/select.js | 427 +++ src/widgets.js | 1029 +------- test/AngularSpec.js | 39 +- test/ApiSpecs.js | 7 + test/BinderSpec.js | 135 +- test/BrowserSpecs.js | 10 - test/FormattersSpec.js | 45 - test/JsonSpec.js | 4 + test/ParserSpec.js | 19 - test/ScopeSpec.js | 19 +- test/ValidatorsSpec.js | 172 -- test/directivesSpec.js | 11 + test/jQueryPatchSpec.js | 57 + test/jqLiteSpec.js | 32 +- test/markupSpec.js | 20 +- test/scenario/dslSpec.js | 63 +- test/scenario/e2e/widgets.html | 18 +- test/service/formFactorySpec.js | 218 ++ test/service/invalidWidgetsSpec.js | 41 - test/service/routeSpec.js | 20 +- test/testabilityPatch.js | 70 +- test/widget/formSpec.js | 97 + test/widget/inputSpec.js | 547 ++++ test/widget/selectSpec.js | 510 ++++ test/widgetsSpec.js | 820 +----- 104 files changed, 7044 insertions(+), 3963 deletions(-) create mode 100644 docs/content/api/angular.inputType.ngdoc create mode 100644 docs/content/guide/dev_guide.forms.ngdoc delete mode 100644 docs/content/guide/dev_guide.templates.formatters.creating_formatters.ngdoc delete mode 100644 docs/content/guide/dev_guide.templates.formatters.ngdoc delete mode 100644 docs/content/guide/dev_guide.templates.formatters.using_formatters.ngdoc delete mode 100644 docs/content/guide/dev_guide.templates.validators.creating_validators.ngdoc delete mode 100644 docs/content/guide/dev_guide.templates.validators.ngdoc create mode 100644 docs/img/form_data_flow.png create mode 100644 images/docs/guide/form_data_flow.graffle delete mode 100644 src/formatters.js create mode 100644 src/service/formFactory.js delete mode 100644 src/service/invalidWidgets.js delete mode 100644 src/validators.js create mode 100644 src/widget/form.js create mode 100644 src/widget/input.js create mode 100644 src/widget/select.js delete mode 100644 test/FormattersSpec.js delete mode 100644 test/ValidatorsSpec.js create mode 100644 test/jQueryPatchSpec.js create mode 100644 test/service/formFactorySpec.js delete mode 100644 test/service/invalidWidgetsSpec.js create mode 100644 test/widget/formSpec.js create mode 100644 test/widget/inputSpec.js create mode 100644 test/widget/selectSpec.js 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 `` element has `required` attribute. This means that the + widget will have `REQUIRED` validation error if empty. + - **`$disabled`**: When the `` element has `disabled` attribute. + - **`$readonly`**: When the `` 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 + + + + +
+
+
+ Required:
+ Disabled:
+ Readonly:
+
data={{data}}
+
myForm={{myForm}}
+
+
+
+ + 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'); + }); + +
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.
-
-

+
-
-
- , - -

+
+

- - [ add ] -
- - - [ X ] -
- - +
+
+ , + +

+ + + [ add ] +
+ + + [ X ] +
+ + +

Debug View: @@ -90,7 +91,7 @@ master.$equals(form)}}">Save 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'); }); 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.
- +
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.

-

+


-
- , - -

+
+ , + +

[ add ]
- - + [ X ]

@@ -68,19 +69,21 @@ ng:validate="regexp:zip"/>

}); 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/); }); @@ -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 @@ - Your name: -
- Hello {{name}}! + +
+ Your name: +
+ Hello {{name}}! +
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: - + 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: -
+ +
Expression: - +
  • @@ -84,9 +90,18 @@ the global state (a common source of subtle bugs). -
    - Name: - + +
    + Name: +
    @@ -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'}]">
    - Search: + Search: diff --git a/docs/content/guide/dev_guide.forms.ngdoc b/docs/content/guide/dev_guide.forms.ngdoc new file mode 100644 index 00000000..6849ff4e --- /dev/null +++ b/docs/content/guide/dev_guide.forms.ngdoc @@ -0,0 +1,610 @@ +@ngdoc overview +@name Developer Guide: Forms +@description + +# Overview + +Forms allow users to enter data into your application. Forms represent the bidirectional data +bindings in Angular. + +Forms consist of all of the following: + + - the individual widgets with which users interact + - the validation rules for widgets + - the form, a collection of widgets that contains aggregated validation information + + +# Form + +A form groups a set of widgets together into a single logical data-set. A form is created using +the {@link api/angular.widget.form <form>} element that calls the +{@link api/angular.service.$formFactory $formFactory} service. The form is responsible for managing +the widgets and for tracking validation information. + +A form is: + +- The collection which contains widgets or other forms. +- Responsible for marshaling data from the model into a widget. This is + triggered by {@link api/angular.scope.$watch $watch} of the model expression. +- Responsible for marshaling data from the widget into the model. This is + triggered by the widget emitting the `$viewChange` event. +- Responsible for updating the validation state of the widget, when the widget emits + `$valid` / `$invalid` event. The validation state is useful for controlling the validation + errors shown to the user in it consist of: + + - `$valid` / `$invalid`: Complementary set of booleans which show if a widget is valid / invalid. + - `$error`: an object which has a property for each validation key emited by the widget. + The value of the key is always true. If widget is valid, then the `$error` + object has no properties. For example if the widget emits + `$invalid` event with `REQUIRED` key. The internal state of the `$error` would be + updated to `$error.REQUIRED == true`. + +- Responsible for aggregating widget validation information into the form. + + - `$valid` / `$invalid`: Complementary set of booleans which show if all the child widgets + (or forms) are valid or if any are invalid. + - `$error`: an object which has a property for each validation key emited by the + child widget. The value of the key is an array of widgets which fired the invalid + event. If all child widgets are valid then, then the `$error` object has no + properties. For example if a child widget emits + `$invalid` event with `REQUIRED` key. The internal state of the `$error` would be + updated to `$error.REQUIRED == [ widgetWhichEmitedInvalid ]`. + + +# Widgets + +In Angular, a widget is the term used for the UI with which the user input. Examples of +bult-in Angular widgets are {@link api/angular.widget.input input} and +{@link api/angular.widget.select select}. Widgets provide the rendering and the user +interaction logic. Widgets should be declared inside a form, if no form is provided an implicit +form {@link api/angular.service.$formFactory $formFactory.rootForm} form is used. + +Widgets are implemented as Angular controllers. A widget controller: + +- implements methods: + + - `$render` - Updates the DOM from the internal state as represented by `$viewValue`. + - `$parseView` - Translate `$viewValue` to `$modelValue`. (`$modelValue` will be assigned to + the model scope by the form) + - `$parseModel` - Translate `$modelValue` to `$viewValue`. (`$viewValue` will be assigned to + the DOM inside the `$render` method) + +- responds to events: + + - `$validate` - Emitted by the form when the form determines that the widget needs to validate + itself. There may be more then one listener on the `$validate` event. The widget responds + by emitting `$valid` / `$invalid` event of its own. + +- emits events: + + - `$viewChange` - Emitted when the user interacts with the widget and it is necessary to update + the model. + - `$valid` - Emitted when the widget determines that it is valid (usually as a response to + `$validate` event or inside `$parseView()` or `$parseModel()` method). + - `$invalid` - Emitted when the widget determines that it is invalid (usually as a response to + `$validate` event or inside `$parseView()` or `$parseModel()` method). + - `$destroy` - Emitted when the widget element is removed from the DOM. + + +# CSS + +Angular-defined widgets and forms set `ng-valid` and `ng-invalid` classes on themselves to allow +the web-designer a way to style them. If you write your own widgets, then their `$render()` +methods must set the appropriate CSS classes to allow styling. +(See {@link dev_guide.templates.css-styling CSS}) + + +# Example + +The following example demonstrates: + + - How an error is displayed when a required field is empty. + - Error highlighting. + - How form submission is disabled when the form is invalid. + - The internal state of the widget and form in the the 'Debug View' area. + + + + + + +
    + +
    + +
    + + + Customer name is required! +

    + + +
    +
    + , + +

    + + + Incomplete address: +
    + Missing state! +
    + Invalid state! +
    + Missing zip! +
    + Invalid zip! + + + + + + + +
    + Debug View: +
    form={{form}}
    +
    master={{master}}
    +
    userForm={{userForm}}
    +
    addressForm={{addressForm}}
    +
    + + + 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'); + }); + + + +# Life-cycle + +- The `
    ` 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 + `` 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. + +- `` 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. + + + + +- 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. + + + + + +
    +
    + HTML:
    + +
    +
    editorForm = {{editorForm}}
    + +
    + + 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/); + }); + +
    + + + +# 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. + + + +
    NamePhone
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameFormatHTMLUI{{input#}}
    textString<input type="text" ng:model="input1">{{input1|json}}
    textareaString<textarea ng:model="input2"></textarea>{{input2|json}}
    radioString + <input type="radio" ng:model="input3" value="A">
    + <input type="radio" ng:model="input3" value="B"> +
    + + + {{input3|json}}
    checkboxBoolean<input type="checkbox" ng:model="input4">{{input4|json}}
    pulldownString + <select ng:model="input5">
    +   <option value="c">C</option>
    +   <option value="d">D</option>
    + </select>
    +
    + + {{input5|json}}
    multiselectArray + <select ng:model="input6" multiple size="4">
    +   <option value="e">E</option>
    +   <option value="f">F</option>
    + </select>
    +
    + + {{input6|json}}
    +
    + + + 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"]'); + }); + +
    + +#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. +
    +  
    +
    + + +
    +
    + +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. +
    +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);
    +  });
    +});
    +
    + +## Custom widgets + +This example demonstrates a login form, where the password has custom validation rules. +
    +  
    +
    + + +
    +
    + +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. +
    +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('');
    +
    +    // 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);
    +  });
    +});
    +
    + + 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.
     
    - 
    + 
      
      
      

    The food is {{spice}} spicy!

    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: - + 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: - Invoice: -
    -
    - - - - - - - -
    QuantityCost
    -
    - Total: {{qty * cost | currency}} + +
    + Invoice: +
    +
    + + + + + + + +
    QuantityCost
    +
    + Total: {{qty * cost | currency}} +
    - +
     // 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'];
     
     

    Let's try this simple notify service, injected into the controller...

    - +
    it('should test service', function(){ - expect(element(':input[name=message]').val()).toEqual('test'); + expect(element(':input[ng\\:model="message"]').val()).toEqual('test'); }); 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 -
    Error message
    +* `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: - -
    -
    -
    +* 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'; + } -
    -No filter: {{text}}
    -Reverse: {{text|reverse}}
    -Reverse + uppercase: {{text|reverse:true}}
    -Reverse + uppercase + blue: {{text|reverse:true:"blue"}} +
    +
    + No filter: {{greeting}}
    + Reverse: {{greeting|reverse}}
    + Reverse + uppercase: {{greeting|reverse:true}}
    + Reverse + uppercase + blue: {{greeting|reverse:true:"blue"}} +
    -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'); + }); 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. - -
    -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();
    -}
    -});
    -
    - - - - - 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 `` tag. - -
    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}:
     
      
      
    -   
    +   
        
    @@ -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:
    -
    -
    -angular.validator('your_validator', function(input [,additional params]) {
    -        [your validation code];
    -        if ( [validation succeeds] ) {
    -                return false;
    -        } else {
    -                return true; // No error message specified
    -                         }
    -}
    -
    - -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: - - - - - - - -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/); -}); - - - -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 - -
    -
    -
    - -* Validators in JavaScript - -
    -angular.validator.[validator_type](parameters)
    -
    - -The following example shows how to use the built-in angular integer validator: - - - - Change me: - - - 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/); - }); - - - -# 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: - -
    -angular.validator('your_validator', function(input [,additional params]) {
    -        [your validation code];
    -        if ( [validation succeeds] ) {
    -                return false;
    -        } else {
    -                return true; // No error message specified
    -                          }
    -}
    -
    - -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: - - - - - - - -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/); -}); - - - -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: - Your name: + Your name:
    Hello {{yourname}}!
    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`:__
     ...
    -   Fulltext Search: 
    +   Fulltext Search: 
     
       
    • 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`:__ ...
      • - Search: + Search:
      • Sort by: - 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`:__
         
        • - Search: + Search:
        • Sort by: - 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: - Uppercased: {{ userInput | uppercase }} + 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 @@ - +
          - - + [ X ]
          @@ -15,4 +15,4 @@
          - \ No newline at end of file + diff --git a/docs/img/form_data_flow.png b/docs/img/form_data_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..60e947a595838afb84a6023f08cb97dd19c62e11 GIT binary patch literal 55400 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 ZKe*R`75LgTS2fOEQ-f+NS1MYE{SUk;A0Ge! literal 0 HcmV?d00001 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\n' + - '' + + '' + '\ngit bla bla\n\n' + '')).toEqual( '

          before

          \n' + - '\n' + + '\n' + 'git bla bla\n' + '\n' + ''); @@ -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){ + html_usage_inputType: 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(''); }); - - 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){ - var self = this; - dom.h('Usage', function(){ - dom.h('In HTML Template Binding', function(){ - dom.code(function(){ - 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(')'); - }); - }); - - 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('>' + - '' + - '' + - ' +
@@ -57,7 +57,7 @@ tweets={{tweets}}

Tweets: {{$anchor.user}}

- [ Filter: + [ Filter: | << All ]
Loading...
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)

Tweets: {{$anchor.user}}

- [ Filter: (TODO: this should act as search box) + [ Filter: (TODO: this should act as search box) | << All ]
Loading...
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 @@ locale test + - -
+ +
date: {{input | date:"medium"}}
date: {{input | date:"longDate"}}
number: {{input | number}}
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 @@ locale test + - -
+ +
date: {{input | date:"medium"}}
date: {{input | date:"longDate"}}
number: {{input | number}}
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 @@ + - +

Datetime/Number/Currency filters demo:

-
+
date(medium): {{input | date:"medium"}}
date(longDate): {{input | date:"longDate"}}
number: {{input | number}}
currency: {{input | currency }}

Pluralization demo:

-
+

- Name of person2:
-
+ Name of person1:
+ Name of person2:
+
+ - -
+ +
date: {{input | date:"medium"}}
date: {{input | date:"longDate"}}
number: {{input | number}}
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 @@ locale test + - -
+ +
date: {{input | date:"medium"}}
date: {{input | date:"longDate"}}
number: {{input | number}}
currency: {{input | currency }}
-
+
+ - +

Datetime/Number/Currency filters demo:

-
+
date(medium): {{input | date:"medium"}}
date(longDate): {{input | date:"longDate"}}
number: {{input | number}}
currency: {{input | currency }}

Pluralization demo:

-
+

Pluralization demo with offsets:

- Name of person1:
- Name of person2:
-
+ Name of person1:
+ Name of person2:
+
+ + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 138.30.0.155892 + + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2011-10-05 20:45:08 -0700 + Creator + Miško Hevery + DisplayScale + 1 0/72 in = 1 0/72 in + GraphDocumentVersion + 6 + GraphicsList + + + Bounds + {{107, 265.5}, {65, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 28 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\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} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ControlPoints + + {0, 0} + {0, 29} + {4.57764e-05, -29.0001} + {0, 0} + {0, 0} + {0, 0} + + Head + + ID + 5 + Info + 8 + + ID + 29 + Points + + {223, 272.5} + {179, 270} + {223, 273} + {223, 272.5} + + Style + + stroke + + Bezier + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 5 + Info + 8 + + + + Bounds + {{334, 405.5}, {136, 44}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 22 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\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} + VerticalPad + 0 + + + + Bounds + {{330, 189.25}, {124, 66.5}} + Class + ShapedGraphic + ID + 21 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\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)} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{151, 215.5}, {65, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 19 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\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()} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{330, 315}, {87, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 17 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\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()} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{121, 315}, {94, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 16 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\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()} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{164, 414.5}, {51, 28}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 15 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\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} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 8 + Info + 4 + + ID + 14 + Points + + {229.332, 257.285} + {216, 222} + {229.332, 186.715} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 5 + Info + 3 + + + + Class + LineGraphic + Head + + ID + 5 + Info + 4 + + ID + 13 + Points + + {229.332, 350.285} + {219, 320} + {229.332, 287.715} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 4 + Info + 3 + + + + Class + LineGraphic + Head + + ID + 4 + Info + 4 + + ID + 12 + Points + + {229.332, 479.49} + {214, 425.705} + {229.332, 380.715} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 3 + Info + 3 + + + + Class + LineGraphic + Head + + ID + 3 + Info + 2 + + ID + 11 + Points + + {313.668, 380.715} + {329, 418.705} + {313.668, 479.49} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 4 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 4 + + ID + 10 + Points + + {313.668, 287.715} + {325, 321} + {313.668, 350.285} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 5 + + + + Class + LineGraphic + Head + + ID + 5 + Info + 2 + + ID + 9 + Points + + {313.668, 186.715} + {325, 218} + {313.668, 257.285} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 8 + Info + 1 + + + + Bounds + {{223, 154}, {97, 35}} + Class + ShapedGraphic + ID + 8 + Magnets + + {1, 1} + {1, -1} + {-1, -1} + {-1, 1} + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.302239 + g + 0.746867 + r + 0.964157 + + + shadow + + Beneath + YES + + stroke + + CornerRadius + 14 + + + Text + + Text + {\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} + + + + Bounds + {{223, 255}, {97, 35}} + Class + ShapedGraphic + ID + 5 + Magnets + + {1, 1} + {1, -1} + {-1, -1} + {-1, 1} + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.59983 + g + 0.937216 + r + 0.609412 + + + shadow + + Beneath + YES + + stroke + + CornerRadius + 14 + + + Text + + Text + {\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} + + + + Bounds + {{223, 348}, {97, 35}} + Class + ShapedGraphic + ID + 4 + Magnets + + {1, 1} + {1, -1} + {-1, -1} + {-1, 1} + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.59983 + g + 0.937216 + r + 0.609412 + + + shadow + + Beneath + YES + + stroke + + CornerRadius + 14 + + + Text + + Text + {\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} + + + + Bounds + {{223, 477.205}, {97, 35}} + Class + ShapedGraphic + ID + 3 + Magnets + + {1, 1} + {1, -1} + {-1, -1} + {-1, 1} + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 0.59983 + g + 0.937216 + r + 0.609412 + + + shadow + + Beneath + YES + + stroke + + CornerRadius + 14 + + + Text + + Text + {\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} + + + + Bounds + {{94, 142}, {365, 259}} + Class + ShapedGraphic + ID + 6 + Magnets + + {1, 1} + {1, -1} + {-1, -1} + {-1, 1} + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 1 + g + 0.928021 + r + 0.860007 + + + shadow + + Beneath + YES + Draws + NO + + stroke + + CornerRadius + 14 + Pattern + 1 + + + Text + + Align + 0 + Text + {\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)} + + TextPlacement + 0 + + + Bounds + {{94, 454}, {365, 87.7054}} + Class + ShapedGraphic + ID + 7 + Magnets + + {1, 1} + {1, -1} + {-1, -1} + {-1, 1} + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + b + 1 + g + 0.930219 + r + 0.859335 + + + shadow + + Beneath + YES + Draws + NO + + stroke + + CornerRadius + 14 + Pattern + 1 + + + Text + + Align + 0 + Text + {\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)} + + TextPlacement + 0 + + + GridInfo + + GuidesLocked + NO + GuidesVisible + YES + HPages + 1 + ImageCounter + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2011-10-05 21:16:40 -0700 + Modifier + Miško Hevery + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSLeftMargin + + float + 18 + + NSPaperSize + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAx7X05TU2l6ZT1mZn2WgWQCgRgDhg== + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + PrintOnePage + + QuickLookPreview + + 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 + eeKbBcY+wZcSYxsNHlewj+ixyY5xwJsW3sf8hELZMt6/rArIJ2EwXjkIMQy8xcMY1pKJ + nSHe63w/wpXD0sGAsL4wCnTAbFgYFmBcPVCZDS4AAbTwepitdH7kqoBQKyfyybOHrwrA + Bo9n/uCe8Mf53eYjcu9HEv9HR9/Js3FMBPly92hRELL9m/P6VIYHtvwm1wr/rTWtzeZ/ + 7dUMSfzBFiJGLeYNHhOu/d57EGKGdY4Ifnr2AQ+LeBU8eXZzfvtl+5opiHcnfJ47sk/o + Z0YRJuzhacC8OUJHD69bMs3PuHPdO4vBRVYeXaDle2el9gJM+EL4MxC2bwcOQrZzu/30 + fnt77ySIJbSMXwLY8bTgNPiGFvmSuhe44she1WP5sbzQvK4PMt993T5X+RDxoJoLd+3E + 4CSGE2vzP4hjX8khH7dXF3fvTo6fU93c0wY38z7Ztf5G7sWnH88//br95h/HaX98D/tn + 1PZgyy2g3721/sps1z1sYzyUs414BKgXjZ1zQlhZzqm80O2oFh6f8xnc1MKf2AdfnZW8 + Og+nsTvVg6ghf+fAUMc3PJBDvfy+7+dYBm889KPTDDvC2Cv+omTh2Hu2gs/sRzTysRFf + IB+ZMSoEGhECN+X1vwFViXzVavAKmTFsz4/FPMm7f3/B/3tYIrxdoL/w1fsEPHx+ePIM + agC0bx83w4PEz/nlxfvzu55shenIf7YyzNWfwWrSmX8Hw5i23b9zhWGWAg+c8hsZ60gJ + OU7cHOl7Ea0jrEEcjUOekTtktInrCONtXLokXD9+D0Uki6sQ/rKF0Sz+lkQiH0YBIAnE + ydicQUFElQKI6RHSp0XupvSTh50fc8ulByIEOGOYQOw1T4elIhyJEuPwmVimwxUZcY/c + TYfpaufH/DIoL7b+CSk8wn7oT2BB9k7xp78AQQi6fAplbmRzdHJlYW0KZW5kb2JqCjYg + MCBvYmoKMzIxNwplbmRvYmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDQg + MCBSIC9SZXNvdXJjZXMgNyAwIFIgL0NvbnRlbnRzIDUgMCBSIC9NZWRpYUJveCBbMCAw + IDU3NiA3MzNdCj4+CmVuZG9iago3IDAgb2JqCjw8IC9Qcm9jU2V0IFsgL1BERiAvVGV4 + dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSSBdIC9Db2xvclNwYWNlIDw8IC9DczIgOSAw + IFIKL0NzMyAxMCAwIFIgL0NzMSA4IDAgUiA+PiAvRm9udCA8PCAvRjMuMCAxNyAwIFIg + L0YyLjAgMTQgMCBSIC9GMS4wIDExIDAgUgo+PiAvWE9iamVjdCA8PCAvSW0xIDEyIDAg + UiAvSW0yIDE1IDAgUiA+PiA+PgplbmRvYmoKMTIgMCBvYmoKPDwgL0xlbmd0aCAxMyAw + IFIgL1R5cGUgL1hPYmplY3QgL1N1YnR5cGUgL0ltYWdlIC9XaWR0aCAyMzggL0hlaWdo + dCAxMTQgL0ludGVycG9sYXRlCnRydWUgL0NvbG9yU3BhY2UgMTggMCBSIC9JbnRlbnQg + L1BlcmNlcHR1YWwgL1NNYXNrIDE5IDAgUiAvQml0c1BlckNvbXBvbmVudAo4IC9GaWx0 + ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4Ae3QMQEAAADCoPVPbQwfiEBhwIABAwYM + GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB + AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg + wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM + GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIAB + AwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBg + wIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYM + GDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwIABAwYMGDBgwMBzYD4DAAEKZW5k + c3RyZWFtCmVuZG9iagoxMyAwIG9iagozNzgKZW5kb2JqCjE1IDAgb2JqCjw8IC9MZW5n + dGggMTYgMCBSIC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggMjM4 + IC9IZWlnaHQgMTE0IC9JbnRlcnBvbGF0ZQp0cnVlIC9Db2xvclNwYWNlIDE4IDAgUiAv + SW50ZW50IC9QZXJjZXB0dWFsIC9TTWFzayAyMSAwIFIgL0JpdHNQZXJDb21wb25lbnQK + OCAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAHt0DEBAAAAwqD1T20MH4hA + YcCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG + DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA + AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw + YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMG + DBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCA + AQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgw + YMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMDAc2A+ + AwABCmVuZHN0cmVhbQplbmRvYmoKMTYgMCBvYmoKMzc4CmVuZG9iagoyMSAwIG9iago8 + PCAvTGVuZ3RoIDIyIDAgUiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1hZ2UgL1dp + ZHRoIDIzOCAvSGVpZ2h0IDExNCAvQ29sb3JTcGFjZQovRGV2aWNlR3JheSAvSW50ZXJw + b2xhdGUgdHJ1ZSAvQml0c1BlckNvbXBvbmVudCA4IC9GaWx0ZXIgL0ZsYXRlRGVjb2Rl + ID4+CnN0cmVhbQp4Ae2c+Ttb+RfHVY2tliC2JLbQEMtE1Iillkp5LDFqN0ztrS2G0uBh + RKla4rGXGlWqdhr73s4z/9r3nM9NULTVmX5ddZ1f+rT15HNe933O+3xu3PvR0bmO6ytw + fQWur8D1Ffhhr8CNSxT/t4uIjLpHcZPmOMpEFzP7ntgazps39fT0frpUAQndvEnIvw8x + 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 + ULlC2dE3Apa8srGzp2nbcyxL049QtB/2djZWwJRH+jqUinIygkQeLhxr3Cmf1JbQcl2E + vkGR8Wn5cgU1gJaAVmtSNKGcY1kN7T7QLuEI6mxWyPPT4iODfGGn/BlaE5Y1bC7EQZGy + dDJucd+4tHJkyedYlqYfOaTdRNrR/k4cuOmyyCAxbC+sWbC9OK0ttZXCm9uMAnndM7JL + 1oxb/DiaSM61LOZHRtAqoe16VicvyMBbXO1m6kxaHmiLtIUVFK12c/ED0c6CtkBbUaih + 5ZGt49m0rp/QHm6lLrm0ZEul0RY2U8dpXa9pdeBbKehbZlUyejJTXIpREwj2UgzaXTBp + 56jHqLsCht3xMexunknf1DDrWzhGfcN6g1HfnjPsNyMM+60Xk36jqcOo31brMOtJBGY9 + ZXIFnyByP3zs4sSvb3V0mPV0GNW45KFOBjz5B+Iy6alOZj2xq8Oop7GxlMnj2Mx40h5L + Gd8GYsZbFFpxP31DpvRHekOm9PxvyJCRi2/xMePtJ4LLmDfbgBaNiilvLR7WMrxYffXf + SEVxGfS2MVXLmtfmr/yb5HgrBK3LlFMCyJ0fOBVDToDQYdbpHke4TDi5hbQuHEH0uVN5 + wi/TqTzh//lUHsCF+wPGnLhEcMGZqdO04OSwH+c0Leq4pW87TQueXMZtBsrLhJPSKFyQ + lyGn4FHVDPJSvFf9hEOUlypn4L36p1fiawdaXiacTKrlRb/SnMV6pU+dpXihoHWZcaIw + 8lIVTSFf9dOiKV4kxi4+DJoPAsezkrWBmWmz/O5/4odflvjucNcfeH0Frq/A9RW4vgIX + dwX+B0Voq7YKZW5kc3RyZWFtCmVuZG9iagoyMiAwIG9iagozMzYzCmVuZG9iagoxOSAw + IG9iago8PCAvTGVuZ3RoIDIwIDAgUiAvVHlwZSAvWE9iamVjdCAvU3VidHlwZSAvSW1h + Z2UgL1dpZHRoIDIzOCAvSGVpZ2h0IDExNCAvQ29sb3JTcGFjZQovRGV2aWNlR3JheSAv + SW50ZXJwb2xhdGUgdHJ1ZSAvQml0c1BlckNvbXBvbmVudCA4IC9GaWx0ZXIgL0ZsYXRl + RGVjb2RlID4+CnN0cmVhbQp4Ae2c+T9b6RfHVY2tRRBbEluiIZaJqBFLLRW8LDFqN0yt + bUPF0GrwYkSpWuJFLKVGW6p2Gvvadl7zr33PeW5SirY606+rrvNLX8XrPs/7fs75nOcm + 93mMjC7i4g5c3IGLO3BxB37YO3DpDMX/7SYio/F+XKY59mdijDP7nth6zsuXTUxMfjpT + ARO6fJmQfx9iSlIARUpTUzMzc0NY0BaGGZibmZma4rwo5P+sMVGVkJoCpoWF5RWMqxBW + tAbOgEzF0sLC3NwMkU1Q4/+iMLIiKiEF0KtW1tY2NiyW7ZkIFsvGxtraCqgt9cQI/G95 + DawgqiWC2gClnT2b7eDgCOFEa+AMHBzYbHs7W1uWDSJbgsSUwP+Gd5/VAlFZtnZsB0B0 + 4XA4XC4PwpXGwPG5XJiKi5OTowPbzpaFwBb/mhe7DeSwmTmwAqo92xFAgdDdw8PTk88X + 0B58vqenh4e7qysPkB3Z9gBs4MX6/aaWRBWsKbBaWYOqjs5A6u7BF3gJhd4+IghfKvxO + PfQD4xx8vIVCLwEfkHkcZ0dQ2NoK9DUlfvUNuJjFJuhNqKsd28mF6wqkQm+Rr3/Az2Jx + oAQjiMYgEwgUi38O8PcVeQuB2JXr4sS2Q33RoU2+wa0AFpzYFLyJsHJ47p4CocgvQCwJ + Cg4OkUpDw8LCD0TEqcWBQcPDwkKl0pDg4CCJOMBPJBR4uvM4hBf8ish7QrOispgIawu6 + 8tz513z8AgKDgqVh4Tcio2JibsbKSMTRFNTosTdjYqIib4SHSYODAgP8fK7x3Xmgry2R + 98TZTClrBhXLsnNw5rrxr4n8xUG/hIZHxsTK4hMSk5JT5HJ5amrqr7QFDA5TSElOSkyI + l8XGRIaH/hIk9hdd47txnR3sWFC9Zoh7AnU/wlrb2jtyXD28fPzF16WAKktISklNS8/I + zM7Oyc3Nozlyc3OyszMz0tNSU5ISZAAsvS729/HycOU42ttanxiX1Cx4sTVkMdddIPQD + 1ohoWUJy6q3MnLz83wsLi4tLSssw7tAUZPDSkuLiwsLf8/NyMm+lJifIoiOA108ocOdi + NoM3E3W/0oiIG0MaA6wzz8NLFCAJiYiJS5TfysorKCwuu3OvvOJ+pVJZ9QdENU2BY1cp + lZX3K8rv3SkrLizIy7olT4yLiQiRBIi8PHjOBBeS+Wt99yCsq6fQVxwcFiVLTM3IyS8s + uau4r6yueVirUtXV1zfQHPX1dSpV7cOaauV9xd2SwvycjNREWVRYsNhX6Ol6Qly0458o + ZV0A1h+FTZCn5xQUlSkqqx88qmtobGpWq1set9Iej1vU6uamxoa6Rw+qKxVlRQU56fIE + lNcfcF306uJTwueTGYoW+ixJY1e+t79EGilLSsvKLyqrUD5QNTQ1t7Q+ae/o7Ozq7tbQ + HN3dXZ2dHe1PWluamxpUD5QVZUX5WWlJskipxN+bT6kLfReM+bO0JI/NLa1YbGdUNig0 + Ki45Pfd2qUL5UNXY3Nre0aXp6e3Tavv7BwYGaY2Bgf5+rbavt0fT1dHe2tyoeqhUlN7O + TU+OiwoNQnWd2SwrS3CqL4iL0ppZXLWxd+J5IGx0vDwjr+huZY2qUd3W0d3Tpx0cejY8 + 8nwU4y8ag0zg+cjws6FBbV9Pd0ebulFVU3m3KC9DHh+NuB48J3ubq9B2Py+uIY/tHDkA + KwmNTpBn5hcrqmob1G2dmt7+oeHno2MvXr4aHx+fgHhNW+DoMIlXL1+MjT4fHurv1XS2 + qRtqqxTF+ZnyhOhQqF0PjqOd9ZUv5fIleBTAonXguHn5SqRR8fKsgpLyalVT61NN3+Dw + 6NjL8YnJyamp6enpGYhZ2gJHh0lMTU1OToy/HBsdHuzTPG1tUlWXlxRkyeOjpBJfLzeO + A7RdzOXjK9eQx1C0ApE4JDJOnllQWlFT19zW1TswPPpi/PWb6Zm5ufmFhUWMJRqDTGBh + YX5ubmb6zevxF6PDA71dbc11NRWlBZnyuMgQsUgApfulXNZLa+fI9RAGBEfIUjLyAbZe + 3a7RDgHr5PTs3MLikk6nW15eWVlZpTVgAsvLMJWlxYW52elJ4B3SatrV9YCbn5EiiwgO + EHpwSS5/TlxjY7QoFtvFzctPEhaTlJ5XTMH2D48B69zCkm4ZINfW1zfORKyvr62urizr + lhbmgHdsuJ/CLc5LT4oJk/h5ubmwWWhUxsbHNCFMZKhaOyce30ccEpWQllukqK5Tt/cM + jLyYmJpbeKsD0I3Nza1tfezQFoYZbG1ubgCy7u3C3NTEi5GBnnZ1XbWiKDctISpE7MPn + OaFRHW/LkMimKC3H/Zr/9XCZPOv23SpVc7sGYCen55d0q4C6vb2zs7t3ZmJ3Z2d7G4BX + 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 + W7CXsQPYYew7HA7HwBniHHB+uBhcMm41rgS3D9eMu4Drww3hJvF4vCreFO+CD8Fz8GJ8 + Ib4Kfxx/Ht+PH8a/J5AJWgRrgg8hliAkbCRUEBoI5wj9hBHCNFGBqE90IoYQecRcYimx + jthBvEkcJk6TFEmGJBdSJCmZtIFUSWoiXSY9Jr0hk8k6ZEdyGFlAXk+uJJ8gXyUPkj9Q + lCgmFE9KHEVC2U45SrlAeUB5Q6VSDahu1FiqmLqdWk+9RH1KfS9HkzOX85fjya2Tq5Fr + leuXeyVPlNeXd5dfLp8nXyF/Sv6m/LgCUcFAwVOBo7BWoUbhtMI9hUlFmqKVYohimmKJ + YoPiNcVRJbySgZK3Ek+pQOmw0iWlIRpC06V50ri0TbQ62mXaMB1HN6T705PpxfQf6L30 + CWUlZVvlKOUc5Rrls8pSBsIwYPgzUhmljJOMu4yP8zTmuc/jz9s2r2le/7wplfkqbip8 + lSKVZpUBlY+qTFVv1RTVnaptqk/UMGomamFq2Wr71S6rjc+nz3eez51fNP/k/IfqsLqJ + erj6avXD6j3qkxqaGr4aGRpVGpc0xjUZmm6ayZrlmuc0x7RoWgu1BFrlWue1XjCVme7M + VGYls4s5oa2u7act0T6k3as9rWOos1hno06zzhNdki5bN0G3XLdTd0JPSy9YL1+vUe+h + PlGfrZ+kv0e/W3/KwNAg2mCLQZvBqKGKob9hnmGj4WMjqpGr0SqjWqM7xjhjtnGK8T7j + WyawiZ1JkkmNyU1T2NTeVGC6z7TPDGvmaCY0qzW7x6Kw3FlZrEbWoDnDPMh8o3mb+SsL + PYtYi50W3RZfLO0sUy3rLB9ZKVkFWG206rD6w9rEmmtdY33HhmrjY7POpt3mta2pLd92 + v+19O5pdsN0Wu067z/YO9iL7JvsxBz2HeIe9DvfYdHYou4R91RHr6OG4zvGM4wcneyex + 00mn351ZzinODc6jCwwX8BfULRhy0XHhuBxykS5kLoxfeHCh1FXbleNa6/rMTdeN53bE + bcTd2D3Z/bj7Kw9LD5FHi8eUp5PnGs8LXoiXr1eRV6+3kvdi72rvpz46Pok+jT4Tvna+ + q30v+GH9Av12+t3z1/Dn+tf7TwQ4BKwJ6AqkBEYEVgc+CzIJEgV1BMPBAcG7gh8v0l8k + XNQWAkL8Q3aFPAk1DF0V+nMYLiw0rCbsebhVeH54dwQtYkVEQ8S7SI/I0shHi40WSxZ3 + RslHxUXVR01Fe0WXRUuXWCxZs+RGjFqMIKY9Fh8bFXskdnKp99LdS4fj7OIK4+4uM1yW + s+zacrXlqcvPrpBfwVlxKh4bHx3fEP+JE8Kp5Uyu9F+5d+UE15O7h/uS58Yr543xXfhl + /JEEl4SyhNFEl8RdiWNJrkkVSeMCT0G14HWyX/KB5KmUkJSjKTOp0anNaYS0+LTTQiVh + irArXTM9J70vwzSjMEO6ymnV7lUTokDRkUwoc1lmu5iO/kz1SIwkmyWDWQuzarLeZ0dl + n8pRzBHm9OSa5G7LHcnzyft+NWY1d3Vnvnb+hvzBNe5rDq2F1q5c27lOd13BuuH1vuuP + bSBtSNnwy0bLjWUb326K3tRRoFGwvmBos+/mxkK5QlHhvS3OWw5sxWwVbO3dZrOtatuX + Il7R9WLL4oriTyXckuvfWX1X+d3M9oTtvaX2pft34HYId9zd6brzWJliWV7Z0K7gXa3l + zPKi8re7V+y+VmFbcWAPaY9kj7QyqLK9Sq9qR9Wn6qTqgRqPmua96nu37Z3ax9vXv99t + f9MBjQPFBz4eFBy8f8j3UGutQW3FYdzhrMPP66Lqur9nf19/RO1I8ZHPR4VHpcfCj3XV + O9TXN6g3lDbCjZLGseNxx2/94PVDexOr6VAzo7n4BDghOfHix/gf754MPNl5in2q6Sf9 + n/a20FqKWqHW3NaJtqQ2aXtMe9/pgNOdHc4dLT+b/3z0jPaZmrPKZ0vPkc4VnJs5n3d+ + 8kLGhfGLiReHOld0Prq05NKdrrCu3suBl69e8blyqdu9+/xVl6tnrjldO32dfb3thv2N + 1h67npZf7H5p6bXvbb3pcLP9luOtjr4Ffef6Xfsv3va6feWO/50bA4sG+u4uvnv/Xtw9 + 6X3e/dEHqQ9eP8x6OP1o/WPs46InCk8qnqo/rf3V+Ndmqb307KDXYM+ziGePhrhDL/+V + +a9PwwXPqc8rRrRG6ketR8+M+YzderH0xfDLjJfT44W/Kf6295XRq59+d/u9Z2LJxPBr + 0euZP0reqL45+tb2bedk6OTTd2nvpqeK3qu+P/aB/aH7Y/THkensT/hPlZ+NP3d8Cfzy + eCZtZubf94Tz+wplbmRzdHJlYW0KZW5kb2JqCjI0IDAgb2JqCjI2MTIKZW5kb2JqCjkg + MCBvYmoKWyAvSUNDQmFzZWQgMjMgMCBSIF0KZW5kb2JqCjI1IDAgb2JqCjw8IC9MZW5n + dGggMjYgMCBSIC9OIDEgL0FsdGVybmF0ZSAvRGV2aWNlR3JheSAvRmlsdGVyIC9GbGF0 + ZURlY29kZSA+PgpzdHJlYW0KeAGFUk9IFFEc/s02EoSIQYV4iHcKCZUprKyg2nZ1WZVt + W5XSohhn37qjszPTm9k1xZMEXaI8dQ+iY3Ts0KGbl6LArEvXIKkgCDx16PvN7OoohG95 + O9/7/f1+33tEbZ2m7zspQVRzQ5UrpaduTk2Lgx8pRR3UTlimFfjpYnGMseu5kr+719Zn + 0tiy3se1dvv2PbWVZWAh6i22txD6IZFmAB+ZnyhlgLPAHZav2D4BPFgOrBrwI6IDD5q5 + MNPRnHSlsi2RU+aiKCqvYjtJrvv5uca+i7WJg/5cj2bWjr2z6qrRTNS090ShvA+uRBnP + X1T2bDUUpw3jnEhDGinyrtXfK0zHEZErEEoGUjVkuZ9qTp114HUYu126k+P49hClPslg + qIm16bKZHYV9AHYqy+wQ8AXo8bJiD+eBe2H/W1HDk8AnYT9kh3nWrR/2F65T4HuEPTXg + zhSuxfHaih9eLQFD91QjaIxzTcTT1zlzpIjvMdQZmPdGOaYLMXeWqhM3gDthH1mqZgqx + Xfuu6iXuewJ30+M70Zs5C1ygHElysRXZFNA8CVgUfYuwSQ48Ps4eVeB3qJjAHLmJ3M0o + 9x7VERtno1KBVnqNV8ZP47nxxfhlbBjPgH6sdtd7fP/p4xV117Y+PPmNetw5rr2dG1Vh + VnFlC93/xzKEj9knOabB06FZWGvYduQPmsxMsAwoxH8FPpf6khNV3NXu7bhFEsxQPixs + JbpLVG4p1Oo9g0qsHCvYAHZwksQsWhy4U2u6OXh32CJ6bflNV7Lrhv769nr72vIebcqo + KSgTzbNEZpSxW6Pk3Xjb/WaREZ84Or7nvYpayf5JRRA/hTlaKvIUVfRWUNbEb2cOfhu2 + flw/pef1Qf08CT2tn9Gv6KMRvgx0Sc/Cc1Efo0nwsGkh4hKgioMz1E5UY40D4inx8rRb + ZJH9D0AZ/WYKZW5kc3RyZWFtCmVuZG9iagoyNiAwIG9iago3MDQKZW5kb2JqCjEwIDAg + b2JqClsgL0lDQ0Jhc2VkIDI1IDAgUiBdCmVuZG9iagoyNyAwIG9iago8PCAvTGVuZ3Ro + IDI4IDAgUiAvTiAzIC9BbHRlcm5hdGUgL0RldmljZVJHQiAvRmlsdGVyIC9GbGF0ZURl + Y29kZSA+PgpzdHJlYW0KeAGFVM9rE0EU/jZuqdAiCFprDrJ4kCJJWatoRdQ2/RFiawzb + H7ZFkGQzSdZuNuvuJrWliOTi0SreRe2hB/+AHnrwZC9KhVpFKN6rKGKhFy3xzW5MtqXq + wM5+8943731vdt8ADXLSNPWABOQNx1KiEWlsfEJq/IgAjqIJQTQlVdvsTiQGQYNz+Xvn + 2HoPgVtWw3v7d7J3rZrStpoHhP1A4Eea2Sqw7xdxClkSAog836Epx3QI3+PY8uyPOU55 + eMG1Dys9xFkifEA1Lc5/TbhTzSXTQINIOJT1cVI+nNeLlNcdB2luZsbIEL1PkKa7zO6r + YqGcTvYOkL2d9H5Os94+wiHCCxmtP0a4jZ71jNU/4mHhpObEhj0cGDX0+GAVtxqp+DXC + FF8QTSeiVHHZLg3xmK79VvJKgnCQOMpkYYBzWkhP10xu+LqHBX0m1xOv4ndWUeF5jxNn + 3tTd70XaAq8wDh0MGgyaDUhQEEUEYZiwUECGPBoxNLJyPyOrBhuTezJ1JGq7dGJEsUF7 + Ntw9t1Gk3Tz+KCJxlEO1CJL8Qf4qr8lP5Xn5y1yw2Fb3lK2bmrry4DvF5Zm5Gh7X08jj + c01efJXUdpNXR5aseXq8muwaP+xXlzHmgjWPxHOw+/EtX5XMlymMFMXjVfPqS4R1WjE3 + 359sfzs94i7PLrXWc62JizdWm5dn/WpI++6qvJPmVflPXvXx/GfNxGPiKTEmdornIYmX + xS7xkthLqwviYG3HCJ2VhinSbZH6JNVgYJq89S9dP1t4vUZ/DPVRlBnM0lSJ93/CKmQ0 + nbkOb/qP28f8F+T3iuefKAIvbODImbptU3HvEKFlpW5zrgIXv9F98LZua6N+OPwEWDyr + Fq1SNZ8gvAEcdod6HugpmNOWls05Uocsn5O66cpiUsxQ20NSUtcl12VLFrOZVWLpdtiZ + 0x1uHKE5QvfEp0plk/qv8RGw/bBS+fmsUtl+ThrWgZf6b8C8/UUKZW5kc3RyZWFtCmVu + ZG9iagoyOCAwIG9iago3MzcKZW5kb2JqCjggMCBvYmoKWyAvSUNDQmFzZWQgMjcgMCBS + IF0KZW5kb2JqCjI5IDAgb2JqCjw8IC9MZW5ndGggMzAgMCBSIC9OIDMgL0FsdGVybmF0 + ZSAvRGV2aWNlUkdCIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4AdWWZ1hT + yRrH55yTXigJXUrovXeQXkOXDjZCQocYQkdUVMQVWFFEREARdEFEwVUpshZEEBVEsPcN + siio62JBVFTuCVxcn+fe/Xa/3DfPzPzynzfvmczMeZ4/AJReFo+XDIsBkMJN5wd6ODPC + IyIZ+IeAAKQBDegCORY7jecUEOAD/jE+3AGQcPKmvrDWP6b99wlxTkwaGwAoAJ2O5qSx + U1A+hTbA5vHTAYBRBsNZ6TyUkQKUJfjoAlGuFHLcAh8VcvQCd8/nBAe6oDm3ACBQWCx+ + HABkAaozMtlxaB0KisCIy0ngomyEsj07nsVBmYeyXkrKGiHXoKwV/UOduB+YxYr+XpPF + ivvOC/8F/SX6YNeENF4yK2f+y/+yS0nOQPdrPmhoT+Em+/mgowzaJjgsV+9F5iXPn9m8 + HsMNCVrUudF+/oscy3cPXGReuvMPHBC8qOfGu/gtckya2/c6iSwv4ZnN1+dnBIYsclpm + kNsi58YHhy0yJ8b1ux6b4M5c1BPSmd+flbTG+/sagCtwAz7ohwFMgBkwAubAHQSAsPSY + bPQMAXBZw8vhJ8TFpzOc0FsXo8dgctkGegwTI2Nj4fT/TQjft4XFvrs+/x5BMsKr/G8t + Fb3H1g/Qu1z/txbVB0B7BQBSZ//W1K4BILoDgM5r7Ax+5kI9jHDAAhIQBRJAFigCVaAF + 9NHdtAC2wBHdXS/gD4JBBFgF2CAepAA+yAJ5YCMoBMVgB9gNqkAtOAgOg2PgBOgAZ8AF + cAkMgGFwGzwEAjAOXoIp8AHMQhCEh6gQHZKFlCB1SBcygawge8gN8oECoQgoCoqDuFAG + lAdthoqhMqgKqoOaoF+h09AF6Ao0At2HRqFJ6C30GUZgCiwBK8AasCFsBTvB3nAwvBKO + g1PhXLgA3g5XwvXwUbgdvgAPwLdhAfwSnkYAQkakEGVEH7FCXBB/JBKJRfjIeqQIqUDq + kRakC+lHbiIC5BXyCYPD0DEMjD7GFuOJCcGwMamY9ZgSTBXmMKYd04u5iRnFTGG+YalY + eawu1gbLxIZj47BZ2EJsBbYB24btw97GjmM/4HA4KZwmzhLniYvAJeLW4kpw+3CtuG7c + CG4MN43H42Xxung7vD+ehU/HF+L34o/iz+Nv4MfxHwlkghLBhOBOiCRwCZsIFYQjhHOE + G4TnhFmiGFGdaEP0J3KIOcRS4iFiF/E6cZw4SxInaZLsSMGkRNJGUiWphdRHekR6RyaT + VcjW5GXkBHI+uZJ8nHyZPEr+RKFRdCgulBWUDMp2SiOlm3Kf8o5KpWpQHamR1HTqdmoT + 9SL1CfWjCF3EQIQpwhHZIFIt0i5yQ+S1KFFUXdRJdJVormiF6EnR66KvxIhiGmIuYiyx + 9WLVYqfF7opNi9PFjcX9xVPES8SPiF8Rn6DhaRo0NxqHVkA7SLtIG6MjdFW6C51N30w/ + RO+jj0vgJDQlmBKJEsUSxySGJKYkaZJmkqGS2ZLVkmclBVKIlIYUUypZqlTqhNQdqc/S + CtJO0jHS26RbpG9Iz8gskXGUiZEpkmmVuS3zWZYh6yabJLtTtkP2sRxGTkdumVyW3H65 + PrlXSySW2C5hLylacmLJA3lYXkc+UH6t/EH5QflpBUUFDwWewl6FiwqvFKUUHRUTFcsV + zylOKtGV7JUSlMqVziu9YEgynBjJjEpGL2NKWV7ZUzlDuU55SHlWRVMlRGWTSqvKY1WS + qpVqrGq5ao/qlJqSmq9anlqz2gN1orqVerz6HvV+9RkNTY0wja0aHRoTmjKaTM1czWbN + R1pULQetVK16rVvaOG0r7STtfdrDOrCOuU68TrXOdV1Y10I3QXef7ogeVs9aj6tXr3dX + n6LvpJ+p36w/aiBl4GOwyaDD4LWhmmGk4U7DfsNvRuZGyUaHjB4a04y9jDcZdxm/NdEx + YZtUm9wypZq6m24w7TR9Y6ZrFmO23+yeOd3c13yreY/5VwtLC75Fi8WkpZpllGWN5V0r + CasAqxKry9ZYa2frDdZnrD/ZWNik25yw+ctW3zbJ9ojtxFLNpTFLDy0ds1OxY9nV2Qns + GfZR9gfsBQ7KDiyHeoenjqqOHMcGx+dO2k6JTkedXjsbOfOd25xnXGxc1rl0uyKuHq5F + rkNuNLcQtyq3J+4q7nHuze5THuYeaz26PbGe3p47Pe8yFZhsZhNzysvSa51XrzfFO8i7 + yvupj44P36fLF/b18t3l+8hP3Y/r1+EP/Jn+u/wfB2gGpAb8tgy3LGBZ9bJngcaBeYH9 + QfSg1UFHgj4EOweXBj8M0QrJCOkJFQ1dEdoUOhPmGlYWJgg3DF8XPhAhF5EQ0RmJjwyN + bIicXu62fPfy8RXmKwpX3FmpuTJ75ZVVcquSV51dLbqatfpkFDYqLOpI1BeWP6ueNR3N + jK6JnmK7sPewX3IcOeWcyRi7mLKY57F2sWWxE3F2cbviJuMd4iviXyW4JFQlvEn0TKxN + nEnyT2pMmksOS25NIaREpZzm0rhJ3N41imuy14zwdHmFPEGqTeru1Cm+N78hDUpbmdaZ + LoEam8EMrYwtGaOZ9pnVmR+zQrNOZotnc7MHc3RytuU8z3XP/WUtZi17bU+ect7GvNF1 + Tuvq1kPro9f3bFDdULBhPN8j//BG0sakjdc2GW0q2/R+c9jmrgKFgvyCsS0eW5oLRQr5 + hXe32m6t/QnzU8JPQ9tMt+3d9q2IU3S12Ki4ovhLCbvk6s/GP1f+PLc9dvtQqUXp/h24 + Hdwdd3Y67DxcJl6WWza2y3dXezmjvKj8/e7Vu69UmFXU7iHtydgjqPSp7NyrtnfH3i9V + 8VW3q52rW2vka7bVzOzj7Lux33F/S61CbXHt5wMJB+7VedS112vUVxzEHcw8+OxQ6KH+ + X6x+aWqQayhu+NrIbRQcDjzc22TZ1HRE/khpM9yc0Tx5dMXR4WOuxzpb9FvqWqVai4+D + 4xnHX/wa9eudE94nek5anWw5pX6qpo3eVtQOtee0T3XEdwg6IzpHTnud7umy7Wr7zeC3 + xjPKZ6rPSp4tPUc6V3Bu7nzu+eluXverC3EXxnpW9zy8GH7xVu+y3qE+777Ll9wvXex3 + 6j9/2e7ymSs2V05ftbraMWAx0D5oPth2zfxa25DFUPt1y+udw9bDXSNLR87dcLhx4abr + zUu3mLcGbvvdHrkTcufe3RV3Bfc49ybuJ99/8yDzwezD/EfYR0WPxR5XPJF/Uv+79u+t + AgvB2VHX0cGnQU8fjrHHXv6R9seX8YJn1GcVz5WeN02YTJyZdJ8cfrH8xfhL3svZV4V/ + iv9Z81rr9am/HP8anAqfGn/DfzP3tuSd7LvG92bve6YDpp98SPkwO1P0Ufbj4U9Wn/o/ + h31+Ppv1Bf+l8qv2165v3t8ezaXMzfFYfNa8F0DQHo6NBeBtIwDUCADowwCQRBb88HwG + tODhURZ6+Xk//5+84Jnn8y0AONgNQHA+AD7oWI2OGo6oB0Gb0Bailg42Nf3eUEUYabGm + JvMAUeRQa9I9N/d2DgB8FABfh+bmZivn5r6ivgZ5D8B5vwUfLswWQ/39ATEjH6+gc3kD + +ULlx/gXSb7pbwplbmRzdHJlYW0KZW5kb2JqCjMwIDAgb2JqCjI2NjkKZW5kb2JqCjE4 + IDAgb2JqClsgL0lDQ0Jhc2VkIDI5IDAgUiBdCmVuZG9iago0IDAgb2JqCjw8IC9UeXBl + IC9QYWdlcyAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvQ291bnQgMSAvS2lkcyBbIDMg + MCBSIF0gPj4KZW5kb2JqCjMxIDAgb2JqCjw8IC9UeXBlIC9DYXRhbG9nIC9PdXRsaW5l + cyAyIDAgUiAvUGFnZXMgNCAwIFIgL1ZlcnNpb24gLzEuNCA+PgplbmRvYmoKMiAwIG9i + ago8PCAvTGFzdCAzMiAwIFIgL0ZpcnN0IDMzIDAgUiA+PgplbmRvYmoKMzMgMCBvYmoK + PDwgL1BhcmVudCAzNCAwIFIgL0NvdW50IDAgL0Rlc3QgWyAzIDAgUiAvWFlaIDAgNzMz + IDAgXSAvVGl0bGUgKENhbnZhcyAxKQo+PgplbmRvYmoKMzQgMCBvYmoKPDwgPj4KZW5k + b2JqCjMyIDAgb2JqCjw8IC9QYXJlbnQgMzQgMCBSIC9Db3VudCAwIC9EZXN0IFsgMyAw + IFIgL1hZWiAwIDczMyAwIF0gL1RpdGxlIChDYW52YXMgMSkKPj4KZW5kb2JqCjM1IDAg + b2JqCjw8IC9MZW5ndGggMzYgMCBSIC9MZW5ndGgxIDEwMDQ4IC9GaWx0ZXIgL0ZsYXRl + RGVjb2RlID4+CnN0cmVhbQp4Ab1aeXiTVdY/975rlqZJmr1Jk5AmadrSlaWllYbSQim0 + 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 + 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 + 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 + /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== + + QuickLookThumbnail + + TU0AKgAAE1aAPuBP9tuN1gADg4JgB9vp9gB/REAAYDgcAP+MAB9Pl8gACR8AAIBgOIP1 + +gCTSeKRaMP+NRyPSAByB+vx+ACbTeVgAAz2Xx2RySZgSUTacUadz0Az+QgIBAChyWTz + mJxWeT6Nx2nU8CAUC1KGQIAAUDAar0uGvqm1yvWCBQ+yWatwx9WqlVCQP+JW+x2W13S1 + ADBR623p/WG4W2P08Dv16gANBYJYPBOl3PN/stptkAPZ6PQAPd7vYADwgEQAN5t5wPiE + RyGRgBzONxAAPiLXuRxOEABYMBmY0S51SUwirPXkAB3u12AAOh8QZTpdPBPB3u4ARx8R + B/Sfqd/weHp06SAkEgsAA4Hg/xe33e/4fH2vB3c0OhCiBD9ABotlwH+XpfmCABjGCXwA + BIE4UsAiCMnM3Tgt634ADSOA7gAYpgl6ABWlOUTSiAIYAHYdZ1AACAIoWdEHgA5DHhLB + QAAkCYKAAJQnim9xuGkX70nYVwAAwBTsJa+UjOmlynmydb2AWFQzAADgSBfI8qytK7qH + xLQAAiAjtgoCQIAAahuHGf5+gGBMWs+hAEAQADrOwu5cloWAACKJQnAAB8UPS9QAHAbx + uOzLYKAsC6zqOm5lGOYbSh/ETRNI0bSBWFwYPAdzmAAeRjjwAAbBIw8sVI8BknEhYPCG + RCoNjUtX1g6a+AiAqOgiBwGAAb5zncf4BgUCNY2FUhumyawABMeBCAACYHze9qHJOXZk + HKAAKglN6TJcep7puu4hhsDT5G2c6bgAGJJT2/dh3ZLB8NEAAGACx4MgtGptHEdJ/gQB + 4LXbf74mqaJlgAFx8EfZlnPcfZ+MOYRnHQ9IFq+iKXHOdjSAyCgFAAG4W38+JtnKh57h + URMggzcWAZW+B7OS9DQA4DNEGcapuH+CgONflmeOobRsWOBJtkKAAZhKi2eyMWhlI6EA + jka3oLgxpOqVksT8O2CQHgaABuHIdZ/gMBsa6rpJ5nkeQAFwVGEBgCJogAFQPzUjOysG + ZRsNAbx/h+AAjikMgAAaBwHbtsq6rVWiO61rld17X9g8NljrHeABAjyOAABaF4ZtCeR0 + pwfaO6oAYDTUCAJg2AAUBYFwABkGocL/yWknrNgFH+eYAA8DbgGkbRwn+CALg92mVnke + J4gAUpPkyAArC2MKGH4h6a3NpKogUBT0HTFfdhAEQAHOchxpQ7qUJM0OXT2B9ggUBlct + m2oQBGEoAAQ8yLoytKjofGbZGVpaO2A8AY9zegTWCNMbY4h/r9A68ZgA4RvjeRkjRwTh + IIHgPodhSQABxKBMIV8eg83dKaIOSMogIgSAmAAM0ZIxgAP0BIiMdToB8l1ABCp+wDQG + nsUsphlis1apcVwrpXivlgQZVipQAA6RzjmhiCOGcSjwkRMON01YAH6gnUUWMtpdyqEf + KIZQ4pQYqGCXfAYBoAjSAYAqQtfC+l+MgjOliGrEYxQVgBHU8A4BuqDTinA68HoQGUN8 + cACKM4ov2gkN1UAOQeR1ZcY9mCUWZgAZqzdnLO12DBF4LkAAC4eLMUMAA3Q4FWkkkSip + 7wEgJI1hG2lPjkTByzj1KF+EfDxDjHDKgBL2juGHHZDWEKhDtgXZTMZ/RLgPAgBDHWIS + toikFHUP8hIFV/jNUYSEnwGgOPFGWMgYq1mpEeJmdkjZYJfscPMxxIsq14uDPSeuXR4V + BDahzCs8CK1qxOigAxP56j2D1hIVUi06wAKaObBsngAilqHOAZRFKNR1PeIqm8uYGANO + qO+4hLkQ3GRGcfEmeqVTmEHOKodqdJTB0elPPmFh34rAAF+LsW4AJ2pckUbSVEfhtwXP + Y1JcQFpkjQGYMk0Jo3VgqBYACNIAB1jrdAOwdLoA2B0D0+o0hVE+JidsaB3DuneO+eA8 + J4lLEjlUUCoOLaiZ6oldAkWlR8qPR5LnGR9MZj4lUOuO1ayhzpUzruYOASe4CwHWC794 + Lw3i1oSsg98r+E1P/pLPcyE337puPAK0VAo0/HsRnNgBx+xttAoSpuZszwfBCCM5KaMR + FcuOiRLSxyRqPQfgpCsFEdRojOYIKwUwoQACDEYJQ91JwANnbSXU7bhExPdHOAB7Z6C5 + 1tcNU+NcbY3gAmpNYB02LapYiwZwdI73yj7AIaQfr52qAFAIWYdI3CDgYApA80yIrwqv + knKEADMZLjPGsN0f7qXwxnvG+gk8PHCwkbSm5NSNJsALlyVQWQvrhANBkUsBYDz0UzcM + PYd5pACjjX8D8GYS3Z35Sra9W6uY4r7X6dKwqbln2EXhZKLtMyylmPiNka41QAIKBWdJ + ItX6oUVAALoWwsnXg0dkOgC40lrAgRqPofBagDAILNh4wdgz3j6HvlgBOPDxDoGrX4Io + IQuS4VzipKtHnFIya3SK2b4hyjkAAMOmuRCLnOA9M8GYNwdAAGgM1gg06jTzTEB2ZpfS + zXICOEsJ6gI/xRimfC4FwhuDcGw68NMMyFOFFoJYWhzgUQPSKBgETUxoi9bgP0fZJwDg + LIsA8Cp7C5gIAWm8ew8zSD5HsR0EQMHwgZBGcAdY36/A4AaFGv6iM3JHyNJUDoGmpwKg + ZA6pyWxyy8RPImJsT5UrqWCPZ9Y8h4DwmKBw56I0SmwKfCqFg1xqZSBGCa3eO5g1aU4P + HdRyDQQ1uiPMEkjgHgXPYOAacqAJAaMmTUk6ZyXDyHa2kBIDU1AKAcxwrpRB1DiRMd0w + /HTCFEbG1wBgEFcjnGw6AHIEQpMoZVtE+VhQHRsAABdMAALFVmsbFSJhuxv7jpng6z6e + wIrBfG+UXQ1hTLICHM/L1rh8kPGiK3ToXAlhx2/bTmh78WRFG8OYdo/wCALMn189pfBa + CyFUAAbQ6Rmm9BO2QAJLmqneHgN80ANQTGoBuDsH0Xiv9qPhU8B1h+dGTa82BsUe/DHU + 39uoM4XUch5ECycwz0yH3tK7Th7ZVzBAmBTkPyJ8r91hOdtaTDNmcM69OeCEbuhrDUbg + DEGgN99pFaoXePMjQAMpA56BjnsTxdhxcvnGEdPjGU26bUBk8qu0loZB0DIG/h/NPBR4 + CQBnE9i7J2btH2jpM/WPbrFMfKZ4HRggv8h1PUu59W1NMiZrv/ZPENYabcAUutrcPgRI + 5CIkrmeQeU30ACKcAA38cqK88KMGngG+iwAANcfsxVAip+m+OiNUM4BOqYPkSKssucRO + lc8IOkoYJcJcngVI5s5w8WP4P8H+AiAwA+PcE8EwXSpmBChUAAx8yAKoO02+MmAINisg + lwa4kMQmIoTecGcLBUMGBskgV1AlAozcgkgojMGOGIGA7gx+dCIe3kdWBWBa0I0MNSG4 + M4Bc9wyDA8HKN2V0NUlNDcCoCyC9BGIWAsN9BG7SSO+QzocgPcv2p4yC9KmWUS94MGLu + MpEOMELuSKLuoY9ERQWCNWGui0Rg/SjqSLEoWQBQBU3cRMqeA20Y/+MFEXFIyIIzEeHe + cqf1D0VLBYNJBc8abCbG/eMGHOzu9APREkpKr6M6OTFE5/FsMG/id02qamwAwEwJGG34 + iezxDAjOiYEmEWWWDiD0EDBLGYpahw+6++VyGyHCHQH+ASeHG0MGrXFaJwAGO3FMZYPI + OyHkIeFQFCE2uGuLHMasIezixbD8pJHMGGGUFwAAHaAakcAcAwVzBQckHYG4cqBGAIc6 + BoBi8HHwMpGK/mTGTKH+/vGYjuQwHWFa+CBU+YjqHUGk3UCSBWDBGzHMuw5wjcIWP6P/ + BlBpGHDOWOG+AmGQTgHS3UHKZEcEAia4JmJIAWAgPRJ+WqA0BIXEHoHe4CHCRMBEBifC + 4kIY6qcEAma4HYHIOaBM79AUHOd0B4AiCs67IrD6tlD/GG9sGcNkA0GgM6HiMeHQG+Yi + LnAQKWAeAoPYHVKjKya4veKJKINCHmgMJCMFAadCJuHmHad0BaB/DGHiHQd0BuAaCg5l + IqsKgIgMAsgQAAG6HKHYH+AKAYIXGGwOGQHWFmi0BxJql0GkFlEqCiB0DO2fIrIvGO9a + k09hGGyMFeFgQ+HiAaWqBABigfMQaqH+H4JcG6GSlQA4ACQWCQCUCozZIqzgpAzm56sZ + GY8mAADmDSC+AACGCQT038OwYY86aSAOyy5ymQMgA8eKBnChExGGL4Am+8T2AaPQYuHi + H+H2AKcLGGqeFgFUFKb8Ca5iJMJuKoaojypyRIdAkSRqoFIqPqRMBAAm1qnoI4HyH+G6 + HMOwAOAaMmXeO2jyKiesi6KSKwJgjMJEJIO6KmKQKsLuKyL+KjRmi64E3IsyTULmK6Yo + fSL4LiUSf5SCLbR3SKL8Lmf4f4HPFwhiNci7SNSchwMpSFEKf5StASf4MpSMpnS4i/AS + epPWMEKiKeJcAMH6NAA6AyvAMGJaTOfSHwJgHkHmNA3030o8o8AXT+OkRdBK30L4o8e0 + Y4Lu3MNIjy30KobQbSqiRMBMBMi4NEgNUYL9B+JhUOL/UUL+IqItU0I6PNSBATUsL+qq + dBT+Y4ouukmALmsKMoxowQ22O3VIL/ViMHVuOLVPVmjMsKSK30y0K/VBExTnGGHRWS3G + ArWYzcM+NAHZWiN7DwUSLux0L8HBWzPeUQh5COMGeTAML9UEOLFWcqBLXOAAHHXUhiBA + OiVjWObspmKo30PapmpmjyG3Xyd3PkOMaQvyo9WylQG/YGv6TYHdYOAAApYUSCAwanVT + NAG6kchId0BFYqUBW0pmBZY0OcA6geGfY/B4GyM4B1ZIlDUBUEF7ZSAAExZZA/HaZ6GV + ZiqgqiAAGnZtW3WmX9YqfDWeAAGPZ+AADdaEAAGraKAABXaQ7USKGxaYAABTaeOlWiOb + VmMoLvT+PREUbqOpESOpEcJ9a8KXbBZda0aqHVbM33UFZ6ldD2MFXBEKBbbg7gG0nxUm + pi5pQ8g8HENrSiujWKcIcLXyp+BJcGRaOSHNcOsyTfPkeLb4pwfzVmKpMValZNawMHWL + Vvae/cPhXgbKL4pmHLdAM63MhzYsjKNjYOOwA5dVaJaMBVddFOsdYmoTYQOLbcA1duAB + WSYjVnbxbxb/cSAAFzeEbjdfd/cCWtWbYeAzeWABXOftXUfKHJekAACherbG7wbLZ+GP + YKNBY+GfYYanYURrZsGmSjdXdQAADbfUa603X2eLXovzcbMU3S3UAvfscElGMpYCRlBJ + d/d1fBcKkpUAjPc4gzbFayJda4OncOig31fEzc02UHdU+GFxgrVqOyJhbMRMA/g4i6P0 + TFeg54GkykBfhKU4bQAAB9hUVABsBsclgK+1Z6qiIOBDhqtqL5YikcBRh3gDcpFRgSJ8 + PbZfEZiCeNTmHOHUOaHu866qJuf5CYOkeS3U6NWKsLRxQsMpAKTaTfWLd6S3ixW+3SlM + QgBREIKoS0gMPWTEMouU0deAo8g6cIPYLvjbS1Vuo8ieWq30ZSdU9mNgJJU4L43MMewU + L/Z6LnU4Ko4AXi+iQjdkLufgVyL5kWpzP4TUAeATKKmAMoHSHW7KHSHoMOfaIXgUZ5gO + SOpnI830sonqg+6GwhOvHwHgHWigBQA8mxMUHAHQV6FiFkFsS4gshMAA+weKbQeUj8UG + /6dcbOeVkG363URhE9jQ0UUIgMRQIXQkJ5m0AOs0oquiJ3mmCKCST0mGdAhkjrVPDPEq + BYBeBjdhFsL4AoAOLUVwPQ7G7KGMGS7nmcl46GOemfkKNWWPnaBpaI/2d2Na7gGwyBFF + BokPF+Medsd1j8AoAqX8kCM+d0ZcNAKoogAACYClLM26N5n+ImL8ggFyFqFiAAGApsAA + EOEgExIqMoiYkqA2AwX9GSwGA2wKbssKjyjyO/O+unBKcpLPlcAAGsHIYIAEAaJuOKao + KcKIHSGuIOB2BoCREtbtpmqeXkXoXs7g+UjnGYsKFcGCecA4B6RqyyzIdoHsHiNIHwGs + KICQByCzPtFs+5P1H5LVH8/fCtaIAKF4RkA068XaHwHqO210xqPCHUGwOaCAA2C0nmPZ + HxbxM4WsTDZqgWgaAtORFsGmGpLcHOAve+1COkFuEypuJEKeA8BUeKGqGGyAI2LU40Y4 + A6BSgeHeHQcqHvMM5KAACCC2CCfuAZsYqgG8IOBsAY2cakojHMyM9UrG54rLO7GHoXfK + G8AahgAkA5bZXSGufLtxs/FKIyHKGyWqA2BOdVlKSK4UlQBCBcmeO+HIGiigB4AwCrMz + Hwo8awWYAgcLr7sM+amGRNrNHqBKCMge4uTUghKcNAGyFiN48s65jBG1D7Fm8fGYpnuw + AAGCGXNYH2ANMOz7OUKeAWACTEB8BoCUi03xrw/fpqv7mHpxN29ek5G1Uc38uThRQYaq + I+K/igcGPZqDpmqcXhq8+DrAxfrHyNh+gzlLycMoKpG6T2AYY4XyHgJcX5yly62isKS6 + gMTAWCHRk8H+HWHtL0T7y9zZEyIyHiHYYi2OPZU5TmHJiQI07MI8Ipogi6+ia483j8xx + MUsLTMnj0AIzj9m4TeJ2HyS2f4oBQGIzjb0XpMLNT6Jhz/iIv7hRSN0rRx0cO3igMpj9 + S1VmL5q6lHAQKf1KJAxwf51Sa4LvZ6jNVuKppqlzRiTWNALmoR0Nq6AWTUA2AqcKvcjG + MGICAA4BAAADAAAAAQBCAAABAQADAAAAAQBGAAABAgADAAAABAAAFAQBAwADAAAAAQAF + AAABBgADAAAAAQACAAABEQAEAAAAAQAAAAgBEgADAAAAAQABAAABFQADAAAAAQAEAAAB + FgADAAAAAQBGAAABFwAEAAAAAQAAE04BHAADAAAAAQABAAABPQADAAAAAQACAAABUgAD + AAAAAQABAAABUwADAAAABAAAFAwAAAAAAAgACAAIAAgAAQABAAEAAQ== + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 1 + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UniqueID + 1 + UseEntirePage + + VPages + 1 + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + + name + Canvas 1 + + + Frame + {{218, 122}, {831, 885}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{-53, 0}, {682, 731}} + Zoom + 1 + ZoomValues + + + Canvas 1 + 1 + 1 + + + + saveQuickLookFiles + YES + + 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 @@

Why doesn't the data goes back to the original?


- Input: + Input:
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 @@ - - + + {{x1}} -- {{x1.bar[0].d}} \ 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 @@ -
+
link 1 (link, don't reload)
link 2 (link, don't reload)
link 3 (link, reload!)
- anchor (link, don't reload)
- anchor (no link)
+ anchor (link, don't reload)
+ anchor (no link)
link (link, change hash) 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']; - + test test 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|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 * * - Salutation:
- Name:
- -
+ +
+ Salutation:
+ Name:
+ +
- The master object is NOT equal to the form object. + The master object is NOT equal to the form object. -
master={{master}}
-
form={{form}}
+
master={{master}}
+
form={{form}}
+
*
* 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 * * - Salutation:
- Name:
-
+ +
+ Salutation:
+ Name:
+
- The greeting object is - NOT equal to - {salutation:'Hello', name:'world'}. + The greeting object is + NOT equal to + {salutation:'Hello', name:'world'}. -
greeting={{greeting}}
+
greeting={{greeting}}
+
*
* 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 -
-
- Index of '{{bookName}}' in the list {{books}} is {{books.$indexOf(bookName)}}. + +
+
+ Index of '{{bookName}}' in the list {{books}} is {{books.$indexOf(bookName)}}. +
it('should correctly calculate the initial index', function() { @@ -146,17 +153,29 @@ var angularArray = { * @example -
+ +
- - - + + + - + @@ -166,8 +185,8 @@ var angularArray = { //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'}]"> - Search: + Search:
QtyDescriptionCostTotal
{{item.qty * item.cost | currency}} [X]
add itemadd item Total: {{invoice.items.$sum('qty*cost') | currency}}
@@ -306,9 +325,9 @@ var angularArray = {
NamePhone

- Any:
- Name only
- Phone only
+ Any:
+ Name only
+ Phone only
@@ -442,22 +461,29 @@ var angularArray = { * with objects created from user input. - [add empty] - [add 'John'] - [add 'Mary'] + +
+ [add empty] + [add 'John'] + [add 'Mary'] -
    -
  • - - - [X] -
  • -
-
people = {{people}}
+
    +
  • + + + [X] +
  • +
+
people = {{people}}
+
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 = {
  • {{item.name}}: points= - +

Number of items which have one point: {{ items.$count('points==1') }}

@@ -585,49 +611,56 @@ var angularArray = { * @example -
- -
Sorting predicate = {{predicate}}; reverse = {{reverse}}
-
- [ unsorted ] -
NamePhone
- - - - - - - - - - -
Name - (^)Phone NumberAge
{{friend.name}}{{friend.phone}}{{friend.age}}
+ +
+
Sorting predicate = {{predicate}}; reverse = {{reverse}}
+
+ [ unsorted ] + + + + + + + + + + + +
Name + (^)Phone NumberAge
{{friend.name}}{{friend.phone}}{{friend.age}}
+
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']); }); @@ -704,14 +737,20 @@ var angularArray = { * @example -
- Limit [1,2,3,4,5,6,7,8,9] to: + +
+ Limit {{numbers}} to:

Output: {{ numbers.$limitTo(limit) | json }}

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){ };
- Name: + Name: [ greet ]
Contact:
  • - - + [ clear | X ]
  • @@ -153,16 +151,16 @@ angularDirective("ng:init", function(expression){ 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'); }); @@ -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. - Enter name:
    - Hello ! + +
    + Enter name:
    + Hello ! +
    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. - Salutation:
    - Name:
    -
    
    +       
    +       
    + Salutation:
    + Name:
    +
    
    +       
    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. - Google for: - - Google + +
    + Google for: + + Google +
    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 -
    + + Enter text and hit enter: - + +
    list={{list}}
    -
    list={{list}}
    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 - Click me:
    + Click me:
    Show: I show up when your checkbox is checked.
    Hide: I hide when your checkbox is checked.
    @@ -730,7 +750,7 @@ angularDirective("ng:show", function(expression, element){ * @example - Click me:
    + Click me:
    Show: I show up when you checkbox is checked?
    Hide: I hide when you checkbox is checked?
    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 -
    - default currency symbol ($): {{amount | currency}}
    - custom currency identifier (USD$): {{amount | currency:"USD$"}} + +
    +
    + default currency symbol ($): {{amount | currency}}
    + custom currency identifier (USD$): {{amount | currency:"USD$"}} +
    it('should init with 1234.56', function(){ @@ -93,10 +100,17 @@ angularFilter.currency = function(amount, currencySymbol){ * @example - Enter number:
    - Default formatting: {{val | number}}
    - No fractions: {{val | number:0}}
    - Negative number: {{-val | number:4}} + +
    + Enter number:
    + Default formatting: {{val | number}}
    + No fractions: {{val | number:0}}
    + Negative number: {{-val | number:4}} +
    it('should format numbers', function(){ @@ -462,36 +476,43 @@ angularFilter.uppercase = uppercase; * @example - Snippet: - - - - - - - - - - - - - - - - - - - - - -
    FilterSourceRendered
    html filter -
    <div ng:bind="snippet | html">
    </div>
    -
    -
    -
    no filter
    <div ng:bind="snippet">
    </div>
    unsafe html filter
    <div ng:bind="snippet | html:'unsafe'">
    </div>
    + +
    + Snippet: + + + + + + + + + + + + + + + + + + + + + +
    FilterSourceRendered
    html filter +
    <div ng:bind="snippet | html">
    </div>
    +
    +
    +
    no filter
    <div ng:bind="snippet">
    </div>
    unsafe html filter
    <div ng:bind="snippet | html:'unsafe'">
    </div>
    +
    it('should sanitize the html snippet ', function(){ @@ -543,12 +564,18 @@ angularFilter.html = function(html, option){ * @example - Snippet: + +
    + Snippet: 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 - - -
    - -
    data={{data}}
    -
    -
    - - 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 }'); - }); - -
    - */ -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 - - - Enter truthy text: - - -
    value={{value}}
    -
    - - it('should format boolean', function(){ - expect(binding('value')).toEqual('value=false'); - input('value').enter('truthy'); - expect(binding('value')).toEqual('value=true'); - }); - -
    - */ -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 - - - Enter valid number: - -
    value={{value}}
    -
    - - it('should format numbers', function(){ - expect(binding('value')).toEqual('value=1234'); - input('value').enter('5678'); - expect(binding('value')).toEqual('value=5678'); - }); - -
    - */ -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 - - - Enter a list of items: - - -
    value={{value}}
    -
    - - 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"]'); - }); - -
    - */ -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 - - - Enter text with leading/trailing spaces: - - -
    value={{value|json}}
    -
    - - 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"'); - }); - -
    - */ -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: -
    +
    link 1 (link, don't reload)
    link 2 (link, don't reload)
    link 3 (link, reload!)
    @@ -262,8 +262,8 @@ angularTextMarkup('option', function(text, textNode, parentElement){ * @example - Click me to toggle:
    - + Click me to toggle:
    +
    it('should toggle button', function() { @@ -292,7 +292,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){ * @example - Check me to check both:
    + Check me to check both:
    @@ -323,7 +323,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){ * @example - Check me check multiple:
    + Check me check multiple:

    + Check me to make text readonly:
    @@ -388,7 +388,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){ * @example - Check me to select:
    + Check me to select:
    +
    +
    editorForm = {{editorForm}}
    + +
    + + 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/); + }); + +
    + */ +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 `
    error for key
    `. + */ + +/** + * @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 `
    error for key
    `. + */ + +/** + * @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 @@

    Reload this page with open console, enter text and hit the log button...

    Message: - + 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 @@
    - +
    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 - + 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 @@
    - - +
    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 - - - - Enter valid SSN: -
    - -
    -
    - - 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/); - }); - -
    - * - */ - '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 - - - Enter number:
    - Enter number greater than 10:
    - Enter number between 100 and 200:
    -
    - - 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/); - }); - -
    - * - */ - '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 - - - Enter integer:
    - Enter integer equal or greater than 10:
    - Enter integer between 100 and 200 (inclusive):
    -
    - - 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/); - }); - -
    - */ - '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 - - - Enter valid date: - - - - 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/); - }); - - - * - */ - '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 - - - Enter valid email: - - - - 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/); - }); - - - * - */ - '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 - - - Enter valid phone number: - - - - 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/); - }); - - - * - */ - '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 - - - Enter valid URL: - - - - 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/); - }); - - - * - */ - '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 - - - - - - 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/); - }); - - - * - */ - '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 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 - - - - This input is validated asynchronously: -
    - -
    -
    - - 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/); - }); - -
    - * - */ - /* - * 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 `
    ` elements, for this + * reason angular provides `` alias which behaves identical to `` but allows + * element nesting. + * + * + * @example + + + +
    + + text: + Required! + + text = {{text}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
    +
    +
    + + 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'); + }); + +
    + */ +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 + + + +
    +
    + Single word: + + Required! + + Single word only! + + text = {{text}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
    +
    +
    + + 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'); + }); + +
    + */ + + +/** + * @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 + + + +
    +
    + Email: + + Required! + + Not valid email! + + text = {{text}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
    + myForm.$error.EMAIL = {{!!myForm.$error.EMAIL}}
    +
    +
    + + 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'); + }); + +
    + */ +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 + + + +
    +
    + URL: + + Required! + + Not valid url! + + text = {{text}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
    + myForm.$error.url = {{!!myForm.$error.url}}
    +
    +
    + + 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'); + }); + +
    + */ +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 + + + +
    +
    + List: + + Required! + + names = {{names}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
    +
    +
    + + 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'); + }); + +
    + */ +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 + + + +
    +
    + Number: + + Required! + + Not valid number! + + value = {{value}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
    +
    +
    + + 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'); + }); + +
    + */ +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 + + + +
    +
    + Integer: + + Required! + + Not valid integer! + + value = {{value}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
    +
    +
    + + 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'); + }); + +
    + */ +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 + + + +
    +
    + Value1:
    + Value2:
    + + value1 = {{value1}}
    + value2 = {{value2}}
    +
    +
    + + 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'); + }); + +
    + */ +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 + + + +
    +
    + Red
    + Green
    + Blue
    + + color = {{color}}
    +
    +
    + + it('should change state', function() { + expect(binding('color')).toEqual('blue'); + + input('color').select('red'); + expect(binding('color')).toEqual('red'); + }); + +
    + */ +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 + + + +
    +
    + text: + + Required! + + text = {{text}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
    +
    +
    + + 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'); + }); + +
    + */ +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 `
    + + + 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'); + }); + +
    + */ + + + //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('
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Filter{{input2|json}}
    radioString - <input type="radio" name="input3" value="A">
    - <input type="radio" name="input3" value="B"> -
    - - - {{input3|json}}
    checkboxBoolean<input type="checkbox" name="input4" value="checked">{{input4|json}}
    pulldownString - <select name="input5">
    -   <option value="c">C</option>
    -   <option value="d">D</option>
    - </select>
    -
    - - {{input5|json}}
    multiselectArray - <select name="input6" multiple size="4">
    -   <option value="e">E</option>
    -   <option value="f">F</option>
    - </select>
    -
    - - {{input6|json}}
    - - - - 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"]'); - }); - - - */ - -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. - * - - - I don't validate: -
    - - I need an integer or nothing: -
    -
    - - 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/); - }); - -
    - */ -/** - * @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. - * - - - I cannot be blank:
    -
    - - 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/); - }); - -
    - */ -/** - * @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. - * - - - Enter a comma separated list of items: - -
    list={{list}}
    -
    - - it('should check ng:format', function(){ - expect(binding('list')).toBe('list=["table","chairs","plate"]'); - input('list').enter(',,, a ,,,'); - expect(binding('list')).toBe('list=["a"]'); - }); - -
    - */ -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 - - -
    - - changeCount {{textCount}}
    - - changeCount {{checkboxCount}}
    -
    - - 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'); - }); - -
    - */ -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 `
-
- Color (null not allowed): -
- - Color (null allowed): -
- -

- - Color grouped by shade: -
- - - Select bogus.
-
- Currently selected: {{ {selected_color:color} }} -
-
-
- - - 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'); - }); - - - */ -// 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(' - - - - url of the template: {{url}} -
- + +
+ + url of the template: {{template.url}} +
+ +
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(''); }); @@ -1064,30 +160,34 @@ angularWidget('ng:include', function(element){ * @example - - switch={{switch}} - - -
Settings Div
- Home Span - default -
- + +
+ + selection={{selection}} +
+ +
Settings Div
+ Home Span + default +
+
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'); }); @@ -1568,27 +668,36 @@ angularWidget('ng:view', function(element) { * @example - Person 1:
- Person 2:
- Number of People:
+ +
+ Person 1:
+ Person 2:
+ Number of People:
- - Without Offset: - -
+ + Without Offset: + +
- - With Offset(2): - - + + With Offset(2): + + +
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(''); - scope.$apply(); - assertEquals('abc', scope.model.price); - }); - - it('ChangingTextareaUpdatesModel', function(){ - var scope = this.compile(''); - scope.$apply(); - assertEquals(scope.model.note, 'abc'); - }); - - it('ChangingRadioUpdatesModel', function(){ - var scope = this.compile('
' + - '
'); - scope.$apply(); - assertEquals(scope.model.price, 'A'); - }); - - it('ChangingCheckboxUpdatesModel', function(){ - var scope = this.compile(''); - assertEquals(true, scope.model.price); - }); - it('BindUpdate', function(){ var scope = this.compile('
'); scope.$digest(); assertEquals(123, scope.a); }); - it('ChangingSelectNonSelectedUpdatesModel', function(){ - var scope = this.compile(''); - assertEquals('A', scope.model.price); - }); - - it('ChangingMultiselectUpdatesModel', function(){ - var scope = this.compile(''); - assertJsonEquals(["A", "B"], scope.Invoice.options); - }); - - it('ChangingSelectSelectedUpdatesModel', function(){ - var scope = this.compile(''); - assertEquals(scope.model.price, 'b'); - }); - it('ExecuteInitialization', function(){ var scope = this.compile('
'); assertEquals(scope.a, 123); @@ -236,14 +192,13 @@ describe('Binder', function(){ }); it('RepeaterAdd', function(){ - var scope = this.compile('
'); + var scope = this.compile('
'); 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('
'); - 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('
Hello {{name}}!
'); scope.name = "World"; @@ -459,7 +405,11 @@ describe('Binder', function(){ it('FillInOptionValueWhenMissing', function(){ var scope = this.compile( - ''); + ''); 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('
' + - '
', - 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('
', 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('
' + - '' + - '' + - '
'); + '' + + '' + + '' + + '' + + '' + + '
'); 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('
' + - '' + - '
'); + '' + + '
'); 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(''); - scope.$apply(); - assertEquals("abc", scope.myName); - }); - - it('ItShouldUseFormaterForText', function(){ - var scope = this.compile(''); - 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('')(); - 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('')(); - 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( - '')(); - 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( - '')(); - 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('
{{ {key:"value", $$key:"hide"} }}
'); + 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('
{{ {key:"value", $$key:"hide"}  }}
'); + 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 = $('
abcxyz
'); + 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('bla'); + }); + + it('should fire on replaceAll()', function(){ + $('bla').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('foo'); @@ -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(''); + compile(''); scope.$digest(); - expect(sortedHtml(element).replace(' selected="true"', '')).toEqual(''); + expect(sortedHtml(element).replace(' selected="true"', '')). + toEqual(''); scope.name = 'Misko'; scope.$digest(); - expect(sortedHtml(element).replace(' selected="true"', '')).toEqual(''); + expect(sortedHtml(element).replace(' selected="true"', '')). + toEqual(''); }); it('should translate {{}} in attributes', function(){ @@ -69,24 +75,24 @@ describe("markups", function(){ it('should populate value attribute on OPTION', function(){ - compile(''); + compile(''); expect(element).toHaveValue('abc'); }); it('should ignore value if already exists', function(){ - compile(''); + compile(''); expect(element).toHaveValue('abc'); }); it('should set value even if newlines present', function(){ - compile(''); + compile(''); 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(''); + compile(''); 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( - '' + + ' ' + + ' ' + '' ); $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( + '' + ); + $root.dsl.select('test').option('one'); + expect(_jQuery('[ng\\:model="test"]').val()).toEqual('A'); }); it('should select multiple options', function() { doc.append( - '' + ' ' + ' ' + ' ' + '' ); $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(''); + doc.append(''); $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( - '
' + - '
' + '
' + + '
' ); 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(''); + doc.append(''); 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(''); - expect(_jQuery('input[name="test.input"]'). + doc.append(''); + 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( - '' + - '' + '' + + '' ); // 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(''); + doc.append(''); $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(''); + doc.append(''); 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 @@ basic - + text.basic={{text.basic}} password - + text.password={{text.password}} hidden - + text.hidden={{text.hidden}} Input selection field radio - Female
- Male + Female
+ Male gender={{gender}} checkbox - Tea
- Coffe + Tea
+ Coffe
checkbox={{checkbox}}
@@ -51,7 +51,7 @@ select - @@ -62,7 +62,7 @@ multiselect - 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 += ''; + 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('$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(''); - 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('
'); + var scope = angular.compile(doc)(); + expect(doc.data('$form')).toBeTruthy(); + }); + + + it('should prevent form submission', function(){ + var startingUrl = '' + window.location; + doc = angular.element(''); + 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(''); + 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(''); + 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(''); + 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('' + + ''); + 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(''); + 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('
'); + scope = angular.compile(doc)(); + expect(doc.find('input').attr('type')).toEqual('text'); + }); + + + it('should not change html5 types to text', function(){ + doc = angular.element('
'); + 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(''); + + 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(''); + 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('
'+ + ''+ + '
'); + 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(''); + + 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(''); + expect(scope.age).toBeNull(); + expect(scope.$element[0].value).toEqual(''); + }); + + + it("should show incorrect text while number does not parse", function(){ + compile(''); + 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(''); + + 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(''); + scope.$digest(); + expect(scope.$element.val()).toEqual(''); + expect(scope.age).toEqual(null); + }); + }); + + + describe("checkbox", function(){ + it("should format booleans", function(){ + compile(''); + expect(scope.name).toBe(false); + expect(scope.$element[0].checked).toBe(false); + }); + + + it('should support type="checkbox" with non-standard capitalization', function(){ + compile(''); + + browserTrigger(element); + expect(scope.checkbox).toBe(true); + + browserTrigger(element); + expect(scope.checkbox).toBe(false); + }); + + + it('should allow custom enumeration', function(){ + compile(''); + + 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('', 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('', + 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(''); + + 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('
' + + '' + + '' + + '' + + '
'); + 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('
' + + '' + + '' + + '' + + '
'); + + 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('
' + + '' + + '' + + '' + + '
'); + + 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(''); + 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(''); + 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(''); + }).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(''); + 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(''); + 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 = [''); + 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(''), + 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(''); + scope.a = 'foo'; + scope.b = 'bar'; + scope.$digest(); + + expect(scope.$element.text()).toBe('foobarC'); + }); + + it('should require', function(){ + compile(''); + 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(''); + + expect(element).toBeValid(); + expect(element).toBePristine(); + }); + + }); + + + describe('select-multiple', function(){ + it('should support type="select-multiple"', function(){ + compile(''); + scope.selection = ['A']; + scope.$digest(); + expect(element[0].childNodes[0].selected).toEqual(true); + }); + + it('should require', function(){ + compile(''); + + 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 = 'blank' : '') + + (unknown ? '' : '') + + ''; + 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(''); + }).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(''); + expect(sortedHtml(options[1])).toEqual(''); + expect(sortedHtml(options[2])).toEqual(''); + }); + + 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(''); + expect(sortedHtml(options[1])).toEqual(''); + expect(sortedHtml(options[2])).toEqual(''); + 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(''); + + 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(''); + + scope.values.push({name:'B'}); + scope.$digest(); + expect(select.find('option').length).toEqual(2); + expect(sortedHtml(select.find('option')[0])).toEqual(''); + expect(sortedHtml(select.find('option')[1])).toEqual(''); + }); + + 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(''); + expect(sortedHtml(select.find('option')[1])).toEqual(''); + + scope.values.pop(); + scope.$digest(); + expect(select.find('option').length).toEqual(1); + expect(sortedHtml(select.find('option')[0])).toEqual(''); + + 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(''); + expect(sortedHtml(options[1])).toEqual(''); + expect(sortedHtml(options[2])).toEqual(''); + }); + + 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(''); - 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(''); - 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('
'+ - ''+ - '
'); - expect(scope.obj['abc'].name).toEqual('Misko'); - }); - - - describe("ng:format", function() { - it("should format text", function() { - compile(''); - 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(''); - expect(scope.age).toBeNull(); - expect(scope.$element[0].value).toEqual(''); - }); - - it("should show incorect text while number does not parse", function() { - compile(''); - 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(''); - scope.age = 456; - scope.$digest(); - expect(scope.$element.val()).toEqual('456'); - }); - - it("should not clober text if model changes due to itself", function() { - compile(''); - - 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(''); - scope.$digest(); - expect(scope.$element.val()).toEqual(''); - expect(scope.age).toEqual(null); - }); - }); - - - describe("checkbox", function() { - it("should format booleans", function() { - compile(''); - expect(scope.name).toEqual(false); - expect(scope.$element[0].checked).toEqual(false); - }); - - it('should support type="checkbox"', function() { - compile(''); - 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(''); - 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('', - 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(''); - 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(''); - 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(''); - expect(element.hasClass('ng-validation-error')).toBeFalsy(); - expect(element.attr('ng-validation-error')).toBeFalsy(); - }); - - it("should ignore readonly widgets", function() { - compile(''); - expect(element.hasClass('ng-validation-error')).toBeFalsy(); - expect(element.attr('ng-validation-error')).toBeFalsy(); - }); - - it("should process ng:required", function() { - compile('', 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('', - 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(''); - 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('
' + - '' + - '' + - '' + - '
'); - 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('
' + - '' + - '' + - '' + - '
'); - - expect(scope.choose).toEqual('C'); - }); - - it('should honor model over html checked keyword before', function() { - compile('
' + - '' + - '' + - '' + - '
'); - - expect(scope.choose).toEqual('A'); - }); - - }); - - - describe('select-one', function() { - it('should initialize to selected', function() { - compile( - ''); - 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(''); - 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(''); - 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(''); - 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(''); - 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(''); - }).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('' + '
first:{{name}}
' + '
second:{{name}}
' + @@ -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 = 'blank' : '') + - (unknown ? '' : '') + - ''; - 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(''); - }).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(''); - expect(sortedHtml(options[1])).toEqual(''); - expect(sortedHtml(options[2])).toEqual(''); - }); - - 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(''); - expect(sortedHtml(options[1])).toEqual(''); - expect(sortedHtml(options[2])).toEqual(''); - 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(''); - - 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(''); - - scope.values.push({name:'B'}); - scope.$digest(); - expect(select.find('option').length).toEqual(2); - expect(sortedHtml(select.find('option')[0])).toEqual(''); - expect(sortedHtml(select.find('option')[1])).toEqual(''); - }); - - 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(''); - expect(sortedHtml(select.find('option')[1])).toEqual(''); - - scope.values.pop(); - scope.$digest(); - expect(select.find('option').length).toEqual(1); - expect(sortedHtml(select.find('option')[0])).toEqual(''); - - 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(''); - expect(sortedHtml(options[1])).toEqual(''); - expect(sortedHtml(options[2])).toEqual(''); - }); - - 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('
');