介绍
嘿,网络冒险家! ð构建Web应用程序可能是一个疯狂的旅程,对吗?好吧,今天,我们正在深入研究超级酷的东西:在您的Rails应用程序中添加拖放功能,所有这些功能都由Hotwire的魔力提供动力!不用担心,我们不会从头开始。 ð«
shhh ...我们的小秘密工具
我们已经有一个秘密武器:UpperBracket。这就像有一个魔术棒来生成全栈轨道应用。它带有所有的好东西 - vite,tailwind CSS,Rodauth,Rubocop等等,因此我们可以专注于有趣的东西。但是,让我们在我们之间保持这一点! ð
对最终结果感到好奇,这是
让我构建
那么,计划是什么?我们正在创建一个简单的课程列表应用程序,用户可以在其中用鼠标轻弹将项目重新排列。超级酷,对吧?让我们通过使用UpperBracket模板生成我们的Rails应用程序来开始问题。
# create app
rails new hotwire-dragndrop \
-d postgresql \
-m https://raw.githubusercontent.com/maful/upperbracket/main/template.rb
# move to the app directory
cd hotwire-dragndrop
- 请确保您在潜水之前启动并运行了PostgreSQL数据库。
创建仅具有标题属性的课程模型,这里没有幻想。
rails g scaffold Course title
打开课程表的生成迁移,并定义标题不应为null。
class CreateCourses < ActiveRecord::Migration[7.0]
def change
create_table :courses do |t|
t.string :title, null: false
t.timestamps
end
end
end
在课程模型app/models/course.rb
中添加title
的存在验证
# frozen_string_literal: true
class Course < ApplicationRecord
validates :title, presence: true
end
更新以将根页设置为课程列表页面的路由config/routes.rb
# frozen_string_literal: true
Rails.application.routes.draw do
root "courses#index"
resources :courses
end
我将用尾风CSS为页面添加一个小型样式,这是可选的。打开课程索引页app/views/courses/index.html.erb
并使用以下代码更新
<div class="container">
<div class="max-w-screen-md mx-auto py-10">
<p style="color: green"><%= notice %></p>
<div class="mb-6">
<%= link_to "New course", new_course_path, class: "rounded border border-slate-500 px-2 py-3" %>
</div>
<h1 class="text-2xl font-medium mb-4">Courses</h1>
<div id="courses" class="flex flex-col gap-4">
<%= render @courses %>
</div>
</div>
</div>
打开app/views/courses/_course.html.erb
中的单课程页面
<div
id="<%= dom_id course %>"
class="bg-gray-50 shadow-sm space-y-6 py-6 px-4"
>
<div class="flex gap-4 items-center">
<div class="text-gray-700 text-base">
<%= course.title %>
</div>
</div>
</div>
运行rails db:migrate
以运行数据库迁移。您现在可以使用bin/dev
在应用程序中添加一些课程,您现在应该有类似的东西
您现在有一门课程列表,对于简单应用来说看起来很不错。但是,这不是结束,并注意到我们仍然无法拖放课程。
添加可排序的库
是时候添加主要功能拖放,添加ranked-model gem来处理后端中的记录订购
bundle add ranked-model
并添加可重新订购库和HTTP请求的节点软件包
yarn add sortablejs @rails/request.js
创建迁移以将row_order
列添加到课程表
rails generate migration AddRowOrderToCourses row_order:integer
row_order
列是无效的,我们已经在上一节中添加了一些课程。让我们填写课程表中现有记录的Row_order。
rails generate migration BackfillRowOrderToCourses
使用此代码更新生成的迁移文件
class BackfillRowOrderToCourses < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def up
Course.unscoped.in_batches do |relation|
relation.update_all('row_order = EXTRACT(EPOCH FROM created_at)')
sleep(0.01)
end
end
end
此迁移基本上是在此文件迁移过程中禁用DDL(数据定义语言),然后在课程中以批量记录进行迭代,并从created_at
数据中更新row_order
。通过运行rails db:migrate
打开课程模型并包括排名模型的宝石并配置它。
# frozen_string_literal: true
class Course < ApplicationRecord
include RankedModel
ranks :row_order
end
现在,打开app/controllers/courses_controller.rb
并在index
方法中更新@courses
变量,以将排名模型用于订购
def index
- @courses = Course.all
+ @courses = Course.rank(:row_order).all
end
运行应用程序并检查课程页面。在UI的上下文中没有什么不同,但是如果您检查开发日志,您会注意到row_order
已用于订购记录。
Course Load (1ms) SELECT "courses".* FROM "courses" ORDER BY "courses"."row_order" ASC
创建可排序的刺激控制器
在上一节中,我们安装了sortable
节点软件包,在本节中,我们将使用它。创建在app/javascript/controllers/sortable_controller.js
中名为“ sortable”的刺激控制器,并添加以下代码
import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs"
export default class extends Controller {
connect() {
const options = {
onEnd: this.onEnd.bind(this)
}
Sortable.create(this.element, options)
}
onEnd(evt) {
const body = { row_order_position: evt.newIndex }
console.log(body)
}
}
解释
- 将sortablej导入控制器
- 覆盖
connect
函数以创建当前元素的可排序实例。这意味着它将注册与可排序控制器连接的元素。 - 想象您刚刚将项目拖到了新的位置。下一步是什么?我们添加了一个称为
onEnd
的函数。就像演出的大结局一样!但是,这是我们尚未将数据发送到后端的转折。相反,我们正在记录新索引以关注更改。
让我们尝试将刺激控制器连接到我们的元素。打开app/views/courses/index.html.erb
文件,然后使用id courses
添加data-controller="sortable"
<div id="courses" class="flex flex-col gap-4" data-controller="sortable">
现在,让我们进行测试!启动您的应用程序,并给该课程列表一个旋转。您会注意到您可以拖放物品,而您拖动的项目将更改其位置。检查您的浏览器控制台以查看新的位置!
但是,当您刷新应用程序时,这就像键入倒带按钮一样。为什么?因为我们还没有将更改保存到数据库中。
该位置基于索引,而JavaScript中的索引从0开始。如果您在sortable_controller.js
中再次检查,则body
变量是我们需要发送到后端并将其保存到数据库中所需的。让我们这样做,在onEnd
函数中,删除console.log
行,因为我们不再需要它了。因此,这是现在的最终sortable_controller.js
。
我们正在谈论的职位基于索引。在JavaScript中快速抬头,该索引从0开始。在sortable_controller.js
上窥视,您会发现body
变量。那是我们需要发送到后端保存数据库的金色掘金。
在onEnd
函数中,我们对console.log
系列说再见 - 我们不再需要它。这是我们抛光的sortable_controller.js
。
import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs"
import { patch } from "@rails/request.js"
export default class extends Controller {
connect() {
const options = {
onEnd: this.onEnd.bind(this)
}
Sortable.create(this.element, options)
}
onEnd(evt) {
const body = { row_order_position: evt.newIndex }
patch(evt.item.dataset.sortableUrl, {
body: JSON.stringify(body),
responseKind: "turbo-stream",
})
}
}
哦,最后一件事 - 您会在代码中发现evt.item.dataset.sortableUrl
。这就像一张地图要发送请求的位置。让我们通过打开路线并在课程资源中使用补丁方法添加等级来为其创建一条路线。
resources :courses do
patch "rank", on: :member
end
打开courses_controller.rb
并添加称为rank
的新方法,这将在课程记录中更新row_order
def rank
@course.update(row_order_position: params[:row_order_position])
end
不要忘记在顶部的before_action
中包括rank
方法
- before_action :set_course, only: [:show, :edit, :update, :destroy]
+ before_action :set_course, only: [:show, :edit, :update, :destroy, :rank]
打开app/views/courses/_course.html.erb
并添加data-sortable-url
属性,其中包含我们现在创建的路由
<div
id="<%= dom_id course %>"
class="bg-gray-50 shadow-sm space-y-6 py-6 px-4"
data-sortable-url="<%= rank_course_path(course) %>"
>
如果您检查元素,则应该看到类似的东西
所以,代码evt.item.dataset.sortableUrl
是在要拖动的项目上找到data-sortable-url
的值。
厌倦了无尽的服务器配置头痛?包裹着为您服务。告别手动设置,并向无麻烦的部署打招呼。发现用包裹起来的红宝石应用程序的便利性并彻底改变了您的开发过程。开始使用WrappedBy
部署
让我们再试一次应用程序,立即在数据库中持续存在该位置的顺序。
奖金1
所以,您已经用铁轨中的热线钉了拖放,但是这是我们可以撒上很酷的UX升级。如何添加一个小标记以突出显示您的物品将在列表中落在列表中的位置?让我告诉你我的意思。打开您可信赖的sortable_controller.js
并在connect
函数中介绍ghostClass
选项。
const options = {
onEnd: this.onEnd.bind(this),
ghostClass: "bg-red-300"
}
如果再次尝试该应用程序,您会在拖动项目的新位置上看到红色背景。
奖金2
但是,还有更多!这是您的另一个提示 - 如果您想对可以拖动哪些元素以及保留哪些元素的超级具体?好吧,这一切都是关于使用handle
选择器。每个元素看起来都这样:
首先,在课程标题的左侧添加移动图标。这是app/views/courses/_course.html.erb
文件的完整
<div
id="<%= dom_id course %>"
class="bg-gray-50 shadow-sm space-y-6 py-6 px-4"
data-sortable-url="<%= rank_course_path(course) %>"
>
<div class="flex gap-4 items-center">
<div class="sortable-handle cursor-grab">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-move h-4 w-4 text-current"><path d="m5 9-3 3 3 3M9 5l3-3 3 3M15 19l-3 3-3-3M19 9l3 3-3 3M2 12h20M12 2v20"/></svg>
</div>
<div class="text-gray-700 text-base">
<%= course.title %>
</div>
</div>
</div>
打开app/views/courses/index.html.erb
并在data-controller
之后添加data-sortable-handle-selector-value
属性
<div id="courses" class="flex flex-col gap-4" data-controller="sortable" data-sortable-handle-selector-value=".sortable-handle">
以及在sortable_controller.js
中添加handle
选择器并定义刺激值以存储data-sortable-handle-selector-value
属性的值。
import { Controller } from "@hotwired/stimulus"
import Sortable from "sortablejs"
import { patch } from "@rails/request.js"
export default class extends Controller {
static values = {
handleSelector: String,
}
connect() {
const options = {
onEnd: this.onEnd.bind(this),
ghostClass: "bg-red-300"
}
if (this.hasHandleSelectorValue) {
options.handle = this.handleSelectorValue
}
Sortable.create(this.element, options)
}
onEnd(evt) {
const body = { row_order_position: evt.newIndex }
patch(evt.item.dataset.sortableUrl, {
body: JSON.stringify(body),
responseKind: "turbo-stream",
})
}
}
让我们再次检查我们的应用程序的最终结果
请注意此处的区别,只有可以拖动的sortable-handle
的元素,其余的元素回到普通元素。
结论
使用刺激,您拥有一个强大的工具,可以将应用程序的交互性提高到一个缺口。有关更多令人敬畏的功能和刺激巫师,请查看Stimulus Documentation。
那是一个包裹,伙计们! ð我们成功地构建了一个光滑的列表,您可以像专业人士一样拖放,猜猜是什么?所有这些都贴在数据库中。但是,嘿,不要在这里停下来 - 您可以通过在每个课程中添加主题并让用户在不同课程中拖放主题来进一步进行。听起来很有趣,对吗?您可以使用sortablejs中的group
选项来实现它。
可以在hotwire-sortable存储库上下载完整的源代码。
您是否厌倦了在铁路应用程序上部署Ruby的复杂性?发现包裹的红宝石爱好者的最终部署解决方案。好奇地看到它是如何工作的?在此处查看我们的YouTube视频: