Skip to content

Commit

Permalink
fix(upgrade): WIP fix multislot transclusion on upgraded components
Browse files Browse the repository at this point in the history
  • Loading branch information
petebacondarwin authored and gkalpak committed May 26, 2017
1 parent 6949510 commit 97f1dcb
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 4 deletions.
75 changes: 71 additions & 4 deletions packages/upgrade/src/static/upgrade_component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
}

ngOnInit() {
console.log('ngOnInit');
const attachChildNodes: angular.ILinkFn | undefined = this.prepareTransclusion(this.directive.transclude)
// Collect contents, insert and compile template
const contentChildNodes = this.extractChildNodes(this.element);
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 All @@ -217,6 +216,73 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
}
}

private prepareTransclusion(transclude: any = false): angular.ILinkFn | undefined {
let childTranscludeFn: angular.ILinkFn | undefined;

if (transclude) {
const slots = Object.create(null);
let $template: angular.IAugmentedJQuery | Node[];

if (typeof transclude !== 'object') {
$template = this.extractChildNodes(this.element);
} else {
$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.
Array.prototype.forEach.call(this.$element.contents !(), (node: Element) => {
console.log(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);
}
});

console.log(slots, filledSlots);

// 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 slot = slots[slotName];
slots[slotName] = (scope: any, cloneAttach: any) => cloneAttach !(angular.element(slot), scope);
});
}

this.$element.empty !();

// default slot transclude fn
childTranscludeFn = (scope, cloneAttach) => cloneAttach !(angular.element($template as any));
(childTranscludeFn as any).$$slots = slots;

return childTranscludeFn;
}
}

ngOnChanges(changes: SimpleChanges) {
if (!this.bindingDestination) {
this.pendingChanges = changes;
Expand Down Expand Up @@ -465,7 +531,6 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
}
}


function getOrCall<T>(property: Function | T): T {
return isFunction(property) ? property() : property;
}
Expand All @@ -478,3 +543,5 @@ function isFunction(value: any): value is Function {
function isMap<T>(value: angular.SingleOrListOrMap<T>): value is {[key: string]: T} {
return value && !Array.isArray(value) && typeof value === 'object';
}

function directiveNormalize(id: string) { return id; }
65 changes: 65 additions & 0 deletions packages/upgrade/test/static/integration/upgrade_component_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,71 @@ export function main() {
});
});

fdescribe('transclusion', () => {
it('should support multizone transclusion', async(() => {
// Define `ng1Component`
const ng1Component: angular.IComponent = {
transclude: { x: 'x', y: 'y' } as any,
// template: 'pre(<div ng-transclude>(original)</div>)post'
template: 'pre(<div ng-transclude="x">(original X)</div>(mid {{ 1 + 2 }})<div ng-transclude="y">(original Y)</div><div ng-transclude>(original default)</div>)post'
};

// Define `Ng1ComponentFacade`
@Directive({selector: 'ng1'})
class Ng1ComponentFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1', elementRef, injector);
}
}

let component: any;

@Component({selector: 'app', template: '<ng1><x><div *ngIf="value">(trans-X)</div></x>(trans-default {{ 1 + 2 }})<y>(trans-Y)</y></ng1>'})
class AppComponent {
constructor() {
component = this;
}
value = true;
}

// Define `ng1Module`
const ng1Module = angular.module('ng1', [])
.component('ng1', ng1Component)
.directive('app', downgradeComponent({component: AppComponent}));

const element = html(`<app></app>`);

// Define `Ng2Module`
@NgModule({
declarations: [Ng1ComponentFacade, AppComponent],
entryComponents: [AppComponent],
imports: [BrowserModule, UpgradeModule],
schemas: [NO_ERRORS_SCHEMA],
})
class Ng2Module {
constructor(private upgrade: UpgradeModule) {}
ngDoBootstrap() {
this.upgrade.bootstrap(element, [ng1Module.name]);
}
}

console.log('bootstrapping');
platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => {
console.log(element);
component.value = false;
$digest(ref.injector.get(UpgradeModule));
console.log(element);
component.value = true;
$digest(ref.injector.get(UpgradeModule));
console.log(element);
}).then(() => {
expect(element.textContent).toEqual('');
}).catch((err) => {
console.log(err);
});
}));
});

describe('lifecycle hooks', () => {
it('should call `$onChanges()` on binding destination (prototype)', fakeAsync(() => {
const scopeOnChanges = jasmine.createSpy('scopeOnChanges');
Expand Down

0 comments on commit 97f1dcb

Please sign in to comment.