带有用户单击所选组件的动态选项卡

Posted

技术标签:

【中文标题】带有用户单击所选组件的动态选项卡【英文标题】:Dynamic tabs with user-click chosen components 【发布时间】:2016-12-28 23:35:29 【问题描述】:

我正在尝试设置一个标签系统,允许组件注册自己(带有标题)。第一个选项卡就像一个收件箱,有很多动作/链接项可供用户选择,每次点击都应该能够在点击时实例化一个新组件。动作/链接来自 JSON。

实例化的组件随后会将自己注册为一个新选项卡。

我不确定这是否是“最佳”方法?到目前为止,我看到的唯一指南是针对静态选项卡的,这没有帮助。

到目前为止,我只有在 main 中引导的 tabs 服务在整个应用程序中持续存在。它看起来像这样:

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

问题:

    如何在收件箱中创建一个动态列表来创建新的(不同的)标签?我有点猜想DynamicComponentBuilder 会被使用? 如何从收件箱中创建组件(单击时)将它们自己注册为选项卡并显示出来?我猜是ng-content,但我找不到太多关于如何使用它的信息

编辑:试图澄清。

将收件箱视为邮件收件箱。项目以 JSON 格式获取,并显示多个项目。单击其中一个项目后,将使用该项目操作“类型”创建一个新选项卡。那么类型就是一个组件。

编辑 2: Image.

【问题讨论】:

如果选项卡中显示的组件在构建时未知,那么 DCL 是正确的方法。 我不太清楚你的要求,所以很难在没有工作代码/plunker的情况下告诉你任何事情。看看这个,如果它可以帮助你plnkr.co/edit/Ud1x10xee7BmtUaSAA2R?p=preview(我不知道它是否相关) @micronyks 我认为你的链接有误 嗨!我正在尝试做你所要求的。到目前为止,我设法创建了具有动态内容的选项卡,但是我没有找到一种令人满意的方法来在选项卡更改时保持组件状态(加载的组件可能非常不同)。你是怎么做到的? 【参考方案1】:

更新

Angular 5 StackBlitz example

更新

ngComponentOutlet 被添加到 4.0.0-beta.3

更新

有一个 NgComponentOutlet 正在进行中的工作与 https://github.com/angular/angular/pull/11235 类似

RC.7

Plunker example 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();
        
  

使用示例

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

另见angular.io DYNAMIC COMPONENT LOADER

旧版本 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

这在 Angular2 RC.5 中再次改变

我将更新下面的示例,但这是假期前的最后一天。

这个Plunker example 演示了如何在 RC.5 中动态创建组件

更新 - 使用 ViewContainerRef.createComponent()

由于DynamicComponentLoader已被弃用,该方法需要再次更新。

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

Plunker example RC.4Plunker example beta.17

更新 - 使用 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();
        
  

Plunker example beta.17

原创

从您的问题中不能完全确定您的要求是什么,但我认为这应该可以满足您的要求。

Tabs 组件获取传递的类型数组,并为数组中的每个项目创建“选项卡”。

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

Plunker example beta.15(不是基于你的 Plunker)

还有一种方法可以将数据传递给动态创建的组件,例如(someData 需要像 type 一样传递)

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

还有一些支持将依赖注入与共享服务一起使用。

更多详情见https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html

【讨论】:

当然,您只需要将组件类型设置为DclWrapper 即可创建实际实例。 @Joseph 可以注入ViewContainerRef 而不是使用ViewChild,然后&lt;dcl-wrapper&gt; 本身成为目标。元素作为目标的兄弟元素添加,因此会在 &lt;dcl-wrapper&gt; 之外。 不支持替换。可以把模板改成''(空字符串)`,把构造函数改成constructor(private target:ViewContainerRef) ,那么动态添加的组件就变成&lt;dcl-wrapper&gt;的兄弟了 我使用的是 RC4,这个例子非常有用。我只想提到的是我必须添加下面的代码才能正常工作 this.cmpRef.changeDetectorRef.detectChanges(); 在使用 ngAfterViewInit 时动态组件有另一个 dynaimc 组件时出现错误。改为 ngAfterContentInit,现在它正在使用嵌套的动态组件【参考方案2】:

我对 cme​​ts 还不够酷。我从接受的答案中修复了 plunker,以便为 rc2 工作。没什么特别的,只是 CDN 的链接被破坏了而已。

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

【讨论】:

【参考方案3】:

有可以使用的组件(rc5 兼容) ng2-steps 它使用Compiler 将组件注入步骤容器 以及将所有东西连接在一起的服务(数据同步)

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


【讨论】:

以上是关于带有用户单击所选组件的动态选项卡的主要内容,如果未能解决你的问题,请参考以下文章

具有用户单击选择的组件的动态选项卡

带有 OnInitializedAsync 的选项卡?

动态创建带有闪亮绘图的选项卡,而无需重新创建现有选项卡

带有 Vue 选项卡的组件标签中的动态道具

带有动态选项卡的角度路由

在带有选项卡式控件的表单中,单击按钮会降低焦点选项卡页中的面板高度,如果移动到另一个选项卡并返回,则会更正