Динамические вкладки с выбранными пользователем компонентами

224

Я пытаюсь настроить систему вкладок, которая позволяет компонентам регистрироваться самостоятельно (с заголовком). Первая вкладка похожа на папку «Входящие», в ней доступно множество действий / элементов ссылок для пользователей, и каждый из этих кликов должен иметь возможность создавать новый компонент по щелчку. Действия / ссылки поступают из JSON.

Созданный экземпляр компонента затем зарегистрируется как новая вкладка.

Я не уверен, что это «лучший» подход? До сих пор я видел только ориентиры для статических вкладок, которые не помогают.

Пока что у меня есть только служба вкладок, которая загружается в main и сохраняется в приложении. Это выглядит примерно так:

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

Вопросы:

  1. Как я могу иметь динамический список в папке «Входящие», который создает новые (разные) вкладки? Я вроде угадал, DynamicComponentBuilderбудет ли использоваться?
  2. Как компоненты могут быть созданы из папки «Входящие» (по щелчку), регистрироваться как вкладки и также отображаться? Я думаю ng-content, но я не могу найти много информации о том, как его использовать

РЕДАКТИРОВАТЬ: попытка уточнить.

Думайте о почтовом ящике как о почтовом ящике. Элементы выбираются как JSON, и он отображает несколько элементов. После щелчка по одному из элементов создается новая вкладка с этим действием действия «тип». Тип тогда является компонентом.

РЕДАКТИРОВАТЬ 2: изображение .

Cuel
источник
Если компоненты, показанные на вкладках, не известны во время сборки, тогда DCL - правильный подход.
Гюнтер Цохбауэр
7
Я не понимаю ваше требование ясно, так что трудно сказать вам что-либо без рабочего кода / plunker. Посмотрите, если это может вам чем-то помочь plnkr.co/edit/Ud1x10xee7BmtUaSAA2R?p=preview (не знаю, актуально это или нет)
micronyks
@micronyks Я думаю, что вы получили не ту ссылку
Cuel
Здравствуй! Я пытаюсь сделать то, что вы просили. До сих пор мне удалось создать вкладку с динамическим содержимым, но я не нашел удовлетворительного способа сохранить состояние компонента при изменении вкладки (загруженные компоненты могут сильно отличаться). Как тебе это удалось?
Гипинани

Ответы:

267

Обновить

Angular 5 StackBlitz пример

Обновить

ngComponentOutlet был добавлен в 4.0.0-бета.3

Обновить

Идет NgComponentOutletработа над чем-то вроде https://github.com/angular/angular/pull/11235

RC.7

Пример плунжера 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 ДИНАМИЧЕСКИЙ КОМПОНЕНТНЫЙ ПОГРУЗЧИК

более старые версии xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Это снова изменилось в Angular2 RC.5

Я обновлю пример ниже, но это последний день перед отпуском.

Этот пример Plunker демонстрирует, как динамически создавать компоненты в 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();
    }    
  }
}

Пример плунжера
RC.4 Пример плунжера 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();
    }    
  }
}

Пример плунжера бета.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];
}

Пример плункера бета.15 (не зависит от вашего плункера)

Существует также способ передачи данных, которые могут быть переданы динамически созданному компоненту (например 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.

Гюнтер Цохбауэр
источник
1
Конечно, вам просто нужно получить тип компонентов для DclWrapperсоздания реального экземпляра.
Гюнтер Цохбауэр
1
@Joseph Вы можете вводить ViewContainerRefвместо использования ViewChild, а затем <dcl-wrapper>сам становится целью. Элементы добавляются как братья и сестры цели и поэтому будут вне <dcl-wrapper>этого пути.
Гюнтер Цохбауэр
1
Замена не поддерживается. Вы можете изменить шаблон на ''(пустая строка) `и изменить конструктор на constructor(private target:ViewContainerRef) {}, тогда динамически добавленные компоненты станут братьями и сестрами<dcl-wrapper>
Günter Zöchbauer
1
Я использую RC4, и пример был довольно полезным. Единственное, что я хотел упомянуть, это то, что мне пришлось добавить ниже код для привязки, чтобы работать должным образом this.cmpRef.changeDetectorRef.detectChanges ();
Раджи
4
Я получил ошибку, когда динамический компонент имел другой компонент dynaimc при использовании ngAfterViewInit. Вместо этого он был изменен на ngAfterContentInit, и теперь он работает с вложенными динамическими компонентами
Abris
20

Я недостаточно крут для комментариев. Я исправил поршень из принятого ответа на работу для 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

davimusprime
источник
16

есть готовый к использованию компонент (совместимый с 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);
    });
  }

}
нейросети
источник