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
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"
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)"
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
#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)
# 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