7
votes

I am using material tabs: https://material.angular.io/components/tabs/overview

I have a page with a tab view that I want to use to populate one custom component per tab with a click of the button. Assume I have two components with the selectors "customcomponent1" and "customcomponent2". I want to make it such that when I click a button, a new 'object' will be added to the list like the following:

{'component_name':'customcomponent1'}

and a new tab "Tab2" will dynamically be created with that component inside of it is if it were like this:

  <mat-tab-group>
    <mat-tab label="Tab1">
<button (click)="createNewTabWithComponent('customcomponent1')"></button>
    </mat-tab>
    <mat-tab label="Tab2">
       <customcomponent1></customcomponent1>
    </mat-tab>
  </mat-tab-group>

If I click on the button again... I get a new tab:

<mat-tab-group>
    <mat-tab label="Tab1">
<button (click)="createNewTabWithComponent('customcomponent1')"></button>
    </mat-tab>
    <mat-tab label="Tab2">
       <customcomponent1></customcomponent1>
    </mat-tab>
    <mat-tab label="Tab3">
       <customcomponent2></customcomponent2>
    </mat-tab>
  </mat-tab-group>

How do I accomplish this with angular2/4? I normally would use something like $compile, but i dont think angular2/4 has one. What I want to avoid is creating a bunch of tabs for each component (imagine I have 10 components, really don't want to create multiple mat-tab placeholders for every single component and setting a show/hide flag on each one) and 'hardcoding' the one component inside of it.

This isn't really a problem I think that is specific to tabs, if the solution shows how to add a component 'dynamically' using the name of the selector (that a user types in a textbox and hits a button to 'add to a list', that would be a considered answer too.

A sample I can think of is if there is a text box, and somebody can type in any string. If a string matches the name of a component, then that component programatically 'dynamically shown' on the screen as if it were part of the original template.

Pseudocode of what I am looking for that I know does not exist (i think, but it would be nice if it did):

<button (click)="addNameOfComponentToListOfCustomComponentStrings('text-from-a-textbox')">Add Component</button>

<div *ngFor="let string_name_of_component in listofcustomcomponentstrings">
    <{{string_name_of_component}}><</{{string_name_of_component}}>
</div>

Getting them into tabs would be a plus. If this is not doable, please post. Otherwise, please post.

If there is maybe a workaround that could be done using some 'nested angular routing' where it pulls up the component based on the name of the component in ngFor, that may work too... am open to any ideas.

3
Have you seen this: angular.io/guide/dynamic-component-loader It is far from what you are asking for, but AFAIK, the only way to dynamically load components right now.DeborahK
that a user types in a textbox so the user must type in the name of an existing component that you have coded? I'm unclear on what it is you are really trying to accomplish. (I do my tabs with routing, but not "user generated" tabs)DeborahK
I found this, which looked interesting but would not work in production: stackoverflow.com/questions/47096635/…DeborahK
“that a user types in a textbox”... what is typed in the text box is the string that matches the name of a components selector that is in your app.Rolando
If you or someone can show how this 'dynamic-component-loader' works for the case I am looking for, that would be acceptable too. If possible, I would like to avoid creating a bunch of 'ngIf' tabs that have the components hardcoded in them... I think of Angular1's $compile function that works very well...Rolando

3 Answers

1
votes

For your stated problem (not wanting to have 10 placeholder mat-tabs with *ngIf to hide stuff), you could do much better with routing and lazy-loading.

Simple navigation: fixed lazy-loaded paths.

Say, your page with tabs is at /tabs and module with it is MyTabsModule. Your route config for that module looks like this:

const routes = [{
  path: 'tabs',
  component: MyTabsComponent,
}];

Let's say you have two tabs, left and right. What you need now is to add child components in lazy modules. Something as simple as:

const routes = [{
  path: 'tabs',
  component: MyTabsComponent,
  children: [
    {
      path: 'left',
      loadChildren: 'app/lazy/left.module#LazyLeftModule'
    },
    {
      path: 'right',
      loadChildren: 'app/lazy/right.module#LazyRightModule'
    }
  ]
}];

Your MyTabsComponent needs to show this somehow, how? Here's what Material docs say (simplified):

<h2>My tabs component</h2>
<nav mat-tab-nav-bar>
  <a mat-tab-link routerLink="left">Left tab</a>
  <a mat-tab-link routerLink="right">Right tab</a>
</nav>

<router-outlet></router-outlet>

But what if you have multiple tabs? Well, the docs say this, I simplified the previous example:

<a mat-tab-link
  *ngFor="let link of navLinks"
  routerLinkActive #rla="routerLinkActive"
  [routerLink]="link.path"
  [active]="rla.isActive">
    {{link.label}}
</a>

So now, you can create several lazy components, and the route config for those lazy routes. In fact, you could probably even create a script that generates1 your route config, and even those lazy modules for you as part of build.

1 - Angular Schematics?

That is assuming that you know what components you have. What if you just wanna let the user type into that textbox of yours? And pick any component on their own? Like, have a dropdown, and let the user decide all the tabs that they want and then on top of that, load them lazily?

Textbox navigation: parametrized children and wrapper component.

How does that go? First, your route children are a bit different now:

{
  path: 'tabs',
  component: MyTabsComponent,
  children: [{
    path: ':tab',
    loadChildren: 'app/lazy/lazy.module#LazyWrapperModule',
  }
}

Now, your LazyWrapperModule exports a LazyWrapperComponent. This component has it's own local router-outlet in the template. It also loads a component based on the url:

constructor(private route: ActivatedRoute, private router: Router) {}
ngOnInit() {
  this.activatedRoute.params.subscribe(params => {
    const tab = params.tab;
    if (!this.isRouteValid(tab)) {
      return this.router.navigate(['error'])
    }
    this.router.navigate([tab]);
  });
}

And the LazyWrapperModule also has router config:

@NgModule({
  imports: [
    RouterModule.forChild([{
      path: 'tab1',
      component: Tab1
    },
    {
      path: 'tab2',
      component: Tab2
    },
    ...
    {
      path: 'tab100',
      component: Tab100
    }]
  ],
  ...

Now, this looks better. You can, from your TabsModule navigate to anything. Then it loads a component by a parameter. Your component can, e.g. show error tab if the user enters the wrong tab, or you can simply provide typeahead with the list of "allowed" tabs, etc etc. Fun stuff!

But what if you don't want to restrict the user? You wanna let them type "my-most-dynamic-component-ever" in that textbox?

Dynamic all: dynamically creating components

You can simply create a component dynamically. Again have a wrapper component which creates and injects the component. E.g. template:

<input [(ngModel)]="name" required>
<textrea [(ngModel)]="template">
<button (click)="createTemplate()">Create template</button>

<div #target></div>

Then the component can be something like:

class MyTabsComponent {
  @ViewChild('target') target;
  name = 'TempComponent';
  template: '<span class="red">Change me!</span>';
  styles: ['.red { border: 1px solid red; }']

  constructor(private compiler: Compiler,
              private injector: Injector,
              private moduleRef: NgModuleRef<any>) {
  }

  createTemplate() {
    const TempComponent = Component({ this.template, this.styles})(class {});
    const TempModule = NgModule({
      declarations: [TempComponent]
    })(class {});

  this.compiler.compileModuleAndAllComponentsAsync(TempModule)
    .then((factories) => {
      const f = factories.componentFactories[0];
      const cmpRef = f.create(this.injector, [], null, this.m);
      cmpRef.instance.name = this.name;
      this.target.insert(cmpRef.hostView);
    });
  }
... // other stuff
}

Now, how specifically to use it, depends on your specific need. E.g. for homework, you can try dynamically creating component like this above, and injecting it into the <mat-tab-group> more above. Or, dynamically create a route to an existing component as a lazily-loaded link. Or... posibilities. We love them.

0
votes

This sample to use dynamic-component-loader.

In your case, you need create module tab own, don't use <mat-tab-group> insteand. Since <mat-tab> compiled by MatTabsModule, you cannot embed view to <mat-tab-group>.

example this code will not work

<mat-tab-group>
   <template>
    <!-- dynamic -->
   </template>
</mat-tab-group>

what you need is, create new component for your own tab and use viewContainerRef.createComponent to create component.

if you see my example (dynamic-component-loader), I put template like this:

  <mat-card>
    <mat-card-content>
      <h2 class="example-h2">Tabs with text labels</h2>
      <mat-tab-group class="demo-tab-group">
        <mat-tab label="Tab 1">
        hallow
        </mat-tab>
      </mat-tab-group>
      <ng-template live-comp></ng-template>
    </mat-card-content>
  </mat-card>

I put <ng-template live-comp></ng-template> under <mat-card-content> not inside of <mat-tab-group> because <mat-tab> compiled by MatTabsModule.

0
votes

Note This is an example from how I use this, for a way more complex application with dynamic data and multiple for loops and multiple childs of childs components (layer). Hope it helps.

I do this aswell but then with a loop and a child component using [data]="data" to set it.

So what you want to do is set the tab programmatic with:

[(selectedIndex)]="selectedTab" (selectedTabChange)="tabChanged($event)"

This will ensure that if your tab change this.selectedIndex will be your current tab. So what you need is now is a mat-tab with a loop. In your template:

<mat-tab-group [(selectedIndex)]="selectedTab" (selectedTabChange)="tabChanged($event)">
   <div *ngFor="let tab of tabs; let j = index">
     <mat-tab label={{tab.name}}>
       <div *ngIf="selectedIndex === j"> //To ensure your component is not loaded in the dom when it's not selected.
         <customcomponent [data]="data"></customcomponent>
       <div>
      </mat-tab>
    <div>
    <mat-tab label="...">
      // can put input fields here or even a button. Up to you. selectedTabChange not really have to change then.
   </mat-tab>
 </mat-tab-group>

Now you want to add an extra tab if you click on ...

In your component:

private tabChanged(_event: any) {
    let index = _event.index;
    if(index < this.tabs.length){
      this.data = this.tabs[index].data; // set data first before showing the component (*ngIf).
      this.selectedIndex = index;
    if(index === this.tabs.length){
      this.tabs.push({name: newName, data: yourData});
      this.selectedIndex = this.tabs.length - 1; // length will be 2 now so new tab will be 1 in this tabs.
    }

Now you are able to make a new tab with a name and the data that's needed for the component. Now you can add ngOnChanges on the customcomponent and fetch the data with @Input() data: any;. What you do there is up to you.