Angular 2 兄弟组件通信

Posted

技术标签:

【中文标题】Angular 2 兄弟组件通信【英文标题】:Angular 2 Sibling Component Communication 【发布时间】:2016-06-23 09:45:08 【问题描述】:

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

我如何告诉 DetailComponent ListComponent 中的哪个项目被点击了?

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


编辑: 但是,如果我需要执行其他代码,通过事件 + @Input 设置所选项目不会触发 DetailComponent。所以我不确定这是一个可以接受的解决方案。


但这两种方法似乎都比 Angular 1 的处理方式复杂得多,后者通过 $rootScope.$broadcast 或 $scope.$parent.$broadcast。

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

是否有另一种/更直接的方法来实现这一点?

【问题讨论】:

你有没有找到任何方法来共享兄弟数据?我需要它作为可观察的.. 【参考方案1】:

This is not what you exactly want but for sure will help you out

我很惊讶没有关于组件通信的更多信息 consider this tutorial by 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。所以请consider this tutorial by angualr2了解更多信息。

【讨论】:

【参考方案2】:

更新到 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);
    

现在,这里是父组件

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

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

    仅在 PARENT 组件中包含共享服务的服务提供者,而不是子组件。 您仍然必须在子项中包含构造函数并导入服务 这个答案最初是针对早期的 Angular 2 beta 版本回答的。改变的只是 import 语句,所以如果您偶然使用了原始版本,您只需更新这些语句。

【讨论】:

这对 angular-rc1 仍然有效吗? 我不相信这会通知兄弟共享服务中的某些内容已更新。如果 child-component1 做了一些 child-component2 需要响应的事情,则此方法不会处理。我相信解决方法是使用 observables? @Sufyan:我猜想将提供者字段添加到子项会导致 Angular 为每个子项创建新的私有实例。当您不添加它们时,它们会使用父级的“单例”实例。 看起来这不再适用于最新更新 这已经过时了。 directives 不再在组件中声明。【参考方案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/

【讨论】:

【参考方案4】:

如果有 2 个不同的组件(不是嵌套组件, parent\child\grandchild ),我建议您这样做:

任务服务:

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

来源:Parent and children communicate via a service

【讨论】:

希望您在此答案中添加一些术语。我相信它符合 RxJS、Observable 模式等。不完全确定,但在其中添加一些描述会对人们(比如我自己)有益。【参考方案5】:

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

这样做的好处是,孩子之间的耦合度不会太高,因为它们不需要特定的共享服务。

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

【讨论】:

【参考方案6】:

这里有讨论。

https://github.com/angular/angular.io/issues/2663

Alex J 的回答很好,但截至 2017 年 7 月,它不再适用于当前的 Angular 4。

这个 plunker 链接将演示如何使用共享服务和 observable 在兄弟姐妹之间进行通信。

https://embed.plnkr.co/P8xCEwSKgcOg07pwDrlO/

【讨论】:

【参考方案7】:

一种方法是使用shared service。

但是我发现以下内容 解决方案更简单,它允许在 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;
   ...

【讨论】:

这不是反对松散耦合的想法,因此也反对组件吗? 有人知道这是干净还是肮脏的方式吗?在一个方向共享数据似乎要简单得多,例如仅从 sibling1 到 sibling2,但不是相反 @Robin 有时你的组件不能松散耦合。例如,在一个组件应该控制另一个组件的情况下,这是一个很好的功能,您可以在模板中决定哪个组件应该与另一个组件通信。如果您想连接组件,服务将无法工作。例如,一个组件提供了一个用户界面来选择显示的列,另一个组件负责呈现一个列表,但您希望在多个模板中重用这两个组件。【参考方案8】:

行为主题。我为此写了blog。

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

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

在这个例子中,我声明了一个类型为 number 的 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']); 
               
          ) 
    ) 

【讨论】:

【参考方案9】:

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

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

在这种情况下,它实际上只是一个“扩展”组件,只能与“父”组件一起使用。我说的是“父级”,但在 DOM 中它是兄弟级 - 随便你怎么称呼它。

就像我说的那样,它甚至不需要是一个完整的组件,在我的情况下它只是一个 &lt;button&gt;(但它可以是一个组件)。

@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
   

ProductPage.component 的 HTML 中,youtube-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 变得简单,并且所有事情的责任都不同。

【讨论】:

要理解的最重要的角度概念之一是组件只是带有模板的指令。一旦你真正理解这意味着什么,那么指令就不会那么可怕了——你会意识到你可以将它们应用到任何元素上以附加行为。 我已经尝试过了,但是我得到了一个与 player 等效的重复标识符错误。如果我第一次提到播放器,我会得到一个 rangeError。我对这应该如何工作感到困惑。 @KatharineOsborne 看起来在我的实际代码中,我使用_player 作为代表玩家的私有字段,所以是的,如果你准确地复制它,你会得到一个错误。会更新。对不起! @Simon_Weaver 如果您有一个包含多个按钮的组件(不是单个按钮),您会怎么做。例如播放、暂停、停止快退、快进等?【参考方案10】:

这里是简单实用的解释:简单解释here

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

你想要调用 observable 的组件通知另一个组件按钮被点击了

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 调用吗?尝试了很多但没有工作:【参考方案11】:

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

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

然后你可以直接提供给你的组件,

constructor(private sharedService: SharedService)
 

使用共享服务,您可以使用函数,也可以创建主题以一次更新多个地点。

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

在你的列表组件中你可以发布被点击的物品信息,

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

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

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

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

【讨论】:

这是这个共享服务概念的最直接(又名简洁)的示例,并且确实应该通过投票来提高其知名度,因为没有公认的答案。【参考方案12】:

我还喜欢通过父组件通过输入和输出在 2 个兄弟姐妹之间进行通信。它比使用通用服务更好地处理 OnPush 更改通知。 或者只使用 NgRx Store。

示例。

@Component(
    selector: 'parent',
    template: `<div><notes-grid 
            [Notes]="(NotesList$ | async)"
            (selectedNote)="ReceiveSelectedNote($event)"
        </notes-grid>
        <note-edit 
            [gridSelectedNote]="(SelectedNote$ | async)"
        </note-edit></div>`,
    styleUrls: ['./parent.component.scss']
)
export class ParentComponent 

    // create empty observable
    NotesList$: Observable<Note[]> = of<Note[]>([]);
    SelectedNote$: Observable<Note> = of<Note>();

    //passed from note-grid for selected note to edit.
    ReceiveSelectedNote(selectedNote: Note) 
    if (selectedNote !== null) 
        // change value direct subscribers or async pipe subscribers will get new value.
        this.SelectedNote$ = of<Note>(selectedNote);
    
    
    //used in subscribe next() to http call response.  Left out all that code for brevity.  This just shows how observable is populated.
    onNextData(n: Note[]): void 
    // Assign to Obeservable direct subscribers or async pipe subscribers will get new value.
    this.NotesList$ = of<Note[]>(n.NoteList);  //json from server
    


//child 1 sibling
@Component(
  selector: 'note-edit',
  templateUrl: './note-edit.component.html', // just a textarea for noteText and submit and cancel buttons.
  styleUrls: ['./note-edit.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
)
export class NoteEditComponent implements OnChanges 
  @Input() gridSelectedNote: Note;

    constructor() 
    

// used to capture @Input changes for new gridSelectedNote input
ngOnChanges(changes: SimpleChanges) 
     if (changes.gridSelectedNote && changes.gridSelectedNote.currentValue !== null)       
      this.noteText = changes.gridSelectedNote.currentValue.noteText;
      this.noteCreateDtm = changes.gridSelectedNote.currentValue.noteCreateDtm;
      this.noteAuthorName = changes.gridSelectedNote.currentValue.noteAuthorName;
      
  



//child 2 sibling

@Component(
    selector: 'notes-grid',
    templateUrl: './notes-grid.component.html',  //just an html table with notetext, author, date
    styleUrls: ['./notes-grid.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
)
export class NotesGridComponent 

// the not currently selected fromt eh grid.
    CurrentSelectedNoteData: Note;

    // list for grid
    @Input() Notes: Note[];

    // selected note of grid sent out to the parent to send to sibling.
    @Output() readonly selectedNote: EventEmitter<Note> = new EventEmitter<Note>();

    constructor() 
    

    // use when you need to send out the selected note to note-edit via parent using output-> input .
    EmitSelectedNote()
    this.selectedNote.emit(this.CurrentSelectedNoteData);
    




// here just so you can see what it looks like.

export interface Note 
    noteText: string;
    noteCreateDtm: string;
    noteAuthorName: string;

【讨论】:

以上是关于Angular 2 兄弟组件通信的主要内容,如果未能解决你的问题,请参考以下文章

typescript Angular2在兄弟组件之间进行通信

typescript Angular2在兄弟组件之间进行通信

12.组件化开发2-非父子组件之间通信-祖先和后代之间的通信

Angular 2中兄弟姐妹之间的单元测试通信

React 父子组件通信

vue父子组件兄弟组件之间的通信和访问