-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(projection): Host Projection service #1756
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment needs some expansion; the key thing to communicate is that it's only the given element that is projected and not its content (children). Also worth mention when you might want to use this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.