Custom software development tailored to your needs. From web and mobile applications to complex backend systems, we build solutions that fit your business goals. Book a free consult today to learn more.
In my previous blog post, we built a LiveView clock application. The application used client-side information during the LiveView mount process. I mentioned that using Phoenix hooks is a different approach, suitable for handling client-side information that changes as the user uses the app. Today, we’ll walk through an example of defining a hook to provide an interactive experience utilizing the user’s real-time location.
First, let’s create a simple app with a LiveView for showing the user their current location.
The Start of Our App
- Create our new Location app by running this in your terminal:
mix phx.new --no-ecto location
. The--no-ecto
flag is because we won’t be using a database. - Add this line to
lib/location_web/router.ex
:
scope "/", LocationWeb do
pipe_through :browser
get "/", PageController, :home
+ live "/location", LocationLive
end
- Finally, create our new LiveView by creating the file
lib/location_web/live/location_live.ex
with this code:
defmodule LocationWeb.LocationLive do
use LocationWeb, :live_view
def mount(_params, _session, socket) do
{:ok,
assign(socket,
lat: 0.0,
long: 0.0,
accuracy: 0.0
)}
end
def render(assigns) do
~H"""
<h1 id="title">Your Location</h1>
<p id="location">
You are located at latitude {@lat} and longitude {@long} with a {@accuracy}-meter margin of error!
</p>
"""
end
end
So far, our app isn’t getting any real-time information. We are just defaulting to latitude 0 and longitude 0. Fortunately, browsers have a cool Geolocation API. We will get the user’s latitude and longitude directly from the user’s browser.
The Geolocation API includes a watchPosition()
function that can be used to run a callback whenever the device’s position changes. The callback will be invoked with an object that includes the device’s latitude and longitude, and an estimate of how accurately the user’s location is being tracked. We’ll make use of those three data points.
We can write some JavaScript that makes use of the watchPosition()
function, but how do we connect the JavaScript to the LiveView? Phoenix hooks allow us to define client-side JavaScript that is tied to a particular HTML element in the server-rendered code (the LiveView).
There are two steps to implementing a simple Phoenix hook:
- Write some JavaScript and include it in the declaration of the app’s liveSocket.
- Annotate the LiveView’s HTML element(s) for the JavaScript to target by adding a
phx-hook
attribute.
That’s it!
In our app’s case, client-side events are going to be generated by watchPosition()
, and the client-side JavaScript is going to send information about those events to the server. So, we need a third step:
- Add a
handle_event()
function to the LiveView to respond to the events from the client.
How Do Hooks Work?
The Phoenix JavaScript client, by default, creates a websocket to serve as the transport layer for client-server communication, such as Phoenix channels and LiveViews. We can define hooks – JavaScript code – and attach them to the “liveSocket” object when it is instantiated for our LiveView app. We tell Phoenix which HTML element(s) the hook is connected to by annotating the HTML element(s) in the LiveView with a phx-hook
attribute.
When a LiveView is rendered and the DOM is ready, the LiveView JavaScript client scans the HTML for elements with the phx-hook
attribute. For each such element, the client invokes the relevant hook code. More specifically, it instantiates the hook object and invokes the hook object’s mounted()
function, passing in the HTML element.
The mounted()
function can manipulate the HTML element before the client paints the HTML. In our app, we’re not manipulating the HTML element. Instead, the mounted()
function will call watchPosition()
with a callback, which causes the browser to invoke the callback every time the device’s position changes.
A hook can define other functions for responding to LiveView server updates, such as updated()
and destroyed()
. The relevant function is invoked by the LiveView JS client when it receives a diff from the server which includes a change to the element with the hook.
The JavaScript Part of Our Hook
Let’s define our hook. Create a directory assets/js/hooks
, and create a location.js
file in that directory with this code:
export default Location = {
mounted() {
const success = (positionObject) => this.pushEvent(
"location-update", {coordinates: positionObject.coords}
)
const error = (err) => console.error(`Error: ${err.code}: ${err.message}.`)
const options = {
enableHighAccuracy: true,
maxAge: 5000 // client can return a cached position for up to 5 seconds
}
navigator.geolocation.watchPosition(success, error, options);
}
}
By invoking watchPosition()
with the callback that includes pushEvent()
, we will be telling the browser to run the callback, which sends an event to the server, every time the device’s position changes. We’ve called the event "location-update"
.
Now, change the instantiation of the liveSocket object to include our hook. Make these two changes in assets/js/app.js
:
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
+ import Location from "./hooks/location";
+
// Establish Phoenix Socket and LiveView configuration.
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
- params: {_csrf_token: csrfToken}
+ params: {_csrf_token: csrfToken},
+ hooks: { Location }
})
All we’ve done here is add our Location object to the LiveSocket constructor. That object is now available on every page of our app! You can see this by visiting any page of the app and typing liveSocket.hooks.Location
in the browser console.
liveSocket.hooks.Location
← Object { mounted: mounted() }
We can see that liveSocket.hooks.Location
is our object with its mounted() function. We can try calling the function in the browser console:
liveSocket.hooks.Location.mounted()
← undefined
⛔︎ Uncaught TypeError: this.pushEvent is not a function
success location.js:3
mounted location.js:13
<anonymous> debugger eval code:1
location.js:3:45
We get an error, which is important.
pushEvent()
is defined by LiveView as the way for the client-side code to push an event to the server. The pushEvent()
function needs to be run by the LiveView client as it processes the HTML from the server. Calling it ourselves in the browser console isn’t going to work.
Let’s implement the server-side part of our hook: annotating our HTML and defining a handle_event()
to respond to the "location-update"
messages from the client.
Server-Side: Annotating the HTML Element With phx-hook
Change our LocationLive render()
function by adding a phx-hook
attribute to the p
element:
def render(assigns) do
~H"""
<h1 id="title">Your Location</h1>
- <p id="location">
+ <p id="location" phx-hook="Location">
Hello lat {@lat} and long {@long} with a {@accuracy}-meter margin of error!
</p>
Because we added the phx-hook="Location"
attribute to the p
element, when the p
is mounted, the client will invoke our Location mounted()
function, passing in that element.
Server-Side: Handling the Events Pushed by the Client
Then add a new handle_event()
function to LocationLive:
def handle_event(
"location-update",
%{"coordinates" => %{"latitude" => lat, "longitude" => long, "accuracy" => accuracy}},
socket
) do
{:noreply,
assign(socket,
lat: lat,
long: long,
accuracy: Float.round(accuracy, 3)
)}
end
Now, each time the client pushes an event to the server, the LiveView will update its assigns and re-render. We’re done!
There’s nothing magic about the placement of the phx-hook
attribute. Technically, since our Location mounted()
function doesn’t manipulate the passed-in element and only invokes the browser’s watchPosition()
function, we could have instead added the attribute to any other element that will get mounted in our render()
function (as long as it has a unique id
attribute). In our app’s case, we could add the phx-hook
attribute to the h1
instead of the p
, and everything would still work.
(A Phoenix hook and the element(s) it is attached to are often more tightly coupled, as you can see in the examples in the LiveView hooks documentation.)
Seeing Location Updates on a Map
Signing up to use a map provider’s API and embedding a map in our LiveView is beyond the scope of this post. But if you’ve been coding along, I encourage you to look into it! It’s interesting to see which devices and situations provide the best location tracking, and seeing your movement plotted on a map is fun.
I hope this example demystified Phoenix hooks a bit. Happy implementing!