JavaScript Template of Amrita2

(caution: This is a experimental feature and not fixed)

Amrita2 supports JavaScripte Template.

Basically, it is a translator from HTML to JavaScript function running on Ruby. And the API and result is very simmilar to Amrita2 Ruby feature.

Sample

This is a sample code.

require 'amrita2/template'
require 'amrita2/js'
js = JS::CodeGenerator.new
t = TemplateText.new '<x><em id="aaa">xxx</em></x>' 

js.define_function(:sample) do 
  t.js_template('t', js)
  @js.code <<-END
  var tmpl = new t() ; 
  var e = document.createElement('div') ; 
  tmpl.expand(e, { 'aaa': '12' }) ;
  // e is  '<div><x><EM id=aaa>12</EM></x></div>'
  return e ;
  END
end
print js.results.join(NL)

The result is a JavaScript code.

function sample() {
  function t() {
    // .. template code for <x><em id="aaa">xxx</em></x>
  }
  var e = document.createElement('div') ; 
  var tmpl = new t() ; 
  tmpl.expand(outer, { 'aaa': '12' }) ;
  // e is  '<div><x><EM id=aaa>12</EM></x></div>'
  return e ;
}


 t.js_template('t', js)

makes a JavaScript function named 't'. It is very similar to Amrita2 Ruby Template Object.

tmpl.expand(outer, { 'aaa': '12' }) ;

returns a DOM Element object which is expanded from template and PO (Presentation Object).

In this case, it is

<div><x><EM id=aaa>12</EM></x></div>

How to use it

In every case, details of UI is a HTML text. Your JavaScript code dose not handle DOM functions or HTML tags. Only you should do is pass a PO object like this

{ 'aaa': '12' }

to the template object.

Stauts and Roadmap

I'd like to make this feature full compatible with Amrita2 Ruby template system. And I think I can make it because JavaScript has enough feature for Amrita2 Template.

But I don't know JavaScript Language well now. So the current version is very restricted and buggy.

Hello World

--- ../sample/js/hello.rb ---
require "amrita2/template"
require "amrita2/js"
include Amrita2

t = JS::TemplateText.new <<-END
   <div>
      <h1 id='title'>title will be inserted here</h1>
      <p>This paragraph is static part</p>
      <p id='body'>body text will be inserted here</p>

      <ul id='list'><li id='item' /></ul>
      <table id='table' border='1'>
         <tr id='detail'><td id='a' /><td id='b' /></tr>
      </table>
      <p id='active'>For detail see<a id='link'></a></p>
   </div>
END

js = JS::CodeGenerator.new
t.js_template('tmpl', js)
jssrc = Amrita2::JS::RUNTIME_SRC + js.results.join("\n") 

tmpl = TemplateText.new <<END
<html>
<head>
<title>Amrita2 JS Hello</title>
<script id='script' language="JavaScript" type="text/javascript"></script>
<script language="JavaScript" type="text/javascript">
function debug(s) {
}

function main()
{ 
  var e = document.getElementById('body') ;
  var t = new tmpl() ;
  var data = { 
    'title': 'Hello JavaScript', 
    'body': 'This paragraph is dynamic parta and it was generated by amrita2 javascript template',
    'list': { 'item': [1,2,3] },
    'table': { 
      'detail': [
         { 'a':11 , 'b': 12 },
         { 'a':21 , 'b': 22 }
      ]
    },
    'active': function(t) {
       t.link('Amrita2 Hope Page', { 'href': 'http://amrita2.rubyforge.org/'} ) ;
    }
  } ;
  t.expand(e, data) ;
}
</script>
</head>

<body onload='main()'>
<div id='body' />
</body>
</html>
END


tmpl.expand(STDOUT, :script => Amrita2::SanitizedString[jssrc], :body=>true)



Ajax with Amrita2

--- ../sample/js/json_ajax.rb ---
require "amrita2/template"
require "amrita2/js"
include Amrita2

# template for the JavaScript
t = JS::TemplateText.new <<-END
   <div>
      <h1 id='title'>title will be inserted here</h1>
      <p>This paragraph is static part</p>
      <p id='body'>body text will be inserted here</p>
      <p id='now'></p>
   </div>
END

# compiling the template to JavaScript functions
js = JS::CodeGenerator.new
t.js_template('tmpl', js)
jssrc = Amrita2::JS::RUNTIME_SRC + js.results.join("\n") 

# template for the document
tmpl = TemplateText.new <<END
<html>
<head>
<title>Amrita2 JS Hello</title>
<script id='script' language="JavaScript" type="text/javascript"></script>
<script language="JavaScript" type="text/javascript">

// This will be removed until release
function debug(s) {
}

// HTTP accessing function
/* from http://www.peej.co.uk/articles/rich-user-experience.html */
function getHTTPObject() {
  var xmlhttp;
  try {  
    xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
  } catch (e) {
    try {
      xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    } catch (E) {
      xmlhttp = false; 
    } 
  } 
  if (!xmlhttp) { 
    if(typeof XMLHttpRequest != 'undefined') {
      try {
        xmlhttp = new XMLHttpRequest(); 
      } catch (e) {
        xmlhttp = false; 
      } 
    }
  }
  return xmlhttp; 
}

// Use the function compiled from from the template
function expand_template(datasrc)
{ 
  // get the element to which the template will be inserted
  var e = document.getElementById('output') ;
  while (e.hasChildNodes()) {
      e.removeChild(e.firstChild) ;
  }
  // make a template object
  var t = new tmpl() ;
  // get data
  eval(datasrc) ;
  // add changing data for demo
  var d = new Date()
  data.now = d.getMinutes() + ":" + d.getSeconds() ;
  // mix the data and the template
  t.expand(e, data) ;

}

/* 
  json_ajax_data.txt is a file contains a 
  JavaScript source generating a data.

  data = {
    'title': 'Ajax with Amrita2 ',
    'body':  'This is a text from the data source.'
  }

*/

function getData() { 
  var http = getHTTPObject(); 
  http.open("GET", "json_ajax_data.txt", true); 
  http.onreadystatechange = function() {
    if (http.readyState == 4) {
      expand_template(http.responseText); 
      setTimeout("getData()", 3000) ;
    } 
  } 
  http.send(null);
}

</script>
</head>

<body onload='getData()'>
<div id='output' />
</body>
</html>
END

# expand the document statically
tmpl.expand(STDOUT, :script => Amrita2::SanitizedString[jssrc], :output=>true)

Ajax with Amrita2 and Ruby on Rails

sample/rails/todoajax/ contains a Ajax version of Rails Tutorial

Function and appearance is same but there is a important difference: this version refreshes every 5 seconds the TODO list asynchronously. If you typed some text for a new TODO, the text field dose not lose foucus and the text you typed will be kept when refreshing is done.

The view related code of this sample is here.

def json_list_script
  tmpl = Amrita2::JS::TemplateText.new <<-END
  <div>
  Not done:
  <div id='not_done'>
     <input checked="checked" id="item_done" name="item[done]" type="checkbox" value="0" />
     <span> </span>
     <span id='description' />
     <span> </span>
     <a id='edit_link' href="/todo/edit/">Edit</a> 
     .....
  END
  # compiling the template to JavaScript functions
  js = Amrita2::JS::CodeGenerator.new
  tmpl.js_template('tmpl', js)
  Amrita2::JS::RUNTIME_SRC + js.results.join("\n") 
end

def json_list_po
  {
    :output=>true,
    :script=>Amrita2::SanitizedString[json_list_script]
  }
end

Using this template, the static JavaScript code below display the page.

function display_item(t, done, item) 
{
  var onclick = 'document.location.href="/todo/toggle_check/' ;
  onclick += item.id  + '"' ;
  var edit_url = "/todo/edit/" + item.id ;
  var destroy_url = "/todo/destroy/" + item.id ;
  var destroy_confirm = 'return confirm("Are you sure you want to delete this entry: ' + item.description + '")' ;
  var f = function(t) {
     if (! ie_flag) {
       t.item_done('',{'onclick': onclick }) ;
     }
     t.description(item.description) ;
     t.edit_link('Edit', { 'href' : edit_url } ) ;
     t.destroy_link('Destroy', { 'href' : destroy_url, 'onclick': destroy_confirm } ) ;
  }
  if (done) {
    t.done(f) ;
  } else {
    t.not_done(f) ;
  }
}


// Use the function compiled from from the template
function expand_template(data)
{ 
  // get the element to which the template will be inserted
  var e = document.getElementById('output') ;
  while (e.hasChildNodes()) {
      e.removeChild(e.firstChild) ;
  }
  // make a template object
  var t = new tmpl() ;
  // add changing data for demo
  var d = new Date()
  data.now = d.getMinutes() + ":" + d.getSeconds() ;
  // mix the data and the template
  t.expand(e, 
    function(t) {
      var not_done = data.not_done ;
      for (var i = 0 ; i < not_done.length ; i++) {
        var item = not_done[i] ;
        display_item(t, false, item) ;

      } 
      var done = data.done ;
      for (var i = 0 ; i < done.length ; i++) {
        var item = done[i] ;
        display_item(t, true, item) ;
      } 
    }
  ) ;

}

And data for refreshing is sent in JSON format. This is very simple with ruby-json library.

def json_data
  not_done = Todo.find_not_done
  done = Todo.find_done
  data = { 
    :not_done => not_done.collect do |r|
      { 
        :description=>r.description,
        :item_done=>r.done,
        :id=> r.id 
      }
    end,
    :done => done.collect do |r|
      { 
        :description=>r.description,
        :item_done=>r.done,
        :id=> r.id 
      }
    end
  }
  json_response = { :ret => 200, :body => data }
  headers["Content-Type"] = 'text/javascript'
  render_text('json_response = ' + json_response.to_json)
end