Onglets dynamiques avec les composants choisis par l'utilisateur

224

J'essaie de configurer un système d'onglets qui permet aux composants de s'enregistrer eux-mêmes (avec un titre). Le premier onglet est comme une boîte de réception, il existe de nombreuses actions / éléments de lien parmi lesquels choisir pour les utilisateurs, et chacun de ces clics devrait pouvoir instancier un nouveau composant, au clic. Les actions / liens proviennent de JSON.

Le composant instancié s'enregistrera alors comme un nouvel onglet.

Je ne sais pas si c'est la «meilleure» approche? Jusqu'à présent, les seuls guides que j'ai vus concernent les onglets statiques, ce qui n'aide pas.

Jusqu'à présent, je n'ai que le service d'onglets qui est démarré principalement pour persister dans l'application. Cela ressemble à ceci:

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

Des questions:

  1. Comment puis-je avoir une liste dynamique dans la boîte de réception qui crée de nouveaux (différents) onglets? Je suppose que le DynamicComponentBuilderserait utilisé?
  2. Comment les composants peuvent-ils être créés à partir de la boîte de réception (au clic) s'enregistrer en tant qu'onglets et également être affichés? Je suppose ng-content, mais je ne trouve pas beaucoup d'informations sur la façon de l'utiliser

ÉDITER: Une tentative de clarification.

Considérez la boîte de réception comme une boîte de réception de courrier. Les éléments sont récupérés au format JSON et il affiche plusieurs éléments. Une fois que l'un des éléments est cliqué, un nouvel onglet est créé avec le type d'action de cet élément. Le type est alors un composant.

EDIT 2: Image .

Cuel
la source
Si les composants affichés dans les onglets ne sont pas connus au moment de la construction, alors DCL est la bonne approche.
Günter Zöchbauer
7
Je ne comprends pas clairement votre exigence, donc je vous dis n'importe quoi sans code / plunker fonctionnel. Regardez ceci si cela peut vous aider quelque part plnkr.co/edit/Ud1x10xee7BmtUaSAA2R?p=preview (je ne sais pas si c'est pertinent ou non)
micronyks
@micronyks Je pense que vous avez obtenu le mauvais lien
Cuel
Salut! J'essaie de faire ce que tu as demandé. Jusqu'à présent, j'ai réussi à créer l'onglet avec un contenu dynamique, mais je n'ai pas trouvé de moyen satisfaisant de conserver l'état du composant lorsque l'onglet est modifié (les composants chargés peuvent être très différents). Comment avez-vous réussi?
gipinani

Réponses:

267

mettre à jour

Exemple Angular 5 StackBlitz

mettre à jour

ngComponentOutlet a été ajouté à 4.0.0-beta.3

mettre à jour

Il y a un NgComponentOutlettravail en cours qui fait quelque chose de similaire https://github.com/angular/angular/pull/11235

RC.7

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

Exemple d'utilisation

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

Voir également angular.io CHARGEUR À COMPOSANTS DYNAMIQUES

Versions plus anciennes xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Cela a encore changé dans Angular2 RC.5

Je mettrai à jour l'exemple ci-dessous mais c'est le dernier jour avant les vacances.

Cet exemple Plunker montre comment créer dynamiquement des composants dans RC.5

Mise à jour - utilisez ViewContainerRef .createComponent ()

Parce qu'elle DynamicComponentLoaderest obsolète, l'approche doit être mise à jour à nouveau.

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

Exemple Plunker
RC.4 Exemple Plunker beta.17

Mise à jour - utilisez 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();
    }    
  }
}

Exemple Plunker beta.17

original

Pas entièrement sûr de votre question quelles sont vos exigences, mais je pense que cela devrait faire ce que vous voulez.

Le Tabscomposant obtient un tableau de types transmis et il crée des "onglets" pour chaque élément du tableau.

@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];
}

Exemple Plunker beta.15 (non basé sur votre Plunker)

Il existe également un moyen de transmettre des données qui peuvent être transmises au composant créé dynamiquement comme ( someDatadevraient être transmises comme type)

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

Il existe également une certaine prise en charge pour utiliser l'injection de dépendance avec des services partagés.

Pour plus de détails, voir https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html

Günter Zöchbauer
la source
1
Bien sûr, il vous suffit d'obtenir le type de composants pour le DclWrapperfaire créer une instance réelle.
Günter Zöchbauer
1
@Joseph Vous pouvez injecter ViewContainerRefau lieu d'utiliser ViewChild, puis <dcl-wrapper>devient lui-même la cible. Les éléments sont ajoutés en tant que frères et sœurs de la cible et seront donc en dehors de <dcl-wrapper>cette façon.
Günter Zöchbauer
1
Le remplacement n'est pas pris en charge. Vous pouvez changer le modèle en ''(chaîne vide) `et changer le constructeur en constructor(private target:ViewContainerRef) {}, puis les composants ajoutés dynamiquement deviennent frères et sœurs de<dcl-wrapper>
Günter Zöchbauer
1
J'utilise RC4 et l'exemple était assez utile. La seule chose que je voulais mentionner est que j'ai dû ajouter ci-dessous le code pour que la liaison fonctionne correctement this.cmpRef.changeDetectorRef.detectChanges ();
Rajee
4
J'ai reçu une erreur lorsque le composant dynamique avait un autre composant dynaimc lors de l'utilisation de ngAfterViewInit. Modifié en ngAfterContentInit à la place et maintenant il fonctionne avec des composants dynamiques imbriqués
Abris
20

Je ne suis pas assez cool pour les commentaires. J'ai corrigé le plongeur de la réponse acceptée pour travailler pour rc2. Rien d'extraordinaire, les liens vers le CDN viennent d'être rompus, c'est tout.

'@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
la source
16

il y a un composant prêt à l'emploi (compatible rc5) ng2-steps qui utilise Compilerpour injecter le composant au conteneur d'étape et le service pour tout câbler ensemble (synchronisation des données)

    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
la source