本文目录导读:

我来详细讲解Symfony表单与抽屉组件(Drawer/Modal)的结合使用。
基础架构设计
抽屉组件配置
# config/packages/twig.yaml
twig:
form_themes: ['bootstrap_5_layout.html.twig']
globals:
drawer_component: 'App\Components\DrawerComponent'
抽屉组件类
// src/Components/DrawerComponent.php
namespace App\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent('drawer')]
class DrawerComponent
{
public string $id;
public string $title;
public string $size = 'md'; // sm, md, lg, xl
public string $position = 'right'; // left, right
public ?string $formAction = null;
public ?string $submitLabel = '保存';
// 表单相关
public ?array $formFields = null;
public ?string $formTemplate = null;
}
抽屉模板
{# templates/components/drawer.html.twig #}
<div class="drawer drawer-{{ position }}" id="{{ id }}">
<div class="drawer-overlay" data-action="click->drawer#close"></div>
<div class="drawer-content drawer-{{ size }}">
<div class="drawer-header">
<h5 class="drawer-title">{{ title }}</h5>
<button type="button" class="btn-close" data-action="drawer#close"></button>
</div>
<div class="drawer-body">
{% if formFields %}
{{ form_start(form, {'action': formAction, 'attr': {'novalidate': 'novalidate'}}) }}
{% for field in formFields %}
<div class="mb-3">
{{ form_label(form[field]) }}
{{ form_widget(form[field]) }}
{{ form_errors(form[field]) }}
</div>
{% endfor %}
<div class="drawer-footer">
<button type="button" class="btn btn-secondary" data-action="drawer#close">
取消
</button>
<button type="submit" class="btn btn-primary">
{{ submitLabel }}
</button>
</div>
{{ form_end(form) }}
{% else %}
{{ block('drawer_content')|raw }}
{% endif %}
</div>
</div>
</div>
表单与抽屉结合示例
创建表单类型
// src/Form/ProductType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Validator\Constraints as Assert;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => '产品名称',
'constraints' => [
new Assert\NotBlank(['message' => '产品名称不能为空']),
new Assert\Length(['max' => 100])
],
'attr' => [
'class' => 'form-control',
'placeholder' => '请输入产品名称'
]
])
->add('price', NumberType::class, [
'label' => '价格',
'constraints' => [
new Assert\NotBlank(),
new Assert\Positive()
],
'attr' => [
'class' => 'form-control',
'step' => '0.01'
]
])
->add('description', TextareaType::class, [
'label' => '描述',
'required' => false,
'attr' => [
'class' => 'form-control',
'rows' => 3
]
]);
}
}
控制器处理
// src/Controller/ProductController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ProductController extends AbstractController
{
#[Route('/products', name: 'product_list')]
public function index(): Response
{
return $this->render('product/index.html.twig', [
'products' => $this->getProducts()
]);
}
#[Route('/products/create', name: 'product_create')]
public function create(Request $request): Response
{
$form = $this->createForm(ProductType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
// 处理数据保存
if ($request->isXmlHttpRequest()) {
return $this->json([
'success' => true,
'message' => '产品创建成功',
'data' => $data
]);
}
$this->addFlash('success', '产品创建成功');
return $this->redirectToRoute('product_list');
}
if ($request->isXmlHttpRequest()) {
return $this->json([
'success' => false,
'html' => $this->renderView('product/_form.html.twig', [
'form' => $form->createView()
])
]);
}
return $this->render('product/create.html.twig', [
'form' => $form->createView()
]);
}
}
集成示例
页面模板
{# templates/product/index.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>产品列表</h1>
<button class="btn btn-primary"
data-action="click->drawer#open"
data-drawer-id="createProductDrawer">
<i class="bi bi-plus"></i> 添加产品
</button>
</div>
<!-- 产品列表 -->
<div id="productList">
{% for product in products %}
<div class="card mb-2">
<div class="card-body">
{{ product.name }} - ¥{{ product.price }}
</div>
</div>
{% endfor %}
</div>
<!-- 创建产品抽屉 -->
<twig:Drawer
id="createProductDrawer"
title="添加产品"
size="lg"
formAction="{{ path('product_create') }}"
submitLabel="创建产品">
{% block drawer_content %}
{{ form_start(form, {'action': path('product_create'), 'attr': {'id': 'productForm', 'novalidate': 'novalidate'}}) }}
<div class="mb-3">
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
{{ form_errors(form.name) }}
</div>
<div class="mb-3">
{{ form_label(form.price) }}
{{ form_widget(form.price) }}
{{ form_errors(form.price) }}
</div>
<div class="mb-3">
{{ form_label(form.description) }}
{{ form_widget(form.description) }}
{{ form_errors(form.description) }}
</div>
{{ form_end(form) }}
{% endblock %}
</twig:Drawer>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const productForm = document.getElementById('productForm');
if (productForm) {
productForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 关闭抽屉
const drawer = document.getElementById('createProductDrawer');
if (drawer) {
drawer.dispatchEvent(new Event('close'));
}
// 刷新列表
location.reload();
} else {
// 更新表单显示错误
const formContainer = document.querySelector('#createProductDrawer .drawer-body');
if (formContainer && data.html) {
formContainer.innerHTML = data.html;
}
}
})
.catch(error => {
console.error('Error:', error);
});
});
}
});
</script>
{% endblock %}
JavaScript交互
// assets/js/drawer.js
class DrawerManager {
constructor() {
this.drawers = new Map();
this.init();
}
init() {
document.querySelectorAll('[data-drawer-id]').forEach(trigger => {
const drawerId = trigger.dataset.drawerId;
const drawer = document.getElementById(drawerId);
if (drawer) {
this.drawers.set(drawerId, drawer);
trigger.addEventListener('click', () => this.open(drawerId));
}
});
// 监听自定义事件
document.addEventListener('drawer:open', (e) => {
this.open(e.detail.id);
});
document.addEventListener('drawer:close', (e) => {
this.close(e.detail.id);
});
}
open(id) {
const drawer = this.drawers.get(id);
if (drawer) {
drawer.classList.add('show');
document.body.classList.add('drawer-open');
// 触发AJAX加载表单
this.loadForm(drawer);
}
}
close(id) {
const drawer = this.drawers.get(id);
if (drawer) {
drawer.classList.remove('show');
document.body.classList.remove('drawer-open');
}
}
loadForm(drawer) {
const formAction = drawer.dataset.formAction;
if (formAction && !drawer.dataset.loaded) {
drawer.dataset.loaded = 'true';
fetch(formAction, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.html) {
const body = drawer.querySelector('.drawer-body');
if (body) {
body.innerHTML = data.html;
this.attachFormHandler(body.querySelector('form'));
}
}
});
}
}
attachFormHandler(form) {
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
this.submitForm(form);
});
}
}
submitForm(form) {
const formData = new FormData(form);
formData.append('_drawer_submit', '1');
fetch(form.action, {
method: form.method || 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
this.close(form.closest('[data-drawer-id]')?.id ||
form.closest('.drawer')?.id);
// 触发成功事件
document.dispatchEvent(new CustomEvent('drawer:form-success', {
detail: data
}));
} else {
// 显示错误
const body = form.closest('.drawer-body');
if (body && data.html) {
body.innerHTML = data.html;
this.attachFormHandler(body.querySelector('form'));
}
}
});
}
}
// 初始化
const drawerManager = new DrawerManager();
export default drawerManager;
CSS样式
// assets/styles/drawer.scss
.drawer {
position: fixed;
top: 0;
z-index: 1050;
height: 100vh;
display: none;
&.show {
display: block;
}
&-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1040;
}
&-content {
position: fixed;
top: 0;
background: white;
z-index: 1050;
overflow-y: auto;
transition: transform 0.3s ease-in-out;
&.drawer-sm { width: 300px; }
&.drawer-md { width: 400px; }
&.drawer-lg { width: 600px; }
&.drawer-xl { width: 800px; }
}
&-right &-content {
right: 0;
transform: translateX(100%);
.show & {
transform: translateX(0);
}
}
&-left &-content {
left: 0;
transform: translateX(-100%);
.show & {
transform: translateX(0);
}
}
&-header {
padding: 1rem;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
&-body {
padding: 1rem;
flex: 1;
}
&-footer {
padding: 1rem;
border-top: 1px solid #dee2e6;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
position: sticky;
bottom: 0;
background: white;
}
}
// 防止页面滚动
body.drawer-open {
overflow: hidden;
}
高级功能
表单验证错误处理
// src/Form/Validation/FormValidationTrait.php
namespace App\Form\Validation;
trait FormValidationTrait
{
private function getFormErrors($form): array
{
$errors = [];
foreach ($form->getErrors(true, true) as $error) {
$fieldName = $error->getOrigin()->getName();
$errors[$fieldName][] = $error->getMessage();
}
return $errors;
}
private function handleAjaxFormSubmission(Request $request, FormInterface $form): JsonResponse
{
$form->handleRequest($request);
if ($form->isSubmitted()) {
if ($form->isValid()) {
// 处理表单
return $this->json([
'success' => true,
'message' => '操作成功'
]);
} else {
return $this->json([
'success' => false,
'errors' => $this->getFormErrors($form),
'html' => $this->renderView('product/_form.html.twig', [
'form' => $form->createView()
])
]);
}
}
// 首次加载表单
return $this->json([
'success' => true,
'html' => $this->renderView('product/_form.html.twig', [
'form' => $form->createView()
])
]);
}
}
这种实现提供了完整的Symfony表单与抽屉组件的集成方案,包括表单验证、AJAX提交、错误处理等功能,根据具体需求可以进一步定制和优化。