The examples we’ve given so far have been trivial ones to introduce individual primitives. Now let’s put them all together in a more realistic example:
the autocomplete functionality you commonly find on websites—done the FRP way (see figure 6.6). When the user selects a city, the example looks up and displays some information about the city (see figure 6.7).
Figure 6.6 Auto-complete implemented with FRP
Figure 6.7 You look up and display city info once the user selects it.
Look at listings 6.12 and 6.13. debounce() is an RxJS method giving you exactly what you need: it fires if there are no events for the specified time. You use this to tell you when the user has stopped typing for 100 ms before you look up the entered text on the server.
<html>
<head>
<title>Autocomplete - Rx.JS</title>
<style>
#info { padding-top: 20px; } #city { width: 300px; }
table { border-collapse: collapse; }
table td { padding: 2px; border: 1px solid black; } </style>
<script src="rx.all.min.js"></script>
<script src="autocomplete.js"></script>
</head>
<body onload="init()">
<div>
<label for="city">City</label>
<input id="city" type="text" />
</div>
<div id="info"></div>
</body>
</html>
var jsonpCallbacks = { cntr: 0
};
function lookup(url, sRequest) { var sResponse = Rx.Observable.create(function (observer) { return sRequest.subscribe(function(req) {
var fnName = "fn" + jsonpCallbacks.cntr++, script = document.createElement("script");
script.type = "text/javascript";
script.src = url+encodeURIComponent(req) +
"&callback=jsonpCallbacks." + fnName;
jsonpCallbacks[fnName] = function(resp) { delete jsonpCallbacks[fnName];
document.body.removeChild(script);
observer.onNext([req, resp]);
};
document.body.appendChild(script);
});
}).publish();
sResponse.connect();
return sResponse;
}
Listing 6.12 autocomplete.html: text field auto-complete, FRP style
Listing 6.13 autocomplete.js
Looks up the city name on the server Constructs a
hot observable
to FRPify the I/O I/O, so you’re allowed
to be stateful
127 Example: autocomplete the FRP way
function escapeHTML(text) {
return text.replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/</g, '<') .replace(/>/g, '>');
}
function calm(s) { return s.scan([null, null], function(prev_out, thiz) { return [thiz, thiz != prev_out[0] ? thiz : null];
}).map(function(tpl) { return tpl[1];
}).filter(function(a) { return a !== null;
});
}
function currentTextOf(input) { var sKeyPresses = Rx.Observable.fromEvent(input, 'keyup'), text = new Rx.BehaviorSubject(input.value);
sKeyPresses.map(function (e) { return input.value; }).subscribe(text);
return text;
}
function autocomplete(textEdit) {
var popup = document.createElement('select');
popup.size = 15;
popup.style.position = 'absolute';
popup.style.zIndex = 100;
popup.style.display = 'none';
popup.style.width = textEdit.offsetWidth;
popup.setAttribute('id', 'popup');
document.body.appendChild(popup);
var sClicked = Rx.Observable.fromEvent(popup, 'change') .map(function (e) {
return popup.value;
});
sClicked.subscribe(function (text) { return textEdit.value = text;
});
var editText = currentTextOf(textEdit),
sKeyPresses = Rx.Observable.fromEvent(textEdit, 'keyup'), sDebounced = sKeyPresses.startWith(null).debounce(100), sTextUpdate = calm(sDebounced.withLatestFrom(editText,
function (key, text) { return text; }));
var sTabKey = sKeyPresses.filter(function(k) { return k.keyCode == 9; }),
sEscapeKey = sKeyPresses.filter(function(k) { return k.keyCode == 27; }),
sEnterKey = sKeyPresses.filter(function(k) { return k.keyCode == 13; });
var sClearPopUp = sEscapeKey.merge(sEnterKey) .merge(sClicked).map(null);
lookedUp = lookup("http://gd.geobytes.com/AutoCompleteCity?q=", Common FRP idiom:
suppresses updates that are the same as previous values
From the add example:
BehaviorSubject gives the current text of an input field.
City names selected from the pop-up
Pokes those into the text field
Fires if key presses are idle for 100 ms
Clears the pop-up if Esc or Enter is pressed or the user selects from the pop-up
sTextUpdate.merge(sTabKey.withLatestFrom(editText, function (key, text) {
return text;
} ))
).map(function (req_resp) { var req = req_resp[0],
resp = req_resp[1];
return resp.length == 1 && (resp[0] == "%s"
|| resp[0] == "" || resp[0] == req) ? null : resp;
}).merge(sClearPopUp).startWith(null);
lookedUp.subscribe(function(items) { if (items !== null) {
var html = '';
for (var i = 0; i < items.length; i++) {
html += '<option>' + escapeHTML(items[i]) + '</option>';
}
popup.innerHTML = html;
if (popup.style.display != 'block') { popup.style.left = textEdit.offsetLeft;
popup.style.top = textEdit.offsetTop +
textEdit.offsetHeight;
popup.style.display = 'block';
} } else {
popup.style.display = 'none';
} });
return sEnterKey.withLatestFrom(editText, function (key, text) { return text;
}).merge(sClicked);
}
function init() {
var cityInput = document.getElementById("city"), infoDiv = document.getElementById("info"), sEntered = autocomplete(cityInput);
lookup("http://getcitydetails.geobytes.com/GetCityDetails?fqcn=", sEntered).subscribe(function (city_info) {
var city = city_info[0], info = city_info[1];
var html = 'Information for <b>' + escapeHTML(city) + '</b>' + '<table>';
for (var key in info) {
html += '<tr><td>' + escapeHTML(key) + '</td><td>' + escapeHTML(info[key]) + '</td></tr>';
}
html += '</table>';
infoDiv.innerHTML = html;
});
}
To run this example, point your browser at sodium/book/web/autocomplete.html.
Looks up on key presses idle or the Tab key
Handles empty response cases from the server
Shows or doesn’t show the pop-up based on lookedUp
Looks up city info
129 RxJS/Sodium cheat sheet