refactor(filter): filters are now injectable and services

BREAK:
 - removed CSS support from filters
This commit is contained in:
Misko Hevery 2011-11-03 15:59:18 -07:00
parent 6022f3df39
commit cb6f832f38
9 changed files with 180 additions and 179 deletions

View file

@ -4,10 +4,6 @@
display: none;
}
.ng-format-negative {
color: red;
}
ng\:form {
display: block;
}

View file

@ -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);

View file

@ -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) {

27
src/service/filter.js Normal file
View file

@ -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);
}

View file

@ -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>
*/
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 = '.';
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);
};
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;
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);
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 (!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;
if (isNumber(date)) {
date = new Date(date);
}
}
forEach(parts, function(value){
fn = DATE_FORMATS[value];
text += fn ? fn(date, $locale.DATETIME_FORMATS)
: value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
});
if (!isDate(date)) {
return date;
}
return text;
};
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, "'");
});
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:/;
function linkyFilter() {
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(''));
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(''));
};
};

View file

@ -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);

View file

@ -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");

View file

@ -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() {

View file

@ -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;