Dojo: Twitter timeline widget

In deze tutorial over Dojo toolkit ga ik een stapje verder zetten in het maken van widgets en het samenbrengen van meerdere widgets.
Ik ga ook enkele nieuwe modules bespreken zoals modules om XHR requests te maken, een loading overlay screen, … .

Warning!

Deze tutorial maakt gebruikt van de Twitter 1.0 REST API. Deze is sinds 11/06 retired, deze tutorial werkt dus niet meer en kan enkel gebruikt worden als referentie voor het maken van Dojo widgets.

Bron

Heads up!

In deze tutorial wordt met regelmaat verwezen naar mijn vorige Dojo tutorials. Het is dus handig dat je deze (zeker de eerste twee) eens doorneemt.

Project opzetten

Omdat we in dit project gebruik gaan maken van de Time ago widget, is het handig om te beginnen met de code uit dat voorbeeld die je hier kan downloaden.

Maak nu in de map my nog een map twitter aan en plaats er de volgende resources:

  • Tweet.js
  • Tweet.html
  • TwitterTimeline.js
  • TwitterTimeline.html

Maak daarnaast ook een map css aan en plaats daarin een nieuw bestand met de naam style.css. De structuur van je project moet er uiteindelijk uitzien als in onderstaande afbeelding.

project-structure

HTML pagina

Als eerste stap gaan we de index HTML pagina aanpassen. Hier gaan we namelijk een TwitterTimeline widget op plaatsen en uiteraard ook verwijzen naar de juiste CSS.
De code hiervoor is:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Twitter Timeline</title>

        <script type="text/javascript">
            dojoConfig = {
                parseOnLoad : true,
                async: true,
                packages: [{
                    name: 'my',
                    location: location.pathname.replace(/\/[^/]+$/, '') + '/assets/js/my'
                }]
            }
        </script>

        <script src="http://ajax.googleapis.com/ajax/libs/dojo/1.8.3/dojo/dojo.js"></script>
        <script type="text/javascript" src="assets/js/additions.js"></script>
        <link type="style/css" rel="stylesheet" href="assets/css/style.css" />
    </head>
    <body>
        <div data-dojo-type="my/twitter/TwitterTimeline" data-dojo-props="count: 4, username: 'g00glen00b'">
        </div>
    </body>
</html>

Zoals je kan zien zijn hier weinig aanpassingen gemaakt ten opzichte van de vorige tutorials.

additions.js

Zoals gewoonlijk hebben we ook een basis-JavaScript bestand waar we eventueel bepaalde logica kunnen plaatsen. Omdat we in dit geval enkel gebruik maken van een widget, is het enige wat je nodig hebt een

require()

call naar de juiste widget, namelijk:

require(["my/twitter/TwitterTimeline"]);

Tweet widget

In deze tutorial gaan we twee extra widgets definiëren. De TwitterTimeline widget zal de nodige XHR calls maken om de Twitter API aan te spreken en om daarna een lijst van Tweet widgets te creëren.
Zo’n Tweet widget zal dus instaan voor het voorzien van de juiste template voor een tweet object (dat via de JSON response uit de REST call komt).

De JavaScript code voor deze widget (assets/js/my/twitter/Tweet.js) is:

define([
        "dojo/_base/declare",
        "dijit/_WidgetBase",
        "dijit/_TemplatedMixin",
        "my/timeAgo",
        "dojo/text!my/twitter/Tweet.html",
    "dojo/parser"
], function(declare, _WidgetBase, _TemplatedMixin, TimeAgo, template, parser) {
    return declare("my/twitter/Tweet", [_WidgetBase, _TemplatedMixin], {
        templateString: template,
        baseClass: 'tweet',

        constructor: function(params, srcNodeRef) {
            if (params.tweet.retweeted_status != null) {
                params.tweet = params.tweet.retweeted_status;
            }
            params.tweet.text = params.tweet.text.replace(/[A-Za-z]+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&~\?\/.=]+/g, function(url) {
                return url.link(url);   
            }).replace(/[#]+[A-Za-z0-9-_]+/g, function(hash) {
                return hash.link("http://twitter.com/search/%23" + hash.replace("#", ""));
            }).replace(/[@]+[A-Za-z0-9-_]+/g, function(mention) {
                return mention.link("http://twitter.com/" + mention.replace("@", ""));
            });
        },
        postCreate: function() {
            parser.parse(this.tweetNode);
        }
    });
});

Het eerste wat we gewijzigd hebben ten ozpichte van de eerder geschreven widgets is dat we geen gebruik meer maken van

require()

maar wel van

define()

.
Het tweede verschil is dat we nu de declaratie van de widget gaan returnen. Deze twee wijzigingen hebben als gevolg dat de Tweet widget gecreëerd kan worden in de JavaScript code zelf (en niet per se declaratief via HTML). Dit is handig omdat we later in de TwitterTimeline widget dynamisch Tweet-widgets gaan aanmaken.

Het volgende dat je ziet is de constructor die we nu aangemaakt hebben, namelijk via de volgende code:

constructor: function(params, srcNodeRef) {
    ...
}

Hiermee kunnen we alvorens er ook maar iets gebeurt met de parameters bij het aanmaken van een widget nog bepaalde acties uitvoeren. In dit geval gaan we namelijk als we te maken hebben met een retweet, de originele tweet tonen. Dit doen we via de volgende code:

if (params.tweet.retweeted_status != null) {
    params.tweet = params.tweet.retweeted_status;
}

De Twitter API geeft namelijk het originele tweet-bericht mee in de

retweeted_status

property. Met andere woorden, als we te maken hebben met een retweet gaan we de retweet vervangen door de originele tweet.

Daarnaast heb ik ook wat JavaScript geschreven om mentions, hashtags en links om te zetten naar HTML links. De code hiervoor is:

params.tweet.text = params.tweet.text.replace(/[A-Za-z]+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&~\?\/.=]+/g, function(url) {
    return url.link(url);   
}).replace(/[#]+[A-Za-z0-9-_]+/g, function(hash) {
    return hash.link("http://twitter.com/search/%23" + hash.replace("#", ""));
}).replace(/[@]+[A-Za-z0-9-_]+/g, function(mention) {
    return mention.link("http://twitter.com/" + mention.replace("@", ""));
});

Deze code is een kortere schrijfwijze van de conversie die ik in een oudere tutorial gemaakt heb, namelijk AJAX: Twitter timeline.

Ten slotte hebben we in de

postCreate

functie ook nog even de widget code geparsed. De reden hiervoor is dat we in de template code gebruik gaan maken van de time ago widget. Omdat we echter gebruik maken van dynamisch aangemaakte widgets, gaat de

parseOnLoad

parameter in

dojoConfig

(zie index HTML pagina) niet veel helpen, de widgets worden namelijk aangemaakt lang nadat de pagina geladen is (en dus de pagina geparsed is).

Tweet widget template

Omdat we een template widget geschreven hebben, hebben we uiteraard ook een template nodig die gebruikt zal worden door de widget. De HTML code hiervoor (assets/js/my/twitter/Tweet.html) is:

<div class="tweet" data-dojo-attach-point="tweetNode">
    <div class="thumbnail">
        <img src="${!tweet.user.profile_image_url}" width="48" height="48" />
    </div>
    <div class="content">
        <h1>
            ${!tweet.user.name}
            <a href="http://twitter.com/${tweet.user.screen_name}">@${!tweet.user.screen_name}</a>
            <span class="time" data-dojo-type="my/timeAgo" data-dojo-props="date: '${!tweet.created_at}', frequency: 15000"></span>
        </h1>
        <p>${!tweet.text}</p>
    </div>
</div>

Zoals je kan zien maken we hier enorm veel gebruik van placeholders. Deze placeholders verwijzen allemaal naar een property van het tweet object (zie Twitter API).
Het uitroepteken bij elke placeholder maakt de template parser duidelijk dat het hier gaat om een object en niet zomaar een eenvoudige string.

Helemaal onderaan hebben we ook nog gebruik gemaakt van de time ago widget (zoals eerder gezegd). Deze gaan we gebruiken om weer te geven wanneer de tweet gepost is.

TwitterTimeline widget

De Tweet widget alleen doet op zich niet veel, om de Tweet widgets aan te maken zal je een call moeten maken naar de Twitter API en dat is waarvoor de TwitterTimeline widget gebruikt zal worden.
De code (assets/js/my/twitter/TwitterTimeline.js) hiervoor is:

define([
        "dojo/_base/declare",
        "dijit/_WidgetBase",
        "dijit/_TemplatedMixin",
        "my/twitter/Tweet",
        "dojo/text!my/twitter/TwitterTimeline.html",
    "dojo/dom-construct",
    "dojo/_base/array",
    "dojo/io/script",
    "dojo/_base/lang",
    "dojox/widget/Standby"
], function(declare, _WidgetBase, _TemplatedMixin, Tweet, template, domConstruct, arrayUtil, script, lang, Standby) {
    return declare("my/twitter/TwitterTimeline", [_WidgetBase, _TemplatedMixin], {
        templateString: template,
        count: 0,
        username: "",
        baseClass: 'timeline',
        lastId: null,
        standby: null,
        _displayTweet: function(tweet, idx) {
            if (idx != this.count) {
                var node = domConstruct.create("div");
                domConstruct.place(node, this.tweetsNode, "last");
                domConstruct.place(domConstruct.create("hr"), this.tweetsNode, "last");
                new Tweet({ tweet: tweet }, node);
            }
            this._set("lastId", tweet.id);
        },
        _loadData: function(data) {
            arrayUtil.forEach(data, lang.hitch(this, "_displayTweet"));
            this.standby.hide();
        },
        _load: function() {
            script.get({
                url: "http://api.twitter.com/1/statuses/user_timeline.json",
                content: (this.lastId == null ? {
                    include_entities: true,
                    include_rts: true,
                    screen_name: this.username,
                    count: this.count + 1
                } : {
                    include_entities: true,
                    include_rts: true,
                    screen_name: this.username,
                    count: this.count + 1,
                    max_id: this.lastId
                }),
                callbackParamName: "callback",
                load: lang.hitch(this, "_loadData")
            });
        },
        _loadMore: function(evt) {
            this.standby.show();
            this._load();
        },
        postCreate: function() {
            var standby = new Standby({ target: this.domNode });
            standby.startup();
            domConstruct.place(standby.domNode, this.domNode, "last");
            this._set("standby", standby);
            this._load();
        }
    });
});

Net zoals in de Tweet widget maken we hier ook gebruik van

define

en returnen we de declaratie van de widget. Hier is het minder verplicht om te doen omdat we de widget toch vanuit een HTML pagina aanspreken, maar je weet nooit wat er in de toekomst zal gebeuren.

postCreate

Zoals je kan zien is deze widget verder redelijk uitgebreid qua functies. Ik ga beginnen door onderaan uit te leggen wat de

postCreate

functie doet.
In deze functie gaan we twee zaken uitvoeren, namelijk een

dojo/widget/Standby

initialiseren en daarnaast de laatste tweets ophalen.

Om de Standby widget te initialiseren moet je enkele dingen doen, ten eerste moet je er één aanmaken met

new Standby({ ... })

, daarna moet je deze opstarten met de

startup()

functie en ten slotte moet je deze nog aan je eigen widget toevoegen door via de dojo/dom-construct module (

domConstruct

) de

place()

functie te gebruiken.

Omdat we vanuit andere functies de Standby widget zullen tonen/verbergen moeten we natuurlijk er ook voor zorgen dat deze widget beschikbaar blijft. Daarom hebben we een extra property voorzien die we instellen via de

_set()

functie.

Ten slotte moeten we ook nog de AJAX request starten, deze is gedefinieerd in een aparte functie omdat we deze op een ander tijdstip ook gaan uitvoeren.

_load

De tweede functie die ik ga uitleggen is de functie die gebruikt zal worden om de XHR call te maken naar de Twitter API. Deze call maken we dankzij de

dojo/io/script

module en de code hiervoor is:

script.get({
    url: "http://api.twitter.com/1/statuses/user_timeline.json",
    content: (this.lastId == null ? {
        include_entities: true,
        include_rts: true,
        screen_name: this.username,
        count: this.count + 1
    } : {
        include_entities: true,
        include_rts: true,
        screen_name: this.username,
        count: this.count + 1,
        max_id: this.lastId
    }),
    callbackParamName: "callback",
    load: lang.hitch(this, "_loadData")
});

Hier geven we namelijk de URL mee en enkele extra parameters. Deze parameters zullen afhangen of de property

lastId

al dan niet ingesteld is. Het doel hiervan is dat we ook willen dat we na de originele n-aantal tweets ook nog de volgende n-aantal tweets willen kunnen laden.
Om dit te kunnen verwezenlijken moeten we bijhouden wat de laatste ID was, zodat we daarna kunnen zeggen aan de Twitter API dat je tweets wilt terug krijgen vanaf die tweet. Dit kan je instellen via de

max_id

parameter, waar we dus ook gebruik van maken.

De callback in de

load

functie is op een vrij speciale manier voorzien. Meestal maken we gebruik van anonieme JavaScript functies, het probleem daarbij is echter dat als je dan verwijst naar

this

, je niet meer bezig bent over de widget zelf, maar over de XHR module zelf. Om de juiste

this

context te kunnen blijven behouden maken we gebruik van de

dojo/_base/lang

module en voeren we de

hitch()

functie uit die op zijn beurt de _loadData functie zal uitvoeren van de widget.

_loadData

Deze functie wordt, zoals eerder gezegd, uitgevoerd wanneer de XHR request voltooid is en je data terug krijgt. Onze data bestaat uit een JSON string die voor ons al omgezet is tot een array van tweet-objecten. Om over deze array van tweets te gaan maken we gebruik van de dojo/_base/array module (

arrayUtil

). Net zoals de vorige keer gaan we ook hier gebruik moeten maken van de

hitch()

functie omdat we anders weer onze context zouden verliezen.

Daarnaast gaat deze functie ook de standby widget verbergen indien deze open stond. De data is op dat moment toch al geladen, dus er moet geen loading screen meer getoond worden.

_displayTweet

De laatste functie die initieel aangeroepen wordt is de

_displayTweet()

functie. Deze zal een nieuwe DOM node aanmaken en een nieuw Tweet-widget daaraan koppelen. Dit doen we dankzij de volgende code:

var node = domConstruct.create("div");
domConstruct.place(node, this.tweetsNode, "last");
domConstruct.place(domConstruct.create("hr"), this.tweetsNode, "last");
new Tweet({ tweet: tweet }, node);

Hier gaan we namelijk eerst een

<div>

aanmaken, deze in de lijst van tweets plaatsen (zodat deze DOM node niet enkel “virtueel” is, maar echt geladen wordt). Daarnaast gaan we ook een

<hr>

node aanmaken, dit zorgt voor een horizontale lijn die dus tussen twee tweets zal terecht komen.

Eindigen doen we met het aanmaken van een Tweet widget op basis van de meegekregen tweet en de net aangemaakte node via:

new Tweet({ tweet: tweet }, node);

Zoals eerder gezegd moeten we ook de laatste ID bijhouden zodat we de volgende keer weten van waar we verder moeten gaan indien we nieuwe tweets opvragen. Het bijhouden van de laatste ID gebeurt dankzij de volgende code:

this._set("lastId", tweet.id);

Omdat Twitter echter de laatste tweet ook mee terug gaat geven, zouden we de laatste tweet steeds dubbel zien wat niet de bedoeling is. Om dit op te lossen halen we één extra tweet op die we niet gaan tonen maar wel gaan meerekenen als laatste ID.
De volgende keer dat we dan tweets ophalen zal deze “verborgen” tweet wel getoond worden. De controle ofdat we te maken hebben met de extra tweet die niet getoond moet worden hebben we de volgende if geschreven:

if (idx != this.count) {
    ...
}

_loadMore

Om de volgende lijst van tweets op te halen ga ik een knop plaatsen op de widget waarmee je die tweets kan ophalen. Die knop zal een on click event bevatten die door zal linken naar deze functie.
Deze functie moet dan gewoon de

_load()

functie aanspreken om de XHR call te maken. Het enige wat we hier nog extra gaan doen is ervoor zorgen dat er een progress bar getoond wordt via de Standby widget door de volgende code te gebruiken:

this.standby.show();

TwitterTimeline widget template

Ook deze widget heeft z’n eigen template. Deze template bevat twee zaken, een DOM node die gebruikt zal worden om tweets aan toe te voegen (namelijk

tweetsNode

) en een knop die de volgende tweets zal tonen.
De HTML code hiervoor (assets/js/my/twitter/TwitterTimeline.html) is vrij eenvoudig, namelijk:

<div class="timeline">
    <div class="tweets" data-dojo-attach-point="tweetsNode"></div>
    <button data-dojo-attach-event='onclick: _loadMore'>Show more tweets</button>
</div>

Nieuw in deze template is het gebruik van het

data-dojo-attach-event

attribuut waarmee we rechtstreeks event handlers kunnen koppelen aan de desbetreffende events. In dit geval maken we daarvan gebruik om het onclick event te kunnen koppelen aan de

_loadMore()

functie.

Styling

Eindigen doen we door nog wat styling te voorzien voor de widgets. De CSS code (assets/css/style.css) hiervoor is:

.timeline {
        width: 455px;
}

.timeline h1 {
        color: #333;
        font: normal normal 700 14px/18px Arial;
        height: 18px;
        margin-bottom: 0px;
}

.timeline .content {
        float: left;
        height: 100%;
        width: 402px;
}

.timeline .thumbnail {
        float: left;
        height: 100%;
        margin-top: 9px;
    margin-right: 5px;
}

.timeline a {
        color: #0088CC;
        font: normal normal 400 14px/18px Arial;
        text-decoration: none;
}

.timeline a:focus {
        outline-offset:  -2px;
        outline: thin dotted #333;
}

.timeline a:hover {
        color: #005580;
        text-decoration: underline;
}

.timeline h1 span.time {
        color: #999;
        font: normal normal 400 12px/18px Arial;
}

.timeline p {
        clear: both;
        color: #333;
        font: normal normal 400 13px/18px Arial;
        margin-top: 0px;
}

.timeline hr {
        clear: both;
        height: 1px;
        margin: 15px 0;
        background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0));
        background-image:    -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0));
        background-image:     -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0));
        background-image:      -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0));
        border: 0;
}

.timeline button {
    background-color: #0084B4;
    width: 100%;
    line-height: 18px;
    padding: 10px 0px;
    font: normal normal 700 16px/20px Arial;
    color: #FFFFFF;
    border: none;
}

.timeline button:hover {
    background-color: #3EA4C9;
}

.timeline button:focus {
    background-color: #006F97;
}

Deze CSS code is voor een groot deel gekopieerd vanuit de AJAX: Twitter timeline tutorial en ga ik in deze tutorial niet verder uitleggen.

Overzicht modules

Omdat we in deze tutorial toch enkele modules gezien hebben ga ik hier even herhalen welke modules je waarvoor kan gebruiken:

Module Beschrijving
dojo/_base/declare Deze module wordt gebruikt om widgets te declareren.
dijit/_WidgetBase Deze module wordt gebruikt als “super class” voor UI widgets.
dijit/TemplatedMixin Deze module wordt gebruikt om in je widget op een eenvoudige manier templates te kunnen gebruiken als de view van je widget.
dojo/text Deze module wordt gebruikt om bepaalde pagina’s te laden, in dit geval om de template HTML pagina te laden.
dojo/dom-construct Met deze module kan je allerlei DOM operaties uitvoeren zoals het aanmaken en het plaatsen van DOM nodes.
dojo/_base/array Deze module biedt extra functionaliteit aan op arrays. In dit geval is daarvan gebruik gemaakt voor de

forEach

.

dojo/io/script Met deze module kan je XHR calls maken naar externe websites (zonder problemen met cross-domain policies), zolang deze maar een JSON string terug geeft.
dojo/_base/lang Deze module wordt vaak gebruikt voor de

hitch()

functie waarmee je functies kan uitvoeren en toch nog steeds de context kan beweren.

dojox/widget/Standby Deze widget maakt het mogelijk een loading overlay screen te maken zodat het voor de gebruiker duidelijk is dat er momenteel iets aan het laden is.
dojo/parser Met deze module kan je widgets die in HTML aangemaakt zijn (declaratief) uit laten voeren. Standaard wordt dit namelijk niet gedaan vanwege performantie.

Uitvoeren

Na deze vrij uitgebreide tutorial over Dojo widgets is het tijd om het geheel te testen. Als je de index HTML pagina in je browser opent zou je het volgende te zien moeten krijgen:

tweets-initial

Als je nu op de knop onderaan klikt zal je merken dat de loading overlay screen getoond wordt waarna de nieuwe tweets getoond worden.

tweets-loading

tweets-next

Hiermee hebben we dan ook deze tutorial afgerond.

Download project

Download – dojo-twitter-timeline-widget.tar.gz (11 kB)

Demo

Tagged , , , , .

g00glen00b

Consultant at Cronos and Tech lead at Aquafin. Usually you can find me trying out new libraries and technologies. Loves both Java and JavaScript.