Karty dynamiczne z klikniętymi przez użytkownika wybranymi komponentami


224

Próbuję skonfigurować system zakładek, który pozwala komponentom rejestrować się (z tytułem). Pierwsza karta jest jak skrzynka odbiorcza, istnieje wiele działań / elementów do wyboru dla użytkowników, a każde z tych kliknięć powinno umożliwiać utworzenie nowego komponentu po kliknięciu. Działania / linki pochodzą z JSON.

Utworzony wystąpienie komponentu zostanie następnie zarejestrowane jako nowa karta.

Nie jestem pewien, czy jest to najlepsze podejście? Jak dotąd, jedyne przewodniki, które widziałem, dotyczą kart statycznych, co nie pomaga.

Do tej pory mam tylko usługę kart, która jest ładowana głównie w celu zachowania w całej aplikacji. Wygląda to mniej więcej tak:

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

Pytania:

  1. Jak mogę mieć dynamiczną listę w skrzynce odbiorczej, która tworzy nowe (różne) karty? Zgaduję, DynamicComponentBuilderże zostaną wykorzystane?
  2. Jak można tworzyć komponenty ze skrzynki odbiorczej (po kliknięciu) zarejestrować się jako zakładki, a także wyświetlić? Zgaduję ng-content, ale nie mogę znaleźć wielu informacji o tym, jak z niego korzystać

EDYCJA: próba wyjaśnienia.

Pomyśl o skrzynce odbiorczej jak o skrzynce pocztowej. Elementy są pobierane jako JSON i wyświetla kilka elementów. Po kliknięciu jednego z elementów tworzona jest nowa karta z „typem” działania tego elementu. Typ jest wówczas składnikiem.

EDYCJA 2: Zdjęcie .


Jeśli komponenty pokazane na kartach nie są znane w czasie kompilacji, DCL jest właściwym podejściem.
Günter Zöchbauer

7
Nie rozumiem twoich wymagań wyraźnie, więc trudno ci cokolwiek powiedzieć bez działającego kodu / plunkera. Spójrz na to, jeśli może ci to pomóc gdzieś plnkr.co/edit/Ud1x10xee7BmtUaSAA2R?p=preview (nie wiem, czy to istotne, czy nie)
micronyks

@micronyks Myślę, że masz niewłaściwy link
Cuel

Cześć! Próbuję zrobić to, o co prosiłeś. Do tej pory udało mi się utworzyć kartę z zawartością dynamiczną, ale nie znalazłem satysfakcjonującego sposobu na utrzymanie stanu składnika po zmianie zakładki (ładowane składniki mogą się bardzo różnić). Jak ci się to udało?
gipinani

Odpowiedzi:


267

aktualizacja

Przykład Angular 5 StackBlitz

aktualizacja

ngComponentOutlet został dodany do 4.0.0-beta.3

aktualizacja

Trwają NgComponentOutletprace, które robią coś podobnego https://github.com/angular/angular/pull/11235

RC.7

Przykład plunkera 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();
    }    
  }
}

Przykład użycia

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

Zobacz także angular.io DYNAMICZNY ŁADOWARKA KOMPONENTÓW

starsze wersje xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Zmieniło się to ponownie w Angular2 RC.5

Zaktualizuję poniższy przykład, ale jest to ostatni dzień przed wakacjami.

Ten przykład Plunkera pokazuje, jak dynamicznie tworzyć komponenty w RC.5

Aktualizacja - użyj ViewContainerRef .createComponent ()

Ponieważ DynamicComponentLoaderjest przestarzałe, podejście musi zostać ponownie zaktualizowane.

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

Przykład plunkera
RC.4 Przykład plunkera beta.17

Aktualizacja - użyj 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();
    }    
  }
}

Przykład plunkera beta.17

oryginalny

Nie jestem do końca pewien, jakie są twoje wymagania, ale myślę, że powinno to zrobić, co chcesz.

TabsKomponent pobiera tablicę typów minęły i tworzy „zakładki” dla każdego elementu w tablicy.

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

Przykład plunkera beta.15 (nie oparty na twoim Plunkerze)

Istnieje również sposób przekazywania danych, które mogą być przekazywane do dynamicznie tworzonego komponentu, takiego jak ( someDatamusiałby być przekazywany jak type)

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

Istnieje również pewne wsparcie dla stosowania wstrzykiwania zależności w przypadku usług wspólnych.

Aby uzyskać więcej informacji, zobacz https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html


1
Jasne, wystarczy ustawić typ komponentów DclWrapperna, aby utworzyć rzeczywistą instancję.
Günter Zöchbauer

1
@Joseph Możesz wstrzykiwać ViewContainerRefzamiast używać ViewChild, a potem <dcl-wrapper>sam staje się celem. Elementy są dodawane jako rodzeństwo celu i dlatego będą poza <dcl-wrapper>tym sposobem.
Günter Zöchbauer

1
Zastąpienie nie jest obsługiwane. Możesz zmienić szablon na ''(pusty ciąg) `i zmienić konstruktor na constructor(private target:ViewContainerRef) {}, a następnie dynamicznie dodawane komponenty stają się rodzeństwem<dcl-wrapper>
Günter Zöchbauer

1
Używam RC4 i przykład był całkiem przydatny. Jedyne, o czym chciałem wspomnieć to to, że musiałem dodać poniższy kod, aby wiązanie działało poprawnie this.cmpRef.changeDetectorRef.detectChanges ();
Rajee

4
Wystąpił błąd, gdy składnik dynamiczny miał inny składnik dynaimc podczas korzystania z ngAfterViewInit. Zamiast tego zmieniono na ngAfterContentInit i teraz działa z zagnieżdżonymi komponentami dynamicznymi
Abris,

20

Nie jestem wystarczająco fajny do komentowania. Naprawiłem plunker z zaakceptowanej odpowiedzi do pracy dla rc2. Nic szczególnego, linki do CDN były po prostu zepsute.

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


16

istnieje komponent gotowy do użycia (kompatybilny z RC5 ) Krok 2-krokowy, który używa Compilerdo wstrzykiwania komponentu do kontenera krokowego i usługi okablowania wszystkiego razem (synchronizacja danych)

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

}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.