As I pointed out in my previous post, Nitrogen standard functionality could be extended with custom elements. Standard set of Nitrogen controls is lacking a number of useful elements, e.g. tabs element is missing. In this post I will explain how to create tabs by writing a wrapper for JQuery UI tabs control.
I will be giving explanations by using code from “Nitrogen Elements” project. This project was started as a collection of useful custom elements but went stale after Nitrogen changed from version 1 ->; 2. All the controls were implemented for Nitrogen v1 and stopped working for v2. I think that a common project for custom Nitrogen controls is still a great idea so I will try to build few controls which are most useful – Tabs and JQGrid to start with and hopefully more people would join and contribute.
If you want to follow the code you can download Nitrogen Elements Examples which is a small collection of examples, including usage for “Tabs” and “JQGrid”.
JQuery Tabs is a great looking, fully functioning control and Nitrogen is already coming with JQuery UI library in the repo. So, all we need to do is to output initial html mark-up in the format that JQuery UI recognizes as a valid template for tabs, call JQuery function to init the control and then bind our control to the tabs events.
Nitrogen uses Erlang records to specify control properties. In nitrogen_elements/include there is a header file nitrogen_elements.hrl with definitions for nitrogen_elements controls.
The following records define “tabs” and “tab” elements (tabs is a collection of tab) and also specify custom actions – records which allow Nitrogen to output javascript to the page.
%% Elements -record(tabs, {?ELEMENT_BASE(element_tabs), tabs=[], options=[], tag}). -record(tab, {id=wf:temp_id(), title="No Title", class="", style="", body=[], tag, url}). %% Actions -record(tab_destroy, {?ACTION_BASE(action_tabs_methods)}). -record(tab_disable, {?ACTION_BASE(action_tabs_methods), tab=-1}). -record(tab_enable, {?ACTION_BASE(action_tabs_methods), tab=-1}). -record(tab_option, {?ACTION_BASE(action_tabs_methods), key, value}). -record(tab_add, {?ACTION_BASE(action_tabs_methods), url, label, index}). -record(tab_remove, {?ACTION_BASE(action_tabs_methods), tab}). -record(tab_select, {?ACTION_BASE(action_tabs_methods), tab}). -record(tab_load, {?ACTION_BASE(action_tabs_methods), tab}). -record(tab_url, {?ACTION_BASE(action_tabs_methods), tab, url}). -record(tab_abort, {?ACTION_BASE(action_tabs_methods)}). -record(tab_rotate, {?ACTION_BASE(action_tabs_methods), ms, continuing=false}). -record(tab_event_on, {?ACTION_BASE(action_tabs_methods), event}). -record(tab_event_off, {?ACTION_BASE(action_tabs_methods), event}).
module src/element_tabs/element_tabs.erl is what Nitrogen uses to render the element when it finds a record #tabs{} on the page. The naming convention is use “element_” for elements modules (for html) and “action_” for modules which output javascript code.
-module (element_tabs). -include ("nitrogen_elements.hrl"). -include_lib("nitrogen_core/include/wf.hrl"). -compile(export_all). reflect() -> record_info(fields, tabs). render_element(Record) ->; ID = Record#tabs.id, %% init jQuery tabs control with specified options Options = action_jquery_effect:options_to_js(Record#tabs.options), wf:wire(ID, wf:f("jQuery(obj('~s')).tabs(~s)", [ID, Options])), %% create html markup; #panel{ id = ID, body = [ #list{ class = wf:to_list(Record#tabs.class), style = wf:to_list(Record#tabs.style), body = [#listitem{body = tab_link(Tab)} || Tab <;- Record#tabs.tabs] }, [#panel{ html_id = Tab#tab.id, class = wf:to_list(Tab#tab.class), style = wf:to_list(Tab#tab.style), body = Tab#tab.body } || Tab <;- Record#tabs.tabs, Tab#tab.url =:= undefined] ] }. tab_link(#tab{url = undefined, id = Id, title = Title}) when is_atom(Id) ->; #link{url = "#" ++ wf:html_encode(atom_to_list(Id)), body = Title}; tab_link(#tab{url = undefined, id = Id, title = Title}) when is_list(Id) ->; #link{url = "#" ++ wf:html_encode(Id), body = Title}; tab_link(#tab{url=Url, id=Id, title=Title}) ->; #link{url=Url, title = wf:html_encode(Id), body=Title}. event(Event) -> ?PRINT({tabsevent, Event}), EventType = wf:q(event), ID = wf:q(tabs_id), TabIndex = wf:q(index), Module = wf:page_module(), Module:tabs_event(list_to_atom(EventType), ID, TabIndex).
we need to include record definitions for both nitrogen_core elements and our custom elements
-include (nitrogen_elements.hrl). -include_lib(nitrogen_core/include/wf.hrl).
according to the documentation for JQuery Tabs, it requires the following html markup to create tabs element:
<div id="tabs"> <ul> <li><a href="#tabs-1">Nunc tincidunt</a></li> <li><a href="#tabs-2">Proin dolor</a></li> </ul> <div id="tabs-1"> <p></p> </div> <div id="tabs-2"> <p></p> </div> </div>
You can find an excellent video tutorial here which explains in small details how JQuery turns this html into functioning tabs control. As far as we concerned, our erlang code needs to create a div element and then add un-ordered list, each element of which is a link to local resource in a form of “#tabs-1”. # character is significant. This link refers to html_id of “Div” element which represents a body of each tab. JQuery shows / hides this divs depending on which tab (link in the list) is clicked on.
We also need an ability to pass customization Options to each instance of Tabs, e.g. which tab is selected by default or which mouse event selects a tab – click or mouseover.
Options are passed to control by “options” attribute of #tabs record:
options=[ {selected, 0} {event, mouseover} ].
We then convert Erlang representation into json with:
Options = action_jquery_effect:options_to_js(Record#tabs.options).
The last thing we need to do is output javascript function which will turn html mark-up into Tabs control and we can do it with this line:
wf:wire(ID, wf:f("jQuery(obj('~s')).tabs(~s)", [ID, Options]))
We also want to be able to catch certain events from the control – to see how it works we need to look at module src/element_tabs/action_tabs_methods.erl:
-module(action_tabs_methods). -include(nitrogen_elements.hrl). -compile(export_all). -define(TABS_ELEMENT, #tabs{}). render_action(#tab_destroy{target = Target}) ->; wf:f(jQuery(obj('~s')).tabs('destroy'), [wf:to_js_id(Target)]); render_action(#tab_disable{target = Target, tab = Index}) ->; wf:f(jQuery(obj('~s')).tabs('disable', '~s'), [wf:to_js_id(Target), Index]); render_action(#tab_enable{target = Target, tab = Index}) ->; wf:f(jQuery(obj('~s')).tabs('enable;, '~s'), [wf:to_js_id(Target), Index]); render_action(#tab_add{target = Target, url = Url, label = Label}) ->; wf:f(jQuery(obj('~s')).tabs('add', '~s', '~s'), [wf:to_js_id(Target), Url, Label]); render_action(#tab_remove{target = Target, tab = Index}) ->; wf:f(jQuery(obj('~s')).tabs('remove', '~s'), [wf:to_js_id(Target), Index]); render_action(#tab_select{target = Target, tab = Index}) ->; wf:f(jQuery(obj('~s')).tabs('select', ~w), [Target, Index]); render_action(#tab_load{target = Target, tab = Index}) ->; wf:f(jQuery(obj('~s')).tabs('load', ~w), [Target, Index]); render_action(#tab_url{target = Target, tab = Index, url = Url}) ->; wf:f(jQuery(obj('~s')).tabs('load', ~w, '~s' ), [Target, Index, Url]); render_action(#tab_abort{target = Target}) ->; wf:f(jQuery(obj('~s')).tabs('abort'), [Target]); render_action(#tab_rotate{target = Target, ms = Ms, continuing = IsContinuing}) ->; wf:f(jQuery(obj('~s')).tabs('rotate', ~w, ~s), [Target, Ms, IsContinuing]); render_action(#tab_event_off{target = Target, event = Event}) ->; wf:f(jQuery(obj('~s')).unbind('~s'), [Target, Event]); render_action(#tab_event_on{target = Target, event = Event}) ->; PickledPostbackInfo = wf_event:serialize_event_context(tabsevent, Target, Target, ?TABS_ELEMENT#tabs.module), wf:f(jQuery(obj('~s')).bind('~s', function(e, ui) { Nitrogen.$queue_event('~s','~s',\"event=\" + e.type + \"&tabs_id=\" + '~s' + \"&index=\" + ui.index)})", [Target, Event, Target, PickledPostbackInfo, Target]).
This module allows to render javascript which specifies certain actions that you want to apply to the control. You can also bind to native tabs events. For example, you might want to get a callback in your erlang code everytime ‘tabsshow’ event happens.
You can do it with:
wf:wire(tabs, #tab_event_on{event = ?EVENT_TABSSHOW})
This will bind to ‘tabsshow’ even and send the event back to your control and you can catch it with the event function:
event(Event) -> ?PRINT({tabsevent, Event}), EventType = wf:q(event), ID = wf:q(tabs_id), TabIndex = wf:q(index), Module = wf:page_module(), Module:tabs_event(list_to_atom(EventType), ID, TabIndex).
This function calls tabs_event function on the page where the instance of the tabs control lives and you can process it like this:
tabs_event(?EVENT_TABSSHOW, _Tabs_Id, TabIndex) ->; ?PRINT({tabsevent, ?EVENT_TABSSHOW}).
This is pretty much it, to use tabs in your Nitrogen page you just need to add #tabs{} record with a list of #tab{} which define a body of each tab.
body() -> wf:wire(tabs, #tab_event_on{event = ?EVENT_TABSSHOW}), [ #tabs{ id = tabs, options=[ {selected, 0} {event, mouseover} ], tabs=[ #tab{title="Tab 1", url = "/content/tabs2.htm"}, #tab{title="Tab 2", body=["Tab two body..."]}, #tab{title="Tab 3", body=["Tab three body..."]} ] } ].
For usage example, see tabs.erl in nitrogen_elements_examples repo.