diff --git a/Rakefile b/Rakefile index 0c9efcf949d5..e71fd0cf288b 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 8e52731bd4ea..906c3f98fc38 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 89519da60ab2..d11462157091 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 000000000000..434fe6c2235e --- /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 874fe4bb5b51..50fe156056ce 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 05928ab46a79..2ec86346425a 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 585c66a6c213..d38008f29d57 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 a1e4a8b2e90a..fad4c1ffb021 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 2aeafc4d2d2b..c74b203b5204 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 8018a399ece3..9562aaff7cda 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 0f99e46b035d..3b233551f6cf 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 177a5e87a051..ab5a897bc1c3 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 000000000000..6849ff4eba01 --- /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 15ae3b34b49f..7a6653e94358 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 a35541d01c46..b4659b0cf840 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 f5db7f948654..fcf15044d0ed 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 0046dd7fea93..44206f7c9b33 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 4a4b2d657965..4bd3f1b22086 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 ebb7d92346e9..27daec9fe420 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 2ecd8f196824..000000000000 --- 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 82a14fb4c149..000000000000 --- 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 bf983cd582ae..000000000000 --- 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 ca0ca99a33d0..32514eb90357 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 835b0b510bb0..000000000000
    --- 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 76df92b531fb..000000000000 --- 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 b2aab1610fbb..8d609afa4677 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 3bf71cf1fc8a..591fb859f0b1 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 ec5469560822..89a1b0cb7917 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 72aa26c976e0..d05a8e7cd657 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 fa0c1e1f13c5..eaf7f4ab8c9c 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 80b10f652a51..7d8e34303a97 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 2fa5dca88601..74500b35ad1f 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 000000000000..60e947a59583 Binary files /dev/null and b/docs/img/form_data_flow.png differ diff --git a/docs/spec/ngdocSpec.js b/docs/spec/ngdocSpec.js index 106fd22b47e8..2afcc3d4e5af 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 8a20e64a0bd2..1a4f5d257716 100644 --- a/docs/src/ngdoc.js +++ b/docs/src/ngdoc.js @@ -13,6 +13,11 @@ exports.scenarios = scenarios; exports.merge = merge; exports.Doc = Doc; +var BOOLEAN_ATTR = {}; +['multiple', 'selected', 'checked', 'disabled', 'readOnly', 'required'].forEach(function(value, key) { + BOOLEAN_ATTR[value] = true; +}); + ////////////////////////////////////////////////////////// function Doc(text, file, line) { if (typeof text == 'object') { @@ -385,69 +390,21 @@ Doc.prototype = { }); }, - html_usage_formatter: function(dom){ - var self = this; - dom.h('Usage', function(){ - dom.h('In HTML Template Binding', function(){ - dom.code(function(){ - if (self.inputType=='select') - dom.text(''); - }); - }); - - dom.h('In JavaScript', function(){ - dom.code(function(){ - dom.text('angular.validator.'); - dom.text(self.shortName); - dom.text('('); - self.parameters(dom, ', '); - dom.text(')'); + dom.code(function(){ + dom.text(''); }); - 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 @@

Address Book

Tweets: {{$anchor.user}}

- [ Filter: + [ Filter: | << All ]
Loading...
diff --git a/example/tweeter/tweeter_demo.html b/example/tweeter/tweeter_demo.html index 0df794f4317b..6966192a5114 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 0df9fbb463fc..3c74339e906d 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 a2e1966ee860..2d8845a23803 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 931c56dd24bb..8618c44dd79b 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 ca151c309839..de77681b7981 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 f9ae87f7ab99..ab0bb2a5cc75 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 + + JVBERi0xLjMKJcTl8uXrp/Og0MTGCjUgMCBvYmoKPDwgL0xlbmd0aCA2IDAgUiAvRmls + dGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAGlWsluHTcW3fMruDAQGWiXi1OxuI2c + AB3ASKctJAu7F+rn51hpyZJlxUH+Pudcjm/QgHSM4PGyOB6eO/BSn/VP+rOe8S/ERUfn + 9O1W/6I/6ZenX4zefNFG/n3Z6BfzFDT/Hxp+YDPLZvO0rouNflHzlHyY08qO6GbmVduY + 9JX2PkjpUvtgp+isSD4k/GZZykvQG7RA7Rwmm3xU7JC0SXEKmKL1NsnIdxm4CXnGJl5i + AfNklxX9c3OVfB8L5ToNZh2l+8q5FRZ5ieaqrrxOwp3WHW/0x1a+0h8Axw/4/zf9Vnv8 + +w8gf59RPn2jZ8UjeHOKszByGi/4w+PYXMkgPvgCIUsdQkpExy95lygrj91mCIONU5yD + kw5JBxcm76JrEAZv8neeTRMyhFUE/HV3tYpAtbFYrtMIOE0avwxlJa24SELYVl4n4Y4I + IX8rhCxf6TfgKvgGTMA3cuuzamDZuICaFhs0HhsEat+eaeMLll6/MBH00S+sn9ZAOpxd + 6Zffm2kG3mcf9Ft1cvqcFLf65Po52IzfT+X37nb/y2X5Un+3tUFpqU50afHu5Ev9tqmF + Ov5NadN6v3v+HLQ4+0F/d5Z3Ss2CPkGz1sU4rCp5590cseaiWQGknufZFG40sROkVZEZ + YfVUPCOUCUBE+mauOL8Umji3isZUNXVQVRKEv5kbKA20YD0OsnZjEWPJKbNQ5F7tpoT/ + VhiSvAAMlVpdXWGlQ1s/OdGESoxWQQXb1x1VdMeA5ZyxgNTEDlKrEkPjuX0osgghSF+Q + lqJbYwUpCZSoLbYLh0CQPH4zSCx13aFECHI3GiGOlUFCoci9ugJiygKoLK2urrCApNr6 + CVITKkit4pgKdXuTVcgvZgpgFME7rkLGwAuQc7sqdPLLc332W+bucBKPj7r4yc/JQkWP + D/xWn1wUVXlffn+VX3XSVOfu/1U4BdPyr+3tZntz9/v5pb69IJtMEvsRorbGZl2ht4jJ + w8DCfvzzyuhX19TVf28vz+8uvm5Pry+vby+utne3FxuFQaoXnadlTt5YUWIXLU6VvhRa + YMFLKLN1RRktzlsmuNJuXsqkpQ6K4ozLGtzaOTuXVj5NYtqGGph3WazudQ68RR3Haj1h + v+fFrsPo+CQ9+xpqzbjWUoexrE20GQaF0tNaV9ZVR+81ZQ2g/0GdOqg5bLPTjy6Me+Qa + 6owFCYxVV1VRPcSZWnJYS3vyNvvogczikqkZrYOHDQMnLGxLg6rVDcfV6hrofnUTXJVV + vYYRCsfa9KPxCcaIdcNxBXAwgUT9uALoyVaqraHWEKpKrVY3HFera+DV0ftBtDUMx9Xq + Wr/7a2A7h1Zlj8NxVSTQqhxXQ6utvdZgrOG4au3R4GC0bNmmAQqgWbT32zO4VvF/Z7DX + tkQKMEN2ndbZLzD22cJB83OQcHJze32zvb37sxu6wWZQ3Y/bDOcQ0j5qLmhzirnAwp5q + LtwaRupRHFgHsdPLxVWi7sY3F+mqurgwsOxiWCQKaBbHhbnzC8JILYoDqyi2I3dloF6B + iTadXg7izrcdYeAO1zvQpu6nskZx840wFHa4wopjUQKPpveCIemarJzYlY4AxAEfn/GB + wRMauWVHcx0Y1OFUDgEq7zx9sNV1k+EgDJpKcYQTYgclDzTil9i3Gj/Oe7+w05BWpTZV + LuT9VDgRxOHmVm0HhV04UfFYPFG0zi0IvUs8cVzlXALhF4aY+yr37Or6/fby5/PL3+Hr + S3ihBq2731N7+LPHte7vOWkfR5poipeqnSzFRhMf4Z947pUmnuwfREQXnSYIIcUEDoP5 + gSYewkATigNNKNbjrAO1CuUx0XD6FHvjfWFsKGahN837Af2arR5owq3v0CQjdRibj1qn + EMsMWqcpDkaMYkPPIjzl9W2oIPuriAs5bzFV1BZOdkfrbJq71lEY4KTY4VQU27brQL0C + Ew0ocd6db6MwOkArZqF/LftpcHK3Teso7MCJCvVErWNGQPwOovh7tI7hZVgX7ZSE8t3R + Pft6sf1jV+meFh4Hm6joiIztA5FxcXW83vJSG23wiLSnuEYTUkTJxRSTMWoMjIMkkXqA + A3EgCb+2Uw85n2SGmgUGaBAD48XePuTkSdPg4HE9q9E3hYElEMd4l1/baYYy0FBBfdgR + jwoqYEVjQ0kw9aZ1Q40m3G+jCYWRJipj9bDWaSNJlbZNigOgFCtAypTsUq3QRlJMXYyi + sdXEmTXfiRueZsXpFDwVhQFPil3rNMW27zpQr8BEHSZlIO582xF6Q1lv93UQ84E3OLFb + 1eDk1kc4C1KH6acjEWbg9Xn0dVbvhZe4wOHaNyNTkH0d7iwlvHz14+vm444mM4jnAuTN + iljC5EuHiRPTDsyOGm9pKQEJoLYuQbEpGDJLSnZhLiOHhCEyxQr3jpwqGpoFQS8CGOvj + tOJygS5lv5IFZnZSUm7OIEqIMxJCzKeZ6GCOnIH7Dkh/IHcD7BikcaVpmZFpxFLnID7u + I2r3OktANiMvOiMvOmTzWuqz9YDhm/yK7G+fziILsjIpM0xnPbYIPwgNHaYbOnNTNRG7 + f7GjT6oQ2xWZU4EYrjhDbHEVW+dEqxJKxMeEReS9udUgtyZglwa4S+Bk6P0NkhxzVI5X + 3wUpPI6aMAvCLDPlfBnXxg37lR4PYCLXx7wn1WxxMBAuwTQuJgOM5LhJi3LJYsdYQ93x + fu8PALcA3Pm6A3DuATTDggHH+bzFxKKfzuT5sFw4E2x6b77SG6ttCCsc6dEZM4ndCotf + SYybSVwBE1JcLiJ/6xBdSFDtZ9xsPUxurzFoXG/JtZXDQpkkJs7QEObu4M0crA6IDfxA + ziVNKfBxQVWckRIG4UEs7EkCEPDYGgOVWTgpemdmeYe0xuKQOEZCqMLMzgxcaufHYZbp + qDZLSDvT+QgkYDiYkoHecDqu1nikU8bpROdK54YyFecQZQnPkJfFSDFN8K644yD1bUEZ + jA0TBT7BMvgCMs7ZLAhgWgX4XDCudQrZK6wchGWGxyMhxlTnhKcehH9ovtiEY1sTrIf4 + Bi7Qgv4Lcl0a90GkpkR7rbXQWiiC53LE57CZNwZWiInaFZlu7vpY5/tBlqRMmw7+B0A7 + 2bVMh5WRYpLi7tMxeW6wo93pSucSZz1kLOg+BGTuz65yIyIW67qCilAUk2YYWdCKXIbu + EzhyrtRQs2EuugiIgQghCpb6PjPHjH1w0Jh4ZAn2bWF2VvRMMELMhWjfwRrD3vKcYRAS + QMRLz2QM3kDo+KDcCTTA+wH6S3BREGbENvS+H2GJ2Ikw57M4aL8iVhvms0l2vkh0VOaj + zZyZ/2gQ7/V+Mo9tQKKtQoxHnJWXfR8AFAMwaLmYC+sQ7y+wmr3G8lZeRWWhYHAcDNYR + 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 + + 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 + + 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 4160fc6af687..0ff6111a9e75 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 e18c4f2e0a03..80902dc2d19e 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 3f061e1b8249..c93d4aa40eb0 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 8410adf4ab6b..e356919765d8 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 775a6009f5a5..b44ae5edf63d 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 caa51a065c19..7c218c6e3acc 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:
- -
- - The master object is NOT equal to the form object. - -
master={{master}}
-
form={{form}}
+ +
+ Salutation:
+ Name:
+ +
+ + The master object is NOT equal to the form object. + +
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:
-
- - The greeting object is - NOT equal to - {salutation:'Hello', name:'world'}. - -
greeting={{greeting}}
+ +
+ Salutation:
+ Name:
+
+ + The greeting object is + NOT equal to + {salutation:'Hello', name:'world'}. + +
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 ed12441a3bda..77d1c684ea54 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 c5f5bf1be0f2..e4fc0622c5fd 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 f9d9643d7a2e..7a1752d278b5 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 bec54b8e350f..6a5bf6c4ef26 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'] - -
    -
  • - - - [X] -
  • -
-
people = {{people}}
+ +
+ [add empty] + [add 'John'] + [add 'Mary'] + +
    +
  • + + + [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 dd67ddc787e1..852d04cd34f8 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 c5d886eac079..0fcd442b96f2 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 2fadc9d7337e..000000000000 --- 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 5f761f929951..0052055cf020 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 1adad3e0cfb6..40f4322b43f1 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 7c1b2a9f5320..000000000000 --- 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 09945732e0ae..3dacd1179a4f 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 f6e0be183a72..915f2d92b054 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 73c73b04f1b2..b78cca911828 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 2f3f677a26ae..9795e4fcfbb7 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 09e7d0708d8c..4981c078b7f7 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 72a995fcc1d3..000000000000 --- 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 000000000000..bc34bf0d068f --- /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 000000000000..f82027f482d8 --- /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 000000000000..f397180e5c7f --- /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:
- - - Without Offset: - -
- - - With Offset(2): - - + +
+ Person 1:
+ Person 2:
+ Number of People:
+ + + Without Offset: + +
+ + + With Offset(2): + + +
it('should show correct pluralized string', function(){ diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 9a1a20c77e8a..0332c01bc1fc 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 9683a7b769e4..bd77d734af7d 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 224c449f9110..fa7fde6098b4 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 de4354a01b7f..692bc5aebfb2 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 8f438671ec13..000000000000 --- 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 b0bb15bca309..2bd7241f32b8 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 a5e1901c3948..980a673c344d 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 492396c5094f..fa41e5a95d52 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 f44a9a594019..000000000000 --- 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 c925bdb51e0a..1cbb92b0a6ee 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 000000000000..0953bdac79bc --- /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 bb00ca25fee5..28cc7b90b497 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 2704e0dca1dc..bd77c0580b32 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 c5d0a29d3e7d..3fc69c14c162 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 e19a33f499c0..fb27f72ec063 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 000000000000..5223cededecd --- /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 fe7efe3813de..000000000000 --- 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 c8c8cbeb5bb0..5aba2a1f3ea0 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 3b9d9208ff87..41a6455c8f76 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 000000000000..7c575c3384e8 --- /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 000000000000..31f8c59ccbe8 --- /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 000000000000..6adf8b937fc8 --- /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 02d0ef71f69a..9361d28d6b4a 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('
');