Rolando
Rolando

Reputation: 62674

How to programmatically dynamically create multiple independent instances of component in angular2?

I have a button component fancy-textbox. I want to make it such that users can dynamically add new fancy-textbox, but they have different labels above the textbox that is based on a scope variable that is unique to the fancy-textbox itself (or maybe from a parent scope variable that is not shared among all fancy-textboxes). How do I do this? Currently, I use this directly in my template showing and hiding, but I want to be able to "dynamically add more instances of this" programatically:

<div *ngIf="showTextBox">
    <fancy-textbox labelForBox="TextBox 1"></fancy-textbox>  
</div>

That is good, if fancy-textboxes are only created in the DOM in one specific area. However, what I want to do, is to be able to dynamically create components in different sections of the DOM.

    <div class="container">
<div class="navsection"><input id="classname" type="text"><button (click)="createFancyButton()">Create Fancy Button</button></div>
    <div class="topsection">
    </div>
    <div class="midsection">
    </div>
    <div class="botsection">
    </div>
<div class="footsection"></div></div>

Given the template above... assuming that users input the classname (e.g. botsection) into the textbox and hit the "createfancybutton button), I want "" "<fancy-button></fancy-button>" to be put into the appropriate section of the page, I want to be able to dynamically "create" instances of independent "fancy-button" within different sections of the page template. I could stick 3 ng-if statements with ng-for, though it seems impractical. Looking for a better alternative...

UPDATE: So the steps would be:

1) User enters "midsection" into textbox. 2) User clicks on the button "Create Fancy Button" 3) - The <fancybutton></fancybutton> component would be added under the div with the classname "midsection"-

The user can repeat clicking on the same "Create Fancy Button" button to create more under that. If user changes the input box to "topsection", then when user clicks on "Create Fancy Button", the fancybutton component would be added under div with "topsection".

If the user enters "newsection", then a new div with classname "newsection" would be created under the div with the classname "container", and fancybutton component would be added to the div with classname "newsection".

Upvotes: 8

Views: 3664

Answers (5)

Jeffrey Patterson
Jeffrey Patterson

Reputation: 2562

I don't believe you need any other scopes/components. Something like this should work.

component (TypeScript):

sections: {[key] : string[]} = {};

createFancyButton: (section: string) => {
    if (!this.sections[section]) {
        this.sections[section] = [];
    }
    this.sections[section].push('');
}

getSectionKeys: () => {
    return Object.keys(this.sections);
}

The "sections" property is an indexable object, which means it behaves like a hash table. Each property ("key") of the sections object is an array of strings, which represent the fancy button values (ngModel).

template (HTML)

<div class="container">
    <div class="navsection">
        <input #section type="text" placeholder="section" />
        <button (click)="createFancyButton(#section.value)">Create Fancy Button</button>
    </div>
    <div *ngFor="let key of getSectionKeys()" [class]="key">
        <fancy-textbox *ngFor="let textBox of sections[key]" [(ngModel)]="textBox"></fancy-textbox>
    </div>
</div>

There is a template reference variable (#section), which provide for easy access to a DOM element in the template. Then we *ngFor over the keys from the sections hashtable to create each section div. Finally we *ngFor over the array of strings for each section.

More information on template reference variables can be found here. https://angular.io/docs/ts/latest/guide/template-syntax.html#!#ref-vars

Note: There may be typos as I did not test this.

Upvotes: 1

VRPF
VRPF

Reputation: 3118

This is how you construct and add components programmatically:

import {ComponentRef, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';

@Component({
    selector: 'fancy-box',
    template: `<div>{{fancyContent}}</div> `,
})
export class FancyBox {

    fancyContent;

    doStuff() {
        console.log('done');
    }
}

@Component({
    selector: 'fancy-parent',
    template: `<div (click)="addNewFancyBox()">Add Box</div> `,
})
export class FancyParent {
    private counter = 0;

    constructor(
        private viewContainerRef: ViewContainerRef,
        private resolver: ComponentFactoryResolver) {
    }

    addNewFancyBox() {

        const factory = this.resolver.resolveComponentFactory(FancyBox);
        fancybox = this.viewContainerRef.createComponent(factory);

        const fancyboxElement = fancybox.instance as FancyBox;
        fancyboxElement.content = 'box number: ' + counter;

        fancyboxElement.doStuff();
        counter++;
    }
}

Upvotes: 0

Jagadeesh Govindaraj
Jagadeesh Govindaraj

Reputation: 8315

You need load component dynamically

Here is my solution

Parent.component.ts

import { Component, OnInit, ViewChild, ViewContainerRef, Input, ComponentFactoryResolver, ReflectiveInjector } from
    "@angular/core";

import { FancyButtonCompoent } from "../FancyButton.component";

@Component({
    moduleId: module.id,
    selector: "app-parent",
    templateUrl: "parent.component.html",
    styleUrls: ["parent.component.css"],
    entryComponents: [FancyButtonCompoent]

})
export class ParentCompoent {

    @ViewChild("midsection", { read: ViewContainerRef })
    midsectionContainer: ViewContainerRef;

    constructor(private resolver: ComponentFactoryResolver) {
    }

    createFancyButton() {
        //Name Is Fancybutton data binding property
        var yourdatavalues= {Name:'myname'}
        this.createDynamicbutton({
            input: yourdatavalues,
        });
    }
    //you can add your own model to get what you want like remove,move
     // var dynamiccompoent={Data:yourmodel,compoentcontainer:any}
     //fancybuttonarray:dynamiccompoent[];

 fancybuttonarray:any[];

    createDynamicbutton(elementData) {
        if (!elementData) {
            return;
        }

        // Inputs need to be in the following format to be resolved properly
        let inputProviders = Object.keys(elementData.inputs)
            .map((inputName) => { return { provide: inputName, useValue: elementData.inputs[inputName] }; });
        let resolvedInputs = ReflectiveInjector.resolve(inputProviders);

        // We create an injector out of the data we want to pass down and this components injector
        let injector = ReflectiveInjector
            .fromResolvedProviders(resolvedInputs, this.midsectionContainer.parentInjector);

        // We create a factory out of the component we want to create
        let factory = this.resolver.resolveComponentFactory(DefaultButtonComponent);

        // We create the component using the factory and the injector
        let component = factory.create(injector);

        this.midsectionContainer.insert(component.hostView)

         //your getting every instance of fancy button instance into array
         this.fancybuttonarray.push.(component )

         //if you want to clear elment if you wish
         //this.fancybuttonarray[0].destroy()
          //this.fancybuttonarray[1].destroy()


    }
}

parent.component.html

<div   class="row col-lg-12" >
    <div #midsection >

    </div>
</div>

Fancybutton.compoent.ts

    import { Component, OnInit, Injector } from '@angular/core';


@Component({
    moduleId: module.id,
    selector: 'Fancy-button',
    templateUrl: 'Fancybutton.component.html'
})
export class FancybuttonComponent {

    inputelement:yourDatamodel
    constructor(private injector: Injector) {
        this.inputElement = this.injector.get('inputElement');
    }

}

Fancybutton.compoent.html

    <div>
    <button title="inputelement.Name"(click)r ="alert(inputelement.Name)"></button>
    </div>    

Update

here is the nice post about load dynamically child component using angular2

Also Avilable Plunker example

Upvotes: 1

MoMo
MoMo

Reputation: 1876

Looking at this 'architecturally', and leveraging JS/TS OO class inheritance, I would personally consider adding a 'compound component' ('cc': follows an OO 'composite' pattern) to each section (eg: <top-section-cc-fancytextboxes...></> or <bot-section-cc-fancytextboxes...></>). These serve as 'place holders' for the various <fancy-textbox> types which could be zero or more instances inside each cc (compound component). Now each compound component derives/implements from a base class/interface component(eg: <base-section-cc-fancytextboxes>) which contains the base methods to manage adding multiple <fancy-textbox> types. The derived class methods, however, would 'know' where to add the proper <fancy-textbox>s (probably a *ngFor'd array as mentioned above). As for instantiating a specific type of <fancy-textbox>, perhaps leveraging a class factory pattern will be useful as well--probably an AJ2 service provider that returns instances and driven by the compound components.

Regardless, with AJ2's built in TS OO, and details notwithstanding, this is the vector I personally will use to solve this particular problem. This is just a thought.

Upvotes: 1

JB Nizet
JB Nizet

Reputation: 692023

Have an array of labels in your component.

Use *ngFor to iterate over this array and generate a fancy textbox for each of those labels.

To add a new fancy-textbox, add a new label to the array.

<fancy-textbox *ngFor="let label of labels" [labelForBox]="label"></fancy-textbox>  

And in the component:

labels: string[] = [];

// to add a fancy textbox:
this.labels.push('new label');

Upvotes: 1

Related Questions