aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--css/angular.css4
-rw-r--r--src/Angular.js3
-rw-r--r--src/directives.js2
-rw-r--r--src/service/filter.js27
-rw-r--r--src/service/filter/filters.js182
-rw-r--r--src/service/parse.js26
-rw-r--r--test/directivesSpec.js22
-rw-r--r--test/service/filter/filtersSpec.js72
-rw-r--r--test/service/parseSpec.js19
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;