DEV Community

Daniel Kukula
Daniel Kukula

Posted on

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.

Discussion (0)