89 RX – Observable fromEvent ascoltando il keyup
function sarchBooks() { const searchEle = document.querySelector('#search'); if (searchEle) { getBooks('game of thrones'); } }
eravamo rimasti con questa funzione, importiamo da rxjs formEvent, prima dell’if di controllo
const {formEvent} = rxjs;
fromEvent crea un observable da un evento.
Prima dell’esecuzione della funzione getBooks, passiamo a fromEvent l’elemento sul quale costruire l’observable, nel nostro caso searchEle e come secondo parametro l’evento da ascoltare, nel nostro caso keyup che è l’evento di scrittura dei caratteri
fromEvent(searchEle, 'keyup')
a questo punto possiamo già inserire la subscribe, che per verifica farà partire un alert di test
function searchBooks() { var searchEle = document.querySelector('#search'); var fromEvent = rxjs.fromEvent; if (searchEle) { fromEvent(searchEle, 'keyup') .subscribe(function (ele) { return alert(ele.target.value); }); getBooks('game of thrones'); } } searchBooks();
Dopo aver verificato che i caratteri inseriti nel campo ricerca vengono stampati a video dall’alert, passiamo ad impostare la regola che la stringa viene catturata solo dopo una lunghezza di tre caratteri, questo lo possiamo fare utilizzando l’operatore filter utilizzando il pipe per intervenire sul flusso dei dati.
.pipe( filter( ele => ele.target.value.length > 2) )
ovviamente bisogna importare l’operatore filter
const {filter} = rxjs.operators;
per non dover calcolare sempre ele.target.value e poi la lunghezza, possiamo snellire i dati mappando la proprietà target.value con map in quanto non ci interessano le altre proprietà dell’elemento, ma solamente il value
map( ele => ele.target.value),
Con il map riceveremo un elemento di tipo stringa, a questo punto sul filter modifichiamo dicendo che l’elemento è di tipo string e accorciamo la proprietà dell’elemento importando anche l’operatore map da rxjs. Tipizziamo i dati dicendo che ele è di tipo any.
const {filter, map} = rxjs.operators; if (searchEle) { fromEvent(searchEle, 'keyup') .pipe( map( (ele:any) => ele.target.value), filter( (ele: string) => ele.length > 2) ) .subscribe( (ele: string) => alert(ele)); getBooks('game of thrones'); }
Ora se andiamo a provare ad inserire i caratteri, l’alert scatterà quando ne avremo inserito almeno 3.
Adesso dobbiamo passare il parametro inserito nel campo ricerca del browser e che otteniamo con la subscribe, all’altra chiamata, quella iniziale che ottiene i records, richiamati dalla funzione getBooks.
Spostiamo quindi la subscribe che ritorna la promise ( .subscribe( (book: Book) => displayBook(book) ); ) nella funzione di ricerca searchBooks() al posto della subscribe che stampa l’alert
function searchBooks() { const searchEle = document.querySelector('#search'); const {fromEvent} = rxjs; const {filter, map} = rxjs.operators; if (searchEle) { fromEvent(searchEle, 'keyup') .pipe( map( (ele:any) => ele.target.value), filter( (ele: string) => ele.length > 2) ) .subscribe( (book: Book) => displayBook(book) ); //.subscribe( (ele: string) => alert(ele)); getBooks('game of thrones'); } }
adesso il problema da risolvere è quello di direzionare la stringa superiore a 3 caratteri che stiamo ricevendo dal campo di ricerca nella subscribe che lancia il metodo displayBook() e nella funzione getBook() dobbiamo ritornare non più la subscribe, ma l’ observable, aggiungendo il return sul from.
return from(p)
Ritornando l’observable possiamo utilizzare dopo il filter che filtra l’elemento solo a quelli con almeno 3 caratteri, l’operatore switchMap, ovviamente importandolo da rxjs anch’esso
const {filter, map, switchMap} = rxjs.operators;
L’ operatore switchMap riceverà l’elemento di tipo stringa con almeno 3 caratteri e lo passerà al metodo getBooks che restituirà un observable che sarà risolto dalla subscribe passata alla funzione searchBooks()
switchMap( (ele:string) => getBooks(ele) )
Testiamo se funziona con una ricerca di prova e notiamo che avremo i nostri risultati a browser. Ispezionando vedremo le chiamate effettuate e nella risposta avremo restituite tutte le proprietà dei libri e quindi degli items, come il kind, il volumeInfo, description, title…. Però il problema è che mentre noi scriviamo, ogni lettera che aggiungiamo è una chiamata con relativa risposta e quindi un notevole impegno di risorse con molte chiamate. Inoltre se effettuiamo più ricerche, quelle precedenti verranno concatenate nel DOM, quindi avremo risultati indesiderati, Sostanzialmente avremo il risultato della ricerca attuale e quelle precedenti.
Risolveremo tale problema nella prossima lezione, intanto inserisco il codice completo fino a questo momento
declare const rxjs: any; interface GoogleBook { totalItems: number items: [] kind: string } interface BookThumbnails { smallThumbnail: string thumbnail: string } interface VolumeInfo { authors: [] description: string imageLinks: BookThumbnails infoLink: string language: string previewLink: string title: string categories: [] } interface Book { title: string description: string authors: [] categories: [] thumbnail: string } interface BookItem { volumeInfo: VolumeInfo id: string } function getBooks(booktitle: string) { const { from } = rxjs; const { map, switchMap, tap, } = rxjs.operators; let apiurl = 'https://www.googleapis.com/books/v1/volumes?q='; const p = fetch(apiurl + booktitle).then(res => res.json()); //.then( books => console.log(books) ); return from(p) .pipe( switchMap( (data:GoogleBook) => from(data.items || [])), map( (ele: BookItem) => { const book:Book = { title : ele.volumeInfo.title, categories : ele.volumeInfo.categories, authors : ele.volumeInfo.authors, description : ele.volumeInfo.description, thumbnail : ele.volumeInfo.imageLinks.thumbnail }; return book; } ) ) } function displayBook(book: Book) { const bookTpl = `<div class="card mb-4 shadow-sm"> <img src="${book.thumbnail}" title="${book.title}" alt="${book.title}"> <div class="card-body"> <h5>${book.title}</h5> <p class="card-text">${book.description || ''}</p> <div class="d-flex justify-content-between align-items-center"> <div class="btn-group"> <button type="button" class="btn btn-sm btn-outline-secondary">View</button> <button type="button" class="btn btn-sm btn-outline-secondary">Edit</button> </div> <small class="text-muted">9 mins</small> </div> </div> </div>`; const div = document.createElement('div'); div.setAttribute('class','col-md-3'); div.innerHTML = bookTpl; const books = document.querySelector('#books'); if (books) { books.appendChild(div); } } function searchBooks() { const searchEle = document.querySelector('#search'); const {fromEvent} = rxjs; const {filter, map, switchMap} = rxjs.operators; if (searchEle) { fromEvent(searchEle, 'keyup') .pipe( map( (ele:any) => ele.target.value), filter( (ele: string) => ele.length > 2), switchMap( (ele:string) => getBooks(ele) ) ) .subscribe( (book: Book) => displayBook(book) ); getBooks('game of thrones'); } } searchBooks();