mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(projection): Host Projection service (#1756)
This commit is contained in:
@@ -33,6 +33,7 @@ import {SnackBarDemo} from './snack-bar/snack-bar-demo';
|
||||
import {PortalDemo, ScienceJoke} from './portal/portal-demo';
|
||||
import {MenuDemo} from './menu/menu-demo';
|
||||
import {TabsDemo, SunnyTabContent, RainyTabContent, FoggyTabContent} from './tabs/tabs-demo';
|
||||
import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-demo';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -66,6 +67,8 @@ import {TabsDemo, SunnyTabContent, RainyTabContent, FoggyTabContent} from './tab
|
||||
PortalDemo,
|
||||
ProgressBarDemo,
|
||||
ProgressCircleDemo,
|
||||
ProjectionDemo,
|
||||
ProjectionTestComponent,
|
||||
RadioDemo,
|
||||
RippleDemo,
|
||||
RotiniPanel,
|
||||
|
||||
@@ -34,6 +34,7 @@ export class DemoApp {
|
||||
{name: 'Live Announcer', route: 'live-announcer'},
|
||||
{name: 'Overlay', route: 'overlay'},
|
||||
{name: 'Portal', route: 'portal'},
|
||||
{name: 'Projection', route: 'projection'},
|
||||
{name: 'Progress Bar', route: 'progress-bar'},
|
||||
{name: 'Progress Circle', route: 'progress-circle'},
|
||||
{name: 'Radio', route: 'radio'},
|
||||
|
||||
@@ -27,6 +27,7 @@ import {RippleDemo} from '../ripple/ripple-demo';
|
||||
import {DialogDemo} from '../dialog/dialog-demo';
|
||||
import {TooltipDemo} from '../tooltip/tooltip-demo';
|
||||
import {SnackBarDemo} from '../snack-bar/snack-bar-demo';
|
||||
import {ProjectionDemo} from '../projection/projection-demo';
|
||||
import {TABS_DEMO_ROUTES} from '../tabs/routes';
|
||||
|
||||
export const DEMO_APP_ROUTES: Routes = [
|
||||
@@ -41,6 +42,7 @@ export const DEMO_APP_ROUTES: Routes = [
|
||||
{path: 'progress-circle', component: ProgressCircleDemo},
|
||||
{path: 'progress-bar', component: ProgressBarDemo},
|
||||
{path: 'portal', component: PortalDemo},
|
||||
{path: 'projection', component: ProjectionDemo},
|
||||
{path: 'overlay', component: OverlayDemo},
|
||||
{path: 'checkbox', component: CheckboxDemo},
|
||||
{path: 'input', component: InputDemo},
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {Component, ViewChild, ElementRef, OnInit, Input} from '@angular/core';
|
||||
import {DomProjectionHost, DomProjection} from '@angular/material';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: '[projection-test]',
|
||||
template: `
|
||||
<div class="demo-outer {{cssClass}}">
|
||||
Before
|
||||
<dom-projection-host><ng-content></ng-content></dom-projection-host>
|
||||
After
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.demo-outer {
|
||||
background-color: #663399;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ProjectionTestComponent implements OnInit {
|
||||
@ViewChild(DomProjectionHost) _host: DomProjectionHost;
|
||||
@Input('class') cssClass: any;
|
||||
|
||||
constructor(private _projection: DomProjection, private _ref: ElementRef) {}
|
||||
|
||||
ngOnInit() {
|
||||
this._projection.project(this._ref, this._host);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'projection-app',
|
||||
template: `
|
||||
<div projection-test class="demo-inner">
|
||||
<div class="content">Content: {{binding}}</div>
|
||||
</div>
|
||||
<br/>
|
||||
<input projection-test [(ngModel)]="binding" [class]="binding" [ngClass]="{'blue': true}">
|
||||
<input [(ngModel)]="binding" class="my-class" [ngClass]="{'blue': true}">
|
||||
`,
|
||||
styles: [`
|
||||
.demo-inner {
|
||||
background-color: #DAA520;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ProjectionDemo {
|
||||
binding: string = 'abc';
|
||||
}
|
||||
@@ -26,6 +26,9 @@ export {
|
||||
} from './portal/portal-directives';
|
||||
export {DomPortalHost} from './portal/dom-portal-host';
|
||||
|
||||
// Projection
|
||||
export * from './projection/projection';
|
||||
|
||||
// Overlay
|
||||
export {Overlay, OVERLAY_PROVIDERS} from './overlay/overlay';
|
||||
export {OverlayContainer} from './overlay/overlay-container';
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import {TestBed, async} from '@angular/core/testing';
|
||||
import {
|
||||
NgModule,
|
||||
Component,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
import {ProjectionModule, DomProjection, DomProjectionHost} from './projection';
|
||||
|
||||
|
||||
describe('Projection', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ProjectionModule.forRoot(), ProjectionTestModule],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
it('should project properly', async(() => {
|
||||
const fixture = TestBed.createComponent(ProjectionTestApp);
|
||||
const appEl: HTMLDivElement = fixture.nativeElement;
|
||||
const outerDivEl = appEl.querySelector('.outer');
|
||||
const innerDivEl = appEl.querySelector('.inner');
|
||||
|
||||
// Expect the reverse of the tests down there.
|
||||
expect(appEl.querySelector('dom-projection-host')).not.toBeNull();
|
||||
expect(outerDivEl.querySelector('.inner')).not.toBe(innerDivEl);
|
||||
|
||||
const innerHtml = appEl.innerHTML;
|
||||
|
||||
// Trigger OnInit (and thus the projection).
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(appEl.innerHTML).not.toEqual(innerHtml);
|
||||
|
||||
// Assert `<dom-projection-host>` is not in the DOM anymore.
|
||||
expect(appEl.querySelector('dom-projection-host')).toBeNull();
|
||||
|
||||
// Assert the outerDiv contains the innerDiv.
|
||||
expect(outerDivEl.querySelector('.inner')).toBe(innerDivEl);
|
||||
|
||||
// Assert the innerDiv contains the content.
|
||||
expect(innerDivEl.querySelector('.content')).not.toBeNull();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
/** Test-bed component that contains a projection. */
|
||||
@Component({
|
||||
selector: '[projection-test]',
|
||||
template: `
|
||||
<div class="outer">
|
||||
<dom-projection-host><ng-content></ng-content></dom-projection-host>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class ProjectionTestComponent {
|
||||
@ViewChild(DomProjectionHost) _host: DomProjectionHost;
|
||||
|
||||
constructor(private _projection: DomProjection, private _ref: ElementRef) {}
|
||||
ngOnInit() { this._projection.project(this._ref, this._host); }
|
||||
}
|
||||
|
||||
|
||||
/** Test-bed component that contains a portal host and a couple of template portals. */
|
||||
@Component({
|
||||
selector: 'projection-app',
|
||||
template: `
|
||||
<div projection-test class="inner">
|
||||
<div class="content"></div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class ProjectionTestApp {
|
||||
}
|
||||
|
||||
|
||||
|
||||
const TEST_COMPONENTS = [ProjectionTestApp, ProjectionTestComponent];
|
||||
@NgModule({
|
||||
imports: [ProjectionModule],
|
||||
exports: TEST_COMPONENTS,
|
||||
declarations: TEST_COMPONENTS,
|
||||
entryComponents: TEST_COMPONENTS,
|
||||
})
|
||||
class ProjectionTestModule { }
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import {Injectable, Directive, ModuleWithProviders, NgModule, ElementRef} from '@angular/core';
|
||||
|
||||
|
||||
// "Polyfill" for `Node.replaceWith()`.
|
||||
// cf. https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/replaceWith
|
||||
function _replaceWith(toReplaceEl: HTMLElement, otherEl: HTMLElement) {
|
||||
toReplaceEl.parentElement.replaceChild(otherEl, toReplaceEl);
|
||||
}
|
||||
|
||||
|
||||
@Directive({
|
||||
selector: 'dom-projection-host'
|
||||
})
|
||||
export class DomProjectionHost {
|
||||
constructor(public ref: ElementRef) {}
|
||||
}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class DomProjection {
|
||||
/**
|
||||
* Project an element into a host element.
|
||||
* Replace a host element by another element. This also replaces the children of the element
|
||||
* by the children of the host.
|
||||
*
|
||||
* It should be used like this:
|
||||
*
|
||||
* ```
|
||||
* @Component({
|
||||
* template: `<div>
|
||||
* <dom-projection-host>
|
||||
* <div>other</div>
|
||||
* <ng-content></ng-content>
|
||||
* </dom-projection-host>
|
||||
* </div>`
|
||||
* })
|
||||
* class Cmpt {
|
||||
* constructor(private _projector: DomProjection, private _el: ElementRef) {}
|
||||
* ngOnInit() { this._projector.project(this._el, this._projector); }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This component will move the content of the element it's applied to in the outer div. Because
|
||||
* `project()` also move the children of the host inside the projected element, the element will
|
||||
* contain the `<div>other</div>` HTML as well as its own children.
|
||||
*
|
||||
* Note: without `<ng-content></ng-content>` the projection will project an empty element.
|
||||
*/
|
||||
project(ref: ElementRef, host: DomProjectionHost): void {
|
||||
const projectedEl = ref.nativeElement;
|
||||
const hostEl = host.ref.nativeElement;
|
||||
const childNodes = projectedEl.childNodes;
|
||||
let child = childNodes[0];
|
||||
|
||||
// We hoist all of the projected element's children out into the projected elements position
|
||||
// because we *only* want to move the projected element and not its children.
|
||||
_replaceWith(projectedEl, child);
|
||||
let l = childNodes.length;
|
||||
while (l--) {
|
||||
child.parentNode.insertBefore(childNodes[0], child.nextSibling);
|
||||
child = child.nextSibling; // nextSibling is now the childNodes[0].
|
||||
}
|
||||
|
||||
// Insert all host children under the projectedEl, then replace host by component.
|
||||
l = hostEl.childNodes.length;
|
||||
while (l--) {
|
||||
projectedEl.appendChild(hostEl.childNodes[0]);
|
||||
}
|
||||
_replaceWith(hostEl, projectedEl);
|
||||
|
||||
// At this point the host is replaced by the component. Nothing else to be done.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NgModule({
|
||||
exports: [DomProjectionHost],
|
||||
declarations: [DomProjectionHost],
|
||||
})
|
||||
export class ProjectionModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: ProjectionModule,
|
||||
providers: [DomProjection]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PortalModule,
|
||||
OverlayModule,
|
||||
A11yModule,
|
||||
ProjectionModule,
|
||||
StyleCompatibilityModule,
|
||||
} from './core/index';
|
||||
|
||||
@@ -59,6 +60,7 @@ const MATERIAL_MODULES = [
|
||||
PortalModule,
|
||||
RtlModule,
|
||||
A11yModule,
|
||||
ProjectionModule,
|
||||
StyleCompatibilityModule,
|
||||
];
|
||||
|
||||
@@ -78,6 +80,7 @@ const MATERIAL_MODULES = [
|
||||
MdTabsModule.forRoot(),
|
||||
MdToolbarModule.forRoot(),
|
||||
PortalModule.forRoot(),
|
||||
ProjectionModule.forRoot(),
|
||||
RtlModule.forRoot(),
|
||||
|
||||
// These modules include providers.
|
||||
|
||||
Reference in New Issue
Block a user