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();