N2O Web Applications


After downloading and bootstraping a sample shipped with N2O you should be able to see colored repl. This is regular Erlang shell, but it is aware of OTP applications placed in apps and deps directories, how to resolve them and perform proper starting.

Single Page Application

The page is usually contains HTML controls upon which event handlers will binds. Also you should specify a list of n2o.js modules, like protocols, components, translations or other modules with up to date client functionality. Here is sample SPA:

During each initial route connection, main/0 function followed by event(init) is being called to render postbacks of active controls. Same page module could serve SPA and/or DTL pages. N2O_start() boots up a supported connection and send initial <<"N2O,">> authorization packet to open a protocol stream connection.

Listing 1. Route module

main()    -> #dtl { file="index",
                    bindings=[{body,body()} ] }.

body()    -> [ #panel{id=history},
                       postback=chat,source=[message]} ].

event(init) -> wf:info(?MODULE,"init~n",[]);
event(E)    -> wf:info(?MODULE,"event ~p~n",[E]).

After establishing WebSocket connection you should be able to see IO answer terminated in JSON encoding, e.g. {eval:"console.log('ok');",data:''}. Here is the IO answer generated for 'send' button postback:

Listing 2. Test WebSocket connection
$ tcpkali -T1s --verbose 3 \
     --first-message "N2O," --ws

Destination: []:8000
Data(161): ➧HTTP/1.1 101 Switching Protocols\r\n
  connection: Upgrade\r\n
  Access-Control-Allow-Origin: *\r\n
  upgrade: websocket\r\n
  sec-websocket-accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=\r\n
Ramped up to 1 connections.
Data(414): ➧\201~\001\232{"eval":"{ var x = qi('send'); 
x && x.addEventListener('click',function (event){{ if 
(validateSources(['message'])) ws.send(enc(tuple(atom(
tuple(utf8_toByteArray('send'), bin('detail')), event.
detail)])));else console.log('Validation Error'); }});
Total data sent:     166 bytes (166 bytes)
Total data received: 575 bytes (575 bytes)
Bandwidth per channel: 0.006 Mbps, 0.7 kBps
Aggregate bandwidth: 0.005↓, 0.001↑ Mbps
Test duration: 1.00634 s.

Web Logic

Web logic shoud be as much declarative as possible at the same time the web logic language should be readable by Java, .NET engineers. Web Logic of Deposits Application is powered by N2O WebSocket Application Server.

Note: Operating clean and minimalistic API will allow you engineers to think on core features of the system, SpawnProc provides most minimal Business Processing frameworks and libraries.

Autogenerated Forms

You have total freedom in developing your web form applications: using low-level nitrogen DSL erlang records markup; or you may use declarative forms definition, using nitrogen DSL as target language. There are other options, like non-HTML control elements with spa protocol or DTL or other template enginer rendering.

Picture 1. One-Time Password Form
Listing 3. OTP Form
document(Name,Phone) -> #document { name = Name,

    sections = [   #sec { name= [ deposits:translate({?MODULE,message}), 
                          wf:to_list(Phone#phone.number)] } ],
    buttons  = [   #but { name='decline', title=deposits:translate(decline),
                          class=cancel, postback={'CloseOpenedForm', Name} },
                   #but { name='next', title = deposits:translate(proceed),
                         class = [button,sgreen], sources = [otp],
                         postback = {'Spinner', {'OpenForm',Name}} } ],
    fields   = [ #field { name='otp', type=otp, 
                          title= deposits:translate({?MODULE,pass}),
                          labelClass=label, fieldClass=column3} ] }.
Picture 2. Open Application Form
Listing 4. Open Application Form
document(Name,Curr) ->
                #document { name = Name,
    sections = [    #sec  { name= deposits:translate(name) } ],
    buttons  = [    #but {  name='decline', title = deposits:translate(decline),
                            class=cancel, postback={'CloseOpenedForm', Name} },
                    #but {  name='next', title = deposits:translate(proceed),
                            postback={'Spinner', {'CheckApp',Name}},
                            sources= [depositName,money,getMoneyCard,
                            partialWithdrawal,codeValue] } ],
    fields   = [   #field { name='duration', type=integer,
                            title= deposits:translate(duration),
                            pos = #deposit_app.duration,
                            format = deposits:translate(durationformat),
                            tooltips = [deposits:translate(tooltip1)] },
                   #field { name='rate', type=integer,
                            title= deposits:translate(rate),
                            pos = #deposit_app.rate,
                            format = deposits:translate(rateformat),
                            postfun = fun(X) -> proplists:get_value(rate,X) end,
                            tooltips = [deposits:translate(tooltip2)]},
                   #field { name=money, type=money, curr= Curr,
                            min=20, max=100000,
                            title= deposits:translate(ammount),
                            pos = #deposit_app.amount,
                            tooltips = [deposits:translate(tooltip3) ]},
                    #field { name='depositName', type=string,
                             length=30,min=0, max=30,
                            title= deposits:translate(title),
                            pos = #deposit_app.name,
                            tooltips = [deposits:translate(tooltip4)] },
                    #field { name='bonusCode', type=combo,
                            title= deposits:translate(bonus),
                            options= [
                                    title = deposits:translate(no)},
                                    ,title = deposits:translate(yes)}],
                             tooltips = [deposits:translate({?MODULE, tooltip5})] },
                    #field { name='codeInput', type=empty },
                    #field { name='charge', type=combo,
                            title= deposits:translate(source),
                            options= [
                                      title = deposits:translate(cash)},
                                      title = deposits:translate(card)}],
                            tooltips = [deposits:translate(tooltip7),
                                        deposits:translate(tooltip6)] },
                    #field { name=[cardsFrom,userCardsFrom,cardSelectFrom],
                            title= deposits:translate(yourcard) },
                    #field  { name='percents', type=combo,
                            title= deposits:translate(action),
                            options= [
                                     title = deposits:translate(add)},
                                     title = deposits:translate(move)}],
                            tooltips = [deposits:translate({?MODULE, tooltip9}),
                                        deposits:translate(tooltip8)] },
                    #field { name=cardsPercent, type=empty } ] }.

Rendered DOM javascript command, Bytes:
> wf:actions(undefined).
> wf:update("1",open_app:new('DP00_12_uah')).
> size(iolist_to_binary(wf:render(wf:actions()))).
Compressed DSL form in BERT, Bytes:
> size(term_to_binary(open_app:new('DP00_12_uah'),[compressed])).
> size(term_to_binary(open_app:new('DP00_12_uah'))).

Declarative compressed FORM in BERT, Bytes:
> size(term_to_binary(open_app:document(maxim,"uah",y,y,y))).
> size(term_to_binary(open_app:document(maxim,"uah",y,y,y),