Skip to content

Commit

Permalink
URL-oriented improvements (#117)
Browse files Browse the repository at this point in the history
* data table view `url_query_parameters` variable with an array of URL parameters for the specific data table
* improved url generators for filter clearing buttons, column sorting and (new) pagination controls
* new `state` Stimulus controller in place of previous `persistence` to load the state of the data table - currently it adds `url_query_parameters` to URL in the browser
  • Loading branch information
Kreyu authored Jul 30, 2024
1 parent f3e85dd commit 7cadde7
Show file tree
Hide file tree
Showing 25 changed files with 543 additions and 114 deletions.
23 changes: 0 additions & 23 deletions assets/controllers/persistence.js

This file was deleted.

41 changes: 41 additions & 0 deletions assets/controllers/state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
static values = {
urlQueryParameters: Object,
}

connect() {
this.#appendUrlQueryParameters();
}

#appendUrlQueryParameters() {
const url = new URL(window.location.href);

const parameters = this.#flattenParameters(this.urlQueryParametersValue);

for (const [key, value] of Object.entries(parameters)) {
if (!url.searchParams.has(key)) {
url.searchParams.set(key, String(value));
}
}

window.history.replaceState(null, null, url);
}

#flattenParameters(input, keyName) {
let result = {};

for (const key in input) {
const newKey = keyName ? `${keyName}[${key}]` : key;

if (typeof input[key] === "object" && !Array.isArray(input[key])) {
result = {...result, ...this.#flattenParameters(input[key], newKey)}
} else {
result[newKey] = input[key];
}
}

return result;
}
}
4 changes: 2 additions & 2 deletions assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"fetch": "eager",
"enabled": true
},
"persistence": {
"main": "controllers/persistence.js",
"state": {
"main": "controllers/state.js",
"fetch": "eager",
"enabled": true
}
Expand Down
4 changes: 2 additions & 2 deletions docs/src/docs/features/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,14 @@ class ProductController extends AbstractController

By default, the filters loaded from the persistence are not visible in the URL.

It is recommended to make sure the **persistence** controller is enabled in your `assets/controllers.json`,
It is recommended to make sure the **state** controller is enabled in your `assets/controllers.json`,
which will automatically append the filters to the URL, even if multiple data tables are visible on the same page.

```json
{
"controllers": {
"@kreyu/data-table-bundle": {
"persistence": {
"state": {
"enabled": true
}
}
Expand Down
19 changes: 19 additions & 0 deletions docs/src/docs/features/pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,25 @@ class ProductController extends AbstractController
```
:::

### Adding pagination loaded from persistence to URL

By default, the pagination loaded from the persistence is not visible in the URL.

It is recommended to make sure the **state** controller is enabled in your `assets/controllers.json`,
which will automatically append the pagination parameters to the URL, even if multiple data tables are visible on the same page.

```json
{
"controllers": {
"@kreyu/data-table-bundle": {
"state": {
"enabled": true
}
}
}
}
```

## Default pagination

The default pagination data can be overridden using the data table builder's `setDefaultPaginationData()` method:
Expand Down
19 changes: 19 additions & 0 deletions docs/src/docs/features/sorting.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,25 @@ class ProductController extends AbstractController
```
:::

### Adding sorting loaded from persistence to URL

By default, the sorting loaded from the persistence is not visible in the URL.

It is recommended to make sure the **state** controller is enabled in your `assets/controllers.json`,
which will automatically append the sorting parameters to the URL, even if multiple data tables are visible on the same page.

```json
{
"controllers": {
"@kreyu/data-table-bundle": {
"state": {
"enabled": true
}
}
}
}
```

## Default sorting

The default sorting data can be overridden using the data table builder's `setDefaultSortingData()` method:
Expand Down
2 changes: 1 addition & 1 deletion docs/src/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Now, add `@kreyu/data-table-bundle` controllers to your `assets/controllers.json
"personalization": {
"enabled": true
},
"persistence": {
"state": {
"enabled": true
},
"batch": {
Expand Down
12 changes: 11 additions & 1 deletion src/Column/ColumnSortUrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Kreyu\Bundle\DataTableBundle\Column;

use Kreyu\Bundle\DataTableBundle\DataTableView;
use Kreyu\Bundle\DataTableBundle\Exception\LogicException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
Expand All @@ -17,7 +18,7 @@ public function __construct(
) {
}

public function generate(ColumnHeaderView ...$columnHeaderViews): string
public function generate(DataTableView $dataTableView, ColumnHeaderView ...$columnHeaderViews): string
{
$request = $this->getRequest();

Expand All @@ -27,10 +28,19 @@ public function generate(ColumnHeaderView ...$columnHeaderViews): string

$parameters = [...$routeParams, ...$queryParams];

// Recursively replace/merge with the URL query parameters defined in the data table view.
// This allows the user to define custom query parameters that should be preserved when sorting columns.
$parameters = array_replace_recursive($parameters, $dataTableView->vars['url_query_parameters'] ?? []);

foreach ($columnHeaderViews as $columnHeaderView) {
$parameters = array_replace_recursive($parameters, $this->getColumnSortQueryParameters($columnHeaderView));
}

// Clearing the filters should reset the pagination to the first page.
if ($dataTableView->vars['pagination_enabled']) {
$parameters[$dataTableView->vars['page_parameter_name']] = 1;
}

return $this->urlGenerator->generate($route, $parameters);
}

Expand Down
4 changes: 3 additions & 1 deletion src/Column/ColumnSortUrlGeneratorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Kreyu\Bundle\DataTableBundle\Column;

use Kreyu\Bundle\DataTableBundle\DataTableView;

interface ColumnSortUrlGeneratorInterface
{
public function generate(ColumnHeaderView $columnHeaderView): string;
public function generate(DataTableView $dataTableView, ColumnHeaderView ...$columnHeaderView): string;
}
1 change: 1 addition & 0 deletions src/DependencyInjection/KreyuDataTableExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public function load(array $configs, ContainerBuilder $container): void
$loader->load('actions.php');
$loader->load('exporter.php');
$loader->load('filtration.php');
$loader->load('pagination.php');
$loader->load('personalization.php');
$loader->load('twig.php');

Expand Down
23 changes: 18 additions & 5 deletions src/Filter/FilterClearUrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Kreyu\Bundle\DataTableBundle\Filter;

use Kreyu\Bundle\DataTableBundle\DataTableView;
use Kreyu\Bundle\DataTableBundle\Exception\LogicException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
Expand All @@ -17,7 +18,7 @@ public function __construct(
) {
}

public function generate(FilterView ...$filterViews): string
public function generate(DataTableView $dataTableView, FilterView ...$filterViews): string
{
$request = $this->getRequest();

Expand All @@ -27,23 +28,35 @@ public function generate(FilterView ...$filterViews): string

$parameters = [...$routeParams, ...$queryParams];

// Recursively replace/merge with the URL query parameters defined in the data table view.
// This allows the user to define custom query parameters that should be preserved when clearing filters.
$parameters = array_replace_recursive($parameters, $dataTableView->vars['url_query_parameters'] ?? []);

foreach ($filterViews as $filterView) {
$parameters = array_replace_recursive($parameters, $this->getFilterClearQueryParameters($filterView));
}

// Clearing the filters should reset the pagination to the first page.
if ($dataTableView->vars['pagination_enabled']) {
$parameters[$dataTableView->vars['page_parameter_name']] = 1;
}

return $this->urlGenerator->generate($route, $parameters);
}

private function getFilterClearQueryParameters(FilterView $filterView): array
{
$parameters = ['value' => ''];

if ($filterView->vars['operator_selectable']) {
$parameters['operator'] = null;
}

$dataTableView = $filterView->parent;

return [
$dataTableView->vars['filtration_parameter_name'] => [
$filterView->vars['name'] => [
'value' => '',
'operator' => null,
],
$filterView->vars['name'] => $parameters,
],
];
}
Expand Down
4 changes: 3 additions & 1 deletion src/Filter/FilterClearUrlGeneratorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Kreyu\Bundle\DataTableBundle\Filter;

use Kreyu\Bundle\DataTableBundle\DataTableView;

interface FilterClearUrlGeneratorInterface
{
public function generate(FilterView ...$filterViews): string;
public function generate(DataTableView $dataTableView, FilterView ...$filterViews): string;
}
48 changes: 48 additions & 0 deletions src/Pagination/PaginationUrlGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Kreyu\Bundle\DataTableBundle\Pagination;

use Kreyu\Bundle\DataTableBundle\DataTableView;
use Kreyu\Bundle\DataTableBundle\Exception\LogicException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class PaginationUrlGenerator implements PaginationUrlGeneratorInterface
{
public function __construct(
private readonly RequestStack $requestStack,
private readonly UrlGeneratorInterface $urlGenerator,
) {
}

public function generate(DataTableView $dataTableView, int $page): string
{
$request = $this->getRequest();

$route = $request->attributes->get('_route');
$routeParams = $request->attributes->get('_route_params', []);
$queryParams = $request->query->all();

$parameters = [...$routeParams, ...$queryParams];

// Recursively replace/merge with the URL query parameters defined in the data table view.
// This allows the user to define custom query parameters that should be preserved when changing pages.
$parameters = array_replace_recursive($parameters, $dataTableView->vars['url_query_parameters'] ?? []);

$parameters[$dataTableView->vars['page_parameter_name']] = $page;

return $this->urlGenerator->generate($route, $parameters);
}

private function getRequest(): Request
{
if (null === $request = $this->requestStack->getCurrentRequest()) {
throw new LogicException('Unable to retrieve current request.');
}

return $request;
}
}
12 changes: 12 additions & 0 deletions src/Pagination/PaginationUrlGeneratorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Kreyu\Bundle\DataTableBundle\Pagination;

use Kreyu\Bundle\DataTableBundle\DataTableView;

interface PaginationUrlGeneratorInterface
{
public function generate(DataTableView $dataTableView, int $page): string;
}
1 change: 1 addition & 0 deletions src/Pagination/PaginationView.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public function __construct(
PaginationInterface $pagination,
) {
$this->vars = [
'data_table' => $this->parent,
'page_parameter_name' => $this->parent->vars['page_parameter_name'],
'current_page_number' => $pagination->getCurrentPageNumber(),
'current_page_item_count' => $pagination->getCurrentPageItemCount(),
Expand Down
23 changes: 23 additions & 0 deletions src/Resources/config/pagination.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

use Kreyu\Bundle\DataTableBundle\Pagination\PaginationUrlGenerator;
use Kreyu\Bundle\DataTableBundle\Pagination\PaginationUrlGeneratorInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

return static function (ContainerConfigurator $configurator) {
$services = $configurator->services();

$services
->set('kreyu_data_table.pagination.url_generator', PaginationUrlGenerator::class)
->args([
service('request_stack'),
service(UrlGeneratorInterface::class),
])
->alias(PaginationUrlGeneratorInterface::class, 'kreyu_data_table.pagination.url_generator')
;
};
1 change: 1 addition & 0 deletions src/Resources/config/twig.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
->args([
service('kreyu_data_table.column.column_sort_url_generator'),
service('kreyu_data_table.filter.filter_clear_url_generator'),
service('kreyu_data_table.pagination.url_generator'),
])
;
};
Loading

0 comments on commit 7cadde7

Please sign in to comment.