Skip to content

Commit

Permalink
[Feature] Integration with Symfony Forms (FormType column type)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kreyu committed Feb 27, 2023
1 parent 29021f5 commit 688e2ad
Show file tree
Hide file tree
Showing 13 changed files with 411 additions and 19 deletions.
249 changes: 249 additions & 0 deletions docs/advanced/integration-with-symfony-forms.md
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.
2 changes: 2 additions & 0 deletions docs/reference/columns.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The following column types are natively available in the bundle:
- [LinkType](#linktype)
- Special types
- [CollectionType](#collectiontype)
- [FormType](#formtype)
- [TemplateType](#templatetype)
- [ActionsType](#actionstype)
- Base types
Expand All @@ -29,6 +30,7 @@ The following column types are natively available in the bundle:
{% include-markdown "columns/types/datetime.md" heading-offset=2 %}
{% include-markdown "columns/types/link.md" heading-offset=2 %}
{% include-markdown "columns/types/collection.md" heading-offset=2 %}
{% include-markdown "columns/types/form.md" heading-offset=2 %}
{% include-markdown "columns/types/template.md" heading-offset=2 %}
{% include-markdown "columns/types/actions.md" heading-offset=2 %}
{% include-markdown "columns/types/column.md" heading-offset=2 %}
27 changes: 27 additions & 0 deletions docs/reference/columns/types/form.md
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).
16 changes: 15 additions & 1 deletion docs/reference/twig.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ they are very useful because they take use the theme configured in bundle.

### `data_table(data_table_view, variables)`

Renders the HTML of a complete data table.
Renders the HTML of a complete data table, with action bar, filtration, pagination, etc.

```twig
{# render the data table and disable the filtration feature #}
Expand All @@ -18,6 +18,14 @@ You will mostly use this helper for prototyping or if you use custom theme.
If you need more flexibility in rendering the data table, you should use the other helpers
to render individual parts of the data table instead.

### `data_table_table(data_table_view, variables)`

Renders the HTML of the data table.

### `data_table_action_bar(data_table_view, variables)`

Renders the HTML of the data table action bar, which includes filtration, exporting and personalization features.

### `data_table_headers_row(headers_row_view, variables)`

Renders the headers row of the data table.
Expand Down Expand Up @@ -59,6 +67,9 @@ If given value is instance of `FormInterface`, the `createView()` method will be

Renders the pagination controls.

Additionally, accepts the data table view as a first argument.
In this case, the pagination view is extracted from the data table view "pagination" variable.

## Variables

Certain types may define even more variables, and some variables here only really apply to certain types.
Expand Down Expand Up @@ -87,6 +98,8 @@ The following variables are common to every data table type.
| `headers_row` | Holds an instance of the headers row view. |
| `values_rows` | List of value rows views. |
| `pagination` | Holds an instance of the pagination view. |
| `has_active_filters` | Contains information whether the data table has at least one filter active. |
| `label_translation_domain` | Contains a translation domain used to translate column & filter labels, unless specified manually on the column or filter |

### Column Variables

Expand All @@ -102,6 +115,7 @@ The following variables are common to every column type.
| `block_name` | See [block_name option documentation](/reference/columns/#block_name). |
| `block_prefix` | See [block_prefix option documentation](/reference/columns/#block_prefix). |
| `value` | Final value that can be rendered to the user. |
| `exportable_value` | Final value that can be used in exports. |

### Filter Variables

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ nav:
- 'Twig': 'reference/twig.md'
- 'Advanced':
- 'Integration with Symfony UX Turbo': 'advanced/integration-with-symfony-ux-turbo.md'
- 'Integration with Symfony Forms': 'advanced/integration-with-symfony-forms.md'
- 'Creating custom request handler': 'advanced/creating-custom-request-handler.md'
- 'Creating data table type extension': 'advanced/creating-data-table-type-extension.md'

Expand Down
30 changes: 30 additions & 0 deletions src/Column/Type/FormType.php
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'])
;
}
}
Loading

0 comments on commit 688e2ad

Please sign in to comment.