0

Problem Statement: I'm working on one of the angular applications. In this application, I've 4 components named as below,

  1. common-component
  2. box-one-component
  3. box-two-component
  4. box-three-component

Now, the "common-component" is responsible for rendering the rest of the components inside it by iterating over an array from it's ".ts" file. So we have below array of object which gives us the selector name for those 3 components.

[
  {'componentName': "Box One", 'selector': "app-box-one"},
  {'componentName': "Box Two", 'selector': "app-box-two"},
  {'componentName': "Box Three", 'selector': "app-box-three"}
]

Now, I used ngFor to iterate over an array as below,

<div *ngFor="let com of components;">
  <com.selector> </com.selector>    // not giving selector name
</div>

Now, the problem is, <com.selector> </com.selector> statement is unable to get the selector name from an object and that is obvious because in angular we use string interpolation as {{com.selector}} to extract the data.

But, the problem is, that we can not use interpolation in the HTML Tag angel bracket. So, if I write something like <{{com.selector}}> </{{com.selector}}> then I get the error on the browser console window.

So, how can we extract the selector name in the HTML Tag angel bracket from each object that we are iterating over?

Expected Output after every iteration of for loop:

<div>
  <app-box-one> </app-box-one>
  // <app-box-two> </app-box-two>
  // <app-box-three> </app-box-three>
</div>
  • You can't do that (in the template). Go with [`ngSwitch`](https://angular.io/api/common/NgSwitch) instead. – Octavian Mărculescu Jul 14 '22 at 12:28
  • We use `ngSwitch` to check conditions. But, in this example, we don't want to check any condition instead we directly want to extract the selector name from every object that we iterate upon and want to give that selector name inside an angel bracket as ` `. – Rupesh Bharuka Jul 14 '22 at 12:37
  • I understood what you are trying to do, but it is not possible. You cannot dynamically render the component selector. If you could have done that, it would have been documented, and it is not. You can use ngSwitch or create the component you need using the [`ViewContainerRef.createComponent`](https://angular.io/api/core/ViewContainerRef#createComponent). For a starting point, take a look at [this SO question](https://stackoverflow.com/questions/70946038/replace-deprecated-angular-componentfactoryresolver-componentfactory) – Octavian Mărculescu Jul 14 '22 at 12:39
  • Oh, so you mean, I shall iterate over each object as is and use `ngSwitch` which will have a value of every selector, and inside it, I will have 3 switchCases which will be matched in every iteration and rendered. Right? If yes, what if tomorrow I get 100 objects in an array then I will have to write 100 switchCase statements. – Rupesh Bharuka Jul 14 '22 at 12:50
  • I appreciate your efforts in providing a solution but this doesn't seem to be a good solution. **In the 2nd scenario of 100 objects in an array, this will unnecessarily add noisy code to our template file.** – Rupesh Bharuka Jul 14 '22 at 12:52
  • Check out https://indepth.dev/posts/1400/components-by-selector-name-angular (h/t to Andentures in Angular podcast [312](https://topenddevs.com/podcasts/adventures-in-angular/episodes/components-by-selector-name-with-tarang-khandelwal-aia-312)) – JSmart523 Jul 15 '22 at 04:27

3 Answers3

0

It's not possible to do that the way you want. I would suggest you to change the architecture to something that uses just one component to represent all the boxes (parameterized by a state passed to the component using the @Input() decorator). Check the docs .

But if you still want to keep this architecture, a way to do this is to add the HTML elements directly to the DOM from the typescript of the parent component (inside ngAfterViewInit lifecycle hook). But I don't recommend this. Usually it's always possible to modify the architecture to make it with only one parameterized component. The only case when I modify the DOM from typescript is when I work with canvas.


UPDATE: After some research, I think the easiest way to add the components to the DOM at runtime is by using Angular's ViewContainerRef.

The typescript of the component that will load other components at runtime will be something like this:

import {Component, AfterViewInit, ViewContainerRef} from '@angular/core';

import {Child1Component} from './child1/child1.component';
import {Child2Component} from './child2/child2.component';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent implements AfterViewInit {
  components: any[] = [Child1Component, Child2Component];

  constructor(public viewContainerRef: ViewContainerRef) {}

  ngAfterViewInit() {
    this.loadComponent();
  }

  loadComponent(){
    this.viewContainerRef.clear();

    for(const component of this.components){
      this.viewContainerRef.createComponent<typeof component>(component);
    }
  }
}

You import the components you want to load dynamically, you create an array with them and you load them with ViewContainerRef.

For a full example, check the explanation here.

joaopfg
  • 1,227
  • 2
  • 9
  • 18
  • John, will it be good, if we pass `" "` as a selector name? This will potentially solve the problem. Because, now we only need string interpolation to extract the selector name. And this selector name itself is a HTML Tag. I hope you understand my point. – Rupesh Bharuka Jul 14 '22 at 12:59
  • So, the array should look something like, [ {'componentName': "box-one-component", 'selector': " "} ] – Rupesh Bharuka Jul 14 '22 at 13:01
  • @RupeshBharuka What are the differences between box-one-component, box-two-component, etc ? – joaopfg Jul 14 '22 at 13:03
  • @RupeshBharuka Can't they be abstracted into a more general component ? – joaopfg Jul 14 '22 at 13:04
  • John, I've given names as box-one, box-two etc. here to keep it simple. In the real application, these component names are different and every component has different data and design. – Rupesh Bharuka Jul 14 '22 at 13:06
  • I see. Then, I think the only solution is to add them dynamically to the DOM from the typescript of the parent component. How many of those components do you have ? – joaopfg Jul 14 '22 at 13:11
  • No, they can't be made general because they have their own specific data. So, the scenario is like this, **I select component names from the UI.** For Example, I have selected `"box-one"` and `"box-two"` then I need to load these two components into a **carousel**. So, carousel is a `common-component` that is responsible for rendering these 2 selected components. We pass an array to this carousel component so that it understands which components need to be rendered. – Rupesh Bharuka Jul 14 '22 at 13:12
  • Answer to your question, I've total 4 different components. The names of these components are displayed on the UI. Now, it depends on the user, how many names he want to select from the UI. Only those many components need to be rendered in the **Carousel**. **So, let's say we display 4 different names to user and user select on 2 names then we only need to load those 2 selected components in the carousel.** – Rupesh Bharuka Jul 14 '22 at 13:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/246435/discussion-between-john-doe-and-rupesh-bharuka). – joaopfg Jul 14 '22 at 13:19
  • **what do you think about changing the selector name to a full HTML tag name as discussed in the comment above?** – Rupesh Bharuka Jul 14 '22 at 13:20
  • Sorry for the intrusin, what about use multiple router-outlet? – Eliseo Jul 14 '22 at 20:41
0
You can execute for loop into one function and emit component name by using service subject variable, then subscribe it to all components, if the emitted component  name will be matched that time render that component 
    
component 1( where you want to render component one by one)
components = [
    { 'componentName': "box-one-component", 'selector': "app-box-one" },
    { 'componentName': "box-two-component", 'selector': "app-box-two" },
    { 'componentName': "box-three-component", 'selector': "app-box-three" }
  ]

  ngOnInit(): void {
    for (let comp of this.components) {
      this.service.currentComponent.next(comp.selector)
    }
  }

in service
  public currentComponent = new Subject();

in component app-box-one
public subscription: Subscription;
render: boolean = false // if render value is truthy that time you need to show html page
ngOnInit(): void {
    this.subscription = this.service.currentComponent.subscribe(name => {
      if (name == "app-box-one") {
        this.render = true;
      }
    })
}

ngOnDestroy(): void {
    if(this.subscription){
      this.subscription.unsubscribe()
    }
  }

do the same thing into another two
  • Dhruvil, your solution is literally confusing me. Because, what you've suggested is completely out of the context to my problem. – Rupesh Bharuka Jul 15 '22 at 14:57
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jul 17 '22 at 10:07
0

After giving thought and some research I came to a conclusion that I shall use the ngIf directive to keep the solution simple.

I've also checked solution of @John Doe and believe that if implemented that way should also work. But, since we've only 4 components to be rendered dynamically, using ViewContainerRef approach will unnecessarily make the solution complex by adding extra peace of code. For live example using ViewContainerRef approach visit https://angular.io/guide/dynamic-component-loader

Below solution using ngIf directive worked for me:

<div *ngFor="let com of components;">
    <ng-container *ngIf="com.componentName === 'Box One'">
        <app-box-one> </app-box-one>
    </ng-container>

    <ng-container *ngIf="com.componentName === 'Box Two'">
        <app-box-two> </app-box-two>
    </ng-container>

    <ng-container *ngIf="com.componentName === 'Box Three'">
        <app-box-three> </app-box-three>
    </ng-container>
</div>