Subscribe to access all episodes. View plans →
Published May 5, 2021
Here we have a movie trivia application, but as you can see, there’s not currently a way to play it. In this episode, we’ll create a way for us to play movie trivia. Our trivia game will display a trivia question along with a button we can click to reveal the answer to the question.
To implement the interface for this we’ll use Surface, which is a server-side rendering component library that is built on Phoenix LiveView.
If you’re not familiar with LiveView or LiveView components, check out episode #129.
Alright, let’s get started by grabbing the surface
package from hex.pm.
Then we’ll open our applications Mixfile and add surface
to our list of dependencies.
mix.exs
...
defp deps do
...
{:surface, "~> 0.4.0"},
...
end
...
Then let’s go to the command line and run mix deps.get
.
$ mix deps.get
...
New:
surface 0.4
If you use mix format
you’ll need to update the formatter to use surface
. Let’s open .formatter.exs
and update the import_deps
to include surface
.
.formatter.exs
[
import_deps: [:ecto, :phoenix, :surface],
...
]
Surface also has its own templates. To have our app live reload them, we’ll open our config/dev.exs
and Surface templates use the .sface
file extension so we’ll update live_reload: [patterns: [...]
with a regular expression for Surface templates ~r"lib/teacher_web/live/.*(sface)$",
. Remember to replace teacher
with the name of your application.
config/dev.exs
...
config :teacher, TeacherWeb.Endpoint,
live_reload: [
patterns: [
...
~r"lib/teacher_web/live/.*(sface)$",
...
]
]
...
Alright, now let’s open our up PageLive
this is the LiveView that handles our app homepage. Surface is built on top of Phoenix LiveView, it supports both stateless and stateful components. Let’s turn our “Movie Trivia” heading into our first Surface component. Before we can do that, we’ll need to update our PageLive to use Surface. Here we’re calling use TeacherWeb, :live_view
. Surface provides a wrapper around LiveView called Surface.LiveView
- let’s update this to use it. Surface also provides the ~H
sigil. Let’s update our render
callback to use that.
lib/teacher_web/live/page_live.ex
defmodule TeacherWeb.PageLive do
use Surface.LiveView
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<a href="/">Home</a>
<h1>Movie Trivia</h1>
"""
end
end
Now let’s create a module named heading.ex
- this will be the Surface component for our page heading. We’ll define our module and because our heading component won’t need to own its state we’ll want to use Suface.Component
- this is the component type for stateless components.
Then we’ll define the render
callback and using the ~H
sigil again we’ll include the content we want to render.
lib/teacher_web/live/heading.ex
defmodule TeacherWeb.Heading do
use Surface.Component
def render(assigns) do
~H"""
<h1>Movie Trivia with Surface!</h1>
"""
end
end
Now that we have our component module defined. Let’s go back to our page_live.ex
and because components are just modules we can alias TeacherWeb.Heading
and then include <Heading />
in our template.
lib/teacher_web/live/page_live.ex
defmodule TeacherWeb.PageLive do
use Surface.LiveView
alias TeacherWeb.Heading
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<a href="/">Home</a>
<Heading />
"""
end
end
Let’s test that this works. We’ll go to the command line and start our server.
$ mix phx.server
...
Then if we go back to the browser, our page is now displaying the Heading
component. Congratulations you just created your first Surface component! Now let’s create another. We’ll go back to our app and it has a Card
schema module that if we open we can see has two fields answer
and question
. These are the question and answer for each trivia card. Let’s create a stateful Surface component that we can use to play trivia. It will display a trivia question and have a button we can click to reveal the answer.
We’ll create a new module in the “live” directory named trivia_card.ex
Because our TriviaCard
component will need to own its state we’ll use Surface.LiveComponent
- this is the component type for stateful components.
Alright now, let’s add the render
callback to render our trivia card template. I”ll paste in some HTML to use with in trivia card then we’ll want to render either the trivia question or answer, depending on whether its been answered.
Let’s store that value in @trivia_value
. Wrapping it in the double curly braces will tell the compiler to inject the @trivia_value
into the generated code and any valid expression is accepted. Now we need to set @trivia_value
in the assigns. With LiveView we’d write a mount
callback and then initialize any assigns. In Surface those assigns are called data assigns. And to declare a data assign we use the data
macro.
Let’s do this in our component. We’ll comment out our mount
callback and then add the data
macro followed by the name
- trivia_value
the type
of string
and then any options
we want to add. Let’s give this a default value. Because we’re using a default value for our data assign here, Surface initializes it for us - we don’t need to implement the mount
callback.
lib/teacher_web/live/trivia_card.ex
defmodule TeacherWeb.TriviaCard do
use Surface.LiveComponent
data trivia_value, :string, default: "a trivia question"
def render(assigns) do
~H"""
<div>
<div class="card">
<h3>
{{ @trivia_value }}
</h3>
</div>
</div>
"""
end
end
Now that we have our component let’s include it on the page. We’ll go back to the PageLive
module and alias our TriviaCard
module so we can call it without the prefix. Then we can include our TriviaCard
component below our Heading
component and because our TriviaCard
component is stateful we’ll want to give it an ID.
lib/teacher_web/live/page_live.ex
defmodule TeacherWeb.PageLive do
...
alias TeacherWeb.{TriviaCard, Heading}
...
@impl true
def render(assigns) do
~H"""
<a href="/">Home</a>
<Heading />
<TriviaCard id="trivia-card" />
"""
end
end
When we go back to the browser, great our TriviaCard
component is being rendered with the default text we provided. Now let’s update the component to use real trivia data.
We’ll go back to the TriviaCard
component. Let’s start by updating our HTML to include a button we can use to get an answer to our question and then another button to get a new trivia question. Let’s also add a few more data assigns to help us manage our component. We’ll add a boolean data assign called answered
with the default value of false. We’ll use this to determine whether the trivia card has been answered or not. Then another data assign called trivia_card
, with the type any
. Let’s also remove the default value from our trivia_value
.
Because trivia_value
and trivia_card
don’t have default values, we’ll want to initialize them. I’ll uncomment our mount
callback and we’ll need a trivia card, so let’s write a little helper function that returns a trivia card and since we’re now calling the Game
context module, let’s add an alias for it. Then let’s take our socket and assign an initial trivia_value
which will be the card.question
and then we’ll assign the card itself to the socket.
Now back in the render
callback we’ll want to update our <button>
s to trigger events that update the state of our component either to show the answer to the question or get a new trivia question. Surface provides directives to help us do that. Surface provides several different directives. For our button let’s use the :on-click
directive. We’ll add :on-click="answer"
.
For our other <button>
we only want this to display if the question has been answered. So let’s go back to the different Surface directives and we’ll use the :if
directive to conditionally render our <button>
. We’ll update it to only display if @answered
is true. Let’s also add the :on-click
directive for when we want to get a new trivia question.
Now we need to handle our “answer” and “new” events. Let’s start by defining a handle_event
callback for the “answer” event. This will be triggered when someone clicks to answer a trivia question. Inside it, we’ll get the current trivia card from the socket. Then we’ll return a :noreply
tuple and update our socket with the trivia_value
of the card.answer
and update answered
to true
. And we’ll create another handle_event
for the “new” event. This will fire when someone requests a new trivia card. We’ll want to get a new random trivia card. Then we’ll set answered
to false
set the trivia_value
to the card.question
and update the trivia_card
.
lib/teacher_web/live/trivia_card.ex
defmodule TeacherWeb.TriviaCard do
use Surface.LiveComponent
alias Teacher.Game
data answered, :boolean, default: false
data trivia_value, :string
data trivia_card, :any
def mount(socket) do
card = random_card()
{:ok,
socket
|> assign(trivia_value: card.question)
|> assign(trivia_card: card)}
end
def render(assigns) do
~H"""
<div>
<div class="card">
<h3>
{{ @trivia_value }}
</h3>
</div>
<button :on-click="answer">
Answer
</button>
<button :if={{ @answered }} :on-click="new">
Another
</button>
</div>
"""
end
def handle_event("answer", _value, socket) do
card = socket.assigns.trivia_card
{:noreply,
socket
|> assign(trivia_value: card.answer)
|> assign(answered: true)}
end
def handle_event("new", _value, socket) do
card = random_card()
{:noreply,
socket
|> assign(answered: false)
|> assign(trivia_value: card.question)
|> assign(trivia_card: card)}
end
defp random_card do
Game.list_cards() |> Enum.random()
end
end
Alright, let’s test our changes. We’ll go back to the browser and we can now see an actual movie trivial question. Let’s click to get the answer - and perfect - the answer is updated on the page and the button to get another trivia question is revealed! Clicking it fetches a new trivia question.
Now let’s look at one more feature of Surface. In our Heading
component, we’ve hardcoded this “Movie Trivia with Surface” text. Instead, it would be nice to use this component in multiple places, specifying what text we want to display when we call the component.
Luckily, Surface provides a way to do just that with Properties. Properties allow you to pass information from a parent component down to a child. Let’s update our Heading
component to use a property to specify what text to display.
First, we’ll include the prop
macro giving it the name - title
the type - :string
and because we’ll always want the title provided let’s make this a required property. Then in our H1 element, we’ll include the @title
property.
lib/teacher_web/live/heading.ex
defmodule TeacherWeb.Heading do
use Surface.Component
prop title, :string, required: true
@impl Phoenix.LiveComponent
def render(assigns) do
~H"""
<h1>{{ @title }}</h1>
"""
end
end
Now, all we need to do is go back to where we’re calling this component in our PageLive
and include the title
property with the value we want to pass it, "Movie trivia"
.
lib/teacher_web/live/page_live.ex
...
@impl true
def render(assigns) do
~H"""
<a href="/">Home</a>
<Heading title="Movie trivia" />
<Card id="trivia-card" />
"""
end
...
With that, we can go back to the browser. And perfect our Heading
component is now using the title
property.
Surface is a great library for building component-based interfaces with Elixir and Phoenix and there’s a lot more to cover. If you want to learn more and maybe contribute to the project, check out the project on GitHub.
Romenig Lima Damasio
3 years agoNice, please more with Surface!
Alekx
3 years ago👍