LiveView learning application

I thought I’d share a small project I’m working on to gain some familiarty with LiveView in a Phoenix app.

Github Repo

Deployment

It’s a little timer designed around interval exercises. Basically it allows you to build a list of sets of timed exercise intervals, which can be repeated, with periods of rest in between each interval.

You can browse the commit history to see how the project developed. I started with a bare bones Ecto-less Phoenix app, and I just added a simple LiveView module to handle form events for the exercises. I also added a timer with controls that would loop through the data from the form and update the assigns sent to the view, allowing the user to see a count down.

As a next step I added the concept of ‘sets’ so that intervals could be grouped and repeated, with rest in between.

This was all (template, helpers, timer etc) in a single module, so finally I refactored a bit by pull out the different parts into separate files, namely, a template and helper file. If I continue, I will probably extract some of the data structures from the LiveView logic into schemas.

Overall, I’m quite impressed with LiveView. It was really nice not to have to wire a bunch of boilerplate JS API integration in order to see a working UX. I ran into a lot fewer typical issues with JS syntax errors, browser refreshing and console logging, etc. That whole painful process was just skipped. Conversely, I immediately ran into the typical challenges in managing state for a dynamic UX, immutable data updates, etc. I think that’s a good thing, because it brought the real problems in my domain into clearer focus, and let me bring the power of Elixir to bear in solving them, which for me was quite an advantage of any flavor of JS with any number of libraries assisting.

I should note that this application is probably not the ideal candidate for LiveView, since it requires a very long-running BEAM process just to countdown a timer, something that would probably be better handling on the client side so it takes up fewer server resources. But it was something I actually wanted to use so it gave me some motivation.

Hope this is of some help to anyone else interested in getting started with LiveView. Let me know your thoughts.

8 Likes

As promised, in the latest version I’ve converted some of the data structures into Ecto schemas. For extra learning, I put off actually setting up a Repo, and used embedded_schema for everything. It was pretty cool how easy it that was to accomplish.

On the LiveView side, this separation naturally created some separation in the UI, between the phase when a timer is being created and when it is being viewed/used. I took advantage of this by separating the form UI into a separate component. I began by trying to keep the change as incremental as possible by using LiveComponent to ‘embed’ the form into the timer view like it worked before, but that proved really awkward with the timer event handling. Again, this probably mostly shows that the timer logic shouldn’t be in Elixir at all and in a real application I probably would have converted it to JS at this time (if I hadn’t started there, which I almost certainly would have). But another easy solution was just to add a new top level live view for the timer, which I wanted anyway so that there would be a unique URL users could visit to reuse the same timer. Since this still isn’t backed by a DB, I added a simple Agent to store uuids.

Next step will probably be added some style/UI sugar so it’s a bit more usable, then maybe think about more permanent storage options. Rather than a relational DB, one option I’m considering is just adding a JSON export/import.

2 Likes

Latest update: JS beep sound alerts via Hooks.

Another “MVP” feature for this toy app is the ability to play sounds to alert the user when an interval is over. Can’t really expect someone that’s in the middle of a strenuous exercise to keep their eyes trained on a screen. So after adding bulma and smoothing out the design a bit (CSS ugh) I started looking into how to implement some sort of audio component.

First armed with what I thought was a prefab npm package, I utterly failed to get things working. Although I was able to require the package in app.js just fine, the function wasn’t behaving as expected in my .leex template. At first I thought I was having problems with my webpacker config (one of the many reasons I want to learn LV in the first place), but actually I think the issue is with how LV injects its own scripts into the page.

After reading a bit, specifically @mindok’s very helpful posts here, it sounded like the easiest way to manage template scripts is to use LV’s hook API. And it turned out to be very easy. I just c/ped some SO code (as one does) and dropped it into the mounted hook for a span tag I would render when I wanted a beep.

Definitely unsure if it’s best practice to conditionally render tags with hooks assigned in this way. It’d definitely feels like a bit of an awkward work around, but seems to work pretty well. It’s also worth noting again that it is again the original bad design of implementing the timing code server-side that made this a problem in the first place. Good example of how not to architect a production app, but in this case that’s a good thing because the point is to learn and now I know about hooks (and it never hurts to get a reminder about the benefits of using the right tools either).

Link to deployed app is in the OP. If you have any experience using LV hooks and have an opinion about their proper use, lmk.

2 Likes

good fun with the beep - maybe just note ios and desktop safari is auto-muted:/

1 Like

Good tip! I’ll look into that.

1 Like

Deployed app has been moved to https://saitama.onrender.com/

HI there.

I recently added “beep” to a live view page and I also used the mounted callback.
However, I did not do it by calling beep() directly in the mounted callback. Instead, I used handleEvent like this:

In app.js:

Hooks.MyElement = {
  mounted() {
    this.handleEvent("play_beep", (payload) => {
      beep();
    });
  },
};

Then whenever my LiveView component handles an event with this kind of return:

In my_live.index.ex:

  def handle_info({SomeModule, [:something | [:happened]], _}, socket) do
    {:noreply, push_event(fetch(socket), "play_beep", %{})}
  end

So you could have the interval done in elixir instead. There are probably other ways to approach this, and my use case is not the same as yours (I don’t need an interval). Still, hopefully it helps.

Docs here: https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks

P.S. the client can push events too. Look at pushEvent in the docs.

P.P.S. You might have to upgrade your phoenix_live_view version.

1 Like