Radial progress bar using TailwinUI

Background

I have a small Phoenix 1.7 app where I am trying to add a radial progress bar, using the default TailwindUI components: Tailwind CSS Components - Tailwind UI

Unfortunately for me, I was only able to find normal progress bars:

Namely:

<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
  <div class="bg-blue-600 h-2.5 rounded-full" style="width: 45%"></div>
</div>

The only radial progress bar I found was one using DaisyUI:

However, I want to avoid installing anything extra, I really just want to use the default tailwindUI and tailwindCSS that come with Phoenix 1.7

Questions

  1. Does someone know how to create a a radial progress bar component using the default setup that comes with Phoenix LiveView 1.7?
  2. Am I missing some component that already does this?

you can do it like this: Ultimate Guide: Build A Circular Progress Bar With Tailwind CSS

I did try something similar before, unfortunately, I get the following error in my heex file:

code:

<div
      x-data="scrollProgress"
      class="fixed inline-flex items-center justify-center overflow-hidden rounded-full bottom-5 left-5"
    >
      <!-- Building a Progress Ring: https://css-tricks.com/building-progress-ring-quickly/ -->
      <svg class="w-20 h-20">
        <circle
          class="text-gray-300"
          stroke-width="5"
          stroke="currentColor"
          fill="transparent"
          r="30"
          cx="40"
          cy="40"
        />
        <circle
          class="text-blue-600"
          stroke-width="5"
          :stroke-dasharray="circumference"
          :stroke-dashoffset="circumference - percent / 100 * circumference"
          stroke-linecap="round"
          stroke="currentColor"
          fill="transparent"
          r="30"
          cx="40"
          cy="40"
        />
      </svg>
      <span class="absolute text-xl text-blue-700" x-text="`${percent}%`"></span>
    </div>

error

== Compilation error in file lib/web_interface/live/activate_live.ex ==
** (Phoenix.LiveView.Tokenizer.ParseError) lib/web_interface/live/activate_live.html.heex:22:11: unsupported attribute ":stroke-dasharray" in tags
   |
19 |         <circle
20 |           class="text-blue-600"
21 |           stroke-width="5"
22 |           :stroke-dasharray="circumference"
   |           ^
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/tag_engine.ex:1383: Phoenix.LiveView.TagEngine.raise_syntax_error!/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/tag_engine.ex:656: Phoenix.LiveView.TagEngine.handle_token/2
    (elixir 1.15.4) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/tag_engine.ex:182: Phoenix.LiveView.TagEngine.handle_body/1
    (phoenix_live_view 0.18.18) expanding macro: Phoenix.LiveView.HTMLEngine.compile/1
    lib/web_interface/live/activate_live.html.heex:1: WebInterface.ActivateLive.render/1
    (phoenix_live_view 0.18.18) c:/Users/pedro/Documents/market_manager/apps/web_interface/lib/web_interface/live/activate_live.ex:1: Phoenix.LiveView.Renderer.__before_compile__/1

Use of invalid html attributes is not allowed in heex. You can use the longer x-bind:… format.

That’s fair, but thus far I was not able to find an example that does not use such attributes.

What is it and what does it do? I was not able to find anything in the docs about it:

https://hexdocs.pm/phoenix_live_view/search.html?q=x-bind

I mean sure if you’re looking at the LV docs and not the Alpine docs. :attr is a short form for x-bind:attr in alpine.

1 Like

I am unsure on how the Tailwind package and Apline.js are connected:

Clearly i am looking in the wrong place. Do Phoenix 1.7 applications now also use AlpineJS by default?

No. Not sure where you got that html you posted, but it’s using alpine.

1 Like

apologies for posting a link that depends on alpine… I believe it’s best avoided nowadays with all the liveviewjs improvements…

think you need to go with this underlying example Building a Progress Ring, Quickly | CSS-Tricks - CSS-Tricks - and then maybe take some tailwind styling from the alpine dependent one…

1 Like

No need to apologize !
Even though the link and code you shared used alpine, it was still very educative. I was able to analyze the code and create a component that does exactly what I want. Its also quite pretty, so I am very proud. This code uses only Phoniex LV vanilla, no extra CSS no extra anything !

core_components:

  @doc """
  Renders a progress bar for an ongoing operation.

  ## Examples

      <.progress_bar hidden=false progress=15 />
      <.progress_bar hidden=false progress=20 message="Activating system ..." />
      <.progress_bar hidden=false class="cool-bar" />
  """
  attr :hidden, :boolean, default: true, doc: "whether or not to show the progress bar"
  attr :progress, :integer, default: 0, doc: "the current progress of the bar"
  attr :message, :string, default: "Operation in progress ...", doc: "the message to show while the bar is progressing"
  attr :class, :string, default: nil

  def progress_bar(assigns) do

    assigns = assign(assigns, :circumference, 2 * 22 / 7 * 120)
    assigns = assign(assigns, :offset, assigns.circumference - assigns.progress / 100 * assigns.circumference)

    ~H"""
    <div class={@class} hidden={@hidden}>
      <div class="flex items-center justify-center">
        <p class="text-lg font-semibold"><%= @message %></p>
      </div>

      <div class="flex items-center justify-center">
        <svg class="transform -rotate-90 w-72 h-72">
            <circle cx="145" cy="145" r="120" stroke-width="30" fill="transparent" class="stroke-gray-700" />

            <circle cx="145" cy="145" r="120" stroke-width="30" fill="transparent"
                stroke-dasharray={@circumference}
                stroke-dashoffset={@offset}
                class="stroke-indigo-500" />
        </svg>
        <span class="absolute text-5xl stroke-black"><%= @progress %></span>
      </div>

    </div>
    """
  end

Usage in my_app_live.heex.html:

<div class="min-h-full max-w-full mx-auto py-6 sm:px-6 lg:px-8">
  <.progress_bar hidden={false} progress={40} message="Activating system ..." />
</div>

Will produce the following:

Possible improvements

Currently i am not sure if there is a more elixir way of updating the assigns:

assigns = assign(assigns, :circumference, 2 * 22 / 7 * 120)
assigns = assign(assigns, :offset, assigns.circumference - assigns.progress / 100 * assigns.circumference)

But other than that, I am pretty happy with it.

Additional info

For those of you curious, you can also check this other solution someone else provided in SO:

I hope this helps!

4 Likes

You can compute this circumference outside of the function since it’s never changing.

Eg

# 2 * 22 / 7 * 120
@circumference 754.285714285714286

(not sure how important precision is for that variable)

2 Likes