2

I want to make a structural directive named showWrapperIf, which either keeps the whole element and children it sits on, or, when the condition is true, removes the element (wrapper) it sits on, and takes all the children and keep it in the DOM.

So, the result of this

<div *showWrapperIf="true">
    <h2></h2>
    <p></p>
    <any-component></any-component>
</div>

would be

<div>
    <h2></h2>
    <p></p>
    <any-component></any-component>
</div>

but, if the condition is false, only the children need to be displayed. E.g:

<h2></h2>
<p></p>
<any-component></any-component>

StackBlitz demo

I know I can do it with <ng-template> and *ngIf, but the wrapper has lots of children, it would double the size of the file and make it less readable.

nash11
  • 8,220
  • 3
  • 19
  • 55
MCFreddie777
  • 1,069
  • 1
  • 12
  • 21

2 Answers2

2

This approach seems to be too complicated because your content also contains your own components. Instead, you could move this logic to a separate component which would make your code neat and concise. Create a component, say conditional-wrapper, and add it to your module. In the component just place a basic Input to accept the wrapperElement from the parent.

@Input() wrapperElement: boolean;

In the template, just add code to conditionally show any element as a wrapper or no wrapper as the default using a ngSwitch.

<ng-container [ngSwitch]="wrapperElement">
    <div *ngSwitchCase="'div'">
        <ng-container *ngTemplateOutlet="noWrapper"></ng-container>
    </div>
    <h2 *ngSwitchCase="'h2'">
        <ng-container *ngTemplateOutlet="noWrapper"></ng-container>
    </h2>
    <ng-container *ngSwitchDefault>
        <ng-container *ngTemplateOutlet="noWrapper"></ng-container>
    </ng-container>
</ng-container>

<ng-template #noWrapper>
    <ng-content></ng-content>
</ng-template>

Now all you need to is recursively show conditional-wrapper from the parent component and the ng-content will show project the data from your parent component to the child.

<conditional-wrapper [wrapperElement]="condition ? 'div': ''">
    <h2>Child 1</h2>
    <p>Child 2</p>
    <footer>Child 3</footer>
    <conditional-wrapper [wrapperElement]="condition ? 'div': ''">
        <p>Nested Child 4</p>
    </conditional-wrapper>
</conditional-wrapper>

Here is a working example on StackBlitz.

nash11
  • 8,220
  • 3
  • 19
  • 55
  • This looks like closest solution. The problem is - that in real project (can't share company stuff), it is a "bit" more complicated =D. Using `ng-template` also changes the context, and therefore we need to pass all the variables used in that template, while using the directive would only delete the parent and in that place render its children. There was an `replace` option in AngularJS, but it was and is deprecated, so it didn't make into Angular2+. Also, coming up with naming of the templates is not fun, and using multiple templates when you have nested multiple times - its not fun at all :D – MCFreddie777 Sep 28 '19 at 21:56
  • How do you pass a context to the `else` branch? – MCFreddie777 Sep 28 '19 at 22:14
  • @MCFreddie777 - I have updated my answer to a more neater solution. – nash11 Sep 28 '19 at 23:56
  • Using the extra component is the approach we want to avoid, because of functions, and whole context you need to pass down to that `conditional-wrapper`. Furthermore, the `div` is hard-coded in there, therefore it is not reusable. – MCFreddie777 Sep 29 '19 at 08:10
  • That context issue shouldn't be there anymore since each `conditional-wrapper` creates its own instance and just projects the content so the data will really be controlled by the parent component itself. The `div` issue can be worked around using `ngSwitch`. I have updated my answer and StackBlitz to show the same. – nash11 Sep 29 '19 at 11:47
  • Okay, it's the closest we can get - I don't blame you, but it won't work if you want that wrapper with other attributes, classes and data- attributes. I'll stick with ngIf and ng templates. Will look messy a bit but it's the furthest we can get. It's jus a pity it's not possible to do with structural directive. – MCFreddie777 Sep 29 '19 at 11:50
  • I just came across a similar question and the [answer](https://stackoverflow.com/a/41727533/9739129) seems to have a pre-Angular 4 structural directive. Maybe that could help you out but I'm not sure how it works with custom components. – nash11 Sep 29 '19 at 12:06
1

I don't think you can do that. Please note that when the well documented angular way does not suffice, you might be heading into a wrong direction!

Anyway, something that might get close to what you need (I think ) is to add a class on the host element!

You can do that as follows

this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef);

if (this.condition) {
  const elementRef = (this.viewContainer.get(0) as any)
     .rootNodes[0] as ElementRef;
  this.renderer.addClass(elementRef, "myclass");
}

stackblitz

and you can do the rest with css

Jeanluca Scaljeri
  • 26,343
  • 56
  • 205
  • 333
  • I need to get rid of the element wrapping it. – MCFreddie777 Sep 28 '19 at 19:15
  • Why not simply use `ngTemplate` with `*ngIf`: https://stackblitz.com/edit/angular-a6rbwr Probably a better solution than the one in my answer :) – Jeanluca Scaljeri Sep 28 '19 at 19:34
  • Because not all projects are that simple, and ngTemplates - whether using ngIf or ngSwitch would double/triple the amount of HTML and we want to keep our codebase clear. (look at the html: https://stackblitz.com/edit/angular-grydzz?file=src%2Fapp%2Fapp.component.html) – MCFreddie777 Sep 28 '19 at 19:36
  • But doesn't my stackblitz with `*ngIf` does exactly what you need? It doesn't create more HTML – Jeanluca Scaljeri Sep 28 '19 at 19:39
  • Well it does, if you have three line HTML code :) Having 100 lines inside the wrapper you want to get rid of would using ng-template double the code. And the problem is, if you want to reuse it, and nest it - just look at my HTML, and you'll know. – MCFreddie777 Sep 28 '19 at 19:41
  • The cleanest solution would be directive via componentFactory and creating it somehow. Do you know how to do it this way? – MCFreddie777 Sep 28 '19 at 19:42
  • 1
    That will make you code complex, and I would go for readability over less html/complexity, and the componentFactory is not solving you issue (I think). Anyway, I've a last simple solution, using a component: https://stackblitz.com/edit/angular-grydzz?file=src%2Fapp%2Fapp.component.html Your main component still has less html and is super readable, I think :) – Jeanluca Scaljeri Sep 28 '19 at 19:58
  • 1
    Sorry, incorrect stackblitz, the [component stackblitz](https://stackblitz.com/edit/angular-1ddqu3?file=src%2Fapp%2Fshared%2Fcomponents%2Fbarfoo%2Fbarfoo.component.html) – Jeanluca Scaljeri Sep 28 '19 at 20:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/200100/discussion-between-mcfreddie777-and-jeanluca-scaljeri). – MCFreddie777 Sep 28 '19 at 20:27
  • Your requirements go beyond my knowledge! I did found an interesting question [here](https://stackoverflow.com/questions/35733699/angular2-replace-host-element-with-components-template) with interesting solution to what you need. I checked two of them [here](https://stackblitz.com/edit/angular-sahuh9?file=src%2Fapp%2Fmy-items.component.ts) and [here](https://stackblitz.com/edit/angular-9mpvve?file=src%2Fapp%2Fmy-item.component.ts) – Jeanluca Scaljeri Sep 29 '19 at 08:03