2

I am creating a dynamic tab based components and passing the necessary component properties to build the component through an object.

How to pass @Input as a parameter and then use the input with ComponentFactoryResolver and create inputs.

Here is the object to hold the necessary Tab Component Properties,

Here is the StackBlitz

tab-item.ts

import { Type, Input } from '@angular/core';

export class TabItem {

    componentLoaded = false;

    constructor(public title: string, public active: boolean, public component: Type<object>, public inputs: Input[] = []) {

    }
}

tab-component.ts

import { Component, OnInit, Input, ComponentFactoryResolver, ComponentFactory, Injector, ApplicationRef, ElementRef } from '@angular/core';
import { TabItem } from 'src/app/models/tab-item';

@Component({
    selector: 'app-tab',
    template: `
              <div class="row">
                <div class="col-2">
                   <ul class="nav flex-column">
                      <li class="nav-item" *ngFor="let tab of tabItems">
                         <a class="nav-link" (click)="setActiveTabItem(tab)" [class.active]="tab.active" [id]="tab.title+'-tab'"
                           data-toggle="tab" [href]="'#' + tab.title" role="tab" [attr.aria-controls]="tab.title"
                           aria-selected="true">{{tab.title}}</a>
                      </li>
                  </ul>
              </div>
             <div class="col-10">
                <div id="tab-container" class="tab-content">
               </div>
             </div>
           </div>`,
    styleUrls: ['./tab.component.scss']
})
export class TabComponent implements OnInit {

    @Input() tabItems: TabItem[];
    componentsCreated: ComponentFactory<object>[] = [];

    constructor(private componentFactorResolver: ComponentFactoryResolver, private injector: Injector,
                private app: ApplicationRef,
                private elementRef: ElementRef) { }

    loadTabContent(selectedIndex: number, isHome: boolean = false): void {
        const tabItem = this.tabItems[selectedIndex];
        const componentFactory = 
        this.componentFactorResolver.resolveComponentFactory(tabItem.component);

        // Assign Inputs
        if (tabItem.inputs) {
            tabItem.inputs.forEach((item: Input) => {
                 componentFactory.inputs.push(item);
            });
        }

        const newNode = document.createElement('div');
        newNode.id = tabItem.title;
        document.getElementById('tab-container').appendChild(newNode);

        const ref = componentFactory.create(this.injector, [], newNode);
        this.app.attachView(ref.hostView);
    }
}

Here, in the above code, I am getting the below error with componentFactory.inputs.push(item);

Argument of type 'Input' is not assignable to parameter of type '{ propName: string; templateName: string; }'.

Type 'Input' is missing the following properties from type '{ propName: string; templateName: string; }': propName, templateNamets(2345)

Finally, Creating tab items like below and passing it from the parent component

<app-tab [tabItems]="tabItems"></app-tab>


@Input() header = 'Test';
tabItems = [
            new TabItem('Home', true, HomeComponent),
            new TabItem('Profile', false, ProfileComponent),
            new TabItem('Employee', false,  EmployeeComponent, [this.header])
        ];

Here, the error is,

Type 'string' has no properties in common with type 'Input'.ts(2559)

Is this is the right way to pass inputs? If so, what's wrong with this?

Amirhossein Mehrvarzi
  • 18,024
  • 7
  • 45
  • 70
Hary
  • 5,690
  • 7
  • 42
  • 79

2 Answers2

1

I made it to work with the below changes. Still looking for the best approach if any.

Used rest operator for arguments without specifying the Input type

import { Type, Input } from '@angular/core';

export class TabItem {

    componentLoaded = false;
    args;

    constructor(public title: string, public active: boolean, public component: Type<object>, ...args) {

    }
}

tab-component.ts

loadTabContent(selectedIndex: number, isHome: boolean = false): void {
    const tabItem = this.tabItems[selectedIndex];
    const componentFactory = 
    this.componentFactorResolver.resolveComponentFactory(tabItem.component);

    const newNode = document.createElement('div');
    newNode.id = tabItem.title;
    document.getElementById('tab-container').appendChild(newNode);

    const ref = componentFactory.create(this.injector, [], newNode);
    this.app.attachView(ref.hostView);

    if (tabItem.args) {
        tabItem.args.forEach((item) => {
             Object.keys(item).forEach(key => {
                 const value = item[key];
                 
                 ref.instance[key] = value; //Here it works!
             });
        });
    }

}

When created, here it is no need to be of Input type and just a key:value pair would suffice

<app-tab [tabItems]="tabItems"></app-tab>

tabItems = [
            new TabItem('Home', true, HomeComponent),
            new TabItem('Employee', false,  EmployeeComponent, {'header': 'Test data'})
        ];
Hary
  • 5,690
  • 7
  • 42
  • 79
-1

According to ComponentFactory docs it has an inputs property which expects Array of { propName: string; templateName: string; }.

Your problem comes from 2 violations :

  1. Inside app.component.ts where you try to consider @Input directive as Input type while you set it as string!

  2. Inside tab.component.ts where you are trying to push an Input as type of ComponentFactory's inputs!

My changes in Stackblitz may guide you to write a better code.

Amirhossein Mehrvarzi
  • 18,024
  • 7
  • 45
  • 70