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("&times;"), variants: [primary: [class: "primary"]]

alert :primary do
  "Content"
end
<div class="alert alert-primary">
  <button aria-label="Close" class="close" data-dismiss="alert">
    <span>&times;</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 a component/3 (component/2 for deftag) 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.

Link to this section Summary

Link to this section Functions

Link to this macro

defcontenttag(name, options)

View Source (macro)
Link to this macro

deftag(name, options)

View Source (macro)