1

I have a working code which injects any component via a service to the HTML:

ModalWindow.ts:

@Component({
  selector: 'modal-window'
  template: `
    <div class="modal-dialog" role="document">
        <div class="modal-content"><ng-content></ng-content></div>
    </div>
  `
})
export class ModalWindow {
}

Modalcontent.ts :

@Component({
  selector: 'modal-content'
  template: `
    I'm beeing opened as modal!
  `
})
export class ModalContent {
}

ModalService.ts :

/*1*/   @Injectable()
/*2*/   export class ModalService {
/*3*/     
/*4*/     constructor(private _appRef: ApplicationRef, private _cfr: ComponentFactoryResolver, private _injector: Injector) {
/*5*/     }
/*6*/     
/*7*/     open(content: any) {
/*8*/       const contentCmpFactory = this._cfr.resolveComponentFactory(content);
/*9*/       const windowCmpFactory = this._cfr.resolveComponentFactory(ModalWindow); 
/*10*/       
/*11*/       const contentCmpt = contentCmpFactory.create(this._injector);
/*12*/       const windowCmpt = windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement]]);
/*13*/       
/*14*/       document.querySelector('body').appendChild(windowCmpt.location.nativeElement);
/*15*/       
/*16*/       this._appRef.attachView(contentCmpt.hostView);
/*17*/       this._appRef.attachView(windowCmpt.hostView);
/*18*/     }
/*19*/   }

App.ts:

@Component({
  selector: 'my-app',
  template: `
    <button (click)="open()">Open modal window</button>
  `,
})

Result (when click a button which calls this service method ) :

enter image description here

I already know what contentCmpFactory and windowCmpFactory are (lines #8,9)

But I don't udnerstnad what's going on later. Regarding lines #11,#12 - the docs says "creates a new component".

Questions :

1 - line #12 : What does [[contentCmpt.location.nativeElement]] do ? (the docs says its type is projectableNodes?: any[][] - What do they mean ??)

2 - line #14 : What does [[windowCmpt.location.nativeElement]] do ?

3 - line #16,#17 : what and why do I need them if I already did appendChild ? (docs says : Attaches a view so that it will be dirty checked. - so ?).

PLUNKER

Royi Namir
  • 144,742
  • 138
  • 468
  • 792

1 Answers1

5

Answers:

1) Angular takes ComponentFactory and create component instance with given element injector and with array of projectable nodes

windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement]]);

1.1 Element Injector will be used when angular will resolve dependency

const value = startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);

Here is also simple illustration of dependency resolution algorithm for app without lazy loading. With lazy loading it will look a litte more complicated.

enter image description here

For more details see design doc element injector vs module injector

1.2 Projectable nodes are the node elements, which are "projected"(transcluded) in the ng-content that we have in the template of our component.

In order to project something our component template has to contain ng-content node.

@Component({
  selector: 'modal-window',
  template: `
    <div class="modal-dialog">
      <div class="modal-content">
        <ng-content></ng-content> // <== place for projection
      </div>
    </div>
  ` 
})
export class ModalWindow {

We can use component above in parent component template as follows:

<modal-window>
  <modal-content></modal-content>
  <div>Some other content</div>
</modal-window>

So our final result will look like:

<modal-window>
  <div class="modal-dialog">
    <div class="modal-content">
       <modal-content></modal-content> // our projectable nodes
       <div>Some other content</div>   // replaced ng-content
    </div>
  </div>
</modal-window>

So when we're passing projectable nodes to create method

windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement]]);

we do the same things as described above.

We'are getting reference (contentCmpt.location) to the host element of created early contentCmpt component. This is modal-content element. And then angular will do all magic to project it in ng-content place.

In example above i added one div

<modal-window>
  <modal-content></modal-content>
  <div>Some other content</div> <== here
</modal-window>

So the real code should looks like:

let div = document.createElement('div');
div.textContent = 'Some other content';
windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement, div]]);

In conclusion Why is projectableNodes an any[][]?

2) During the next line

document.querySelector('body').appendChild(windowCmpt.location.nativeElement);

we're getting reference to created in memory modal-window element. ComponentRef allows us to do this because it stores reference to the host element in location getter

export abstract class ComponentRef<C> {
  /**
   * Location of the Host Element of this Component Instance.
   */
  abstract get location(): ElementRef;

and then inseting it in document.body tag as last child. So we see it on the page.

3) Let's say our ModalContent has not just static content but will perform some operations for interaction.

@Component({
  selector: 'modal-content',
  template: `
    I'm beeing opened as modal! {{ counter }}
    <button (click)="counter = counter + 1">Increment</button>
  `
})
export class ModalContent {
  counter = 1;
}

If we remove

 this._appRef.attachView(contentCmpt.hostView);

then our view will not being updated during change detection cycle because we created view via ComponentFactory.create and our view is not part of any item in change detection tree (unlike creation via ViewContainerRef.createComponent). Angular opened API for such purposes and we can easily add view to root views https://github.com/angular/angular/blob/master/packages/core/src/application_ref.ts#L428 and after that our component will be updated during Application.tick https://github.com/angular/angular/blob/master/packages/core/src/application_ref.ts#L558

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • @yuruzi , but if I have two (named ` – Royi Namir Jun 14 '17 at 11:04
  • `[contentCmpt.location.nativeElement.children]` https://plnkr.co/edit/UnahRZEVa0diSxnKH3D8?p=preview – yurzui Jun 14 '17 at 11:15
  • @yuruzi , the focused part should be _under_ the line https://i.imgur.com/MXxKVsf.jpg , https://i.imgur.com/TUssTKp.jpg – Royi Namir Jun 14 '17 at 11:18
  • https://plnkr.co/edit/UHvScVvluGB3TiDKjGia?p=preview Seems in such cases we can't use `ng-content select` we should relay only on index of `ng-content` – yurzui Jun 14 '17 at 11:21
  • Thank you. _My goal is to create modal which can be injected with anything. ( I wanted to create one of my own)_ .The only thing I've left is to handle events from inside content to the outside world. I thought about creating Subject inside the injected component and subscribe to them from the main window. I'll try to do that ( and open a new question if I won't succeed.). - Is that the correct way of doing that ? – Royi Namir Jun 14 '17 at 11:24
  • I think service is good option. There are another ways to do that such CustomEvent, subscribing to Output event, method on parent component, passing callbacks, using injectors and so on but service is the best option. Maybe you can find something new here https://github.com/shlomiassaf/angular2-modal. – yurzui Jun 14 '17 at 11:32