3

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);
    }
}
Sarvesh Yadav
  • 2,600
  • 7
  • 23
  • 40
Deej
  • 137
  • 2
  • 9
  • I should note on this that I was able to work around this by just subscribing in ngOnInit and assigning to a variable, and then setting the "selectingCondition" to it. I'm still curious what is going on in my question example, though. – Deej Oct 10 '16 at 03:16

2 Answers2

3

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.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • ReplaySubject seems to work. I think I will go ahead and stick with ngOnInit, though, since that seems to kinda sorta be best practice, but I'll switch to a ReplaySubject for that property. Thanks! – Deej Oct 10 '16 at 03:56
1

As @peeskillet suggests, the use of Subject might be the problem here. For the usecase you described, I usually use a BehaviorSubject. You can initialize it with null if there is no better option.

Actually, I do think that in most cases you want to use a BehaviorSubject over a Subject (or any other *Subject), if you are sharing values between components / services. It just gives you the 'current/latest' value. Here is a nice post on angular-university about that matter.

Ramon Rambo
  • 145
  • 2
  • 8