Pestañas dinámicas con componentes elegidos por clic del usuario

224

Estoy tratando de 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 ser capaz de instanciar un nuevo componente, al hacer clic. Las acciones / enlaces vienen 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 está inicializado en main 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 crea pestañas nuevas (diferentes)? ¿Estoy adivinando DynamicComponentBuilderque se 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 puedo encontrar 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 ese tipo de acción de elementos. El tipo es entonces un componente.

EDITAR 2: Imagen .

Cuel
fuente
Si los componentes que se muestran en las pestañas no se conocen en el momento de la compilación, entonces DCL es el enfoque correcto.
Günter Zöchbauer
77
No entiendo su requerimiento claramente tan difícil de decirle algo sin código de trabajo / plunker. Mira esto si te puede ayudar en algún lugar plnkr.co/edit/Ud1x10xee7BmtUaSAA2R?p=preview (no sé si es relevante o no)
micronyks
@micronyks Creo que te equivocaste de enlace
Cuel
¡Hola! Estoy tratando de hacer lo que me pediste. Hasta ahora logré crear la pestaña con contenido dinámico, pero no encontré una forma satisfactoria de persistir el estado del componente cuando se cambia la pestaña (los componentes cargados pueden ser muy diferentes). ¿Cómo lo lograste?
gipinani

Respuestas:

267

actualizar

Angular 5 StackBlitz ejemplo

actualizar

ngComponentOutlet fue 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 Plunker 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ÁMICO

versiones anteriores xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

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 muestra cómo crear dinámicamente componentes en RC.5

Actualización: use ViewContainerRef .createComponent ()

Debido a que DynamicComponentLoaderestá 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

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

El Tabscomponente obtiene una matriz 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 pueden pasarse al componente creado dinámicamente como ( someDatanecesitaría pasarse como type)

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

También hay algún soporte para usar la inyección de dependencia con servicios compartidos.

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

Günter Zöchbauer
fuente
1
Claro, solo necesita obtener el tipo de componentes DclWrapperpara que cree una instancia real.
Günter Zöchbauer
1
@Joseph Puedes inyectar en ViewContainerReflugar de usar ViewChild, luego <dcl-wrapper>se convierte en el objetivo. Los elementos se agregan como hermanos del objetivo y, por lo tanto, estarán fuera de <dcl-wrapper>esta manera.
Günter Zöchbauer
1
Reemplazar no es compatible. Puede cambiar la plantilla a ''(cadena vacía) `y cambiar el constructor a constructor(private target:ViewContainerRef) {}, luego los componentes agregados dinámicamente se convierten en hermanos de<dcl-wrapper>
Günter Zöchbauer
1
Estoy usando RC4 y el ejemplo fue bastante útil. Lo único que quería mencionar es que tuve que agregar el siguiente código para que el enlace funcione correctamente this.cmpRef.changeDetectorRef.detectChanges ();
Rajee
44
Recibí un error cuando el componente dinámico tenía otro componente dynaimc cuando usaba ngAfterViewInit. En su lugar, cambió a ngAfterContentInit y ahora funciona con componentes dinámicos anidados
Abris
20

No soy lo suficientemente bueno para los comentarios. Arreglé el plunker de la respuesta aceptada para trabajar para rc2. Nada de lujos, los enlaces a la CDN se rompieron, 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
fuente
16

hay componentes listos para usar (compatibles con rc5) ng2-steps que se usan Compilerpara inyectar componentes en el contenedor de pasos y el 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);
    });
  }

}
neuroneta
fuente