FooBar - Create You Own Hello Bar!

| Comments

FooBar - A little series on building your own Hello Bar. Code can be found at GitHub

There is a popular little JavaScript widget called the Hello Bar. You embed it in your website, and it displays a bar, with a line of text at the top of your webpages. Visitors are drawn to it, because of its prominent position.

What follows is a mini series of tutorials, where we’ll make ‘FooBar’ - our own interpretation of the Hello Bar.

Like the Hello Bar, our bar must be easy for site owners across the inter-webs to embed. With this in mind, FooBar should…

  • Start life as a copy and paste snippet.

  • Be free of dependencies. We won’t be using jQuery et al.

  • Load fast.

  • Be customisable, where possible.

  • Image free. In the name of leanness, no images will be used.

FooBar will be built with modern web technologies in mind. For this reason it’ll only work on modern browsers. If FooBar encounters IE9 or under it should respectfully do nothing.

Let’s Begin

We’ll start with a self invoking, anonymous function, to protect our code from the global namespace.

1
2
3
(function(){

}());

Next, an init function that we’ll call when the window fires its onLoad event:

1
2
3
4
5
6
7
8
(function(){
  var init = function(){
    // run!
  };

  window.onload = init;

}());

Right away we’ll create and append the DOM elements that’ll make up FooBar’s markup. We’ll call the function inject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var inject = function(){
  foobar = document.createElement("div");
  foobar.id = "foobar";

  content = document.createElement("div");
  content.id = "foobar-content";

  close = document.createElement("span");
  close.id = "foobar-close";

  foobar.appendChild(content);
  foobar.appendChild(close);

  open = document.createElement("div");
  open.id = "foobar-open";
  open.innerHTML = "<span class='foobar-plus'></span>"

  document.body.appendChild(foobar);
  document.body.appendChild(open);
};

Other functions will want to access foobar, content, `open’ and ‘close, so we’ll define them in the outer scope:

1
2
3
4
5
6
7
8
9
10
11
12
(function(){
  var foobar, content, open, close;

  var inject = function(){

  };

  var init = function(){
    inject();
  };

  ...

Customising FooBar

After applying some basic styling to the FooBar (see foobar.css) it’s time to consider how users can customise parts of the bar. Naturally, a user will want to adjust the content of the bar, but also being able to change the bar’s colours without touching the CSS would be handy. By defining a my_foobar object in their webpages, a user will be able to override default settings:

1
2
3
4
5
6
7
8
<script>
  my_foobar = {
    content: "Hello from my FooBar",
    text_colour: 'white',
    background_colour: 'black',
    border_colour: 'orange'
  };
</script>

We can set our default settings within the FooBar script and then merge it with the user’s my_foobar object. Here’s a function for the job:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var extend_options = function(){

  // Continue only if my_foobar has been defined as an object
  if(typeof my_foobar === "object"){
    for (var prop in my_foobar) {

      // If the property from my_foobar exists in our default options object,
      // then replace the value, with the value in my_foobar.
      if (options[prop] !== undefined) {
        options[prop] = my_foobar[prop];
      }
    }
  }
};

To acquire these custom style settings, we’ll generate CSS on the fly, appending them to the DOM. It makes sense to do this before injecting FooBar’s markup. We’ll start with a styles function, which returns an object of style rules - it’s syntactical closeness to CSS doesn’t need mentioning. We pass each value to an as of yet undefined function called colour. colour will take three types of colour description and return something we can use in a CSS rule. Look at colour in applicatio.js to see what it does.

1
2
3
4
5
6
7
8
9
10
11
12
13
var styles = function(){
  return {
    '#foobar': {
      "background": colour(options.background_colour),
      "color": options.text_colour,
      "border-color": colour(options.border_colour),
    },
    '#foobar-open': {
      "border-color": colour(options.border_colour),
      "background": colour(options.background_colour)
    }
  }
};

We need to now use the styles object as a template for generating the styles rules to be injected into the HTML document:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var injectStyles = function(){
  var
      // Create the style tag
      tag = document.createElement('style'),

      // Memo will hold our concatenated CSS string
      memo = '';

  // Iterate over each property in the styles object
  for (element in styles()) {
    memo += element + "{";
    for( style in styles()[element]){
      memo += style + ":" + styles()[element][style] + ';';
    }
    memo += "}";
  }

  tag.textContent = memo;
  document.body.insertBefore(tag, document.body.childNodes[0]);
};

We’ve covered enough ground for a first part of what will be a series of posts. Next we can consider how best to minify and concatenate FooBar’s content (JavaScript and CSS) into a single file, thus limiting to one, the number of HTTP requests required to get FooBar working on a browser.

JavaScript Objects, Prototypes and a Tweet

| Comments

A look at Prototypal Inheritance in JavaScript and how we can implement it.

Here we have a JavaScript object, describing a single tweet…

1
2
3
4
5
6
7
8
var tweet = {
  "created_at": "Sun, 05 Aug 2012 10:30:12 +0000",
  "from_user": "NME",
  "id": 231185161608376321,
  "id_str": "231185161608376321",
  "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/1731598749\/twitter3_normal.jpg",
  "text": "Arctic Monkeys set to score a Top 20 hit with their cover of The Beatles' 'Come Together' http:\/\/t.co\/1GvDZXoc"
};

…from which we can extract information:

1
2
3
4
5
6
7
8
9
tweet.text;
// Using dot notation.
// Returns Artic Monkeys set to score...

// Or...

tweet['text'];
// Using bracket notation.
// Also returns Artic Monkeys set to score...

We can derive new information from the tweet, such as the tweet’s full HTTP address. Here’s a short function for the job:

1
2
3
4
5
6
7
8
9
10
11
// function 'where' takes a tweet object and returns a URL string,
// constructed from the properties of the passed tweet.

var where = function(tweet) {
  return "https://twitter.com/" + tweet.from_user + "/status/" + tweet.id_str;
};

// Invoking the function
where(tweet);

// Returns https://twitter.com/NME/status/231185161608376321

As it stands, we need to pass our object to the function where, but what if we could ask the object directly, like so:

1
tweet.where();

Not a problem. We just need to make the function a property of the object:

1
2
3
4
5
6
tweet.where = function() {
  return "https://twitter.com/" + this.from_user + "/status/" + this.id_str;
}

tweet.where();
// Returns https://twitter.com/NME/status/231185161608376321

Notice above I’m using JavaScript’s this keyword for accessing a_tweets properties.

Working with Multiple Objects

What if we have an array of tweets and we want each object to respond to where?

1
2
3
4
5
6
7
8
9
// Loop through array of tweets, assigning copy of the
// 'where' function to each tweet object.
for(var x, length = tweets.length; x < length; x++){
  tweets[x].where = function(){
    return "https://twitter.com/" + this.user_name + "/status/" + this.tweet_id;
  }
}

tweets[0].where();

Assigning a copy of the ‘where’ function to every tweet isn’t a very efficient use of memory or code. What if we could have one instance of the where function and have all the tweet objects access that instance. This is where Prototypal Inheritance steps forward.

Prototypal Inheritance

Any JavaScript object can have a pointer to an object called its prototype. Multiple objects can all have the same prototype. Crucially, objects inherit properties from their prototypes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Here's an empty object created using Object constructor's 'create' method:

var emptyObject = Object.create(null);

// When creating new objects with this method, you can set the object's prototype 
// by passing the aspiring prototype object:

// Elixir is a regular JavaScript object, created using object literal notation. It will act as our prototype.
var elixir = {
  'immortal': true
};

// Create a new object passing elixir
var mortalObject = Object.create(elixir);

mortalObject.immortal;
// Returns true!

mortalObject is inheriting immortality from its prototype object (the elixir object).

Other objects can drink from the elixir of life too:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ordinaryObject = Object.create(elixir);

ordinaryObject.immortal;
// Returns true

// What if the elixir runs dry?

elixir.immortal = false;

// Now all objects inheriting from elixir have lost their immortality:

ordinaryObject.immortal;
// Returns false

mortalObject.immortal;
// Returns false

With that very brief tour of JavaScript prototypes, we can return to our tweets example. Using prototypal inheritance, we’ll endow the tweets with the ‘where’ method:

1
2
3
4
5
var cortex = {
  where: function(){
    return "https://twitter.com/" + this.username + "/status/" + this.id;
  }
};

cortex will be the prototype object our tweet objects will inherit from:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Helper function takes a regular tweet object

function generateSmartTweet(tweet){
  var obj = Object.create(cortex)
  obj.id = tweet.id_str;
  obj.text = tweet.text;
  obj.username = tweet.from_user;
  return obj;
}

// Container for our smart tweets
var smart_tweets = [];

// Iterate over our array of tweets passing each tweet to generateSmartTweet,
// pushing the returned 'smart' tweet to our smart tweets array.

for(var x, var length = tweets.length; x < length; x++){
  smart_tweets.push(generateSmartTweet(tweets[x]));
}

smart_tweets[0].where();
// Returns https://twitter.com/NME/status/231185161608376321

All tweets whose prototype object is the cortex will immediately have access to any new methods we assign to the cortex. Perhaps we’d like to know how long ago in words the tweet was tweeted? I won’t re-invent the wheel for this method - below is some code borrowed from John Resig (of jQuery fame):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// I'll pop this function into another, more generic object (`Helper`). 
// It has general appeal, and could be used by other code.
var Helper = {};

Helper.timeAgoInWords = function(time){
    var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ")),
      diff = (((new Date()).getTime() - date.getTime()) / 1000),
      day_diff = Math.floor(diff / 86400);

    if ( isNaN(day_diff) || day_diff < 0 || day_diff >= 31 )
      return;

    return day_diff == 0 && (
        diff < 60 && "just now" ||
        diff < 120 && "1 minute ago" ||
        diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
        diff < 7200 && "1 hour ago" ||
        diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
      day_diff == 1 && "Yesterday" ||
      day_diff < 7 && day_diff + " days ago" ||
      day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago";
    }
  }
};

// Back to the cortex...
var cortex = {
  where: function(){
    return "https://twitter.com/" + this.user_name + "/status/" + this.tweet_id;
  },

  when: function(){
    return Helper.timeAgoInWords(this.created_at);
  }
};

It would also be handy if we could have the tweet text ‘linkified’ for us:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cortex.linkified_text = function(){
    var text = this.text;
    var url_expression = /([A-Za-z]+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+)/g;
    var username_expression = /(^|\s)@(\w+)/g;
    var hash_expression = /(^|\s)#(\w+)/g;

    return text.replace(url_expression, '<a href="$1" target="_blank">$1</a>').
                replace(username_expression, '$1@<a href="http://www.twitter.com/$2" target="_blank">$2</a>').
                replace(hash_expression, '$1#<a href="http://search.twitter.com/search?q=%23$2" target="_blank">$2</a>');
};


smart_tweets[0].linkified_text();
// Returns something like @<a href="http://www.twitter.com/foo target="_blank">foo</a> #<a href="http://search.twitter.com/search?q=%23baa" target="_blank">baa</a> <a href="http://google.com" target="_blank">http://google.com</a>

And with that, we have an example (with some practical value) of how JavaScript’s prototypal inheritance allows objects to easily inherit from other, more general objects.

Love Thy JSON

| Comments

JSON is fantastic stuff. Learn to love it, if you want to reach Web Dev Nirvana.

As an aspiring Web App developer, it’s important you get down and dirty with JSON.

What is JSON?

Simply, JSON is a data format. It’s a standardised way of formatting structured information. When you query almost any API today, it’s going to format its response in JSON - copy and paste this query into your browser to see what I mean:

http://search.twitter.com/search.json?q=monkey

Twitter’s response is a screen full of JSON formatted tweets about monkeys. Notice how JSON is very readable - it is actually based on a subset of JavaScript. If you’re familiar with JavaScript objects, then you’re going to find JSON a breeze.

What follows is a simple demonstration of what JSON looks like and how we can can work with it in a web environment.

Fire up your JavaScript console, and follow along:

1
2
3
// Here's a JSON object of someone's name:

'{"firstname": "Tim", "surname" : "Berners-Lee"}'

With JavaScript, let’s try and extract some information out of it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var obj = '{"firstname": "Tim", "surname" : "Berners-Lee"}';
typeof obj;
>> "string"

// We can't get much out of a string, without complicated RegExp pattern matching. 
// JSON.parse to the rescue...

obj = JSON.parse(obj);
typeof obj;
>> "object"

// Nice. We now have a JavaScript object!

obj.firstname;
>> "Tim"

// OK. Let's write a simple function which takes the above object and returns a full name:

function fullName(person){
  return person.firstname + ' ' + person.surname;
}

fullName(obj);

>> "Tim Berners-Lee"

Often, when working with JSON responses from APIs, you’ll get an array of objects. Here’s an example from Twitter’s search API (the response has been heavily trimmed, for brevity’s sake).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "completed_in": 0.125,
  "max_id_str": "229283820036304896",
  "next_page": "?page=2&max_id=229283820036304896&q=blue%20angels&rpp=5&include_entities=1&result_type=mixed",
  "page": 1,
  "query": "blue+angels",
  "results": [
    {
      "created_at": "Sat, 28 Jul 2012 18:34:57 +0000",
      "from_user": "msinfoblog",
      "from_user_name": "Kelley  Wilson",
      "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/2287764130\/2tmsb7eyhs9v7qkvdbps_normal.png",
      "text": "On our way to the blue Angels air museum http:\/\/t.co\/FJFDRUG1"
    },
    {
      "created_at": "Sat, 28 Jul 2012 18:29:22 +0000",
      "from_user": "papawheelee",
      "from_user_name": "Jeremy Wheelock",
      "profile_image_url": "http:\/\/a0.twimg.com\/profile_images\/2317606886\/1339975449024_normal.jpg",
      "text": "They look fast parked. @Brad202b: Blue angels http:\/\/t.co\/aA4VCL2s"
    },
    {
      "created_at": "Sat, 28 Jul 2012 18:26:50 +0000",
      "from_user": "preciousalexan2",
      "from_user_name": "precious alexander",
      "profile_image_url": "http:\/\/a0.twimg.com\/sticky\/default_profile_images\/default_profile_0_normal.png",
      "text":"RT @ShamraStar: My Angels gave me pink purple &amp; blue kisses! @starquality @pinkyinthebrain @my_dollhouse \ud83d\ude18 http:\/\/t.co\/oC5OVNh9"
    }
  ]
}

See just how readable JSON is? Notice the results property 6 lines down, this is where all the action is at. The property’s value is an array of tweets. Let’s programmatically do something with that data…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// JSON segment from above, duplicated below as a string, 
// on one line so you can copy and paste into your browser's JavaScript console.

var data = '{"completed_in":0.125,"max_id_str":"229283820036304896","next_page":"?page=2&max_id=229283820036304896&q=blue%20angels&rpp=5&include_entities=1&result_type=mixed","page":1,"query":"blue+angels","results":[{"created_at":"Sat, 28 Jul 2012 18:34:57 +0000","from_user":"msinfoblog","from_user_name":"Kelley  Wilson","profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2287764130\/2tmsb7eyhs9v7qkvdbps_normal.png","text":"On our way to the blue Angels air museum http:\/\/t.co\/FJFDRUG1"},{"created_at":"Sat, 28 Jul 2012 18:29:22 +0000","from_user":"papawheelee","from_user_name":"Jeremy Wheelock","profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2317606886\/1339975449024_normal.jpg","text":"They look fast parked. @Brad202b: Blue angels http:\/\/t.co\/aA4VCL2s"},{"created_at":"Sat, 28 Jul 2012 18:26:50 +0000","from_user":"preciousalexan2","from_user_name":"precious alexander","profile_image_url":"http:\/\/a0.twimg.com\/sticky\/default_profile_images\/default_profile_0_normal.png","text":"RT @ShamraStar: My Angels gave me pink purple &amp; blue kisses! @starquality @pinkyinthebrain @my_dollhouse \ud83d\ude18 http:\/\/t.co\/oC5OVNh9"}]}'

typeof data;
// "string"

data = JSON.parse(data);
typeof data
// "object"

data.results;
// [> Object, > Object, > Object]

data.results[0].text;
// "On our way to the blue Angels air museum http://t.co/FJFDRUG1"

function showTweets(tweets){
  for (var i=0; i < tweets.length; i++) {
    console.log(i+1 + '.',tweets[i].text, 'by', tweets[i].from_user_name);
  };
}

showTweets(data.results);

// 1. On our way to the blue Angels air museum http://t.co/FJFDRUG1 by Kelley  Wilson
// 2. They look fast parked. @Brad202b: Blue angels http://t.co/aA4VCL2s by Jeremy Wheelock
// 3. RT @ShamraStar: My Angels gave me pink purple &amp; blue kisses! @starquality @pinkyinthebrain @my_dollhouse http://t.co/oC5OVNh9 by precious alexander

I’ll post some short tutorials over the coming weeks, exploring further the significance of JSON in Web App Development. In the mean time, have a peep at the following:

JSON-P - What and Why?

| Comments

This post explains JSON-P, a widely used technique for circumnavigating the web browsers security block on cross domain Ajax requests.

Web browsers support XMLHttpRequest’s (Ajax) API for easily retrieving and posting data without reloading the document. The XMLHttpRequest JavaScript object, however, does not support cross-domain requests due to security restrictions imposed by the browser.

So, if I make an Ajax request to Twitter’s Search API, I’ll be communicating with another domain, and so can expect a security error. Try the following in a JavaScript console on your favourite browser. I’m using Chrome.

1
2
3
4
5
6
7
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js'></script>
// Using jQuery's Ajax API

var resource = '//search.twitter.com/search.json'
$.get(resource, { q:'chimpanzee'})

> XMLHttpRequest cannot load https://search.twitter.com/search.json?q=chimpanzee. Origin https://twitter.com is not allowed by Access-Control-Allow-Origin.

We get an XMLHttpRequest cannot load error, so no surprises there.

I’m sure you’re familiar with Twitter’s JavaScript widget which one can embed directly into a website. The widget queries Twitter’s Search API and displays the results on the page the script is loaded from. But how? Why doesn’t the script get the XMLHttpRequest cannot load we saw above?

This is how: the widget dynamically injects a script tag into the web document. script tags are permitted by browsers to load scripts hosted on other domains. So, by dynamically injecting a script tag after your web page has loaded, Twitter’s widget can load data into your web document (originating from Twitter’s domain) without having to reload your web page.

Let’s give it a try. In the same directory create two documents - index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
  <head></head>
  <body>

    <script>

      function get(url){
        // Create script tag
        var scriptTag = document.createElement('script');

        scriptTag.src = url;

        // Inject script tag into the head of the web document
        document.getElementsByTagName('head')[0].appendChild(scriptTag);
      }

      // Call get
      get('data.json');

    </script>

  </body>
</html>

…and data.json:

1
2
3
4
5
6
  { animals: [
    'cat',
    'dog',
    'mouse',
    'koala']
  }

Notice the get function in index.html. This basic function, when called, dynamically generates and inserts a script tag into the loaded web document, setting src to the URL passed.

Load index.html in your browser. You’ll get a blank window, but reveal the Network panel and you’ll see that data.json was successfully loaded into our document, despite the script tag being inserted after the web page had loaded.

Browsers immediately run code delivered to the document via a script tag. In our example above, we’re just returning data - there are no statements to be executed, that’s why nothing happens.

The obvious question now is how then do we get our hands on that returned data - currently it’s vanishing into the ether…

Well, make the following edit to data.json, and refresh index.html with the console open:

1
2
3
4
5
6
7
8
process(
  { animals: [
    'cat',
    'dog',
    'mouse',
    'koala']
  }
);

Notice the error displayed in the console:

1
Uncaught ReferenceError: process is not defined

This is a good error to get. It means code is being executed - the function process is being invoked. Let’s define process:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- index.html -->

  function get(url){
    var scriptTag = document.createElement('script');
    scriptTag.src = url;
    document.getElementsByTagName('head')[0].appendChild(scriptTag);
  }

  function process(data){
    var body = document.getElementsByTagName('body')[0];
    var h1;

    // Iterate over the array of animals, appending each
    // as a h1 heading, to the document's body
    for(var i = data.animals.length - 1; i >= 0; i--){
      h1 = document.createElement('h1');
      h1.textContent = data.animals[i];
      body.appendChild(h1);
    }
  }

  get('data.json');

Refresh the browser and you should see a list of animals displayed to the window.

This technique of passing JSON encoded data (which has been requested via a script tag) as an argument to a function is know as JSON-P. How do we get this technique to work with JSON-P savvy API’s in the wild?

Returning to Twitter’s Search API, take this valid API request and paste it directly into your browser:

1
https://search.twitter.com/search.json?q=chimpanzee

You’ll see, as a response, a page full of JSON encoded data. Twitter’s Search API is JSON-P enabled, so, with that in mind, add one additional query parameter (callback) to the URL and reload:

1
https://search.twitter.com/search.json?q=chimpanzee&callback=process

Notice how Twitter’s response has changed? It’s now doing exactly what we did in our data.json file. It’s calling process, passing as its only argument the JSON encoded data. Change the value of callback and the name of the invoked function changes accordingly.

Let’s update our process function to work with Twitter’s JSON response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function get(url){
  var scriptTag = document.createElement('script');
  scriptTag.src = url;
  document.getElementsByTagName('head')[0].appendChild(scriptTag);
}

function process(data){
  var body = document.getElementsByTagName('body')[0];
  var p;

  for(var i = data.results.length - 1; i >= 0; i--){
    p = document.createElement('p');
    p.textContent = data.results[i].text;
    body.appendChild(p);
  }
}

get('https://search.twitter.com/search.json?q=chimpanzee&callback=process');

Twitter’s API response should now appear on the screen as a list of tweets. We have successfully made a cross-domain request, handling the API’s response.

Taking this knowledge with you on future projects, keep in mind not all APIs support JSON-P. Review the API’s documentation, or try adding a callback parameter and look at the APIs response.

I’ll leave you with an improved version of the get function from before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var jsonpID = 0;

function JSONP(url, callback){

  // create script tag
  var script = d.createElement('script');

  // Dynamically generate a callback function name.
  // Make the name unique so there's no chance we
  // override another function of the same name. By
  // attaching the counter jsonpID to the name we can make
  // invoke JSONP multiple times, each time with a different
  // function name
  var callbackName = '_JSONP_callback__' + (++jsonpID)

  // replace question mark with callbackName
  script.src = url.replace(/=\?/, '=' + callbackName)

  // append script to document's head
  document.getElementsByTagName('head')[0].appendChild(script)

  // create and assign the callback function
  window[callbackName] = function(data){
    delete window[callbackName]
    // remove script element from document
    script.parentElement.removeChild(script)

    // call the passed function passing data
    callback(data);
  }
}

You can use JSONP as follows:

1
2
3
  JSONP('https://search.twitter.com/search.json?q=chimpanzee&callback=?', function(data){
    console.log(data);
  });

Instagram Web App Tutorial Part 2 - Implementing Pagination

| Comments

This post follows on from last week’s. I’ll be implementing pagination.

A working version of this app is located here, and you can view the code at github.

I quite like google-image’s solution to this problem, which is to have a single ‘Show more results’ button at the bottom of the page.

Instagram’s API makes pagination a doddle. There’s nothing in the API’s documentation about pagination, but in the API’s response there is an object property called pagination:

1
2
3
4
5
6
7
8
"pagination": {
    "next_max_tag_id": "1335465136530",
    "deprecation_warning": "next_max_id and min_id are deprecated for this endpoint; use min_tag_id and max_tag_id instead",
    "next_max_id": "1335465136530",
    "next_min_id": "1335465556125",
    "min_tag_id": "1335465556125",
    "next_url": "https:\/\/api.instagram.com\/v1\/tags\/cats\/media\/recent?callback=jQuery17201362594123929739_1335440328560&_=133544032857&client_id=xxx&max_tag_id=1335465136530"
}

Within the object there’s a property called next_url, which conveniently contains a prepared URL for accessing the next page of photos. Studying the URL you’ll notice there’s an additional parameter - max_tag_id, which can also be retrieved from the pagination object as next_max_id.

The plan then is to display the first page of photos, with a ‘View More’ button at the bottom of the page. The button will store the max_tag_id as a data attribute. When clicked, a handler will pass the max_tag_id as a query parameter in a request to Instagram’s API.

Here’s the markup for the button:

1
2
3
4
<!-- index.html -->
<div class='paginate'>
  <a class='button'  style='display:none;' data-max-tag-id='' href='#'>View More...</a>
</div>

I’ve made a number of tweaks to the code since part 1, the most significant of which is the way I now generate the URL for querying the API:

1
2
3
4
5
6
7
8
9
10
11
function generateResource(tag){
  var config = Instagram.Config;
  url = config.apiHost + "/v1/tags/" + tag + "/media/recent?callback=?&client_id=" + config.clientID;

  return function(max_id){
    if(typeof max_id === 'string' && max_id.trim() !== '') {
      url += "&max_id=" + max_id;
    }
    return url;
  };
}

Calling generateResource with a search string like ‘cats’ would return another function, which when invoked would then return the URL with ‘cats’ passed in the query string.

1
2
3
4
5
> var resource = generateResource('cats');
  undefined

> resource();
  "https://api.instagram.com/v1/tags/cats/media/recent?callback=?&client_id=xxx"

I can invoke resource at anytime. If and when the user wants to see the next page of photos I need only invoke resource, this time passing the max ID as a parameter. The returned string is the same URL as before, with ‘cats’ as the tag, but now also with the max_id query parameter slapped on to the end.

1
2
3
4
5
6
7
8
> var resource = generateResource('cats');
  undefined

> resource();
  "https://api.instagram.com/v1/tags/cats/media/recent?callback=?&client_id=xxx"

> resource('12345');
  "https://api.instagram.com/v1/tags/cats/media/recent?callback=?&client_id=xxx&max_id=12345"

The search function has changed to accommodate this new way of generating the query URL. search simply prepares the resource URL by invoking generateResource with the tag. The actual GET request to the API is delegated to a new function, fetchPhotos.

1
2
3
4
5
6
function search(tag){
  resource = generateResource(tag);
  $('.paginate a').hide();
  $('#photos-wrap *').remove();
  fetchPhotos();
}

The toScreen function which previously appended photos to the screen, now also updates the ‘view more’ button with the max_id for the next page of photos:

1
2
3
4
5
6
7
8
9
10
11
12
13
function toScreen(photos){
  var photos_html = '';

  // Update data-max-tag-id with the next_max_id.
  $('.paginate a').attr('data-max-tag-id', photos.pagination.next_max_id)
                  .fadeIn();

  $.each(photos.data, function(index, photo){
    photos_html += toTemplate(photo);
  });

  $('div#photos-wrap').append(photos_html);
}