Angular structural directive: wrap the host element with some another component
Asked Answered
R

1

14

I have simplest Angular structural directive:

import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[hello]' })
export class HelloDirective {
  constructor(
    private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) {
      this.viewContainer.createEmbeddedView(this.templateRef);
  }
}

I use it this way:

<div *hello>Hello Directive</div>

It shows me "Hello Directive" message as expected. Now I want to change the content by wrapping it with some another component:

<my-component>Hello Directive</my-component>

And I want the directive to do it for me. I know that I can use a Component paradigm and create HelloComponent instead of HelloDirective and use ng-template etc with the template defined by template or templateUrl property on the @Component decorator... But is there an approach that could be used with a Directive paradigm to achieve such a result?

Rumery answered 18/11, 2017 at 3:18 Comment(4)
Do you want to transclude directive's contents into component?Donatus
@Donatus Yes, and then pass the result into the initial template instead of directive's content. So that it could be interpreted as <div *hello><my-component>Hello Directive</my-component></div> at the end (instead of <div *hello>Hello Directive</div>).Rumery
Does my-component contain ng-content?Donatus
@Donatus It could be fully customized to provide proper behavior. No limitations on my-component implementation in this task.Rumery
D
17

You can create component dynamically and pass projectable nodes to it. So it could look like

@Directive({ selector: '[hello]' })
export class HelloDirective implements OnInit, DoCheck {
  templateView: EmbeddedViewRef<any>;
  constructor(
      private templateRef: TemplateRef<any>,
      private viewContainer: ViewContainerRef,
      private resolver: ComponentFactoryResolver) {
  }

  ngOnInit() {
    this.templateView = this.templateRef.createEmbeddedView({});
    const compFactory = this.resolver.resolveComponentFactory(MyComponent);
    this.viewContainer.createComponent(
      compFactory, null, this.viewContainer.injector, [this.templateView.rootNodes])
  }

  ngDoCheck(): void {
    if (this.templateView) {
        this.templateView.detectChanges();
    }
  }
}

You have to add MyComponent to entryComponents array of your @NgModule

Complete example can be found on Stackblitz

See also

Donatus answered 18/11, 2017 at 4:59 Comment(4)
If I use an nested component inside hello directive, it doesn't work. Do you have a sugesstion to fix that: stackblitz.com/edit/angular-97ckey?embed=1&file=app/…Virtuosic
@MuratÇorlu Did you mean hello component didn't get input? That's because its view is not part of angular change detection tree. Try this stackblitz.com/edit/angular-jrtnmn?file=app/app.component.tsDonatus
Great, thanks. Another question is, now hello component in hello directive is initializing when directive is embedded the content, but not destroying when content is removed: stackblitz.com/edit/… do you also have a suggestion to destroy hello component when we remove content from dom?Virtuosic
@Donatus I have had a dx-text-box from devextreme, and code was not working, after I fixed the issue I updated your answer (since many developers copy paste and they want it to work out of the box)Ramonitaramos

© 2022 - 2024 — McMap. All rights reserved.