feat(forms): new and improved forms

This commit is contained in:
Misko Hevery 2011-09-08 13:56:29 -07:00 committed by Igor Minar
parent df6d2ba326
commit 4f78fd692c
104 changed files with 7044 additions and 3963 deletions

View file

@ -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'),

8
angularFiles.js vendored
View file

@ -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'
],

View file

@ -7,12 +7,3 @@
.ng-format-negative {
color: red;
}
/*****************
* indicators
*****************/
.ng-input-indicator-wait {
background-image: url("data:image/png;base64,R0lGODlhEAAQAPQAAP///wAAAPDw8IqKiuDg4EZGRnp6egAAAFhYWCQkJKysrL6+vhQUFJycnAQEBDY2NmhoaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA==");
background-position: right;
background-repeat: no-repeat;
}

View file

@ -0,0 +1,92 @@
@ngdoc overview
@name angular.inputType
@description
Angular {@link guide/dev_guide.forms forms} allow you to build complex widgets. However for
simple widget which are based on HTML input text element a simpler way of providing the validation
and parsing is also provided. `angular.inputType` is a short hand for creating a widget which
already has the DOM listeners and `$render` method supplied. The only thing which needs to
be provided by the developer are the optional `$validate` listener and
`$parseModel` or `$parseModel` methods.
All `inputType` widgets support:
- CSS classes:
- **`ng-valid`**: when widget is valid.
- **`ng-invalid`**: when widget is invalid.
- **`ng-pristine`**: when widget has not been modified by user action.
- **`ng-dirty`**: when has been modified do to user action.
- Widget properties:
- **`$valid`**: When widget is valid.
- **`$invalid`**: When widget is invalid.
- **`$pristine`**: When widget has not been modified by user interaction.
- **`$dirty`**: When user has been modified do to user interaction.
- **`$required`**: When the `<input>` element has `required` attribute. This means that the
widget will have `REQUIRED` validation error if empty.
- **`$disabled`**: When the `<input>` element has `disabled` attribute.
- **`$readonly`**: When the `<input>` element has `readonly` attribute.
- Widget Attribute Validators:
- **`required`**: Sets `REQUIRED` validation error key if the input is empty
- **`ng:pattern`** Sets `PATTERN` validation error key if the value does not match the
RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
patterns defined as scope expressions.
# Example
<doc:example>
<doc:source>
<script>
angular.inputType('json', function(){
this.$parseView = function(){
try {
this.$modelValue = angular.fromJson(this.$viewValue);
if (this.$error.JSON) {
this.$emit('$valid', 'JSON');
}
} catch (e) {
this.$emit('$invalid', 'JSON');
}
}
this.$parseModel = function(){
this.$viewValue = angular.toJson(this.$modelValue);
}
});
function Ctrl(){
this.data = {
framework:'angular',
codenames:'supper-powers'
}
this.required = false;
this.disabled = false;
this.readonly = false;
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
<input type="json" ng:model="data" size="80"
ng:required="{{required}}" ng:disabled="{{disabled}}"
ng:readonly="{{readonly}}"/><br/>
Required: <input type="checkbox" ng:model="required"> <br/>
Disabled: <input type="checkbox" ng:model="disabled"> <br/>
Readonly: <input type="checkbox" ng:model="readonly"> <br/>
<pre>data={{data}}</pre>
<pre>myForm={{myForm}}</pre>
</form>
</div>
</doc:source>
<doc:scenario>
it('should invalidate on wrong input', function(){
expect(element('form[name=myForm]').prop('className')).toMatch('ng-valid');
input('data').enter('{}');
expect(binding('data')).toEqual('data={\n }');
input('data').enter('{');
expect(element('form[name=myForm]').prop('className')).toMatch('ng-invalid');
});
</doc:scenario>
</doc:example>

View file

@ -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

View file

@ -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

View file

@ -9,9 +9,7 @@ detection, and preventing invalid form submission.
<doc:example>
<doc:source>
<script>
UserForm.$inject = ['$invalidWidgets'];
function UserForm($invalidWidgets){
this.$invalidWidgets = $invalidWidgets;
function UserForm(){
this.state = /^\w\w$/;
this.zip = /^\d\d\d\d\d$/;
this.master = {
@ -42,31 +40,34 @@ detection, and preventing invalid form submission.
</script>
<div ng:controller="UserForm">
<label>Name:</label><br/>
<input type="text" name="form.name" ng:required/> <br/><br/>
<form name="myForm">
<label>Address:</label><br/>
<input type="text" name="form.address.line1" size="33" ng:required/> <br/>
<input type="text" name="form.address.city" size="12" ng:required/>,
<input type="text" name="form.address.state" size="2" ng:required ng:validate="regexp:state"/>
<input type="text" name="form.address.zip" size="5" ng:required
ng:validate="regexp:zip"/><br/><br/>
<label>Name:</label><br/>
<input type="text" ng:model="form.name" required/> <br/><br/>
<label>Contacts:</label>
[ <a href="" ng:click="form.contacts.$add()">add</a> ]
<div ng:repeat="contact in form.contacts">
<select name="contact.type">
<option>email</option>
<option>phone</option>
<option>pager</option>
<option>IM</option>
</select>
<input type="text" name="contact.value" ng:required/>
[ <a href="" ng:click="form.contacts.$remove(contact)">X</a> ]
</div>
<button ng:click="cancel()" ng:disabled="{{master.$equals(form)}}">Cancel</button>
<button ng:click="save()" ng:disabled="{{$invalidWidgets.visible() ||
master.$equals(form)}}">Save</button>
<label>Address:</label> <br/>
<input type="text" ng:model="form.address.line1" size="33" required/> <br/>
<input type="text" ng:model="form.address.city" size="12" required/>,
<input type="text" ng:model="form.address.state" size="2"
ng:pattern="state" required/>
<input type="text" ng:model="form.address.zip" size="5"
ng:pattern="zip" required/><br/><br/>
<label>Contacts:</label>
[ <a href="" ng:click="form.contacts.$add()">add</a> ]
<div ng:repeat="contact in form.contacts">
<select ng:model="contact.type">
<option>email</option>
<option>phone</option>
<option>pager</option>
<option>IM</option>
</select>
<input type="text" ng:model="contact.value" required/>
[ <a href="" ng:click="form.contacts.$remove(contact)">X</a> ]
</div>
<button ng:click="cancel()" ng:disabled="{{master.$equals(form)}}">Cancel</button>
<button ng:click="save()" ng:disabled="{{myForm.$invalid || master.$equals(form)}}">Save</button>
</form>
<hr/>
Debug View:
@ -90,7 +91,7 @@ master.$equals(form)}}">Save</button>
expect(element(':button:contains(Cancel)').attr('disabled')).toBeFalsy();
element(':button:contains(Cancel)').click();
expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy();
expect(element(':input[name="form.name"]').val()).toEqual('John Smith');
expect(element(':input[ng\\:model="form.name"]').val()).toEqual('John Smith');
});
</doc:scenario>
</doc:example>

View file

@ -15,6 +15,7 @@ to retrieve Buzz activity and comments.
<script>
BuzzController.$inject = ['$resource'];
function BuzzController($resource) {
this.userId = 'googlebuzz';
this.Activity = $resource(
'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
{alt: 'json', callback: 'JSON_CALLBACK'},
@ -32,7 +33,7 @@ to retrieve Buzz activity and comments.
};
</script>
<div ng:controller="BuzzController">
<input name="userId" value="googlebuzz"/>
<input ng:model="userId"/>
<button ng:click="fetch()">fetch</button>
<hr/>
<div class="buzz" ng:repeat="item in activities.data.items">

View file

@ -24,25 +24,26 @@ allow a user to enter data.
<div ng:controller="FormController" class="example">
<label>Name:</label><br/>
<input type="text" name="user.name" ng:required/> <br/><br/>
<input type="text" ng:model="user.name" required/> <br/><br/>
<label>Address:</label><br/>
<input type="text" name="user.address.line1" size="33" ng:required/> <br/>
<input type="text" name="user.address.city" size="12" ng:required/>,
<input type="text" name="user.address.state" size="2" ng:required ng:validate="regexp:state"/>
<input type="text" name="user.address.zip" size="5" ng:required
ng:validate="regexp:zip"/><br/><br/>
<input type="text" ng:model="user.address.line1" size="33" required> <br/>
<input type="text" ng:model="user.address.city" size="12" required>,
<input type="text" ng:model="user.address.state" size="2"
ng:pattern="state" required>
<input type="text" ng:model="user.address.zip" size="5"
ng:pattern="zip" required><br/><br/>
<label>Phone:</label>
[ <a href="" ng:click="user.contacts.$add()">add</a> ]
<div ng:repeat="contact in user.contacts">
<select name="contact.type">
<select ng:model="contact.type">
<option>email</option>
<option>phone</option>
<option>pager</option>
<option>IM</option>
</select>
<input type="text" name="contact.value" ng:required/>
<input type="text" ng:model="contact.value" required/>
[ <a href="" ng:click="user.contacts.$remove(contact)">X</a> ]
</div>
<hr/>
@ -68,19 +69,21 @@ ng:validate="regexp:zip"/><br/><br/>
});
it('should validate zip', function(){
expect(using('.example').element(':input[name="user.address.zip"]').prop('className'))
.not().toMatch(/ng-validation-error/);
expect(using('.example').
element(':input[ng\\:model="user.address.zip"]').
prop('className')).not().toMatch(/ng-invalid/);
using('.example').input('user.address.zip').enter('abc');
expect(using('.example').element(':input[name="user.address.zip"]').prop('className'))
.toMatch(/ng-validation-error/);
expect(using('.example').
element(':input[ng\\:model="user.address.zip"]').
prop('className')).toMatch(/ng-invalid/);
});
it('should validate state', function(){
expect(using('.example').element(':input[name="user.address.state"]').prop('className'))
.not().toMatch(/ng-validation-error/);
expect(using('.example').element(':input[ng\\:model="user.address.state"]').prop('className'))
.not().toMatch(/ng-invalid/);
using('.example').input('user.address.state').enter('XXX');
expect(using('.example').element(':input[name="user.address.state"]').prop('className'))
.toMatch(/ng-validation-error/);
expect(using('.example').element(':input[ng\\:model="user.address.state"]').prop('className'))
.toMatch(/ng-invalid/);
});
</doc:scenario>
</doc:example>
@ -94,7 +97,7 @@ available in
* For debugging purposes we have included a debug view of the model to better understand what
is going on.
* The {@link api/angular.widget.HTML input widgets} simply refer to the model and are auto bound.
* The inputs {@link api/angular.validator validate}. (Try leaving them blank or entering non digits
* The inputs {@link guide/dev_guide.forms validate}. (Try leaving them blank or entering non digits
in the zip field)
* In your application you can simply read from or write to the model and the form will be updated.
* By clicking the 'add' link you are adding new items into the `user.contacts` array which are then

View file

@ -5,9 +5,16 @@
<doc:example>
<doc:source>
Your name: <input type="text" name="name" value="World"/>
<hr/>
Hello {{name}}!
<script>
function HelloCntl(){
this.name = 'World';
}
</script>
<div ng:controller="HelloCntl">
Your name: <input type="text" ng:model="name" value="World"/>
<hr/>
Hello {{name}}!
</div>
</doc:source>
<doc:scenario>
it('should change the binding when user enters text', function(){

View file

@ -16,7 +16,7 @@ directives per element.
You add angular directives to a standard HTML tag as in the following example, in which we have
added the {@link api/angular.directive.ng:click ng:click} directive to a button tag:
<button name="button1" ng:click="foo()">Click This</button>
<button ng:model="button1" ng:click="foo()">Click This</button>
In the example above, `name` is the standard HTML attribute, and `ng:click` is the angular
directive. The `ng:click` directive lets you implement custom behavior in an associated controller

View file

@ -51,9 +51,15 @@ You can try evaluating different expressions here:
<doc:example>
<doc:source>
<div ng:init="exprs=[]" class="expressions">
<script>
function Cntl2(){
this.exprs = [];
this.expr = '3*10|currency';
}
</script>
<div ng:controller="Cntl2" class="expressions">
Expression:
<input type='text' name="expr" value="3*10|currency" size="80"/>
<input type='text' ng:model="expr" size="80"/>
<button ng:click="exprs.$add(expr)">Evaluate</button>
<ul>
<li ng:repeat="expr in exprs">
@ -84,9 +90,18 @@ the global state (a common source of subtle bugs).
<doc:example>
<doc:source>
<div class="example2" ng:init="$window = $service('$window')">
Name: <input name="name" type="text" value="World"/>
<button ng:click="($window.mockWindow || $window).alert('Hello ' + name)">Greet</button>
<script>
function Cntl1($window){
this.name = 'World';
this.greet = function() {
($window.mockWindow || $window).alert('Hello ' + this.name);
}
}
</script>
<div class="example2" ng:controller="Cntl1">
Name: <input ng:model="name" type="text"/>
<button ng:click="greet()">Greet</button>
</div>
</doc:source>
<doc:scenario>
@ -158,7 +173,7 @@ Extensions: You can further extend the expression vocabulary by adding new metho
{name:'Mike', phone:'555-4321'},
{name:'Adam', phone:'555-5678'},
{name:'Julie', phone:'555-8765'}]"></div>
Search: <input name="searchText"/>
Search: <input ng:model="searchText"/>
<table class="example3">
<tr><th>Name</th><th>Phone</th><tr>
<tr ng:repeat="friend in friends.$filter(searchText)">

View file

@ -0,0 +1,610 @@
@ngdoc overview
@name Developer Guide: Forms
@description
# Overview
Forms allow users to enter data into your application. Forms represent the bidirectional data
bindings in Angular.
Forms consist of all of the following:
- the individual widgets with which users interact
- the validation rules for widgets
- the form, a collection of widgets that contains aggregated validation information
# Form
A form groups a set of widgets together into a single logical data-set. A form is created using
the {@link api/angular.widget.form &lt;form&gt;} element that calls the
{@link api/angular.service.$formFactory $formFactory} service. The form is responsible for managing
the widgets and for tracking validation information.
A form is:
- The collection which contains widgets or other forms.
- Responsible for marshaling data from the model into a widget. This is
triggered by {@link api/angular.scope.$watch $watch} of the model expression.
- Responsible for marshaling data from the widget into the model. This is
triggered by the widget emitting the `$viewChange` event.
- Responsible for updating the validation state of the widget, when the widget emits
`$valid` / `$invalid` event. The validation state is useful for controlling the validation
errors shown to the user in it consist of:
- `$valid` / `$invalid`: Complementary set of booleans which show if a widget is valid / invalid.
- `$error`: an object which has a property for each validation key emited by the widget.
The value of the key is always true. If widget is valid, then the `$error`
object has no properties. For example if the widget emits
`$invalid` event with `REQUIRED` key. The internal state of the `$error` would be
updated to `$error.REQUIRED == true`.
- Responsible for aggregating widget validation information into the form.
- `$valid` / `$invalid`: Complementary set of booleans which show if all the child widgets
(or forms) are valid or if any are invalid.
- `$error`: an object which has a property for each validation key emited by the
child widget. The value of the key is an array of widgets which fired the invalid
event. If all child widgets are valid then, then the `$error` object has no
properties. For example if a child widget emits
`$invalid` event with `REQUIRED` key. The internal state of the `$error` would be
updated to `$error.REQUIRED == [ widgetWhichEmitedInvalid ]`.
# Widgets
In Angular, a widget is the term used for the UI with which the user input. Examples of
bult-in Angular widgets are {@link api/angular.widget.input input} and
{@link api/angular.widget.select select}. Widgets provide the rendering and the user
interaction logic. Widgets should be declared inside a form, if no form is provided an implicit
form {@link api/angular.service.$formFactory $formFactory.rootForm} form is used.
Widgets are implemented as Angular controllers. A widget controller:
- implements methods:
- `$render` - Updates the DOM from the internal state as represented by `$viewValue`.
- `$parseView` - Translate `$viewValue` to `$modelValue`. (`$modelValue` will be assigned to
the model scope by the form)
- `$parseModel` - Translate `$modelValue` to `$viewValue`. (`$viewValue` will be assigned to
the DOM inside the `$render` method)
- responds to events:
- `$validate` - Emitted by the form when the form determines that the widget needs to validate
itself. There may be more then one listener on the `$validate` event. The widget responds
by emitting `$valid` / `$invalid` event of its own.
- emits events:
- `$viewChange` - Emitted when the user interacts with the widget and it is necessary to update
the model.
- `$valid` - Emitted when the widget determines that it is valid (usually as a response to
`$validate` event or inside `$parseView()` or `$parseModel()` method).
- `$invalid` - Emitted when the widget determines that it is invalid (usually as a response to
`$validate` event or inside `$parseView()` or `$parseModel()` method).
- `$destroy` - Emitted when the widget element is removed from the DOM.
# CSS
Angular-defined widgets and forms set `ng-valid` and `ng-invalid` classes on themselves to allow
the web-designer a way to style them. If you write your own widgets, then their `$render()`
methods must set the appropriate CSS classes to allow styling.
(See {@link dev_guide.templates.css-styling CSS})
# Example
The following example demonstrates:
- How an error is displayed when a required field is empty.
- Error highlighting.
- How form submission is disabled when the form is invalid.
- The internal state of the widget and form in the the 'Debug View' area.
<doc:example>
<doc:source>
<style>
.ng-invalid { border: solid 1px red; }
.ng-form {display: block;}
</style>
<script>
function UserFormCntl(){
this.state = /^\w\w$/;
this.zip = /^\d\d\d\d\d$/;
this.master = {
customer: 'John Smith',
address:{
line1: '123 Main St.',
city:'Anytown',
state:'AA',
zip:'12345'
}
};
this.cancel();
}
UserFormCntl.prototype = {
cancel: function(){
this.form = angular.copy(this.master);
},
save: function(){
this.master = this.form;
this.cancel();
}
};
</script>
<div ng:controller="UserFormCntl">
<form name="userForm">
<label>Name:</label><br/>
<input type="text" name="customer" ng:model="form.customer" required/>
<span class="error" ng:show="userForm.customer.$error.REQUIRED">
Customer name is required!</span>
<br/><br/>
<ng:form name="addressForm">
<label>Address:</label> <br/>
<input type="text" name="line1" size="33" required
ng:model="form.address.line1"/> <br/>
<input type="text" name="city" size="12" required
ng:model="form.address.city"/>,
<input type="text" name="state" ng:pattern="state" size="2" required
ng:model="form.address.state"/>
<input type="text" name="zip" ng:pattern="zip" size="5" required
ng:model="form.address.zip"/><br/><br/>
<span class="error" ng:show="addressForm.$invalid">
Incomplete address:
<div class="error" ng:show="addressForm.state.$error.REQUIRED">
Missing state!</span>
<div class="error" ng:show="addressForm.state.$error.PATTERN">
Invalid state!</span>
<div class="error" ng:show="addressForm.zip.$error.REQUIRED">
Missing zip!</span>
<div class="error" ng:show="addressForm.zip.$error.PATTERN">
Invalid zip!</span>
</span>
</ng:form>
<button ng:click="cancel()"
ng:disabled="{{master.$equals(form)}}">Cancel</button>
<button ng:click="save()"
ng:disabled="{{userForm.$invalid || master.$equals(form)}}">
Save</button>
</form>
<hr/>
Debug View:
<pre>form={{form}}</pre>
<pre>master={{master}}</pre>
<pre>userForm={{userForm}}</pre>
<pre>addressForm={{addressForm}}</pre>
</div>
</doc:source>
<doc:scenario>
it('should enable save button', function(){
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
input('form.customer').enter('');
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
input('form.customer').enter('change');
expect(element(':button:contains(Save)').attr('disabled')).toBeFalsy();
element(':button:contains(Save)').click();
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
});
it('should enable cancel button', function(){
expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy();
input('form.customer').enter('change');
expect(element(':button:contains(Cancel)').attr('disabled')).toBeFalsy();
element(':button:contains(Cancel)').click();
expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy();
expect(element(':input[ng\\:model="form.customer"]').val()).toEqual('John Smith');
});
</doc:scenario>
</doc:example>
# Life-cycle
- The `<form>` element triggers creation of a new form {@link dev_guide.scopes scope} using the
{@link api/angular.service.$formFactory $formfactory}. The new form scope is added to the
`<form>` element using the jQuery `.data()` method for later retrieval under the key `$form`.
The form also sets up these listeners:
- `$destroy` - This event is emitted by nested widget when it is removed from the view. It gives
the form a chance to clean up any validation references to the destroyed widget.
- `$valid` / `$invalid` - This event is emitted by the widget on validation state change.
- `<input>` element triggers the creation of the widget using the
{@link api/angular.service.$formFactory $formfactory.$createWidget()} method. The `$createWidget()`
creates new widget instance by calling the current scope {@link api/angular.scope.$new .$new()} and
registers these listeners:
- `$watch` on the model scope.
- `$viewChange` event on the widget scope.
- `$validate` event on the widget scope.
- Element `change` event when the user enters data.
<img class="center" src="img/form_data_flow.png" border="1" />
- When the user interacts with the widget:
1. The DOM element fires the `change` event which the widget intercepts. Widget then emits
a `$viewChange` event which includes the new user-entered value. (Remember that the DOM events
are outside of the Angular environment so the widget must emit its event within the
{@link api/angular.scope.$apply $apply} method).
2. The form's `$viewChange` listener copies the user-entered value to the widget's `$viewValue`
property. Since the `$viewValue` is the raw value as entered by user, it may need to be
translated to a different format/type (for example, translating a string to a number).
If you need your widget to translate between the internal `$viewValue` and the external
`$modelValue` state, you must declare a `$parseView()` method. The `$parseView()` method
will copy `$viewValue` to `$modelValue` and perform any necessary translations.
3. The `$modelValue` is written into the application model.
4. The form then emits a `$validate` event, giving the widget's validators chance to validate the
input. There can be any number of validators registered. Each validator may in turn
emit a `$valid` / `$invalid` event with the validator's validation key. For example `REQUIRED`.
5. Form listens to `$valid`/`$invalid` events and updates both the form as well as the widget
scope with the validation state. The validation updates the `$valid` and `$invalid`, property
as well as `$error` object. The widget's `$error` object is updated with the validation key
such that `$error.REQUIRED == true` when the validation emits `$invalid` with `REQUIRED`
validation key. Similarly the form's `$error` object gets updated, but instead of boolean
`true` it contains an array of invalid widgets (widgets which fired `$invalid` event with
`REQUIRED` validation key).
- When the model is updated:
1. The model `$watch` listener assigns the model value to `$modelValue` on the widget.
2. The form then calls `$parseModel` method on widget if present. The method converts the
value to renderable format and assigns it to `$viewValue` (for example converting number to a
string.)
3. The form then emits a `$validate` which behaves as described above.
4. The form then calls `$render` method on the widget to update the DOM structure from the
`$viewValue`.
# Writing Your Own Widget
This example shows how to implement a custom HTML editor widget in Angular.
<doc:example>
<doc:source>
<script>
function EditorCntl(){
this.htmlContent = '<b>Hello</b> <i>World</i>!';
}
function HTMLEditorWidget(element) {
var self = this;
var htmlFilter = angular.filter('html');
this.$parseModel = function(){
// need to protect for script injection
try {
this.$viewValue = htmlFilter(
this.$modelValue || '').get();
if (this.$error.HTML) {
// we were invalid, but now we are OK.
this.$emit('$valid', 'HTML');
}
} catch (e) {
// if HTML not parsable invalidate form.
this.$emit('$invalid', 'HTML');
}
}
this.$render = function(){
element.html(this.$viewValue);
}
element.bind('keyup', function(){
self.$apply(function(){
self.$emit('$viewChange', element.html());
});
});
}
angular.directive('ng:html-editor-model', function(){
function linkFn($formFactory, element) {
var exp = element.attr('ng:html-editor-model'),
form = $formFactory.forElement(element),
widget;
element.attr('contentEditable', true);
widget = form.$createWidget({
scope: this,
model: exp,
controller: HTMLEditorWidget,
controllerArgs: [element]});
// if the element is destroyed, then we need to
// notify the form.
element.bind('$destroy', function(){
widget.$destroy();
});
}
linkFn.$inject = ['$formFactory'];
return linkFn;
});
</script>
<form name='editorForm' ng:controller="EditorCntl">
<div ng:html-editor-model="htmlContent"></div>
<hr/>
HTML: <br/>
<textarea ng:model="htmlContent" cols="80"></textarea>
<hr/>
<pre>editorForm = {{editorForm}}</pre>
</form>
</doc:source>
<doc:scenario>
it('should enter invalid HTML', function(){
expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
input('htmlContent').enter('<');
expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
});
</doc:scenario>
</doc:example>
# HTML Inputs
The most common widgets you will use will be in the form of the
standard HTML set. These widgets are bound using the `name` attribute
to an expression. In addition, they can have `required` attribute to further control their
validation.
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.input1 = '';
this.input2 = '';
this.input3 = 'A';
this.input4 = false;
this.input5 = 'c';
this.input6 = [];
}
</script>
<table style="font-size:.9em;" ng:controller="Ctrl">
<tr>
<th>Name</th>
<th>Format</th>
<th>HTML</th>
<th>UI</th>
<th ng:non-bindable>{{input#}}</th>
</tr>
<tr>
<th>text</th>
<td>String</td>
<td><tt>&lt;input type="text" ng:model="input1"&gt;</tt></td>
<td><input type="text" ng:model="input1" size="4"></td>
<td><tt>{{input1|json}}</tt></td>
</tr>
<tr>
<th>textarea</th>
<td>String</td>
<td><tt>&lt;textarea ng:model="input2"&gt;&lt;/textarea&gt;</tt></td>
<td><textarea ng:model="input2" cols='6'></textarea></td>
<td><tt>{{input2|json}}</tt></td>
</tr>
<tr>
<th>radio</th>
<td>String</td>
<td><tt>
&lt;input type="radio" ng:model="input3" value="A"&gt;<br>
&lt;input type="radio" ng:model="input3" value="B"&gt;
</tt></td>
<td>
<input type="radio" ng:model="input3" value="A">
<input type="radio" ng:model="input3" value="B">
</td>
<td><tt>{{input3|json}}</tt></td>
</tr>
<tr>
<th>checkbox</th>
<td>Boolean</td>
<td><tt>&lt;input type="checkbox" ng:model="input4"&gt;</tt></td>
<td><input type="checkbox" ng:model="input4"></td>
<td><tt>{{input4|json}}</tt></td>
</tr>
<tr>
<th>pulldown</th>
<td>String</td>
<td><tt>
&lt;select ng:model="input5"&gt;<br>
&nbsp;&nbsp;&lt;option value="c"&gt;C&lt;/option&gt;<br>
&nbsp;&nbsp;&lt;option value="d"&gt;D&lt;/option&gt;<br>
&lt;/select&gt;<br>
</tt></td>
<td>
<select ng:model="input5">
<option value="c">C</option>
<option value="d">D</option>
</select>
</td>
<td><tt>{{input5|json}}</tt></td>
</tr>
<tr>
<th>multiselect</th>
<td>Array</td>
<td><tt>
&lt;select ng:model="input6" multiple size="4"&gt;<br>
&nbsp;&nbsp;&lt;option value="e"&gt;E&lt;/option&gt;<br>
&nbsp;&nbsp;&lt;option value="f"&gt;F&lt;/option&gt;<br>
&lt;/select&gt;<br>
</tt></td>
<td>
<select ng:model="input6" multiple size="4">
<option value="e">E</option>
<option value="f">F</option>
</select>
</td>
<td><tt>{{input6|json}}</tt></td>
</tr>
</table>
</doc:source>
<doc:scenario>
it('should exercise text', function(){
input('input1').enter('Carlos');
expect(binding('input1')).toEqual('"Carlos"');
});
it('should exercise textarea', function(){
input('input2').enter('Carlos');
expect(binding('input2')).toEqual('"Carlos"');
});
it('should exercise radio', function(){
expect(binding('input3')).toEqual('"A"');
input('input3').select('B');
expect(binding('input3')).toEqual('"B"');
input('input3').select('A');
expect(binding('input3')).toEqual('"A"');
});
it('should exercise checkbox', function(){
expect(binding('input4')).toEqual('false');
input('input4').check();
expect(binding('input4')).toEqual('true');
});
it('should exercise pulldown', function(){
expect(binding('input5')).toEqual('"c"');
select('input5').option('d');
expect(binding('input5')).toEqual('"d"');
});
it('should exercise multiselect', function(){
expect(binding('input6')).toEqual('[]');
select('input6').options('e');
expect(binding('input6')).toEqual('["e"]');
select('input6').options('e', 'f');
expect(binding('input6')).toEqual('["e","f"]');
});
</doc:scenario>
</doc:example>
#Testing
When unit-testing a controller it may be desirable to have a reference to form and to simulate
different form validation states.
This example demonstrates a login form, where the login button is enabled only when the form is
properly filled out.
<pre>
<div ng:controller="LoginController">
<form name="loginForm">
<input type="text" ng:model="username" required/>
<input type="password" ng:model="password" required/>
<button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login>
</form>
</div>
</pre>
In the unit tests we do not have access to the DOM, and therefore the `loginForm` reference does
not get set on the controller. This example shows how it can be unit-tested, by creating a mock
form.
<pre>
function LoginController() {
this.disableLogin = function() {
return this.loginForm.$invalid;
};
}
describe('LoginController', function() {
it('should disable login button when form is invalid', function() {
var scope = angular.scope();
var loginController = scope.$new(LoginController);
// In production the 'loginForm' form instance gets set from the view,
// but in unit-test we have to set it manually.
loginController.loginForm = scope.$service('$formFactory')();
expect(loginController.disableLogin()).toBe(false);
// Now simulate an invalid form
loginController.loginForm.$emit('$invalid', 'MyReason');
expect(loginController.disableLogin()).toBe(true);
// Now simulate a valid form
loginController.loginForm.$emit('$valid', 'MyReason');
expect(loginController.disableLogin()).toBe(false);
});
});
</pre>
## Custom widgets
This example demonstrates a login form, where the password has custom validation rules.
<pre>
<div ng:controller="LoginController">
<form name="loginForm">
<input type="text" ng:model="username" required/>
<input type="@StrongPassword" ng:model="password" required/>
<button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login>
</form>
</div>
</pre>
In the unit tests we do not have access to the DOM, and therefore the `loginForm` and custom
input type reference does not get set on the controller. This example shows how it can be
unit-tested, by creating a mock form and a mock custom input type.
<pre>
function LoginController(){
this.disableLogin = function() {
return this.loginForm.$invalid;
};
this.StrongPassword = function(element) {
var widget = this;
element.attr('type', 'password'); // act as password.
this.$on('$validate', function(){
widget.$emit(widget.$viewValue.length > 5 ? '$valid' : '$invalid', 'PASSWORD');
});
};
}
describe('LoginController', function() {
it('should disable login button when form is invalid', function() {
var scope = angular.scope();
var loginController = scope.$new(LoginController);
var input = angular.element('<input>');
// In production the 'loginForm' form instance gets set from the view,
// but in unit-test we have to set it manually.
loginController.loginForm = scope.$service('$formFactory')();
// now instantiate a custom input type
loginController.loginForm.$createWidget({
scope: loginController,
model: 'password',
alias: 'password',
controller: loginController.StrongPassword,
controllerArgs: [input]
});
// Verify that the custom password input type sets the input type to password
expect(input.attr('type')).toEqual('password');
expect(loginController.disableLogin()).toBe(false);
// Now simulate an invalid form
loginController.loginForm.password.$emit('$invalid', 'PASSWORD');
expect(loginController.disableLogin()).toBe(true);
// Now simulate a valid form
loginController.loginForm.password.$emit('$valid', 'PASSWORD');
expect(loginController.disableLogin()).toBe(false);
// Changing model state, should also influence the form validity
loginController.password = 'abc'; // too short so it should be invalid
scope.$digest();
expect(loginController.loginForm.password.$invalid).toBe(true);
// Changeing model state, should also influence the form validity
loginController.password = 'abcdef'; // should be valid
scope.$digest();
expect(loginController.loginForm.password.$valid).toBe(true);
});
});
</pre>

View file

@ -68,7 +68,7 @@ Putting any presentation logic into controllers significantly affects testabilit
logic. Angular offers {@link dev_guide.templates.databinding} for automatic DOM manipulation. If
you have to perform your own manual DOM manipulation, encapsulate the presentation logic in {@link
dev_guide.compiler.widgets widgets} and {@link dev_guide.compiler.directives directives}.
- Input formatting — Use {@link dev_guide.templates.formatters angular formatters} instead.
- Input formatting — Use {@link dev_guide.forms angular form widgets} instead.
- Output filtering — Use {@link dev_guide.templates.filters angular filters} instead.
- Run stateless or stateful code shared across controllers — Use {@link dev_guide.services angular
services} instead.
@ -139,7 +139,7 @@ previous example.
<pre>
<body ng:controller="SpicyCtrl">
<input name="customSpice" value="wasabi">
<input ng:model="customSpice" value="wasabi">
<button ng:click="spicy('chili')">Chili</button>
<button ng:click="spicy(customSpice)">Custom spice</button>
<p>The food is {{spice}} spicy!</p>

View file

@ -41,7 +41,7 @@ when processing the following template constructs:
* Form input, select, textarea and other form elements:
<input name="query" value="fluffy cloud">
<input ng:model="query" value="fluffy cloud">
The code above creates a model called "query" on the current scope with the value set to "fluffy
cloud".

View file

@ -42,19 +42,27 @@ easier a web developer's life can if they're using angular:
<doc:example>
<doc:source>
<b>Invoice:</b>
<br />
<br />
<table>
<tr><td> </td><td> </td>
<tr><td>Quantity</td><td>Cost</td></tr>
<tr>
<td><input name="qty" value="1" ng:validate="integer:0" ng:required /></td>
<td><input name="cost" value="19.95" ng:validate="number" ng:required /></td>
</tr>
</table>
<hr />
<b>Total:</b> {{qty * cost | currency}}
<script>
function InvoiceCntl(){
this.qty = 1;
this.cost = 19.95;
}
</script>
<div ng:controller="InvoiceCntl">
<b>Invoice:</b>
<br />
<br />
<table>
<tr><td> </td><td> </td>
<tr><td>Quantity</td><td>Cost</td></tr>
<tr>
<td><input type="integer" min="0" ng:model="qty" required ></td>
<td><input type="number" ng:model="cost" required ></td>
</tr>
</table>
<hr />
<b>Total:</b> {{qty * cost | currency}}
</div>
</doc:source>
<!--
<doc:scenario>
@ -89,18 +97,18 @@ In the `<script>` tag we do two angular setup tasks:
From the `name` attribute of the `<input>` tags, angular automatically sets up two-way data
binding, and we also demonstrate some easy input validation:
Quantity: <input name="qty" value="1" ng:validate="integer:0" ng:required/>
Cost: <input name="cost" value="199.95" ng:validate="number" ng:required/>
Quantity: <input type="integer" min="0" ng:model="qty" required >
Cost: <input type="number" ng:model="cost" required >
These input widgets look normal enough, but consider these points:
* When this page loaded, angular bound the names of the input widgets (`qty` and `cost`) to
variables of the same name. Think of those variables as the "Model" component of the
Model-View-Controller design pattern.
* Note the angular directives, {@link api/angular.widget.@ng:validate ng:validate} and {@link
api/angular.widget.@ng:required ng:required}. You may have noticed that when you enter invalid data
* Note the angular/HTML widget, {@link api/angular.widget.input input}.
You may have noticed that when you enter invalid data
or leave the the input fields blank, the borders turn red color, and the display value disappears.
These `ng:` directives make it easier to implement field validators than coding them in JavaScript,
These widgets make it easier to implement field validation than coding them in JavaScript,
no? Yes.
And finally, the mysterious `{{ double curly braces }}`:

View file

@ -612,7 +612,7 @@ https://github.com/angular/angular.js/issues/404 issue}). If you should require
you will need to specify an extra property that has two watchers. For example:
<pre>
<!-- html -->
<input type="text" name="locationPath" />
<input type="text" ng:model="locationPath" />
</pre>
<pre>
// js - controller

View file

@ -54,13 +54,13 @@ myController.$inject = ['notify'];
<div ng:controller="myController">
<p>Let's try this simple notify service, injected into the controller...</p>
<input ng:init="message='test'" type="text" name="message" />
<input ng:init="message='test'" type="text" ng:model="message" />
<button ng:click="callNotify(message);">NOTIFY</button>
</div>
</doc:source>
<doc:scenario>
it('should test service', function(){
expect(element(':input[name=message]').val()).toEqual('test');
expect(element(':input[ng\\:model="message"]').val()).toEqual('test');
});
</doc:scenario>
</doc:example>

View file

@ -4,48 +4,32 @@
@description
Angular includes built-in CSS classes, which in turn have predefined CSS styles.
Angular sets these CSS classes. It is up to your application to provide useful styling.
# Built-in CSS classes
# CSS classes used by angular
* `ng-exception`
* `ng-invalid`, `ng-valid`
- **Usage:** angular applies this class to an input widget element if that element's input does
notpass validation. (see {@link api/angular.widget.input input} widget).
**Usage:** angular applies this class to a DOM element if that element contains an Expression that
threw an exception when evaluated.
* `ng-pristine`, `ng-dirty`
- **Usage:** angular {@link api/angular.widget.input input} widget applies `ng-pristine` class
to a new input widget element which did not have user interaction. Once the user interacts with
the input widget the class is changed to `ng-dirty`.
**Styling:** The built-in styling of the ng-exception class displays an error message surrounded
by a solid red border, for example:
# Marking CSS classes
<div class="ng-exception">Error message</div>
* `ng-widget`, `ng-directive`
- **Usage:** angular sets these class on elements where {@link api/angular.widget widget} or
{@link api/angular.directive directive} has bound to.
You can try to evaluate malformed expressions in {@link dev_guide.expressions expressions} to see
the `ng-exception` class' styling.
* `ng-validation-error`
**Usage:** angular applies this class to an input widget element if that element's input does not
pass validation. Note that you set the validation criteria on the input widget element using the
Ng:validate or Ng:required directives.
**Styling:** The built-in styling of the ng-validation-error class turns the border of the input
box red and includes a hovering UI element that includes more details of the validation error. You
can see an example in {@link api/angular.widget.@ng:validate ng:validate example}.
## Overriding Styles for Angular CSS Classes
To override the styles for angular's built-in CSS classes, you can do any of the following:
* Download the source code, edit angular.css, and host the source on your own server.
* Create a local CSS file, overriding any styles that you'd like, and link to it from your HTML file
as you normally would:
<pre>
<link href="yourfile.css" rel="stylesheet" type="text/css">
</pre>
* Old browser support
- Pre v9, IE browsers could not select `ng:include` elements in CSS, because of the `:`
character. For this reason angular also sets `ng-include` class on any element which has `:`
character in the name by replacing `:` with `-`.
## Related Topics
* {@link dev_guide.templates Angular Templates}
* {@link dev_guide.templates.formatters Angular Formatters}
* {@link dev_guide.templates.formatters.creating_formatters Creating Angular Formatters}
* {@link dev_guide.forms Angular Forms}

View file

@ -35,20 +35,26 @@ text upper-case and assigns color.
}
return out;
});
function Ctrl(){
this.greeting = 'hello';
}
</script>
<input name="text" type="text" value="hello" /><br>
No filter: {{text}}<br>
Reverse: {{text|reverse}}<br>
Reverse + uppercase: {{text|reverse:true}}<br>
Reverse + uppercase + blue: {{text|reverse:true:"blue"}}
<div ng:controller="Ctrl">
<input ng:model="greeting" type="greeting"><br>
No filter: {{greeting}}<br>
Reverse: {{greeting|reverse}}<br>
Reverse + uppercase: {{greeting|reverse:true}}<br>
Reverse + uppercase + blue: {{greeting|reverse:true:"blue"}}
</div>
</doc:source>
<doc:scenario>
it('should reverse text', function(){
expect(binding('text|reverse')).toEqual('olleh');
input('text').enter('ABC');
expect(binding('text|reverse')).toEqual('CBA');
});
it('should reverse greeting', function(){
expect(binding('greeting|reverse')).toEqual('olleh');
input('greeting').enter('ABC');
expect(binding('greeting|reverse')).toEqual('CBA');
});
</doc:scenario>
</doc:example>

View file

@ -1,55 +0,0 @@
@workInProgress
@ngdoc overview
@name Developer Guide: Templates: Angular Formatters: Creating Angular Formatters
@description
To create your own formatter, you can simply register a pair of JavaScript functions with
`angular.formatter`. One of your functions is used to parse text from the input widget into the
data storage format; the other function is used to format stored data into user-readable text.
The following example demonstrates a "reverse" formatter. Data is stored in uppercase and in
reverse, but it is displayed in lower case and non-reversed. When a user edits the data model via
the input widget, the input is automatically parsed into the internal data storage format, and when
the data changes in the model, it is automatically formatted to the user-readable form for display
in the view.
<pre>
function reverse(text) {
var reversed = [];
for (var i = 0; i < text.length; i++) {
reversed.unshift(text.charAt(i));
}
return reversed.join('');
}
angular.formatter('reverse', {
parse: function(value){
return reverse(value||'').toUpperCase();
},
format: function(value){
return reverse(value||'').toLowerCase();
}
});
</pre>
<doc:example>
<doc:source>
<script type="text/javascript">
function reverse(text) {
var reversed = [];
for (var i = 0; i < text.length; i++) {
reversed.unshift(text.charAt(i));
}
return reversed.join('');
}
angular.formatter('reverse', {
parse: function(value){
return reverse(value||'').toUpperCase();
},
format: function(value){
return reverse(value||'').toLowerCase();
}
});
</script>

View file

@ -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}

View file

@ -1,9 +0,0 @@
@workInProgress
@ngdoc overview
@name Developer Guide: Templates: Angular Formatters: Using Angular Formatters
@description
The following snippet shows how to use a formatter in a template. The formatter below is
`ng:format="reverse"`, added as an attribute to an `<input>` tag.
<pre>

View file

@ -18,9 +18,7 @@ is {@link api/angular.widget.@ng:repeat ng:repeat}.
* {@link dev_guide.compiler.markup Markup} — Shorthand for a widget or a directive. The double
curly brace notation `{{ }}` to bind expressions to elements is built-in angular markup.
* {@link dev_guide.templates.filters Filter} — Formats your data for display to the user.
* {@link dev_guide.templates.validators Validator} — Lets you validate user input.
* {@link dev_guide.templates.formatters Formatter} — Lets you format the input object into a user
readable view.
* {@link dev_guide.forms Form widgets} — Lets you validate user input.
Note: In addition to declaring the elements above in templates, you can also access these elements
in JavaScript code.
@ -33,7 +31,7 @@ and {@link dev_guide.expressions expressions}:
<html>
<!-- Body tag augmented with ng:controller directive -->
<body ng:controller="MyController">
<input name="foo" value="bar">
<input ng:model="foo" value="bar">
<!-- Button tag with ng:click directive, and
string expression 'buttonText'
wrapped in "{{ }}" markup -->
@ -55,8 +53,7 @@ eight.
## Related Topics
* {@link dev_guide.templates.filters Angular Filters}
* {@link dev_guide.templates.formatters Angular Formatters}
* {@link dev_guide.templates.validators Angular Validators}
* {@link dev_guide.forms Angular Forms}
## Related API

View file

@ -1,82 +0,0 @@
@workInProgress
@ngdoc overview
@name Developer Guide: Validators: Creating Angular Validators
@description
To create a custom validator, you simply add your validator code as a method onto the
`angular.validator` object and provide input(s) for the validator function. Each input provided is
treated as an argument to the validator function. Any additional inputs should be separated by
commas.
The following bit of pseudo-code shows how to set up a custom validator:
<pre>
angular.validator('your_validator', function(input [,additional params]) {
[your validation code];
if ( [validation succeeds] ) {
return false;
} else {
return true; // No error message specified
}
}
</pre>
Note that this validator returns "true" when the user's input is incorrect, as in "Yes, it's true,
there was a problem with that input". If you prefer to provide more information when a validator
detects a problem with input, you can specify an error message in the validator that angular will
display when the user hovers over the input widget.
To specify an error message, replace "`return true;`" with an error string, for example:
return "Must be a value between 1 and 5!";
Following is a sample UPS Tracking Number validator:
<doc:example>
<doc:source>
<script>
angular.validator('upsTrackingNo', function(input, format) {
var regexp = new RegExp("^" + format.replace(/9/g, '\\d') + "$");
return input.match(regexp)?"":"The format must match " + format;
});
</script>
<input type="text" name="trackNo" size="40"
ng:validate="upsTrackingNo:'1Z 999 999 99 9999 999 9'"
value="1Z 123 456 78 9012 345 6"/>
</doc:source>
<doc:scenario>
it('should validate correct UPS tracking number', function() {
expect(element('input[name=trackNo]').attr('class')).
not().toMatch(/ng-validation-error/);
});
it('should not validate in correct UPS tracking number', function() {
input('trackNo').enter('foo');
expect(element('input[name=trackNo]').attr('class')).
toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
In this sample validator, we specify a regular expression against which to test the user's input.
Note that when the user's input matches `regexp`, the function returns "false" (""); otherwise it
returns the specified error message ("true").
Note: you can also access the current angular scope and DOM element objects in your validator
functions as follows:
* `this` === The current angular scope.
* `this.$element` === The DOM element that contains the binding. This allows the filter to
manipulate the DOM in addition to transforming the input.
## Related Topics
* {@link dev_guide.templates Angular Templates}
* {@link dev_guide.templates.filters Angular Filters}
* {@link dev_guide.templates.formatters Angular Formatters}
## Related API
* {@link api/angular.validator API Validator Reference}

View file

@ -1,131 +0,0 @@
@workInProgress
@ngdoc overview
@name Developer Guide: Templates: Understanding Angular Validators
@description
Angular validators are attributes that test the validity of different types of user input. Angular
provides a set of built-in input validators:
* {@link api/angular.validator.phone phone number}
* {@link api/angular.validator.number number}
* {@link api/angular.validator.integer integer}
* {@link api/angular.validator.date date}
* {@link api/angular.validator.email email address}
* {@link api/angular.validator.json JSON}
* {@link api/angular.validator.regexp regular expressions}
* {@link api/angular.validator.url URLs}
* {@link api/angular.validator.asynchronous asynchronous}
You can also create your own custom validators.
# Using Angular Validators
You can use angular validators in HTML template bindings, and in JavaScript:
* Validators in HTML Template Bindings
<pre>
<input ng:validator="validator_type:parameters" [...]>
</pre>
* Validators in JavaScript
<pre>
angular.validator.[validator_type](parameters)
</pre>
The following example shows how to use the built-in angular integer validator:
<doc:example>
<doc:source>
Change me: <input type="text" name="number" ng:validate="integer" value="123">
</doc:source>
<doc:scenario>
it('should validate the default number string', function() {
expect(element('input[name=number]').attr('class')).
not().toMatch(/ng-validation-error/);
});
it('should not validate "foo"', function() {
input('number').enter('foo');
expect(element('input[name=number]').attr('class')).
toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
# Creating an Angular Validator
To create a custom validator, you simply add your validator code as a method onto the
`angular.validator` object and provide input(s) for the validator function. Each input provided is
treated as an argument to the validator function. Any additional inputs should be separated by
commas.
The following bit of pseudo-code shows how to set up a custom validator:
<pre>
angular.validator('your_validator', function(input [,additional params]) {
[your validation code];
if ( [validation succeeds] ) {
return false;
} else {
return true; // No error message specified
}
}
</pre>
Note that this validator returns "true" when the user's input is incorrect, as in "Yes, it's true,
there was a problem with that input". If you prefer to provide more information when a validator
detects a problem with input, you can specify an error message in the validator that angular will
display when the user hovers over the input widget.
To specify an error message, replace "`return true;`" with an error string, for example:
return "Must be a value between 1 and 5!";
Following is a sample UPS Tracking Number validator:
<doc:example>
<doc:source>
<script>
angular.validator('upsTrackingNo', function(input, format) {
var regexp = new RegExp("^" + format.replace(/9/g, '\\d') + "$");
return input.match(regexp)?"":"The format must match " + format;
});
</script>
<input type="text" name="trackNo" size="40"
ng:validate="upsTrackingNo:'1Z 999 999 99 9999 999 9'"
value="1Z 123 456 78 9012 345 6"/>
</doc:source>
<doc:scenario>
it('should validate correct UPS tracking number', function() {
expect(element('input[name=trackNo]').attr('class')).
not().toMatch(/ng-validation-error/);
});
it('should not validate in correct UPS tracking number', function() {
input('trackNo').enter('foo');
expect(element('input[name=trackNo]').attr('class')).
toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
In this sample validator, we specify a regular expression against which to test the user's input.
Note that when the user's input matches `regexp`, the function returns "false" (""); otherwise it
returns the specified error message ("true").
Note: you can also access the current angular scope and DOM element objects in your validator
functions as follows:
* `this` === The current angular scope.
* `this.$element` === The DOM element that contains the binding. This allows the filter to
manipulate the DOM in addition to transforming the input.
## Related Topics
* {@link dev_guide.templates Angular Templates}
## Related API
* {@link api/angular.validator Validator API}

View file

@ -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}

View file

@ -67,7 +67,7 @@ This example demonstrates angular's two-way data binding:
<doc:example>
<doc:source>
Your name: <input type="text" name="yourname" value="World"/>
Your name: <input type="text" ng:model="yourname" value="World"/>
<hr/>
Hello {{yourname}}!
</doc:source>

View file

@ -32,7 +32,7 @@ We made no changes to the controller.
__`app/index.html`:__
<pre>
...
Fulltext Search: <input name="query"/>
Fulltext Search: <input ng:model="query"/>
<ul class="phones">
<li ng:repeat="phone in phones.$filter(query)">

View file

@ -27,11 +27,11 @@ __`app/index.html`:__
...
<ul class="controls">
<li>
Search: <input type="text" name="query"/>
Search: <input type="text" ng:model="query"/>
</li>
<li>
Sort by:
<select name="orderProp">
<select ng:model="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>

View file

@ -122,11 +122,11 @@ __`app/partials/phone-list.html`:__
<pre>
<ul class="predicates">
<li>
Search: <input type="text" name="query"/>
Search: <input type="text" ng:model="query"/>
</li>
<li>
Sort by:
<select name="orderProp">
<select ng:model="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>

View file

@ -109,7 +109,7 @@ following bindings to `index.html`:
* We can also create a model with an input element, and combine it with a filtered binding. Add
the following to index.html:
<input name="userInput"> Uppercased: {{ userInput | uppercase }}
<input ng:model="userInput"> Uppercased: {{ userInput | uppercase }}
# Summary

View file

@ -1,13 +1,13 @@
<label>Name:</label>
<input type="text" name="form.name" ng:required>
<input type="text" ng:model="form.name" required>
<div ng:repeat="contact in form.contacts">
<select name="contact.type">
<select ng:model="contact.type">
<option>url</option>
<option>email</option>
<option>phone</option>
</select>
<input type="text" name="contact.url">
<input type="text" ng:model="contact.url">
[ <a href="" ng:click="form.contacts.$remove(contact)">X</a> ]
</div>
<div>
@ -15,4 +15,4 @@
</div>
<button ng:click="cancel()">Cancel</button>
<button ng:click="save()">Save</button>
<button ng:click="save()">Save</button>

BIN
docs/img/form_data_flow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -194,12 +194,12 @@ describe('ngdoc', function(){
it('should ignore nested doc widgets', function() {
expect(new Doc().markdown(
'before<doc:tutorial-instructions>\n' +
'<doc:tutorial-instruction id="git-mac" name="Git on Mac/Linux">' +
'<doc:tutorial-instruction id="git-mac" ng:model="Git on Mac/Linux">' +
'\ngit bla bla\n</doc:tutorial-instruction>\n' +
'</doc:tutorial-instructions>')).toEqual(
'<p>before</p><doc:tutorial-instructions>\n' +
'<doc:tutorial-instruction id="git-mac" name="Git on Mac/Linux">\n' +
'<doc:tutorial-instruction id="git-mac" ng:model="Git on Mac/Linux">\n' +
'git bla bla\n' +
'</doc:tutorial-instruction>\n' +
'</doc:tutorial-instructions>');
@ -543,38 +543,6 @@ describe('ngdoc', function(){
});
});
describe('validator', function(){
it('should format', function(){
var doc = new Doc({
ngdoc:'validator',
shortName:'myValidator',
param: [
{name:'a'},
{name:'b'}
]
});
doc.html_usage_validator(dom);
expect(dom).toContain('ng:validate="myValidator:b"');
expect(dom).toContain('angular.validator.myValidator(a, b)');
});
});
describe('formatter', function(){
it('should format', function(){
var doc = new Doc({
ngdoc:'formatter',
shortName:'myFormatter',
param: [
{name:'a'},
]
});
doc.html_usage_formatter(dom);
expect(dom).toContain('ng:format="myFormatter:a"');
expect(dom).toContain('var userInputString = angular.formatter.myFormatter.format(modelValue, a);');
expect(dom).toContain('var modelValue = angular.formatter.myFormatter.parse(userInputString, a);');
});
});
describe('property', function(){
it('should format', function(){
var doc = new Doc({

View file

@ -13,6 +13,11 @@ exports.scenarios = scenarios;
exports.merge = merge;
exports.Doc = Doc;
var BOOLEAN_ATTR = {};
['multiple', 'selected', 'checked', 'disabled', 'readOnly', 'required'].forEach(function(value, key) {
BOOLEAN_ATTR[value] = true;
});
//////////////////////////////////////////////////////////
function Doc(text, file, line) {
if (typeof text == 'object') {
@ -385,69 +390,21 @@ Doc.prototype = {
});
},
html_usage_formatter: function(dom){
html_usage_inputType: function(dom){
var self = this;
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.code(function(){
if (self.inputType=='select')
dom.text('<select name="bindExpression"');
else
dom.text('<input type="text" name="bindExpression"');
dom.text(' ng:format="');
dom.text(self.shortName);
self.parameters(dom, ':', false, true);
dom.text('">');
dom.code(function(){
dom.text('<input type="' + self.shortName + '"');
(self.param||[]).forEach(function(param){
dom.text('\n ');
dom.text(param.optional ? ' [' : ' ');
dom.text(param.name);
dom.text(BOOLEAN_ATTR[param.name] ? '' : '="..."');
dom.text(param.optional ? ']' : '');
});
dom.text('>');
});
dom.h('In JavaScript', function(){
dom.code(function(){
dom.text('var userInputString = angular.formatter.');
dom.text(self.shortName);
dom.text('.format(modelValue');
self.parameters(dom, ', ', false, true);
dom.text(');');
dom.text('\n');
dom.text('var modelValue = angular.formatter.');
dom.text(self.shortName);
dom.text('.parse(userInputString');
self.parameters(dom, ', ', false, true);
dom.text(');');
});
});
self.html_usage_parameters(dom);
self.html_usage_this(dom);
self.html_usage_returns(dom);
});
},
html_usage_validator: function(dom){
var self = this;
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.code(function(){
dom.text('<input type="text" ng:validate="');
dom.text(self.shortName);
self.parameters(dom, ':', true);
dom.text('"/>');
});
});
dom.h('In JavaScript', function(){
dom.code(function(){
dom.text('angular.validator.');
dom.text(self.shortName);
dom.text('(');
self.parameters(dom, ', ');
dom.text(')');
});
});
self.html_usage_parameters(dom);
self.html_usage_this(dom);
self.html_usage_returns(dom);
});
},
@ -473,11 +430,11 @@ Doc.prototype = {
dom.text('<');
dom.text(self.shortName);
(self.param||[]).forEach(function(param){
if (param.optional) {
dom.text(' [' + param.name + '="..."]');
} else {
dom.text(' ' + param.name + '="..."');
}
dom.text('\n ');
dom.text(param.optional ? ' [' : ' ');
dom.text(param.name);
dom.text(BOOLEAN_ATTR[param.name] ? '' : '="..."');
dom.text(param.optional ? ']' : '');
});
dom.text('></');
dom.text(self.shortName);
@ -533,12 +490,18 @@ Doc.prototype = {
dom.h('Events', this.events, function(event){
dom.h(event.shortName, event, function(){
dom.html(event.description);
dom.tag('div', {class:'inline'}, function(){
dom.h('Type:', event.type);
});
dom.tag('div', {class:'inline'}, function(){
dom.h('Target:', event.target);
});
if (event.type == 'listen') {
dom.tag('div', {class:'inline'}, function(){
dom.h('Listen on:', event.target);
});
} else {
dom.tag('div', {class:'inline'}, function(){
dom.h('Type:', event.type);
});
dom.tag('div', {class:'inline'}, function(){
dom.h('Target:', event.target);
});
}
event.html_usage_parameters(dom);
self.html_usage_this(dom);
@ -632,10 +595,9 @@ var KEYWORD_PRIORITY = {
'.angular.Object': 7,
'.angular.directive': 7,
'.angular.filter': 7,
'.angular.formatter': 7,
'.angular.scope': 7,
'.angular.service': 7,
'.angular.validator': 7,
'.angular.inputType': 7,
'.angular.widget': 7,
'.angular.mock': 8,
'.dev_guide.overview': 1,

View file

@ -81,14 +81,16 @@
fiddleSrc = fiddleSrc.replace(new RegExp('^\\s{' + stripIndent + '}', 'gm'), '');
return '<form class="jsfiddle" method="post" action="' + fiddleUrl + '" target="_blank">' +
'<textarea name="css">' +
'<textarea ng:model="css">' +
'.ng-invalid { border: 1px solid red; } \n' +
'body { font-family: Arial,Helvetica,sans-serif; }\n' +
'body, td, th { font-size: 14px; margin: 0; }\n' +
'table { border-collapse: separate; border-spacing: 2px; display: table; margin-bottom: 0; margin-top: 0; -moz-box-sizing: border-box; text-indent: 0; }\n' +
'a:link, a:visited, a:hover { color: #5D6DB6; text-decoration: none; }\n' +
'.error { color: red; }\n' +
'</textarea>' +
'<input type="text" name="title" value="AngularJS Live Example">' +
'<textarea name="html">' +
'<input type="text" ng:model="title" value="AngularJS Live Example">' +
'<textarea ng:model="html">' +
'<script src="' + angularJsUrl + '" ng:autobind></script>\n\n' +
'<!-- AngularJS Example Code: -->\n\n' +
fiddleSrc +

View file

@ -49,6 +49,10 @@ li {
margin: 0.3em 0 0.3em 0;
}
.ng-invalid {
border: 1px solid red;
}
/*----- Upgrade IE Prompt -----*/
@ -426,7 +430,7 @@ li {
}
table {
border-collapse: collapse;
border-collapse: collapse;
}
td {
@ -448,7 +452,7 @@ td.empty-corner-lt {
.html5-hashbang-example {
height: 255px;
margin-left: -40px;
padding-left: 30px;
padding-left: 30px;
}
.html5-hashbang-example div {
@ -459,3 +463,7 @@ td.empty-corner-lt {
.html5-hashbang-example div input {
width: 360px;
}
.error {
color: red;
}

View file

@ -99,7 +99,7 @@
</ul>
<div id="sidebar">
<input type="text" name="search" id="search-box" placeholder="search the docs"
<input type="text" ng:model="search" id="search-box" placeholder="search the docs"
tabindex="1" accesskey="s">
<ul id="content-list" ng:class="sectionId" ng:cloak>

View file

@ -12,11 +12,11 @@
<span>&lt;angular/&gt; Buzz</span>
<span>
filter:
<input type="text" name="filterText"/>
<input type="text" ng:model="filterText"/>
</span>
<span>
user:
<input type="text" name="userId" ng:required/>
<input type="text" ng:model="userId" required/>
<button ng:click="$location.hashPath = userId">fetch</button>
</span>
</div>

View file

@ -12,7 +12,7 @@
<body ng:controller="example.personalLog.LogCtrl">
<form action="" ng:submit="addLog(newMsg)">
<input type="text" name="newMsg" />
<input type="text" ng:model="newMsg" />
<input type="submit" value="add" />
<input type="button" value="remove all" ng:click="rmLogs()" />
</form>

View file

@ -11,7 +11,7 @@
<body ng:class="status" ng:init="mute={}" ng:watch="$anchor.user: tweets = fetchTweets($anchor.user)">
<div class="addressbook box">
<h1>Address Book</h1>
[ Filter: <input type="text" name="userFilter"/>]
[ Filter: <input type="text" ng:model="userFilter"/>]
<ul>
<li ng:repeat="user in users.$filter(userFilter).$orderBy('screen_name')" ng:class-even="'even'" ng:class-odd="'odd'">
<a href="" ng:click="$anchor.user=user.screen_name"><img src="{{user.profile_image_url}}"/></a>
@ -29,13 +29,13 @@
<div ng:show="$anchor.edituser" ng:eval="user = users.$find({:$.screen_name == $anchor.edituser})">
<div class="editor">
<label>Username:</label>
<input type="text" name="user.screen_name" disabled="disabled"/>
<input type="text" ng:model="user.screen_name" disabled="disabled"/>
<label>Name:</label>
<input type="text" name="user.name"/>
<input type="text" ng:model="user.name"/>
<label>Image:</label>
<input type="text" name="user.profile_image_url"/>
<input type="text" ng:model="user.profile_image_url"/>
<label>Notes:</label>
<textarea type="text" name="user.notes"></textarea>
<textarea type="text" ng:model="user.notes"></textarea>
<input type="button" ng:click="$anchor.edituser=undefined" value="Close"/>
</div>
@ -57,7 +57,7 @@ tweets={{tweets}}
</div>
<div class="tweeter box">
<h1>Tweets: {{$anchor.user}}</h1>
[ Filter: <input type="text" name="tweetFilter"/>
[ Filter: <input type="text" ng:model="tweetFilter"/>
<span ng:show="$anchor.user">| <a href="#user=">&lt;&lt; All</a></span>
]
<div class="loading">Loading...</div>

View file

@ -12,7 +12,7 @@
(TODO: I should fetch current tweets)
<div class="tweeter box">
<h1>Tweets: {{$anchor.user}}</h1>
[ Filter: <input type="text" name="tweetFilter"/> (TODO: this should act as search box)
[ Filter: <input type="text" ng:model="tweetFilter"/> (TODO: this should act as search box)
<span ng:show="$anchor.user">| <a href="#user=">&lt;&lt; All</a></span>
]
<div class="loading">Loading...</div>

View file

@ -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

View file

@ -5,9 +5,14 @@
<title>locale test</title>
<script src="../../build/angular.js" ng:autobind></script>
<script src="../../build/i18n/angular-locale_cs.js"></script>
<script>
function AppCntl(){
this.input = 234234443432;
}
</script>
</head>
<body>
<input type="text" name="input" value="234234443432"><br>
<body ng:controller="AppCntl">
<input type="text" ng:model="input"><br>
date: {{input | date:"medium"}}<br>
date: {{input | date:"longDate"}}<br>
number: {{input | number}}<br>

View file

@ -5,9 +5,14 @@
<title>locale test</title>
<script src="../../build/angular.js" ng:autobind></script>
<script src="../../build/i18n/angular-locale_de.js"></script>
<script>
function AppCntl(){
this.input = 234234443432;
}
</script>
</head>
<body>
<input type="text" name="input" value="234234443432"><br>
<body ng:controller="AppCntl">
<input type="text" ng:model="input"><br>
date: {{input | date:"medium"}}<br>
date: {{input | date:"longDate"}}<br>
number: {{input | number}}<br>

View file

@ -7,17 +7,26 @@
<!-- not needed, already bundled in angular.js
<script src="../../build/i18n/angular-locale_en.js"></script>
-->
<script>
function AppCntl(){
this.input = 234234443432;
this.plInput = 1;
this.person1 = "Shanjian";
this.person2 = "Di";
this.plInput2 = 1;
}
</script>
</head>
<body>
<body ng:controller="AppCntl">
<h3>Datetime/Number/Currency filters demo:</h3>
<input type="text" name="input" value="234234443432"><br>
<input type="text" ng:model="input" value="234234443432"><br>
date(medium): {{input | date:"medium"}}<br>
date(longDate): {{input | date:"longDate"}}<br>
number: {{input | number}}<br>
currency: {{input | currency }}
<hr/>
<h3>Pluralization demo:</h3>
<input type="text" name="plInput" value="1"><br>
<input type="text" ng:model="plInput"><br>
<ng:pluralize count="plInput"
when= "{ '0': 'You have no email!',
'one': 'You have one email!',
@ -25,9 +34,9 @@
</ng:pluralize>
<hr/>
<h3>Pluralization demo with offsets:</h3>
Name of person1:<input type="text" name="person1" value="Shanjian"/><br/>
Name of person2:<input type="text" name="person2" value="Di"/><br/>
<input type="text" name="plInput2" value="1"><br>
Name of person1:<input type="text" ng:model="person1"/><br/>
Name of person2:<input type="text" ng:model="person2"/><br/>
<input type="text" ng:model="plInput2"><br>
<ng:pluralize count="plInput2" offset=2
when= "{'0':'Nobody is viewing!',
'1': '{{person1}} is viewing!',

View file

@ -5,9 +5,14 @@
<title>locale test</title>
<script src="../../build/angular.js" ng:autobind></script>
<script src="../../build/i18n/angular-locale_es.js"></script>
<script>
function AppCntl(){
this.input = 234234443432;
}
</script>
</head>
<body>
<input type="text" name="input" value="234234443432"><br>
<body ng:controller="AppCntl">
<input type="text" ng:model="input" value="234234443432"><br>
date: {{input | date:"medium"}}<br>
date: {{input | date:"longDate"}}<br>
number: {{input | number}}<br>

View file

@ -5,15 +5,21 @@
<title>locale test</title>
<script src="../../build/angular.js" ng:autobind></script>
<script src="../../build/i18n/angular-locale_sk-sk.js"></script>
<script>
function AppCntl(){
this.input = 234234443432;
this.plInput = 1;
}
</script>
</head>
<body>
<input type="text" name="input" value="234234443432"><br>
<body ng:controller="AppCntl">
<input type="text" ng:model="input" value="234234443432"><br>
date: {{input | date:"medium"}}<br>
date: {{input | date:"longDate"}}<br>
number: {{input | number}}<br>
currency: {{input | currency }}
<hr/>
<input type="text" name="plInput" value="1"><br>
<input type="text" ng:model="plInput"><br>
<ng:pluralize count="plInput"
when= "{ 'one': 'Mas jeden email!',
'few': 'Mas {} emaily!',

View file

@ -5,25 +5,34 @@
<title>locale test</title>
<script src="../../build/angular.js" ng:autobind></script>
<script src="../../build/i18n/angular-locale_zh-cn.js"></script>
<script>
function AppCntl(){
this.input = 234234443432;
this.plInput = 1;
this.person1 = "Shanjian";
this.person2 = "Di";
this.plInput2 = 1;
}
</script>
</head>
<body>
<body ng:controller="AppCntl">
<h3>Datetime/Number/Currency filters demo:</h3>
<input type="text" name="input" value="234234443432"><br>
<input type="text" ng:model="input"><br>
date(medium): {{input | date:"medium"}}<br>
date(longDate): {{input | date:"longDate"}}<br>
number: {{input | number}}<br>
currency: {{input | currency }}
<hr/>
<h3>Pluralization demo:</h3>
<input type="text" name="plInput" value="1"><br>
<input type="text" ng:model="plInput"><br>
<ng:pluralize count="plInput"
when= "{'other':'{}人在浏览该文件!'}">
</ng:pluralize>
<hr/>
<h3>Pluralization demo with offsets:</h3>
Name of person1:<input type="text" name="person1" value="Shanjian"/><br/>
Name of person2:<input type="text" name="person2" value="Di"/><br/>
<input type="text" name="plInput2" value="1"><br>
Name of person1:<input type="text" ng:model="person1"/><br/>
Name of person2:<input type="text" ng:model="person2"/><br/>
<input type="text" ng:model="plInput2"><br>
<ng:pluralize count="plInput2" offset=2
when= "{'0':'没有人在浏览该文件!',
'1': '{{person1}} 在浏览该文件!',

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@
</ol>
<p>Why doesn't the data goes back to the original?</p>
<hr>
Input: <input type="text" name="filterName" id="filterInputField"/>
Input: <input type="text" ng:model="filterName" id="filterInputField"/>
<br/>
<table ng:eval="filtered_data = data.$filter(filterName)" style="border: 1px solid black">
<tr>

View file

@ -3,8 +3,8 @@
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
<body>
<span ng:init='x = {d:3}; x1 = {bar:[x,5]}; x1.bar[0].d = 4'>
<input name="x1.bar[0].d" type="text"></input>
<input name="x.d" type="text"></input>
<input ng:model="x1.bar[0].d" type="text"></input>
<input ng:model="x.d" type="text"></input>
<span> {{x1}} -- {{x1.bar[0].d}}</span>
</body>
</html>

View file

@ -2,12 +2,12 @@
<html xmlns:ng="http://angularjs.org">
<script type="text/javascript" src="../build/angular.js" ng:autobind></script>
<body ng:init="scope = { itemId: 12345 }">
<input name="value" /><br />
<input ng:model="value" /><br />
<a id="link-1" href ng:click="value = 1">link 1</a> (link, don't reload)<br />
<a id="link-2" href="" ng:click="value = 2">link 2</a> (link, don't reload)<br />
<a id="link-3" ng:href="#{{'123'}}" ng:click="value = 3">link 3</a> (link, reload!)<br />
<a id="link-4" href="" name="xx" ng:click="value = 4">anchor</a> (link, don't reload)<br />
<a id="link-5" name="xxx" ng:click="value = 5">anchor</a> (no link)<br />
<a id="link-4" href="" ng:model="xx" ng:click="value = 4">anchor</a> (link, don't reload)<br />
<a id="link-5" ng:model="xxx" ng:click="value = 5">anchor</a> (no link)<br />
<a id="link-6" ng:href="#/{{value}}">link</a> (link, change hash)
</body>
</html>

View file

@ -11,7 +11,7 @@
}
Cntl.$inject = ['$route'];
</script>
<body ng:controller="Cntl">
<body ng:controller="Ctrl">
<a href="#/item1">test</a>
<a href="#/item2">test</a>
</body>

View file

@ -2,7 +2,7 @@
<html xmlns:ng="http://angularjs.org">
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
<body>
<textarea name="html" rows="10" cols="100"></textarea>
<textarea ng:model="html" rows="10" cols="100"></textarea>
<div>{{html|html}}</div>
</body>
</html>

View file

@ -55,7 +55,6 @@ function fromCharCode(code) { return String.fromCharCode(code); }
var _undefined = undefined,
_null = null,
$$scope = '$scope',
$$validate = '$validate',
$angular = 'angular',
$array = 'array',
$boolean = 'boolean',
@ -93,12 +92,10 @@ var _undefined = undefined,
angularDirective = extensionMap(angular, 'directive'),
/** @name angular.widget */
angularWidget = extensionMap(angular, 'widget', lowercase),
/** @name angular.validator */
angularValidator = extensionMap(angular, 'validator'),
/** @name angular.fileter */
/** @name angular.filter */
angularFilter = extensionMap(angular, 'filter'),
/** @name angular.formatter */
angularFormatter = extensionMap(angular, 'formatter'),
/** @name angular.service */
angularInputType = extensionMap(angular, 'inputType', lowercase),
/** @name angular.service */
angularService = extensionMap(angular, 'service'),
angularCallbacks = extensionMap(angular, 'callbacks'),
@ -156,10 +153,18 @@ function forEach(obj, iterator, context) {
return obj;
}
function forEachSorted(obj, iterator, context) {
function sortedKeys(obj) {
var keys = [];
for (var key in obj) keys.push(key);
keys.sort();
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
keys.push(key);
}
}
return keys.sort();
}
function forEachSorted(obj, iterator, context) {
var keys = sortedKeys(obj)
for ( var i = 0; i < keys.length; i++) {
iterator.call(context, obj[keys[i]], keys[i]);
}
@ -180,7 +185,6 @@ function formatError(arg) {
}
/**
* @description
* A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric
* characters such as '012ABC'. The reason why we are not using simply a number counter is that
* the number string gets longer over time, and it can also overflow, where as the the nextId
@ -599,20 +603,33 @@ function isLeafNode (node) {
* @example
* <doc:example>
* <doc:source>
Salutation: <input type="text" name="master.salutation" value="Hello" /><br/>
Name: <input type="text" name="master.name" value="world"/><br/>
<button ng:click="form = master.$copy()">copy</button>
<hr/>
<script>
function Ctrl(){
this.master = {
salutation: 'Hello',
name: 'world'
};
this.copy = function (){
this.form = angular.copy(this.master);
}
}
</script>
<div ng:controller="Ctrl">
Salutation: <input type="text" ng:model="master.salutation" ><br/>
Name: <input type="text" ng:model="master.name"><br/>
<button ng:click="copy()">copy</button>
<hr/>
The master object is <span ng:hide="master.$equals(form)">NOT</span> equal to the form object.
The master object is <span ng:hide="master.$equals(form)">NOT</span> equal to the form object.
<pre>master={{master}}</pre>
<pre>form={{form}}</pre>
<pre>master={{master}}</pre>
<pre>form={{form}}</pre>
</div>
* </doc:source>
* <doc:scenario>
it('should print that initialy the form object is NOT equal to master', function() {
expect(element('.doc-example-live input[name="master.salutation"]').val()).toBe('Hello');
expect(element('.doc-example-live input[name="master.name"]').val()).toBe('world');
expect(element('.doc-example-live input[ng\\:model="master.salutation"]').val()).toBe('Hello');
expect(element('.doc-example-live input[ng\\:model="master.name"]').val()).toBe('world');
expect(element('.doc-example-live span').css('display')).toBe('inline');
});
@ -691,20 +708,31 @@ function copy(source, destination){
* @example
* <doc:example>
* <doc:source>
Salutation: <input type="text" name="greeting.salutation" value="Hello" /><br/>
Name: <input type="text" name="greeting.name" value="world"/><br/>
<hr/>
<script>
function Ctrl(){
this.master = {
salutation: 'Hello',
name: 'world'
};
this.greeting = angular.copy(this.master);
}
</script>
<div ng:controller="Ctrl">
Salutation: <input type="text" ng:model="greeting.salutation"><br/>
Name: <input type="text" ng:model="greeting.name"><br/>
<hr/>
The <code>greeting</code> object is
<span ng:hide="greeting.$equals({salutation:'Hello', name:'world'})">NOT</span> equal to
<code>{salutation:'Hello', name:'world'}</code>.
The <code>greeting</code> object is
<span ng:hide="greeting.$equals(master)">NOT</span> equal to
<code>{salutation:'Hello', name:'world'}</code>.
<pre>greeting={{greeting}}</pre>
<pre>greeting={{greeting}}</pre>
</div>
* </doc:source>
* <doc:scenario>
it('should print that initialy greeting is equal to the hardcoded value object', function() {
expect(element('.doc-example-live input[name="greeting.salutation"]').val()).toBe('Hello');
expect(element('.doc-example-live input[name="greeting.name"]').val()).toBe('world');
expect(element('.doc-example-live input[ng\\:model="greeting.salutation"]').val()).toBe('Hello');
expect(element('.doc-example-live input[ng\\:model="greeting.name"]').val()).toBe('world');
expect(element('.doc-example-live span').css('display')).toBe('none');
});
@ -915,24 +943,19 @@ function angularInit(config, document){
if (config.css)
$browser.addCss(config.base_url + config.css);
else if(msie<8)
$browser.addJs(config.ie_compat, config.ie_compat_id);
scope.$apply();
}
}
function angularJsConfig(document, config) {
function angularJsConfig(document) {
bindJQuery();
var scripts = document.getElementsByTagName("script"),
config = {},
match;
config = extend({
ie_compat_id: 'ng-ie-compat'
}, config);
for(var j = 0; j < scripts.length; j++) {
match = (scripts[j].src || "").match(rngScript);
if (match) {
config.base_url = match[1];
config.ie_compat = match[1] + 'angular-ie-compat' + (match[2] || '') + '.js';
extend(config, parseKeyValue(match[6]));
eachAttribute(jqLite(scripts[j]), function(value, name){
if (/^ng:/.exec(name)) {
@ -974,11 +997,13 @@ function assertArg(arg, name, reason) {
(reason || "required"));
throw error;
}
return arg;
}
function assertArgFn(arg, name) {
assertArg(isFunction(arg), name, 'not a function, got ' +
assertArg(isFunction(arg), name, 'not a function, got ' +
(typeof arg == 'object' ? arg.constructor.name : typeof arg));
return arg;
}

View file

@ -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() {

View file

@ -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);

View file

@ -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);
}

View file

@ -103,9 +103,16 @@ var angularArray = {
* @example
<doc:example>
<doc:source>
<div ng:init="books = ['Moby Dick', 'Great Gatsby', 'Romeo and Juliet']"></div>
<input name='bookName' value='Romeo and Juliet'> <br>
Index of '{{bookName}}' in the list {{books}} is <em>{{books.$indexOf(bookName)}}</em>.
<script>
function Ctrl(){
this.books = ['Moby Dick', 'Great Gatsby', 'Romeo and Juliet'];
this.bookName = 'Romeo and Juliet';
}
</script>
<div ng:controller="Ctrl">
<input ng:model='bookName'> <br>
Index of '{{bookName}}' in the list {{books}} is <em>{{books.$indexOf(bookName)}}</em>.
</div>
</doc:source>
<doc:scenario>
it('should correctly calculate the initial index', function() {
@ -146,17 +153,29 @@ var angularArray = {
* @example
<doc:example>
<doc:source>
<table ng:init="invoice= {items:[{qty:10, description:'gadget', cost:9.95}]}">
<script>
function Ctrl(){
this.invoice = {
items:[ {
qty:10,
description:'gadget',
cost:9.95
}
]
};
}
</script>
<table class="invoice" ng:controller="Ctrl">
<tr><th>Qty</th><th>Description</th><th>Cost</th><th>Total</th><th></th></tr>
<tr ng:repeat="item in invoice.items">
<td><input name="item.qty" value="1" size="4" ng:required ng:validate="integer"></td>
<td><input name="item.description"></td>
<td><input name="item.cost" value="0.00" ng:required ng:validate="number" size="6"></td>
<td><input type="integer" ng:model="item.qty" size="4" required></td>
<td><input type="text" ng:model="item.description"></td>
<td><input type="number" ng:model="item.cost" required size="6"></td>
<td>{{item.qty * item.cost | currency}}</td>
<td>[<a href ng:click="invoice.items.$remove(item)">X</a>]</td>
</tr>
<tr>
<td><a href ng:click="invoice.items.$add()">add item</a></td>
<td><a href ng:click="invoice.items.$add({qty:1, cost:0})">add item</a></td>
<td></td>
<td>Total:</td>
<td>{{invoice.items.$sum('qty*cost') | currency}}</td>
@ -166,8 +185,8 @@ var angularArray = {
<doc:scenario>
//TODO: these specs are lame because I had to work around issues #164 and #167
it('should initialize and calculate the totals', function() {
expect(repeater('.doc-example-live table tr', 'item in invoice.items').count()).toBe(3);
expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(1)).
expect(repeater('table.invoice tr', 'item in invoice.items').count()).toBe(3);
expect(repeater('table.invoice tr', 'item in invoice.items').row(1)).
toEqual(['$99.50']);
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
@ -178,7 +197,7 @@ var angularArray = {
using('.doc-example-live tr:nth-child(3)').input('item.qty').enter('20');
using('.doc-example-live tr:nth-child(3)').input('item.cost').enter('100');
expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(2)).
expect(repeater('table.invoice tr', 'item in invoice.items').row(2)).
toEqual(['$2,000.00']);
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$2,099.50');
});
@ -297,7 +316,7 @@ var angularArray = {
{name:'Adam', phone:'555-5678'},
{name:'Julie', phone:'555-8765'}]"></div>
Search: <input name="searchText"/>
Search: <input ng:model="searchText"/>
<table id="searchTextResults">
<tr><th>Name</th><th>Phone</th><tr>
<tr ng:repeat="friend in friends.$filter(searchText)">
@ -306,9 +325,9 @@ var angularArray = {
<tr>
</table>
<hr>
Any: <input name="search.$"/> <br>
Name only <input name="search.name"/><br>
Phone only <input name="search.phone"/><br>
Any: <input ng:model="search.$"/> <br>
Name only <input ng:model="search.name"/><br>
Phone only <input ng:model="search.phone"/><br>
<table id="searchObjResults">
<tr><th>Name</th><th>Phone</th><tr>
<tr ng:repeat="friend in friends.$filter(search)">
@ -442,22 +461,29 @@ var angularArray = {
* with objects created from user input.
<doc:example>
<doc:source>
[<a href="" ng:click="people.$add()">add empty</a>]
[<a href="" ng:click="people.$add({name:'John', sex:'male'})">add 'John'</a>]
[<a href="" ng:click="people.$add({name:'Mary', sex:'female'})">add 'Mary'</a>]
<script>
function Ctrl(){
this.people = [];
}
</script>
<div ng:controller="Ctrl">
[<a href="" ng:click="people.$add()">add empty</a>]
[<a href="" ng:click="people.$add({name:'John', sex:'male'})">add 'John'</a>]
[<a href="" ng:click="people.$add({name:'Mary', sex:'female'})">add 'Mary'</a>]
<ul ng:init="people=[]">
<li ng:repeat="person in people">
<input name="person.name">
<select name="person.sex">
<option value="">--chose one--</option>
<option>male</option>
<option>female</option>
</select>
[<a href="" ng:click="people.$remove(person)">X</a>]
</li>
</ul>
<pre>people = {{people}}</pre>
<ul>
<li ng:repeat="person in people">
<input ng:model="person.name">
<select ng:model="person.sex">
<option value="">--chose one--</option>
<option>male</option>
<option>female</option>
</select>
[<a href="" ng:click="people.$remove(person)">X</a>]
</li>
</ul>
<pre>people = {{people}}</pre>
</div>
</doc:source>
<doc:scenario>
beforeEach(function() {
@ -466,7 +492,7 @@ var angularArray = {
it('should create an empty record when "add empty" is clicked', function() {
element('.doc-example-live a:contains("add empty")').click();
expect(binding('people')).toBe('people = [{\n "name":"",\n "sex":null}]');
expect(binding('people')).toBe('people = [{\n }]');
});
it('should create a "John" record when "add \'John\'" is clicked', function() {
@ -521,7 +547,7 @@ var angularArray = {
<ul>
<li ng:repeat="item in items">
{{item.name}}: points=
<input type="text" name="item.points"/> <!-- id="item{{$index}} -->
<input type="text" ng:model="item.points"/> <!-- id="item{{$index}} -->
</li>
</ul>
<p>Number of items which have one point: <em>{{ items.$count('points==1') }}</em></p>
@ -585,49 +611,56 @@ var angularArray = {
* @example
<doc:example>
<doc:source>
<div ng:init="friends = [{name:'John', phone:'555-1212', age:10},
{name:'Mary', phone:'555-9876', age:19},
{name:'Mike', phone:'555-4321', age:21},
{name:'Adam', phone:'555-5678', age:35},
{name:'Julie', phone:'555-8765', age:29}]"></div>
<pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
<hr/>
[ <a href="" ng:click="predicate=''">unsorted</a> ]
<table ng:init="predicate='-age'">
<tr>
<th><a href="" ng:click="predicate = 'name'; reverse=false">Name</a>
(<a href ng:click="predicate = '-name'; reverse=false">^</a>)</th>
<th><a href="" ng:click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th>
<th><a href="" ng:click="predicate = 'age'; reverse=!reverse">Age</a></th>
<tr>
<tr ng:repeat="friend in friends.$orderBy(predicate, reverse)">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
<tr>
</table>
<script>
function Ctrl(){
this.friends =
[{name:'John', phone:'555-1212', age:10},
{name:'Mary', phone:'555-9876', age:19},
{name:'Mike', phone:'555-4321', age:21},
{name:'Adam', phone:'555-5678', age:35},
{name:'Julie', phone:'555-8765', age:29}]
this.predicate = '-age';
}
</script>
<div ng:controller="Ctrl">
<pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
<hr/>
[ <a href="" ng:click="predicate=''">unsorted</a> ]
<table class="friend">
<tr>
<th><a href="" ng:click="predicate = 'name'; reverse=false">Name</a>
(<a href ng:click="predicate = '-name'; reverse=false">^</a>)</th>
<th><a href="" ng:click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th>
<th><a href="" ng:click="predicate = 'age'; reverse=!reverse">Age</a></th>
<tr>
<tr ng:repeat="friend in friends.$orderBy(predicate, reverse)">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
<tr>
</table>
</div>
</doc:source>
<doc:scenario>
it('should be reverse ordered by aged', function() {
expect(binding('predicate')).toBe('Sorting predicate = -age; reverse = ');
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.age')).
expect(repeater('table.friend', 'friend in friends').column('friend.age')).
toEqual(['35', '29', '21', '19', '10']);
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')).
expect(repeater('table.friend', 'friend in friends').column('friend.name')).
toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']);
});
it('should reorder the table when user selects different predicate', function() {
element('.doc-example-live a:contains("Name")').click();
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')).
expect(repeater('table.friend', 'friend in friends').column('friend.name')).
toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']);
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.age')).
expect(repeater('table.friend', 'friend in friends').column('friend.age')).
toEqual(['35', '10', '29', '19', '21']);
element('.doc-example-live a:contains("Phone")').click();
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.phone')).
expect(repeater('table.friend', 'friend in friends').column('friend.phone')).
toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']);
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')).
expect(repeater('table.friend', 'friend in friends').column('friend.name')).
toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']);
});
</doc:scenario>
@ -704,14 +737,20 @@ var angularArray = {
* @example
<doc:example>
<doc:source>
<div ng:init="numbers = [1,2,3,4,5,6,7,8,9]">
Limit [1,2,3,4,5,6,7,8,9] to: <input name="limit" value="3"/>
<script>
function Ctrl(){
this.numbers = [1,2,3,4,5,6,7,8,9];
this.limit = 3;
}
</script>
<div ng:controller="Ctrl">
Limit {{numbers}} to: <input type="integer" ng:model="limit"/>
<p>Output: {{ numbers.$limitTo(limit) | json }}</p>
</div>
</doc:source>
<doc:scenario>
it('should limit the numer array to first three items', function() {
expect(element('.doc-example-live input[name=limit]').val()).toBe('3');
expect(element('.doc-example-live input[ng\\:model=limit]').val()).toBe('3');
expect(binding('numbers.$limitTo(limit) | json')).toEqual('[1,2,3]');
});
@ -840,7 +879,7 @@ var angularFunction = {
* Hash of a:
* string is string
* number is number as string
* object is either result of calling $$hashKey function on the object or uniquely generated id,
* object is either result of calling $$hashKey function on the object or uniquely generated id,
* that is also assigned to the $$hashKey property of the object.
*
* @param obj
@ -864,7 +903,9 @@ function hashKey(obj) {
/**
* HashMap which can use objects as keys
*/
function HashMap(){}
function HashMap(array){
forEach(array, this.put, this);
}
HashMap.prototype = {
/**
* Store key value pair

View file

@ -19,8 +19,6 @@
* to `ng:bind`, but uses JSON key / value pairs to do so.
* * {@link angular.directive.ng:bind-template ng:bind-template} - Replaces the text value of an
* element with a specified template.
* * {@link angular.directive.ng:change ng:change} - Executes an expression when the value of an
* input widget changes.
* * {@link angular.directive.ng:class ng:class} - Conditionally set a CSS class on an element.
* * {@link angular.directive.ng:class-even ng:class-even} - Like `ng:class`, but works in
* conjunction with {@link angular.widget.@ng:repeat} to affect even rows in a collection.
@ -133,16 +131,16 @@ angularDirective("ng:init", function(expression){
};
</script>
<div ng:controller="SettingsController">
Name: <input type="text" name="name"/>
Name: <input type="text" ng:model="name"/>
[ <a href="" ng:click="greet()">greet</a> ]<br/>
Contact:
<ul>
<li ng:repeat="contact in contacts">
<select name="contact.type">
<select ng:model="contact.type">
<option>phone</option>
<option>email</option>
</select>
<input type="text" name="contact.value"/>
<input type="text" ng:model="contact.value"/>
[ <a href="" ng:click="clearContact(contact)">clear</a>
| <a href="" ng:click="removeContact(contact)">X</a> ]
</li>
@ -153,16 +151,16 @@ angularDirective("ng:init", function(expression){
<doc:scenario>
it('should check controller', function(){
expect(element('.doc-example-live div>:input').val()).toBe('John Smith');
expect(element('.doc-example-live li[ng\\:repeat-index="0"] input').val())
expect(element('.doc-example-live li:nth-child(1) input').val())
.toBe('408 555 1212');
expect(element('.doc-example-live li[ng\\:repeat-index="1"] input').val())
expect(element('.doc-example-live li:nth-child(2) input').val())
.toBe('john.smith@example.org');
element('.doc-example-live li:first a:contains("clear")').click();
expect(element('.doc-example-live li:first input').val()).toBe('');
element('.doc-example-live li:last a:contains("add")').click();
expect(element('.doc-example-live li[ng\\:repeat-index="2"] input').val())
expect(element('.doc-example-live li:nth-child(3) input').val())
.toBe('yourname@example.org');
});
</doc:scenario>
@ -200,8 +198,15 @@ angularDirective("ng:controller", function(expression){
* Enter a name in the Live Preview text box; the greeting below the text box changes instantly.
<doc:example>
<doc:source>
Enter name: <input type="text" name="name" value="Whirled"> <br>
Hello <span ng:bind="name"></span>!
<script>
function Ctrl(){
this.name = 'Whirled';
}
</script>
<div ng:controller="Ctrl">
Enter name: <input type="text" ng:model="name"> <br/>
Hello <span ng:bind="name"></span>!
</div>
</doc:source>
<doc:scenario>
it('should check ng:bind', function(){
@ -320,9 +325,17 @@ function compileBindTemplate(template){
* Try it here: enter text in text box and watch the greeting change.
<doc:example>
<doc:source>
Salutation: <input type="text" name="salutation" value="Hello"><br/>
Name: <input type="text" name="name" value="World"><br/>
<pre ng:bind-template="{{salutation}} {{name}}!"></pre>
<script>
function Ctrl(){
this.salutation = 'Hello';
this.name = 'World';
}
</script>
<div ng:controller="Ctrl">
Salutation: <input type="text" ng:model="salutation"><br/>
Name: <input type="text" ng:model="name"><br/>
<pre ng:bind-template="{{salutation}} {{name}}!"></pre>
</div>
</doc:source>
<doc:scenario>
it('should check ng:bind', function(){
@ -351,13 +364,6 @@ angularDirective("ng:bind-template", function(expression, element){
};
});
var REMOVE_ATTRIBUTES = {
'disabled':'disabled',
'readonly':'readOnly',
'checked':'checked',
'selected':'selected',
'multiple':'multiple'
};
/**
* @ngdoc directive
* @name angular.directive.ng:bind-attr
@ -395,9 +401,16 @@ var REMOVE_ATTRIBUTES = {
* Enter a search string in the Live Preview text box and then click "Google". The search executes instantly.
<doc:example>
<doc:source>
Google for:
<input type="text" name="query" value="AngularJS"/>
<a href="http://www.google.com/search?q={{query}}">Google</a>
<script>
function Ctrl(){
this.query = 'AngularJS';
}
</script>
<div ng:controller="Ctrl">
Google for:
<input type="text" ng:model="query"/>
<a href="http://www.google.com/search?q={{query}}">Google</a>
</div>
</doc:source>
<doc:scenario>
it('should check ng:bind-attr', function(){
@ -417,18 +430,15 @@ angularDirective("ng:bind-attr", function(expression){
var values = scope.$eval(expression);
for(var key in values) {
var value = compileBindTemplate(values[key])(scope, element),
specialName = REMOVE_ATTRIBUTES[lowercase(key)];
specialName = BOOLEAN_ATTR[lowercase(key)];
if (lastValue[key] !== value) {
lastValue[key] = value;
if (specialName) {
if (toBoolean(value)) {
element.attr(specialName, specialName);
element.attr('ng-' + specialName, value);
} else {
element.removeAttr(specialName);
element.removeAttr('ng-' + specialName);
}
(element.data($$validate)||noop)();
} else {
element.attr(key, value);
}
@ -505,12 +515,22 @@ angularDirective("ng:click", function(expression, element){
* @example
<doc:example>
<doc:source>
<form ng:submit="list.push(text);text='';" ng:init="list=[]">
<script>
function Ctrl(){
this.list = [];
this.text = 'hello';
this.submit = function(){
this.list.push(this.text);
this.text = '';
};
}
</script>
<form ng:submit="submit()" ng:controller="Ctrl">
Enter text and hit enter:
<input type="text" name="text" value="hello"/>
<input type="text" ng:model="text"/>
<input type="submit" id="submit" value="Submit" />
<pre>list={{list}}</pre>
</form>
<pre>list={{list}}</pre>
</doc:source>
<doc:scenario>
it('should check ng:submit', function(){
@ -537,7 +557,7 @@ function ngClass(selector) {
return function(element) {
this.$watch(expression, function(scope, newVal, oldVal) {
if (selector(scope.$index)) {
element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal)
element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal);
element.addClass(isArray(newVal) ? newVal.join(' ') : newVal);
}
});
@ -689,7 +709,7 @@ angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;}));
* @example
<doc:example>
<doc:source>
Click me: <input type="checkbox" name="checked"><br/>
Click me: <input type="checkbox" ng:model="checked"><br/>
Show: <span ng:show="checked">I show up when your checkbox is checked.</span> <br/>
Hide: <span ng:hide="checked">I hide when your checkbox is checked.</span>
</doc:source>
@ -730,7 +750,7 @@ angularDirective("ng:show", function(expression, element){
* @example
<doc:example>
<doc:source>
Click me: <input type="checkbox" name="checked"><br/>
Click me: <input type="checkbox" ng:model="checked"><br/>
Show: <span ng:show="checked">I show up when you checkbox is checked?</span> <br/>
Hide: <span ng:hide="checked">I hide when you checkbox is checked?</span>
</doc:source>

View file

@ -48,9 +48,16 @@
* @example
<doc:example>
<doc:source>
<input type="text" name="amount" value="1234.56"/> <br/>
default currency symbol ($): {{amount | currency}}<br/>
custom currency identifier (USD$): {{amount | currency:"USD$"}}
<script>
function Ctrl(){
this.amount = 1234.56;
}
</script>
<div ng:controller="Ctrl">
<input type="number" ng:model="amount"/> <br/>
default currency symbol ($): {{amount | currency}}<br/>
custom currency identifier (USD$): {{amount | currency:"USD$"}}
</div>
</doc:source>
<doc:scenario>
it('should init with 1234.56', function(){
@ -93,10 +100,17 @@ angularFilter.currency = function(amount, currencySymbol){
* @example
<doc:example>
<doc:source>
Enter number: <input name='val' value='1234.56789' /><br/>
Default formatting: {{val | number}}<br/>
No fractions: {{val | number:0}}<br/>
Negative number: {{-val | number:4}}
<script>
function Ctrl(){
this.val = 1234.56789;
}
</script>
<div ng:controller="Ctrl">
Enter number: <input ng:model='val'><br/>
Default formatting: {{val | number}}<br/>
No fractions: {{val | number:0}}<br/>
Negative number: {{-val | number:4}}
</div>
</doc:source>
<doc:scenario>
it('should format numbers', function(){
@ -462,36 +476,43 @@ angularFilter.uppercase = uppercase;
* @example
<doc:example>
<doc:source>
Snippet: <textarea name="snippet" cols="60" rows="3">
&lt;p style="color:blue"&gt;an html
&lt;em onmouseover="this.textContent='PWN3D!'"&gt;click here&lt;/em&gt;
snippet&lt;/p&gt;</textarea>
<table>
<tr>
<td>Filter</td>
<td>Source</td>
<td>Rendered</td>
</tr>
<tr id="html-filter">
<td>html filter</td>
<td>
<pre>&lt;div ng:bind="snippet | html"&gt;<br/>&lt;/div&gt;</pre>
</td>
<td>
<div ng:bind="snippet | html"></div>
</td>
</tr>
<tr id="escaped-html">
<td>no filter</td>
<td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng:bind="snippet"></div></td>
</tr>
<tr id="html-unsafe-filter">
<td>unsafe html filter</td>
<td><pre>&lt;div ng:bind="snippet | html:'unsafe'"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng:bind="snippet | html:'unsafe'"></div></td>
</tr>
</table>
<script>
function Ctrl(){
this.snippet =
'<p style="color:blue">an html\n' +
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
'snippet</p>';
}
</script>
<div ng:controller="Ctrl">
Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
<td>Filter</td>
<td>Source</td>
<td>Rendered</td>
</tr>
<tr id="html-filter">
<td>html filter</td>
<td>
<pre>&lt;div ng:bind="snippet | html"&gt;<br/>&lt;/div&gt;</pre>
</td>
<td>
<div ng:bind="snippet | html"></div>
</td>
</tr>
<tr id="escaped-html">
<td>no filter</td>
<td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng:bind="snippet"></div></td>
</tr>
<tr id="html-unsafe-filter">
<td>unsafe html filter</td>
<td><pre>&lt;div ng:bind="snippet | html:'unsafe'"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng:bind="snippet | html:'unsafe'"></div></td>
</tr>
</table>
</div>
</doc:source>
<doc:scenario>
it('should sanitize the html snippet ', function(){
@ -543,12 +564,18 @@ angularFilter.html = function(html, option){
* @example
<doc:example>
<doc:source>
Snippet: <textarea name="snippet" cols="60" rows="3">
Pretty text with some links:
http://angularjs.org/,
mailto:us@somewhere.org,
another@somewhere.org,
and one more: ftp://127.0.0.1/.</textarea>
<script>
function Ctrl(){
this.snippet =
'Pretty text with some links:\n'+
'http://angularjs.org/,\n'+
'mailto:us@somewhere.org,\n'+
'another@somewhere.org,\n'+
'and one more: ftp://127.0.0.1/.';
}
</script>
<div ng:controller="Ctrl">
Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
<td>Filter</td>

View file

@ -1,202 +0,0 @@
'use strict';
/**
* @workInProgress
* @ngdoc overview
* @name angular.formatter
* @description
*
* Formatters are used for translating data formats between those used for display and those used
* for storage.
*
* Following is the list of built-in angular formatters:
*
* * {@link angular.formatter.boolean boolean} - Formats user input in boolean format
* * {@link angular.formatter.json json} - Formats user input in JSON format
* * {@link angular.formatter.list list} - Formats user input string as an array
* * {@link angular.formatter.number number} - Formats user input strings as a number
* * {@link angular.formatter.trim trim} - Trims extras spaces from end of user input
*
* For more information about how angular formatters work, and how to create your own formatters,
* see {@link guide/dev_guide.templates.formatters Understanding Angular Formatters} in the angular
* Developer Guide.
*/
function formatter(format, parse) {return {'format':format, 'parse':parse || format};}
function toString(obj) {
return (isDefined(obj) && obj !== null) ? "" + obj : obj;
}
var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/;
angularFormatter.noop = formatter(identity, identity);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.json
*
* @description
* Formats the user input as JSON text.
*
* @returns {?string} A JSON string representation of the model.
*
* @example
<doc:example>
<doc:source>
<div ng:init="data={name:'misko', project:'angular'}">
<input type="text" size='50' name="data" ng:format="json"/>
<pre>data={{data}}</pre>
</div>
</doc:source>
<doc:scenario>
it('should format json', function(){
expect(binding('data')).toEqual('data={\n \"name\":\"misko\",\n \"project\":\"angular\"}');
input('data').enter('{}');
expect(binding('data')).toEqual('data={\n }');
});
</doc:scenario>
</doc:example>
*/
angularFormatter.json = formatter(toJson, function(value){
return fromJson(value || 'null');
});
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.boolean
*
* @description
* Use boolean formatter if you wish to store the data as boolean.
*
* @returns {boolean} Converts to `true` unless user enters (blank), `f`, `false`, `0`, `no`, `[]`.
*
* @example
<doc:example>
<doc:source>
Enter truthy text:
<input type="text" name="value" ng:format="boolean" value="no"/>
<input type="checkbox" name="value"/>
<pre>value={{value}}</pre>
</doc:source>
<doc:scenario>
it('should format boolean', function(){
expect(binding('value')).toEqual('value=false');
input('value').enter('truthy');
expect(binding('value')).toEqual('value=true');
});
</doc:scenario>
</doc:example>
*/
angularFormatter['boolean'] = formatter(toString, toBoolean);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.number
*
* @description
* Use number formatter if you wish to convert the user entered string to a number.
*
* @returns {number} Number from the parsed string.
*
* @example
<doc:example>
<doc:source>
Enter valid number:
<input type="text" name="value" ng:format="number" value="1234"/>
<pre>value={{value}}</pre>
</doc:source>
<doc:scenario>
it('should format numbers', function(){
expect(binding('value')).toEqual('value=1234');
input('value').enter('5678');
expect(binding('value')).toEqual('value=5678');
});
</doc:scenario>
</doc:example>
*/
angularFormatter.number = formatter(toString, function(obj){
if (obj == null || NUMBER.exec(obj)) {
return obj===null || obj === '' ? null : 1*obj;
} else {
throw "Not a number";
}
});
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.list
*
* @description
* Use list formatter if you wish to convert the user entered string to an array.
*
* @returns {Array} Array parsed from the entered string.
*
* @example
<doc:example>
<doc:source>
Enter a list of items:
<input type="text" name="value" ng:format="list" value=" chair ,, table"/>
<input type="text" name="value" ng:format="list"/>
<pre>value={{value}}</pre>
</doc:source>
<doc:scenario>
it('should format lists', function(){
expect(binding('value')).toEqual('value=["chair","table"]');
this.addFutureAction('change to XYZ', function($window, $document, done){
$document.elements('.doc-example-live :input:last').val(',,a,b,').trigger('change');
done();
});
expect(binding('value')).toEqual('value=["a","b"]');
});
</doc:scenario>
</doc:example>
*/
angularFormatter.list = formatter(
function(obj) { return obj ? obj.join(", ") : obj; },
function(value) {
var list = [];
forEach((value || '').split(','), function(item){
item = trim(item);
if (item) list.push(item);
});
return list;
}
);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.trim
*
* @description
* Use trim formatter if you wish to trim extra spaces in user text.
*
* @returns {String} Trim excess leading and trailing space.
*
* @example
<doc:example>
<doc:source>
Enter text with leading/trailing spaces:
<input type="text" name="value" ng:format="trim" value=" book "/>
<input type="text" name="value" ng:format="trim"/>
<pre>value={{value|json}}</pre>
</doc:source>
<doc:scenario>
it('should format trim', function(){
expect(binding('value')).toEqual('value="book"');
this.addFutureAction('change to XYZ', function($window, $document, done){
$document.elements('.doc-example-live :input:last').val(' text ').trigger('change');
done();
});
expect(binding('value')).toEqual('value="text"');
});
</doc:scenario>
</doc:example>
*/
angularFormatter.trim = formatter(
function(obj) { return obj ? trim("" + obj) : ""; }
);

View file

@ -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;

View file

@ -163,7 +163,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* This example uses `link` variable inside `href` attribute:
<doc:example>
<doc:source>
<input name="value" /><br />
<input ng:model="value" /><br />
<a id="link-1" href ng:click="value = 1">link 1</a> (link, don't reload)<br />
<a id="link-2" href="" ng:click="value = 2">link 2</a> (link, don't reload)<br />
<a id="link-3" ng:href="/{{'123'}}" ng:ext-link>link 3</a> (link, reload!)<br />
@ -262,8 +262,8 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
Click me to toggle: <input type="checkbox" name="checked"><br/>
<button name="button" ng:disabled="{{checked}}">Button</button>
Click me to toggle: <input type="checkbox" ng:model="checked"><br/>
<button ng:model="button" ng:disabled="{{checked}}">Button</button>
</doc:source>
<doc:scenario>
it('should toggle button', function() {
@ -292,7 +292,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
Check me to check both: <input type="checkbox" name="master"><br/>
Check me to check both: <input type="checkbox" ng:model="master"><br/>
<input id="checkSlave" type="checkbox" ng:checked="{{master}}">
</doc:source>
<doc:scenario>
@ -323,7 +323,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
Check me check multiple: <input type="checkbox" name="checked"><br/>
Check me check multiple: <input type="checkbox" ng:model="checked"><br/>
<select id="select" ng:multiple="{{checked}}">
<option>Misko</option>
<option>Igor</option>
@ -358,7 +358,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
Check me to make text readonly: <input type="checkbox" name="checked"><br/>
Check me to make text readonly: <input type="checkbox" ng:model="checked"><br/>
<input type="text" ng:readonly="{{checked}}" value="I'm Angular"/>
</doc:source>
<doc:scenario>
@ -388,7 +388,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @example
<doc:example>
<doc:source>
Check me to select: <input type="checkbox" name="checked"><br/>
Check me to select: <input type="checkbox" ng:model="checked"><br/>
<select>
<option>Hello!</option>
<option id="greet" ng:selected="{{checked}}">Greetings!</option>
@ -408,10 +408,10 @@ angularTextMarkup('option', function(text, textNode, parentElement){
var NG_BIND_ATTR = 'ng:bind-attr';
var SPECIAL_ATTRS = {};
var SIDE_EFFECT_ATTRS = {};
forEach('src,href,checked,disabled,multiple,readonly,selected'.split(','), function(name) {
SPECIAL_ATTRS['ng:' + name] = name;
forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) {
SIDE_EFFECT_ATTRS['ng:' + name] = name;
});
angularAttrMarkup('{{}}', function(value, name, element){
@ -421,10 +421,10 @@ angularAttrMarkup('{{}}', function(value, name, element){
value = decodeURI(value);
var bindings = parseBindings(value),
bindAttr;
if (hasBindings(bindings) || SPECIAL_ATTRS[name]) {
if (hasBindings(bindings) || SIDE_EFFECT_ATTRS[name]) {
element.removeAttr(name);
bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}");
bindAttr[SPECIAL_ATTRS[name] || name] = value;
bindAttr[SIDE_EFFECT_ATTRS[name] || name] = value;
element.attr(NG_BIND_ATTR, toJson(bindAttr));
}
});

View file

@ -247,8 +247,6 @@ function parser(text, json){
assignable: assertConsumed(assignable),
primary: assertConsumed(primary),
statements: assertConsumed(statements),
validator: assertConsumed(validator),
formatter: assertConsumed(formatter),
filter: assertConsumed(filter)
};
@ -361,36 +359,6 @@ function parser(text, json){
return pipeFunction(angularFilter);
}
function validator(){
return pipeFunction(angularValidator);
}
function formatter(){
var token = expect();
var formatter = angularFormatter[token.text];
var argFns = [];
if (!formatter) throwError('is not a valid formatter.', token);
while(true) {
if ((token = expect(':'))) {
argFns.push(expression());
} else {
return valueFn({
format:invokeFn(formatter.format),
parse:invokeFn(formatter.parse)
});
}
}
function invokeFn(fn){
return function(self, input){
var args = [input];
for ( var i = 0; i < argFns.length; i++) {
args.push(argFns[i](self));
}
return fn.apply(self, args);
};
}
}
function _pipeFunction(fnScope){
var fn = functionIdent(fnScope);
var argsFn = [];
@ -735,16 +703,19 @@ function getterFn(path) {
code += 'if(!s) return s;\n' +
'l=s;\n' +
's=s' + key + ';\n' +
'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l' +
key + '.apply(l, arguments); };\n';
'if(typeof s=="function" && !(s instanceof RegExp)) {\n' +
' fn=function(){ return l' + key + '.apply(l, arguments); };\n' +
' fn.$unboundFn=s;\n' +
' s=fn;\n' +
'}\n';
if (key.charAt(1) == '$') {
// special code for super-imposed functions
var name = key.substr(2);
code += 'if(!s) {\n' +
' t = angular.Global.typeOf(l);\n' +
' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' +
' if (fn) s = function(){ return fn.apply(l, ' +
'[l].concat(Array.prototype.slice.call(arguments, 0))); };\n' +
' if (fn) ' +
's = function(){ return fn.apply(l, [l].concat(Array.prototype.slice.call(arguments, 0))); };\n' +
'}\n';
}
});

View file

@ -247,7 +247,7 @@ function browserTrigger(element, type) {
'radio': 'click',
'select-one': 'change',
'select-multiple': 'change'
}[element.type] || 'click';
}[lowercase(element.type)] || 'click';
}
if (lowercase(nodeName_(element)) == 'option') {
element.parentNode.value = element.value;

View file

@ -180,7 +180,7 @@ angular.scenario.dsl('input', function() {
chain.enter = function(value) {
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function($window, $document, done) {
var input = $document.elements(':input[name="$1"]', this.name);
var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
input.val(value);
input.trigger('keydown');
done();
@ -189,7 +189,7 @@ angular.scenario.dsl('input', function() {
chain.check = function() {
return this.addFutureAction("checkbox '" + this.name + "' toggle", function($window, $document, done) {
var input = $document.elements(':checkbox[name="$1"]', this.name);
var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':checkbox');
input.trigger('click');
done();
});
@ -198,7 +198,7 @@ angular.scenario.dsl('input', function() {
chain.select = function(value) {
return this.addFutureAction("radio button '" + this.name + "' toggle '" + value + "'", function($window, $document, done) {
var input = $document.
elements(':radio[name$="@$1"][value="$2"]', this.name, value);
elements('[ng\\:model="$1"][value="$2"]', this.name, value).filter(':radio');
input.trigger('click');
done();
});
@ -206,7 +206,7 @@ angular.scenario.dsl('input', function() {
chain.val = function() {
return this.addFutureAction("return input val", function($window, $document, done) {
var input = $document.elements(':input[name="$1"]', this.name);
var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
done(null,input.val());
});
};
@ -268,8 +268,16 @@ angular.scenario.dsl('select', function() {
chain.option = function(value) {
return this.addFutureAction("select '" + this.name + "' option '" + value + "'", function($window, $document, done) {
var select = $document.elements('select[name="$1"]', this.name);
select.val(value);
var select = $document.elements('select[ng\\:model="$1"]', this.name);
var option = select.find('option[value="' + value + '"]');
if (option.length) {
select.val(value);
} else {
option = select.find('option:contains("' + value + '")');
if (option.length) {
select.val(option.val());
}
}
select.trigger('change');
done();
});
@ -278,7 +286,7 @@ angular.scenario.dsl('select', function() {
chain.options = function() {
var values = arguments;
return this.addFutureAction("select '" + this.name + "' options '" + values + "'", function($window, $document, done) {
var select = $document.elements('select[multiple][name="$1"]', this.name);
var select = $document.elements('select[multiple][ng\\:model="$1"]', this.name);
select.val(values);
select.trigger('change');
done();

394
src/service/formFactory.js Normal file
View file

@ -0,0 +1,394 @@
'use strict';
/**
* @ngdoc service
* @name angular.service.$formFactory
*
* @description
* Use `$formFactory` to create a new instance of a {@link guide/dev_guide.forms form}
* controller or to find the nearest form instance for a given DOM element.
*
* The form instance is a collection of widgets, and is responsible for life cycle and validation
* of widget.
*
* Keep in mind that both form and widget instances are {@link api/angular.scope scopes}.
*
* @param {Form=} parentForm The form which should be the parent form of the new form controller.
* If none specified default to the `rootForm`.
* @returns {Form} A new <a href="#form">form</a> instance.
*
* @example
*
* This example shows how one could write a widget which would enable data-binding on
* `contenteditable` feature of HTML.
*
<doc:example>
<doc:source>
<script>
function EditorCntl(){
this.html = '<b>Hello</b> <i>World</i>!';
}
function HTMLEditorWidget(element) {
var self = this;
var htmlFilter = angular.filter('html');
this.$parseModel = function(){
// need to protect for script injection
try {
this.$viewValue = htmlFilter(this.$modelValue || '').get();
if (this.$error.HTML) {
// we were invalid, but now we are OK.
this.$emit('$valid', 'HTML');
}
} catch (e) {
// if HTML not parsable invalidate form.
this.$emit('$invalid', 'HTML');
}
}
this.$render = function(){
element.html(this.$viewValue);
}
element.bind('keyup', function(){
self.$apply(function(){
self.$emit('$viewChange', element.html());
});
});
}
angular.directive('ng:contenteditable', function(){
function linkFn($formFactory, element) {
var exp = element.attr('ng:contenteditable'),
form = $formFactory.forElement(element),
widget;
element.attr('contentEditable', true);
widget = form.$createWidget({
scope: this,
model: exp,
controller: HTMLEditorWidget,
controllerArgs: [element]});
// if the element is destroyed, then we need to notify the form.
element.bind('$destroy', function(){
widget.$destroy();
});
}
linkFn.$inject = ['$formFactory'];
return linkFn;
});
</script>
<form name='editorForm' ng:controller="EditorCntl">
<div ng:contenteditable="html"></div>
<hr/>
HTML: <br/>
<textarea ng:model="html" cols=80></textarea>
<hr/>
<pre>editorForm = {{editorForm}}</pre>
</form>
</doc:source>
<doc:scenario>
it('should enter invalid HTML', function(){
expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
input('html').enter('<');
expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
});
</doc:scenario>
</doc:example>
*/
angularServiceInject('$formFactory', function(){
/**
* @ngdoc proprety
* @name rootForm
* @propertyOf angular.service.$formFactory
* @description
* Static property on `$formFactory`
*
* Each application ({@link guide/dev_guide.scopes.internals root scope}) gets a root form which
* is the top-level parent of all forms.
*/
formFactory.rootForm = formFactory(this);
/**
* @ngdoc method
* @name forElement
* @methodOf angular.service.$formFactory
* @description
* Static method on `$formFactory` service.
*
* Retrieve the closest form for a given element or defaults to the `root` form. Used by the
* {@link angular.widget.form form} element.
* @param {Element} element The element where the search for form should initiate.
*/
formFactory.forElement = function (element) {
return element.inheritedData('$form') || formFactory.rootForm;
};
return formFactory;
function formFactory(parent) {
return (parent || formFactory.rootForm).$new(FormController);
}
});
function propertiesUpdate(widget) {
widget.$valid = !(widget.$invalid =
!(widget.$readonly || widget.$disabled || equals(widget.$error, {})));
}
/**
* @ngdoc property
* @name $error
* @propertyOf angular.service.$formFactory
* @description
* Property of the form and widget instance.
*
* Summary of all of the errors on the page. If a widget emits `$invalid` with `REQUIRED` key,
* then the `$error` object will have a `REQUIRED` key with an array of widgets which have
* emitted this key. `form.$error.REQUIRED == [ widget ]`.
*/
/**
* @workInProgress
* @ngdoc property
* @name $invalid
* @propertyOf angular.service.$formFactory
* @description
* Property of the form and widget instance.
*
* True if any of the widgets of the form are invalid.
*/
/**
* @workInProgress
* @ngdoc property
* @name $valid
* @propertyOf angular.service.$formFactory
* @description
* Property of the form and widget instance.
*
* True if all of the widgets of the form are valid.
*/
/**
* @ngdoc event
* @name angular.service.$formFactory#$valid
* @eventOf angular.service.$formFactory
* @eventType listen on form
* @description
* Upon receiving the `$valid` event from the widget update the `$error`, `$valid` and `$invalid`
* properties of both the widget as well as the from.
*
* @param {String} validationKey The validation key to be used when updating the `$error` object.
* The validation key is what will allow the template to bind to a specific validation error
* such as `<div ng:show="form.$error.KEY">error for key</div>`.
*/
/**
* @ngdoc event
* @name angular.service.$formFactory#$invalid
* @eventOf angular.service.$formFactory
* @eventType listen on form
* @description
* Upon receiving the `$invalid` event from the widget update the `$error`, `$valid` and `$invalid`
* properties of both the widget as well as the from.
*
* @param {String} validationKey The validation key to be used when updating the `$error` object.
* The validation key is what will allow the template to bind to a specific validation error
* such as `<div ng:show="form.$error.KEY">error for key</div>`.
*/
/**
* @ngdoc event
* @name angular.service.$formFactory#$validate
* @eventOf angular.service.$formFactory
* @eventType emit on widget
* @description
* Emit the `$validate` event on the widget, giving a widget a chance to emit a
* `$valid` / `$invalid` event base on its state. The `$validate` event is triggered when the
* model or the view changes.
*/
/**
* @ngdoc event
* @name angular.service.$formFactory#$viewChange
* @eventOf angular.service.$formFactory
* @eventType listen on widget
* @description
* A widget is responsible for emitting this event whenever the view changes do to user interaction.
* The event takes a `$viewValue` parameter, which is the new value of the view. This
* event triggers a call to `$parseView()` as well as `$validate` event on widget.
*
* @param {*} viewValue The new value for the view which will be assigned to `widget.$viewValue`.
*/
function FormController(){
var form = this,
$error = form.$error = {};
form.$on('$destroy', function(event){
var widget = event.targetScope;
if (widget.$widgetId) {
delete form[widget.$widgetId];
}
forEach($error, removeWidget, widget);
});
form.$on('$valid', function(event, error){
var widget = event.targetScope;
delete widget.$error[error];
propertiesUpdate(widget);
removeWidget($error[error], error, widget);
});
form.$on('$invalid', function(event, error){
var widget = event.targetScope;
addWidget(error, widget);
widget.$error[error] = true;
propertiesUpdate(widget);
});
propertiesUpdate(form);
function removeWidget(queue, errorKey, widget) {
if (queue) {
widget = widget || this; // so that we can be used in forEach;
for (var i = 0, length = queue.length; i < length; i++) {
if (queue[i] === widget) {
queue.splice(i, 1);
if (!queue.length) {
delete $error[errorKey];
}
}
}
propertiesUpdate(form);
}
}
function addWidget(errorKey, widget) {
var queue = $error[errorKey];
if (queue) {
for (var i = 0, length = queue.length; i < length; i++) {
if (queue[i] === widget) {
return;
}
}
} else {
$error[errorKey] = queue = [];
}
queue.push(widget);
propertiesUpdate(form);
}
}
/**
* @ngdoc method
* @name $createWidget
* @methodOf angular.service.$formFactory
* @description
*
* Use form's `$createWidget` instance method to create new widgets. The widgets can be created
* using an alias which makes the accessible from the form and available for data-binding,
* useful for displaying validation error messages.
*
* The creation of a widget sets up:
*
* - `$watch` of `expression` on `model` scope. This code path syncs the model to the view.
* The `$watch` listener will:
*
* - assign the new model value of `expression` to `widget.$modelValue`.
* - call `widget.$parseModel` method if present. The `$parseModel` is responsible for copying
* the `widget.$modelValue` to `widget.$viewValue` and optionally converting the data.
* (For example to convert a number into string)
* - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid`
* event.
* - call `widget.$render()` method on widget. The `$render` method is responsible for
* reading the `widget.$viewValue` and updating the DOM.
*
* - Listen on `$viewChange` event from the `widget`. This code path syncs the view to the model.
* The `$viewChange` listener will:
*
* - assign the value to `widget.$viewValue`.
* - call `widget.$parseView` method if present. The `$parseView` is responsible for copying
* the `widget.$viewValue` to `widget.$modelValue` and optionally converting the data.
* (For example to convert a string into number)
* - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid`
* event.
* - Assign the `widget.$modelValue` to the `expression` on the `model` scope.
*
* - Creates these set of properties on the `widget` which are updated as a response to the
* `$valid` / `$invalid` events:
*
* - `$error` - object - validation errors will be published as keys on this object.
* Data-binding to this property is useful for displaying the validation errors.
* - `$valid` - boolean - true if there are no validation errors
* - `$invalid` - boolean - opposite of `$valid`.
* @param {Object} params Named parameters:
*
* - `scope` - `{Scope}` - The scope to which the model for this widget is attached.
* - `model` - `{string}` - The name of the model property on model scope.
* - `controller` - {WidgetController} - The controller constructor function.
* The controller constructor should create these instance methods.
* - `$parseView()`: optional method responsible for copying `$viewVale` to `$modelValue`.
* The method may fire `$valid`/`$invalid` events.
* - `$parseModel()`: optional method responsible for copying `$modelVale` to `$viewValue`.
* The method may fire `$valid`/`$invalid` events.
* - `$render()`: required method which needs to update the DOM of the widget to match the
* `$viewValue`.
*
* - `controllerArgs` - `{Array}` (Optional) - Any extra arguments will be curried to the
* WidgetController constructor.
* - `onChange` - `{(string|function())}` (Optional) - Expression to execute when user changes the
* value.
* - `alias` - `{string}` (Optional) - The name of the form property under which the widget
* instance should be published. The name should be unique for each form.
* @returns {Widget} Instance of a widget scope.
*/
FormController.prototype.$createWidget = function(params) {
var form = this,
modelScope = params.scope,
onChange = params.onChange,
alias = params.alias,
scopeGet = parser(params.model).assignable(),
scopeSet = scopeGet.assign,
widget = this.$new(params.controller, params.controllerArgs);
widget.$error = {};
// Set the state to something we know will change to get the process going.
widget.$modelValue = Number.NaN;
// watch for scope changes and update the view appropriately
modelScope.$watch(scopeGet, function (scope, value) {
if (!equals(widget.$modelValue, value)) {
widget.$modelValue = value;
widget.$parseModel ? widget.$parseModel() : (widget.$viewValue = value);
widget.$emit('$validate');
widget.$render && widget.$render();
}
});
widget.$on('$viewChange', function(event, viewValue){
if (!equals(widget.$viewValue, viewValue)) {
widget.$viewValue = viewValue;
widget.$parseView ? widget.$parseView() : (widget.$modelValue = widget.$viewValue);
scopeSet(modelScope, widget.$modelValue);
if (onChange) modelScope.$eval(onChange);
widget.$emit('$validate');
}
});
propertiesUpdate(widget);
// assign the widgetModel to the form
if (alias && !form.hasOwnProperty(alias)) {
form[alias] = widget;
widget.$widgetId = alias;
} else {
alias = null;
}
return widget;
};

View file

@ -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;
});

View file

@ -18,12 +18,13 @@
<script>
function LogCtrl($log) {
this.$log = $log;
this.message = 'Hello World!';
}
</script>
<div ng:controller="LogCtrl">
<p>Reload this page with open console, enter text and hit the log button...</p>
Message:
<input type="text" name="message" value="Hello World!"/>
<input type="text" ng:model="message"/>
<button ng:click="$log.log(message)">log</button>
<button ng:click="$log.warn(message)">warn</button>
<button ng:click="$log.info(message)">info</button>

View file

@ -160,6 +160,7 @@
<doc:source jsfiddle="false">
<script>
function BuzzController($resource) {
this.userId = 'googlebuzz';
this.Activity = $resource(
'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
{alt:'json', callback:'JSON_CALLBACK'},
@ -179,7 +180,7 @@
</script>
<div ng:controller="BuzzController">
<input name="userId" value="googlebuzz"/>
<input ng:model="userId"/>
<button ng:click="fetch()">fetch</button>
<hr/>
<div ng:repeat="item in activities.data.items">

View file

@ -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);

View file

@ -17,7 +17,7 @@
* @example
<doc:example>
<doc:source>
<input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" name="greeting" />
<input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" ng:model="greeting" />
<button ng:click="$window.alert(greeting)">ALERT</button>
</doc:source>
<doc:scenario>

View file

@ -111,6 +111,7 @@
<script>
function FetchCntl($xhr) {
var self = this;
this.url = 'index.html';
this.fetch = function() {
self.code = null;
@ -133,11 +134,11 @@
FetchCntl.$inject = ['$xhr'];
</script>
<div ng:controller="FetchCntl">
<select name="method">
<select ng:model="method">
<option>GET</option>
<option>JSON</option>
</select>
<input type="text" name="url" value="index.html" size="80"/>
<input type="text" ng:model="url" size="80"/>
<button ng:click="fetch()">fetch</button><br>
<button ng:click="updateModel('GET', 'index.html')">Sample GET</button>
<button ng:click="updateModel('JSON', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button>

View file

@ -1,482 +0,0 @@
'use strict';
/**
* @workInProgress
* @ngdoc overview
* @name angular.validator
* @description
*
* Most of the built-in angular validators are used to check user input against defined types or
* patterns. You can easily create your own custom validators as well.
*
* Following is the list of built-in angular validators:
*
* * {@link angular.validator.asynchronous asynchronous()} - Provides asynchronous validation via a
* callback function.
* * {@link angular.validator.date date()} - Checks user input against default date format:
* "MM/DD/YYYY"
* * {@link angular.validator.email email()} - Validates that user input is a well-formed email
* address.
* * {@link angular.validator.integer integer()} - Validates that user input is an integer
* * {@link angular.validator.json json()} - Validates that user input is valid JSON
* * {@link angular.validator.number number()} - Validates that user input is a number
* * {@link angular.validator.phone phone()} - Validates that user input matches the pattern
* "1(123)123-1234"
* * {@link angular.validator.regexp regexp()} - Restricts valid input to a specified regular
* expression pattern
* * {@link angular.validator.url url()} - Validates that user input is a well-formed URL.
*
* For more information about how angular validators work, and how to create your own validators,
* see {@link guide/dev_guide.templates.validators Understanding Angular Validators} in the angular
* Developer Guide.
*/
extend(angularValidator, {
'noop': function() { return null; },
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.regexp
* @description
* Use regexp validator to restrict the input to any Regular Expression.
*
* @param {string} value value to validate
* @param {string|regexp} expression regular expression.
* @param {string=} msg error message to display.
* @css ng-validation-error
*
* @example
<doc:example>
<doc:source>
<script> function Cntl(){
this.ssnRegExp = /^\d\d\d-\d\d-\d\d\d\d$/;
}
</script>
Enter valid SSN:
<div ng:controller="Cntl">
<input name="ssn" value="123-45-6789" ng:validate="regexp:ssnRegExp" >
</div>
</doc:source>
<doc:scenario>
it('should invalidate non ssn', function(){
var textBox = element('.doc-example-live :input');
expect(textBox.prop('className')).not().toMatch(/ng-validation-error/);
expect(textBox.val()).toEqual('123-45-6789');
input('ssn').enter('123-45-67890');
expect(textBox.prop('className')).toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
*
*/
'regexp': function(value, regexp, msg) {
if (!value.match(regexp)) {
return msg ||
"Value does not match expected format " + regexp + ".";
} else {
return null;
}
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.number
* @description
* Use number validator to restrict the input to numbers with an
* optional range. (See integer for whole numbers validator).
*
* @param {string} value value to validate
* @param {int=} [min=MIN_INT] minimum value.
* @param {int=} [max=MAX_INT] maximum value.
* @css ng-validation-error
*
* @example
<doc:example>
<doc:source>
Enter number: <input name="n1" ng:validate="number" > <br>
Enter number greater than 10: <input name="n2" ng:validate="number:10" > <br>
Enter number between 100 and 200: <input name="n3" ng:validate="number:100:200" > <br>
</doc:source>
<doc:scenario>
it('should invalidate number', function(){
var n1 = element('.doc-example-live :input[name=n1]');
expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
input('n1').enter('1.x');
expect(n1.prop('className')).toMatch(/ng-validation-error/);
var n2 = element('.doc-example-live :input[name=n2]');
expect(n2.prop('className')).not().toMatch(/ng-validation-error/);
input('n2').enter('9');
expect(n2.prop('className')).toMatch(/ng-validation-error/);
var n3 = element('.doc-example-live :input[name=n3]');
expect(n3.prop('className')).not().toMatch(/ng-validation-error/);
input('n3').enter('201');
expect(n3.prop('className')).toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
*
*/
'number': function(value, min, max) {
var num = 1 * value;
if (num == value) {
if (typeof min != $undefined && num < min) {
return "Value can not be less than " + min + ".";
}
if (typeof min != $undefined && num > max) {
return "Value can not be greater than " + max + ".";
}
return null;
} else {
return "Not a number";
}
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.integer
* @description
* Use number validator to restrict the input to integers with an
* optional range. (See integer for whole numbers validator).
*
* @param {string} value value to validate
* @param {int=} [min=MIN_INT] minimum value.
* @param {int=} [max=MAX_INT] maximum value.
* @css ng-validation-error
*
* @example
<doc:example>
<doc:source>
Enter integer: <input name="n1" ng:validate="integer" > <br>
Enter integer equal or greater than 10: <input name="n2" ng:validate="integer:10" > <br>
Enter integer between 100 and 200 (inclusive): <input name="n3" ng:validate="integer:100:200" > <br>
</doc:source>
<doc:scenario>
it('should invalidate integer', function(){
var n1 = element('.doc-example-live :input[name=n1]');
expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
input('n1').enter('1.1');
expect(n1.prop('className')).toMatch(/ng-validation-error/);
var n2 = element('.doc-example-live :input[name=n2]');
expect(n2.prop('className')).not().toMatch(/ng-validation-error/);
input('n2').enter('10.1');
expect(n2.prop('className')).toMatch(/ng-validation-error/);
var n3 = element('.doc-example-live :input[name=n3]');
expect(n3.prop('className')).not().toMatch(/ng-validation-error/);
input('n3').enter('100.1');
expect(n3.prop('className')).toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
*/
'integer': function(value, min, max) {
var numberError = angularValidator['number'](value, min, max);
if (numberError) return numberError;
if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) {
return "Not a whole number";
}
return null;
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.date
* @description
* Use date validator to restrict the user input to a valid date
* in format in format MM/DD/YYYY.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
<doc:example>
<doc:source>
Enter valid date:
<input name="text" value="1/1/2009" ng:validate="date" >
</doc:source>
<doc:scenario>
it('should invalidate date', function(){
var n1 = element('.doc-example-live :input');
expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
input('text').enter('123/123/123');
expect(n1.prop('className')).toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
*
*/
'date': function(value) {
var fields = /^(\d\d?)\/(\d\d?)\/(\d\d\d\d)$/.exec(value);
var date = fields ? new Date(fields[3], fields[1]-1, fields[2]) : 0;
return (date &&
date.getFullYear() == fields[3] &&
date.getMonth() == fields[1]-1 &&
date.getDate() == fields[2])
? null
: "Value is not a date. (Expecting format: 12/31/2009).";
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.email
* @description
* Use email validator if you wist to restrict the user input to a valid email.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
<doc:example>
<doc:source>
Enter valid email:
<input name="text" ng:validate="email" value="me@example.com">
</doc:source>
<doc:scenario>
it('should invalidate email', function(){
var n1 = element('.doc-example-live :input');
expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
input('text').enter('a@b.c');
expect(n1.prop('className')).toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
*
*/
'email': function(value) {
if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) {
return null;
}
return "Email needs to be in username@host.com format.";
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.phone
* @description
* Use phone validator to restrict the input phone numbers.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
<doc:example>
<doc:source>
Enter valid phone number:
<input name="text" value="1(234)567-8901" ng:validate="phone" >
</doc:source>
<doc:scenario>
it('should invalidate phone', function(){
var n1 = element('.doc-example-live :input');
expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
input('text').enter('+12345678');
expect(n1.prop('className')).toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
*
*/
'phone': function(value) {
if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) {
return null;
}
if (value.match(/^\+\d{2,3} (\(\d{1,5}\))?[\d ]+\d$/)) {
return null;
}
return "Phone number needs to be in 1(987)654-3210 format in North America " +
"or +999 (123) 45678 906 internationally.";
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.url
* @description
* Use phone validator to restrict the input URLs.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
<doc:example>
<doc:source>
Enter valid URL:
<input name="text" value="http://example.com/abc.html" size="40" ng:validate="url" >
</doc:source>
<doc:scenario>
it('should invalidate url', function(){
var n1 = element('.doc-example-live :input');
expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
input('text').enter('abc://server/path');
expect(n1.prop('className')).toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
*
*/
'url': function(value) {
if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) {
return null;
}
return "URL needs to be in http://server[:port]/path format.";
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.json
* @description
* Use json validator if you wish to restrict the user input to a valid JSON.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
<doc:example>
<doc:source>
<textarea name="json" cols="60" rows="5" ng:validate="json">
{name:'abc'}
</textarea>
</doc:source>
<doc:scenario>
it('should invalidate json', function(){
var n1 = element('.doc-example-live :input');
expect(n1.prop('className')).not().toMatch(/ng-validation-error/);
input('json').enter('{name}');
expect(n1.prop('className')).toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
*
*/
'json': function(value) {
try {
fromJson(value);
return null;
} catch (e) {
return e.toString();
}
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.asynchronous
* @description
* Use asynchronous validator if the validation can not be computed
* immediately, but is provided through a callback. The widget
* automatically shows a spinning indicator while the validity of
* the widget is computed. This validator caches the result.
*
* @param {string} value value to validate
* @param {function(inputToValidate,validationDone)} validate function to call to validate the state
* of the input.
* @param {function(data)=} [update=noop] function to call when state of the
* validator changes
*
* @paramDescription
* The `validate` function (specified by you) is called as
* `validate(inputToValidate, validationDone)`:
*
* * `inputToValidate`: value of the input box.
* * `validationDone`: `function(error, data){...}`
* * `error`: error text to display if validation fails
* * `data`: data object to pass to update function
*
* The `update` function is optionally specified by you and is
* called by <angular/> on input change. Since the
* asynchronous validator caches the results, the update
* function can be called without a call to `validate`
* function. The function is called as `update(data)`:
*
* * `data`: data object as passed from validate function
*
* @css ng-input-indicator-wait, ng-validation-error
*
* @example
<doc:example>
<doc:source>
<script>
function MyCntl(){
this.myValidator = function (inputToValidate, validationDone) {
setTimeout(function(){
validationDone(inputToValidate.length % 2);
}, 500);
}
}
</script>
This input is validated asynchronously:
<div ng:controller="MyCntl">
<input name="text" ng:validate="asynchronous:myValidator">
</div>
</doc:source>
<doc:scenario>
it('should change color in delayed way', function(){
var textBox = element('.doc-example-live :input');
expect(textBox.prop('className')).not().toMatch(/ng-input-indicator-wait/);
expect(textBox.prop('className')).not().toMatch(/ng-validation-error/);
input('text').enter('X');
expect(textBox.prop('className')).toMatch(/ng-input-indicator-wait/);
sleep(.6);
expect(textBox.prop('className')).not().toMatch(/ng-input-indicator-wait/);
expect(textBox.prop('className')).toMatch(/ng-validation-error/);
});
</doc:scenario>
</doc:example>
*
*/
/*
* cache is attached to the element
* cache: {
* inputs : {
* 'user input': {
* response: server response,
* error: validation error
* },
* current: 'current input'
* }
* }
*
*/
'asynchronous': function(input, asynchronousFn, updateFn) {
if (!input) return;
var scope = this;
var element = scope.$element;
var cache = element.data('$asyncValidator');
if (!cache) {
element.data('$asyncValidator', cache = {inputs:{}});
}
cache.current = input;
var inputState = cache.inputs[input],
$invalidWidgets = scope.$service('$invalidWidgets');
if (!inputState) {
cache.inputs[input] = inputState = { inFlight: true };
$invalidWidgets.markInvalid(scope.$element);
element.addClass('ng-input-indicator-wait');
asynchronousFn(input, function(error, data) {
inputState.response = data;
inputState.error = error;
inputState.inFlight = false;
if (cache.current == input) {
element.removeClass('ng-input-indicator-wait');
$invalidWidgets.markValid(element);
}
element.data($$validate)();
});
} else if (inputState.inFlight) {
// request in flight, mark widget invalid, but don't show it to user
$invalidWidgets.markInvalid(scope.$element);
} else {
(updateFn||noop)(inputState.response);
}
return inputState.error;
}
});

81
src/widget/form.js Normal file
View file

@ -0,0 +1,81 @@
'use strict';
/**
* @workInProgress
* @ngdoc widget
* @name angular.widget.form
*
* @description
* Angular widget that creates a form scope using the
* {@link angular.service.$formFactory $formFactory} API. The resulting form scope instance is
* attached to the DOM element using the jQuery `.data()` method under the `$form` key.
* See {@link guide/dev_guide.forms forms} on detailed discussion of forms and widgets.
*
*
* # Alias: `ng:form`
*
* In angular forms can be nested. This means that the outer form is valid when all of the child
* forms are valid as well. However browsers do not allow nesting of `<form>` elements, for this
* reason angular provides `<ng:form>` alias which behaves identical to `<form>` but allows
* element nesting.
*
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.text = 'guest';
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
text: <input type="text" name="input" ng:model="text" required>
<span class="error" ng:show="myForm.text.$error.REQUIRED">Required!</span>
</form>
<tt>text = {{text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should initialize to model', function(){
expect(binding('text')).toEqual('guest');
expect(binding('myForm.input.$valid')).toEqual('true');
});
it('should be invalid if empty', function(){
input('text').enter('');
expect(binding('text')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
angularWidget('form', function(form){
this.descend(true);
this.directives(true);
return annotate('$formFactory', function($formFactory, formElement) {
var name = formElement.attr('name'),
parentForm = $formFactory.forElement(formElement),
form = $formFactory(parentForm);
formElement.data('$form', form);
formElement.bind('submit', function(event){
event.preventDefault();
});
if (name) {
this[name] = form;
}
watch('valid');
watch('invalid');
function watch(name) {
form.$watch('$' + name, function(scope, value) {
formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
});
}
});
});
angularWidget('ng:form', angularWidget('form'));

773
src/widget/input.js Normal file
View file

@ -0,0 +1,773 @@
'use strict';
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
var INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;
/**
* @workInProgress
* @ngdoc inputType
* @name angular.inputType.text
*
* @description
* Standard HTML text input with angular data binding.
*
* @param {string} ng:model Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the widgets is published.
* @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
* @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
* patterns defined as scope expressions.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.text = 'guest';
this.word = /^\w*$/;
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
Single word: <input type="text" name="input" ng:model="text"
ng:pattern="word" required>
<span class="error" ng:show="myForm.input.$error.REQUIRED">
Required!</span>
<span class="error" ng:show="myForm.input.$error.PATTERN">
Single word only!</span>
</form>
<tt>text = {{text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('text')).toEqual('guest');
expect(binding('myForm.input.$valid')).toEqual('true');
});
it('should be invalid if empty', function() {
input('text').enter('');
expect(binding('text')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
it('should be invalid if multi word', function() {
input('text').enter('hello world');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
/**
* @workInProgress
* @ngdoc inputType
* @name angular.inputType.email
*
* @description
* Text input with email validation. Sets the `EMAIL` validation error key if not a valid email
* address.
*
* @param {string} ng:model Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the widgets is published.
* @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
* @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
* patterns defined as scope expressions.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.text = 'me@example.com';
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
Email: <input type="email" name="input" ng:model="text" required>
<span class="error" ng:show="myForm.input.$error.REQUIRED">
Required!</span>
<span class="error" ng:show="myForm.input.$error.EMAIL">
Not valid email!</span>
</form>
<tt>text = {{text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
<tt>myForm.$error.EMAIL = {{!!myForm.$error.EMAIL}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('text')).toEqual('me@example.com');
expect(binding('myForm.input.$valid')).toEqual('true');
});
it('should be invalid if empty', function() {
input('text').enter('');
expect(binding('text')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
it('should be invalid if not email', function() {
input('text').enter('xxx');
expect(binding('text')).toEqual('xxx');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
angularInputType('email', function() {
var widget = this;
this.$on('$validate', function(event){
var value = widget.$viewValue;
widget.$emit(!value || value.match(EMAIL_REGEXP) ? "$valid" : "$invalid", "EMAIL");
});
});
/**
* @workInProgress
* @ngdoc inputType
* @name angular.inputType.url
*
* @description
* Text input with URL validation. Sets the `URL` validation error key if the content is not a
* valid URL.
*
* @param {string} ng:model Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the widgets is published.
* @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
* @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
* patterns defined as scope expressions.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.text = 'http://google.com';
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
URL: <input type="url" name="input" ng:model="text" required>
<span class="error" ng:show="myForm.input.$error.REQUIRED">
Required!</span>
<span class="error" ng:show="myForm.input.$error.url">
Not valid url!</span>
</form>
<tt>text = {{text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
<tt>myForm.$error.url = {{!!myForm.$error.url}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('text')).toEqual('http://google.com');
expect(binding('myForm.input.$valid')).toEqual('true');
});
it('should be invalid if empty', function() {
input('text').enter('');
expect(binding('text')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
it('should be invalid if not url', function() {
input('text').enter('xxx');
expect(binding('text')).toEqual('xxx');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
angularInputType('url', function() {
var widget = this;
this.$on('$validate', function(event){
var value = widget.$viewValue;
widget.$emit(!value || value.match(URL_REGEXP) ? "$valid" : "$invalid", "URL");
});
});
/**
* @workInProgress
* @ngdoc inputType
* @name angular.inputType.list
*
* @description
* Text input that converts between comma-seperated string into an array of strings.
*
* @param {string} ng:model Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the widgets is published.
* @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
* @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
* patterns defined as scope expressions.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.names = ['igor', 'misko', 'vojta'];
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
List: <input type="list" name="input" ng:model="names" required>
<span class="error" ng:show="myForm.list.$error.REQUIRED">
Required!</span>
</form>
<tt>names = {{names}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('names')).toEqual('["igor","misko","vojta"]');
expect(binding('myForm.input.$valid')).toEqual('true');
});
it('should be invalid if empty', function() {
input('names').enter('');
expect(binding('names')).toEqual('[]');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
angularInputType('list', function() {
function parse(viewValue) {
var list = [];
forEach(viewValue.split(/\s*,\s*/), function(value){
if (value) list.push(trim(value));
});
return list;
}
this.$parseView = function() {
isString(this.$viewValue) && (this.$modelValue = parse(this.$viewValue));
};
this.$parseModel = function() {
var modelValue = this.$modelValue;
if (isArray(modelValue)
&& (!isString(this.$viewValue) || !equals(parse(this.$viewValue), modelValue))) {
this.$viewValue = modelValue.join(', ');
}
};
});
/**
* @workInProgress
* @ngdoc inputType
* @name angular.inputType.number
*
* @description
* Text input with number validation and transformation. Sets the `NUMBER` validation
* error if not a valid number.
*
* @param {string} ng:model Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the widgets is published.
* @param {string=} min Sets the `MIN` validation error key if the value entered is less then `min`.
* @param {string=} max Sets the `MAX` validation error key if the value entered is greater then `min`.
* @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
* @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
* patterns defined as scope expressions.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.value = 12;
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
Number: <input type="number" name="input" ng:model="value"
min="0" max="99" required>
<span class="error" ng:show="myForm.list.$error.REQUIRED">
Required!</span>
<span class="error" ng:show="myForm.list.$error.NUMBER">
Not valid number!</span>
</form>
<tt>value = {{value}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('value')).toEqual('12');
expect(binding('myForm.input.$valid')).toEqual('true');
});
it('should be invalid if empty', function() {
input('value').enter('');
expect(binding('value')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
it('should be invalid if over max', function() {
input('value').enter('123');
expect(binding('value')).toEqual('123');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
angularInputType('number', numericRegexpInputType(NUMBER_REGEXP, 'NUMBER'));
/**
* @workInProgress
* @ngdoc inputType
* @name angular.inputType.integer
*
* @description
* Text input with integer validation and transformation. Sets the `INTEGER`
* validation error key if not a valid integer.
*
* @param {string} ng:model Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the widgets is published.
* @param {string=} min Sets the `MIN` validation error key if the value entered is less then `min`.
* @param {string=} max Sets the `MAX` validation error key if the value entered is greater then `min`.
* @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
* @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
* patterns defined as scope expressions.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.value = 12;
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
Integer: <input type="integer" name="input" ng:model="value"
min="0" max="99" required>
<span class="error" ng:show="myForm.list.$error.REQUIRED">
Required!</span>
<span class="error" ng:show="myForm.list.$error.INTEGER">
Not valid integer!</span>
</form>
<tt>value = {{value}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('value')).toEqual('12');
expect(binding('myForm.input.$valid')).toEqual('true');
});
it('should be invalid if empty', function() {
input('value').enter('1.2');
expect(binding('value')).toEqual('12');
expect(binding('myForm.input.$valid')).toEqual('false');
});
it('should be invalid if over max', function() {
input('value').enter('123');
expect(binding('value')).toEqual('123');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));
/**
* @workInProgress
* @ngdoc inputType
* @name angular.inputType.checkbox
*
* @description
* HTML checkbox.
*
* @param {string} ng:model Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the widgets is published.
* @param {string=} true-value The value to which the expression should be set when selected.
* @param {string=} false-value The value to which the expression should be set when not selected.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.value1 = true;
this.value2 = 'YES'
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
Value1: <input type="checkbox" ng:model="value1"> <br/>
Value2: <input type="checkbox" ng:model="value2"
true-value="YES" false-value="NO"> <br/>
</form>
<tt>value1 = {{value1}}</tt><br/>
<tt>value2 = {{value2}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should change state', function() {
expect(binding('value1')).toEqual('true');
expect(binding('value2')).toEqual('YES');
input('value1').check();
input('value2').check();
expect(binding('value1')).toEqual('false');
expect(binding('value2')).toEqual('NO');
});
</doc:scenario>
</doc:example>
*/
angularInputType('checkbox', function (inputElement) {
var widget = this,
trueValue = inputElement.attr('true-value'),
falseValue = inputElement.attr('false-value');
if (!isString(trueValue)) trueValue = true;
if (!isString(falseValue)) falseValue = false;
inputElement.bind('click', function() {
widget.$apply(function() {
widget.$emit('$viewChange', inputElement[0].checked);
});
});
widget.$render = function() {
inputElement[0].checked = widget.$viewValue;
};
widget.$parseModel = function() {
widget.$viewValue = this.$modelValue === trueValue;
};
widget.$parseView = function() {
widget.$modelValue = widget.$viewValue ? trueValue : falseValue;
};
});
/**
* @workInProgress
* @ngdoc inputType
* @name angular.inputType.radio
*
* @description
* HTML radio.
*
* @param {string} ng:model Assignable angular expression to data-bind to.
* @param {string} value The value to which the expression should be set when selected.
* @param {string=} name Property name of the form under which the widgets is published.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.color = 'blue';
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
<input type="radio" ng:model="color" value="red"> Red <br/>
<input type="radio" ng:model="color" value="green"> Green <br/>
<input type="radio" ng:model="color" value="blue"> Blue <br/>
</form>
<tt>color = {{color}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should change state', function() {
expect(binding('color')).toEqual('blue');
input('color').select('red');
expect(binding('color')).toEqual('red');
});
</doc:scenario>
</doc:example>
*/
angularInputType('radio', function(inputElement) {
var widget = this,
value = inputElement.attr('value');
//correct the name
inputElement.attr('name', widget.$id + '@' + inputElement.attr('name'));
inputElement.bind('click', function() {
widget.$apply(function() {
if (inputElement[0].checked) {
widget.$emit('$viewChange', value);
}
});
});
widget.$render = function() {
inputElement[0].checked = value == widget.$viewValue;
};
if (inputElement[0].checked) {
widget.$viewValue = value;
}
});
function numericRegexpInputType(regexp, error) {
return function(inputElement) {
var widget = this,
min = 1 * (inputElement.attr('min') || Number.MIN_VALUE),
max = 1 * (inputElement.attr('max') || Number.MAX_VALUE);
widget.$on('$validate', function(event){
var value = widget.$viewValue,
filled = value && trim(value) != '',
valid = isString(value) && value.match(regexp);
widget.$emit(!filled || valid ? "$valid" : "$invalid", error);
filled && (value = 1 * value);
widget.$emit(valid && value < min ? "$invalid" : "$valid", "MIN");
widget.$emit(valid && value > max ? "$invalid" : "$valid", "MAX");
});
widget.$parseView = function() {
if (widget.$viewValue.match(regexp)) {
widget.$modelValue = 1 * widget.$viewValue;
} else if (widget.$viewValue == '') {
widget.$modelValue = null;
}
};
widget.$parseModel = function() {
if (isNumber(widget.$modelValue)) {
widget.$viewValue = '' + widget.$modelValue;
}
};
};
}
var HTML5_INPUTS_TYPES = makeMap(
"search,tel,url,email,datetime,date,month,week,time,datetime-local,number,range,color," +
"radio,checkbox,text,button,submit,reset,hidden");
/**
* @workInProgress
* @ngdoc widget
* @name angular.widget.input
*
* @description
* HTML input element widget with angular data-binding. Input widget follows HTML5 input types
* and polyfills the HTML5 validation behavior for older browsers.
*
* The {@link angular.inputType custom angular.inputType}s provides a short hand for declaring new
* inputs. This is a shart hand for text-box based inputs, and there is no need to go through the
* full {@link angular.service.$formFactory $formFactory} widget lifecycle.
*
*
* @param {string} type Widget types as defined by {@link angular.inputType}. If the
* type is in the format of `@ScopeType` then `ScopeType` is loaded from the
* current scope, allowing quick definition of type.
* @param {string} ng:model Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the widgets is published.
* @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
* @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
* patterns defined as scope expressions.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl(){
this.text = 'guest';
}
</script>
<div ng:controller="Ctrl">
<form name="myForm">
text: <input type="text" name="input" ng:model="text" required>
<span class="error" ng:show="myForm.input.$error.REQUIRED">
Required!</span>
</form>
<tt>text = {{text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
</div>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('text')).toEqual('guest');
expect(binding('myForm.input.$valid')).toEqual('true');
});
it('should be invalid if empty', function() {
input('text').enter('');
expect(binding('text')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
angularWidget('input', function (inputElement){
this.directives(true);
this.descend(true);
var modelExp = inputElement.attr('ng:model');
return modelExp &&
annotate('$defer', '$formFactory', function($defer, $formFactory, inputElement){
var form = $formFactory.forElement(inputElement),
// We have to use .getAttribute, since jQuery tries to be smart and use the
// type property. Trouble is some browser change unknown to text.
type = inputElement[0].getAttribute('type') || 'text',
TypeController,
modelScope = this,
patternMatch, widget,
pattern = trim(inputElement.attr('ng:pattern')),
loadFromScope = type.match(/^\s*\@\s*(.*)/);
if (!pattern) {
patternMatch = valueFn(true);
} else {
if (pattern.match(/^\/(.*)\/$/)) {
pattern = new RegExp(pattern.substring(1, pattern.length - 2));
patternMatch = function(value) {
return pattern.test(value);
}
} else {
patternMatch = function(value) {
var patternObj = modelScope.$eval(pattern);
if (!patternObj || !patternObj.test) {
throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj);
}
return patternObj.test(value);
}
}
}
type = lowercase(type);
TypeController = (loadFromScope
? (assertArgFn(this.$eval(loadFromScope[1]), loadFromScope[1])).$unboundFn
: angularInputType(type)) || noop;
if (!HTML5_INPUTS_TYPES[type]) {
try {
// jquery will not let you so we have to go to bare metal
inputElement[0].setAttribute('type', 'text');
} catch(e){
// also turns out that ie8 will not allow changing of types, but since it is not
// html5 anyway we can ignore the error.
}
}
!TypeController.$inject && (TypeController.$inject = []);
widget = form.$createWidget({
scope: modelScope,
model: modelExp,
onChange: inputElement.attr('ng:change'),
alias: inputElement.attr('name'),
controller: TypeController,
controllerArgs: [inputElement]});
widget.$pattern =
watchElementProperty(this, widget, 'required', inputElement);
watchElementProperty(this, widget, 'readonly', inputElement);
watchElementProperty(this, widget, 'disabled', inputElement);
widget.$pristine = !(widget.$dirty = false);
widget.$on('$validate', function(event) {
var $viewValue = trim(widget.$viewValue);
var inValid = widget.$required && !$viewValue;
var missMatch = $viewValue && !patternMatch($viewValue);
if (widget.$error.REQUIRED != inValid){
widget.$emit(inValid ? '$invalid' : '$valid', 'REQUIRED');
}
if (widget.$error.PATTERN != missMatch){
widget.$emit(missMatch ? '$invalid' : '$valid', 'PATTERN');
}
});
forEach(['valid', 'invalid', 'pristine', 'dirty'], function (name) {
widget.$watch('$' + name, function(scope, value) {
inputElement[value ? 'addClass' : 'removeClass']('ng-' + name);
}
);
});
inputElement.bind('$destroy', function() {
widget.$destroy();
});
if (type != 'checkbox' && type != 'radio') {
// TODO (misko): checkbox / radio does not really belong here, but until we can do
// widget registration with CSS, we are hacking it this way.
widget.$render = function() {
inputElement.val(widget.$viewValue || '');
};
inputElement.bind('keydown change', function(event){
var key = event.keyCode;
if (/*command*/ key != 91 &&
/*modifiers*/ !(15 < key && key < 19) &&
/*arrow*/ !(37 < key && key < 40)) {
$defer(function() {
widget.$dirty = !(widget.$pristine = false);
var value = trim(inputElement.val());
if (widget.$viewValue !== value ) {
widget.$emit('$viewChange', value);
}
});
}
});
}
});
});
angularWidget('textarea', angularWidget('input'));
function watchElementProperty(modelScope, widget, name, element) {
var bindAttr = fromJson(element.attr('ng:bind-attr') || '{}'),
match = /\s*{{(.*)}}\s*/.exec(bindAttr[name]);
widget['$' + name] =
// some browsers return true some '' when required is set without value.
isString(element.prop(name)) || !!element.prop(name) ||
// this is needed for ie9, since it will treat boolean attributes as false
!!element[0].attributes[name];
if (bindAttr[name] && match) {
modelScope.$watch(match[1], function(scope, value){
widget['$' + name] = !!value;
widget.$emit('$validate');
});
}
}

427
src/widget/select.js Normal file
View file

@ -0,0 +1,427 @@
'use strict';
/**
* @workInProgress
* @ngdoc widget
* @name angular.widget.select
*
* @description
* HTML `SELECT` element with angular data-binding.
*
* # `ng:options`
*
* Optionally `ng:options` attribute can be used to dynamically generate a list of `<option>`
* elements for a `<select>` element using an array or an object obtained by evaluating the
* `ng:options` expression.
*
* When an item in the select menu is select, the value of array element or object property
* represented by the selected option will be bound to the model identified by the `name` attribute
* of the parent select element.
*
* Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
* be nested into the `<select>` element. This element will then represent `null` or "not selected"
* option. See example below for demonstration.
*
* Note: `ng:options` provides iterator facility for `<option>` element which must be used instead
* of {@link angular.widget.@ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with
* `<option>` element because of the following reasons:
*
* * value attribute of the option element that we need to bind to requires a string, but the
* source of data for the iteration might be in a form of array containing objects instead of
* strings
* * {@link angular.widget.@ng:repeat ng:repeat} unrolls after the select binds causing
* incorect rendering on most browsers.
* * binding to a value not in list confuses most browsers.
*
* @param {string} name assignable expression to data-bind to.
* @param {string=} required The widget is considered valid only if value is entered.
* @param {comprehension_expression=} ng:options in one of the following forms:
*
* * for array data sources:
* * `label` **`for`** `value` **`in`** `array`
* * `select` **`as`** `label` **`for`** `value` **`in`** `array`
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
* * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
* * for object data sources:
* * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
* * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
* * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
* * `select` **`as`** `label` **`group by`** `group`
* **`for` `(`**`key`**`,`** `value`**`) in`** `object`
*
* Where:
*
* * `array` / `object`: an expression which evaluates to an array / object to iterate over.
* * `value`: local variable which will refer to each item in the `array` or each property value
* of `object` during iteration.
* * `key`: local variable which will refer to a property name in `object` during iteration.
* * `label`: The result of this expression will be the label for `<option>` element. The
* `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
* * `select`: The result of this expression will be bound to the model of the parent `<select>`
* element. If not specified, `select` expression will default to `value`.
* * `group`: The result of this expression will be used to group options using the `<optgroup>`
* DOM element.
*
* @example
<doc:example>
<doc:source>
<script>
function MyCntrl(){
this.colors = [
{name:'black', shade:'dark'},
{name:'white', shade:'light'},
{name:'red', shade:'dark'},
{name:'blue', shade:'dark'},
{name:'yellow', shade:'light'}
];
this.color = this.colors[2]; // red
}
</script>
<div ng:controller="MyCntrl">
<ul>
<li ng:repeat="color in colors">
Name: <input ng:model="color.name">
[<a href ng:click="colors.$remove(color)">X</a>]
</li>
<li>
[<a href ng:click="colors.push({})">add</a>]
</li>
</ul>
<hr/>
Color (null not allowed):
<select ng:model="color" ng:options="c.name for c in colors"></select><br>
Color (null allowed):
<div class="nullable">
<select ng:model="color" ng:options="c.name for c in colors">
<option value="">-- chose color --</option>
</select>
</div><br/>
Color grouped by shade:
<select ng:model="color" ng:options="c.name group by c.shade for c in colors">
</select><br/>
Select <a href ng:click="color={name:'not in list'}">bogus</a>.<br>
<hr/>
Currently selected: {{ {selected_color:color} }}
<div style="border:solid 1px black; height:20px"
ng:style="{'background-color':color.name}">
</div>
</div>
</doc:source>
<doc:scenario>
it('should check ng:options', function(){
expect(binding('color')).toMatch('red');
select('color').option('0');
expect(binding('color')).toMatch('black');
using('.nullable').select('color').option('');
expect(binding('color')).toMatch('null');
});
</doc:scenario>
</doc:example>
*/
//00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;
angularWidget('select', function (element){
this.directives(true);
this.descend(true);
return element.attr('ng:model') && annotate('$formFactory', function($formFactory, selectElement){
var modelScope = this,
match,
form = $formFactory.forElement(selectElement),
multiple = selectElement.attr('multiple'),
optionsExp = selectElement.attr('ng:options'),
modelExp = selectElement.attr('ng:model'),
widget = form.$createWidget({
scope: this,
model: modelExp,
onChange: selectElement.attr('ng:change'),
alias: selectElement.attr('name'),
controller: optionsExp ? Options : (multiple ? Multiple : Single)});
selectElement.bind('$destroy', function(){ widget.$destroy(); });
widget.$pristine = !(widget.$dirty = false);
watchElementProperty(modelScope, widget, 'required', selectElement);
watchElementProperty(modelScope, widget, 'readonly', selectElement);
watchElementProperty(modelScope, widget, 'disabled', selectElement);
widget.$on('$validate', function(){
var valid = !widget.$required || !!widget.$modelValue;
if (valid && multiple && widget.$required) valid = !!widget.$modelValue.length;
if (valid !== !widget.$error.REQUIRED) {
widget.$emit(valid ? '$valid' : '$invalid', 'REQUIRED');
}
});
widget.$on('$viewChange', function(){
widget.$pristine = !(widget.$dirty = true);
});
forEach(['valid', 'invalid', 'pristine', 'dirty'], function (name) {
widget.$watch('$' + name, function(scope, value) {
selectElement[value ? 'addClass' : 'removeClass']('ng-' + name);
});
});
////////////////////////////
function Multiple(){
var widget = this;
this.$render = function(){
var items = new HashMap(this.$viewValue);
forEach(selectElement.children(), function(option){
option.selected = isDefined(items.get(option.value));
});
};
selectElement.bind('change', function (){
widget.$apply(function(){
var array = [];
forEach(selectElement.children(), function(option){
if (option.selected) {
array.push(option.value);
}
});
widget.$emit('$viewChange', array);
});
});
}
function Single(){
var widget = this;
widget.$render = function(){
selectElement.val(widget.$viewValue);
};
selectElement.bind('change', function(){
widget.$apply(function(){
widget.$emit('$viewChange', selectElement.val());
});
});
widget.$viewValue = selectElement.val();
}
function Options(){
var widget = this,
match;
if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
throw Error(
"Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
" but got '" + optionsExp + "'.");
}
var widgetScope = this,
displayFn = expressionCompile(match[2] || match[1]),
valueName = match[4] || match[6],
keyName = match[5],
groupByFn = expressionCompile(match[3] || ''),
valueFn = expressionCompile(match[2] ? match[1] : valueName),
valuesFn = expressionCompile(match[7]),
// we can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
optionTemplate = jqLite(document.createElement('option')),
optGroupTemplate = jqLite(document.createElement('optgroup')),
nullOption = false, // if false then user will not be able to select it
// This is an array of array of existing option groups in DOM. We try to reuse these if possible
// optionGroupsCache[0] is the options with no option group
// optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
optionGroupsCache = [[{element: selectElement, label:''}]],
inChangeEvent;
// find existing special options
forEach(selectElement.children(), function(option){
if (option.value == '')
// User is allowed to select the null.
nullOption = {label:jqLite(option).text(), id:''};
});
selectElement.html(''); // clear contents
selectElement.bind('change', function(){
widgetScope.$apply(function(){
var optionGroup,
collection = valuesFn(modelScope) || [],
key = selectElement.val(),
tempScope = inherit(modelScope),
value, optionElement, index, groupIndex, length, groupLength;
if (multiple) {
value = [];
for (groupIndex = 0, groupLength = optionGroupsCache.length;
groupIndex < groupLength;
groupIndex++) {
// list of options for that group. (first item has the parent)
optionGroup = optionGroupsCache[groupIndex];
for(index = 1, length = optionGroup.length; index < length; index++) {
if ((optionElement = optionGroup[index].element)[0].selected) {
if (keyName) tempScope[keyName] = key;
tempScope[valueName] = collection[optionElement.val()];
value.push(valueFn(tempScope));
}
}
}
} else {
if (key == '?') {
value = undefined;
} else if (key == ''){
value = null;
} else {
tempScope[valueName] = collection[key];
if (keyName) tempScope[keyName] = key;
value = valueFn(tempScope);
}
}
if (isDefined(value) && modelScope.$viewVal !== value) {
widgetScope.$emit('$viewChange', value);
}
});
});
widgetScope.$watch(render);
widgetScope.$render = render;
function render() {
var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
optionGroupNames = [''],
optionGroupName,
optionGroup,
option,
existingParent, existingOptions, existingOption,
modelValue = widget.$modelValue,
values = valuesFn(modelScope) || [],
keys = keyName ? sortedKeys(values) : values,
groupLength, length,
groupIndex, index,
optionScope = inherit(modelScope),
selected,
selectedSet = false, // nothing is selected yet
lastElement,
element;
if (multiple) {
selectedSet = new HashMap(modelValue);
} else if (modelValue === null || nullOption) {
// if we are not multiselect, and we are null then we have to add the nullOption
optionGroups[''].push(extend({selected:modelValue === null, id:'', label:''}, nullOption));
selectedSet = true;
}
// We now build up the list of options we need (we merge later)
for (index = 0; length = keys.length, index < length; index++) {
optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index];
optionGroupName = groupByFn(optionScope) || '';
if (!(optionGroup = optionGroups[optionGroupName])) {
optionGroup = optionGroups[optionGroupName] = [];
optionGroupNames.push(optionGroupName);
}
if (multiple) {
selected = selectedSet.remove(valueFn(optionScope)) != undefined;
} else {
selected = modelValue === valueFn(optionScope);
selectedSet = selectedSet || selected; // see if at least one item is selected
}
optionGroup.push({
id: keyName ? keys[index] : index, // either the index into array or key from object
label: displayFn(optionScope) || '', // what will be seen by the user
selected: selected // determine if we should be selected
});
}
if (!multiple && !selectedSet) {
// nothing was selected, we have to insert the undefined item
optionGroups[''].unshift({id:'?', label:'', selected:true});
}
// Now we need to update the list of DOM nodes to match the optionGroups we computed above
for (groupIndex = 0, groupLength = optionGroupNames.length;
groupIndex < groupLength;
groupIndex++) {
// current option group name or '' if no group
optionGroupName = optionGroupNames[groupIndex];
// list of options for that group. (first item has the parent)
optionGroup = optionGroups[optionGroupName];
if (optionGroupsCache.length <= groupIndex) {
// we need to grow the optionGroups
optionGroupsCache.push(
existingOptions = [existingParent = {
element: optGroupTemplate.clone().attr('label', optionGroupName),
label: optionGroup.label
}]
);
selectElement.append(existingParent.element);
} else {
existingOptions = optionGroupsCache[groupIndex];
existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
// update the OPTGROUP label if not the same.
if (existingParent.label != optionGroupName) {
existingParent.element.attr('label', existingParent.label = optionGroupName);
}
}
lastElement = null; // start at the begining
for(index = 0, length = optionGroup.length; index < length; index++) {
option = optionGroup[index];
if ((existingOption = existingOptions[index+1])) {
// reuse elements
lastElement = existingOption.element;
if (existingOption.label !== option.label) {
lastElement.text(existingOption.label = option.label);
}
if (existingOption.id !== option.id) {
lastElement.val(existingOption.id = option.id);
}
if (existingOption.selected !== option.selected) {
lastElement.prop('selected', (existingOption.selected = option.selected));
}
} else {
// grow elements
// jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
// in this version of jQuery on some browser the .text() returns a string
// rather then the element.
(element = optionTemplate.clone())
.val(option.id)
.attr('selected', option.selected)
.text(option.label);
existingOptions.push(existingOption = {
element: element,
label: option.label,
id: option.id,
selected: option.selected
});
if (lastElement) {
lastElement.after(element);
} else {
existingParent.element.append(element);
}
lastElement = element;
}
}
// remove any excessive OPTIONs in a group
index++; // increment since the existingOptions[0] is parent element not OPTION
while(existingOptions.length > index) {
existingOptions.pop().element.remove();
}
}
// remove any excessive OPTGROUPs from select
while(optionGroupsCache.length > groupIndex) {
optionGroupsCache.pop()[0].element.remove();
}
};
}
});
});

File diff suppressed because it is too large Load diff

View file

@ -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/'});
});
});

View file

@ -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);
});
});

View file

@ -28,56 +28,12 @@ describe('Binder', function(){
}
});
it('text-field should default to value attribute', function(){
var scope = this.compile('<input type="text" name="model.price" value="abc">');
scope.$apply();
assertEquals('abc', scope.model.price);
});
it('ChangingTextareaUpdatesModel', function(){
var scope = this.compile('<textarea name="model.note">abc</textarea>');
scope.$apply();
assertEquals(scope.model.note, 'abc');
});
it('ChangingRadioUpdatesModel', function(){
var scope = this.compile('<div><input type="radio" name="model.price" value="A" checked>' +
'<input type="radio" name="model.price" value="B"></div>');
scope.$apply();
assertEquals(scope.model.price, 'A');
});
it('ChangingCheckboxUpdatesModel', function(){
var scope = this.compile('<input type="checkbox" name="model.price" value="true" checked ng:format="boolean"/>');
assertEquals(true, scope.model.price);
});
it('BindUpdate', function(){
var scope = this.compile('<div ng:init="a=123"/>');
scope.$digest();
assertEquals(123, scope.a);
});
it('ChangingSelectNonSelectedUpdatesModel', function(){
var scope = this.compile('<select name="model.price"><option value="A">A</option><option value="B">B</option></select>');
assertEquals('A', scope.model.price);
});
it('ChangingMultiselectUpdatesModel', function(){
var scope = this.compile('<select name="Invoice.options" multiple="multiple">' +
'<option value="A" selected>Gift wrap</option>' +
'<option value="B" selected>Extra padding</option>' +
'<option value="C">Expedite</option>' +
'</select>');
assertJsonEquals(["A", "B"], scope.Invoice.options);
});
it('ChangingSelectSelectedUpdatesModel', function(){
var scope = this.compile('<select name="model.price"><option>A</option><option selected value="b">B</option></select>');
assertEquals(scope.model.price, 'b');
});
it('ExecuteInitialization', function(){
var scope = this.compile('<div ng:init="a=123">');
assertEquals(scope.a, 123);
@ -236,14 +192,13 @@ describe('Binder', function(){
});
it('RepeaterAdd', function(){
var scope = this.compile('<div><input type="text" name="item.x" ng:repeat="item in items"></div>');
var scope = this.compile('<div><input type="text" ng:model="item.x" ng:repeat="item in items"></div>');
scope.items = [{x:'a'}, {x:'b'}];
scope.$apply();
var first = childNode(scope.$element, 1);
var second = childNode(scope.$element, 2);
expect(first.val()).toEqual('a');
expect(second.val()).toEqual('b');
return
first.val('ABC');
browserTrigger(first, 'keydown');
@ -440,15 +395,6 @@ describe('Binder', function(){
assertEquals('123{{a}}{{b}}{{c}}', scope.$element.text());
});
it('RepeaterShouldBindInputsDefaults', function () {
var scope = this.compile('<div><input value="123" name="item.name" ng:repeat="item in items"></div>');
scope.items = [{}, {name:'misko'}];
scope.$apply();
expect(scope.$eval('items[0].name')).toEqual("123");
expect(scope.$eval('items[1].name')).toEqual("misko");
});
it('ShouldTemplateBindPreElements', function () {
var scope = this.compile('<pre>Hello {{name}}!</pre>');
scope.name = "World";
@ -459,7 +405,11 @@ describe('Binder', function(){
it('FillInOptionValueWhenMissing', function(){
var scope = this.compile(
'<select name="foo"><option selected="true">{{a}}</option><option value="">{{b}}</option><option>C</option></select>');
'<select ng:model="foo">' +
'<option selected="true">{{a}}</option>' +
'<option value="">{{b}}</option>' +
'<option>C</option>' +
'</select>');
scope.a = 'A';
scope.b = 'B';
scope.$apply();
@ -477,52 +427,14 @@ describe('Binder', function(){
expect(optionC.text()).toEqual('C');
});
it('ValidateForm', function(){
var scope = this.compile('<div id="test"><input name="name" ng:required>' +
'<input ng:repeat="item in items" name="item.name" ng:required/></div>',
jqLite(document.body));
var items = [{}, {}];
scope.items = items;
scope.$apply();
assertEquals(3, scope.$service('$invalidWidgets').length);
scope.name = '';
scope.$apply();
assertEquals(3, scope.$service('$invalidWidgets').length);
scope.name = ' ';
scope.$apply();
assertEquals(3, scope.$service('$invalidWidgets').length);
scope.name = 'abc';
scope.$apply();
assertEquals(2, scope.$service('$invalidWidgets').length);
items[0].name = 'abc';
scope.$apply();
assertEquals(1, scope.$service('$invalidWidgets').length);
items[1].name = 'abc';
scope.$apply();
assertEquals(0, scope.$service('$invalidWidgets').length);
});
it('ValidateOnlyVisibleItems', function(){
var scope = this.compile('<div><input name="name" ng:required><input ng:show="show" name="name" ng:required></div>', jqLite(document.body));
scope.show = true;
scope.$apply();
assertEquals(2, scope.$service('$invalidWidgets').length);
scope.show = false;
scope.$apply();
assertEquals(1, scope.$service('$invalidWidgets').visible());
});
it('DeleteAttributeIfEvaluatesFalse', function(){
var scope = this.compile('<div>' +
'<input name="a0" ng:bind-attr="{disabled:\'{{true}}\'}"><input name="a1" ng:bind-attr="{disabled:\'{{false}}\'}">' +
'<input name="b0" ng:bind-attr="{disabled:\'{{1}}\'}"><input name="b1" ng:bind-attr="{disabled:\'{{0}}\'}">' +
'<input name="c0" ng:bind-attr="{disabled:\'{{[0]}}\'}"><input name="c1" ng:bind-attr="{disabled:\'{{[]}}\'}"></div>');
'<input ng:model="a0" ng:bind-attr="{disabled:\'{{true}}\'}">' +
'<input ng:model="a1" ng:bind-attr="{disabled:\'{{false}}\'}">' +
'<input ng:model="b0" ng:bind-attr="{disabled:\'{{1}}\'}">' +
'<input ng:model="b1" ng:bind-attr="{disabled:\'{{0}}\'}">' +
'<input ng:model="c0" ng:bind-attr="{disabled:\'{{[0]}}\'}">' +
'<input ng:model="c1" ng:bind-attr="{disabled:\'{{[]}}\'}"></div>');
scope.$apply();
function assertChild(index, disabled) {
var child = childNode(scope.$element, index);
@ -556,8 +468,8 @@ describe('Binder', function(){
it('ItShouldSelectTheCorrectRadioBox', function(){
var scope = this.compile('<div>' +
'<input type="radio" name="sex" value="female"/>' +
'<input type="radio" name="sex" value="male"/></div>');
'<input type="radio" ng:model="sex" value="female">' +
'<input type="radio" ng:model="sex" value="male"></div>');
var female = jqLite(scope.$element[0].childNodes[0]);
var male = jqLite(scope.$element[0].childNodes[1]);
@ -603,23 +515,4 @@ describe('Binder', function(){
assertEquals("3", scope.$element.text());
});
it('ItBindHiddenInputFields', function(){
var scope = this.compile('<input type="hidden" name="myName" value="abc" />');
scope.$apply();
assertEquals("abc", scope.myName);
});
it('ItShouldUseFormaterForText', function(){
var scope = this.compile('<input name="a" ng:format="list" value="a,b">');
scope.$apply();
assertEquals(['a','b'], scope.a);
var input = scope.$element;
input[0].value = ' x,,yz';
browserTrigger(input, 'change');
assertEquals(['x','yz'], scope.a);
scope.a = [1 ,2, 3];
scope.$apply();
assertEquals('1, 2, 3', input[0].value);
});
});

View file

@ -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]);

View file

@ -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);
});
});
});

View file

@ -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\"");
});

View file

@ -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});
});
});
});

View file

@ -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>');

View file

@ -1,172 +0,0 @@
'use strict';
describe('Validator', function(){
it('ShouldHaveThisSet', function() {
var validator = {};
angular.validator.myValidator = function(first, last){
validator.first = first;
validator.last = last;
validator._this = this;
};
var scope = compile('<input name="name" ng:validate="myValidator:\'hevery\'"/>')();
scope.name = 'misko';
scope.$digest();
assertEquals('misko', validator.first);
assertEquals('hevery', validator.last);
expect(validator._this.$id).toEqual(scope.$id);
delete angular.validator.myValidator;
scope.$element.remove();
});
it('Regexp', function() {
assertEquals(angular.validator.regexp("abc", /x/, "E1"), "E1");
assertEquals(angular.validator.regexp("abc", '/x/'),
"Value does not match expected format /x/.");
assertEquals(angular.validator.regexp("ab", '^ab$'), null);
assertEquals(angular.validator.regexp("ab", '^axb$', "E3"), "E3");
});
it('Number', function() {
assertEquals(angular.validator.number("ab"), "Not a number");
assertEquals(angular.validator.number("-0.1",0), "Value can not be less than 0.");
assertEquals(angular.validator.number("10.1",0,10), "Value can not be greater than 10.");
assertEquals(angular.validator.number("1.2"), null);
assertEquals(angular.validator.number(" 1 ", 1, 1), null);
});
it('Integer', function() {
assertEquals(angular.validator.integer("ab"), "Not a number");
assertEquals(angular.validator.integer("1.1"), "Not a whole number");
assertEquals(angular.validator.integer("1.0"), "Not a whole number");
assertEquals(angular.validator.integer("1."), "Not a whole number");
assertEquals(angular.validator.integer("-1",0), "Value can not be less than 0.");
assertEquals(angular.validator.integer("11",0,10), "Value can not be greater than 10.");
assertEquals(angular.validator.integer("1"), null);
assertEquals(angular.validator.integer(" 1 ", 1, 1), null);
});
it('Date', function() {
var error = "Value is not a date. (Expecting format: 12/31/2009).";
expect(angular.validator.date("ab")).toEqual(error);
expect(angular.validator.date("12/31/2009")).toEqual(null);
expect(angular.validator.date("1/1/1000")).toEqual(null);
expect(angular.validator.date("12/31/9999")).toEqual(null);
expect(angular.validator.date("2/29/2004")).toEqual(null);
expect(angular.validator.date("2/29/2000")).toEqual(null);
expect(angular.validator.date("2/29/2100")).toEqual(error);
expect(angular.validator.date("2/29/2003")).toEqual(error);
expect(angular.validator.date("41/1/2009")).toEqual(error);
expect(angular.validator.date("13/1/2009")).toEqual(error);
expect(angular.validator.date("1/1/209")).toEqual(error);
expect(angular.validator.date("1/32/2010")).toEqual(error);
expect(angular.validator.date("001/031/2009")).toEqual(error);
});
it('Phone', function() {
var error = "Phone number needs to be in 1(987)654-3210 format in North America " +
"or +999 (123) 45678 906 internationally.";
assertEquals(angular.validator.phone("ab"), error);
assertEquals(null, angular.validator.phone("1(408)757-3023"));
assertEquals(null, angular.validator.phone("+421 (0905) 933 297"));
assertEquals(null, angular.validator.phone("+421 0905 933 297"));
});
it('URL', function() {
var error = "URL needs to be in http://server[:port]/path format.";
assertEquals(angular.validator.url("ab"), error);
assertEquals(angular.validator.url("http://server:123/path"), null);
});
it('Email', function() {
var error = "Email needs to be in username@host.com format.";
assertEquals(error, angular.validator.email("ab"));
assertEquals(null, angular.validator.email("misko@hevery.com"));
});
it('Json', function() {
assertNotNull(angular.validator.json("'"));
assertNotNull(angular.validator.json("''X"));
assertNull(angular.validator.json("{}"));
});
describe('asynchronous', function(){
var asynchronous = angular.validator.asynchronous;
var self;
var value, fn;
beforeEach(function(){
value = null;
fn = null;
self = angular.compile('<input />')();
jqLite(document.body).append(self.$element);
self.$element.data('$validate', noop);
self.$root = self;
});
afterEach(function(){
if (self.$element) self.$element.remove();
});
it('should make a request and show spinner', function(){
var value, fn;
var scope = angular.compile(
'<input type="text" name="name" ng:validate="asynchronous:asyncFn"/>')();
jqLite(document.body).append(scope.$element);
var input = scope.$element;
scope.asyncFn = function(v,f){
value=v; fn=f;
};
scope.name = "misko";
scope.$digest();
expect(value).toEqual('misko');
expect(input.hasClass('ng-input-indicator-wait')).toBeTruthy();
fn("myError");
expect(input.hasClass('ng-input-indicator-wait')).toBeFalsy();
expect(input.attr(NG_VALIDATION_ERROR)).toEqual("myError");
scope.$element.remove();
});
it("should not make second request to same value", function(){
asynchronous.call(self, "kai", function(v,f){value=v; fn=f;});
expect(value).toEqual('kai');
expect(self.$service('$invalidWidgets')[0]).toEqual(self.$element);
var spy = jasmine.createSpy();
asynchronous.call(self, "kai", spy);
expect(spy).not.toHaveBeenCalled();
asynchronous.call(self, "misko", spy);
expect(spy).toHaveBeenCalled();
});
it("should ignore old callbacks, and not remove spinner", function(){
var firstCb, secondCb;
asynchronous.call(self, "first", function(v,f){value=v; firstCb=f;});
asynchronous.call(self, "second", function(v,f){value=v; secondCb=f;});
firstCb();
expect(self.$element.hasClass('ng-input-indicator-wait')).toBeTruthy();
secondCb();
expect(self.$element.hasClass('ng-input-indicator-wait')).toBeFalsy();
});
it("should handle update function", function(){
var scope = angular.compile(
'<input name="name" ng:validate="asynchronous:asyncFn:updateFn"/>')();
scope.asyncFn = jasmine.createSpy();
scope.updateFn = jasmine.createSpy();
scope.name = 'misko';
scope.$digest();
expect(scope.asyncFn).toHaveBeenCalledWith('misko', scope.asyncFn.mostRecentCall.args[1]);
assertTrue(scope.$element.hasClass('ng-input-indicator-wait'));
scope.asyncFn.mostRecentCall.args[1]('myError', {id: 1234, data:'data'});
assertFalse(scope.$element.hasClass('ng-input-indicator-wait'));
assertEquals('myError', scope.$element.attr('ng-validation-error'));
expect(scope.updateFn.mostRecentCall.args[0]).toEqual({id: 1234, data:'data'});
scope.$element.remove();
});
});
});

View file

@ -80,6 +80,11 @@ describe("directive", function() {
expect(scope.$element.text()).toEqual('-0false');
});
it('should render object as JSON ignore $$', function(){
var scope = compile('<div>{{ {key:"value", $$key:"hide"} }}</div>');
scope.$digest();
expect(fromJson(scope.$element.text())).toEqual({key:'value'});
});
});
describe('ng:bind-template', function() {
@ -103,6 +108,12 @@ describe("directive", function() {
expect(innerText).toEqual('INNER');
});
it('should render object as JSON ignore $$', function(){
var scope = compile('<pre>{{ {key:"value", $$key:"hide"} }}</pre>');
scope.$digest();
expect(fromJson(scope.$element.text())).toEqual({key:'value'});
});
});
describe('ng:bind-attr', function() {

57
test/jQueryPatchSpec.js Normal file
View file

@ -0,0 +1,57 @@
'use strict';
if (window.jQuery) {
describe('jQuery patch', function(){
var doc = null;
var divSpy = null;
var spy1 = null;
var spy2 = null;
beforeEach(function(){
divSpy = jasmine.createSpy('div.$destroy');
spy1 = jasmine.createSpy('span1.$destroy');
spy2 = jasmine.createSpy('span2.$destroy');
doc = $('<div><span class=first>abc</span><span class=second>xyz</span></div>');
doc.find('span.first').bind('$destroy', spy1);
doc.find('span.second').bind('$destroy', spy2);
});
afterEach(function(){
expect(divSpy).not.toHaveBeenCalled();
expect(spy1).toHaveBeenCalled();
expect(spy1.callCount).toEqual(1);
expect(spy2).toHaveBeenCalled();
expect(spy2.callCount).toEqual(1);
});
describe('$detach event', function(){
it('should fire on detach()', function(){
doc.find('span').detach();
});
it('should fire on remove()', function(){
doc.find('span').remove();
});
it('should fire on replaceWith()', function(){
doc.find('span').replaceWith('<b>bla</b>');
});
it('should fire on replaceAll()', function(){
$('<b>bla</b>').replaceAll(doc.find('span'));
});
it('should fire on empty()', function(){
doc.empty();
});
it('should fire on html()', function(){
doc.html('abc');
});
});
});
}

View file

@ -110,6 +110,7 @@ describe('jqLite', function(){
});
});
describe('scope', function() {
it('should retrieve scope attached to the current element', function() {
var element = jqLite('<i>foo</i>');
@ -138,7 +139,7 @@ describe('jqLite', function(){
describe('data', function(){
it('should set and get ande remove data', function(){
it('should set and get and remove data', function(){
var selected = jqLite([a, b, c]);
expect(selected.data('prop', 'value')).toEqual(selected);
@ -158,6 +159,14 @@ describe('jqLite', function(){
expect(jqLite(b).data('prop')).toEqual(undefined);
expect(jqLite(c).data('prop')).toEqual(undefined);
});
it('should call $destroy function if element removed', function(){
var log = '';
var element = jqLite(a);
element.bind('$destroy', function(){log+= 'destroy;';});
element.remove();
expect(log).toEqual('destroy;');
});
});
@ -242,6 +251,21 @@ describe('jqLite', function(){
var selector = jqLite([a, b]);
expect(selector.hasClass('abc')).toEqual(false);
});
it('should make sure that partial class is not checked as a subset', function(){
var selector = jqLite([a, b]);
selector.addClass('a');
selector.addClass('b');
selector.addClass('c');
expect(selector.addClass('abc')).toEqual(selector);
expect(selector.removeClass('abc')).toEqual(selector);
expect(jqLite(a).hasClass('abc')).toEqual(false);
expect(jqLite(b).hasClass('abc')).toEqual(false);
expect(jqLite(a).hasClass('a')).toEqual(true);
expect(jqLite(a).hasClass('b')).toEqual(true);
expect(jqLite(a).hasClass('c')).toEqual(true);
});
});
@ -318,16 +342,10 @@ describe('jqLite', function(){
describe('removeClass', function(){
it('should allow removal of class', function(){
var selector = jqLite([a, b]);
selector.addClass('a');
selector.addClass('b');
selector.addClass('c');
expect(selector.addClass('abc')).toEqual(selector);
expect(selector.removeClass('abc')).toEqual(selector);
expect(jqLite(a).hasClass('abc')).toEqual(false);
expect(jqLite(b).hasClass('abc')).toEqual(false);
expect(jqLite(a).hasClass('a')).toEqual(true);
expect(jqLite(a).hasClass('b')).toEqual(true);
expect(jqLite(a).hasClass('c')).toEqual(true);
});

View file

@ -26,12 +26,18 @@ describe("markups", function(){
});
it('should translate {{}} in terminal nodes', function(){
compile('<select name="x"><option value="">Greet {{name}}!</option></select>');
compile('<select ng:model="x"><option value="">Greet {{name}}!</option></select>');
scope.$digest();
expect(sortedHtml(element).replace(' selected="true"', '')).toEqual('<select name="x"><option ng:bind-template="Greet {{name}}!">Greet !</option></select>');
expect(sortedHtml(element).replace(' selected="true"', '')).
toEqual('<select ng:model="x">' +
'<option ng:bind-template="Greet {{name}}!">Greet !</option>' +
'</select>');
scope.name = 'Misko';
scope.$digest();
expect(sortedHtml(element).replace(' selected="true"', '')).toEqual('<select name="x"><option ng:bind-template="Greet {{name}}!">Greet Misko!</option></select>');
expect(sortedHtml(element).replace(' selected="true"', '')).
toEqual('<select ng:model="x">' +
'<option ng:bind-template="Greet {{name}}!">Greet Misko!</option>' +
'</select>');
});
it('should translate {{}} in attributes', function(){
@ -69,24 +75,24 @@ describe("markups", function(){
it('should populate value attribute on OPTION', function(){
compile('<select name="x"><option>abc</option></select>');
compile('<select ng:model="x"><option>abc</option></select>');
expect(element).toHaveValue('abc');
});
it('should ignore value if already exists', function(){
compile('<select name="x"><option value="abc">xyz</option></select>');
compile('<select ng:model="x"><option value="abc">xyz</option></select>');
expect(element).toHaveValue('abc');
});
it('should set value even if newlines present', function(){
compile('<select name="x"><option attr="\ntext\n" \n>\nabc\n</option></select>');
compile('<select ng:model="x"><option attr="\ntext\n" \n>\nabc\n</option></select>');
expect(element).toHaveValue('\nabc\n');
});
it('should set value even if self closing HTML', function(){
// IE removes the \n from option, which makes this test pointless
if (msie) return;
compile('<select name="x"><option>\n</option></select>');
compile('<select ng:model="x"><option>\n</option></select>');
expect(element).toHaveValue('\n');
});

View file

@ -203,29 +203,40 @@ describe("angular.scenario.dsl", function() {
describe('Select', function() {
it('should select single option', function() {
doc.append(
'<select name="test">' +
' <option>A</option>' +
' <option selected>B</option>' +
'<select ng:model="test">' +
' <option value=A>one</option>' +
' <option value=B selected>two</option>' +
'</select>'
);
$root.dsl.select('test').option('A');
expect(_jQuery('[name="test"]').val()).toEqual('A');
expect(_jQuery('[ng\\:model="test"]').val()).toEqual('A');
});
it('should select option by name', function(){
doc.append(
'<select ng:model="test">' +
' <option value=A>one</option>' +
' <option value=B selected>two</option>' +
'</select>'
);
$root.dsl.select('test').option('one');
expect(_jQuery('[ng\\:model="test"]').val()).toEqual('A');
});
it('should select multiple options', function() {
doc.append(
'<select name="test" multiple>' +
'<select ng:model="test" multiple>' +
' <option>A</option>' +
' <option selected>B</option>' +
' <option>C</option>' +
'</select>'
);
$root.dsl.select('test').options('A', 'B');
expect(_jQuery('[name="test"]').val()).toEqual(['A','B']);
expect(_jQuery('[ng\\:model="test"]').val()).toEqual(['A','B']);
});
it('should fail to select multiple options on non-multiple select', function() {
doc.append('<select name="test"></select>');
doc.append('<select ng:model="test"></select>');
$root.dsl.select('test').options('A', 'B');
expect($root.futureError).toMatch(/did not match/);
});
@ -477,12 +488,12 @@ describe("angular.scenario.dsl", function() {
it('should prefix selector in $document.elements()', function() {
var chain;
doc.append(
'<div id="test1"><input name="test.input" value="something"></div>' +
'<div id="test2"><input name="test.input" value="something"></div>'
'<div id="test1"><input ng:model="test.input" value="something"></div>' +
'<div id="test2"><input ng:model="test.input" value="something"></div>'
);
chain = $root.dsl.using('div#test2');
chain.input('test.input').enter('foo');
var inputs = _jQuery('input[name="test.input"]');
var inputs = _jQuery('input[ng\\:model="test.input"]');
expect(inputs.first().val()).toEqual('something');
expect(inputs.last().val()).toEqual('foo');
});
@ -501,10 +512,10 @@ describe("angular.scenario.dsl", function() {
describe('Input', function() {
it('should change value in text input', function() {
doc.append('<input name="test.input" value="something">');
doc.append('<input ng:model="test.input" value="something">');
var chain = $root.dsl.input('test.input');
chain.enter('foo');
expect(_jQuery('input[name="test.input"]').val()).toEqual('foo');
expect(_jQuery('input[ng\\:model="test.input"]').val()).toEqual('foo');
});
it('should return error if no input exists', function() {
@ -514,16 +525,16 @@ describe("angular.scenario.dsl", function() {
});
it('should toggle checkbox state', function() {
doc.append('<input type="checkbox" name="test.input" checked>');
expect(_jQuery('input[name="test.input"]').
doc.append('<input type="checkbox" ng:model="test.input" checked>');
expect(_jQuery('input[ng\\:model="test.input"]').
prop('checked')).toBe(true);
var chain = $root.dsl.input('test.input');
chain.check();
expect(_jQuery('input[name="test.input"]').
expect(_jQuery('input[ng\\:model="test.input"]').
prop('checked')).toBe(false);
$window.angular.reset();
chain.check();
expect(_jQuery('input[name="test.input"]').
expect(_jQuery('input[ng\\:model="test.input"]').
prop('checked')).toBe(true);
});
@ -535,20 +546,20 @@ describe("angular.scenario.dsl", function() {
it('should select option from radio group', function() {
doc.append(
'<input type="radio" name="0@test.input" value="foo">' +
'<input type="radio" name="0@test.input" value="bar" checked="checked">'
'<input type="radio" name="r" ng:model="test.input" value="foo">' +
'<input type="radio" name="r" ng:model="test.input" value="bar" checked="checked">'
);
// HACK! We don't know why this is sometimes false on chrome
_jQuery('input[name="0@test.input"][value="bar"]').prop('checked', true);
expect(_jQuery('input[name="0@test.input"][value="bar"]').
_jQuery('input[ng\\:model="test.input"][value="bar"]').prop('checked', true);
expect(_jQuery('input[ng\\:model="test.input"][value="bar"]').
prop('checked')).toBe(true);
expect(_jQuery('input[name="0@test.input"][value="foo"]').
expect(_jQuery('input[ng\\:model="test.input"][value="foo"]').
prop('checked')).toBe(false);
var chain = $root.dsl.input('test.input');
chain.select('foo');
expect(_jQuery('input[name="0@test.input"][value="bar"]').
expect(_jQuery('input[ng\\:model="test.input"][value="bar"]').
prop('checked')).toBe(false);
expect(_jQuery('input[name="0@test.input"][value="foo"]').
expect(_jQuery('input[ng\\:model="test.input"][value="foo"]').
prop('checked')).toBe(true);
});
@ -560,7 +571,7 @@ describe("angular.scenario.dsl", function() {
describe('val', function() {
it('should return value in text input', function() {
doc.append('<input name="test.input" value="something">');
doc.append('<input ng:model="test.input" value="something">');
$root.dsl.input('test.input').val();
expect($root.futureResult).toEqual("something");
});
@ -570,10 +581,10 @@ describe("angular.scenario.dsl", function() {
describe('Textarea', function() {
it('should change value in textarea', function() {
doc.append('<textarea name="test.textarea">something</textarea>');
doc.append('<textarea ng:model="test.textarea">something</textarea>');
var chain = $root.dsl.input('test.textarea');
chain.enter('foo');
expect(_jQuery('textarea[name="test.textarea"]').val()).toEqual('foo');
expect(_jQuery('textarea[ng\\:model="test.textarea"]').val()).toEqual('foo');
});
it('should return error if no textarea exists', function() {

View file

@ -15,34 +15,34 @@
<tr>
<td>basic</td>
<td id="text-basic-box">
<input type="text" name="text.basic"/>
<input type="text" ng:model="text.basic"/>
</td>
<td>text.basic={{text.basic}}</td>
</tr>
<tr>
<td>password</td>
<td><input type="password" name="text.password" /></td>
<td><input type="password" ng:model="text.password" /></td>
<td>text.password={{text.password}}</td>
</tr>
<tr>
<td>hidden</td>
<td><input type="hidden" name="text.hidden" value="hiddenValue" /></td>
<td><input type="hidden" ng:model="text.hidden" value="hiddenValue" /></td>
<td>text.hidden={{text.hidden}}</td>
</tr>
<tr><th colspan="3">Input selection field</th></tr>
<tr id="gender-box">
<td>radio</td>
<td>
<input type="radio" name="gender" value="female"/> Female <br/>
<input type="radio" name="gender" value="male" checked="checked"/> Male
<input type="radio" ng:model="gender" value="female"/> Female <br/>
<input type="radio" ng:model="gender" value="male" checked="checked"/> Male
</td>
<td>gender={{gender}}</td>
</tr>
<tr>
<td>checkbox</td>
<td>
<input type="checkbox" name="checkbox.tea" checked value="on"/> Tea<br/>
<input type="checkbox" name="checkbox.coffee" value="on"/> Coffe
<input type="checkbox" ng:model="checkbox.tea" checked value="on"/> Tea<br/>
<input type="checkbox" ng:model="checkbox.coffee" value="on"/> Coffe
</td>
<td>
<pre>checkbox={{checkbox}}</pre>
@ -51,7 +51,7 @@
<tr>
<td>select</td>
<td>
<select name="select">
<select ng:model="select">
<option>A</option>
<option>B</option>
<option>C</option>
@ -62,7 +62,7 @@
<tr>
<td>multiselect</td>
<td>
<select name="multiselect" multiple>
<select ng:model="multiselect" multiple>
<option>A</option>
<option>B</option>
<option>C</option>

View file

@ -0,0 +1,218 @@
'use strict';
describe('$formFactory', function(){
var rootScope;
var formFactory;
beforeEach(function(){
rootScope = angular.scope();
formFactory = rootScope.$service('$formFactory');
});
it('should have global form', function(){
expect(formFactory.rootForm).toBeTruthy();
expect(formFactory.rootForm.$createWidget).toBeTruthy();
});
describe('new form', function(){
var form;
var scope;
var log;
function WidgetCtrl($formFactory){
this.$formFactory = $formFactory;
log += '<init>';
this.$render = function(){
log += '$render();';
};
this.$on('$validate', function(e){
log += '$validate();';
});
}
WidgetCtrl.$inject = ['$formFactory'];
WidgetCtrl.prototype = {
getFormFactory: function() {
return this.$formFactory;
}
};
beforeEach(function(){
log = '';
scope = rootScope.$new();
form = formFactory(scope);
});
describe('$createWidget', function(){
var widget;
beforeEach(function() {
widget = form.$createWidget({
scope:scope,
model:'text',
alias:'text',
controller:WidgetCtrl});
});
describe('data flow', function(){
it('should have status properties', function(){
expect(widget.$error).toEqual({});
expect(widget.$valid).toBe(true);
expect(widget.$invalid).toBe(false);
});
it('should update view when model changes', function(){
scope.text = 'abc';
scope.$digest();
expect(log).toEqual('<init>$validate();$render();');
expect(widget.$modelValue).toEqual('abc');
scope.text = 'xyz';
scope.$digest();
expect(widget.$modelValue).toEqual('xyz');
});
it('should have controller prototype methods', function(){
expect(widget.getFormFactory()).toEqual(formFactory);
});
});
describe('validation', function(){
it('should update state on error', function(){
widget.$emit('$invalid', 'E');
expect(widget.$valid).toEqual(false);
expect(widget.$invalid).toEqual(true);
widget.$emit('$valid', 'E');
expect(widget.$valid).toEqual(true);
expect(widget.$invalid).toEqual(false);
});
it('should have called the model setter before the validation', function(){
var modelValue;
widget.$on('$validate', function(){
modelValue = scope.text;
});
widget.$emit('$viewChange', 'abc');
expect(modelValue).toEqual('abc');
});
describe('form', function(){
it('should invalidate form when widget is invalid', function(){
expect(form.$error).toEqual({});
expect(form.$valid).toEqual(true);
expect(form.$invalid).toEqual(false);
widget.$emit('$invalid', 'REASON');
expect(form.$error.REASON).toEqual([widget]);
expect(form.$valid).toEqual(false);
expect(form.$invalid).toEqual(true);
var widget2 = form.$createWidget({
scope:scope, model:'text',
alias:'text',
controller:WidgetCtrl
});
widget2.$emit('$invalid', 'REASON');
expect(form.$error.REASON).toEqual([widget, widget2]);
expect(form.$valid).toEqual(false);
expect(form.$invalid).toEqual(true);
widget.$emit('$valid', 'REASON');
expect(form.$error.REASON).toEqual([widget2]);
expect(form.$valid).toEqual(false);
expect(form.$invalid).toEqual(true);
widget2.$emit('$valid', 'REASON');
expect(form.$error).toEqual({});
expect(form.$valid).toEqual(true);
expect(form.$invalid).toEqual(false);
});
});
});
describe('id assignment', function(){
it('should default to name expression', function(){
expect(form.text).toEqual(widget);
});
it('should use ng:id', function() {
widget = form.$createWidget({
scope:scope,
model:'text',
alias:'my.id',
controller:WidgetCtrl
});
expect(form['my.id']).toEqual(widget);
});
it('should not override existing names', function() {
var widget2 = form.$createWidget({
scope:scope,
model:'text',
alias:'text',
controller:WidgetCtrl
});
expect(form.text).toEqual(widget);
expect(widget2).not.toEqual(widget);
});
});
describe('dealocation', function() {
it('should dealocate', function() {
var widget2 = form.$createWidget({
scope:scope,
model:'text',
alias:'myId',
controller:WidgetCtrl
});
expect(form.myId).toEqual(widget2);
var widget3 = form.$createWidget({
scope:scope,
model:'text',
alias:'myId',
controller:WidgetCtrl
});
expect(form.myId).toEqual(widget2);
widget3.$destroy();
expect(form.myId).toEqual(widget2);
widget2.$destroy();
expect(form.myId).toBeUndefined();
});
it('should remove invalid fields from errors, when child widget removed', function(){
widget.$emit('$invalid', 'MyError');
expect(form.$error.MyError).toEqual([widget]);
expect(form.$invalid).toEqual(true);
widget.$destroy();
expect(form.$error.MyError).toBeUndefined();
expect(form.$invalid).toEqual(false);
});
});
});
});
});

View file

@ -1,41 +0,0 @@
'use strict';
describe('$invalidWidgets', function() {
var scope;
beforeEach(function(){
scope = angular.scope();
});
afterEach(function(){
dealoc(scope);
});
it("should count number of invalid widgets", function(){
var element = jqLite('<input name="price" ng:required ng:validate="number">');
jqLite(document.body).append(element);
scope = compile(element)();
var $invalidWidgets = scope.$service('$invalidWidgets');
expect($invalidWidgets.length).toEqual(1);
scope.price = 123;
scope.$digest();
expect($invalidWidgets.length).toEqual(0);
scope.$element.remove();
scope.price = 'abc';
scope.$digest();
expect($invalidWidgets.length).toEqual(0);
jqLite(document.body).append(scope.$element);
scope.price = 'abcd'; //force revalidation, maybe this should be done automatically?
scope.$digest();
expect($invalidWidgets.length).toEqual(1);
jqLite(document.body).html('');
scope.$digest();
expect($invalidWidgets.length).toEqual(0);
});
});

View file

@ -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');

View file

@ -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);

Some files were not shown because too many files have changed in this diff Show more