mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(overlay): Render the templates before placing them in the overlay. (#2989)
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.
This commit is contained in:
committed by
Jeremy Elbourn
parent
eef57f601c
commit
da33c03c70
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <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;
|
||||
|
||||
@@ -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 {
|
||||
<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) { }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.`);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user