Skip to content

Commit

Permalink
refactor(upgrade): use UpgradeHelper in the dynamic version
Browse files Browse the repository at this point in the history
This also improves `upgrade/dynamic` and brings 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 is the equivalet of angular#16627 for `upgrade/dynamic`.)

Fixes angular#11044
  • Loading branch information
gkalpak committed May 29, 2017
1 parent 90c9106 commit a5ccc1e
Show file tree
Hide file tree
Showing 5 changed files with 598 additions and 301 deletions.
188 changes: 110 additions & 78 deletions packages/upgrade/src/common/upgrade_helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {controllerKey, directiveNormalize, isFunction} from './util';


// Constants
export const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/;
const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/;

// Interfaces
export interface IBindingDestination {
Expand All @@ -38,67 +38,78 @@ export class UpgradeHelper {

private readonly $compile: angular.ICompileService;
private readonly $controller: angular.IControllerService;
private readonly $templateCache: angular.ITemplateCacheService;

constructor(private injector: Injector, private name: string, elementRef: ElementRef) {
constructor(private injector: Injector, private name: string, elementRef: ElementRef, directive?: angular.IDirective) {
this.$injector = injector.get($INJECTOR);
this.$compile = this.$injector.get($COMPILE);
this.$controller = this.$injector.get($CONTROLLER);
this.$templateCache = this.$injector.get($TEMPLATE_CACHE);

this.element = elementRef.nativeElement;
this.$element = angular.element(this.element);

this.directive = this.getDirective();
this.directive = directive || UpgradeHelper.getDirective(this.$injector, name);
}

buildController(controllerType: angular.IController, $scope: angular.IScope) {
// TODO: Document that we do not pre-assign bindings on the controller instance.
// Quoted properties below so that this code can be optimized with Closure Compiler.
const locals = {'$scope': $scope, '$element': this.$element};
const controller = this.$controller(controllerType, locals, null, this.directive.controllerAs);
static getDirective($injector: angular.IInjectorService, name: string): angular.IDirective {
const directives: angular.IDirective[] = $injector.get(name + 'Directive');
if (directives.length > 1) {
throw new Error(`Only support single directive definition for: ${name}`);
}

this.$element.data !(controllerKey(this.directive.name !), controller);
const directive = directives[0];
if (directive.replace) notSupported(name, 'replace');
if (directive.terminal) notSupported(name, 'terminal');
// if (directive.compile) notSupported(name, 'compile');

return controller;
return directive;
}

compileLocalTemplate() {
const handleRemoteTemplate = () => {
throw new Error('loading directive templates asynchronously is not supported');
};
static getTemplate($injector: angular.IInjectorService, directive: angular.IDirective, fetchRemoteTemplate = false): string | Promise<string> {
if (directive.template !== undefined) {
return getOrCall<string>(directive.template);
} else if (directive.templateUrl) {
const $templateCache = $injector.get($TEMPLATE_CACHE) as angular.ITemplateCacheService;
const url = getOrCall<string>(directive.templateUrl);
const template = $templateCache.get(url);

if (template !== undefined) {
return template;
} else if (!fetchRemoteTemplate) {
throw new Error('loading directive templates asynchronously is not supported');
}

return this.compileTemplate(handleRemoteTemplate);
return new Promise((resolve, reject) => {
const $httpBackend = $injector.get($HTTP_BACKEND) as angular.IHttpBackendService;
$httpBackend('GET', url, null, (status: number, response: string) => {
if (status === 200) {
resolve($templateCache.put(url, response));
} else {
reject(`GET component template from '${url}' returned '${status}: ${response}'`);
}
});
});
} else {
throw new Error(`Directive '${directive.name}' is not a component, it is missing template.`);
}
}

fetchAndCompileTemplate(): angular.ILinkFn | Promise<angular.ILinkFn> {
const handleRemoteTemplate = (url: string) => new Promise((resolve, reject) => {
const $httpBackend = this.$injector.get($HTTP_BACKEND) as angular.IHttpBackendService;
buildController(controllerType: angular.IController, $scope: angular.IScope) {
// TODO: Document that we do not pre-assign bindings on the controller instance.
// Quoted properties below so that this code can be optimized with Closure Compiler.
const locals = {'$scope': $scope, '$element': this.$element};
const controller = this.$controller(controllerType, locals, null, this.directive.controllerAs);

$httpBackend('GET', url, null, (status: number, response: string) => {
if (status === 200) {
resolve(this.compileHtml(this.$templateCache.put(url, response)));
} else {
reject(`GET component template from '${url}' returned '${status}: ${response}'`);
}
});
});
this.$element.data !(controllerKey(this.directive.name !), controller);

return this.compileTemplate(handleRemoteTemplate);
return controller;
}

getDirective(): angular.IDirective {
const directives: angular.IDirective[] = this.$injector.get(this.name + 'Directive');
if (directives.length > 1) {
throw new Error(`Only support single directive definition for: ${this.name}`);
compileTemplate(template?: string): angular.ILinkFn {
if (template === undefined) {
template = UpgradeHelper.getTemplate(this.$injector, this.directive) as string;
}

const directive = directives[0];
if (directive.replace) this.notSupported('replace');
if (directive.terminal) this.notSupported('terminal');
if (directive.compile) this.notSupported('compile');

return directive;
return this.compileHtml(template);
}

prepareTransclusion(): angular.ILinkFn | undefined {
Expand Down Expand Up @@ -177,7 +188,56 @@ export class UpgradeHelper {
return attachChildrenFn;
}

resolveRequire(require: angular.DirectiveRequireProperty): angular.SingleOrListOrMap<IControllerInstance> | null {
resolveAndBindRequiredControllers(controllerInstance: IControllerInstance | null) {
const directiveRequire = this.getDirectiveRequire();
const requiredControllers = this.resolveRequire(directiveRequire);

if (controllerInstance && this.directive.bindToController && isMap(directiveRequire)) {
const requiredControllersMap = requiredControllers as {[key: string]: IControllerInstance};
Object.keys(requiredControllersMap).forEach(key => {
controllerInstance[key] = requiredControllersMap[key];
});
}

return requiredControllers;
}

private compileHtml(html: string): angular.ILinkFn {
this.element.innerHTML = html;
return this.$compile(this.element.childNodes);
}

private extractChildNodes(): Node[] {
const childNodes: Node[] = [];
let childNode: Node|null;

while (childNode = this.element.firstChild) {
this.element.removeChild(childNode);
childNodes.push(childNode);
}

return childNodes;
}

private getDirectiveRequire(): angular.DirectiveRequireProperty {
const require = this.directive.require || (this.directive.controller && this.directive.name) !;

if (isMap(require)) {
Object.keys(require).forEach(key => {
const value = require[key];
const match = value.match(REQUIRE_PREFIX_RE) !;
const name = value.substring(match[0].length);

if (!name) {
require[key] = match[0] + key;
}
});
}

return require;
}

private resolveRequire(require: angular.DirectiveRequireProperty, controllerInstance?: any): angular.SingleOrListOrMap<IControllerInstance> | null {
if (!require) {
return null;
} else if (Array.isArray(require)) {
Expand Down Expand Up @@ -208,45 +268,17 @@ export class UpgradeHelper {
throw new Error(`Unrecognized 'require' syntax on upgraded directive '${this.name}': ${require}`);
}
}
}

private compileHtml(html: string): angular.ILinkFn {
this.element.innerHTML = html;
return this.$compile(this.element.childNodes);
}

private compileTemplate<T>(handleRemoteTemplate: (url: string) => T): angular.ILinkFn | T {
if (this.directive.template !== undefined) {
return this.compileHtml(this.getOrCall<string>(this.directive.template));
} else if (this.directive.templateUrl) {
const url = this.getOrCall<string>(this.directive.templateUrl);
const html = this.$templateCache.get(url) as string;
if (html !== undefined) {
return this.compileHtml(html);
} else {
return handleRemoteTemplate(url);
}
} else {
throw new Error(`Directive '${this.name}' is not a component, it is missing template.`);
}
}

private extractChildNodes(): Node[] {
const childNodes: Node[] = [];
let childNode: Node|null;

while (childNode = this.element.firstChild) {
this.element.removeChild(childNode);
childNodes.push(childNode);
}

return childNodes;
}
function getOrCall<T>(property: T | Function): T {
return isFunction(property) ? property() : property;
}

private getOrCall<T>(property: T | Function): T {
return isFunction(property) ? property() : property;
}
// NOTE: Only works for `typeof T !== 'object'`.
function isMap<T>(value: angular.SingleOrListOrMap<T>): value is {[key: string]: T} {
return value && !Array.isArray(value) && typeof value === 'object';
}

private notSupported(feature: string) {
throw new Error(`Upgraded directive '${this.name}' contains unsupported feature: '${feature}'.`);
}
function notSupported(name: string, feature: string) {
throw new Error(`Upgraded directive '${name}' contains unsupported feature: '${feature}'.`);
}
4 changes: 2 additions & 2 deletions packages/upgrade/src/dynamic/upgrade_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
Loading

0 comments on commit a5ccc1e

Please sign in to comment.