aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMisko Hevery2011-11-22 21:28:39 -0800
committerMisko Hevery2012-01-25 11:50:37 -0800
commit9ee2cdff44e7d496774b340de816344126c457b3 (patch)
tree476ffcb4425e7160865029d6b57d41b766750285 /src
parent8af4fde18246ac1587b471a549e70d5d858bf0ee (diff)
downloadangular.js-9ee2cdff44e7d496774b340de816344126c457b3.tar.bz2
refactor(directives): connect new compiler
- turn everything into a directive
Diffstat (limited to 'src')
-rw-r--r--src/Angular.js13
-rw-r--r--src/AngularPublic.js45
-rw-r--r--src/Injector.js11
-rw-r--r--src/directives.js308
-rw-r--r--src/jqLite.js11
-rw-r--r--src/markups.js128
-rw-r--r--src/scenario/Scenario.js76
-rw-r--r--src/scenario/dsl.js6
-rw-r--r--src/service/compiler.js6
-rw-r--r--src/service/filter.js1
-rw-r--r--src/service/filter/filter.js6
-rw-r--r--src/service/filter/filters.js118
-rw-r--r--src/service/filter/orderBy.js2
-rw-r--r--src/widget/form.js51
-rw-r--r--src/widget/input.js89
-rw-r--r--src/widget/select.js554
-rw-r--r--src/widgets.js591
17 files changed, 925 insertions, 1091 deletions
diff --git a/src/Angular.js b/src/Angular.js
index 4a0589c3..f7c3e318 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -90,15 +90,7 @@ var $$scope = '$scope',
/** @name angular */
angular = window.angular || (window.angular = {}),
- angularModule = null,
- /** @name angular.markup */
- angularTextMarkup = extensionMap(angular, 'markup'),
- /** @name angular.attrMarkup */
- angularAttrMarkup = extensionMap(angular, 'attrMarkup'),
- /** @name angular.directive */
- angularDirective = extensionMap(angular, 'directive', lowercase),
- /** @name angular.widget */
- angularWidget = extensionMap(angular, 'widget', shivForIE),
+ angularModule,
/** @name angular.module.ng */
angularInputType = extensionMap(angular, 'inputType', lowercase),
nodeName_,
@@ -988,8 +980,7 @@ function assertArg(arg, name, reason) {
}
function assertArgFn(arg, name) {
- assertArg(arg, name);
assertArg(isFunction(arg), name, 'not a function, got ' +
- (typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg));
+ (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg));
return arg;
}
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index bfc50ef8..516bbad4 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -60,16 +60,43 @@ function publishExternalAPI(angular){
angularModule('ng', ['ngLocale'], ['$provide',
function ngModule($provide) {
- // TODO(misko): temporary services to get the compiler working;
- $provide.value('$textMarkup', angularTextMarkup);
- $provide.value('$attrMarkup', angularAttrMarkup);
- $provide.value('$directive', angularDirective);
- $provide.value('$widget', angularWidget);
-
$provide.service('$anchorScroll', $AnchorScrollProvider);
$provide.service('$browser', $BrowserProvider);
$provide.service('$cacheFactory', $CacheFactoryProvider);
- $provide.service('$compile', $CompileProvider);
+ $provide.service('$compile', $CompileProvider).
+ directive({
+ a: htmlAnchorDirective,
+ input: inputDirective,
+ textarea: inputDirective,
+ form: ngFormDirective,
+ select: selectDirective,
+ option: optionDirective,
+ ngBind: ngBindDirective,
+ ngBindHtml: ngBindHtmlDirective,
+ ngBindHtmlUnsafe: ngBindHtmlUnsafeDirective,
+ ngBindTemplate: ngBindTemplateDirective,
+ ngBindAttr: ngBindAttrDirective,
+ ngClass: ngClassDirective,
+ ngClassEven: ngClassEvenDirective,
+ ngClassOdd: ngClassOddDirective,
+ ngCloak: ngCloakDirective,
+ ngController: ngControllerDirective,
+ ngForm: ngFormDirective,
+ ngHide: ngHideDirective,
+ ngInclude: ngIncludeDirective,
+ ngInit: ngInitDirective,
+ ngNonBindable: ngNonBindableDirective,
+ ngPluralize: ngPluralizeDirective,
+ ngRepeat: ngRepeatDirective,
+ ngShow: ngShowDirective,
+ ngSubmit: ngSubmitDirective,
+ ngStyle: ngStyleDirective,
+ ngSwitch: ngSwitchDirective,
+ ngOptions: ngOptionsDirective,
+ ngView: ngViewDirective
+ }).
+ directive(ngEventDirectives).
+ directive(ngAttributeAliasDirectives);
$provide.service('$controller', $ControllerProvider);
$provide.service('$cookies', $CookiesProvider);
$provide.service('$cookieStore', $CookieStoreProvider);
@@ -89,9 +116,9 @@ function publishExternalAPI(angular){
$provide.service('$routeParams', $RouteParamsProvider);
$provide.service('$rootScope', $RootScopeProvider);
$provide.service('$q', $QProvider);
+ $provide.service('$sanitize', $SanitizeProvider);
$provide.service('$sniffer', $SnifferProvider);
$provide.service('$templateCache', $TemplateCacheProvider);
$provide.service('$window', $WindowProvider);
}]);
-}
-
+};
diff --git a/src/Injector.js b/src/Injector.js
index 38cf652d..94154bec 100644
--- a/src/Injector.js
+++ b/src/Injector.js
@@ -219,6 +219,7 @@ function inferInjectionArgs(fn) {
* - `Constructor`: a new instance of the provider will be created using
* {@link angular.module.AUTO.$injector#instantiate $injector.instantiate()}, then treated as `object`.
*
+ * @returns {Object} registered provider instance
*/
/**
@@ -232,6 +233,7 @@ function inferInjectionArgs(fn) {
* @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provide'` key.
* @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand for
* `$provide.service(name, {$get:$getFn})`.
+ * @returns {Object} registered provider instance
*/
@@ -246,6 +248,7 @@ function inferInjectionArgs(fn) {
* @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provide'` key.
* @param {function()} value The $getFn for the instance creation. Internally this is a short hand for
* `$provide.service(name, {$get:function(){ return value; }})`.
+ * @returns {Object} registered provider instance
*/
@@ -285,7 +288,7 @@ function createInjector(modulesToLoad) {
if (isObject(key)) {
forEach(key, reverseParams(delegate));
} else {
- delegate(key, value);
+ return delegate(key, value);
}
}
}
@@ -297,12 +300,12 @@ function createInjector(modulesToLoad) {
if (!provider.$get) {
throw Error('Provider ' + name + ' must define $get factory method.');
}
- providerCache[name + providerSuffix] = provider;
+ return providerCache[name + providerSuffix] = provider;
}
- function factory(name, factoryFn) { service(name, { $get:factoryFn }); }
+ function factory(name, factoryFn) { return service(name, { $get:factoryFn }); }
- function value(name, value) { factory(name, valueFn(value)); }
+ function value(name, value) { return factory(name, valueFn(value)); }
function decorator(serviceName, decorFn) {
var origProvider = providerInjector.get(serviceName + providerSuffix),
diff --git a/src/directives.js b/src/directives.js
index eb35addb..dc0a986f 100644
--- a/src/directives.js
+++ b/src/directives.js
@@ -58,10 +58,14 @@
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:init", function(expression){
- return function(element){
- this.$eval(expression);
- };
+var ngInitDirective = valueFn({
+ compile: function() {
+ return {
+ pre: function(scope, element, attrs) {
+ scope.$eval(attrs.ngInit);
+ }
+ }
+ }
});
/**
@@ -158,16 +162,24 @@ angularDirective("ng:init", function(expression){
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:controller", function(expression) {
- this.scope(true);
- return ['$controller', '$window', function($controller, $window) {
- var scope = this,
- Controller = getter(scope, expression, true) || getter($window, expression, true);
-
- assertArgFn(Controller, expression);
- $controller(Controller, scope);
- }];
-});
+var ngControllerDirective = ['$controller', '$window', function($controller, $window) {
+ return {
+ scope: true,
+ compile: function() {
+ return {
+ pre: function(scope, element, attr) {
+ var expression = attr.ngController,
+ Controller = getter(scope, expression, true) || getter($window, expression, true);
+
+ assertArgFn(Controller, expression);
+ $controller(Controller, scope);
+ }
+ };
+ }
+ }
+}];
+
+
/**
* @ngdoc directive
@@ -208,55 +220,30 @@ angularDirective("ng:controller", function(expression) {
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:bind", function(expression, element){
- element.addClass('ng-binding');
- return ['$exceptionHandler', '$parse', '$element', function($exceptionHandler, $parse, element) {
- var exprFn = $parse(expression),
- lastValue = Number.NaN,
- scope = this;
+var ngBindDirective = valueFn(function(scope, element, attr) {
+ element.addClass('ng-binding').data('$binding', attr.ngBind);
+ scope.$watch(attr.ngBind, function(value) {
+ element.text(value == undefined ? '' : value);
+ });
+});
- scope.$watch(function() {
- // TODO(misko): remove error handling https://github.com/angular/angular.js/issues/347
- var value, html, isHtml, isDomElement,
- hadOwnElement = scope.hasOwnProperty('$element'),
- oldElement = scope.$element;
- // TODO(misko): get rid of $element https://github.com/angular/angular.js/issues/348
- scope.$element = element;
- try {
- value = exprFn(scope);
- // If we are HTML than save the raw HTML data so that we don't recompute sanitization since
- // it is expensive.
- // TODO(misko): turn this into a more generic way to compute this
- if ((isHtml = (value instanceof HTML)))
- value = (html = value).html;
- if (lastValue === value) return;
- isDomElement = isElement(value);
- if (!isHtml && !isDomElement && isObject(value)) {
- value = toJson(value, true);
- }
- if (value != lastValue) {
- lastValue = value;
- if (isHtml) {
- element.html(html.get());
- } else if (isDomElement) {
- element.html('');
- element.append(value);
- } else {
- element.text(value == undefined ? '' : value);
- }
- }
- } catch (e) {
- $exceptionHandler(e);
- } finally {
- if (hadOwnElement) {
- scope.$element = oldElement;
- } else {
- delete scope.$element;
- }
+var ngBindHtmlUnsafeDirective = valueFn(function(scope, element, attr) {
+ element.addClass('ng-binding').data('$binding', attr.ngBindHtmlUnsafe);
+ scope.$watch(attr.ngBindHtmlUnsafe, function(value) {
+ element.html(value == undefined ? '' : value);
+ });
+});
+
+var ngBindHtmlDirective = ['$sanitize', function($sanitize) {
+ return function(scope, element, attr) {
+ element.addClass('ng-binding').data('$binding', attr.ngBindHtml);
+ scope.$watch(attr.ngBindHtml, function(value) {
+ if (value = $sanitize(value)) {
+ element.html(value);
}
});
- }];
-});
+ }
+}];
/**
@@ -292,32 +279,29 @@ angularDirective("ng:bind", function(expression, element){
</doc:source>
<doc:scenario>
it('should check ng:bind', function() {
- expect(using('.doc-example-live').binding('{{salutation}} {{name}}')).
- toBe('Hello World!');
+ expect(using('.doc-example-live').binding('salutation')).
+ toBe('Hello');
+ expect(using('.doc-example-live').binding('name')).
+ toBe('World');
using('.doc-example-live').input('salutation').enter('Greetings');
using('.doc-example-live').input('name').enter('user');
- expect(using('.doc-example-live').binding('{{salutation}} {{name}}')).
- toBe('Greetings user!');
+ expect(using('.doc-example-live').binding('salutation')).
+ toBe('Greetings');
+ expect(using('.doc-example-live').binding('name')).
+ toBe('user');
});
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:bind-template", function(expression, element){
- element.addClass('ng-binding');
- var templateFn = compileBindTemplate(expression);
- return function(element) {
- var lastValue,
- scope = this;
-
- scope.$watch(function() {
- var value = templateFn(scope, element, true);
- if (value != lastValue) {
- element.text(value);
- lastValue = value;
- }
+var ngBindTemplateDirective = ['$interpolate', function($interpolate) {
+ return function(scope, element, attr) {
+ var interpolateFn = $interpolate(attr.ngBindTemplate);
+ element.addClass('ng-binding').data('$binding', interpolateFn);
+ scope.$watch(interpolateFn, function(value) {
+ element.text(value);
});
- };
-});
+ }
+}];
/**
* @ngdoc directive
@@ -392,23 +376,25 @@ angularDirective("ng:bind-template", function(expression, element){
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:bind-attr", function(expression){
- return function(element){
- var lastValue = {},
- scope = this;
+var ngBindAttrDirective = ['$interpolate', function($interpolate) {
+ return function(scope, element, attr) {
+ var lastValue = {};
+ var interpolateFns = {};
scope.$watch(function() {
- var values = scope.$eval(expression);
+ var values = scope.$eval(attr.ngBindAttr);
for(var key in values) {
- var value = compileBindTemplate(values[key])(scope, element);
+ var exp = values[key],
+ fn = (interpolateFns[exp] ||
+ (interpolateFns[values[key]] = $interpolate(exp))),
+ value = fn(scope);
if (lastValue[key] !== value) {
- lastValue[key] = value;
- element.attr(key, BOOLEAN_ATTR[lowercase(key)] ? toBoolean(value) : value);
+ attr.$set(key, lastValue[key] = value);
}
}
});
- };
-});
+ }
+}];
/**
@@ -448,19 +434,33 @@ angularDirective("ng:bind-attr", function(expression){
*
* TODO: maybe we should consider allowing users to control event propagation in the future.
*/
-angularDirective("ng:click", function(expression, element){
- return function(element){
- var self = this;
- element.bind('click', function(event){
- self.$apply(expression);
+var ngEventDirectives = {};
+forEach('click dblclick mousedown mouseup mouseover mousemove'.split(' '), function(name) {
+ var directiveName = camelCase('ng-' + name);
+ ngEventDirectives[directiveName] = valueFn(function(scope, element, attr) {
+ element.bind(lowercase(name), function(event) {
+ scope.$apply(attr[directiveName]);
event.stopPropagation();
});
- };
+ });
});
/**
* @ngdoc directive
+ * @name angular.directive.ng:dblclick
+ *
+ * @description
+ * The ng:dblclick allows you to specify custom behavior when
+ * element is double-clicked.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate upon
+ * double-click.
+ */
+
+/**
+ * @ngdoc directive
* @name angular.directive.ng:submit
*
* @description
@@ -496,48 +496,42 @@ angularDirective("ng:click", function(expression, element){
</doc:source>
<doc:scenario>
it('should check ng:submit', function() {
- expect(binding('list')).toBe('list=[]');
+ expect(binding('list')).toBe('[]');
element('.doc-example-live #submit').click();
- expect(binding('list')).toBe('list=["hello"]');
+ expect(binding('list')).toBe('["hello"]');
expect(input('text').val()).toBe('');
});
it('should ignore empty strings', function() {
- expect(binding('list')).toBe('list=[]');
+ expect(binding('list')).toBe('[]');
element('.doc-example-live #submit').click();
element('.doc-example-live #submit').click();
- expect(binding('list')).toBe('list=["hello"]');
+ expect(binding('list')).toBe('["hello"]');
});
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:submit", function(expression, element) {
- return function(element) {
- var self = this;
- element.bind('submit', function() {
- self.$apply(expression);
- });
- };
+var ngSubmitDirective = valueFn(function(scope, element, attrs) {
+ element.bind('submit', function() {
+ scope.$apply(attrs.ngSubmit);
+ });
});
-function ngClass(selector) {
- return function(expression, element) {
- return function(element) {
- var scope = this;
- scope.$watch(expression, function(newVal, oldVal) {
- if (selector(scope.$index)) {
- if (oldVal && (newVal !== oldVal)) {
- if (isObject(oldVal) && !isArray(oldVal))
- oldVal = map(oldVal, function(v, k) { if (v) return k });
- element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal);
- }
- if (isObject(newVal) && !isArray(newVal))
+function classDirective(name, selector) {
+ name = 'ngClass' + name;
+ return valueFn(function(scope, element, attr) {
+ scope.$watch(attr[name], function(newVal, oldVal) {
+ if (selector === true || scope.$index % 2 === selector) {
+ if (oldVal && (newVal !== oldVal)) {
+ if (isObject(oldVal) && !isArray(oldVal))
+ oldVal = map(oldVal, function(v, k) { if (v) return k });
+ element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal);
+ }
+ if (isObject(newVal) && !isArray(newVal))
newVal = map(newVal, function(v, k) { if (v) return k });
- if (newVal) element.addClass(isArray(newVal) ? newVal.join(' ') : newVal);
- }
- });
- };
- };
+ if (newVal) element.addClass(isArray(newVal) ? newVal.join(' ') : newVal); }
+ });
+ });
}
/**
@@ -584,7 +578,7 @@ function ngClass(selector) {
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:class", ngClass(function() {return true;}));
+var ngClassDirective = classDirective('', true);
/**
* @ngdoc directive
@@ -624,7 +618,7 @@ angularDirective("ng:class", ngClass(function() {return true;}));
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;}));
+var ngClassOddDirective = classDirective('Odd', 0);
/**
* @ngdoc directive
@@ -663,7 +657,7 @@ angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;}));
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;}));
+var ngClassEvenDirective = classDirective('Even', 1);
/**
* @ngdoc directive
@@ -697,13 +691,11 @@ angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;}));
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:show", function(expression, element){
- return function(element) {
- var scope = this;
- scope.$watch(expression, function(value) {
- element.css('display', toBoolean(value) ? '' : 'none');
- });
- };
+//TODO(misko): refactor to remove element from the DOM
+var ngShowDirective = valueFn(function(scope, element, attr){
+ scope.$watch(attr.ngShow, function(value){
+ element.css('display', toBoolean(value) ? '' : 'none');
+ });
});
/**
@@ -738,13 +730,11 @@ angularDirective("ng:show", function(expression, element){
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:hide", function(expression, element){
- return function(element) {
- var scope = this;
- scope.$watch(expression, function(value) {
- element.css('display', toBoolean(value) ? 'none' : '');
- });
- };
+//TODO(misko): refactor to remove element from the DOM
+var ngHideDirective = valueFn(function(scope, element, attr){
+ scope.$watch(attr.ngHide, function(value){
+ element.css('display', toBoolean(value) ? 'none' : '');
+ });
});
/**
@@ -779,16 +769,13 @@ angularDirective("ng:hide", function(expression, element){
</doc:scenario>
</doc:example>
*/
-angularDirective("ng:style", function(expression, element) {
- return function(element) {
- var scope = this;
- scope.$watch(expression, function(newStyles, oldStyles) {
- if (oldStyles && (newStyles !== oldStyles)) {
- forEach(oldStyles, function(val, style) { element.css(style, '');});
- }
- if (newStyles) element.css(newStyles);
- });
- };
+var ngStyleDirective = valueFn(function(scope, element, attr) {
+ scope.$watch(attr.ngStyle, function(newStyles, oldStyles) {
+ if (oldStyles && (newStyles !== oldStyles)) {
+ forEach(oldStyles, function(val, style) { element.css(style, '');});
+ }
+ if (newStyles) element.css(newStyles);
+ });
});
@@ -845,7 +832,22 @@ angularDirective("ng:style", function(expression, element) {
</doc:example>
*
*/
-angularDirective("ng:cloak", function(expression, element) {
- element.removeAttr('ng:cloak');
- element.removeClass('ng-cloak');
+var ngCloakDirective = valueFn({
+ compile: function(element, attr) {
+ attr.$set(attr.$attr.ngCloak, undefined);
+ element.removeClass('ng-cloak');
+ }
});
+
+function ngAttributeAliasDirective(propName, attrName) {
+ ngAttributeAliasDirectives[camelCase('ng-' + attrName)] = ['$interpolate', function($interpolate) {
+ return function(scope, element, attr) {
+ scope.$watch($interpolate(attr[camelCase('ng-' + attrName)]), function(value) {
+ attr.$set(attrName, value);
+ });
+ }
+ }];
+}
+var ngAttributeAliasDirectives = {};
+forEach(BOOLEAN_ATTR, ngAttributeAliasDirective);
+ngAttributeAliasDirective(null, 'src');
diff --git a/src/jqLite.js b/src/jqLite.js
index e48d250b..2505a307 100644
--- a/src/jqLite.js
+++ b/src/jqLite.js
@@ -405,15 +405,14 @@ forEach({
text: extend((msie < 9)
? function(element, value) {
- // NodeType == 3 is text node
- if (element.nodeType == 3) {
- if (isUndefined(value))
- return element.nodeValue;
- element.nodeValue = value;
- } else {
+ if (element.nodeType == 1 /** Element */) {
if (isUndefined(value))
return element.innerText;
element.innerText = value;
+ } else {
+ if (isUndefined(value))
+ return element.nodeValue;
+ element.nodeValue = value;
}
}
: function(element, value) {
diff --git a/src/markups.js b/src/markups.js
index f6f2143a..9e3e16f0 100644
--- a/src/markups.js
+++ b/src/markups.js
@@ -1,109 +1,5 @@
'use strict';
-/**
- * @ngdoc overview
- * @name angular.markup
- * @description
- *
- * Angular markup transforms the content of DOM elements or portions of the content into other
- * text or DOM elements for further compilation.
- *
- * Markup extensions do not themselves produce linking functions. Think of markup as a way to
- * produce shorthand for a {@link angular.widget widget} or a {@link angular.directive directive}.
- *
- * The most prominent example of a markup in Angular is the built-in, double curly markup
- * `{{expression}}`, which is shorthand for `<span ng:bind="expression"></span>`.
- *
- * Create custom markup like this:
- *
- * <pre>
- * angular.markup('newMarkup', function(text, textNode, parentElement){
- * //tranformation code
- * });
- * </pre>
- *
- * For more information, see {@link guide/dev_guide.compiler.markup Understanding Angular Markup}
- * in the Angular Developer Guide.
- */
-
-/**
- * @ngdoc overview
- * @name angular.attrMarkup
- * @description
- *
- * Attribute markup allows you to modify the state of an attribute's text.
- *
- * Attribute markup extends the Angular complier in a way similar to {@link angular.markup},
- * which allows you to modify the content of a node.
- *
- * The most prominent example of an attribute markup in Angular is the built-in double curly markup
- * which is a shorthand for {@link angular.directive.ng:bind-attr ng:bind-attr}.
- *
- * ## Example
- *
- * <pre>
- * angular.attrMarkup('newAttrMarkup', function(attrValue, attrName, element){
- * //tranformation code
- * });
- * </pre>
- *
- * For more information about Angular attribute markup, see {@link guide/dev_guide.compiler.markup
- * Understanding Angular Markup} in the Angular Developer Guide.
- */
-
-
-angularTextMarkup('{{}}', function(text, textNode, parentElement) {
- var bindings = parseBindings(text),
- self = this;
- if (hasBindings(bindings)) {
- if (isLeafNode(parentElement[0])) {
- parentElement.attr('ng:bind-template', text);
- } else {
- var cursor = textNode, newElement;
- forEach(parseBindings(text), function(text){
- var exp = binding(text);
- if (exp) {
- newElement = jqLite('<span>');
- newElement.attr('ng:bind', exp);
- } else {
- newElement = jqLite(document.createTextNode(text));
- }
- if (msie && text.charAt(0) == ' ') {
- newElement = jqLite('<span>&nbsp;</span>');
- var nbsp = newElement.html();
- newElement.text(text.substr(1));
- newElement.html(nbsp + newElement.html());
- }
- cursor.after(newElement);
- cursor = newElement;
- });
- textNode.remove();
- }
- }
-});
-
-/**
- * This tries to normalize the behavior of value attribute across browsers. If value attribute is
- * not specified, then specify it to be that of the text.
- */
-angularTextMarkup('option', function(text, textNode, parentElement){
- if (lowercase(nodeName_(parentElement)) == 'option') {
- if (msie <= 7) {
- // In IE7 The issue is that there is no way to see if the value was specified hence
- // we have to resort to parsing HTML;
- htmlParser(parentElement[0].outerHTML, {
- start: function(tag, attrs) {
- if (isUndefined(attrs.value)) {
- parentElement.attr('value', text);
- }
- }
- });
- } else if (parentElement[0].getAttribute('value') == null) {
- // jQuery does normalization on 'value' so we have to bypass it.
- parentElement.attr('value', text);
- }
- }
-});
/**
* @ngdoc directive
@@ -171,7 +67,7 @@ angularTextMarkup('option', function(text, textNode, parentElement){
it('should execute ng:click but not reload when no href but name specified', function() {
element('#link-5').click();
expect(input('value').val()).toEqual('5');
- expect(element('#link-5').attr('href')).toBe(undefined);
+ expect(element('#link-5').attr('href')).toBe("");
});
it('should only change url when only ng:href', function() {
@@ -371,25 +267,3 @@ angularTextMarkup('option', function(text, textNode, parentElement){
* @param {template} template any string which can contain '{{}}' markup.
*/
-
-var NG_BIND_ATTR = 'ng:bind-attr';
-var SIDE_EFFECT_ATTRS = {};
-
-forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) {
- SIDE_EFFECT_ATTRS['ng:' + name] = name;
-});
-
-angularAttrMarkup('{{}}', function(value, name, element){
- // don't process existing attribute markup
- if (angularDirective(name) || angularDirective("@" + name)) return;
- if (msie && name == 'src')
- value = decodeURI(value);
- var bindings = parseBindings(value),
- bindAttr;
- if (hasBindings(bindings) || SIDE_EFFECT_ATTRS[name]) {
- element.removeAttr(name);
- bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}");
- bindAttr[SIDE_EFFECT_ATTRS[name] || name] = value;
- element.attr(NG_BIND_ATTR, toJson(bindAttr));
- }
-});
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
index a7979342..7e33181c 100644
--- a/src/scenario/Scenario.js
+++ b/src/scenario/Scenario.js
@@ -345,25 +345,71 @@ function browserTrigger(element, type, keys) {
* Finds all bindings with the substring match of name and returns an
* array of their values.
*
- * @param {string} name The name to match
+ * @param {string} bindExp The name to match
* @return {Array.<string>} String of binding values
*/
-_jQuery.fn.bindings = function(name) {
- function contains(text, value) {
- return value instanceof RegExp
- ? value.test(text)
- : text && text.indexOf(value) >= 0;
+_jQuery.fn.bindings = function(windowJquery, bindExp) {
+ var result = [], match,
+ bindSelector = '.ng-binding:visible';
+ if (angular.isString(bindExp)) {
+ bindExp = bindExp.replace(/\s/g, '');
+ match = function (actualExp) {
+ if (actualExp) {
+ actualExp = actualExp.replace(/\s/g, '');
+ if (actualExp == bindExp) return true;
+ if (actualExp.indexOf(bindExp) == 0) {
+ return actualExp.charAt(bindExp.length) == '|';
+ }
+ }
+ }
+ } else if (bindExp) {
+ match = function(actualExp) {
+ return actualExp && bindExp.exec(actualExp);
+ }
+ } else {
+ match = function(actualExp) {
+ return !!actualExp;
+ };
+ }
+ var selection = this.find(bindSelector);
+ if (this.is(bindSelector)) {
+ selection = selection.add(this);
}
- var result = [];
- this.find('.ng-binding:visible').each(function() {
- var element = new _jQuery(this);
- if (!angular.isDefined(name) ||
- contains(element.attr('ng:bind'), name) ||
- contains(element.attr('ng:bind-template'), name)) {
- if (element.is('input, textarea')) {
- result.push(element.val());
+
+ function push(value) {
+ if (value == undefined) {
+ value = '';
+ } else if (typeof value != 'string') {
+ value = angular.toJson(value);
+ }
+ result.push('' + value);
+ }
+
+ selection.each(function() {
+ var element = windowJquery(this),
+ binding;
+ if (binding = element.data('$binding')) {
+ if (typeof binding == 'string') {
+ if (match(binding)) {
+ push(element.scope().$eval(binding));
+ }
} else {
- result.push(element.html());
+ if (!angular.isArray(binding)) {
+ binding = [binding];
+ }
+ for(var fns, j=0, jj=binding.length; j<jj; j++) {
+ fns = binding[j];
+ if (fns.parts) {
+ fns = fns.parts;
+ } else {
+ fns = [fns];
+ }
+ for (var scope, fn, i = 0, ii = fns.length; i < ii; i++) {
+ if(match((fn = fns[i]).exp)) {
+ push(fn(scope = scope || element.scope()));
+ }
+ }
+ }
}
}
});
diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js
index bbe29948..fb0037e0 100644
--- a/src/scenario/dsl.js
+++ b/src/scenario/dsl.js
@@ -180,7 +180,7 @@ angular.scenario.dsl('using', function() {
angular.scenario.dsl('binding', function() {
return function(name) {
return this.addFutureAction("select binding '" + name + "'", function($window, $document, done) {
- var values = $document.elements().bindings(name);
+ var values = $document.elements().bindings($window.angular.element, name);
if (!values.length) {
return done("Binding selector '" + name + "' did not match.");
}
@@ -260,7 +260,7 @@ angular.scenario.dsl('repeater', function() {
chain.column = function(binding) {
return this.addFutureAction("repeater '" + this.label + "' column '" + binding + "'", function($window, $document, done) {
- done(null, $document.elements().bindings(binding));
+ done(null, $document.elements().bindings($window.angular.element, binding));
});
};
@@ -269,7 +269,7 @@ angular.scenario.dsl('repeater', function() {
var matches = $document.elements().slice(index, index + 1);
if (!matches.length)
return done('row ' + index + ' out of bounds');
- done(null, matches.bindings());
+ done(null, matches.bindings($window.angular.element));
});
};
diff --git a/src/service/compiler.js b/src/service/compiler.js
index 6185c909..c663baac 100644
--- a/src/service/compiler.js
+++ b/src/service/compiler.js
@@ -33,7 +33,7 @@
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
- function(scope, value) {
+ function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
@@ -631,7 +631,7 @@ function $CompileProvider($provide) {
bindings = parent.data('$binding') || [];
bindings.push(interpolateFn);
parent.data('$binding', bindings).addClass('ng-binding');
- scope.$watch(interpolateFn, function(scope, value) {
+ scope.$watch(interpolateFn, function(value) {
node[0].nodeValue = value;
});
})
@@ -656,7 +656,7 @@ function $CompileProvider($provide) {
compile: function(element, attr) {
if (interpolateFn) {
return function(scope, element, attr) {
- scope.$watch(interpolateFn, function(scope, value){
+ scope.$watch(interpolateFn, function(value) {
attr.$set(name, value);
});
};
diff --git a/src/service/filter.js b/src/service/filter.js
index 2947d84b..4ed3f620 100644
--- a/src/service/filter.js
+++ b/src/service/filter.js
@@ -94,7 +94,6 @@ function $FilterProvider($provide) {
register('currency', currencyFilter);
register('date', dateFilter);
register('filter', filterFilter);
- register('html', htmlFilter);
register('json', jsonFilter);
register('limitTo', limitToFilter);
register('linky', linkyFilter);
diff --git a/src/service/filter/filter.js b/src/service/filter/filter.js
index 61cfc80f..49960546 100644
--- a/src/service/filter/filter.js
+++ b/src/service/filter/filter.js
@@ -64,17 +64,17 @@
<doc:scenario>
it('should search across all fields when filtering with a string', function() {
input('searchText').enter('m');
- expect(repeater('#searchTextResults tr', 'friend in friends').column('name')).
+ expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')).
toEqual(['Mary', 'Mike', 'Adam']);
input('searchText').enter('76');
- expect(repeater('#searchTextResults tr', 'friend in friends').column('name')).
+ expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')).
toEqual(['John', 'Julie']);
});
it('should search in specific fields when filtering with a predicate object', function() {
input('search.$').enter('i');
- expect(repeater('#searchObjResults tr', 'friend in friends').column('name')).
+ expect(repeater('#searchObjResults tr', 'friend in friends').column('friend.name')).
toEqual(['Mary', 'Mike', 'Julie']);
});
</doc:scenario>
diff --git a/src/service/filter/filters.js b/src/service/filter/filters.js
index 69bfbacf..58a3a869 100644
--- a/src/service/filter/filters.js
+++ b/src/service/filter/filters.js
@@ -385,7 +385,7 @@ function dateFilter($locale) {
</doc:source>
<doc:scenario>
it('should jsonify filtered objects', function() {
- expect(binding('| json')).toBe('{\n "name":"value"}');
+ expect(binding("{'name':'value'}")).toBe('{\n "name":"value"}');
});
</doc:scenario>
</doc:example>
@@ -422,108 +422,6 @@ var uppercaseFilter = valueFn(uppercase);
/**
* @ngdoc filter
- * @name angular.module.ng.$filter.html
- * @function
- *
- * @description
- * Prevents the input from getting escaped by angular. By default the input is sanitized and
- * inserted into the DOM as is.
- *
- * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
- * then serialized back to properly escaped html string. This means that no unsafe input can make
- * it into the returned string, however, since our parser is more strict than a typical browser
- * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
- * browser, won't make it through the sanitizer.
- *
- * If you hate your users, you may call the filter with optional 'unsafe' argument, which bypasses
- * the html sanitizer, but makes your application vulnerable to XSS and other attacks. Using this
- * option is strongly discouraged and should be used only if you absolutely trust the input being
- * filtered and you can't get the content through the sanitizer.
- *
- * @param {string} html Html input.
- * @param {string=} option If 'unsafe' then do not sanitize the HTML input.
- * @returns {string} Sanitized or raw html.
- *
- * @example
- <doc:example>
- <doc:source>
- <script>
- function Ctrl($scope) {
- $scope.snippet =
- '<p style="color:blue">an html\n' +
- '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
- 'snippet</p>';
- }
- </script>
- <div ng:controller="Ctrl">
- Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
- <table>
- <tr>
- <td>Filter</td>
- <td>Source</td>
- <td>Rendered</td>
- </tr>
- <tr id="html-filter">
- <td>html filter</td>
- <td>
- <pre>&lt;div ng:bind="snippet | html"&gt;<br/>&lt;/div&gt;</pre>
- </td>
- <td>
- <div ng:bind="snippet | html"></div>
- </td>
- </tr>
- <tr id="escaped-html">
- <td>no filter</td>
- <td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
- <td><div ng:bind="snippet"></div></td>
- </tr>
- <tr id="html-unsafe-filter">
- <td>unsafe html filter</td>
- <td><pre>&lt;div ng:bind="snippet | html:'unsafe'"&gt;<br/>&lt;/div&gt;</pre></td>
- <td><div ng:bind="snippet | html:'unsafe'"></div></td>
- </tr>
- </table>
- </div>
- </doc:source>
- <doc:scenario>
- it('should sanitize the html snippet ', function() {
- expect(using('#html-filter').binding('snippet | html')).
- toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
- });
-
- it('should escape snippet without any filter', function() {
- expect(using('#escaped-html').binding('snippet')).
- toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
- "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
- "snippet&lt;/p&gt;");
- });
-
- it('should inline raw snippet if filtered as unsafe', function() {
- expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).
- toBe("<p style=\"color:blue\">an html\n" +
- "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
- "snippet</p>");
- });
-
- it('should update', function() {
- input('snippet').enter('new <b>text</b>');
- expect(using('#html-filter').binding('snippet | html')).toBe('new <b>text</b>');
- expect(using('#escaped-html').binding('snippet')).toBe("new &lt;b&gt;text&lt;/b&gt;");
- expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new <b>text</b>');
- });
- </doc:scenario>
- </doc:example>
- */
-//TODO(misko): turn sensitization into injectable service
-function htmlFilter() {
- return function(html, option){
- return new HTML(html, option);
- };
-}
-
-
-/**
- * @ngdoc filter
* @name angular.module.ng.$filter.linky
* @function
*
@@ -558,10 +456,10 @@ function htmlFilter() {
<tr id="linky-filter">
<td>linky filter</td>
<td>
- <pre>&lt;div ng:bind="snippet | linky"&gt;<br/>&lt;/div&gt;</pre>
+ <pre>&lt;div ng:bind-html="snippet | linky"&gt;<br/>&lt;/div&gt;</pre>
</td>
<td>
- <div ng:bind="snippet | linky"></div>
+ <div ng:bind-html="snippet | linky"></div>
</td>
</tr>
<tr id="escaped-html">
@@ -574,10 +472,10 @@ function htmlFilter() {
<doc:scenario>
it('should linkify the snippet with urls', function() {
expect(using('#linky-filter').binding('snippet | linky')).
- toBe('Pretty text with some links:\n' +
- '<a href="http://angularjs.org/">http://angularjs.org/</a>,\n' +
- '<a href="mailto:us@somewhere.org">us@somewhere.org</a>,\n' +
- '<a href="mailto:another@somewhere.org">another@somewhere.org</a>,\n' +
+ toBe('Pretty text with some links:&#10;' +
+ '<a href="http://angularjs.org/">http://angularjs.org/</a>,&#10;' +
+ '<a href="mailto:us@somewhere.org">us@somewhere.org</a>,&#10;' +
+ '<a href="mailto:another@somewhere.org">another@somewhere.org</a>,&#10;' +
'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
});
@@ -624,6 +522,6 @@ function linkyFilter() {
raw = raw.substring(i + match[0].length);
}
writer.chars(raw);
- return new HTML(html.join(''));
+ return html.join('');
};
};
diff --git a/src/service/filter/orderBy.js b/src/service/filter/orderBy.js
index c67d2769..e7528a4b 100644
--- a/src/service/filter/orderBy.js
+++ b/src/service/filter/orderBy.js
@@ -63,7 +63,7 @@
</doc:source>
<doc:scenario>
it('should be reverse ordered by aged', function() {
- expect(binding('predicate')).toBe('Sorting predicate = -age; reverse = ');
+ expect(binding('predicate')).toBe('-age');
expect(repeater('table.friend', 'friend in friends').column('friend.age')).
toEqual(['35', '29', '21', '19', '10']);
expect(repeater('table.friend', 'friend in friends').column('friend.name')).
diff --git a/src/widget/form.js b/src/widget/form.js
index 962cb6b8..405aae74 100644
--- a/src/widget/form.js
+++ b/src/widget/form.js
@@ -82,28 +82,31 @@
</doc:scenario>
</doc:example>
*/
-angularWidget('form', function(form){
- this.descend(true);
- this.directives(true);
- return ['$formFactory', '$element', function($formFactory, formElement) {
- var name = formElement.attr('name'),
- parentForm = $formFactory.forElement(formElement),
- form = $formFactory(parentForm);
- formElement.data('$form', form);
- formElement.bind('submit', function(event) {
- if (!formElement.attr('action')) event.preventDefault();
- });
- if (name) {
- this[name] = form;
+var ngFormDirective = ['$formFactory', function($formFactory) {
+ return {
+ restrict: 'E',
+ compile: function() {
+ return {
+ pre: function(scope, formElement, attr) {
+ var name = attr.name,
+ parentForm = $formFactory.forElement(formElement),
+ form = $formFactory(parentForm);
+ formElement.data('$form', form);
+ formElement.bind('submit', function(event){
+ if (!attr.action) event.preventDefault();
+ });
+ if (name) {
+ scope[name] = form;
+ }
+ watch('valid');
+ watch('invalid');
+ function watch(name) {
+ form.$watch('$' + name, function(value) {
+ formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ });
+ }
+ }
+ };
}
- watch('valid');
- watch('invalid');
- function watch(name) {
- form.$watch('$' + name, function(value) {
- formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
- });
- }
- }];
-});
-
-angularWidget('ng:form', angularWidget('form'));
+ };
+}];
diff --git a/src/widget/input.js b/src/widget/input.js
index e666a0c1..9f9d9852 100644
--- a/src/widget/input.js
+++ b/src/widget/input.js
@@ -542,23 +542,23 @@ angularInputType('checkbox', function(inputElement, widget) {
</doc:scenario>
</doc:example>
*/
-angularInputType('radio', function(inputElement, widget) {
+angularInputType('radio', function(inputElement, widget, attr) {
//correct the name
- inputElement.attr('name', widget.$id + '@' + inputElement.attr('name'));
+ attr.$set('name', widget.$id + '@' + attr.name);
inputElement.bind('click', function() {
widget.$apply(function() {
if (inputElement[0].checked) {
- widget.$emit('$viewChange', widget.$value);
+ widget.$emit('$viewChange', attr.value);
}
});
});
widget.$render = function() {
- inputElement[0].checked = isDefined(widget.$value) && (widget.$value == widget.$viewValue);
+ inputElement[0].checked = isDefined(attr.value) && (attr.value == widget.$viewValue);
};
if (inputElement[0].checked) {
- widget.$viewValue = widget.$value;
+ widget.$viewValue = attr.value;
}
});
@@ -664,28 +664,28 @@ var HTML5_INPUTS_TYPES = makeMap(
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
- expect(binding('user')).toEqual('{\n \"last\":\"visitor",\n \"name\":\"guest\"}');
+ expect(binding('user')).toEqual('{"last":"visitor","name":"guest"}');
expect(binding('myForm.userName.$valid')).toEqual('true');
expect(binding('myForm.$valid')).toEqual('true');
});
it('should be invalid if empty when required', function() {
input('user.name').enter('');
- expect(binding('user')).toEqual('{\n \"last\":\"visitor",\n \"name\":\"\"}');
+ expect(binding('user')).toEqual('{"last":"visitor","name":""}');
expect(binding('myForm.userName.$valid')).toEqual('false');
expect(binding('myForm.$valid')).toEqual('false');
});
it('should be valid if empty when min length is set', function() {
input('user.last').enter('');
- expect(binding('user')).toEqual('{\n \"last\":\"",\n \"name\":\"guest\"}');
+ expect(binding('user')).toEqual('{"last":"","name":"guest"}');
expect(binding('myForm.lastName.$valid')).toEqual('true');
expect(binding('myForm.$valid')).toEqual('true');
});
it('should be invalid if less than required min length', function() {
input('user.last').enter('xx');
- expect(binding('user')).toEqual('{\n \"last\":\"xx",\n \"name\":\"guest\"}');
+ expect(binding('user')).toEqual('{"last":"xx","name":"guest"}');
expect(binding('myForm.lastName.$valid')).toEqual('false');
expect(binding('myForm.lastName.$error')).toMatch(/MINLENGTH/);
expect(binding('myForm.$valid')).toEqual('false');
@@ -694,7 +694,7 @@ var HTML5_INPUTS_TYPES = makeMap(
it('should be valid if longer than max length', function() {
input('user.last').enter('some ridiculously long name');
expect(binding('user'))
- .toEqual('{\n \"last\":\"some ridiculously long name",\n \"name\":\"guest\"}');
+ .toEqual('{"last":"some ridiculously long name","name":"guest"}');
expect(binding('myForm.lastName.$valid')).toEqual('false');
expect(binding('myForm.lastName.$error')).toMatch(/MAXLENGTH/);
expect(binding('myForm.$valid')).toEqual('false');
@@ -702,26 +702,24 @@ var HTML5_INPUTS_TYPES = makeMap(
</doc:scenario>
</doc:example>
*/
-angularWidget('input', function(inputElement){
- this.directives(true);
- this.descend(true);
- var modelExp = inputElement.attr('ng:model');
- return modelExp &&
- ['$defer', '$formFactory', '$element',
- function($defer, $formFactory, inputElement) {
+var inputDirective = ['$defer', '$formFactory', function($defer, $formFactory) {
+ return {
+ restrict: 'E',
+ link: function(modelScope, inputElement, attr) {
+ if (!attr.ngModel) return;
+
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',
+ type = attr.type || 'text',
TypeController,
- modelScope = this,
patternMatch, widget,
- pattern = trim(inputElement.attr('ng:pattern')),
- minlength = parseInt(inputElement.attr('ng:minlength'), 10),
- maxlength = parseInt(inputElement.attr('ng:maxlength'), 10),
+ pattern = attr.ngPattern,
+ modelExp = attr.ngModel,
+ minlength = parseInt(attr.ngMinlength, 10),
+ maxlength = parseInt(attr.ngMaxlength, 10),
loadFromScope = type.match(/^\s*\@\s*(.*)/);
-
if (!pattern) {
patternMatch = valueFn(true);
} else {
@@ -743,7 +741,7 @@ angularWidget('input', function(inputElement){
type = lowercase(type);
TypeController = (loadFromScope
- ? (assertArgFn(this.$eval(loadFromScope[1]), loadFromScope[1])).$unboundFn
+ ? (assertArgFn(modelScope.$eval(loadFromScope[1]), loadFromScope[1])).$unboundFn
: angularInputType(type)) || noop;
if (!HTML5_INPUTS_TYPES[type]) {
@@ -757,26 +755,21 @@ angularWidget('input', function(inputElement){
}
//TODO(misko): setting $inject is a hack
- !TypeController.$inject && (TypeController.$inject = ['$element', '$scope']);
+ !TypeController.$inject && (TypeController.$inject = ['$element', '$scope', '$attr']);
widget = form.$createWidget({
scope: modelScope,
model: modelExp,
- onChange: inputElement.attr('ng:change'),
- alias: inputElement.attr('name'),
+ onChange: attr.ngChange,
+ alias: attr.name,
controller: TypeController,
- controllerArgs: {$element: inputElement}
+ controllerArgs: {$element: inputElement, $attr: attr}
});
- watchElementProperty(this, widget, 'value', inputElement);
- watchElementProperty(this, widget, 'required', inputElement);
- watchElementProperty(this, widget, 'readonly', inputElement);
- watchElementProperty(this, widget, 'disabled', inputElement);
-
widget.$pristine = !(widget.$dirty = false);
widget.$on('$validate', function() {
var $viewValue = trim(widget.$viewValue),
- inValid = widget.$required && !$viewValue,
+ inValid = attr.required && !$viewValue,
tooLong = maxlength && $viewValue && $viewValue.length > maxlength,
tooShort = minlength && $viewValue && $viewValue.length < minlength,
missMatch = $viewValue && !patternMatch($viewValue);
@@ -812,7 +805,7 @@ angularWidget('input', function(inputElement){
inputElement.val(widget.$viewValue || '');
};
- inputElement.bind('keydown change input', function(event) {
+ inputElement.bind('keydown change input', function(event){
var key = event.keyCode;
if (/*command*/ key != 91 &&
/*modifiers*/ !(15 < key && key < 19) &&
@@ -827,8 +820,9 @@ angularWidget('input', function(inputElement){
}
});
}
- }];
-});
+ }
+ };
+}];
/**
@@ -856,24 +850,3 @@ angularWidget('input', function(inputElement){
* @param {string=} ng:change Angular expression to be executed when input changes due to user
* interaction with the input element.
*/
-angularWidget('textarea', angularWidget('input'));
-
-
-function watchElementProperty(modelScope, widget, name, element) {
- var bindAttr = fromJson(element.attr('ng:bind-attr') || '{}'),
- match = /\s*\{\{(.*)\}\}\s*/.exec(bindAttr[name]),
- isBoolean = BOOLEAN_ATTR[name];
- widget['$' + name] = isBoolean
- ? ( // 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])
- : element.attr(name);
- if (bindAttr[name] && match) {
- modelScope.$watch(match[1], function(value) {
- widget['$' + name] = isBoolean ? !!value : value;
- widget.$emit('$validate');
- widget.$render && widget.$render();
- });
- }
-}
diff --git a/src/widget/select.js b/src/widget/select.js
index a3633ddd..ae9e1b7c 100644
--- a/src/widget/select.js
+++ b/src/widget/select.js
@@ -112,321 +112,335 @@
</doc:source>
<doc:scenario>
it('should check ng:options', function() {
- expect(binding('color')).toMatch('red');
+ expect(binding('{selected_color:color}')).toMatch('red');
select('color').option('0');
- expect(binding('color')).toMatch('black');
+ expect(binding('{selected_color:color}')).toMatch('black');
using('.nullable').select('color').option('');
- expect(binding('color')).toMatch('null');
+ expect(binding('{selected_color: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') &&
- ['$formFactory', '$compile', '$parse', '$element',
- function($formFactory, $compile, $parse, 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: modelScope,
- model: modelExp,
- onChange: selectElement.attr('ng:change'),
- alias: selectElement.attr('name'),
- controller: ['$scope', 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(value) {
- selectElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+var ngOptionsDirective = valueFn({ terminal: true });
+var selectDirective = ['$formFactory', '$compile', '$parse',
+ function($formFactory, $compile, $parse){
+ //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+(.*)$/;
+
+ return {
+ restrict: 'E',
+ link: function(modelScope, selectElement, attr) {
+ if (!attr.ngModel) return;
+ var form = $formFactory.forElement(selectElement),
+ multiple = attr.multiple,
+ optionsExp = attr.ngOptions,
+ modelExp = attr.ngModel,
+ widget = form.$createWidget({
+ scope: modelScope,
+ model: modelExp,
+ onChange: attr.ngChange,
+ alias: attr.name,
+ controller: ['$scope', optionsExp ? Options : (multiple ? Multiple : Single)]});
+
+ selectElement.bind('$destroy', function() { widget.$destroy(); });
+
+ widget.$pristine = !(widget.$dirty = false);
+
+ widget.$on('$validate', function() {
+ var valid = !attr.required || !!widget.$modelValue;
+ if (valid && multiple && attr.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);
+ });
- function Multiple(widget) {
- widget.$render = function() {
- var items = new HashMap(widget.$viewValue);
- forEach(selectElement.children(), function(option){
- option.selected = isDefined(items.get(option.value));
+ forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) {
+ widget.$watch('$' + name, function(value) {
+ selectElement[value ? 'addClass' : 'removeClass']('ng-' + name);
});
- };
+ });
+
+ ////////////////////////////
- selectElement.bind('change', function() {
- widget.$apply(function() {
- var array = [];
+ function Multiple(widget) {
+ widget.$render = function() {
+ var items = new HashMap(this.$viewValue);
forEach(selectElement.children(), function(option){
- if (option.selected) {
- array.push(option.value);
- }
+ 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);
});
- widget.$emit('$viewChange', array);
});
- });
- }
+ }
- function Single(widget) {
- widget.$render = function() {
- selectElement.val(widget.$viewValue);
- };
+ function Single(widget) {
+ widget.$render = function() {
+ selectElement.val(widget.$viewValue);
+ };
- selectElement.bind('change', function() {
- widget.$apply(function() {
- widget.$emit('$viewChange', selectElement.val());
+ selectElement.bind('change', function() {
+ widget.$apply(function() {
+ widget.$emit('$viewChange', selectElement.val());
+ });
});
- });
-
- widget.$viewValue = selectElement.val();
- }
-
- function Options(widget) {
- var 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 + "'.");
+ widget.$viewValue = selectElement.val();
}
- var displayFn = $parse(match[2] || match[1]),
- valueName = match[4] || match[6],
- keyName = match[5],
- groupByFn = $parse(match[3] || ''),
- valueFn = $parse(match[2] ? match[1] : valueName),
- valuesFn = $parse(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:''}]];
-
- // find existing special options
- forEach(selectElement.children(), function(option) {
- if (option.value == '') {
- // developer declared null option, so user should be able to select it
- nullOption = jqLite(option).remove();
- // compile the element since there might be bindings in it
- $compile(nullOption)(modelScope);
- }
- });
- selectElement.html(''); // clear contents
+ function Options(widget) {
+ var match;
- selectElement.bind('change', function() {
- widget.$apply(function() {
- var optionGroup,
- collection = valuesFn(modelScope) || [],
- key = selectElement.val(),
- tempScope = inherit(modelScope),
- value, optionElement, index, groupIndex, length, groupLength;
+ 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 + "'.");
+ }
- 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));
+ var displayFn = $parse(match[2] || match[1]),
+ valueName = match[4] || match[6],
+ keyName = match[5],
+ groupByFn = $parse(match[3] || ''),
+ valueFn = $parse(match[2] ? match[1] : valueName),
+ valuesFn = $parse(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:''}]];
+
+ // find existing special options
+ forEach(selectElement.children(), function(option) {
+ if (option.value == '') {
+ // developer declared null option, so user should be able to select it
+ nullOption = jqLite(option).remove();
+ // compile the element since there might be bindings in it
+ $compile(nullOption)(modelScope);
+ }
+ });
+ selectElement.html(''); // clear contents
+
+ selectElement.bind('change', function() {
+ widget.$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 (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) {
- widget.$emit('$viewChange', value);
- }
+ if (isDefined(value) && modelScope.$viewVal !== value) {
+ widget.$emit('$viewChange', value);
+ }
+ });
});
- });
- widget.$watch(render);
- widget.$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({selected:modelValue === null, id:'', label:''});
- selectedSet = true;
- }
+ widget.$watch(render);
+ widget.$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;
- // 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
+ 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({selected:modelValue === null, id:'', label:''});
+ selectedSet = true;
}
- 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
- existingParent = {
- element: optGroupTemplate.clone().attr('label', optionGroupName),
- label: optionGroup.label
- };
- existingOptions = [existingParent];
- optionGroupsCache.push(existingOptions);
- 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);
+ // 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});
}
- 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.element.selected !== option.selected) {
- lastElement.prop('selected', (existingOption.selected = option.selected));
- }
+ // 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
+ existingParent = {
+ element: optGroupTemplate.clone().attr('label', optionGroupName),
+ label: optionGroup.label
+ };
+ existingOptions = [existingParent];
+ optionGroupsCache.push(existingOptions);
+ selectElement.append(existingParent.element);
} else {
- // grow elements
+ existingOptions = optionGroupsCache[groupIndex];
+ existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
- // if it's a null option
- if (option.id === '' && nullOption) {
- // put back the pre-compiled element
- element = nullOption;
- } else {
- // 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);
+ // update the OPTGROUP label if not the same.
+ if (existingParent.label != optionGroupName) {
+ existingParent.element.attr('label', existingParent.label = optionGroupName);
}
+ }
- existingOptions.push(existingOption = {
- element: element,
- label: option.label,
- id: option.id,
- selected: option.selected
- });
- if (lastElement) {
- lastElement.after(element);
+ 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.element.selected !== option.selected) {
+ lastElement.prop('selected', (existingOption.selected = option.selected));
+ }
} else {
- existingParent.element.append(element);
+ // grow elements
+
+ // if it's a null option
+ if (option.id === '' && nullOption) {
+ // put back the pre-compiled element
+ element = nullOption;
+ } else {
+ // 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;
}
- 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 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();
}
+ };
+ }
+ }
+ }
+}];
+
+var optionDirective = ['$interpolate', function($interpolate) {
+ return {
+ priority: 100,
+ compile: function(element, attr) {
+ if (isUndefined(attr.value)) {
+ var interpolateFn = $interpolate(element.text(), true);
+ if (interpolateFn) {
+ return function (scope, element, attr) {
+ scope.$watch(interpolateFn, function(value) {
+ attr.$set('value', value);
+ });
+ }
+ } else {
+ attr.$set('value', element.text());
}
- // remove any excessive OPTGROUPs from select
- while(optionGroupsCache.length > groupIndex) {
- optionGroupsCache.pop()[0].element.remove();
- }
- };
+ }
}
- }];
-});
+ }
+}];
diff --git a/src/widgets.js b/src/widgets.js
index 53be8b14..cf32bdc1 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -67,7 +67,7 @@
</select>
url of the template: <tt><a href="{{template.url}}">{{template.url}}</a></tt>
<hr/>
- <ng:include src="template.url"></ng:include>
+ <div class="ng-include" src="template.url"></div>
</div>
</doc:source>
<doc:scenario>
@@ -87,64 +87,62 @@
</doc:scenario>
</doc:example>
*/
-angularWidget('ng:include', function(element){
- var compiler = this,
- srcExp = element.attr("src"),
- scopeExp = element.attr("scope") || '',
- onloadExp = element[0].getAttribute('onload') || '', //workaround for jquery bug #7537
- autoScrollExp = element.attr('autoscroll');
-
- if (element[0]['ng:compiled']) {
- this.descend(true);
- this.directives(true);
- } else {
- element[0]['ng:compiled'] = true;
- return ['$http', '$templateCache', '$anchorScroll', '$element',
- function($http, $templateCache, $anchorScroll, element) {
- var scope = this,
- changeCounter = 0,
- childScope;
-
- function incrementChange() { changeCounter++;}
- this.$watch(srcExp, incrementChange);
- this.$watch(function() {
- var includeScope = scope.$eval(scopeExp);
- if (includeScope) return includeScope.$id;
- }, incrementChange);
- this.$watch(function() {return changeCounter;}, function(newChangeCounter) {
- var src = scope.$eval(srcExp),
- useScope = scope.$eval(scopeExp);
-
- function clearContent() {
- // if this callback is still desired
- if (newChangeCounter === changeCounter) {
- if (childScope) childScope.$destroy();
- childScope = null;
- element.html('');
- }
- }
-
- if (src) {
- $http.get(src, {cache: $templateCache}).success(function(response) {
- // if this callback is still desired
- if (newChangeCounter === changeCounter) {
- element.html(response);
- if (childScope) childScope.$destroy();
- childScope = useScope ? useScope : scope.$new();
- compiler.compile(element)(childScope);
- if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
- $anchorScroll();
+var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile',
+ function($http, $templateCache, $anchorScroll, $compile) {
+ return {
+ compile: function(element, attr) {
+ var srcExp = attr.src,
+ scopeExp = attr.scope || '',
+ onloadExp = attr.onload || '', //workaround for jquery bug #7537
+ autoScrollExp = attr.autoscroll;
+ if (!element[0]['ng:compiled']) {
+ element[0]['ng:compiled'] = true;
+ return function(scope, element, attr){
+ var changeCounter = 0,
+ childScope;
+
+ function incrementChange() { changeCounter++;}
+ scope.$watch(srcExp, incrementChange);
+ scope.$watch(function() {
+ var includeScope = scope.$eval(scopeExp);
+ if (includeScope) return includeScope.$id;
+ }, incrementChange);
+ scope.$watch(function() {return changeCounter;}, function(newChangeCounter) {
+ var src = scope.$eval(srcExp),
+ useScope = scope.$eval(scopeExp);
+
+ function clearContent() {
+ // if this callback is still desired
+ if (newChangeCounter === changeCounter) {
+ if (childScope) childScope.$destroy();
+ childScope = null;
+ element.html('');
}
- scope.$eval(onloadExp);
}
- }).error(clearContent);
- } else {
- clearContent();
- }
- });
- }];
+
+ if (src) {
+ $http.get(src, {cache: $templateCache}).success(function(response) {
+ // if this callback is still desired
+ if (newChangeCounter === changeCounter) {
+ element.html(response);
+ if (childScope) childScope.$destroy();
+ childScope = useScope ? useScope : scope.$new();
+ $compile(element)(childScope);
+ if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
+ $anchorScroll();
+ }
+ scope.$eval(onloadExp);
+ }
+ }).error(clearContent);
+ } else {
+ clearContent();
+ }
+ });
+ };
+ }
+ }
}
-});
+}];
/**
* @ngdoc widget
@@ -203,58 +201,62 @@ angularWidget('ng:include', function(element){
</doc:scenario>
</doc:example>
*/
-angularWidget('ng:switch', function(element) {
- var compiler = this,
- watchExpr = element.attr("on"),
- changeExpr = element.attr('change'),
- casesTemplate = {},
- defaultCaseTemplate,
- children = element.children(),
- length = children.length,
- child,
- when;
-
- if (!watchExpr) throw new Error("Missing 'on' attribute.");
- while(length--) {
- child = jqLite(children[length]);
- // this needs to be here for IE
- child.remove();
- when = child.attr('ng:switch-when');
- if (isString(when)) {
- casesTemplate[when] = compiler.compile(child);
- } else if (isString(child.attr('ng:switch-default'))) {
- defaultCaseTemplate = compiler.compile(child);
- }
- }
- children = null; // release memory;
- element.html('');
+var ngSwitchDirective = ['$compile', function($compile){
+ return {
+ compile: function(element, attr) {
+ var watchExpr = attr.on,
+ changeExpr = attr.change,
+ casesTemplate = {},
+ defaultCaseTemplate,
+ children = element.children(),
+ length = children.length,
+ child,
+ when;
+
+ if (!watchExpr) throw new Error("Missing 'on' attribute.");
+ while(length--) {
+ child = jqLite(children[length]);
+ // this needs to be here for IE
+ child.remove();
+ // TODO(misko): this attr reading is not normilized
+ when = child.attr('ng:switch-when');
+ if (isString(when)) {
+ casesTemplate[when] = $compile(child);
+ // TODO(misko): this attr reading is not normilized
+ } else if (isString(child.attr('ng:switch-default'))) {
+ defaultCaseTemplate = $compile(child);
+ }
+ }
+ children = null; // release memory;
+ element.html('');
- return function(element){
- var changeCounter = 0;
- var childScope;
- var selectedTemplate;
- var scope = this;
+ return function(scope, element, attr){
+ var changeCounter = 0;
+ var childScope;
+ var selectedTemplate;
- this.$watch(watchExpr, function(value) {
- element.html('');
- if ((selectedTemplate = casesTemplate[value] || defaultCaseTemplate)) {
- changeCounter++;
- if (childScope) childScope.$destroy();
- childScope = scope.$new();
- childScope.$eval(changeExpr);
- }
- });
+ scope.$watch(watchExpr, function(value) {
+ element.html('');
+ if ((selectedTemplate = casesTemplate[value] || defaultCaseTemplate)) {
+ changeCounter++;
+ if (childScope) childScope.$destroy();
+ childScope = scope.$new();
+ childScope.$eval(changeExpr);
+ }
+ });
- this.$watch(function() {return changeCounter;}, function() {
- element.html('');
- if (selectedTemplate) {
- selectedTemplate(childScope, function(caseElement) {
- element.append(caseElement);
+ scope.$watch(function() {return changeCounter;}, function() {
+ element.html('');
+ if (selectedTemplate) {
+ selectedTemplate(childScope, function(caseElement) {
+ element.append(caseElement);
+ });
+ }
});
- }
- });
+ };
+ }
};
-});
+}];
/*
@@ -265,25 +267,24 @@ angularWidget('ng:switch', function(element) {
* changing the location or causing page reloads, e.g.:
* <a href="" ng:click="model.$save()">Save</a>
*/
-angularWidget('a', function() {
- this.descend(true);
- this.directives(true);
-
- return function(element) {
- var hasNgHref = ((element.attr('ng:bind-attr') || '').indexOf('"href":') !== -1);
-
+var htmlAnchorDirective = valueFn({
+ restrict: 'E',
+ compile: function(element, attr) {
// 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 (!attr.href) {
+ attr.$set('href', '');
}
- if (element.attr('href') === '' && !hasNgHref) {
+ return function(scope, element) {
element.bind('click', function(event){
- event.preventDefault();
+ // if we have no href url, then don't navigate anywhere.
+ if (!element.attr('href')) {
+ event.preventDefault();
+ }
});
}
- };
+ }
});
@@ -344,125 +345,131 @@ angularWidget('a', function() {
</doc:scenario>
</doc:example>
*/
-angularWidget('@ng:repeat', function(expression, element){
- element.removeAttr('ng:repeat');
- element.replaceWith(jqLite('<!-- ng:repeat: ' + expression + ' -->'));
- var linker = this.compile(element);
- return function(iterStartElement){
- var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
- lhs, rhs, valueIdent, keyIdent;
- if (! match) {
- throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" +
- 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 parentScope = this;
- // Store a list of elements from previous run. This is a hash where key is the item from the
- // iterator, and the value is an array of objects with following properties.
- // - scope: bound scope
- // - element: previous element.
- // - index: position
- // We need an array of these objects since the same object can be returned from the iterator.
- // We expect this to be a rare case.
- var lastOrder = new HashQueueMap();
- this.$watch(function(scope){
- var index, length,
- collection = scope.$eval(rhs),
- collectionLength = size(collection, true),
- childScope,
- // Same as lastOrder but it has the current state. It will become the
- // lastOrder on the next iteration.
- nextOrder = new HashQueueMap(),
- key, value, // key/value of iteration
- array, last, // last object information {scope, element, index}
- cursor = iterStartElement; // current position of the node
-
- if (!isArray(collection)) {
- // if object, extract keys, sort them and use to determine order of iteration over obj props
- array = [];
- for(key in collection) {
- if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
- array.push(key);
- }
+var ngRepeatDirective = ['$compile', function($compile) {
+ return {
+ priority: 1000,
+ terminal: true,
+ compile: function(element, attr) {
+ var expression = attr.ngRepeat;
+ attr.$set(attr.$attr.ngRepeat);
+ element.replaceWith(jqLite('<!-- ng:repeat: ' + expression + ' -->'));
+ var linker = $compile(element);
+ return function(scope, iterStartElement, attr){
+ var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
+ lhs, rhs, valueIdent, keyIdent;
+ if (! match) {
+ throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" +
+ expression + "'.");
}
- array.sort();
- } else {
- array = collection || [];
- }
-
- // we are not using forEach for perf reasons (trying to avoid #call)
- for (index = 0, length = array.length; index < length; index++) {
- key = (collection === array) ? index : array[index];
- value = collection[key];
- last = lastOrder.shift(value);
- if (last) {
- // if we have already seen this object, then we need to reuse the
- // associated scope/element
- childScope = last.scope;
- nextOrder.push(value, last);
-
- if (index === last.index) {
- // do nothing
- cursor = last.element;
+ 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];
+
+ // Store a list of elements from previous run. This is a hash where key is the item from the
+ // iterator, and the value is an array of objects with following properties.
+ // - scope: bound scope
+ // - element: previous element.
+ // - index: position
+ // We need an array of these objects since the same object can be returned from the iterator.
+ // We expect this to be a rare case.
+ var lastOrder = new HashQueueMap();
+ scope.$watch(function(scope){
+ var index, length,
+ collection = scope.$eval(rhs),
+ collectionLength = size(collection, true),
+ childScope,
+ // Same as lastOrder but it has the current state. It will become the
+ // lastOrder on the next iteration.
+ nextOrder = new HashQueueMap(),
+ key, value, // key/value of iteration
+ array, last, // last object information {scope, element, index}
+ cursor = iterStartElement; // current position of the node
+
+ if (!isArray(collection)) {
+ // if object, extract keys, sort them and use to determine order of iteration over obj props
+ array = [];
+ for(key in collection) {
+ if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
+ array.push(key);
+ }
+ }
+ array.sort();
} else {
- // existing item which got moved
- last.index = index;
- // This may be a noop, if the element is next, but I don't know of a good way to
- // figure this out, since it would require extra DOM access, so let's just hope that
- // the browsers realizes that it is noop, and treats it as such.
- cursor.after(last.element);
- cursor = last.element;
+ array = collection || [];
}
- } else {
- // new item which we don't know about
- childScope = parentScope.$new();
- }
- childScope[valueIdent] = value;
- if (keyIdent) childScope[keyIdent] = key;
- childScope.$index = index;
- childScope.$position = index === 0 ?
- 'first' :
- (index == collectionLength - 1 ? 'last' : 'middle');
-
- if (!last) {
- linker(childScope, function(clone){
- cursor.after(clone);
- last = {
- scope: childScope,
- element: (cursor = clone),
- index: index
- };
- nextOrder.push(value, last);
- });
- }
- }
+ // we are not using forEach for perf reasons (trying to avoid #call)
+ for (index = 0, length = array.length; index < length; index++) {
+ key = (collection === array) ? index : array[index];
+ value = collection[key];
+ last = lastOrder.shift(value);
+ if (last) {
+ // if we have already seen this object, then we need to reuse the
+ // associated scope/element
+ childScope = last.scope;
+ nextOrder.push(value, last);
+
+ if (index === last.index) {
+ // do nothing
+ cursor = last.element;
+ } else {
+ // existing item which got moved
+ last.index = index;
+ // This may be a noop, if the element is next, but I don't know of a good way to
+ // figure this out, since it would require extra DOM access, so let's just hope that
+ // the browsers realizes that it is noop, and treats it as such.
+ cursor.after(last.element);
+ cursor = last.element;
+ }
+ } else {
+ // new item which we don't know about
+ childScope = scope.$new();
+ }
- //shrink children
- for (key in lastOrder) {
- if (lastOrder.hasOwnProperty(key)) {
- array = lastOrder[key];
- while(array.length) {
- value = array.pop();
- value.element.remove();
- value.scope.$destroy();
+ childScope[valueIdent] = value;
+ if (keyIdent) childScope[keyIdent] = key;
+ childScope.$index = index;
+ childScope.$position = index === 0 ?
+ 'first' :
+ (index == collectionLength - 1 ? 'last' : 'middle');
+
+ if (!last) {
+ linker(childScope, function(clone){
+ cursor.after(clone);
+ last = {
+ scope: childScope,
+ element: (cursor = clone),
+ index: index
+ };
+ nextOrder.push(value, last);
+ });
+ }
}
- }
- }
- lastOrder = nextOrder;
- });
+ //shrink children
+ for (key in lastOrder) {
+ if (lastOrder.hasOwnProperty(key)) {
+ array = lastOrder[key];
+ while(array.length) {
+ value = array.pop();
+ value.element.remove();
+ value.scope.$destroy();
+ }
+ }
+ }
+
+ lastOrder = nextOrder;
+ });
+ };
+ }
};
-});
+}];
/**
@@ -496,7 +503,7 @@ angularWidget('@ng:repeat', function(expression, element){
</doc:scenario>
</doc:example>
*/
-angularWidget("@ng:non-bindable", noop);
+var ngNonBindableDirective = valueFn({ terminal: true });
/**
@@ -564,49 +571,48 @@ angularWidget("@ng:non-bindable", noop);
</doc:scenario>
</doc:example>
*/
-angularWidget('ng:view', function(element) {
- var compiler = this;
-
- if (!element[0]['ng:compiled']) {
- element[0]['ng:compiled'] = true;
- return ['$http', '$templateCache', '$route', '$anchorScroll', '$element',
- function($http, $templateCache, $route, $anchorScroll, element) {
- var template;
- var changeCounter = 0;
-
- this.$on('$afterRouteChange', function() {
- changeCounter++;
- });
+var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile',
+ function($http, $templateCache, $route, $anchorScroll, $compile) {
+ return {
+ compile: function(element, attr) {
+ if (!element[0]['ng:compiled']) {
+ element[0]['ng:compiled'] = true;
+
+ return function(scope, element, attrs) {
+ var changeCounter = 0;
+
+ scope.$on('$afterRouteChange', function() {
+ changeCounter++;
+ });
- this.$watch(function() {return changeCounter;}, function(newChangeCounter) {
- var template = $route.current && $route.current.template;
+ scope.$watch(function() {return changeCounter;}, function(newChangeCounter) {
+ var template = $route.current && $route.current.template;
- function clearContent() {
- // ignore callback if another route change occured since
- if (newChangeCounter == changeCounter) {
- element.html('');
- }
- }
+ function clearContent() {
+ // ignore callback if another route change occured since
+ if (newChangeCounter == changeCounter) {
+ element.html('');
+ }
+ }
- if (template) {
- $http.get(template, {cache: $templateCache}).success(function(response) {
- // ignore callback if another route change occured since
- if (newChangeCounter == changeCounter) {
- element.html(response);
- compiler.compile(element)($route.current.scope);
- $anchorScroll();
+ if (template) {
+ $http.get(template, {cache: $templateCache}).success(function(response) {
+ // ignore callback if another route change occured since
+ if (newChangeCounter == changeCounter) {
+ element.html(response);
+ $compile(element)($route.current.scope);
+ $anchorScroll();
+ }
+ }).error(clearContent);
+ } else {
+ clearContent();
}
- }).error(clearContent);
- } else {
- clearContent();
- }
- });
- }];
- } else {
- compiler.descend(true);
- compiler.directives(true);
- }
-});
+ });
+ };
+ }
+ }
+ };
+}];
/**
@@ -715,81 +721,80 @@ angularWidget('ng:view', function(element) {
<!--- Example with simple pluralization rules for en locale --->
Without Offset:
- <ng:pluralize count="personCount"
+ <ng-pluralize count="personCount"
when="{'0': 'Nobody is viewing.',
'one': '1 person is viewing.',
'other': '{} people are viewing.'}">
- </ng:pluralize><br>
+ </ng-pluralize><br>
<!--- Example with offset --->
With Offset(2):
- <ng:pluralize count="personCount" offset=2
+ <ng-pluralize count="personCount" offset=2
when="{'0': 'Nobody is viewing.',
'1': '{{person1}} is viewing.',
'2': '{{person1}} and {{person2}} are viewing.',
'one': '{{person1}}, {{person2}} and one other person are viewing.',
'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
- </ng:pluralize>
+ </ng-pluralize>
</div>
</doc:source>
<doc:scenario>
it('should show correct pluralized string', function() {
- expect(element('.doc-example-live .ng-pluralize:first').text()).
+ expect(element('.doc-example-live ng-pluralize:first').text()).
toBe('1 person is viewing.');
- expect(element('.doc-example-live .ng-pluralize:last').text()).
+ expect(element('.doc-example-live ng-pluralize:last').text()).
toBe('Igor is viewing.');
using('.doc-example-live').input('personCount').enter('0');
- expect(element('.doc-example-live .ng-pluralize:first').text()).
+ expect(element('.doc-example-live ng-pluralize:first').text()).
toBe('Nobody is viewing.');
- expect(element('.doc-example-live .ng-pluralize:last').text()).
+ expect(element('.doc-example-live ng-pluralize:last').text()).
toBe('Nobody is viewing.');
using('.doc-example-live').input('personCount').enter('2');
- expect(element('.doc-example-live .ng-pluralize:first').text()).
+ expect(element('.doc-example-live ng-pluralize:first').text()).
toBe('2 people are viewing.');
- expect(element('.doc-example-live .ng-pluralize:last').text()).
+ expect(element('.doc-example-live ng-pluralize:last').text()).
toBe('Igor and Misko are viewing.');
using('.doc-example-live').input('personCount').enter('3');
- expect(element('.doc-example-live .ng-pluralize:first').text()).
+ expect(element('.doc-example-live ng-pluralize:first').text()).
toBe('3 people are viewing.');
- expect(element('.doc-example-live .ng-pluralize:last').text()).
+ expect(element('.doc-example-live ng-pluralize:last').text()).
toBe('Igor, Misko and one other person are viewing.');
using('.doc-example-live').input('personCount').enter('4');
- expect(element('.doc-example-live .ng-pluralize:first').text()).
+ expect(element('.doc-example-live ng-pluralize:first').text()).
toBe('4 people are viewing.');
- expect(element('.doc-example-live .ng-pluralize:last').text()).
+ expect(element('.doc-example-live ng-pluralize:last').text()).
toBe('Igor, Misko and 2 other people are viewing.');
});
it('should show data-binded names', function() {
using('.doc-example-live').input('personCount').enter('4');
- expect(element('.doc-example-live .ng-pluralize:last').text()).
+ expect(element('.doc-example-live ng-pluralize:last').text()).
toBe('Igor, Misko and 2 other people are viewing.');
using('.doc-example-live').input('person1').enter('Di');
using('.doc-example-live').input('person2').enter('Vojta');
- expect(element('.doc-example-live .ng-pluralize:last').text()).
+ expect(element('.doc-example-live ng-pluralize:last').text()).
toBe('Di, Vojta and 2 other people are viewing.');
});
</doc:scenario>
</doc:example>
*/
-angularWidget('ng:pluralize', function(element) {
- var numberExp = element.attr('count'),
- whenExp = element.attr('when'),
- offset = element.attr('offset') || 0;
-
- return ['$locale', '$element', function($locale, element) {
- var scope = this,
+var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) {
+ var BRACE = /{}/g;
+ return function(scope, element, attr) {
+ var numberExp = attr.count,
+ whenExp = attr.when,
+ offset = attr.offset || 0,
whens = scope.$eval(whenExp),
whensExpFns = {};
forEach(whens, function(expression, key) {
- whensExpFns[key] = compileBindTemplate(expression.replace(/{}/g,
- '{{' + numberExp + '-' + offset + '}}'));
+ whensExpFns[key] =
+ $interpolate(expression.replace(BRACE, '{{' + numberExp + '-' + offset + '}}'));
});
scope.$watch(function() {
@@ -806,5 +811,5 @@ angularWidget('ng:pluralize', function(element) {
}, function(newVal) {
element.text(newVal);
});
- }];
-});
+ };
+}];