feat(upgrade): allow non-element selectors for downgraded components (#14037)
This affects the dynamic version of `upgrade` and makes it more consistent with the static version, while removing an artificial limitation. This commit also refactors the file layout and code, in order to share code wrt to dowgrading components between the dynamic and static versions.
This commit is contained in:
parent
1f90f29369
commit
9aafdc7b02
|
@ -8,9 +8,8 @@
|
||||||
|
|
||||||
import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angular/core';
|
import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angular/core';
|
||||||
|
|
||||||
import * as angular from '../common/angular1';
|
import * as angular from './angular1';
|
||||||
import {$INJECTOR, $PARSE, INJECTOR_KEY, REQUIRE_NG_MODEL} from '../common/constants';
|
import {$INJECTOR, $PARSE, INJECTOR_KEY, REQUIRE_NG_MODEL} from './constants';
|
||||||
|
|
||||||
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
|
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
|
||||||
|
|
||||||
let downgradeCount = 0;
|
let downgradeCount = 0;
|
|
@ -8,11 +8,10 @@
|
||||||
|
|
||||||
import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||||
|
|
||||||
import * as angular from '../common/angular1';
|
import * as angular from './angular1';
|
||||||
import {$SCOPE} from '../common/constants';
|
|
||||||
import {hookupNgModel} from '../common/util';
|
import {hookupNgModel} from '../common/util';
|
||||||
|
|
||||||
import {ComponentInfo, PropertyBinding} from './component_info';
|
import {ComponentInfo, PropertyBinding} from './component_info';
|
||||||
|
import {$SCOPE} from './constants';
|
||||||
|
|
||||||
const INITIAL_VALUE = {
|
const INITIAL_VALUE = {
|
||||||
__UNINITIALIZED__: true
|
__UNINITIALIZED__: true
|
|
@ -9,63 +9,32 @@
|
||||||
import {DirectiveResolver} from '@angular/compiler';
|
import {DirectiveResolver} from '@angular/compiler';
|
||||||
import {Directive, Type} from '@angular/core';
|
import {Directive, Type} from '@angular/core';
|
||||||
|
|
||||||
|
import {PropertyBinding} from '../common/component_info';
|
||||||
|
|
||||||
|
|
||||||
const COMPONENT_SELECTOR = /^[\w|-]*$/;
|
const COMPONENT_SELECTOR = /^[\w|-]*$/;
|
||||||
const SKEWER_CASE = /-(\w)/g;
|
const SKEWER_CASE = /-(\w)/g;
|
||||||
const directiveResolver = new DirectiveResolver();
|
const directiveResolver = new DirectiveResolver();
|
||||||
|
|
||||||
export interface AttrProp {
|
|
||||||
prop: string;
|
|
||||||
attr: string;
|
|
||||||
bracketAttr: string;
|
|
||||||
bracketParenAttr: string;
|
|
||||||
parenAttr: string;
|
|
||||||
onAttr: string;
|
|
||||||
bindAttr: string;
|
|
||||||
bindonAttr: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComponentInfo {
|
export interface ComponentInfo {
|
||||||
type: Type<any>;
|
type: Type<any>;
|
||||||
selector: string;
|
selector: string;
|
||||||
inputs?: AttrProp[];
|
inputs?: PropertyBinding[];
|
||||||
outputs?: AttrProp[];
|
outputs?: PropertyBinding[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getComponentInfo(type: Type<any>): ComponentInfo {
|
export function getComponentInfo(type: Type<any>): ComponentInfo {
|
||||||
const resolvedMetadata: Directive = directiveResolver.resolve(type);
|
const resolvedMetadata: Directive = directiveResolver.resolve(type);
|
||||||
let selector = resolvedMetadata.selector;
|
const selector = resolvedMetadata.selector;
|
||||||
if (!selector.match(COMPONENT_SELECTOR)) {
|
|
||||||
throw new Error('Only selectors matching element names are supported, got: ' + selector);
|
|
||||||
}
|
|
||||||
selector = selector.replace(
|
|
||||||
SKEWER_CASE, (all: any /** TODO #9100 */, letter: string) => letter.toUpperCase());
|
|
||||||
return {
|
return {
|
||||||
type: type,
|
type,
|
||||||
selector: selector,
|
selector,
|
||||||
inputs: parseFields(resolvedMetadata.inputs),
|
inputs: parseFields(resolvedMetadata.inputs),
|
||||||
outputs: parseFields(resolvedMetadata.outputs)
|
outputs: parseFields(resolvedMetadata.outputs)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseFields(names: string[]): AttrProp[] {
|
export function parseFields(bindings: string[]): PropertyBinding[] {
|
||||||
const attrProps: AttrProp[] = [];
|
return (bindings || []).map(binding => new PropertyBinding(binding));
|
||||||
if (names) {
|
|
||||||
for (let i = 0; i < names.length; i++) {
|
|
||||||
const parts = names[i].split(':');
|
|
||||||
const prop = parts[0].trim();
|
|
||||||
const attr = (parts[1] || parts[0]).trim();
|
|
||||||
const capitalAttr = attr.charAt(0).toUpperCase() + attr.substr(1);
|
|
||||||
attrProps.push(<AttrProp>{
|
|
||||||
prop: prop,
|
|
||||||
attr: attr,
|
|
||||||
bracketAttr: `[${attr}]`,
|
|
||||||
parenAttr: `(${attr})`,
|
|
||||||
bracketParenAttr: `[(${attr})]`,
|
|
||||||
onAttr: `on${capitalAttr}`,
|
|
||||||
bindAttr: `bind${capitalAttr}`,
|
|
||||||
bindonAttr: `bindon${capitalAttr}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return attrProps;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ export class UpgradeAdapter {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
private ng1ComponentsToBeUpgraded: {[name: string]: UpgradeNg1ComponentAdapterBuilder} = {};
|
private ng1ComponentsToBeUpgraded: {[name: string]: UpgradeNg1ComponentAdapterBuilder} = {};
|
||||||
private providers: Provider[] = [];
|
private upgradedProviders: Provider[] = [];
|
||||||
private ngZone: NgZone;
|
private ngZone: NgZone;
|
||||||
private ng1Module: angular.IModule;
|
private ng1Module: angular.IModule;
|
||||||
private moduleRef: NgModuleRef<any> = null;
|
private moduleRef: NgModuleRef<any> = null;
|
||||||
|
@ -437,11 +437,11 @@ export class UpgradeAdapter {
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public upgradeNg1Provider(name: string, options?: {asToken: any}) {
|
upgradeNg1Provider(name: string, options?: {asToken: any}) {
|
||||||
const token = options && options.asToken || name;
|
const token = options && options.asToken || name;
|
||||||
this.providers.push({
|
this.upgradedProviders.push({
|
||||||
provide: token,
|
provide: token,
|
||||||
useFactory: (ng1Injector: angular.IInjectorService) => ng1Injector.get(name),
|
useFactory: ($injector: angular.IInjectorService) => $injector.get(name),
|
||||||
deps: [$INJECTOR]
|
deps: [$INJECTOR]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -557,7 +557,7 @@ export class UpgradeAdapter {
|
||||||
providers: [
|
providers: [
|
||||||
{provide: $INJECTOR, useFactory: () => ng1Injector},
|
{provide: $INJECTOR, useFactory: () => ng1Injector},
|
||||||
{provide: $COMPILE, useFactory: () => ng1Injector.get($COMPILE)},
|
{provide: $COMPILE, useFactory: () => ng1Injector.get($COMPILE)},
|
||||||
this.providers
|
this.upgradedProviders
|
||||||
],
|
],
|
||||||
imports: [this.ng2AppModule]
|
imports: [this.ng2AppModule]
|
||||||
}).Class({
|
}).Class({
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
* Entry point for all public APIs of the upgrade/static package, allowing
|
* Entry point for all public APIs of the upgrade/static package, allowing
|
||||||
* Angular 1 and Angular 2+ to run side by side in the same application.
|
* Angular 1 and Angular 2+ to run side by side in the same application.
|
||||||
*/
|
*/
|
||||||
|
export {downgradeComponent} from './src/common/downgrade_component';
|
||||||
export {downgradeInjectable} from './src/common/downgrade_injectable';
|
export {downgradeInjectable} from './src/common/downgrade_injectable';
|
||||||
export {downgradeComponent} from './src/static/downgrade_component';
|
|
||||||
export {UpgradeComponent} from './src/static/upgrade_component';
|
export {UpgradeComponent} from './src/static/upgrade_component';
|
||||||
export {UpgradeModule} from './src/static/upgrade_module';
|
export {UpgradeModule} from './src/static/upgrade_module';
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {PropertyBinding} from '@angular/upgrade/src/aot/component_info';
|
import {PropertyBinding} from '@angular/upgrade/src/common/component_info';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('PropertyBinding', () => {
|
describe('PropertyBinding', () => {
|
|
@ -7,23 +7,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {describe, expect, it} from '@angular/core/testing/testing_internal';
|
|
||||||
import {getComponentInfo, parseFields} from '@angular/upgrade/src/dynamic/metadata';
|
import {getComponentInfo, parseFields} from '@angular/upgrade/src/dynamic/metadata';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('upgrade metadata', () => {
|
describe('upgrade metadata', () => {
|
||||||
it('should extract component selector', () => {
|
it('should extract component selector', () => {
|
||||||
expect(getComponentInfo(ElementNameComponent).selector).toEqual('elementNameDashed');
|
expect(getComponentInfo(ElementNameComponent).selector).toBe('element-name-dashed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('errors', () => {
|
describe('errors', () => {
|
||||||
it('should throw on missing selector', () => {
|
it('should throw on missing selector', () => {
|
||||||
expect(() => getComponentInfo(AttributeNameComponent))
|
|
||||||
.toThrowError('Only selectors matching element names are supported, got: [attr-name]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw on non element names', () => {
|
|
||||||
expect(() => getComponentInfo(NoAnnotationComponent))
|
expect(() => getComponentInfo(NoAnnotationComponent))
|
||||||
.toThrowError('No Directive annotation found on NoAnnotationComponent');
|
.toThrowError('No Directive annotation found on NoAnnotationComponent');
|
||||||
});
|
});
|
||||||
|
@ -34,7 +28,7 @@ export function main() {
|
||||||
|
|
||||||
it('should process values', () => {
|
it('should process values', () => {
|
||||||
expect(parseFields([' name ', ' prop : attr '])).toEqual([
|
expect(parseFields([' name ', ' prop : attr '])).toEqual([
|
||||||
{
|
jasmine.objectContaining({
|
||||||
prop: 'name',
|
prop: 'name',
|
||||||
attr: 'name',
|
attr: 'name',
|
||||||
bracketAttr: '[name]',
|
bracketAttr: '[name]',
|
||||||
|
@ -43,8 +37,8 @@ export function main() {
|
||||||
onAttr: 'onName',
|
onAttr: 'onName',
|
||||||
bindAttr: 'bindName',
|
bindAttr: 'bindName',
|
||||||
bindonAttr: 'bindonName'
|
bindonAttr: 'bindonName'
|
||||||
},
|
}),
|
||||||
{
|
jasmine.objectContaining({
|
||||||
prop: 'prop',
|
prop: 'prop',
|
||||||
attr: 'attr',
|
attr: 'attr',
|
||||||
bracketAttr: '[attr]',
|
bracketAttr: '[attr]',
|
||||||
|
@ -53,7 +47,7 @@ export function main() {
|
||||||
onAttr: 'onAttr',
|
onAttr: 'onAttr',
|
||||||
bindAttr: 'bindAttr',
|
bindAttr: 'bindAttr',
|
||||||
bindonAttr: 'bindonAttr'
|
bindonAttr: 'bindonAttr'
|
||||||
}
|
})
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -271,6 +271,25 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('downgrade ng2 component', () => {
|
describe('downgrade ng2 component', () => {
|
||||||
|
it('should allow non-element selectors for downgraded components', async(() => {
|
||||||
|
@Component({selector: '[itWorks]', template: 'It works'})
|
||||||
|
class WorksComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({declarations: [WorksComponent], imports: [BrowserModule]})
|
||||||
|
class Ng2Module {
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const ng1Module = angular.module('ng1', []);
|
||||||
|
ng1Module.directive('ng2', adapter.downgradeNg2Component(WorksComponent));
|
||||||
|
|
||||||
|
const element = html('<ng2></ng2>');
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
expect(multiTrim(document.body.textContent)).toBe('It works');
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should bind properties, events', async(() => {
|
it('should bind properties, events', async(() => {
|
||||||
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
const ng1Module = angular.module('ng1', []);
|
const ng1Module = angular.module('ng1', []);
|
||||||
|
@ -458,7 +477,6 @@ export function main() {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
it('should fallback to the root ng2.injector when compiled outside the dom', async(() => {
|
it('should fallback to the root ng2.injector when compiled outside the dom', async(() => {
|
||||||
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
const ng1Module = angular.module('ng1', []);
|
const ng1Module = angular.module('ng1', []);
|
||||||
|
|
|
@ -273,6 +273,30 @@ export function main() {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should allow attribute selectors for downgraded components', async(() => {
|
||||||
|
@Component({selector: '[itWorks]', template: 'It works'})
|
||||||
|
class WorksComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [WorksComponent],
|
||||||
|
entryComponents: [WorksComponent],
|
||||||
|
imports: [BrowserModule, UpgradeModule]
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ng1Module = angular.module('ng1', []).directive(
|
||||||
|
'worksComponent', downgradeComponent({component: WorksComponent}));
|
||||||
|
|
||||||
|
const element = html('<works-component></works-component>');
|
||||||
|
|
||||||
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
||||||
|
expect(multiTrim(document.body.textContent)).toBe('It works');
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should allow attribute selectors for components in ng2', async(() => {
|
it('should allow attribute selectors for components in ng2', async(() => {
|
||||||
@Component({selector: '[itWorks]', template: 'It works'})
|
@Component({selector: '[itWorks]', template: 'It works'})
|
||||||
class WorksComponent {
|
class WorksComponent {
|
||||||
|
|
Loading…
Reference in New Issue