Angular 2同级组件通信


118

我有一个ListComponent。在ListComponent中单击某个项目时,该项目的详细信息应显示在DetailComponent中。两者同时显示在屏幕上,因此不涉及路由。

如何告诉DetailComponent单击ListComponent中的哪个项目?

我考虑过向父对象(AppComponent)发出事件,并让父对象使用@Input在DetailComponent上设置selectedItem.id。或者,我可以将共享服务与可观察的订阅一起使用。


编辑:通过事件+ @Input设置所选项目不会触发DetailComponent,但是,以防万一我需要执行其他代码。因此,我不确定这是否可以接受。


但是,这两种方法似乎都比通过Angular 1做事的方式复杂得多,后者是通过$ rootScope。$ broadcast或$ scope。$ parent。$ broadcast实现的。

由于Angular 2中的所有内容都是组件,我很惊讶没有更多有关组件通信的信息。

是否有另一种/更直接的方法来完成此任务?


您找到了兄弟共享数据的任何方法吗?我需要它作为观察到的..

Answers:


65

更新到rc.4: 当试图使数据在angular 2的兄弟组件之间传递时,目前最简单的方法(angular.rc.4)是利用angular2的分层依赖注入并创建共享服务。

这将是服务:

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

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

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

现在,这里是PARENT组件

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{

} 

及其两个孩子

儿童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;
    }
}

孩子2(同级)

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 = '';
    }
}

现在:使用此方法时要注意的事项。

  1. 仅在PARENT组件中包括共享服务的服务提供者,而不在子组件中。
  2. 您仍然必须包括构造函数并将服务导入子项中
  3. 此答案最初是针对早期的angular 2 beta版本回答的。尽管所有更改都不过是import语句,所以如果您偶然使用了原始版本,则只需更新即可。

2
这对angular-rc1仍然有效吗?
塞尔吉奥

4
我不相信这会通知兄弟姐妹共享服务中的某些内容已更新。如果child-component1执行了child-component2需要响应的操作,则此方法将无法处理该问题。我相信解决方法就是观察到的东西?
dennis.sheppard

1
@Sufyan:我猜想将提供者字段添加到子代会导致Angular为每个子代创建新的私有实例。当您不添加它们时,它们将使用父级的“ singleton”实例。
拉尔夫

1
似乎最新的更新已不再起作用
Sufyan Jabr

3
这已经过时了。directives不再在组件中声明。
内特·加德纳

26

对于2个不同的组件(非嵌套组件,parent \ child \ grandchild),我建议您这样做:

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

}

宇航员

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

资料来源:父母和子女通过服务进行沟通


2
希望您在此答案中添加一些术语。我认为它不完全符合RxJS,Observable模式等。但是对其中一些内容添加说明将对人们(例如我自己)有利。
karns

13

一种方法是使用共享服务

但是我发现以下解决方案要简单得多,它允许在2个同级之间共享数据。(我仅在Angular 5上对此进行了测试)

在您的父组件模板中:

<!-- 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;
   ...
}

这是否与松散耦合以及部件松散的想法相反?
罗宾(Robin)

有人知道这是干净还是肮脏的方法?在一个方向上共享数据似乎要简单得多,例如仅从sibiling1到sibiling2共享数据,而不是相反
Sarah


7

指令在某些情况下可以“连接”组件。实际上,连接的东西甚至不需要是完整的组件,有时候,如果不是的话,它更轻巧,实际上更简单。

例如,我有一个Youtube Player组件(包装Youtube API),我想要一些控制器按钮。这些按钮不属于我的主要组件的唯一原因是它们位于DOM中的其他位置。

在这种情况下,它实际上只是一个“扩展”组件,只能与“父”组件一起使用。我说“父母”,但在DOM中是兄弟姐妹-随便叫它。

就像我说的那样,它甚至不必是一个完整的组件,就我而言,它只是一个<button>(但它可以是一个组件)。

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

在的HTML中ProductPage.componentyoutube-player显然是包装Youtube API的组件在哪里。

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

... lots more DOM ...

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

该指令为我提供了一切,而我不必在HTML中声明(click)事件。

因此,该指令可以很好地连接到视频播放器,而不必参与ProductPage调解器。

这是我第一次真正做到这一点,因此尚不确定在更复杂的情况下它的可伸缩性。为此,尽管我很高兴,但它却使我的HTML变得简单,而所有事情的职责却截然不同。


要理解的最重要的角度概念之一是,组件只是带有模板的指令。一旦您真正理解了这意味着什么,那么指令就不会太吓人了-您将意识到可以将它们应用于任何元素以将行为附加到它。
Simon_Weaver

我已经尝试过此操作,但出现了与等效的标识符错误player。如果我不提玩家的第一提,就会遇到rangeError。我对此应该如何工作感到困惑。
凯瑟琳·奥斯本

@KatharineOsborne看起来像在我_player用于代表玩家的私有字段的实际代码中一样,所以是的,如果您完全复制了此代码,将会得到一个错误。将更新。抱歉!
Simon_Weaver

4

这是简单的实际说明:这里简单说明

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

您要调用的组件可观察到,以通知其他组件单击了按钮

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

要在按钮上执行操作的组件,单击另一个组件

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

});

}

我的理解通过阅读以下内容清楚地了解了组件通信:http : //musttoknow.com/angular-4-angular-5-communicate-two-components-using-observable-subject/


嘿,非常感谢您提供的简单解决方案>我在stackblitz中尝试了一下,效果很好。但是我的应用程序具有延迟加载的路由(已使用提供的“ root”)和HTTP调用来进行设置和获取。您能帮我HTTP呼叫吗?尝试了很多但没有起作用:
Kshri

4

共享服务是解决此问题的好方法。如果您还想存储一些活动信息,则可以将共享服务添加到主模块(app.module)提供程序列表中。

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

然后,您可以直接将其提供给您的组件,

constructor(private sharedService: SharedService)

使用共享服务,您可以使用功能,也可以创建主题以一次更新多个位置。

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

在列表组件中,您可以发布点击的项目信息,

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

然后您可以在详细信息组件中获取此信息:

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

显然,列出组件共享的数据可以是任何东西。希望这可以帮助。


这是共享服务这一概念的最直接的示例(又名简洁),并且由于没有公认的答案,因此应该通过投票来提高其可见性。
iGanja

3

您需要在组件之间设置父子关系。问题在于您可能只是将子组件注入到父组件的构造函数中,并将其存储在局部变量中。相反,您应该使用@ViewChild属性声明器在父组件中声明子组件。这是您的父组件的外观:

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

当心,子组件在ngAfterViewInit生命周期挂钩被调用之后将无法在父组件的构造函数中使用。为了抓住这个钩子AfterViewInit,可以用和父类相同的方法来实现接口OnInit

但是,如本博客说明中所述,还有其他属性声明符:http : //blog.mgechev.com/2016/01/23/angular2-viewchildren-contentchildren-difference-viewproviders/


2

行为主体。我写了一个博客

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

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

在此示例中,我声明了类型为noid的行为对象。这也是可观察的。而且,如果“发生了某些事情”,它将随着new(){}函数而改变。

因此,在同级组件中,一个将调用该函数以进行更改,而另一个将受到该更改的影响,反之亦然。

例如,我从URL获取ID,并从行为主题更新Noid。

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

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

从另一面来看,我可以询问该ID是否为“我想要的”,然后在此之后做出选择,就我而言,如果我想删除一个任务,并且该任务是当前的url,则必须重定向我到家:

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

1

这不是您真正想要的,但是可以肯定会帮助您

我很惊讶没有更多有关组件通信的信息 <=> angualr2考虑本教程

对于同级组件通信,建议使用sharedService。但是,还有其他选项可用。

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,[]);

请参阅顶部的链接以获取更多代码。

编辑:这是一个非常小的演示。您已经提到您已经尝试过使用sharedService。因此,请阅读angualr2的本教程以获取更多信息。


0

我一直在通过绑定将父级的setter方法传递给它的子级之一,并使用子级组件中的数据调用该方法,这意味着父级组件已更新,然后可以使用新数据更新其第二个子级组件。它确实需要绑定“ this”或使用箭头功能。

这样做的好处是,孩子不需要特别的共享服务,因此彼此之间不会那么耦合。

我并不完全确定这是最佳做法,听到其他人对此的看法会很有趣。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.