Skip to content

arizona-framework/arizona

Arizona

arizona_256x256

Arizona is a web framework for Erlang.

⚠️ Warning

Work in progress.

Use it at your own risk, as the API may change at any time.

Template Syntax

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.

Basic Usage

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.

"Counter Example"

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.

Sponsors

If you like this tool, please consider sponsoring me. I'm thankful for your never-ending support ❤️

I also accept coffees ☕

"Buy Me A Coffee"

Contributing

Issues

Feel free to submit an issue on Github.

License

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.