Search code examples
angularangular-dynamic-components

Pass @Input as Parameters fires Type 'string' has no properties in common with type


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?


Solution

  • 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'})
            ];