Sistem de rating-uri cu MooTools


Acum ca am vazut cateva detalii despre MooTools (mai exact, programarea orientata pe obiecte cu MooTools, putina programare functionala, manipularea DOM, lucrul cu evenimente si cereri AJAX), a venit timpul sa punem in practica toate aceste concepte. O sa incercam sa cream un sistem de rating-uri cu ajutorul MooTools, pornind de la un caz concret si incercand ca in final sa obtinem o clasa cat mai generica si customizabila.


Sa incepem cu markup-ul HTML. Vrem ca in final sa obtinem cod care sa fie utilizabil atat in absenta JavaScript, cat si chiar in absenta CSS (asa cum am vorbit in articolul trecut, acesta este modul in care ar trebui programate marea majoritate a interfetelor web, cu mica exceptie a paginilor RIA), asadar am ales urmatorul mark-up:

<ul class="rating">
    <li class="now">Current rating: 2.4 stars</li>
    <li class="star star-5"><a href="rate.php?id=1&amp;stars=5">Give 5 stars</a></li>
    <li class="star star-4"><a href="rate.php?id=1&amp;stars=4">Give 4 stars</a></li>
    <li class="star star-3"><a href="rate.php?id=1&amp;stars=3">Give 3 stars</a></li>
    <li class="star star-2"><a href="rate.php?id=1&amp;stars=2">Give 2 stars</a></li>
    <li class="star star-1"><a href="rate.php?id=1&amp;stars=1">Give 1 star</a></li>
</ul>
<p class="message"></p>

Este vorba asadar de o lista neordonata, al carei prim element este rating-ul curent, apoi in ordine descrescatoare valorile posibile ale voturilor. Rezultatul este disponibil aici.

Sa trecem mai departe si sa stilizam acest markup cu ajutorul CSS. Nu o sa listez in articol intreg codul CSS, insa o sa fac un scurt rezumat al acestuia (fisierul este disponibil aici, rezultatul obtinut – aici). Daca va uitati atent pe pagina rezultata, sunt doua modificari:

  • in primul rand, fiecarui element cu clasa now i-am adaugat, cu ajutorul atributului style (iar acesta este unul dintre putinele cazuri in care este preferat CSS inline), un width
  • apoi, am adaugat fisierul extern stars.css

In fisier am adaugat imaginile efective de stele – ca background-uri:

  • background-ul elementelor ul este format din stele goale
  • background-ul elementelor cu clasa now este format din stele pline
  • background-ul elementelor cu clasa star este format din stele pline, insa este pozitionat in asa fel incat nu se vede cand mouse-ul nu este deasupra listei de stele
  • am folosit pseudo-clasa :hover ca sa:
    • ascundem rating-ul curent cand mouse-ul intra deasupra listei de stele (astfel ca daca vrem sa votam un numar mai mic de stele decat rating-ul curent, sa avem vizual imaginea numarului de stele – altfel, daca vrem sa acordam un numar mai mare de stele, acesta n-ar prezenta nicio problema)
    • sa afisam background-ul pentru fiecare link care duce catre rating-uri

O sa observati ca toate li-urile sunt pozitionate absolut – pentru ca stelele sa aiba acelasi punct de plecare. Restul codului CSS reseteaza margini si padding-uri, sau sunt clase ajutatoare care ne vor folosi cand implementam partea de JavaScript.

Important! Internet Explorer 6 (ce surpriza!) nu suporta pseudo-clasa :hover decat pentru elemente de tip ancora. Astfel ca, desi restul incompatibilitatilor le-am eliminat prin mici trucuri, pentru acesta nu exista nicio solutie care sa nu foloseasca JavaScript – ori, scopul nostru declarat era ca pagina noastra sa fie utilizabila si in absenta JS, asadar nu a avut sens sa ne complicam cu rezolvarea acestei probleme. (Ca o nota, probabil ca pana la sfarsitul lui 2009 nu va mai trebui sa ne punem problema compatibilitatii site-urilor si cu IE6 pentru ca se pare ca in acest an va cobori sub cota de 10%)

Sa incercam acum sa adaugam si partea de JavaScript: vrem sa facem votul efectiv cu ajutorul unei cereri AJAX si sa afisam informatii utilizatorului despre rezultatul votului (de exemplu, votul nu a reusit din diverse motive – poate utilizatorul a votat deja – sau, dimpotriva, s-a realizat cu succes, caz in care afisam un mesaj de succes). Sa incercam intai sa implementam functionalitatea dorita pentru cazul nostru particular, iar dupa sa incercam sa transformam acest cod intr-o clasa care sa poate fi folosita cat mai generic.

Pentru realizarea votului, ar fi sa adaugam ascultatori la click pe stele, iar in codul care trateaza aceste evenimente transmitem cererea JavaScript. Deoarece avem deja adresele la care se face votul in atributele href, va fi suficient sa facem cererea la acea adresa, insa va trebui sa transmitem o informatie suplimentara pentru ca scriptul din spate sa stie ca cererea a fost AJAX si nu un simplu click pe link (ca in cazul in care JS e dezactivat). Pentru cazul nostru, sa presupunem ca primim raspunsul in format JSON: un boolean success care va fi true daca votul s-a realizat cu succes, un mesaj care sa descrie rezultatul votului, si, daca cererea a fost un succes, noul rating si noua latime pentru elementul cu clasa now, care sa reflecte acest nou rating. La final, cand votul s-a incheiat, va trebui si sa eliminam toate elementele cu clasa star – nu mai are sens inca un vot (altfel rating-urile nu prea ar reflecta realitatea din moment ce un utilizator ar putea vota de o infinitate de ori). Atentie, scriptul PHP folosit pentru procesarea cererilor AJAX intoarce aceleasi rezultate de fiecare data – este doar un script dummy.

Iata si codul corespunzator (cu comentariile necesare; rezultatul e disponibil aici):

$$('.rating').each(function(container) {
    container.getElements('.star a').each(function(star) {
        // toate ancorele din li-uri cu clasa star, din container
        // sunt elementele cu ajutorul carora se fac voturile
        star.addEvent('click', function(event) {
            event.stop();
            // indica utilizatorului ca se petrece ceva
            container.addClass('busy');
            // votul se transmite printr-o cerere AJAX care va primi ca raspuns un JSON
            var req = new Request.JSON({
                // adaugam un parametru suplimentar pentru ca serverul sa stie ca cererea e AJAX
                url: this.get('href') + '&amp;amp;amp;amp;ajax=true',
                onComplete: function(response) {
                    var messageContainer = container.getNext();
                    // elimin din container stelele - nu se mai pot face voturi
                    container.getElements('.star a').destroy();
                    // cererea s-a incheiat - nu mai e nevoie de indicatorul vizual
                    container.removeClass('busy');
                    // afisez utilizatorului rasounsul
                    messageContainer.set('html', response.message);
                    if (!response.success)
                        messageContainer.addClass('error');
                    else
                        container.getElement('.now').setStyle('width', response.width);
                    container.getElement('.now').addClass('voted');
                }
            });
            req.send();
        });
    });
});

Ce observam din codul de mai sus este ca avem nevoie de urmatoarele:

  • containerul in care sunt toate stelele,
  • un selector care sa gaseasca toate elementele dintr-un container care declanseaza un vot (i. e. .star a)
  • vrem sa facem totul cat mai generic, deci o functie care sa intoarca un Request (sau o subclasa a acestuia – sau, de fapt, orice obiect care sa aiba o metoda send() si caruia sa-i poate fi atasat un eveniment complete, care sa se ocupe de transmiterea catre server a votului) ar fi grozava
  • cele doua evenimente importante care se petrec in procesul votarii sunt votarea efectiva si finalizarea votului (finalizarea cererii AJAX si intoarcerea raspunsului), deci ar fi util sa putem sa atasam ascultatori acestor evenimente la fel de usor cum atasam ascultatori evenimentelor clasice (click, mouseover, etc.)
  • ca sa facem clasa cat mai usor de folosit, am putea oferi niste optiuni implicite decente, dar care sa poata fi modificate cu usurinta la instantierea clasei

Vom crea o clasa MooTools (deci ar fi util sa revedeti articolul Clase in MooTools si sa aveti la indemana documentatia oficiala), si ne vor fi extrem de utile clasele din Class.Extras (vom implementa pentru clasa noastra, cele doua clase Options si Events), deci haideti sa vorbim pe scurt despre cele doua (documentatia fiind disponibila pentru orice alte nelamuriri).

Atunci cand o clasa MooTools implementeaza Options, acea clasa va capata un membru options (implicit un obiect gol), si o metoda setOptions. Optiunile implicite pot fi specificate intr-un membru options al clasei noastre ca un hash de optiuni, iar apeland setOptions cu un hash de noi optiuni ca parametru, vor fi suprascrise in membrul options doar acei membri care sunt prezenti in acel parametru. Pentru clasa noastra, vom primi in constructor un singur parametru, optional: un hash de optiuni, si vom specifica un set implicit de optiuni care sa fie cat mai generic.

Daca o clasa MooTools implementeaza Events, acelei clase ii devin disponibile o serie de metode – addEvent, addEvents, removeEvent, removeEvents, fireEvent – care sunt extrem de asemanatoare cu metodele cu acelasi nume de la evenimentele DOM. Chiar mai mult, daca clasa implementeaza si Options, orice optiune care incepe cu on (de exemplu, onVote), va fi atasata ca ascultator la un eveniment de tipul acelei optiuni mai putin on (in cazul nostru, un eveniment vote).

Asadar, iata listarea codului clasei; am folosit un namespace IW pentru a nu crea conflicte – exista destul de multe alte scripturi pentru ratinguri cu MooTools:

if (!$defined(window.IW)) {
    IW = {};
}
IW.Ratings = new Class({
    Implements: [Options, Events],
    options: {
        selector: '.rating',
        stars: '.star',
        getRequest: function(url, onComplete) {
            /* url este link-ul catre care ar trebui facuta cererea AJAX, fara parametrii suplimentari
            onComplete este functia care ar trebui atasata ca ascultator atunci cand cererea AJAX s-a terminat */
            return new Request.JSON({url: url + '&amp;ajax=true', onComplete: onComplete})
        }
    },
    bind: function() {
        $$(this.options.selector).each(function(container) {
            container.getElements(this.options.stars).each(function(star){
                var self = this;
                star.addEvent('click', function(event) {
                    event.stop();
                    self.fireEvent('vote', star);
                });
            }.bind(this));
        }.bind(this));
    },
    initialize: function(options) {
        this.addEvent('vote', function(star) {
            var req = this.options.getRequest(
                star.getProperty("href"),
                function(r) {
                    this.self.fireEvent('complete',[r, this.star]);
                }.bind({star: star, self: this})
            );
            req.send();
        }, true);
        this.setOptions(options).bind();
    }
});

Codul nu prea are nevoie de explicatii suplimentare daca ati urmarit prima implementare de mai sus (cea fara clase), ce ar fi de adaugat ar fi ca instantelor acestei clase ii pot fi atasate doua tipuri de ascultatori:

  • vote, care va fi declansat atunci cand se executa votul (in mod tipic, cand se face click pe un link), ai carei ascultatori primesc ca parametru elementul care a declansat evenimentul si al carei context este instanta clasei
  • complete, care va fi declansat atunci cand cererea AJAX prin care s-a transmis votul catre server s-a incheiat. Primeste doi parametri: primul, raspunsul de la server, iar al doilea, elementul care declansase evenimentul. Contextul acestei functii va fi tot instanta clasei

Ar trebui ca intotdeauna sa ii fie atasat clasei un ascultator la complete (pentru ca nu exista unul implicit).

Ca sa demonstram cum se comporta in practica clasa noastra, am lasat intentionat din vedere un lucru care tine de utilizabilitatea site-ului si de experienta utilizatorului: nicaieri in codul clasei, nu se anunta vizual utilizatorului ca se petrece ceva dupa ce a facut click pe o stea. Sa rezolvam asta adaugand ascultatori la vote, respectiv complete. Astfel, cand utilizatorul voteaza, vom anunta vizual acest lucru in doua moduri: containerul stelelor (in cazul nostru, listele cu clasa rating) va avea clasa busy in timp ce se efectueaza cererea (astfel, cursorul utilizatorului va indica “busy”), iar in campul in care afisam mesaje vom pune deja celebrul spinner AJAX. Cererea AJAX va dura cel putin doua secunde pentru ca scriptul din spatele acesteia asteapta doua secunde inainte sa intoarca raspunsul. Cand se termina cererea, eliminam clasa busy de la container (spinnerul va fi automat inlocuit cu mesajul primit ca raspuns de la server). Acesta ar fi codul:

var ratings = new IW.Ratings({
    stars: '.star a',
    // adaugam o optiune care incepe cu on, deci va fi automat adaugata ca ascultator
    onComplete: function(response, element) {
        var parent = element.getParent(this.options.selector);
        var messageContainer = parent.getNext();
        parent.removeClass('busy');
        parent.getElements(this.options.stars).destroy();
        parent.getElement('.now').addClass("voted");
        if (!response.success)
            messageContainer.addClass('error');
        else
            parent.getElement('.now').setStyle('width', response.width);
        messageContainer.set("html",response.message);
    }
});
// putem adauga si explicit ascultatorul
ratings.addEvent('vote', function(star) {
    star.getParent(this.options.selector).getNext().set('html','<img src="/images/loading.gif" alt="Asteptati..." />');
    star.getParent(this.options.selector).addClass('busy');
});

Rezultatul este disponibil aici.

Data viitoare vom implementa ratinguri ca plugin pentru jQuery.

, , , , , , ,

  • mihai92
    salut . la mine nu merge . nu styu de ce
    plzz un ajutor
blog comments powered by Disqus