Angular 2 Geschwisterkomponenten-Kommunikation

118

Ich habe eine ListComponent. Wenn Sie in ListComponent auf ein Element klicken, sollten die Details dieses Elements in DetailComponent angezeigt werden. Beide werden gleichzeitig auf dem Bildschirm angezeigt, sodass kein Routing erforderlich ist.

Wie kann ich DetailComponent mitteilen, auf welches Element in ListComponent geklickt wurde?

Ich habe überlegt, ein Ereignis bis zum übergeordneten Element (AppComponent) auszugeben, und das übergeordnete Element hat die selectedItem.id in DetailComponent mit einem @Input festgelegt. Oder ich könnte einen gemeinsamen Dienst mit beobachtbaren Abonnements verwenden.


BEARBEITEN: Das Festlegen des ausgewählten Elements über Ereignis + @Input löst die DetailComponent jedoch nicht aus, falls ich zusätzlichen Code ausführen muss. Ich bin mir also nicht sicher, ob dies eine akzeptable Lösung ist.


Beide Methoden scheinen jedoch weitaus komplexer zu sein als die Methode von Angular 1, die entweder über $ rootScope. $ Broadcast oder $ scope. $ Parent. $ Broadcast durchgeführt wurde.

Da alles in Angular 2 eine Komponente ist, bin ich überrascht, dass es keine weiteren Informationen zur Komponentenkommunikation gibt.

Gibt es einen anderen / einfacheren Weg, dies zu erreichen?

dennis.sheppard
quelle
Haben Sie einen Weg gefunden, um Geschwisterdaten auszutauschen? Ich brauche es als beobachtbar ..
Mensch

Antworten:

65

Auf rc.4 aktualisiert: Wenn Sie versuchen, Daten zwischen Geschwisterkomponenten in Winkel 2 zu übertragen, besteht der derzeit einfachste Weg (angle.rc.4) darin, die hierarchische Abhängigkeitsinjektion von Winkel 2 zu nutzen und einen gemeinsamen Dienst zu erstellen.

Hier wäre der Service:

import {Injectable} from '@angular/core';

@Injectable()
export class SharedService {
    dataArray: string[] = [];

    insertData(data: string){
        this.dataArray.unshift(data);
    }
}

Hier wäre nun die PARENT-Komponente

import {Component} from '@angular/core';
import {SharedService} from './shared.service';
import {ChildComponent} from './child.component';
import {ChildSiblingComponent} from './child-sibling.component';
@Component({
    selector: 'parent-component',
    template: `
        <h1>Parent</h1>
        <div>
            <child-component></child-component>
            <child-sibling-component></child-sibling-component>
        </div>
    `,
    providers: [SharedService],
    directives: [ChildComponent, ChildSiblingComponent]
})
export class parentComponent{

} 

und seine zwei Kinder

Kind 1

import {Component, OnInit} from '@angular/core';
import {SharedService} from './shared.service'

@Component({
    selector: 'child-component',
    template: `
        <h1>I am a child</h1>
        <div>
            <ul *ngFor="#data in data">
                <li>{{data}}</li>
            </ul>
        </div>
    `
})
export class ChildComponent implements OnInit{
    data: string[] = [];
    constructor(
        private _sharedService: SharedService) { }
    ngOnInit():any {
        this.data = this._sharedService.dataArray;
    }
}

Kind 2 (Es ist Geschwister)

import {Component} from 'angular2/core';
import {SharedService} from './shared.service'

@Component({
    selector: 'child-sibling-component',
    template: `
        <h1>I am a child</h1>
        <input type="text" [(ngModel)]="data"/>
        <button (click)="addData()"></button>
    `
})
export class ChildSiblingComponent{
    data: string = 'Testing data';
    constructor(
        private _sharedService: SharedService){}
    addData(){
        this._sharedService.insertData(this.data);
        this.data = '';
    }
}

JETZT: Dinge, die Sie bei dieser Methode beachten sollten.

  1. Fügen Sie nur den Dienstanbieter für den gemeinsam genutzten Dienst in die PARENT-Komponente ein und NICHT die untergeordneten.
  2. Sie müssen weiterhin Konstruktoren einschließen und den Dienst in die untergeordneten Elemente importieren
  3. Diese Antwort wurde ursprünglich für eine frühe Beta-Version von Angular 2 beantwortet. Alles, was sich geändert hat, sind die Importanweisungen. Das ist alles, was Sie aktualisieren müssen, wenn Sie zufällig die Originalversion verwendet haben.
Alex J.
quelle
2
Gilt das noch für angle-rc1?
Sergio
4
Ich glaube nicht, dass dies das Geschwister darüber informiert, dass im Shared Service etwas aktualisiert wurde. Wenn child-component1 etwas tut, auf das child-component2 reagieren muss, behandelt diese Methode dies nicht. Ich glaube, der Weg dahin ist mit Observablen?
dennis.sheppard
1
@ Sufyan: Ich gehe davon aus, dass Angular durch Hinzufügen der Providerfelder zu den untergeordneten Feldern für jede neue private Instanzen erstellt wird. Wenn Sie sie nicht hinzufügen, verwenden sie die "Singleton" -Instanz des übergeordneten Elements.
Ralph
1
Sieht so aus, als würde dies mit den neuesten Updates nicht mehr funktionieren
Sufyan Jabr
3
Das ist veraltet. directivessind nicht mehr in der Komponente deklariert.
Nate Gardner
26

Bei 2 verschiedenen Komponenten (nicht verschachtelte Komponenten, Eltern \ Kind \ Enkelkind) schlage ich Folgendes vor:

MissionService:

import { Injectable } from '@angular/core';
import { Subject }    from 'rxjs/Subject';

@Injectable()

export class MissionService {
  // Observable string sources
  private missionAnnouncedSource = new Subject<string>();
  private missionConfirmedSource = new Subject<string>();
  // Observable string streams
  missionAnnounced$ = this.missionAnnouncedSource.asObservable();
  missionConfirmed$ = this.missionConfirmedSource.asObservable();
  // Service message commands
  announceMission(mission: string) {
    this.missionAnnouncedSource.next(mission);
  }
  confirmMission(astronaut: string) {
    this.missionConfirmedSource.next(astronaut);
  }

}

AstronautComponent:

import { Component, Input, OnDestroy } from '@angular/core';
import { MissionService } from './mission.service';
import { Subscription }   from 'rxjs/Subscription';
@Component({
  selector: 'my-astronaut',
  template: `
    <p>
      {{astronaut}}: <strong>{{mission}}</strong>
      <button
        (click)="confirm()"
        [disabled]="!announced || confirmed">
        Confirm
      </button>
    </p>
  `
})
export class AstronautComponent implements OnDestroy {
  @Input() astronaut: string;
  mission = '<no mission announced>';
  confirmed = false;
  announced = false;
  subscription: Subscription;
  constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
      mission => {
        this.mission = mission;
        this.announced = true;
        this.confirmed = false;
    });
  }
  confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
  }
  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
}

Quelle: Eltern und Kinder kommunizieren über einen Dienst

Dudi
quelle
2
Ich möchte, dass Sie dieser Antwort eine Terminologie hinzufügen. Ich glaube, dass es im Einklang mit RxJS, Observable Pattern usw. steht, nicht ganz sicher, aber das Hinzufügen von Beschreibungen zu einigen davon wäre für Menschen (wie mich selbst) von Vorteil.
Karns
13

Eine Möglichkeit hierfür ist die Verwendung eines gemeinsam genutzten Dienstes .

Die folgende Lösung ist jedoch viel einfacher. Sie ermöglicht den Austausch von Daten zwischen zwei Geschwistern. (Ich habe dies nur auf Angular 5 getestet. )

In Ihrer übergeordneten Komponentenvorlage:

<!-- Assigns "AppSibling1Component" instance to variable "data" -->
<app-sibling1 #data></app-sibling1>
<!-- Passes the variable "data" to AppSibling2Component instance -->
<app-sibling2 [data]="data"></app-sibling2> 

app-sibling2.component.ts

import { AppSibling1Component } from '../app-sibling1/app-sibling1.component';
...

export class AppSibling2Component {
   ...
   @Input() data: AppSibling1Component;
   ...
}
Caner
quelle
Widerspricht dies nicht der Idee einer losen Kopplung und damit von Bauteilen?
Robin
Hat jemand eine Idee, ob dies ein sauberer oder schmutziger Weg ist? Es scheint viel einfacher zu sein, Daten in eine Richtung zu teilen, zum Beispiel nur von Geschwister1 zu Geschwister2, aber nicht umgekehrt
Sarah
7

Eine Anweisung kann in bestimmten Situationen sinnvoll sein, Komponenten zu verbinden. Tatsächlich müssen die zu verbindenden Dinge nicht einmal vollständige Komponenten sein, und manchmal ist es leichter und sogar einfacher, wenn dies nicht der Fall ist.

Zum Beispiel habe ich eine Youtube PlayerKomponente (umschließende Youtube-API) und ich wollte einige Controller-Schaltflächen dafür. Der einzige Grund, warum die Schaltflächen nicht Teil meiner Hauptkomponente sind, ist, dass sie sich an einer anderen Stelle im DOM befinden.

In diesem Fall handelt es sich eigentlich nur um eine Erweiterungskomponente, die nur mit der übergeordneten Komponente von Nutzen ist. Ich sage 'Eltern', aber im DOM ist es ein Geschwister - also nenne es so, wie du willst.

Wie ich schon sagte, es muss nicht einmal eine vollständige Komponente sein, in meinem Fall ist es nur eine <button>(aber es könnte eine Komponente sein).

@Directive({
    selector: '[ytPlayerPlayButton]'
})
export class YoutubePlayerPlayButtonDirective {

    _player: YoutubePlayerComponent; 

    @Input('ytPlayerVideo')
    private set player(value: YoutubePlayerComponent) {
       this._player = value;    
    }

    @HostListener('click') click() {
        this._player.play();
    }

   constructor(private elementRef: ElementRef) {
       // the button itself
   }
}

Im HTML für ProductPage.component, wo youtube-playerist offensichtlich meine Komponente, die die Youtube-API umschließt.

<youtube-player #technologyVideo videoId='NuU74nesR5A'></youtube-player>

... lots more DOM ...

<button class="play-button"        
        ytPlayerPlayButton
        [ytPlayerVideo]="technologyVideo">Play</button>

Die Direktive verbindet alles für mich und ich muss das (Klick-) Ereignis nicht im HTML deklarieren.

So kann die Richtlinie eine gute Verbindung zum Videoplayer herstellen, ohne ProductPageals Vermittler hinzuziehen zu müssen.

Dies ist das erste Mal, dass ich dies tatsächlich mache, daher noch nicht sicher, wie skalierbar es für viel komplexere Situationen sein könnte. Dafür bin ich aber glücklich und es lässt mein HTML einfach und Verantwortlichkeiten von allem verschieden.

Simon_Weaver
quelle
Eines der wichtigsten Winkelkonzepte ist, dass eine Komponente nur eine Direktive mit einer Vorlage ist. Sobald Sie wirklich verstanden haben, was dies bedeutet, sind Direktiven nicht mehr so ​​beängstigend - und Sie werden feststellen, dass Sie sie auf jedes Element anwenden können, um ihm Verhalten zuzuweisen.
Simon_Weaver
Ich habe es versucht, aber ich erhalte einen doppelten Bezeichnungsfehler für das Äquivalent von player. Wenn ich die erste Erwähnung des Spielers weglasse, erhalte ich einen rangeError. Ich bin ratlos, wie das funktionieren soll.
Katharine Osborne
@KatharineOsborne Sieht aus wie in meinem eigentlichen Code, den ich _playerfür das private Feld verwende, das den Player darstellt. Ja, Sie würden eine Fehlermeldung erhalten, wenn Sie dies genau kopieren würden. Werde dich auf den neuesten Stand bringen. Es tut uns leid!
Simon_Weaver
4

Hier ist eine einfache praktische Erklärung: Einfach hier erklärt

In call.service.ts

import { Observable } from 'rxjs';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class CallService {
 private subject = new Subject<any>();

 sendClickCall(message: string) {
    this.subject.next({ text: message });
 }

 getClickCall(): Observable<any> {
    return this.subject.asObservable();
 }
}

Komponente, von der aus Sie Observable aufrufen möchten, um eine andere Komponente darüber zu informieren, dass auf die Schaltfläche geklickt wird

import { CallService } from "../../../services/call.service";

export class MarketplaceComponent implements OnInit, OnDestroy {
  constructor(public Util: CallService) {

  }

  buttonClickedToCallObservable() {
   this.Util.sendClickCall('Sending message to another comp that button is clicked');
  }
}

Komponente, bei der Sie eine Aktion ausführen möchten, indem Sie auf eine andere Komponente klicken

import { Subscription } from 'rxjs/Subscription';
import { CallService } from "../../../services/call.service";


ngOnInit() {

 this.subscription = this.Util.getClickCall().subscribe(message => {

 this.message = message;

 console.log('---button clicked at another component---');

 //call you action which need to execute in this component on button clicked

 });

}

import { Subscription } from 'rxjs/Subscription';
import { CallService } from "../../../services/call.service";


ngOnInit() {

 this.subscription = this.Util.getClickCall().subscribe(message => {

 this.message = message;

 console.log('---button clicked at another component---');

 //call you action which need to execute in this component on button clicked

});

}

Mein Verständnis der Komponentenkommunikation ist klar, wenn ich Folgendes lese: http://musttoknow.com/angular-4-angular-5-communicate-two-components-using-observable-subject/

Prashant M Bhavsar
quelle
Hey, vielen Dank für die einfache Lösung> Ich habe es in Stackblitz versucht, was gut funktioniert hat. Aber meine Anwendung hat faul geladene Routen (habe bereitgestellt in: 'root') und HTTP-Aufrufe zum Setzen und Abrufen. Könnten Sie mir bei HTTP-Aufrufen helfen? viel versucht, aber nicht funktioniert:
Kshri
4

Shared Service ist eine gute Lösung für dieses Problem. Wenn Sie auch einige Aktivitätsinformationen speichern möchten, können Sie Shared Service zu Ihrer Anbieterliste für Hauptmodule (app.module) hinzufügen.

@NgModule({
    imports: [
        ...
    ],
    bootstrap: [
        AppComponent
    ],
    declarations: [
        AppComponent,
    ],
    providers: [
        SharedService,
        ...
    ]
});

Dann können Sie es direkt Ihren Komponenten zur Verfügung stellen,

constructor(private sharedService: SharedService)

Mit Shared Service können Sie entweder Funktionen verwenden oder einen Betreff erstellen, um mehrere Stellen gleichzeitig zu aktualisieren.

@Injectable()
export class FolderTagService {
    public clickedItemInformation: Subject<string> = new Subject(); 
}

In Ihrer Listenkomponente können Sie angeklickte Elementinformationen veröffentlichen.

this.sharedService.clikedItemInformation.next("something");

und dann können Sie diese Informationen an Ihrer Detailkomponente abrufen:

this.sharedService.clikedItemInformation.subscribe((information) => {
    // do something
});

Offensichtlich können die Daten, die Komponentenfreigaben auflisten, alles sein. Hoffe das hilft.

Nick Greaves
quelle
Dies ist das direkteste (auch bekannt als knapp) Beispiel für dieses Konzept eines gemeinsam genutzten Dienstes und sollte wirklich verbessert werden, um seine Sichtbarkeit zu erhöhen, da es keine akzeptierte Antwort gibt.
iGanja
3

Sie müssen die Eltern-Kind-Beziehung zwischen Ihren Komponenten einrichten. Das Problem ist, dass Sie die untergeordneten Komponenten einfach in den Konstruktor der übergeordneten Komponente einfügen und in einer lokalen Variablen speichern können. Stattdessen sollten Sie die untergeordneten Komponenten in Ihrer übergeordneten Komponente mithilfe des @ViewChildEigenschaftsdeklarators deklarieren . So sollte Ihre übergeordnete Komponente aussehen:

import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ListComponent } from './list.component';
import { DetailComponent } from './detail.component';

@Component({
  selector: 'app-component',
  template: '<list-component></list-component><detail-component></detail-component>',
  directives: [ListComponent, DetailComponent]
})
class AppComponent implements AfterViewInit {
  @ViewChild(ListComponent) listComponent:ListComponent;
  @ViewChild(DetailComponent) detailComponent: DetailComponent;

  ngAfterViewInit() {
    // afther this point the children are set, so you can use them
    this.detailComponent.doSomething();
  }
}

https://angular.io/docs/ts/latest/api/core/index/ViewChild-var.html

https://angular.io/docs/ts/latest/cookbook/component-communication.html#parent-to-view-child

Beachten Sie, dass die untergeordnete Komponente im Konstruktor der übergeordneten Komponente nicht verfügbar ist, unmittelbar nachdem der ngAfterViewInitLifecycle-Hook aufgerufen wurde. Um diesen Haken zu fangen, implementieren AfterViewInitSie einfach die Schnittstelle in Ihrer übergeordneten Klasse auf die gleiche Weise, wie Sie es tun würden OnInit.

Es gibt jedoch auch andere Eigenschaftsdeklaratoren, die in diesem Blog-Hinweis erläutert werden: http://blog.mgechev.com/2016/01/23/angular2-viewchildren-contentchildren-difference-viewproviders/

Vereb
quelle
2

Verhaltensthemen. Ich habe einen Blog darüber geschrieben.

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
private noId = new BehaviorSubject<number>(0); 
  defaultId = this.noId.asObservable();

newId(urlId) {
 this.noId.next(urlId); 
 }

In diesem Beispiel deklariere ich ein Noid-Verhalten mit der Typennummer. Auch ist es ein beobachtbares. Und wenn "etwas passiert ist", ändert sich dies mit der Funktion new () {}.

In den Komponenten des Geschwisters ruft einer die Funktion auf, um die Änderung vorzunehmen, und der andere wird von dieser Änderung betroffen sein oder umgekehrt.

Zum Beispiel erhalte ich die ID von der URL und aktualisiere die Noid vom Verhaltensthema.

public getId () {
  const id = +this.route.snapshot.paramMap.get('id'); 
  return id; 
}

ngOnInit(): void { 
 const id = +this.getId ();
 this.taskService.newId(id) 
}

Und von der anderen Seite kann ich fragen, ob diese ID "was immer ich will" ist und danach eine Auswahl treffen. In meinem Fall, wenn ich eine Aufgabe löschen möchte und diese Aufgabe die aktuelle URL ist, muss sie mich umleiten nach Hause:

delete(task: Task): void { 
  //we save the id , cuz after the delete function, we  gonna lose it 
  const oldId = task.id; 
  this.taskService.deleteTask(task) 
      .subscribe(task => { //we call the defaultId function from task.service.
        this.taskService.defaultId //here we are subscribed to the urlId, which give us the id from the view task 
                 .subscribe(urlId => {
            this.urlId = urlId ;
                  if (oldId == urlId ) { 
                // Location.call('/home'); 
                this.router.navigate(['/home']); 
              } 
          }) 
    }) 
}
ValRob
quelle
1

Dies ist nicht genau das, was Sie wollen, aber es wird Ihnen sicherlich helfen

Ich bin überrascht, dass es keine weiteren Informationen zur Komponentenkommunikation gibt. <=> Betrachten Sie dieses Tutorial von angualr2

Für die Kommunikation mit Geschwisterkomponenten würde ich empfehlen, mit zu gehen sharedService. Es gibt jedoch auch andere Optionen.

import {Component,bind} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {HTTP_PROVIDERS} from 'angular2/http';
import {NameService} from 'src/nameService';


import {TheContent} from 'src/content';
import {Navbar} from 'src/nav';


@Component({
  selector: 'app',
  directives: [TheContent,Navbar],
  providers: [NameService],
  template: '<navbar></navbar><thecontent></thecontent>'
})


export class App {
  constructor() {
    console.log('App started');
  }
}

bootstrap(App,[]);

Weitere Informationen finden Sie unter dem Link oben.

Bearbeiten: Dies ist eine sehr kleine Demo. Sie haben bereits erwähnt, dass Sie es bereits versucht haben sharedService. Weitere Informationen finden Sie in diesem Tutorial von angualr2 .

Mikronyken
quelle
0

Ich habe Setter-Methoden vom übergeordneten Element über eine Bindung an eines ihrer untergeordneten Elemente weitergegeben und diese Methode mit den Daten der untergeordneten Komponente aufgerufen. Dies bedeutet, dass die übergeordnete Komponente aktualisiert wird und dann ihre zweite untergeordnete Komponente mit den neuen Daten aktualisieren kann. Es erfordert jedoch das Binden von 'this' oder die Verwendung einer Pfeilfunktion.

Dies hat den Vorteil, dass die Kinder nicht so miteinander verbunden sind, dass sie keinen bestimmten gemeinsamen Dienst benötigen.

Ich bin mir nicht ganz sicher, ob dies die beste Vorgehensweise ist. Es wäre interessant, andere Ansichten dazu zu hören.

Thingamajig
quelle