Today we are stepping into the footsteps of a young student, Mark Zuckerberg, and recreate what almost got him dismissed from Harvard University and would ultimately lead to the creation of Facebook. You can read more about the controversy here or watch the movie "The Social Network" if you are interested. If only we were born a few years earlier - it would be us making those billions. We'll maybe there is still time for you! :)
Facemash
presents the user with two random pictures of college students and page vistors have to choose the "hotter" one. Two new pictures are selected, and the process repeats:
votes are counted for each image and if there is at least one vote, a button enables that allows displaying of the votes. I have used Flowbite's default progress bar to implement it:
All images are copyright free and AI-generated (after all we don't want to get dismissed from the university) and can be downloaded here. Make sure your images end up under
/priv/static/images/facemash/1.jpg
...
/priv/static/images/facemash/18.jpg
Next its time to recall Phoenix Components - the topic this lecture is about.
LiveComponents are a mechanism to compartmentalize state, markup, and events for sharing across LiveViews.
The idea is to keep our LiveView as clean and empty as possible and handle functionality within isolated LiveComponent. Those can even talk to each other. We are doing this today.
First create the following Liveview. You should not have any need to ever change that LiveView again, we will handle all the rest in PollComponent
and RateComponent
.
defmodule AppWeb.FacemashLive do
use AppWeb, :live_view
alias AppWeb.Components.Live.{RateComponent, PollComponent}
def render(assigns) do
~H"""
<.live_component module={RateComponent} id="rate-component" images={@images} />
<.live_component module={PollComponent} id="poll-component" images={@images} />
"""
end
def mount(_params, _session, socket) do
# choose 1..8 for female pictures and 9..18 for male pictures
images = Enum.map(1..8, &AppWeb.Endpoint.static_path("/images/facemash/#{&1}.jpg"))
{:ok, assign(socket, :images, images)}
end
end
Notice how the Liveview loads our two LiveComponents, each with a unique id, passing in an attribute @images
that is a list of paths to our image files.
Note the comment, if you prefer to switch to male models then just change the numbers from 9..18
. If you care to bother you can also replace the images all together. Maybe you are more into Pokemon?
Also, don't forget to add a route in your router.ex
. You may also want to add a new link to Facemash in navbar.
live "/facemash", FacemashLive
Don't forget to check that VSCode has'nt automatically added a conflicting alias to FacemashLive
to the router. Lately it tends to have a nasty habit of doing this.
Alright now its time to discuss the purpose and design of each component.
RateComponent
- must randomly display two images from the list
- clicking on one image should:
- display two newly selected random images (may be the same)
- send a message to the RateComponent with the index of the selected image
PollComponent
- displays a
total_votes
counter that starts at0
- displays a button that allows you to show/hide the results. If total_votes == 0 the button should be disabled.
- results show in form of a list that contain a small image and a progress bar that shows the percentage
Thats a lot of functionality but I managed to pack both components into 109
lines of code, so I am sure you can do it as well!
Phoenix Component Life Cycle and Tips and Tricks
You will definately want to read the life cycle section on phoenix components to understand which callback functions you will need to implement.
Thus in my RateComponent
i have the following functions:
render(assigns)
update(assigns, socket)
handle_event("rate", %{"index" => index}, socket)
My PollComponent
consists of the following functions:
render(assigns)
mount(socket)
update(%{images: images}, socket)
update(%{index: index}, socket)
Notice the update
function has two clauses in the poll component. We use it to react to updates both on initial render (attributes passed via the <.live_component>
tag) as well as updates send via a message from another component:
send_update(AppWeb.Components.Live.PollComponent, id: "poll-component", index: index)
I used the function above inside the handle_event
function of the RateComponent
to send the selected index over to the PollComponent
.
Finally if you are wondering how you can achieve a brightness transition when hovering over an image you can cast a little TailwindCSS magic:
<img
src={...}
class="h-auto w-full brightness-50 hover:brightness-110 transition duration-300"
/>