How to create a basic type

Basic types in Erlang Web have been described here.

In this tutorial we will learn how to create and use new basic type. As an example, we will implement new basic type of type e_mail.

Basic type description

Our basic type will be responsible for handling e-mail addresses fields. New type should provide the following features:

Each of the basic type consists at least of two files: one that is for displaying the field on the auto-generated form (wpart_) and one for the validation and formatting process (wtype_). The first one must implement behaviour wpart , the latter: wtype. Let's then start from the wpart part.

Implementing wpart_email

wpart behaviour forces us to export at least three functions:

load_tpl/0

Let's start from the middle: load_tpl/0. This function is very easy and intuitive: we must point out which template file is our tpl and under which key we want to keep it. The implementation should look like:

   1 load_tpl() ->
   2     wpart_gen:load_tpl(email,
   3                        filename:join([code:priv_dir(myapp),"html","email.tpl"])).

The system during the application start will call wpart_email:load_tpl() and it will cause loading the template stored in the priv/html/email.tpl directory of the myapp application into memory under the {wpart, email} key (we have used wpart_gen:load_tpl/2 which assumes that the namespace is wpart. We can specify a different namespace (first element of the key tuple) by calling wpart_gen:load_tpl/3, e.g. wpart_gen:load_tpl(my_namespace, email, Path)).

Now we should prepare a proper template (and save it in priv/html/email.tpl subdirectory of myapp application):

<input type="text" value="<% value %>" <% rest_html %> /><br/>

The code snippet quoted above will be cut into pieces in the places of <% ... %> and will be filled during the template assembly process (wpart_gen:build_html/2).

build_html_tag/3

Now comes the last function: build_html_tag/3. What it does is to return a form element that will be inserted into the whole form. It takes the following arguments:

The implementation should then look like:

   1 build_html_tag(Id, Params, Default) ->
   2     Attrs0 = wpart:normalize_html_attrs(proplists:get_value(html_attrs, Params, [])),
   3     Attrs = [{"name", Id}, {"id", Id} | proplists:delete("name", Attrs0)],
   4 
   5     wpart_gen:build_html(wpart_gen:tpl_get(email), 
   6                          [{"rest_html", wpart:proplist2html(Attrs)}, 
   7                           {"value", Default}]).

The html_attrs will be taken from the record definition parameters and converted into the proper format (property list containing {"string", "string"} tuples). Then we will add the "name" and "id" properties and assembly the previously loaded template (wpart_gen:tpl_get/1 loads the template and wpart_gen:build_html/2 assemblies it). The list of arguments is then converted into HTML format (key="value") and inserted into the given places (rest_html and value - as defined in the template snippet). We return a string that holds a fragment of HTML code that will be injected into the big, auto generated form.

handle_call/1

The first function (the last one described), handle_call/1 must accept a Xmerl record, #xmlElement, as an only argument and return #xmlText which will be displayed on the site. Unfortunately, we do not have an access to the record definition file, so all HTML arguments that are specified in the templates and which are not the meta-arguments (such as format in date) will be passed through:

   1 handle_call(#xmlElement{attributes = Attrs0}) ->
   2     Attrs = wpart:xml2proplist(Attrs0),
   3     
   4     #xmlText{value=wpart_gen:build_html(wpart_gen:tpl_get(string), 
   5                          [{"rest_html", wpart:proplist2html(Attrs)}]),
   6              type=cdata}.

We are using the same assembly tool, but surround it with #xmlText record. Moreover, we are not specifying the default widget value (will be blank). For conversion from #xmlAttribute into the property list, wpart:xml2proplist/1 is used.

The whole file is now ready to use - we can test it by embedding

<wpart:email />

somewhere on our template page.

Since the additional parameters are passed through, we can do something more:

<wpart:email size="100" />

Although it is possible to use the email basic type in our record definitions, though it does not have a sense unless we have a validator (wtype). Let's then move to it.

Implementing wtype_email

wtype behaviour defines the following callback functions:

handle_call/2

Since 1.3 version handle_call/2 accepts two parameters:

Suppose we want to support four format types:

Let's go!:

   1 handle_call("mailto", Email) when is_list(Email) ->
   2     "<a href=\"mailto:" ++ Email ++ "\"> ++ Email ++ "</a>";
   3 handle_call("no_login", Email) when is_list(Email) ->
   4     [Login, Domain] = string:tokens(Email),
   5     [hd(Login)] ++ "..." ++ [hd(lists:reverse(Login))] ++ "@" ++ Domain;
   6 handle_call("anti_spam", Email) when is_list(Email) ->
   7     re:replace(re:replace(Email, "@", " AT "), "\\.", " DOT ", [{return, list}]);
   8 handle_call(_Format, Email) when is_list(Email) ->
   9     Email;
  10 handle_call(_Format, undefined) ->
  11     [].

Great! From now, assuming we keep an email address under the my_email key in the request dictionary we are able to call:

<wpart:lookup key="my_email" format="mailto" />
<img src="my_email_address.png" wpart:alt="{[email(anti_spam)]my_email}" />

and other.

validate/2

Last implementation step is to write the validation function. The validation function accepts one argument: a two element tuple: {RecordParameters, Input}. In case when user does not provide any value in the form, Input will be set to undefined. The validation function should return the following values:

In our validator we should check for the following constraints:

The implementation will look like:

   1 validate({Types,undefined}) -> 
   2     case wpart_valid:is_private(Types) of
   3         true ->
   4             {ok, undefined};
   5         false ->
   6             case lists:keysearch(optional, 1, Types) of
   7                 {value, {optional, Default}} -> 
   8                     {ok, Default};
   9                 _ ->  
  10                     {error, {empty_input, undefined}}
  11             end
  12     end;
  13 
  14 validate({Types,Email}) when is_list(Email) -> 
  15     case wpart_valid:is_private(Types) of
  16         true ->
  17             {ok, Email};
  18         false ->
  19             case regexp:run(Email, "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$") of
  20                 match ->
  21                     case check_domain_suffix(Email, Types) of
  22                         {ok, Email} ->
  23                             check_domain(Email, Types);
  24                         ErrorSuffix ->
  25                             ErrorSuffix
  26                     end;
  27                 nomatch ->
  28                     {error, {bad_format, Email}}
  29             end
  30     end.
  31 
  32 check_domain_suffix(Email, Params) ->
  33     case proplists:get_value(domain_suffix, Params) of
  34         undefined ->
  35             {ok, Email};
  36         Suffix ->
  37             case lists:suffix(Suffix, Email) of
  38                 true ->
  39                     {ok, Email};
  40                 false ->
  41                     {error, {bad_suffix, Email}}
  42             end
  43     end.
  44 
  45 check_domain(Email, Params) ->
  46     case proplists:get_value(domain, Params) of
  47         undefined ->
  48             {ok, Email};
  49         Domain ->
  50             case string:tokens(Email, "@") of
  51                 [_, Domain] ->
  52                     {ok, Email};
  53                 _ ->
  54                     {error, {bad_domain, Email}}
  55             end
  56     end.

And that is everything when it comes to the implementation.

project.conf modification

The last part of the new basic type creation is registering it in our framework. It could be done by editing the project configuration file: project.conf by adding the {primitive_types, [ListOfUserDefinedPrimitiveTypes]} configuration tuple. In our case it will be:

   1 {primitive_types, [email]}.

From now, the new basic type: email is available all over the framework!

HowTo/CreatePrimitiveType (last edited 2009-05-05 10:36:13 by Michal Ptaszek)