Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add new tabs with components using the 'name/selector of the component' programmatically using angular 2/4?

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.

like image 395
Rolando Avatar asked Mar 12 '18 22:03

Rolando


1 Answers

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.

like image 117
Zlatko Avatar answered Oct 22 '22 17:10

Zlatko