feat(upgrade): use ComponentFactory.inputs/outputs/ngContentSelectors

DEPRECATION:
- the arguments `inputs` / `outputs` / `ngContentSelectors` of `downgradeComponent`
  are no longer used as Angular calculates these automatically now.
- Compiler.getNgContentSelectors is deprecated. Use
  ComponentFactory.ngContentSelectors instead.
This commit is contained in:
Tobias Bosch
2017-03-14 14:55:37 -07:00
committed by Chuck Jazdzewski
parent 1171f91a80
commit a3e32fb7e1
22 changed files with 94 additions and 418 deletions

View File

@ -6,15 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Type} from '@angular/core';
export interface ComponentInfo {
component: Type<any>;
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
@ -22,8 +13,6 @@ export interface ComponentInfo {
* and attribute have the same identifier.
*/
export class PropertyBinding {
prop: string;
attr: string;
bracketAttr: string;
bracketParenAttr: string;
parenAttr: string;
@ -31,12 +20,9 @@ export class PropertyBinding {
bindAttr: string;
bindonAttr: string;
constructor(public binding: string) { this.parseBinding(); }
constructor(public prop: string, public attr: 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})]`;

View File

@ -1,20 +0,0 @@
/**
* @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<any>, nodes: Node[]):
Node[][] {
// By default, do not support multi-slot projection,
// as `upgrade/static` does not support it yet.
return [nodes];
}
}

View File

@ -11,7 +11,6 @@ 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;
@ -38,15 +37,6 @@ 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
@ -55,28 +45,17 @@ let downgradeCount = 0;
* The parameter contains information about the Component that is being downgraded:
*
* * `component: Type<any>`: 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: /* ComponentInfo */ {
export function downgradeComponent(info: {
component: Type<any>;
/** @deprecated since v4. This parameter is no longer used */
inputs?: string[];
/** @deprecated since v4. This parameter is no longer used */
outputs?: string[];
selectors?: string[]
/** @deprecated since v4. This parameter is no longer used */
selectors?: string[];
}): any /* angular.IInjectable */ {
const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`;
let idCount = 0;
@ -114,7 +93,7 @@ export function downgradeComponent(info: /* ComponentInfo */ {
const id = idPrefix + (idCount++);
const injectorPromise = new ParentInjectorPromise(element);
const facade = new DowngradeComponentAdapter(
id, info, element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
id, element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
componentFactory);
const projectableNodes = facade.compileContents();

View File

@ -9,9 +9,8 @@
import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core';
import * as angular from './angular1';
import {ComponentInfo, PropertyBinding} from './component_info';
import {PropertyBinding} from './component_info';
import {$SCOPE} from './constants';
import {NgContentSelectorHelper} from './ng_content_selector_helper';
import {getAttributesAsArray, getComponentName, hookupNgModel} from './util';
const INITIAL_VALUE = {
@ -27,7 +26,7 @@ export class DowngradeComponentAdapter {
private changeDetector: ChangeDetectorRef = null;
constructor(
private id: string, private info: ComponentInfo, private element: angular.IAugmentedJQuery,
private id: string, 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,
@ -67,9 +66,9 @@ export class DowngradeComponentAdapter {
setupInputs(): void {
const attrs = this.attrs;
const inputs = this.info.inputs || [];
const inputs = this.componentFactory.inputs || [];
for (let i = 0; i < inputs.length; i++) {
const input = new PropertyBinding(inputs[i]);
const input = new PropertyBinding(inputs[i].propName, inputs[i].templateName);
let expr: any /** TODO #9100 */ = null;
if (attrs.hasOwnProperty(input.attr)) {
@ -103,7 +102,7 @@ export class DowngradeComponentAdapter {
}
}
const prototype = this.info.component.prototype;
const prototype = this.componentFactory.componentType.prototype;
if (prototype && (<OnChanges>prototype).ngOnChanges) {
// Detect: OnChanges interface
this.inputChanges = {};
@ -118,9 +117,9 @@ export class DowngradeComponentAdapter {
setupOutputs() {
const attrs = this.attrs;
const outputs = this.info.outputs || [];
const outputs = this.componentFactory.outputs || [];
for (let j = 0; j < outputs.length; j++) {
const output = new PropertyBinding(outputs[j]);
const output = new PropertyBinding(outputs[j].propName, outputs[j].templateName);
let expr: any /** TODO #9100 */ = null;
let assignExpr = false;
@ -158,7 +157,7 @@ export class DowngradeComponentAdapter {
});
} else {
throw new Error(
`Missing emitter '${output.prop}' on component '${getComponentName(this.info.component)}'!`);
`Missing emitter '${output.prop}' on component '${getComponentName(this.componentFactory.componentType)}'!`);
}
}
}
@ -183,49 +182,31 @@ export class DowngradeComponentAdapter {
}
groupProjectableNodes() {
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;
let ngContentSelectors = this.componentFactory.ngContentSelectors;
return groupNodesBySelector(ngContentSelectors, this.element.contents());
}
}
let _matches: (this: any, selector: string) => boolean;
/**
* 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;
function matchesSelector(el: any, selector: string): boolean {
if (!_matches) {
const elProto = <any>Element.prototype;
_matches = elProto.matchesSelector || elProto.mozMatchesSelector || elProto.msMatchesSelector ||
elProto.oMatchesSelector || elProto.webkitMatchesSelector;
for (let i = 0, ii = ngContentSelectors.length; i < ii; ++i) {
projectableNodes[i] = [];
}
return _matches.call(el, selector);
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;
}
function findMatchingNgContentIndex(element: any, ngContentSelectors: string[]): number {
@ -247,4 +228,15 @@ 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 = <any>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;
}

View File

@ -1,24 +0,0 @@
/**
* @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 || ['*'];
}
}

View File

@ -1,70 +0,0 @@
/**
* @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<any>, 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;
}
}

View File

@ -1,24 +0,0 @@
/**
* @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);
}
}

View File

@ -6,19 +6,15 @@
* 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;
@ -104,7 +100,6 @@ let upgradeCount: number = 0;
*/
export class UpgradeAdapter {
private idPrefix: string = `NG2_UPGRADE_${upgradeCount++}_`;
private directiveResolver: DirectiveResolver = new DirectiveResolver();
private downgradedComponents: Type<any>[] = [];
/**
* An internal map of ng1 components which need to up upgraded to ng2.
@ -190,10 +185,7 @@ export class UpgradeAdapter {
downgradeNg2Component(component: Type<any>): Function {
this.downgradedComponents.push(component);
const metadata: Directive = this.directiveResolver.resolve(component);
const info: ComponentInfo = {component, inputs: metadata.inputs, outputs: metadata.outputs};
return downgradeComponent(info);
return downgradeComponent({component});
}
/**
@ -561,7 +553,6 @@ export class UpgradeAdapter {
providers: [
{provide: $INJECTOR, useFactory: () => ng1Injector},
{provide: $COMPILE, useFactory: () => ng1Injector.get($COMPILE)},
{provide: NgContentSelectorHelper, useClass: DynamicNgContentSelectorHelper},
this.upgradedProviders
],
imports: [this.ng2AppModule],

View File

@ -10,7 +10,6 @@ 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';
@ -130,7 +129,7 @@ import {angular1Providers, setTempInjectorRef} from './angular1_providers';
*
* @experimental
*/
@NgModule({providers: [angular1Providers, NgContentSelectorHelper]})
@NgModule({providers: [angular1Providers]})
export class UpgradeModule {
/**
* The AngularJS `$injector` for the upgrade application.