8

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".

Rolando
  • 58,640
  • 98
  • 266
  • 407
  • Maybe you can just define `#target1`, `#target2` ... like ids wherever you want to dynamically insert your component by following this simple answer [How to place a dynamic component in a container](http://stackoverflow.com/a/37201171/5612697) – Ankit Singh Nov 16 '16 at 13:31

5 Answers5

1

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');
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • The problem is, I want fancy-textbox to show up in a few different parts of my HTML template. For example, there may be different specific nested divs that I want to insert fancy-textbox. – Rolando Oct 31 '16 at 19:53
  • I'm afraid it's not possible for me to help more with such a fague description of what you want to achieve. – JB Nizet Oct 31 '16 at 20:45
  • Updated OP. Should be a little clearer what I am looking for. – Rolando Nov 04 '16 at 22:05
  • I'm not sure I understand how the createFancyButton() decides in which of the three section to add the fancy button, but anyway, the solution is the same: have one array per section, one ngFor per section, and add the new label to the appropriate array. If you want to avoid repeating yourself, use ngFor to generate your three sections, or create a component that represents a section. – JB Nizet Nov 05 '16 at 07:26
  • createFancyButton() knows because of what is typed into the input box next to it. The one array per section may be the right way to go, but I am concerned that if I have too many "sections" it causes a lot of redundant code. I've added updated OP, and added the criteria: "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"." – Rolando Nov 05 '16 at 14:00
  • Well, as I said, use a loop for your sections, too. – JB Nizet Nov 05 '16 at 14:04
  • Could you show a plunkr example of the situation I am describing, I think it would help. – Rolando Nov 07 '16 at 22:17
1

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

Jagadeesh Govindaraj
  • 6,977
  • 6
  • 32
  • 52
  • if you want clear the genereated compoent. just add inserted compoents to array and clear them – Jagadeesh Govindaraj Nov 14 '16 at 12:53
  • ReflectiveInjector is deprecated. You can use Injector instead using " let injector = Injector.create({ providers: [resolvedInputs], parent: this.midsectionContainer.parentInjector, });` " – snorberhuis Aug 09 '18 at 12:48
1

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.

MoMo
  • 1,836
  • 1
  • 21
  • 38
  • 1
    This looks like the best solution @MoMo. Can you please try to create a plunker of it. It will of immense help to everyone. – rahulthakur319 Nov 17 '16 at 19:32
1

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.

Jeffrey Patterson
  • 2,342
  • 1
  • 13
  • 9
0

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++;
    }
}
VRPF
  • 3,118
  • 1
  • 14
  • 15