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:
- e-mail format validation
- accepting e-mails only from the selected domain
- accepting e-mails only having the correct domain suffix
displaying the clickable e-mail link (mailto)
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:
handle_call/1 - that will be responsible for expanding the tag <wpart:email ... />
load_tpl/0 - that will be called during the system start and should load the tpl file into the memory
build_html_tag/3 - which will returned a form element for the automatic form builder
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:
Id - the generated element id - for the field identification
Params - the parameters from the record definition (such as format or, what is more important, html_attrs)
Default - the default/editing value that should be inserted into the HTML widget
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 - which will be responsible for formatting the value before displaying it on the site
validate/1 - which checks the correctness of the entered value
handle_call/2
Since 1.3 version handle_call/2 accepts two parameters:
Format - which specifies the format which developer put inside the template (string)
Value - the actual value we must format
Suppose we want to support four format types:
- the default one which will display the e-mail as it is, as a text
the clickable link which will launch the e-mail client (mailto as a Format value)
a string that will contain only the first and last letter of the login and the full domain (no_login)
a string that will have replaced @ sign with AT keyword and each . sign with DOT keyword (anti_spam)
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:
{ok, Value - when the validation succeeded (sometimes the conversion string -> other type is needed - like in date)
{error, {Reason, Value}} - when something goes wrong. The Reason will be displayed on the form next to its field. Its translation (the text that should be displayed for user) should be put in config/errors_description.conf file.
In our validator we should check for the following constraints:
the e-mail format ("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$") - mandatory check
- the domain suffix equality - optional check
- the domain equality - optional check
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!
