fix(upgrade): bring the dynamic version closer to the static one
(#17971) This commit changes the dynamic version of ngUpgrade to use `UpgradeHelper`, thus bringing its behavior (wrt upgraded components) much closer to `upgrade/static`. Fixes/features include: - Fix template compilation: Now takes place in the correct DOM context, instead of in a detached node (thus has access to required ancestors etc). - Fix support for the `$onInit()` lifecycle hook. - Fix single-slot transclusion (including optional transclusion and fallback content). - Add support for multi-slot transclusion (inclusing optional slots and fallback content). - Add support for binding required controllers to the directive's controller (and make the `require` behavior more consistent with AngularJS). - Add support for pre-/post-linking functions. (This also ports the fixes from #16627 to the dynamic version.) Fixes #11044
This commit is contained in:

committed by
Jason Aden

parent
0193be7c9b
commit
11db3bd85e
@ -546,8 +546,8 @@ export class UpgradeAdapter {
|
||||
(ng1Injector: angular.IInjectorService, rootScope: angular.IRootScopeService) => {
|
||||
UpgradeNg1ComponentAdapterBuilder.resolve(this.ng1ComponentsToBeUpgraded, ng1Injector)
|
||||
.then(() => {
|
||||
// At this point we have ng1 injector and we have lifted ng1 components into ng2, we
|
||||
// now can bootstrap ng2.
|
||||
// At this point we have ng1 injector and we have prepared
|
||||
// ng1 components to be upgraded, we now can bootstrap ng2.
|
||||
const DynamicNgUpgradeModule =
|
||||
NgModule({
|
||||
providers: [
|
||||
|
@ -6,26 +6,12 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Directive, DoCheck, ElementRef, EventEmitter, Inject, OnChanges, OnInit, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||
import {Directive, DoCheck, ElementRef, EventEmitter, Inject, Injector, OnChanges, OnInit, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||
|
||||
import * as angular from '../common/angular1';
|
||||
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $SCOPE, $TEMPLATE_CACHE} from '../common/constants';
|
||||
import {controllerKey, strictEquals} from '../common/util';
|
||||
|
||||
|
||||
interface IBindingDestination {
|
||||
[key: string]: any;
|
||||
$onChanges?: (changes: SimpleChanges) => void;
|
||||
}
|
||||
|
||||
interface IControllerInstance extends IBindingDestination {
|
||||
$doCheck?: () => void;
|
||||
$onDestroy?: () => void;
|
||||
$onInit?: () => void;
|
||||
$postLink?: () => void;
|
||||
}
|
||||
|
||||
type LifecycleHook = '$doCheck' | '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
|
||||
import {$SCOPE} from '../common/constants';
|
||||
import {IBindingDestination, IControllerInstance, UpgradeHelper} from '../common/upgrade_helper';
|
||||
import {isFunction, strictEquals} from '../common/util';
|
||||
|
||||
|
||||
const CAMEL_CASE = /([A-Z])/g;
|
||||
@ -44,51 +30,31 @@ export class UpgradeNg1ComponentAdapterBuilder {
|
||||
propertyOutputs: string[] = [];
|
||||
checkProperties: string[] = [];
|
||||
propertyMap: {[name: string]: string} = {};
|
||||
linkFn: angular.ILinkFn|null = null;
|
||||
directive: angular.IDirective|null = null;
|
||||
$controller: angular.IControllerService|null = null;
|
||||
template: string;
|
||||
|
||||
constructor(public name: string) {
|
||||
const selector = name.replace(
|
||||
CAMEL_CASE, (all: any /** TODO #9100 */, next: string) => '-' + next.toLowerCase());
|
||||
const selector =
|
||||
name.replace(CAMEL_CASE, (all: string, next: string) => '-' + next.toLowerCase());
|
||||
const self = this;
|
||||
this.type = Directive({
|
||||
selector: selector,
|
||||
inputs: this.inputsRename,
|
||||
outputs: this.outputsRename
|
||||
}).Class({
|
||||
constructor: [
|
||||
new Inject($SCOPE), ElementRef,
|
||||
function(scope: angular.IScope, elementRef: ElementRef) {
|
||||
return new UpgradeNg1ComponentAdapter(
|
||||
self.linkFn !, scope, self.directive !, elementRef, self.$controller !, self.inputs,
|
||||
self.outputs, self.propertyOutputs, self.checkProperties, self.propertyMap);
|
||||
}
|
||||
],
|
||||
ngOnInit: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
ngOnChanges: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
ngDoCheck: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
ngOnDestroy: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
});
|
||||
}
|
||||
|
||||
extractDirective(injector: angular.IInjectorService): angular.IDirective {
|
||||
const directives: angular.IDirective[] = injector.get(this.name + 'Directive');
|
||||
if (directives.length > 1) {
|
||||
throw new Error('Only support single directive definition for: ' + this.name);
|
||||
}
|
||||
const directive = directives[0];
|
||||
if (directive.replace) this.notSupported('replace');
|
||||
if (directive.terminal) this.notSupported('terminal');
|
||||
const link = directive.link;
|
||||
if (typeof link == 'object') {
|
||||
if ((<angular.IDirectivePrePost>link).post) this.notSupported('link.post');
|
||||
}
|
||||
return directive;
|
||||
}
|
||||
|
||||
private notSupported(feature: string) {
|
||||
throw new Error(`Upgraded directive '${this.name}' does not support '${feature}'.`);
|
||||
this.type =
|
||||
Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename})
|
||||
.Class({
|
||||
constructor: [
|
||||
new Inject($SCOPE), Injector, ElementRef,
|
||||
function(scope: angular.IScope, injector: Injector, elementRef: ElementRef) {
|
||||
const helper = new UpgradeHelper(injector, name, elementRef, this.directive);
|
||||
return new UpgradeNg1ComponentAdapter(
|
||||
helper, scope, self.template, self.inputs, self.outputs, self.propertyOutputs,
|
||||
self.checkProperties, self.propertyMap);
|
||||
}
|
||||
],
|
||||
ngOnInit: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
ngOnChanges: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
ngDoCheck: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
ngOnDestroy: function() { /* needs to be here for ng2 to properly detect it */ },
|
||||
});
|
||||
}
|
||||
|
||||
extractBindings() {
|
||||
@ -148,66 +114,22 @@ export class UpgradeNg1ComponentAdapterBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
compileTemplate(
|
||||
compile: angular.ICompileService, templateCache: angular.ITemplateCacheService,
|
||||
httpBackend: angular.IHttpBackendService): Promise<angular.ILinkFn>|null {
|
||||
if (this.directive !.template !== undefined) {
|
||||
this.linkFn = compileHtml(
|
||||
isFunction(this.directive !.template) ? (this.directive !.template as Function)() :
|
||||
this.directive !.template);
|
||||
} else if (this.directive !.templateUrl) {
|
||||
const url = isFunction(this.directive !.templateUrl) ?
|
||||
(this.directive !.templateUrl as Function)() :
|
||||
this.directive !.templateUrl;
|
||||
const html = templateCache.get(url);
|
||||
if (html !== undefined) {
|
||||
this.linkFn = compileHtml(html);
|
||||
} else {
|
||||
return new Promise((resolve, err) => {
|
||||
httpBackend(
|
||||
'GET', url, null,
|
||||
(status: any /** TODO #9100 */, response: any /** TODO #9100 */) => {
|
||||
if (status == 200) {
|
||||
resolve(this.linkFn = compileHtml(templateCache.put(url, response)));
|
||||
} else {
|
||||
err(`GET ${url} returned ${status}: ${response}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Directive '${this.name}' is not a component, it is missing template.`);
|
||||
}
|
||||
return null;
|
||||
function compileHtml(html: any /** TODO #9100 */): angular.ILinkFn {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return compile(div.childNodes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade ng1 components into Angular.
|
||||
*/
|
||||
static resolve(
|
||||
exportedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder},
|
||||
injector: angular.IInjectorService): Promise<angular.ILinkFn[]> {
|
||||
const promises: Promise<angular.ILinkFn>[] = [];
|
||||
const compile: angular.ICompileService = injector.get($COMPILE);
|
||||
const templateCache: angular.ITemplateCacheService = injector.get($TEMPLATE_CACHE);
|
||||
const httpBackend: angular.IHttpBackendService = injector.get($HTTP_BACKEND);
|
||||
const $controller: angular.IControllerService = injector.get($CONTROLLER);
|
||||
for (const name in exportedComponents) {
|
||||
if ((<any>exportedComponents).hasOwnProperty(name)) {
|
||||
const exportedComponent = exportedComponents[name];
|
||||
exportedComponent.directive = exportedComponent.extractDirective(injector);
|
||||
exportedComponent.$controller = $controller;
|
||||
exportedComponent.extractBindings();
|
||||
const promise: Promise<angular.ILinkFn> =
|
||||
exportedComponent.compileTemplate(compile, templateCache, httpBackend) !;
|
||||
if (promise) promises.push(promise);
|
||||
}
|
||||
}
|
||||
$injector: angular.IInjectorService): Promise<string[]> {
|
||||
const promises = Object.keys(exportedComponents).map(name => {
|
||||
const exportedComponent = exportedComponents[name];
|
||||
exportedComponent.directive = UpgradeHelper.getDirective($injector, name);
|
||||
exportedComponent.extractBindings();
|
||||
|
||||
return Promise
|
||||
.resolve(UpgradeHelper.getTemplate($injector, exportedComponent.directive, true))
|
||||
.then(template => exportedComponent.template = template);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
@ -216,28 +138,31 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
||||
private controllerInstance: IControllerInstance|null = null;
|
||||
destinationObj: IBindingDestination|null = null;
|
||||
checkLastValues: any[] = [];
|
||||
componentScope: angular.IScope;
|
||||
private directive: angular.IDirective;
|
||||
element: Element;
|
||||
$element: any = null;
|
||||
componentScope: angular.IScope;
|
||||
|
||||
constructor(
|
||||
private linkFn: angular.ILinkFn, scope: angular.IScope, private directive: angular.IDirective,
|
||||
elementRef: ElementRef, private $controller: angular.IControllerService,
|
||||
private helper: UpgradeHelper, scope: angular.IScope, private template: string,
|
||||
private inputs: string[], private outputs: string[], private propOuts: string[],
|
||||
private checkProperties: string[], private propertyMap: {[key: string]: string}) {
|
||||
this.element = elementRef.nativeElement;
|
||||
this.componentScope = scope.$new(!!directive.scope);
|
||||
this.$element = angular.element(this.element);
|
||||
const controllerType = directive.controller;
|
||||
if (directive.bindToController && controllerType) {
|
||||
this.controllerInstance = this.buildController(controllerType);
|
||||
this.directive = helper.directive;
|
||||
this.element = helper.element;
|
||||
this.$element = helper.$element;
|
||||
this.componentScope = scope.$new(!!this.directive.scope);
|
||||
|
||||
const controllerType = this.directive.controller;
|
||||
|
||||
if (this.directive.bindToController && controllerType) {
|
||||
this.controllerInstance = this.helper.buildController(controllerType, this.componentScope);
|
||||
this.destinationObj = this.controllerInstance;
|
||||
} else {
|
||||
this.destinationObj = this.componentScope;
|
||||
}
|
||||
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
(this as any /** TODO #9100 */)[inputs[i]] = null;
|
||||
(this as any)[inputs[i]] = null;
|
||||
}
|
||||
for (let j = 0; j < outputs.length; j++) {
|
||||
const emitter = (this as any)[outputs[j]] = new EventEmitter<any>();
|
||||
@ -250,39 +175,43 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.directive.bindToController && this.directive.controller) {
|
||||
this.controllerInstance = this.buildController(this.directive.controller);
|
||||
// Collect contents, insert and compile template
|
||||
const attachChildNodes: angular.ILinkFn|undefined = this.helper.prepareTransclusion();
|
||||
const linkFn = this.helper.compileTemplate(this.template);
|
||||
|
||||
// Instantiate controller (if not already done so)
|
||||
const controllerType = this.directive.controller;
|
||||
const bindToController = this.directive.bindToController;
|
||||
if (controllerType && !bindToController) {
|
||||
this.controllerInstance = this.helper.buildController(controllerType, this.componentScope);
|
||||
}
|
||||
|
||||
// Require other controllers
|
||||
const requiredControllers =
|
||||
this.helper.resolveAndBindRequiredControllers(this.controllerInstance);
|
||||
|
||||
// Hook: $onInit
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$onInit)) {
|
||||
this.controllerInstance.$onInit();
|
||||
}
|
||||
|
||||
let link = this.directive.link;
|
||||
if (typeof link == 'object') link = (<angular.IDirectivePrePost>link).pre;
|
||||
if (link) {
|
||||
const attrs: angular.IAttributes = NOT_SUPPORTED;
|
||||
const transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
|
||||
const linkController = this.resolveRequired(this.$element, this.directive.require !);
|
||||
(<angular.IDirectiveLinkFn>this.directive.link)(
|
||||
this.componentScope, this.$element, attrs, linkController, transcludeFn);
|
||||
// Linking
|
||||
const link = this.directive.link;
|
||||
const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre;
|
||||
const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link;
|
||||
const attrs: angular.IAttributes = NOT_SUPPORTED;
|
||||
const transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
|
||||
if (preLink) {
|
||||
preLink(this.componentScope, this.$element, attrs, requiredControllers, transcludeFn);
|
||||
}
|
||||
|
||||
const childNodes: Node[] = [];
|
||||
let childNode: any /** TODO #9100 */;
|
||||
while (childNode = this.element.firstChild) {
|
||||
this.element.removeChild(childNode);
|
||||
childNodes.push(childNode);
|
||||
}
|
||||
this.linkFn(this.componentScope, (clonedElement, scope) => {
|
||||
for (let i = 0, ii = clonedElement !.length; i < ii; i++) {
|
||||
this.element.appendChild(clonedElement ![i]);
|
||||
}
|
||||
}, {
|
||||
parentBoundTranscludeFn: (scope: any /** TODO #9100 */,
|
||||
cloneAttach: any /** TODO #9100 */) => { cloneAttach(childNodes); }
|
||||
});
|
||||
linkFn(this.componentScope, null !, {parentBoundTranscludeFn: attachChildNodes});
|
||||
|
||||
if (postLink) {
|
||||
postLink(this.componentScope, this.$element, attrs, requiredControllers, transcludeFn);
|
||||
}
|
||||
|
||||
// Hook: $postLink
|
||||
if (this.controllerInstance && isFunction(this.controllerInstance.$postLink)) {
|
||||
this.controllerInstance.$postLink();
|
||||
}
|
||||
@ -329,56 +258,4 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
||||
setComponentProperty(name: string, value: any) {
|
||||
this.destinationObj ![this.propertyMap[name]] = value;
|
||||
}
|
||||
|
||||
private buildController(controllerType: any /** TODO #9100 */) {
|
||||
const locals = {$scope: this.componentScope, $element: this.$element};
|
||||
const controller: any =
|
||||
this.$controller(controllerType, locals, null, this.directive.controllerAs);
|
||||
this.$element.data(controllerKey(this.directive.name !), controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
private resolveRequired(
|
||||
$element: angular.IAugmentedJQuery, require: angular.DirectiveRequireProperty): any {
|
||||
if (!require) {
|
||||
return undefined;
|
||||
} else if (typeof require == 'string') {
|
||||
let name: string = <string>require;
|
||||
let isOptional = false;
|
||||
let startParent = false;
|
||||
let searchParents = false;
|
||||
if (name.charAt(0) == '?') {
|
||||
isOptional = true;
|
||||
name = name.substr(1);
|
||||
}
|
||||
if (name.charAt(0) == '^') {
|
||||
searchParents = true;
|
||||
name = name.substr(1);
|
||||
}
|
||||
if (name.charAt(0) == '^') {
|
||||
startParent = true;
|
||||
name = name.substr(1);
|
||||
}
|
||||
|
||||
const key = controllerKey(name);
|
||||
if (startParent) $element = $element.parent !();
|
||||
const dep = searchParents ? $element.inheritedData !(key) : $element.data !(key);
|
||||
if (!dep && !isOptional) {
|
||||
throw new Error(`Can not locate '${require}' in '${this.directive.name}'.`);
|
||||
}
|
||||
return dep;
|
||||
} else if (require instanceof Array) {
|
||||
const deps: any[] = [];
|
||||
for (let i = 0; i < require.length; i++) {
|
||||
deps.push(this.resolveRequired($element, require[i]));
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
throw new Error(
|
||||
`Directive '${this.directive.name}' require syntax unrecognized: ${this.directive.require}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isFunction(value: any): value is Function {
|
||||
return typeof value === 'function';
|
||||
}
|
||||
|
Reference in New Issue
Block a user