diff --git a/modules/angular2/src/compiler/output/output_jit.ts b/modules/angular2/src/compiler/output/output_jit.ts index e0273b5561..bb2e01edb2 100644 --- a/modules/angular2/src/compiler/output/output_jit.ts +++ b/modules/angular2/src/compiler/output/output_jit.ts @@ -9,6 +9,7 @@ import { import * as o from './output_ast'; import {EmitterVisitorContext} from './abstract_emitter'; import {AbstractJsEmitterVisitor} from './abstract_js_emitter'; +import {sanitizeIdentifier} from '../util'; export function jitStatements(sourceUrl: string, statements: o.Statement[], resultVar: string): any { @@ -36,13 +37,10 @@ class JitEmitterVisitor extends AbstractJsEmitterVisitor { if (id === -1) { id = this._evalArgValues.length; this._evalArgValues.push(value); - this._evalArgNames.push(sanitizeJitArgName(`jit_${ast.value.name}${id}`)); + var name = isPresent(ast.value.name) ? sanitizeIdentifier(ast.value.name) : 'val'; + this._evalArgNames.push(sanitizeIdentifier(`jit_${name}${id}`)); } ctx.print(this._evalArgNames[id]); return null; } } - -function sanitizeJitArgName(name: string): string { - return StringWrapper.replaceAll(name, /[\.\/]/g, '_'); -} diff --git a/modules/angular2/src/compiler/runtime_metadata.ts b/modules/angular2/src/compiler/runtime_metadata.ts index fd71a8bf6e..de8bf28d65 100644 --- a/modules/angular2/src/compiler/runtime_metadata.ts +++ b/modules/angular2/src/compiler/runtime_metadata.ts @@ -24,7 +24,7 @@ import {LifecycleHooks, LIFECYCLE_HOOKS_VALUES} from 'angular2/src/core/metadata import {reflector} from 'angular2/src/core/reflection/reflection'; import {Injectable, Inject, Optional} from 'angular2/src/core/di'; import {PLATFORM_DIRECTIVES, PLATFORM_PIPES} from 'angular2/src/core/platform_directives_and_pipes'; -import {MODULE_SUFFIX} from './util'; +import {MODULE_SUFFIX, sanitizeIdentifier} from './util'; import {assertArrayOfStrings} from './assertions'; import {getUrlScheme} from 'angular2/src/compiler/url_resolver'; import {Provider} from 'angular2/src/core/di/provider'; @@ -52,20 +52,18 @@ export class RuntimeMetadataResolver { @Optional() @Inject(PLATFORM_DIRECTIVES) private _platformDirectives: Type[], @Optional() @Inject(PLATFORM_PIPES) private _platformPipes: Type[]) {} - /** - * Wrap the stringify method to avoid naming things `function (arg1...) {` - */ - private sanitizeName(obj: any): string { - let result = StringWrapper.replaceAll(stringify(obj), /[\s-]/g, '_'); - if (result.indexOf('(') < 0) { - return result; + private sanitizeTokenName(token: any): string { + let identifier = stringify(token); + if (identifier.indexOf('(') >= 0) { + // case: anonymous functions! + let found = this._anonymousTypes.get(token); + if (isBlank(found)) { + this._anonymousTypes.set(token, this._anonymousTypeIndex++); + found = this._anonymousTypes.get(token); + } + identifier = `anonymous_token_${found}_`; } - let found = this._anonymousTypes.get(obj); - if (isBlank(found)) { - this._anonymousTypes.set(obj, this._anonymousTypeIndex++); - found = this._anonymousTypes.get(obj); - } - return `anonymous_type_${found}_`; + return sanitizeIdentifier(identifier); } getDirectiveMetadata(directiveType: Type): cpl.CompileDirectiveMetadata { @@ -130,7 +128,7 @@ export class RuntimeMetadataResolver { getTypeMetadata(type: Type, moduleUrl: string): cpl.CompileTypeMetadata { return new cpl.CompileTypeMetadata({ - name: this.sanitizeName(type), + name: this.sanitizeTokenName(type), moduleUrl: moduleUrl, runtime: type, diDeps: this.getDependenciesMetadata(type, null) @@ -139,7 +137,7 @@ export class RuntimeMetadataResolver { getFactoryMetadata(factory: Function, moduleUrl: string): cpl.CompileFactoryMetadata { return new cpl.CompileFactoryMetadata({ - name: this.sanitizeName(factory), + name: this.sanitizeTokenName(factory), moduleUrl: moduleUrl, runtime: factory, diDeps: this.getDependenciesMetadata(factory, null) @@ -227,17 +225,16 @@ export class RuntimeMetadataResolver { }); } - getRuntimeIdentifier(value: any): cpl.CompileIdentifierMetadata { - return new cpl.CompileIdentifierMetadata({runtime: value, name: this.sanitizeName(value)}); - } - getTokenMetadata(token: any): cpl.CompileTokenMetadata { token = resolveForwardRef(token); var compileToken; if (isString(token)) { compileToken = new cpl.CompileTokenMetadata({value: token}); } else { - compileToken = new cpl.CompileTokenMetadata({identifier: this.getRuntimeIdentifier(token)}); + compileToken = new cpl.CompileTokenMetadata({ + identifier: new cpl.CompileIdentifierMetadata( + {runtime: token, name: this.sanitizeTokenName(token)}) + }); } return compileToken; } @@ -266,7 +263,9 @@ export class RuntimeMetadataResolver { return new cpl.CompileProviderMetadata({ token: this.getTokenMetadata(provider.token), useClass: isPresent(provider.useClass) ? this.getTypeMetadata(provider.useClass, null) : null, - useValue: isPresent(provider.useValue) ? this.getRuntimeIdentifier(provider.useValue) : null, + useValue: isPresent(provider.useValue) ? + new cpl.CompileIdentifierMetadata({runtime: provider.useValue}) : + null, useFactory: isPresent(provider.useFactory) ? this.getFactoryMetadata(provider.useFactory, null) : null, diff --git a/modules/angular2/src/compiler/util.ts b/modules/angular2/src/compiler/util.ts index f1292a2f07..715acc61c0 100644 --- a/modules/angular2/src/compiler/util.ts +++ b/modules/angular2/src/compiler/util.ts @@ -23,3 +23,7 @@ export function splitAtColon(input: string, defaultValues: string[]): string[] { return defaultValues; } } + +export function sanitizeIdentifier(name: string): string { + return StringWrapper.replaceAll(name, /\W/g, '_'); +} diff --git a/modules/angular2/src/compiler/view_compiler/compile_view.ts b/modules/angular2/src/compiler/view_compiler/compile_view.ts index 9d7a8b22a2..41fad43044 100644 --- a/modules/angular2/src/compiler/view_compiler/compile_view.ts +++ b/modules/angular2/src/compiler/view_compiler/compile_view.ts @@ -1,5 +1,6 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang'; import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; +import {BaseException} from 'angular2/src/facade/exceptions'; import * as o from '../output/output_ast'; import {Identifiers, identifierToken} from '../identifiers'; @@ -47,7 +48,7 @@ export class CompileView implements NameResolver { public dirtyParentQueriesMethod: CompileMethod; public updateViewQueriesMethod: CompileMethod; public detectChangesInInputsMethod: CompileMethod; - public detectChangesHostPropertiesMethod: CompileMethod; + public detectChangesRenderPropertiesMethod: CompileMethod; public afterContentLifecycleCallbacksMethod: CompileMethod; public afterViewLifecycleCallbacksMethod: CompileMethod; public destroyMethod: CompileMethod; @@ -78,7 +79,7 @@ export class CompileView implements NameResolver { this.dirtyParentQueriesMethod = new CompileMethod(this); this.updateViewQueriesMethod = new CompileMethod(this); this.detectChangesInInputsMethod = new CompileMethod(this); - this.detectChangesHostPropertiesMethod = new CompileMethod(this); + this.detectChangesRenderPropertiesMethod = new CompileMethod(this); this.afterContentLifecycleCallbacksMethod = new CompileMethod(this); this.afterViewLifecycleCallbacksMethod = new CompileMethod(this); @@ -124,7 +125,18 @@ export class CompileView implements NameResolver { } createPipe(name: string): o.Expression { - var pipeMeta: CompilePipeMetadata = this.pipeMetas.find((pipeMeta) => pipeMeta.name == name); + var pipeMeta: CompilePipeMetadata = null; + for (var i = this.pipeMetas.length - 1; i >= 0; i--) { + var localPipeMeta = this.pipeMetas[i]; + if (localPipeMeta.name == name) { + pipeMeta = localPipeMeta; + break; + } + } + if (isBlank(pipeMeta)) { + throw new BaseException( + `Illegal state: Could not find pipe ${name} although the parser should have detected this error!`); + } var pipeFieldName = pipeMeta.pure ? `_pipe_${name}` : `_pipe_${name}_${this.pipes.size}`; var pipeExpr = this.pipes.get(pipeFieldName); if (isBlank(pipeExpr)) { diff --git a/modules/angular2/src/compiler/view_compiler/property_binder.ts b/modules/angular2/src/compiler/view_compiler/property_binder.ts index 9b1da586ed..266c60bfb5 100644 --- a/modules/angular2/src/compiler/view_compiler/property_binder.ts +++ b/modules/angular2/src/compiler/view_compiler/property_binder.ts @@ -71,7 +71,7 @@ export function bindRenderText(boundText: BoundTextAst, compileNode: CompileNode view.bindings.push(new CompileBinding(compileNode, boundText)); var currValExpr = createCurrValueExpr(bindingIndex); var valueField = createBindFieldExpr(bindingIndex); - view.detectChangesInInputsMethod.resetDebugInfo(compileNode.nodeIndex, boundText); + view.detectChangesRenderPropertiesMethod.resetDebugInfo(compileNode.nodeIndex, boundText); bind(view, currValExpr, valueField, boundText.value, o.THIS_EXPR.prop('context'), [ @@ -79,7 +79,7 @@ export function bindRenderText(boundText: BoundTextAst, compileNode: CompileNode .callMethod('setText', [compileNode.renderNode, currValExpr]) .toStmt() ], - view.detectChangesInInputsMethod); + view.detectChangesRenderPropertiesMethod); } function bindAndWriteToRenderer(boundProps: BoundElementPropertyAst[], context: o.Expression, @@ -89,7 +89,7 @@ function bindAndWriteToRenderer(boundProps: BoundElementPropertyAst[], context: boundProps.forEach((boundProp) => { var bindingIndex = view.bindings.length; view.bindings.push(new CompileBinding(compileElement, boundProp)); - view.detectChangesHostPropertiesMethod.resetDebugInfo(compileElement.nodeIndex, boundProp); + view.detectChangesRenderPropertiesMethod.resetDebugInfo(compileElement.nodeIndex, boundProp); var fieldExpr = createBindFieldExpr(bindingIndex); var currValExpr = createCurrValueExpr(bindingIndex); var renderMethod: string; @@ -125,7 +125,7 @@ function bindAndWriteToRenderer(boundProps: BoundElementPropertyAst[], context: .toStmt()); bind(view, currValExpr, fieldExpr, boundProp.value, context, updateStmts, - view.detectChangesHostPropertiesMethod); + view.detectChangesRenderPropertiesMethod); }); } diff --git a/modules/angular2/src/compiler/view_compiler/view_builder.ts b/modules/angular2/src/compiler/view_compiler/view_builder.ts index 275591f77d..71ba87cdd6 100644 --- a/modules/angular2/src/compiler/view_compiler/view_builder.ts +++ b/modules/angular2/src/compiler/view_compiler/view_builder.ts @@ -538,8 +538,8 @@ function generateDetectChangesMethod(view: CompileView): o.Statement[] { var stmts = []; if (view.detectChangesInInputsMethod.isEmpty() && view.updateContentQueriesMethod.isEmpty() && view.afterContentLifecycleCallbacksMethod.isEmpty() && - view.detectChangesHostPropertiesMethod.isEmpty() && view.updateViewQueriesMethod.isEmpty() && - view.afterViewLifecycleCallbacksMethod.isEmpty()) { + view.detectChangesRenderPropertiesMethod.isEmpty() && + view.updateViewQueriesMethod.isEmpty() && view.afterViewLifecycleCallbacksMethod.isEmpty()) { return stmts; } ListWrapper.addAll(stmts, view.detectChangesInInputsMethod.finish()); @@ -551,7 +551,7 @@ function generateDetectChangesMethod(view: CompileView): o.Statement[] { if (afterContentStmts.length > 0) { stmts.push(new o.IfStmt(o.not(DetectChangesVars.throwOnChange), afterContentStmts)); } - ListWrapper.addAll(stmts, view.detectChangesHostPropertiesMethod.finish()); + ListWrapper.addAll(stmts, view.detectChangesRenderPropertiesMethod.finish()); stmts.push(o.THIS_EXPR.callMethod('detectViewChildrenChanges', [DetectChangesVars.throwOnChange]) .toStmt()); var afterViewStmts = diff --git a/modules/angular2/test/core/linker/regression_integration_spec.ts b/modules/angular2/test/core/linker/regression_integration_spec.ts new file mode 100644 index 0000000000..e0993791df --- /dev/null +++ b/modules/angular2/test/core/linker/regression_integration_spec.ts @@ -0,0 +1,150 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + dispatchEvent, + expect, + iit, + inject, + beforeEachProviders, + it, + xit, + containsRegexp, + stringifyElement, + TestComponentBuilder, + fakeAsync, + tick, + clearPendingTimers, + ComponentFixture +} from 'angular2/testing_internal'; + +import {IS_DART} from 'angular2/src/facade/lang'; + +import { + Component, + Pipe, + PipeTransform, + provide, + ViewMetadata, + PLATFORM_PIPES, + OpaqueToken, + Injector +} from 'angular2/core'; +import {CompilerConfig} from 'angular2/compiler'; + +export function main() { + if (IS_DART) { + declareTests(false); + } else { + describe('jit', () => { + beforeEachProviders( + () => [provide(CompilerConfig, {useValue: new CompilerConfig(true, false, true)})]); + declareTests(true); + }); + + describe('no jit', () => { + beforeEachProviders( + () => [provide(CompilerConfig, {useValue: new CompilerConfig(true, false, false)})]); + declareTests(false); + }); + } +} + +function declareTests(isJit: boolean) { + // Place to put reproductions for regressions + describe('regressions', () => { + + describe('platform pipes', () => { + beforeEachProviders(() => [provide(PLATFORM_PIPES, {useValue: [PlatformPipe], multi: true})]); + + it('should overwrite them by custom pipes', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView( + MyComp, new ViewMetadata({template: '{{true | somePipe}}', pipes: [CustomPipe]})) + .createAsync(MyComp) + .then((fixture) => { + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('someCustomPipe'); + async.done(); + }); + })); + + }); + + describe('providers', () => { + function createInjector(tcb: TestComponentBuilder, proviers: any[]): Promise { + return tcb.overrideProviders(MyComp, [proviers]) + .createAsync(MyComp) + .then((fixture) => fixture.componentInstance.injector); + } + + it('should support providers with an OpaqueToken that contains a `.` in the name', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var token = new OpaqueToken('a.b'); + var tokenValue = 1; + createInjector(tcb, [provide(token, {useValue: tokenValue})]) + .then((injector: Injector) => { + expect(injector.get(token)).toEqual(tokenValue); + async.done(); + }); + })); + + it('should support providers with an anonymous function', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var token = () => true; + var tokenValue = 1; + createInjector(tcb, [provide(token, {useValue: tokenValue})]) + .then((injector: Injector) => { + expect(injector.get(token)).toEqual(tokenValue); + async.done(); + }); + })); + + it('should support providers with an OpaqueToken that has a StringMap as value', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var token1 = new OpaqueToken('someToken'); + var token2 = new OpaqueToken('someToken'); + var tokenValue1 = {'a': 1}; + var tokenValue2 = {'a': 1}; + createInjector( + tcb, + [provide(token1, {useValue: tokenValue1}), provide(token2, {useValue: tokenValue2})]) + .then((injector: Injector) => { + expect(injector.get(token1)).toEqual(tokenValue1); + expect(injector.get(token2)).toEqual(tokenValue2); + async.done(); + }); + })); + }); + + it('should allow logging a previous elements class binding via interpolation', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideTemplate(MyComp, `
Class: {{el.className}}
`) + .createAsync(MyComp) + .then((fixture) => { + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('Class: a'); + async.done(); + }); + })); + + }); +} + +@Component({selector: 'my-comp', template: ''}) +class MyComp { + constructor(public injector: Injector) {} +} + +@Pipe({name: 'somePipe', pure: true}) +class PlatformPipe implements PipeTransform { + transform(value: any, args: any[]): any { return 'somePlatformPipe'; } +} + +@Pipe({name: 'somePipe', pure: true}) +class CustomPipe implements PipeTransform { + transform(value: any, args: any[]): any { return 'someCustomPipe'; } +} diff --git a/modules/angular2/test/core/linker/view_injector_integration_spec.ts b/modules/angular2/test/core/linker/view_injector_integration_spec.ts index c8bb0bad99..62ce924fad 100644 --- a/modules/angular2/test/core/linker/view_injector_integration_spec.ts +++ b/modules/angular2/test/core/linker/view_injector_integration_spec.ts @@ -88,6 +88,8 @@ const ALL_PIPES = CONST_EXPR([ forwardRef(() => PipeNeedsService), forwardRef(() => PurePipe), forwardRef(() => ImpurePipe), + forwardRef(() => DuplicatePipe1), + forwardRef(() => DuplicatePipe2), ]); @Directive({selector: '[simpleDirective]'}) @@ -254,6 +256,15 @@ export class PipeNeedsService implements PipeTransform { transform(value: any, args: any[] = null): any { return this; } } +@Pipe({name: 'duplicatePipe'}) +export class DuplicatePipe1 implements PipeTransform { + transform(value: any, args: any[] = null): any { return this; } +} + +@Pipe({name: 'duplicatePipe'}) +export class DuplicatePipe2 implements PipeTransform { + transform(value: any, args: any[] = null): any { return this; } +} @Component({selector: 'root'}) class TestComp { @@ -628,6 +639,11 @@ export function main() { expect(el.children[0].inject(SimpleDirective).value.service).toEqual('pipeService'); })); + it('should overwrite pipes with later entry in the pipes array', fakeAsync(() => { + var el = createComp('
', tcb); + expect(el.children[0].inject(SimpleDirective).value).toBeAnInstanceOf(DuplicatePipe2); + })); + it('should inject ChangeDetectorRef into pipes', fakeAsync(() => { var el = createComp( '
',