本文目录导读:

我来详细讲解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提交、错误处理等功能,根据具体需求可以进一步定制和优化。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播