Triggering repeatable animations from the server in LiveView & Elixir

A information for Elixir builders (and why you may wish to)

Photograph by Ján Jakub Naništa on Unsplash

Phoenix LiveView is my favourite approach to create net functions nowadays — the PETAL stack is effortlessly enjoyable to make use of and can (for my part) quickly be a mainstream stack of alternative for net builders seeking to create real-time functions with out having to fret in regards to the present-day client-side worries that accompany as we speak’s hottest instruments of alternative.

Chris McCord eloquently describes the ability you acquire alongside the god-send of with the ability to virtually overlook about 90% of the client-side code in his Fly.io blog post of how LiveView came to be — extremely beneficial studying.

I’ve constructed a number of LiveView functions (and contemplate myself fortunate sufficient to have the ability to use it at work) together with:

  • 6words.xyz — a wordle impressed net sport
  • niceice.io — a SaaS service for capturing suggestions out of your customers as simply as attainable
Petal Stack?
Phoenix, Elixir, Tailwind, Alpine & LiveView.

LiveView is nice at constructing real-time functions — once I say real-time I imply immediately reactive throughout all customers at present looking the positioning.

I’m at present engaged on a platform to allow the sharing of user-created fictional tales on-line once I managed to stumble throughout the premise of why this tutorial is required while making an attempt to carry a brand new characteristic to life.

As a part of my new platform customers can submit tales, chapters and produce content material for customers to learn. Wanting so as to add some extra pizzazz to my utility — I figured it’d be cool to have a Dwell World Statistics element on the entrance web page of my website so customers might see how energetic the positioning was in real-time!

def mount(_params, _session, socket) do
socket =
socket
|> assign(:story_count, total_story_count())
|> assign(:word_count, total_word_count())
|> assign(:chapter_count, total_chapter_count())
:okay, socket
finish

def render(assigns) do
~H"""
<p><span id="stories-count"><%= @stories_count %></span> tales submitted</p>
<p><span id="chapters-count"><%= @chapters_count %></span> chapters printed</p>
<p><span id="word-count"><%= @word_count %></span> phrases written</p>
"""
finish

So that is cool — I’ve a element that updates every time a consumer accesses the web page, however that’s not very real-time is it?

Presently the knowledge is just up to date on mount — let’s change that with the magic of the Phoenix.PubSub module that ships with Phoenix by default.

To take action we have to create a subject for our PubSub to subscribe to (and allow PubSub in my functions supervisor tree):

# MyApplication.Submissions  @matter examine(__MODULE__)  def subscribe do
PubSub.subscribe(MyApplication.PubSub, @matter)
finish
defp notify_subscribers(:okay, consequence, occasion) do
PubSub.broadcast(MyApplication.PubSub, @matter, __MODULE__, occasion, consequence)
:okay, consequence
finish

I can now use this notify_subscribers/2 perform every time I wish to alert one thing subscribed to an replace I am desirous about broadcasting like so:

def update_story(%Story = story, attrs) do
story
|> Story.update_changeset(attrs)
|> Repo.replace()
|> notify_subscribers([:story, :updated]) # ⬅️ the attention-grabbing bit
finish

Then we have to make sure that when our live_component mounts and connects to the WebSocket, it subscribes to the subject.

def mount(_params, _session, socket) do
if linked?(socket) do
MyApplication.Submissions.subscribe()
finish

# and add an occasion listener to make sure our LiveView is aware of to react when it receives a message from our subscribed matter
def handle_info(MyApplication.Submissions, [:story, _], _, socket) do
socket =
socket
|> assign(:story_count, total_story_count())

:noreply, socket
finish
finish

Now once we replace our tales — discover I’m ignoring the second atom so I’ll name my new project every time any story change occurs — our front-end will replace for all customers!

We’ve got a problem although.

There’s no animation! This may be fairly jarring for customers so let’s get onto the true level of this submit; triggering animations from the backend to essentially delight our readers.

For my instance, I’m utilizing Tailwind (yay, PETAL 🌸 stack) however this can work with any CSS class as long as the animation and keyframe attributes have been set appropriately.

First, let’s outline our animation in CSS (in our tailwind.config.js):

theme: {
prolong:
keyframes:
wiggle:
'0%': rework: 'translateY(0px) scale(1,1)' ,
'25%': rework: 'translateY(-4px) scale(1.05,1.05)', background: 'aquamarine' ,
'100%': rework: 'translateY(0px) scale(1,1)' ,

,
animation:
wiggle: 'wiggle 0.5s linear 1 forwards',

,
},

All we’re doing is making it leap a bit of; let’s press on with really integrating this.

At first, I believed I might merely use the LiveView.JS library so as to add a category to the ingredient in query from the backend and move it to the entrance finish like so:

def do_animation do
JS.add_class("animate-wiggle", to: "#word-count")
finish

Consider I used to be additionally testing this utilizing a easy button with a click on handler phx-click=do_animation for ease of not having to really set off backend occasions every time – so I used to be utilizing phx-click…

This added the category and the animation did a bit of leap — nice.

I clicked it once more and nothing occurred, not nice.

It’s because the category lived on the ingredient so including it once more meant nothing would occur — my animation wasn’t repeatable. Whoops.

Let’s take away the category after the category has been added.

def do_animation do
JS.add_class("animate-wiggle", to: "#word-count")
ship(self(), JS.remove_class("animate-wiggle", to: "#word-count"))
finish

This didn’t work as a result of the category was being eliminated because it was being added. I might’ve added a timeout however that appears far too hacky.

def animate_wiggle(element_id) do
JS.transition(%JS, "animate-wiggle", to: element_id, time: 500)
finish

JS.transition/2 to the rescue! The LiveView crew constructed a particular perform for triggering transitions repeatedly.

However there was a problem — LiveView.JS features merely generate JavaScript, so that they need to be rendered within the web page!

So what can we do?

RTFM after all! Onwards!

I needed to push the occasion to the browser in order that some JavaScript might execute the wiggle animation for me — so the circulate goes like this:

  • PubSub broadcasts occasion
  • Every subscribed LiveView course of listens to that occasion and triggers an occasion to their purchasers
  • The consumer has a JavaScript occasion listener to choose up on phx occasions to react to them
  • JavaScript fires a name to the consumer to set off the animation
  • JS.transition/2 fires
  • Wiggle wiggle

Let’s add the JS occasion listener in our App.js:

window.addEventListener(`phx:wiggle`, (e) => 
let el = doc.getElementById(e.element.id)
if(el)
liveSocket.execJS(el, el.getAttribute("data-wiggle"))

)

Let’s replace our occasion handler on when to push the occasion to the consumer:

def handle_info(MyApplication.Submissions, [:story, _], _, socket) do
socket =
socket
|> assign(:story_count, total_story_count())
|> push_event("wiggle", %id: "stories-count") # ⬅️ the brand new addition

:noreply, socket
finish

We additionally want to make sure we add an id and an information attribute to the ingredient we wish to wiggle so our JavaScript can discover it and know what to do with it:

<p><span id="stories-count" data-wiggle=animate_wiggle("#stories-count")><%= @stories_count %></span> tales submitted</p>

What you may’t see is I’ve one other window triggering the aforementioned occasions.

We’re executed!

We’ve efficiently triggered repeatable front-end animations from reside occasions coming from different customers of our utility with minimal code (and actually 6 traces of JavaScript).

I really like LiveView and I hope this submit has given you a taste of why.

Follow me on Twitter for extra LiveView, Elixir, and common programming tutorials and ideas.

More Posts