应用PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的Instagram Web应用程序
<!--more-->
在第 4 局部中,咱们增加了个人资料帖子局部和帖子页面,在这部分中,咱们将解决显示帖子页面。您能够赶上Instagram 克隆 GitHub Repo。
让咱们首先为显示页面增加根本模板,关上lib/instagram_clone_web/live/post_live/show.html.leex
并增加以下内容:
<section class="flex"> <!-- Post Image section --> <%= img_tag @post.photo_url, class: "w-3/5 object-contain h-full" %> <!-- End Post Image section --> <div class="w-2/5 border-2 h-full"> <div class="flex p-4 items-center border-b-2"> <!-- Post header section --> <%= live_redirect to: Routes.user_profile_path(@socket, :index, @post.user.username) do %> <%= img_tag @post.user.avatar_url, class: "w-8 h-8 rounded-full object-cover object-center" %> <% end %> <div class="ml-3"> <%= live_redirect @post.user.username, to: Routes.user_profile_path(@socket, :index, @post.user.username), class: "truncate font-bold text-sm text-gray-500 hover:underline" %> </div> <!-- End post header section --> </div> <div class="no-scrollbar h-96 overflow-y-scroll p-4 flex flex-col"> <%= if @post.description do %> <!-- Description section --> <div class="flex mt-2"> <%= live_redirect to: Routes.user_profile_path(@socket, :index, @post.user.username) do %> <%= img_tag Avatar.get_thumb(@post.user.avatar_url), class: "w-8 h-8 rounded-full object-cover object-center" %> <% end %> <div class="px-4 w-11/12"> <%= live_redirect @post.user.username, to: Routes.user_profile_path(@socket, :index, @post.user.username), class: "font-bold text-sm text-gray-500 hover:underline" %> <span class="text-sm text-gray-700"> <p class="inline"><%= @post.description %></p></span> </span> <div class="flex mt-3"> <div class="text-gray-400 text-xs"><%= Timex.from_now @post.inserted_at %></div> </div> </div> </div> <!-- End Description Section --> <% end %> </div> <div class="w-full border-t-2"> <!-- Action icons section --> <div class="flex pl-4 pr-2 pt-2"> <div class="w-8 h-8 cursor-pointer"> <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="1" 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> <svg class="hidden text-red-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd" /> </svg> </div> <div class="ml-4 w-8 h-8 cursor-pointer"> <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="1" 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> </div> <div class="ml-4 w-8 h-8 cursor-pointer"> <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="1" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" /> </svg> </div> <div class="w-8 h-8 ml-auto cursor-pointer"> <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="1" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /> </svg> </div> </div> <!-- End Action icons section --> <!-- Description section --> <button class="px-5 text-xs text-gray-500 font-bold focus:outline-none"><%= @post.total_likes %> likes</button> <h6 class="px-5 text-xs text-gray-400"><%= Timex.format!(@post.inserted_at, "{Mfull} {D}, {YYYY}") %></h6> <!-- End Description Section --> <!-- Comment input section --> <div class="p-2 flex items-center mt-3 border-t-2 border-gray-100"> <div class="w-full"> <textarea aria-label="Add a comment..." placeholder="Add a comment..." class="w-full border-0 focus:ring-transparent resize-none" autocomplete="off" autocorrect="off" rows="1"></textarea> </div> <div><button class="text-light-blue-500 font-bold pb-2 text-sm">Post</button></div> </div> <!-- End Comment input section --> </div> </div></section>
关上assets/css/app.scss
并将以下款式增加到文件底部,以使页面评论局部不显示滚动条:
/* Chrome, Safari and Opera */.no-scrollbar::-webkit-scrollbar { display: none;}.no-scrollbar { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */}
喜爱
让咱们在终端中创立喜爱的上下文:
$ mix phx.gen.context Likes Like likes user_id:references:users liked_id:integer
在生成的迁徙中:
defmodule InstagramClone.Repo.Migrations.CreateLikes do use Ecto.Migration def change do create table(:likes) do add :liked_id, :integer add :user_id, references(:users, on_delete: :nothing) timestamps() end create index(:likes, [:user_id, :liked_id]) endend
回到咱们的终端:$ mix ecto.migrate
外面lib/instagram_clone/likes/like.ex
:
defmodule InstagramClone.Likes.Like do use Ecto.Schema schema "posts_likes" do field :liked_id, :integer belongs_to :user, InstagramClone.Accounts.User timestamps() endend
将喜爱关系增加到帖子架构中,关上lib/instagram_clone/posts/post.ex
:
...has_many :likes, InstagramClone.Likes.Like, foreign_key: :liked_id...
将喜爱关系增加到用户架构中,关上lib/instagram_clone/accounts/user.ex
:
...has_many :likes, InstagramClone.Likes.Like...
外面lib/instagram_clone/likes.ex
:
defmodule InstagramClone.Likes do import Ecto.Query, warn: false alias InstagramClone.Repo alias InstagramClone.Likes.Like def create_like(user, liked) do user = Ecto.build_assoc(user, :likes) like = Ecto.build_assoc(liked, :likes, user) update_total_likes = liked.__struct__ |> where(id: ^liked.id) Ecto.Multi.new() |> Ecto.Multi.insert(:like, like) |> Ecto.Multi.update_all(:update_total_likes, update_total_likes, inc: [total_likes: 1]) |> Repo.transaction() end def unlike(user_id, liked) do like = get_like(user_id, liked) update_total_likes = liked.__struct__ |> where(id: ^liked.id) Ecto.Multi.new() |> Ecto.Multi.delete(:like, like) |> Ecto.Multi.update_all(:update_total_likes, update_total_likes, inc: [total_likes: -1]) |> Repo.transaction() end # Returns nil if not found defp get_like(user_id, liked) do Enum.find(liked.likes, fn l -> l.user_id == user_id end) endend
让咱们创立一个组件来解决点赞,在上面lib/instagram_clone_web/live/post_live
增加一个名为的文件like_component.ex
并增加以下内容:
defmodule InstagramCloneWeb.PostLive.LikeComponent do use InstagramCloneWeb, :live_component alias InstagramClone.Likes @impl true def update(assigns, socket) do get_btn_status(socket, assigns) end @impl true def render(assigns) do ~L""" <button phx-target="<%= @myself %>" phx-click="toggle-status" class="<%= @w_h %> focus:outline-none"> <%= @icon %> </button> """ end @impl true def handle_event("toggle-status", _params, socket) do current_user = socket.assigns.current_user liked = socket.assigns.liked if liked?(current_user.id, liked.likes) do unlike(socket, current_user.id, liked) else like(socket, current_user, liked) end end defp like(socket, current_user, liked) do Likes.create_like(current_user, liked) send_msg(liked) {:noreply, socket |> assign(icon: unlike_icon(socket.assigns))} end defp unlike(socket, current_user_id, liked) do Likes.unlike(current_user_id, liked) send_msg(liked) {:noreply, socket |> assign(icon: like_icon(socket.assigns))} end defp send_msg(liked) do msg = get_struct_msg_atom(liked) send(self(), {__MODULE__, msg, liked.id}) end defp get_btn_status(socket, assigns) do if liked?(assigns.current_user.id, assigns.liked.likes) do get_socket_assigns(socket, assigns, unlike_icon(assigns)) else get_socket_assigns(socket, assigns, like_icon(assigns)) end end defp get_socket_assigns(socket, assigns, icon) do {:ok, socket |> assign(assigns) |> assign(icon: icon)} end defp get_struct_name(struct) do struct.__struct__ |> Module.split() |> List.last() |> String.downcase() end defp get_struct_msg_atom(struct) do name = get_struct_name(struct) update_struct_likes = "update_#{name}_likes" String.to_atom(update_struct_likes) end defp like_icon(assigns) do ~L""" <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="1" 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> """ end defp unlike_icon(assigns) do ~L""" <svg class="text-red-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd" /> </svg> """ end # Returns true if id found in list defp liked?(user_id, likes) do Enum.any?(likes, fn l -> l.user_id == user_id end) endend
在第 50 行内lib/instagram_clone_web/live/post_live/show.html.leex
,将蕴含心形图标的 div 替换为以下内容:
... <%= if @current_user do %> <%= live_component @socket, InstagramCloneWeb.PostLive.LikeComponent, id: @post.id, liked: @post, w_h: "w-8 h-8", current_user: @current_user %> <% else %> <%= link to: Routes.user_session_path(@socket, :new) do %> <button class="w-8 h-8 focus:outline-none"> <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="1" 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> </button> <% end %> <% end %> ...
在外部lib/instagram_clone_web/live/post_live/show.ex
咱们须要解决从组件发送的音讯以更新喜爱计数:
... alias InstagramCloneWeb.PostLive.LikeComponent @impl true def handle_info({LikeComponent, :update_post_likes, post_id}, socket) do {:noreply, socket |> assign(post: Posts.get_post!(post_id))} end
关上并lib/instagram_clone/posts.ex
更新函数来预加载belongs_to等用户:get_post!()
get_post_by_url()
... def get_post!(id) do Repo.get!(Post, id) |> Repo.preload([:user, :likes]) end def get_post_by_url!(id) do Repo.get_by!(Post, url_id: id) |> Repo.preload([:user, :likes]) end...
发表评论
让咱们为评论创立一个评论上下文,在终端中输出以下命令:
$ mix phx.gen.context Comments Comment comments post_id:references:posts user_id:references:users body:text total_likes:integer
在生成的迁徙中:
defmodule InstagramClone.Repo.Migrations.CreateComments do use Ecto.Migration def change do create table(:comments) do add :body, :text add :total_likes, :integer, default: 0 add :post_id, references(:posts, on_delete: :nothing) add :user_id, references(:users, on_delete: :nothing) timestamps() end create index(:comments, [:post_id]) create index(:comments, [:user_id]) endend
回到咱们的终端:$ mix ecto.migrate
外面lib/instagram_clone/comments/comment.ex
:
defmodule InstagramClone.Comments.Comment do use Ecto.Schema import Ecto.Changeset schema "comments" do field :body, :string field :total_likes, :integer, default: 0 belongs_to :post, InstagramClone.Posts.Post belongs_to :user, InstagramClone.Accounts.User has_many :likes, InstagramClone.Likes.Like, foreign_key: :liked_id timestamps() end @doc false def changeset(comment, attrs) do comment |> cast(attrs, [:body]) |> validate_required([:body]) endend
lib/instagram_clone/accounts/user.ex
在和内增加以下内容lib/instagram_clone/posts/post.ex
:
... has_many :comments, InstagramClone.Comments.Comment...
外面lib/instagram_clone/comments.ex
增加以下函数:
... @doc """ Returns paginated comments sorted by current user id or by id if public """ def list_post_comments(assigns, public: public) do user = assigns.current_user post_id = assigns.post.id per_page = assigns.per_page page = assigns.page Comment |> where(post_id: ^post_id) |> get_post_comments_sorting(public, user) |> limit(^per_page) |> offset(^((page - 1) * per_page)) |> preload([:user, :likes]) |> Repo.all end defp get_post_comments_sorting(module, public, user) do if public do order_by(module, asc: :id) else order_by(module, fragment("(CASE WHEN user_id = ? then 1 else 2 end)", ^user.id)) end end @doc """ Gets a single comment. Raises `Ecto.NoResultsError` if the Comment does not exist. ## Examples iex> get_comment!(123) %Comment{} iex> get_comment!(456) ** (Ecto.NoResultsError) """ def get_comment!(id) do Repo.get!(Comment, id) |> Repo.preload([:user, :likes]) end @doc """ Creates a comment and updates total comments count in post Returns the comment created with likes preloaded """ def create_comment(user, post, attrs \\ %{}) do update_total_comments = post.__struct__ |> where(id: ^post.id) comment_attrs = %Comment{} |> Comment.changeset(attrs) comment = comment_attrs |> Ecto.Changeset.put_assoc(:user, user) |> Ecto.Changeset.put_assoc(:post, post) Ecto.Multi.new() |> Ecto.Multi.update_all(:update_total_comments, update_total_comments, inc: [total_comments: 1]) |> Ecto.Multi.insert(:comment, comment) |> Repo.transaction() |> case do {:ok, %{comment: comment}} -> comment |> Repo.preload(:likes) end end...
让咱们更新lib/instagram_clone_web/live/post_live/show.ex
以下内容:
defmodule InstagramCloneWeb.PostLive.Show do use InstagramCloneWeb, :live_view alias InstagramClone.Posts alias InstagramClone.Uploaders.Avatar alias InstagramCloneWeb.PostLive.LikeComponent alias InstagramClone.Comments alias InstagramClone.Comments.Comment @impl true def mount(%{"id" => id}, session, socket) do socket = assign_defaults(session, socket) post = Posts.get_post_by_url!(URI.decode(id)) {:ok, socket |> assign(changeset: Comments.change_comment(%Comment{})) |> assign(comments_section_update: "prepend") |> assign(post: post) |> assign(page: 1, per_page: 15) |> assign_comments() |> set_load_more_comments_btn(), temporary_assigns: [comments: []]} end defp assign_comments(socket) do current_user = socket.assigns.current_user if current_user do comments = Comments.list_post_comments(socket.assigns, public: false) socket |> assign(comments: comments) else comments = Comments.list_post_comments(socket.assigns, public: true) socket |> assign(comments: comments) end end defp set_load_more_comments_btn(socket) do post_total_comments = socket.assigns.post.total_comments per_page = socket.assigns.per_page if post_total_comments > per_page do socket |> assign(load_more_comments_btn: "flex") else socket |> assign(load_more_comments_btn: "hidden") end end @impl true def handle_info({LikeComponent, :update_comment_likes, comment_id}, socket) do comment = Comments.get_comment!(comment_id) {:noreply, socket |> update(:comments, fn comments -> [comment | comments] end)} end @impl true def handle_info({LikeComponent, :update_post_likes, post_id}, socket) do {:noreply, socket |> assign(post: Posts.get_post!(post_id))} end @impl true def handle_event("load-more-comments", _, socket) do {:noreply, socket |> assign(comments_section_update: "append") |> load_comments()} end @impl true def handle_event("save", %{"comment" => comment_param}, socket) do %{"body" => body} = comment_param current_user = socket.assigns.current_user post = socket.assigns.post if body == "" do {:noreply, socket} else comment = Comments.create_comment(current_user, post, comment_param) {:noreply, socket |> update(:comments, fn comments -> [comment | comments] end) |> assign(comments_section_update: "prepend") |> assign(changeset: Comments.change_comment(%Comment{}))} end end defp load_comments(socket) do total_comments = socket.assigns.post.total_comments page = socket.assigns.page per_page = socket.assigns.per_page total_pages = ceil(total_comments / per_page) socket |> hide_btn?(page, total_pages) |> update(:page, &(&1 + 1)) |> assign_comments() end defp hide_btn?(socket, page, total_pages) do if (page + 1) == total_pages do socket |> assign(load_more_comments_btn: "hidden") else socket end endend
在上面lib/instagram_clone_web/live/post_live
创立评论组件comment_component.ex
:
defmodule InstagramCloneWeb.PostLive.CommentComponent do use InstagramCloneWeb, :live_component alias InstagramClone.Uploaders.Avatarend
下的评论组件模板lib/instagram_clone_web/live/post_live/comment_component.html.leex
:
<div class="flex py-2" id="comment-<%= @comment.id %>"> <div class="w-1/12 pt-1"> <%= live_redirect to: Routes.user_profile_path(@socket, :index, @comment.user.username) do %> <%= img_tag Avatar.get_thumb(@comment.user.avatar_url), class: "w-8 h-8 rounded-full object-cover object-center" %> <% end %> </div> <div class="px-4 w-10/12"> <%= live_redirect @comment.user.username, to: Routes.user_profile_path(@socket, :index, @comment.user.username), class: "truncate font-bold text-sm text-gray-500 hover:underline" %> <span class="text-sm text-gray-700"> <p class="inline"><%= @comment.body %></p></span> </span> <div class="flex mt-3"> <div class="text-gray-400 text-xs"><%= Timex.from_now @comment.inserted_at %></div> <button class="px-3 text-xs text-gray-700 focus:outline-none"><%= @comment.total_likes %> likes</button> <button class="text-xs text-gray-700 focus:outline-none">Reply</button> </div> </div> <%= if @current_user do %> <%= live_component @socket, InstagramCloneWeb.PostLive.LikeComponent, id: @comment.id, liked: @comment, w_h: "w-6 h-6", current_user: @current_user %> <% else %> <%= link to: Routes.user_session_path(@socket, :new) do %> <button class="w-6 h-6 focus:outline-none"> <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="1" 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> </button> <% end %> <% end %></div>
最初更新一下lib/instagram_clone_web/live/post_live/show.html.leex
:
<section class="flex"> <!-- Post Image section --> <%= img_tag @post.photo_url, class: "w-3/5 object-contain h-full" %> <!-- End Post Image section --> <div class="w-2/5 border-2 h-full"> <div class="flex p-4 items-center border-b-2"> <!-- Post header section --> <%= live_redirect to: Routes.user_profile_path(@socket, :index, @post.user.username) do %> <%= img_tag @post.user.avatar_url, class: "w-8 h-8 rounded-full object-cover object-center" %> <% end %> <div class="ml-3"> <%= live_redirect @post.user.username, to: Routes.user_profile_path(@socket, :index, @post.user.username), class: "truncate font-bold text-sm text-gray-500 hover:underline" %> </div> <!-- End post header section --> </div> <div class="no-scrollbar h-96 overflow-y-scroll p-4 flex flex-col"> <%= if @post.description do %> <!-- Description section --> <div class="flex mt-2"> <%= live_redirect to: Routes.user_profile_path(@socket, :index, @post.user.username) do %> <%= img_tag Avatar.get_thumb(@post.user.avatar_url), class: "w-8 h-8 rounded-full object-cover object-center" %> <% end %> <div class="px-4 w-11/12"> <%= live_redirect @post.user.username, to: Routes.user_profile_path(@socket, :index, @post.user.username), class: "font-bold text-sm text-gray-500 hover:underline" %> <span class="text-sm text-gray-700"> <p class="inline"><%= @post.description %></p></span> </span> <div class="flex mt-3"> <div class="text-gray-400 text-xs"><%= Timex.from_now @post.inserted_at %></div> </div> </div> </div> <!-- End Description Section --> <% end %> <section id="comments" phx-update="<%= @comments_section_update %>"> <%= for comment <- @comments do %> <%= live_component @socket, InstagramCloneWeb.PostLive.CommentComponent, id: comment.id, current_user: @current_user, comment: comment %> <% end %> </section> <button class="w-full <%= @load_more_comments_btn %> justify-center pt-2 focus:outline-none" phx-click="load-more-comments"> <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-gray-400" 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> </button> </div> <div class="w-full border-t-2"> <!-- Action icons section --> <div class="flex pl-4 pr-2 pt-2"> <%= if @current_user do %> <%= live_component @socket, InstagramCloneWeb.PostLive.LikeComponent, id: @post.id, liked: @post, w_h: "w-8 h-8", current_user: @current_user %> <% else %> <%= link to: Routes.user_session_path(@socket, :new) do %> <button class="w-8 h-8 focus:outline-none"> <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="1" 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> </button> <% end %> <% end %> <div class="ml-4 w-8 h-8 cursor-pointer"> <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="1" 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> </div> <div class="ml-4 w-8 h-8 cursor-pointer"> <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="1" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" /> </svg> </div> <div class="w-8 h-8 ml-auto cursor-pointer"> <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="1" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /> </svg> </div> </div> <!-- End Action icons section --> <!-- Description section --> <button class="px-5 text-xs text-gray-500 font-bold focus:outline-none"><%= @post.total_likes %> likes</button> <h6 class="px-5 text-xs text-gray-400"><%= Timex.format!(@post.inserted_at, "{Mfull} {D}, {YYYY}") %></h6> <!-- End Description Section --> <!-- Comment input section --> <%= if @current_user do %> <%= f = form_for @changeset, "#", phx_submit: "save", class: "p-2 flex items-center mt-3 border-t-2 border-gray-100", x_data: "{ disableSubmit: true, inputText: null, displayCommentBtn: (refs) => { refs.cbtn.classList.remove('opacity-30') refs.cbtn.classList.remove('cursor-not-allowed') }, disableCommentBtn: (refs) => { refs.cbtn.classList.add('opacity-30') refs.cbtn.classList.add('cursor-not-allowed') } }" %> <div class="w-full"> <%= textarea f, :body, class: "w-full border-0 focus:ring-transparent resize-none", rows: 1, placeholder: "Add a comment...", aria_label: "Add a comment...", autocorrect: "off", autocomplete: "off", x_model: "inputText", "@input": "[ (inputText.length != 0) ? [disableSubmit = false, displayCommentBtn($refs)] : [disableSubmit = true, disableCommentBtn($refs)] ]" %> </div> <div> <%= submit "Post", phx_disable_with: "Posting...", class: "text-light-blue-500 opacity-30 cursor-not-allowed font-bold pb-2 text-sm focus:outline-none", x_ref: "cbtn", "@click": "inputText = null", "x_bind:disabled": "disableSubmit" %> </div> </form> <% else %> <div class="p-4 flex items-center mt-3 border-t-2 border-gray-100"> <%= link "Log in to comment", to: Routes.user_session_path(@socket, :new), class: "text-light-blue-600" %> </div> <% end %> <!-- End Comment input section --> </div> </div></section>
咱们增加了几个 AlpineJS 指令,以在文本区域为空时禁用评论提交按钮。
这部分就是这样,咱们在这个系列中学到了很多货色,还有很多工作要做,开发永无止境。
转自:Elixirprogrammer