-
Notifications
You must be signed in to change notification settings - Fork 6.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(projection): Host Projection service (#1756)
- Loading branch information
Showing
8 changed files
with
237 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { } | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters