From d972879e635e281a346e208c1700d33d5fc33e0d Mon Sep 17 00:00:00 2001 From: Thomas Shafer Date: Wed, 8 Feb 2017 10:35:00 -0800 Subject: [PATCH] fix(overlay): Render the templates before placing them in the overlay. This fixes positioning issues when rendering templates which contain embedded templates. The templates need to be rendered in order to properly determine width which can then determine placement. --- src/demo-app/overlay/overlay-demo.html | 8 +++++ src/demo-app/overlay/overlay-demo.scss | 9 +++++ src/demo-app/overlay/overlay-demo.ts | 18 ++++++++++ src/lib/core/portal/dom-portal-host.ts | 1 + src/lib/core/portal/portal.spec.ts | 49 ++++++++++++++++++++++++-- src/lib/select/select.spec.ts | 40 +++++++++++---------- 6 files changed, 104 insertions(+), 21 deletions(-) diff --git a/src/demo-app/overlay/overlay-demo.html b/src/demo-app/overlay/overlay-demo.html index c431cffc15fc..df048577ba7d 100644 --- a/src/demo-app/overlay/overlay-demo.html +++ b/src/demo-app/overlay/overlay-demo.html @@ -10,6 +10,10 @@ Pasta 3 + + diff --git a/src/demo-app/overlay/overlay-demo.scss b/src/demo-app/overlay/overlay-demo.scss index 8e5d1cbd4743..275b5161e5f5 100644 --- a/src/demo-app/overlay/overlay-demo.scss +++ b/src/demo-app/overlay/overlay-demo.scss @@ -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; +} diff --git a/src/demo-app/overlay/overlay-demo.ts b/src/demo-app/overlay/overlay-demo.ts index c8f3708658a2..c4e61eba6d21 100644 --- a/src/demo-app/overlay/overlay-demo.ts +++ b/src/demo-app/overlay/overlay-demo.ts @@ -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>; @ViewChild(OverlayOrigin) _overlayOrigin: OverlayOrigin; + @ViewChild('tortelliniOrigin') tortelliniOrigin: OverlayOrigin; + @ViewChild('tortelliniTemplate') tortelliniTemplate: TemplatePortalDirective; constructor(public overlay: Overlay, public viewContainerRef: ViewContainerRef) { } @@ -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(); diff --git a/src/lib/core/portal/dom-portal-host.ts b/src/lib/core/portal/dom-portal-host.ts index d44b9e0a901c..7a2ebf360779 100644 --- a/src/lib/core/portal/dom-portal-host.ts +++ b/src/lib/core/portal/dom-portal-host.ts @@ -64,6 +64,7 @@ export class DomPortalHost extends BasePortalHost { attachTemplatePortal(portal: TemplatePortal): Map { 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) diff --git a/src/lib/core/portal/portal.spec.ts b/src/lib/core/portal/portal.spec.ts index 6654647082d2..8fc3c5743e19 100644 --- a/src/lib/core/portal/portal.spec.ts +++ b/src/lib/core/portal/portal.spec.ts @@ -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'; @@ -123,6 +124,28 @@ describe('Portals', () => { expect(hostContainer.textContent).toContain('Mango'); }); + it('should load a 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; @@ -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); @@ -384,14 +416,21 @@ class ArbitraryViewContainerRefComponent { Cake
Pie
- - {{fruit}} `, + {{fruit}} + + +
    +
  • {{fruitName}}
  • +
+
+ `, }) class PortalTestApp { @ViewChildren(TemplatePortalDirective) portals: QueryList; @ViewChild(PortalHostDirective) portalHost: PortalHostDirective; selectedPortal: Portal; fruit: string = 'Banana'; + fruits = ['Apple', 'Pineapple', 'Durian']; constructor(public injector: Injector) { } @@ -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, diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 1577f252e85c..4036be250a74 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -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.`); + }); + })); }); });