Stickybits

Blog Categories
Twitter Updates
    follow me on Twitter
    Currently Reading
    Powered by Squarespace
    Git Projects
    « Erlang Web Development Frameworks | Main | My CouchDBX version »
    Friday
    Dec122008

    5 Minute Blog Using Nitrogen and CouchDB

    Introduction

    NOTE: This tutorial is very very old and uses early versions of both Nitrogen and CouchDB and many things here may not work as advertised.

    Hey. Today I'm going to show you how to create a really, really simple blog in a few minutes using the Nitrogen Web Framework and the CouchDB document system. I presume you have basic erlang skills but this should be simple for anyone. I also presume you have already seen the Nitrogen Screencasts and are familiar with the basic behavior of Nitrogen.

    Ingredients

    Here is what yo will need:

    I assume that Ecouch and Nitrogen are set up in the library path (see here for installation info). If you need any further help on this see this post

    If you are using Mac OS 10.5 I recommend you use my implementation of CouchDBX to get you started quickly.

    First we create a new project in a location of your choosing with the command

    nitrogen create myblog
    

    In there you will see a basic project to get started with. Now unzip the Extra Files and copy the files to your project. You will see a basic template I have set up for you at wwwroot/template.html. You will also see the module json_obj wich I savagely stole from the StickyNotes tutorial. It's basically a module that will assist you dealing with JSON data in Erlang. As JSON's data types are incompatible with Erlang it's best to grab help wherever you can.

    First we will need to start ecouch. Open up src/myblog_app.erl and modify start() to start up ecouch before nitrogen

    start(_, _) -> 
      application:start(ecouch),
      nitrogen:start().
    

    To start the development server open up a terminal to the project root, type in "./start.sh" and visit the website http://localhost:8000. You should see a nice website with the body "web_index body.". Whenever you change the code in the project you will have to recompile with the command "sync:go().".

    CouchDB

    First thing we want to do is create the database and our view. Open up CouchDB at http://127.0.0.1:5984/_utils/, create a new database and name it "myblog". Next we are going to need a convenient view for Nitrogen. We are going to create a really simple view that returns a simple array with first the title and then the body. This makes it easy for us when we need to map the data later in Nitrogen as you will see later. We are also going to use the field timestamp as a key so the data is in time order.

    To do that we select "Custom Query" under "Select view". Expand the "View Code" flap and replace the map function with the following.

    function(doc) {
      emit(doc.timestamp, [doc.title, doc.body]);
    }
    

    Select "Save As" and save as the design document: "nitrogen" and view name: "flat".

    The Model

    We could call the data from the pages themselves but I prefer to create a little model class to seperate the logic. Create the file blog_posts.erl inside src and export get_posts/0 and add_post/1. First create the get_post/1.

    get_posts() ->
        {ok, Return} = ecouch:view_access("myblog", "nitrogen", "flat"),
        RawRows = json_obj:get_value("rows", Return),
        F = fun(X) ->
                json_obj:get_value("value", X)
        end,
        Rows = lists:map(F, RawRows),
        lists:reverse(Rows).
    

    First of all we made a call to CouchDB using ecouch requesting the nitrogen/flat view for the myblog database. Now as Erlang has incompatible types to JSON there is a lot of ugly extra data in the raw return which looks like this

    {obj,[{"total_rows",2},
              {"offset",0},
              {"rows",
               [{obj,[{"id",<<"b8ffeaa68f2fd4010a105ddc56460466">>},
                      {"key",123},
                      {"value",[<<"The Title">>,<<"TheBody">>]}]},
                {obj,[{"id",<<"53b075b3633f8d8b3654576806ef18b3">>},
                      {"key",321},
                      {"value",[<<"Another Title">>,<<"Another Body">>]}]}]}]}
    

    Now thats not very pretty is it so we use the json_obj module to query the value of "rows" so we don't have to parse this data by hand. We then make a fun that does the same thing for the "value" field of a row and use the map function to call it on each of the rows. We then reverse the list so the latest post is first. After doing all this the result of the function would be as following:

    [ [<<"Another Title">>,<<"Another Body">>], [<<"The Title">>,<<"TheBody">>] ]
    

    Ahh... Much Nicer.

    We also need to create the data so we add the add_post/1 method.

    add_post({post, Title, Body}) ->
        JSONTemplate = {obj, []},
        WithTitle = json_obj:set_value("title", list_to_binary(Title), JSONTemplate),
        WithTitleAndBody = json_obj:set_value("body", list_to_binary(Body), WithTitle),
        Complete = json_obj:set_value("timestamp", calendar:time_to_seconds(now()), WithTitleAndBody),
        ecouch:doc_create("myblog", Complete).
    

    This does not need much explanation. We start by creating an empty JSON object so that we can use the json_obj functions on it. We then add the Title and Body parameters to the object and create a timestamp as well. Finally we ask ecouch to create the document.

    The Index Page

    Now edit the file called src/pages/web_index.erl and edit the function body() to look like this.

    body() ->
        Data = blog_posts:get_posts(),
        Map = [titleHeader@text, bodyLabel@text],
        #bind { id=simpleBinding, data=Data, map=Map, body=[
            #panel { class="post", body=[
                #h2 { class="title", id=titleHeader },
                #panel { class="entry", body=[
                    #label{ id=bodyLabel }
                ]}
            ]}
        ]}.
    

    The main thing here is the data binding. We first take the output from our model and bind it to Data. As you remember the output of the model is a list of lists. Then we create the Map to describe how the elements of those lists should should bind to the page elements. So we are saying that the first element should bind to the "text" variable on the element with the id "titleHeader". We then use the #bind record, which is a sort of a for loop, and feed to it the Data and the Map.

    You should now be able to sync and go see the webpage. Of course you can not see anything yes as we have yet to create any data.

    The Create Page

    Next create a new page using the command nitrogen page /web/create in your project root. Open up the newly generate src/pages/web_create.erl and replace body():

    body() ->
        Body = [
            #label { text="Title:" },
            #textbox{ id=titleTextBox, class="create_title", next=body },
            #label { text="Body (HTML Allowed):" }, 
            #textarea{ id=bodyTextArea, class="create_body" },
            #br {},
            #link{ id=commitButton, text="Submit", postback=commit }
        ],
    
        %% Validations
        wf:wire(commitButton, titleTextBox, #validate { validators=[
            #is_required { text="Required." },
            #min_length { length=2, text="Title must be at least 2 characters long." }
        ]}),
        wf:wire(commitButton, bodyTextArea, #validate { validators=[
            #is_required { text="Required." }
        ]}),
    
        %% Render
        wf:render(Body).
    

    Next up add a new event() just above the line that says ´event(_) -> ok.´

    event(commit) ->
        [Title] = wf:q(titleTextBox),
        [Body] = wf:q(bodyTextArea),
        blog_posts:add_post({post, Title, Body}),
        Message = wf:f("Post ~p created. <a href=\"/web/index\">click here</a> to see it.", [Title]),
        wf:flash(Message);
    

    There is nothing really complicated here as we have made most of the work already in the model. We set up a textfield and a textarea. We then bind the commitButton the generate the commit event. We then set up Validations so we can't submit empty data.

    The commit event takes the value of the titleTextBox and bodyTextArea and sends it's data to blog_posts:add_post/1. It then formats a nice message and flashes it to the user.

    Now sync and visit the page. Enter the create page and try submitting empty field. Then fill in the data and add a few posts. Visit the Index page to view your posts.

    Conclusion

    Even though this is not a very thorough nor complex tutorial it really shows how easily you can use together these two projects and create something really powerful with little code.

    If you hit a problem please check my completed code here

    Reader Comments (19)

    Nice Jon! Thanks for posting this.

    One gotcha though that had me guessing for a bit: you have to have ecouch up and running (which means you have to have inets up and running) so things work. Even better is to actually get ecouch up and running with the right parameters as I was passing an int for the CouchDB port number (sensible enough?) just to find that it wanted a string for the CouchDB port number. Another gotcha is having the proper ERL_LIBS settings to which I added "./" and "./src".

    This sequence did it for me:
    $ export ERL_LIBS="/path/to/nitrogen/et/al/:./:./src/"

    and in the Erlang shell:
    1> inets:start().
    [msgs omitted]
    2> ecouch:start(foobar, {"localhost", "5984"}).
    [msgs omitted]

    All and all: very nice!

    Dec 12, 2008 at 9:14 | Unregistered CommenterUlises

    That's strange. ./ and ./src should be set in the code paths automatically by erlang and should be no need to put in ERL_LIBS. Also you should not point it to the Nitrogen folder but instead have a folder with all your standar modules and point ERL_LIBS there. Erlang takes ERL_LIBS and adds to the path the src folder of all the subfolders. So it's like including $ERL_LIBS/**/src

    As for inets and ecouch... Those should have been started by ./quickstart.sh. Don't know why you needed to it manually.

    Dec 12, 2008 at 16:30 | Registered CommenterJón Grétar Borgþórsson

    I don't understand this part:

    ----
    To do that we select "Custom Query" under "Select view". Expand the "View Code" flap and replace the map function with the following.

    function(doc) {
    emit(doc.timestamp, [doc.title, doc.body]);
    }
    Select "Save As" and save as the design document: "nitrogen" and view name: "flat".
    ----

    Where are we selecting and expanding and saving? It almost sounds like there's an IDE involved. I don't see any of this stuff in CouchDB's Futon web utility.

    Dec 18, 2008 at 16:47 | Unregistered CommenterKevin

    Kevin:
    in the Futon web utility (http://127.0.0.1:5984/_utils/) there is a drop down list on the top right(when you are inside a database). In that you find "custom query". In the resulting page there is a "View Code" flap that is close but you can open using the triangle next to it.

    EDIT:
    I seem to have not pointed to the right path in the article (http://127.0.0.1:5984/ instead of http://127.0.0.1:5984/_utils/.)

    Dec 18, 2008 at 17:08 | Registered CommenterJón Grétar Borgþórsson

    What is section1 in the above?


    I get compile error since it isn't a defined attribute of the template record.

    Jan 9, 2009 at 10:01 | Unregistered Commenteretnt

    Yeah this wont work anymore. There was a major change to the template system. In the old days template sections were numbered and defined in the #template record which then got transformed to [[[section1]]] in the template.

    Now you define function names in the template like [[[page:myfunction()]]] which calls myfunction in the current page module.

    Jan 9, 2009 at 12:35 | Registered CommenterJón Grétar Borgþórsson

    Fixed the tutorial for the updated version og Nitrogen.

    Jan 9, 2009 at 14:07 | Registered CommenterJón Grétar Borgþórsson

    Great post! It works great with inets but fails with mochiweb. Here's the error report I see:


    ** Reason for termination ==
    ** {noproc,
    {gen_server,call,
    [httpc_manager,
    {request,
    {request,undefined,<0.72.0>,0,http,
    {"127.0.0.1",5984},
    "/myblog/_view/nitrogen/flat",[],get,
    {http_request_h,undefined,"keep-alive",undefined,
    undefined,undefined,undefined,undefined,undefined,
    undefined,undefined,undefined,undefined,undefined,
    undefined,undefined,undefined,"127.0.0.1",undefined,
    undefined,undefined,undefined,undefined,undefined,
    undefined,undefined,undefined,[],undefined,undefined,
    undefined,undefined,"0",undefined,undefined,undefined,
    undefined,undefined,undefined,[]},
    {[],[]},
    {http_options,"HTTP/1.1",infinity,true,[],undefined,false},
    "http://127.0.0.1:5984/myblog/_view/nitrogen/flat",[],
    none,[]}},
    infinity]}}

    Do you have any suggestions? Do you have a preference of inets, mochiweb, or yaws? Thanks!

    Feb 10, 2009 at 22:52 | Unregistered CommenterJason

    Disregard the last, ecouch assumes inets is running

    Feb 11, 2009 at 2:10 | Unregistered CommenterJason

    Thanks for the post, this was really helpful. It may be worth looking at other clients like erlang_couchdb. After only about ten minutes with ecouch I discovered a major bug. The maintainer seems to be responsive though.

    Mar 8, 2009 at 3:49 | Unregistered Commenter5hundy

    Nice howto but having issues making it work.

    When I click on link submit. It does nothing.

    #link { id=commitButton, text="Submit", postback=commit }

    Nov 20, 2009 at 2:25 | Unregistered Commenternewbie

    I've encountered the same problem as “newbie”, which is very weird 'cause with all my other projects/sites nitrogen works just fine, but here the link point on “javascript:” and that's all.
    Any idea why Jon ?

    Jan 1, 2010 at 0:58 | Unregistered CommenterDrk-Sd

    After doing some tests it seems that the problem comes from the Extrafiles which are in your archiv (at least the one which must go in the wwwroot dir, don't ask me why, but that's what i noticed).
    Happy new year everyone!

    Jan 1, 2010 at 1:33 | Unregistered CommenterDrk-Sd

    Hi , Looks like your template file contains links to version'ed jQuery files that nitrogen no longer provides.

    I updated the jquery includes in template.html to resolve the javascript errors

    jquery-1.2.6.js to jquery.js
    jquery-ui-personalized-1.5.3.min.js to jquery-ui.js

    Cheers.

    Feb 6, 2010 at 9:14 | Unregistered CommenterCarlos

    When I click on the create new I have this error:


    {error,error,undef,
    [{wf,render,
    [[{label,is_element,element_label,undefined,undefined,undefined,
    true,[],[],"Title:",true},
    {textbox,is_element,element_textbox,titleTextBox,undefined,
    undefined,true,"create_title",[],[],true,body,
    undefined,undefined},
    {label,is_element,element_label,undefined,undefined,undefined,
    true,[],[],"Body (HTML Allowed):",true},
    {textarea,is_element,element_textarea,bodyTextArea,undefined,
    undefined,true,"create_body",[],[],true},
    {br,is_element,element_br,undefined,undefined,undefined,true,[],
    []},
    {link,is_element,element_link,commitButton,undefined,undefined,
    true,[],[],"Submit",[],true,"javascript:",commit,
    undefined}]]},
    {element_function,call_next_function,1},
    {wf_render_elements,call_element_render,2},
    {wf_render_elements,render_element,1},
    {wf_render_elements,render_elements,2},
    {lists,foldl,3},
    {wf_render_elements,render_elements,2},
    {wf_render_elements,call_element_render,2}]}

    May 16, 2010 at 21:51 | Unregistered CommenterKind

    @Kind: Yeah.. Nitrogen is now in release 2.0 and CouchDB is also a few releases ahead. This tutorial is way out of date.

    May 17, 2010 at 15:30 | Registered CommenterJón Grétar Borgþórsson

    Thanks for your response, but there is another way to make this tutorial work on nitrogen 2.0 and couchdb or I must use the older versions of nitrogen and couchdb.
    Cheers!

    May 19, 2010 at 21:48 | Unregistered Commenterkind

    Hello everyone!
    I try to run this exemple of blog, to understand better nitrogen and I don't know how to make it work. I'm using nitrogen 1x and couchdb v 11 and of course ecouch. My first error that I had was on ecouch, this one:
    CAUGHT ERROR: error-undef
    [{ecouch,db_info,["test_suite_db"]},
    {blog_posts,get_posts,0},
    {web_index,body,0},
    {element_template,eval_callbacks,2},
    {element_template,eval,2},
    {element_template,eval,2},
    {element_template,eval,2},
    {element_template,eval,2}]

    then I put ecouch in may erlang lib and create a symlink to support/include.mk and then when I run it again I had this error:
    CAUGHT ERROR: exit-{noproc,
    {gen_server,call,
    [ec_listener,{get,"/test_suite_db",[]},30000]}}
    [{gen_server,call,3},
    {ecouch,db_info,1},
    {blog_posts,get_posts,0},
    {web_index,body,0},
    {element_template,eval_callbacks,2},
    {element_template,eval,2},
    {element_template,eval,2},
    {element_template,eval,2}]


    I don't understand this ecouch, what I must do first with it, I wget it and then I put it in $ERL_LIBS, but I don't see the connection with ecouch, couchdb and this application.

    Please if anyone understand what it's happening here, please share with me :D.
    Thanks in advance!

    Jun 1, 2010 at 13:47 | Unregistered Commenterdamina

    By the way, sorry if I disturb you, this blog is made with nitrogen and coucdb or another programing language.

    Jun 1, 2010 at 13:49 | Unregistered Commenterdamina

    PostPost a New Comment

    Enter your information below to add a new comment.

    My response is on my own website »
    Author Email (optional):
    Author URL (optional):
    Post:
     
    Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>
    Fork me on GitHub