btihen
5/10/2020 - 8:36 AM

Phoenix 1.5 - User Voting LiveView & PubSub

Phoenix 1.5 Live View & PubSub

https://dev.to/joseph_lozano/setting-up-a-new-phoenix-1-5-project-with-phoenix-liveview-309n https://github.com/joseph-lozano/Feenix https://dev.to/joseph_lozano/magic-sign-in-links-with-elixir-and-phoenix-1-5-496k

PART 1 - LiveView (in page updates without JS)

https://dev.to/joseph_lozano/setting-up-a-new-phoenix-1-5-project-with-phoenix-liveview-309n

ensure the newest version of elixir

exenv install 1.10.3
exenv global
exenv local 1.10.3

Install the 1.5.1 phx_new generator mix archive.install hex phx_new 1.5.1

Create and enter the project mix phx.new feenix --live && cd feenix

create init commit git init && git add -A && git commit -m "init"

create uses without password (we will use magic links)

mix phx.gen.live Accounts User users email:string username:string upvotes:integer downvotes:integer

Then configure your database in config/dev.exs and run:

$ mix ecto.create

Start your Phoenix app with:

$ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

$ iex -S mix phx.server

Add the live routes to your browser scope in lib/feenix_web/router.ex:

# lib/feenix_web/router.ex
    live "/users", UserLive.Index, :index
    live "/users/new", UserLive.Index, :new
    live "/users/:id/edit", UserLive.Index, :edit

    live "/users/:id", UserLive.Show, :show
    live "/users/:id/show/edit", UserLive.Show, :edit

Next, let's go into the CreateUsers migration and unique indexes for email and username to the users table. The changeset function should now look like

# priv/repo/migrations/XXXXXXXXXXX_create_users.exs
def change do
  create table(:users) do
    add :email, :string
    add :username, :string
    add :upvotes, :integer
    add :downvotes, :integer

    timestamps()
  end

  create unique_index(:users, :email)
  create unique_index(:users, :username)
end

Remember to update your repository by running migrations:

$ mix ecto.migrate

Next, let's update the changeset to enforce the new unique constraint. The new changeset should look like

# lib/feenix/acconts/user.ex
def changeset(user, attrs) do
  user
  |> cast(attrs, [:email, :username])
  |> validate_required([:email, :username])
  |> validate_length(:email, min: 6, max: 127)
  |> validate_length(:username, min: 3, max: 127)
  |> unique_constraint(:email,
    name: "users_email_index",
    message: "Account already exists. Please log in."
  )
  |> unique_constraint(:username,
    name: "users_username_index",
    message: "Username already in use. Please use another."
  )
end

change text_input to email_input

# lib/feenix_web/live/user_live/form_component.html.leex
<%= email_input f, :email %> 

Now that we have the users set up, let's commit

git add -A && git commit -m "Set up user accounts"

fix the unique DB save test: create another set of attributes:

@save_attrs %{email: "other@email.com", username: "some username"}

lets run our tests and see if all is still good: mix test

now we need to update the user tests to accomodate the unique contraints: its no longer ok to make a second account with the same info so we will make a new variable with new values:

# test/feenix_web/live/user_live_test.exs
@save_attrs %{email: "other@email.com", username: "some username"}
# test/feenix_web/live/user_live_test.exs:45
{:ok, _, html} =
  index_live
  |> form("#user-form", user: @save_attrs)
  |> render_submit()
  |> follow_redirect(conn, Routes.user_index_path(conn, :index))

now the tests should be ok! mix test

fix the input in our new/edit form -- change text_input to email_input

# lib/feenix_web/live/user_live/form_component.html.leex
<%= email_input f, :email %> 

Now that we have the users set up, let's commit git add -A && git commit -m "Set up user accounts - with LiveView (in place webpage updates without JS)"

PART 2 - PubSub - LiveView for all clients connected

https://www.youtube.com/watch?v=MZvmYaFkNJI

Lets make it possible to subscribe multiple webclients to the same web socket so info is updated on all machines (at the end of the file add subscribe and broadcast methods) we will name the channel "users"

# lib/feenix/accounts.ex
  def subscribe do
    Phoenix.PubSub.subscribe(Feenix.PubSub, "users")
  end

  defp broadcast({:error, _reason} = error, _event ), do: error
  defp broadcast({:ok, user}, event) do
    Phoenix.PubSub.broadcast(Feenix.PubSub, "users", {event, user})
    {:ok, user}
  end

Now when we update (or create a user - we need to publish our info via the broadcast method)

# lib/feenix/accounts.ex
  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
    |> broadcast(:user_updated)  # add updated user broadcast
  end

Lets do the same when a user is created:

# lib/feenix/accounts.ex
  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
    |> broadcast(:user_created)
  end

Finally, lets create a sort order for all users displayed:

# lib/feenix/accounts.ex
   def list_users do
    # Repo.all(User)
    Repo.all(from u in User, order_by: [desc: u.id])
  end

Now lets have the LiveView web context subscribe to the "users" channel when connected:

# lib/feenix_web/live/user_live/index.ex
  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket), do: Feenix.Accounts.subscribe()  # add this line to sub
    # optimization for updates (also add prepend to liveview page!)
    {:ok, assign(socket, :users, fetch_users()), temporary_assigns: [users: []]}
  end

add the ability to handle this new info via the socket API:

# lib/feenix_web/live/user_live/index.ex
  @impl true
  def handle_info({:user_created, user}, socket) do
    {:noreply, update(socket, :users,
                      fn users -> [user, users] end)}
  end
  def handle_info({:user_updated, user}, socket) do
    {:noreply, update(socket, :users,
                      fn users -> [user, users] end)}
  end

Now as promised we add prepend to our html (to use the temporary assigns optimization)

# lib/feenix_web/live/user_live/index.html.leex
  <tbody id="users" phx-update="prepend">

Now fix the text

  1. test Index deletes user in listing (FeenixWeb.UserLiveTest) test/feenix_web/live/user_live_test.exs:77 Expected false or nil, got true code: refute has_element?(index_live, "#user-#{user.id}") arguments:
         # 1
         #Phoenix.LiveViewTest.View<
           endpoint: FeenixWeb.Endpoint,
           id: "phx-Fg1TxAtFBEizKQGk",
           module: FeenixWeb.UserLive.Index,
           pid: #PID<0.486.0>,
           ...
         >

         # 2
         "#user-63"

     stacktrace:
       test/feenix_web/live/user_live_test.exs:81: (test)

PART 3 - Add update button that uses liveview updates (using a component)

https://www.youtube.com/watch?v=MZvmYaFkNJI

# lib/feenix_web/live/user_live/index.html.leex
  <tbody id="users" phx-update="prepend">
    <%= for user <- @users do %>
      <%= live_component @socket, FeenixWeb.UserLive.UserComponent, id: user.id, user: user %>
  </tbody>

Now make the new live component that handles events

touch lib/feenix_web/live/user_live/user_component.ex

# lib/feenix_web/live/user_live/user_component.ex
defmodule FeenixWeb.UserLive.UserComponent do
  use FeenixWeb, :live_component

  def render(assigns) do
    ~L"""
    <tr id="user-<%= @user.id %>">
      <td><%= @user.email %></td>
      <td><%= @user.username %></td>
      <td>
        <a href="#" phx-click="upvote" phx-target="<%= @myself %>">
          <%= @user.upvote_count %>
        </a>
      </td>
      <td>
        <a href="#" phx-click="downvote" phx-target="<%= @myself %>">
          <%= @user.downvote_count %>
        </a>
      </td>
      <td><%= @user.upvote_count - @user.downvote_count %></td>

      <td>
        <span><%= live_redirect "Show", to: Routes.user_show_path(@socket, :show, @user) %></span>
        <span><%= live_patch "Edit", to: Routes.user_index_path(@socket, :edit, @user) %></span>
        <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: @user.id, data: [confirm: "Are you sure?"] %></span>
      </td>
    </tr>
    """
  end

  def handle_event("upvote", _, socket) do
    Feenix.Accounts.increment_upvote(socket.assigns.user)
    {:noreply, socket}
  end

  def handle_event("downvote", _, socket) do
    Feenix.Accounts.increment_downvote(socket.assigns.user)
    {:noreply, socket}
  end

end

Part 4 - send an email on signup