mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
feat(forms): new and improved forms
This commit is contained in:
parent
df6d2ba326
commit
4f78fd692c
104 changed files with 7044 additions and 3963 deletions
61
Rakefile
61
Rakefile
|
|
@ -77,66 +77,8 @@ task :compile_jstd_scenario_adapter => :init do
|
|||
end
|
||||
|
||||
|
||||
desc 'Generate IE css js patch'
|
||||
task :generate_ie_compat => :init do
|
||||
css = File.open('css/angular.css', 'r') {|f| f.read }
|
||||
|
||||
# finds all css rules that contain backround images and extracts the rule name(s), content type of
|
||||
# the image and base64 encoded image data
|
||||
r = /\n([^\{\n]+)\s*\{[^\}]*background-image:\s*url\("data:([^;]+);base64,([^"]+)"\);[^\}]*\}/
|
||||
|
||||
images = css.scan(r)
|
||||
|
||||
# create a js file with multipart header containing the extracted images. the entire file *must*
|
||||
# be CRLF (\r\n) delimited
|
||||
File.open(path_to('angular-ie-compat.js'), 'w') do |f|
|
||||
f.write("/*\r\n" +
|
||||
"Content-Type: multipart/related; boundary=\"_\"\r\n" +
|
||||
"\r\n")
|
||||
|
||||
images.each_index do |idx|
|
||||
f.write("--_\r\n" +
|
||||
"Content-Location:img#{idx}\r\n" +
|
||||
"Content-Transfer-Encoding:base64\r\n" +
|
||||
"\r\n" +
|
||||
images[idx][2] + "\r\n")
|
||||
end
|
||||
|
||||
f.write("--_--\r\n" +
|
||||
"*/\r\n")
|
||||
|
||||
# generate a css string containing *background-image rules for IE that point to the mime type
|
||||
# images in the header
|
||||
cssString = ''
|
||||
images.each_index do |idx|
|
||||
cssString += "#{images[idx][0]}{*background-image:url(\"mhtml:' + jsUri + '!img#{idx}\")}"
|
||||
end
|
||||
|
||||
# generate a javascript closure that contains a function which will append the generated css
|
||||
# string as a stylesheet to the current html document
|
||||
jsString = "(function(){ \r\n" +
|
||||
" var jsUri = document.location.href.replace(/\\/[^\\\/]+(#.*)?$/, '/') + \r\n" +
|
||||
" document.getElementById('ng-ie-compat').src,\r\n" +
|
||||
" css = '#{cssString}',\r\n" +
|
||||
" s = document.createElement('style'); \r\n" +
|
||||
"\r\n" +
|
||||
" s.setAttribute('type', 'text/css'); \r\n" +
|
||||
"\r\n" +
|
||||
" if (s.styleSheet) { \r\n" +
|
||||
" s.styleSheet.cssText = css; \r\n" +
|
||||
" } else { \r\n" +
|
||||
" s.appendChild(document.createTextNode(css)); \r\n" +
|
||||
" } \r\n" +
|
||||
" document.getElementsByTagName('head')[0].appendChild(s); \r\n" +
|
||||
"})();\r\n"
|
||||
|
||||
f.write(jsString)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
desc 'Compile JavaScript'
|
||||
task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter, :generate_ie_compat] do
|
||||
task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter] do
|
||||
|
||||
deps = [
|
||||
'src/angular.prefix',
|
||||
|
|
@ -193,7 +135,6 @@ task :package => [:clean, :compile, :docs] do
|
|||
['src/angular-mocks.js',
|
||||
path_to('angular.js'),
|
||||
path_to('angular.min.js'),
|
||||
path_to('angular-ie-compat.js'),
|
||||
path_to('angular-scenario.js'),
|
||||
path_to('jstd-scenario-adapter.js'),
|
||||
path_to('jstd-scenario-adapter-config.js'),
|
||||
|
|
|
|||
8
angularFiles.js
vendored
8
angularFiles.js
vendored
|
|
@ -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'
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
92
docs/content/api/angular.inputType.ngdoc
Normal file
92
docs/content/api/angular.inputType.ngdoc
Normal 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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(){
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)">
|
||||
|
|
|
|||
610
docs/content/guide/dev_guide.forms.ngdoc
Normal file
610
docs/content/guide/dev_guide.forms.ngdoc
Normal 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 <form>} element that calls the
|
||||
{@link api/angular.service.$formFactory $formFactory} service. The form is responsible for managing
|
||||
the widgets and for tracking validation information.
|
||||
|
||||
A form is:
|
||||
|
||||
- The collection which contains widgets or other forms.
|
||||
- Responsible for marshaling data from the model into a widget. This is
|
||||
triggered by {@link api/angular.scope.$watch $watch} of the model expression.
|
||||
- Responsible for marshaling data from the widget into the model. This is
|
||||
triggered by the widget emitting the `$viewChange` event.
|
||||
- Responsible for updating the validation state of the widget, when the widget emits
|
||||
`$valid` / `$invalid` event. The validation state is useful for controlling the validation
|
||||
errors shown to the user in it consist of:
|
||||
|
||||
- `$valid` / `$invalid`: Complementary set of booleans which show if a widget is valid / invalid.
|
||||
- `$error`: an object which has a property for each validation key emited by the widget.
|
||||
The value of the key is always true. If widget is valid, then the `$error`
|
||||
object has no properties. For example if the widget emits
|
||||
`$invalid` event with `REQUIRED` key. The internal state of the `$error` would be
|
||||
updated to `$error.REQUIRED == true`.
|
||||
|
||||
- Responsible for aggregating widget validation information into the form.
|
||||
|
||||
- `$valid` / `$invalid`: Complementary set of booleans which show if all the child widgets
|
||||
(or forms) are valid or if any are invalid.
|
||||
- `$error`: an object which has a property for each validation key emited by the
|
||||
child widget. The value of the key is an array of widgets which fired the invalid
|
||||
event. If all child widgets are valid then, then the `$error` object has no
|
||||
properties. For example if a child widget emits
|
||||
`$invalid` event with `REQUIRED` key. The internal state of the `$error` would be
|
||||
updated to `$error.REQUIRED == [ widgetWhichEmitedInvalid ]`.
|
||||
|
||||
|
||||
# Widgets
|
||||
|
||||
In Angular, a widget is the term used for the UI with which the user input. Examples of
|
||||
bult-in Angular widgets are {@link api/angular.widget.input input} and
|
||||
{@link api/angular.widget.select select}. Widgets provide the rendering and the user
|
||||
interaction logic. Widgets should be declared inside a form, if no form is provided an implicit
|
||||
form {@link api/angular.service.$formFactory $formFactory.rootForm} form is used.
|
||||
|
||||
Widgets are implemented as Angular controllers. A widget controller:
|
||||
|
||||
- implements methods:
|
||||
|
||||
- `$render` - Updates the DOM from the internal state as represented by `$viewValue`.
|
||||
- `$parseView` - Translate `$viewValue` to `$modelValue`. (`$modelValue` will be assigned to
|
||||
the model scope by the form)
|
||||
- `$parseModel` - Translate `$modelValue` to `$viewValue`. (`$viewValue` will be assigned to
|
||||
the DOM inside the `$render` method)
|
||||
|
||||
- responds to events:
|
||||
|
||||
- `$validate` - Emitted by the form when the form determines that the widget needs to validate
|
||||
itself. There may be more then one listener on the `$validate` event. The widget responds
|
||||
by emitting `$valid` / `$invalid` event of its own.
|
||||
|
||||
- emits events:
|
||||
|
||||
- `$viewChange` - Emitted when the user interacts with the widget and it is necessary to update
|
||||
the model.
|
||||
- `$valid` - Emitted when the widget determines that it is valid (usually as a response to
|
||||
`$validate` event or inside `$parseView()` or `$parseModel()` method).
|
||||
- `$invalid` - Emitted when the widget determines that it is invalid (usually as a response to
|
||||
`$validate` event or inside `$parseView()` or `$parseModel()` method).
|
||||
- `$destroy` - Emitted when the widget element is removed from the DOM.
|
||||
|
||||
|
||||
# CSS
|
||||
|
||||
Angular-defined widgets and forms set `ng-valid` and `ng-invalid` classes on themselves to allow
|
||||
the web-designer a way to style them. If you write your own widgets, then their `$render()`
|
||||
methods must set the appropriate CSS classes to allow styling.
|
||||
(See {@link dev_guide.templates.css-styling CSS})
|
||||
|
||||
|
||||
# Example
|
||||
|
||||
The following example demonstrates:
|
||||
|
||||
- How an error is displayed when a required field is empty.
|
||||
- Error highlighting.
|
||||
- How form submission is disabled when the form is invalid.
|
||||
- The internal state of the widget and form in the the 'Debug View' area.
|
||||
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<style>
|
||||
.ng-invalid { border: solid 1px red; }
|
||||
.ng-form {display: block;}
|
||||
</style>
|
||||
<script>
|
||||
function UserFormCntl(){
|
||||
this.state = /^\w\w$/;
|
||||
this.zip = /^\d\d\d\d\d$/;
|
||||
this.master = {
|
||||
customer: 'John Smith',
|
||||
address:{
|
||||
line1: '123 Main St.',
|
||||
city:'Anytown',
|
||||
state:'AA',
|
||||
zip:'12345'
|
||||
}
|
||||
};
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
UserFormCntl.prototype = {
|
||||
cancel: function(){
|
||||
this.form = angular.copy(this.master);
|
||||
},
|
||||
|
||||
save: function(){
|
||||
this.master = this.form;
|
||||
this.cancel();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<div ng:controller="UserFormCntl">
|
||||
|
||||
<form name="userForm">
|
||||
|
||||
<label>Name:</label><br/>
|
||||
<input type="text" name="customer" ng:model="form.customer" required/>
|
||||
<span class="error" ng:show="userForm.customer.$error.REQUIRED">
|
||||
Customer name is required!</span>
|
||||
<br/><br/>
|
||||
|
||||
<ng:form name="addressForm">
|
||||
<label>Address:</label> <br/>
|
||||
<input type="text" name="line1" size="33" required
|
||||
ng:model="form.address.line1"/> <br/>
|
||||
<input type="text" name="city" size="12" required
|
||||
ng:model="form.address.city"/>,
|
||||
<input type="text" name="state" ng:pattern="state" size="2" required
|
||||
ng:model="form.address.state"/>
|
||||
<input type="text" name="zip" ng:pattern="zip" size="5" required
|
||||
ng:model="form.address.zip"/><br/><br/>
|
||||
|
||||
<span class="error" ng:show="addressForm.$invalid">
|
||||
Incomplete address:
|
||||
<div class="error" ng:show="addressForm.state.$error.REQUIRED">
|
||||
Missing state!</span>
|
||||
<div class="error" ng:show="addressForm.state.$error.PATTERN">
|
||||
Invalid state!</span>
|
||||
<div class="error" ng:show="addressForm.zip.$error.REQUIRED">
|
||||
Missing zip!</span>
|
||||
<div class="error" ng:show="addressForm.zip.$error.PATTERN">
|
||||
Invalid zip!</span>
|
||||
</span>
|
||||
</ng:form>
|
||||
|
||||
<button ng:click="cancel()"
|
||||
ng:disabled="{{master.$equals(form)}}">Cancel</button>
|
||||
<button ng:click="save()"
|
||||
ng:disabled="{{userForm.$invalid || master.$equals(form)}}">
|
||||
Save</button>
|
||||
</form>
|
||||
|
||||
<hr/>
|
||||
Debug View:
|
||||
<pre>form={{form}}</pre>
|
||||
<pre>master={{master}}</pre>
|
||||
<pre>userForm={{userForm}}</pre>
|
||||
<pre>addressForm={{addressForm}}</pre>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should enable save button', function(){
|
||||
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
|
||||
input('form.customer').enter('');
|
||||
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
|
||||
input('form.customer').enter('change');
|
||||
expect(element(':button:contains(Save)').attr('disabled')).toBeFalsy();
|
||||
element(':button:contains(Save)').click();
|
||||
expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
|
||||
});
|
||||
it('should enable cancel button', function(){
|
||||
expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy();
|
||||
input('form.customer').enter('change');
|
||||
expect(element(':button:contains(Cancel)').attr('disabled')).toBeFalsy();
|
||||
element(':button:contains(Cancel)').click();
|
||||
expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy();
|
||||
expect(element(':input[ng\\:model="form.customer"]').val()).toEqual('John Smith');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
# Life-cycle
|
||||
|
||||
- The `<form>` element triggers creation of a new form {@link dev_guide.scopes scope} using the
|
||||
{@link api/angular.service.$formFactory $formfactory}. The new form scope is added to the
|
||||
`<form>` element using the jQuery `.data()` method for later retrieval under the key `$form`.
|
||||
The form also sets up these listeners:
|
||||
|
||||
- `$destroy` - This event is emitted by nested widget when it is removed from the view. It gives
|
||||
the form a chance to clean up any validation references to the destroyed widget.
|
||||
- `$valid` / `$invalid` - This event is emitted by the widget on validation state change.
|
||||
|
||||
- `<input>` element triggers the creation of the widget using the
|
||||
{@link api/angular.service.$formFactory $formfactory.$createWidget()} method. The `$createWidget()`
|
||||
creates new widget instance by calling the current scope {@link api/angular.scope.$new .$new()} and
|
||||
registers these listeners:
|
||||
|
||||
- `$watch` on the model scope.
|
||||
- `$viewChange` event on the widget scope.
|
||||
- `$validate` event on the widget scope.
|
||||
- Element `change` event when the user enters data.
|
||||
|
||||
<img class="center" src="img/form_data_flow.png" border="1" />
|
||||
|
||||
|
||||
- When the user interacts with the widget:
|
||||
|
||||
1. The DOM element fires the `change` event which the widget intercepts. Widget then emits
|
||||
a `$viewChange` event which includes the new user-entered value. (Remember that the DOM events
|
||||
are outside of the Angular environment so the widget must emit its event within the
|
||||
{@link api/angular.scope.$apply $apply} method).
|
||||
2. The form's `$viewChange` listener copies the user-entered value to the widget's `$viewValue`
|
||||
property. Since the `$viewValue` is the raw value as entered by user, it may need to be
|
||||
translated to a different format/type (for example, translating a string to a number).
|
||||
If you need your widget to translate between the internal `$viewValue` and the external
|
||||
`$modelValue` state, you must declare a `$parseView()` method. The `$parseView()` method
|
||||
will copy `$viewValue` to `$modelValue` and perform any necessary translations.
|
||||
3. The `$modelValue` is written into the application model.
|
||||
4. The form then emits a `$validate` event, giving the widget's validators chance to validate the
|
||||
input. There can be any number of validators registered. Each validator may in turn
|
||||
emit a `$valid` / `$invalid` event with the validator's validation key. For example `REQUIRED`.
|
||||
5. Form listens to `$valid`/`$invalid` events and updates both the form as well as the widget
|
||||
scope with the validation state. The validation updates the `$valid` and `$invalid`, property
|
||||
as well as `$error` object. The widget's `$error` object is updated with the validation key
|
||||
such that `$error.REQUIRED == true` when the validation emits `$invalid` with `REQUIRED`
|
||||
validation key. Similarly the form's `$error` object gets updated, but instead of boolean
|
||||
`true` it contains an array of invalid widgets (widgets which fired `$invalid` event with
|
||||
`REQUIRED` validation key).
|
||||
|
||||
- When the model is updated:
|
||||
|
||||
1. The model `$watch` listener assigns the model value to `$modelValue` on the widget.
|
||||
2. The form then calls `$parseModel` method on widget if present. The method converts the
|
||||
value to renderable format and assigns it to `$viewValue` (for example converting number to a
|
||||
string.)
|
||||
3. The form then emits a `$validate` which behaves as described above.
|
||||
4. The form then calls `$render` method on the widget to update the DOM structure from the
|
||||
`$viewValue`.
|
||||
|
||||
|
||||
|
||||
# Writing Your Own Widget
|
||||
|
||||
This example shows how to implement a custom HTML editor widget in Angular.
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
function EditorCntl(){
|
||||
this.htmlContent = '<b>Hello</b> <i>World</i>!';
|
||||
}
|
||||
|
||||
function HTMLEditorWidget(element) {
|
||||
var self = this;
|
||||
var htmlFilter = angular.filter('html');
|
||||
|
||||
this.$parseModel = function(){
|
||||
// need to protect for script injection
|
||||
try {
|
||||
this.$viewValue = htmlFilter(
|
||||
this.$modelValue || '').get();
|
||||
if (this.$error.HTML) {
|
||||
// we were invalid, but now we are OK.
|
||||
this.$emit('$valid', 'HTML');
|
||||
}
|
||||
} catch (e) {
|
||||
// if HTML not parsable invalidate form.
|
||||
this.$emit('$invalid', 'HTML');
|
||||
}
|
||||
}
|
||||
|
||||
this.$render = function(){
|
||||
element.html(this.$viewValue);
|
||||
}
|
||||
|
||||
element.bind('keyup', function(){
|
||||
self.$apply(function(){
|
||||
self.$emit('$viewChange', element.html());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
angular.directive('ng:html-editor-model', function(){
|
||||
function linkFn($formFactory, element) {
|
||||
var exp = element.attr('ng:html-editor-model'),
|
||||
form = $formFactory.forElement(element),
|
||||
widget;
|
||||
element.attr('contentEditable', true);
|
||||
widget = form.$createWidget({
|
||||
scope: this,
|
||||
model: exp,
|
||||
controller: HTMLEditorWidget,
|
||||
controllerArgs: [element]});
|
||||
// if the element is destroyed, then we need to
|
||||
// notify the form.
|
||||
element.bind('$destroy', function(){
|
||||
widget.$destroy();
|
||||
});
|
||||
}
|
||||
linkFn.$inject = ['$formFactory'];
|
||||
return linkFn;
|
||||
});
|
||||
</script>
|
||||
<form name='editorForm' ng:controller="EditorCntl">
|
||||
<div ng:html-editor-model="htmlContent"></div>
|
||||
<hr/>
|
||||
HTML: <br/>
|
||||
<textarea ng:model="htmlContent" cols="80"></textarea>
|
||||
<hr/>
|
||||
<pre>editorForm = {{editorForm}}</pre>
|
||||
</form>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should enter invalid HTML', function(){
|
||||
expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
|
||||
input('htmlContent').enter('<');
|
||||
expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
|
||||
|
||||
# HTML Inputs
|
||||
|
||||
The most common widgets you will use will be in the form of the
|
||||
standard HTML set. These widgets are bound using the `name` attribute
|
||||
to an expression. In addition, they can have `required` attribute to further control their
|
||||
validation.
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
function Ctrl(){
|
||||
this.input1 = '';
|
||||
this.input2 = '';
|
||||
this.input3 = 'A';
|
||||
this.input4 = false;
|
||||
this.input5 = 'c';
|
||||
this.input6 = [];
|
||||
}
|
||||
</script>
|
||||
<table style="font-size:.9em;" ng:controller="Ctrl">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Format</th>
|
||||
<th>HTML</th>
|
||||
<th>UI</th>
|
||||
<th ng:non-bindable>{{input#}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>text</th>
|
||||
<td>String</td>
|
||||
<td><tt><input type="text" ng:model="input1"></tt></td>
|
||||
<td><input type="text" ng:model="input1" size="4"></td>
|
||||
<td><tt>{{input1|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>textarea</th>
|
||||
<td>String</td>
|
||||
<td><tt><textarea ng:model="input2"></textarea></tt></td>
|
||||
<td><textarea ng:model="input2" cols='6'></textarea></td>
|
||||
<td><tt>{{input2|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>radio</th>
|
||||
<td>String</td>
|
||||
<td><tt>
|
||||
<input type="radio" ng:model="input3" value="A"><br>
|
||||
<input type="radio" ng:model="input3" value="B">
|
||||
</tt></td>
|
||||
<td>
|
||||
<input type="radio" ng:model="input3" value="A">
|
||||
<input type="radio" ng:model="input3" value="B">
|
||||
</td>
|
||||
<td><tt>{{input3|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>checkbox</th>
|
||||
<td>Boolean</td>
|
||||
<td><tt><input type="checkbox" ng:model="input4"></tt></td>
|
||||
<td><input type="checkbox" ng:model="input4"></td>
|
||||
<td><tt>{{input4|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>pulldown</th>
|
||||
<td>String</td>
|
||||
<td><tt>
|
||||
<select ng:model="input5"><br>
|
||||
<option value="c">C</option><br>
|
||||
<option value="d">D</option><br>
|
||||
</select><br>
|
||||
</tt></td>
|
||||
<td>
|
||||
<select ng:model="input5">
|
||||
<option value="c">C</option>
|
||||
<option value="d">D</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><tt>{{input5|json}}</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>multiselect</th>
|
||||
<td>Array</td>
|
||||
<td><tt>
|
||||
<select ng:model="input6" multiple size="4"><br>
|
||||
<option value="e">E</option><br>
|
||||
<option value="f">F</option><br>
|
||||
</select><br>
|
||||
</tt></td>
|
||||
<td>
|
||||
<select ng:model="input6" multiple size="4">
|
||||
<option value="e">E</option>
|
||||
<option value="f">F</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><tt>{{input6|json}}</tt></td>
|
||||
</tr>
|
||||
</table>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
|
||||
it('should exercise text', function(){
|
||||
input('input1').enter('Carlos');
|
||||
expect(binding('input1')).toEqual('"Carlos"');
|
||||
});
|
||||
it('should exercise textarea', function(){
|
||||
input('input2').enter('Carlos');
|
||||
expect(binding('input2')).toEqual('"Carlos"');
|
||||
});
|
||||
it('should exercise radio', function(){
|
||||
expect(binding('input3')).toEqual('"A"');
|
||||
input('input3').select('B');
|
||||
expect(binding('input3')).toEqual('"B"');
|
||||
input('input3').select('A');
|
||||
expect(binding('input3')).toEqual('"A"');
|
||||
});
|
||||
it('should exercise checkbox', function(){
|
||||
expect(binding('input4')).toEqual('false');
|
||||
input('input4').check();
|
||||
expect(binding('input4')).toEqual('true');
|
||||
});
|
||||
it('should exercise pulldown', function(){
|
||||
expect(binding('input5')).toEqual('"c"');
|
||||
select('input5').option('d');
|
||||
expect(binding('input5')).toEqual('"d"');
|
||||
});
|
||||
it('should exercise multiselect', function(){
|
||||
expect(binding('input6')).toEqual('[]');
|
||||
select('input6').options('e');
|
||||
expect(binding('input6')).toEqual('["e"]');
|
||||
select('input6').options('e', 'f');
|
||||
expect(binding('input6')).toEqual('["e","f"]');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
||||
#Testing
|
||||
|
||||
When unit-testing a controller it may be desirable to have a reference to form and to simulate
|
||||
different form validation states.
|
||||
|
||||
This example demonstrates a login form, where the login button is enabled only when the form is
|
||||
properly filled out.
|
||||
<pre>
|
||||
<div ng:controller="LoginController">
|
||||
<form name="loginForm">
|
||||
<input type="text" ng:model="username" required/>
|
||||
<input type="password" ng:model="password" required/>
|
||||
<button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login>
|
||||
</form>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
In the unit tests we do not have access to the DOM, and therefore the `loginForm` reference does
|
||||
not get set on the controller. This example shows how it can be unit-tested, by creating a mock
|
||||
form.
|
||||
<pre>
|
||||
function LoginController() {
|
||||
this.disableLogin = function() {
|
||||
return this.loginForm.$invalid;
|
||||
};
|
||||
}
|
||||
|
||||
describe('LoginController', function() {
|
||||
it('should disable login button when form is invalid', function() {
|
||||
var scope = angular.scope();
|
||||
var loginController = scope.$new(LoginController);
|
||||
|
||||
// In production the 'loginForm' form instance gets set from the view,
|
||||
// but in unit-test we have to set it manually.
|
||||
loginController.loginForm = scope.$service('$formFactory')();
|
||||
|
||||
expect(loginController.disableLogin()).toBe(false);
|
||||
|
||||
// Now simulate an invalid form
|
||||
loginController.loginForm.$emit('$invalid', 'MyReason');
|
||||
expect(loginController.disableLogin()).toBe(true);
|
||||
|
||||
// Now simulate a valid form
|
||||
loginController.loginForm.$emit('$valid', 'MyReason');
|
||||
expect(loginController.disableLogin()).toBe(false);
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
## Custom widgets
|
||||
|
||||
This example demonstrates a login form, where the password has custom validation rules.
|
||||
<pre>
|
||||
<div ng:controller="LoginController">
|
||||
<form name="loginForm">
|
||||
<input type="text" ng:model="username" required/>
|
||||
<input type="@StrongPassword" ng:model="password" required/>
|
||||
<button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login>
|
||||
</form>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
In the unit tests we do not have access to the DOM, and therefore the `loginForm` and custom
|
||||
input type reference does not get set on the controller. This example shows how it can be
|
||||
unit-tested, by creating a mock form and a mock custom input type.
|
||||
<pre>
|
||||
function LoginController(){
|
||||
this.disableLogin = function() {
|
||||
return this.loginForm.$invalid;
|
||||
};
|
||||
|
||||
this.StrongPassword = function(element) {
|
||||
var widget = this;
|
||||
element.attr('type', 'password'); // act as password.
|
||||
this.$on('$validate', function(){
|
||||
widget.$emit(widget.$viewValue.length > 5 ? '$valid' : '$invalid', 'PASSWORD');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
describe('LoginController', function() {
|
||||
it('should disable login button when form is invalid', function() {
|
||||
var scope = angular.scope();
|
||||
var loginController = scope.$new(LoginController);
|
||||
var input = angular.element('<input>');
|
||||
|
||||
// In production the 'loginForm' form instance gets set from the view,
|
||||
// but in unit-test we have to set it manually.
|
||||
loginController.loginForm = scope.$service('$formFactory')();
|
||||
|
||||
// now instantiate a custom input type
|
||||
loginController.loginForm.$createWidget({
|
||||
scope: loginController,
|
||||
model: 'password',
|
||||
alias: 'password',
|
||||
controller: loginController.StrongPassword,
|
||||
controllerArgs: [input]
|
||||
});
|
||||
|
||||
// Verify that the custom password input type sets the input type to password
|
||||
expect(input.attr('type')).toEqual('password');
|
||||
|
||||
expect(loginController.disableLogin()).toBe(false);
|
||||
|
||||
// Now simulate an invalid form
|
||||
loginController.loginForm.password.$emit('$invalid', 'PASSWORD');
|
||||
expect(loginController.disableLogin()).toBe(true);
|
||||
|
||||
// Now simulate a valid form
|
||||
loginController.loginForm.password.$emit('$valid', 'PASSWORD');
|
||||
expect(loginController.disableLogin()).toBe(false);
|
||||
|
||||
// Changing model state, should also influence the form validity
|
||||
loginController.password = 'abc'; // too short so it should be invalid
|
||||
scope.$digest();
|
||||
expect(loginController.loginForm.password.$invalid).toBe(true);
|
||||
|
||||
// Changeing model state, should also influence the form validity
|
||||
loginController.password = 'abcdef'; // should be valid
|
||||
scope.$digest();
|
||||
expect(loginController.loginForm.password.$valid).toBe(true);
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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".
|
||||
|
|
|
|||
|
|
@ -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 }}`:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
BIN
docs/img/form_data_flow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 +
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@
|
|||
<span><angular/> Buzz</span>
|
||||
<span>
|
||||
filter:
|
||||
<input type="text" name="filterText"/>
|
||||
<input type="text" ng:model="filterText"/>
|
||||
</span>
|
||||
<span>
|
||||
user:
|
||||
<input type="text" name="userId" ng:required/>
|
||||
<input type="text" ng:model="userId" required/>
|
||||
<button ng:click="$location.hashPath = userId">fetch</button>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<body ng:class="status" ng:init="mute={}" ng:watch="$anchor.user: tweets = fetchTweets($anchor.user)">
|
||||
<div class="addressbook box">
|
||||
<h1>Address Book</h1>
|
||||
[ Filter: <input type="text" name="userFilter"/>]
|
||||
[ Filter: <input type="text" ng:model="userFilter"/>]
|
||||
<ul>
|
||||
<li ng:repeat="user in users.$filter(userFilter).$orderBy('screen_name')" ng:class-even="'even'" ng:class-odd="'odd'">
|
||||
<a href="" ng:click="$anchor.user=user.screen_name"><img src="{{user.profile_image_url}}"/></a>
|
||||
|
|
@ -29,13 +29,13 @@
|
|||
<div ng:show="$anchor.edituser" ng:eval="user = users.$find({:$.screen_name == $anchor.edituser})">
|
||||
<div class="editor">
|
||||
<label>Username:</label>
|
||||
<input type="text" name="user.screen_name" disabled="disabled"/>
|
||||
<input type="text" ng:model="user.screen_name" disabled="disabled"/>
|
||||
<label>Name:</label>
|
||||
<input type="text" name="user.name"/>
|
||||
<input type="text" ng:model="user.name"/>
|
||||
<label>Image:</label>
|
||||
<input type="text" name="user.profile_image_url"/>
|
||||
<input type="text" ng:model="user.profile_image_url"/>
|
||||
<label>Notes:</label>
|
||||
<textarea type="text" name="user.notes"></textarea>
|
||||
<textarea type="text" ng:model="user.notes"></textarea>
|
||||
|
||||
<input type="button" ng:click="$anchor.edituser=undefined" value="Close"/>
|
||||
</div>
|
||||
|
|
@ -57,7 +57,7 @@ tweets={{tweets}}
|
|||
</div>
|
||||
<div class="tweeter box">
|
||||
<h1>Tweets: {{$anchor.user}}</h1>
|
||||
[ Filter: <input type="text" name="tweetFilter"/>
|
||||
[ Filter: <input type="text" ng:model="tweetFilter"/>
|
||||
<span ng:show="$anchor.user">| <a href="#user="><< All</a></span>
|
||||
]
|
||||
<div class="loading">Loading...</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
(TODO: I should fetch current tweets)
|
||||
<div class="tweeter box">
|
||||
<h1>Tweets: {{$anchor.user}}</h1>
|
||||
[ Filter: <input type="text" name="tweetFilter"/> (TODO: this should act as search box)
|
||||
[ Filter: <input type="text" ng:model="tweetFilter"/> (TODO: this should act as search box)
|
||||
<span ng:show="$anchor.user">| <a href="#user="><< All</a></span>
|
||||
]
|
||||
<div class="loading">Loading...</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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!',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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!',
|
||||
|
|
|
|||
|
|
@ -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}} 在浏览该文件!',
|
||||
|
|
|
|||
2301
images/docs/guide/form_data_flow.graffle
Normal file
2301
images/docs/guide/form_data_flow.graffle
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
3
src/angular-bootstrap.js
vendored
3
src/angular-bootstrap.js
vendored
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
171
src/apis.js
171
src/apis.js
|
|
@ -103,9 +103,16 @@ var angularArray = {
|
|||
* @example
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<div ng:init="books = ['Moby Dick', 'Great Gatsby', 'Romeo and Juliet']"></div>
|
||||
<input name='bookName' value='Romeo and Juliet'> <br>
|
||||
Index of '{{bookName}}' in the list {{books}} is <em>{{books.$indexOf(bookName)}}</em>.
|
||||
<script>
|
||||
function Ctrl(){
|
||||
this.books = ['Moby Dick', 'Great Gatsby', 'Romeo and Juliet'];
|
||||
this.bookName = 'Romeo and Juliet';
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="Ctrl">
|
||||
<input ng:model='bookName'> <br>
|
||||
Index of '{{bookName}}' in the list {{books}} is <em>{{books.$indexOf(bookName)}}</em>.
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should correctly calculate the initial index', function() {
|
||||
|
|
@ -146,17 +153,29 @@ var angularArray = {
|
|||
* @example
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<table ng:init="invoice= {items:[{qty:10, description:'gadget', cost:9.95}]}">
|
||||
<script>
|
||||
function Ctrl(){
|
||||
this.invoice = {
|
||||
items:[ {
|
||||
qty:10,
|
||||
description:'gadget',
|
||||
cost:9.95
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<table class="invoice" ng:controller="Ctrl">
|
||||
<tr><th>Qty</th><th>Description</th><th>Cost</th><th>Total</th><th></th></tr>
|
||||
<tr ng:repeat="item in invoice.items">
|
||||
<td><input name="item.qty" value="1" size="4" ng:required ng:validate="integer"></td>
|
||||
<td><input name="item.description"></td>
|
||||
<td><input name="item.cost" value="0.00" ng:required ng:validate="number" size="6"></td>
|
||||
<td><input type="integer" ng:model="item.qty" size="4" required></td>
|
||||
<td><input type="text" ng:model="item.description"></td>
|
||||
<td><input type="number" ng:model="item.cost" required size="6"></td>
|
||||
<td>{{item.qty * item.cost | currency}}</td>
|
||||
<td>[<a href ng:click="invoice.items.$remove(item)">X</a>]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href ng:click="invoice.items.$add()">add item</a></td>
|
||||
<td><a href ng:click="invoice.items.$add({qty:1, cost:0})">add item</a></td>
|
||||
<td></td>
|
||||
<td>Total:</td>
|
||||
<td>{{invoice.items.$sum('qty*cost') | currency}}</td>
|
||||
|
|
@ -166,8 +185,8 @@ var angularArray = {
|
|||
<doc:scenario>
|
||||
//TODO: these specs are lame because I had to work around issues #164 and #167
|
||||
it('should initialize and calculate the totals', function() {
|
||||
expect(repeater('.doc-example-live table tr', 'item in invoice.items').count()).toBe(3);
|
||||
expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(1)).
|
||||
expect(repeater('table.invoice tr', 'item in invoice.items').count()).toBe(3);
|
||||
expect(repeater('table.invoice tr', 'item in invoice.items').row(1)).
|
||||
toEqual(['$99.50']);
|
||||
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
|
||||
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
|
||||
|
|
@ -178,7 +197,7 @@ var angularArray = {
|
|||
using('.doc-example-live tr:nth-child(3)').input('item.qty').enter('20');
|
||||
using('.doc-example-live tr:nth-child(3)').input('item.cost').enter('100');
|
||||
|
||||
expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(2)).
|
||||
expect(repeater('table.invoice tr', 'item in invoice.items').row(2)).
|
||||
toEqual(['$2,000.00']);
|
||||
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$2,099.50');
|
||||
});
|
||||
|
|
@ -297,7 +316,7 @@ var angularArray = {
|
|||
{name:'Adam', phone:'555-5678'},
|
||||
{name:'Julie', phone:'555-8765'}]"></div>
|
||||
|
||||
Search: <input name="searchText"/>
|
||||
Search: <input ng:model="searchText"/>
|
||||
<table id="searchTextResults">
|
||||
<tr><th>Name</th><th>Phone</th><tr>
|
||||
<tr ng:repeat="friend in friends.$filter(searchText)">
|
||||
|
|
@ -306,9 +325,9 @@ var angularArray = {
|
|||
<tr>
|
||||
</table>
|
||||
<hr>
|
||||
Any: <input name="search.$"/> <br>
|
||||
Name only <input name="search.name"/><br>
|
||||
Phone only <input name="search.phone"/><br>
|
||||
Any: <input ng:model="search.$"/> <br>
|
||||
Name only <input ng:model="search.name"/><br>
|
||||
Phone only <input ng:model="search.phone"/><br>
|
||||
<table id="searchObjResults">
|
||||
<tr><th>Name</th><th>Phone</th><tr>
|
||||
<tr ng:repeat="friend in friends.$filter(search)">
|
||||
|
|
@ -442,22 +461,29 @@ var angularArray = {
|
|||
* with objects created from user input.
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
[<a href="" ng:click="people.$add()">add empty</a>]
|
||||
[<a href="" ng:click="people.$add({name:'John', sex:'male'})">add 'John'</a>]
|
||||
[<a href="" ng:click="people.$add({name:'Mary', sex:'female'})">add 'Mary'</a>]
|
||||
<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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
113
src/filters.js
113
src/filters.js
|
|
@ -48,9 +48,16 @@
|
|||
* @example
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<input type="text" name="amount" value="1234.56"/> <br/>
|
||||
default currency symbol ($): {{amount | currency}}<br/>
|
||||
custom currency identifier (USD$): {{amount | currency:"USD$"}}
|
||||
<script>
|
||||
function Ctrl(){
|
||||
this.amount = 1234.56;
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="Ctrl">
|
||||
<input type="number" ng:model="amount"/> <br/>
|
||||
default currency symbol ($): {{amount | currency}}<br/>
|
||||
custom currency identifier (USD$): {{amount | currency:"USD$"}}
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should init with 1234.56', function(){
|
||||
|
|
@ -93,10 +100,17 @@ angularFilter.currency = function(amount, currencySymbol){
|
|||
* @example
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
Enter number: <input name='val' value='1234.56789' /><br/>
|
||||
Default formatting: {{val | number}}<br/>
|
||||
No fractions: {{val | number:0}}<br/>
|
||||
Negative number: {{-val | number:4}}
|
||||
<script>
|
||||
function Ctrl(){
|
||||
this.val = 1234.56789;
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="Ctrl">
|
||||
Enter number: <input ng:model='val'><br/>
|
||||
Default formatting: {{val | number}}<br/>
|
||||
No fractions: {{val | number:0}}<br/>
|
||||
Negative number: {{-val | number:4}}
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should format numbers', function(){
|
||||
|
|
@ -462,36 +476,43 @@ angularFilter.uppercase = uppercase;
|
|||
* @example
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
Snippet: <textarea name="snippet" cols="60" rows="3">
|
||||
<p style="color:blue">an html
|
||||
<em onmouseover="this.textContent='PWN3D!'">click here</em>
|
||||
snippet</p></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Filter</td>
|
||||
<td>Source</td>
|
||||
<td>Rendered</td>
|
||||
</tr>
|
||||
<tr id="html-filter">
|
||||
<td>html filter</td>
|
||||
<td>
|
||||
<pre><div ng:bind="snippet | html"><br/></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng:bind="snippet | html"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="escaped-html">
|
||||
<td>no filter</td>
|
||||
<td><pre><div ng:bind="snippet"><br/></div></pre></td>
|
||||
<td><div ng:bind="snippet"></div></td>
|
||||
</tr>
|
||||
<tr id="html-unsafe-filter">
|
||||
<td>unsafe html filter</td>
|
||||
<td><pre><div ng:bind="snippet | html:'unsafe'"><br/></div></pre></td>
|
||||
<td><div ng:bind="snippet | html:'unsafe'"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
<script>
|
||||
function Ctrl(){
|
||||
this.snippet =
|
||||
'<p style="color:blue">an html\n' +
|
||||
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
|
||||
'snippet</p>';
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="Ctrl">
|
||||
Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Filter</td>
|
||||
<td>Source</td>
|
||||
<td>Rendered</td>
|
||||
</tr>
|
||||
<tr id="html-filter">
|
||||
<td>html filter</td>
|
||||
<td>
|
||||
<pre><div ng:bind="snippet | html"><br/></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng:bind="snippet | html"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="escaped-html">
|
||||
<td>no filter</td>
|
||||
<td><pre><div ng:bind="snippet"><br/></div></pre></td>
|
||||
<td><div ng:bind="snippet"></div></td>
|
||||
</tr>
|
||||
<tr id="html-unsafe-filter">
|
||||
<td>unsafe html filter</td>
|
||||
<td><pre><div ng:bind="snippet | html:'unsafe'"><br/></div></pre></td>
|
||||
<td><div ng:bind="snippet | html:'unsafe'"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should sanitize the html snippet ', function(){
|
||||
|
|
@ -543,12 +564,18 @@ angularFilter.html = function(html, option){
|
|||
* @example
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
Snippet: <textarea name="snippet" cols="60" rows="3">
|
||||
Pretty text with some links:
|
||||
http://angularjs.org/,
|
||||
mailto:us@somewhere.org,
|
||||
another@somewhere.org,
|
||||
and one more: ftp://127.0.0.1/.</textarea>
|
||||
<script>
|
||||
function Ctrl(){
|
||||
this.snippet =
|
||||
'Pretty text with some links:\n'+
|
||||
'http://angularjs.org/,\n'+
|
||||
'mailto:us@somewhere.org,\n'+
|
||||
'another@somewhere.org,\n'+
|
||||
'and one more: ftp://127.0.0.1/.';
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="Ctrl">
|
||||
Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Filter</td>
|
||||
|
|
|
|||
|
|
@ -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) : ""; }
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
394
src/service/formFactory.js
Normal 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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
81
src/widget/form.js
Normal 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
773
src/widget/input.js
Normal 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
427
src/widget/select.js
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
1029
src/widgets.js
1029
src/widgets.js
File diff suppressed because it is too large
Load diff
|
|
@ -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/'});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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\"");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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
57
test/jQueryPatchSpec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
218
test/service/formFactorySpec.js
Normal file
218
test/service/formFactorySpec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue