Issue 06
Jul 22, 2022 · 7 minute read
Phoenix LiveView is my favourite way to create web applications these days - the PETAL stack is effortlessly fun to use and will (in my opinion) soon be a mainstream stack of choice for web developers looking to create real-time applications without having to worry about the present-day client-side worries that acompany todays most popular tools of choice.
Chris McCord eloquently describes the power you gain alongside the god-send of being able to practically forget about 90% of the client-side code in his Fly.io blog post of how LiveView came to be - highly recommended reading.
I've built a few LiveView applications (and consider myself lucky enough to be able to use it at work) including:
If you want to hear more about any of these projects, send me a message on Twitter and I'll be happy to dive into more about how they're built.
Phoenix, Elixir, Tailwind, Alpine & LiveView.
LiveView is great at building real-time applications - when I say real-time I mean instantly reactive across all users currently browsing the site.
I'm currently working on a platform to enable the sharing of user-created fictional stories online when I managed to stumble across the basis of why this tutorial is needed whilst trying to bring a new feature to life.
As part of my new platform users can submit stories, chapters and produce content for users to read. Wanting to add some more pizzazz to my application - I figured it'd be cool to have a Live Global Statistics component on the front-page of my site so users could see how active the site 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())
{:ok, socket}
end
def render(assigns) do
~H"""
<p><span id="stories-count"><%= @stories_count %></span> stories submitted</p>
<p><span id="chapters-count"><%= @chapters_count %></span> chapters published</p>
<p><span id="word-count"><%= @word_count %></span> words written</p>
"""
end
So this is cool - I have a component that updates whenever a user access the page, but that's not very real-time is it?
Presently the information is only updated on mount
- lets change that with the magic of the Phoenix.PubSub
module that ships with Phoenix by default.
To do so we need to create a topic for our PubSub to subscribe to (and enable PubSub in my applications supervisor tree):
# MyApplication.Submissions
@topic inspect(__MODULE__)
def subscribe do
PubSub.subscribe(MyApplication.PubSub, @topic)
end
defp notify_subscribers({:ok, result}, event) do
PubSub.broadcast(MyApplication.PubSub, @topic, {__MODULE__, event, result})
{:ok, result}
end
I can now use this notify_subscribers/2
function whenever I want to alert something subscribed to an update I'm interested in broadcasting like so:
def update_story(%Story{} = story, attrs) do
story
|> Story.update_changeset(attrs)
|> Repo.update()
|> notify_subscribers([:story, :updated]) # ⬅️ the interesting bit
end
Then we need to ensure that when our live_component
mounts and connects to the websocket, it subscribes to the topic.
def mount(_params, _session, socket) do
if connected?(socket) do
MyApplication.Submissions.subscribe()
end
# and add an event listener to ensure our LiveView knows to react when it receives a message from our subscribed topic
def handle_info({MyApplication.Submissions, [:story, _], _}, socket) do
socket =
socket
|> assign(:story_count, total_story_count())
{:noreply, socket}
end
end
Now when we update our stories - notice I'm ignoring the second atom so I'll call my new assignment whenever any story change happens - our front-end will update for all users!
We have an issue though.
There's no animation! This can be pretty jarring for users so let's get onto the real point of this post; triggering animations from the backend to really delight our readers.
For my example I'm using Tailwind (yay, PETAL 🌸 stack) but this will work with any CSS class so long as the animation and keyframe attributes have been set appropriately.
First let's define our animation in CSS (in our tailwind.config.js
):
theme: {
extend: {
keyframes: {
wiggle: {
'0%': { transform: 'translateY(0px) scale(1,1)' },
'25%': { transform: 'translateY(-4px) scale(1.05,1.05)', background: 'aquamarine' },
'100%': { transform: 'translateY(0px) scale(1,1)' },
}
},
animation: {
wiggle: 'wiggle 0.5s linear 1 forwards',
}
},
},
All we're doing is making it jump a little; let's press on with actually integrating this.
At first I believed I could simply use the LiveView.JS
library to add a class to the element in question from the backend and pass it to the front end like so:
def do_animation do
JS.add_class("animate-wiggle", to: "#word-count")
end
Keep in mind I was also testing this using a simple button with a click handler phx-click={do_animation}
for ease of not having to actually trigger backend events each time - so I was using phx-click... 👀
This added the class and the animation did a little jump - great.
I clicked it again and nothing happened, not great.
This is because the class lived on the element so adding it again meant nothing would happen - my animation wasn't repeatable. Whoops.
Let's remove the class after the class has been added.
def do_animation do
JS.add_class("animate-wiggle", to: "#word-count")
send(self(), JS.remove_class("animate-wiggle", to: "#word-count"))
end
This didn't work because the class was being removed as it was being added. I could've added a timeout but that seems far too hacky.
def animate_wiggle(element_id) do
JS.transition(%JS{}, "animate-wiggle", to: element_id, time: 500)
end
JS.transition/2
to the rescue! The LiveView team built a specific function for triggering transitions repeatedly. ❤️
But there was an issue - LiveView.JS
functions simply generate JavaScript, so they have to be rendered in the page!
So what do we do?
RTFM of course! Onwards!
I had to push the event to the browser so that some JavaScript could execute the wiggle animation for me - so the flow goes like this:
phx
events to react to themJS.transition/2
firesLet's add the JS event listener in our App.js
:
window.addEventListener(`phx:wiggle`, (e) => {
let el = document.getElementById(e.detail.id)
if(el) {
liveSocket.execJS(el, el.getAttribute("data-wiggle"))
}
})
Let's update our event handler when to push the event to the client:
def handle_info({MyApplication.Submissions, [:story, _], _}, socket) do
socket =
socket
|> assign(:story_count, total_story_count())
|> push_event("wiggle", %{id: "stories-count"}) # ⬅️ the new addition
{:noreply, socket}
end
We also need to ensure we add an id and a data attribute to the element we want to wiggle so our JavaScript can find it and know what to do with it:
<p><span id="stories-count" data-wiggle={animate_wiggle("#stories-count")}><%= @stories_count %></span> stories submitted</p>
What you can't see is I have another window triggering the aforementioned events.
We're done! 🚀
We've successfully triggered repeatable front-end animations from live events coming from other users of our application with minimal code (and literally 6 lines of JavaScript).
I love LiveView and I hope this post has given you a flavour of why.
Subscribe to my Substack below for similar content and follow me on Twitter for more LiveView, Elixir, and general programming tutorials & tips.
Want to learn and master LiveView?
Check out the book I'm writing
The Phoenix LiveView Cookbook