Arizona is a web framework for Erlang.
Work in progress.
Use it at your own risk, as the API may change at any time.
Arizona utilizes a templating approach where Erlang code is embedded within HTML
using curly braces {}
. This allows dynamic content generation by executing Erlang
functions directly within the HTML structure. For example:
<ul>
{arizona:render_list(fun(Item) ->
arizona:render_nested_template(~"""
<li>{Item}</li>
""")
end, arizona:get_binding(list, View))}
</ul>
No macros, no special syntaxes, just dynamic Erlang code embedded in static HTML.
The example below is a simplified version of the code from the example repository. Please refer to it for the complete code.
Create a new rebar3 app:
$ rebar3 new app arizona_example
===> Writing arizona_example/src/arizona_example_app.erl
===> Writing arizona_example/src/arizona_example_sup.erl
===> Writing arizona_example/src/arizona_example.app.src
===> Writing arizona_example/rebar.config
===> Writing arizona_example/.gitignore
===> Writing arizona_example/LICENSE.md
===> Writing arizona_example/README.md
Navigate to the project folder and compile it:
$ cd arizona_example && rebar3 compile
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling arizona_example
Add Arizona as a dependency in rebar.config
:
{deps, [
{arizona, {git, "https://github.com/arizona-framework/arizona", {branch, "main"}}}
]}.
Include Arizona in the src/arizona_example.app.src
file:
{application, arizona_example, [
% ...
{applications, [
kernel,
stdlib,
arizona
]},
% ...
]}.
Update the dependencies:
$ rebar3 get-deps
===> Verifying dependencies...
Create a config/sys.config
file:
[
{arizona, [
{endpoint, #{
% Routes are plain Cowboy routes for now.
routes => [
% Static files
{"/assets/[...]", cowboy_static, {priv_dir, arizona_example, "assets"}},
% Views are stateful and keep their state in memory.
% Use the 'arizona_view_handler' to render Arizona views.
% The 'arizona_example_page' will be mounted with the bindings 'title' and 'id'.
% The layout is optional and wraps the view. It does not have a state;
% it simply places the view within its structure.
{"/", arizona_view_handler,
{arizona_example_page, #{title => ~"Arizona Example", id => ~"app"}, #{
layout => arizona_example_layout
}}}
]
}}
]}
].
Set the config file in rebar.config
:
{shell, [
{config, "config/sys.config"},
{apps, [arizona_example]}
]}.
Create the src/arizona_example_page.erl
file:
-module(arizona_example_page).
-compile({parse_transform, arizona_transform}).
-behaviour(arizona_view).
-export([mount/2]).
-export([render/1]).
-export([handle_event/3]).
mount(Bindings, _Socket) ->
View = arizona:new_view(?MODULE, Bindings),
{ok, View}.
render(View) ->
arizona:render_view_template(View, ~"""
<div id="{arizona:get_binding(id, View)}">
{arizona:render_view(arizona_example_counter, #{
id => ~"counter",
count => 0
})}
</div>
""").
handle_event(_Event, _Payload, View) ->
View.
Create the src/arizona_example_counter.erl
view, which is defined in the render function of the page:
-module(arizona_example_counter).
-compile({parse_transform, arizona_transform}).
-behaviour(arizona_view).
-export([mount/2]).
-export([render/1]).
-export([handle_event/3]).
mount(Bindings, _Socket) ->
View = arizona:new_view(?MODULE, Bindings),
{ok, View}.
render(View) ->
arizona:render_view_template(View, ~"""
<div id="{arizona:get_binding(id, View)}">
<span>{integer_to_binary(arizona:get_binding(count, View))}</span>
{arizona:render_component(arizona_example_components, button, #{
handler => arizona:get_binding(id, View),
event => ~"incr",
payload => 1,
text => ~"Increment"
})}
</div>
""").
handle_event(~"incr", Incr, View) ->
Count = arizona:get_binding(count, View),
arizona:put_binding(count, Count + Incr, View).
Create the button in src/arizona_example_components.erl
, which is defined in the render
function of the view:
-module(arizona_example_components).
-export([button/1]).
button(View) ->
arizona:render_component_template(View, ~"""
<button
type="{arizona:get_binding(type, View, ~"button")}"
onclick="{arizona:render_js_event(
arizona:get_binding(handler, View),
arizona:get_binding(event, View),
arizona:get_binding(payload, View)
)}"
>
{arizona:get_binding(text, View)}
</button>
""").
Create the optional layout src/arizona_example_layout.erl
, which is defined in the config file:
-module(arizona_example_layout).
-compile({parse_transform, arizona_transform}).
-behaviour(arizona_layout).
-export([mount/2]).
-export([render/1]).
mount(Bindings, _Socket) ->
arizona:new_view(?MODULE, Bindings).
render(View) ->
arizona:render_layout_template(View, ~""""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{arizona:get_binding(title, View)}</title>
{arizona:render_html_scripts()}
<script src="assets/main.js"></script>
</head>
<body>
{% The 'inner_content' binding is auto-binded by Arizona in the view. }
{arizona:get_binding(inner_content, View)}
</body>
</html>
"""").
Start the app:
$ rebar3 shell
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling arizona_example
Erlang/OTP 27 [erts-15.2.2] [source] [64-bit] [smp:24:24] [ds:24:24:10] [async-threads:1] [jit:ns]
Eshell V15.2.2 (press Ctrl+G to abort, type help(). for help)
===> Booted syntax_tools
===> Booted cowlib
===> Booted ranch
===> Booted cowboy
===> Booted arizona
===> Booted arizona_example
The server is up and running at http://localhost:8080, but it is not yet connected to the server.
To establish the connection, create priv/assets/main.js
in your static assets directory (matching
the configured static route path in config/sys.config
and matching the script added to the HTML of
the layout file previously) and add the connection initialization code to it:
arizona.connect();
Open the browser again, and the button click will now increase the count value by one.
The value is updated in arizona_example_counter:handle_event/3
via WebSocket, and the DOM patch
used the morphdom library under the hood.
Note that only the changed part is sent as a small payload from the server to the client.
If you like this tool, please consider sponsoring me. I'm thankful for your never-ending support ❤️
I also accept coffees ☕
Feel free to submit an issue on Github.
Copyright (c) 2023-2025 William Fank Thomé
Arizona is 100% open-source and community-driven. All components are available under the Apache 2 License on GitHub.
See LICENSE.md for more information.