ExComponent v0.2.0 ExComponent View Source
A DSL for easily building dynamic, reusable components for your frontend framework in Elixir.
defcontenttag :card, tag: :div, class: "card"
card do
"Content!"
end
#=> <div class="card">Content!</div>
defcontenttag :alert,
tag: :div,
class: "alert",
variants: [
primary: [class: "primary"],
success: [class: "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 an anonymous function (when using defcontenttag
),
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 is used to build
variants and options. See the Variants section below for details.
Variants
A variant generates a name/3
function clause that takes the variant name as its first argument.
Variants are a handy way to define the same component in different contexts.
defcontenttag :button,
tag: :button,
class: "btn",
variants: [
success: [class: "success"],
primary: [class: "primary"],
dropdown: [
class: "toggle-dropdown", prefix: false,
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>
Merge
Each declared variant has a :merge
option that defaults to true
. While it's handy for declaring
contextual (class="alert alert-{success|danger}"
) variants that inherit the parent component class, sometimes
you may want to customise or remove the component class.
defcontenttag :dropdown,
tag: :ul,
class: "dropdown",
variants: [
dropup: [class: "dropup", merge: false]
]
dropdown :dropup do
"Dropup!"
end
#=> <ul class="dropup">...</ul>
dropdown :dropdown do
"Dropup!"
end
#=> <ul class="dropdown">...</ul>
Prefix
The prefix is a shortcut for prefixing the component's class to the variant class. It default's to
the component's :class
option. The following three examples are equivalent.
defcontenttag :alert,
tag: :div,
class: "alert",
variants: [
primary: [class: "primary"]
]
defcontenttag :alert,
tag: :div,
class: "alert",
variants: [
primary: [class: "alert-primary", prefix: false]
]
defcontenttag :alert,
tag: :div,
class: "alert",
variants: [
primary: [class: "primary", prefix: "alert"]
]
Declaring Options
You can declare a list of options that can be used during function calls. This is handy for combining with variants to create complex class combinations.
defcontenttag :col,
tag: :div,
class: "col",
options: [:sm, :md, :lg, :auto]
col auto: true, sm: 6, md: 4 do
"Col!"
end
#=> <div class="col col-auto col-sm-6 col-md-4">...</div>
In the above example, you may not want to use the col
class since you are declaring col
. In this case,
combine with variants for the desired combinations.
defcontenttag :col,
tag: :div,
class: "col",
variants: [
auto: [class: "auto"],
sm: [class: "sm"],
md: [class: "md"],
lg: [class: "lg"],
],
options: [:auto, :sm, :md, :lg]
col :auto, sm: 6, md: 4 do
"Col!"
end
#=> <div class="col-auto col-sm-6 col-md-4">...</div>
Note that, options can:
have their component's class prefixed;
be passed
true
to use the option's name as the class rather than an explicit value.
On Variants And Options
While combining these 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: [:auto, :sm, :md, :lg]
defcontenttag :col_sm,
tag: :div,
class: "col-sm",
options: [:auto, :sm, :md, :lg]
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.