Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(upgrade): WIP fix multislot transclusion on upgraded components #16627

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/upgrade/src/common/angular1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface ICompileService {
}
export interface ILinkFn {
(scope: IScope, cloneAttachFn?: ICloneAttachFunction, options?: ILinkFnOptions): IAugmentedJQuery;
$$slots?: {[slotName: string]: ILinkFn};
}
export interface ILinkFnOptions {
parentBoundTranscludeFn?: Function;
Expand Down Expand Up @@ -75,9 +76,10 @@ export interface IDirective {
templateUrl?: string|Function;
templateNamespace?: string;
terminal?: boolean;
transclude?: boolean|'element'|{[key: string]: string};
transclude?: DirectiveTranscludeProperty;
}
export type DirectiveRequireProperty = SingleOrListOrMap<string>;
export type DirectiveTranscludeProperty = boolean | 'element' | {[key: string]: string};
export interface IDirectiveCompileFn {
(templateElement: IAugmentedJQuery, templateAttributes: IAttributes,
transclude: ITranscludeFunction): IDirectivePrePost;
Expand All @@ -97,7 +99,7 @@ export interface IComponent {
require?: DirectiveRequireProperty;
template?: string|Function;
templateUrl?: string|Function;
transclude?: boolean;
transclude?: DirectiveTranscludeProperty;
}
export interface IAttributes { $observe(attr: string, fn: (v: string) => void): void; }
export interface ITranscludeFunction {
Expand Down
8 changes: 8 additions & 0 deletions packages/upgrade/src/common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import {Type} from '@angular/core';
import * as angular from './angular1';

const DIRECTIVE_PREFIX_REGEXP = /^(?:x|data)[:\-_]/i;
const DIRECTIVE_SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g;

export function onError(e: any) {
// TODO: (misko): We seem to not have a stack trace here!
if (console.error) {
Expand All @@ -24,6 +27,11 @@ export function controllerKey(name: string): string {
return '$' + name + 'Controller';
}

export function directiveNormalize(name: string): string {
return name.replace(DIRECTIVE_PREFIX_REGEXP, '')
.replace(DIRECTIVE_SPECIAL_CHARS_REGEXP, (_, letter) => letter.toUpperCase());
}

export function getAttributesAsArray(node: Node): [string, string][] {
const attributes = node.attributes;
let asArray: [string, string][] = undefined !;
Expand Down
68 changes: 63 additions & 5 deletions packages/upgrade/src/static/upgrade_component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import {DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, SimpleChanges, ɵlooseIdentical as looseIdentical} from '@angular/core';
import * as angular from '../common/angular1';
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $INJECTOR, $SCOPE, $TEMPLATE_CACHE} from '../common/constants';
import {controllerKey} from '../common/util';
import {controllerKey, directiveNormalize} from '../common/util';

const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/;
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
Expand Down Expand Up @@ -144,7 +144,8 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {

ngOnInit() {
// Collect contents, insert and compile template
const contentChildNodes = this.extractChildNodes(this.element);
const attachChildNodes: angular.ILinkFn|undefined =
this.prepareTransclusion(this.directive.transclude);
const linkFn = this.compileTemplate(this.directive);

// Instantiate controller
Expand Down Expand Up @@ -203,8 +204,6 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
preLink(this.$componentScope, this.$element, attrs, requiredControllers, transcludeFn);
}

const attachChildNodes: angular.ILinkFn = (scope, cloneAttach) =>
cloneAttach !(contentChildNodes);
linkFn(this.$componentScope, null !, {parentBoundTranscludeFn: attachChildNodes});

if (postLink) {
Expand Down Expand Up @@ -333,6 +332,66 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
return bindings;
}

private prepareTransclusion(transclude: angular.DirectiveTranscludeProperty = false):
angular.ILinkFn|undefined {
const contentChildNodes = this.extractChildNodes(this.element);
let $template = contentChildNodes;
let attachChildrenFn: angular.ILinkFn|undefined = (scope, cloneAttach) =>
cloneAttach !($template, scope);

if (transclude) {
const slots = Object.create(null);

if (typeof transclude === 'object') {
$template = [];

const slotMap = Object.create(null);
const filledSlots = Object.create(null);

// Parse the element selectors.
Object.keys(transclude).forEach(slotName => {
let selector = transclude[slotName];
const optional = selector.charAt(0) === '?';
selector = optional ? selector.substring(1) : selector;

slotMap[selector] = slotName;
slots[slotName] = null; // `null`: Defined but not yet filled.
filledSlots[slotName] = optional; // Consider optional slots as filled.
});

// Add the matching elements into their slot.
contentChildNodes.forEach(node => {
const slotName = slotMap[directiveNormalize(node.nodeName.toLowerCase())];
if (slotName) {
filledSlots[slotName] = true;
slots[slotName] = slots[slotName] || [];
slots[slotName].push(node);
} else {
$template.push(node);
}
});

// Check for required slots that were not filled.
Object.keys(filledSlots).forEach(slotName => {
if (!filledSlots[slotName]) {
throw new Error(`Required transclusion slot '${slotName}' on directive: ${this.name}`);
}
});

Object.keys(slots).filter(slotName => slots[slotName]).forEach(slotName => {
const nodes = slots[slotName];
slots[slotName] = (scope: angular.IScope, cloneAttach: angular.ICloneAttachFunction) =>
cloneAttach !(nodes, scope);
});
}

// Attach `$$slots` to default slot transclude fn.
attachChildrenFn.$$slots = slots;
}

return attachChildrenFn;
}

private extractChildNodes(element: Element): Node[] {
const childNodes: Node[] = [];
let childNode: Node|null;
Expand Down Expand Up @@ -465,7 +524,6 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
}
}


function getOrCall<T>(property: Function | T): T {
return isFunction(property) ? property() : property;
}
Expand Down
5 changes: 3 additions & 2 deletions packages/upgrade/test/common/test_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ export function html(html: string): Element {
return div;
}

export function multiTrim(text: string | null | undefined): string {
export function multiTrim(text: string | null | undefined, allSpace = false): string {
if (typeof text == 'string') {
return text.replace(/\n/g, '').replace(/\s\s+/g, ' ').trim();
const repl = allSpace ? '' : ' ';
return text.replace(/\n/g, '').replace(/\s+/g, repl).trim();
}
throw new Error('Argument can not be undefined.');
}
Expand Down
Loading