ExComponent v0.5.0 ExComponent View Source
A DSL for easily building dynamic, reusable components for your frontend framework in Elixir.
defcontenttag :alert, tag: :div, class: "alert",
variants: [
primary: [class: "alert-primary"],
success: [class: "alert-success"]
]
alert :primary, "Alert!"
#=> <div class="alert alert-primary">Alert!</div>
alert :primary, "Alert!", class: "extra"
#=> <div class="alert alert-primary extra">Alert!</div>
alert :success, "Alert!"
#=> <div class="alert alert-success">Alert!</div>
Generated function clauses accept a block and a list of opts.
alert :primary, class: "extra" do
"Alert!"
end
#=> <div class="alert alert-primary extra">Alert!</div>
Usage
The lib defines two macros: deftag
and defcontenttag
.
The deftag
macro defines void components, those that do not accept their
own content, like hr
, while the defcontenttag
macro defines components that accept
their own content, like div
.
Function Delegation
The :tag
option accepts an atom and, when using defcontenttag
, an anonymous function,
which allows you to generate components that defer execution to another function.
This is useful if you want to use Phoenix.HTML.Link.link/2
, for example.
defcontenttag :list_group_item, tag: &Phoenix.HTML.Link.link/2, class: "list-group-item"
list_group_item "Action", to: "#"
#=> <a href="#" class: "list-group-item">Action</a>
CSS Class
The :class
option is the base class of the component and can be used to build
variants and options. See the Variants section below for details.
Variants
Variants are a handy way to define the same component in different contexts and generate
a name/3
function clause that takes the variant name as its first argument.
defcontenttag :button, tag: :button, class: "btn",
variants: [
success: [class: "btn-success"],
primary: [class: "btn-primary"],
dropdown: [
class: "toggle-dropdown", data: [toggle: "dropdown"], aria: [haspopup: true, expanded: false]
]
]
button :success do
"Success!"
end
#=> <button class="btn btn-success">Success!</button>
button :dropdown do
"Dropdown!"
end
#=> <button class="btn toggle-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown!</button>
You can combine variants by passing a named option with a list.
button variants: [:success, :dropdown] do
"Dropdown!"
end
#=> <button class="btn btn-success toggle-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown!</button>
Variant Class vs Component Class
Each declared variant has a :merge
option that defaults to true
. When true
this option
appends the variant class to the component class.
defcontenttag :alert, tag: :div, class: "alert",
variants: [
primary: [class: "alert-primary"],
success: [class: "alert-success"],
]
alert :primary, "Alert!"
#=> <div class="alert alert-primary">Alert!</div>
The above example gets the class alert
from the component and the class alert-primary
from
the variant.
While this is handy for declaring contextual variants that inherit the component's class, you may
want more control over this behaviour. In such cases, :merge
can be set to false
or a custom value.
The Bootstrap dropup is a great example when control over the :merge
option can be handy.
#=> <ul class="dropdown">...</ul>
#=> <ul class="dropup">...</ul>
defcontenttag :dropdown, tag: :ul, class: "dropdown",
variants: [
dropup: [class: "dropup", merge: false]
]
dropdown :dropup do
"Dropup!"
end
#=> <ul class="dropup">...</ul>
dropdown :dropdown do
"Dropdown!"
end
#=> <ul class="dropdown">...</ul>
Variant Prefix
The :prefix
is a shortcut for prefixing the component's or a custom class to the variant class. It defaults
to false
. The following three examples are equivalent.
defcontenttag :alert, tag: :div, class: "alert",
variants: [
primary: [class: "primary", prefix: true]
]
defcontenttag :alert, tag: :div, class: "alert",
variants: [
primary: [class: "alert-primary"]
]
defcontenttag :alert, tag: :div, class: "alert",
variants: [
primary: [class: "primary", prefix: "alert"]
]
Using Variants As Options
Any variant can be used as an option by passing it option: true
. This means you can use the variant
as a key-value pair along with other options, like :class
, for example.
defcontenttag :alert, tag: :div, class: "alert",
variants: [
primary: [class: "alert-primary", option: true]
]
alert primary: true do
"..."
end
#=> <div class="alert alert-primary">...</div>
You can pass a boolean or a custom string to the option when making the function call.
When you enable a variant as an option, it uses the variant's :prefix
setting.
See Declaring Options for more information.
Adding Custom Options
You can create and use any custom options, which are similar to variants but they do not
define a name/3
clause and only affect the CSS class of the component.
Any defined options can be passed as key-value pairs along with other options when calling
the fuction, as you would for :class
or any other option.
col :variant, option: value, option: value, ... do
"Col!"
end
Unlike variants, options only compose CSS classes, while variants accept any HTML options that they forward onto the HTML.
defcontenttag :col, tag: :div, class: "col",
options: [
sm: [class: "col-sm"],
auto: [class: "col-auto"]
]
col auto: true, sm: 6 do
"Col!"
end
#=> <div class="col col-auto col-sm-6">...</div>
Option Prefix
Like variants, options also accept a :prefix
, which is a shortcut for prefixing the component's or a custom class to the option's
class and value. It works the same way that the :variant
prefix does.
The following examples are all equivalent. See Variants for more.
defcontenttag :col, tag: :div, class: "col",
options: [
sm: [class: "sm", prefix: true],
]
defcontenttag :col, tag: :div, class: "col",
options: [
sm: [class: "col-sm"],
]
defcontenttag :col, tag: :div, class: "col",
options: [
sm: [class: "sm", prefix: "col"]
]
Combining Options And Variants
Combine variants and options to create complex class combinations.
For example, in Bootstrap, col
and col-auto
are mutually exclusive. By combining variants and options
you can create your desired combination.
defcontenttag :col, tag: :div, class: "col",
variants: [
sm: [class: "col-sm", merge: false], # `merge: false` removes the component class, `col`.
auto: [class: "col-auto", merge: false]
],
options: [
sm: [class: "col-sm"],
auto: [class: "col-auto"]
]
col :auto, sm: 6 do
"..."
end
#=> <div class="col-auto col-sm-6">...</div>
Note that, options can be passed true
to use the only the option's class rather than an explicit value.
col auto: true do
"..."
end
#=> <div class="col-auto">...</div>
On Variants And Options
While combining variants and options is powerful, sometimes it's best to go for simpliciy. The examples above can be declared as separate components.
defcontenttag :col_auto, tag: :div, class: "col-auto",
options: [
sm: [class: "col-sm"]
md: [class: "col-md"]
]
defcontenttag :col_sm, tag: :div, class: "col-auto",
options: [
md: [class: "col-md"]
]
col_auto sm: 6, md: 4 do
"Col!"
end
#=> <div class="col-auto col-sm-6 col-md-4">...</div>
col_sm md: 4 do
"Col!"
end
#=> <div class="col-sm col-md-4">...</div>
Appending And Prepending Content
You can append or prepend additional components to your component's content by using :append
and/or :prepend
.
For example, a Bootstrap alert can have a close button.
defcontenttag :close, tag: :button, wrap_content: :span, class: "close", data: [dismiss: "alert"], aria: [label: "Close"]
defcontenttag :alert, tag: :div, class: "alert", prepend: close("×"), variants: [primary: [class: "primary"]]
alert :primary do
"Content"
end
<div class="alert alert-primary">
<button aria-label="Close" class="close" data-dismiss="alert">
<span>×</span>
</button>
Content
</div>
Both options accept an atom or a tuple in one the forms {:safe, iodata}
, {:tag, opts}
, {:tag, "content"}
, and {:tag, "content", opts}
.
Nesting Components
The :parent
option is useful for nesting a component in an additional tag. You can pass
an atom, or a tuple with either a function or an atom, and a list of parent options.
For example, breadcrumbs in Bootstrap are built with an ol
tag wrapped in a nav
tag.
<nav role="nav">
<ol class="breadcrumbs">
<li class="breadcrumbs-item">...</li>
</ol>
</nav>
You can use the parent: :nav
or parent: {:nav, [role: "nav"]}
to address this case.
defcontenttag :breadcrumbs, tag: :ol, ..., parent: :nav
defcontenttag :breadcrumbs, tag: :ol, ..., parent: {:nav, [role: "nav"]}
You can also pass an anonymouse function to the parent option.
defcontenttag :breadcrumbs, tag: :ol, ..., parent: &fun/1
Wrapping Content
The :wrap_content
option works exactly like the :parent
option except that it wraps the
content of the component rather than the component itself.
For example, a Bootstrap button whose text is wrapped in a span
.
defcontenttag :button, tag: :button, ..., wrap_content: :span
Default HTML Options
Any additional options declared in the component definition are forwarded onto the underlying HTML. Default options can be overriden during function calls.
Options
:tag
- the component's tag. Can be an atom or an anonymous function.:class
- the component's class name. This option is required.:parent
- wraps the component in the given tag. Accepts an atom, a anonymous function, or a tuple where the first element is the parent tag and the second is a list of parent options. For example,{:div, [class: "something"]}
.:prepend
- prepends the given tag to the component's content. Accepts a tuple in the following format:{:safe, iodata}
,{:tag, opts}
,{:tag, "content"}
, or{:tag, "content", opts}
. For example,{:hr, [class: "divider"]}
or{:button, "Dropdown", class: "extra"}
.:append
- appends the given content to the component. See the:prepend
option for usage.:wrap_content
- wraps the inner content of the component in the given tag. See the:parent
option for usage.:variants
- a keyword list of component variants. Each variant generates acomponent/3
(component/2
fordeftag
) function clause where an atom variant name is the first argument.:options
- a list of options that the component can accept, which generate additional classes.