Subscribe to access all episodes. View plans →

#145: Petal Components

Published May 31, 2022

Elixir 1.12

Phoenix 1.6

Petal Components

Petal Pro

Follow along with the episode starter on GitHub


The word ‘Petal’ in the relatively new Petal Stack, is an acronym that stands for Phoenix - Elixir - Tailwind CSS - Alpine.js - and LiveView. And Petal Components is an open source component library that supercharges this stack. Written in HEEX and styled with Tailwind, Petal Components make it easy to build beautiful web applications by providing pre-built components you can easily drop into your application and customize. In this episode, we’ll get an introduction to Petal Components.

If you’re starting a new Phoenix application and want to get started quickly with the PETAL stack using Petal Components, I’d suggest taking a look at the Petal Boilerplate project here. It comes pre-installed with Tailwind CSS, Alpine JS, as well as the Petal Components Library. However, in this episode, we won’t be starting a new project. Instead, we’ll update this Phoenix LiveView project to use Petal Components. Let’s get started. We’ll go to Hex and grab the latest version of petal_components. Then we’ll open our Mixfile and add it as a dependency. Because Petal Components use Tailwind CSS, we’ll add the tailwind package, setting the runtime to development.

mix.exs

...

defp deps do
  ...
  {:petal_components, "~> 0.16"},
  {:tailwind, "~> 0.1", runtime: Mix.env() == :dev},
  ...
end

...

Then let’s go to the command line and get our new dependencies.

$ mix deps.get
...
New:
  petal_components 0.16.0
  tailwind 0.1.5

With those downloaded let’s configure tailwind. We’ll go to our config.exs and paste the config into our tailwind config. For our project, we’ll be using Tailwind version 3.

config/config.exs

...

config :tailwind,
  version: "3.0.12",
  default: [
  args: ~w(
    --config=tailwind.config.js
    --input=css/app.css
    --output=../priv/static/assets/app.css
  ),
    cd: Path.expand("../assets", __DIR__)
  ]
  
...

Tailwind has a watcher that will scan your project’s HTML code for classes, which it will then combine and include in your final CSS file. While in development, we need to allow Tailwind to do this on the fly. Let’s open our dev.exs find the watchers list in our Endpoint config and update it.

config/dev.exs

...
config :teacher, TeacherWeb.Endpoint,
  ...
  watchers: [ 
    # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
    esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
    tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
  ]
  ...

Now we can install tailwind.

$ mix tailwind.install
...

Great, it was installed successfully! Because we’re using Tailwind, we won’t need the default styles.css stylesheet, so we can go ahead and remove that.

Then let’s open our app.css and when we installed tailwind, it added imports for tailwind base, components, and utilities. Since those are now being imported, we can go ahead and remove all of our old CSS replacing them with a couple LiveView specific styles.

assets/css/app.css

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

/* LiveView specific classes for your customization */
.phx-no-feedback.invalid-feedback,
.phx-no-feedback .invalid-feedback {
  display: none;
}

.phx-click-loading {
  opacity: 0.5;
  transition: opacity 1s ease-out;
}

.phx-disconnected{
  cursor: wait;
}
.phx-disconnected *{
  pointer-events: none;
}

With that updated let’s go back to our Mixfile and update the assets.deploy alias to include the Tailwind CSS build step. Now our CSS will get built when we deploy our app.

mix.exs

...

defp aliases do
  [
    ...
    "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
    ...
  ]
end

...

Now when we installed Tailwind, it created a tailwind config, let’s open that.

We’ll need to update it to include Petal Components. Here we can also set any primary and secondary colors we want to use. Let’s use tailwinds colors here - blue for our primary color and pink for our secondary. Because our application has a form we want to style, we’ll want to ensure the tailwind forms plugin is included.

assets/tailwind.config.js

const colors = require("tailwindcss/colors");

module.exports = {
  content: [
    './js/**/*.js',
    '../lib/*_web.ex',
    '../lib/*_web/**/*.*ex',
    '../deps/petal_components/**/*.*ex'
  ],
  theme: {
    extend: {
      colors: {
        primary: colors.blue,
        secondary: colors.pink
      }
    },
  },
  plugins: [
    require('@tailwindcss/forms')
  ]
}

Now let’s open the teacher_web.ex module and in the view_helpers let’s include use PetalComponents, which makes it easier to call components in our templates.

lib/teacher_web.ex

...

defp view_helpers do
  quote do

    ...

    use PetalComponents
  end
end

...

With those changes let’s go to the command line and start up our server.

$ mix phx.server
...

When we do we get an error - the modal function is being imported from both PetalComponents.Modal and TeacherWeb.LiveHelpers. This is because PetalComponents comes with its own modal that we’ll want to use.

So open the LiveHelpers module and we’ll remove the modal function.

lib/teacher_web/live/live_helpers.ex

defmodule TeacherWeb.LiveHelpers do
end

With that update, let’s try to start the server again. And great it starts up.

$ mix phx.server
...

If we go check our albums page it’s not much to look at without any styling, so let’s update that now. We’ll go to our live.html.heex layout and in it, we can see there’s a container along with our info and error flash messages. If we open the Petal Components docs, we see there’s an alert component as well as a container component. Each component shows you how to use it in a template, as well as what properties are available to customize it.

Let’s update our layout to use these components. We won’t go through all component properties in this episode, but let’s walk through our changes here. We have this container component, which has a class property where we can give the component any CSS classes. Here we’re including the my-10 to add a margin to the top and bottom of the component. Then we’re nesting two alert components in our container component. Our first alert is the info alert, with the color info, a class, a label, and then the phx-click binding and phx-value-key attribute. Then a second alert for error messages.

Template path: lib/teacher_web/templates/layout/live.html.heex

<.container class="my-10">
  <.alert
    color="info"
    class="mb-5"
    label={live_flash(@flash, :info)}
    phx-click="lv:clear-flash"
    phx-value-key="info"
  />

  <.alert
    color="danger"
    class="mb-5"
    label={live_flash(@flash, :error)}
    phx-click="lv:clear-flash"
    phx-value-key="error"
  />

  <%= @inner_content %>
</.container>

Then let’s go to our album’s index.html.heex template and we’ll update our page heading to use the h2 component and the link to our new album modal to use the button component. All our albums are displayed below in a table. Luckily Petal Components has components just for tables - let’s go back to our template and update it to use these. For each album we have our album actions, to edit and delete an album. Let’s style these with the button component. We’ll update our modal component to work with Petal, replacing the return_to property with the title. With that our index template has been updated to use components.

Template path: lib/teacher_web/live/album_live/index.html.heex

<div class="mb-8 sm:flex sm:justify-between sm:items-center">
  <div class="mb-4 sm:mb-0">
    <.h2 class="!mb-0">Listing Albums</.h2>
  </div>

  <div>
    <.button link_type="live_patch" label="New Album" to={Routes.album_index_path(@socket, :new)} />
  </div>
</div>

<%= if @live_action in [:new, :edit] do %>
  <.modal title={@page_title}>
    <.live_component
      module={TeacherWeb.AlbumLive.FormComponent}
      id={@album.id || :new}
      title={@page_title}
      action={@live_action}
      album={@album}
      return_to={Routes.album_index_path(@socket, :index)}
    />
  </.modal>
<% end %>

<.table>
  <thead>
    <.tr>
      <.th>Title</.th>
      <.th>Artist</.th>
      <.th>Summary</.th>
      <.th>Year</.th>

      <.th></.th>
    </.tr>
  </thead>
  <tbody id="albums">
    <%= for album <- @albums do %>
      <.tr id={"album-#{album.id}"}>
        <.td><%= album.title %></.td>
        <.td><%= album.artist %></.td>
        <.td><%= album.summary %></.td>
        <.td><%= album.year %></.td>
        <.td class="text-right whitespace-nowrap">
          <.button
            color="white"
            variant="outline"
            size="xs"
            link_type="live_patch"
            label="Edit"
            to={Routes.album_index_path(@socket, :edit, album)}
          />
          <.button
            color="danger"
            variant="outline"
            link_type="a"
            to="#"
            size="xs"
            label="Delete"
            phx-click="delete"
            phx-value-id={album.id}
            data-confirm="Are you sure?"
          />
        </.td>
      </.tr>
    <% end %>
  </tbody>
</.table>

Now we just need to go to our AlbumLive.Index LiveView module and the Petal Component modal expects us to handle the “close_modal” action, so let’s add a handle_event callback for that, pattern matching on “close_modal”. When the modal is closed we’ll send the user back to the album index page.

lib/teacher_web/live/album_live/index.ex

...

@impl true
def handle_event("close_modal", _, socket) do
  {:noreply, push_patch(socket, to: Routes.album_index_path(socket, :index))}
end

...

Now let’s go back to the browser and great we see our changes! Our app looks a lot better! But if we edit an album, the modal works, but the form could look a lot better. Let’s update that now. We’ll go to the form_component.html.heex template and it’s already using a form component, so we’ll just need to update it to use the form_field and button components.

Template path: lib/teacher_web/live/album_live/form_component.html.heex

<div>
  <.form
    let={f}
    for={@changeset}
    id="album-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

    <.form_field type="text_input" form={f} field={:title} />
    <.form_field type="text_input" form={f} field={:artist} />
    <.form_field type="textarea" form={f} field={:summary} />
    <.form_field type="number_input" form={f} field={:year} />

    <div class="flex justify-end">
      <.button
        type="submit"
        phx_disable_with="Saving..."
        label="Save"
      />
    </div>
  </.form>
</div>

Let’s go back to the browser and our form modal looks a lot better. With very little effort we were able to get our app updated to use Tailwind and Petal Components.

Now if you want to save even more development time there’s a paid version of Petal, which helps support the continued development of Petal Components and comes with some pretty great features like authentication, social logins, layouts, and HTML email components all built-in. Petal Pro also has modified versions of both the phx.gen.html and phx.gen.live scaffold commands to save you even more time.

© 2024 HEXMONSTER LLC