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

<!--more-->

在第 6 局部中,咱们增加了主页,在这部分中,咱们将钻研顶部题目导航菜单中的搜寻性能。您能够赶上Instagram 克隆 GitHub Repo。

搜寻性能将提供按用户名或全名搜寻用户的能力,咱们只须要一个蕴含头像 URL、用户名和全名的地图,让咱们增加一个函数来在咱们的帐户上下文中获取它。外面lib/instagram_clone/accounts.ex

...  def search_users(q) do    User    |> where([u], ilike(u.username, ^"%#{q}%"))    |> or_where([u], ilike(u.full_name, ^"%#{q}%"))    |> select([u], map(u, [:avatar_url, :username, :full_name]))    |> Repo.all()  end...

咱们将解决该事件以在标头导航组件 open 中进行搜寻lib/instagram_clone_web/templates/layout/live.html.leex,而后将 ID 发送到咱们的组件以便可能解决该事件:

<%= if @current_user do %>  <%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, id: 1, current_user: @current_user %><% else %>  <%= if @live_action !== :root_path do %>    <%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, id: 1, current_user: @current_user %>  <% end %><% end %><main role="main" class="container mx-auto max-w-full md:w-11/12 2xl:w-6/12 pt-24">  <p class="alert alert-info" role="alert"    phx-click="lv:clear-flash"    phx-value-key="info"><%= live_flash(@flash, :info) %></p>  <p class="alert alert-danger" role="alert"    phx-click="lv:clear-flash"    phx-value-key="error"><%= live_flash(@flash, :error) %></p>  <%= @inner_content %></main>

在外面lib/instagram_clone_web/live/header_nav_component.html.leex让咱们应用 AlpineJs 关上 UL,当输出至多有一个字母时,咱们将在其中显示后果,如果外面没有任何内容或单击输出,则不会显示任何内容。让咱们应用phx-change表单事件来运行咱们的搜寻,咱们还将调配给咱们的套接字 a@overflow_y_scroll_ul以在后果大于 6 时显示滚动条。

    <div x-data="{open: false, inputText: null}" class="w-2/5 flex justify-end relative">      <form id="search-users-form" phx-change="search_users" phx-target="<%= @myself %>">        <input          phx-debounce="800"          x-model="inputText"          x-on:input="[(inputText.length != 0) ? open = true : open = false]"          name="q"          type="search"          placeholder="Search"          autocomplete="off"          class="h-7 bg-gray-50 shadow-sm border-gray-300 focus:ring-gray-300 focus:ring-opacity-50 focus:border-gray-400  px-0.5 rounded-sm">      </form>      <ul      x-show="open"      @click.away="open = false"      class="<%= @overflow_y_scroll_ul %> absolute top-10 -right-24 w-96 shadow-md h-96 bg-gray-50">      </ul>    </div>

在咱们将显示搜寻后果的 UL 中,咱们须要 3 个赋值,@searched_users这将是咱们将循环遍历的后果,@while_searching_users?这将是一个布尔值,用于确定在连贯失常的状况下何时显示加载指示器迟缓或须要一段时间,为了用户界面敌对的反馈,@users_not_found?另一个布尔值显示未找到后果音讯。

      <ul      x-show="open"      @click.away="open = false"      class="<%= @overflow_y_scroll_ul %> absolute top-10 -right-24 w-96 shadow-md h-96 bg-gray-50">        <%= for user <- @searched_users do %>          <%= live_redirect to: Routes.user_profile_path(@socket, :index, user.username) do %>            <li class="flex items-center px-4 py-3 hover:bg-gray-100">              <%= img_tag Avatar.get_thumb(user.avatar_url), class: "w-10 h-10 rounded-full object-cover object-center" %>              <div class="ml-3">                <h2 class="truncate font-bold text-sm text-gray-500"><%= user.username %></h2>                <h3 class="truncate text-sm text-gray-500"><%= user.full_name %></h3>              </div>            </li>          <% end %>        <% end %>        <%= if @while_searching_users? do %>          <li class="flex justify-center items-center h-full">            <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>          </svg>          </li>        <% end %>        <%= if @users_not_found? do %>          <li class="text-sm text-gray-400 flex justify-center items-center h-full">No results found.</li>        <% end %>      </ul>      

咱们的更新lib/instagram_clone_web/live/header_nav_component.html.leex应如下所示:

<div class="h-14 border-b-2 flex fixed w-full bg-white z-40">  <header class="flex items-center container mx-auto max-w-full md:w-11/12 2xl:w-6/12">    <%= live_redirect to: Routes.page_path(@socket, :index) do %>      <h1 class="text-2xl font-bold italic">#InstagramClone</h1>    <% end %>    <div x-data="{open: false, inputText: null}" class="w-2/5 flex justify-end relative">      <form id="search-users-form" phx-change="search_users" phx-target="<%= @myself %>">        <input          phx-debounce="800"          x-model="inputText"          x-on:input="[(inputText.length != 0) ? open = true : open = false]"          name="q"          type="search"          placeholder="Search"          autocomplete="off"          class="h-7 bg-gray-50 shadow-sm border-gray-300 focus:ring-gray-300 focus:ring-opacity-50 focus:border-gray-400  px-0.5 rounded-sm">      </form>      <ul      x-show="open"      @click.away="open = false"      class="<%= @overflow_y_scroll_ul %> absolute top-10 -right-24 w-96 shadow-md h-96 bg-gray-50">        <%= for user <- @searched_users do %>          <%= live_redirect to: Routes.user_profile_path(@socket, :index, user.username) do %>            <li class="flex items-center px-4 py-3 hover:bg-gray-100">              <%= img_tag Avatar.get_thumb(user.avatar_url), class: "w-10 h-10 rounded-full object-cover object-center" %>              <div class="ml-3">                <h2 class="truncate font-bold text-sm text-gray-500"><%= user.username %></h2>                <h3 class="truncate text-sm text-gray-500"><%= user.full_name %></h3>              </div>            </li>          <% end %>        <% end %>        <%= if @while_searching_users? do %>          <li class="flex justify-center items-center h-full">            <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>          </svg>          </li>        <% end %>        <%= if @users_not_found? do %>          <li class="text-sm text-gray-400 flex justify-center items-center h-full">No results found.</li>        <% end %>      </ul>    </div>    <nav class="w-3/5 relative">      <ul x-data="{open: false}" class="flex justify-end">        <%= if @current_user do %>          <li class="w-7 h-7 text-gray-600">            <%= live_redirect to: Routes.page_path(@socket, :index) do %>              <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />              </svg>            <% end %>          </li>          <li class="w-7 h-7 ml-6 text-gray-600">            <%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.PostLive.New) do %>              <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />              </svg>            <% end %>          </li>          <li class="w-7 h-7 ml-6 text-gray-600">            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />            </svg>          </li>          <li class="w-7 h-7 ml-6 text-gray-600">            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />            </svg>          </li>          <li class="w-7 h-7 ml-6 text-gray-600">            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />            </svg>          </li>          <li            @click="open = true"            class="w-7 h-7 ml-6 shadow-md rounded-full overflow-hidden cursor-pointer"          >            <%= img_tag InstagramClone.Uploaders.Avatar.get_thumb(@current_user.avatar_url),            class: "w-full h-full object-cover object-center" %>          </li>          <ul class="absolute top-14 w-56 bg-white shadow-md text-sm -right-8"              x-show="open"              @click.away="open = false"              x-transition:enter="transition ease-out duration-200"              x-transition:enter-start="opacity-0 transform scale-90"              x-transition:enter-end="opacity-100 transform scale-100"              x-transition:leave="transition ease-in duration-200"              x-transition:leave-start="opacity-100 transform scale-100"              x-transition:leave-end="opacity-0 transform scale-90"            >              <%= live_redirect to: Routes.user_profile_path(@socket, :index, @current_user.username) do %>                <li class="py-2 px-4 hover:bg-gray-50">Profile</li>              <% end %>              <li class="py-2 px-4 hover:bg-gray-50">Saved</li>              <%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Settings) do %>                <li class="py-2 px-4 hover:bg-gray-50">Settings</li>              <% end %>              <%= link to: Routes.user_session_path(@socket, :delete), method: :delete do %>                <li class="border-t-2 py-2 px-4 hover:bg-gray-50">Log Out</li>              <% end %>            </ul>        <% else %>          <li>            <%= link "Log In", to: Routes.user_session_path(@socket, :new), class: "w-24 py-1 px-3 border-none shadow rounded text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 font-semibold" %>          </li>          <li>            <%= link "Sign Up", to: Routes.user_registration_path(@socket, :new), class: "w-24 py-1 px-3 border-none text-light-blue-500 hover:text-light-blue-600 font-semibold" %>          </li>        <% end %>      </ul>    </nav>  </header></div>

外面lib/instagram_clone_web/live/header_nav_component.ex

defmodule InstagramCloneWeb.HeaderNavComponent do  use InstagramCloneWeb, :live_component  alias InstagramClone.Uploaders.Avatar  @impl true  def mount(socket) do    {:ok,      socket      |> assign(while_searching_users?: false)      |> assign(users_not_found?: false)      |> assign(overflow_y_scroll_ul: "")      |> assign(searched_users: [])}  end  @impl true  def handle_event("search_users", %{"q" => search}, socket) do    if search == "" do      {:noreply, socket}    else      send(self(), {__MODULE__, :search_users_event, search})      {:noreply,        socket        |> assign(users_not_found?: false)        |> assign(searched_users: [])        |> assign(overflow_y_scroll_ul: "")        |> assign(while_searching_users?: true)}    end  endend

在咱们的处理事件函数中,首先,咱们查看参数是否为空字符串,什么都不会产生。当参数不为空时,咱们将发送一条带有搜寻参数的音讯,以在父 LiveView 中运行搜寻,这样咱们就能够在搜寻时显示加载指示器,每次表单更改时,咱们都必须重置咱们的调配,设置while_searching_users?boolean true 以在搜寻时显示加载指示器。

咱们必须发送音讯,因为如果咱们尝试在标头导航组件套接字中执行此操作,则调配首先同时产生,因而如果咱们这样做,咱们将无奈在搜寻时以及在组件,咱们无奈将handle_info其音讯发送到父级并将父级中的调配更新回组件。

题目导航组件在每个页面上都应用,因而咱们不用在每个 LiveView 上解决音讯,而是为每个 LiveView 解决一次,在第lib/instagram_clone_web.ex45 行函数内live_view()增加以下内容:

...  def live_view do    quote do      use Phoenix.LiveView,        layout: {InstagramCloneWeb.LayoutView, "live.html"}      unquote(view_helpers())      import InstagramCloneWeb.LiveHelpers      alias InstagramClone.Accounts.User      alias InstagramClone.Accounts            @impl true      def handle_info(%{event: "logout_user", payload: %{user: %User{id: id}}}, socket) do        with %User{id: ^id} <- socket.assigns.current_user do          {:noreply,            socket            |> redirect(to: "/")            |> put_flash(:info, "Logged out successfully.")}        else          _any -> {:noreply, socket}        end      end      @impl true      def handle_info({InstagramCloneWeb.HeaderNavComponent, :search_users_event, search}, socket) do        case Accounts.search_users(search) do          [] ->            send_update(InstagramCloneWeb.HeaderNavComponent,              id: 1,              searched_users: [],              users_not_found?: true,              while_searching_users?: false            )            {:noreply, socket}          users ->            send_update(InstagramCloneWeb.HeaderNavComponent,              id: 1,              searched_users: users,              users_not_found?: false,              while_searching_users?: false,              overflow_y_scroll_ul: check_search_result(users)            )            {:noreply, socket}        end      end      defp check_search_result(users) do        if length(users) > 6, do: "overflow-y-scroll", else: ""      end    end  end...

咱们应用在帐户上下文中增加的性能创立一个案例,咱们send_update/3增加到题目导航组件中,while_searching_users?在每个案例上设置为 false,以便在搜寻实现时不显示加载指示器。

就是这样,当初你有了一个功能齐全的搜寻输出,还有很多事件要做,很多能够增加的性能,但咱们曾经走了很长一段路,咱们有一个值得咱们骄傲的大应用程序,直到下一个工夫。

转自:Elixirprogrammer