mozilla

Creating the Area Tweet app

With all of the design goals and coding standards laid out, it's time to get into the coding of our app.

HTML

As with any Web application template, we need an HTML page with the HTML5 doctype, <head>, and <body> elements.

<!doctype html>
<html>
<head>
  <title></title>
 
  <meta charset="utf-8" />
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
 
  <meta name="viewport" content="width=device-width, initial-scale=1">
 
  <!-- stylesheets go here -->
 
</head>
<body>
 
  <!-- HTML app structure goes here -->
 
 
  <!-- javascript goes here -->
 
</body>
</html>

We'll be using jQuery Mobile as the JavaScript framework. That means we must pull in the CSS and JavaScript resources required by jQuery Mobile:

<!-- stylesheets -->
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" />
	
<!-- scripts -->
<script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
<script src="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js"></script>

With our basic application boilerplate and jQuery Mobile resource in place, it's time to add content and widgets to the page. Since we've decided to use jQuery Mobile for our application framework, our app's HTML structure will follow their prescribed widget structures. The first pane provides a search box and a history list:

<!-- home pane -->
<div data-role="page" id="home">
 
  <div data-role="header">
    <h1>Area Tweet</h1>
  </div>
 
  <div data-role="content">
    
    <div class="ui-body ui-body-b">
      <h2>Location Search</h2>
      <form id="locationForm">
        <input type="search" name="location" id="locationInput" placeholder="Your Location" />
        <button type="submit" data-role="button" data-theme="b">Search</button>
      </form>
    </div>
    
    <div id="prevLocationsContainer" class="opaque">
      <h2>
        Previous Locations
        <a href="#" id="clearHistoryButton" data-role="button" data-icon="delete" data-iconpos="notext"
                data-theme="b" data-inline="true" style="top: 5px;">Clear History</a>
      </h2>
      <ul id="prevLocationsList" data-role="listview" data-inset="true" data-filter="true"></ul>
    </div>
    
  </div>
</div>

There are a few things to notice with the code above:

  • The structure of the HTML5 sticks to semantics, which ensures maximum accessibility.
  • As with any Web-based app, we assign CSS classes and IDs to elements we want to style and to access to from JavaScript code.

If you have questions about individual jQuery Mobile capabilities, node attributes, or structures, please consult the jQuery Mobile Documentation.

The second pane will be used for displaying a list of tweets:

<!-- tweets pane -->
<div data-role="page" id="tweets">
 
  <div data-role="header">
    <a href="#home" id="tweetsBackButton">Back</a>
    
    <h1><span id="tweetsHeadTerm"></span> Tweets</h1>
  </div>
 
  <ul id="tweetsList" data-role="listview" data-inset="true"></ul>
 
</div>

CSS

Our Web app's CSS contains a CSS animation for fading in any element, as well as for overriding jQuery Mobile styles and general element styling.

/* animations */
@-moz-keyframes fadeIn {
  0%    { opacity: 0; }
  100%  { opacity: 1; }
}
@-webkit-keyframes fadeIn {
  0%    { opacity: 0; }
  100%  { opacity: 1; }
}

.fadeIn {
  -moz-animation-name: fadeIn;
  -moz-animation-duration: 2s;
 
  -webkit-animation-name: fadeIn;
  -webkit-animation-duration: 2s;
 
  opacity: 1 !important;
}

/* Custom CSS classes */
.clear        { clear: both; }
.hidden       { display: none; }
.opaque       { opacity: 0; }


/* Basic styling */
#prevLocationsContainer { margin-top: 40px; }

/* Customizing jQuery Styles */
#tweetsList li.ui-li-divider,
#tweetsList li.ui-li-static     { font-weight: normal !important; }


/* Tweet list */
#tweetsList li {
 
}

#tweetsList li .tweetImage {
  float: left;
  margin: 10px 10px 0 10px;
}

#tweetsList li .tweetContent {
  float: left;
  /* margin: 10px 0 0 0; */
}

#tweetsList li .tweetContent strong {
  display: block;
  padding-bottom: 5px;
}

/* Media Queries for device support */
@media screen and (max-device-width: 480px) {
  #prevLocationsContainer { margin-top: 10px; }
}

JavaScript

JavaScript is the last major component to add to our Web app. The following encompasses all capabilities for the app:

$(document).ready(function() {
 
  // Start from scratch on page load
  if(window.location.hash) {
    window.location = "index.html";
  }
 
  // Feature tests and settings
  var hasLocalStorage = "localStorage" in window,
    maxHistory = 10;
 
  // List of elements we'll use
  var $locationForm = $("#locationForm"),
    $locationInput = $("#locationInput"),
    
    $prevLocationsContainer = $("#prevLocationsContainer"),
    
    $tweetsHeadTerm = $("#tweetsHeadTerm"),
    $tweetsList = $("#tweetsList");
    
  // Hold last request jqXHR's so we can cancel to prevent multiple requests
  var lastRequest;
 
  // Create an application object
  app = {
    
    // App initialization
    init: function() {
      var self = this;
      
      // Focus on the search box
      focusOnLocationBox();
      
      // Add the form submission event
      $locationForm.on("submit", onFormSubmit);
      
      // Show history if there are items there
      this.history.init();
      
      // When the back button is clicked in the tweets pane, reset the form and focus
      $("#tweetsBackButton").on("click", function(e) {
        $locationInput.val("");
        setTimeout(focusOnLocationBox, 1000);
      });
      
      // Geolocate!
      geolocate();
      
      // When the tweets pane is swiped, go back to home
      $("#tweets").on("swiperight", function() {
        window.location.hash = "";
      });
      
      // Clear history when button clicked
      $("#clearHistoryButton").on("click", function(e) {
        e.preventDefault();
        localStorage.removeItem("history");
        self.history.hideList();
      })
    },
    
    // History modules
    history: {
      $listNode: $("#prevLocationsList"),
      $blockNode: $("#homePrev"),
      init: function() {
        var history = this.getItemsFromHistory(),
          self = this;
        
        // Add items to the list
        if(history.length) {
          history.forEach(function(item) {
            self.addItemToList(item);
          });
          self.showList();
        }
        
        // Use event delegation to look for list items clicked
        this.$listNode.delegate("a", "click", function(e) {
          $locationInput.val(e.target.textContent);
          onFormSubmit();
        });
      },
      getItemsFromHistory: function() {
        var history = "";
        
        if(hasLocalStorage) {
          history = localStorage.getItem("history");
        }
        
        return history ? JSON.parse(history) : [];
      },
      addItemToList: function(text, addToTop) {
        var $li = $("<li><a href='#'>" + text + "</a></li>"),
          listNode = this.$listNode[0];
          
        if(addToTop && listNode.childNodes.length) {
          $li.insertBefore(listNode.childNodes[0]);
        }
        else {
          $li.appendTo(this.$listNode);
        }
        
        this.$listNode.listview("refresh");
      },
      addItemToHistory: function(text, addListItem) {
        var currentItems = this.getItemsFromHistory(),
          newHistory = [text],
          self = this,
          found = false;
        
        // Cycle through the history, see if this is there
        $.each(currentItems, function(index, item) {
          if(item.toLowerCase() != text.toLowerCase()) {
            newHistory.push(item);
          }
          else {
            // We've hit a "repeater": signal to remove from list
            found = true;
            self.moveItemToTop(text);
          }
        });
        
        // Add a new item to the top of the list
        if(!found && addListItem) {
          this.addItemToList(text, true);
        }
        
        // Limit history to 10 items
        if(newHistory.length > maxHistory) {
          newHistory.length = maxHistory;
        }
        
        // Set new history
        if(hasLocalStorage) {
          // Wrap in try/catch block to prevent mobile safari issues with private browsing
          // http://frederictorres.blogspot.com/2011/11/quotaexceedederr-with-safari-mobile.html
          try {
            localStorage.setItem("history", JSON.stringify(newHistory));
          }
          catch(e){}
        }
        
        // Show the list
        this.showList();
      },
      showList: function() {
        $prevLocationsContainer.addClass("fadeIn");
        this.$listNode.listview("refresh");
      },
      hideList: function() {
        $prevLocationsContainer.removeClass("fadeIn");
      },
      moveItemToTop: function(text) {
        var self = this,
          $listNode = this.$listNode;
        
        $listNode.children().each(function() {
          if($.trim(this.textContent.toLowerCase()) == text.toLowerCase()) {
            $listNode[0].removeChild(this);
            self.addItemToList(text, true);
          }
        });
        
        $listNode.listview("refresh");
      }
    }
    
  };
 
  // Search submission
  function onFormSubmit(e) {
    if(e) e.preventDefault();
    
    // Trim the value
    var value = $.trim($locationInput.val());
    
    // Move to the tweets pane
    if(value) {
      
      // Add the search to history
      app.history.addItemToHistory(value, true);
      
      // Update the pane 2 header
      $tweetsHeadTerm.html(value);
      
      // If there's another request at the moment, cancel it
      if(lastRequest && lastRequest.readyState != 4) {
        lastRequest.abort();
      }
      
      // Make the JSONP call to Twitter
      lastRequest = $.ajax("http://search.twitter.com/search.json", {
        cache: false,
        crossDomain: true,
        data: {
          q: value
        },
        dataType: "jsonp",
        jsonpCallback: "twitterCallback",
        timeout: 3000
      });
    }
    else {
      // Focus on the search box
      focusOnLocationBox();
    }
    
    return false;
  }
 
  // Twitter reception
  window.twitterCallback = function(json) {
    
    var template = "<li><img src='{profile_image_url}' class='tweetImage' /><div class='tweetContent'>"
                   +"<strong>{from_user}</strong>{text}</div><div class='clear'></div></li>",
      tweetHTMLs = [];
      
    // Basic error handling
    if(json.error) { // Error for twitter
      showDialog("Twitter Error", "Twitter cannot provide tweet data.");
      return;
    }
    else if(!json.results.length) { // No results
      showDialog("Twitter Error", "No tweets could be found in your area.");
      return;
    }
    
    // Format the tweets
    $.each(json.results, function(index, item) {
      item.text = item.text.
            replace(/(https?:\/\/\S+)/gi,'<a href="$1" target="_blank">$1</a>').
            replace(/(^|\s)@(\w+)/g,'$1<a href="http://twitter.com/$2" target="_blank">@$2</a>').
            replace(/(^|\s)#(\w+)/g,'$1<a href="http://search.twitter.com/search?q=%23$2" target="_blank">#$2</a>')
      tweetHTMLs.push(substitute(template, item));
    });
    
    // Place tweet data into the form
    $tweetsList.html(tweetHTMLs.join(""));
    
    // Refresh the list view for proper formatting
    try {
      $tweetsList.listview("refresh");
    }
    catch(e) {}
    
    // Go to the tweets view
    window.location.hash = "tweets";
  };
 
  // Template substitution
  function substitute(str, obj) {
    return str.replace((/\\?{([^{}]+)}/g), function(match, name){
      if (match.charAt(0) == '\\') return match.slice(1);
      return (obj[name] != null) ? obj[name] : "";
    });
  }
 
  // Geolocates the user
  function geolocate() {
    if("geolocation" in navigator) {
      // Attempt to get the user position
      navigator.geolocation.getCurrentPosition(function(position) {
        // Set the address position
        if(position.address && position.address.city) {
          $locationInput.val(position.address.city);
        }
      });
    }
  }
 
  // Focuses on the input box
  function focusOnLocationBox() {
    $locationInput[0].focus();
  }
 
  // Modal function
  function showDialog(title, message) {
    $("#errorDialog h2.error-title").html(title);
    $("#errorDialog p.error-message").html(message);
    $.mobile.changePage("#errorDialog");
  }
 
  // Initialize the app
  app.init();
 
});

We won't go over every part of the app's JavaScript, but a few items are worth pointing out:

  • The script uses feature testing for both localStorage and geolocation. The app will not attempt to use either feature if they aren't present in the browser.
  • The geolocation API is used to detect the user's current location and the localStorage API is used to keep the user's search history.
  • The JavaScript has been written in a modular manner for easier testing.
  • A swipe event is listened for on the tweet pane. When the pane is swiped, the user is taken back to the first pane.
  • Twitter is the source of all data. No custom server-side processing is used for this app.

 

Document Tags and Contributors

Tags: 
Contributors to this page: Sheppy, markg, kohei.yoshino, Selinger, Machado, rsage
Last updated by: rsage,