Copyright © Yariv Sadan 2006-2007
Authors: Yariv Sadan.
Introduction
Directory Structure
Components
Models
Controllers
Views
Containers
The App Controller
Yaws Configuration
ErlyWeb is a component-oriented web development framework that simplifies the creation of web applications in Erlang according to the tried-and-true MVC pattern. ErlyWeb's goal is to let web developers enjoy all the benefits of Erlang/OTP while creating web applications with simplicity, productivity and fun.
ErlyWeb is designed to work with Yaws, a high-performance Erlang web server. For more information on Yaws, visit http://yaws.hyber.org.
To install ErlyWeb, obtain the latest zip file from http://erlyweb.org and unzip it in your root Erlang code path. This path varies depending on your operating system (on OS X, it's '/usr/local/lib/erlang/lib'). If you don't know your path, you can discover it by starting the Erlang shell and calling "code:lib_dir()." (note that the last directory of the code path always erlang/lib).
ErlyWeb applications have the following directory structure:
[AppName]/
src/ contains non-component source files
[AppName]_app_controller.erl the app controller
[AppName]_app_view.rt the app view
components/ contains controller, view and
model source files
www/ contains static assets
ebin/ contains compiled .beam files
where AppName is the name of the application. The 'src', 'components' and 'www' directories may contain additional subdirectories, whose contents are of the same type as those in the parent directory.
An ErlyWeb component is made of a controller and a view, both of which are Erlang modules. Controllers may use one or more models, but this isn't required.
(Technically, a component can be implemented without a view, but most components have views.)
Controllers are implemented in Erlang source files. Views are typically implemented in ErlTL files, but views can be implemented in plain Erlang as well. The controller file is named '[ComponentName]_controller.erl' and the view file is named '[ComponentName]_view.[Extension]', where [ComponentName] is the name of the component, and [Extension] is 'erl' for Erlang files and 'et' for ErlTL files.
When ErlyWeb receives a request such as http://hostname.com/foo/bar/1/2/3,
ErlyWeb interprets it as follows: 'foo' is the component name, 'bar' is the
function name, and ["1", "2", "3"] are the parameters (note that
parameters from browser requests are always strings).
When only the component's name is present, ErlyWeb assumes the request is
for the component's 'index' function and with no parameters (i.e.
http://hostname.com/foo is equivalent to http://hostname.com/foo/index).
If the module 'foo_controller' exists and it exports the function
'bar/4', ErlyWeb invokes the function foo_controller:bar/4
with the parameters [A, "1", "2", "3"], where A is the Arg record
that Yaws passes into the appmod's out/1 function. (For more information,
visit http://yaws.hyber.org/.)
Since ErlyWeb 0.7, you can use the catch_all/2 hook to override the default behavior, i.e. treating the second token as a function name instead of as parameter. For more information, see catch_all/2.
ErlyWeb treats all Erlang modules whose names don't end with '_controller' or '_view' and whose files exist under 'src/components' as models. If such modules exist, erlyweb:compile() passes their names to erlydb:code_gen() in order to generate their ErlyDB database abstraction functions.
If your application uses ErlyDB for database abstraction, you have to call erlydb:start() before erlyweb:compile() (otherwise, the call to erlydb_codegen() will fail). If you aren't using ErlyDB, don't keep any models in 'src/components' and then you won't have to call erlydb:start().
Controllers contain most of your application's logic. They are the glue between Yaws and your applications models and views.
Controller functions always accept the Yaws Arg for the request as the
first parameter, and they may return any of the values that Yaws
appmods may return (please read
the section below on the {response, Elems} tuple to avoid
running into trouble). In addition, controller functions may return
a few special values, which are listed below with their meanings
(note: 'ewr' stands for 'ErlyWeb redirect' and 'ewc' stands for 'ErlyWeb
Component.').
ewr
Redirect the browser to the application's root url.
{ewr, ComponentName}
Redirect the browser to the component's default ('index')
function
{ewr, ComponentName, FuncName}
Redirect the browser to the URL for the given component/function combination.
{ewr, ComponentName, FuncName, Params}
Redirect the browser to the URL with
the given component/function/parameters combination.
{data, Data}
Call the view function, passing into it the Data variable as a parameter.
{ewc, A}
Analyze the Yaws Arg record 'A'. If the request matches a component,
render the component and send the result to the browser.
Otherwise, return {page, Path}, where Path is the Arg's appmoddata field.
{ewc, ComponentName, Params}
Render the component's 'index' function with the given parameters
and send the result to the view function.
{ewc, ComponentName, FuncName, Params}
Render the component's function with the given parameters, and send
the result to the view function.
{ewc, ControllerModule, ViewModule, FuncName, Params}
This tuple lets you specify exacly which controller and
view modules ErlyWeb uses to render the sub-component. It is considered for
advanced uses only.
{replace, Ewc}
This tuple, which was introduced in ErlyWeb 0.6, tells ErlyWeb to
render a different component from the requested one (the new component, Ewc,
can be described using any of the 'ewc' tuples listed above).
This mechanism is conceptually similar to an internal redirect.
It is useful, for example, when you want multiple components to share
a common error screen. Instead of having multiple view functions
check if the controller result indicates an error has occurred, you can
use the replace tuple to simply redirect the rendering to an error
component.
Controller functions may also return (nested) lists of 'ewc' and 'data' tuples, telling ErlyWeb to render the items in their order of appearance and then send the list of their results to the view function. This feature lets you build components that are composed of a mix of dynamic data and one or more sub-components.
If a component is only supposed to be used as a subcomponent, you should
implement the function "private() -> true." in its controller.
This tells ErlyWeb to call exit({illegal_request, Controller})
when clients try to access the component by requesting
its corresponding URL directly.
ErlyWeb provides the infrastructure for rendering components and redirecting
to other components in the same application. This is sufficient for simple
applications, but sometimes you may want to return
to Yaws tuples that Yaws understands and that aren't directly
supported by ErlyWeb (these are documented
at http://yaws.hyber.org/yman.yaws?page=yaws_api). A common requirement,
for example, is to instruct Yaws to include arbitrary HTTP headers, such as
cookies, in the response Yaws sends to the browser.
ErlyWeb lets you do this by returning a tuple of the
form {response, Elems} from controller functions.
The second element in the {response, Elems} is a list of values that
ErlyWeb should return to Yaws verbatim, with the exception of the (optional)
{body, Body} for HTML or {body, MimeType, Body} tuple and any of the ewr
tuples listed above (ErlyWeb translates the latter into their redirect_local
equivalents). If, in addition to returning standard Yaws tuples, you want
ErlyWeb to render the response's body using the component's view, you can
include the {body, Body} or {body, MimeType, Body} tuple in Elems.
Body may be any single ewc or data tuple, or a list thereof.
ErlyWeb renders the elements of Body, sends the result to the view function,
and embeds the resulting iolist in an {html, Iolist} tuple prior to
returning it to Yaws.
There is currently a restriction on the usage of {response, Elems}: only the
top-level component for the request may return the
{response, Elems} tuple. Sub-components may return only data and/or
ewc tuples. This restriction makes sense because you normally only use
sub-components for rendering segments of the response's body and not
for setting HTTP headers or implementing arbitrary application logic.
catch_all/2
before_call/2
before_return/3
after_render/3
The catch_all/2 hook was introduced in ErlyWeb 0.7. By default, ErlyWeb treats the URL token following the component name as a function name. catch_all/2 lets you override this behavior on a per-controller basis by treating all URL tokens following the component name as parameters.
For example, if a user requests the url
http://my-cool-app.com/people/bob
ErlyWeb by default would try to invoke the function 'bob/1' in people_controller. You can tell ErlyWeb to override this behavior and instead pass "bob" as a parameter to people_controller:catch_all/2 as follows:
people_controller.erl:
-module(people_controller).
-export(index/1, catch_all/2).
catch_all(A, [Name]) ->
{data, Name}.
people_view.et:
<%@ catch_all(Name) %> <% Name %>'s homepage ...
Note that the second parameter to catch_all/2 in the controller is a list of URL tokens (separated by "/") following the controller name. For example, when ErlyWeb receives a request with the URL
http://my-cool-app.com/people/bob/smith
ErlyWeb would call people_controller:catch_all(A, ["bob", "smith"]).
ErlyWeb lets you implement the before_call/2 and before_return/3 hooks in controllers to intercept function calls before they are applied and/or manipulate controller return values before ErlyWeb processes them.
The signature of before_call/2 is
before_call(FuncName::atom(), Params::[term()]) ->
{NewFuncName::atom(), NewParams::[term()]}
You can use the before_call/2 hook to change the controller function ErlyWeb calls and/pr the parameters ErlyWeb passes to the function. This hook can be convenient, for example, for implementing authentication logic for some or all functions of a controller.
The signature of before_return/3 is
before_return(FuncName::atom(), Params::[term()], Response::term()) -> NewResponse::term()
By implementing the before_return/3 hook, you can change all return values based on the names and/or parameters of their originating controller functions. This hook can simplify the definition of response elements such HTTP headers common to some or all of a controller's functions.
ErlyWeb lets you implement the after_render/3 hook in controllers to get access to the rendered output.
The signature of after_render/2 is
after_render(FuncName::atom(), Params::[term()], Rendered::iolist()) -> Ignored::term().
You can use the after_render/3 hook to implement a granular caching system.
Views are Erlang modules whose functions return iolists (nested lists of strings and/or binaries). A view function that ErlyWeb uses has the same name as its corresponding controller function, and it accepts a single parameter, which is the result of ErlyWeb's processing of the controller function's return value.
Containers are ErlyWeb components in which other components can be nested. ErlyWeb contains no special logic to enable containers; 'container' is just a word used to describe components that applications use in a special way.
What sets containers apart from standard components is that containers'
controller functions accept as parameters
ewc and/or data tuples representing nested sub-components.
The controller functions include those parameters in their return values,
telling ErlyWeb to render the nested components and pass the results
to the container's corresponding view functions. The view functions embed
the rendered subcomponents in the container's static HTML,
and ErlyWeb returns the final result Yaws.
The code below illustrates how to implement a
simple HTML container. The container's controller function tells ErlyWeb
to renders its sub-component (index/2's Ewc parameter), and pass the result
to the component's view, which defines a basic HTML page.
-module(html_container_controller). -export([index/2, private/0]). %% tells ErlyWeb to reject direct HTTP requests for this %% container private() -> true. index(_A, Ewc) -> Ewc.html_container_view.et:
<%@ index(Data) %> <html> <head> <title>My ErlyWeb App</title> </head> <body> <% Data %> </body> </html>
The example below shows how to pass multiple 'song' components into an 'album' container, and show the result in the 'home' component.
song_controller.erl:index(A, Num, Title) ->
{data, {integer_to_list(Num), Title}}.
song_view.et:
<%@ index({Num, Title}) %>
<% Num %>: <% Title %><br/>
album_controller.erl:
index(A, Title, Songs) ->
[{data, Title}, Songs].
album_view.et:
<% index([Title, Songs]) %>
album title: <% Title %><br/>
songs:</br>
<% Songs %>
home_controller.erl:
```
index(A) ->
Songs =
[{ewc, song, [A, 1, <<"Back in the U.S.S.R">>]},
{ewc, song, [A, 2, <<"Dear Prudence">>]}],
{ewc, album, [A, <<"White Album">>, Songs]}.
home_view.et:
<% index(Album) %> Your favorite album is:<br/> <% Album %>
ErlyWeb applications have a module called [AppName]_app_controller, whose source file is in the 'src' directory by convention. The app controller provides the entry-point into ErlyWeb applications via the hook/1 function. Starting from ErlyWeb 0.6, the app controller can also have an error-trapping, error/3.
The signature for hook/1 is
hook(A::arg()) -> hook_result() | [hook_result()]
hook_result = ewc() | yaws_term() | response() |
{phased, Ewc::ewc() | Response::response(),
fun(ExpandedEwc::ewc(), Data::iolist(), PhasedVars::proplist()) ->
FinalEwc::ewc()}
ewc() = any `ewc' tuple
yaws_term() = any legal Yaws appmod return value
response() = a tuple of the form {response, [Elems::response_elem()]}
response_elem() = yaws_term() | {body, ewc()}
(Note: support for the response() return value was introduced in ErlyWeb 6.2.)
The simplest way of telling ErlyWeb to process a client request is to implement hook/1 as follows:
hook(A) -> {ewc, A}.
This tells ErlyWeb to check if the component that's mapped to the
request's URL is a top-level component (i.e., it doesn't implement
private() -> true.). If it is top-level, ErlyWeb renders the component
and returns the to Yaws. Otherwise, ErlyWeb calls
exit({illegal_request, ControllerName}).
Warning: ErlyWeb only checks if tuples of the form {ewc, A} represent
top-level components. If you return a different ewc tuple (e.g. one
returned by calling erlyweb:get_initial_ewc/1), ErlyWeb expects you
to ensure the safety of your components manually.
In ErlyWeb 0.5, the phased return type for hook/1 was introduced to let you
embed components in containers after those components are rendered
but before the result of the rendering is sent to the browser
(before ErlyWeb 0.5, a similar but weaker functionality was provided by
app views, which ErlyWeb no longer supports). The most common use for this
feature is to make ErlyWeb render different components
with the same outer layout (header, footer, and/or sidebars).
The reason this is feature called 'phased rendering' is that it makes ErlyWeb render the response in two phases: first, the requested component, and second, the container in which the component should be embedded.
The benefits of phased rendering are:
To use phased rendering, you should return a tuple of the form
{phased, Ewc, Fun} from hook/1. This return value instructs
ErlyWeb to first render the
requested component, and if the result includes a rendered
iolist (i.e., the requested component returned a body and not just headers)
then pass the iolist to the function Fun.
Fun takes 3 parameters: the fully expanded 'ewc' tuple
that ErlyWeb has rendered (this is usually the result of calling
erlyweb:get_initial_ewc({ewc, A})), the rendered
iolist, and an opaque parameter called 'PhasedVars' (PhasedVars was
introduced in ErlyWeb 0.7). Through its return value, Fun can tell
ErlyWeb to pass the iolist as a parameter to the container component
function that will be used in the second rendering phase.
PhasedVars is a list of values returned from
the rendered controller function, in a special tuple of the type
{phased_vars, Vars} (an example is shown below). This tuple should be
included in the Elems list of the {response, Elems} tuple that controller
functions may return (see the section Returning Arbitrary YAWS Tuples for more info).
The purpose of 'PhasedVars' is to let the controller function parameterize the component that is rendered in the second phase. It is useful, for example, for letting the controller function set the title of a page when the page's header is rendered by the container in the second rendering phase.
Fun may return any 'ewc' or 'data' tuple. ErlyWeb takes the result of Fun, renders it, and send the result back to the browser.
Consider this example return value from hook/1:
{phased, {ewc, A}, fun(_Ewc, Data, _PhasedVars) -> {data, Data} end}
It is equivalent to returning
{ewc, A}
Both return values tells ErlyWeb to render the component that corresponds to the arg's appmoddata field, and then return the rendered iolist to the browser verbatim.
Below is a more advanced example.
{phased, {ewc, A},
fun(_Ewc, Data, PhasedVars) ->
{html_container,
[A, {data, Data}, proplists:get_value(title, PhasedVars)]}
end}
This return value tells ErlyWeb to first render the component that corresponds to the arg's appmoddata field, and then pass the rendered iolist (the 'Data' variable) as the second parameter to html_container_controller:index/3. The third parameter is the 'title' property that may exist in the PhasedVars property list, or 'undefined' if the 'title' property doesn't exist.
How do you set the value of PhasedVars? Below is an example controller function for an 'album' component that fetches from the database an album and its related songs, passes the song names to the view function, and sets the page's title according to the album name.
album_controller.erl:
show(A, Id) ->
Album = album:find_id(Id),
AlbumName = album:name(Album),
Songs = album:songs(Album),
{response,
[{phased_vars, [{title, [<<"song list for ">>, AlbumName]}]},
{body, {data, [{song:name(S) || S <- Songs}]}}]}.
This code snippet exemplifies how a controller function can tell ErlyWeb both what data to pass to the view function as well as the value of the PhasedVars parameter that will be passed to Fun in the second rendering phase.
Finally, below is an example html_container component that takes the rendered iolist and the 'title' parameter and inserts them into the proper positions in the HTML page.
html_container_controller.erl:
index(A, Ewc, Title) ->
[Ewc, {data, Title}].
html_container_view.et:
<%@ index([Data, Title]) %> <html> <head> <title><% Title %></title> </head> <body> <% Data %> </body> </html>
Starting from ErlyWeb 0.6, the app controller may implement an error-trapping function whose signature is
error(A, Ewc, Err) -> result()
When ErlyWeb catches an exit signal when rendering a component, ErlyWeb tries to call the app controller's error/3 function. error/3's parameters are the Yaws arg, the Ewc tuple representing the original request, and the second element of the {'EXIT', Err} tuple that ErlyWeb has caught. error/3 may return any of the values that hook/1 may return, allowing you to use ErlyWeb components to render an error page.
App controllers may implement two additional functions that ErlyWeb uses: before_compile/1 and after_compile/1. These functions let you hook into the compilation process and extend it in arbitrary ways. Both functions take a single parameter indicating the last compilation time (as returned from calendar:local_time()), or 'undefined' if the last compilation time is unknown.
docroot = /path/to/app/www appmods = <[UrlPrefix], erlyweb> <opaque> appname = [AppName] </opaque>
where AppName is the name of your application and UrlPrefix is the URL prefix that Yaws uses to identify requests for this application (common values are '/[AppName]', for deploying multiple applications on the same server, or '/', for deploying a single application).
Note: It is recommended to use ErlyWeb with Yaws v1.66 and above.