nathanielb91
nathanielb91

Reputation: 83

How can two observables run sequentially in function?

I am wanting to have this function getUpcomingEvents() run when a button is clicked. I put comments in the function to help explain it, but the gist is that it takes in a string of a U.S. city and state selected by the user and returns a list of all upcoming events in that city. As you can see below, there are two observable subscriptions, however the problem is that when you click the button that triggers this function call, the desired array return does not get returned until you click the button two or three times. I believe this is a timing issue with the observables happening one after another. I realize that this function may be too long and I'm wondering if I need to also use switchMap or something in here to get this all working properly. If anything is unclear here let me know and I can clarify. Didn't include HTML as it is just a button calling the function with the city and string passed in the function.

  citySearchURL = '';
  metroAreaSearchURL: any;
  metroID = '';
  metroAreaResults: any = [];
  cityResult: any;
  upcomingMetroEvents: any[];
  upcomingCityEvents: any[];
  stateAbbrvs = {
    'AL': 'Alabama',
    'AK': 'Alaska',
    'AS': 'American Samoa',
    'AZ': 'Arizona',
    'AR': 'Arkansas',
    'CA': 'California',
    'CO': 'Colorado',
    'CT': 'Connecticut',
    'DE': 'Delaware',
    'DC': 'District Of Columbia',
    'FM': 'Federated States Of Micronesia',
    'FL': 'Florida',
    'GA': 'Georgia',
    'GU': 'Guam',
    'HI': 'Hawaii',
    'ID': 'Idaho',
    'IL': 'Illinois',
    'IN': 'Indiana',
    'IA': 'Iowa',
    'KS': 'Kansas',
    'KY': 'Kentucky',
    'LA': 'Louisiana',
    'ME': 'Maine',
    'MH': 'Marshall Islands',
    'MD': 'Maryland',
    'MA': 'Massachusetts',
    'MI': 'Michigan',
    'MN': 'Minnesota',
    'MS': 'Mississippi',
    'MO': 'Missouri',
    'MT': 'Montana',
    'NE': 'Nebraska',
    'NV': 'Nevada',
    'NH': 'New Hampshire',
    'NJ': 'New Jersey',
    'NM': 'New Mexico',
    'NY': 'New York',
    'NC': 'North Carolina',
    'ND': 'North Dakota',
    'MP': 'Northern Mariana Islands',
    'OH': 'Ohio',
    'OK': 'Oklahoma',
    'OR': 'Oregon',
    'PW': 'Palau',
    'PA': 'Pennsylvania',
    'PR': 'Puerto Rico',
    'RI': 'Rhode Island',
    'SC': 'South Carolina',
    'SD': 'South Dakota',
    'TN': 'Tennessee',
    'TX': 'Texas',
    'UT': 'Utah',
    'VT': 'Vermont',
    'VI': 'Virgin Islands',
    'VA': 'Virginia',
    'WA': 'Washington',
    'WV': 'West Virginia',
    'WI': 'Wisconsin',
    'WY': 'Wyoming'
};


  constructor(private http: HttpClient) { }

  // Function takes in a user-selected city and a state separated by a comma.
  // Function returns all upcoming events that match the city name.
  getUpcomingEvents(cityAndState: string) {
    const cityStateSplit = cityAndState.split(', ');
    const city = cityStateSplit[0];
    const state = cityStateSplit[1];

    // Create URL to send to songkick API that returns all areas that match the city name.
    this.citySearchURL = 'https://api.songkick.com/api/3.0/search/locations.json?query=' + city + '&apikey=xxxxxxxx';
    const metroAreaObservable = this.http.get(this.citySearchURL);
    metroAreaObservable.subscribe(
      data => {
        // Songkick returns two objects - a city object and a metro area object - that matches the city used in the citySearchURL,
        // one that has city info and one that has it's respective metro area info. Only the metro area object
        // has an "id", which can in turn be used to search for events in that area.
        // Here I assign these two objects to metroAreaResults.
        this.metroAreaResults = data.resultsPage.results.location;

        // Because there may be some cities returned that don't match the desired state as well, Check if user-selected city
        // matches songkicks returned city and state, and pull metro id.
        // The metro id will be used to create a url for another API request.
        for (let i = 0; i < this.metroAreaResults.length; i++) {
          if ((this.metroAreaResults[i].city.displayName === city) &&
            (this.stateAbbrvs[this.metroAreaResults[i].city.state.displayName] === state)) {
              this.cityResult = this.metroAreaResults[i];
              this.metroID = (this.metroAreaResults[i].metroArea.id.toString());
              this.metroAreaSearchURL = 'https://api.songkick.com/api/3.0/metro_areas/' +
                this.metroID + '/calendar.json?apikey=xxxxxxxx';
          }
        }
      },
      error => {
        throw error;
      },
      () => {   }
    );

    const eventsObservable = this.http.get(this.metroAreaSearchURL);
    eventsObservable.subscribe(
      data => {
        // Array of all upcoming events in metro area
        this.upcomingMetroEvents = Array.of(data)[0].resultsPage.results.event;
      },
      error => {
        throw error;
      },
      () => { }
    );

    // Because the end goal of this entire function is to return all of the upcoming events for the
    // user-selected CITY (not metro area), this for loop checks that the upcoming event's city location
    // matches the user selected city, and adds it to an array. After for loop, array gets returned.
    for (let i = 0; i < this.upcomingMetroEvents.length; i++) {
      if ((this.upcomingMetroEvents[i].location.city.split(', ')[0]) === city) {
        this.upcomingCityEvents.push(this.upcomingMetroEvents[i]);
      }
    }
    return this.upcomingCityEvents;
  }

Upvotes: 1

Views: 149

Answers (1)

MoxxiManagarm
MoxxiManagarm

Reputation: 9124

This is a typical asynchronous issue. You create the second observable before the callback of the first observable run and you second observable depends on the first.

I tried to refactor your code, that is my result:

getUpcomingEvents(cityAndState: string) {
    const [city, state] = cityAndState.split(', ');

    this.http.get(`https://api.songkick.com/api/3.0/search/locations.json?query=${city}&apikey=xxxxxxxx`).pipe(
      map(data => data.resultsPage.results.location),
      map(metroAreaResults => metroAreaResults.filter(metroAreaResult => metroAreaResult.city.displayName === city)),
      map(metroAreaResults => metroAreaResults.filter(metroAreaResult => this.stateAbbrvs[metroAreaResult.city.state.displayName] === state)),
      map(metroAreaResults => metroAreaResults.map(metroAreaResult => this.http.get(`https://api.songkick.com/api/3.0/metro_areas/${metroAreaResult.metroArea.id.toString()}/calendar.json?apikey=xxxxxxxx`))),
      switchMap(metroAreaResultsRequests => forkJoin(metroAreaResultsRequests.pipe(map(data => data.resultsPage.results.event)))),
      map(upcomingEvents => upcomingEvents.flat()),
    ).subscribe(
      upcomingEvents => this.upcomingCityEvents = upcomingEvents,
      error => throw error,
      () => {},
    );
  }

Upvotes: 1

Related Questions