AJAX

The key feature provided by Ganelon and the main purpose for its existence is a framework allowing server-side code to finely control the contents and behaviour of browser's page, while still having the ability to render full page contents using the same code.

There are other popular frameworks following this principle, such as: Vaadin (Java, widget-oriented, only client-side rendering), Weblocks (Common LISP, continuations-based) or ZK (XML oriented, only client-side rendering) and Wicket (just like Vaadin, but with less complex widgets).

Actions

Action in its simplest is an AJAX-enabled web request handler, which returns a list of parametrised operations to a thin layer of JavaScript running in the browser.

One example of such operation can be an update of a certain page fragment or fragments. Another - displaying of a modal window or a Growl-style notification. But most importantly, it is very easy to provide in additional actions - just by referencing another .js file.

Client side JavaScript action interface is provided through the following files (assuming that /public resources are published as /):

(hiccup/include-js "/ganelon/js/jquery-1.8.1.min.js") ;jQuery - required
(hiccup/include-js "/ganelon/js/bootstrap.js") ;Bootstrap - optional
(hiccup/include-js "/ganelon/js/ganelon.js") ;basic actions support
(hiccup/include-js "/ganelon/js/ext/ganelon.ops.bootstrap.js") ;additional Bootstrap related actions
(hiccup/include-js "/ganelon/js/ext/ganelon.ops.gritter.js") ;growl-style notifications through gritter.js
(hiccup/include-js "/ganelon/actions.js") ;dynamic actions interface

The Ganelon JavaScript files are available in public.ganelon.js package- in case you need to merge them with your other client-side modules.

The Bootstrap and gritter support is optional - you can easily overwrite them with your own plugins.

You might also want to include CSS file for Growl-style notifications or Bootstrap (2.3.0) - or merge them with your CSS files:

(hiccup/include-css "/ganelon/css/bootstrap.css") ;Bootstrap - optional
(hiccup/include-css "/ganelon/css/jquery.gritter.css") ;growl-style notifications - optional

The Ganelon CSS files are available in public.ganelon.css package- in case you need to merge them with your other client-side stylesheets.

Code defining DIV#sample0 and a link invoking the action:

[:div#sample0.alert.alert-success "I'm a DIV, update me with AJAX!"]
[:p (widgets/action-link "ajax/sample0" {:msg "There ya go!"} {} [:span "Sure!"])]

And the code defining Ganelon action is as follows:

(actions/defjsonaction "ajax/sample0" [msg]
  (ui/fade "#sample0" (hiccup.util/escape-html msg)))

Try it in action:

I'm a DIV, update me with AJAX!

Sure!

Referencing actions - server-side

To generate HTML/JavaScript code invoking an action, we can use one of the provided macros/functions:

  • ganelon.web.widgets/action-link - a simple link, which adds widget-id parameter automatically (you can overwrite it though)
  • ganelon.web.widgets/action-button - a button, which works as link above
  • ganelon.web.widgets/action-form - render a form, which will invoke action on its submit

The links for actions above are rendered using the simplest code of all:

(widgets/action-link "ajax/sample0" {:msg "There ya go!"} {} [:span "Sure!"])

Referencing actions - client-side

If you include ganelon.web.app/javascript-handler in your ring routes, and reference /ganelon/actions.js in your web page, all the defined actions will be available to you as JavaScript functions:

Invoke:

GanelonAction.ajax_sample0($('#input1').val(), null,
                       function(data) { alert('done: ' + JSON.stringify(data)););});

Just as we have referenced /ajax/sample0 action using ganelon.web.widgets/action-link, we can reference it using JavaScript function GanelonAction.ajax_sample0 and integrate it with our client-side logic.

Custom actions

Technically, anything that returns JSON string can be an action in Ganelon:

(dyna-routes/setroute! :ajax-sample1
  (compojure/ANY "/a/ajax/sample1" []
    (response/json
      [{:type "dom-fade"
        :id "#sample0"
        :value "Polo!"}])))

Try it:

Marco?

Widgets

Widget in Ganelon framework simply defines a scope in a browser's DOM tree - usually a DIV, that is marked with an identificator - either fixed or randomly generated.

This widget's identificator can be accessed by internal action references and used to seamlessly update div contents.

When we want to mark a DOM tree element, we can use one of the following macros:

  • ganelon.web.widgets/with-div - generate random widget UID, and wrap its body with a DIV tag.
  • ganelon.web.widgets/with-span - generate random widget UID, and wrap its body with a SPAN tag.
  • ganelon.web.widgets/with-widget - take widget UID as parameter, and wrap its body with a DIV tag.
  • ganelon.web.widgets/with-set-id - just establish taken widget id as a scope, and evalute the body.
  • ganelon.web.widgets/with-id - just establish generated widget id as a scope, and evalute the body.

Example - microblog

With widgets, we can provide fully functional AJAX UI fragments, with a distinct separation between presentation (widgets) and UI logic (actions). The example below provides a simple microblogging engine (with entries stored in user's session, so don't worry!

Widgets

First we define the widget displaying single entry, with an Edit button:

(defn microblog-entry-widget [e]
  (widgets/with-div
    [:h4 (hiccup.util/escape-html (:title e))]
    (widgets/action-link "microblog-entry-edit"
      {:id (:id e)} {:class "btn"} "Edit")))

Please note, that we have used with-div macro to establish a widget, and a widget-update-link function to render a button that enables edit mode. Widget's id is passed silently as a parameter.

An entry edit form:

(defn microblog-edit-widget [e]
  (widgets/with-div
    (widgets/action-form "microblog-entry-update" {} {:class "well"}
      [:input {:type "hidden" :name "id" :value (:id e)}]
      [:span "Title: "]
      [:input {:type "text" :name "title" :value (:title e)}] [:br]
      [:button {:type "submit" :class "btn"} "Update entry"])))

The new element here is widget-form, which renders a form invoking AJAX action on submit.

Then, a widget with form for new entry and a list of existing ones:

(defn microblog-widget []
  (widgets/with-div
    [:h3 "My microblog"]
    ;the form
    (widgets/action-form "microblog-entry-add" {} {:class "well"}
      [:span "Title: "] [:input {:type "text" :name "title"}] [:br]
      [:button {:type "submit" :class "btn"} "Post entry"])
    ;display existing entries    
    (for [e (sort (comparator :id) (map second (or (sess/get :microblog) {})))]
      (microblog-entry-widget e) [:br] [:br])  [:br]
    (widgets/action-link "microblog-clear" {} {:class "btn"} "Remove all")))

No new components were used, but please take note on the widget hierarchy - the main widget contains all of the sub-widgets, nesting their respective widget ids.

Actions

Now, we can define corresponding actions. Since they are div-oriented actions, they just invoke necessary widget function.

Action for adding the entry updates session, and returns new version of the widget's HTML code:

(actions/defwidgetaction "microblog-entry-add" [title]
  (let [id (str (java.util.UUID/randomUUID))]
    (sess/put! :microblog
      (assoc (or (sess/get :microblog) {}) id {:id id :title title})))
  (microblog-widget))

Action that enables edit of an entry simply replaces one div with another in-place:

(actions/defwidgetaction "microblog-entry-edit" [id]
  (microblog-edit-widget (get (sess/get :microblog) id)))

Entry content update action replaces the view widget back:

(actions/defwidgetaction "microblog-entry-update" [id title]
  (sess/put! :microblog
    (assoc (or (sess/get :microblog) {}) id {:id id :title title}))
  (microblog-entry-widget {:id id :title title}))

And finally, an action to clear all entries in session and update widget (with subwidgets):

(actions/defwidgetaction "microblog-clear" []
  (sess/put! :microblog {})
  (microblog-widget))

Try it here:

My microblog

Title:

Remove all

Operations

An operation is a JSON object, trasmitted from server-side (Clojure), to client-side (JavaScript), usually performing side effects - from simple operations such as DOM manipulation to communication with complex application-tailored browser-side constructs.

Client-side Ganelon framework expects to receive JSON array with objects. They are directed to specific JavaScript functions using type: field, for example, operations data can be a vector with Clojure map defined below and sent from Ganelon action:

[{:type "dom-html"
  :id "#alink1"
  :value "Polo!"}]

This operation would invoke dom-html dispatcher, registered with Ganelon.registerOperation:

Ganelon.registerOperation('dom-html', function(o) {$(o.id).html(o.value);});

It is possible and adviced to define own operation types, for example

Ganelon framework comes with a set of prebuilt AJAX components and operations, including mapping for most of the jQuery Manipulation methods, for example:

  • ganelon.web.widgets/div-replace - replace content of a div named by :id keyword
  • ganelon.web.widgets/div-fade - replace content of a div named by :id keyword - with a fade effect
  • ganelon.web.widgets/refresh-page - reload page contents
  • ganelon.web.widgets/modal - display modal window (using Twitter Bootstrap modal plugin)
  • ganelon.web.widgets/remove-modal - remove modal window (using Twitter Bootstrap modal plugin)
  • ganelon.web.widgets/notification - display Growl-style notification

Operation queue

A real life operation does not always return all side effects in the end. Sometimes it is more convienent and understable, to apply certain effect before the final result.

With ganelon.web.actions/put-operation! it is possible to achieve that in an idiomatic way. The code below produces two side effects:

  • Two Growl-style notifications, put into queue with put-operation! calls.
  • Replacement with fade effect of a DOM tree fragment.

(actions/defjsonaction "demo-action" [msg widget-id]
  (actions/put-operation! (ui/notification "Success"
      (h/html "Message set to: " [:b (hiccup.util/escape-html msg)])))
  (actions/put-operation! (ui/notification "Another message"
      (h/html "Widget-id is: " [:b (hiccup.util/escape-html widget-id)])))
  (ui/fade widget-id (demo1-widget msg)))

Try it here:

The message is: Operations queue example