Skip to content

Render template portal elements to properly determine placement #2989

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

Merged
merged 1 commit into from
Apr 11, 2017
Merged
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
8 changes: 8 additions & 0 deletions src/demo-app/overlay/overlay-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
Pasta 3
</button>

<button cdk-overlay-origin #tortelliniOrigin="cdkOverlayOrigin" (click)="openTortelliniPanel()">
Pasta 4
</button>


<button cdk-overlay-origin #trigger="cdkOverlayOrigin" (click)="isMenuOpen = !isMenuOpen">
Open menu
Expand All @@ -27,4 +31,8 @@
<p class="demo-fusilli"> Fusilli </p>
</ng-template>

<ng-template cdk-portal #tortelliniTemplate="cdkPortal">
<ul class="demo-tortellini"><li *ngFor="let filling of tortelliniFillings">{{filling}}</li></ul>
</ng-template>

<button (click)="openPanelWithBackdrop()">Backdrop panel</button>
9 changes: 9 additions & 0 deletions src/demo-app/overlay/overlay-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@
background-color: rebeccapurple;
opacity: 0.5;
}

.demo-tortellini {
margin: 0;
padding: 10px;
border: 1px solid black;
color: white;
background-color: orangered;
opacity: 0.5;
}
18 changes: 18 additions & 0 deletions src/demo-app/overlay/overlay-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ import {
export class OverlayDemo {
nextPosition: number = 0;
isMenuOpen: boolean = false;
tortelliniFillings = ['cheese and spinach', 'mushroom and broccoli'];

@ViewChildren(TemplatePortalDirective) templatePortals: QueryList<Portal<any>>;
@ViewChild(OverlayOrigin) _overlayOrigin: OverlayOrigin;
@ViewChild('tortelliniOrigin') tortelliniOrigin: OverlayOrigin;
@ViewChild('tortelliniTemplate') tortelliniTemplate: TemplatePortalDirective;

constructor(public overlay: Overlay, public viewContainerRef: ViewContainerRef) { }

Expand Down Expand Up @@ -75,6 +78,21 @@ export class OverlayDemo {
overlayRef.attach(new ComponentPortal(SpagettiPanel, this.viewContainerRef));
}

openTortelliniPanel() {
let strategy = this.overlay.position()
.connectedTo(
this.tortelliniOrigin.elementRef,
{originX: 'start', originY: 'bottom'},
{overlayX: 'end', overlayY: 'top'} );

let config = new OverlayState();
config.positionStrategy = strategy;

let overlayRef = this.overlay.create(config);

overlayRef.attach(this.tortelliniTemplate);
}

openPanelWithBackdrop() {
let config = new OverlayState();

Expand Down
1 change: 1 addition & 0 deletions src/lib/core/portal/dom-portal-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class DomPortalHost extends BasePortalHost {
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
let viewContainer = portal.viewContainerRef;
let viewRef = viewContainer.createEmbeddedView(portal.templateRef);
viewRef.detectChanges();

// The method `createEmbeddedView` will add the view as a child of the viewContainer.
// But for the DomPortalHost the view can be added everywhere in the DOM (e.g Overlay Container)
Expand Down
49 changes: 46 additions & 3 deletions src/lib/core/portal/portal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Injector,
ApplicationRef,
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {TemplatePortalDirective, PortalHostDirective, PortalModule} from './portal-directives';
import {Portal, ComponentPortal} from './portal';
import {DomPortalHost} from './dom-portal-host';
Expand Down Expand Up @@ -123,6 +124,28 @@ describe('Portals', () => {
expect(hostContainer.textContent).toContain('Mango');
});

it('should load a <ng-template> portal with an inner template', () => {
let testAppComponent = fixture.debugElement.componentInstance;

// Detect changes initially so that the component's ViewChildren are resolved.
fixture.detectChanges();

// Set the selectedHost to be a TemplatePortal.
testAppComponent.selectedPortal = testAppComponent.portalWithTemplate;
fixture.detectChanges();

// Expect that the content of the attached portal is present.
let hostContainer = fixture.nativeElement.querySelector('.portal-container');
expect(hostContainer.textContent).toContain('Pineapple');

// When updating the binding value.
testAppComponent.fruits = ['Mangosteen'];
fixture.detectChanges();

// Expect the new value to be reflected in the rendered output.
expect(hostContainer.textContent).toContain('Mangosteen');
});

it('should change the attached portal', () => {
let testAppComponent = fixture.debugElement.componentInstance;

Expand Down Expand Up @@ -258,6 +281,15 @@ describe('Portals', () => {
expect(someDomElement.textContent).toContain('Cake');
});

it('should render a template portal with an inner template', () => {
let fixture = TestBed.createComponent(PortalTestApp);
fixture.detectChanges();

fixture.componentInstance.portalWithTemplate.attach(host);

expect(someDomElement.textContent).toContain('Durian');
});

it('should attach and detach a template portal with a binding', () => {
let fixture = TestBed.createComponent(PortalTestApp);

Expand Down Expand Up @@ -384,14 +416,21 @@ class ArbitraryViewContainerRefComponent {
<ng-template cdk-portal>Cake</ng-template>

<div *cdk-portal>Pie</div>

<ng-template cdk-portal> {{fruit}} </ng-template>`,
<ng-template cdk-portal> {{fruit}} </ng-template>

<ng-template cdk-portal>
<ul>
<li *ngFor="let fruitName of fruits"> {{fruitName}} </li>
</ul>
</ng-template>
`,
})
class PortalTestApp {
@ViewChildren(TemplatePortalDirective) portals: QueryList<TemplatePortalDirective>;
@ViewChild(PortalHostDirective) portalHost: PortalHostDirective;
selectedPortal: Portal<any>;
fruit: string = 'Banana';
fruits = ['Apple', 'Pineapple', 'Durian'];

constructor(public injector: Injector) { }

Expand All @@ -406,13 +445,17 @@ class PortalTestApp {
get portalWithBinding() {
return this.portals.toArray()[2];
}

get portalWithTemplate() {
return this.portals.toArray()[3];
}
}

// Create a real (non-test) NgModule as a workaround for
// https://github.com/angular/angular/issues/10760
const TEST_COMPONENTS = [PortalTestApp, ArbitraryViewContainerRefComponent, PizzaMsg];
@NgModule({
imports: [PortalModule],
imports: [CommonModule, PortalModule],
exports: TEST_COMPONENTS,
declarations: TEST_COMPONENTS,
entryComponents: TEST_COMPONENTS,
Expand Down
40 changes: 22 additions & 18 deletions src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1040,34 +1040,38 @@ describe('MdSelect', () => {
select.style.marginRight = '20px';
});

it('should adjust for the checkbox in ltr', () => {
it('should adjust for the checkbox in ltr', async(() => {
trigger.click();
multiFixture.detectChanges();

const triggerLeft = trigger.getBoundingClientRect().left;
const firstOptionLeft =
document.querySelector('.cdk-overlay-pane md-option').getBoundingClientRect().left;
multiFixture.whenStable().then(() => {
const triggerLeft = trigger.getBoundingClientRect().left;
const firstOptionLeft =
document.querySelector('.cdk-overlay-pane md-option').getBoundingClientRect().left;

// 48px accounts for the checkbox size, margin and the panel's padding.
expect(firstOptionLeft.toFixed(2))
.toEqual((triggerLeft - 48).toFixed(2),
`Expected trigger label to align along x-axis, accounting for the checkbox.`);
});
// 48px accounts for the checkbox size, margin and the panel's padding.
expect(firstOptionLeft.toFixed(2))
.toEqual((triggerLeft - 48).toFixed(2),
`Expected trigger label to align along x-axis, accounting for the checkbox.`);
});
}));

it('should adjust for the checkbox in rtl', () => {
it('should adjust for the checkbox in rtl', async(() => {
dir.value = 'rtl';
trigger.click();
multiFixture.detectChanges();

const triggerRight = trigger.getBoundingClientRect().right;
const firstOptionRight =
document.querySelector('.cdk-overlay-pane md-option').getBoundingClientRect().right;
multiFixture.whenStable().then(() => {
const triggerRight = trigger.getBoundingClientRect().right;
const firstOptionRight =
document.querySelector('.cdk-overlay-pane md-option').getBoundingClientRect().right;

// 48px accounts for the checkbox size, margin and the panel's padding.
expect(firstOptionRight.toFixed(2))
.toEqual((triggerRight + 48).toFixed(2),
`Expected trigger label to align along x-axis, accounting for the checkbox.`);
});
// 48px accounts for the checkbox size, margin and the panel's padding.
expect(firstOptionRight.toFixed(2))
.toEqual((triggerRight + 48).toFixed(2),
`Expected trigger label to align along x-axis, accounting for the checkbox.`);
});
}));
});

});
Expand Down