Связь двух компонентов в Angular

118

У меня есть ListComponent. При щелчке по элементу в ListComponent подробности этого элемента должны отображаться в DetailComponent. Оба отображаются на экране одновременно, поэтому маршрутизация не требуется.

Как мне сообщить DetailComponent, какой элемент в ListComponent был нажат?

Я подумал о том, чтобы передать событие родительскому элементу (AppComponent), и чтобы родитель установил selectedItem.id на DetailComponent с @Input. Или я мог бы использовать общий сервис с наблюдаемыми подписками.


РЕДАКТИРОВАТЬ: установка выбранного элемента через событие + @Input не запускает компонент DetailComponent, хотя на тот случай, если мне понадобится выполнить дополнительный код. Так что я не уверен, что это приемлемое решение.


Но оба эти метода кажутся намного более сложными, чем способ выполнения действий в Angular 1, который осуществлялся либо через $ rootScope. $ Broadcast, либо через $ scope. $ Parent. $ Broadcast.

Поскольку все в Angular 2 является компонентом, я удивлен, что больше нет информации о взаимодействии компонентов.

Есть ли другой / более простой способ сделать это?

dennis.sheppard
источник
Вы нашли способ обмена данными между братьями и сестрами? Мне это нужно как наблюдаемое ..
Человек

Ответы:

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. Все, что изменилось, - это операторы импорта, так что это все, что вам нужно обновить, если вы случайно использовали исходную версию.
Алекс Дж
источник
2
Это все еще действительно для angular-rc1?
Серхио
4
Однако я не верю, что это уведомляет брата или сестру о том, что что-то обновилось в общей службе. Если дочерний компонент1 делает что-то, на что дочерний компонент2 должен ответить, этот метод этого не обрабатывает. Я считаю, что это с наблюдаемыми?
dennis.sheppard
1
@Sufyan: Я собираюсь предположить, что добавление полей провайдера к дочерним элементам заставляет Angular создавать новые частные экземпляры для каждого из них. Когда вы их не добавляете, они используют экземпляр «singleton» родителя.
Ральф
1
Похоже, это больше не работает с последними обновлениями
Суфьян Джабр
3
Это устарело. directivesбольше не объявляются в компоненте.
Nate Gardner
26

В случае двух разных компонентов (не вложенных компонентов, 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);
  }

}

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

Источник: Родители и дети общаются через службу

Дуди
источник
2
Хотел бы вы добавить терминологию к этому ответу. Я считаю, что это соответствует RxJS, паттерну Observable и т. Д., Не совсем уверен, но добавление описаний к некоторым из них было бы полезно для людей (таких как я).
karns
13

Один из способов сделать это - использовать общий сервис .

Однако я считаю следующее решение намного проще: оно позволяет обмениваться данными между двумя братьями и сестрами (я тестировал это только на 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> 

приложение-sibling2.component.ts

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

export class AppSibling2Component {
   ...
   @Input() data: AppSibling1Component;
   ...
}
Джанер
источник
Разве это не противоречит идее слабой связи и, следовательно, компонентов?
Робин
Кто-нибудь знает, чистый это или грязный путь? Кажется, намного проще обмениваться данными в одном направлении, например, только от sibiling1 к sibiling2, но не наоборот
Сара
9

Об этом идет обсуждение.

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

Ответ Alex J хорош, но он больше не работает с текущим Angular 4 по состоянию на июль 2017 года.

И эта плункерная ссылка продемонстрирует, как общаться между братьями и сестрами, используя общие службы и наблюдаемые.

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

Жоао Силва
источник
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.component, где youtube-player, очевидно, находится мой компонент, который обертывает Youtube API.

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

... lots more DOM ...

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

Директива подключает все для меня, и мне не нужно объявлять событие (щелчок) в HTML.

Таким образом, директива может легко подключаться к видеоплееру без необходимости участия в ProductPageкачестве посредника.

Я впервые это сделал, поэтому еще не уверен, насколько это может быть масштабируемым для более сложных ситуаций. Хотя я счастлив и это оставляет мой HTML простым и четким.

Simon_Weaver
источник
Одна из наиболее важных концепций angular, которую нужно понять, - это то, что компонент - это просто директива с шаблоном. Как только вы действительно поймете, что это значит, директивы не будут такими уж страшными - и вы поймете, что можете применить их к любому элементу, чтобы привязать к нему поведение.
Simon_Weaver 07
Я пробовал это, но получаю ошибку повторяющегося идентификатора для эквивалента 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, который работал нормально. Но у моего приложения есть ленивые загружаемые маршруты (используются Providedin: 'root') и HTTP-вызовы для установки и получения. Не могли бы вы помочь мне с HTTP-вызовами. много пробовал, но не работало:
Кшри
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/

Vereb
источник
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-адреса и обновляю noid из объекта поведения.

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

ngOnInit(): void { 
 const id = +this.getId ();
 this.taskService.newId(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']); 
              } 
          }) 
    }) 
}
ValRob
источник
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 для получения дополнительной информации.

micronyks
источник
0

Я передавал методы установки от родительского элемента одному из его дочерних элементов через привязку, вызывая этот метод с данными из дочернего компонента, что означает, что родительский компонент обновляется и затем может обновлять свой второй дочерний компонент новыми данными. Однако для этого требуется привязка this или использование функции стрелки.

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

Я не совсем уверен, что это лучшая практика, было бы интересно услышать мнение других по этому поводу.

Thingamajig
источник