Guias dinâmicas com componentes escolhidos pelo clique do usuário

224

Estou tentando configurar um sistema de guias que permita que os componentes se registrem (com um título). A primeira guia é como uma caixa de entrada, há muitas ações / itens de link para escolher para os usuários, e cada um desses cliques deve poder instanciar um novo componente, ao clicar. As ações / links vêm do JSON.

O componente instanciado será registrado como uma nova guia.

Não tenho certeza se essa é a melhor abordagem? Até agora, os únicos guias que vi são para guias estáticas, o que não ajuda.

Até agora, só tenho o serviço de guias, que é iniciado principalmente para persistir em todo o aplicativo. Parece algo como isto:

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);
    }
}

Questões:

  1. Como posso ter uma lista dinâmica na caixa de entrada que cria novas guias (diferentes)? Estou meio que adivinhando o DynamicComponentBuilderque seria usado?
  2. Como os componentes podem ser criados a partir da caixa de entrada (no clique) se registram como guias e também são mostrados? Acho que ng-contentsim, mas não consigo encontrar muita informação sobre como usá-lo

EDIT: Uma tentativa de esclarecer.

Pense na caixa de entrada como uma caixa de correio. Os itens são buscados como JSON e exibem vários itens. Depois que um dos itens é clicado, uma nova guia é criada com a ação desses itens 'type'. O tipo é então um componente.

EDIT 2: Imagem .

Cuel
fonte
Se os componentes mostrados nas guias não forem conhecidos no momento da criação, o DCL é a abordagem correta.
Günter Zöchbauer 31/03
7
Eu não entendo sua exigência claramente tão difícil dizer qualquer coisa sem trabalhar com código / afunilador. Olhe isso, se ele pode ajudá-lo a algum lugar plnkr.co/edit/Ud1x10xee7BmtUaSAA2R?p=preview (Eu não sei se as suas relevantes ou não)
micronyks
@micronyks Eu acho que você tem o link errado
Cuel
Oi! Estou tentando fazer o que você pediu. Até agora, consegui criar a guia com conteúdo dinâmico, mas não achei uma maneira satisfatória de persistir o estado do componente quando a guia é alterada (os componentes carregados podem ser muito diferentes). Como você conseguiu isso?
Gipinani # 8/19

Respostas:

267

atualizar

Exemplo de Angular 5 StackBlitz

atualizar

ngComponentOutlet foi adicionado ao 4.0.0-beta.3

atualizar

Há um NgComponentOutlettrabalho em andamento que faz algo semelhante https://github.com/angular/angular/pull/11235

RC.7

Exemplo de desentupidor 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();
    }    
  }
}

Exemplo 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 {}

Veja também angular.io CARREGADOR DINÂMICO DE COMPONENTES

versões anteriores xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Isso mudou novamente no Angular2 RC.5

Vou atualizar o exemplo abaixo, mas é o último dia antes das férias.

Este exemplo do Plunker demonstra como criar componentes dinamicamente no RC.5

Atualização - use ViewContainerRef .createComponent ()

Como DynamicComponentLoaderfoi preterido, a abordagem precisa ser atualizada novamente.

@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();
    }    
  }
}

Exemplo de plunker
RC.4 Exemplo de plunker beta.17

Atualização - 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();
    }    
  }
}

Exemplo de desentupidor beta.17

original

Não tenho muita certeza da sua pergunta sobre quais são seus requisitos, mas acho que isso deve fazer o que você deseja.

O Tabscomponente obtém uma matriz dos tipos passados ​​e cria "guias" para cada item da 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];
}

Exemplo do Plunker beta.15 (não baseado no seu Plunker)

Também existe uma maneira de transmitir dados que podem ser transmitidos para o componente criado dinamicamente como ( someDataprecisaria ser transmitido como type)

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

Também há algum suporte para usar injeção de dependência com serviços compartilhados.

Para obter mais detalhes, consulte https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html

Günter Zöchbauer
fonte
1
Claro, você só precisa obter o tipo de componentes para DclWrappercriar uma instância real.
Günter Zöchbauer 31/03
1
@ Joseph Você pode injetar em ViewContainerRefvez de usar ViewChild, depois <dcl-wrapper>se torna o alvo. Os elementos são adicionados como irmãos do destino e, portanto, ficarão fora <dcl-wrapper>desse caminho.
Günter Zöchbauer
1
A substituição não é suportada. Você pode alterar o modelo para ''(string vazia) `e alterar o construtor para constructor(private target:ViewContainerRef) {}, e os componentes adicionados dinamicamente se tornam irmãos de<dcl-wrapper>
Günter Zöchbauer
1
Estou usando o RC4 e o exemplo foi bastante útil. A única coisa que eu queria mencionar é que tive que adicionar o código abaixo para que a ligação funcionasse corretamente this.cmpRef.changeDetectorRef.detectChanges ();
Rajee
4
Recebi um erro quando o componente dinâmico tinha outro componente dynaimc ao usar o ngAfterViewInit. Alterado para ngAfterContentInit em vez disso, e agora está trabalhando com componentes dinâmicos aninhados
Abris 28/10
20

Eu não sou legal o suficiente para comentários. Corrigi o plunker da resposta aceita para trabalhar para rc2. Nada extravagante, links para a CDN foram quebrados é tudo.

'@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
fonte
16

existe um componente pronto para uso (compatível com rc5) ng2-steps que usa Compilerpara injetar componente no container e serviço de etapa para conectar tudo (sincronização de dados)

    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
fonte