diff --git a/composer.json b/composer.json
index 5e14842df..917749513 100644
--- a/composer.json
+++ b/composer.json
@@ -47,6 +47,7 @@
"yiisoft/router": "^3.0",
"yiisoft/strings": "^2.0",
"yiisoft/translator": "^3.0",
+ "yiisoft/validator": "^1.1",
"yiisoft/view": "^8.0",
"yiisoft/widget": "^2.0"
},
diff --git a/config/widgets-themes.php b/config/widgets-themes.php
index f69970b3e..2240b67e1 100644
--- a/config/widgets-themes.php
+++ b/config/widgets-themes.php
@@ -3,6 +3,8 @@
declare(strict_types=1);
use Yiisoft\Yii\DataView\Column\ActionColumnRenderer;
+use Yiisoft\Yii\DataView\Filter\Widget\DropdownFilter;
+use Yiisoft\Yii\DataView\Filter\Widget\TextInputFilter;
use Yiisoft\Yii\DataView\GridView;
use Yiisoft\Yii\DataView\KeysetPagination;
use Yiisoft\Yii\DataView\OffsetPagination;
@@ -17,6 +19,9 @@
'sortableHeaderPrepend()' => ['
⭥
'],
'sortableHeaderAscPrepend()' => ['⭡
'],
'sortableHeaderDescPrepend()' => ['⭣
'],
+ 'filterCellAttributes()' => [['class' => 'align-top']],
+ 'filterCellInvalidClass()' => ['bg-danger bg-opacity-10'],
+ 'filterErrorsContainerAttributes()' => [['class' => 'text-danger mt-1']],
'addColumnRendererConfigs()' => [
[
ActionColumnRenderer::class => [
@@ -25,6 +30,12 @@
],
],
],
+ DropdownFilter::class => [
+ 'attributes()' => [['class' => 'form-select']],
+ ],
+ TextInputFilter::class => [
+ 'attributes()' => [['class' => 'form-control']],
+ ],
OffsetPagination::class => [
'listTag()' => ['ul'],
'listAttributes()' => [['class' => 'pagination']],
diff --git a/src/BaseListView.php b/src/BaseListView.php
index 7ec16e2b6..6d16d581b 100644
--- a/src/BaseListView.php
+++ b/src/BaseListView.php
@@ -12,7 +12,9 @@
use Yiisoft\Data\Paginator\PageToken;
use Yiisoft\Data\Paginator\PaginatorInterface;
use Yiisoft\Data\Reader\CountableDataInterface;
+use Yiisoft\Data\Reader\Filter\All;
use Yiisoft\Data\Reader\FilterableDataInterface;
+use Yiisoft\Data\Reader\FilterInterface;
use Yiisoft\Data\Reader\LimitableDataInterface;
use Yiisoft\Data\Reader\OffsetableDataInterface;
use Yiisoft\Data\Reader\ReadableDataInterface;
@@ -26,6 +28,7 @@
use Yiisoft\Translator\SimpleMessageFormatter;
use Yiisoft\Translator\Translator;
use Yiisoft\Translator\TranslatorInterface;
+use Yiisoft\Validator\Result as ValidationResult;
use Yiisoft\Widget\Widget;
use Yiisoft\Yii\DataView\Exception\DataReaderNotSetException;
@@ -83,7 +86,7 @@ abstract class BaseListView extends Widget
protected array $urlArguments = [];
protected array $urlQueryParameters = [];
- private UrlParameterProviderInterface|null $urlParameterProvider = null;
+ protected UrlParameterProviderInterface|null $urlParameterProvider = null;
private bool $ignoreMissingPage = true;
@@ -170,11 +173,11 @@ final public function urlParameterProvider(?UrlParameterProviderInterface $provi
/**
* Renders the data models.
*
- * @return string the rendering result.
+ * @return string The rendering result.
*
* @psalm-param array $items
*/
- abstract protected function renderItems(array $items): string;
+ abstract protected function renderItems(array $items, ValidationResult $filterValidationResult): string;
final public function containerTag(?string $tag): static
{
@@ -240,11 +243,21 @@ public function getDataReader(): ReadableDataInterface
}
/**
+ * @psalm-return list{FilterInterface[],ValidationResult}
+ */
+ protected function makeFilters(): array
+ {
+ return [[], new ValidationResult()];
+ }
+
+ /**
+ * @param FilterInterface[] $filters
+ *
* @throws PageNotFoundException
*
* @psalm-return array
*/
- private function prepareDataReaderAndGetItems(): array
+ private function prepareDataReaderAndGetItems(array $filters): array
{
$page = $this->urlParameterProvider?->get(
$this->urlConfig->getPageParameterName(),
@@ -263,7 +276,7 @@ private function prepareDataReaderAndGetItems(): array
$this->urlConfig->getSortParameterType(),
);
- $this->preparedDataReader = $this->prepareDataReaderByParams($page, $previousPage, $pageSize, $sort);
+ $this->preparedDataReader = $this->prepareDataReaderByParams($page, $previousPage, $pageSize, $sort, $filters);
try {
return $this->getItems($this->preparedDataReader);
@@ -271,7 +284,7 @@ private function prepareDataReaderAndGetItems(): array
}
if ($this->ignoreMissingPage) {
- $this->preparedDataReader = $this->prepareDataReaderByParams(null, null, $pageSize, $sort);
+ $this->preparedDataReader = $this->prepareDataReaderByParams(null, null, $pageSize, $sort, $filters);
try {
return $this->getItems($this->preparedDataReader);
} catch (PageNotFoundException $exception) {
@@ -296,11 +309,15 @@ private function getItems(ReadableDataInterface $dataReader): array
return is_array($items) ? $items : iterator_to_array($items);
}
+ /**
+ * @param FilterInterface[] $filters
+ */
private function prepareDataReaderByParams(
?string $page,
?string $previousPage,
?string $pageSize,
?string $sort,
+ array $filters,
): ReadableDataInterface {
$dataReader = $this->getDataReader();
@@ -345,6 +362,10 @@ private function prepareDataReaderByParams(
}
}
+ if ($dataReader->isFilterable() && !empty($filters)) {
+ $dataReader = $dataReader->withFilter(new All(...$filters));
+ }
+
return $dataReader;
}
@@ -528,7 +549,8 @@ protected function renderEmpty(int $colspan): Td
public function render(): string
{
- $items = $this->prepareDataReaderAndGetItems();
+ [$filters, $filterValidationResult] = $this->makeFilters();
+ $items = $this->prepareDataReaderAndGetItems($filters);
$content = trim(
strtr(
@@ -536,7 +558,7 @@ public function render(): string
[
'{header}' => $this->renderHeader(),
'{toolbar}' => $this->toolbar,
- '{items}' => $this->renderItems($items),
+ '{items}' => $this->renderItems($items, $filterValidationResult),
'{summary}' => $this->renderSummary(),
'{pager}' => $this->renderPagination(),
],
diff --git a/src/Column/Base/Cell.php b/src/Column/Base/Cell.php
index acbf025bc..2a3e0ed0c 100644
--- a/src/Column/Base/Cell.php
+++ b/src/Column/Base/Cell.php
@@ -12,11 +12,17 @@ final class Cell
{
private bool $doubleEncode = true;
+ /**
+ * @psalm-var array
+ */
+ private array $content;
+
public function __construct(
private array $attributes = [],
private ?bool $encode = null,
- private string|Stringable $content = '',
+ string|Stringable ...$content,
) {
+ $this->content = $content;
}
/**
@@ -48,7 +54,7 @@ public function doubleEncode(bool $doubleEncode): self
/**
* @param string|Stringable $content Tag content.
*/
- public function content(string|Stringable $content): self
+ public function content(string|Stringable ...$content): self
{
$new = clone $this;
$new->content = $content;
@@ -115,8 +121,21 @@ public function isDoubleEncode(): bool
return $this->doubleEncode;
}
- public function getContent(): string|Stringable
+ /**
+ * @psalm-return array
+ */
+ public function getContent(): array
{
return $this->content;
}
+
+ public function isEmptyContent(): bool
+ {
+ foreach ($this->content as $content) {
+ if (!empty((string) $content)) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/src/Column/Base/FilterContext.php b/src/Column/Base/FilterContext.php
new file mode 100644
index 000000000..19ee50d9a
--- /dev/null
+++ b/src/Column/Base/FilterContext.php
@@ -0,0 +1,26 @@
+urlParameterProvider?->get($name, UrlParameterType::QUERY);
+ }
+}
diff --git a/src/Column/Base/MakeFilterContext.php b/src/Column/Base/MakeFilterContext.php
new file mode 100644
index 000000000..a518bcb82
--- /dev/null
+++ b/src/Column/Base/MakeFilterContext.php
@@ -0,0 +1,23 @@
+urlParameterProvider?->get($name, UrlParameterType::QUERY);
+ }
+}
diff --git a/src/Column/DataColumn.php b/src/Column/DataColumn.php
index e19f06a66..bee0dd8b2 100644
--- a/src/Column/DataColumn.php
+++ b/src/Column/DataColumn.php
@@ -4,6 +4,10 @@
namespace Yiisoft\Yii\DataView\Column;
+use Yiisoft\Validator\RuleInterface;
+use Yiisoft\Yii\DataView\Filter\Factory\FilterFactoryInterface;
+use Yiisoft\Yii\DataView\Filter\Widget\FilterWidget;
+
/**
* DetailColumn is the default column type for the {@see GridView} widget.
*
@@ -20,6 +24,10 @@ final class DataColumn implements ColumnInterface
{
public readonly ?string $queryProperty;
+ /**
+ * @psalm-param bool|array>|FilterWidget $filter
+ * @psalm-param RuleInterface[]|RuleInterface|null $filterValidation
+ */
public function __construct(
public readonly ?string $property = null,
?string $queryProperty = null,
@@ -32,6 +40,9 @@ public function __construct(
public readonly bool $withSorting = true,
public readonly mixed $content = null,
public readonly ?string $dateTimeFormat = null,
+ public readonly bool|array|FilterWidget $filter = false,
+ public readonly string|FilterFactoryInterface|null $filterFactory = null,
+ public readonly array|RuleInterface|null $filterValidation = null,
private readonly bool $visible = true,
) {
$this->queryProperty = $queryProperty ?? $this->property;
diff --git a/src/Column/DataColumnRenderer.php b/src/Column/DataColumnRenderer.php
index fbc16b94e..7e06a9308 100644
--- a/src/Column/DataColumnRenderer.php
+++ b/src/Column/DataColumnRenderer.php
@@ -6,17 +6,32 @@
use DateTimeInterface;
use InvalidArgumentException;
+use Psr\Container\ContainerInterface;
use Yiisoft\Arrays\ArrayHelper;
+use Yiisoft\Data\Reader\FilterInterface;
use Yiisoft\Html\Html;
+use Yiisoft\Validator\ValidatorInterface;
use Yiisoft\Yii\DataView\Column\Base\Cell;
use Yiisoft\Yii\DataView\Column\Base\DataContext;
+use Yiisoft\Yii\DataView\Column\Base\FilterContext;
use Yiisoft\Yii\DataView\Column\Base\GlobalContext;
use Yiisoft\Yii\DataView\Column\Base\HeaderContext;
-
-final class DataColumnRenderer implements ColumnRendererInterface
+use Yiisoft\Yii\DataView\Column\Base\MakeFilterContext;
+use Yiisoft\Yii\DataView\Filter\Factory\EqualsFilterFactory;
+use Yiisoft\Yii\DataView\Filter\Factory\FilterFactoryInterface;
+use Yiisoft\Yii\DataView\Filter\Factory\LikeFilterFactory;
+use Yiisoft\Yii\DataView\Filter\Widget\Context;
+use Yiisoft\Yii\DataView\Filter\Widget\DropdownFilter;
+use Yiisoft\Yii\DataView\Filter\Widget\TextInputFilter;
+
+final class DataColumnRenderer implements FilterableColumnRendererInterface
{
public function __construct(
+ private readonly ContainerInterface $filterFactoryContainer,
+ private readonly ValidatorInterface $validator,
private readonly string $dateTimeFormat = 'Y-m-d H:i:s',
+ private readonly string $defaultFilterFactory = LikeFilterFactory::class,
+ private readonly string $defaultArrayFilterFactory = EqualsFilterFactory::class,
) {
}
@@ -53,6 +68,82 @@ public function renderHeader(ColumnInterface $column, Cell $cell, HeaderContext
return $cell->content($prepend . ($link ?? $label) . $append);
}
+ public function renderFilter(ColumnInterface $column, Cell $cell, FilterContext $context): ?Cell
+ {
+ $this->checkColumn($column);
+
+ if ($column->queryProperty === null || $column->filter === false) {
+ return null;
+ }
+
+ if ($column->filter === true) {
+ $widget = TextInputFilter::widget();
+ } elseif (is_array($column->filter)) {
+ $widget = DropdownFilter::widget()->optionsData($column->filter);
+ } else {
+ $widget = $column->filter;
+ }
+
+ $content = [
+ $widget->withContext(
+ new Context(
+ $column->queryProperty,
+ $context->getQueryValue($column->queryProperty),
+ $context->formId
+ )
+ ),
+ ];
+
+ $errors = $context->validationResult->getAttributeErrorMessages($column->queryProperty);
+ if (!empty($errors)) {
+ $cell = $cell->addClass($context->cellInvalidClass);
+ $content[] = Html::div(attributes: $context->errorsContainerAttributes)
+ ->content(...array_map(static fn(string $error) => Html::div($error), $errors));
+ }
+
+ return $cell->content(...$content)->encode(false);
+ }
+
+ public function makeFilter(ColumnInterface $column, MakeFilterContext $context): ?FilterInterface
+ {
+ $this->checkColumn($column);
+ if ($column->queryProperty === null) {
+ return null;
+ }
+
+ $value = $context->getQueryValue($column->queryProperty);
+ if ($column->filterValidation !== null) {
+ $result = $this->validator->validate($value, $column->filterValidation);
+ if (!$result->isValid()) {
+ foreach ($result->getErrors() as $error) {
+ $context->validationResult->addError(
+ $error->getMessage(),
+ $error->getParameters(),
+ [$column->queryProperty]
+ );
+ }
+ return null;
+ }
+ }
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ if ($column->filterFactory === null) {
+ /** @var FilterFactoryInterface $factory */
+ $factory = $this->filterFactoryContainer->get(
+ is_array($column->filter) ? $this->defaultArrayFilterFactory : $this->defaultFilterFactory
+ );
+ } elseif (is_string($column->filterFactory)) {
+ /** @var FilterFactoryInterface $factory */
+ $factory = $this->filterFactoryContainer->get($column->filterFactory);
+ } else {
+ $factory = $column->filterFactory;
+ }
+
+ return $factory->create($column->queryProperty, $value);
+ }
+
public function renderBody(ColumnInterface $column, Cell $cell, DataContext $context): Cell
{
$this->checkColumn($column);
diff --git a/src/Column/FilterableColumnRendererInterface.php b/src/Column/FilterableColumnRendererInterface.php
new file mode 100644
index 000000000..aded5537e
--- /dev/null
+++ b/src/Column/FilterableColumnRendererInterface.php
@@ -0,0 +1,17 @@
+> $data
+ */
+ public function optionsData(
+ array $data,
+ bool $encode = true,
+ array $optionsAttributes = [],
+ array $groupsAttributes = []
+ ): self {
+ $new = clone $this;
+ $new->select = $this->getSelect()->optionsData($data, $encode, $optionsAttributes, $groupsAttributes);
+ return $new;
+ }
+
+ /**
+ * Add a set of attributes to existing tag attributes.
+ * Same named attributes are replaced.
+ *
+ * @param array $attributes Name-value set of attributes.
+ *
+ * @see Select::addAttributes()
+ */
+ public function addAttributes(array $attributes): static
+ {
+ $new = clone $this;
+ $new->select = $this->getSelect()->addAttributes($attributes);
+ return $new;
+ }
+
+ /**
+ * Replace attributes with a new set.
+ *
+ * @param array $attributes Name-value set of attributes.
+ *
+ * @see Select::attributes()
+ */
+ public function attributes(array $attributes): self
+ {
+ $new = clone $this;
+ $new->select = $this->getSelect()->attributes($attributes);
+ return $new;
+ }
+
+ public function renderFilter(Context $context): string
+ {
+ $select = $this->getSelect()
+ ->name($context->property)
+ ->form($context->formId)
+ ->attribute('onChange', 'this.form.submit()');
+
+ if ($context->value !== null) {
+ $select = $select->value($context->value);
+ }
+
+ return $select->render();
+ }
+
+ private function getSelect(): Select
+ {
+ return $this->select ?? Select::tag()->prompt('');
+ }
+}
diff --git a/src/Filter/Widget/FilterWidget.php b/src/Filter/Widget/FilterWidget.php
new file mode 100644
index 000000000..f271a5e1e
--- /dev/null
+++ b/src/Filter/Widget/FilterWidget.php
@@ -0,0 +1,26 @@
+context = $context;
+ return $new;
+ }
+
+ final public function render(): string
+ {
+ return $this->renderFilter($this->context);
+ }
+
+ abstract public function renderFilter(Context $context): string;
+}
diff --git a/src/Filter/Widget/TextInputFilter.php b/src/Filter/Widget/TextInputFilter.php
new file mode 100644
index 000000000..936368b5a
--- /dev/null
+++ b/src/Filter/Widget/TextInputFilter.php
@@ -0,0 +1,56 @@
+input = $this->getInput()->addAttributes($attributes);
+ return $new;
+ }
+
+ /**
+ * Replace attributes with a new set.
+ *
+ * @param array $attributes Name-value set of attributes.
+ *
+ * @see Input::attributes()
+ */
+ public function attributes(array $attributes): self
+ {
+ $new = clone $this;
+ $new->input = $this->getInput()->attributes($attributes);
+ return $new;
+ }
+
+ public function renderFilter(Context $context): string
+ {
+ return $this->getInput()
+ ->name($context->property)
+ ->value($context->value)
+ ->form($context->formId)
+ ->render();
+ }
+
+ private function getInput(): Input
+ {
+ return $this->input ?? Html::textInput();
+ }
+}
diff --git a/src/GridView.php b/src/GridView.php
index a420911bf..dfcd79065 100644
--- a/src/GridView.php
+++ b/src/GridView.php
@@ -14,13 +14,17 @@
use Yiisoft\Html\Html;
use Yiisoft\Html\Tag\Tr;
use Yiisoft\Translator\TranslatorInterface;
-use Yiisoft\Yii\DataView\Column\ActionColumn;
+use Yiisoft\Validator\Result as ValidationResult;
use Yiisoft\Yii\DataView\Column\Base\Cell;
+use Yiisoft\Yii\DataView\Column\Base\FilterContext;
use Yiisoft\Yii\DataView\Column\Base\GlobalContext;
use Yiisoft\Yii\DataView\Column\Base\DataContext;
use Yiisoft\Yii\DataView\Column\Base\HeaderContext;
+use Yiisoft\Yii\DataView\Column\Base\MakeFilterContext;
use Yiisoft\Yii\DataView\Column\Base\RendererContainer;
use Yiisoft\Yii\DataView\Column\ColumnInterface;
+use Yiisoft\Yii\DataView\Column\ColumnRendererInterface;
+use Yiisoft\Yii\DataView\Column\FilterableColumnRendererInterface;
/**
* The GridView widget is used to display data in a grid.
@@ -42,6 +46,11 @@ final class GridView extends BaseListView
*/
private array $columns = [];
+ /**
+ * @var ColumnInterface[]
+ */
+ private ?array $columnsCache = null;
+
private bool $columnsGroupEnabled = false;
private string $emptyCell = ' ';
private bool $footerEnabled = false;
@@ -69,8 +78,17 @@ final class GridView extends BaseListView
private ?string $sortableLinkAscClass = null;
private ?string $sortableLinkDescClass = null;
+ private array $filterCellAttributes = [];
+ private ?string $filterCellInvalidClass = null;
+ private array $filterErrorsContainerAttributes = [];
+
private RendererContainer $columnRendererContainer;
+ /**
+ * @var ColumnRendererInterface[]
+ */
+ private ?array $columnRenderersCache = null;
+
public function __construct(
ContainerInterface $columnRenderersDependencyContainer,
TranslatorInterface|null $translator = null,
@@ -89,6 +107,32 @@ public function addColumnRendererConfigs(array $configs): self
return $new;
}
+ /**
+ * Return new instance with the HTML attributes for the filter cell (`td`) tag.
+ *
+ * @param array $attributes The tag attributes in terms of name-value pairs.
+ */
+ public function filterCellAttributes(array $attributes): self
+ {
+ $new = clone $this;
+ $new->filterCellAttributes = $attributes;
+ return $new;
+ }
+
+ public function filterCellInvalidClass(?string $class): self
+ {
+ $new = clone $this;
+ $new->filterCellInvalidClass = $class;
+ return $new;
+ }
+
+ public function filterErrorsContainerAttributes(array $attributes): self
+ {
+ $new = clone $this;
+ $new->filterErrorsContainerAttributes = $attributes;
+ return $new;
+ }
+
public function enableMultiSort(bool $value = true): self
{
$new = clone $this;
@@ -397,19 +441,13 @@ public function sortableHeaderDescAppend(string|Stringable $content): self
/**
* Renders the data active record classes for the grid view.
*/
- protected function renderItems(array $items): string
+ protected function renderItems(array $items, ValidationResult $filterValidationResult): string
{
- $columns = array_filter(
- $this->columns,
- static fn(ColumnInterface $column) => $column->isVisible()
- );
-
- $renderers = [];
- foreach ($columns as $i => $column) {
- $renderers[$i] = $this->columnRendererContainer->get($column->getRenderer());
- }
+ $columns = $this->getColumns();
+ $renderers = $this->getColumnRenderers();
$blocks = [];
+ $filtersForm = '';
$dataReader = $this->getDataReader();
$globalContext = new GlobalContext(
@@ -420,6 +458,69 @@ protected function renderItems(array $items): string
$this->translationCategory,
);
+ if ($this->preparedDataReader instanceof PaginatorInterface) {
+ $pageToken = $this->preparedDataReader->isOnFirstPage() ? null : $this->preparedDataReader->getToken();
+ $pageSize = $this->preparedDataReader->getPageSize();
+ if ($pageSize === $this->getDefaultPageSize()) {
+ $pageSize = null;
+ }
+ } else {
+ $pageToken = null;
+ $pageSize = null;
+ }
+
+ $tags = [];
+ $hasFilters = false;
+ $filterContext = new FilterContext(
+ formId: Html::generateId(),
+ validationResult: $filterValidationResult,
+ cellInvalidClass: $this->filterCellInvalidClass,
+ errorsContainerAttributes: $this->filterErrorsContainerAttributes,
+ urlParameterProvider: $this->urlParameterProvider,
+ );
+ foreach ($columns as $i => $column) {
+ $cell = $renderers[$i] instanceof FilterableColumnRendererInterface
+ ? $renderers[$i]->renderFilter($column, new Cell(attributes: $this->filterCellAttributes), $filterContext)
+ : null;
+ if ($cell === null) {
+ $tags[] = Html::td(' ')->encode(false);
+ } else {
+ $tags[] = Html::td(attributes: $cell->getAttributes())
+ ->content(...$cell->getContent())
+ ->encode($cell->isEncode())
+ ->doubleEncode($cell->isDoubleEncode());
+ $hasFilters = true;
+ }
+ }
+ if ($hasFilters) {
+ $sort = $this->urlParameterProvider?->get(
+ $this->urlConfig->getSortParameterName(),
+ $this->urlConfig->getSortParameterType()
+ );
+ $url = $this->urlCreator === null ? '' : call_user_func_array(
+ $this->urlCreator,
+ UrlParametersFactory::create(
+ null,
+ $this->urlConfig->getPageSizeParameterType() === UrlParameterType::PATH ? $pageSize : null,
+ $this->urlConfig->getSortParameterType() === UrlParameterType::PATH ? $sort : null,
+ $this->urlConfig,
+ )
+ );
+ $content = [Html::submitButton()];
+ if ($this->urlConfig->getPageSizeParameterType() === UrlParameterType::QUERY && !empty($pageSize)) {
+ $content[] = Html::hiddenInput($this->urlConfig->getPageSizeParameterName(), $pageSize);
+ }
+ if ($this->urlConfig->getSortParameterType() === UrlParameterType::QUERY && !empty($sort)) {
+ $content[] = Html::hiddenInput($this->urlConfig->getSortParameterName(), $sort);
+ }
+ $filtersForm = Html::form($url, 'GET', ['id' => $filterContext->formId, 'style' => 'display:none'])
+ ->content(...$content)
+ ->render();
+ $filterRow = Html::tr()->cells(...$tags);
+ } else {
+ $filterRow = null;
+ }
+
if ($this->columnsGroupEnabled) {
$tags = [];
foreach ($columns as $i => $column) {
@@ -430,16 +531,6 @@ protected function renderItems(array $items): string
}
if ($this->headerTableEnabled) {
- if ($this->preparedDataReader instanceof PaginatorInterface) {
- $pageToken = $this->preparedDataReader->isOnFirstPage() ? null : $this->preparedDataReader->getToken();
- $pageSize = $this->preparedDataReader->getPageSize();
- if ($pageSize === $this->getDefaultPageSize()) {
- $pageSize = null;
- }
- } else {
- $pageToken = null;
- $pageSize = null;
- }
$headerContext = new HeaderContext(
$this->getSort($dataReader),
$this->getSort($this->preparedDataReader),
@@ -469,12 +560,16 @@ protected function renderItems(array $items): string
$tags[] = $cell === null
? Html::th(' ')->encode(false)
: Html::th(attributes: $cell->getAttributes())
- ->content($cell->getContent())
+ ->content(...$cell->getContent())
->encode($cell->isEncode())
->doubleEncode($cell->isDoubleEncode());
}
$headerRow = Html::tr($this->headerRowAttributes)->cells(...$tags);
- $blocks[] = Html::thead()->rows($headerRow)->render();
+ $thead = Html::thead()->rows($headerRow);
+ if ($filterRow !== null) {
+ $thead = $thead->addRows($filterRow);
+ }
+ $blocks[] = $thead->render();
}
if ($this->footerEnabled) {
@@ -485,10 +580,8 @@ protected function renderItems(array $items): string
(new Cell())->content(' ')->encode(false),
$globalContext
);
- /** @var string|Stringable $content */
- $content = $cell->getContent();
$tags[] = Html::td(attributes: $cell->getAttributes())
- ->content($content)
+ ->content(...$cell->getContent())
->encode($cell->isEncode())
->doubleEncode($cell->isDoubleEncode());
}
@@ -511,11 +604,10 @@ protected function renderItems(array $items): string
foreach ($columns as $i => $column) {
$context = new DataContext($column, $value, $key, $index);
$cell = $renderers[$i]->renderBody($column, new Cell(), $context);
- $content = $cell->getContent();
- $tags[] = empty($content)
+ $tags[] = $cell->isEmptyContent()
? Html::td()->content($this->emptyCell)->encode(false)
: Html::td(attributes: $this->prepareBodyAttributes($cell->getAttributes(), $context))
- ->content($content)
+ ->content(...$cell->getContent())
->encode($cell->isEncode())
->doubleEncode($cell->isDoubleEncode());
}
@@ -537,13 +629,36 @@ protected function renderItems(array $items): string
->render()
: Html::tbody($this->tbodyAttributes)->rows(...$rows)->render();
- return Html::tag('table', attributes: $this->tableAttributes)->open()
+ return
+ $filtersForm .
+ Html::tag('table', attributes: $this->tableAttributes)->open()
. "\n"
. implode("\n", $blocks)
. "\n"
. '';
}
+ protected function makeFilters(): array
+ {
+ $columns = $this->getColumns();
+ $renderers = $this->getColumnRenderers();
+
+ $validationResult = new ValidationResult();
+ $context = new MakeFilterContext($validationResult, $this->urlParameterProvider);
+
+ $filters = [];
+ foreach ($columns as $i => $column) {
+ if ($renderers[$i] instanceof FilterableColumnRendererInterface) {
+ $filter = $renderers[$i]->makeFilter($column, $context);
+ if ($filter !== null) {
+ $filters[] = $filter;
+ }
+ }
+ }
+
+ return [$filters, $validationResult];
+ }
+
private function prepareBodyAttributes(array $attributes, DataContext $context): array
{
foreach ($attributes as $i => $attribute) {
@@ -567,4 +682,34 @@ private function getSort(?ReadableDataInterface $dataReader): ?Sort
return null;
}
+
+ /**
+ * @return ColumnInterface[]
+ */
+ private function getColumns(): array
+ {
+ if ($this->columnsCache === null) {
+ $this->columnsCache = array_filter(
+ $this->columns,
+ static fn(ColumnInterface $column) => $column->isVisible()
+ );
+ }
+
+ return $this->columnsCache;
+ }
+
+ /**
+ * @return ColumnRendererInterface[]
+ */
+ private function getColumnRenderers(): array
+ {
+ if ($this->columnRenderersCache === null) {
+ $this->columnRenderersCache = array_map(
+ fn(ColumnInterface $column) => $this->columnRendererContainer->get($column->getRenderer()),
+ $this->getColumns()
+ );
+ }
+
+ return $this->columnRenderersCache;
+ }
}
diff --git a/src/ListView.php b/src/ListView.php
index c17adeed0..af4e483ae 100644
--- a/src/ListView.php
+++ b/src/ListView.php
@@ -216,7 +216,7 @@ protected function renderItem(array|object $data, mixed $key, int $index): strin
*
* @throws Throwable|ViewNotFoundException
*/
- protected function renderItems(array $items): string
+ protected function renderItems(array $items, \Yiisoft\Validator\Result $filterValidationResult): string
{
$keys = array_keys($items);
$rows = [];
diff --git a/tests/GridView/ImmutableTest.php b/tests/GridView/ImmutableTest.php
index b3a027428..40eee4bb8 100644
--- a/tests/GridView/ImmutableTest.php
+++ b/tests/GridView/ImmutableTest.php
@@ -67,7 +67,7 @@ public function testGridView(): void
private function createBaseListView(): DataView\BaseListView
{
return new class () extends DataView\BaseListView {
- public function renderItems(array $items): string
+ public function renderItems(array $items, \Yiisoft\Validator\Result $filterValidationResult): string
{
return '';
}
diff --git a/tests/GridView/TranslatorTest.php b/tests/GridView/TranslatorTest.php
index 94f1f6c16..ab540ec17 100644
--- a/tests/GridView/TranslatorTest.php
+++ b/tests/GridView/TranslatorTest.php
@@ -11,9 +11,10 @@
use Yiisoft\Definitions\Reference;
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;
-use Yiisoft\Di\NotFoundException;
use Yiisoft\Translator\Translator;
use Yiisoft\Translator\TranslatorInterface;
+use Yiisoft\Validator\Validator;
+use Yiisoft\Validator\ValidatorInterface;
use Yiisoft\Widget\WidgetFactory;
use Yiisoft\Yii\DataView\Column\DataColumn;
use Yiisoft\Yii\DataView\Column\SerialColumn;
@@ -33,12 +34,6 @@ final class TranslatorTest extends TestCase
];
private TranslatorInterface $translator;
- /**
- * @throws CircularReferenceException
- * @throws InvalidConfigException
- * @throws NotInstantiableException
- * @throws NotFoundException
- */
protected function setUp(): void
{
$container = new Container(ContainerConfig::create()->withDefinitions($this->config()));
@@ -329,6 +324,7 @@ private function config(): array
'categories' => Reference::to('tag@translation.categorySource'),
],
],
+ ValidatorInterface::class => Validator::class,
],
$containerDefinitions,
);
diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php
index 5c66f4b1b..7abdb1208 100644
--- a/tests/Support/TestTrait.php
+++ b/tests/Support/TestTrait.php
@@ -16,6 +16,8 @@
use Yiisoft\Router\CurrentRoute;
use Yiisoft\Router\Route;
use Yiisoft\Router\UrlGeneratorInterface;
+use Yiisoft\Validator\Validator;
+use Yiisoft\Validator\ValidatorInterface;
use Yiisoft\Widget\WidgetFactory;
use Yiisoft\Yii\DataView\Column\ActionColumnRenderer;
use Yiisoft\Yii\DataView\GridView;
@@ -73,6 +75,7 @@ private function config(): array
return [
CurrentRoute::class => $currentRoute,
UrlGeneratorInterface::class => Mock::urlGenerator([], $currentRoute),
+ ValidatorInterface::class => Validator::class,
];
}
}