Marzhill Musings

...

Creating Custom Nitrogen Elements

Published On: 2009-05-22 19:05:57
Nitrogen is a web framework written in erlang for Fast AJAX Web applications. You can get Nitrogen on github Nitrogen comes with a set of useful controls, or elements in nitrogen parlance, but if you are going to do anything more fancy than a basic hello world you probably want to create some custom controls. This tutorial will walk you through the ins and outs of writing a custom element for Nitrogen. We will be creating a simple notification element similar to one I use in the Iterate! project. It will need to be able to:
  • show a message
  • have a way to dismiss it
  • and optionally expire and disappear after a configurable period of time
Every Nitrogen element has two main pieces: the Record and the Module. I'll go through each in order and walk you through creating our notification element.

The Record

The record defines all the state required to create a nitrogen element. Every record needs a certain base set of fields. These fields can be added to your record with the ?ELEMENT_BASE macro. The macro is available in the nitrogen include file wf.inc. That include file also gives you access to all the included nitrogen element records. Below you can see the record definition for our notify element. Since it is very simple in it's design it only needs the base elements and two additional fields. expire to handle our optional expiration time and default to false to indicate no expiration. msg to hold the contents of our notification.
    1 %Put this line in an include file for your elements
    2 -record(notify, {?ELEMENT_BASE(element_notify), expire=false, msg}).
    1 % put these at the top of your elements module
    2 -include_lib("nitrogen/include/wf.inc").
    3 % the above mentioned include file you may call it whatever you want
    4 -include("elements.inc").
The ELEMENT_BASE macro gives your element several fields and identifies for the element which module handles the rendering of your nitrogen element. You can specify any module you want but the convention is to name the module with element_element_name. The fields provided are: id, class, style, actions, and show_if. You can use them as you wish when it comes time to render your element. Which brings us to the module.

The Module

Of the two pieces of a nitrogen element the module does the manual labor. It renders and in some cases defines the handlers for events fired by the element. The module must export a render/2 function. This function will be called whenever nitrogen needs to render a particular instance of your element. It's two arguments are: The ControlId, and the Record defining this element instance. Of these the ControlID is probably the least understood. It is passed into your render method by nitrogen and is the assigned HTML Id for your particular element. This is important to understand because, when you call the next render method in your elements tree, you will have to pass an ID on. The rule of thumb I use is that if you want to use a different Id for your toplevel element then you can ignore the ControlId. Otherwise you should use it as the id for your toplevel element in the control. So your element's module should start out with something like this:
    1 -module(element_notify).
    2 -compile(export_all).
    3 -include_lib("nitrogen/include/wf.inc").
    4 -include("elements.hrl").
    5 % give us a way to inspect the fields of this elements record
    6 % useful in the shell where record_info isn't available
    7 reflect() -> record_info(fields, notify).
    8 % Render the custom element
    9 render(ControlId, R) ->
   10     % get a temp id for our notify element instance
   11     Id = ControlId,
   12     % Our toplevel of the element will be a panel (div)
   13     Panel = #panel{id=Id},
   14     % the element_panel module is used to render the panel element
   15     element_panel:render(Id, Panel),
   16     % Or use the alternative method:
   17     Module = Panel#panel.module,
   18     Module:render(Id, Panel).
Notice that the records module attribute tells us what module we should call to render the element in the alternative method. In our case we will just hardcode the module since it's known to us. So now we have a basic element that renders a div with a temp id to our page. That's not terribly useful though. We actually need this element to render our msg, and with some events attached. Lets add the code to add our message to the panels contents.
    1 Panel = #panel{id=Id, body=R#notify.msg},
    2 element_panel:render(ControlId, Panel)
Now whatever is in the msg attribute of our notify record will be in the body of the panel when it gets rendered. All we need is a way to dismiss it. A link should do the trick. But now we have a slight problem. In order to add our dismiss link we need to add it to the body of the Panel. but the msg is already occupying that space. We could use a list and prepend the link to the end of the list for the body but that doesn't really give us a lot of control over styling the element. what we really need is for the msg to be in an inner panel and the outer panel will hold any controls the element needs.
    1 Link = #link{text="dismiss"},
    2 InnerPanel = #panel{body=R#notify.msg},
    3 Panel = #panel{id=Id, body=[InnerPanel,Link]},
    4 element_panel:render(ControlId, Panel)
Our link doesn't actually dismiss the notification yet though. To add that we need to add a click event to the link. Nitrogen has a large set of events and effects available. You can find them . We will be using the click event and the hide effect.
    1 Event = #event{type=click,
    2 actions=#hide{effect=blind, target=Id}},
    3 Link = #link{text="dismiss", actions=Event},
Now our module should look something like this:
    1 -module(element_notify).
    2 -compile(export_all).
    3 -include_lib("nitrogen/include/wf.inc").
    4 -include("elements.hrl").
    5 % give us a way to inspect the fields of this elements record
    6 % useful in the shell where record_info isn't available
    7 reflect() -> record_info(fields, notify).
    8 % Render the custom element
    9 render(ControlId, R) ->
   10     % get a temp id for our notify element instance
   11     Id = ControlId,
   12     % Our toplevel of the element will be a panel (div)
   13     Event = #event{type=click, actions=#hide{effect=blind, target=Id}},
   14     Link = #link{text="dismiss", actions=Event},
   15     InnerPanel = #panel{body=R#notify.msg},
   16     Panel = #panel{id=Id, body=[InnerPanel,Link]},
   17     % the element_panel module is used to render the panel element
   18     element_panel:render(Id, Panel).
This is a fully functional nitrogen element. But it's missing a crucial feature to really shine. Our third feature for this element was an optional expiration for the notification. Right now you have to click dismiss to get rid of the element on the page. But sometimes we might want the element to go away after a predetermined time. This is what our expire record field is meant to determine for us. There are three possible cases for this field.
  • set to false (the default)
  • set to some integer (the number of seconds after which we want to go away)
  • set to anything else (the error condition)
This is the kind of thing erlang's case statement was made for:
    1 case R#notify.expire of
    2   false ->
    3     undefined;
    4   N when is_integer(N) ->
    5     % we expire in this many seconds
    6     wf:wire(Id, #event{type='timer', delay=N, actions=#hide{effect=blind, target=Id}});
    7   _ ->
    8     % log error and don't expire
    9     undefined
   10 end
Notice the wf:wire statement. wf:wire is an alternate way to add events to a nitrogen element. Just specify the id and then the event record/javascript string you want to use. I've noticed that for events of type timer wf:wire works better than assigning them to the actions field of the event record. No idea why because I have not looked into it real closely yet. Now our module looks like this:
    1 -module(element_notify).
    2 -compile(export_all).
    3 -include_lib("nitrogen/include/wf.inc").
    4 -include("elements.hrl").
    5 % give us a way to inspect the fields of this elements record
    6 % useful in the shell where record_info isn't available
    7 reflect() ->record_info(fields, notify).
    8 % Render the custom element
    9 render(_, R) ->
   10   % get a temp id for our notify element instance
   11   Id = ControlId,
   12   % Our toplevel of the element will be a panel (div)
   13   case R#notify.expire of
   14     false ->
   15       undefined;
   16     N when is_integer(N) ->
   17       % we expire in this many seconds
   18       wf:wire(Id, #event{type='timer', delay=N, actions=#hide{effect=blind, target=Id}});
   19     _ ->
   20       % log error and don't expire
   21       undefined
   22   end,
   23   Event = #event{type=click, actions=#hide{effect=blind, target=Id}},
   24   Link = #link{text="dismiss", actions=Event},
   25   InnerPanel = #panel{body=R#notify.msg},
   26   Panel = #panel{id=Id, body=[InnerPanel,Link]},
   27   % the element_panel module is used to render the panel element
   28   element_panel:render(ControlId, Panel).
We have now fulfilled all of our criteria for the element. It shows a message of our choosing. It can be dismissed with a click. And it has an optional expiration. One last thing to really polish it off though would to allow styling through the use of css classes. The ELEMENT_BASE macro we used in our record definition gives our element a class field. We can use that to set our Panel's class, allowing any user of the element to set the class as they wish like so:
    1 Panel = #panel{id=Id, class=["notify ", R#notify.class],
    2 body=[InnerPanel,Link]},
This gives us the final module for our custom element:
    1 -module(element_notify).
    2 -compile(export_all).
    3 -include_lib("nitrogen/include/wf.inc").
    4 -include("elements.hrl").
    5 % give us a way to inspect the fields of this elements record
    6 % useful in the shell where record_info isn't available
    7 reflect() -> record_info(fields, notify).
    8   % Render the custom element
    9   render(_, R) ->
   10   % get a temp id for our notify element instance
   11   Id = ControlId,
   12   % Our toplevel of the element will be a panel (div)
   13   case R#notify.expire of
   14     false ->
   15       undefined;
   16     N when is_integer(N) ->
   17       % we expire in this many seconds
   18       wf:wire(Id, #event{type='timer', delay=N, actions=#hide{effect=blind, target=Id}});
   19     _ ->
   20       % log error and don't expire
   21       undefined
   22   end,
   23   Event = #event{type=click, actions=#hide{effect=blind, target=Id}},
   24   Link = #link{text="dismiss", actions=Event},
   25   InnerPanel = #panel{body=R#notify.msg},
   26   Panel = #panel{id=Id, class=["notify ", R#notify.class],
   27   body=[InnerPanel,Link]},
   28   % the element_panel module is used to render the panel element
   29   element_panel:render(ControlId, Panel).
I will cover delegated events and more advanced topics in a later tutorial.

Tags:
...