ugrás a tartalomhoz

Sablonkezelés jQuery alatt

Poetro · 2010. Május. 14. (P), 17.15
Sablonkezelés jQuery alatt

Szinte minden webes nyelv rendelkezik valamilyen sablonkezelő rendszerrel (template system), amivel a egyes blokkokat fel tudjuk tölteni változó tartalommal.

PHP-s körökben az egyik legelterjedtebb a Smarty, valamint a PHPTAL, Ruby esetén a szabványos könyvtár része az ERB csomag. A többi nyelvhez is elérhető, vagy már az alap függvénytár része a sablonok kezelése több kevesebb hozzáadott szolgáltatással.

Az alapvető követelmény minden esetben, hogy a sablonmotor a kapott sablonba beírja az átadott változókat. A változók helyének meghatározása és a behelyettesítés módja a sablonmotortól függ, ugyanakkor léteznek erre kvázi szabványok, ahogyan meg lehet határozni egy változó helyét. A JSP (Java Server Pages) JSTL sablonkezelője a ${változonév} formát preferálja, a perles Template Toolkit például a [% változónév %], a Smarty a {$változónév} formát és így tovább. Amiben hasonlítanak, hogy van valami határoló karaktersorozat és közöttük a változó neve.

JavaScript

Kedvenc nyelvünk, a JavaScript esetén sincs ez másként, itt is léteznek sablonkezelő motorok speciálisan egy adott keretrendszer köré csoportosítva. Ext JS esetén az Ext.Template szolgál segítségül, használata viszonylag egyszerű.

Ennél valamivel kifinomultabb a MooTools Mooml sablonkezelője, ahol JavaScripttel is lehet generálni a HTML sablont, és közvetlenül a DOM-ban is elhelyezhető <script type="text/mooml" name="sablon-neve">Sablon</script> formában. Természetesen a többi keretrendszer is kínál vagy már a rendszer részeként, vagy külső modulként sablonkezelőt.

jQuery sablonkezelők

jQuery esetén is elérhető rengeteg bővítmény. Ezek közül is fontosnak tartom azt, melyet John Resig, a jQuery megalkotója JavaScript Micro-Templating néven tett közzé; ezen alapszik a jQuery Micro Template plugin.

JavaScript Micro-Templating

A rendszer sokmindenre képes, viszont ezt érdekes megvalósítással teszi, ami nem feltétlenül szimpatikus több fejlesztő számára, akik úgy gondolják hogy az eval és társai maga a gonosz. Ugyanis a rendszer lényege, hogy new Function híváson keresztül a sablonból JavaScript kódot generál, aminek lehet biztonsági kockázata, viszont ha a sablon elkészült, akkor nagyon gyors, és a sablonban levő JavaScript kódot is képes lefuttatni. Álljon itt egy példa egy majdnem statikus sablonra:


<div id="<%=id%>" class="<%=(i % 2 == 1 ? " even" : "odd")%>">
    <div class="alpha">
        <img src="<%=profile_image_url%>"/>
    </div>
    <div class="omega contents">
        <p><b><a href="/<%=from_user%>"><%=from_user%></a>:</b> <%=text%></p>
    </div>
</div>

Mint látható, az első sorban a <div> id tulajdonsága a majdan átadott id változó értéke lesz, az osztálya pedig attól függően, hogy i páros-e vagy páratlan, even illetve odd értéket vehet fel. A nyelv szabályai viszonlag egyszerűek. A <% jelzi, hogy JavaScript kód következik, ha ezt = jel követi, akkor egy értéket fog beírni, egyébként pedig az azt követő JavaScript utasítássorozat fog lefutni, például:


<% for (var i = 0; i < users.length; i++ ) { %>
    <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li>
<% } %>

Amennyiben a sablonunkat egy HTML elemben helyeztük el, akkor hivatkozhatunk rá annak azonosítójával, vagy átadhatjuk magát a sablont is. Például:


<script type="text/html" id="link_tmpl">
    <a href="<%=link%>"><%=title%></a>
</script>

Ezen HTML kód esetén a használat:


tmpl('link_tmpl', {'link': 'http://weblabor.hu', 'title': 'Weblabor'});

Az ehhez kapcsolódó jQuery bővítmény már nem rendelkezik ilyen szép kóddal, és igazából csak röviden szeretném bemutatni, mivel ugyanazt tudja mint az eredeti, csak szerintem rosszabbul.


<div id="link_tmpl">
    <a href="<:=link:>"><%=title%></a>
</div>

A kapcsolódó JavaScript kód pedig:


$('#link_tmpl').drink({'link': 'http://weblabor.hu', 'title': 'Weblabor'});

jQuery Templates

A jQuery Templates kicsit más alapokon nyugszik. A lényeg, hogy itt is reguláris kifejezésekkel zajlik a cserélés, de csak abban az esetben történik eval hívás, ha le is fordítjuk a cserélési procedúrát, ami sokat tud gyorsítani a sablon alkalmazása esetén, ugyanakkor nem tud vezérlési szerkezeteket.

Ami mégis előnye szerintem, hogy a változókon végrehajthatunk a sablonban függvényeket. Ezekkel testre szabhatjuk, hogy a változó milyen formán jelenjen meg, nem kell minden formáját előkészíteni, valamint a sablon építőjének sem kell ismernie a JavaScript sajátosságait, csak azokat a függvényeket, amiket alkalmaz.

Ezt demonstrálandó készítettem is egy kisebb mintaalkalmazást, ami több JavaScript „szépséget” is tartalmaz.

Flickr képkereső

Az alkalmazásunk egy keresést indít a Flickr azon képeire, melyek rendelkeznek egy megadott kulcsszóval. Magának a keresésnek a lebonyolításához jelen esetben Yahoo! Query Language-t fogunk használni, ezzel annak lehetőségeit is demonstrálva. A YQL eredményhalmazából egy listát fogunk képezni, amit majd jQuery Templates sablonok használatával fogunk a felhasználónak megmutatni.

Yahoo! Query Language

A YQL egy URL alapú lekérdező szolgáltatás, amivel a Yahoo!, és más szervezetek – akik YQL formában is elérhetővé tették adataikat –, adatbázisában kereshetünk. A Yahoo! esetében ez lehet maga a Web, a Yahoo! saját motorját használva, vagy más szolgáltatása, példánk kedvéért a Flickr.

A lekérdezésünk ebben az esetben a következőképpen néz ki:


SELECT * FROM flickr.photos.info WHERE photo_id IN (SELECT id FROM flickr.photos.search(1, 10) WHERE tags = "cimke")

Ezzel kikeressük azon Flickr fényképek információit, amik azonosítója megtalálható azon 1-től 10-ig terjedő fotó között, amelyek rendelkeznek a „cimke” címkével.

Az 1-től 10-ig azért lett felhasználva, hogy ha később például lapozót szeretnénk készíteni, könnyedén ki lehessen bővíteni az alkalmazásunkat, ne kelljen a lekérdezésünket is újraírni.

A lekérdezést a YQL-nek átadva egy XML dokumentumot vagy JSON-t kapunk, melyet feldolgozunk az alkalazásunk igényeinek megfelelően. A lekérdezés URL-je a következőképpen néz ki:

http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20flickr.photos.info%20where%20photo_id%20in%20(select%20id%20from%20flickr.photos.search(1,10)%20where%20tags%3D%22cimke%22)&format=json&callback=cbfunc

A q paraméter tartalmazza a lekérdezésünket, természetesen megfelelően kódolva, a format a visszatérés formáját (xml vagy json), valamint megadhatunk egy opcionális callback paramétert, ami JSONP esetén lesz hasznos.

Sablonok

Alkalmazásunk váza a következőképp néz ki:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>jQuery template</title>
    </head>
    <body>
        <!-- Űrlap, ami majd a keresést végzi -->
        <form method="get" action="#">
            <label>Kulcsszó: <input type="text" /></label>
            <input type="submit" value="Keresés" />
        </form>

        <div id="results" class="section">
            <!-- Ide kerülnek majd az eredmények -->
        </div>

        <!-- Eredménylista sablon -->
        <script type="text/html" id="template-results">
            <![CDATA[
                <h3>Eredmények</h3>
                <ul class="hfeed"></ul>
            ]]>
        </script>

        <!-- "Nincs eredmény" sablon -->
        <script type="text/html" id="template-no-result">
            <![CDATA[
                <h3>Nincs eredmény</h3>
            ]]>
        </script>

        <!-- Sablon az egyes képekhez -->
        <script type="text/html" id="template-image">
            <![CDATA[
                <li class="hentry">
                    <h3 class="entry-title">
                    <a href="${link:checkPlain}" title="${title:checkPlain}">${title:truncate(40,1)}</a>
                    </h3>
                    <p class="vcard">
                        <a class="url fn" href="${authorURI:checkPlain}">${authorName}</a>
                        @
                        <span class="published">${date}</span>
                    </p>
                    <div class="entry-content">
                        <a href="${link:checkPlain}">
                            <img src="${thumbnail:checkPlain}" alt="${title:checkPlain}" height="80" longdesc="${link:checkPlain}" />
                        </a>
                        <div class="description" title="${description:checkPlain}">
                            ${description:truncate(100,1)}
                        </div>
                    </div>
                </li>
            ]]>
        </script>

        <!-- Szükség van jQuery-re -->
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
        
        <!-- Szükség van jQuery Templates-re -->
        <script type="text/javascript" src="http://bitbucket.org/stanlemon/jquery-templates/raw/abe394368c73/jquery.template.js"></script>
    </body>
</html>

Mint látható, a HTML dokumentumban létrehoztunk egy <form>-ot a keresésre, valamint deklaráltuk a sablonokat <script type="text/html" id="sablon-azonosito"></script> formában. Mivel a böngésző nem tud mit kezdeni a text/html formájú <script>-tel, ezért figyelmen kívül hagyja. A CDATA részt az érvényes HTML forma miatt raktam bele, igazából elhagyható, és csak XHTML formátum esetén van értelme.

A sablonokat ezek a CDATA részek tartalmazzák, és akkor térjünk is rá, hogyan kell a sablonokat értelmezni.


<li class="hentry">
    <h3 class="entry-title">
        <a href="${link:checkPlain}" title="${title:checkPlain}">${title:truncate(40,1)}</a>
    </h3>
    <p class="vcard">
        <a class="url fn" href="${authorURI:checkPlain}">${authorName}</a>
        @
        <span class="published">${date}</span>
    </p>
    <div class="entry-content">
        <a href="${link:checkPlain}">
            <img src="${thumbnail:checkPlain}" alt="${title:checkPlain}" height="80" longdesc="${link:checkPlain}" />
        </a>
        <div class="description" title="${description:checkPlain}">
            ${description:truncate(100,1)}
        </div>
    </div>
</li>

Ez egy hagyományos HTML, ugyanakkor vannak benne változók és azon végzett műveletek. A későbbiekben majd részletezem a műveleteket, a használt formátum ugyanakkor a következő: ${valtozonev[:muvelet[([paraméterek])]]}.

Azaz tartalmaz egy változónevet, amit majd a motor be fog helyettesíteni, opcionálisan azon végrehajthat egy műveletet, aminek opcionálisan átadhatunk paramétereket.

Legegyszerűbb forma tehát például a ${date}, ahol egyszerűen behelyettesítésre kerül majd a date változó. A ${link:checkPlain} esetében a link változón fogja végrehajtani a checkPlain műveletet, aminek nem adunk át paramétereket. Legbonyolultabb forma a ${description:truncate(100,1)} ahol a description változón lefut a truncate függvény, aminek átadunk két paramétert, a 100-at és az 1-et.

Fontos hangsúlyozni, hogy az átadott paraméterek sztringként fognak a függvényhez jutni, ezért előbb ét kell alakítani azokat, amennyiben nem sztringre van szükségünk.

Ahhoz, hogy kivegyük a sablont a HTML elemből, írtam egy kisebb jQuery plugint.


$.fn.extend({'parseTemplate': function () {
    var templates = [];

    if (this.length) {
        this.each(function () {
            templates.push(this.innerHTML.replace(/^[\n\r\t ]*<!\[CDATA\[([^\f]*)\]\]>[\n\r\t ]*$/, '$1'));
        });

        return this.length == 1 ? templates[0] : templates;
    }

    return '';
}});

Igazából nem csinál sokat, csak kiveszi az elem tartalmát és amennyiben a tartalom CDATA-ban van, akkor kiveszi a CDATA tartalmát. Ha több elem van, akkor tömbként adja vissza a template-eket, ha csak egy elemet választottunk ki jQuery-vel, akkor pedig csak azt az egy sablont adja vissza sztringként. Használata egyszerűen: $('#template-image').parseTemplate();.

Változó műveletek

Korábban már említettem, hogy a sablonban levő változókon műveleteket lehet végrehajtani. A jQuery Templates plugin alapból csak egy substr() függvényt tartalmaz, de általában ennél többre van szükségünk. Ezért deklaráljuk a sablonunkban előforduló checkPlain() és a truncate() függvényünket. Mivel a jQuery Templates függvénytár kiterjeszthető, igen egyszerű dolgunk van.


$.extend($.template.helpers, {

    /**
     * Segédfüggvény, szöveget alakít Bool értékké.
     */
    _boolValue: function(value) {
        switch (value) {
            case 'false':
            case '0':
                return false;
            case 'true':
            case '1':
                return true;
            default:
                return !!value;
        }
    },

    /**
     * Megfelelő hosszúságúra vág egy szöveget.
     *
     * @param value
     *   A szöveg, amit vágni akarunk.
     * @param length
     *   A méret amire vágni szeretnénk.
     * @param wordSafe
     *   Szóhatáron vágjon-e.
     *   (Használjunk Bool-ra hasonlító értékeket 0 / 1 vagy true / false).
     * @return
     *   A méretre vágott szöveg.
     */
    truncate: function (value, length, wordSafe) {
        var text = String(value);
        wordSafe = this._boolValue(wordSafe);

        if (text.length > length) {
            if (wordSafe) {
                var lastSpace = text.lastIndexOf(' ', length);
                
                // létezik a space ÉS nem a 0. pozícióban
                text = (lastSpace > 0) ? text.substring(0, lastSpace) : text.substring(0, length);
            } else {
                text = text.substring(0, length);
            }

            text += '…';
        }

        return text;
    },

    /**
     * HTML szöveg speciális karaktereit kódolja,
     * így elemek tulajdonságaiban is szabadon fel lehet használni.
     *
     * @param value
     *   A szöveg, amiben kódolni kell a speciális karaktereket.
     * @return
     *   Kódolt szöveg.
     */
    checkPlain: function (value) {
        return value.replace(/[<>&"']/g, function (match) {
            switch (match) {
                case '"':
                    return '"';
                case "'":
                    return ''';
                case '&':
                    return '&';
                case '<':
                    return '<';
                case '>':
                    return '>'
            }

            return match;
        });
    },
});

Amint látható, kiterjesztettük a $.template.helpers objektumot, ahol a jQuery Templates a változó műveleteit tárolja, és kibővítettük pár újabb művelettel.

A truncate() függvény a szöveget vágja megfelelő hosszúságúra, akár szóhatárt figyelembe véve is, a checkPlain() pedig a HTML-ben használt karaktereket kódolja.

A _boolValue() a függvények között igazából kakukktojás, mivel ez egy segédfüggvény, amire a többi sablonműveletünk támaszkodhat, amennyiben Bool értéket szeretne használni a kapott paraméterek között.

Alkalmazás

A jQuery Templates alkalmazása a következőképpen néz ki:

  1. Előkészítjük a sablonunkat var sablon = $.template('sablon', opciok); formában.
  2. Alkalmazzuk a sablonra a változókat. Ennek több módja létezik:
    • Szöveget generálunk a sablonból későbbi felhasználásra a sablon.apply(valtozok); formában
    • Beszúrjuk közvetlenül a HTML-be, akármelyik DOM manipuláló függvény segítségével, például:
      • $('body').html(sablon, valtozok);
      • $('body').append(sablon, valtozok);
      • $('#results').after(sablon, valtozok);
      • stb.

A tényleges alkalmazásunk pedig:


(function ($) {
    // 'Globális' változók előkészítése
    
    var flickrURL = $.template('http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20flickr.photos.info%20where%20photo_id%20in%20(select%20id%20from%20flickr.photos.search(${start}%2C${to})%20where%20tags%3D%22${query}%22)&format=json&callback=?', {'compile': true}),
    
    parse = function (items) {
        // Feldolgozza a Flickr-től kaptott tömböt,
        // és a sablonnak szükséges objektumokat gyárt belőle.
        
        return $.map(items, function (entry) {
            return {
                'authorName':  entry.owner.realname || entry.owner.username,
                'authorURI':   'http://www.flickr.com/photos/' + entry.owner.nsid,
                'title':       entry.title || 'N/A',
                'link':        entry.urls && entry.urls.url && entry.urls.url.content,
                'thumbnail':   'http://farm' + entry.farm + '.static.flickr.com/' + entry.server + '/' + entry.id + '_' + entry.secret + '_t.jpg',
                'description': entry.description || '',
                'date':        entry.dates.taken
            };
        });
    };

    // Ha készen áll a DOM, indulhat az alkalmazás
    $(function () { 
        var imageTemplate = $.template($('#template-image').parseTemplate(), {'compile': true}),
        listTemplate      = $('#template-results').parseTemplate();
        noListTemplate    = $('#template-no-result').parseTemplate();

        $('form:first').submit(function (event) {
            // Előkészítjük a paramétereket.
            var params = {
                'start': 1,
                'to':    10,
                'query': encodeURIComponent($(':text', this).val())
            };

            // Nem fog lefutni a form alapértelmezett submit eseménye.
            event.preventDefault();

            // Csinálunk egy JSON(P) lekérdezést YQL-en keresztül a Flick-re,
            // és feldolgozzuk az eredményeket.
            $.getJSON(flickrURL.apply(params), function (data) {
                var items = (
                    data.query &&                   // Megnézzük,
                    data.query.results &&           // hogy van-e eredmény,
                    data.query.results.photo &&     // azok fotók-e,
                    parse(data.query.results.photo) // és feldolgozzuk,
                ) || [],                            // vagy üres tömmbel térünk vissza.
                results = $('#results').empty(),    // Kiürítjük az eredménylistát.
                list;                               // Lista.

                // Attól függően, hogy van-e eredmény megfelelő sablont választunk.
                results.append(items.length ? listTemplate : noListTemplate);
                
                if (items.length) {
                    // Vannak eredmények, feltöltjük a listát.
                    list = results.find('ul:first');
                    
                    if (list.length) {
                        // Végigmegyünk az eredényeken, és berakjuk a listába.
                        $.each(items, function () {
                            // Mivel átadjuk a sablonnak a változókat,
                            // ezért az szépen fel lesz töltve.
                            list.append(imageTemplate, this);
                        });
                    }
                }
            });
        });
    });
})(jQuery);

A kód azoknak, akik nem szoktak hozza a jQuery-hez kicsit fura lehet, ezért részletezem. A függvényünkben elérhető lesz a jQuery $ néven. Erre igazából általában nincs szükség, azonban ha több keretrenszerrel dolgozunk egyszerre, akkor összeütközés lehet a változók tekintentében, ezt hivatott elkerülni a (function ($) {})(jQuery) forma, így a függvényen belül a $ biztosan a jQuery-nek felel meg, valamint a függvényben deklarált változók nem fogják beszennyezni a globális névteret.

Első változónk a flickrURL már maga is egy sablon, aminek átadhatunk 3 változót: start, to és query, ahol a start a lista első elemének indexe, a to az utolsóé, valamint a query, ami maga a keresés. A callback=? rész biztosítja, hogy a jQuery AJAX lekérdezés esetén JSONP-ként kezelje a kérést.

A parse nevű változónk egy függvény, ami a YQL lekérdezés által visszadott JSON tömböt dolgozza fel, és veszi ki változókba az elemeket, melyeket a sablonban majd felhasználunk.

A működés nem túl bonyolult. A <form> elküldése esetén nem az alapértelmezett művelet fog lefutni, hanem egy AJAX lekérdezés az előbb létrehozott flickrURL sablon elemeinek kitöltésőből kapott URL segítségével. Amikor visszatér az AJAX kérés a JSON eredénnyel, azt feldolgozzuk, hogy át tudjuk adni az előkészített sablonoknak. Annak függvényében, hogy volt-e találat, megjelenítjük a sablont.

Zárszó

Remélem sikerült rávilágítanom a sablonkezelés rejtelmeire, és arra, hogy a sablonok kezelésének nem csak a szerveroldalon van meg a helye, hanem a mai webes világban elterjedt AJAX-os weboldalak is sokat profitálhatnak belőle. Kezelésük általában nem bonyolult, igazából csak megszokás kérdése, és szerintem kényelmesebb, mint stringeket összefűzni minden egyes eredmény formázásához. A designerek is szabadabban tudnak mozogni, mivel meg tudják tervezni a kinézetet már a böngészőben HTML és CSS segítségével, nem szükséges nekik további JavaScript ismereteket elsajátítani.

 
Poetro arcképe
Poetro
1998 óta foglalkozik webfejlesztéssel, amikor is a HTML és a JavaScript világa elvarázsolta. Azóta jópár évet dolgozott reklámügynökségeknél, és nemzetközi hírportálok fejlesztésével. Legfőképpen Drupal fejlesztéssel, site buildinggel és JavaScripttel, azon belül is jQuery-vel és Node.js-sel foglalkozik.
1

Nagyon jó!

presidento · 2010. Május. 15. (Szo), 10.11
Jó a cikk, és milyen ötletes, hogy a JavaScript Micro Templating kódja is mikro méretű. :)
2

folytatás?

Dohány Tamás · 2010. Május. 15. (Szo), 17.25
jó cikk! lesz folytatása?
3

Folytatás

Poetro · 2010. Május. 18. (K), 20.06
Egyenlőre nem terveztem folytatást, de amennyiben elegendő igény mutatkozik, elgondolkodom a dolgon.
Addig is a fenti kódot élesben is ki lehet próbálni.
4

jQuery Template Proposal

Kevlar · 2010. Jún. 6. (V), 14.41
Éppen dolgoznak a jQuery-s srácok egy template módszeren:
http://forum.jquery.com/topic/jquery-templates-proposal

Itt ki is próbálható:
http://github.com/nje/jquery-tmpl