# Overview
## Installation
The package can be installed by adding `vaux` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:vaux, "~> 0.3"}
]
end
```
## Introduction
Vaux (rhymes with yo) provides composable html templates for Elixir. It uses
a customized verion of the excellent html parsing library
[htmerl](https://hex.pm/packages/htmerl) to offer a simple, but still
expressive template syntax.
It builds upon HEEx template syntax, which means it offers good editor support
out of the box.
A minimal example looks like this:
```elixir
defmodule Component.Example1 do
import Vaux.Component
attr :title, :string
~H"""
<h1>{@title}</h1>
"""vaux
end
iex> Vaux.render!(Component.Example1, %{"title" => "Hello World"})
"<h1>Hello World</h1>"
```
If you are familiar with Phoenix components, a Vaux component will look pretty
similar, as it uses the same sigil and template expression syntax as HEEx
templates. To make sure HEEx and Vaux templates can't be mixed up, Vaux
requires the `vaux` modifier for its `~H` sigil. A key difference is that Vaux
only supports a single template per module.
In ordder to call another component, it needs to be known at compile time. Vaux
provides the `components/1` macro that both requires and aliases components:
```elixir
defmodule Component.Example2 do
import Vaux.Component
attr :title, :string
~H"""
<title>{@title}</title>
"""vaux
end
defmodule Page.Page1 do
import Vaux.Component
components Component.{Example1, Example2}
var title: "Hello World"
~H"""
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width"/>
<Example2 title={@title}/>
</head>
<body>
<Example1 title={@title}/>
</body>
</html>
"""vaux
end
iex> Vaux.render!(Page.Page1)
"<html><head><meta charset=\"UTF-8\"/><meta name=\"viewport\" content=\"width=device-width\"/><title>Hello World</title></head><body><h1>Hello World</h1></body></html>"
```
## Slots
Every component has a default slot that holds the element's content:
```elixir
defmodule Layout.Layout1 do
import Vaux.Component
~H"""
<html>
<body>
<slot><p>FALLBACK CONTENT</p></slot>
</body>
</html>
"""vaux
end
defmodule Page.Page2 do
import Vaux.Component
components [
Component.Example1,
Layout.Layout1
]
~H"""
<Layout1>
<Example1 title="Hello World"/>
</Layout1>
"""vaux
end
iex> Vaux.render!(Page.Page2)
"<html><body><h1>Hello World</h1></body></html>"
defmodule Page.Page3 do
import Vaux.Component
components Layout.Layout1
~H"""
<!-- Render fallback content if the component doesn't have any child elements -->
<Layout1></Layout1>
"""vaux
end
iex> Vaux.render!(Page.Page3)
"<html><body><p>FALLBACK CONTENT</p></body></html>"
```
Vaux also supports named slots. This allows you to easily separate page layout from page content.
Named slots need to be defined with the `slot/1` macro. This allows Vaux to
catch typos in the template at compile time and gives a component user a quick
overview what slots are available.
```elixir
defmodule Layout.Layout2 do
import Vaux.Component
slot :head
slot :body
~H"""
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width"/>
<slot #head></slot>
</head>
<body>
<slot #body></slot>
</body>
</html>
"""vaux
end
defmodule Page.Page4 do
import Vaux.Component
components [
Component.{Example1, Example2},
Layout.Layout2
]
var title: "Hello World"
~H"""
<Layout2>
<template #head>
<Example2 title={@title}/>
</template>
<template #body>
<Example1 title={@title}/>
</template>
</Layout2>
"""vaux
end
iex> Vaux.render!(Page.Page4)
"<html><head><meta charset=\"UTF-8\"/><meta name=\"viewport\" content=\"width=device-width\"/><title>Hello World</title></head><body><h1>Hello World</h1></body></html>"
```
## Directives
Vaux doesn't support block expressions, but it has an extensive set of
directives to use:
```elixir
defmodule Component.DirectivesExample do
import Vaux.Component
attr :fruit, {:enum, ~w(apple banana pear orange)}
attr :count, :integer
~H"""
<body>
<!-- case expressions, just like in regular Elixir -->
<div :case={@fruit}>
<span :clause={"apple"}>{String.upcase(@fruit)}</span>
<span :clause={"banana"}>{String.reverse(@fruit)}</span>
<!-- If the pattern is a string, you can ommit the curly braces -->
<span :clause="pear">{String.capitalize(@fruit)}</span>
<span :clause="orange">{String.replace(@fruit, "g", "j")}</span>
<!-- Guards can be used too -->
<span :clause={a when is_atom(a)}>Unexpected</span>
</div>
<!-- Loops can be expressed with the :for directive -->
<div :for={number <- 1..@count}>{number}</div>
<!-- The first element with a truthy :cond expression gets rendered -->
<div :cond={@count >= 5}>Too many</div>
<div :cond={@count >= 3}>Ok</div>
<!-- :else can be used as the equivalent of `true -> ...` in a regular Elixir cond expression -->
<div :else>Too little</div>
<!-- :if can be used too -->
<div :if={@fruit == "apple"}></div>
</body>
"""vaux
end
iex> Vaux.render!(Component.DirectivesExample, %{"fruit" => "orange", "count" => 3})
"<body><div><span>oranje</span></div><div>1</div><div>2</div><div>3</div><div>Ok</div></body>"
```
#### Applying directives to multiple elements
If you want to apply a directive to a list of elements, you can use the
`template` element as a wrapper, as it won't get rendered by default (you can
use the `:keep` directive to keep te `template` element in the rendered output).
```elixir
defmodule Component.Example3 do
import Vaux.Component
attr :fruit, {:enum, ~w(apple banana pear orange)}
~H"""
<template :if={String.starts_with?(@fruit, "a")}>
<a></a>
<b></b>
</template>
"""vaux
end
iex> Vaux.render!(Component.Example3, %{"fruit" => "apple"})
"<a></a><b></b>"
```
#### Using `:bind` and `:let` directives
Vaux templates also offer `:bind` and `:let` directives. These directives make
it possible to bind data in a template and make it available to the consumer of
the component.
```elixir
defmodule Component.BindingExample do
import Vaux.Component
attr :title, :string
~H"""
<slot :bind={String.upcase(@title)}></slot>
"""vaux
end
defmodule Page.Page5 do
import Vaux.Component
components Component.BindingExample
~H"""
<BindingExample title="Hello World" :let={upcased}>{upcased}</BindingExample>
"""vaux
end
iex> Vaux.render!(Page)
"HELLO WORLD"
```
When using named slots, the `:let` directive can be used on the named template element.
## Attribute validation
Vaux uses [JSV](https://hexdocs.pm/jsv/), a modern JSON Schema
validation library. When defining an attribute with the `attr/3` macro, most
JSON schema validation options can be used:
```elixir
defmodule Component.Validations do
import Vaux.Component
# Both Elixir friendly snake_case and JSON Schema's camelCase notation can be used
attr :title, :string, min_length: 8, maxLength: 16, required: true
attr :count, :integer, required: true
# If the type of an array doesn't need extra validation, a shorthand notation can be used
attr :numbers1, :array, items: :integer
attr :numbers2, {:array, :integer}
# Shorthand notation for objects is available too
attr :person1, :object, properties: %{name: {:string, pattern: ~r/\w+\s+\w+/}, age: :integer}
attr :person2, %{name: {:string, pattern: ~r/\w+\s+\w+/}, age: :integer}
~H""vaux
end
```
## Vaux.Component behaviour and `handle_state/1` callback
Every component implements the `Vaux.Component` behaviour. This behaviour
requires two functions to be implemented: `handle_state/1` and `render/1`. Both
receive a struct that is defined by the `sigil_H/2` macro. This struct contains
all defined attributes, variables and slots. The `handle_state/1` function
allows you to preprocess atributes, setup internal variables, etc. Finally, the
returned struct from `handle_state/1` is passed to the `render/1` function.
The `sigil_H/2` macro defines `render/1` and also provides an overridable
default implementation for `handle_state/1`.
The main idea behind the `handle_state/1` callback is that it allows you to
keep most complex control flow and data transformations out of the template.
For top level components however, it can be convenient to treat the callback as
a type of view controller and let it fetch data from a data source itself. When
to apply this strategy boils down to the same arguments when thinking about
side effects in regular code: pure functions tend to be easier to compose and
reason about, so that is a good default. However, making some key components
responsible for fetching data makes it simpler to reuse these components in
different contexts.
```elixir
defmodule Component.StateExample do
import Vaux.Component
@some_data_source %{name: "Jan Jansen", hobbies: ~w(cats drawing)}
attr :title, :string
var :hobbies
~H"""
<section>
<h1>{@title}</h1>
<p>Current hobbies:{@hobbies}</p>
</section>
"""vaux
def handle_state(%__MODULE__{title: title} = state) do
%{name: name, hobbies: hobbies} = @some_data_source
title = EEx.eval_string(title, assigns: [name: name])
hobbies = hobbies |> Enum.map(&String.capitalize/1) |> Enum.join(", ")
{:ok, %{state | title: title, hobbies: " " <> hobbies}}
end
end
iex> Vaux.render!(Component.StateExample, %{"title" => "Hello <%= @name %>"})
"<section><h1>Hello Jan Jansen</h1><p>Current hobbies: Cats, Drawing</p></section>"
```
## Root modules
Vaux allows you to define root modules. These modules can be used to bundle
common elements that components are able to use.
```elixir
defmodule MyRoot do
import Vaux.Root
components [
Component.{Example1, Example2},
Layout.Layout2
]
const title: "Hello World"
end
defmodule Page.Page6 do
use MyRoot
~H"""
<Layout2>
<template #head>
<Example2 title={@!title}/>
</template>
<template #body>
<Example1 title={@!title}/>
</template>
</Layout2>
"""vaux
end
iex> Vaux.render!(Page.Page6)
"<html><head><meta charset=\"UTF-8\"/><meta name=\"viewport\" content=\"width=device-width\"/><title>Hello World</title></head><body><h1>Hello World</h1></body></html>"
```
The `const/1` macro allow you to define static data that is reused by multiple
components. These can be accessed in templates by the special `@!my_const`
syntax. The `components/1` macro works the same as in component definitions.
When at least one `const/1` or `components/1` definition is included in a root
module, a `__using__/1` macro is created for the root module that allows it to be used
in a component.