Vaux.Component behaviour (Vaux v0.3.9)
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:
booleanobjectarraynumberintegerstringtruefalse
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:
propertiesitemscontainsmaxLengthminLengthpatternexclusiveMaximumexclusiveMinimummaximumminimummultipleOfrequiredmaxItemsminItemsmaxContainsminContainsuniqueItemsdescriptiondefaultformat
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: trueit is also possible to write
attr :numbers, {:array, :integer}, required: trueWhen 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.Componentgets translated to
require My.Component, as: ComponentA 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>{String.capitalize(@title)}</h1>"vauxOnly variable interpolations are allowed inside script and style tags.
These are exressions in the form of {@field} or {@!const}. {1 + @field} is not valid
variable interpolation for example.
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>"}