Subscribe to access all episodes. View plans →

#133: Surface

Published May 5, 2021

Surface

Surface Directives


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.

© 2024 HEXMONSTER LLC