Saturday 16 May 2009

Using Clojure and Ext/JS

Ext/JS is a popular framework for building Ajax interfaces. The model for Ajax applications is dead simple. Build a UI using JavaScript, and populate it using JSON retrieved from the server (I'm pretty sure there's more to it, but I like simple!).

JavaScript and Lisp languages should go together pretty well, Doug Crockford has described JavaScript as Lisp in C's clothing.

I haven't done enough JavaScript programming to be dangerous just yet, so I'll start with something simple from the Ext tutorial. The goal will be to display a data-grid populated by stuff retrieved using the persistence API I looked at previously. Here's the end goal:

Ext Datagrid populated with data from Clojure.

Firstly we need to write a servlet that will retrieve all the stories. If we were going to do this properly we'd parametrize this with restrictions (find me all stories between X and Y, or retrieve the top 100 stories). I'm confident that no-one will ever use this site, so I'll just go for brute force retrieve everything!

Using the persistence API we prepare a query object to find everything matching the given type and lazily convert this (map) each element across using entity-as-map


(defn get-all
[type]
(let [query (Query. (str type))
service (DatastoreServiceFactory/getDatastoreService)]
(map entity-to-map (.asIterable (.prepare service query)))))


Now we can retrieve stories, we need to squirt this back as JSON. There's a package, clojure-json that does this for you, but I (foolishly) decided to do it the quick and dirty way and print out a string!


(ns news.liststory
(:use (news appengine))
(:gen-class :extends javax.servlet.http.HttpServlet))

(defn story-to-json
[s]
(str "{\"title\":\"" (get s "title") "\"" ",\"body\":" "\"" (get s "body") "\"},"))

(defn -doGet
[_ request response]
(.setContentType response "text/xml")
(let [w (.getWriter response) stories (get-all "story")]
(.println w (str "{\"totalCount\":" (count stories) ",\"stories\":["))
(doseq [story stories]
(.println w (str \tab (story-to-json story))))
(.println w (str "]}"))))


So what were aiming to print out is a JSON representation of the stories that looks a little like this.


{"totalCount":3,"stories":[
{"title":"Home of the Clojure Language","body":"http://www.clojure.org/"},
{"title":"Jeff's web page, full of rubbish!","body":"http://www.fatvat.co.uk"},
{"title":"Wikipedia on Clojure","body":"http://en.wikipedia.org/wiki/Clojure"},
]}


Finally, we need something to render this. I took an example as a starting point and ended up with this. (Note that it's referencing local host because I'm running off a local development environment)



Ext.onReady(function(){
var store = new Ext.data.JsonStore({
root: 'stories',
totalProperty: 'totalCount',
idProperty: 'storyId',
fields: [
'body', 'title'
],

proxy: new Ext.data.HttpProxy({
url: 'http://localhost:8080/liststory?'
})
});

var grid = new Ext.grid.GridPanel({
el: 'story-grid',
title: 'Clojure News!',
store: store,
loadMask: true,
height: 400,
columns:[{
header: 'Link',
dataIndex: 'body'
},{
header: 'Description',
dataIndex: 'title'
}]
});

grid.render();
store.load({});
});


That was pretty painless to build. The painful bit was writing out JSON as a string. Lesson learnt, use a library (or build a decent abstraction).

I'm not sure I like mixing my languages, I'd really like a way to have Lisp goodness across the whole stack. One potential option for this is to use Google Web Toolkit. GWT compiles Java into cross-platform Javascript. I could (probably) have Clojure compile to Java which in turn is compiled to Javascript. That sounds fun!

2 comments:

  1. No need to go through GWT from Clojure. Just write a "little language" (er, excuse me! D.S.L.) in Clojure for describing the UI. Have that language generate Javascript and whatever else (HTML, SVG, ...) either on the fly or generate files that can be cached on the web, etc.

    ReplyDelete
  2. Your example is not working.
    I'm getting the following error:

    this.el is null
    [Break on this error] container = this.el.dom.parentNode;

    ReplyDelete