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.