Combo.HTML (combo v0.10.0)
View SourceBuilding blocks for working with HTML.
Features
- Components
- Form handling
Note
This module is built on top of:
And, by design, they are hidden from daily use.
Syntax
EEx
As the name "CEEx" suggests, CEEx is built on top of EEx, and therefore it
supports all EEx features.
Tags
<%= expression %>- expression tag, which inserts the value of expression.<% expression %>- execution tag, which executes expression, but doesn't insert the value.<%!-- comments --%>- comment tag, which is removed from the final output.<%% content %>- quotation tag, which inserts literal<% content %>.
Interpolating blocks
if:
<%= if ... do %>
...
<% end %>case:
<%= case ... do %>
<% ... -> %>
...
<% ... -> %>
...
<% end %>for:
<%= for ... do %>
...
<% end %>CEEx extensions
Curly-interpolation
Besides EEx's intepolation syntax - <%= expression %>, CEEx introduces a
syntax for HTML-aware interpolation - {expression}. It can be used within
HTML tag attributes and HTML tag contents.
Interpolating the value of tag attributes
To interpolate the value of an tag attribute, use {} to assign an
expression to the tag attribute:
<div class={expression}>
...
</div>Additionally, there are values which have special meanings when they are used as the values of tag attributes:
if a value is
true, the attribute is treated as boolean attribute, and it will be rendered with no value at all. For example,<input required={true}>is rendered as<input required>.if a value is
falseornil, the attribute is treated as boolean attribute, and it won't be rendered at all. For example,<input required={false}>is rendered as<input>.
Interpolating multiple attributes
To interpolate multiple attributes, use {} without assigning expression
to any specific attribute:
<div {expression}>
...
</div>And the expression must be either a keyword list or a map containing the
key-value pairs representing the attributes.
Interpolating tag contents
To interpolate a tag content:
<p>Hello, {expression}</p>Limitations
Curly-interpolation is easy to use, but it has limitations:
it can't be used inside
<script>and<style>tags, as that would make writing JS and CSS quite tedious.it doesn't support interpolating blocks, such as
if,fororcaseblocks. (But, for conditionals and for-comprehensions, there are built-in support in CEEx, which we will explained later.)
For these cases, you have to use EEx tags as the workaround. For example:
<script>
window.URL = "<%= expression %>"
</script><%= if condition do %>
<p>Hello, {expression}</p>
<% end %>Disabling curly-interpolation
Curly-interpolation is allowed to be disabled in a given tag and its children by
adding the ceex-no-curly-interpolation attribute. For example:
<p ceex-no-curly-interpolation>
Hello, {expression}
</p>Curly braces in text within tag content
If you have text in your tag content, which includes curly braces you can
use { or <%= "{" %> to prevent them from being considered the
start of interpolation.
Special attributes
Besides normal HTML attributes, CEEx supports some special attributes.
:if and :for
They are syntax sugar for <%= if ... do %> and <%= for ... do %>. They can
be used in HTML tags, components and slots.
<p :if={@admin?}>secret</p>
<%!-- same as --%>
<%= if @admin? do %>
<p>secret</p>
<% end %><table>
<tr :for={user <- @users}>
<td>{user.name}</td>
</tr>
<table>
<%!-- same as --%>
<table>
<%= for user <- @users do %>
<tr>
<td>{user.name}</td>
</tr>
<% end %>
<table>We can also combine :for and :if:
<table>
<tr :for={user <- @users} :if={user.vip? == true}>
<td>{user.name}</td>
</tr>
<table>
<%!-- same as --%>
<table>
<%= for user <- @users, user.vip? == true do %>
<tr>
<td>{user.name}</td>
</tr>
<% end %>
<table>These syntax sugars is easy to use, but it still has limitations:
- Unlike Elixir's regular
for,:fordoes not support multiple generators in one expression.
:let
It's used to yield a value back to the caller of component. They can be used by components and slots.
This is used by components and slots which want to yield a value back to the caller. For an example:
<.form :let={f} for={@form} >
<.input field={f[:username]} type="text" />
...
</.form>We'll talk about it when introducing slots.
assigns and @ symbol
assigns refers to the external data which is available in templates. If we
want to pass external data into templates, we put data into assigns.
And, when accessing external data, we can use assigns directly, or its
syntax sugar @.
Creating templates
There are two ways to create templates, inline templates or template files.
Inline templates are good choice for small templates. And, template files are good choice when having a lot of markup.
Inline templates
Inline templates are defined with ~CE sigil. For example:
~CE"""
<p>Hello, {@name}!</p>
"""Template files
Template files are those with the .html.ceex extension.
For example, imagine a file welcome.html.ceex with following content:
<p>Hello, {@name}!</p>Creating components
In fact, standalone templates are useless on their own, as there is no way to call them. To make them callable, templates must be wrapped as components.
But, what is a component?
A component is a function that accepts an assigns map as an argument and
returns a template.
Next, we will wrap templates as components.
Components using inline templates
defmodule DemoWeb.Component do
use Combo.HTML
def welcome(assigns) do
~CE"""
<p>Hello, {@name}!</p>
"""
end
endComponents using template files
Imagine a directory listing:
├── pages.ex
├── pages
│ ├── welcome.html.ceex
│ └── contact.html.ceexWe can embed the template files as components into a module:
defmodule DemoWeb.Pages do
use Combo.HTML
embed_templates "pages/*"
endEffectively, it is equivalent to:
defmodule DemoWeb.Pages do
def welcome(assigns) do
# the content of compiled welcome.html.ceex
end
def contact(assigns) do
# the content of compiled contact.html.ceex
end
endCalling components
Before introducing how to call a component, let's explain two terms:
- remote components, which are components defined in external modules.
- local components, which are components defined in current module, or components imported into current module.
To call components, CEEx provides an HTML-like notation.
For a remote component, the caller should call it with the qualified name of the component:
<DemoWeb.Component.welcome name="Charlie Brown" />For a local component, the caller can call it with the component name prefixing a leading dot:
<.greet name="Charlie Brown" />Declarative API of assigns
Declaring attributes
Combo.HTML provides attr/3 used to declare an attribute for a component.
For example:
attr :name, :string, required: true
attr :age, :integer, required: true
def welcome(assigns) do
~CE"""
<p>Hello, {@name}!</p>
"""
endSee Combo.Template.CEExEngine.DeclarativeAssigns.attr/3 for more
information.
Global attributes
There is a special case that requires detailed explanation - global attributes.
A global attribute is a special attribute which collects all attributes that
are not explicitly declared by attr/3.
The collected attributes can be:
- attributes listed in HTML standard. See Global attributes for a complete list of attributes.
- attributes specified by
:includeoption (to be explained later). - attributes prefixed with custom global prefixes (to be explained later).
Let's look at an example first. Below is a component that accepts a global attribute:
attr :message, :string, required: true
attr :rest, :global
def notification(assigns) do
~H"""
<span {@rest}>{@message}</span>
"""
endThe caller can pass multiple attributes to it, such as class, data-*, etc:
<.notification message="You've got mail!" class="bg-green-200" data-action="close" />Rendering the following HTML:
<span class="bg-green-200" data-action="close">You've got mail!</span>Note that the component did not explicitly declare a class or data-state
attribute.
:default option
The :default option specifies the default value, which will be merged with
attributes provided by the caller. For example, we can declare a default
class:
attr :rest, :global, default: %{class: "bg-blue-200"}Now, we can call the component without a class attribute:
<.notification message="You've got mail!" data-action="close" />Rendering the following HTML:
<span class="bg-blue-200" data-action="close">You've got mail!</span>:include option
The :include option specifies extra attributes to be included. For example:
attr :rest, :global, include: ~w(form)
The :include option is useful to apply global additions on a case-by-case
basis, but sometimes we want to extend existing components with new global
attributes, such as Alpine.js' x- prefixes, which we'll outline next.
Global prefixes
All attributes prefixed with global prefixes, will be collected by a global attribute. By default, the following global prefixes are supported:
data-aria-
To add extra global prefixes, let's say adding the x- prefix used by
Alpine.js, we can pass the :global_attr_prefixes option
to use Combo.HTML:
use Combo.HTML, global_prefixes: ~w(x-)Declaring slots
In addition to attributes, components can accept blocks of CEEx content, referred to as slots.
Combo.HTML provides slot/3 used to declare a slot for a component.
slot :inner_block, required: true
def button(assigns) do
~H"""
<button>
{render_slot(@inner_block)}
</button>
"""
endThe expression render_slot(@inner_block) renders the CEEx content. You can
call this component like:
<.button>
This renders <strong>inside</strong> the button!
</.button>Which renders the following HTML:
<button>
This renders <strong>inside</strong> the button!
</button>The example above uses the default slot, accessible as an assign named
@inner_block, to render CEEx content by calling render_slot/1.
Passing value to slots
If the values rendered in the slot need to be dynamic, you can pass a value
back to the CEEx content by calling render_slot/2:
attr :entries, :list, default: []
slot :inner_block, required: true
def list(assigns) do
~H"""
<ul>
<li :for={entry <- @entries}>{render_slot(@inner_block, entry)}</li>
</ul>
"""
endWhen calling the component, we can use the special attribute :let to take
the value that the component passes back and bind it to a variable:
<.list :let={fruit} entries={~w(apples bananas cherries)}>
I like <b>{fruit}</b>!
</.list>Which renders the following HTML:
<ul>
<li>I like <b>apples</b>!</li>
<li>I like <b>bananas</b>!</li>
<li>I like <b>cherries</b>!</li>
</ul>Named slots
In addition to the default slot, components can accept multiple, named slots. For example, imagine a modal component that has a header, body, and footer:
slot :header
slot :inner_block, required: true
slot :footer, required: true
def modal(assigns) do
~H"""
<div class="modal">
<div class="modal-header">
{render_slot(@header) || "Modal"}
</div>
<div class="modal-body">
{render_slot(@inner_block)}
</div>
<div class="modal-footer">
{render_slot(@footer)}
</div>
</div>
"""
endYou can call this component using the named slot syntax:
<.modal>
This is the body, everything not in a named slot is rendered in the default slot.
<:footer>
This is the bottom of the modal.
</:footer>
</.modal>Which renders the following HTML:
<div class="modal">
<div class="modal-header">
Modal
</div>
<div class="modal-body">
This is the body, everything not in a named slot is rendered in the default slot.
</div>
<div class="modal-footer">
This is the bottom of the modal.
</div>
</div>As shown in the example, render_slot/1 returns nil when an optional slot
is declared and none is given. This can be used to attach default behaviour.
Slot attributes
Named slots can also accept attributes, defined by passing a block to the
slot/3 macro. If multiple pieces of content are passed, render_slot/2
will merge and render all the values.
For example, image a table component:
slot :column do
attr :label, :string, required: true
end
attr :rows, :list, default: []
def table(assigns) do
~H"""
<table>
<tr>
<th :for={col <- @column}>{col.label}</th>
</tr>
<tr :for={row <- @rows}>
<td :for={col <- @column}>{render_slot(col, row)}</td>
</tr>
</table>
"""
endYou can call this component like:
<.table rows={[%{name: "Jane", age: "34"}, %{name: "Bob", age: "51"}]}>
<:column :let={user} label="Name">
{user.name}
</:column>
<:column :let={user} label="Age">
{user.age}
</:column>
</.table>which renders the following HTML:
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Jane</td>
<td>34</td>
</tr>
<tr>
<td>Bob</td>
<td>51</td>
</tr>
</table>See Combo.Template.CEExEngine.DeclarativeAssigns.slot/3 for more
information.
Embedded templates
Declarative API of assigns can be used for embedded templates, too.
For example:
defmodule DemoWeb.Components do
use Combo.HTML
embed_templates "pages/*"
attr :name, :string, required: true
def welcome(assigns)
slot :header
def about(assigns)
endDynamic Component Rendering
Sometimes you might need to decide at runtime which component to render.
Because components are just regular functions, we can leverage Elixir's
apply/3 function to dynamically call a module and/or function passed in
For example, image a component like this:
attr :module, :atom, required: true
attr :function, :atom, required: true
# any shared attributes
attr :shared, :string, required: true
# any shared slots
slot :named_slot, required: true
slot :inner_block, required: true
def dynamic_component(assigns) do
{mod, assigns} = Map.pop(assigns, :module)
{func, assigns} = Map.pop(assigns, :function)
# call the function with remaining assigns
apply(mod, func, [assigns])
endAnd, a component like this:
defmodule DemoWeb.Components do
attr :shared, :string, required: true
slot :named_slot, required: true
slot :inner_block, required: true
def example(assigns) do
~H"""
<p>Dynamic component with shared assigns: {@shared}</p>
{render_slot(@inner_block)}
{render_slot(@named_slot)}
"""
end
endThen, we can use the dynamic_component function like:
<.dynamic_component
module={DemoWeb.Components}
function={:example}
shared="Yay Elixir!"
>
<p>Howdy from the inner block!</p>
<:named_slot>
<p>Howdy from the named slot!</p>
</:named_slot>
</.dynamic_component>Which renders the following HTML:
<p>Dynamic component with shared assigns: Yay Elixir!</p>
<p>Howdy from the inner block!</p>
<p>Howdy from the named slot!</p>
Summary
Functions
Merge values in a list as a string, which can be used as the value of attributes.
This function bulits the final string by by joining all truthy elements in
the list with " ".
Examples
iex> ml(["btn", nil, false, "btn-primary"])
"btn btn-primary"
iex> ml(["btn", nil, false, [nil, "btn-primary"]])
"btn btn-primary"
@spec raw(Combo.SafeHTML.safe() | iodata() | nil) :: Combo.SafeHTML.safe()
Marks the given content as raw.
This means any HTML code inside the given string won't be escaped.
By default, interpolated data in templates is considered unsafe:
<%= "<hello>" %>which renders:
<hello>However, in some cases, you may want to tag it as safe and show its "raw" contents:
<%= raw "<hello>" %>which renders:
<hello>Examples
iex> raw({:safe, "<hello>"})
{:safe, "<hello>"}
iex> raw("<hello>")
{:safe, "<hello>"}
iex> raw(nil)
{:safe, ""}