diff --git a/modules/angular2/src/common/pipes.ts b/modules/angular2/src/common/pipes.ts index 7c7a25c1ef..0f7f9a9952 100644 --- a/modules/angular2/src/common/pipes.ts +++ b/modules/angular2/src/common/pipes.ts @@ -10,6 +10,7 @@ import {JsonPipe} from './pipes/json_pipe'; import {SlicePipe} from './pipes/slice_pipe'; import {DatePipe} from './pipes/date_pipe'; import {DecimalPipe, PercentPipe, CurrencyPipe} from './pipes/number_pipe'; +import {ReplacePipe} from './pipes/replace_pipe'; import {CONST_EXPR} from 'angular2/src/facade/lang'; export {AsyncPipe} from './pipes/async_pipe'; @@ -19,6 +20,7 @@ export {SlicePipe} from './pipes/slice_pipe'; export {LowerCasePipe} from './pipes/lowercase_pipe'; export {NumberPipe, DecimalPipe, PercentPipe, CurrencyPipe} from './pipes/number_pipe'; export {UpperCasePipe} from './pipes/uppercase_pipe'; +export {ReplacePipe} from './pipes/replace_pipe'; /** * A collection of Angular core pipes that are likely to be used in each and every @@ -36,5 +38,6 @@ export const COMMON_PIPES = CONST_EXPR([ DecimalPipe, PercentPipe, CurrencyPipe, - DatePipe + DatePipe, + ReplacePipe ]); diff --git a/modules/angular2/src/common/pipes/common_pipes.ts b/modules/angular2/src/common/pipes/common_pipes.ts index ac6dc36c09..2bc6eeed07 100644 --- a/modules/angular2/src/common/pipes/common_pipes.ts +++ b/modules/angular2/src/common/pipes/common_pipes.ts @@ -10,6 +10,7 @@ import {JsonPipe} from './json_pipe'; import {SlicePipe} from './slice_pipe'; import {DatePipe} from './date_pipe'; import {DecimalPipe, PercentPipe, CurrencyPipe} from './number_pipe'; +import {ReplacePipe} from './replace_pipe'; import {CONST_EXPR} from 'angular2/src/facade/lang'; /** @@ -28,5 +29,6 @@ export const COMMON_PIPES = CONST_EXPR([ DecimalPipe, PercentPipe, CurrencyPipe, - DatePipe + DatePipe, + ReplacePipe ]); diff --git a/modules/angular2/src/common/pipes/replace_pipe.ts b/modules/angular2/src/common/pipes/replace_pipe.ts new file mode 100644 index 0000000000..de3c0e9f73 --- /dev/null +++ b/modules/angular2/src/common/pipes/replace_pipe.ts @@ -0,0 +1,91 @@ +import { + isBlank, + isString, + isNumber, + isFunction, + RegExpWrapper, + StringWrapper +} from 'angular2/src/facade/lang'; +import {BaseException} from 'angular2/src/facade/exceptions'; +import {Injectable, PipeTransform, Pipe} from 'angular2/core'; +import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception'; + +/** + * Creates a new String with some or all of the matches of a pattern replaced by + * a replacement. + * + * The pattern to be matched is specified by the 'pattern' parameter. + * + * The replacement to be set is specified by the 'replacement' parameter. + * + * An optional 'flags' parameter can be set. + * + * ### Usage + * + * expression | replace:pattern:replacement + * + * All behavior is based on the expected behavior of the JavaScript API + * String.prototype.replace() function. + * + * Where the input expression is a [String] or [Number] (to be treated as a string), + * the `pattern` is a [String] or [RegExp], + * the 'replacement' is a [String] or [Function]. + * + * --Note--: The 'pattern' parameter will be converted to a RegExp instance. Make sure to escape the + * string properly if you are matching for regular expression special characters like parenthesis, + * brackets etc. + */ + +@Pipe({name: 'replace'}) +@Injectable() +export class ReplacePipe implements PipeTransform { + transform(value: any, args: any[]): any { + if (isBlank(args) || args.length !== 2) { + throw new BaseException('ReplacePipe requires two arguments'); + } + + if (isBlank(value)) { + return value; + } + + if (!this._supportedInput(value)) { + throw new InvalidPipeArgumentException(ReplacePipe, value); + } + + var input = value.toString(); + var pattern = args[0]; + var replacement = args[1]; + + + if (!this._supportedPattern(pattern)) { + throw new InvalidPipeArgumentException(ReplacePipe, pattern); + } + if (!this._supportedReplacement(replacement)) { + throw new InvalidPipeArgumentException(ReplacePipe, replacement); + } + // template fails with literal RegExp e.g /pattern/igm + // var rgx = pattern instanceof RegExp ? pattern : RegExpWrapper.create(pattern); + + if (isFunction(replacement)) { + var rgxPattern = isString(pattern) ? RegExpWrapper.create(pattern) : pattern; + + return StringWrapper.replaceAllMapped(input, rgxPattern, replacement); + } + if (pattern instanceof RegExp) { + // use the replaceAll variant + return StringWrapper.replaceAll(input, pattern, replacement); + } + + return StringWrapper.replace(input, pattern, replacement); + } + + private _supportedInput(input: any): boolean { return isString(input) || isNumber(input); } + + private _supportedPattern(pattern: any): boolean { + return isString(pattern) || pattern instanceof RegExp; + } + + private _supportedReplacement(replacement: any): boolean { + return isString(replacement) || isFunction(replacement); + } +} diff --git a/modules/angular2/test/common/pipes/replace_pipe_spec.ts b/modules/angular2/test/common/pipes/replace_pipe_spec.ts new file mode 100644 index 0000000000..fee33b2168 --- /dev/null +++ b/modules/angular2/test/common/pipes/replace_pipe_spec.ts @@ -0,0 +1,73 @@ +import { + ddescribe, + describe, + it, + iit, + xit, + expect, + beforeEach, + afterEach, + browserDetection, + inject, + TestComponentBuilder, + AsyncTestCompleter +} from 'angular2/testing_internal'; + +import {ReplacePipe} from 'angular2/common'; +import {RegExpWrapper, StringJoiner} from 'angular2/src/facade/lang'; + +export function main() { + describe("ReplacePipe", () => { + var someNumber: number; + var str; + var pipe; + + beforeEach(() => { + someNumber = 42; + str = 'Douglas Adams'; + pipe = new ReplacePipe(); + }); + + describe("transform", () => { + + it("should not support input other than strings and numbers", () => { + expect(() => pipe.transform({}, ["Douglas", "Hugh"])).toThrow(); + expect(() => pipe.transform([1, 2, 3], ["Douglas", "Hugh"])).toThrow(); + }); + + it("should not support patterns other than strings and regular expressions", () => { + expect(() => pipe.transform(str, [{}, "Hugh"])).toThrow(); + expect(() => pipe.transform(str, [null, "Hugh"])).toThrow(); + expect(() => pipe.transform(str, [123, "Hugh"])).toThrow(); + }); + + it("should not support replacements other than strings and functions", () => { + expect(() => pipe.transform(str, ["Douglas", {}])).toThrow(); + expect(() => pipe.transform(str, ["Douglas", null])).toThrow(); + expect(() => pipe.transform(str, ["Douglas", 123])).toThrow(); + }); + + it("should return a new string with the pattern replaced", () => { + var result1 = pipe.transform(str, ["Douglas", "Hugh"]); + + var result2 = pipe.transform(str, [RegExpWrapper.create("a"), "_"]); + + var result3 = pipe.transform(str, [RegExpWrapper.create("a", "i"), "_"]); + + var f = (x => { return "Adams!"; }); + + var result4 = pipe.transform(str, ["Adams", f]); + + var result5 = pipe.transform(someNumber, ["2", "4"]); + + expect(result1).toEqual("Hugh Adams"); + expect(result2).toEqual("Dougl_s Ad_ms"); + expect(result3).toEqual("Dougl_s _d_ms"); + expect(result4).toEqual("Douglas Adams!"); + expect(result5).toEqual("44"); + }); + + }); + + }); +} diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 0becc25025..0714a7d014 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -431,6 +431,8 @@ var NG_COMMON = [ 'PatternValidator.validate()', 'PercentPipe', 'PercentPipe.transform()', + 'ReplacePipe', + 'ReplacePipe.transform()', 'RequiredValidator', 'SelectControlValueAccessor', 'SelectControlValueAccessor.onChange', diff --git a/tools/public_api_guard/public_api_spec.ts b/tools/public_api_guard/public_api_spec.ts index 1b71680138..dcbfc7a7f1 100644 --- a/tools/public_api_guard/public_api_spec.ts +++ b/tools/public_api_guard/public_api_spec.ts @@ -777,6 +777,8 @@ const COMMON = [ 'PatternValidator.validate(c:Control):{[key:string]:any}', 'PercentPipe', 'PercentPipe.transform(value:any, args:any[]):string', + 'ReplacePipe', + 'ReplacePipe.transform(value:any, args:any[]):any', 'RequiredValidator', 'SelectControlValueAccessor', 'SelectControlValueAccessor.constructor(_renderer:Renderer, _elementRef:ElementRef, query:QueryList)',