Basil Bear
Basil Bear

Reputation: 463

Angular 2 Observable for custom component

I have made a component - let's call it CUSTOMSELECT, which takes a few inputs, e.g. the list of options for the dropdown, the default-selected option, a label and so on.

I want to use this component multiple times on some pages. For instance, I need a CUSTOMSELECT of employees, and within a nested component on the page, I need a CUSTOMSELECT of departments and another of roles.

OK, all fine so far. Each instance is correctly displayed with the desired data.

Now I want to publish any change of selection within an instance of a CUSTOMSELECT, so that I can so that I can subscribe to it. For instance, when an employee is selected, I want to refresh the rest of the page.

I created a service for this purpose. I linked the click event in the CUSTOMSELECT to a function which publishes the selected value to the service. In the parent component I have subscribed to the 'subject' so that I can action the change of employee.

That works.

But, if I then change the selection with the nested component's department CUSTOMSELECT, this value is published and the parent component's employee subscriber picks up the change and processes it. Not what I want to happen. Not at all!

So, how can I tell the observers to pay attention only to messages of interest to them? ie, how can I publish to the appropriate observer: the employee instance does not need to know that the department instance has changed value, and so on.

Thanks for any help - a whole day trying to figure this one out so far.

It may help to see the component tree like this:

employee-roles.component
  select-dropdown.component (employees)
  employee-roles-form.component
    select-dropdown.component (departments)
    select-dropdown.component (roles)

Here's the code.

select-dropdown.component.html

<div style="margin-top: 12px" class="input-group">
<span class="input-group-addon" style="height:30px">{{label}}</span>
<input type="text" class="form-control" style="height:30px" 
(change)="filterTextChanged($event.target.value)" placeholder="Enter a 
value to search for ..." />
</div>
<div style="margin-top: -6px" class="input-group">
<select class="form-control" (click)="publishChange($event)" size="5">
<option>Please select an option ...</option>
<option *ngFor="let item of list" 
  [selected]="item.dropdownValue==selected"
  [value]="item.dropdownValue"
  >{{item.dropdownText}}</option>
</select>
</div>

select-dropdown.component.ts

import { Component, EventEmitter, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { SelectDropdownService } from 'common/services/select-dropdown.service';
import { SelectModule } from 'ng2-select';
import * as _ from 'lodash';

@Component({
  inputs: ['enablefilter', 'label', 'list', 'selected'],
  outputs: ['filterTextChanged'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => SelectDropdownComponent),
      multi: true
    }
  ],
  selector: 'app-select-dropdown',
  templateUrl: './select-dropdown.component.html',
  styleUrls: ['./select-dropdown.component.scss']
})
export class SelectDropdownComponent implements ControlValueAccessor {

  propagateChange = (_: any) => { };
  private enablefilter: boolean;
  private label       : string;
  private list        : string[];
  private originalList: string[];
  private value       : any = {};

  /**
   * TODO: Need to review the methods in this class which have been copied from the web
   */
  constructor(
    public selectDropdownService: SelectDropdownService
  ) {
    this.enablefilter = false; // please do not change as would affect many forms!
  }

  /**
   * Let the parent component know that an option has been selected
   * @param event 
   */
  public publishChange(event) {
    // let target = event.target;
    let selectedValue = event.target.value;
    this.propagateChange(selectedValue);
    this.selectDropdownService.sendSelectedValue(selectedValue);
  }

select-dropdown.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class SelectDropdownService {

  className: string = this.constructor['name'];
  private dropdownValueSubject: Subject<number> = new Subject<number>();

  constructor() { }

  sendSelectedValue(id: number) {
    this.dropdownValueSubject.next(id);
  }

  getServiceObservable(): Observable<number> {
    return this.dropdownValueSubject.asObservable();
  }
}

employee-roles.component.ts - extracts (this is the parent component)

import { SelectDropdownService } from 'common/services/select-dropdown.service';

constructor(
  private activatedRoute       : ActivatedRoute,
  private sabreAPIService      : SabreAPIService,
  private selectDropdownService: SelectDropdownService
) { }

ngOnInit() {   
  this.enableEmployeeFilter = true;
  this.apiEmployeeProfile   = new EmployeeProfile(this.sabreAPIService, null);
  this.readParameters();
  this.listEmployees();
  this.observeEmployeeDropdown();
}

/**
 * Read URL parameters
 */
private readParameters() {
  this.activatedRoute.params
    .subscribe((params: Params) => {
      this.selectedEmployeeID = params['id'];
    });
}

/**
 * subscribe to change of emplyee dropdown
 */
observeEmployeeDropdown() {
  this.selectDropdownService.getServiceObservable()
    .subscribe(selectedEmployeeID => {
      this.selectedEmployeeID = selectedEmployeeID;
      this.refreshRequired();
    })
}

employee-roles.component.html - extract

<app-select-dropdown [enablefilter]="enableEmployeeFilter" label="Employees" [selected]="selectedEmployeeID" [list]="employeeList">
</app-select-dropdown>

employee-roles-form.component.html - extract (2 instances of custom component)

<app-select-dropdown label="Department" [list]="payrollDepartmentsList" [selected]="selectedDeptID"></app-select-dropdown>
<app-select-dropdown label="Role" [list]="businessRolesList" [selected]="selectedBroleID"></app-select-dropdown>

employee-roles-form.component.ts (extracts)

import { SelectDropdownService } from 'common/services/select-dropdown.service';


constructor(
  private formBuilder: FormBuilder,
  private sabreAPI: SabreAPIService,
  private selectDropdownService: SelectDropdownService
) { }

ngOnInit() {
  this.apiEmployeeRole = new EmployeeRole(this.sabreAPI, null);
  this.generateFormControls();
  this.listBusinessRoles();
  this.listPayrollDepartments();
  this.observeDepartmentDropdown();
}

observeDepartmentDropdown() {
  this.selectDropdownService.getServiceObservable()
    .subscribe(selectedDeptID => {
      this.selectedDeptID = selectedDeptID;
    })
}

Upvotes: 1

Views: 872

Answers (2)

Simon Z.
Simon Z.

Reputation: 497

There are a few options depending on your preconditions:

If you can distinguish the type of the selected dropdown value by it's ID only then just put a .filter in front of every .subscribe. e.g. if employee IDs are always less than 100:

this.selectDropdownService.getServiceObservable()
    .filter((selectedId: number) => selectedId <= 100)
    .subscribe(selectedEmployeeID => {
        this.selectedEmployeeID = selectedEmployeeID;
        this.refreshRequired();
    });

If you CAN'T determine the type by it's ID only. Then you need to emit more than just the ID. Create an interface in the service, which also holds a type:

interface SelectedItem {
    id: number;
    itemType: "employee" | "department";
}

then change your Subject<number> to Subject<SelectedItem> and your Service methods to:

sendSelectedValue(id: number, type: string) {
    this.dropdownValueSubject.next({id: id, itemType: type});
}

getServiceObservable(type: string): Observable<number> {
    return this.dropdownValueSubject.asObservable()
           .filter((item: SelectedItem) => item.itemType === type) //filter by type
           .map((item: SelectedItem) => item.id); // only return the id
}

now when you run this.selectDropdownService.getServiceObservable() just put the corresponding itemType in the parameter: this.selectDropdownService.getServiceObservable("employee") or this.selectDropdownService.getServiceObservable("department")

Of course you'd have to be aware of the type within the select component, which you can be by just passing the type as an input...

Upvotes: 1

Neil S
Neil S

Reputation: 2304

I'm not sure exactly what your potential values are of the different selects are, but is it possible for you to have some sort of logic check on the value to differentiate between your scenarios?

for instance

subscribe(selectedDeptID => {
    if (regex.test(selectedDeptID)) {
        this.selectedDeptID = selectedDeptID;
    }
})

where regex could be your matching conditional, or replace that logic with some sort of value map, etc.

Upvotes: 0

Related Questions