Archive pour la catégorie ‘javascript’

OnNewComment callback for Disqus 2012

Samedi 18 août 2012

PunchTab supports Disqus comments to give points and badges when users post a message. Their recommended solution for this is using:

function disqus_config() {
    this.callbacks.onNewComment = [function() {
        trackComment();
    }];
}

This solution works only if you define disqus_config before loading Disqus. In our case, we have no guaranty. So we ended up using a more reliable solution which works in both cases:

if (window.DISQUS.bind) {
    window.DISQUS.bind('comment.onCreate', function() {
        PT.event.trigger("main.comment", {})
    });
}

But with the new Disqus 2012 product, this was not working anymore. They still support the onNewComment callback, which works if PunchTab loads before Disqus, but the bind solution was broken if PunchTab loads after. We’ve finally found a solution for it:

var app;
if (window.DISQUS.App) {
    app = window.DISQUS.App.get(0);
    if (app) {
        app.bind('posts.create', function(message) {
            PT.event.trigger("main.comment", message)
        });
    }
}

This is not documented so it can break at any time. We’re in touch with the Disqus team to see if there is a better way to achieve this.

Cross-domain events in Javascript

Lundi 30 juillet 2012

As I was telling you in my previous post, we’re currently rewriting most of our Javascript code at PunchTab. As it’s now stable and released for some of our publishers (including this blog), I’m gonna take time to explain the awesome features we’ve built.

The context

As I was saying earlier, we’re dealing with the same technical challenges as Facebook or Google. Our publishers install our Javascript code snippet on their websites, as you would do for a Like button, and it loads our code which then opens some iframes. For many reasons (like connecting the user through Facebook), we have to communicate between the publisher site and our iframes. To achieve this, we’ve been using easyXdm, a cross-domain messaging library. But even so, every time we’ve needed to add a feature using cross-domain communication, it has been painful. That’s why we’ve decided that cross-domain should be one of the core features of our new framework when we started to talk about it.

Cross-domain

Our basic usage of easyXdm was the following:

var socket = new easyXDM.Socket({
    remote: 'iframe_you_want_to_open.html',
    ...,
    onReady: function(){
        socket.postMessage('first message to the iframe');
    },
    onMessage: function(message, origin){
        doSomethingWith(message);
    }
});
On the other side (in the iframe), the loaded page had quite the same code except the remote.
After several months, we’ve faced some difficulties. First, everybody was defining their own format for the messages, making it harder to understand and to treat. Then we had some sockets created multiple times in different places for the same iframe, with different treatments making hard to track where a message was coming from and who was receiving it. We didn’t want to deal with this communication layer anymore, which was not part of our business logic. So one day, someone said: « what if we could use events everywhere? »

Events

Before coming back to cross-domain, I will have to explain our event layer. If you’ve used jQuery, you probably know how to use events like $(‘.button’).click(callback);. Since the beginning, we’ve wanted our new framework to be event driven, it makes it far easier to write independent modules which can interact with each other by triggering and binding on events. So the first piece was to be able to do this:

PT.event.trigger('myEvent', message) // send an event of type 'myEvent' with a message object

PT.event.bind('myEvent', function(message){ alert(message) }); // when an event of type 'myEvent' is triggered, execute the callback

That part was easily done – it’s a few lines of code. As we were dealing with events, we also decided to solve the most annoying problem of events: their volatility.

Indeed, in the previous example, if I execute the code in this order, nothing will happen because we bind the callback after the event has been triggered so we miss it. In some cases, it doesn’t matter. Like for a click, if the user clicks before we are ready to bind on a click event, it doesn’t matter if we miss it, since the user may click again. But if you take a look back at my previous post, imagine we replace our Facebook init code with this (what we’ve actually done):

var previous_fbAsyncInit = window.fbAsyncInit;
if ((window.fbAsyncInit && window.fbAsyncInit.hasRun) ||
    (!window.fbAsyncInit &&
    window.FB !== undefined &&
    FB.Event !== undefined)) {
    PT.event.trigger('facebook.ready');
} else {
    window.fbAsyncInit = function () {
        if (previous_fbAsyncInit) {
            previous_fbAsyncInit();
        }
        PT.event.trigger('facebook.ready');
    };
}

It becomes better since we can just bind on the facebook.ready event for all our features relying on Facebook. But it means they all have to be bound to this event before this piece of code is executed to be safe. What we really want is to execute our callback when facebook.ready happens or execute it directly if facebook.ready has happened in the past.

That’s why we introduced PT.event.persistent(‘facebook.ready’). This triggers and stores the event. Then on a PT.event.bind(), if the event has already happened, it executes the callback directly. We’re typically using it for some events like dom.ready, user.connected, twitter.ready, google.ready, … No need to pay attention in which order you bind on one-time events now.

Propagating events

Ok, now we have an awesome event system. What if we could send all events through easyXdm to the other iframes? That’s what we’ve build by default in the PT.event.trigger function. Whenever we trigger an event, we loop on every iframe we’ve opened through easyXdm and we send them the event serialized in JSON. Then you can trigger and bind on events anywhere and consider all the iframes as the whole and single place. Even if you open an iframe later, we send it the history of persistent events which have already been triggered to make it work the same way.

So now, we just open sockets for cross domain communication and that’s it. All the work is then done with events which makes our life infinitely easier.

To sum up, have a look at this example which sends a message to an iframe through an event:

The code on this side (for explaination about ptReady, see previous post):

<input type="text" id="example" value="Example" />
<input type="submit" id="send" value="Send" />
<div id="iframe-container"></div>
<script type="text/javascript">
window.ptReady = window.ptReady || [];
ptReady.push(function(){
    document.getElementById('send').onclick = function(){
        PT.event.trigger(
            'message.sent',
            document.getElementById('example').value
        );
        return false;
    };
    PT.xdm.socket('iframe', {
        remote: 'http://static.alfbox.net/iframe.html',
        container: 'iframe-container',
    });
});
</script>

The code in the iframe:

<script type="text/javascript">
    PT.event.bind(
        'message.sent',
        function(message){ document.body.innerHTML = message}
    );
    PT.xdm.socket('parent');
</script>

Callback for 3rd party javascript

Vendredi 6 juillet 2012

At PunchTab, we’re dealing with the same technical challenges as Facebook or Twitter: we’re providing a 3rd party Javascript snippet that publishers install on their website.

One of the features we have to provide is a callback that we or our publishers can use to execute code once the PunchTab Javascript has been loaded and executed.

The Facebook approach

Currently, we’re using the Facebook approach: if the publisher has defined a specific function in the global namespace, we execute it when we’re done with our own functions. Where Facebook use fbAsyncInit, we use ptAsyncInit. The publisher may define this function as follows:

window.ptAsyncInit = function(){alert('PunchTab is ready')};

And at the end of our javascript, we simply add:

if (window.ptAsyncInit !== undefined) {
    window.ptAsyncInit();
}

It’s convenient for basic usage cases, but not when multiple 3rd party libraries are involved.

Indeed, a publisher may load Facebook and PunchTab asynchronously. As we’re registering some callbacks for Facebook, we have to use fbAsyncInit, but we never know which will be loaded first – them or us. Here is the code we use to execute our code for Facebook:

var previous_fbAsyncInit = window.fbAsyncInit;
if ((window.fbAsyncInit && window.fbAsyncInit.hasRun) ||
    (!window.fbAsyncInit && window.FB !== undefined && FB.Event !== undefined)) {
    // execute our code relying on FB
} else {
    window.fbAsyncInit = function () {
        if (previous_fbAsyncInit) {
            previous_fbAsyncInit();
        }
        // execute our code relying on FB
    };
}

The issue here is that we have to first detect if Facebook is not already loaded, which is tricky. If it is already loaded, we just execute our code directly. If Facebook isn’t loaded, we redefine the window.fbAsyncInit function to call our code once Facebook is ready. You can notice that our fbAsyncInit is a monkey patch to execute the previous fbAsyncInit if it exists.

As you can see, the global callback is not convenient for two reasons:

  • It is difficult to have multiple callbacks
  • It is not made for the case when Facebook is loaded before another application

The Twitter approach

To achieve the same effect with Twitter, you have to use the snippet on this page. Instead of using a public callback, it defines the global twttr object directly in the snippet (if it doesn’t exist) and then adds a useful function to it: ready():

return window.twttr || (t = { _e: [], ready: function(f){ t._e.push(f) } });

You can then call twttr.ready(your_callback) as many times as you want. It will be pushed into a queue (_e) which will be executed when Twitter is loaded. If Twitter is already loaded, this overwrites the function to directly execute the callback (smart!) and it solves the two previous issues.

The future PunchTab approach

We’re currently rewriting our Javascript SDK, where we are going to switch to the Twitter approach – but in an even simpler way. Twitter does this in a slightly trickier way to avoid leaking in the global namespace. But we chose to use a global variable ptReady to achieve the same.

You will able to do the following:

window.ptReady = window.ptReady || [];

which defines a basic Javascript array if it doesn’t already exist. And then, you just add this:

ptReady.push(your_callback)

if PunchTab is not ready, the queue will be processed when we are. If we are already loaded, we redefine the push function to directly execute the callback. Kaboom!