新产品表单
带有表单、ValidatableComponentTrait
和 saveProduct()
LiveAction 的 Live 组件,用于即时验证和 AJAX 提交。
真正的魔力来自 #[LiveListener('category:created')
。这由 NewCategoryForm
组件(在模态框中打开)在新分类创建时发出。
注意:category:created
事件发出 category
作为整数。然后,由于 Category
类型提示 + Symfony 标准的 控制器参数行为,Symfony 使用该 ID 来查询 Category
对象。
// ... 隐藏 use 语句 - 点击显示
#[AsLiveComponent]
class NewProductForm extends AbstractController
{
use DefaultActionTrait;
use ValidatableComponentTrait;
public function __construct(private CategoryRepository $categoryRepository)
{
}
#[LiveProp(writable: true)]
#[NotBlank]
public string $name = '';
#[LiveProp(writable: true)]
public int $price = 0;
#[LiveProp(writable: true)]
#[NotBlank]
public ?Category $category = null;
#[ExposeInTemplate]
public function getCategories(): array
{
return $this->categoryRepository->findAll();
}
#[LiveListener('category:created')]
public function onCategoryCreated(#[LiveArg] Category $category): void
{
// change category to the new one
$this->category = $category;
// the re-render will also cause the <select> to re-render with
// the new option included
}
public function isCurrentCategory(Category $category): bool
{
return $this->category && $this->category === $category;
}
#[LiveAction]
public function saveProduct(EntityManagerInterface $entityManager): Response
{
$this->validate();
$product = new Product();
$product->setName($this->name);
$product->setPrice($this->price);
$product->setCategory($this->category);
$entityManager->persist($product);
$entityManager->flush();
$this->addFlash('live_demo_success', 'Product created! Add another one!');
return $this->redirectToRoute('app_demo_live_component_product_form');
}
}
新产品模板
在底部附近,这里使用另一个组件 - NewCategoryForm
- 在其内部渲染了 BootstrapModal
组件。模态框的打开完全通过正常的 Bootstrap 逻辑完成:带有 data-bs-toggle="modal"
和 data-bs-target="#new-category-modal"
的 a
标签。
<div {{ attributes }}>
<form
data-action="live#action:prevent"
data-live-action-param="saveProduct"
>
<div class="row align-items-center">
<div class="col-2">
<label for="product-name">Product name:</label>
</div>
<div class="col-3">
<input
type="text"
data-model="name"
class="form-control {{ _errors.has('name') ? 'is-invalid' }}"
id="product-name"
>
{% if _errors.has('name') %}
<div class="invalid-feedback">
{{ _errors.get('name') }}
</div>
{% endif %}
</div>
</div>
<div class="row align-items-center mt-3">
<div class="col-2">
<label for="product-price">Price:</label>
</div>
<div class="col-3">
<input
type="text"
data-model="price"
class="form-control {{ _errors.has('price') ? 'is-invalid' }}"
id="product-price"
>
{% if _errors.has('price') %}
<div class="invalid-feedback">
{{ _errors.get('price') }}
</div>
{% endif %}
</div>
</div>
<div class="row align-items-center mt-3">
<div class="col-2">
<label for="product-category">Category:</label>
</div>
<div class="col-3">
<select
data-model="category"
id="product-category"
class="form-control {{ _errors.has('category') ? 'is-invalid' }}"
>
<option value="">Choose a category</option>
{% for category_option in categories %}
<option value="{{ category_option.id }}" {{ this.isCurrentCategory(category_option) ? 'selected' }}>{{ category_option.name }}</option>
{% endfor %}
</select>
{% if _errors.has('category') %}
<div class="invalid-feedback">
{{ _errors.get('category') }}
</div>
{% endif %}
</div>
<div class="col-auto">
<div class="form-text">
<a
type="button"
data-bs-toggle="modal"
data-bs-target="#new-category-modal"
>+ Add Category
</a>
</div>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary">Save Product</button>
</div>
</form>
{% component BootstrapModal with {id: 'new-category-modal'} %}
{% block modal_header %}
<h5>Add a Category</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
{% endblock %}
{% block modal_body %}
<twig:NewCategoryForm />
{% endblock %}
{% endcomponent %}
</div>
新分类表单
此组件在模态框中打开!它有一个 #[LiveAction]
,用于将新的 Category
保存到数据库,然后执行两个重要的操作
- 发出带有新
Category
的 ID 的category:created
事件(参见NewProductForm.php
)。 - 调度一个名为
modal:closed
的浏览器事件来关闭模态框(参见bootstrap-modal-controller.js
)。
// ... 隐藏 use 语句 - 点击显示
#[AsLiveComponent]
class NewCategoryForm
{
use ComponentToolsTrait;
use DefaultActionTrait;
use ValidatableComponentTrait;
#[LiveProp(writable: true)]
#[NotBlank]
public string $name = '';
#[LiveAction]
public function saveCategory(EntityManagerInterface $entityManager, LiveResponder $liveResponder): void
{
$this->validate();
$category = new Category();
$category->setName($this->name);
$entityManager->persist($category);
$entityManager->flush();
$this->dispatchBrowserEvent('modal:close');
$this->emit('category:created', [
'category' => $category->getId(),
]);
// reset the fields in case the modal is opened again
$this->name = '';
$this->resetValidation();
}
}
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class BootstrapModal
{
public ?string $id = null;
}
<div {{ attributes.defaults({
class: 'modal fade',
tabindex: '-1',
'aria-hidden': 'true',
id: id ? id : false,
}) }}
data-controller="bootstrap-modal"
>
<div class="modal-dialog">
<div class="modal-content">
{% block modal_full_content %}
{% if block('modal_header') %}
<div class="modal-header">
{% block modal_header %}{% endblock %}
</div>
{% endif %}
<div class="modal-body">
{% block modal_body %}{% endblock %}
</div>
{% if block('modal_footer') %}
<div class="modal-footer">
{% block modal_footer %}{% endblock %}
</div>
{% endif %}
{% endblock %}
</div>
</div>
</div>
import { Controller } from '@hotwired/stimulus';
import { Modal } from 'bootstrap';
/**
* Allows you to dispatch a "modal:close" JavaScript event to close it.
*
* This is useful inside a LiveComponent, where you can emit a browser event
* to open or close the modal.
*
* See templates/components/BootstrapModal.html.twig to see how this is
* attached to Bootstrap modal.
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
modal = null;
connect() {
this.modal = Modal.getOrCreateInstance(this.element);
document.addEventListener('modal:close', () => this.modal.hide());
}
}
作者 weaverryan
发布日期 2023-04-20