What additional frameworks do you use together with Phoenix LiveView?

Background

I have a vanilla Phoenix LiveView project. This project has grown and now it has come to a point where I need to add a progress bar to display the progress of a long operation.

Now, LiveView comes with a minimalist CSS framework called milligram. This framework has no support for progress bars afaik. It also has no JavaScript.

Problem

The way I see it, have 3 options:

  1. start adding custom JavaScript scripts
  2. add a JavaScript library (like jQuery)
  3. add a JavaScript/CSS library (no idea which one to use here)

I frown upon option 1, mainly because for this specific use case, it means I will be re-inventing the wheel. I am fairly sure that progress bars are an issue that was solved long ago.

In regards to options 2 and 3, I am a little bit lost. I don’t know where I would add a JavaScript framework to Phoenix, nor do I know if doing so is recommended, as it may cause issues with LiveView’s native JavaScript code.

Questions

What I am looking for is the general consensus when it comes to LiveView apps:

  • Do you add JavaScript libraries to your projects, or do you prefer going with JavaScript/CSS frameworks?
  • If so, how/where do you put these? In which folders/files?
  • What are the safest (the ones that cause less issues) libraries to use with LiveView?
  • Which library/framework would you recommend ?

Please do let me know !

3 Likes

I use tailwind for css and alpine js for javascript.

2 Likes

Where do you add them in the folder structure of the project?

1 Like

There are a lot of threads on Alpine and Tailwind on this forum. Tailwind even has a hex package these days AFAIK. I am trying to avoid Alpine these days in favour of the built in JS functionality.

Javascript libraries for things like drag and drop, sliders, charts etc are included using LiveView Hooks or Web Components. You either go the npm route or you put the JS straight in assets/js/vendor.

You can read about the web components at this blog post or his talk at last year’s ElixirConf.

3 Likes

If you dont know anything about Tailwind or Alpine, there is a semi steep learning curve. Its worth it though in my opinion.
You could also check out Bootstrap (Bootstrap 5 and Phoenix LiveView - Tutorials and screencasts for Elixir, Phoenix and LiveView)

2 Likes

This one I really try to avoid as much as I can…

2 Likes

Since phoenix 1.6 there’s only a soft convention remaining of using the assets folder. Generally phoenix doesn’t care how you do your assets pipeline as long as eventually the results are put into priv/static/… (and even that’s configurable). In dev.exs you can setup watchers for phoenix to start, which can be anything handling your assets pipeline.

So you could probably follow any (even non phoenix specific) tutorial for as long as you can make the resulting files be put into the correct folder.

For LV specifically you probably want to avoid things, which don’t work based on html updates alone, or you’ll need to write a lot of glue code / hooks. If you’re fine with writing hooks I’d still walk around most things using virtual DOM / frameworks, which want to own the DOM.

2 Likes

@LostKobrakai do you have any tutorial/recommendations in mind?
I don’t need something super evolved like Vue.js, for me, the simpler/dumier, the better.
I want to keep my focus on Phoenix LV, not on learning a new frontend framework.

If you know of a way to have progress-bars using only LV, then even better.

1 Like

There’s <progress /> which you can control from LV. Iirc it’s used here for uploads: Phoenix LiveView Uploads Deep Dive - YouTube

6 Likes

Edit: LOL that’s what happens when I don’t read to the bottom of the thread first :stuck_out_tongue:

FWIW, depending on your specific needs you may not even need any Javascript - consider trying out a progress element updated by LiveView:

4 Likes

For Bonfire we use Surface on top of LiveView, and also Tailwind (with DaisyUI) and Alpine.JS (along with a sprinkling of other JS for specific things like rich editors and maps). These is are the dev dependencies for managing and compiling them:

3 Likes

Nice, I didn’t know that. I always use a colored div inside another uncolored div with dynamic width set by lv.

1 Like

The Pragmatic Studio tutorial is solid and has good examples. Here’s a progress bar of sorts that they explain in the free sections of their course.

See a live demo of the progress bar-ish thing. I think you’ll see that this fits perfectly with the progress HTML element that @al2o3cr has suggested.

Looks like this btw:

As for JS in LiveView more generally, I personally still sprinkle in some Alpine here and there for some added development speed but you really don’t need it. Much more important is LiveView JS Commands that let you do common things, and LiveView Hooks as well as event listeners like window.addEventListener which along with JS.push and JS.dispatch let you create custom events (in bold because, well, if you really take in that section of the docs you’ll see why). So in this way LiveView has a kind of semi-prepared-foods counter of a JavaScript framework already baked in you can toss into the cooker with some enduring standards-based vanilla JS et voilà dinner is served.


Hmmm, did (does?) the (supposedly) late Anthony Balducci not bear a suspicious resemblance to Chris McCord? Birds aren’t real – Huh wait, maybe… is that why Aston always seems so into this longevity stuff? Balducci wanted to go on with life but needed a clean break. Because of his gourmet grocery skills he had access to all of the top stem cell scientists in the NYC area and was made “young again” – haven’t you noticed how McCord seems wise for his years? And just as Balducci invented gourmet groceries that were frameworks for delicious meals, he now under his new identity fires up a framework for gourmet webapps, which “just came to him” while he was munching gourmet chocolate-covered coffee beans JUST LIKE THOSE YOU CAN BUY AT BALDUCCI’S, he came up with Phoenix, Phoenix, Phoenix, THE Phoenix, The PHOENIX which rises again from the ashes ahem… omg the soup thickens and yes, Elixir is the alchemical agent, Phoenix an alchemical precursor to it, to… to the Elixir of life eternal!!! Call Liebnitz, Frege and Boole, this logic is unimpeachable, something is cooking in the state of Denmark!

1 Like

@malloryerik The main issue I have with that course, is that it uses an old version of Phoenix LV. The newer versions do not have tailwind, and come instead with milligram.

The difference is so massive, that I had to remake my entire project, from zero, to use milligram.
Unfortunately for me, it looks like absolutely no one in the planet is using milligram, so tutorials and guides using it with LV are close to non-existent.

I am using the HTML progress element (as suggested by @LostKobrakai and @al2o3cr ), which seems to work just fine for the time being. Now I need to polish it into something that wont make my user’s eyes bleed :smiley:

1 Like

Why rewrite your entire project for milligram? What prevents you to use tailwind or whatever with the last LiveView version ? I do not think that LiveView has anything to do with CSS, you should be able to use whatever you want.

3 Likes

Milligram afaik was chosen to be the least intrusive to the generated html, or in other words the easierst to remove from a phoenix project, while still given reasonable well styling out of the box. Personally I’d never leave this in any project becoming serious.

4 Likes

We’re using Tailwind and our own home-made library to help us at writing liveview components :

4 Likes

The were a bunch of NPM and Node issues that basically forced the re-write of the project. Tailwind and SASS were both the final nail in the coffin.

What would you replace it?
Any guides/tutorials you would recommend?

Has anyone used Stimulus JS in place of Alpine? From their Github repo it seems like Stimulus was designed specifically for adding javascript “sparkle” to MVC apps, Rails in Stimulus’ case but still.

Stimulus

After more research, I came to the conclusion that the progress HTML tag was not as customizable as I wanted.
So I decided to do some research for a CSS/HTML solution compatible with vanilla milligram.

I found none.

But my research was not fruitless. Based on all the things I had learned, I came up with this HTML/CSS progress bar that you can use with vanilla milligram CSS.

CSS:

:root {
    --darkpurple: #42389d;
    --lightpurple: #6961b3;
}

/* Progress bar */
.wrap-circles {
    display: grid;
    justify-content: center;
    align-items: center;
    padding: 30%;
}

.wrap-circles p {
    text-align: center;
    font-weight: bold;
    font-size: 1.1em;
}


.circle {
    position: relative;
    width: 9.375em;
    height: 9.375em;
    margin: auto;
    border-radius: 50%;
    background: var(--darkpurple);
    overflow: hidden;
}

.circle.per-0 {
    background-image: conic-gradient(var(--darkpurple) 0%, var(--lightpurple) 0);
}

.circle.per-1 {
    background-image: conic-gradient(var(--darkpurple) 1%, var(--lightpurple) 0);
}

.circle.per-2 {
    background-image: conic-gradient(var(--darkpurple) 2%, var(--lightpurple) 0);
}

.circle.per-3 {
    background-image: conic-gradient(var(--darkpurple) 3%, var(--lightpurple) 0);
}

.circle.per-4 {
    background-image: conic-gradient(var(--darkpurple) 4%, var(--lightpurple) 0);
}

.circle.per-5 {
    background-image: conic-gradient(var(--darkpurple) 5%, var(--lightpurple) 0);
}

.circle.per-6 {
    background-image: conic-gradient(var(--darkpurple) 6%, var(--lightpurple) 0);
}

.circle.per-7 {
    background-image: conic-gradient(var(--darkpurple) 7%, var(--lightpurple) 0);
}

.circle.per-8 {
    background-image: conic-gradient(var(--darkpurple) 8%, var(--lightpurple) 0);
}

.circle.per-9 {
    background-image: conic-gradient(var(--darkpurple) 9%, var(--lightpurple) 0);
}

.circle.per-10 {
    background-image: conic-gradient(var(--darkpurple) 10%, var(--lightpurple) 0);
}

.circle.per-11 {
    background-image: conic-gradient(var(--darkpurple) 11%, var(--lightpurple) 0);
}

.circle.per-12 {
    background-image: conic-gradient(var(--darkpurple) 12%, var(--lightpurple) 0);
}

.circle.per-13 {
    background-image: conic-gradient(var(--darkpurple) 13%, var(--lightpurple) 0);
}

.circle.per-14 {
    background-image: conic-gradient(var(--darkpurple) 14%, var(--lightpurple) 0);
}

.circle.per-15 {
    background-image: conic-gradient(var(--darkpurple) 15%, var(--lightpurple) 0);
}

.circle.per-16 {
    background-image: conic-gradient(var(--darkpurple) 16%, var(--lightpurple) 0);
}

.circle.per-17 {
    background-image: conic-gradient(var(--darkpurple) 17%, var(--lightpurple) 0);
}

.circle.per-18 {
    background-image: conic-gradient(var(--darkpurple) 18%, var(--lightpurple) 0);
}

.circle.per-19 {
    background-image: conic-gradient(var(--darkpurple) 19%, var(--lightpurple) 0);
}

.circle.per-20 {
    background-image: conic-gradient(var(--darkpurple) 20%, var(--lightpurple) 0);
}

.circle.per-21 {
    background-image: conic-gradient(var(--darkpurple) 21%, var(--lightpurple) 0);
}

.circle.per-22 {
    background-image: conic-gradient(var(--darkpurple) 22%, var(--lightpurple) 0);
}

.circle.per-23 {
    background-image: conic-gradient(var(--darkpurple) 23%, var(--lightpurple) 0);
}

.circle.per-24 {
    background-image: conic-gradient(var(--darkpurple) 24%, var(--lightpurple) 0);
}

.circle.per-25 {
    background-image: conic-gradient(var(--darkpurple) 25%, var(--lightpurple) 0);
}

.circle.per-26 {
    background-image: conic-gradient(var(--darkpurple) 26%, var(--lightpurple) 0);
}

.circle.per-27 {
    background-image: conic-gradient(var(--darkpurple) 27%, var(--lightpurple) 0);
}

.circle.per-28 {
    background-image: conic-gradient(var(--darkpurple) 28%, var(--lightpurple) 0);
}

.circle.per-29 {
    background-image: conic-gradient(var(--darkpurple) 29%, var(--lightpurple) 0);
}

.circle.per-30 {
    background-image: conic-gradient(var(--darkpurple) 30%, var(--lightpurple) 0);
}

.circle.per-31 {
    background-image: conic-gradient(var(--darkpurple) 31%, var(--lightpurple) 0);
}

.circle.per-32 {
    background-image: conic-gradient(var(--darkpurple) 32%, var(--lightpurple) 0);
}

.circle.per-33 {
    background-image: conic-gradient(var(--darkpurple) 33%, var(--lightpurple) 0);
}

.circle.per-34 {
    background-image: conic-gradient(var(--darkpurple) 34%, var(--lightpurple) 0);
}

.circle.per-35 {
    background-image: conic-gradient(var(--darkpurple) 35%, var(--lightpurple) 0);
}

.circle.per-36 {
    background-image: conic-gradient(var(--darkpurple) 36%, var(--lightpurple) 0);
}

.circle.per-37 {
    background-image: conic-gradient(var(--darkpurple) 37%, var(--lightpurple) 0);
}

.circle.per-38 {
    background-image: conic-gradient(var(--darkpurple) 38%, var(--lightpurple) 0);
}

.circle.per-39 {
    background-image: conic-gradient(var(--darkpurple) 39%, var(--lightpurple) 0);
}

.circle.per-40 {
    background-image: conic-gradient(var(--darkpurple) 40%, var(--lightpurple) 0);
}

.circle.per-41 {
    background-image: conic-gradient(var(--darkpurple) 41%, var(--lightpurple) 0);
}

.circle.per-42 {
    background-image: conic-gradient(var(--darkpurple) 42%, var(--lightpurple) 0);
}

.circle.per-43 {
    background-image: conic-gradient(var(--darkpurple) 43%, var(--lightpurple) 0);
}

.circle.per-44 {
    background-image: conic-gradient(var(--darkpurple) 44%, var(--lightpurple) 0);
}

.circle.per-45 {
    background-image: conic-gradient(var(--darkpurple) 45%, var(--lightpurple) 0);
}

.circle.per-46 {
    background-image: conic-gradient(var(--darkpurple) 46%, var(--lightpurple) 0);
}

.circle.per-47 {
    background-image: conic-gradient(var(--darkpurple) 47%, var(--lightpurple) 0);
}

.circle.per-48 {
    background-image: conic-gradient(var(--darkpurple) 48%, var(--lightpurple) 0);
}

.circle.per-49 {
    background-image: conic-gradient(var(--darkpurple) 49%, var(--lightpurple) 0);
}

.circle.per-50 {
    background-image: conic-gradient(var(--darkpurple) 50%, var(--lightpurple) 0);
}

.circle.per-51 {
    background-image: conic-gradient(var(--darkpurple) 51%, var(--lightpurple) 0);
}

.circle.per-52 {
    background-image: conic-gradient(var(--darkpurple) 52%, var(--lightpurple) 0);
}

.circle.per-53 {
    background-image: conic-gradient(var(--darkpurple) 53%, var(--lightpurple) 0);
}

.circle.per-54 {
    background-image: conic-gradient(var(--darkpurple) 54%, var(--lightpurple) 0);
}

.circle.per-55 {
    background-image: conic-gradient(var(--darkpurple) 55%, var(--lightpurple) 0);
}

.circle.per-56 {
    background-image: conic-gradient(var(--darkpurple) 56%, var(--lightpurple) 0);
}

.circle.per-57 {
    background-image: conic-gradient(var(--darkpurple) 57%, var(--lightpurple) 0);
}

.circle.per-58 {
    background-image: conic-gradient(var(--darkpurple) 58%, var(--lightpurple) 0);
}

.circle.per-59 {
    background-image: conic-gradient(var(--darkpurple) 59%, var(--lightpurple) 0);
}

.circle.per-60 {
    background-image: conic-gradient(var(--darkpurple) 60%, var(--lightpurple) 0);
}

.circle.per-61 {
    background-image: conic-gradient(var(--darkpurple) 61%, var(--lightpurple) 0);
}

.circle.per-62 {
    background-image: conic-gradient(var(--darkpurple) 62%, var(--lightpurple) 0);
}

.circle.per-63 {
    background-image: conic-gradient(var(--darkpurple) 63%, var(--lightpurple) 0);
}

.circle.per-64 {
    background-image: conic-gradient(var(--darkpurple) 64%, var(--lightpurple) 0);
}

.circle.per-65 {
    background-image: conic-gradient(var(--darkpurple) 65%, var(--lightpurple) 0);
}

.circle.per-66 {
    background-image: conic-gradient(var(--darkpurple) 66%, var(--lightpurple) 0);
}

.circle.per-67 {
    background-image: conic-gradient(var(--darkpurple) 67%, var(--lightpurple) 0);
}

.circle.per-68 {
    background-image: conic-gradient(var(--darkpurple) 68%, var(--lightpurple) 0);
}

.circle.per-69 {
    background-image: conic-gradient(var(--darkpurple) 69%, var(--lightpurple) 0);
}

.circle.per-70 {
    background-image: conic-gradient(var(--darkpurple) 70%, var(--lightpurple) 0);
}

.circle.per-71 {
    background-image: conic-gradient(var(--darkpurple) 71%, var(--lightpurple) 0);
}

.circle.per-71 {
    background-image: conic-gradient(var(--darkpurple) 71%, var(--lightpurple) 0);
}

.circle.per-72 {
    background-image: conic-gradient(var(--darkpurple) 72%, var(--lightpurple) 0);
}

.circle.per-73 {
    background-image: conic-gradient(var(--darkpurple) 73%, var(--lightpurple) 0);
}

.circle.per-74 {
    background-image: conic-gradient(var(--darkpurple) 74%, var(--lightpurple) 0);
}

.circle.per-75 {
    background-image: conic-gradient(var(--darkpurple) 75%, var(--lightpurple) 0);
}

.circle.per-76 {
    background-image: conic-gradient(var(--darkpurple) 76%, var(--lightpurple) 0);
}

.circle.per-77 {
    background-image: conic-gradient(var(--darkpurple) 77%, var(--lightpurple) 0);
}

.circle.per-78 {
    background-image: conic-gradient(var(--darkpurple) 78%, var(--lightpurple) 0);
}

.circle.per-79 {
    background-image: conic-gradient(var(--darkpurple) 79%, var(--lightpurple) 0);
}

.circle.per-80 {
    background-image: conic-gradient(var(--darkpurple) 80%, var(--lightpurple) 0);
}

.circle.per-81 {
    background-image: conic-gradient(var(--darkpurple) 81%, var(--lightpurple) 0);
}

.circle.per-82 {
    background-image: conic-gradient(var(--darkpurple) 82%, var(--lightpurple) 0);
}

.circle.per-83 {
    background-image: conic-gradient(var(--darkpurple) 83%, var(--lightpurple) 0);
}

.circle.per-84 {
    background-image: conic-gradient(var(--darkpurple) 84%, var(--lightpurple) 0);
}

.circle.per-85 {
    background-image: conic-gradient(var(--darkpurple) 85%, var(--lightpurple) 0);
}

.circle.per-86 {
    background-image: conic-gradient(var(--darkpurple) 86%, var(--lightpurple) 0);
}

.circle.per-87 {
    background-image: conic-gradient(var(--darkpurple) 87%, var(--lightpurple) 0);
}

.circle.per-88 {
    background-image: conic-gradient(var(--darkpurple) 88%, var(--lightpurple) 0);
}

.circle.per-89 {
    background-image: conic-gradient(var(--darkpurple) 89%, var(--lightpurple) 0);
}

.circle.per-90 {
    background-image: conic-gradient(var(--darkpurple) 90%, var(--lightpurple) 0);
}

.circle.per-91 {
    background-image: conic-gradient(var(--darkpurple) 91%, var(--lightpurple) 0);
}

.circle.per-92 {
    background-image: conic-gradient(var(--darkpurple) 92%, var(--lightpurple) 0);
}

.circle.per-93 {
    background-image: conic-gradient(var(--darkpurple) 93%, var(--lightpurple) 0);
}

.circle.per-94 {
    background-image: conic-gradient(var(--darkpurple) 94%, var(--lightpurple) 0);
}

.circle.per-95 {
    background-image: conic-gradient(var(--darkpurple) 95%, var(--lightpurple) 0);
}

.circle.per-96 {
    background-image: conic-gradient(var(--darkpurple) 96%, var(--lightpurple) 0);
}

.circle.per-97 {
    background-image: conic-gradient(var(--darkpurple) 97%, var(--lightpurple) 0);
}

.circle.per-98 {
    background-image: conic-gradient(var(--darkpurple) 98%, var(--lightpurple) 0);
}

.circle.per-99 {
    background-image: conic-gradient(var(--darkpurple) 99%, var(--lightpurple) 0);
}

.circle.per-100 {
    background-image: conic-gradient(var(--darkpurple) 100%, var(--lightpurple) 0);
}

.circle .inner {
    display: flex;
    justify-content: center;
    align-items: center;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 115px;
    height: 115px;
    background: white;
    border-radius: 50%;
    font-size: 1.85em;
    font-weight: 300;
    color: black;
}

HTML:

<div class="column column-100">
  <div class="wrap-circles">
    <p>Operation in progress</p>
    <div class={circle(@progress_bar_value)}>
      <div class="inner"><%= @progress_bar_value %>%</div>
    </div>
  </div>
</div>

You can improve upon it using SASS for any other variant, framework.
This works from 0 to 100 (integer numbers).

Hope it is of use to someone.

3 Likes