Jak udostępniać dane między komponentami w Angular 2?

84

W Angular 1.xx po prostu prosisz o tę samą usługę i otrzymujesz tę samą instancję, umożliwiając udostępnianie danych w usłudze.

Teraz w Angular 2 mam komponent, który ma odniesienie do mojej usługi. Potrafię czytać i modyfikować dane w serwisie, co jest dobre. Kiedy próbuję wstrzyknąć tę samą usługę w innym komponencie, wydaje się, że otrzymuję nową instancję.

Co ja robię źle? Czy sam wzorzec jest nieprawidłowy (używanie usługi do udostępniania danych), czy też muszę oznaczyć usługę jako singleton (w ramach jednej instancji aplikacji), czy coś?

Tak przy 2.0.0-alpha.27/ okazji

Wstrzykuję usługę przez appInjector(edit: now providers) w @Componentadnotacji, a następnie zapisuję odwołanie w konstruktorze. Działa lokalnie w komponencie - ale nie między komponentami (nie współużytkują tego samego wystąpienia usługi), jak myślałem, że będą.

AKTUALIZACJA : Od Angular 2.0.0 mamy teraz @ngModule, w którym można zdefiniować usługę w ramach wspomnianej providerswłaściwości @ngModule. Zapewni to przekazanie tego samego wystąpienia tej usługi do każdego komponentu, usługi itp. W tym module. https://angular.io/docs/ts/latest/guide/ngmodule.html#providers

AKTUALIZACJA : Generalnie wiele się wydarzyło w rozwoju Angulara i FE. Jak wspomniał @noririco, możesz również użyć systemu zarządzania stanem, takiego jak NgRx: https://ngrx.io/

Per Hornshøj-Schierbeck
źródło
Sześć metod udostępniania danych między komponentami kątowymi: - angulartutorial.net/2017/12/...
Prashobh
Jeśli tu dotrzesz, rozważ skorzystanie z systemu STATE Management
noririco

Odpowiedzi:

63

Serwisowy singleton to fajne rozwiązanie. W inny sposób - data/events bindings.

Oto przykład obu:

class BazService{
  n: number = 0;
  inc(){
    this.n++;
  }
}

@Component({
  selector: 'foo'
})
@View({
  template: `<button (click)="foobaz.inc()">Foo {{ foobaz.n }}</button>`
})
class FooComponent{
  constructor(foobaz: BazService){
    this.foobaz = foobaz;
  }
}

@Component({
  selector: 'bar',
  properties: ['prop']
})
@View({
  template: `<button (click)="barbaz.inc()">Bar {{ barbaz.n }}, Foo {{ prop.foobaz.n }}</button>`
})
class BarComponent{
  constructor(barbaz: BazService){
    this.barbaz = barbaz;
  }
}

@Component({
    selector: 'app',
    viewInjector: [BazService]
})
@View({
  template: `
    <foo #f></foo>
    <bar [prop]="f"></bar>
  `,
  directives: [FooComponent, BarComponent]
})
class AppComponent{}

bootstrap(AppComponent);

Oglądać na żywo

Alexander Ermolov
źródło
20
Rozgryzłem to. Wprowadzasz tylko jedną instancję usługi - w „aplikacji”. Ta sama instancja jest dziedziczona automatycznie podczas dodawania parametru do konstruktorów potomnych :) Popełniłem błąd dodając kolejną aplikację appInjector do komponentów potomnych, co tworzy nowe instancje.
Per Hornshøj-Schierbeck
1
@AlexanderCrush czy możesz zaktualizować swoją odpowiedź? Ponieważ w późniejszych wersjach alfa (alfa 30+) appInjector został usunięty . Na razie poprawną odpowiedzią powinno być użycie viewInjector.
Eric Martinez
1
@EricMartinez dzięki, odpowiedź i plunker zostały zaktualizowane.
Alexander Ermolov
1
Ciekawe źródło, aby zrozumieć, dlaczego i jak to działa: blog.oughttram.io/angular/2015/08/20/… .
soyuka
2
Miałem ten sam problem, ponieważ wstrzykiwałem usługę w głównej aplikacji, a także w samym komponencie za pomocą providers: [MyService]. Po usunięciu dostawców stała się jedyną instancją aplikacji
maufarinelli
43

Komentarz @maufarinelli zasługuje na własną odpowiedź, ponieważ dopóki go nie zobaczyłem, wciąż waliłem głową o ścianę tym problemem, nawet z odpowiedzią @Alexander Ermolov.

Problem polega na tym, że po dodaniu pliku providers do component:

@Component({
    selector: 'my-selector',
    providers: [MyService],
    template: `<div>stuff</div>`
})

Powoduje to, że nowe wystąpienie Twojej usługi zostanie wstrzyknięte ... zamiast być pojedynczym .

Więc usuń wszystkie wystąpienia providers: [MyService]w swojej aplikacji z wyjątkiem modulei to zadziała!

Serj Sagan
źródło
2
Tylko komentarz, to nigdy nie jest singleton - to tylko ta sama instancja, która jest przekazywana. Nadal możesz poprosić o nową instancję ...
Per Hornshøj-Schierbeck
10

Musisz użyć wejść i wyjść dekoratora @Component. Oto najbardziej podstawowy przykład użycia obu;

import { bootstrap } from 'angular2/platform/browser';
import { Component, EventEmitter } from 'angular2/core';
import { NgFor } from 'angular2/common';

@Component({
  selector: 'sub-component',
  inputs: ['items'],
  outputs: ['onItemSelected'],
  directives: [NgFor],
  template: `
    <div class="item" *ngFor="#item of items; #i = index">
      <span>{{ item }}</span>
      <button type="button" (click)="select(i)">Select</button>
    </div>
  `
})

class SubComponent {
  onItemSelected: EventEmitter<string>;
  items: string[];

  constructor() {
    this.onItemSelected = new EventEmitter();
  }

  select(i) {
    this.onItemSelected.emit(this.items[i]);
  }
}

@Component({
  selector: 'app',
  directives: [SubComponent],
  template: `
    <div>
      <sub-component [items]="items" (onItemSelected)="itemSelected($event)">
      </sub-component>
    </div>
  `
})

class App {
  items: string[];

  constructor() {
    this.items = ['item1', 'item2', 'item3'];
  }

  itemSelected(item: string): void {
    console.log('Selected item:', item);
  }
}

bootstrap(App);
Jan Kuri
źródło
7
Nie ma potrzeby importowania ngFor,
Richard Hamilton,
7

W szablonie komponentu nadrzędnego:

<hero-child [hero]="hero">
</hero-child>

W komponencie podrzędnym:

@Input() hero: Hero;

Źródło: https://angular.io/docs/ts/latest/cookbook/component-communication.html

Alexis Gamarra
źródło
Być może, ale wymagałoby to więcej szczegółów. W prawdziwym świecie nie jest to takie proste. wyobraź sobie, że masz klasę, którą chcesz udostępnić wielu komponentom i uzyskać dostęp do danych. To nie działa.
sancelot
Używam tego podejścia w dużym rozwiązaniu do udostępniania danych między wieloma komponentami. Możesz mieć wiele dzieci i każde otrzyma ten sam przedmiot. Czy próbowałeś to zrobić, zanim powiedziałeś, że to nie działa?
Alexis Gamarra,
Tak . to zadziała… ale z pewnymi „hackami” w celu rozwiązania niektórych problemów. Twoja odpowiedź nie pozwala nikomu jej używać.
sancelot
2

Jest wiele sposobów. Ten jest przykładem wykorzystującym propagację między elementami nadrzędnymi i podrzędnymi. To jest bardzo wydajne.

Przedstawiłem przykład, który pozwala zobaczyć użycie dwóch sposobów wiązania danych w dwóch formularzach. Jeśli ktoś może dostarczyć próbkę plunkr, byłoby to bardzo miłe ;-)

Możesz poszukać innego sposobu, korzystając z usługodawcy. Możesz również obejrzeć ten film w celach informacyjnych: ( Udostępnianie danych między komponentami w Angular )

mymodel.ts (dane do udostępnienia)

// Some data we want to share against multiple components ...
export class mymodel {
    public data1: number;
    public data2: number;
    constructor(
    ) {
        this.data1 = 8;
        this.data2 = 45;
    }
}

Pamiętaj: musi istnieć rodzic, który będzie współdzielił „mymodel” z komponentami podrzędnymi.

Komponent nadrzędny

import { Component, OnInit } from '@angular/core';
import { mymodel } from './mymodel';
@Component({
    selector: 'app-view',
    template: '<!-- [model]="model" indicates you share model to the child component -->
        <app-mychild [model]="model" >
        </app-mychild>'

        <!-- I add another form component in my view,
         you will see two ways databinding is working :-) -->
        <app-mychild [model]="model" >
        </app-mychild>',
})

export class MainComponent implements OnInit {
    public model: mymodel;
    constructor() {
        this.model = new mymodel();
    }
    ngOnInit() {
    }
}

Komponent podrzędny, mychild.component.ts

import { Component, OnInit,Input } from '@angular/core';
import { FormsModule }   from '@angular/forms'; // <-- NgModel lives here
import { mymodel } from './mymodel';

@Component({
    selector: 'app-mychild',
    template: '
        <form #myForm="ngForm">
            <label>data1</label>
            <input type="number"  class="form-control" required id="data1 [(ngModel)]="model.data1" name="data1">
            <label>val {{model.data1}}</label>

            label>data2</label>
            <input  id="data2"  class="form-control" required [(ngModel)]="model.data2" name="data2" #data2="ngModel">
            <div [hidden]="data2.valid || data2.pristine"
                class="alert alert-danger">
                data2 is required
            </div>

            <label>val2 {{model.data2}}</label>
        </form>
    ',
})

export class MychildComponent implements OnInit {
    @Input() model: mymodel ;  // Here keywork @Input() is very important it indicates that model is an input for child component
    constructor() {
    }
    ngOnInit() {
    }
}

Uwaga: w rzadkich przypadkach może wystąpić błąd podczas analizowania kodu HTML, ponieważ model nie jest „gotowy” do użycia podczas inicjalizacji strony. W takim przypadku poprzedź kod HTML warunkiem ngIf:

<div *ngIf="model"> {{model.data1}} </div>
sancelot
źródło
1

To zależy, czy istnieje prosty przypadek

a) A -> B -> C A ma dwa potomne B i C i jeśli chcesz udostępniać dane między A i B lub A i C, użyj (wejście / wyjście)

Jeśli chcesz udostępniać pliki między B i C, możesz również użyć (wejście / wyjście), ale zaleca się użycie usługi.

b) Jeśli drzewo jest duże i złożone. (jeśli jest tak wiele poziomów połączeń rodziców i dzieci). W tym przypadku, jeśli chcesz udostępniać dane, sugerowałbym ngrx

Implementuje architekturę flux, która tworzy magazyn po stronie klienta, do którego może subskrybować dowolny komponent i który może aktualizować bez tworzenia warunków wyścigu.

Pratiyush
źródło