From 9ee2cdff44e7d496774b340de816344126c457b3 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Tue, 22 Nov 2011 21:28:39 -0800 Subject: refactor(directives): connect new compiler - turn everything into a directive --- src/Angular.js | 13 +- src/AngularPublic.js | 45 +++- src/Injector.js | 11 +- src/directives.js | 308 +++++++++++----------- src/jqLite.js | 11 +- src/markups.js | 128 +-------- src/scenario/Scenario.js | 76 ++++-- src/scenario/dsl.js | 6 +- src/service/compiler.js | 6 +- src/service/filter.js | 1 - src/service/filter/filter.js | 6 +- src/service/filter/filters.js | 118 +-------- src/service/filter/orderBy.js | 2 +- src/widget/form.js | 51 ++-- src/widget/input.js | 89 +++---- src/widget/select.js | 554 ++++++++++++++++++++------------------- src/widgets.js | 591 +++++++++++++++++++++--------------------- 17 files changed, 925 insertions(+), 1091 deletions(-) (limited to 'src') 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 @@ */ -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){ */ -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) { */ -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){ 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'); }); */ -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){ */ -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,17 +434,31 @@ 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 @@ -496,48 +496,42 @@ angularDirective("ng:click", function(expression, element){ 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"]'); }); */ -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) { */ -angularDirective("ng:class", ngClass(function() {return true;})); +var ngClassDirective = classDirective('', true); /** * @ngdoc directive @@ -624,7 +618,7 @@ angularDirective("ng:class", ngClass(function() {return true;})); */ -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;})); */ -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;})); */ -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){ */ -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){ */ -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) { * */ -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 ``. - * - * Create custom markup like this: - * - *
- *   angular.markup('newMarkup', function(text, textNode, parentElement){
- *     //tranformation code
- *   });
- * 
- * - * 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 - * - *
- *   angular.attrMarkup('newAttrMarkup', function(attrValue, attrName, element){
- *     //tranformation code
- *   });
- * 
- * - * 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(''); - newElement.attr('ng:bind', exp); - } else { - newElement = jqLite(document.createTextNode(text)); - } - if (msie && text.charAt(0) == ' ') { - newElement = jqLite(' '); - 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 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 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']); }); 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) { it('should jsonify filtered objects', function() { - expect(binding('| json')).toBe('{\n "name":"value"}'); + expect(binding("{'name':'value'}")).toBe('{\n "name":"value"}'); }); @@ -420,108 +420,6 @@ var lowercaseFilter = valueFn(lowercase); 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 - - - -
- Snippet: - - - - - - - - - - - - - - - - - - - - - -
FilterSourceRendered
html filter -
<div ng:bind="snippet | html">
</div>
-
-
-
no filter
<div ng:bind="snippet">
</div>
unsafe html filter
<div ng:bind="snippet | html:'unsafe'">
</div>
-
-
- - it('should sanitize the html snippet ', function() { - expect(using('#html-filter').binding('snippet | html')). - toBe('

an html\nclick here\nsnippet

'); - }); - - it('should escape snippet without any filter', function() { - expect(using('#escaped-html').binding('snippet')). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); - - it('should inline raw snippet if filtered as unsafe', function() { - expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")). - toBe("

an html\n" + - "click here\n" + - "snippet

"); - }); - - it('should update', function() { - input('snippet').enter('new text'); - expect(using('#html-filter').binding('snippet | html')).toBe('new text'); - expect(using('#escaped-html').binding('snippet')).toBe("new <b>text</b>"); - expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new text'); - }); -
-
- */ -//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 @@ -558,10 +456,10 @@ function htmlFilter() { linky filter -
<div ng:bind="snippet | linky">
</div>
+
<div ng:bind-html="snippet | linky">
</div>
-
+
@@ -574,10 +472,10 @@ function htmlFilter() { it('should linkify the snippet with urls', function() { expect(using('#linky-filter').binding('snippet | linky')). - toBe('Pretty text with some links:\n' + - 'http://angularjs.org/,\n' + - 'us@somewhere.org,\n' + - 'another@somewhere.org,\n' + + toBe('Pretty text with some links: ' + + 'http://angularjs.org/, ' + + 'us@somewhere.org, ' + + 'another@somewhere.org, ' + 'and one more: ftp://127.0.0.1/.'); }); @@ -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 @@ 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 @@ */ -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) { */ -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( 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( */ -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 @@ 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'); }); */ - - //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('