Follow me: @D_mitar

Most read posts recently



Jul 6th 2011 × MooTools pattern fun: Class Implements + Extends at the same time

Update: Thanks to @CarstenSchwede, who pointed out that the MooTools Class Constructor expects the Extends mutator __before__ the Implements one, meaning you don’t need to worry about any of the stuff I had to come up with (described in this post). Upon further investigation with Arian, it turns out that MooTools 2.0 will NOT be dependent on order of Extends and Implements declarations in classes, thanks to this change by Kamicane.

The neverending debacle of Extends vs Implements, which one to use and how to use them together – has been a source of discomfort for me in MooTools for a while so I thought I’d write up parts of my experiences with it in the hope it may help somebody else.

I came to have a need to write a class that serves as my new mixin and brings a new method ‘kill‘ – (say, Class ninja) to another class (Class badass) that extends Class human. Pretty crazy, right?

The problem is, the pattern of:

var ninja = new Class({
    kill: function() {
        alert("kill!");
    }
});

var human = new Class({
    initialize: function(){
        alert("i r human!");
    }
});

var badass = new Class({
    Implements: [ninja],
    Extends: human,
    initialize: function() {
        alert("i r badass and.. ");
        this.parent();
        this.kill();
    }
});

new badass(); // i r badass, i r human, this.kill is not a function exception.

… simply does not work. The Extend here means the human prototype is the base and it lacks the kill method. Wut?

The main practical difference in subclassing approaches is that the `Implements` (mixin) does not instantiate the subclass whereas `Extends` does, so there can be only one class that is extended whereas implemented ones just copy the methods from the prototypes. How to get around being able to extend a single class only? You need class human to implement ninja instead and class badass to simply extend human. Aside from the side-effect of humans getting a new kill method (which they may or may not know about), it will mean that badass will be able to use .kill as well as call upon his direct parent human.

The whole point is, you may not always be at liberty to rewrite / refactor classes the way you want them without creating complications with other parts of your web app. You could also be be extending a Mootools native class like Request.JSONP and then decide to mixin a new storage class into your extended one (true story!)…

An interesting pattern (or rather, the only pattern I have found to work… ) to overcome this goes something like this:

var ninja = new Class({
    kill: function() {
        alert("kill!");
    }
})

var human = new Class({
    initialize: function(){
        alert("i r human!");
    }
});

human.implement(new ninja);

var badass = new Class({
    Extends: human,
    initialize: function() {
        alert("i r badass and.. ");
        this.parent();
        this.kill();
    }
});

new badass(); // this.kill works.

You could even write this as:

Extends: human.implement(new ninja),

Obviously, you can simply modiy the prototype of the parent class and do:

human.implement({
    kill: function() {
        alert("kills!");
    }
});

But this means the code you already have in ninja needs to be duplicated and this will go for all methods or properties you want to gain access to.

A practical example with Request.JSONP and an example caching class, created to allow use of localStorage. Why would you want to do that? Well, having a class that can allow you to cache replies from Request or Request.JSONP or anything at all is handy. And doing things that fetch non-volatile data through a rate-limited API is expensive, you are encouraged to cache. In other words, if you read a particular user’s Twitter profile / timeline, you should not have to request it again on a reload within the session. Same applies if you are expanding tiny URLs like bit.ly – the URL won’t change for any given hash so there’s no point in continuously fetching it.

In the following example, we cache the response for a user profile JSONP request via the Twitter API:

View the full fiddle here: http://jsfiddle.net/dimitar/AjP5A/.

For a real detailed explanation of Extends vs Implements in MooTools, grab a copy of Pro Javascript with MooTools by keeto.


May 13th 2011 × mootools flickr api class via Request.JSONP

updated for mootools 1.3.2

I needed to bring some images into a thumbnail/gallery and decided to use flickr’s API for easy access. The result is a mini-api which allows you to control your options and parse the images that flickr sends back.

// the class
Request.flickr = new Class({
    Extends: Request.JSONP,
    options: {
        callbackKey: "jsoncallback",
        url: "http://www.flickr.com/services/rest/?",
        log: true
    },
    initialize: function(params, options) {
        this.parent(options);
        this.options.url = this.options.url + Object.toQueryString(params);
    },
    success: function(data, script) {
        this.parent(data, script);
    },
    imageURL: function(obj) {
        return "http://farm{farm}.static.flickr.com/{server}/{id}_{secret}.jpg".substitute(obj);
    }
});

// how to use
new Request.flickr({
    format: 'json',
    api_key: "e7df6c74d2545f55414423463bf99723", // your api here
    per_page: 4,
    tags: "mountains",
    method: "flickr.photos.search"
}, {
    onSuccess: function(data) {
        target = $("action");
        var self = this;
        data.photos.photo.each(function(el) {
            new Asset.image(self.imageURL(el), {
                onload: function() {
                    this.inject(target);
                }
           });
        });
    }
}).send();

Want to see it in action? Here’s an embedded jsfiddle:



Aug 13th 2010 × mootools Request.JSONP to decode shortened url hashes

I tend to get annoyed about URL baiting that takes place over social networks – you really don’t know what’s behind a shortened URL until you click it. Or… until you run a URL resolver that can help. In building my Twitter Trend Aggregator, I came to explore this and found an invaluable service through the API of longurl.org that can uncrunch almost any shortened URL to its original.

Here is the extended Class (http://www.jsfiddle.net/dimitar/3Ntnr/)

Request.longURL = new Class({
    // decode a long url
    Extends: Request.JSONP,
    options: {
        log: !true,
        url: "http://api.longurl.org/v2/expand",
        data: {
            url: "",
            format: "json"
        }
    },
    initialize: function(loc, options) {
        this.parent(options);
        this.options.data.url = loc;
    },
    success: function(data, script) {
        this.parent(data, script);
    }
});
// example usage
var urls = [
    "http://bit.ly/b5Ukzp",
    "http://tinyurl.com/33wz7sv",
    "http://is.gd/e7S7R",
    "http://post.ly/r49y",
    "http://www.google.com/" // won't do anything to it.
];

var output = document.id("output");
urls.each(function(url) {
    new Request.longURL(url, {
        onSuccess: function(data) {
            new Element("a", {
                href: data["long-url"],
                text: url + " -> " + data["long-url"]
            }).inject(output);
        }
    }).send();
});

For a better example of this, visit the Twitter Trends Thingie I wrote and mouseover any encoded URL (or press the ‘Unshorten Tiny URLs’ link in the top right bar).

If you want to just deal with the good old bit.ly, then you can go to them direct like so:

// get all links on page that are bit.ly and decode them
document.getElements("a[href^=http://bit.ly]").each(function(el) {
    var url = el.get("href");
    new Request.JSONP({
        url: 'http://api.bit.ly/v3/expand',
        data: {
            shortUrl: url,
            apiKey: 'R_e744c730bdde44a7eacc88cb892074e7', // change
            login: 'webtogs2' //change this
        },
        onComplete: function(response) {
            el.set({
                href: response.data.expand[0].long_url,
                text: response.data.expand[0].long_url
            });
        }
    }).send();
});

Happy social networking


Jul 9th 2010 × Create bit.ly addresses on the fly with mootools and Request.JSONP

I was writing my own re-tweet class and found that I need to crunch URLs on the fly to make them fit within the 140 character limit on Twitter. Come bit.ly and their API, easily accessible via REST and with a JSONP callback interface, just the job for extending Request.JSONP:

Request.bitly = new Class({
    // shorten urls via bit.ly
    Extends: Request.JSONP,
    options: {
        log: !true,
        bitly: {
            api: "R_e744c730bdde44a7eacc88cb892074e7", // GET YOUR OWN API KEY
            login: "webtogs2" // GET YOUR OWN LOGIN
        },
        url: "http://api.bit.ly/v3/shorten?login={login}&apiKey={api}&longUrl={longUrl}&format=json"
    },
    initialize: function(loc, options) {
        this.parent(options);
        this.options.bitly.longUrl = loc;
        this.options.url = this.options.url.substitute(this.options.bitly);
    },
    success: function(data, script) {
        this.parent(data, script);
    }
});

This is the class, now for the example uses, this will convert all links from the page into bit.ly URLs (if they are shorter than the original)

document.getElements("a").each(function(el) {
    new Request.bitly(el.get("href"), {
        onSuccess: function(data) {
            if (data && data.data && data.status_code == 200) {
                var orig = data.data.long_url, hash = data.data.url, difference = orig.length - hash.length;

                if (difference > 0) {
                    el.set({
                        href: hash,
                        title: hash + " - saved " + difference + " chars"
                    }).addClass("shortened");
                }
                else {
                    var error = "ERROR:\n your bit.ly hash '" + hash + "' is longer than the original '" + orig + "'";
                    el.set("html", el.get("html") + error);
                }
            }
        }
    }).send();

});

See it all in action on the little jsfilddle here: http://www.jsfiddle.net/dimitar/xVtZy/


Mar 10th 2010 × Cross-domain AJAX calls via YQL as proxy and mootools JSONP

I wrote this a while back when I needed to do pseudo AJAX calls to a remote host. Due to XSS restrictions and security policies, a normal XMLHttpRequest (XHR) call is not allowed to work across domains or even sub-domains. But Yahoo’s YQL interface kindly lets you GET any URL (which also means being able to submit via GET), irregardless of whether the domain is local or not.

Here is how it works. You need mootools (also download mootools-more as this class will extend Request.JSONP which is a part of mootools-more).

Request.YQLajax = new Class({
    // gets basic info such as country and latitude data
    Extends: Request.JSONP,
    options: {
        log: !true,
        url: "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20html%20where%20url%3D%22{location}%22&format=xml"
    },
    initialize: function(location, options) {
        this.parent(options);
        if (!location)
            return;

        this.options.url = this.options.url.substitute({location: encodeURIComponent(location)});
    },
    success: function(data, script) {
        this.parent(data, script);
    }
});

// example use to fetch BBC's page
new Request.YQLajax("http://www.bbc.co.uk/", {
    onSuccess: function(data) {
        $("result").set("html", data.results);
    }
}).send();

You can see this in action on mooshell or play with it below. Happy cross-domain ajax-ing.


Mar 4th 2010 × Getting latest tweets through mootools JSONP

Nothing like being egocentric so here’s what you can do to add latest tweets of a particular user to your site through mootools. This class totally `forked` from a post by AppDen (Scott Kyle) so feel free to give him some love, he deserves it for being such a bright spark. You can also “play” with this live on JSFiddle

Request.twitterAPI = new Class({
    // return json data with latest tweets form a particular user
    Extends: Request.JSONP,
    options: {
        target: $empty(),
        tweetBits: {
            textClass: "tweetBody",
            dateClass: "tweetDate"
        },
        maxTweets: 4,
        autoLink: true,
        url: "http://twitter.com/statuses/user_timeline/{user}.json"
    },
    initialize: function(user, options) {
        this.parent(options);
        this.options.url = this.options.url.substitute({user: user});
        this.element = document.id(this.options.target);
        if (!this.element)
            return;

        this.element.set("html", "Loading tweets...");
    },
    success: function(data, script) {
        this.element.empty();
        this.parent(data, script);
    },
    showPost: function(post) {
        var creationDate = Date.parse(post.created_at).format("%x %X");
        new Element("div", {
            "class": this.options.tweetBits.textClass,
            "html": this.options.autoLink ? this.linkify(post.text) : post.text
        }).adopt(new Element("div", {
            "class": this.options.tweetBits.dateClass,
            "html": creationDate // + " via " + post.source.replace("\\",'')
        })).inject(this.element);
    },
    addPosts: function(response) {
        if (!response.length || !this.element)
            return;

        response.each(function(post, i) {
            if (i < this.options.maxTweets)
                this.showPost(post);
        }.bind(this));
    },
    linkify: function(text){
        // modified from TwitterGitter by David Walsh (davidwalsh.name)
        // courtesy of Jeremy Parrish (rrish.org)
        return text.replace(/(https?:\/\/[\w\-:;?&=+.%#\/]+)/gi, '$1')
            .replace(/(^|\W)@(\w+)/g, '$1@$2')
            .replace(/(^|\W)#(\w+)/g, '$1#$2');
    }
});

// create an instance
new Request.twitterAPI("D_mitar", {
    target: "myTweets", // target element to 'host' the tweets
    onComplete: function(data) {
        this.addPosts(data); // add all posts to element.
    }
}).send();

Additionally, you can style the tweets by using 2 classes for the element:

div.tweetBody {
    line-height: 1.1;
    font-family: arial;
    font-size: 12px;
    margin-bottom: 10px;
}

div.tweetDate {
    font-size: 10px;
    color: #500;
    text-align: center;
    padding-top: 4px;
}

That’s it, it’s that simple. Happy tweeting.


Feb 11th 2010 × Get a url’s tweet / retweet count via tweetmeme api and mootools JSONP

I needed to get retweet counts as data, as opposed embedding the standard widget that tweetmeme supply.

Turning to their API instead got me joy initially. After a stressful few minutes with a javascript label exception was taking place, it transpired that the tweetmeme API was not returning valid JSON within i’s deignated callback wrapper. Instead, it came back as simple JSON. I was about to give up and write a PHP/curl wrapper when I noticed their ‘format’ is called jsonc whereas I was requesting ‘json’. Oh well, works fine now.

Through extending the mootools’ Request.JSONP class, it’s a manner of seconds to get things going:

Request.tweetmeme = new Class({
    // return json data with extended information of a place / location.
    Extends: Request.JSONP,
    options: {
        url: "http://api.tweetmeme.com/url_info.jsonc?url={location}"
    },
    initialize: function(location, options) {
        this.parent(options);
        this.options.url = this.options.url.substitute({location: location});
    },
    success: function(data, script) {
        this.parent(data, script);
    }
});

// example use
// get retweets for all posts on a page and update the counter. URL in the rel property of the markup.
$$("div.twitterBar").each(function(el) {
    var url = el.get("rel");
    new Request.tweetmeme(url, {
        log: true,
        onSuccess: function(data) {
            el.set("html", data.story.url_count);
        }
    }).send();
});

I hope this helps somebody – once again, how easy it is to extend JSONP and get things done in mootools.

no

Sep 25th 2009 × Extending the Request.JSONP class to fetch geolocation data from YQL and others

Here’s a novel idea (stolen from AppDen‘s Twitter feed fetch class) – extend the mootools-more’s JSONP class and setup some quick and easy geolocation lookups through pidgets.com and YQL (yahoo query language).

Here they are, 3 mini classes. First one uses the client’s IP address (or an optional ip supplied by options) to fetch geolocation info from http://geoip.pidgets.com/. The second instance fetches extended information on a place / city by location from Yahoo’s geoplaces DB, including things like local district, council authority, county and so forth, something that I find useful when creating quick signup forms for users and PAF lookups are not in the budget. The final class was simple test to fetch relevant timezone info of any latitude + longitude combination. It offers local time, offset from GMT or DST etc, but nothing that Date.get(“gmtoffset”) can’t accomplish anyway).

Request.geoLocation = new Class({
    // gets basic info such as country and latitude data
    Extends: Request.JSONP,
    options: {
        ip: $empty(), // default is user but you can lookup anything.
        url: "http://geoip.pidgets.com/?format=json",
    },
    initialize: function(options) {
        this.parent(options);
        if (this.options.ip.length)
            this.options.url = this.options.url += "&ip=" + this.options.ip
    },
    success: function(data, script) {
        this.parent(data, script);
    }
});

Request.getPlaceInfo = new Class({
    // return json data with extended information of a place / location.
    Extends: Request.JSONP,
    options: {
        url: "http://query.yahooapis.com/v1/public/yql?q=select * from geo.places where text='{location}'&format=json",
    },
    initialize: function(location, options) {
        this.parent(options);
        this.options.url = this.options.url.substitute({location: location});
    },
    success: function(data, script) {
        this.parent(data, script);
    }
});

Request.timeZone = new Class({
    // return local timezone related date from geo coordinates
    Extends: Request.JSONP,
    options: {
        url: "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20xml%20where%20url%3D'http%3A%2F%2Fws.geonames.org%2Ftimezone%3Flat%3D{latitude}%26lng%3D{longitude}'&format=json"
    },
    initialize: function(latitude, longitude, options){
        this.parent(options);
        this.options.url = this.options.url.substitute({latitude: latitude, longitude: longitude});
    },
    success: function(data, script) {
        this.parent(data, script);
    }
});

Here is a quick example of how it can be used on a form. Note you need mootools-more with JSONP for it to work.