We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
- Elixir
- Phoenix
- LiveView
- Google Maps API
10 min read
Create an address autocomplete using Google API, Elixir and Phoenix Liveview
In this post, I’ll walk you through building an address autocomplete feature using Google API, Elixir, and Phoenix LiveView. Drawing from my experience implementing this in real-world projects, I’ll share the steps, and tips to make the integration seamless.
Whether you’re looking to enhance user experience with dynamic forms or explore how Phoenix LiveView can bring real-time interactivity to your web applications, this guide has something for you. By the end, you’ll have a fully functional autocomplete feature and deeper insights into working with these technologies.
You can find the full source code here. I'm going to add more recipes in the future!
Prerequisites
The first step is to install the google_maps library, open up your mix.exs file and add the following to your list of dependencies:
...
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.2"},
{:google_maps, "~> 0.11.0"}
]
Next install the dependency with:
mix deps.get
Then we must include google_maps as an extra application:
def application do
[
mod: {PhoenixRecipes.Application, []},
extra_applications: [:logger, :runtime_tools, :google_maps]
]
end
Now we are ready to start!
Note: You need to have a Google Maps API key, the post assumes that you have the environment variable GOOGLE_MAPS_API_KEY set when running the application. Generating one is outside of the scope of this post but you can learn more about it here.
1. Building the predictions functionality
Go to your project's non-web directory and create the following module:
defmodule PhoenixRecipes.AddressAutocomplete do
def predictions(text) do
case GoogleMaps.place_autocomplete(text) do
{:ok, %{"predictions" => predictions}} ->
Enum.map(predictions, &extract_fields/1)
end
end
defp extract_fields(%{"description" => description, "place_id" => place_id}) do
%{
description: description,
place_id: place_id
}
end
end
The predictions function will be our interface for interacting with the Google API, it receives a text and returns a list of places. The API have a lot of information about each place, here's when the extract_fields function comes handy, I only need the description and place_id fields but try to inspect the predictions that come from the request to see if there's something that suites your case better.
Let's try it out, open an iex session:
iex -S mix
With the session open let's alias our new module and call the predictions function with the name of a street you know:
iex(1)> alias PhoenixRecipes.AddressAutocomplete
iex(2)> AddressAutocomplete.predictions("Willoughby 100")
[
%{
description: "100 Willoughby Street, Brooklyn, NY, USA",
place_id: "ChIJnaWkaUtawokRtYHwRSB_zaY"
},
%{
description: "100 Willoughby Avenue, Brooklyn, NY, USA",
place_id: "ChIJzxrQVbhbwokRUaDR4EoFQx8"
},
...
]
Great! We have the first building block needed to make an autocomplete input.
2. Render the autocomplete input
The next step is to create a live directory under your project's web directory if you don't have one. Is not required but I like to follow the convention. Underneath create a file called autocomplete_live.ex here's where the input component will be rendered. I added some tailwind to make the example more aesthetic.
defmodule PhoenixRecipesWeb.AutocompleteLive do
use PhoenixRecipesWeb, :live_view
@impl true
def render(assigns) do
~H"""
<div class="h-full w-2xl">
<h1 class="text-2xl font-bold text-gray-800">Autocomplete!</h1>
<div id="autocomplete" class="autocomplete mt-4" phx-hook="AddressAutocompleteHook">
<input
class="w-full rounded-md border-0 py-2 pl-3 pr-12 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 sm:text-sm sm:leading-6"
placeholder="Search address"
/>
<ul
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
role="listbox"
>
</ul>
</div>
</div>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
end
Next, add the new LiveView to the router, here I'm replacing the root route:
scope "/", PhoenixRecipesWeb do
pipe_through :browser
live "/", AutocompleteLive
end
3. Implement a JavaScript Hook to add the search functionality
In order to make the input to actually work we need a little bit of JavaScript, we'll add it using Phoenix LiveView JS hooks.
Let's create the file where we want to implement the search functionality.
// assets/js/hooks/address-autocomplete-hook.js
const AddressAutocompleteHook = {
mounted() {
console.log("Mounted!")
}
}
export default AddressAutocompleteHook;
I like to keep my hooks inside a hooks directory under the js directory so let's create it and an index.js file which will be responsible for exporting all of the hooks.
cd assets/js
mkdir hooks
touch index.js
Your directory structure should be similar to this one:
assets/
├── css
├── js
│ └── hooks
This is just a matter of preference as it is but in more complex applications having an entrypoint file like the hooks/index.js to export everything we need can be benefical to our code organization.
// assets/js/hooks/index.js
import AddressAutocompleteHook from "./address-autocomplete-hook"
export default {
AddressAutocompleteHook
}
If you don't have any hooks in your Phoenix app then you're going to need to add the following code to your app.js file:
// assets/app.js
import hooks from "./hooks" // <-- import the hooks
import topbar from "../vendor/topbar"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
hooks: hooks, // <-- make them available to our liveviews
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}
})
Now let's install the client library that will handle the autocomplete functionality, there are many different out there, I have used this one a few times and it works well enough for my purposes. I use yarn but you can replace the command to use NPM.
cd assets
yarn add @trevoreyre/autocomplete-js
Add the following to your AddreesAutocompleteHook
import Autocomplete from '@trevoreyre/autocomplete-js'
const AddressAutocompleteHook = {
mounted() {
this.autocomplete = new Autocomplete(this.el, {
search: input => {
const url = `/api/address/query?place=${encodeURIComponent(input)}`;
return new Promise(resolve => {
if (input.length === 0) {
return resolve([]);
}
if (input.length < 3) {
return resolve([])
}
return fetch(url, {
headers: {
"Content-Type": "application/json",
}
}).then(response => response.json()).then((data) => {
return resolve(data.data);
})
})
},
getResultValue: result => result.description,
renderResult: (result, props) => {
return `
<li ${props}>
<span className="block truncate">${result.description}</span>
</li>
`},
debounceTime: 500
})
},
destroyed() {
this.autocomplete.destroy();
}
}
export default AddressAutocompleteHook;
When the DOM element mounts, an instance of the Autocomplete object will be created, it allows us the following:
- A way to get search results
- Customize how the results are rendered (good for custom styles)
- Debouncing the keyboard events to avoid requesting the server more than necessary
As you may have noticed, the search callback calls an endpoint we don't currently support, let's write that next!
url = `/api/address/query?place=${encodeURIComponent(input)}`
...
return fetch(url, {
headers: {
"Content-Type": "application/json",
}
})
4. Add endpoint to query our API for prediction results
For this section we are going to need a few this:
- A controller capable of serving the request
- A JSON file for returning the results as JSON
- A new route in the router.
Let's start with the controller, in this example I'm gonna use a controller to handle all API requests (which is only one).
# phoenix_recipes_web/controllers/api_controller.ex
defmodule PhoenixRecipesWeb.ApiController do
use Phoenix.Controller, formats: [:json]
alias PhoenixRecipes.AddressAutocomplete
def query(conn, params) do
case params do
%{"place" => place} ->
predictions = AddressAutocomplete.predictions(place)
render(conn, :query, predictions: predictions)
_ ->
{:error, :not_found}
end
end
end
Then create the API JSON module which will be responsible for formatting the results:
# phoenix_recipes_web/controllers/api_json.ex
defmodule PhoenixRecipesWeb.ApiJSON do
@moduledoc false
def query(%{predictions: predictions}) do
%{data: for(prediction <- predictions, do: data(prediction))}
end
defp data(prediction) do
%{
description: prediction[:description],
place_id: prediction[:place_id]
}
end
end
Finally, we need to add a new route but this time is going be processed by the api pipeline which have a different set of plugs defined to be able to handle API requests, you can learn more about them here:
scope "/api", PhoenixRecipesWeb do
pipe_through :api
get "/address/query", ApiController, :query
end
As a bonus step, I'll show you how to customize the list of results rendered by the autocomplete input, add the following to your app.css file:
.autocomplete input[role="combobox"][disabled] {
@apply bg-gray-50 text-gray-500 ring-gray-200
}
.autocomplete-result {
@apply relative cursor-default select-none py-2 pl-3 pr-9 text-gray-900
}
.autocomplete-result:hover,.autocomplete-result[aria-selected=true] {
@apply text-white bg-indigo-500
}
If you've followed all of the steps above you can now go start the phoenix server.
mix phx.server
Then navigate to http://localhost:4000 and try to search for a place!
5. Use the places API to get more information about a place
We're almost there but there's one more step left, what does it serve an autocomplete input if we cannot select a place and get more information?
To get this to work we need to add an onSubmit handler to our hook and send an event to our LiveView to handle it.
onSubmit: result => {
this.pushEventTo(this.el, "address-selected", { place_id: result.place_id });
},
The code above will send an event for our LiveView to handle, the name of the event is address-selected and we're sending the place ID of the selected search result.
Inside the LiveView we can handle it like this:
@impl true
def handle_event("address-selected", %{"place_id" => place_id}, socket) do
IO.inspect(place_id)
{:noreply, socket}
end
The place ID is something Google uses to uniquely identify each address so we can get the same data for the same ID. In order to get the information we need to implement one more function.
Let's return to the address_autocomplete.ex module and add the following:
def get_place(place_id) do
case GoogleMaps.place_details(place_id) do
{:ok, %{"result" => result}} ->
result
{:error, _, _} ->
nil
end
end
Now we can render the results from the Google Maps API in our LiveView! Update your LiveView to the following:
defmodule PhoenixRecipesWeb.AutocompleteLive do
use PhoenixRecipesWeb, :live_view
alias PhoenixRecipes.AddressAutocomplete
@impl true
def render(assigns) do
~H"""
<div class="h-full w-2xl">
<h1 class="text-2xl font-bold text-gray-800">Autocomplete!</h1>
<div
id="autocomplete"
class="autocomplete mt-4"
phx-hook="AddressAutocompleteHook"
phx-update="ignore"
>
<input
class="w-full rounded-md border-0 py-2 pl-3 pr-12 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 sm:text-sm sm:leading-6"
placeholder="Search address"
/>
<ul
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
role="listbox"
>
</ul>
</div>
<div :if={@result} class="mt-12 border rounded-lg overflow-x-auto overflow-y-auto h-96">
<pre>
<%= inspect(@result, pretty: true)%>
</pre>
</div>
</div>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, result: nil)}
end
@impl true
def handle_event("address-selected", %{"place_id" => place_id}, socket) do
result = AddressAutocomplete.get_place(place_id)
{:noreply, assign(socket, result: result)}
end
end
Final result:
Conclusion
And that’s it! By following these steps, you now have a fully functional address autocomplete feature that integrates Google API with Elixir and Phoenix LiveView. This solution not only enhances user experience with real-time interactivity but also leverages the power and simplicity of the Phoenix framework.
One thing, I wanted to mention that this kind of feature is extremely used in the industry so there are a lot of fronted libraries that give you what we have done here. The point was not to reinvent the wheel for the sake of it but to show you how to integrate a third-party API, to create a boundary module (address_autocomplete) which can be tested if needed, the workflow to add a JS hook that implements a JavaScript library and how it interacts with LiveView and event an API endpoint of our own.
Whether you’re building a similar feature for a project or just exploring the capabilities of these technologies, I hope this guide has been helpful. If you have any questions, run into issues, or want to share your own experience with implementing autocomplete, feel free to reach out or leave a comment. Happy coding!
PS: I was nervious writting this because this is my first blog post ever, I always liked to share my knowledge and this blog is an attempt to reach more people than what I've been able to do before. I hope you enjoy it!
Important Note
I wanted to clarify that this example makes use of a JavaScript library and a API endpoint to make the autocomplete works just to showcase how to put together the different parts of a Phoenix application in case someone is looking for that but it's not really necessary nor something I recommend to do for a feature like this unless you really need that JavaScript library.
I wrote a second part of this post showing how you can implement it using only LiveView. No JS Hook nor API endpoint, less files, less context changes and it's easier to understand and reason about.
Go check it out:
Create an address autocomplete using Google API, Elixir and Phoenix Liveview Part 2