Deej Deej - 1 month ago 21
AngularJS Question

Angular 2 RxJS Observable Skipping Subscribe on First Call

I'm using a shared service to communicate between a number of components which reside within a modal window, and I'm attempting to get a value which is set within the modal in the component within which it resides.

However, when I subscribe to the getSelectedSourceField function in my service, it skips over the subscribe the first time a user selects a source in the modal, but subsequent times it works as expected (i.e. returning the selected field in the success callback).

Any thoughts?

My service:

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { Source, SourceField } from '../source/source';
import { DataService } from './dataService';

@Injectable()
export class SFieldSelectService {
private selectedSource: Source = null;
private selectedSourceField: SourceField = null;
private filteredSourceFields: SourceField[] = [];

private selectedSourceSubj = new Subject<Source>();
private selectedSourceFieldSubj = new Subject<SourceField>();
private filteredSourceFieldsSubj = new Subject<SourceField[]>();
private hasSourceFieldSubj = new Subject<boolean>();

//Source methods
setSelectedSource(source: Source) {
this.selectedSource = source;
this.selectedSourceSubj.next(source);

//Set the availabel source fields
this._dataService.GetSingle('SourceSelect', this.selectedSource["sourceId"])
.subscribe(sourceFields => {
this.filteredSourceFields = sourceFields
this.filteredSourceFieldsSubj.next(sourceFields);
});

this.hasSourceFieldSubj.next(false);
}
getSelectedSource(): Observable<Source> {
return this.selectedSourceSubj.asObservable();
}

//Sourcefield methods
setSelectedSourceField(sourceField: SourceField) {
this.selectedSourceField = sourceField;
this.selectedSourceFieldSubj.next(sourceField);

this.hasSourceFieldSubj.next(true);
}
getSelectedSourceField(): Observable<SourceField> {
return this.selectedSourceFieldSubj.asObservable();
}

//Filtered sourcefields
getFilteredSourceFields(): Observable<SourceField[]> {
return this.filteredSourceFieldsSubj.asObservable();
}

//Whether or not the modal has a selected sourcefield
hasSelectedSourceField(): Observable<boolean> {
return this.hasSourceFieldSubj.asObservable();
}

constructor(private _dataService: DataService) {}
}


Component where I'm subscribing:

import { Component, ViewChild, OnInit } from '@angular/core';
import { DataService } from '../../services/dataService';
import { Response, Headers } from '@angular/http';
import { Condition } from '../transformation';
import { NgbModal, ModalDismissReasons } from '@ng-bootstrap/ng-bootstrap';
import { SourceFieldListComponent } from '../../source/selection/sourcefield-list.component';
import { SourceListComponent } from '../../source/selection/source-list.component';
import { SFieldSelectService } from '../../services/source-select.service';

@Component({
selector: 'condition-addedit',
templateUrl: 'app/transformation/condition/condition-addedit.component.html',
providers: [DataService, SFieldSelectService]
})
export class ConditionAddEditComponent implements OnInit {
active: boolean = true;
condSeqCount = 1;
selectingCondition: Condition;
hasSelectedSourceField: boolean = false;

//List of Conditions currently in the add/edit list
public conditions: Condition[] = [];

//Modal Functions
closeResult: string;
openSourceSelect(content, condition) {
this.selectingCondition = condition;
this.modalService.open(content, { size: 'lg' }).result.then((result) => {
//User selected source field in modal
if (result == 'Select SField') {
this.selectService.getSelectedSourceField()
.subscribe(sourceField => { <--- SKIPS THIS THE FIRST TIME BUT NOT AFTER
alert(sourceField.name);
this.selectingCondition.sourceField = sourceField
}
, (error) => alert(error));
}
}, (reason) => {
this.closeResult = `Dismissed ${this.getDismissReason(reason)}`;
});
}

private getDismissReason(reason: any): string {
if (reason === ModalDismissReasons.ESC) {
return 'by pressing ESC';
} else if (reason === ModalDismissReasons.BACKDROP_CLICK) {
return 'by clicking on a backdrop';
} else {
return `with: ${reason}`;
}
}

//Add a new condition to the list of conditions
addCondition() {
this.conditions.push(new Condition(this.condSeqCount++, (this.condSeqCount == 1) ? '' : 'or', '', '', '', '', null));
}

constructor(private _dataService: DataService, private modalService: NgbModal, private selectService: SFieldSelectService) {}
ngOnInit(): void {
this.selectService.hasSelectedSourceField().subscribe(hasSelectedSourceField => this.hasSelectedSourceField = hasSelectedSourceField);
}
}

Answer

Maybe the ngOnInit is the better approach, not sure. But the reason for the problem is the use of Subject. Once you emit something, if there is no one subscribed at the time of emission, that emission is lost forever.

This is a situation where ReplaySubject can be useful. It allows you to set a buffer size, and if there is anything in the buffer when someone subscribes, they all emissions from that buffer. In many cases you will just want the buffer size to be one, as you only want the last one (most recent) buffered.

import { ReplaySubject } from 'rxjs/ReplaySubject';

class Service {
  something = new ReplaySubject<Source>(1); // buffer size 1
}

With this, you don't need to try and keep a field in the service of the most recent emission. It is already stored in the subject itself.

Comments