|
| 1 | +import {Injectable, Directive, ModuleWithProviders, NgModule, ElementRef} from '@angular/core'; |
| 2 | + |
| 3 | + |
| 4 | +// "Polyfill" for `Node.replaceWith()`. |
| 5 | +// cf. https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/replaceWith |
| 6 | +function _replaceWith(toReplaceEl: HTMLElement, otherEl: HTMLElement) { |
| 7 | + toReplaceEl.parentElement.replaceChild(otherEl, toReplaceEl); |
| 8 | +} |
| 9 | + |
| 10 | + |
| 11 | +@Directive({ |
| 12 | + selector: 'dom-projection-host' |
| 13 | +}) |
| 14 | +export class DomProjectionHost { |
| 15 | + constructor(public ref: ElementRef) {} |
| 16 | +} |
| 17 | + |
| 18 | + |
| 19 | +@Injectable() |
| 20 | +export class DomProjection { |
| 21 | + /** |
| 22 | + * Project an element into a host element. |
| 23 | + * Replace a host element by another element. This also replaces the children of the element |
| 24 | + * by the children of the host. |
| 25 | + * |
| 26 | + * It should be used like this: |
| 27 | + * |
| 28 | + * ``` |
| 29 | + * @Component({ |
| 30 | + * template: `<div> |
| 31 | + * <dom-projection-host> |
| 32 | + * <div>other</div> |
| 33 | + * <ng-content></ng-content> |
| 34 | + * </dom-projection-host> |
| 35 | + * </div>` |
| 36 | + * }) |
| 37 | + * class Cmpt { |
| 38 | + * constructor(private _projector: DomProjection, private _el: ElementRef) {} |
| 39 | + * ngOnInit() { this._projector.project(this._el, this._projector); } |
| 40 | + * } |
| 41 | + * ``` |
| 42 | + * |
| 43 | + * This component will move the content of the element it's applied to in the outer div. Because |
| 44 | + * `project()` also move the children of the host inside the projected element, the element will |
| 45 | + * contain the `<div>other</div>` HTML as well as its own children. |
| 46 | + * |
| 47 | + * Note: without `<ng-content></ng-content>` the projection will project an empty element. |
| 48 | + */ |
| 49 | + project(ref: ElementRef, host: DomProjectionHost): void { |
| 50 | + const projectedEl = ref.nativeElement; |
| 51 | + const hostEl = host.ref.nativeElement; |
| 52 | + const childNodes = projectedEl.childNodes; |
| 53 | + let child = childNodes[0]; |
| 54 | + |
| 55 | + // We hoist all of the projected element's children out into the projected elements position |
| 56 | + // because we *only* want to move the projected element and not its children. |
| 57 | + _replaceWith(projectedEl, child); |
| 58 | + let l = childNodes.length; |
| 59 | + while (l--) { |
| 60 | + child.parentNode.insertBefore(childNodes[0], child.nextSibling); |
| 61 | + child = child.nextSibling; // nextSibling is now the childNodes[0]. |
| 62 | + } |
| 63 | + |
| 64 | + // Insert all host children under the projectedEl, then replace host by component. |
| 65 | + l = hostEl.childNodes.length; |
| 66 | + while (l--) { |
| 67 | + projectedEl.appendChild(hostEl.childNodes[0]); |
| 68 | + } |
| 69 | + _replaceWith(hostEl, projectedEl); |
| 70 | + |
| 71 | + // At this point the host is replaced by the component. Nothing else to be done. |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | + |
| 76 | +@NgModule({ |
| 77 | + exports: [DomProjectionHost], |
| 78 | + declarations: [DomProjectionHost], |
| 79 | +}) |
| 80 | +export class ProjectionModule { |
| 81 | + static forRoot(): ModuleWithProviders { |
| 82 | + return { |
| 83 | + ngModule: ProjectionModule, |
| 84 | + providers: [DomProjection] |
| 85 | + }; |
| 86 | + } |
| 87 | +} |
0 commit comments