-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Feature] Integration with Symfony Forms (FormType column type)
- Loading branch information
Showing
13 changed files
with
411 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
# Integration with Symfony Forms | ||
|
||
Imagine a following requirement: display a list of products, but with name and quantity as a form inputs. | ||
Additionally, display a submit button below the table to update every product name & quantity based on their corresponding inputs value. | ||
|
||
Let's start by creating a form type responsible for updating a product: | ||
|
||
```php | ||
// src/Form/Type/ProductType.php | ||
namespace App\Form\Type; | ||
|
||
use App\Entity\Product; | ||
use Symfony\Component\Form\AbstractType; | ||
use Symfony\Component\Form\Extension\Core\Type\NumberType; | ||
use Symfony\Component\Form\Extension\Core\Type\TextType; | ||
use Symfony\Component\Form\FormBuilderInterface; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
|
||
class ProductType extends AbstractType | ||
{ | ||
public function buildForm(FormBuilderInterface $builder, array $options) | ||
{ | ||
$builder | ||
->add('name', TextType::class) | ||
->add('quantity', NumberType::class) | ||
; | ||
} | ||
|
||
public function configureOptions(OptionsResolver $resolver): void | ||
{ | ||
$resolver->setDefault('data_class', Product::class); | ||
} | ||
} | ||
``` | ||
|
||
Then, create a form using created type. In this example, we're using form & data table builders to keep it simple. | ||
|
||
```php | ||
// src/Controller/ProductController.php | ||
namespace App\Controller; | ||
|
||
use App\Form\Type\ProductType; | ||
use App\Repository\ProductRepository; | ||
use Kreyu\Bundle\DataTableBundle\Column\Type\FormType; | ||
use Kreyu\Bundle\DataTableBundle\Column\Type\NumberType; | ||
use Kreyu\Bundle\DataTableBundle\Column\Type\TextType; | ||
use Kreyu\Bundle\DataTableBundle\DataTableControllerTrait; | ||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||
use Symfony\Component\Form\Extension\Core\Type\CollectionType; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
class ProductController extends AbstractController | ||
{ | ||
use DataTableControllerTrait; | ||
|
||
public function index(Request $request, ProductRepository $repository): Response | ||
{ | ||
$query = $repository->createQueryBuilder('product'); | ||
|
||
$form = $this->createForm(CollectionType::class, options: [ | ||
'entry_type' => ProductType::class, | ||
]); | ||
|
||
$dataTable = $this->createDataTableBuilder($query) | ||
->addColumn('id', NumberType::class) | ||
->addColumn('name', FormType::class, [ | ||
'form' => $form, | ||
]) | ||
->addColumn('quantity', FormType::class, [ | ||
'form' => $form, | ||
// Specifying form child path is optional. | ||
// By default, the column name is used. | ||
'form_child_path' => 'quantity', | ||
]) | ||
->getDataTable(); | ||
|
||
$dataTable->handleRequest($request); | ||
|
||
// Fill form with products on the current data table page. | ||
// Important: remember to do it AFTER handling the request, | ||
// as this is what determines the current page! | ||
$form->setData($dataTable->getPagination()->getItems()); | ||
$form->handleRequest($request); | ||
|
||
if ($form->isSubmitted() && $form->isValid()) { | ||
$products = $form->getData(); | ||
|
||
// Here $products is an ArrayIterator of updated App\Entity\Product entities. | ||
// You can flush the entity manager to save the changes. | ||
|
||
$repository->flush(); | ||
} | ||
|
||
return $this->render('product/index.html.twig', [ | ||
'data_table' => $dataTable->createView(), | ||
'form' => $form->createView(), | ||
]); | ||
} | ||
} | ||
``` | ||
|
||
Now, let's handle the templating part: | ||
|
||
If your data table is **NOT** using neither filtration, exporting nor personalization features, | ||
you can use the [data_table() function](../reference/twig.md#data_tabledata_table_view-variables) as usual, wrapping it in the form: | ||
|
||
```twig | ||
{# templates/product/index.html.twig #} | ||
{% extends 'base.html.twig' %} | ||
{% block content %} | ||
{{ form_start(form) }} | ||
{{ data_table(data_table) }} | ||
<div class="mt-2"> | ||
<button class="btn btn-primary">Update</button> | ||
</div> | ||
{# Important: notice the "render_rest" option! #} | ||
{{ form_end(form, { render_rest: false }) }} | ||
{% endblock %} | ||
``` | ||
|
||
!!! Warning | ||
|
||
Rendering like this is risky - if someone decides to enable one of mentioned features, the whole markup will totally break. | ||
If possible, use below method of wrapping only the table part in the form. | ||
|
||
If your data table is using either a filtration, exporting or personalization feature, you **HAVE TO** render each | ||
part of the table individually, because the [data_table() function](../reference/twig.md#data_tabledata_table_view-variables) | ||
renders out whole data table with corresponding feature form, and HTML forms cannot be nested, and this will totally break the markup; | ||
|
||
```twig | ||
{# templates/product/index.html.twig #} | ||
{% extends 'base.html.twig' %} | ||
{% block content %} | ||
{{ data_table_action_bar(data_table) }} | ||
{{ form_start(form) }} | ||
{{ data_table_table(data_table) }} | ||
<div class="mt-2"> | ||
<button class="btn btn-primary">Update</button> | ||
</div> | ||
{# Important: notice the "render_rest" option! #} | ||
{{ form_end(form, { render_rest: false }) }} | ||
{{ data_table_pagination(data_table) }} | ||
{% endblock %} | ||
``` | ||
|
||
!!! Warning | ||
|
||
You **HAVE TO** disable rendering rest of the fields in the `form_end` helper by passing the `render_rest` option as `false`, | ||
otherwise all the form fields will be rendered again below the table. This is because the Symfony Forms have no way | ||
of knowing the data table has rendered its form fields, because the bundle manually creates each field `FormView` in the background. | ||
|
||
## Passing the form to the data table type class | ||
|
||
While the above example is simple, it's not really re-usable, due to the usage of data table builder in the controller. | ||
That's why it's recommended to pass the form to the data table type: | ||
|
||
```php | ||
// src/DataTable/Type/ProductType.php | ||
namespace App\DataTable\Type; | ||
|
||
use Kreyu\Bundle\DataTableBundle\Column\Type\FormType; | ||
use Kreyu\Bundle\DataTableBundle\Column\Type\NumberType; | ||
use Kreyu\Bundle\DataTableBundle\Column\Type\TextType; | ||
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; | ||
use Kreyu\Bundle\DataTableBundle\Type\AbstractType; | ||
use Symfony\Component\Form\FormInterface; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
|
||
class ProductType extends AbstractType | ||
{ | ||
public function buildDataTable(DataTableBuilderInterface $builder, array $options): void | ||
{ | ||
$builder->addColumn('id', NumberType::class); | ||
|
||
if (null !== $form = $options['form']) { | ||
$builder | ||
->addColumn('name', FormType::class, [ | ||
'form' => $form, | ||
]) | ||
->addColumn('quantity', FormType::class, [ | ||
'form' => $form, | ||
]) | ||
; | ||
} else { | ||
$builder | ||
->addColumn('name', TextType::class) | ||
->addColumn('quantity', NumberType::class) | ||
; | ||
} | ||
} | ||
|
||
public function configureOptions(OptionsResolver $resolver): void | ||
{ | ||
$resolver | ||
->setDefault('form', null) | ||
->setAllowedTypes('form', ['null', FormInterface::class]) | ||
; | ||
} | ||
} | ||
``` | ||
|
||
and in the controller: | ||
|
||
```php | ||
// src/Controller/ProductController.php | ||
namespace App\Controller; | ||
|
||
use App\DataTable\Type as DataTable; | ||
use App\Form\Type as Form; | ||
use App\Repository\ProductRepository; | ||
use Kreyu\Bundle\DataTableBundle\DataTableControllerTrait; | ||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||
use Symfony\Component\Form\Extension\Core\Type\CollectionType; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
class ProductController extends AbstractController | ||
{ | ||
use DataTableControllerTrait; | ||
|
||
public function index(Request $request, ProductRepository $repository): Response | ||
{ | ||
$query = $repository->createQueryBuilder('product'); | ||
|
||
$form = $this->createForm(CollectionType::class, options: [ | ||
'entry_type' => Form\ProductType::class, | ||
]); | ||
|
||
// The data table with "name" and "quantity" columns displayed as a form inputs. | ||
$dataTable = $this->createDataTable(DataTable\ProductType::class, $query, [ | ||
'form' => $form, | ||
]); | ||
|
||
// The data table with "name" and "quantity" columns displayed regularly, because form is not passed. | ||
$dataTable = $this->createDataTable(DataTable\ProductType::class, $query); | ||
|
||
// ... | ||
} | ||
} | ||
``` | ||
|
||
By building it this way, it is possible to re-use same data table type with and without form integration. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# FormType | ||
|
||
The [FormType](https://github.com/Kreyu/data-table-bundle/blob/main/src/Column/Type/FormType.php) column represents a column with value displayed as a form input. | ||
|
||
For more details about how to use this column type, see [integration with Symfony Forms](../../../advanced/integration-with-symfony-forms.md). | ||
|
||
## Options | ||
|
||
### `form` | ||
|
||
**type**: `null` or `Symfony\Component\Form\FormInterface` | ||
|
||
This is the form that contains a collection of fields to display in the column. | ||
|
||
### `form_child_path` | ||
|
||
**type**: `null`, `false` or `string` **default**: `null` - the child path is "guessed" from the column name | ||
|
||
This is the path to the child form of each collection field. | ||
For example, if you have a collection of `ProductType` which contains `name` and `quantity` fields, | ||
and you want to display the `quantity` field on the column, this option value should equal `quantity`. | ||
|
||
Setting this option to `false` disables this functionality and renders the collection field directly. | ||
|
||
## Inherited options | ||
|
||
See [base column type documentation](column.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Kreyu\Bundle\DataTableBundle\Column\Type; | ||
|
||
use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; | ||
use Kreyu\Bundle\DataTableBundle\Column\ColumnView; | ||
use Symfony\Component\Form\FormInterface; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
|
||
class FormType extends AbstractType | ||
{ | ||
public function buildView(ColumnView $view, ColumnInterface $column, array $options): void | ||
{ | ||
if (null === $view->vars['form_child_path']) { | ||
$view->vars['form_child_path'] = $column->getName(); | ||
} | ||
} | ||
|
||
public function configureOptions(OptionsResolver $resolver): void | ||
{ | ||
$resolver | ||
->setRequired('form') | ||
->setDefault('form_child_path', null) | ||
->setAllowedTypes('form', FormInterface::class) | ||
->setAllowedTypes('form_child_path', ['null', 'bool', 'string']) | ||
; | ||
} | ||
} |
Oops, something went wrong.