feat(UpgradeComponent): add support for `require`

This commit also adds/improves/fixes some `UpgradeComponent` tests.
This commit is contained in:
Georgios Kalpakas 2016-10-20 13:47:56 +03:00 committed by vikerman
parent 469010ea8e
commit fe1d0e29c5
5 changed files with 995 additions and 276 deletions

View File

@ -12,6 +12,8 @@ export interface IAnnotatedFunction extends Function { $inject?: Ng1Token[]; }
export type IInjectable = (Ng1Token | Function)[] | IAnnotatedFunction;
export type SingleOrListOrMap<T> = T | T[] | {[key: string]: T};
export interface IModule {
name: string;
requires: (string|IInjectable)[];
@ -44,6 +46,7 @@ export interface IRootScopeService {
$apply(): any;
$apply(exp: string): any;
$apply(exp: Function): any;
$digest(): any;
$evalAsync(): any;
$on(event: string, fn?: (event?: any, ...args: any[]) => void): Function;
$$childTail: IScope;
@ -72,7 +75,7 @@ export interface IDirective {
terminal?: boolean;
transclude?: boolean|'element'|{[key: string]: string};
}
export type DirectiveRequireProperty = Ng1Token[] | Ng1Token | {[key: string]: Ng1Token};
export type DirectiveRequireProperty = SingleOrListOrMap<string>;
export interface IDirectiveCompileFn {
(templateElement: IAugmentedJQuery, templateAttributes: IAttributes,
transclude: ITranscludeFunction): IDirectivePrePost;

View File

@ -11,6 +11,7 @@ export const INJECTOR_KEY = '$$angularInjector';
export const $INJECTOR = '$injector';
export const $PARSE = '$parse';
export const $ROOT_SCOPE = '$rootScope';
export const $SCOPE = '$scope';
export const $COMPILE = '$compile';

View File

@ -14,6 +14,7 @@ import {controllerKey} from '../util';
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $INJECTOR, $SCOPE, $TEMPLATE_CACHE} from './constants';
const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/;
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
@ -101,7 +102,16 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
ngOnInit() {
const attrs: angular.IAttributes = NOT_SUPPORTED;
const transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
const linkController = this.resolveRequired(this.$element, this.directive.require);
const directiveRequire = this.getDirectiveRequire(this.directive);
let requiredControllers =
this.resolveRequire(this.directive.name, this.$element, directiveRequire);
if (this.directive.bindToController && isMap(directiveRequire)) {
const requiredControllersMap = requiredControllers as{[key: string]: IControllerInstance};
Object.keys(requiredControllersMap).forEach(key => {
this.controllerInstance[key] = requiredControllersMap[key];
});
}
this.callLifecycleHook('$onInit', this.controllerInstance);
@ -109,7 +119,7 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre;
const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link;
if (preLink) {
preLink(this.$componentScope, this.$element, attrs, linkController, transcludeFn);
preLink(this.$componentScope, this.$element, attrs, requiredControllers, transcludeFn);
}
var childNodes: Node[] = [];
@ -126,7 +136,7 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
this.linkFn(this.$componentScope, attachElement, {parentBoundTranscludeFn: attachChildNodes});
if (postLink) {
postLink(this.$componentScope, this.$element, attrs, linkController, transcludeFn);
postLink(this.$componentScope, this.$element, attrs, requiredControllers, transcludeFn);
}
this.callLifecycleHook('$postLink', this.controllerInstance);
@ -187,6 +197,24 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
return directive;
}
private getDirectiveRequire(directive: angular.IDirective): angular.DirectiveRequireProperty {
const require = directive.require || (directive.controller && 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 initializeBindings(directive: angular.IDirective) {
const btcIsObject = typeof directive.bindToController === 'object';
if (btcIsObject && Object.keys(directive.scope).length) {
@ -266,9 +294,47 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
return controller;
}
private resolveRequired(
$element: angular.IAugmentedJQuery, require: angular.DirectiveRequireProperty) {
// TODO
private resolveRequire(
directiveName: string, $element: angular.IAugmentedJQuery,
require: angular.DirectiveRequireProperty): angular.SingleOrListOrMap<IControllerInstance> {
if (!require) {
return null;
} else if (Array.isArray(require)) {
return require.map(req => this.resolveRequire(directiveName, $element, req));
} else if (typeof require === 'object') {
const value: {[key: string]: IControllerInstance} = {};
Object.keys(require).forEach(
key => value[key] = this.resolveRequire(directiveName, $element, require[key]));
return value;
} else if (typeof require === 'string') {
const match = require.match(REQUIRE_PREFIX_RE);
const inheritType = match[1] || match[3];
const name = require.substring(match[0].length);
const isOptional = !!match[2];
const searchParents = !!inheritType;
const startOnParent = inheritType === '^^';
const ctrlKey = controllerKey(name);
if (startOnParent) {
$element = $element.parent();
}
const value = searchParents ? $element.inheritedData(ctrlKey) : $element.data(ctrlKey);
if (!value && !isOptional) {
throw new Error(
`Unable to find required '${require}' in upgraded directive '${directiveName}'.`);
}
return value;
} else {
throw new Error(
`Unrecognized require syntax on upgraded directive '${directiveName}': ${require}`);
}
}
private setupOutputs() {
@ -305,3 +371,8 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
function getOrCall<T>(property: Function | T): T {
return typeof(property) === 'function' ? 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';
}

View File

@ -7,6 +7,7 @@
*/
import {PlatformRef, Type} from '@angular/core';
import * as angular from '@angular/upgrade/src/angular_js';
import {$ROOT_SCOPE} from '@angular/upgrade/src/aot/constants';
import {UpgradeModule} from '@angular/upgrade/static';
export function bootstrap(
@ -20,6 +21,11 @@ export function bootstrap(
});
}
export function digest(adapter: UpgradeModule) {
const $rootScope = adapter.$injector.get($ROOT_SCOPE) as angular.IRootScopeService;
$rootScope.$digest();
}
export function html(html: string): Element {
// Don't return `body` itself, because using it as a `$rootElement` for ng1
// will attach `$injector` to it and that will affect subsequent tests.