diff options
| -rw-r--r-- | css/angular.css | 4 | ||||
| -rw-r--r-- | src/Angular.js | 3 | ||||
| -rw-r--r-- | src/directives.js | 2 | ||||
| -rw-r--r-- | src/service/filter.js | 27 | ||||
| -rw-r--r-- | src/service/filter/filters.js | 182 | ||||
| -rw-r--r-- | src/service/parse.js | 26 | ||||
| -rw-r--r-- | test/directivesSpec.js | 22 | ||||
| -rw-r--r-- | test/service/filter/filtersSpec.js | 72 | ||||
| -rw-r--r-- | test/service/parseSpec.js | 19 |
9 files changed, 179 insertions, 178 deletions
diff --git a/css/angular.css b/css/angular.css index 8b55f784..0c90b569 100644 --- a/css/angular.css +++ b/css/angular.css @@ -4,10 +4,6 @@ display: none; } -.ng-format-negative { - color: red; -} - ng\:form { display: block; } diff --git a/src/Angular.js b/src/Angular.js index d8a726c0..17aa2d4a 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -109,8 +109,6 @@ var _undefined = undefined, angularDirective = extensionMap(angular, 'directive', lowercase), /** @name angular.widget */ angularWidget = extensionMap(angular, 'widget', shivForIE), - /** @name angular.filter */ - angularFilter = extensionMap(angular, 'filter'), /** @name angular.service */ angularInputType = extensionMap(angular, 'inputType', lowercase), /** @name angular.service */ @@ -1054,6 +1052,7 @@ function ngModule($provide, $injector) { $provide.service('$defer', $DeferProvider); $provide.service('$document', $DocumentProvider); $provide.service('$exceptionHandler', $ExceptionHandlerProvider); + $provide.service('$filter', $FilterProvider); $provide.service('$formFactory', $FormFactoryProvider); $provide.service('$locale', $LocaleProvider); $provide.service('$location', $LocationProvider); diff --git a/src/directives.js b/src/directives.js index 20235f64..b511541f 100644 --- a/src/directives.js +++ b/src/directives.js @@ -236,7 +236,7 @@ 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 = parser(expression), + var exprFn = $parse(expression), lastValue = Number.NaN; this.$watch(function(scope) { diff --git a/src/service/filter.js b/src/service/filter.js new file mode 100644 index 00000000..7b85c23d --- /dev/null +++ b/src/service/filter.js @@ -0,0 +1,27 @@ +'use strict'; + +$FilterProvider.$inject = ['$provide']; +function $FilterProvider($provide) { + var suffix = '$Filter'; + + $provide.filter = function(name, factory) { + return $provide.factory(name + suffix, factory); + }; + + this.$get = ['$injector', function($injector) { + return function(name) { + return $injector(name + suffix); + } + }]; + + //////////////////////////////////////// + + $provide.filter('currency', currencyFilter); + $provide.filter('number', numberFilter); + $provide.filter('date', dateFilter); + $provide.filter('json', jsonFilter); + $provide.filter('lowercase', lowercaseFilter); + $provide.filter('uppercase', uppercaseFilter); + $provide.filter('html', htmlFilter); + $provide.filter('linky', linkyFilter); +} diff --git a/src/service/filter/filters.js b/src/service/filter/filters.js index b9c3d2ef..eea3cbf2 100644 --- a/src/service/filter/filters.js +++ b/src/service/filter/filters.js @@ -72,13 +72,15 @@ </doc:scenario> </doc:example> */ -angularFilter.currency = function(amount, currencySymbol){ - var formats = this.$service('$locale').NUMBER_FORMATS; - this.$element.toggleClass('ng-format-negative', amount < 0); - if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). - replace(/\u00A4/g, currencySymbol); -}; +currencyFilter.$inject = ['$locale']; +function currencyFilter($locale) { + var formats = $locale.NUMBER_FORMATS; + return function(amount, currencySymbol){ + if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; + return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). + replace(/\u00A4/g, currencySymbol); + }; +} /** * @ngdoc filter @@ -126,14 +128,17 @@ angularFilter.currency = function(amount, currencySymbol){ </doc:example> */ -var DECIMAL_SEP = '.'; -angularFilter.number = function(number, fractionSize) { - var formats = this.$service('$locale').NUMBER_FORMATS; - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, - formats.DECIMAL_SEP, fractionSize); -}; +numberFilter.$inject = ['$locale']; +function numberFilter($locale) { + var formats = $locale.NUMBER_FORMATS; + return function(number, fractionSize) { + return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + fractionSize); + }; +} +var DECIMAL_SEP = '.'; function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { if (isNaN(number) || !isFinite(number)) return ''; @@ -260,9 +265,7 @@ var DATE_FORMATS = { Z: timeZoneGetter }; -var GET_TIME_ZONE = /[A-Z]{3}(?![+\-])/, - DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, - OPERA_TOSTRING_PATTERN = /^[\d].*Z$/, +var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, NUMBER_STRING = /^\d+$/; /** @@ -343,49 +346,51 @@ var GET_TIME_ZONE = /[A-Z]{3}(?![+\-])/, </doc:scenario> </doc:example> */ -angularFilter.date = function(date, format) { - var $locale = this.$service('$locale'), - text = '', - parts = [], - fn, match; - - format = format || 'mediumDate' - format = $locale.DATETIME_FORMATS[format] || format; - if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = parseInt(date, 10); - } else { - date = angularString.toDate(date); +dateFilter.$inject = ['$locale']; +function dateFilter($locale) { + return function(date, format) { + var text = '', + parts = [], + fn, match; + + format = format || 'mediumDate' + format = $locale.DATETIME_FORMATS[format] || format; + if (isString(date)) { + if (NUMBER_STRING.test(date)) { + date = parseInt(date, 10); + } else { + date = angularString.toDate(date); + } } - } - if (isNumber(date)) { - date = new Date(date); - } + if (isNumber(date)) { + date = new Date(date); + } - if (!isDate(date)) { - return date; - } + if (!isDate(date)) { + return date; + } - while(format) { - match = DATE_FORMATS_SPLIT.exec(format); - if (match) { - parts = concat(parts, match, 1); - format = parts.pop(); - } else { - parts.push(format); - format = null; + while(format) { + match = DATE_FORMATS_SPLIT.exec(format); + if (match) { + parts = concat(parts, match, 1); + format = parts.pop(); + } else { + parts.push(format); + format = null; + } } - } - forEach(parts, function(value){ - fn = DATE_FORMATS[value]; - text += fn ? fn(date, $locale.DATETIME_FORMATS) - : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); - }); + forEach(parts, function(value){ + fn = DATE_FORMATS[value]; + text += fn ? fn(date, $locale.DATETIME_FORMATS) + : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + }); - return text; -}; + return text; + }; +} /** @@ -417,10 +422,11 @@ angularFilter.date = function(date, format) { </doc:example> * */ -angularFilter.json = function(object) { - this.$element.addClass("ng-monospace"); - return toJson(object, true, /^(\$|this$)/); -}; +function jsonFilter() { + return function(object) { + return toJson(object, true); + }; +} /** @@ -430,7 +436,7 @@ angularFilter.json = function(object) { * * @see angular.lowercase */ -angularFilter.lowercase = lowercase; +var lowercaseFilter = valueFn(lowercase); /** @@ -440,7 +446,7 @@ angularFilter.lowercase = lowercase; * * @see angular.uppercase */ -angularFilter.uppercase = uppercase; +var uppercaseFilter = valueFn(uppercase); /** @@ -537,9 +543,11 @@ angularFilter.uppercase = uppercase; </doc:scenario> </doc:example> */ -angularFilter.html = function(html, option){ - return new HTML(html, option); -}; +function htmlFilter() { + return function(html, option){ + return new HTML(html, option); + }; +} /** @@ -619,29 +627,31 @@ angularFilter.html = function(html, option){ </doc:scenario> </doc:example> */ -var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, - MAILTO_REGEXP = /^mailto:/; - -angularFilter.linky = function(text) { - if (!text) return text; - var match; - var raw = text; - var html = []; - var writer = htmlSanitizeWriter(html); - var url; - var i; - while ((match = raw.match(LINKY_URL_REGEXP))) { - // We can not end in these as they are sometimes found at the end of the sentence - url = match[0]; - // if we did not match ftp/http/mailto then assume mailto - if (match[2] == match[3]) url = 'mailto:' + url; - i = match.index; - writer.chars(raw.substr(0, i)); - writer.start('a', {href:url}); - writer.chars(match[0].replace(MAILTO_REGEXP, '')); - writer.end('a'); - raw = raw.substring(i + match[0].length); - } - writer.chars(raw); - return new HTML(html.join('')); +function linkyFilter() { + var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, + MAILTO_REGEXP = /^mailto:/; + + return function(text) { + if (!text) return text; + var match; + var raw = text; + var html = []; + var writer = htmlSanitizeWriter(html); + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/mailto then assume mailto + if (match[2] == match[3]) url = 'mailto:' + url; + i = match.index; + writer.chars(raw.substr(0, i)); + writer.start('a', {href:url}); + writer.chars(match[0].replace(MAILTO_REGEXP, '')); + writer.end('a'); + raw = raw.substring(i + match[0].length); + } + writer.chars(raw); + return new HTML(html.join('')); + }; }; diff --git a/src/service/parse.js b/src/service/parse.js index 465f416e..36f6b715 100644 --- a/src/service/parse.js +++ b/src/service/parse.js @@ -217,7 +217,7 @@ function lex(text){ ///////////////////////////////////////// -function parser(text, json){ +function parser(text, json, $filter){ var ZERO = valueFn(0), value, tokens = lex(text), @@ -227,8 +227,7 @@ function parser(text, json){ fieldAccess = _fieldAccess, objectIndex = _objectIndex, filterChain = _filterChain, - functionIdent = _functionIdent, - pipeFunction = _pipeFunction; + functionIdent = _functionIdent; if(json){ // The extra level of aliasing is here, just in case the lexer misses something, so that // we prevent any accidental execution in JSON. @@ -239,7 +238,6 @@ function parser(text, json){ assignable = filterChain = functionIdent = - pipeFunction = function() { throwError("is not valid json", {text:text, index:0}); }; value = primary(); } else { @@ -346,13 +344,9 @@ function parser(text, json){ } function filter() { - return pipeFunction(angularFilter); - } - - function _pipeFunction(fnScope){ - var fn = functionIdent(fnScope); + var token = expect(); + var fn = $filter(token.text); var argsFn = []; - var token; while(true) { if ((token = expect(':'))) { argsFn.push(expression()); @@ -719,13 +713,13 @@ function getterFn(path) { function $ParseProvider() { var cache = {}; - this.$get = ['$injector', function($injector) { + this.$get = ['$filter', function($filter) { return function(exp) { switch(typeof exp) { case 'string': return cache.hasOwnProperty(exp) ? cache[exp] - : cache[exp] = parser(exp); + : cache[exp] = parser(exp, false, $filter); case 'function': return exp; default: @@ -735,10 +729,14 @@ function $ParseProvider() { }]; } +function noFilters(){ + throw Error('Filters not supported!'); +} + // This is a special access for JSON parser which bypasses the injector var parseJson = function(json) { - return parser(json, true); + return parser(json, true, noFilters); }; // TODO(misko): temporary hack, until we get rid of the type augmentation -var expressionCompile = new $ParseProvider().$get[1](null); +var expressionCompile = new $ParseProvider().$get[1](noFilters); diff --git a/test/directivesSpec.js b/test/directivesSpec.js index 7acf87bc..93682fa0 100644 --- a/test/directivesSpec.js +++ b/test/directivesSpec.js @@ -41,25 +41,15 @@ describe("directive", function() { expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>'); })); - it('should set element element', inject(function($rootScope, $compile) { - angularFilter.myElement = function() { + it('should set element element', inject(function($rootScope, $compile, $provide) { + $provide.filter('myElement', valueFn(function() { return jqLite('<a>hello</a>'); - }; + })); var element = $compile('<div ng:bind="0|myElement"></div>')($rootScope); $rootScope.$digest(); expect(lowercase(element.html())).toEqual('<a>hello</a>'); })); - it('should have $element set to current bind element', inject(function($rootScope, $compile) { - angularFilter.myFilter = function() { - this.$element.addClass("filter"); - return 'HELLO'; - }; - var element = $compile('<div>before<div ng:bind="0|myFilter"></div>after</div>')($rootScope); - $rootScope.$digest(); - expect(sortedHtml(element)).toEqual('<div>before<div class="filter" ng:bind="0|myFilter">HELLO</div>after</div>'); - })); - it('should suppress rendering of falsy values', inject(function($rootScope, $compile) { var element = $compile('<div>{{ null }}{{ undefined }}{{ "" }}-{{ 0 }}{{ false }}</div>')($rootScope); @@ -83,12 +73,12 @@ describe("directive", function() { expect(element.text()).toEqual('Hello Misko!'); })); - it('should have $element set to current bind element', inject(function($rootScope, $compile) { + it('should have $element set to current bind element', inject(function($rootScope, $compile, $provide) { var innerText; - angularFilter.myFilter = function(text) { + $provide.filter('myFilter', valueFn(function(text) { innerText = innerText || this.$element.text(); return text; - }; + })); var element = $compile('<div>before<span ng:bind-template="{{\'HELLO\'|myFilter}}">INNER</span>after</div>')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual("beforeHELLOafter"); diff --git a/test/service/filter/filtersSpec.js b/test/service/filter/filtersSpec.js index 8c567441..39d034f5 100644 --- a/test/service/filter/filtersSpec.js +++ b/test/service/filter/filtersSpec.js @@ -2,27 +2,18 @@ describe('filter', function() { - var filter = angular.filter; + var filter; - it('should called the filter when evaluating expression', inject(function($rootScope) { - filter.fakeFilter = function() {}; - spyOn(filter, 'fakeFilter'); - - $rootScope.$eval('10|fakeFilter'); - expect(filter.fakeFilter).toHaveBeenCalledWith(10); - delete filter['fakeFilter']; + beforeEach(inject(function($filter){ + filter = $filter; })); - it('should call filter on scope context', inject(function($rootScope) { - $rootScope.name = 'misko'; - filter.fakeFilter = function() { - expect(this.name).toEqual('misko'); - }; - spyOn(filter, 'fakeFilter').andCallThrough(); + it('should called the filter when evaluating expression', inject(function($rootScope, $provide) { + var filter = jasmine.createSpy('myFilter'); + $provide.filter('myFilter', valueFn(filter)); - $rootScope.$eval('10|fakeFilter'); - expect(filter.fakeFilter).toHaveBeenCalled(); - delete filter['fakeFilter']; + $rootScope.$eval('10|myFilter'); + expect(filter).toHaveBeenCalledWith(10); })); describe('formatNumber', function() { @@ -81,40 +72,31 @@ describe('filter', function() { }); describe('currency', function() { - var currency, html, context; + var currency; - beforeEach(inject(function($rootScope) { - html = jqLite('<span></span>'); - context = $rootScope; - context.$element = html; - currency = bind(context, filter.currency); - })); + beforeEach(function() { + currency = filter('currency'); + }); it('should do basic currency filtering', function() { expect(currency(0)).toEqual('$0.00'); - expect(html.hasClass('ng-format-negative')).toBeFalsy(); expect(currency(-999)).toEqual('($999.00)'); - expect(html.hasClass('ng-format-negative')).toBeTruthy(); expect(currency(1234.5678, "USD$")).toEqual('USD$1,234.57'); - expect(html.hasClass('ng-format-negative')).toBeFalsy(); }); it('should return empty string for non-numbers', function() { expect(currency()).toBe(''); - expect(html.hasClass('ng-format-negative')).toBeFalsy(); expect(currency('abc')).toBe(''); - expect(html.hasClass('ng-format-negative')).toBeFalsy(); }); }); describe('number', function() { - var context, number; + var number; beforeEach(inject(function($rootScope) { - context = $rootScope; - number = bind(context, filter.number); + number = filter('number'); })); @@ -151,34 +133,39 @@ describe('filter', function() { describe('json', function () { it('should do basic filter', function() { - expect(filter.json.call({$element:jqLite('<div></div>')}, {a:"b"})).toEqual(toJson({a:"b"}, true)); + expect(filter('json')({a:"b"})).toEqual(toJson({a:"b"}, true)); }); }); describe('lowercase', function() { it('should do basic filter', function() { - expect(filter.lowercase('AbC')).toEqual('abc'); - expect(filter.lowercase(null)).toBeNull(); + expect(filter('lowercase')('AbC')).toEqual('abc'); + expect(filter('lowercase')(null)).toBeNull(); }); }); describe('uppercase', function() { it('should do basic filter', function() { - expect(filter.uppercase('AbC')).toEqual('ABC'); - expect(filter.uppercase(null)).toBeNull(); + expect(filter('uppercase')('AbC')).toEqual('ABC'); + expect(filter('uppercase')(null)).toBeNull(); }); }); describe('html', function() { it('should do basic filter', function() { - var html = filter.html("a<b>c</b>d"); + var html = filter('html')("a<b>c</b>d"); expect(html instanceof HTML).toBeTruthy(); expect(html.html).toEqual("a<b>c</b>d"); }); }); describe('linky', function() { - var linky = filter.linky; + var linky; + + beforeEach(inject(function($filter){ + linky = $filter('linky') + })); + it('should do basic filter', function() { expect(linky("http://ab/ (http://a/) <http://a/> http://1.2/v:~-123. c").html). toEqual('<a href="http://ab/">http://ab/</a> ' + @@ -205,11 +192,10 @@ describe('filter', function() { var midnight = new angular.mock.TzDate(+5, '2010-09-03T05:05:08.000Z'); //12am var earlyDate = new angular.mock.TzDate(+5, '0001-09-03T05:05:08.000Z'); - var context, date; + var date; - beforeEach(inject(function($rootScope) { - context = $rootScope; - date = bind(context, filter.date); + beforeEach(inject(function($filter) { + date = $filter('date'); })); it('should ignore falsy inputs', function() { diff --git a/test/service/parseSpec.js b/test/service/parseSpec.js index 5045ec9e..506d3373 100644 --- a/test/service/parseSpec.js +++ b/test/service/parseSpec.js @@ -191,24 +191,19 @@ describe('parser', function() { expect(scope.$eval("'a' + 'b c'")).toEqual("ab c"); }); - it('should parse filters', function() { - angular.filter.substring = function(input, start, end) { + it('should parse filters', inject(function($provide) { + $provide.filter('substring', valueFn(function(input, start, end) { return input.substring(start, end); - }; - - angular.filter.upper = {_case: function(input) { - return input.toUpperCase(); - }}; + })); expect(function() { - scope.$eval("1|nonExistant"); - }).toThrow(new Error("Syntax Error: Token 'nonExistant' should be a function at column 3 of the expression [1|nonExistant] starting at [nonExistant].")); + scope.$eval("1|nonexistent"); + }).toThrow(new Error("Unknown provider for 'nonexistent$Filter'.")); scope.offset = 3; - expect(scope.$eval("'abcd'|upper._case")).toEqual("ABCD"); expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); - expect(scope.$eval("'abcd'|substring:1:3|upper._case")).toEqual("BC"); - }); + expect(scope.$eval("'abcd'|substring:1:3|uppercase")).toEqual("BC"); + })); it('should access scope', function() { scope.a = 123; |
