In our videos we have learned that phoenix components are nothing but functions that take in an assigns
parameter and return heex
(HTML + EEX (embedded elixir)).
In our templates we spawn a phoenix component like any other html element:
<.button color="alternative">Click Me</.button>
Our button
is implemented in our custom ui library, in its own file. I suggest you copy/create this component in your folder.
defmodule AppWeb.Components.UI.Button do
use Phoenix.Component
@doc """
Renders a button.
## Examples
<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
"""
attr :color, :string, default: "default", values: ~w(default alternative)
attr :type, :string, default: "button"
attr :class, :string, default: nil
attr :rest, :global, include: ~w(disabled form name value)
slot :inner_block, required: true
def button(assigns) do
~H"""
<button
type={@type}
class={[
"py-2.5 px-5 me-2 mb-2 focus:ring-4 font-medium text-sm rounded-lg focus:outline-none",
@color == "default" &&
"text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800",
@color == "alternative" &&
"text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700",
@class
]}
{@rest}
>
{render_slot(@inner_block)}
</button>
"""
end
end
To make our component load we have created the UI
module:
defmodule AppWeb.Components.UI do
@moduledoc """
Provides resuable UI components.
Basic UI components are derived from [Flowbite](https://flowbite.com/).
Icons are provided by [Flowbite](https://flowbite.com/icons/). See `icon/1` for usage.
"""
defmacro __using__(_) do
quote do
import AppWeb.Components.UI.{
Button
}
end
end
end
The __using__
macro logic allows us insert use AppWeb.Components.UI
in any module. This will make our entire UI Component library accessible. We have done that in AppWeb.ex
:
defmodule AppWeb do
# ...
defp html_helpers do
quote do
use AppWeb.Components.UI
# ...
end
end
# ...
end
Finally we have removed the obsolete button
function from AppWeb.CoreComponents
.
Objective 1 - Building a UI Component Library
Display a <.button>
element on any of your pages using your brand new UI Component Library.
We are going to expand and build upon this library in the future so that is an important milestone.
Objective 2 - Implementing Badges
To practice core compentents further our main task today is to implement the default Flowbite badges and convert them to reusable phoenix components.
After you have added a AppWeb.Components.UI.Badge
module the following should all display correct badges:
<.badge color="default">Default</.badge>
<.badge color="dark">dark</.badge>
<.badge color="red">red</.badge>
<.badge color="green">green</.badge>
<.badge color="yellow">yellow</.badge>
<.badge color="indigo">indigo</.badge>
<.badge color="purple">purple</.badge>
<.badge color="pink">pink</.badge>
Make sure to display your badges on one of your pages.
To get credit your badges must look like the ones on the documentation:
Objective 3 - Large Badges
To make our badges more versatile we will also implement Flowbites badge sizing attribute:
<.badge color="default" large>Default</.badge>
Notice the large attribute doesn't seem to have a value. In fact it has. Whenever we put an attribute without ="string"
or ={elixir code}
it defaults to true. The following two are the same:
<.badge large>Default</.badge>
<.badge large={true}>Default</.badge>
When you define your attr as a boolean make sure to set the default value to false, as to not accidentally make all badges large.
Add one or two large badges to your page to demonstrate you have completed that part.
Objective: Testing Badges
The final points will be earned if you can demonstrate 100% test coverage on your coverage report for your badges module. Testing phoenix components is straightforward but watch the video to understand why we will need to change formatter.exs
to this:
[
import_deps: [:ecto, :ecto_sql, :phoenix],
subdirectories: ["priv/*/migrations"],
plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{config,lib}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
]
To get credit for this objective running this command should pass all tests:
mix test test/app_web/components/ui/badge_test.exs
As a reminder, here is my testfile that I had for my phoenix buttons:
defmodule AppWeb.Components.UI.ButtonTest do
use AppWeb.ComponentCase
import AppWeb.Components.UI.Button
test "default button" do
assigns = %{}
html =
rendered_to_string(
~H"""
<.button class="custom-class" id="ID">Click</.button>
"""
)
assert html =~ "id=\"ID"
assert html =~ "type=\"button"
assert html =~ "custom-class"
assert html =~ "text-white bg-blue-700 hover:bg-blue-800"
assert html =~ "py-2.5 px-5 me-2 mb-2"
assert html =~ "Click"
assert html =~ "</button>"
end
test "default button with custom classes" do
assigns = %{}
html =
rendered_to_string(
~H"""
<.button color="default" class="custom-class">Click</.button>
"""
)
assert html =~ "type=\"button"
assert html =~ "text-white bg-blue-700 hover:bg-blue-800"
assert html =~ "Click"
end
test "alternative submit button" do
assigns = %{}
html =
rendered_to_string(
~H"""
<.button type="submit" color="alternative">Click</.button>
"""
)
assert html =~ "type=\"submit"
assert html =~ "py-2.5 px-5 me-2 mb-2"
assert html =~ "text-gray-900 bg-white border border-gray-200"
assert html =~ "</button>"
end
end
Here is the ComponentCase
module I added:
defmodule AppWeb.ComponentCase do
@moduledoc """
This module defines the test case to be used by
tests that use simple function components.
"""
use ExUnit.CaseTemplate
using do
quote do
import Phoenix.Component
import Phoenix.LiveViewTest
end
end
end