UPDATE: I am going to expand this tutorial a bit, later on, but, as of now, the tutorial can stand on its own. I only included a discussion on displaying blog entries and creating new entries. Later, I will be adding a discussion on adding an edit page and adding delete functionality.
A week ago, I started to play with Erlang. I wanted to look into web development with functional languages, so I invested time in a certain toolkit called
ErlyWeb.
Here is a small tutorial of creating a simple weblog application in it. I am assuming you went through the musician tutorial, which explains in detail, getting Erlang, ErlyWeb, MySQL, blah blah blah. This tutorial is just a rough sketch of discoveries I made while using it.
(I typed this really quickly. So, if you try this, you need to fix all the HTML ... Blogger was rendering the HTML and I am too tired to find out why. So, I merely stuck a space after each < for now. I'll fix later.)
To start, we need to create an application like such, in the erl shell:
erlyweb:create_app("blog", "/path/to/apps").
Then, we create a table in MySQL, like such:
create table entries (
id integer auto_increment primary key,
title varchar(100),
body text,
author varchar(100)
);
We create an entries component:
erlyweb:create_component("entries", "/path/to/apps/blog").
While here, create a file called "entries_validate.erl" in your "src" directory in your app directory.
I created a small file that I put on the root of the blog directory which will start up a MySQL connection in ErlyDB as well as compile the whole app. I put this file on the root of the blog directory (and will reference to this place in other places in the tutorial.) Here is the file (edit where necessary):
-module(start).
-export([boot/0, boot/1]).
boot() ->
boot(true).
boot(false) ->
compile();
boot(true) ->
mysql_start(),
compile().
mysql_start() ->
erlydb:start(mysql, [{hostname, "localhost"},
{username, "username"},
{password, "password"},
{database, "blog"}]).
compile() ->
erlyweb:compile("/path/to/app/blog",
[{erlydb_driver, mysql}]).
This comes in handy, if you restart yaws, for example - you don't need to type in the connection function every time.
Edit your yaws.conf file:
< server junk >
port = 8000
listen = 127.0.0.1
docroot = /path/to/blog/www
appmods = <"/blog", erlyweb>
appname = blog
< /server >
Then, start yaws:
yaws -i
Cruise over to your localhost:8000/blog/entries page, and enter a few new entries, for some starter data.
In your src/components/entries_controller.erl file, comment out the erlyweb_magic line. We are turning off the stuff we were using just before.
Now, we will start with the front page. We will want to create an index function that merely shows all the entries on the screen. In the entries_controller.erl file, enter this function in:
index(A) ->
Entries = entries:find(),
{data, Entries}
and put in your export line:
-export([index/1]).
What that function is merely saying is that it should get all the entries in the the entries table, and send the entries off to the view function.
Open up entries_view.erl, and enter this:
index(Data) ->
entries_show:show_entries(Data).
and in your export line:
-export([index/1]).
In our view function, we are getting the data ("Entries") from the controller function, and now we are merely passing the data to the display function. Make a file called "entries_show.et" in the src/components directory and enter this code in:
<%@ show_entries(Entries) %>
<% [entry(E) || E <- Entries] %>
<%@ entry(Entry) %>
<% helpers:value(entries:title(Entry)) %><% helpers:value(entries:body(Entry)) %>
by: <% helpers:value(entries:author(Entry)) %> We are almost done. In the current version of ErlyWeb, I was having problems when the value of a particular field, like the output from entries:title(), would return undefined, and it would crash the yaws process (of course, it is not going to crash the server!) So, I created a small set of functions in a "helpers" module. Make this file in your src directory, and add this code:
-module(helpers).
-export([value/1]).
value(Val) ->
case Val of
undefined ->
"";
_ ->
Val
end.
We'll use this little code snippet for now.
Now, go to the root of the blog directory (you can use the cd() function in yaws, so you don't have to go back to your shell) and type:
start:boot().
Hopefully, everything will be fine. It will finish up with a tuple starting with "ok" at the beginning.
Go to your page, http://www.localhost:8000/blog/entries and you will hopefully see, in all its glory, the records you entered in before.
Now, we are going to expand the helpers module. Here is the complete helpers module file:
-module(helpers).
-export([value/1, validate/3]).
value(Val) ->
case Val of
undefined ->
"";
_ ->
Val
end.
validate(ValidatorModule, Model, Item) ->
Fields = Model:use_fields(),
Results = [ValidatorModule:Field(Model:Field(Item)) || Field <- Fields],
Errors = [Error || Error <- Results,element(1, Error) == error],
case Errors of [] ->
ok;
_ ->
Errors
end.
We added a validation function here. We are going to add a simple form to the mix; something to add new entries.
Back in the entries_controller.erl file, enter in this code:
new(A) ->
case yaws_arg:method(A) of
'GET' ->
{data, {[], new_get()}};
'POST' ->
Vals = yaws_api:parse_post(A),
{Errors, Entry} = new_post(Vals),
case Errors of
ok ->
{ewr, index};
_ ->
{data, {Errors, Entry}}
end
end.
new_get() ->
Entry = entries:new(),
Entry.
new_post(Vals) ->
Entry = entries:set_fields_from_strs(entries:new(), Vals),
Errors = helpers:validate(entries_validate, entries, Entry),
case Errors of
ok ->
entries:save(Entry),
{ok, Entry};
_ ->
{Errors, Entry}
end.
and our export line:
-export([index/1, new/1, new_get/0, new_post/1]).
(We could make the new_get/0 and new_post/1 functions private to the web, but we'll consider this later.)
The new/1 function is the function that is exposed to the web. I split the logic of supplying a new form and creating a new entry because it will prove easier to unit test instead of mucking around with making a value to submit into new/1.
For GET in new/1, we merely drop in a new entries tuple into the form:
'GET' ->
{data, {[], new_get()}};
...
new_get() ->
Entry = entries:new(),
Entry.
I return from new/1 in the 'GET' case with a "data" tuple, with a 2-tuple. The first item in the 2-tuple is an empty list - for the 'POST' part, we would use that location in the 2-tuple for errors. The second location is the entry we are considering.
For 'POST':
'POST' ->
Vals = yaws_api:parse_post(A),
{Errors, Entry} = new_post(Vals),
case Errors of
ok ->
{ewr, index};
_ ->
{data, {Errors, Entry}}
end
Here, we get all the values from the post payload, and then run the new_post/1 function. If new_post/1 returns {ok, Entry}, then it will redirect to the index page. If not, it will show the form again, and display the errors, and fill in the form elements that are already filled in. Here, we can see now the corollary between the return value in 'GET':
{data, {Errors, Entry}}
"Errors" contains a list of tuples containing our error messages. The Entry variable contains an ErlyDB tuple that we received from new_post/1. Let's look at new_post/1.
new_post(Vals) ->
Entry = entries:set_fields_from_strs(entries:new(), Vals),
Errors = helpers:validate(entries_validate, entries, Entry),
case Errors of
ok ->
entries:save(Entry),
{ok, Entry};
_ ->
{Errors, Entry}
end.
Here, when we get the values from the post payload, we convert the values into an entries tuple. Then, we submit into our validate/3 function (from before) with any errors. If validate/3 returns ok, then there were no errors. Otherwise, we will return the errors.
validate/3 takes the name of your validation module, your model module and the actual data tuple.
I said to create an entries_validate.erl file. Let's create that.
-module(entries_validate).
-export([title/1, body/1, author/1]).
title(undefined) ->
{error, title, "The title field is blank."};
title(_) ->
ok.
body(undefined) ->
{error, body, "The body field is blank."};
body(_) ->
ok.
author(undefined) ->
{error, author, "The author field is blank."};
author(_) ->
ok.
In entries.erl, put this:
-export([use_fields/0]).
use_fields() ->
[title, body, author].
The validate/3 function loops through the list of names from use_fields() and checks the data in the model tuple, using the functions you provide in entries_validate.erl.
(I say, isn't validation extremely simple in ErlyWeb? A tiny function to get rid of boilerplate code, and a small convention on how to present the data, and that's it!)
Once you got validation in place, enter this into your entries_view.erl file:
new(Data) ->
entries_show:display_form(Data).
and the export line:
-export([index/1, new/1]).
and now, the form, which we will stick also at the bottom of entries_show.et:
<%@ display_form(Items) %>
<%? {Errors, Data} = Items %>
<% helpers_html:show_errors(Errors) %>
< form action="new" method="post">
< table>
< tbody>< tr>< td valign="top">Title:< /td>
< td>
<% erlyweb_html:input("title", text_field, undefined, helpers:value(entries:title(Data))) %>
< /td>
< /tr>
< tr>< td valign="top">Body:< /td>
< td>
<% erlyweb_html:input("body", text_area, undefined, helpers:value(entries:body(Data))) %>
< /td>
< /tr>
< tr>Author:< /td> < td><% erlyweb_html:input("author", text_field, undefined, helpers:value(entries:author(Data))) %>< /td> < /tr> < tr> | < input value="Submit" type="submit">< /td>< /tr> < /tbody>< /table> < /form>
Hopefully, all this code is self-evident.
Before we finish, one more thing. I added a small module in ErlTL called helpers_html.et in my src/ directory:
<%@ show_errors(Errors) %> < ul> <% [error(E) || E <- Errors] %> < /ul>
<%@ error(Error) %> < li><% element(3, Error) %>< /li>
This will show us our errors, if we make a mistake in our form.
Now, back in yaws, type in:
start:boot().
and go back to your browser and go to localhost:8000/entries/new.
Make a mistake. Hit submit. Do you see the errors pop up? Fill in the form correctly. Does it go to the index page properly? If so, we have a simple blog app working.
Of course, I totally ignored editing posts in this tutorial. Maybe I will add this later. But, I hope you are seeing what I am seeing. ErlyWeb has INCREDIBLE POTENTIAL, since it avoids a lot of complexity. Any gaps we found thus far were easily plugged in. Even though ErlyWeb is new, this, so far, blows Rails out of the water in terms of brevity. Of course, it took me longer to figure this all out how to do this, since the docs are not up to par yet. Hopefully, this can be fixed soon.
|