diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index 713ffd1e42..106f5f07d4 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -156,6 +156,7 @@ export class CompileMetadataResolver { const templateName = inputs[propName]; factory.inputs.push({propName, templateName}); } + const outputsArr: {propName: string, templateName: string}[] = []; for (let propName in outputs) { const templateName = outputs[propName]; factory.outputs.push({propName, templateName}); diff --git a/packages/compiler/test/aot/compiler_spec.ts b/packages/compiler/test/aot/compiler_spec.ts index 072af8adf8..2dab0973cd 100644 --- a/packages/compiler/test/aot/compiler_spec.ts +++ b/packages/compiler/test/aot/compiler_spec.ts @@ -279,6 +279,7 @@ describe('compiler (unbundled Angular)', () => { const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles); const aotHost = new MockAotCompilerHost(host); let generatedFiles: GeneratedFile[]; + const warnSpy = spyOn(console, 'warn'); compile(host, aotHost, expectNoDiagnostics).then((f) => generatedFiles = f); tick(); diff --git a/packages/examples/upgrade/static/ts/module.ts b/packages/examples/upgrade/static/ts/module.ts index ef5b3b44bc..6c6d0aeb6f 100644 --- a/packages/examples/upgrade/static/ts/module.ts +++ b/packages/examples/upgrade/static/ts/module.ts @@ -145,7 +145,12 @@ ng1AppModule.factory('heroesService', downgradeInjectable(HeroesService)); // #docregion ng2-heroes-wrapper // This is directive will act as the interface to the "downgraded" Angular component -ng1AppModule.directive('ng2Heroes', downgradeComponent({component: Ng2HeroesComponent})); +ng1AppModule.directive( + 'ng2Heroes', + downgradeComponent( + // The inputs and outputs here must match the relevant names of the properties on the + // "downgraded" component + {component: Ng2HeroesComponent, inputs: ['heroes'], outputs: ['addHero', 'removeHero']})); // #enddocregion // #docregion example-app diff --git a/packages/upgrade/src/common/component_info.ts b/packages/upgrade/src/common/component_info.ts index 898bb72456..3419fd178e 100644 --- a/packages/upgrade/src/common/component_info.ts +++ b/packages/upgrade/src/common/component_info.ts @@ -6,6 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ +import {Type} from '@angular/core'; + +export interface ComponentInfo { + component: Type; + inputs?: string[]; + outputs?: string[]; + selectors?: string[]; +} + /** * A `PropertyBinding` represents a mapping between a property name * and an attribute name. It is parsed from a string of the form @@ -13,6 +22,8 @@ * and attribute have the same identifier. */ export class PropertyBinding { + prop: string; + attr: string; bracketAttr: string; bracketParenAttr: string; parenAttr: string; @@ -20,9 +31,12 @@ export class PropertyBinding { bindAttr: string; bindonAttr: string; - constructor(public prop: string, public attr: string) { this.parseBinding(); } + constructor(public binding: string) { this.parseBinding(); } private parseBinding() { + const parts = this.binding.split(':'); + this.prop = parts[0].trim(); + this.attr = (parts[1] || this.prop).trim(); this.bracketAttr = `[${this.attr}]`; this.parenAttr = `(${this.attr})`; this.bracketParenAttr = `[(${this.attr})]`; diff --git a/packages/upgrade/src/common/content_projection_helper.ts b/packages/upgrade/src/common/content_projection_helper.ts new file mode 100644 index 0000000000..df9eb42960 --- /dev/null +++ b/packages/upgrade/src/common/content_projection_helper.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Type} from '@angular/core'; +import * as angular from './angular1'; + + +export class ContentProjectionHelper { + groupProjectableNodes($injector: angular.IInjectorService, component: Type, nodes: Node[]): + Node[][] { + // By default, do not support multi-slot projection, + // as `upgrade/static` does not support it yet. + return [nodes]; + } +} diff --git a/packages/upgrade/src/common/downgrade_component.ts b/packages/upgrade/src/common/downgrade_component.ts index 4becae7b03..a25609be84 100644 --- a/packages/upgrade/src/common/downgrade_component.ts +++ b/packages/upgrade/src/common/downgrade_component.ts @@ -11,6 +11,7 @@ import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angul import * as angular from './angular1'; import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants'; import {DowngradeComponentAdapter} from './downgrade_component_adapter'; +import {NgContentSelectorHelper} from './ng_content_selector_helper'; import {controllerKey, getComponentName} from './util'; let downgradeCount = 0; @@ -37,6 +38,15 @@ let downgradeCount = 0; * * {@example upgrade/static/ts/module.ts region="ng2-heroes-wrapper"} * + * In this example you can see that we must provide information about the component being + * "downgraded". This is because once the AoT compiler has run, all metadata about the + * component has been removed from the code, and so cannot be inferred. + * + * We must do the following: + * * specify the Angular component class that is to be downgraded + * * specify all inputs and outputs that the AngularJS component expects + * * specify the selectors used in any `ng-content` elements in the component's template + * * @description * * A helper function that returns a factory function to be used for registering an @@ -45,17 +55,28 @@ let downgradeCount = 0; * The parameter contains information about the Component that is being downgraded: * * * `component: Type`: The type of the Component that will be downgraded + * * `inputs: string[]`: A collection of strings that specify what inputs the component accepts + * * `outputs: string[]`: A collection of strings that specify what outputs the component emits + * * `selectors: string[]`: A collection of strings that specify what selectors are expected on + * `ng-content` elements in the template to enable content projection (a.k.a. transclusion in + * AngularJS) + * + * The `inputs` and `outputs` are strings that map the names of properties to camelCased + * attribute names. They are of the form `"prop: attr"`; or simply `"propAndAttr" where the + * property and attribute have the same identifier. + * + * The `selectors` are the values of the `select` attribute of each of the `ng-content` elements + * that appear in the downgraded component's template. + * These selectors must be provided in the order that they appear in the template as they are + * mapped by index to the projected nodes. * * @experimental */ -export function downgradeComponent(info: { +export function downgradeComponent(info: /* ComponentInfo */ { component: Type; - /** @deprecated since v4. This parameter is no longer used */ inputs?: string[]; - /** @deprecated since v4. This parameter is no longer used */ outputs?: string[]; - /** @deprecated since v4. This parameter is no longer used */ - selectors?: string[]; + selectors?: string[] }): any /* angular.IInjectable */ { const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`; let idCount = 0; @@ -93,7 +114,7 @@ export function downgradeComponent(info: { const id = idPrefix + (idCount++); const injectorPromise = new ParentInjectorPromise(element); const facade = new DowngradeComponentAdapter( - id, element, attrs, scope, ngModel, injector, $injector, $compile, $parse, + id, info, element, attrs, scope, ngModel, injector, $injector, $compile, $parse, componentFactory); const projectableNodes = facade.compileContents(); diff --git a/packages/upgrade/src/common/downgrade_component_adapter.ts b/packages/upgrade/src/common/downgrade_component_adapter.ts index 8af71d8a65..36f9e0e808 100644 --- a/packages/upgrade/src/common/downgrade_component_adapter.ts +++ b/packages/upgrade/src/common/downgrade_component_adapter.ts @@ -9,8 +9,9 @@ import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core'; import * as angular from './angular1'; -import {PropertyBinding} from './component_info'; +import {ComponentInfo, PropertyBinding} from './component_info'; import {$SCOPE} from './constants'; +import {NgContentSelectorHelper} from './ng_content_selector_helper'; import {getAttributesAsArray, getComponentName, hookupNgModel} from './util'; const INITIAL_VALUE = { @@ -26,7 +27,7 @@ export class DowngradeComponentAdapter { private changeDetector: ChangeDetectorRef = null; constructor( - private id: string, private element: angular.IAugmentedJQuery, + private id: string, private info: ComponentInfo, private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes, private scope: angular.IScope, private ngModel: angular.INgModelController, private parentInjector: Injector, private $injector: angular.IInjectorService, private $compile: angular.ICompileService, @@ -66,9 +67,9 @@ export class DowngradeComponentAdapter { setupInputs(): void { const attrs = this.attrs; - const inputs = this.componentFactory.inputs || []; + const inputs = this.info.inputs || []; for (let i = 0; i < inputs.length; i++) { - const input = new PropertyBinding(inputs[i].propName, inputs[i].templateName); + const input = new PropertyBinding(inputs[i]); let expr: any /** TODO #9100 */ = null; if (attrs.hasOwnProperty(input.attr)) { @@ -102,7 +103,7 @@ export class DowngradeComponentAdapter { } } - const prototype = this.componentFactory.componentType.prototype; + const prototype = this.info.component.prototype; if (prototype && (prototype).ngOnChanges) { // Detect: OnChanges interface this.inputChanges = {}; @@ -117,9 +118,9 @@ export class DowngradeComponentAdapter { setupOutputs() { const attrs = this.attrs; - const outputs = this.componentFactory.outputs || []; + const outputs = this.info.outputs || []; for (let j = 0; j < outputs.length; j++) { - const output = new PropertyBinding(outputs[j].propName, outputs[j].templateName); + const output = new PropertyBinding(outputs[j]); let expr: any /** TODO #9100 */ = null; let assignExpr = false; @@ -157,7 +158,7 @@ export class DowngradeComponentAdapter { }); } else { throw new Error( - `Missing emitter '${output.prop}' on component '${getComponentName(this.componentFactory.componentType)}'!`); + `Missing emitter '${output.prop}' on component '${getComponentName(this.info.component)}'!`); } } } @@ -182,31 +183,49 @@ export class DowngradeComponentAdapter { } groupProjectableNodes() { - let ngContentSelectors = this.componentFactory.ngContentSelectors; - return groupNodesBySelector(ngContentSelectors, this.element.contents()); + const ngContentSelectorHelper = + this.parentInjector.get(NgContentSelectorHelper) as NgContentSelectorHelper; + const ngContentSelectors = ngContentSelectorHelper.getNgContentSelectors(this.info); + + if (!ngContentSelectors) { + throw new Error('Expecting ngContentSelectors for: ' + getComponentName(this.info.component)); + } + + return this._groupNodesBySelector(ngContentSelectors, this.element.contents()); + } + + /** + * Group a set of DOM nodes into `ngContent` groups, based on the given content selectors. + */ + private _groupNodesBySelector(ngContentSelectors: string[], nodes: Node[]): Node[][] { + const projectableNodes: Node[][] = []; + let wildcardNgContentIndex: number; + + for (let i = 0, ii = ngContentSelectors.length; i < ii; ++i) { + projectableNodes[i] = []; + } + + for (let j = 0, jj = nodes.length; j < jj; ++j) { + const node = nodes[j]; + const ngContentIndex = findMatchingNgContentIndex(node, ngContentSelectors); + if (ngContentIndex != null) { + projectableNodes[ngContentIndex].push(node); + } + } + + return projectableNodes; } } -/** - * Group a set of DOM nodes into `ngContent` groups, based on the given content selectors. - */ -export function groupNodesBySelector(ngContentSelectors: string[], nodes: Node[]): Node[][] { - const projectableNodes: Node[][] = []; - let wildcardNgContentIndex: number; +let _matches: (this: any, selector: string) => boolean; - for (let i = 0, ii = ngContentSelectors.length; i < ii; ++i) { - projectableNodes[i] = []; +function matchesSelector(el: any, selector: string): boolean { + if (!_matches) { + const elProto = Element.prototype; + _matches = elProto.matchesSelector || elProto.mozMatchesSelector || elProto.msMatchesSelector || + elProto.oMatchesSelector || elProto.webkitMatchesSelector; } - - for (let j = 0, jj = nodes.length; j < jj; ++j) { - const node = nodes[j]; - const ngContentIndex = findMatchingNgContentIndex(node, ngContentSelectors); - if (ngContentIndex != null) { - projectableNodes[ngContentIndex].push(node); - } - } - - return projectableNodes; + return _matches.call(el, selector); } function findMatchingNgContentIndex(element: any, ngContentSelectors: string[]): number { @@ -228,15 +247,4 @@ function findMatchingNgContentIndex(element: any, ngContentSelectors: string[]): ngContentIndices.push(wildcardNgContentIndex); } return ngContentIndices.length ? ngContentIndices[0] : null; -} - -let _matches: (this: any, selector: string) => boolean; - -function matchesSelector(el: any, selector: string): boolean { - if (!_matches) { - const elProto = Element.prototype; - _matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector || - elProto.msMatchesSelector || elProto.oMatchesSelector || elProto.webkitMatchesSelector; - } - return el.nodeType === Node.ELEMENT_NODE ? _matches.call(el, selector) : false; -} +} \ No newline at end of file diff --git a/packages/upgrade/src/common/ng_content_selector_helper.ts b/packages/upgrade/src/common/ng_content_selector_helper.ts new file mode 100644 index 0000000000..62ea7aa564 --- /dev/null +++ b/packages/upgrade/src/common/ng_content_selector_helper.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentInfo} from './component_info'; + +/** + * This class gives an extension point between the static and dynamic versions + * of ngUpgrade: + * * In the static version (this one) we must specify them manually as part of + * the call to `downgradeComponent(...)`. + * * In the dynamic version (`DynamicNgContentSelectorHelper`) we are able to + * ask the compiler for the selectors of a component. + */ +export class NgContentSelectorHelper { + getNgContentSelectors(info: ComponentInfo): string[] { + // if no selectors are passed then default to a single "wildcard" selector + return info.selectors || ['*']; + } +} diff --git a/packages/upgrade/src/dynamic/content_projection_helper.ts b/packages/upgrade/src/dynamic/content_projection_helper.ts new file mode 100644 index 0000000000..338cdef363 --- /dev/null +++ b/packages/upgrade/src/dynamic/content_projection_helper.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CssSelector, SelectorMatcher, createElementCssSelector} from '@angular/compiler'; +import {Compiler, Type} from '@angular/core'; + +import * as angular from '../common/angular1'; +import {COMPILER_KEY} from '../common/constants'; +import {ContentProjectionHelper} from '../common/content_projection_helper'; +import {getAttributesAsArray, getComponentName} from '../common/util'; + + +export class DynamicContentProjectionHelper extends ContentProjectionHelper { + groupProjectableNodes($injector: angular.IInjectorService, component: Type, nodes: Node[]): + Node[][] { + const ng2Compiler = $injector.get(COMPILER_KEY) as Compiler; + const ngContentSelectors = ng2Compiler.getNgContentSelectors(component); + + if (!ngContentSelectors) { + throw new Error('Expecting ngContentSelectors for: ' + getComponentName(component)); + } + + return this.groupNodesBySelector(ngContentSelectors, nodes); + } + + /** + * Group a set of DOM nodes into `ngContent` groups, based on the given content selectors. + */ + groupNodesBySelector(ngContentSelectors: string[], nodes: Node[]): Node[][] { + const projectableNodes: Node[][] = []; + let matcher = new SelectorMatcher(); + let wildcardNgContentIndex: number; + + for (let i = 0, ii = ngContentSelectors.length; i < ii; ++i) { + projectableNodes[i] = []; + + const selector = ngContentSelectors[i]; + if (selector === '*') { + wildcardNgContentIndex = i; + } else { + matcher.addSelectables(CssSelector.parse(selector), i); + } + } + + for (let j = 0, jj = nodes.length; j < jj; ++j) { + const ngContentIndices: number[] = []; + const node = nodes[j]; + const selector = + createElementCssSelector(node.nodeName.toLowerCase(), getAttributesAsArray(node)); + + matcher.match(selector, (_, index) => ngContentIndices.push(index)); + ngContentIndices.sort(); + + if (wildcardNgContentIndex !== undefined) { + ngContentIndices.push(wildcardNgContentIndex); + } + + if (ngContentIndices.length) { + projectableNodes[ngContentIndices[0]].push(node); + } + } + + return projectableNodes; + } +} diff --git a/packages/upgrade/src/dynamic/ng_content_selector_helper.ts b/packages/upgrade/src/dynamic/ng_content_selector_helper.ts new file mode 100644 index 0000000000..085d101afe --- /dev/null +++ b/packages/upgrade/src/dynamic/ng_content_selector_helper.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Compiler, Injectable} from '@angular/core'; + +import {ComponentInfo} from '../common/component_info'; +import {NgContentSelectorHelper} from '../common/ng_content_selector_helper'; + + +/** + * See `NgContentSelectorHelper` for more information about this class. + */ +@Injectable() +export class DynamicNgContentSelectorHelper extends NgContentSelectorHelper { + constructor(private compiler: Compiler) { super(); } + getNgContentSelectors(info: ComponentInfo): string[] { + return this.compiler.getNgContentSelectors(info.component); + } +} diff --git a/packages/upgrade/src/dynamic/upgrade_adapter.ts b/packages/upgrade/src/dynamic/upgrade_adapter.ts index e13f0daa96..14efd6870d 100644 --- a/packages/upgrade/src/dynamic/upgrade_adapter.ts +++ b/packages/upgrade/src/dynamic/upgrade_adapter.ts @@ -6,15 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ +import {DirectiveResolver} from '@angular/compiler'; import {Compiler, CompilerOptions, Directive, Injector, NgModule, NgModuleRef, NgZone, Provider, Testability, Type} from '@angular/core'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import * as angular from '../common/angular1'; +import {ComponentInfo} from '../common/component_info'; import {$$TESTABILITY, $COMPILE, $INJECTOR, $ROOT_SCOPE, COMPILER_KEY, INJECTOR_KEY, NG_ZONE_KEY} from '../common/constants'; import {downgradeComponent} from '../common/downgrade_component'; import {downgradeInjectable} from '../common/downgrade_injectable'; +import {NgContentSelectorHelper} from '../common/ng_content_selector_helper'; import {Deferred, controllerKey, onError} from '../common/util'; +import {DynamicNgContentSelectorHelper} from './ng_content_selector_helper'; import {UpgradeNg1ComponentAdapterBuilder} from './upgrade_ng1_adapter'; let upgradeCount: number = 0; @@ -100,6 +104,7 @@ let upgradeCount: number = 0; */ export class UpgradeAdapter { private idPrefix: string = `NG2_UPGRADE_${upgradeCount++}_`; + private directiveResolver: DirectiveResolver = new DirectiveResolver(); private downgradedComponents: Type[] = []; /** * An internal map of ng1 components which need to up upgraded to ng2. @@ -185,7 +190,10 @@ export class UpgradeAdapter { downgradeNg2Component(component: Type): Function { this.downgradedComponents.push(component); - return downgradeComponent({component}); + const metadata: Directive = this.directiveResolver.resolve(component); + const info: ComponentInfo = {component, inputs: metadata.inputs, outputs: metadata.outputs}; + + return downgradeComponent(info); } /** @@ -553,6 +561,7 @@ export class UpgradeAdapter { providers: [ {provide: $INJECTOR, useFactory: () => ng1Injector}, {provide: $COMPILE, useFactory: () => ng1Injector.get($COMPILE)}, + {provide: NgContentSelectorHelper, useClass: DynamicNgContentSelectorHelper}, this.upgradedProviders ], imports: [this.ng2AppModule], diff --git a/packages/upgrade/src/static/upgrade_module.ts b/packages/upgrade/src/static/upgrade_module.ts index e663ca9356..5c32a814c9 100644 --- a/packages/upgrade/src/static/upgrade_module.ts +++ b/packages/upgrade/src/static/upgrade_module.ts @@ -10,6 +10,7 @@ import {Injector, NgModule, NgZone, Testability} from '@angular/core'; import * as angular from '../common/angular1'; import {$$TESTABILITY, $DELEGATE, $INJECTOR, $PROVIDE, $ROOT_SCOPE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from '../common/constants'; +import {NgContentSelectorHelper} from '../common/ng_content_selector_helper'; import {controllerKey} from '../common/util'; import {angular1Providers, setTempInjectorRef} from './angular1_providers'; @@ -129,7 +130,7 @@ import {angular1Providers, setTempInjectorRef} from './angular1_providers'; * * @experimental */ -@NgModule({providers: [angular1Providers]}) +@NgModule({providers: [angular1Providers, NgContentSelectorHelper]}) export class UpgradeModule { /** * The AngularJS `$injector` for the upgrade application. diff --git a/packages/upgrade/test/common/component_info_spec.ts b/packages/upgrade/test/common/component_info_spec.ts index 5bddf8d2ae..b4b13f2fe4 100644 --- a/packages/upgrade/test/common/component_info_spec.ts +++ b/packages/upgrade/test/common/component_info_spec.ts @@ -11,7 +11,8 @@ import {PropertyBinding} from '@angular/upgrade/src/common/component_info'; export function main() { describe('PropertyBinding', () => { it('should process a simple binding', () => { - const binding = new PropertyBinding('someBinding', 'someBinding'); + const binding = new PropertyBinding('someBinding'); + expect(binding.binding).toEqual('someBinding'); expect(binding.prop).toEqual('someBinding'); expect(binding.attr).toEqual('someBinding'); expect(binding.bracketAttr).toEqual('[someBinding]'); @@ -23,7 +24,21 @@ export function main() { }); it('should process a two-part binding', () => { - const binding = new PropertyBinding('someProp', 'someAttr'); + const binding = new PropertyBinding('someProp:someAttr'); + expect(binding.binding).toEqual('someProp:someAttr'); + expect(binding.prop).toEqual('someProp'); + expect(binding.attr).toEqual('someAttr'); + expect(binding.bracketAttr).toEqual('[someAttr]'); + expect(binding.bracketParenAttr).toEqual('[(someAttr)]'); + expect(binding.parenAttr).toEqual('(someAttr)'); + expect(binding.onAttr).toEqual('onSomeAttr'); + expect(binding.bindAttr).toEqual('bindSomeAttr'); + expect(binding.bindonAttr).toEqual('bindonSomeAttr'); + }); + + it('should cope with whitespace', () => { + const binding = new PropertyBinding(' someProp : someAttr '); + expect(binding.binding).toEqual(' someProp : someAttr '); expect(binding.prop).toEqual('someProp'); expect(binding.attr).toEqual('someAttr'); expect(binding.bracketAttr).toEqual('[someAttr]'); diff --git a/packages/upgrade/test/common/downgrade_component_adapter_spec.ts b/packages/upgrade/test/common/downgrade_component_adapter_spec.ts index 730c2495cd..c92b6f601b 100644 --- a/packages/upgrade/test/common/downgrade_component_adapter_spec.ts +++ b/packages/upgrade/test/common/downgrade_component_adapter_spec.ts @@ -6,13 +6,25 @@ * found in the LICENSE file at https://angular.io/license */ import * as angular from '@angular/upgrade/src/common/angular1'; -import {groupNodesBySelector} from '@angular/upgrade/src/common/downgrade_component_adapter'; +import {DowngradeComponentAdapter} from '@angular/upgrade/src/common/downgrade_component_adapter'; +import {NgContentSelectorHelper} from '@angular/upgrade/src/common/ng_content_selector_helper'; import {nodes} from './test_helpers'; export function main() { describe('DowngradeComponentAdapter', () => { describe('groupNodesBySelector', () => { + function createAdapter(selectors: string[], contentNodes: Node[]): DowngradeComponentAdapter { + const selectorHelper = new NgContentSelectorHelper(); + const fakeInjector = {get: function() { return selectorHelper; }}; + const fakeScope = { $new: function() {} } as any; + const element = angular.element('
'); + element.append(contentNodes); + return new DowngradeComponentAdapter( + 'id', {component: null, selectors}, element, null, fakeScope, null, fakeInjector, null, + null, null, null); + } + it('should return an array of node collections for each selector', () => { const contentNodes = nodes( '
div-1 content
' + @@ -22,7 +34,8 @@ export function main() { '
div-2 content
'); const selectors = ['input[type=date]', 'span', '.x']; - const projectableNodes = groupNodesBySelector(selectors, contentNodes); + const adapter = createAdapter(selectors, contentNodes); + const projectableNodes = adapter.groupProjectableNodes(); expect(projectableNodes[0]).toEqual(nodes('')); expect(projectableNodes[1]).toEqual(nodes('span content')); @@ -41,7 +54,8 @@ export function main() { '
div-2 content
'); const selectors = ['.x', '*', 'input[type=date]']; - const projectableNodes = groupNodesBySelector(selectors, contentNodes); + const adapter = createAdapter(selectors, contentNodes); + const projectableNodes = adapter.groupProjectableNodes(); expect(projectableNodes[0]) .toEqual(nodes( @@ -56,7 +70,8 @@ export function main() { it('should return an array of empty arrays if there are no nodes passed in', () => { const selectors = ['.x', '*', 'input[type=date]']; - const projectableNodes = groupNodesBySelector(selectors, []); + const adapter = createAdapter(selectors, []); + const projectableNodes = adapter.groupProjectableNodes(); expect(projectableNodes).toEqual([[], [], []]); }); @@ -68,10 +83,12 @@ export function main() { 'span content' + '
div-2 content
'); - const projectableNodes = groupNodesBySelector([], contentNodes); + const adapter1 = createAdapter([], contentNodes); + const projectableNodes = adapter1.groupProjectableNodes(); expect(projectableNodes).toEqual([]); - const noMatchSelectorNodes = groupNodesBySelector(['.not-there'], contentNodes); + const adapter2 = createAdapter(['.not-there'], contentNodes); + const noMatchSelectorNodes = adapter2.groupProjectableNodes(); expect(noMatchSelectorNodes).toEqual([[]]); }); }); diff --git a/packages/upgrade/test/dynamic/group_projectable_nodes_spec.ts b/packages/upgrade/test/dynamic/group_projectable_nodes_spec.ts new file mode 100644 index 0000000000..4c7389e1f3 --- /dev/null +++ b/packages/upgrade/test/dynamic/group_projectable_nodes_spec.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {DynamicContentProjectionHelper} from '@angular/upgrade/src/dynamic/content_projection_helper'; +import {nodes} from './test_helpers'; + + +export function main() { + describe('groupNodesBySelector', () => { + let groupNodesBySelector: (ngContentSelectors: string[], nodes: Node[]) => Node[][]; + + beforeEach(() => { + const projectionHelper = new DynamicContentProjectionHelper(); + groupNodesBySelector = projectionHelper.groupNodesBySelector.bind(projectionHelper); + }); + + + it('should return an array of node collections for each selector', () => { + const contentNodes = nodes( + '
div-1 content
' + + '' + + '' + + 'span content' + + '
div-2 content
'); + + const selectors = ['input[type=date]', 'span', '.x']; + const projectableNodes = groupNodesBySelector(selectors, contentNodes); + + expect(projectableNodes[0]).toEqual(nodes('')); + expect(projectableNodes[1]).toEqual(nodes('span content')); + expect(projectableNodes[2]) + .toEqual(nodes( + '
div-1 content
' + + '
div-2 content
')); + }); + + it('should collect up unmatched nodes for the wildcard selector', () => { + const contentNodes = nodes( + '
div-1 content
' + + '' + + '' + + 'span content' + + '
div-2 content
'); + + const selectors = ['.x', '*', 'input[type=date]']; + const projectableNodes = groupNodesBySelector(selectors, contentNodes); + + expect(projectableNodes[0]) + .toEqual(nodes( + '
div-1 content
' + + '
div-2 content
')); + expect(projectableNodes[1]) + .toEqual(nodes( + '' + + 'span content')); + expect(projectableNodes[2]).toEqual(nodes('')); + }); + + it('should return an array of empty arrays if there are no nodes passed in', () => { + const selectors = ['.x', '*', 'input[type=date]']; + const projectableNodes = groupNodesBySelector(selectors, []); + expect(projectableNodes).toEqual([[], [], []]); + }); + + it('should return an empty array for each selector that does not match', () => { + const contentNodes = nodes( + '
div-1 content
' + + '' + + '' + + 'span content' + + '
div-2 content
'); + + const noSelectorNodes = groupNodesBySelector([], contentNodes); + expect(noSelectorNodes).toEqual([]); + + const noMatchSelectorNodes = groupNodesBySelector(['.not-there'], contentNodes); + expect(noMatchSelectorNodes).toEqual([[]]); + }); + }); +} diff --git a/packages/upgrade/test/static/integration/change_detection_spec.ts b/packages/upgrade/test/static/integration/change_detection_spec.ts index 9f43a3b0bc..96eae146a4 100644 --- a/packages/upgrade/test/static/integration/change_detection_spec.ts +++ b/packages/upgrade/test/static/integration/change_detection_spec.ts @@ -73,7 +73,7 @@ export function main() { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;'); // https://github.com/angular/angular.js/issues/12983 - expect(log).toEqual(['1A', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); + expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); }); })); diff --git a/packages/upgrade/test/static/integration/content_projection_spec.ts b/packages/upgrade/test/static/integration/content_projection_spec.ts index ef211d229e..d4e636db6f 100644 --- a/packages/upgrade/test/static/integration/content_projection_spec.ts +++ b/packages/upgrade/test/static/integration/content_projection_spec.ts @@ -72,14 +72,16 @@ export function main() { ngDoBootstrap() {} } - const ng1Module = angular.module('ng1', []) - .directive('ng2', downgradeComponent({component: Ng2Component})) - .run(($rootScope: angular.IRootScopeService) => { - $rootScope['items'] = [ - {id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]}, - {id: 'c', subitems: [7, 8, 9]} - ]; - }); + const ng1Module = + angular.module('ng1', []) + .directive( + 'ng2', downgradeComponent({component: Ng2Component, inputs: ['itemId']})) + .run(($rootScope: angular.IRootScopeService) => { + $rootScope['items'] = [ + {id: 'a', subitems: [1, 2, 3]}, {id: 'b', subitems: [4, 5, 6]}, + {id: 'c', subitems: [7, 8, 9]} + ]; + }); const element = html(` @@ -160,7 +162,7 @@ export function main() { } const ng1Module = angular.module('ng1', []).directive( - 'ng2', downgradeComponent({component: Ng2Component})); + 'ng2', downgradeComponent({component: Ng2Component, selectors: ['.ng1a', '.ng1b']})); // The ng-if on one of the projected children is here to make sure // the correct slot is targeted even with structural directives in play. diff --git a/packages/upgrade/test/static/integration/downgrade_component_spec.ts b/packages/upgrade/test/static/integration/downgrade_component_spec.ts index 0e44113338..59339416dc 100644 --- a/packages/upgrade/test/static/integration/downgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_component_spec.ts @@ -108,9 +108,15 @@ export function main() { }; } - ng1Module.directive('ng2', downgradeComponent({ - component: Ng2Component, - })); + ng1Module.directive( + 'ng2', downgradeComponent({ + component: Ng2Component, + inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], + outputs: [ + 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', + 'twoWayBEmitter: twoWayBChange' + ] + })); @NgModule({ declarations: [Ng2Component], diff --git a/packages/upgrade/test/static/integration/examples_spec.ts b/packages/upgrade/test/static/integration/examples_spec.ts index 9e9f8dbcad..831db3bf00 100644 --- a/packages/upgrade/test/static/integration/examples_spec.ts +++ b/packages/upgrade/test/static/integration/examples_spec.ts @@ -71,7 +71,9 @@ export function main() { }; }) // This is wrapping (downgrading) an Angular component to be used in AngularJS - .directive('ng2', downgradeComponent({component: Ng2Component})); + .directive( + 'ng2', + downgradeComponent({component: Ng2Component, inputs: ['nameProp: name']})); // This is the (AngularJS) application bootstrap element // Notice that it is actually a downgraded Angular component diff --git a/packages/upgrade/test/static/integration/upgrade_component_spec.ts b/packages/upgrade/test/static/integration/upgrade_component_spec.ts index a8a3546c9f..b1690b7126 100644 --- a/packages/upgrade/test/static/integration/upgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/upgrade_component_spec.ts @@ -2627,10 +2627,12 @@ export function main() { } // Define `ng1Module` - const ng1Module = angular.module('ng1Module', []) - .directive('ng1A', () => ng1DirectiveA) - .directive('ng1B', () => ng1DirectiveB) - .directive('ng2', downgradeComponent({component: Ng2Component})); + const ng1Module = + angular.module('ng1Module', []) + .directive('ng1A', () => ng1DirectiveA) + .directive('ng1B', () => ng1DirectiveB) + .directive( + 'ng2', downgradeComponent({component: Ng2Component, inputs: ['show']})); // Define `Ng2Module` @NgModule({ @@ -2727,10 +2729,12 @@ export function main() { } // Define `ng1Module` - const ng1Module = angular.module('ng1Module', []) - .directive('ng1A', () => ng1DirectiveA) - .directive('ng1B', () => ng1DirectiveB) - .directive('ng2', downgradeComponent({component: Ng2Component})); + const ng1Module = + angular.module('ng1Module', []) + .directive('ng1A', () => ng1DirectiveA) + .directive('ng1B', () => ng1DirectiveB) + .directive( + 'ng2', downgradeComponent({component: Ng2Component, inputs: ['show']})); // Define `Ng2Module` @NgModule({ @@ -3082,7 +3086,11 @@ export function main() { const ng1Module = angular.module('ng1', []) .component('ng1X', ng1Component) .directive('ng2A', downgradeComponent({component: Ng2ComponentA})) - .directive('ng2B', downgradeComponent({component: Ng2ComponentB})); + .directive('ng2B', downgradeComponent({ + component: Ng2ComponentB, + inputs: ['ng2BInputA: ng2BInput1', 'ng2BInputC'], + outputs: ['ng2BOutputC'] + })); // Define `Ng2Module` @NgModule({ diff --git a/tools/public_api_guard/core/typings/core.d.ts b/tools/public_api_guard/core/typings/core.d.ts index 668ec82df6..1868d6001c 100644 --- a/tools/public_api_guard/core/typings/core.d.ts +++ b/tools/public_api_guard/core/typings/core.d.ts @@ -193,7 +193,7 @@ export declare class Compiler { compileModuleAndAllComponentsSync(moduleType: Type): ModuleWithComponentFactories; compileModuleAsync(moduleType: Type): Promise>; compileModuleSync(moduleType: Type): NgModuleFactory; - /** @deprecated */ getNgContentSelectors(component: Type): string[]; + getNgContentSelectors(component: Type): string[]; } /** @experimental */ @@ -226,15 +226,6 @@ export interface ComponentDecorator { /** @stable */ export declare abstract class ComponentFactory { readonly abstract componentType: Type; - readonly abstract inputs: { - propName: string; - templateName: string; - }[]; - readonly abstract ngContentSelectors: string[]; - readonly abstract outputs: { - propName: string; - templateName: string; - }[]; readonly abstract selector: string; abstract create(injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string | any, ngModule?: NgModuleRef): ComponentRef; } diff --git a/tools/public_api_guard/upgrade/typings/static/index.d.ts b/tools/public_api_guard/upgrade/typings/static/index.d.ts index 40cee4c38d..6398773b84 100644 --- a/tools/public_api_guard/upgrade/typings/static/index.d.ts +++ b/tools/public_api_guard/upgrade/typings/static/index.d.ts @@ -1,9 +1,9 @@ /** @experimental */ export declare function downgradeComponent(info: { component: Type; - /** @deprecated */ inputs?: string[]; - /** @deprecated */ outputs?: string[]; - /** @deprecated */ selectors?: string[]; + inputs?: string[]; + outputs?: string[]; + selectors?: string[]; }): any; /** @experimental */