Monday, December 11, 2006

ErlyWeb Tutorial Part 2

I have been looking forward to getting back to adding to this tutorial, but I have not had the chance the past few days. Now that I have a little time, I can jump in and finish the tutorial. What this part of the tutorial will address is the edit functionality for our blog application. Taking a hint from the CRUD functions that is in ErlyWeb, I will do something similar.

(Tip for Erlang newbies: use m(module_name) to find out about what functions are exported from a module. Very handy feature!)

ErlyDB looks like a smart fellow, because it knows that, despite using basic tuples, can figure out if the data is in the database or not. It probably isn't anything special, and Yariv will probably agree (since he wrote it), but it is interesting nonetheless.

I said this for a reason. We can similify our code, transforming the functionality in new_post/1 into something that calls a generic function, new_or_edit/2, which will also handle post editing. This is possible because ErlyDB saves records with same function.

In your entries_controller.erl file, get rid of the new_post/1 function, and then stick this in:


new_post(Vals) ->
Entry = entries:set_fields_from_strs(entries:new(), Vals),
new_or_edit(Entry, Vals).


We exported most of the logic that was in new_post/1 into new_or_edit/2. Put the new_or_edit/2 function in:



new_or_edit(Entry, Vals) ->
EntryV = entries:set_fields_from_strs(Entry, Vals),
Errors = helpers:validate(entries_validate, entries, EntryV),

case Errors of
ok ->
entries:save(EntryV),
{ok, EntryV};
_ ->
{Errors, EntryV}
end.



It is a fact that this function applies to both creating a new entry and editing an entry. This is true because what we are going to show now, with the edit functionality. Put this code in:


%% Edit functions.

edit(A, Id) ->
case yaws_arg:method(A) of
'GET' ->
{data, {"", edit_get(Id)}};
'POST' ->
Vals = yaws_api:parse_post(A),
{Errors, Entry} = edit_post(Vals, Id),
case Errors of
ok ->
{ewr, entries, index};
_ ->
{data, {Errors, Entry}}
end
end.


edit_get(Id) ->
Entry = entries:find_id(Id),
Entry.

edit_post(Vals, Id) ->
Entry = entries:find_id(Id),
new_or_edit(Entry, Vals).



WAIT A MINUTE! We are doing almost the same thing that is being done in our new function! Time to do some parameterization, to get rid of that look of cut-and-paste yuckiness. Look at where the differences are ... The function that is called to get an entry in "GET" and possibly, the space where we do a redirect. Let's add a library function! But, for you neophytes to functional programming ... we are going to use higher-order functions.

Here is the generic function:



form_process(A,
GetAction,
ModelAction,
PostSuccessAction,
PostFailureAction) ->
case yaws_arg:method(A) of
'GET' ->
GetAction();
'POST' ->
Vals = yaws_api:parse_post(A),
{Errors, Record} = ModelAction(Vals),
case Errors of
ok ->
PostSuccessAction();
_ ->
PostFailureAction(Errors, Record)
end
end.


Essentially, the form_process/5 function is just a generalization of what we have been doing before. Except, this time, we replaced the specific places with function calls - to functions we will define in our new/1 function.


new(A) ->
GetAction = fun() ->
{data, {[], new_get()}} end,
ModelAction = fun(Vals) ->
new_post(Vals) end,
PostSuccessAction = fun() ->
{ewr, entries, index} end,
PostFailureAction = fun(Errors, Entry) ->
{data, {Errors, Entry}} end,

form_process(A, GetAction, ModelAction, PostSuccessAction, PostFailureAction).


and then edit/1:


edit(A, Id) ->
GetAction = fun() ->
{data, {[], edit_get(Id)}} end,
ModelAction = fun(Vals) ->
edit_post(Vals, Id) end,
PostSuccessAction = fun() ->
{ewr, entries, index} end,
PostFailureAction = fun(Errors, Entry) ->
{data, {Errors, Entry}} end,

form_process(A, GetAction, ModelAction, PostSuccessAction, PostFailureAction).



(NOTE! For some weird reason, when I am running new/1 through the browser, and hit submit, it takes a bit too long to load. Is this a bug in my code? edit/1, though, works like a charm. Go figure.)

So, we developed a pattern using common code and, more importantly, a common convention, meaning a common set of expectations. There is nothing grandiose about the convention, and yes, you can completely ignore it. The lines of code in my function is not my concern, but whether or not I can make the code more declarative, in the sense that I say "do this in this location, do this in this other location." But, I am thinking that the code there can be tightened up even more.

In your entries_view.erl file, add these functions:


new(Data) ->
entry_form:display_form(Data, "../new", "").

edit(Data) ->
entry_form:display_form(Data, "../edit",
integer_to_list(entries:id(element(2,Data)))).


Yep, we are using the same form for editing and displaying. However, we made a change. I needed to make the action in the form generic. So the new parameters represent the url to go to. I moved the form to its own module, in a file called "entry_form.et" - I am going for some sense of template organization, by putting one page per ErlTL file. But, I originally put it in "entries_show.et". Note the changes I made with the parameters. Here is the new lines:

<%@ display_form(Items, GoTo, Id) %>

and

< form action="<% GoTo %>/<% Id %>" method="POST" >


We also want to make a small addition, by adding a "view" function. This one is quite simple. In your entries_controller.erl file, add this function:


view(A, Id) ->
Entry = get_entry(Id),
{data, Entry}.


In your entries_view.erl file, add this function:


view(Data) ->
entry_view:view(Data).


Add a file called "entry_view.et" for the view, and add this to the file:


<%@ view(Data) %>
< b><% entries:title(Data) %>< /b>
< br />< br />
<% entries:body(Data) %>
< br />

< i>by: <% entries:author(Data) %>< /i>
< br />< br />
<% erlyweb_html:a(["../edit", integer_to_list(entries:id(Data))], "Edit Entry") %>


Maybe, also, add a link to the view in your entries_show.et file.


<% erlyweb_html:a(["view", integer_to_list(entries:id(Entry))], "view") %>


Put it in your entry/1 function in the template.

Also, maybe you can finish off the show_entries/1 function in the same template file with a link to the new entry form. I'm lazy - you do it yourself. :)

Now, go to Yaws, type "start:boot()" and go to your index page. All of the elements should be visible. Is it? Any problems? If there is, this is a good time to learn what Erlang is saying to you. :P Now, if there is a problem in the tutorial, do contact me.

1 Comments:

Anonymous Anonymous said...

The code of "ErlyWeb Tutorial" can download?

8:10 AM  

Post a Comment

<< Home