Example: autocomplete the FRP way

Một phần của tài liệu Manning functional reactive programming (Trang 148 - 152)

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, '&amp;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;');

}

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

Một phần của tài liệu Manning functional reactive programming (Trang 148 - 152)

Tải bản đầy đủ (PDF)

(362 trang)