Vaux.Component behaviour (Vaux v0.3.7)
View SourceImport this module to define a component
In addition to compiling the component template, the sigil_H/2
macro
collects all defined attributes, variables and slots to define a struct that
holds all this data as the component's state. This state struct is passed to
the compiled template and it's fields are accessible via the @
assign syntax
inside the template.
The module's behaviour requires the callback functions handle_state/1
and
render/1
to be implemented. However when defining a html template with
sigil_H/2
, both functions will be defined automatically. handle_state/1
is
overridable to make it possible to process the state struct before it gets
passed to render/1
.
iex> 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>"
Summary
Callbacks
Functions
Define an attribute that can hold any value. See attr/3
for more information about attributes.
Define an attribute
Attributes, together with slots, provide the inputs to component templates. Attribute values are currently always html escaped. Vaux uses JSV for attribute validation. This means that most JSON Schema validation keywords are available for validating attributes.
JSON Schema keywords are camelCased and can be used as such. However, the attr/3
macro also supports Elixir friendly snake_case naming of JSON Schema keywords.
The following types are currently supported:
boolean
object
array
number
integer
string
true
false
Note that the types true
and false
are different from type boolean
.
true
means that any type will be accepted and false
disallows any type.
These are mostly added for completeness, but especially true
might be useful
in some cases.
Options can be both applicator and validation keywords. Currently supported options are:
properties
items
contains
maxLength
minLength
pattern
exclusiveMaximum
exclusiveMinimum
maximum
minimum
multipleOf
required
maxItems
minItems
maxContains
minContains
uniqueItems
description
default
format
A good resource to learn more about the use of these keywords is www.learnjsonschema.com.
The attr/3
macro provides some syntactic sugar for defining types. Instead of writing
attr :numbers, :array, items: :integer, required: true
it is also possible to write
attr :numbers, {:array, :integer}, required: true
When defining objects
attr :person, :object, properties: %{name: :string, age: :integer}
it is also possible to write
attr :person, %{name: :string, age: :integer}
All validation options can be used when defining (sub)properties by using a tuple
attr :person, %{
name: {:string, min_length: 8, max_length: 16},
age: {:integer, minimum: 0}
}
Convenience macro to declare components to use inside a template.
components My.Component
gets translated to
require My.Component, as: Component
A more complete example
defmodule MyComponent do
import Vaux.Component
components Some.{OtherComponent1, OtherComponent2}
components [
Some.Layout,
Another.Component
]
~H"""
<Layout>
<Component/>
<OtherComponent1/>
<OtherComponent2/>
</Layout>
"""vaux
end
Define a html template
Vaux templates support {...}
for HTML-aware interpolation inside tag
attributes and the body. @field
can be used to access any field of the the
template's state struct. To access a root module constant,
@!constant
can be used. Note that Vaux templates require vaux
as sigil
modifier in order to distinguish them from HEEx templates.
~H"<h1>{@title}</h1>"vaux
An extensive set of directives is available for expressing control flow, iteration, template bindings and visibility within templates. Available control flow directives are:
:if
:else
:cond
:case
:clause
Most of these directives work like the equivalent in regular Elixir code. A
notable difference is that the :cond
directive won't raise an exception when
there is no truthy condition, it simply skips rendering all elements with the
:cond
directive. However, when using the :case
directive and there is no
matching :clause
, an exception will be raised. This behaviour might change in
future releases.
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>
<!-- 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 (with or without a following :else) can be used too -->
<div :if={@fruit == "apple"}></div>
</body>
"""vaux
end
:for
can be used for iterating. It supports a single Elixir for
generator.
<div :for={n <- 1..10}>Number: {n}</div>
By using the :bind
and :let
directives, it is possible to bind data in a
template and make it available to the consumer of the component. When using
named slots, the :let
directive can be used on the named template element.
iex> defmodule Component do
...> import Vaux.Component
...>
...> attr :title, :string
...>
...> ~H"""
...> <slot :bind={String.upcase(@title)}></slot>
...> """vaux
...> end
iex> defmodule Page do
...> import Vaux.Component
...>
...> components Component
...>
...> ~H"""
...> <Component title="Hello World" :let={upcased}>{upcased}</Component>
...> """vaux
...> end
iex> Vaux.render!(Page)
"HELLO WORLD"
Finally, the :keep
directive can be used on template or slot elements to
keep them in the rendered output.
Define a named slot
defmodule Layout do
import Vaux.Component
slot :content
slot :footer
~H"""
<body>
<main>
<slot #content></slot>
</main>
<footer>
<slot #footer><p>footer fallback content</p></slot>
</footer>
</body>
"""vaux
end
defmodule Page do
import Vaux.Component
components Layout
~H"""
<html>
<head>
<title>Hello World</title>
</head>
<Layout>
<template #content>
<h1>Hello World</h1>
</template>
</Layout>
</html>
"""vaux
end
Define a variable
A component variable can be either used as a constant, or in combination with
handle_state/1
as a place to store internal data that can be accessed inside
a template with the same @
syntax as attributes.
iex> defmodule Hello do
...> import Vaux.Component
...>
...> var title: "Hello"
...>
...> ~H"<h1>{@title}</h1>"vaux
...> end
iex> Vaux.render(Hello)
{:ok, "<h1>Hello</h1>"}