Wednesday, December 19, 2012

Easy MicroTemplating

I'm a huge fan of templates in that I always want to separate my view from my code. But I don't like to include hundreds of kilobytes of some library just to get templating - so I made my own in a few lines which I can just copy into any project.

The most basic usage is to pass it data and a template and get the result:
microTemplate({name:'Jack'}, '<h1>Hello {{name}}</h1>');
This will return:
<h1>Hello Jack</h1>

Now there are different ways of using it. You can specify a DOM element which contains your template so if you have this in your HTML:
<div id="op"><h1>Hello {{name}}</h1></div>
You can just do:
microTemplate({name:'Jack'}, $('#op')[0]);
This will automatically write the result back into your div.

It supports arrays so:
var friends = [{name:'Jack'},{name:'Jill'}];
microTemplate(friends, '<h1>Hello {{name}}</h1>', $('#op3')[0]);
Will result in:
<h1>Hello Jack</h1>
<h1>Hello Jill</h1>

If you want a table or an ordered list you'll need to specify header and footer:
microTemplate(friends, '<li>{{name}}</li>', $('#op4')[0], '<ol>', '</ol>');
Which will result in:
<ol>
 <li>Jack</li>
 <li>Jill</li>
</ol>

I think it's awesome!


<!doctype html>
<html>
<head>
  <script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
  <script>
  $(function() {
    microTemplate({name:'Jack'}, $('#op')[0]);
    microTemplate({name:'Jack'}, '<h1>Hello {{name}}</h1>', $('#op2')[0]);
    microTemplate([{name:'Jack'},{name:'>>Jill<<'}], '<h1>Hello {{name}}</h1>', $('#op3')[0]);
    microTemplate([{name:'Jack'},{name:'Jill'}], '<li>{{name}}</li>', $('#op4')[0], '<ol>', '</ol>');
  });
  
  function microTemplate(data, temp, el, head, foot) {
    if (typeof temp.innerHTML=='string') {el=temp;temp=temp.innerHTML;}
    data = [].concat(data);
    var res = head||'';
    for (var i in data) {
      res += temp;
      for (var f in data[i]) {
        res = res.replace(new RegExp('{{{'+f+'}}}', 'ig'), data[i][f]);
        res = res.replace(new RegExp('{{'+f+'}}', 'ig'), data[i][f].replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'));
      }
    }
    res += foot||'';
    if (el && typeof el.innerHTML == 'string') el.innerHTML = res;
    return res;
  } 
  </script>
</head>
<body>

<div id="op"><h1>Hello {{name}}</h1></div>
<div id="op2"></div>
<div id="op3"></div>
<div id="op4"></div>

</body>
</html>


Saturday, December 15, 2012

ssiJS: Server Side Includes on the Client

I cut my teeth on classic ASP where you build applications by including various blocks using SSI (server-side include) syntax. If you wanted to include common content (e.g. branding or a copyright statement) inside your <body> you would include it like this:
<!-- #include file="copyright.asp" -->

Aside from <iframe> there is no way to include content from other sites using HTML. You need JavaScript so I wrote a 3-liner jQuery script which does just that. Consider this HTML:
<div class="ssi" data-url="copyright.html"></div>

Add the following JavaScript to your $(function(){}); and you'll have client side includes:

$.each($('div.ssi'), function(a,b) {
$(b).load(b.getAttribute('data-url'));
});

What does this do? It iterates through all divs with the class 'ssi' and loads their content asynchronously from the url specified in their 'data-url' attribute. I think this is neat.

Full source:


<!doctype html>
<html>
<head>
<script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
<script>
$(function() {
$.each($('div.ssi'), function(a,b) {
$(b).load(b.getAttribute('data-url'));
});
});
</script>
</head>
<body>
<div class="ssi" data-url="a.html"></div>
<div class="ssi" data-url="b.html"></div>
</body>
</html>

Tuesday, November 1, 2011

YQL: All the Data you can Eat

I love SQL and I love data. Just give me a database and some SQL and I'll analyse the bejeesuz out of it. I also like flexible interfaces. YQL provides all of this. The Yahoo Query Language is basically 2 things:
  1. A huge data source
  2. An easy query language which resembles SQL
  3. Standardized output format (JSON or XML)
OK, that was 3 things but I'm not going back to change my assertion just because I thought of an extra thing whilst typing. And, speaking of inaccuracies, I saaay "data source" but actually it's mostly just passing on data it gets from elsewhere. Let's dive in:

Say you wanna query an RSS feed:
SELECT * FROM rss WHERE url = 'http://sports.yahoo.com/top/rss.xml'

This will return the RSS items from the Yahoo! sports site. You can of course specify multiple feeds using IN (url1, url2) and you can also sort (example).

You can query weather, flickr for photos, find restaurants in San Francisco , list music videos ... I think you get the picture.

There is also the YQL Console, a place to test out your queries. It will generate the link which you can then use in your applications to query data. As you will see, you can specify the output format as XML or JSON:
http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20geo.oceans&format=json&callback=showOceans

The callback function wraps your data in a javascript function which means you just AJAX the URL above and your callback function will automatically be executed.

Finally the really gr8 thing about YQL is that it's free and it's cross-domain compatible. If you've ever made an application which fetches data from a different domain you'll know of the issues involved. Modern browsers just don't allow it unless the server says it's OK. YQL always adds the "it's OK header" (Access-Control-Allow-Origin: *) which means it damn well works. Very nice of dem folks at Yahoo!.

Finally, the other of the final 3 great things is that you can literally query (scrape) any web page on the, er, web. You can query normal HTML pages or XHTML or XML sources.

Coming soon, my jQuery Mobile RSS Reader app which uses YQL and Local Storage to save your RSS.

Sunday, September 11, 2011

SOAP2REST Proxy with node.js

Given the rebirth of JavaScript in the past decade it shouldn't surprise us that some wise-guy asked why we don't use the same language on the server as we use on the client: JavaScript!

Actually Microsoft was doing this in the 90's with their <script runat="server"> where you could write either VBScript or JScript both client and server side. Then browsers like Firefox took off and VBScript was unsupported leading to a rebirth of the powerful JavaScript language.

Node.js (or node.exe) is basically an interpreter like php.exe except you can actually use it stand-alone as it's own web service. You write a simple server.js like this:
var http = require('http');
var srv = http.createServer(function (request, response) {
  response.writeHead(200, {'Content-Type': 'text/html'});
  response.write("<h1>Hello World!</h1>");
  response.end();
}).listen(8080);
Next you run your script from the command prompt:
C:\node.exe server.js
Finally, open up your browser to http://localhost:8080 and you should see Hello World! in large, friendly letters.

Because I'm planning a killer hip mobile/offline app which consumes SOAP web services I decided to write a node.js script which will allow me to make simple HTTP GET requests and receive JSON data in return. The soap2rest.js proxy I wrote converts the GET request to a SOAP request and should parse the SOAP response to return JSON:
myApp.html -> HTTP GET -> (soap2rest.js) -> SOAP Request -> SOAP Response -> JSON

The first thing I need is an HTTP client. It's important to remember that our soap2rest.js is going to proxy the SOAP request so I made a generic HTTP request function:

function doHTTP(options, success, error) {

  var req = http.request(options, function(res) {
    var data = '';
    res.setEncoding(options.encoding || 'utf8');
    res.on('data', function (chunk) { data += chunk; });
    res.on('end', function () { success({ data: data, response: res }); });
  });

  if (typeof options.body != 'undefined') req.write(options.body);
  req.end();
  
  req.on('error', function(e) { error('HTTP ERROR: ' + e.message); });
}
As you can see the function simply takes all it's parameters via the options object and has two callback functions for success and error. Now all we have to do is map the incoming GET to an HTTP SOAP request. In order to make this proxy as generic as possible I defined the following syntax for the URL: http://whatever/service/method?parameters=values So, for a simple weather service I would call: http://localhost:8080/weather/fetch?city=Zurich Before I can call my soap2rest.js proxy I need to define the service call. This is done in a configuration file called fetch.json corresponding to the fetch method of my weather service. The configuration file sits in a folder called weather with other potential methods: weather/fetch.json:
{
 "url": "http://www.webservicex.net/globalweather.asmx?op=GetWeather",
 "headers": {
  "SOAPAction": "http://www.webserviceX.NET/GetWeather",
  "Content-Type": "text/xml"
 }
}
This configuration file tells SOAP2REST.js almost everything it needs to know about calling the SOAP service globalweather.asmx. What it still needs is the request format. This can be discovered by parsing the WSDL of the service but this is seriously complicated and liable to error. And I'm too lazy. So, the developer "just" has to provide the sample request body in fetch.xml inside the weather/ folder:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope  
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetWeather xmlns="http://www.webserviceX.NET">
      <CityName>##CITY##</CityName>
      <CountryName></CountryName>
    </GetWeather>
  </soap:Body>
</soap:Envelope>
The ##CITY## is a placeholder which will be replaced with the city parameter of the incoming GET request. Here is where the magic happens. The SOAP2REST function does the following:
  1. Parse the URL and determine the service and method
  2. Read the service/method.json configuration file
  3. Read the service/method.xml request file
  4. Replace all placeholders from the query string (e.g. city)
  5. Call the SOAP service POSTing the method.xml data
  6. Return the response to the client
So here's the magic SOAP2REST:
function SOAP2REST(URL, success, error) {

  URL = url.parse(URL, true);

  // Load Service Definition
  try {
    var soapJSON = fs.readFileSync('./soap' + URL.pathname + '.json', 'UTF-8');
  } catch (e) {
    return die({msg: "Service description " + URL.pathname + ".json could not be found!", err: e}, 404);
  }
  
  // Load SOAP Request
  var soapXML = '';
  try {
    soapXML = fs.readFileSync('./soap' + URL.pathname + '.xml', 'UTF-8');
  } catch (e) {
    // No problem
  }
  
  // Parse SOAP Configuration
  try {
    var soapJSON = JSON.parse(soapJSON);
 var soapURL = url.parse(soapJSON.url, true);
  } catch (e) {
    return die({msg: "Service JSON could not be parsed!", name: e.name, message: e.message}, 500);
  }
  
  // Set Parameters
  for (p in URL.query) soapXML = soapXML.replace('##' + p.toUpperCase() + '##', URL.query[p]);

  // Prepare SOAP Request Headers
  soapJSON.headers = soapJSON.headers || {};
  soapJSON.headers["Content-Length"] = soapXML.length;
  soapJSON.headers["Connection"] = "close";
  
  // Do SOAP Call
  var httpOptions = {
    host:     soapURL.hostname,
    post:     soapURL.port || 80,
    method:   soapJSON.method || 'POST',
    path:     soapURL.pathname,
    headers:  soapJSON.headers,
  };
  
  httpOptions.body = soapXML;
  
  doHTTP(httpOptions,
    function(d) {
      success(d);
    },
    function(e){
      error(e, 500);
    }
  );

}
Technically, I guess, this ain't REST because I'm not using /weather/fetch/city/Zurich/ but I prefer /weather/fetch?city=Zurich because this is the standard HTTP way of passing parameters and it means AJAX forms will work automagically!

If I do say so myself, I like the way you can easily add a new service on the fly simply by adding a .json and .xml file.

Now to find an XML2JSON converter...

Note: This code is all for node.js v5.6 and the functions seem to change with every release so no doubt this won't work in future! Full Source Code

Tuesday, August 30, 2011

Tinkering with Javascript Libraries on jsfiddle.net

Man I'm out of it. Such great tools that I missed during my post-2.0 slumber.

jsfiddle allows you to quickly and easily make and test JavaScript apps using some of the cool libraries out there (e.g. jQuery or mootools).

Just go to http://jsfiddle.net and start hacking in your HTML, Javascript and CSS. Press Ctrl+Enter to run and your app will, er, run!

I think it's cool.

Check out my sandbox application which says hello to you via ajax. Oh the wonders of modern technology.

Monday, August 29, 2011

Mongoose: Free Easy Web Server

I've always been an IIS fan but the move to Windows 7 and IIS7 has shown me that I'm getting old. I can't get IIS to do anything right and so I've decided: let's re-rethink this. Back to basics. KISS!

I googled "simple http server" and discovered mongoose.

It's Simple™. It Works™. I Like It™.

Download the mongoose.exe and run it and you've just started your webserver.

By default, mongoose serves from C:\ so if you browse to http://localhost/ you should see a directory listing.

Mongoose is configurable via the command line or via a configuration file.

Example: Server on Port 81
C:\>mongoose.exe -p 81

or, via config file you create a mongoose.conf file with the following content:
p 81


You'll probably do something wrong at some stage so you need to tell mongoose where to log errors. This is done via the e parameter:
C:\>mongoose.exe -e error.log -p 81

or
e error.log


Check out the full manual explaining all commands.

This thing can even run PHP! Just tell it which file extensions you want to be processed via CGI:
c .php

Next, tell it where your php executable lies:
I c:\\php\\php-cgi.exe

(The double backslashes are required for windows PCs like mine)

Here is my sample/example mongoose.conf:
# Error Log
e error.log

# Ports (csv)
p 80

# Root directory
r c:\\mywebsite\\html

# Default documents
# (disabled with # comment)
#i index.htm,index.html

# Add your special mime types here
m .manifest=text/cache-manifest,.asp=text/html

# CGI File extensions (csb)
c .php

# Location of CGI interpreter (e.g. PHP)
I c:\\php\\php-cgi.exe


Happy serving!

Thursday, December 2, 2010

Offline Bible on the iPhone with jQuery and Lawnchair


Using jQuery and Lawnchair as well as the new offline capabilities of HTML5 I've developed an offline Bible reader for the iPhone though it should work on other devices as well.

The interface has been kept lean and mean and you can simply swipe to scroll down the chapter, use the <> buttons to navigate a chapter and the dropdown list to select a book.

The first time you start the application it loads all books from the server. So far I've only got the ESV New Testament and when I find a free Bible version and get it converted to JSON I'll publish the app.

The app works offline as well so you can browse the texts using the new offline cache of HTML5. This great new standard allows you to define which scripts and resources the browser should keep available offline (i.e. cache). You include your manifest in the HTML tag as follows:


The DOCTYPE command tells the browser that this is an HTML5 document. Also, my manifest is an ASP file because your web server must serve the manifest with the Content-Type set to "text/cache-manifest". To avoid configuring your web server I added the header in the ASP code so it will run on any unconfigured IIS server:
<% Response.ContentType = "text/cache-manifest" %>CACHE MANIFEST


CACHE:
jquery-1.4.4.js
LawnchairAdaptorHelpers.js
DOMStorageAdaptor.js
Lawnchair.js
bible.js
bible.css
bible.png
Lawnchair is a free JavaScript library which allows you to save data on the client. My application loads the books of the New Testament from simples text files like "Matthew.txt" which contain the chapters and verses in JSON format. Here's 2 John.txt:
{

b:"2 John",
i:63,
c:[
{v:[
{t:"The elder to the elect lady and her children, whom I love in truth, and not only I, but also all who know the truth,"},
{t:"because of the truth that abides in us and will be with us forever:"},
{t:"Grace, mercy, and peace will be with us, from God the Father and from Jesus Christ the Father's Son, in truth and love."},
{t:"I rejoiced greatly to find some of your children walking in the truth, just as we were commanded by the Father."},
{t:"And now I ask you, dear lady - not as though I were writing you a new commandment, but the one we have had from the beginning - that we love one another."},
{t:"And this is love, that we walk according to his commandments; this is the commandment, just as you have heard from the beginning, so that you should walk in it."},
{t:"For many deceivers have gone out into the world, those who do not confess the coming of Jesus Christ in the flesh. Such a one is the deceiver and the antichrist."},
{t:"Watch yourselves, so that you may not lose what we have worked for, but may win a full reward."},
{t:"Everyone who goes on ahead and does not abide in the teaching of Christ, does not have God. Whoever abides in the teaching has both the Father and the Son."},
{t:"If anyone comes to you and does not bring this teaching, do not receive him into your house or give him any greeting,"},
{t:"for whoever greets him takes part in his wicked works."},
{t:"Though I have much to write to you, I would rather not use paper and ink. Instead I hope to come to you and talk face to face, so that our joy may be complete."},
{t:"The children of your elect sister greet you."}
]}
]}
As you can see each book has 3 properties:
  1. The name b:"2 John"
  2. The index order in the canon i:63
  3. The chapter list c:[]
The index order is used for displaying the dropdown list for selecting a book.

These text files are loaded via the jQuery.ajax() function which is then parsed as a JavaScript Object. This object is then stored locally via the Lawnchair library which supports various types of storage including a SQLite database. Although Apple's iPhone supports this "WebSQL" format, FireFox doesn't, so I stuck to localStorage aka. "DOM Storage".

To retrieve a book which is cached, you use the get() function which is a bit unwieldy since it uses callbacks instead of just returning a value:
db.bible = new Lawnchair({table:'bible'});

db.bible.get('2 John', function(r){
db.currentBook = r;
});
The first command basically says I want a new data store or table called "bible" and I use get() to fetch the book object. This r object has the b, i and c[] properties and I can loop through the chapters or the verses in a specific chapter and display them.

To display the chapter list we need a dropdown list in our HTML:


Which we then fill with the cool jQuery function each() which loops through our array:
$('#chapter').children().remove();

$.each(db.currentBook.c, function(index, value) {
$('#chapter').append($("").attr("value",index+1).text(index+1));
});
Listing verses in a chapter is also easy:
 $('#verseList').empty();

$.each(db.currentBook.c[chapter], function(i,v) {
$('#verseList').append(''+(i+1)+''+v.t+' ');
});
window.scrollTo(0,1);

I added the scrollTo command for two reasons: 1) The user should be taken to the top of the screen and 2) That this automatically hides Safari's toolbar on the iPhone which can get in the way.

The little next ">" and previous "<" buttons also use jQuery eventing:
$('#next,#next1').click(nextChapter);

$('#prev,#prev1').click(prevChapter);

And I have a function to disable them when we are at the start or end of a chapter:
function showNextPrev() {

$('#prev1, #prev').attr("disabled",db.lastBook.c==1)
$('#next1, #next').attr("disabled",db.lastBook.c==db.currentBook.c.length)
}
To make the app look sexy you should always add a nice icon and include it in your HTML as follows:


The viewport meta tag is also important because it ensures the iPhone browser displays your app in the correct size in any orientation!


Finally, I tried the jQuery swipe plugin because I wanted to be able to swipe forwards and backwards. Unfortunately the swiping doesn't work well and it disabled the ability to scroll up and down so I've removed it but I may come back to it again because that would be awesome.