2011-06-06 21:44:49 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @workInProgress
|
|
|
|
|
|
* @ngdoc overview
|
|
|
|
|
|
* @name angular.widget
|
|
|
|
|
|
* @description
|
|
|
|
|
|
*
|
2011-06-07 20:53:05 +00:00
|
|
|
|
* Widgets are custom DOM elements. An angular widget can be either a custom
|
2011-06-06 21:44:49 +00:00
|
|
|
|
* attribute that modifies an existing DOM elements or an entirely new DOM element.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Following is the list of built-in angular widgets:
|
|
|
|
|
|
*
|
2011-06-07 05:02:30 +00:00
|
|
|
|
* * {@link angular.widget.@ng:format ng:format} - Formats data for display to user and for storage.
|
|
|
|
|
|
* * {@link angular.widget.@ng:non-bindable ng:non-bindable} - Blocks angular from processing an
|
2011-06-06 21:44:49 +00:00
|
|
|
|
* HTML element.
|
2011-06-07 05:02:30 +00:00
|
|
|
|
* * {@link angular.widget.@ng:repeat ng:repeat} - Creates and manages a collection of cloned HTML
|
2011-06-06 21:44:49 +00:00
|
|
|
|
* elements.
|
2011-06-07 05:02:30 +00:00
|
|
|
|
* * {@link angular.widget.@ng:required ng:required} - Verifies presence of user input.
|
|
|
|
|
|
* * {@link angular.widget.@ng:validate ng:validate} - Validates content of user input.
|
|
|
|
|
|
* * {@link angular.widget.HTML HTML} - Standard HTML processed by angular.
|
|
|
|
|
|
* * {@link angular.widget.ng:view ng:view} - Works with $route to "include" partial templates
|
|
|
|
|
|
* * {@link angular.widget.ng:switch ng:switch} - Conditionally changes DOM structure
|
|
|
|
|
|
* * {@link angular.widget.ng:include ng:include} - Includes an external HTML fragment
|
|
|
|
|
|
*
|
|
|
|
|
|
* For more information about angular widgets, see {@link guide/dev_guide.compiler.widgets
|
|
|
|
|
|
* Understanding Angular Widgets} in the angular Developer Guide.
|
2011-06-06 21:44:49 +00:00
|
|
|
|
*/
|
|
|
|
|
|
|
2010-10-27 22:31:10 +00:00
|
|
|
|
/**
|
2010-11-19 00:28:42 +00:00
|
|
|
|
* @workInProgress
|
2010-11-07 21:04:48 +00:00
|
|
|
|
* @ngdoc widget
|
|
|
|
|
|
* @name angular.widget.HTML
|
2010-10-27 22:31:10 +00:00
|
|
|
|
*
|
2010-11-07 21:04:48 +00:00
|
|
|
|
* @description
|
2011-04-08 23:03:39 +00:00
|
|
|
|
* 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
|
2010-11-07 21:04:48 +00:00
|
|
|
|
* to an expression. In addition they can have `ng:validate`, `ng:required`,
|
|
|
|
|
|
* `ng:format`, `ng:change` attribute to further control their behavior.
|
2011-01-13 18:35:26 +00:00
|
|
|
|
*
|
2010-11-07 21:04:48 +00:00
|
|
|
|
* @usageContent
|
|
|
|
|
|
* see example below for usage
|
2011-01-13 18:35:26 +00:00
|
|
|
|
*
|
2010-11-07 21:04:48 +00:00
|
|
|
|
* <input type="text|checkbox|..." ... />
|
|
|
|
|
|
* <textarea ... />
|
|
|
|
|
|
* <select ...>
|
|
|
|
|
|
* <option>...</option>
|
|
|
|
|
|
* </select>
|
2011-01-13 18:35:26 +00:00
|
|
|
|
*
|
2010-11-07 21:04:48 +00:00
|
|
|
|
* @example
|
2011-02-01 00:21:29 +00:00
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
<table style="font-size:.9em;">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>Name</th>
|
|
|
|
|
|
<th>Format</th>
|
|
|
|
|
|
<th>HTML</th>
|
|
|
|
|
|
<th>UI</th>
|
|
|
|
|
|
<th ng:non-bindable>{{input#}}</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>text</th>
|
|
|
|
|
|
<td>String</td>
|
|
|
|
|
|
<td><tt><input type="text" name="input1"></tt></td>
|
|
|
|
|
|
<td><input type="text" name="input1" size="4"></td>
|
|
|
|
|
|
<td><tt>{{input1|json}}</tt></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>textarea</th>
|
|
|
|
|
|
<td>String</td>
|
|
|
|
|
|
<td><tt><textarea name="input2"></textarea></tt></td>
|
|
|
|
|
|
<td><textarea name="input2" cols='6'></textarea></td>
|
|
|
|
|
|
<td><tt>{{input2|json}}</tt></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>radio</th>
|
|
|
|
|
|
<td>String</td>
|
|
|
|
|
|
<td><tt>
|
|
|
|
|
|
<input type="radio" name="input3" value="A"><br>
|
|
|
|
|
|
<input type="radio" name="input3" value="B">
|
|
|
|
|
|
</tt></td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<input type="radio" name="input3" value="A">
|
|
|
|
|
|
<input type="radio" name="input3" value="B">
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td><tt>{{input3|json}}</tt></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>checkbox</th>
|
|
|
|
|
|
<td>Boolean</td>
|
|
|
|
|
|
<td><tt><input type="checkbox" name="input4" value="checked"></tt></td>
|
|
|
|
|
|
<td><input type="checkbox" name="input4" value="checked"></td>
|
|
|
|
|
|
<td><tt>{{input4|json}}</tt></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>pulldown</th>
|
|
|
|
|
|
<td>String</td>
|
|
|
|
|
|
<td><tt>
|
|
|
|
|
|
<select name="input5"><br>
|
|
|
|
|
|
<option value="c">C</option><br>
|
|
|
|
|
|
<option value="d">D</option><br>
|
|
|
|
|
|
</select><br>
|
|
|
|
|
|
</tt></td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<select name="input5">
|
|
|
|
|
|
<option value="c">C</option>
|
|
|
|
|
|
<option value="d">D</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td><tt>{{input5|json}}</tt></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>multiselect</th>
|
|
|
|
|
|
<td>Array</td>
|
|
|
|
|
|
<td><tt>
|
|
|
|
|
|
<select name="input6" multiple size="4"><br>
|
|
|
|
|
|
<option value="e">E</option><br>
|
|
|
|
|
|
<option value="f">F</option><br>
|
|
|
|
|
|
</select><br>
|
|
|
|
|
|
</tt></td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<select name="input6" multiple size="4">
|
|
|
|
|
|
<option value="e">E</option>
|
|
|
|
|
|
<option value="f">F</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td><tt>{{input6|json}}</tt></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
|
|
|
|
|
|
it('should exercise text', function(){
|
|
|
|
|
|
input('input1').enter('Carlos');
|
|
|
|
|
|
expect(binding('input1')).toEqual('"Carlos"');
|
|
|
|
|
|
});
|
|
|
|
|
|
it('should exercise textarea', function(){
|
|
|
|
|
|
input('input2').enter('Carlos');
|
|
|
|
|
|
expect(binding('input2')).toEqual('"Carlos"');
|
|
|
|
|
|
});
|
|
|
|
|
|
it('should exercise radio', function(){
|
|
|
|
|
|
expect(binding('input3')).toEqual('null');
|
|
|
|
|
|
input('input3').select('A');
|
|
|
|
|
|
expect(binding('input3')).toEqual('"A"');
|
|
|
|
|
|
input('input3').select('B');
|
|
|
|
|
|
expect(binding('input3')).toEqual('"B"');
|
|
|
|
|
|
});
|
|
|
|
|
|
it('should exercise checkbox', function(){
|
|
|
|
|
|
expect(binding('input4')).toEqual('false');
|
|
|
|
|
|
input('input4').check();
|
|
|
|
|
|
expect(binding('input4')).toEqual('true');
|
|
|
|
|
|
});
|
|
|
|
|
|
it('should exercise pulldown', function(){
|
|
|
|
|
|
expect(binding('input5')).toEqual('"c"');
|
|
|
|
|
|
select('input5').option('d');
|
|
|
|
|
|
expect(binding('input5')).toEqual('"d"');
|
|
|
|
|
|
});
|
|
|
|
|
|
it('should exercise multiselect', function(){
|
|
|
|
|
|
expect(binding('input6')).toEqual('[]');
|
|
|
|
|
|
select('input6').options('e');
|
|
|
|
|
|
expect(binding('input6')).toEqual('["e"]');
|
|
|
|
|
|
select('input6').options('e', 'f');
|
|
|
|
|
|
expect(binding('input6')).toEqual('["e","f"]');
|
|
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-10-27 22:31:10 +00:00
|
|
|
|
*/
|
|
|
|
|
|
|
2010-03-26 23:27:18 +00:00
|
|
|
|
function modelAccessor(scope, element) {
|
2010-05-11 03:24:20 +00:00
|
|
|
|
var expr = element.attr('name');
|
2011-04-19 23:34:49 +00:00
|
|
|
|
var exprFn, assignFn;
|
2010-12-10 06:09:46 +00:00
|
|
|
|
if (expr) {
|
2011-04-19 23:34:49 +00:00
|
|
|
|
exprFn = parser(expr).assignable();
|
|
|
|
|
|
assignFn = exprFn.assign;
|
|
|
|
|
|
if (!assignFn) throw new Error("Expression '" + expr + "' is not assignable.");
|
2010-12-10 06:09:46 +00:00
|
|
|
|
return {
|
|
|
|
|
|
get: function() {
|
2011-04-19 23:34:49 +00:00
|
|
|
|
return exprFn(scope);
|
2010-12-10 06:09:46 +00:00
|
|
|
|
},
|
|
|
|
|
|
set: function(value) {
|
2011-03-26 23:06:38 +00:00
|
|
|
|
if (value !== undefined) {
|
2011-01-13 18:35:26 +00:00
|
|
|
|
return scope.$tryEval(function(){
|
2011-04-19 23:34:49 +00:00
|
|
|
|
assignFn(scope, value);
|
2011-01-13 18:35:26 +00:00
|
|
|
|
}, element);
|
2010-12-10 06:09:46 +00:00
|
|
|
|
}
|
2010-05-11 03:24:20 +00:00
|
|
|
|
}
|
2010-12-10 06:09:46 +00:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2010-03-26 23:27:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2010-05-13 23:40:41 +00:00
|
|
|
|
function modelFormattedAccessor(scope, element) {
|
|
|
|
|
|
var accessor = modelAccessor(scope, element),
|
2010-07-13 18:20:11 +00:00
|
|
|
|
formatterName = element.attr('ng:format') || NOOP,
|
2011-01-13 18:35:26 +00:00
|
|
|
|
formatter = compileFormatter(formatterName);
|
2010-12-10 06:09:46 +00:00
|
|
|
|
if (accessor) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
get: function() {
|
2011-01-13 18:35:26 +00:00
|
|
|
|
return formatter.format(scope, accessor.get());
|
2010-12-10 06:09:46 +00:00
|
|
|
|
},
|
|
|
|
|
|
set: function(value) {
|
2011-01-13 18:35:26 +00:00
|
|
|
|
return accessor.set(formatter.parse(scope, value));
|
2010-12-10 06:09:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2010-05-13 23:40:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2010-03-26 23:27:18 +00:00
|
|
|
|
function compileValidator(expr) {
|
2010-10-15 22:28:58 +00:00
|
|
|
|
return parser(expr).validator()();
|
2010-03-26 23:27:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2011-01-13 18:35:26 +00:00
|
|
|
|
function compileFormatter(expr) {
|
|
|
|
|
|
return parser(expr).formatter()();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2010-11-12 23:16:33 +00:00
|
|
|
|
/**
|
2010-11-19 00:28:42 +00:00
|
|
|
|
* @workInProgress
|
2010-11-18 07:12:38 +00:00
|
|
|
|
* @ngdoc widget
|
|
|
|
|
|
* @name angular.widget.@ng:validate
|
2010-11-12 23:16:33 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @description
|
2010-11-18 07:12:38 +00:00
|
|
|
|
* The `ng:validate` attribute widget validates the user input. If the input does not pass
|
|
|
|
|
|
* validation, the `ng-validation-error` CSS class and the `ng:error` attribute are set on the input
|
|
|
|
|
|
* element. Check out {@link angular.validator validators} to find out more.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} validator The name of a built-in or custom {@link angular.validator validator} to
|
|
|
|
|
|
* to be used.
|
|
|
|
|
|
*
|
2010-11-12 23:16:33 +00:00
|
|
|
|
* @element INPUT
|
|
|
|
|
|
* @css ng-validation-error
|
|
|
|
|
|
*
|
2011-02-01 00:21:29 +00:00
|
|
|
|
* @example
|
2010-11-18 07:12:38 +00:00
|
|
|
|
* This example shows how the input element becomes red when it contains invalid input. Correct
|
|
|
|
|
|
* the input to make the error disappear.
|
|
|
|
|
|
*
|
2011-02-01 00:21:29 +00:00
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
I don't validate:
|
|
|
|
|
|
<input type="text" name="value" value="NotANumber"><br/>
|
2010-11-18 07:12:38 +00:00
|
|
|
|
|
2011-02-01 00:21:29 +00:00
|
|
|
|
I need an integer or nothing:
|
|
|
|
|
|
<input type="text" name="value" ng:validate="integer"><br/>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
it('should check ng:validate', function(){
|
|
|
|
|
|
expect(element('.doc-example-live :input:last').attr('className')).
|
|
|
|
|
|
toMatch(/ng-validation-error/);
|
2010-11-18 07:12:38 +00:00
|
|
|
|
|
2011-02-01 00:21:29 +00:00
|
|
|
|
input('value').enter('123');
|
|
|
|
|
|
expect(element('.doc-example-live :input:last').attr('className')).
|
|
|
|
|
|
not().toMatch(/ng-validation-error/);
|
|
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-11-12 23:16:33 +00:00
|
|
|
|
*/
|
|
|
|
|
|
/**
|
2010-11-19 00:28:42 +00:00
|
|
|
|
* @workInProgress
|
2010-11-18 07:12:38 +00:00
|
|
|
|
* @ngdoc widget
|
|
|
|
|
|
* @name angular.widget.@ng:required
|
2010-11-12 23:16:33 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @description
|
2010-11-18 07:12:38 +00:00
|
|
|
|
* The `ng:required` attribute widget validates that the user input is present. It is a special case
|
|
|
|
|
|
* of the {@link angular.widget.@ng:validate ng:validate} attribute widget.
|
2011-01-13 18:35:26 +00:00
|
|
|
|
*
|
2010-11-12 23:16:33 +00:00
|
|
|
|
* @element INPUT
|
|
|
|
|
|
* @css ng-validation-error
|
|
|
|
|
|
*
|
2011-02-01 00:21:29 +00:00
|
|
|
|
* @example
|
2010-11-18 07:12:38 +00:00
|
|
|
|
* This example shows how the input element becomes red when it contains invalid input. Correct
|
|
|
|
|
|
* the input to make the error disappear.
|
|
|
|
|
|
*
|
2011-02-01 00:21:29 +00:00
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
I cannot be blank: <input type="text" name="value" ng:required><br/>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
it('should check ng:required', function(){
|
|
|
|
|
|
expect(element('.doc-example-live :input').attr('className')).toMatch(/ng-validation-error/);
|
|
|
|
|
|
input('value').enter('123');
|
|
|
|
|
|
expect(element('.doc-example-live :input').attr('className')).not().toMatch(/ng-validation-error/);
|
|
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-11-12 23:16:33 +00:00
|
|
|
|
*/
|
|
|
|
|
|
/**
|
2010-11-19 00:28:42 +00:00
|
|
|
|
* @workInProgress
|
2010-11-18 07:12:38 +00:00
|
|
|
|
* @ngdoc widget
|
|
|
|
|
|
* @name angular.widget.@ng:format
|
2010-11-12 23:16:33 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @description
|
2010-11-18 07:12:38 +00:00
|
|
|
|
* The `ng:format` attribute widget formats stored data to user-readable text and parses the text
|
|
|
|
|
|
* back to the stored form. You might find this useful for example if you collect user input in a
|
|
|
|
|
|
* text field but need to store the data in the model as a list. Check out
|
|
|
|
|
|
* {@link angular.formatter formatters} to learn more.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} formatter The name of the built-in or custom {@link angular.formatter formatter}
|
|
|
|
|
|
* to be used.
|
|
|
|
|
|
*
|
2010-11-12 23:16:33 +00:00
|
|
|
|
* @element INPUT
|
|
|
|
|
|
*
|
2011-02-01 00:21:29 +00:00
|
|
|
|
* @example
|
2010-11-18 07:12:38 +00:00
|
|
|
|
* This example shows how the user input is converted from a string and internally represented as an
|
|
|
|
|
|
* array.
|
|
|
|
|
|
*
|
2011-02-01 00:21:29 +00:00
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
Enter a comma separated list of items:
|
|
|
|
|
|
<input type="text" name="list" ng:format="list" value="table, chairs, plate">
|
|
|
|
|
|
<pre>list={{list}}</pre>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
it('should check ng:format', function(){
|
|
|
|
|
|
expect(binding('list')).toBe('list=["table","chairs","plate"]');
|
|
|
|
|
|
input('list').enter(',,, a ,,,');
|
|
|
|
|
|
expect(binding('list')).toBe('list=["a"]');
|
|
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-11-12 23:16:33 +00:00
|
|
|
|
*/
|
2010-03-30 03:25:42 +00:00
|
|
|
|
function valueAccessor(scope, element) {
|
2010-07-13 18:20:11 +00:00
|
|
|
|
var validatorName = element.attr('ng:validate') || NOOP,
|
2010-03-26 23:27:18 +00:00
|
|
|
|
validator = compileValidator(validatorName),
|
2010-07-13 18:20:11 +00:00
|
|
|
|
requiredExpr = element.attr('ng:required'),
|
|
|
|
|
|
formatterName = element.attr('ng:format') || NOOP,
|
2011-01-13 18:35:26 +00:00
|
|
|
|
formatter = compileFormatter(formatterName),
|
2010-10-12 06:14:39 +00:00
|
|
|
|
format, parse, lastError, required,
|
2011-01-04 19:53:23 +00:00
|
|
|
|
invalidWidgets = scope.$service('$invalidWidgets') || {markValid:noop, markInvalid:noop};
|
2010-03-26 23:27:18 +00:00
|
|
|
|
if (!validator) throw "Validator named '" + validatorName + "' not found.";
|
2010-05-11 03:24:20 +00:00
|
|
|
|
format = formatter.format;
|
|
|
|
|
|
parse = formatter.parse;
|
2010-06-03 00:13:10 +00:00
|
|
|
|
if (requiredExpr) {
|
2010-06-03 18:03:11 +00:00
|
|
|
|
scope.$watch(requiredExpr, function(newValue) {
|
|
|
|
|
|
required = newValue;
|
|
|
|
|
|
validate();
|
|
|
|
|
|
});
|
2010-06-03 00:13:10 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
required = requiredExpr === '';
|
|
|
|
|
|
}
|
2010-05-11 03:24:20 +00:00
|
|
|
|
|
2010-12-02 04:29:54 +00:00
|
|
|
|
element.data($$validate, validate);
|
2010-05-11 03:24:20 +00:00
|
|
|
|
return {
|
|
|
|
|
|
get: function(){
|
|
|
|
|
|
if (lastError)
|
2011-03-26 23:06:38 +00:00
|
|
|
|
elementError(element, NG_VALIDATION_ERROR, null);
|
2010-05-11 03:24:20 +00:00
|
|
|
|
try {
|
2011-01-13 18:35:26 +00:00
|
|
|
|
var value = parse(scope, element.val());
|
2010-05-12 22:25:16 +00:00
|
|
|
|
validate();
|
|
|
|
|
|
return value;
|
2010-05-11 03:24:20 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
lastError = e;
|
|
|
|
|
|
elementError(element, NG_VALIDATION_ERROR, e);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
set: function(value) {
|
2010-05-11 03:41:12 +00:00
|
|
|
|
var oldValue = element.val(),
|
2011-01-13 18:35:26 +00:00
|
|
|
|
newValue = format(scope, value);
|
2010-05-11 03:24:20 +00:00
|
|
|
|
if (oldValue != newValue) {
|
2010-05-11 03:41:12 +00:00
|
|
|
|
element.val(newValue || ''); // needed for ie
|
2010-05-11 03:24:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
validate();
|
2010-04-17 00:03:06 +00:00
|
|
|
|
}
|
2010-05-11 03:24:20 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function validate() {
|
2010-05-11 03:41:12 +00:00
|
|
|
|
var value = trim(element.val());
|
2010-04-23 00:11:56 +00:00
|
|
|
|
if (element[0].disabled || element[0].readOnly) {
|
2011-03-26 23:06:38 +00:00
|
|
|
|
elementError(element, NG_VALIDATION_ERROR, null);
|
2010-04-16 21:01:29 +00:00
|
|
|
|
invalidWidgets.markValid(element);
|
2010-05-11 03:24:20 +00:00
|
|
|
|
} else {
|
2010-07-15 20:13:21 +00:00
|
|
|
|
var error, validateScope = inherit(scope, {$element:element});
|
2011-02-12 16:58:11 +00:00
|
|
|
|
error = required && !value
|
|
|
|
|
|
? 'Required'
|
2011-03-26 23:06:38 +00:00
|
|
|
|
: (value ? validator(validateScope, value) : null);
|
2010-03-30 22:39:51 +00:00
|
|
|
|
elementError(element, NG_VALIDATION_ERROR, error);
|
2010-03-26 23:27:18 +00:00
|
|
|
|
lastError = error;
|
2010-04-21 21:29:05 +00:00
|
|
|
|
if (error) {
|
2010-04-07 21:13:10 +00:00
|
|
|
|
invalidWidgets.markInvalid(element);
|
2010-04-21 21:29:05 +00:00
|
|
|
|
} else {
|
2010-04-07 21:13:10 +00:00
|
|
|
|
invalidWidgets.markValid(element);
|
2010-04-21 21:29:05 +00:00
|
|
|
|
}
|
2010-01-12 01:32:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2010-03-26 23:27:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2010-03-30 03:25:42 +00:00
|
|
|
|
function checkedAccessor(scope, element) {
|
2010-04-13 21:25:12 +00:00
|
|
|
|
var domElement = element[0], elementValue = domElement.value;
|
2010-03-26 23:27:18 +00:00
|
|
|
|
return {
|
2010-03-30 03:25:42 +00:00
|
|
|
|
get: function(){
|
|
|
|
|
|
return !!domElement.checked;
|
|
|
|
|
|
},
|
|
|
|
|
|
set: function(value){
|
2010-04-13 21:25:12 +00:00
|
|
|
|
domElement.checked = toBoolean(value);
|
2010-03-30 03:25:42 +00:00
|
|
|
|
}
|
2010-03-26 23:27:18 +00:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2010-03-30 03:25:42 +00:00
|
|
|
|
function radioAccessor(scope, element) {
|
2010-03-26 23:27:18 +00:00
|
|
|
|
var domElement = element[0];
|
|
|
|
|
|
return {
|
2010-03-30 03:25:42 +00:00
|
|
|
|
get: function(){
|
2011-03-26 23:06:38 +00:00
|
|
|
|
return domElement.checked ? domElement.value : null;
|
2010-03-30 03:25:42 +00:00
|
|
|
|
},
|
|
|
|
|
|
set: function(value){
|
|
|
|
|
|
domElement.checked = value == domElement.value;
|
|
|
|
|
|
}
|
2010-03-26 23:27:18 +00:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2010-03-30 03:25:42 +00:00
|
|
|
|
function optionsAccessor(scope, element) {
|
2011-01-13 18:35:26 +00:00
|
|
|
|
var formatterName = element.attr('ng:format') || NOOP,
|
|
|
|
|
|
formatter = compileFormatter(formatterName);
|
2010-03-26 23:27:18 +00:00
|
|
|
|
return {
|
|
|
|
|
|
get: function(){
|
|
|
|
|
|
var values = [];
|
2011-01-13 18:35:26 +00:00
|
|
|
|
forEach(element[0].options, function(option){
|
|
|
|
|
|
if (option.selected) values.push(formatter.parse(scope, option.value));
|
2010-03-26 23:27:18 +00:00
|
|
|
|
});
|
|
|
|
|
|
return values;
|
|
|
|
|
|
},
|
|
|
|
|
|
set: function(values){
|
|
|
|
|
|
var keys = {};
|
2011-01-13 18:35:26 +00:00
|
|
|
|
forEach(values, function(value){
|
|
|
|
|
|
keys[formatter.format(scope, value)] = true;
|
|
|
|
|
|
});
|
|
|
|
|
|
forEach(element[0].options, function(option){
|
2010-03-26 23:27:18 +00:00
|
|
|
|
option.selected = keys[option.value];
|
2010-01-12 01:32:33 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2010-03-26 23:27:18 +00:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function noopAccessor() { return { get: noop, set: noop }; }
|
|
|
|
|
|
|
2011-01-13 18:35:26 +00:00
|
|
|
|
/*
|
|
|
|
|
|
* TODO: refactor
|
|
|
|
|
|
*
|
|
|
|
|
|
* The table bellow is not quite right. In some cases the formatter is on the model side
|
|
|
|
|
|
* and in some cases it is on the view side. This is a historical artifact
|
|
|
|
|
|
*
|
|
|
|
|
|
* The concept of model/view accessor is useful for anyone who is trying to develop UI, and
|
|
|
|
|
|
* so it should be exposed to others. There should be a form object which keeps track of the
|
|
|
|
|
|
* accessors and also acts as their factory. It should expose it as an object and allow
|
|
|
|
|
|
* the validator to publish errors to it, so that the the error messages can be bound to it.
|
|
|
|
|
|
*
|
|
|
|
|
|
*/
|
2010-12-10 21:55:18 +00:00
|
|
|
|
var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, initWidgetValue(), true),
|
2010-04-02 18:10:36 +00:00
|
|
|
|
buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop),
|
2010-03-26 23:27:18 +00:00
|
|
|
|
INPUT_TYPE = {
|
|
|
|
|
|
'text': textWidget,
|
|
|
|
|
|
'textarea': textWidget,
|
|
|
|
|
|
'hidden': textWidget,
|
|
|
|
|
|
'password': textWidget,
|
|
|
|
|
|
'button': buttonWidget,
|
|
|
|
|
|
'submit': buttonWidget,
|
|
|
|
|
|
'reset': buttonWidget,
|
|
|
|
|
|
'image': buttonWidget,
|
2010-05-13 23:40:41 +00:00
|
|
|
|
'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)),
|
|
|
|
|
|
'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit),
|
2011-03-26 23:06:38 +00:00
|
|
|
|
'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(null)),
|
2011-01-13 18:35:26 +00:00
|
|
|
|
'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([]))
|
2010-03-26 23:27:18 +00:00
|
|
|
|
// 'file': fileWidget???
|
|
|
|
|
|
};
|
2010-02-18 04:50:13 +00:00
|
|
|
|
|
2010-12-02 04:29:54 +00:00
|
|
|
|
|
2010-04-02 18:10:36 +00:00
|
|
|
|
function initWidgetValue(initValue) {
|
|
|
|
|
|
return function (model, view) {
|
2010-05-11 03:24:20 +00:00
|
|
|
|
var value = view.get();
|
2010-05-11 03:41:12 +00:00
|
|
|
|
if (!value && isDefined(initValue)) {
|
2010-05-11 03:24:20 +00:00
|
|
|
|
value = copy(initValue);
|
2010-05-11 03:41:12 +00:00
|
|
|
|
}
|
2010-05-11 03:24:20 +00:00
|
|
|
|
if (isUndefined(model.get()) && isDefined(value)) {
|
2010-04-02 18:10:36 +00:00
|
|
|
|
model.set(value);
|
2010-05-11 03:24:20 +00:00
|
|
|
|
}
|
2010-04-02 18:10:36 +00:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2010-04-02 18:49:48 +00:00
|
|
|
|
function radioInit(model, view, element) {
|
|
|
|
|
|
var modelValue = model.get(), viewValue = view.get(), input = element[0];
|
2010-05-13 20:57:39 +00:00
|
|
|
|
input.checked = false;
|
2010-04-02 18:49:48 +00:00
|
|
|
|
input.name = this.$id + '@' + input.name;
|
2010-05-13 20:57:39 +00:00
|
|
|
|
if (isUndefined(modelValue)) {
|
2011-03-26 23:06:38 +00:00
|
|
|
|
model.set(modelValue = null);
|
2010-05-13 20:57:39 +00:00
|
|
|
|
}
|
2011-03-26 23:06:38 +00:00
|
|
|
|
if (modelValue == null && viewValue !== null) {
|
2010-05-13 20:57:39 +00:00
|
|
|
|
model.set(viewValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
view.set(modelValue);
|
2010-04-02 18:10:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2010-11-12 23:16:33 +00:00
|
|
|
|
/**
|
2010-11-19 00:28:42 +00:00
|
|
|
|
* @workInProgress
|
2010-11-12 23:16:33 +00:00
|
|
|
|
* @ngdoc directive
|
|
|
|
|
|
* @name angular.directive.ng:change
|
|
|
|
|
|
*
|
|
|
|
|
|
* @description
|
|
|
|
|
|
* The directive executes an expression whenever the input widget changes.
|
2011-01-13 18:35:26 +00:00
|
|
|
|
*
|
2010-11-12 23:16:33 +00:00
|
|
|
|
* @element INPUT
|
|
|
|
|
|
* @param {expression} expression to execute.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @example
|
2011-02-01 00:21:29 +00:00
|
|
|
|
* @example
|
|
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
<div ng:init="checkboxCount=0; textCount=0"></div>
|
|
|
|
|
|
<input type="text" name="text" ng:change="textCount = 1 + textCount">
|
|
|
|
|
|
changeCount {{textCount}}<br/>
|
|
|
|
|
|
<input type="checkbox" name="checkbox" ng:change="checkboxCount = 1 + checkboxCount">
|
|
|
|
|
|
changeCount {{checkboxCount}}<br/>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
it('should check ng:change', function(){
|
|
|
|
|
|
expect(binding('textCount')).toBe('0');
|
|
|
|
|
|
expect(binding('checkboxCount')).toBe('0');
|
|
|
|
|
|
|
|
|
|
|
|
using('.doc-example-live').input('text').enter('abc');
|
|
|
|
|
|
expect(binding('textCount')).toBe('1');
|
|
|
|
|
|
expect(binding('checkboxCount')).toBe('0');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
using('.doc-example-live').input('checkbox').check();
|
|
|
|
|
|
expect(binding('textCount')).toBe('1');
|
|
|
|
|
|
expect(binding('checkboxCount')).toBe('1');
|
|
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-11-12 23:16:33 +00:00
|
|
|
|
*/
|
2010-12-10 21:55:18 +00:00
|
|
|
|
function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) {
|
2011-04-18 23:33:30 +00:00
|
|
|
|
return annotate('$updateView', '$defer', function($updateView, $defer, element) {
|
2010-03-26 23:27:18 +00:00
|
|
|
|
var scope = this,
|
|
|
|
|
|
model = modelAccessor(scope, element),
|
2010-03-30 03:25:42 +00:00
|
|
|
|
view = viewAccessor(scope, element),
|
2010-07-13 22:21:42 +00:00
|
|
|
|
action = element.attr('ng:change') || '',
|
2010-05-12 22:25:16 +00:00
|
|
|
|
lastValue;
|
2010-12-10 06:09:46 +00:00
|
|
|
|
if (model) {
|
|
|
|
|
|
initFn.call(scope, model, view, element);
|
|
|
|
|
|
this.$eval(element.attr('ng:init')||'');
|
2010-12-10 21:55:18 +00:00
|
|
|
|
element.bind(events, function(event){
|
|
|
|
|
|
function handler(){
|
2010-12-10 06:09:46 +00:00
|
|
|
|
var value = view.get();
|
2010-12-10 21:55:18 +00:00
|
|
|
|
if (!textBox || value != lastValue) {
|
2010-12-10 06:09:46 +00:00
|
|
|
|
model.set(value);
|
|
|
|
|
|
lastValue = model.get();
|
|
|
|
|
|
scope.$tryEval(action, element);
|
2010-12-10 21:55:18 +00:00
|
|
|
|
$updateView();
|
2010-12-10 06:09:46 +00:00
|
|
|
|
}
|
2010-12-10 21:55:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
event.type == 'keydown' ? $defer(handler) : handler();
|
|
|
|
|
|
});
|
2010-12-10 06:09:46 +00:00
|
|
|
|
scope.$watch(model.get, function(value){
|
|
|
|
|
|
if (lastValue !== value) {
|
|
|
|
|
|
view.set(lastValue = value);
|
2010-12-09 00:14:18 +00:00
|
|
|
|
}
|
2010-04-04 00:04:36 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2010-12-10 21:55:18 +00:00
|
|
|
|
});
|
2010-03-26 23:27:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function inputWidgetSelector(element){
|
2010-03-30 21:55:04 +00:00
|
|
|
|
this.directives(true);
|
2011-01-13 18:50:33 +00:00
|
|
|
|
this.descend(true);
|
2010-03-26 23:27:18 +00:00
|
|
|
|
return INPUT_TYPE[lowercase(element[0].type)] || noop;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2010-07-20 23:55:32 +00:00
|
|
|
|
angularWidget('input', inputWidgetSelector);
|
|
|
|
|
|
angularWidget('textarea', inputWidgetSelector);
|
|
|
|
|
|
angularWidget('button', inputWidgetSelector);
|
2010-04-02 18:10:36 +00:00
|
|
|
|
|
2011-04-19 23:34:49 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @workInProgress
|
|
|
|
|
|
* @ngdoc directive
|
|
|
|
|
|
* @name angular.directive.ng:options
|
|
|
|
|
|
*
|
|
|
|
|
|
* @description
|
|
|
|
|
|
* Dynamically generate a list of `<option>` elements for a `<select>` element using the array
|
|
|
|
|
|
* obtained by evaluating the `ng:options` expression.
|
|
|
|
|
|
*
|
|
|
|
|
|
* When an item in the select menu is select, the array element represented by the selected option
|
|
|
|
|
|
* will be bound to the model identified by the `name` attribute of the parent select element.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
|
|
|
|
|
|
* be nested into the `<select>` element. This element will then represent `null` or "not selected"
|
|
|
|
|
|
* option. See example below for demonstration.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Note: `ng:options` provides iterator facility for `<option>` element which must be used instead
|
|
|
|
|
|
* of {@link angular.widget.@ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with
|
|
|
|
|
|
* `<option>` element because of the following reasons:
|
|
|
|
|
|
*
|
|
|
|
|
|
* * value attribute of the option element that we need to bind to requires a string, but the
|
|
|
|
|
|
* source of data for the iteration might be in a form of array containing objects instead of
|
|
|
|
|
|
* strings
|
|
|
|
|
|
* * {@link angular.widget.@ng:repeat ng:repeat} unrolls after the select binds causing
|
|
|
|
|
|
* incorect rendering on most browsers.
|
|
|
|
|
|
* * binding to a value not in list confuses most browsers.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @element select
|
|
|
|
|
|
* @param {comprehension_expression} comprehension _expresion_ `for` _item_ `in` _array_.
|
|
|
|
|
|
*
|
|
|
|
|
|
* * _array_: an expression which evaluates to an array of objects to bind.
|
|
|
|
|
|
* * _item_: local variable which will reffer to the item in the _array_ during the itteration
|
|
|
|
|
|
* * _expression_: The result of this expression will is `option` label. The
|
|
|
|
|
|
* `expression` most likely reffers to the _item_ varibale.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @example
|
|
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
function MyCntrl(){
|
|
|
|
|
|
this.colors = [
|
|
|
|
|
|
{name:'black'},
|
|
|
|
|
|
{name:'white'},
|
|
|
|
|
|
{name:'red'},
|
|
|
|
|
|
{name:'blue'},
|
|
|
|
|
|
{name:'green'}
|
|
|
|
|
|
];
|
|
|
|
|
|
this.color = this.colors[2]; // red
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<div ng:controller="MyCntrl">
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li ng:repeat="color in colors">
|
|
|
|
|
|
Name: <input name="color.name"/> [<a href ng:click="colors.$remove(color)">X</a>]
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<li>
|
|
|
|
|
|
[<a href ng:click="colors.push({})">add</a>]
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<hr/>
|
|
|
|
|
|
Color (null not allowed):
|
|
|
|
|
|
<select name="color" ng:options="c.name for c in colors"></select><br/>
|
2010-12-02 04:29:54 +00:00
|
|
|
|
|
2011-04-19 23:34:49 +00:00
|
|
|
|
Color (null allowed):
|
|
|
|
|
|
<select name="color" ng:options="c.name for c in colors">
|
|
|
|
|
|
<option value="">-- chose color --</option>
|
|
|
|
|
|
</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;"
|
|
|
|
|
|
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');
|
|
|
|
|
|
select('color').option('');
|
|
|
|
|
|
expect(binding('color')).toMatch('null');
|
|
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-12-02 04:29:54 +00:00
|
|
|
|
*/
|
2011-04-19 23:34:49 +00:00
|
|
|
|
|
|
|
|
|
|
var NG_OPTIONS_REGEXP = /^(.*)\s+for\s+([\$\w][\$\w\d]*)\s+in\s+(.*)$/;
|
|
|
|
|
|
angularWidget('select', function(element){
|
2010-09-21 17:20:34 +00:00
|
|
|
|
this.descend(true);
|
|
|
|
|
|
this.directives(true);
|
2011-04-19 23:34:49 +00:00
|
|
|
|
var isMultiselect = element.attr('multiple');
|
|
|
|
|
|
var expression = element.attr('ng:options');
|
|
|
|
|
|
var match;
|
|
|
|
|
|
if (!expression) {
|
|
|
|
|
|
return inputWidgetSelector.call(this, element);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (! (match = expression.match(NG_OPTIONS_REGEXP))) {
|
|
|
|
|
|
throw Error(
|
|
|
|
|
|
"Expected ng:options in form of '_expresion_ for _item_ in _collection_' but got '" +
|
|
|
|
|
|
expression + "'.");
|
|
|
|
|
|
}
|
|
|
|
|
|
var displayFn = expressionCompile(match[1]).fnSelf;
|
|
|
|
|
|
var itemName = match[2];
|
|
|
|
|
|
var collectionFn = expressionCompile(match[3]).fnSelf;
|
|
|
|
|
|
// we can't just jqLite('<option>') since jqLite is not smart enough
|
|
|
|
|
|
// to create it in <select> and IE barfs otherwise.
|
|
|
|
|
|
var option = jqLite(document.createElement('option'));
|
|
|
|
|
|
return function(select){
|
|
|
|
|
|
var scope = this;
|
|
|
|
|
|
var optionElements = [];
|
|
|
|
|
|
var optionTexts = [];
|
|
|
|
|
|
var lastSelectValue = isMultiselect ? {} : false;
|
|
|
|
|
|
var nullOption = option.clone().val('');
|
|
|
|
|
|
var missingOption = option.clone().val('?');
|
|
|
|
|
|
var model = modelAccessor(scope, element);
|
|
|
|
|
|
|
|
|
|
|
|
// find existing special options
|
|
|
|
|
|
forEach(select.children(), function(option){
|
|
|
|
|
|
if (option.value == '') nullOption = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
select.bind('change', function(){
|
|
|
|
|
|
var collection = collectionFn(scope) || [];
|
|
|
|
|
|
var value = select.val();
|
|
|
|
|
|
var index, length;
|
|
|
|
|
|
if (isMultiselect) {
|
|
|
|
|
|
value = [];
|
|
|
|
|
|
for (index = 0, length = optionElements.length; index < length; index++) {
|
|
|
|
|
|
if (optionElements[index][0].selected) {
|
|
|
|
|
|
value.push(collection[index]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (value == '?') {
|
|
|
|
|
|
value = undefined;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
value = (value == '' ? null : collection[value]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!isUndefined(value)) model.set(value);
|
|
|
|
|
|
scope.$tryEval(function(){
|
|
|
|
|
|
scope.$root.$eval();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
scope.$onEval(function(){
|
|
|
|
|
|
var scope = this;
|
|
|
|
|
|
var collection = collectionFn(scope) || [];
|
|
|
|
|
|
var value;
|
|
|
|
|
|
var length;
|
|
|
|
|
|
var fragment;
|
|
|
|
|
|
var index;
|
|
|
|
|
|
var optionText;
|
|
|
|
|
|
var optionElement;
|
|
|
|
|
|
var optionScope = scope.$new();
|
|
|
|
|
|
var modelValue = model.get();
|
|
|
|
|
|
var currentItem;
|
|
|
|
|
|
var selectValue = '';
|
|
|
|
|
|
var isMulti = isMultiselect;
|
|
|
|
|
|
|
|
|
|
|
|
if (isMulti) {
|
|
|
|
|
|
selectValue = new HashMap();
|
|
|
|
|
|
if (modelValue && isNumber(length = modelValue.length)) {
|
|
|
|
|
|
for (index = 0; index < length; index++) {
|
|
|
|
|
|
selectValue.put(modelValue[index], true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
for (index = 0, length = collection.length; index < length; index++) {
|
|
|
|
|
|
currentItem = optionScope[itemName] = collection[index];
|
|
|
|
|
|
optionText = displayFn(optionScope);
|
|
|
|
|
|
if (optionTexts.length > index) {
|
|
|
|
|
|
// reuse
|
|
|
|
|
|
optionElement = optionElements[index];
|
|
|
|
|
|
if (optionText != optionTexts[index]) {
|
|
|
|
|
|
(optionElement).text(optionTexts[index] = optionText);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// grow
|
|
|
|
|
|
if (!fragment) {
|
|
|
|
|
|
fragment = document.createDocumentFragment();
|
|
|
|
|
|
}
|
|
|
|
|
|
optionTexts.push(optionText);
|
|
|
|
|
|
optionElements.push(optionElement = option.clone());
|
|
|
|
|
|
optionElement.attr('value', index).text(optionText);
|
|
|
|
|
|
fragment.appendChild(optionElement[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isMulti) {
|
|
|
|
|
|
if (lastSelectValue[index] != (value = selectValue.remove(currentItem))) {
|
|
|
|
|
|
optionElement[0].selected = !!(lastSelectValue[index] = value);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (modelValue == currentItem) {
|
|
|
|
|
|
selectValue = index;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2010-12-02 04:29:54 +00:00
|
|
|
|
}
|
2011-04-19 23:34:49 +00:00
|
|
|
|
if (fragment) select.append(jqLite(fragment));
|
|
|
|
|
|
// shrink children
|
|
|
|
|
|
while(optionElements.length > index) {
|
|
|
|
|
|
optionElements.pop().remove();
|
|
|
|
|
|
delete lastSelectValue[optionElements.length];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isMulti) {
|
|
|
|
|
|
if (selectValue === '' && modelValue) {
|
|
|
|
|
|
// We could not find a match
|
|
|
|
|
|
selectValue = '?';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// update the selected item
|
|
|
|
|
|
if (lastSelectValue !== selectValue) {
|
|
|
|
|
|
if (nullOption) {
|
|
|
|
|
|
if (lastSelectValue == '') nullOption.remove();
|
|
|
|
|
|
if (selectValue === '') select.prepend(nullOption);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (missingOption) {
|
|
|
|
|
|
if (lastSelectValue == '?') missingOption.remove();
|
|
|
|
|
|
if (selectValue === '?') select.prepend(missingOption);
|
2011-01-13 18:35:26 +00:00
|
|
|
|
}
|
2011-04-19 23:34:49 +00:00
|
|
|
|
|
|
|
|
|
|
select.val(lastSelectValue = selectValue);
|
2011-01-13 18:35:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2011-04-19 23:34:49 +00:00
|
|
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
optionScope = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2010-09-21 17:20:34 +00:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2010-11-04 21:24:31 +00:00
|
|
|
|
/**
|
2010-11-19 00:28:42 +00:00
|
|
|
|
* @workInProgress
|
2010-11-04 21:24:31 +00:00
|
|
|
|
* @ngdoc widget
|
|
|
|
|
|
* @name angular.widget.ng:include
|
2010-10-27 22:31:10 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @description
|
2010-11-04 21:24:31 +00:00
|
|
|
|
* Include external HTML fragment.
|
2011-01-13 18:35:26 +00:00
|
|
|
|
*
|
|
|
|
|
|
* Keep in mind that Same Origin Policy applies to included resources
|
2010-11-04 21:24:31 +00:00
|
|
|
|
* (e.g. ng:include won't work for file:// access).
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} src expression evaluating to URL.
|
2011-01-13 18:35:26 +00:00
|
|
|
|
* @param {Scope=} [scope=new_child_scope] optional expression which evaluates to an
|
2010-12-22 23:44:27 +00:00
|
|
|
|
* instance of angular.scope to set the HTML fragment to.
|
2010-11-16 19:31:41 +00:00
|
|
|
|
* @param {string=} onload Expression to evaluate when a new partial is loaded.
|
2010-10-27 22:31:10 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @example
|
2011-02-01 00:21:29 +00:00
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
<select name="url">
|
2011-06-07 20:53:05 +00:00
|
|
|
|
<option value="api/angular.filter.date.html">date filter</option>
|
|
|
|
|
|
<option value="api/angular.filter.html.html">html filter</option>
|
2011-02-01 00:21:29 +00:00
|
|
|
|
<option value="">(blank)</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<tt>url = <a href="{{url}}">{{url}}</a></tt>
|
|
|
|
|
|
<hr/>
|
|
|
|
|
|
<ng:include src="url"></ng:include>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
it('should load date filter', function(){
|
2011-03-03 17:52:35 +00:00
|
|
|
|
expect(element('.doc-example-live ng\\:include').text()).toMatch(/angular\.filter\.date/);
|
2011-02-01 00:21:29 +00:00
|
|
|
|
});
|
2011-04-19 22:24:20 +00:00
|
|
|
|
it('should change to html filter', function(){
|
2011-06-07 20:53:05 +00:00
|
|
|
|
select('url').option('api/angular.filter.html.html');
|
2011-03-03 17:52:35 +00:00
|
|
|
|
expect(element('.doc-example-live ng\\:include').text()).toMatch(/angular\.filter\.html/);
|
2011-02-01 00:21:29 +00:00
|
|
|
|
});
|
|
|
|
|
|
it('should change to blank', function(){
|
2011-03-03 17:52:35 +00:00
|
|
|
|
select('url').option('');
|
|
|
|
|
|
expect(element('.doc-example-live ng\\:include').text()).toEqual('');
|
2011-02-01 00:21:29 +00:00
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-10-27 22:31:10 +00:00
|
|
|
|
*/
|
2010-07-20 23:55:32 +00:00
|
|
|
|
angularWidget('ng:include', function(element){
|
2010-04-02 18:10:36 +00:00
|
|
|
|
var compiler = this,
|
2010-04-16 21:01:29 +00:00
|
|
|
|
srcExp = element.attr("src"),
|
2010-11-16 19:31:41 +00:00
|
|
|
|
scopeExp = element.attr("scope") || '',
|
|
|
|
|
|
onloadExp = element[0].getAttribute('onload') || ''; //workaround for jquery bug #7537
|
2010-07-02 22:39:47 +00:00
|
|
|
|
if (element[0]['ng:compiled']) {
|
2010-04-07 17:17:15 +00:00
|
|
|
|
this.descend(true);
|
|
|
|
|
|
this.directives(true);
|
|
|
|
|
|
} else {
|
2010-07-02 22:39:47 +00:00
|
|
|
|
element[0]['ng:compiled'] = true;
|
Introduced injector and $new to scope, and injection into link methods and controllers
- added angular.injector(scope, services, instanceCache) which returns inject
- inject method can return, instance, or call function which have $inject
property
- initialize services with $creation=[eager|eager-publish] this means that
only some of the services are now globally accessible
- upgraded $become on scope to use injector hence respect the $inject property
for injection
- $become should not be run multiple times and will most likely be removed
in future version
- added $new on scope to create a child scope
- $inject is respected on constructor function
- simplified scopes so that they no longer have separate __proto__ for
parent, api, behavior and instance this should speed up execution since
scope will now create one __proto__ chain per scope (not three).
BACKWARD COMPATIBILITY WARNING:
- services now need to have $inject instead of inject property for proper
injection this breaks backward compatibility
- not all services are now published into root scope
(only: $location, $cookie, $window)
- if you have widget/directive which uses services on scope
(such as this.$xhr), you will now have to inject that service in
(as it is not published on the root scope anymore)
2010-10-09 00:30:13 +00:00
|
|
|
|
return extend(function(xhr, element){
|
2010-04-07 17:17:15 +00:00
|
|
|
|
var scope = this, childScope;
|
2010-04-16 21:01:29 +00:00
|
|
|
|
var changeCounter = 0;
|
2011-01-24 06:24:53 +00:00
|
|
|
|
var preventRecursion = false;
|
2010-04-16 21:01:29 +00:00
|
|
|
|
function incrementChange(){ changeCounter++;}
|
|
|
|
|
|
this.$watch(srcExp, incrementChange);
|
|
|
|
|
|
this.$watch(scopeExp, incrementChange);
|
2011-01-24 21:27:28 +00:00
|
|
|
|
|
|
|
|
|
|
// note that this propagates eval to the current childScope, where childScope is dynamically
|
|
|
|
|
|
// bound (via $route.onChange callback) to the current scope created by $route
|
2011-01-24 06:24:53 +00:00
|
|
|
|
scope.$onEval(function(){
|
|
|
|
|
|
if (childScope && !preventRecursion) {
|
|
|
|
|
|
preventRecursion = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
childScope.$eval();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
preventRecursion = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2010-04-16 21:01:29 +00:00
|
|
|
|
this.$watch(function(){return changeCounter;}, function(){
|
|
|
|
|
|
var src = this.$eval(srcExp),
|
2010-11-16 19:31:41 +00:00
|
|
|
|
useScope = this.$eval(scopeExp);
|
|
|
|
|
|
|
2010-04-16 21:01:29 +00:00
|
|
|
|
if (src) {
|
2011-03-30 16:35:59 +00:00
|
|
|
|
xhr('GET', src, null, function(code, response){
|
2010-04-16 21:01:29 +00:00
|
|
|
|
element.html(response);
|
2011-01-24 06:22:42 +00:00
|
|
|
|
childScope = useScope || createScope(scope);
|
2011-02-12 18:13:28 +00:00
|
|
|
|
compiler.compile(element)(childScope);
|
2010-11-16 19:31:41 +00:00
|
|
|
|
scope.$eval(onloadExp);
|
2011-03-30 16:35:59 +00:00
|
|
|
|
}, false, true);
|
2010-08-16 23:47:39 +00:00
|
|
|
|
} else {
|
2010-10-05 17:50:16 +00:00
|
|
|
|
childScope = null;
|
2010-08-16 23:47:39 +00:00
|
|
|
|
element.html('');
|
2010-04-16 21:01:29 +00:00
|
|
|
|
}
|
2010-04-07 17:17:15 +00:00
|
|
|
|
});
|
Introduced injector and $new to scope, and injection into link methods and controllers
- added angular.injector(scope, services, instanceCache) which returns inject
- inject method can return, instance, or call function which have $inject
property
- initialize services with $creation=[eager|eager-publish] this means that
only some of the services are now globally accessible
- upgraded $become on scope to use injector hence respect the $inject property
for injection
- $become should not be run multiple times and will most likely be removed
in future version
- added $new on scope to create a child scope
- $inject is respected on constructor function
- simplified scopes so that they no longer have separate __proto__ for
parent, api, behavior and instance this should speed up execution since
scope will now create one __proto__ chain per scope (not three).
BACKWARD COMPATIBILITY WARNING:
- services now need to have $inject instead of inject property for proper
injection this breaks backward compatibility
- not all services are now published into root scope
(only: $location, $cookie, $window)
- if you have widget/directive which uses services on scope
(such as this.$xhr), you will now have to inject that service in
(as it is not published on the root scope anymore)
2010-10-09 00:30:13 +00:00
|
|
|
|
}, {$inject:['$xhr.cache']});
|
2010-04-07 17:17:15 +00:00
|
|
|
|
}
|
2010-04-02 18:10:36 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2010-11-04 21:24:31 +00:00
|
|
|
|
/**
|
2010-11-19 00:28:42 +00:00
|
|
|
|
* @workInProgress
|
2010-11-04 21:24:31 +00:00
|
|
|
|
* @ngdoc widget
|
|
|
|
|
|
* @name angular.widget.ng:switch
|
|
|
|
|
|
*
|
|
|
|
|
|
* @description
|
|
|
|
|
|
* Conditionally change the DOM structure.
|
2011-01-13 18:35:26 +00:00
|
|
|
|
*
|
2010-11-04 21:24:31 +00:00
|
|
|
|
* @usageContent
|
2010-11-11 00:08:54 +00:00
|
|
|
|
* <any ng:switch-when="matchValue1">...</any>
|
|
|
|
|
|
* <any ng:switch-when="matchValue2">...</any>
|
2010-11-04 21:24:31 +00:00
|
|
|
|
* ...
|
2010-11-11 00:08:54 +00:00
|
|
|
|
* <any ng:switch-default>...</any>
|
2011-01-13 18:35:26 +00:00
|
|
|
|
*
|
2010-11-04 21:24:31 +00:00
|
|
|
|
* @param {*} on expression to match against <tt>ng:switch-when</tt>.
|
2011-01-13 18:35:26 +00:00
|
|
|
|
* @paramDescription
|
2010-11-05 22:05:24 +00:00
|
|
|
|
* On child elments add:
|
2011-01-13 18:35:26 +00:00
|
|
|
|
*
|
2010-11-05 22:05:24 +00:00
|
|
|
|
* * `ng:switch-when`: the case statement to match against. If match then this
|
|
|
|
|
|
* case will be displayed.
|
2010-11-11 00:08:54 +00:00
|
|
|
|
* * `ng:switch-default`: the default case when no other casses match.
|
2010-11-04 21:24:31 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @example
|
2011-02-01 00:21:29 +00:00
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
<select name="switch">
|
|
|
|
|
|
<option>settings</option>
|
|
|
|
|
|
<option>home</option>
|
|
|
|
|
|
<option>other</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<tt>switch={{switch}}</tt>
|
|
|
|
|
|
</hr>
|
|
|
|
|
|
<ng:switch on="switch" >
|
|
|
|
|
|
<div ng:switch-when="settings">Settings Div</div>
|
|
|
|
|
|
<span ng:switch-when="home">Home Span</span>
|
|
|
|
|
|
<span ng:switch-default>default</span>
|
|
|
|
|
|
</ng:switch>
|
|
|
|
|
|
</code>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
it('should start in settings', function(){
|
2011-03-03 17:52:35 +00:00
|
|
|
|
expect(element('.doc-example-live ng\\:switch').text()).toEqual('Settings Div');
|
2011-02-01 00:21:29 +00:00
|
|
|
|
});
|
|
|
|
|
|
it('should change to home', function(){
|
|
|
|
|
|
select('switch').option('home');
|
2011-03-03 17:52:35 +00:00
|
|
|
|
expect(element('.doc-example-live ng\\:switch').text()).toEqual('Home Span');
|
2011-02-01 00:21:29 +00:00
|
|
|
|
});
|
|
|
|
|
|
it('should select deafault', function(){
|
|
|
|
|
|
select('switch').option('other');
|
2011-03-03 17:52:35 +00:00
|
|
|
|
expect(element('.doc-example-live ng\\:switch').text()).toEqual('default');
|
2011-02-01 00:21:29 +00:00
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-11-04 21:24:31 +00:00
|
|
|
|
*/
|
2011-02-15 06:12:45 +00:00
|
|
|
|
//TODO(im): remove all the code related to using and inline equals
|
2010-07-20 23:55:32 +00:00
|
|
|
|
var ngSwitch = angularWidget('ng:switch', function (element){
|
2010-04-02 18:10:36 +00:00
|
|
|
|
var compiler = this,
|
2010-04-05 18:46:53 +00:00
|
|
|
|
watchExpr = element.attr("on"),
|
2010-04-21 19:50:05 +00:00
|
|
|
|
usingExpr = (element.attr("using") || 'equals'),
|
2010-05-10 17:36:02 +00:00
|
|
|
|
usingExprParams = usingExpr.split(":"),
|
2010-04-21 19:50:05 +00:00
|
|
|
|
usingFn = ngSwitch[usingExprParams.shift()],
|
2010-04-07 17:17:15 +00:00
|
|
|
|
changeExpr = element.attr('change') || '',
|
2010-04-05 18:46:53 +00:00
|
|
|
|
cases = [];
|
2010-04-21 19:50:05 +00:00
|
|
|
|
if (!usingFn) throw "Using expression '" + usingExpr + "' unknown.";
|
2010-11-11 00:08:54 +00:00
|
|
|
|
if (!watchExpr) throw "Missing 'on' attribute.";
|
2010-04-05 18:46:53 +00:00
|
|
|
|
eachNode(element, function(caseElement){
|
2010-07-02 22:39:47 +00:00
|
|
|
|
var when = caseElement.attr('ng:switch-when');
|
2010-11-11 00:08:54 +00:00
|
|
|
|
var switchCase = {
|
2010-04-07 17:17:15 +00:00
|
|
|
|
change: changeExpr,
|
2010-04-05 18:46:53 +00:00
|
|
|
|
element: caseElement,
|
|
|
|
|
|
template: compiler.compile(caseElement)
|
2010-11-11 00:08:54 +00:00
|
|
|
|
};
|
|
|
|
|
|
if (isString(when)) {
|
|
|
|
|
|
switchCase.when = function(scope, value){
|
|
|
|
|
|
var args = [value, when];
|
2011-01-08 06:02:23 +00:00
|
|
|
|
forEach(usingExprParams, function(arg){
|
2010-11-11 00:08:54 +00:00
|
|
|
|
args.push(arg);
|
|
|
|
|
|
});
|
|
|
|
|
|
return usingFn.apply(scope, args);
|
|
|
|
|
|
};
|
|
|
|
|
|
cases.unshift(switchCase);
|
|
|
|
|
|
} else if (isString(caseElement.attr('ng:switch-default'))) {
|
|
|
|
|
|
switchCase.when = valueFn(true);
|
|
|
|
|
|
cases.push(switchCase);
|
2010-04-05 18:46:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2010-04-21 19:50:05 +00:00
|
|
|
|
|
2010-04-23 05:44:48 +00:00
|
|
|
|
// this needs to be here for IE
|
2011-01-08 06:02:23 +00:00
|
|
|
|
forEach(cases, function(_case){
|
2010-04-23 05:44:48 +00:00
|
|
|
|
_case.element.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2010-04-05 18:46:53 +00:00
|
|
|
|
element.html('');
|
|
|
|
|
|
return function(element){
|
2010-04-07 17:17:15 +00:00
|
|
|
|
var scope = this, childScope;
|
2010-04-02 18:10:36 +00:00
|
|
|
|
this.$watch(watchExpr, function(value){
|
2010-11-11 00:08:54 +00:00
|
|
|
|
var found = false;
|
2010-04-05 18:46:53 +00:00
|
|
|
|
element.html('');
|
2010-04-09 23:20:15 +00:00
|
|
|
|
childScope = createScope(scope);
|
2011-01-08 06:02:23 +00:00
|
|
|
|
forEach(cases, function(switchCase){
|
2010-11-11 00:08:54 +00:00
|
|
|
|
if (!found && switchCase.when(childScope, value)) {
|
|
|
|
|
|
found = true;
|
2010-04-07 17:17:15 +00:00
|
|
|
|
childScope.$tryEval(switchCase.change, element);
|
2011-02-14 00:13:21 +00:00
|
|
|
|
switchCase.template(childScope, function(caseElement){
|
|
|
|
|
|
element.append(caseElement);
|
|
|
|
|
|
});
|
2010-04-02 18:10:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2010-04-07 17:17:15 +00:00
|
|
|
|
scope.$onEval(function(){
|
|
|
|
|
|
if (childScope) childScope.$eval();
|
|
|
|
|
|
});
|
2010-04-02 18:10:36 +00:00
|
|
|
|
};
|
2010-04-07 17:17:15 +00:00
|
|
|
|
}, {
|
|
|
|
|
|
equals: function(on, when) {
|
2010-11-11 00:08:54 +00:00
|
|
|
|
return ''+on == when;
|
2011-02-15 06:12:45 +00:00
|
|
|
|
}
|
2010-04-02 18:10:36 +00:00
|
|
|
|
});
|
2010-09-30 15:07:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
* Modifies the default behavior of html A tag, so that the default action is prevented when href
|
|
|
|
|
|
* attribute is empty.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The reasoning for this change is to allow easy creation of action links with ng:click without
|
|
|
|
|
|
* changing the location or causing page reloads, e.g.:
|
|
|
|
|
|
* <a href="" ng:click="model.$save()">Save</a>
|
|
|
|
|
|
*/
|
2010-11-18 06:32:35 +00:00
|
|
|
|
angularWidget('a', function() {
|
2010-09-30 15:07:36 +00:00
|
|
|
|
this.descend(true);
|
|
|
|
|
|
this.directives(true);
|
|
|
|
|
|
|
|
|
|
|
|
return function(element) {
|
2011-05-31 12:51:47 +00:00
|
|
|
|
var hasNgHref = ((element.attr('ng:bind-attr') || '').indexOf('"href":') !== -1);
|
|
|
|
|
|
|
|
|
|
|
|
// turn <a href ng:click="..">link</a> into a link in IE
|
|
|
|
|
|
// but only if it doesn't have name attribute, in which case it's an anchor
|
|
|
|
|
|
if (!hasNgHref && !element.attr('name') && !element.attr('href')) {
|
|
|
|
|
|
element.attr('href', '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (element.attr('href') === '' && !hasNgHref) {
|
2010-09-30 15:07:36 +00:00
|
|
|
|
element.bind('click', function(event){
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2010-11-18 06:32:35 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2010-11-19 00:28:42 +00:00
|
|
|
|
* @workInProgress
|
2010-11-18 06:32:35 +00:00
|
|
|
|
* @ngdoc widget
|
|
|
|
|
|
* @name angular.widget.@ng:repeat
|
|
|
|
|
|
*
|
|
|
|
|
|
* @description
|
2011-04-08 23:03:39 +00:00
|
|
|
|
* The `ng:repeat` widget instantiates a template once per item from a collection. The collection is
|
2011-05-19 15:38:23 +00:00
|
|
|
|
* enumerated with the `ng:repeat-index` attribute, starting from 0. Each template instance gets
|
|
|
|
|
|
* its own scope, where the given loop variable is set to the current collection item, and `$index`
|
2011-04-08 23:03:39 +00:00
|
|
|
|
* is set to the item index or key.
|
2010-11-18 06:32:35 +00:00
|
|
|
|
*
|
2011-04-08 23:03:39 +00:00
|
|
|
|
* Special properties are exposed on the local scope of each template instance, including:
|
2010-11-18 06:32:35 +00:00
|
|
|
|
*
|
|
|
|
|
|
* * `$index` – `{number}` – iterator offset of the repeated element (0..length-1)
|
2011-05-19 15:38:23 +00:00
|
|
|
|
* * `$position` – `{string}` – position of the repeated element in the iterator. One of:
|
2011-04-08 23:03:39 +00:00
|
|
|
|
* * `'first'`,
|
2011-05-19 15:38:23 +00:00
|
|
|
|
* * `'middle'`
|
2011-04-08 23:03:39 +00:00
|
|
|
|
* * `'last'`
|
2010-11-18 06:32:35 +00:00
|
|
|
|
*
|
2011-04-08 23:03:39 +00:00
|
|
|
|
* Note: Although `ng:repeat` looks like a directive, it is actually an attribute widget.
|
2010-11-18 06:32:35 +00:00
|
|
|
|
*
|
|
|
|
|
|
* @element ANY
|
|
|
|
|
|
* @param {string} repeat_expression The expression indicating how to enumerate a collection. Two
|
|
|
|
|
|
* formats are currently supported:
|
|
|
|
|
|
*
|
|
|
|
|
|
* * `variable in expression` – where variable is the user defined loop variable and `expression`
|
|
|
|
|
|
* is a scope expression giving the collection to enumerate.
|
|
|
|
|
|
*
|
|
|
|
|
|
* For example: `track in cd.tracks`.
|
2011-04-08 23:03:39 +00:00
|
|
|
|
*
|
2010-11-18 06:32:35 +00:00
|
|
|
|
* * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers,
|
|
|
|
|
|
* and `expression` is the scope expression giving the collection to enumerate.
|
|
|
|
|
|
*
|
|
|
|
|
|
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
|
|
|
|
|
|
*
|
2011-02-01 00:21:29 +00:00
|
|
|
|
* @example
|
2010-11-18 06:32:35 +00:00
|
|
|
|
* This example initializes the scope to a list of names and
|
2011-04-08 23:03:39 +00:00
|
|
|
|
* then uses `ng:repeat` to display every person:
|
2011-02-01 00:21:29 +00:00
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
<div ng:init="friends = [{name:'John', age:25}, {name:'Mary', age:28}]">
|
|
|
|
|
|
I have {{friends.length}} friends. They are:
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li ng:repeat="friend in friends">
|
|
|
|
|
|
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
it('should check ng:repeat', function(){
|
|
|
|
|
|
var r = using('.doc-example-live').repeater('ul li');
|
|
|
|
|
|
expect(r.count()).toBe(2);
|
|
|
|
|
|
expect(r.row(0)).toEqual(["1","John","25"]);
|
|
|
|
|
|
expect(r.row(1)).toEqual(["2","Mary","28"]);
|
|
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-11-18 06:32:35 +00:00
|
|
|
|
*/
|
2011-02-14 00:13:21 +00:00
|
|
|
|
angularWidget('@ng:repeat', function(expression, element){
|
2010-11-18 06:32:35 +00:00
|
|
|
|
element.removeAttr('ng:repeat');
|
2011-02-14 00:13:21 +00:00
|
|
|
|
element.replaceWith(jqLite('<!-- ng:repeat: ' + expression + ' --!>'));
|
2011-02-07 23:29:56 +00:00
|
|
|
|
var linker = this.compile(element);
|
2011-02-14 00:13:21 +00:00
|
|
|
|
return function(iterStartElement){
|
2010-11-18 06:32:35 +00:00
|
|
|
|
var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
|
|
|
|
|
|
lhs, rhs, valueIdent, keyIdent;
|
|
|
|
|
|
if (! match) {
|
2011-04-19 23:34:49 +00:00
|
|
|
|
throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" +
|
2010-11-18 06:32:35 +00:00
|
|
|
|
expression + "'.");
|
|
|
|
|
|
}
|
|
|
|
|
|
lhs = match[1];
|
|
|
|
|
|
rhs = match[2];
|
|
|
|
|
|
match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/);
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
|
throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
|
|
|
|
|
|
keyValue + "'.");
|
|
|
|
|
|
}
|
|
|
|
|
|
valueIdent = match[3] || match[1];
|
|
|
|
|
|
keyIdent = match[2];
|
|
|
|
|
|
|
|
|
|
|
|
var children = [], currentScope = this;
|
|
|
|
|
|
this.$onEval(function(){
|
|
|
|
|
|
var index = 0,
|
|
|
|
|
|
childCount = children.length,
|
2011-02-14 00:13:21 +00:00
|
|
|
|
lastIterElement = iterStartElement,
|
|
|
|
|
|
collection = this.$tryEval(rhs, iterStartElement),
|
2011-03-27 22:58:24 +00:00
|
|
|
|
collectionLength = size(collection, true),
|
2011-03-29 00:51:51 +00:00
|
|
|
|
fragment = (element[0].nodeName != 'OPTION') ? document.createDocumentFragment() : null,
|
|
|
|
|
|
addFragment,
|
2010-11-18 06:32:35 +00:00
|
|
|
|
childScope,
|
|
|
|
|
|
key;
|
|
|
|
|
|
|
|
|
|
|
|
for (key in collection) {
|
2011-02-16 16:53:11 +00:00
|
|
|
|
if (collection.hasOwnProperty(key)) {
|
2010-11-18 06:32:35 +00:00
|
|
|
|
if (index < childCount) {
|
|
|
|
|
|
// reuse existing child
|
|
|
|
|
|
childScope = children[index];
|
|
|
|
|
|
childScope[valueIdent] = collection[key];
|
|
|
|
|
|
if (keyIdent) childScope[keyIdent] = key;
|
2011-02-14 00:13:21 +00:00
|
|
|
|
lastIterElement = childScope.$element;
|
2011-03-26 22:23:41 +00:00
|
|
|
|
childScope.$eval();
|
2010-11-18 06:32:35 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// grow children
|
2011-02-07 23:29:56 +00:00
|
|
|
|
childScope = createScope(currentScope);
|
2010-11-18 06:32:35 +00:00
|
|
|
|
childScope[valueIdent] = collection[key];
|
|
|
|
|
|
if (keyIdent) childScope[keyIdent] = key;
|
|
|
|
|
|
childScope.$index = index;
|
2011-02-12 16:58:11 +00:00
|
|
|
|
childScope.$position = index == 0
|
|
|
|
|
|
? 'first'
|
|
|
|
|
|
: (index == collectionLength - 1 ? 'last' : 'middle');
|
2010-11-18 06:32:35 +00:00
|
|
|
|
children.push(childScope);
|
2011-02-14 00:13:21 +00:00
|
|
|
|
linker(childScope, function(clone){
|
|
|
|
|
|
clone.attr('ng:repeat-index', index);
|
2011-03-29 00:51:51 +00:00
|
|
|
|
|
|
|
|
|
|
if (fragment) {
|
|
|
|
|
|
fragment.appendChild(clone[0]);
|
|
|
|
|
|
addFragment = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
//temporarily preserve old way for option element
|
|
|
|
|
|
lastIterElement.after(clone);
|
|
|
|
|
|
lastIterElement = clone;
|
|
|
|
|
|
}
|
2011-02-14 00:13:21 +00:00
|
|
|
|
});
|
2010-11-18 06:32:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
index ++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2011-03-29 00:51:51 +00:00
|
|
|
|
|
|
|
|
|
|
//attach new nodes buffered in doc fragment
|
|
|
|
|
|
if (addFragment) {
|
|
|
|
|
|
lastIterElement.after(jqLite(fragment));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2010-11-18 06:32:35 +00:00
|
|
|
|
// shrink children
|
|
|
|
|
|
while(children.length > index) {
|
|
|
|
|
|
children.pop().$element.remove();
|
|
|
|
|
|
}
|
2011-02-14 00:13:21 +00:00
|
|
|
|
}, iterStartElement);
|
2010-11-18 06:32:35 +00:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2010-11-19 00:28:42 +00:00
|
|
|
|
* @workInProgress
|
2010-11-18 06:32:35 +00:00
|
|
|
|
* @ngdoc widget
|
|
|
|
|
|
* @name angular.widget.@ng:non-bindable
|
|
|
|
|
|
*
|
|
|
|
|
|
* @description
|
|
|
|
|
|
* Sometimes it is necessary to write code which looks like bindings but which should be left alone
|
|
|
|
|
|
* by angular. Use `ng:non-bindable` to make angular ignore a chunk of HTML.
|
|
|
|
|
|
*
|
|
|
|
|
|
* NOTE: `ng:non-bindable` looks like a directive, but is actually an attribute widget.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @element ANY
|
|
|
|
|
|
*
|
2011-02-01 00:21:29 +00:00
|
|
|
|
* @example
|
2010-11-18 06:32:35 +00:00
|
|
|
|
* In this example there are two location where a siple binding (`{{}}`) is present, but the one
|
|
|
|
|
|
* wrapped in `ng:non-bindable` is left alone.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @example
|
2011-02-01 00:21:29 +00:00
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
<div>Normal: {{1 + 2}}</div>
|
|
|
|
|
|
<div ng:non-bindable>Ignored: {{1 + 2}}</div>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
it('should check ng:non-bindable', function(){
|
|
|
|
|
|
expect(using('.doc-example-live').binding('1 + 2')).toBe('3');
|
|
|
|
|
|
expect(using('.doc-example-live').element('div:last').text()).
|
|
|
|
|
|
toMatch(/1 \+ 2/);
|
|
|
|
|
|
});
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2010-11-18 06:32:35 +00:00
|
|
|
|
*/
|
|
|
|
|
|
angularWidget("@ng:non-bindable", noop);
|
2011-01-19 22:50:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @ngdoc widget
|
|
|
|
|
|
* @name angular.widget.ng:view
|
|
|
|
|
|
*
|
|
|
|
|
|
* @description
|
|
|
|
|
|
* # Overview
|
|
|
|
|
|
* `ng:view` is a widget that complements the {@link angular.service.$route $route} service by
|
|
|
|
|
|
* including the rendered template of the current route into the main layout (`index.html`) file.
|
|
|
|
|
|
* Every time the current route changes, the included view changes with it according to the
|
|
|
|
|
|
* configuration of the `$route` service.
|
|
|
|
|
|
*
|
2011-05-18 12:16:09 +00:00
|
|
|
|
* This widget provides functionality similar to {@link angular.widget.ng:include ng:include} when
|
2011-01-19 22:50:29 +00:00
|
|
|
|
* used like this:
|
|
|
|
|
|
*
|
|
|
|
|
|
* <ng:include src="$route.current.template" scope="$route.current.scope"></ng:include>
|
|
|
|
|
|
*
|
|
|
|
|
|
*
|
|
|
|
|
|
* # Advantages
|
|
|
|
|
|
* Compared to `ng:include`, `ng:view` offers these advantages:
|
|
|
|
|
|
*
|
|
|
|
|
|
* - shorter syntax
|
|
|
|
|
|
* - more efficient execution
|
|
|
|
|
|
* - doesn't require `$route` service to be available on the root scope
|
|
|
|
|
|
*
|
|
|
|
|
|
*
|
2011-02-01 00:21:29 +00:00
|
|
|
|
* @example
|
|
|
|
|
|
<doc:example>
|
|
|
|
|
|
<doc:source>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
function MyCtrl($route) {
|
|
|
|
|
|
$route.when('/overview', {controller: OverviewCtrl, template: 'guide.overview.html'});
|
|
|
|
|
|
$route.when('/bootstrap', {controller: BootstrapCtrl, template: 'guide.bootstrap.html'});
|
|
|
|
|
|
console.log(window.$route = $route);
|
|
|
|
|
|
};
|
|
|
|
|
|
MyCtrl.$inject = ['$route'];
|
2011-01-19 22:50:29 +00:00
|
|
|
|
|
2011-02-01 00:21:29 +00:00
|
|
|
|
function BootstrapCtrl(){}
|
|
|
|
|
|
function OverviewCtrl(){}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<div ng:controller="MyCtrl">
|
|
|
|
|
|
<a href="#/overview">overview</a> | <a href="#/bootstrap">bootstrap</a> | <a href="#/undefined">undefined</a><br/>
|
|
|
|
|
|
The view is included below:
|
|
|
|
|
|
<hr/>
|
|
|
|
|
|
<ng:view></ng:view>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</doc:source>
|
|
|
|
|
|
<doc:scenario>
|
|
|
|
|
|
</doc:scenario>
|
|
|
|
|
|
</doc:example>
|
2011-01-19 22:50:29 +00:00
|
|
|
|
*/
|
|
|
|
|
|
angularWidget('ng:view', function(element) {
|
|
|
|
|
|
var compiler = this;
|
|
|
|
|
|
|
|
|
|
|
|
if (!element[0]['ng:compiled']) {
|
|
|
|
|
|
element[0]['ng:compiled'] = true;
|
2011-04-18 23:33:30 +00:00
|
|
|
|
return annotate('$xhr.cache', '$route', function($xhr, $route, element){
|
2011-01-24 21:27:28 +00:00
|
|
|
|
var parentScope = this,
|
|
|
|
|
|
childScope;
|
|
|
|
|
|
|
2011-01-19 22:50:29 +00:00
|
|
|
|
$route.onChange(function(){
|
2011-01-24 21:27:28 +00:00
|
|
|
|
var src;
|
2011-01-19 22:50:29 +00:00
|
|
|
|
|
|
|
|
|
|
if ($route.current) {
|
|
|
|
|
|
src = $route.current.template;
|
2011-01-24 21:27:28 +00:00
|
|
|
|
childScope = $route.current.scope;
|
2011-01-19 22:50:29 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (src) {
|
2011-04-04 22:28:21 +00:00
|
|
|
|
//xhr's callback must be async, see commit history for more info
|
|
|
|
|
|
$xhr('GET', src, function(code, response){
|
2011-01-19 22:50:29 +00:00
|
|
|
|
element.html(response);
|
2011-02-12 18:13:28 +00:00
|
|
|
|
compiler.compile(element)(childScope);
|
2011-04-04 22:28:21 +00:00
|
|
|
|
});
|
2011-01-19 22:50:29 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
element.html('');
|
|
|
|
|
|
}
|
2011-02-04 21:29:45 +00:00
|
|
|
|
})(); //initialize the state forcefully, it's possible that we missed the initial
|
|
|
|
|
|
//$route#onChange already
|
2011-01-24 21:27:28 +00:00
|
|
|
|
|
|
|
|
|
|
// note that this propagates eval to the current childScope, where childScope is dynamically
|
|
|
|
|
|
// bound (via $route.onChange callback) to the current scope created by $route
|
|
|
|
|
|
parentScope.$onEval(function() {
|
|
|
|
|
|
if (childScope) {
|
|
|
|
|
|
childScope.$eval();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2011-01-19 22:50:29 +00:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.descend(true);
|
|
|
|
|
|
this.directives(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|