DEV Community

Daniel Kukula
Daniel Kukula

Posted on

4 1

Porting files generated by phoenix to surface

This post is intended to get you started with surface provided components. I provided the original code and surface versions so you can compare the differences yourself without installing anything.
After installing surface following the installation guide https://surface-ui.org/getting_started
add surface_bulma in your mix.exs, this will allow you to use the table component.

{:surface_bulma, "~> 0.2.0"},
{:surface, "~> 0.6.0"},
Enter fullscreen mode Exit fullscreen mode

Now add new context for our post:
mix phx.gen.live Posts Post post title:string body:string
This will generate a bunch of files in lib/my_app_web/live/post_live which we will convert to surface versions.
Let's start with adding some imports in index.ex.
Change the line:

  #use MyAppWeb, :live_view
Enter fullscreen mode Exit fullscreen mode

to the following code

  use Surface.LiveView

  alias MyAppWeb.Router.Helpers, as: Routes
  alias SurfaceBulma.Table
  alias SurfaceBulma.Table.Column
  alias Surface.Components.{LivePatch, Link, LiveRedirect}
Enter fullscreen mode Exit fullscreen mode

Now rename the index.html.heex to index.sface and replace the code

<h1>Listing Post</h1>

<%= if @live_action in [:new, :edit] do %>
  <%= live_modal MyAppWeb.PostLive.FormComponent,
    id: @post.id || :new,
    title: @page_title,
    action: @live_action,
    post: @post,
    return_to: Routes.post_index_path(@socket, :index) %>
<% end %>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Body</th>
      <th>Links</th>
    </tr>
  </thead>
  <tbody id="post">
    <%= for post <- @post_collection do %>
      <tr id={"post-#{post.id}"}>
        <td><%= post.title %></td>
        <td><%= post.body %></td>
        <td>
          <span><%= live_redirect "Show", to: Routes.post_show_path(@socket, :show, post) %></span>
          <span><%= live_patch "Edit", to: Routes.post_index_path(@socket, :edit, post) %></span>
          <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: post.id, data: [confirm: "Are you sure?"] %></span>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>
<span><%= live_patch "New Post", to: Routes.post_index_path(@socket, :new) %></span>
Enter fullscreen mode Exit fullscreen mode

with this content. It's the same code but it uses surface table component

<h1>Listing Post</h1>

{#if @live_action in [:new, :edit]}
  {MyAppWeb.LiveHelpers.live_modal MyAppWeb.PostLive.FormComponent,
    id: @post.id || :new,
    title: @page_title,
    action: @live_action,
    post: @post,
    return_to: Routes.post_index_path(@socket, :index)}
{/if}

<Table data={post <- @post_collection} id={:table} bordered>
  <Column label="Title">
    {post.title}
  </Column>
  <Column label="Body">
    {post.body}
  </Column>
  <Column label="Links">
    <span><LiveRedirect to={Routes.post_show_path(@socket, :show, post)}>Show</LiveRedirect></span>
    <span><LivePatch to={Routes.post_index_path(@socket, :edit, post)}>Edit</LivePatch></span>
    <span><Link click="delete" to="#" values={id: post.id} opts={data: [confirm: "Are you sure?"]}>Delete</Link> </span>
  </Column>
</Table>
<span><LivePatch to={Routes.post_index_path(@socket, :new)}>New Post</LivePatch></span>
Enter fullscreen mode Exit fullscreen mode

We will follow the same steps in show.ex

  use Surface.LiveView

  alias MyApp.Posts
  alias MyAppWeb.Router.Helpers, as: Routes
  alias Surface.Components.{LivePatch, LiveRedirect}
Enter fullscreen mode Exit fullscreen mode

original code looks like that - we need to rename the file and use our new version

<h1>Show Post</h1>

<%= if @live_action in [:edit] do %>
  <%= live_modal MyAppWeb.PostLive.FormComponent,
    id: @post.id,
    title: @page_title,
    action: @live_action,
    post: @post,
    return_to: Routes.post_show_path(@socket, :show, @post) %>
<% end %>

<ul>
  <li>
    <strong>Title:</strong>
    <%= @post.title %>
  </li>
  <li>
    <strong>Body:</strong>
    <%= @post.body %>
  </li>
</ul>

<span><%= live_patch "Edit", to: Routes.post_show_path(@socket, :edit, @post), class: "button" %></span> |
<span><%= live_redirect "Back", to: Routes.post_index_path(@socket, :index) %></span>
Enter fullscreen mode Exit fullscreen mode

show.sface content:

<h1>Show Post</h1>

{#if @live_action in [:edit]}
  {MyAppWeb.LiveHelpers.live_modal MyAppWeb.PostLive.FormComponent,
    id: @post.id,
    title: @page_title,
    action: @live_action,
    post: @post,
    return_to: Routes.post_show_path(@socket, :show, @post)}
    {/if}

<ul>
  <li>
    <strong>Title:</strong>
    {@post.title}
  </li>
  <li>
    <strong>Body:</strong>
    {@post.body}
  </li>
</ul>

<span><LivePatch to={Routes.post_show_path(@socket, :edit, @post)}, class="button">Edit</LivePatch></span>
<span><LiveRedirect to={Routes.post_index_path(@socket, :index)}>Back</LiveRedirect></span>
Enter fullscreen mode Exit fullscreen mode

Last component in this directory is the form_component.ex where we need to add:

  use Surface.LiveComponent

  alias MyApp.Posts
  alias Surface.Components.Form
  alias Surface.Components.Form.{Field, Label, TextInput, Submit}
Enter fullscreen mode Exit fullscreen mode

The template for this component:

<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="post-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

    <%= label f, :title %>
    <%= text_input f, :title %>
    <%= error_tag f, :title %>

    <%= label f, :body %>
    <%= textarea f, :body %>
    <%= error_tag f, :body %>

    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
Enter fullscreen mode Exit fullscreen mode

It needs to be replaced with a form_component.sface with this code:


<div>
  <h2>{@title}</h2>
  <Form for={@changeset} change="validate" submit="save" opts={autocomplete: "off"}>
    <Field name={:title}>
      <Label/>
      <div class="control">
        <TextInput value={@post.title}/>
      </div>
    </Field>
    <Field name={:body}>
      <Label/>
      <div class="control">
        <TextInput value={@post.body}/>
      </div>
    </Field>
    <Submit>Save</Submit>
  </Form>
</div>
Enter fullscreen mode Exit fullscreen mode

Last thing that we can replace with surface version is the modal_component.ex which you can find in the parent directory.

defmodule MyAppWeb.ModalComponent do
  use MyAppWeb, :live_component

  @impl true
  def render(assigns) do
    ~H"""
    <div
      id={@id}
      class="phx-modal"
      phx-capture-click="close"
      phx-window-keydown="close"
      phx-key="escape"
      phx-target={@myself}
      phx-page-loading>

      <div class="phx-modal-content">
        <%= live_patch raw("&times;"), to: @return_to, class: "phx-modal-close" %>
        <%= live_component @component, @opts %>
      </div>
    </div>
    """
  end

  @impl true
  def handle_event("close", _, socket) do
    {:noreply, push_patch(socket, to: socket.assigns.return_to)}
  end
end
Enter fullscreen mode Exit fullscreen mode

the surface version looks like that:

defmodule MyAppWeb.ModalComponent do
  use Surface.LiveComponent
  alias Surface.Components.{LivePatch, Raw}

  data return_to, :string
  data component, :fun
  data opts, :keyword

  @impl true
  def render(assigns) do
    ~F"""
    <div
      id={@id}
      class="phx-modal"
      phx-capture-click="close"
      phx-window-keydown="close"
      phx-key="escape"
      phx-target={@myself}
      phx-page-loading>

      <div class="phx-modal-content">
        <LivePatch to={@return_to} class="phx-modal-close">
          <#Raw>
            &times;
          </#Raw>
        </LivePatch>
        {live_component @component, @opts}
      </div>
    </div>
    """
  end

  @impl true
  def handle_event("close", _, socket) do
    {:noreply, push_patch(socket, to: socket.assigns.return_to)}
  end
end
Enter fullscreen mode Exit fullscreen mode

Surface provides also replacemints for phx-[event] but I had some problems to set it up.
At this point your app should still be functional but using surface components instead live view provided ones.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay