Using Subject for cascading lists

So, what's the point? Why should we use Subjects over Observables? That's actually a quite deep question. There are many ways of solving most streaming-related problems; problems where it is tempting to use a Subject can often be solved through some other way. Let's have a look at what you could be using it for, though. Let's talk about cascading drop-down lists. What we mean by that is that we want to know what restaurants exist in a city. Imagine, therefore, that we have a drop-down list that allows us to select what country we are interested in. Once we select a country, we should select the city we are interested in from a drop-down list of cities. Thereafter, we get to select from a list of restaurants, and, finally, pick the restaurant that interests us. In the markup, it most likely looks like this:

// subjects/cascading.html

<html>
<body>
<select id="countries"></select>
<select id="cities"></select>
<select id="restaurants"></select>

<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
<script src="cascadingIV.js"></script>
</body>
</html>

At the start of the application, we haven't selected anything, and the only drop-down list that is selected is the first one, and it is filled with countries. Imagine that we therefore set up the following code in JavaScript:

// subjects/cascadingI.js

let countriesElem = document.getElementById("countries");
let citiesElem = document.getElementBtyId("cities");
let restaurantsElem = document.getElementById("restaurants");

// talk to /cities/country/:country, get us cities by selected country
let countriesStream = Rx.Observable.fromEvent(countriesElem, "select");

// talk to /restaurants/city/:city, get us restaurants by selected restaurant
let citiesStream = Rx.Observable.fromEvent(citiesElem, "select");

// talk to /book/restaurant/:restaurant, book selected restaurant
let restaurantsElem = Rx.Observable.fromEvent(restaurantsElem, "select");

At this point, we have established that we want to listen to the selected events of each drop-down list, and we want, in the cases of countries or cities droplist, filter the upcoming droplist. Say we select a specific country then we want to repopulate/filter the cities droplist so that it only shows cities for the selected country. For the restaurant drop-down list, we want to perform a booking based on our restaurant selection. Sounds pretty simple, right? We need some subscribers. The cities drop-down list needs to listen to changes in the countries drop-down list. So we add that to our code:

// subjects/cascadingII.js

let countriesElem = document.getElementById("countries");
let citiesElem = document.getElementBtyId("cities");
let restaurantsElem = document.getElementById("restaurants");

fetchCountries();

function buildList(list, items) {
list.innerHTML ="";
items.forEach(item => {
let elem = document.createElement("option");
elem.innerHTML = item;
list.appendChild(elem);
});
}

function fetchCountries() {
return Rx.Observable.ajax("countries.json")
.map(r => r.response)
.subscribe(countries => buildList(countriesElem, countries.data));
}

function populateCountries() {
fetchCountries()
.map(r => r.response)
.subscribe(countries => buildDropList(countriesElem, countries));
}

let cities$ = new Subject();
cities$.subscribe(cities => buildList(citiesElem, cities
));

Rx.Observable.fromEvent(countriesElem, "change")
.map(ev => ev.target.value)
.do(val => clearSelections())
.switchMap(selectedCountry => fetchBy(selectedCountry))
.subscribe( cities => cities$.next(cities.data
));

Rx.Observable.from(citiesElem, "select");

Rx.Observable.from(restaurantsElem, "select");

So, here, we have a behavior of performing an AJAX request when we select a country; we get a filtered list of cities, and we introduce the new subject instance cities$.  We call the next() method on it with our filtered cities as a parameter. Finally, we listen to changes to the cities$ stream by calling the subscribe() method on the stream. As you can see, when data arrives, we rebuild our cities drop-down list there.

We realize that our next step is to react to changes from us doing a selection in the cities drop-down list. So, let's set that up:

// subjects/cascadingIII.js

let countriesElem = document.getElementById("countries");
let citiesElem = document.getElementBtyId("cities");
let restaurantsElem = document.getElementById("restaurants");

fetchCountries();

function buildList(list, items) {
list.innerHTML = "";
items.forEach(item => {
let elem = document.createElement("option");
elem.innerHTML = item;
list.appendChild(elem);
});
}

function fetchCountries() {
return Rx.Observable.ajax("countries.json")
.map(r => r.response)
.subscribe(countries => buildList(countriesElem, countries.data));
}

function populateCountries() {
fetchCountries()
.map(r => r.response)
.subscribe(countries => buildDropList(countriesElem, countries));
}

let cities$ = new Subject();
cities$.subscribe(cities => buildList(citiesElem, cities));

let restaurants$ = new Rx.Subject();
restaurants$.subscribe(restaurants => buildList(restaurantsElem, restaurants
));

Rx.Observable.fromEvent(countriesElem, "change")
.map(ev => ev.target.value)
.do( val => clearSelections())
.switchMap(selectedCountry => fetchBy(selectedCountry))
.subscribe( cities => cities$.next(cities.data));

Rx.Observable.from(citiesElem, "select")
.map(ev => ev.target.value)
.switchMap(selectedCity => fetchBy(selectedCity))
.subscribe( restaurants => restaurants$.next(restaurants.data));

// talk to /book/restaurant/:restaurant, book selected restaurant
Rx.Observable.from(restaurantsElem, "select");

In the preceding code, we added some code to react to a selection being made in our cities drop-down list. We also added some code to listen to changes in the restaurants$ stream, which finally led to our restaurants drop-down list being repopulated. The last step is to listen to changes on us selecting a restaurant in the restaurants drop-down list. What should happen here is up to you, dear reader. A suggestion is that we query some API for the selected restaurant's opening hours, or its menu. Use your creativity. We will leave you with some final subscription code, though:

// subjects/cascadingIV.js

let cities$ = new Rx.Subject();
cities$.subscribe(cities => buildList(citiesElem, cities));

let restaurants$ = new Rx.Subject();
restaurants$.subscribe(restaurants => buildList(restaurantsElem, restaurants));

function buildList(list, items) {
list.innerHTML = "";
items.forEach(item => {
let elem = document.createElement("option");
elem.innerHTML = item;
list.appendChild(elem);
});
}

function fetchCountries() {
return Rx.Observable.ajax("countries.json")
.map(r => r.response)
.subscribe(countries => buildList(countriesElem, countries.data));
}

function fetchBy(by) {
return Rx.Observable.ajax(`${by}.json`)
.map(r=> r.response);
}

function clearSelections() {
citiesElem.innerHTML = "";
restaurantsElem.innerHTML = "";
}

let countriesElem = document.getElementById("countries");
let citiesElem = document.getElementById("cities");
let restaurantsElem = document.getElementById("restaurants");

fetchCountries();

Rx.Observable.fromEvent(countriesElem, "change")
.map(ev => ev.target.value)
.do(val => clearSelections())
.switchMap(selectedCountry => fetchBy(selectedCountry))
.subscribe(cities => cities$.next(cities.data));

Rx.Observable.fromEvent(citiesElem, "change")
.map(ev => ev.target.value)
.switchMap(selectedCity => fetchBy(selectedCity))
.subscribe(restaurants => restaurants$.next(restaurants.data));

Rx.Observable.fromEvent(restaurantsElem, "change")
.map(ev => ev.target.value)
.subscribe(selectedRestaurant => console.log("selected restaurant", selectedRestaurant));

This became a quite long code example, and it should be said that this is not the best way of solving a problem like this, but it does demonstrate how a Subject works: it can add value to the stream when it wants, and it can be subscribed to.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.140.88