1

I am trying to figure out cross component communication in Angular 2 and failing right now. Basically I have 3 components: a AddProduct component which in this case is the parent, a CategorySelector component which uses a service to populate a dropdown with a list of categories, and a ProductSelector component which takes a category as a parameter and populates a dropdown with a list of Products that belong to the selected category.

What I am trying to figure out is how to make it so that when the CategorySelector changes the ProductSelector runs the function necessary to get the new list of products.

Here is my code:

add-product.html

<h1 class="ui header">Add product</h1>

<form class="ui form">
  <div class="four wide field">
    <label>Category</label>
    <category-selector (selection)="setCategory($event)" defaultText="Please Choose a Category"></category-selector>
  </div>
  <div class="four wide field" *ngIf="selectedCategory">
    <label>Product</label>
    <product-selector (selection)="setProduct($event)" [category]="selectedCategory" defaultText="Select a Product"></product-selector>
  </div>
</form>

add-product.component.ts

import {Component, OnInit, NgZone} from 'angular2/core';
import {StoreProduct} from './storeproduct.service';
import {Product} from './product.service';
import {CategorySelector} from './category-selector.component';
import {ProductSelector} from './product-selector.component';

declare var __resourcePath: string;

@Component({
    selector: 'add-product',
    templateUrl: __resourcePath + '/html/add-product.html',
    providers: [Product, StoreProduct],
    directives: [CategorySelector, ProductSelector]
})
export class AddProduct {

    public categories: string[];
    public selectedCategory: string;
    public selectedProduct: Product__c;

    constructor(private storeproduct: StoreProduct, private product: Product, private zone: NgZone) {}

    setCategory(selection: string) {
        this.selectedCategory = selection;
    }

    setProduct() {

    }

}

product-selector.component.ts

import {Component, Input, Output, EventEmitter, OnInit} from 'angular2/core';
import {Product} from './product.service';

@Component({
    selector: 'product-selector',
    template: `
        <select #sel (change)="selection.emit(sel.value)" class="ui fluid dropdown">
            <option value="" selected>{{defaultText}}</option>
            <option *ngFor="#product of products" value="{{product}}">{{product.Name}}</option>
        </select>
    `,
    providers: [Product]
})
export class ProductSelector implements OnInit {

    @Output() selection = new EventEmitter();
    @Input() defaultText: string = 'No product selected';
    @Input() category: string;

    private products: Product__c[];

    constructor(private product: Product) {}

  fetchProducts() {
    let source = this.product.fetch(this.category);
        let sub = source.toPromise().then((val: JSForce.SOQLQueryResult<Product__c>) => {
            this.products = val.records;
        });
  }

    ngOnInit(): any {
    this.fetchProducts();
    }

}

category-selector.component.ts

import {Component, Input, Output, EventEmitter, OnInit} from 'angular2/core';
import {StoreProduct} from './storeproduct.service';

@Component({
    selector: 'category-selector',
    template: `
        <form class="ui form">
            <select #sel (change)="selection.emit(sel.value)" class="ui fluid dropdown">
                <option value="" selected>{{defaultText}}</option>
                <option *ngFor="#category of categories" value="{{category}}">{{category}}</option>
            </select>
        </form>
    `,
    providers: [StoreProduct]
})
export class CategorySelector implements OnInit {

    @Output() selection = new EventEmitter();
    @Input() defaultText: string = 'No category selected';

    categories: string[];

    constructor(private sp: StoreProduct) {}

    ngOnInit(): any {

        let source = this.sp.fetchCategories();
        let sub = source.toPromise().then((val: string[]) => {
            this.categories = val;
        });
    }

}
watzon
  • 2,179
  • 1
  • 26
  • 56
  • 2
    You could try implementing the ngOnChanges lifecycle hook on ProductSelector, and run `fetchProducts` whenever its category input property changes - I think that'll work. – rrjohnson85 Mar 23 '16 at 00:13
  • @rrjohnson85 I literally just found that! Where has this been for the past week?! Haha – watzon Mar 23 '16 at 00:14
  • Haha, I just stumbled upon it a week or so ago when doing something similar. I need to explore the hooks in more depth, I'm sure they'll come in handy sooner than later. – rrjohnson85 Mar 23 '16 at 00:16
  • @rrjohnson85 do you know if there is an actual way to send an event from A to B rather than just listening for the change? – watzon Mar 23 '16 at 00:18
  • 1
    I don't know for certain, but I suspect you could do it with a service - so it would be more of an A to Service to B type workflow rather than simply A to B. If there is another way, I haven't found it yet, but please let me know if you do. – rrjohnson85 Mar 23 '16 at 00:23
  • Actually, what if you used a getter/setter for category? Then when setting the category from the parent, you could call fetchProducts from within the setter. – rrjohnson85 Mar 23 '16 at 00:27

1 Answers1

2

Since component A and component B are siblings, and you want A to notify B, you have two choices:

  • Emit an event/value from A up to the parent, then databind to an input property on B. If you need to run some logic in B when the value changes, implement lifecycle hook ngOnChanges() (as @rrhohnson85 already mentioned in a comment). I don't recommend triggering this logic inside of a setter.
  • Use a shared service (which was also already mentioned by @rrhohnson85 in a comment) with a Subject or an Observable. Component A will call a method on the service that calls next() on the Subject or Observable. Component B will subscribe() to the Subject or Observable to be notified of changes/events.
    See the cookbook for an example of using a Subject.
    See this SO question, Delegation: EventEmitter or Observable in Angular2, for an example using an Observable.
Community
  • 1
  • 1
Mark Rajcok
  • 348,511
  • 112
  • 482
  • 482