efabrica/neoforms

渲染表单的更好方式

3.2.1 2024-08-27 12:30 UTC

README

NeoForms

Packagist Version Packagist Downloads

NeoForms 是 Nette\Forms 所急需的良药。

  • 为您提供更轻松的方式编写自己的渲染模板
  • 为您提供规范以更有效地与团队协作
  • 为您提供使用 {formRow}{formLabel}{formInput} 标签渲染表单字段的途径,这些标签您可能已从 Symfony 使用过。
  • 为您提供在 PHP 中构建表单时渲染行和列的途径
  • 为您提供 readonly 模式,以渲染无法编辑的人的表单,通过渲染常规文本而不是输入来代替。(告别灰色禁用的字段!)
  • 为您提供 FormCollection,用于预样式 AJAX 无 Multiplier,并内置 diff 计算器
  • ControlGroup 内部嵌套 ControlGroup(树形结构)

安装

composer require efabrica/neoforms
# config.neon
includes:
    - ../../vendor/efabrica/neoforms/config.neon

文档

使用 ActiveRowForm

use Efabrica\NeoForms\Build\NeoForm;
use Efabrica\NeoForms\Build\NeoFormFactory;
use Efabrica\NeoForms\Build\NeoFormControl;
use Efabrica\NeoForms\Build\ActiveRowForm;
use Nette\Database\Table\ActiveRow;
use Nette\Application\UI\Template;

class CategoryForm extends ActiveRowForm
{
    private NeoFormFactory $formFactory;
    private CategoryRepository $repository;

    // There is no parent constructor
    public function __construct(NeoFormFactory $formFactory, CategoryRepository $repository) {
        $this->formFactory = $formFactory;
        $this->repository = $repository;
    }

    /**
     * NeoFormControl is attached to presenter and used in template.
     * 
     * @param ActiveRow|null $row Optional ActiveRow parameter to fill the form with data
     * @return NeoFormControl
     */
    public function create(?ActiveRow $row = null): NeoFormControl
    {
        $form = $this->formFactory->create();
        
        $form->addText('name', 'Category Name')
            ->setHtmlAttribute('placeholder', 'Enter category name')
            ->setRequired('Name is required to fill');
        
        $form->addText('description', 'Category Description')
            ->setHtmlAttribute('placeholder', 'Type category description')
            ->setRequired('Description is required to fill');
        
        $form->addSubmit('save', ($row === null ? 'Edit' : 'Create') . ' Category')
            ->setIcon('save');
        
        return $this->control($form, $row);
    }
}

展示者中的组件使用

class CategoryPresenter extends AdminPresenter 
{
    private CategoryForm $form;
    private CategoryRepository $repository;

    public function actionCreate(): void
    {
        $this->addComponent($this->form->create(), 'categoryForm');
    }
    
    public function actionUpdate(int $id): void
    {
        $row = $this->repository->findOneById($id);
        if (!$row instanceof \Nette\Database\Table\ActiveRow) {
            throw BadRequestException();
        }
        $this->addComponent($this->form->create($row), 'categoryForm');
    }
}

latte 模板中的组件使用

简单渲染

{* create.latte *}
{block content}
<div class="category-card">
    <div class="category-form-wrapper">
        {control categoryForm}
    </div>
</div>

<form> 标签内自定义 HTML 结构

{* create.latte *}
{block content}
<div class="category-card">
    <div class="category-form-wrapper">
        {neoForm categoryForm}
            {formRow $form['name'], data-joke => 123} {* adds [data-joke="123"] to the wrapping div *}
            {formRow $form['description']}
            <img src="whatever.png" alt="whatever" />
            <div class="row">
              <div class="col">{formRow $form['is_pinned']}</div>
              <div class="col">{formRow $form['is_highlight']}</div>
              <div class="col">{formRow $form['is_published']}</div>
            </div>
            {formRow $form['save'], input => [class => 'reverse']} {* sets input's class to 'reverse' *}
        {/neoForm}
    </div>
</div>

独立的 HTML 表单模板

{* categoryForm.latte *}
{neoForm categoryForm}
    {formRow $form['name'], data-joke => 123} {* adds [data-joke="123"] to the wrapping div *}
    {formRow $form['description']}
    {formRow $form['save'], input => [class => 'reverse']} {* sets input's class to 'reverse' *}
{/neoForm}

分组表单元素

/** @var \Efabrica\NeoForms\Build\NeoForm $form */
$names = $form->group('names');
$names->addText('id', 'ID');
$names->addText('icon', 'Icon');

$checkboxes = $form->group('checkboxes');
$checkboxes->addToggleSwitch('enabled', 'Enabled');
$checkboxes->addCheckbox('verified', 'Verified');
{neoForm categoryForm}
<div class="row">
    <div class="col-6">
        {formGroup $form->getGroup('names')} {* renders id & icon *}
    </div>
    <div class="col-6">
        {formGroup $form->getGroup('checkboxes')} {* renders enabled & verified *}
    </div>
</div>
{/neoForm}

.row .col 网格布局在 PHP 中

/** @var \Efabrica\NeoForms\Build\NeoForm $form */
$row1 = $form->row(); // returns a row instance
$col1 = $row1->col('6'); // returns a new col instance, class="col-6"
$col1->addText('a');
$col1->addTextArea('b');
$col2 = $row1->col('6'); // returns a new different col instance
$col2->addCheckbox('c');

$a = $form->row('main');
$b = $form->row('main');
assert($a === $b); // true, it's the same instance

Latte 标签(API)文档

{neoForm}

{neoForm} 标签用于在 HTML 中渲染 <form> 元素。它还可以渲染表单末尾所有未渲染的输入。此标签的参数是控制器的名称,不带引号。

如果您想渲染整个表单而不指定任何子元素,请使用以下语法

{neoForm topicForm}{/neoForm}
<!-- This is equivalent to {control topicForm} -->

如果您想排除某些表单字段从渲染中,可以使用 rest => false 如此

{neoForm topicForm, rest => false}
{/neoForm}
<!-- This is similar to {form topicform}{/form} -->

这将渲染一个空的 <form>,类似于使用一个空的 {form} 标签。

{formRow}

{formRow} 标签用于在包装组内渲染表单标签和表单输入。它接受各种选项。此标签的参数是一个 BaseControl 实例(例如,$form['title'])。

以下是一些使用 {formRow} 的示例

{formRow $form['title'], class => 'mt-3'}

这渲染了一个具有自定义类的表单行,结果为 <div class="mt-3">...</div>

{formRow $form['title'], '+class' => 'mt-3'}

如果您正在使用 Bootstrap 模板,这将渲染一个具有类的表单组,结果为 <div class="form-group mt-3">...</div>

您还可以使用选项向输入或标签元素添加属性

{formRow $form['title'], input => [data-tooltip => 'HA!']}

这渲染了一个具有 data-tooltip 属性的输入元素的表单行。

{formRow $form['title'], label => [data-toggle => 'modal']}

这渲染了一个具有 data-toggle 属性的标签元素的表单行。

{formGroup}

{formGroup} 标签接受一个 ControlGroup 作为必需参数并渲染组中的所有控件。它内部使用 {formRow} 来处理渲染。

示例用法

{formGroup $form->getGroup('main')}

{formLabel}

{formLabel} 标签用于渲染一个 <label> 元素。参数是一个 BaseControl 实例。

示例用法

{formLabel $form['title'], class => 'text-large', data-yes="no"}

这将渲染一个具有自定义类和数据属性的标签元素。

如果表单元素是一个隐藏字段或复选框,则标签以空 HTML 字符串的形式渲染。

{formInput}

{formInput} 标签用于渲染 <input><textarea><button> 或任何表单行的基本部分。参数是一个 BaseControl 实例。

示例用法

{formInput $form['category'], data-select2 => true}

这将渲染一个带有空 data-select2 属性的输入元素。

应用属性

可以使用选项将属性应用到表单元素。以下是一些常用属性:

"icon"

当应用于按钮时,"icon" 属性会在文本之前添加一个图标。例如

$form->addSubmit('save', 'Save')->setOption('icon', 'fa fa-save');

您可以在模板中自定义图标添加的方式。

"description"

"description" 属性在输入元素下方添加灰色辅助文本。例如

$form->addPassword('password', 'Password')->setOption('description', 'At least 8 characters.');

"info"

"info" 属性在标签旁边添加一个蓝色信息圆圈提示。例如

$form->addText('title', 'Title')->setOption('info', 'This appears on homepage');

"readonly"

"readonly" 属性,当设置为 true 时,使值不可修改且不提交。它以徽章的形式渲染。例如

$form->addText('title', 'Title')->setOption('readonly', true);
// or
{formRow $form['title'], readonly => true}
// or
$form->setReadonly(true); // to make the entire form readonly
// or
{neoForm yourForm, readonly => true} // to make the entire form readonly

您还可以提供一个回调函数以实现动态只读行为。

"class"

"class" 属性允许您覆盖行/输入/标签的类或任何其他 HTML 属性。例如

$form->addText('title', 'Title')->setOption('class', 'form-control form-control-lg');

如果您想保留模板中的类,请使用 +class 替代

$form->addText('title', 'Title')->setOption('+class', 'form-control-lg');

这也同样有效

{formRow $form['title'], input => ['+class' => 'form-control-lg']}`

如果您想强制从模板中移除一个类,请使用 false 替代

$form->addText('title', 'Title')->setOption('class', false);

"input""label"

您可以将这些属性应用到 {formRow} 标签,分别将 HTML 属性传递给输入和标签元素。例如

{formRow $form['title'], 'input' => ['class' => 'special']}
{formRow $form['title'], 'label' => ['class' => 'special']}

FormCollection

用法

use Efabrica\NeoForms\Build\NeoContainer;
// Create a new collection called "sources"
$form->addCollection('sources', 'Sources', function (NeoContainer $container) {
    // Add some fields to the collection
    $container->addText('bookTitle', 'Book Title');
    $container->addInteger('Year', 'Year');
    // Add another collection for authors
    $container->addCollection('authors', 'Authors', function (NeoContainer $container) {
        $container->addText('author', 'Author');
    });
});

您可以将表单渲染为表单中的任何其他控件。({formRow} 或自动)

处理

protected function onUpdate(NeoForm $form, array $values, ActiveRow $row): void
{
    // To process the form, you can get the new state of the collection like this:
    $sources = $values['sources'];
    
    // If you want to use the Diff API, you can do something like this:
    $diff = $form['sources']->getDiff();
    foreach($diff->getAdded() as $newRow) {
        $this->sourceRepository->insert($newRow);
    }
    foreach($diff->getDeleted() as $removedRow) {
        $this->sourceRepository->delete($removedRow);
    }
    foreach($diff->getModified() as $updatedRow) {
        $row = $this->sourceRepository->findOneBy($updatedRow->getOldRow());
        $row->update($updatedRow->getDiff());
    }
}

您不需要使用 Diff API。如果您例如只使用简单的单个文本输入集合,您可能会发现仅持久化新值数组更容易。

自定义模板

要创建自己的扩展模板以渲染表单,您可以遵循我们的示例。以下是一个使用 Bootstrap 4 作为示例的创建自定义扩展模板的逐步指南

步骤 1:创建一个新的 PHP 类

通过扩展基本模板类创建一个新的 PHP 类用于您的扩展模板。在这个示例中,我们将它命名为 Bootstrap5FormTemplate。确保将此类放置在适当的命名空间中,就像在提供的代码中一样。

namespace Your\Namespace\Here;

use Efabrica\NeoForms\Render\Template\NeoFormTemplate;
// ... Import other necessary classes here ...

class Bootstrap4FormTemplate extends NeoFormTemplate
{
    // Your template implementation goes here
}

步骤 2:自定义表单元素

覆盖扩展模板类中的方法,根据您首选的 Bootstrap 4 风格自定义表单元素的渲染。例如,您可以定义文本输入、按钮、复选框和其他表单元素应该如何与 Bootstrap 类一起渲染。

以下是自定义文本输入渲染的示例

protected function textInput(TextInput $control, array $attrs): Html
{
    $el = $control->getControl();
    $el->class ??= 'form-control'; // Add Bootstrap class, if no class was specified through ->setHtmlAttribute()
    return $this->applyAttrs($el, $attrs);
}

步骤 3:自定义表单标签

您也可以自定义表单标签的渲染方式。在 Bootstrap 中,您可能想要添加 col-form-label 类以实现正确对齐。通过覆盖 formLabel 方法来实现这一点

public function formLabel(BaseControl $control, array $attrs): Html
{
    $el = $control->getLabel();
    $el->class ??= 'col-form-label'; // Add Bootstrap class
    $el->class('required', $control->isRequired())->class('text-danger', $errors !== []);
    $this->addInfo($control, $el);

    foreach ($errors as $error) {
        $el->addHtml(
            Html::el('span')->class('c-button -icon -error -tooltip js-form-tooltip-error')
                ->setAttribute('data-bs-toggle', 'tooltip')
                ->title($control->translate($error))
                ->addHtml(Html::el('i', 'warning')->class('material-icons-round'))
        );
    }
    // Customize label rendering as needed
    return $this->applyAttrs($el, $attrs);
}

步骤 4:自定义按钮

对于按钮,您可以根据需要添加 Bootstrap 类和图标。像这样自定义按钮的渲染

protected function button(Button $control, array $attrs): Html
{
    $el = $control->getControl();
    $el->class ??= 'btn btn-primary'; // Add Bootstrap 4 classes
    $icon = $control->getOption('icon');
    if (is_string($icon) && trim($icon) !== '') {
        $el->insert(0, Html::el('i')->class("fa fa-$icon")); // Add an icon if available
    }
    // Customize button rendering as needed
    return $this->applyAttrs($el, $attrs);
}

步骤 5:自定义其他表单元素

根据您的所需样式,对复选框、单选按钮、选择框等其他表单元素重复类似的自定义操作。

步骤 6:实现额外样式

如果您的模板需要针对特定元素或表单组进行额外样式设置,您可以在扩展模板类中这样做。

步骤 7:应用您的自定义模板 要使用您的自定义模板,您需要实例化它并将其设置为渲染表单时的模板。例如

use Your\Namespace\Here\BootstrapFormTemplate;
use Efabrica\NeoForms\Build\NeoForm;

// Instantiate your custom template
$template = new Bootstrap4FormTemplate();

// Create a NeoForm instance and set the custom template
$form = new NeoForm();
$form->setTemplate($template);

// Render your form
echo $form;

如果您想为所有表单使用您的自定义模板,您可以通过重新连接自动连接将其设置为默认模板:)

# config.neon
services:
   neoForms.template: Your\Namespace\Here\Bootstrap4FormTemplate()

按照以下步骤,您可以创建自己的扩展模板,以便以符合Bootstrap 4或您偏好的任何自定义样式的方式渲染表单。根据您的具体样式需求自定义模板方法。