使用 Ruby on Rails、CableReady、Mrujs、Stimulus 和 Tailwind 实现服务器端渲染的模态表单
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
Rails 生态系统持续蓬勃发展,Rails 开发人员拥有构建现代化、响应式、可扩展 Web 应用程序所需的一切工具,能够快速高效地完成任务。如果您重视提供卓越的用户体验,那么在 Rails 领域,您的选择从未如此丰富。
今天我们将深入了解这个生态系统,并使用两个前沿的 Rails 项目,使用户能够提交在模态框内渲染的表单。
表单将在模态框中打开,内容由服务器动态填充,服务器将处理表单提交,DOM 将更新而无需整个页面翻转。
为了实现这一目标,我们将使用Stimulus进行前端交互,使用CableReady全新的CableCar 功能从服务器发送内容,并使用Mrujs启用 AJAX 请求并自动处理 CableCar 的操作。
会非常精致。
完成后,我们的应用程序将如下所示:
本文包含相当多的 JavaScript 代码,并假定读者对 Ruby on Rails 的基础知识有扎实的了解。
如果您之前从未使用过 Rails,本文的节奏可能对您来说有点快。虽然您需要熟悉 Rails 和 JavaScript,但您无需事先了解 CableReady 或 Stimulus。
和往常一样,您可以在Github上找到本文的完整源代码。
让我们开始吧!
设置
如果您想跳过设置步骤直接开始编码,您可以克隆此存储库的主分支,然后向下滚动到“客户布局”部分。
为了完成所有安装,我们将冒险使用新发布的、仍处于 alpha 测试阶段的jsbundling-rails和cssbundling-rails gems。
首先,我们将创建一个 Rails 应用程序,并使用 alpha js/cssbundling gems 从终端安装 Webpack 和 Tailwind:
rails new tiny_crm --skip-webpack-install --skip-javascript
cd tiny_crm
bundle add jsbundling-rails cssbundling-rails
rails javascript:install:webpack
rails css:install:tailwind
bin/dev
请注意,这些 gems 非常新,如果您遇到错误,请检查文档以查看命令是否已更改,或者联系我并告诉我您遇到的错误。
安装好 Webpack 和 Tailwind 之后,接下来我们将从终端安装本指南的核心依赖项:Stimulus、CableReady(以及 Action Cable)和 Mrujs:
bundle add hotwire-rails
be rails hotwire:install
然后更新你的 Gemfile 文件,引入最新的 cable_ready 库。
gem "cable_ready", github: "stimulusreflex/cable_ready"
请注意,如果您将来阅读此内容,本指南使用的是 5.0 版本。
然后从您的终端执行:
bundle
yarn add mrujs cable_ready @rails/actioncable
接下来,app/javascript/packs/application.js像这样进行更新,以引入新的依赖项并配置 Mrujs 以使用其CableCar 插件。
import "./controllers"
import mrujs from "mrujs";
import CableReady from "cable_ready"
import { CableCar } from "mrujs/plugins"
import * as Turbo from "@hotwired/turbo"
window.Turbo = Turbo;
mrujs.start({
plugins: [
new CableCar(CableReady)
]
})
需要设置的依赖项太多了。我们真的需要这么多东西才能显示一个模态框吗?不,真的没必要。
本文中使用的技术只需要 Action Cable(一个核心 Rails 库)、CableReady 和 Mrujs。
要按照本指南一步一步操作,需要用到 Tailwind 和 Stimulus,但我们只是用它们来实现一些可以用您自己的 CSS 和原生 JavaScript 实现的功能(如果您更喜欢后者的话)。
最终,你唯一需要的 UI 组件就是一个可以打开和关闭的模态框。Stimulus 和 Tailwind 是实现这一目标的简单方法,但它们并非唯一方法!
接下来,为了完成复制/粘贴的设置工作,我们将在此应用程序中创建和编辑客户,所以让我们先搭建这个资源框架:
rails g scaffold Customer name:string
rails db:migrate
设置完成!目前为止一切顺利。现在我们可以开始编写代码了。
客户布局
首先,我们将对客户首页应用一些基本样式:
<div class="max-w-3xl mx-auto mt-8">
<div data-controller="modal" class="flex justify-between items-baseline mb-6">
<h1 class="text-3xl text-gray-900">Customers</h1>
<%= link_to 'New Customer', new_customer_path, class: "text-blue-600", data: { action: "click->modal#open" } %>
<%= render 'modal' %>
</div>
<div id="customers" class="flex flex-col items-baseline space-y-6 p-4 shadow-lg rounded">
<% @customers.each do |customer| %>
<%= render "customer", customer: customer %>
<% end %>
</div>
</div>
索引视图会显示客户列表,以及包含创建新客户链接的标题。
头部容器 div 包含一个controller="modal"数据属性,该属性引用了一个尚不存在的 Stimulus 控制器。同样,新客户链接的属性modal中也引用了同一个控制器data-action。
我们很快就会创建那个控制器,不过现在,点击新客户链接会将浏览器导航到……customers/new
索引视图还会渲染两个尚不存在的局部视图modal。customer接下来,让我们创建并填充它们,以便我们可以再次渲染索引页面。
首先,客户部分:
touch app/views/customers/_customer.html.erb
目前,客户部分视图仅渲染客户姓名:
<div class="text-gray-700 border-b border-gray-200 w-full pb-2">
<%= customer.name %>
</div>
接下来创建模态框局部视图:
touch app/views/customers/_modal.html.erb
请填写以下内容:
<div data-modal-target="container"
class="hidden fixed inset-0 overflow-y-auto flex items-center justify-center"
style="z-index: 9999;">
<div class="max-w-lg max-h-screen w-full relative">
<div class="m-1 bg-white rounded shadow">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Customer
</h3>
</div>
<form id="customer_form"></form>
</div>
</div>
</div>
这里重要的元素是modal-target="container"数据属性(Stimulus 控制器将使用该属性来打开/关闭模态框)和空<form>元素。
当用户打开模态框时,此表单元素最终会填充来自服务器的内容。
索引标记就位,服务器通过运行bin/dev,前往http://localhost:3000/customers,确保一切显示正常。
接下来我们将创建modalStimulus 控制器,并用服务器渲染的内容填充它。我也很兴奋。
显示新客户模态框
首先,我们需要使用便捷的生成器创建一个新的刺激控制器。在您的终端中:
rails g stimulus modal
并在控制器中填写以下内容:
// Credit: This controller is an edited-to-the-essentials version
// of the modal component created by @excid3 as part of the essential
// tailwind-stimulus-components package found here:
// https://github.com/excid3/tailwindcss-stimulus-components
// In production, use the full component from the
// library or expand this controller to allow for
// keyboard closing and dealing with scroll positions
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ['container'];
connect() {
this.toggleClass = 'hidden';
this.backgroundId = 'modal-background';
this.backgroundHtml = this._backgroundHTML();
this.allowBackgroundClose = true;
}
disconnect() {
this.close();
}
open() {
document.body.classList.add('fixed', 'inset-x-0', 'overflow-hidden');
this.containerTarget.classList.remove(this.toggleClass);
document.body.insertAdjacentHTML('beforeend', this.backgroundHtml);
this.background = document.querySelector(`#${this.backgroundId}`);
}
close() {
if (typeof event !== 'undefined') {
event.preventDefault()
}
this.containerTarget.classList.add(this.toggleClass);
if (this.background) { this.background.remove() }
}
_backgroundHTML() {
return `<div id="${this.backgroundId}" class="fixed top-0 left-0 w-full h-full" style="background-color: rgba(0, 0, 0, 0.7); z-index: 9998;"></div>`;
}
}
这里有很多 JavaScript 代码,但它并没有执行任何太复杂的操作。
在 中connect,我们设置控制器运行所需的默认值。
open只是简单地将类应用于 body 和模态容器,使模态框在屏幕上可见,并将标准的灰色背景应用于页面的其余部分。
调用该函数时close,背景将被移除,模态框将被隐藏。
如果您决定在实际项目中使用这种方法,请考虑使用本代码所基于的Stimulus 组件。以上代码为了简洁而进行了编辑,但这些编辑会引入一些滚动和辅助功能方面的问题,而完整的组件可以很好地处理这些问题。
刺激控制器创建完毕,我们几乎可以渲染模态框了。在继续之前,让我们先退一步,确保我们清楚自己想要实现什么目标。
我们的目标是创建一个服务器端渲染的模态框,允许用户创建新客户。模态框中的表单提交后,新创建的客户应添加到客户列表中,并且模态框应关闭。
首要任务是打开模态框并显示来自服务器的内容,这意味着当用户点击首页上的“新客户”链接时:
- 应向服务器发出请求,以检索客户表单的内容。
- 内容应替换页面初始加载时模态框部分渲染的空白客户表单。
- 模态框应该打开
这比听起来要容易得多。
我们将使用 Mrujs 向目标服务器发出 AJAX 请求customers#new,我们将使用 CableCar 对操作进行排队,Mrujs 将自动为我们处理这些操作。
首先,我们需要告诉 Mrujs 将新客户链接转换为支持 CableCar 的链接。
如文档所述,我们将通过更新首页上的链接来实现这一点,如下所示:
<%= link_to 'New Customer', new_customer_path, class: "text-blue-600", data: { action: "click->modal#open", cable_car: "" } %>
在这里我们添加了,剩下的就交给data-cable-car=""Mrujs来处理了。
这项更改生效后,当用户点击“新客户”链接时,将向服务器发送一个 AJAX 请求customers#new。
由于我们稍后将渲染表单的局部视图,现在就让我们更新该局部视图吧:
<%= form_with(model: customer, id: "customer_form") do |form| %>
<div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<% if customer.errors.any? %>
<div class="p-4 border border-red-600">
<h2>
Could not save customer
</h2>
<ul>
<% customer.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
</div>
<div class="rounded-b mt-6 px-4 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
<%= form.button class: "w-full sm:col-start-2 bg-blue-600 px-4 py-2 mb-4 text-white rounded-sm hover:bg-blue-700" %>
<button data-action="click->modal#close" class="mt-3 w-full sm:mt-0 sm:col-start-1 mb-4 bg-gray-100 hover:bg-gray-200 rounded-sm px-4 py-2">
Cancel
</button>
</div>
<% end %>
其中大部分是标准的 Tailwind 类,用于对表单应用一些简单的样式。
重要的部分是表单的 id(在第 1 行分配)和data-action分配给关闭按钮的 id,它会触发close我们modal之前在 Stimulus 控制器中定义的函数。
我们还可以使用 Tailwind 的表单插件来美化表单。这是可选的,但如果您想使用它,请先在终端中使用 yarn 安装它:
yarn add @tailwindcss/forms
然后更新tailwind.config.js:
module.exports = {
mode: 'jit',
purge: [
'./app/views/**/*.html.erb',
'./app/helpers/**/*.rb',
'./app/javascript/**/*.js'
],
plugins: [
require('@tailwindcss/forms')
]
}
接下来,我们需要更新new方法以CustomersController渲染CableReady 操作,使用新引入的CableCar。为此,我们将对方法进行两处更改CustomersController。
首先更新控制器,使其包含 CableReady::Broadcaster,以便控制器能够访问cable_car:
class CustomersController < ApplicationController
include CableReady::Broadcaster
# snip
end
如果您愿意,可以随意添加include。ApplicationController
然后按如下方式更新CustomersController新方法:
def new
html = render_to_string(partial: 'form', locals: { customer: Customer.new })
render operations: cable_car
.outer_html('#customer_form', html: html)
end
在这里,我们将表单的部分内容渲染成一个字符串,然后将其传递给 cable_car,并在outer_html 操作中使用,目标是(当前为空的)客户表单。
完成以上步骤后,返回http://localhost:3000/customers并点击“新建客户”链接。如果一切顺利,您应该会看到模态框打开并显示客户表单。
目前为止,工作做得非常出色。
接下来,我们将使用相同的 CableCar 方法来处理表单提交。
提交表格
这一部分内容看起来应该很熟悉。首先,我们将更新客户表单,添加缆车数据属性,就像我们在上一节中添加到新客户链接中一样:
<%= form_with(model: customer, id: "customer_form", data: { cable_car: "" }) do |form| %>
这再次告诉 Mrujs 使用 AJAX 请求提交表单,并期望 CableReady 操作作为响应执行。
接下来,返回customers_controller.rb并更新创建方法:
def create
@customer = Customer.new(customer_params)
if @customer.save
html = render_to_string(partial: 'customer', locals: { customer: @customer })
render operations: cable_car
.append('#customers', html: html)
else
# TODO: Handle errors
end
end
这里我们再次将局部视图渲染成字符串,并将该字符串传递给一个操作(这次是 `addCustomer` append)。目标是客户索引视图中渲染的客户列表,新创建的客户将被添加到列表底部。如果您愿意,也可以使用`prepend`将客户添加到列表顶部。
设置完成后,打开模态框,输入姓名,然后提交表单。您应该会看到新创建的客户如预期添加到列表中,但模态框不会关闭。
这并非理想情况。
表单提交成功后,我们如何关闭模态框?这可以通过利用 CableReady 的一些强大功能来实现——链式操作和发出自定义 DOM 事件。
为了实现这一点,我们首先需要在发送给 Mrujs 的操作链中添加另一个操作:
def create
# snip
render operations: cable_car
.append('#customers', html: html)
.dispatch_event(name: 'submit:success')
end
该dispatch_event 操作允许我们发出任何我们想要的事件。有了这个在成功提交时触发的新事件,关闭模态框就变得非常简单,只需向模态框的 Stimulus 控制器添加一个事件监听器,如下所示:
open() {
document.addEventListener("submit:success", () => {
this.close()
}, { once: true });
// snip
}
当模态框打开时,会创建一个事件监听器,该监听器会针对从 cable_car 有效负载分发的事件名称进行调整。
现在,当您提交模态表单时,成功提交表单后,会同时发送`and`append和 `operations`操作,Mrujs 的魔法会自动执行这些操作,并且事件监听器会关闭模态框。dispatch_eventsubmit:success
处理错误
目前为止工作进展顺利。接下来我们将再次处理表单错误render operations。
validates_presence_of :name首先,通过添加以下内容,允许客户提交的内容出现验证错误:models/customer.rb
这样一来,当表单提交时姓名为空,表单提交就会失败。此时,我们希望在模态框内渲染客户表单,并附带验证错误信息。
要在提交失败时显示错误信息,请按如下方式更新 create 方法:
def create
@customer = Customer.new(customer_params)
if @customer.save
html = render_to_string(partial: 'customer', locals: { customer: @customer })
render operations: cable_car
.append('#customers', html: html)
.dispatch_event(name: 'submit:success')
else
html = render_to_string(partial: 'form', locals: { customer: @customer })
render operations: cable_car
.inner_html('#customer_form', html: html), status: :unprocessable_entity
end
end
在这个else分支中,我们再次将局部视图渲染成字符串并执行渲染操作。这次,由于我们不希望模态框关闭,也不需要替换<form>元素本身,所以我们只需要使用一个inner_html操作即可。
打开模态框,提交一个空白表单,可以看到表单按预期重新渲染并显示错误信息。
你能坚持到最后真是太棒了!接下来,我们来看看如何轻松地复用这个模态框来编辑客户信息,并对模态框的打开方式进行一些小的优化。
缆车乘客编辑
空白客户表单模态框的一个优点是,我们可以无需修改即可重复使用它来编辑现有客户,这样我们就只需要一个很小的模态框容器,就可以在页面上重复使用它来创建任意数量的模态框。
首先,cable_car在局部视图中添加一个启用模态框的链接customer:
<div class="text-gray-700 border-b border-gray-200 w-full pb-2" id='<%= "customer-#{customer.id}" %>'>
<%= link_to customer.name, edit_customer_path(customer), data: { cable_car: "", action: "click->modal#open" } %>
</div>
在这里,我们设置了链接的相关数据属性,并在外层 div 元素中添加了一个唯一 ID。当编辑表单提交时,我们将使用该 ID 来替换客户信息的内容。
接下来,回到CustomersController调整edit和update方法的部分:
def edit
html = render_to_string(partial: 'form', locals: { customer: @customer })
render operations: cable_car
.replace('#customer_form', html: html)
end
def update
if @customer.update(customer_params)
html = render_to_string(partial: 'customer', locals: { customer: @customer })
render operations: cable_car
.replace("#customer-#{@customer.id}", html: html)
.dispatch_event(name: 'submit:success')
else
html = render_to_string(partial: 'form', locals: { customer: @customer })
render operations: cable_car
.inner_html('#customer_form', html: html), status: :unprocessable_entity
end
end
这看起来应该很眼熟。这个edit方法是另一个方法的镜像new,而另一个update方法又是另一个方法的镜像create。同样,submit:success当客户信息更新时,我们会触发事件,否则表单会重新渲染并显示错误。
最后,为了让页面上的每个模态链接都使用同一个模态控制器,我们将声明data-controller="modal"向上移动一级 DOM 树。customers/index.html.erb
<div class="max-w-3xl mx-auto mt-8" data-controller="modal">
<div class="flex justify-between items-baseline mb-6">
<!-- Snip -->
</div>
</div>
完成这些更改后,刷新客户索引页面,点击客户姓名,可以看到客户信息会在模态框中更新,并且在表单成功提交后,客户信息会在列表中直接更新。
优化模态开口
您在学习本指南的过程中可能已经注意到,模态框会在服务器内容渲染完成之前打开,因此模态框打开时会出现非常短暂的闪烁,然后迅速替换掉空白表单或表单的先前内容:
这是因为点击模态框链接时模态框会立即打开,但从服务器检索表单片段的往返过程并非完全即时。
我们有多种方法可以避免这种情况,例如给模态框添加加载状态,以减少重新渲染带来的突兀感。但我将要演示的方法是,在从服务器获取内容之前,保持模态框隐藏。这给了我们另一个使用 CableReady 和 Stimulus 的机会,而这正是我们来这里的目的,对吧?
首先,向控制器添加另一个事件监听器modal:
open() {
document.addEventListener("modal:loaded", () => {
this.containerTarget.classList.remove(this.toggleClass);
}, { once: true });
document.addEventListener("submit:success", () => {
this.close()
}, { once: true });
document.body.classList.add('fixed', 'inset-x-0', 'overflow-hidden');
document.body.insertAdjacentHTML('beforeend', this.backgroundHtml);
this.background = document.querySelector(`#${this.backgroundId}`);
}
我们在此进行了更新open,将调用containerTarget.classList.remove从立即执行改为响应modal:loadedDOM 事件执行。
这一变化意味着所有模态链接现在都已失效,因为modal:loaded事件永远不会发生,因此containerTarget.classList.remove永远不会运行,模态容器也始终保持隐藏状态。
CustomersController我们可以通过如下更新来修复模态框链接:
def new
html = render_to_string(partial: 'form', locals: { customer: Customer.new })
render operations: cable_car
.outer_html('#customer_form', html: html)
.dispatch_event(name: 'modal:loaded')
end
def edit
html = render_to_string(partial: 'form', locals: { customer: @customer })
render operations: cable_car
.outer_html('#customer_form', html: html)
.dispatch_event(name: 'modal:loaded')
end
new在这两种edit方法中,我们再次利用 CableReady 的链式操作在替换modal:loaded后进行分发。outer_html
更改后,用户点击模态链接时的事件顺序如下:
- 向服务器发出请求
- 打开操作已触发
- 页面已应用模态背景,但模态框尚未可见。
- 表单内容已替换
- 模态框加载事件已触发。
- 模态框的隐藏类被移除,使其可见。
在我们的测试环境中,这一系列操作发生得非常快,用户几乎感觉不到背景应用和模态框显示之间的延迟。在生产环境中,您可能会发现,对于立即打开的模态框,加载状态可能是一种更具可扩展性的选择,但我们在这里的目的是学习 CableReady 和 Mrujs,而不是构建生产应用程序。
经过这些更改后,模态框打开时将预先填充更新后的内容,从而消除旧内容的闪烁。
单个modal连接的 div 可以用于显示任意数量的模态框,从而减少传统应用程序中的初始页面加载时间,因为传统应用程序可能会预先渲染每个编辑模态框。
总结
今天我们学习了如何构建由 Stimulus、CableReady 和 Mrujs 提供支持的服务器端渲染模态表单。
Stimulus 和 CableReady 是两款功能强大、久经考验的工具,拥有成熟的功能集,任何现代 Rails 应用都应该考虑使用它们。CableReady 可以独立运行,通过多种方式向最终用户提供实时更新;也可以与StimulusReflex结合使用,提供类似 SPA 的体验,但无需 SPA 本身。
Mrujs 是一个较新的工具,目前仍在积极开发中,旨在作为现代、稳定的替代品rails/ujs,取代不再积极开发且将在 Rails 7 发布时弃用的工具。
除了我们今天看到的与 CableReady 的 Cable Car 的紧密集成之外,Mrujs 还以现代化的软件包形式提供了简单的确认对话框、禁用链接以及 Rails UJS 的其他便利功能。
在结束之前,需要特别说明一点:我们可以使用 Rails 生态系统中的各种工具来构建非常类似的用户体验,包括 Turbo Streams(这里有一份相关指南)。
虽然完整的Hotwire 堆栈也能以大致相同的努力实现这种体验,但恕我直言,CableReady 的链式操作的强大功能和灵活性使得 CableReady + Mrujs 比完整的 Hotwire 堆栈更适合这种特定的使用场景。
真正令人兴奋的是,作为 Rails 开发者,我们拥有大量强大的工具来构建实时响应式应用程序。这意味着无论我们最常使用哪种工具,我们都能从中受益。
继续使用 CableReady、Stimulus 和 Mrujs,获取以下资源:
- CableReady文档
- 刺激经济方案文件
- Mrujs文档
- 准备就绪后,请查阅StimulusReflex 文档。
- 如果您在使用 CableReady 或 StimulusReflex 时遇到问题,请加入StimulusReflex 的 Discord 服务器。
- (顺便宣传一下)订阅我的每月简讯《Hotwireing Rails》,即可随时了解使用 Rails 和 CableReady、Stimulus 等工具构建现代化、高性能应用程序的最新资讯。
今天就到这里。
一如既往,感谢阅读!
文章来源:https://dev.to/davidcolbyatx/server-rendered-modal-forms-on-rails-with-cableready-mrujs-stimulus-and-tailwind-2mne




