Pestañas dinámicas con componentes elegidos por el usuario

Resuelto Cuel asked hace 8 años • 3 respuestas

Estoy intentando configurar un sistema de pestañas que permita que los componentes se registren solos (con un título). La primera pestaña es como una bandeja de entrada, hay muchas acciones/elementos de enlace para elegir para los usuarios, y cada uno de estos clics debería poder crear una instancia de un nuevo componente al hacer clic. Las acciones/enlaces provienen de JSON.

El componente instanciado se registrará como una nueva pestaña.

¿No estoy seguro de si este es el "mejor" enfoque? Hasta ahora, las únicas guías que he visto son para pestañas estáticas, lo que no ayuda.

Hasta ahora, solo tengo el servicio de pestañas que se inicia en principal para persistir en toda la aplicación. Se parece a esto:

export interface ITab { title: string; }

@Injectable()
export class TabsService {
    private tabs = new Set<ITab>();

    addTab(title: string): ITab {
        let tab: ITab = { title };
        this.tabs.add(tab);
        return tab;
    }

    removeTab(tab: ITab) {
        this.tabs.delete(tab);
    }
}

Preguntas:

  1. ¿Cómo puedo tener una lista dinámica en la bandeja de entrada que cree pestañas nuevas (diferentes)? ¿ Estoy adivinando que DynamicComponentBuilderse usaría?
  2. ¿Cómo se pueden crear los componentes desde la bandeja de entrada (al hacer clic), registrarse como pestañas y también mostrarse? Supongo ng-content, pero no encuentro mucha información sobre cómo usarlo.

EDITAR: Un intento de aclarar.

Piense en la bandeja de entrada como una bandeja de entrada de correo. Los elementos se obtienen como JSON y muestra varios elementos. Una vez que se hace clic en uno de los elementos, se crea una nueva pestaña con el "tipo" de acción de ese elemento. El tipo es entonces un componente.

EDITAR 2: Imagen .

Cuel avatar Mar 31 '16 12:03 Cuel
Aceptado

actualizar

Ejemplo de Angular 5 StackBlitz

actualizar

ngComponentOutletfue agregado a 4.0.0-beta.3

actualizar

Hay un NgComponentOutlettrabajo en progreso que hace algo similar https://github.com/angular/angular/pull/11235

RC.7

Ejemplo de plomero RC.7

// Helper component to add dynamic components
@Component({
  selector: 'dcl-wrapper',
  template: `<div #target></div>`
})
export class DclWrapper {
  @ViewChild('target', {read: ViewContainerRef}) target: ViewContainerRef;
  @Input() type: Type<Component>;
  cmpRef: ComponentRef<Component>;
  private isViewInitialized:boolean = false;

  constructor(private componentFactoryResolver: ComponentFactoryResolver, private compiler: Compiler) {}

  updateComponent() {
    if(!this.isViewInitialized) {
      return;
    }
    if(this.cmpRef) {
      // when the `type` input changes we destroy a previously 
      // created component before creating the new one
      this.cmpRef.destroy();
    }

    let factory = this.componentFactoryResolver.resolveComponentFactory(this.type);
    this.cmpRef = this.target.createComponent(factory)
    // to access the created instance use
    // this.compRef.instance.someProperty = 'someValue';
    // this.compRef.instance.someOutput.subscribe(val => doSomething());
  }

  ngOnChanges() {
    this.updateComponent();
  }

  ngAfterViewInit() {
    this.isViewInitialized = true;
    this.updateComponent();  
  }

  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }    
  }
}

Ejemplo de uso

// Use dcl-wrapper component
@Component({
  selector: 'my-tabs',
  template: `
  <h2>Tabs</h2>
  <div *ngFor="let tab of tabs">
    <dcl-wrapper [type]="tab"></dcl-wrapper>
  </div>
`
})
export class Tabs {
  @Input() tabs;
}
@Component({
  selector: 'my-app',
  template: `
  <h2>Hello {{name}}</h2>
  <my-tabs [tabs]="types"></my-tabs>
`
})
export class App {
  // The list of components to create tabs from
  types = [C3, C1, C2, C3, C3, C1, C1];
}
@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App, DclWrapper, Tabs, C1, C2, C3],
  entryComponents: [C1, C2, C3],
  bootstrap: [ App ]
})
export class AppModule {}

Ver también angular.io CARGADOR DE COMPONENTES DINÁMICOS

versiones anteriores xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Esto cambió nuevamente en Angular2 RC.5

Actualizaré el ejemplo a continuación, pero es el último día antes de las vacaciones.

Este ejemplo de Plunker demuestra cómo crear componentes dinámicamente en RC.5

Actualización: utilice ViewContainerRef .createComponent()

Debido a DynamicComponentLoaderque está en desuso, el enfoque debe actualizarse nuevamente.

@Component({
  selector: 'dcl-wrapper',
  template: `<div #target></div>`
})
export class DclWrapper {
  @ViewChild('target', {read: ViewContainerRef}) target;
  @Input() type;
  cmpRef:ComponentRef;
  private isViewInitialized:boolean = false;

  constructor(private resolver: ComponentResolver) {}

  updateComponent() {
    if(!this.isViewInitialized) {
      return;
    }
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }
   this.resolver.resolveComponent(this.type).then((factory:ComponentFactory<any>) => {
      this.cmpRef = this.target.createComponent(factory)
      // to access the created instance use
      // this.compRef.instance.someProperty = 'someValue';
      // this.compRef.instance.someOutput.subscribe(val => doSomething());
    });
  }

  ngOnChanges() {
    this.updateComponent();
  }

  ngAfterViewInit() {
    this.isViewInitialized = true;
    this.updateComponent();  
  }

  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }    
  }
}

Ejemplo de Plunker RC.4
Ejemplo de Plunker beta.17

Actualización: use loadNextToLocation

export class DclWrapper {
  @ViewChild('target', {read: ViewContainerRef}) target;
  @Input() type;
  cmpRef:ComponentRef;
  private isViewInitialized:boolean = false;

  constructor(private dcl:DynamicComponentLoader) {}

  updateComponent() {
    // should be executed every time `type` changes but not before `ngAfterViewInit()` was called 
    // to have `target` initialized
    if(!this.isViewInitialized) {
      return;
    }
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }
    this.dcl.loadNextToLocation(this.type, this.target).then((cmpRef) => {
      this.cmpRef = cmpRef;
    });
  }

  ngOnChanges() {
    this.updateComponent();
  }

  ngAfterViewInit() {
    this.isViewInitialized = true;
    this.updateComponent();  
  }

  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }    
  }
}

Ejemplo de Plunker beta.17

original

A partir de su pregunta, no estoy del todo seguro de cuáles son sus requisitos, pero creo que esto debería hacer lo que desea.

El Tabscomponente obtiene una serie de tipos pasados ​​y crea "pestañas" para cada elemento de la matriz.

@Component({
  selector: 'dcl-wrapper',
  template: `<div #target></div>`
})
export class DclWrapper {
  constructor(private elRef:ElementRef, private dcl:DynamicComponentLoader) {}
  @Input() type;

  ngOnChanges() {
    if(this.cmpRef) {
      this.cmpRef.dispose();
    }
    this.dcl.loadIntoLocation(this.type, this.elRef, 'target').then((cmpRef) => {
      this.cmpRef = cmpRef;
    });
  }
}

@Component({
  selector: 'c1',
  template: `<h2>c1</h2>`

})
export class C1 {
}

@Component({
  selector: 'c2',
  template: `<h2>c2</h2>`

})
export class C2 {
}

@Component({
  selector: 'c3',
  template: `<h2>c3</h2>`

})
export class C3 {
}

@Component({
  selector: 'my-tabs',
  directives: [DclWrapper],
  template: `
  <h2>Tabs</h2>
  <div *ngFor="let tab of tabs">
    <dcl-wrapper [type]="tab"></dcl-wrapper>
  </div>
`
})
export class Tabs {
  @Input() tabs;
}


@Component({
  selector: 'my-app',
  directives: [Tabs]
  template: `
  <h2>Hello {{name}}</h2>
  <my-tabs [tabs]="types"></my-tabs>
`
})
export class App {
  types = [C3, C1, C2, C3, C3, C1, C1];
}

Ejemplo de Plunker beta.15 (no basado en su Plunker)

También hay una manera de pasar datos que se pueden pasar al componente creado dinámicamente como ( someDatadebería pasarse como type)

    this.dcl.loadIntoLocation(this.type, this.elRef, 'target').then((cmpRef) => {
  cmpRef.instance.someProperty = someData;
  this.cmpRef = cmpRef;
});

También hay cierto soporte para utilizar la inyección de dependencia con servicios compartidos.

Para obtener más detalles, consulte https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html

Günter Zöchbauer avatar Mar 31 '2016 06:03 Günter Zöchbauer

No soy lo suficientemente bueno para los comentarios. Arreglé el plunker de la respuesta aceptada para que funcione para rc2. Nada especial, los enlaces al CDN simplemente estaban rotos, eso es todo.

'@angular/core': {
  main: 'bundles/core.umd.js',
  defaultExtension: 'js'
},
'@angular/compiler': {
  main: 'bundles/compiler.umd.js',
  defaultExtension: 'js'
},
'@angular/common': {
  main: 'bundles/common.umd.js',
  defaultExtension: 'js'
},
'@angular/platform-browser-dynamic': {
  main: 'bundles/platform-browser-dynamic.umd.js',
  defaultExtension: 'js'
},
'@angular/platform-browser': {
  main: 'bundles/platform-browser.umd.js',
  defaultExtension: 'js'
},

https://plnkr.co/edit/kVJvI1vkzrLZJeRFsZuv?p=preview

davimusprime avatar Jun 20 '2016 20:06 davimusprime

hay un componente listo para usar (compatible con rc5) ng2-steps que se utiliza Compilerpara inyectar el componente en el contenedor de pasos y servicio para conectar todo (sincronización de datos)

    import { Directive , Input, OnInit, Compiler , ViewContainerRef } from '@angular/core';

import { StepsService } from './ng2-steps';

@Directive({
  selector:'[ng2-step]'
})
export class StepDirective implements OnInit{

  @Input('content') content:any;
  @Input('index') index:string;
  public instance;

  constructor(
    private compiler:Compiler,
    private viewContainerRef:ViewContainerRef,
    private sds:StepsService
  ){}

  ngOnInit(){
    //Magic!
    this.compiler.compileComponentAsync(this.content).then((cmpFactory)=>{
      const injector = this.viewContainerRef.injector;
      this.viewContainerRef.createComponent(cmpFactory, 0,  injector);
    });
  }

}
neuronet avatar Jul 29 '2016 13:07 neuronet