07c82e21d3
* Move directories for ast tests to match convention * feat!: Rename `:let` to `:let!` We use the "bang" style as the reserved keyword to differentiate it from other possible attributes. * feat: use Phoenix.HTML as the default engine I am choosing to leverage this library in order to quickly get dynamic attributes (see #183) up and running. This also ensures that folks who wish to use Temple outside of a Phoenix project with get some nice HTML functions as well as properly escaped HTML out of the box. This can be made optional if Temple becomes decoupled from the render and it including HTML specific packages becomes a strange. * feat: Allow user to make their own Component module The component module is essentially to defer compiling functions that the user might not need. The component, render_slot, and inner_block functions are only mean to be used when there isn't another implementation. In the case of a LiveView application, LiveView is providing the component runtime implementation. This was causing some compile time warnings for temple, because it was using the LiveView engine at compile time (for Temple, not the user's application) and LiveView hadn't been compiled or loaded. So, now we defer this to the user to make their own module and import it where necessary. * feat: Pass dynamic attributes with the :rest! attribute The :rest! attribute can be used to pass in a dynamic list of attributes to be mixed into the static ones at runtime. Since this cannot be properly escaped by any engine, we have to mark it as safe and then allow the function to escape it for us. I decided to leverage the `attributes_escape/1` function from `phoenix_html`. There isn't really any point in making my own version of this or vendoring it. Now you can also pass a variable as the attributes as well if you only want to pass through attributes from a calling component. The :rest! attribute also works with components, allowing you to pass a dynamic list of args into them. Fixes #183 * Move test components to their own file. * docs(components): Update documentation on Temple.Components * docs(guides): Mention attributes_escape/1 function in the guides * chore(test): Move helper to it's own module * feat: rest! support for slots * docs(guides): Dynamic attributes * ci: downgrade runs-on to support OTP 23
229 lines
5.9 KiB
Markdown
229 lines
5.9 KiB
Markdown
# Components
|
|
|
|
Temple has the concept of components, which allow you an expressive and composable way to break up your templates into re-usable chunks.
|
|
|
|
A component is any arity-1 function that take an argument called `assigns` and returns the result of the `Temple.temple/1` macro.
|
|
|
|
## Definition
|
|
|
|
Here is an example of a simple Temple component. You can observe that it seems very similar to a regular Temple template, and that is because it is a regular template!
|
|
|
|
```elixir
|
|
defmodule MyApp.Components do
|
|
import Temple
|
|
|
|
def button(assigns) do
|
|
temple do
|
|
button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do
|
|
@text
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
## Usage
|
|
|
|
To use a component, you will use the special `c` keyword. This is called a "keyword" because it is not a function or macro, but only exists inside of the `Temple.temple/1` block.
|
|
|
|
The first argument will be the function reference to your component function, followed by any assigns. You can pass dynamic assigns using the `:rest!` keyword the same way you would with a normal tag.
|
|
|
|
```elixir
|
|
defmodule MyApp.ConfirmDialog do
|
|
import Temple
|
|
import MyApp.Components
|
|
|
|
def render(assigns) do
|
|
temple do
|
|
dialog open: true do
|
|
p do: "Are you sure?"
|
|
form method: "dialog" do
|
|
c &button/1, class: "border border-white", text: "Yes"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
## Slots
|
|
|
|
Temple components can take "slots" as well. This is the method for providing dynamic content from the call site into the component.
|
|
|
|
Slots are defined and rendered using the `slot` keyword. This is similar to the `c` keyword, in that it is not defined using a function or macro.
|
|
|
|
### Default Slot
|
|
|
|
The default slot can be rendered from within your component by passing the `slot` the `@inner_block` assign. Let's redefine our button component using slots.
|
|
|
|
```elixir
|
|
defmodule MyApp.Components do
|
|
import Temple
|
|
|
|
def button(assigns) do
|
|
temple do
|
|
button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do
|
|
slot @inner_block
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
You can pass content through the "default" slot of your component simply by passing a `do/end` block to your component at the call site. This is a special case for the default slot.
|
|
|
|
```elixir
|
|
defmodule MyApp.ConfirmDialog do
|
|
import Temple
|
|
import MyApp.Components
|
|
|
|
def render(assigns) do
|
|
temple do
|
|
dialog open: true do
|
|
p do: "Are you sure?"
|
|
form method: "dialog" do
|
|
c &button/1, class: "border border-white" do
|
|
"Yes"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
### Named Slots
|
|
|
|
You can also define a "named" slot, which allows you to pass more than one set of dynamic content to your component.
|
|
|
|
We'll use a "card" example to illustrate this. This example is adapted from the [Surface documentation](https://surface-ui.org/slots) on slots.
|
|
|
|
#### Definition
|
|
|
|
```elixir
|
|
defmodule MyApp.Components do
|
|
import Temple
|
|
|
|
def card(assigns) do
|
|
temple do
|
|
div class: "card" do
|
|
header class: "card-header", style: "background-color: @f5f5f5" do
|
|
p class: "card-header-title" do
|
|
slot @header
|
|
end
|
|
end
|
|
|
|
div class: "card-content" do
|
|
div class: "content" do
|
|
slot @inner_block
|
|
end
|
|
end
|
|
|
|
footer class: "card-footer", style: "background-color: #f5f5f5" do
|
|
slot @footer
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### Usage
|
|
|
|
```elixir
|
|
def MyApp.CardExample do
|
|
import Temple
|
|
import MyApp.Components
|
|
|
|
def render(assigns) do
|
|
temple do
|
|
c &card/1 do
|
|
slot :header do
|
|
"A simple card component"
|
|
end
|
|
|
|
"This example demonstrates how to create components with multiple, named slots"
|
|
|
|
slot :footer do
|
|
a href: "#", class: "card-footer-item", do: "Footer Item 1"
|
|
a href: "#", class: "card-footer-item", do: "Footer Item 2"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
## Passing data to and through Slots
|
|
|
|
Sometimes it is necessary to pass data _into_ a slot (hereby known as *slot attributes*) from the call site and _from_ a component definition (hereby known as *slot arguments*) back to the call site. Dynamic slot attributes can be passed using the `:rest!` attribute in the same way you can with tag attributes.
|
|
|
|
Let's look at what a `table` component could look like. Here we observe we access an attribute in the slot in the header with `col.label`.
|
|
|
|
This example is taken from the HEEx documentation to demonstrate how you can build the same thing with Temple.
|
|
|
|
Note: Slot attributes can only be accessed on an individual slot, so if you define a single slot definition, you still need to loop through it to access it, as they are stored as a list.
|
|
|
|
#### Definition
|
|
|
|
```elixir
|
|
defmodule MyApp.Components do
|
|
import Temple
|
|
|
|
def table(assigns) do
|
|
temple do
|
|
table do
|
|
thead do
|
|
tr do
|
|
for col <- @col do
|
|
th do: col.label # 👈 accessing a slot attribute
|
|
end
|
|
end
|
|
end
|
|
|
|
tbody do
|
|
for row <- @rows do
|
|
tr do
|
|
for col <- @col do
|
|
td do
|
|
slot col, row
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### Usage
|
|
|
|
When we render the slot, we can pattern match on the data passed through the slot via the `:let` attribute.
|
|
|
|
```elixir
|
|
def MyApp.TableExample do
|
|
import Temple
|
|
import MyApp.Componens
|
|
|
|
def render(assigns) do
|
|
temple do
|
|
section do
|
|
h2 do: "Users"
|
|
|
|
c &table/1, rows: @users do
|
|
# 👇 defining the parameter for the slot argument
|
|
slot :col, let!: user, label: "Name" do # 👈 passing a slot attribute
|
|
user.name
|
|
end
|
|
|
|
slot :col, let!: user, label: "Address" do
|
|
user.address
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|