AJAX through JSON RPC

Note: this documentation used to refer to the module 'yaws_jsonrpc'. This module has been depracated in favor of 'yaws_rpc', which handles both JSON RPC, haXe and SOAP remoting. All references to 'yaws_jsonrpc' on this page were therefore changed to 'yaws_rpc'. For more specific information about SOAP, refer to the SOAP page.

The Yaws Json binding is a way to have Javascript code in the browser evaluate a remote procedure call in the Yaws server.JSON itself as described at http://www.json.org/ is basically a simple marshaling format which can be used from a variety of different programming languages, in particular it completely straightforward to implement in Javascript.

The yaws JSON implementation consist of Javascript client and a server side library which must be explicitly invoked by Erlang code in a .yaws page.

It is not particularly easy to show and explain an AJAX setup through JSON RPC, but here is an attempt:

First we have an HTML page which:

  1. Includes the client side of the JSON library. The library is included in the yaws distribution and it is found under "www/jsolait/jsolait.js" .

  2. Second the HTML code defines the name of a method, i.e. the name of a server side function which shall be called by the client side Javascript code

  3. Finally the HTML code defines a FORM which is used to invoke the RPC. This is just a really simple example, really any Javascript code can invoke any RPC in more interesting scenarios than submitting a form

The HTML code looks like

<html>
  <head>
    <title>Testing html-json library</title>
  </head>
  <script src="jsolait/jsolait.js"></script>
  <script>

var serviceURL = "json_sample.yaws";
var methods = [ "test1", "errortest" ];

var jsonrpc = imprt("jsonrpc");
var service = new jsonrpc.ServiceProxy(serviceURL, methods);

function test() {
    try {
     foo = document.getElementById('foo').value;
     bar = document.getElementById('bar').value;
     document.getElementById('result').innerHTML = 
       "<PRE>" + service.test1(foo, bar) + "</PRE>";
     } catch(e) {
        alert(e);
     } 
     return false;
}

function errortest() {
    try {
     document.getElementById('failure').innerHTML = 
       "<PRE>" + service.errortest() + "</PRE>";
     } catch(e) {
     document.getElementById('failure').innerHTML = 
        "<PRE>" + e + "</PRE>";
     } 
     return false;
}

  </script>
  <body>
    <form action="" method="post" onSubmit="return test()">
      <div id="result">
      </div>
      <p>
        First Argument:  <input id="foo" />
      </p>
      <p>
        Second Argument: <input id="bar"/>
      </p>
      <p>
        <input type="submit" value="Do JSON-RPC call"/>
      </p>
    </form>
    <form action="" method="post" onSubmit="return errortest()">
      <p>
        <input type="submit" value="Do JSON-RPC call expected to fail"/>
      </p>
      <div id="failure">
      </div>
    </form>
  </body>
</html>

This HTML code resides in file json_sample.html and it is the HTML code that is the AJAX GUI !!!

Following that we need to take a look at the page json_sample.yaws which is the "serviceURL" according to the Javascript code. This code defines the function to be called. Remember that the Javascript code defined one method, called "test1", this information will be passed to the serviceURL. The code looks like:

<erl module=sample_mod>
-compile(export_all).

out(A) -> 
    Peer = if
               tuple(A#arg.clisock),
               element(1, A#arg.clisock) == sslsocket ->
                   ssl:peername(A#arg.clisock);
               true ->
                   inet:peername(A#arg.clisock)
           end,
    
    {ok,{IP,_}} = Peer,
    A2=A#arg{state = [{ip, IP}]},
    yaws_rpc:handler_session(A2, {?MODULE, counter}).



counter([{ip, IP}] = _State, {call, errortest, Value} = _Request, Session) ->  
    io:format("Request = ~p~n", [_Request]),
    { false, { error, "Expected failure" } };

counter([{ip, IP}] = _State, {call, test1, Value} = _Request, Session) ->  
    io:format("Request = ~p~n", [_Request]),
    IPStr = io_lib:format("Client ip is  ~p~n" , [ IP ]),
    OldSession = io_lib:format("Request is: ~p~nOld session value "
                               "is ~p~n", [ _Request, Session ]),

    case Session of
        undefined -> % create new session
            NewSession = 0;
        10 ->        % reset session after reaching 10
            NewSession = undefined;
        N -> 
            NewSession = N + 1
    end,
    
    NewVal = io_lib:format("New session value is ~p ~n", [ NewSession ]),
    Str = lists:flatten([IPStr,OldSession,NewVal]),
    {true, 0, NewSession, {response,  Str }}.


</erl>

The two important lines on the server side are

  1. yaws_rpc:handler_session(A2, {sample_mod, counter}).
  2. counter([{ip, IP}] = _State, {call, test1, Value} = _Request, Session)

The first line tells Yaws to forward all JSON-RPC methods to the "counter" function in the "sample_mod" module. The second line says, basically, - this is the counter function that will be called when the client invokes a method called 'test1'. We would duplicate this line with a different name than 'test1' for each RPC function we wish to implement.

On the client side we have

var methods = [ "test1" ];
var jsonrpc = imprt("jsonrpc");
var service = new jsonrpc.ServiceProxy(serviceURL, methods);

Which registers the Yaws page with the JSON-RPC handler and gives it a list of methods that the Yaws page can satisfy. In this case, it is only the method called 'test1'.

When we wish to return structured data - we simply let the user defined RPC function return JSON structures such as

 {struct, [{field1, "foo"}, {field2, "bar"}]} 

for a structure and

{array, ["foo", "bar"]}

for an array. We can nest arrays and structs in each other

Finally, we must stress that this example is extremely simple. In order to build a proper AJAX application in Yaws, a lot of client side work is required, all Yaws provides is the basic mechanism whereby the client side Javascript code, can RPC the web server for data which can be subsequently used to populate the DOM. Also required to build a good AJAX application is good knowledge on how the DOM in the browser works

Update:The yaws_rpc:handler will now also call: M:F(cookie_expire) which is expected to return a proper Cookie expire string. This makes it possible to setup the Cookie lifetime. If this calback function is non-existant, the default behaviour is to not set a cookie expiration time, i.e it will live for this session only.

One more example

Here is yet another example, stolen from Tobbes blog

Setup the DOM

In the file ''ex1.html'' we create the DOM with a little HTML and add some Javascript that will talk with the Erlang server side.


<html>
<head>
<script type="text/javascript"
           src="/jquery-1.2.3.js"></script>
</head>
<body>

<script language="javascript" type="text/javascript">

function ex1(what) {
   $.getJSON("/ex1.yaws",
            {'op': "ex1", 'what': what},
            function(x) {
              do_ex1(what, x)
            });
}

function do_ex1(what, x) {
  jQuery.each(x, doit);
}

function doit() {
  $('#'+this.who).html(this.what);
}

</script>

<button onclick="ex1('one')">Update one!</button>
<button onclick="ex1('two')">Update two!</button>
<button onclick="ex1('three')">Update three!</button>

<div id="one">This is one</div>
<div id="two">This is two</div>
<div id="three">This is three</div>

</body>
</html>

The erlang server side

This is the code that needs to be installed and execute on the server side. It nicely illustrates how to return JSON structs to the client.

-module(ex1).
-export([out/1]).

out(A) ->
    L = yaws_api:parse_query(A),
    dispatch(lkup("op", L, false), A, L).

dispatch("ex1", A, L) -> 
    ex1(A, L).

ex1(_A, L) ->
    J = json:encode(array(what(lkup("what", L, false)))),
    return_json(J).

what("one")   -> one();
what("two")   -> one() ++ two();
what("three") -> one() ++ two() ++ three().

array(L) -> {array, L}.

one()   -> obj("one").
two()   -> obj("two").
three() -> obj("three").

obj(M) ->
    obj(M, "r").  

%%%
%%% How ::= "r" | "a"  , r=replace, a=append
%%%
obj(M, How) ->
    C = now2str(),
    [{struct,
      [{"who", M},
       {"how", How},
       {"what", C ++" "++M++" content"}]}].

return_json(Json) ->
    {content, 
    "application/json; charset=iso-8859-1", 
    Json}.

now2str() ->
    {A,B,C} = erlang:now(),
    i2l(A)++"-"++i2l(B)++"-"++i2l(C).

i2l(I) when is_integer(I) -> integer_to_list(I);
i2l(L) when is_list(L)    -> L.

lkup(Key, List, Def) ->
    case lists:keysearch(Key, 1, List) of
    {value,{_,Value}} -> Value;
    _                 -> Def
    end.

The json library

The Yaws JSON library contains 3 simple functions, One to encode and two for decoding. See source code json.erl for detailed instructions on usage

Valid XHTML 1.0!