应用PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的Instagram Web应用程序

<!--more-->

在第 1 局部中,咱们已实现所有设置并筹备好根本布局,让咱们开始解决用户设置。您能够赶上Instagram 克隆 GitHub Repo。

让咱们首先创立路由,关上lib/instagram_clone_web/router.ex并在范畴下增加以下 2 条路由:require_authenticated_user

  scope "/", InstagramCloneWeb do    pipe_through [:browser, :require_authenticated_user]    get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email    live "/accounts/edit", UserLive.Settings    live "/accounts/password/change", UserLive.PassSettings  end

而后咱们须要创立这些 liveview 文件,在该文件夹内创立一个名为user_liveunder 的文件夹lib/instagram_clone_web/live,增加以下 4 个文件:

lib/instagram_clone_web/live/user_live/settings.ex lib/instagram_clone_web/live/user_live/settings.html.leex lib/instagram_clone_web/live/user_live/pass_settings.ex lib/instagram_clone_web/live/user_live/pass_settings.html.leex

在咱们的导航题目中,咱们须要链接到该新路线,lib/instagram_clone_web/live/header_nav_component.html.leex在第 60 行关上,将以下内容增加到Settings live_patch to:

<%= live_patch to:  Routes.live_path(@socket, InstagramCloneWeb.UserLive.Settings)  do  %>  <li  class="py-2 px-4 hover:bg-gray-50">Settings</li><%  end  %>

当初,当咱们拜访该链接时,咱们应该会呈现谬误,因为文件是空的,因而关上lib/instagram_clone_web/live/user_live/settings.ex并增加以下内容:

defmodule  InstagramCloneWeb.UserLive.Settings  do  use InstagramCloneWeb,  :live_view  @impl true  def  mount(_params, session, socket) do    socket =  assign_defaults(session, socket)    {:ok, socket}  endend

当初咱们应该有一个空白页面,只有顶部导航栏,所以让咱们开始工作吧。

咱们将须要AccountsUser上下文,咱们将为它们增加别名并调配变更集,咱们的文件应如下所示:

defmodule  InstagramCloneWeb.UserLive.Settings  do  use InstagramCloneWeb,  :live_view  alias InstagramClone.Accounts  alias InstagramClone.Accounts.User  @impl true  def  mount(_params, session, socket) do    socket =  assign_defaults(session, socket)    changeset = Accounts.change_user(socket.assigns.current_user)    {:ok,       socket      |>  assign(changeset: changeset)}  endend

咱们须要将change_user()函数增加到Accounts上下文中,关上lib/instagram_clone/accounts.ex并在change_user_registration()函数上面增加以下内容:

...def  change_user(user, attrs \\  %{}) do  User.registration_changeset(user, attrs,  register_user:  false)end...

关上lib/instagram_clone_web/live/user_live/settings.html.leex并将表单增加到模板中:

<section class="border-2 flex" x-data="{username: '<%= @current_user.username %>'}">  <div class="w-full py-8">    <!-- Profile Photo -->    <div class="flex items-center">      <div class="w-1/3">        <%= img_tag @current_user.avatar_url, class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %>      </div>      <div class="w-full pl-8">        <h1 x-text="username" class="leading-none font-semibold text-lg truncate text-gray-500"></h1>      </div>    </div>    <!-- END PROFILE PHOTO -->    <%= f = form_for @changeset, "#",      phx_change: "validate",      phx_submit: "save",      class: "space-y-8 md:space-y-10" %>      <div class="flex items-center">        <%= label f, :full_name, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= text_input f, :full_name, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %>          <%= error_tag f, :full_name, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <%= label f, :username, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= text_input f, :username, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", x_model: "username", autocomplete: "off" %>          <%= error_tag f, :username, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <%= label f, :website, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= text_input f, :website, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %>          <%= error_tag f, :website, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <%= label f, :bio, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= textarea f, :bio, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", rows: 3, autocomplete: "off" %>          <%= error_tag f, :bio, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <%= label f, :email, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= email_input f, :email, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 shadow-sm focus:ring-transparent focus:border-black", autocomplete: "off" %>          <%= error_tag f, :email, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <label class="block w-1/3 font-semibold text-right"></label>        <div class="w-full pl-8 pr-20">          <%= submit "Submit", phx_disable_with: "Saving...", class: "w-16 py-1 px-1 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>        </div>      </div>    </form>  </div></section>

咱们增加了表单的根本布局,当您应用 AlpineJs 输出用户名时,用户名题目会更新。当初咱们须要将validate()save()函数增加到咱们的lib/instagram_clone_web/live/user_live/settings.ex实时查看文档中,但咱们首先将咱们调配:page_title给咱们的挂载函数:

  @impl true  def mount(_params, session, socket) do    socket = assign_defaults(session, socket)    changeset = Accounts.change_user(socket.assigns.current_user)    {:ok,      socket      |> assign(changeset: changeset)      |> assign(page_title: "Edit Profile")} #This was added  end

而后关上lib/instagram_clone_web/templates/layout/root.html.leex并更新页面题目后缀:

<%= live_title_tag assigns[:page_title]  ||  "InstagramClone",  suffix:  " · InstagramClone"  %>

当初让咱们将解决表单的函数增加到咱们的lib/instagram_clone_web/live/user_live/settings.ex

  @impl true  def handle_event("validate", %{"user" => user_params}, socket) do    changeset =      socket.assigns.current_user      |> Accounts.change_user(user_params)      |> Map.put(:action, :validate)    {:noreply, socket |> assign(changeset: changeset)}  end  @impl true  def handle_event("save", %{"user" => user_params}, socket) do    case Accounts.update_user(socket.assigns.current_user, user_params) do      {:ok, _user} ->        {:noreply,          socket          |> put_flash(:info, "User updated successfully")          |> push_redirect(to: Routes.live_path(socket, InstagramWeb.UserLive.Settings))}      {:error, %Ecto.Changeset{} = changeset} ->        {:noreply, assign(socket, :changeset, changeset)}    end  end

当初咱们须要将update_user()函数增加到Accounts上下文中:

...  def update_user(user, attrs) do    user    |> User.registration_changeset(attrs, register_user: false)    |> Repo.update()  end...

咱们对用户名的惟一束缚不起作用,因为咱们没有在迁徙中增加惟一索引,所以咱们当初就这样做。在咱们的终端中,让咱们生成一个迁徙$ mix ecto.gen.migration add_users_unique_username_index,而后关上生成的迁徙priv/repo/migrations/20210414220125_add_users_unique_username_index.exs并增加以下内容:

defmodule InstagramClone.Repo.Migrations.AddUsersUniqueUsernameIndex do  use Ecto.Migration  def change do    create unique_index(:users, [:username])  endend

而后返回咱们的终端并运行迁徙$ mix ecto.migrate

当初让咱们更新咱们的注册变更lib/instagram_clone/accounts/user.exunsafe_validate_unique(:username, InstagramClone.Repo)

...  def registration_changeset(user, attrs, opts \\ []) do    user    |> cast(attrs, [:email, :password, :username, :full_name, :avatar_url, :bio, :website])    |> validate_required([:username, :full_name])    |> validate_length(:username, min: 5, max: 30)    |> validate_format(:username, ~r/^[a-zA-Z0-9_.-]*$/, message: "Please use letters and numbers without space(only characters allowed _ . -)")    |> unique_constraint(:username)    |> unsafe_validate_unique(:username, InstagramClone.Repo) # --> This was added    |> validate_length(:full_name, min: 4, max: 30)    |> validate_email()    |> validate_password(opts) end...

:timer.sleep(9000)另外,在测试时,我意识到我在尝试提早实时验证时犯了一个谬误,lib/instagram_clone_web/live/page_live.ex所以让咱们从validate()函数中删除该行,因为它会与表单产生抵触:

  @impl true  def handle_event("validate", %{"user" => user_params}, socket) do    changeset =      %User{}      |> User.registration_changeset(user_params)      |> Map.put(:action, :validate)    #:timer.sleep(9000) <-- REMOVE THIS LINE    {:noreply, socket |> assign(changeset: changeset)}  end

实现后,咱们应该可能毫无问题地编辑配置文件,所以当初让咱们开始上传头像文件。

头像上传

关上lib/instagram_clone_web/live/user_live/settings.ex并容许在实时视图中上传,新的更新文件应如下所示:

defmodule InstagramCloneWeb.UserLive.Settings do  use InstagramCloneWeb, :live_view  alias InstagramClone.Accounts  alias InstagramClone.Accounts.User  #Files extensions accepted to be uploaded  @extension_whitelist ~w(.jpg .jpeg .png)  @impl true  def mount(_params, session, socket) do    socket = assign_defaults(session, socket)    changeset = Accounts.change_user(socket.assigns.current_user)    {:ok,      socket      |> assign(changeset: changeset)      |> assign(page_title: "Edit Profile")      |> allow_upload(:avatar_url,      accept: @extension_whitelist,      max_file_size: 9_000_000,      progress: &handle_progress/3,#Function that will handle automatic uploads      auto_upload: true)}  end  @impl true  def handle_event("validate", %{"user" => user_params}, socket) do    changeset =      socket.assigns.current_user      |> Accounts.change_user(user_params)      |> Map.put(:action, :validate)    {:noreply, socket |> assign(changeset: changeset)}  end  # Updates the socket when the upload form changes, triguers handle_progress()  def handle_event("upload_avatar", _params, socket) do    {:noreply, socket}  end  @impl true  def handle_event("save", %{"user" => user_params}, socket) do    case Accounts.update_user(socket.assigns.current_user, user_params) do      {:ok, _user} ->        {:noreply,          socket          |> put_flash(:info, "User updated successfully")          |> push_redirect(to: Routes.live_path(socket, InstagramCloneWeb.UserLive.Settings))}      {:error, %Ecto.Changeset{} = changeset} ->        {:noreply, assign(socket, :changeset, changeset)}    end  end  # This will handle the upload  defp handle_progress(:avatar_url, entry, socket) do  endend

关上lib/instagram_clone_web/live/user_live/settings.html.leex并在用户名题目下方增加上传表单,并将该表单@uploads调配给咱们的套接字:

<section class="border-2 flex" x-data="{username: '<%= @current_user.username %>'}">  <div class="w-full py-8">    <%= for {_ref, err} <- @uploads.avatar_url.errors do %>      <p class="text-red-500 w-full text-center">        <%= Phoenix.Naming.humanize(err) %>      </p>    <% end %>    <!-- Profile Photo -->    <div class="flex items-center">      <div class="w-1/3">        <%= img_tag @current_user.avatar_url, class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %>      </div>      <div class="w-full pl-8">        <h1 x-text="username" class="leading-none font-semibold text-lg truncate text-gray-500"></h1>                <!-- THIS WAS ADDED -->        <div  class="relative">          <%= form_for @changeset,  "#",            phx_change:  "upload_avatar"  %>            <%= live_file_input @uploads.avatar_url,  class:  "cursor-pointer relative block opacity-0 z-40 -left-24"  %>            <div  class="text-center absolute top-0 left-0 m-auto">              <span  class="font-semibold text-sm text-light-blue-500">                Change Profile Photo              </span>            </div>          </form>        </div>        <!-- THIS WAS ADDED END -->              </div>    </div>    <!-- END PROFILE PHOTO -->    <%= f = form_for @changeset, "#",      phx_change: "validate",      phx_submit: "save",      class: "space-y-8 md:space-y-10" %>      <div class="flex items-center">        <%= label f, :full_name, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= text_input f, :full_name, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %>          <%= error_tag f, :full_name, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <%= label f, :username, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= text_input f, :username, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", x_model: "username", autocomplete: "off" %>          <%= error_tag f, :username, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <%= label f, :website, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= text_input f, :website, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %>          <%= error_tag f, :website, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <%= label f, :bio, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= textarea f, :bio, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", rows: 3, autocomplete: "off" %>          <%= error_tag f, :bio, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <%= label f, :email, class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= email_input f, :email, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 shadow-sm focus:ring-transparent focus:border-black", autocomplete: "off" %>          <%= error_tag f, :email, class:  "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <label class="block w-1/3 font-semibold text-right"></label>        <div class="w-full pl-8 pr-20">          <%= submit "Submit", phx_disable_with: "Saving...", class: "w-16 py-1 px-1 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>        </div>      </div>    </form>  </div></section>

当初让咱们创立一个模块来帮忙咱们解决头像上传。在上面lib/instagram_clone_web/live增加一个名为 的文件夹uploaders,并在该文件夹内增加一个名为 的文件avatar.ex。咱们将调整头像的大小,因而让咱们增加 Mogrify 依赖项来解决它,确保装置了 ImageMagick,关上mix.exs并增加到咱们的我的项目依赖项{:mogrify, "~> 0.8.0"},而后在咱们的终端中$ mix deps.get && mix deps.compile

当初关上lib/instagram_clone_web/live/uploaders/avatar.ex并增加以下内容:

defmodule InstagramClone.Uploaders.Avatar do  alias InstagramCloneWeb.Router.Helpers, as: Routes    # We are going to upload locally so this would be the name of the folder  @upload_directory_name "uploads"  @upload_directory_path "priv/static/uploads"    # Returns the extensions associated with a given MIME type.  defp ext(entry) do    [ext | _] = MIME.extensions(entry.client_type)    ext  end    # Returns the url path  def get_avatar_url(socket, entry) do    Routes.static_path(socket, "/#{@upload_directory_name}/#{entry.uuid}.#{ext(entry)}")  end  def update(socket, old_url, entry) do    # Creates the upload directry path if not exists     if !File.exists?(@upload_directory_path), do: File.mkdir!(@upload_directory_path)        # Consumes an individual uploaded entry    Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn %{} = meta ->      # Destination paths for avatar thumbs      dest = Path.join(@upload_directory_path, "#{entry.uuid}.#{ext(entry)}")      dest_thumb = Path.join(@upload_directory_path, "thumb_#{entry.uuid}.#{ext(entry)}")            # meta.path is the temporary file path      mogrify_thumbnail(meta.path, dest, 300)      mogrify_thumbnail(meta.path, dest_thumb, 150)            # Removes Old Urls Paths      rm_file(old_url)      old_url |> get_thumb() |> rm_file()    end)    :ok  end  def get_thumb(avatar_url) do    file_name = String.replace_leading(avatar_url, "/uploads/", "")    ["/#{@upload_directory_name}", "thumb_#{file_name}"] |> Path.join()  end  def rm_file(old_avatar_url) do    url = String.replace_leading(old_avatar_url, "/uploads/", "")    path = [@upload_directory_path, url] |> Path.join()    if File.exists?(path),  do: File.rm!(path)  end    # Resize the file with a given path, destination, and size  defp mogrify_thumbnail(src_path, dst_path, size) do    try do      Mogrify.open(src_path)      |> Mogrify.resize_to_limit("#{size}x#{size}")      |> Mogrify.save(path: dst_path)    rescue      File.Error -> {:error, :invalid_src_path}      error -> {:error, error}    else      _image -> {:ok, dst_path}    end  endend

在文件顶部关上lib/instagram_clone_web/live/user_live/settings.ex新创建的模块的别名,并应用以下内容更新咱们的函数:Avataralias InstagramClone.Uploaders.Avatarhandle_progress()

  defp handle_progress(:avatar_url, entry, socket) do    # If file is already uploaded to tmp folder    if entry.done? do      avatar_url = Avatar.get_avatar_url(socket, entry)      user_params = %{"avatar_url" => avatar_url}      case Accounts.update_user(socket.assigns.current_user, user_params) do        {:ok, _user} ->          Avatar.update(socket, socket.assigns.current_user.avatar_url, entry)          @doc """            We have to update the current user and assign it back to the socket             to get the header nav thumbnail automatically updated          """          current_user = Accounts.get_user!(socket.assigns.current_user.id)          {:noreply,            socket            |> put_flash(:info, "Avatar updated successfully")            |> assign(current_user: current_user)}        {:error, %Ecto.Changeset{} = changeset} ->          {:noreply, assign(socket, :changeset, changeset)}      end    else      {:noreply, socket}    end  end

最初,咱们须要提供上传目录中将要创立的文件,关上lib/instagram_clone_web/endpoint.ex并更新动态插件中的第 27 行:

only:  ~w(css fonts images js favicon.ico robots.txt uploads)

当初所有都应该工作得很好,但咱们正在上传缩略图,所以让咱们在模板中应用它,关上lib/instagram_clone_web/live/header_nav_component.html.leex并更新第 43 行以应用咱们的缩略图 URL:

<%= img_tag InstagramClone.Uploaders.Avatar.get_thumb(@current_user.avatar_url), class:  "w-full h-full object-cover object-center"  %>

同时关上lib/instagram_clone_web/live/user_live/settings.html.leex并更新第 7 行以应用咱们的缩略图:

<%= img_tag Avatar.get_thumb(@current_user.avatar_url),  class:  "ml-auto w-10 h-10 rounded-full object-cover object-center"  %>

明码更改设置

当初剩下的惟一一件事就是更改明码,咱们将须要一个侧面导航栏,因而让咱们创立一个组件来解决它,因为它将与明码更改 LiveView 共享。在上面lib/instagram_clone_web/live/user_live增加以下2个文件:

lib/instagram_clone_web/live/user_live/settings_sidebar_component.ex lib/instagram_clone_web/live/user_live/settings_sidebar_component.html.leex

增加lib/instagram_clone_web/live/user_live/settings_sidebar_component.ex以下内容:

defmodule  InstagramCloneWeb.UserLive.SettingsSidebarComponent  do  use InstagramCloneWeb,  :live_componentend

增加lib/instagram_clone_web/live/user_live/settings_sidebar_component.html.leex以下内容:

<div class="w-1/4 border-r-2">  <ul>    <%= live_patch content_tag(:li, "Edit Profile", class: "p-4 #{selected_link?(@current_uri_path,  @settings_path)}"),  to:  @settings_path %>    <%= live_patch content_tag(:li, "Change Password", class: "p-4 #{selected_link?(@current_uri_path,  @pass_settings_path)}"),  to:  @pass_settings_path %>  </ul></div>

render_helpers.ex在 下创立一个名为lib/instagram_clone_web/live. 关上lib/instagram_clone_web/live/render_helpers.ex并执行以下操作::

defmodule  InstagramCloneWeb.RenderHelpers  do  def  selected_link?(current_uri, menu_link) when current_uri == menu_link do    "border-l-2 border-black -ml-0.5 text-gray-900 font-semibold"  end    def  selected_link?(_current_uri,  _menu_link) do    "hover:border-l-2 -ml-0.5 hover:border-gray-300 hover:bg-gray-50"  endend

这些性能将帮忙咱们为侧面导航栏中的链接获取正确的款式。当初咱们须要在模板中提供这些函数,关上lib/instagram_clone_web.ex视图助手函数并将其更新为以下内容:

  defp view_helpers do    quote do      # Use all HTML functionality (forms, tags, etc)      use Phoenix.HTML      # Import LiveView helpers (live_render, live_component, live_patch, etc)      import Phoenix.LiveView.Helpers      # Import basic rendering functionality (render, render_layout, etc)      import Phoenix.View      import InstagramCloneWeb.ErrorHelpers      import InstagramCloneWeb.Gettext      import InstagramCloneWeb.RenderHelpers # <-- THIS LINE WAS ADDED      alias InstagramCloneWeb.Router.Helpers, as: Routes    end  end

让咱们将门路调配给套接字,关上lib/instagram_clone_web/live/user_live/settings.ex并在装置中增加以下内容:

  def mount(_params, session, socket) do    socket = assign_defaults(session, socket)    changeset = Accounts.change_user(socket.assigns.current_user)    # THIS WAS ADDED    settings_path = Routes.live_path(socket, __MODULE__)    pass_settings_path = Routes.live_path(socket, InstagramCloneWeb.UserLive.PassSettings)    {:ok,      socket      |> assign(changeset: changeset)      |> assign(page_title: "Edit Profile")      |> assign(settings_path: settings_path, pass_settings_path: pass_settings_path)# <-- THIS WAS ADDED      |> allow_upload(:avatar_url,      accept: @extension_whitelist,      max_file_size: 9_000_000,      progress: &handle_progress/3,      auto_upload: true)}  end

lib/instagram_clone_web/live/user_live/settings.html.leex在标签结尾下方顶部的局部标签内关上,让咱们插入咱们的组件:

<%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent,    settings_path:  @settings_path,    pass_settings_path:  @pass_settings_path,    current_uri_path:  @current_uri_path %>

关上lib/instagram_clone_web/live/user_live/pass_settings.ex增加以下内容:

defmodule InstagramCloneWeb.UserLive.PassSettings do  use InstagramCloneWeb, :live_view  def mount(_params, session, socket) do    socket = assign_defaults(session, socket)    settings_path = Routes.live_path(socket, InstagramCloneWeb.UserLive.Settings)    pass_settings_path = Routes.live_path(socket, __MODULE__)    {:ok,      socket      |> assign(settings_path: settings_path, pass_settings_path: pass_settings_path)}  endend

而后关上lib/instagram_clone_web/live/user_live/pass_settings.html.leex增加以下内容:

<section class="border-2 flex">  <%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent,    settings_path: @settings_path,    pass_settings_path: @pass_settings_path,    current_uri_path: @current_uri_path %></section>

让咱们将表单增加到lib/instagram_clone_web/live/user_live/pass_settings.html.leex

<section class="border-2 flex">  <%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent,    settings_path: @settings_path,    pass_settings_path: @pass_settings_path,    current_uri_path: @current_uri_path %>  <div class="w-full py-5">    <!-- Profile Photo -->    <div class="flex items-center">      <div class="w-1/3">        <%= img_tag Avatar.get_thumb(@current_user.avatar_url), class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %>      </div>      <div class="w-full pl-8">        <h1 class="font-semibold text-xl truncate text-gray-600"><%= @current_user.username %></h1>      </div>    </div>    <!-- End Profile Photo -->    <%= f = form_for @changeset, "#",      phx_submit: "save",      class: "space-y-5 md:space-y-8"  %>      <div class="md:flex items-center">        <%= label f, :old_password, "Old Password", class: "w-1/3 text-right font-semibold", for: "current_password_for_password" %>        <div class="w-full pl-8 pr-20">          <%= password_input f, :current_password, required: true, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black" %>          <%= error_tag f, :current_password, class: "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <%= label f, :password, "New Password", class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= password_input f, :password, required: true, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black" %>          <%= error_tag f, :password, class: "text-red-700 text-sm block" %>        </div>      </div>      <div class="md:flex items-center">        <%= label f, :password_confirmation, "Confirm New Password", class: "w-1/3 text-right font-semibold" %>        <div class="w-full pl-8 pr-20">          <%= password_input f, :password_confirmation, required: true, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black" %>          <%= error_tag f, :password_confirmation, class: "text-red-700 text-sm block" %>        </div>      </div>      <div class="flex items-center">        <label class="w-1/3"></label>        <div class="w-full pl-8 pr-20">          <%= submit "Change Password", phx_disable_with: "Saving...", class: "py-1 px-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>        </div>      </div>      <div class="flex items-center">        <label class="w-1/3"></label>        <div class="w-full pl-8 pr-20 text-right">          <%= link "Forgot Password?", to: Routes.user_reset_password_path(@socket, :new), class: "font-semibold text-xs hover:text-light-blue-600 text-light-blue-500 cursor-pointer hover:underline" %>        </div>      </div>    </form>  </div></section>

最初更新lib/instagram_clone_web/live/user_live/pass_settings.ex如下:

defmodule InstagramCloneWeb.UserLive.PassSettings do  use InstagramCloneWeb, :live_view  alias InstagramClone.Accounts  alias InstagramClone.Accounts.User  alias InstagramClone.Uploaders.Avatar  @impl true  def mount(_params, session, socket) do    socket = assign_defaults(session, socket)    settings_path = Routes.live_path(socket, InstagramCloneWeb.UserLive.Settings)    pass_settings_path = Routes.live_path(socket, __MODULE__)    user = socket.assigns.current_user    {:ok,      socket      |> assign(settings_path: settings_path, pass_settings_path: pass_settings_path)      |> assign(:page_title, "Change Password")      |> assign(changeset: Accounts.change_user_password(user))}  end  @impl true  def handle_event("save", %{"user" => params}, socket) do    %{"current_password" => password} = params    case Accounts.update_user_password(socket.assigns.current_user, password, params) do      {:ok, _user} ->        {:noreply,          socket          |> put_flash(:info, "Password updated successfully.")          |> push_redirect(to: socket.assigns.pass_settings_path)}      {:error, changeset} ->        {:noreply, assign(socket, :changeset, changeset)}    end  endend

转到lib/instagram_clone/accounts.ex第 208 行,并更新update_user_password()为以下内容:

def  update_user_password(user, password, attrs) do  user  |> User.password_changeset(attrs)  |> User.validate_current_password(password)  |> Repo.update()end

在下一部分中,咱们将解决用户的个人资料。

转自:Elixirprogrammer