Skip to content

Commit

Permalink
feature #3220 Allow to render ChoiceField values as badges (javieregu…
Browse files Browse the repository at this point in the history
…iluz)

This PR was squashed before being merged into the 3.0.x-dev branch.

Discussion
----------

Allow to render ChoiceField values as badges

This was one of the main features in my TODO list before the EA3 release. The new `renderAsBadges()` method is very flexible, as explained in its PHPdoc:

```
    /**
     * Possible values of $boolArrayOrCallable:
     *   * true: values are displayed as a normal badge
     *   * false: values are displayed as regular text
     *   * array: keys are the field value and values are the badge type
     *   * callable: the FieldDto is passed as argument and return value is the badge type
     *
     * Possible badge types: 'success', 'warning', 'danger', 'info', 'primary', 'secondary', 'light', 'dark'
     */
    public function renderAsBadges($boolArrayOrCallable = true): self;
```

This is how badges look in the index page:

![badges-index](https://user-images.githubusercontent.com/73419/81980767-7e035b80-962f-11ea-8fd0-01e68ee8c7b6.png)

And in the detail page:

![badges-detail](https://user-images.githubusercontent.com/73419/81980778-8196e280-962f-11ea-9f7e-ba9ece7324a4.png)

Commits
-------

0fab91b Allow to render ChoiceField values as badges
  • Loading branch information
javiereguiluz committed May 15, 2020
2 parents 38b8837 + 0fab91b commit 2a8c4e1
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 148 deletions.
46 changes: 46 additions & 0 deletions assets/css/easyadmin-theme/badges.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Badges
// ----------------------------------------------------------------------------

.badge + .badge {
margin-left: 8px;
}

.badge.badge-pill {
border-radius: 20px;
font-size: var(--font-size-xs);
line-height: 16px;
padding: 2px 8px;
}

.badge.badge-success {
background-color: var(--green-100);
color: var(--text-green-600);
}
.badge.badge-warning {
background-color: var(--yellow-100);
color: var(--text-yellow-600);
}
.badge.badge-danger {
background-color: var(--red-100);
color: var(--text-red-600);
}
.badge.badge-info {
background-color: var(--blue-100);
color: var(--text-blue-600);
}
.badge.badge-primary {
background-color: var(--indigo-100);
color: var(--text-indigo-600);
}
.badge.badge-secondary {
background-color: var(--gray-300);
color: var(--text-color);
}
.badge.badge-light {
background-color: var(--gray-50);
color: var(--text-color);
}
.badge.badge-dark {
background-color: var(--gray-900);
color: var(--gray-50);
}
1 change: 1 addition & 0 deletions assets/css/easyadmin-theme/theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
@import "./easyadmin-theme/images.scss";
@import "./easyadmin-theme/forms.scss";
@import "./easyadmin-theme/buttons.scss";
@import "./easyadmin-theme/badges.scss";
@import "./easyadmin-theme/switches.scss";
@import "./easyadmin-theme/select2.scss";
@import "./easyadmin-theme/errors.scss";
Expand Down
13 changes: 13 additions & 0 deletions assets/css/easyadmin-theme/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
--font-size-base: 0.875rem; /* 14px */
--font-size-lg: 1rem; /* 16px */
--font-size-sm: 0.8125rem; /* 13px */
--font-size-xs: 0.75rem; /* 12px */

--body-bg: hsl(220, 20%, 91%);
--text-color: hsl(225, 15%, 35%);
Expand Down Expand Up @@ -45,6 +46,18 @@
--gray-900: hsl(213, 12%, 15%);
--black: hsl(0, 0%, 0%);

--blue-100: hsl(189, 82%, 86%);
--green-100: hsl(117, 66%, 86%);
--indigo-100: hsl(208, 100%, 91%);
--red-100: hsl(9, 89%, 92%);
--yellow-100: hsl(42, 82%, 84%);

--text-blue-600: hsl(206, 91%, 30%);
--text-green-600: hsl(159, 75%, 21%);
--text-indigo-600: hsl(231, 48%, 45%);
--text-red-600: hsl(338, 71%, 37%);
--text-yellow-600: hsl(20, 94%, 30%);

--color-primary: hsl(230, 55%, 60%);
--color-success: hsl(157, 69%, 38%);
--color-info: hsl(201, 94%, 37%);
Expand Down
2 changes: 2 additions & 0 deletions assets/css/easyadmin-theme/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ $alert-link-font-weight: 500;
$badge-font-size: var(--font-size-sm);
$badge-font-weight: 500;
$badge-border-radius: var(--border-radius);
$badge-pill-padding-x: 8px;
$badge-pill-border-radius: 20px;

$dropdown-link-hover-color: var(--gray-900);
$navbar-inverse-color: var(--gray-400);
Expand Down
36 changes: 34 additions & 2 deletions src/Field/ChoiceField.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@ final class ChoiceField implements FieldInterface
public const OPTION_ALLOW_MULTIPLE_CHOICES = 'allowMultipleChoices';
public const OPTION_AUTOCOMPLETE = 'autocomplete';
public const OPTION_CHOICES = 'choices';
public const OPTION_RENDER_AS_BADGES = 'renderAsBadges';
public const OPTION_RENDER_EXPANDED = 'renderExpanded';

public const VALID_BADGE_TYPES = ['success', 'warning', 'danger', 'info', 'primary', 'secondary', 'light', 'dark'];

public static function new(string $propertyName, ?string $label = null): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setTemplateName('crud/field/select')
->setTemplateName('crud/field/choice')
->setFormType(ChoiceType::class)
->addCssClass('field-select')
->setCustomOption(self::OPTION_ALLOW_MULTIPLE_CHOICES, false)
->setCustomOption(self::OPTION_CHOICES, null)
->setCustomOption(self::OPTION_RENDER_AS_BADGES, null)
->setCustomOption(self::OPTION_RENDER_EXPANDED, false);
}

Expand Down Expand Up @@ -55,6 +58,35 @@ public function setChoices(array $keyValueChoices): self
return $this;
}

/**
* Possible values of $badgeSelector:
* * true: all values are displayed as 'secondary' badges
* * false: no badges are displayed; values are displayed as regular text
* * array: [$fieldValue => $badgeType, ...] (e.g. ['foo' => 'primary', 7 => 'warning', 'cancelled' => 'danger'])
* * callable: function(FieldDto $field): string { return '...' }
* (e.g. function(FieldDto $field) { return $field->getValue() < 10 ? 'warning' : 'primary'; })
*
* Possible badge types: 'success', 'warning', 'danger', 'info', 'primary', 'secondary', 'light', 'dark'
*/
public function renderAsBadges($badgeSelector = true): self
{
if (!is_bool($badgeSelector) && !is_array($badgeSelector) && !is_callable($badgeSelector)) {
throw new \InvalidArgumentException(sprintf('The argument of the "%s" method must be a boolean, an array or a closure ("%s" given).', __METHOD__, \gettype($badgeSelector)));
}

if (is_array($badgeSelector)) {
foreach ($badgeSelector as $fieldValue => $badgeType) {
if (!in_array($badgeType, self::VALID_BADGE_TYPES, true)) {
throw new \InvalidArgumentException(sprintf('The values of the array passed to the "%s" method must be one of the following valid badge types: "%s" ("%s" given).', __METHOD__, implode(', ', self::VALID_BADGE_TYPES), $badgeType));
}
}
}

$this->setCustomOption(self::OPTION_RENDER_AS_BADGES, $badgeSelector);

return $this;
}

public function renderExpanded(bool $expanded = true): self
{
$this->setCustomOption(self::OPTION_RENDER_EXPANDED, $expanded);
Expand Down
60 changes: 45 additions & 15 deletions src/Field/Configurator/ChoiceConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

namespace EasyCorp\Bundle\EasyAdminBundle\Field\Configurator;

use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use Symfony\Contracts\Translation\TranslatorInterface;
use function Symfony\Component\String\u;

/**
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
Expand All @@ -33,6 +35,13 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
throw new \InvalidArgumentException(sprintf('The "%s" select field must define its possible choices using the setChoices() method.', $field->getProperty()));
}

$field->setFormTypeOptionIfNotSet('multiple', $field->getCustomOption(ChoiceField::OPTION_ALLOW_MULTIPLE_CHOICES));
$field->setFormTypeOptionIfNotSet('expanded', $field->getCustomOption(ChoiceField::OPTION_RENDER_EXPANDED));

if (true === $field->getCustomOption(ChoiceField::OPTION_AUTOCOMPLETE)) {
$field->setFormTypeOptionIfNotSet('attr.data-widget', 'select2');
}

$translatedChoices = [];
$translationParameters = $context->getI18n()->getTranslationParameters();
foreach ($choices as $choiceLabel => $choiceValue) {
Expand All @@ -41,25 +50,46 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
}
$field->setFormTypeOptionIfNotSet('choices', $translatedChoices);

if (null !== $value = $field->getValue()) {
// needed to be compatible with fields that allow selecting multiple values
$selectedChoices = [];
$flippedChoices = array_flip($choices);
// $value is a scalar for single selections and an array for multiple selections
foreach (array_values((array) $value) as $selectedValue) {
if (null !== $selectedChoice = $flippedChoices[$selectedValue] ?? null) {
$selectedChoices[] = $this->translator->trans($selectedChoice, $translationParameters);
}
}
$fieldValue = $field->getValue();
$isIndexOrDetail = in_array($context->getCrud()->getCurrentPage(), [Crud::PAGE_INDEX, Crud::PAGE_DETAIL], true);
if (null === $fieldValue || !$isIndexOrDetail) {
return;
}

$badgeSelector = $field->getCustomOption(ChoiceField::OPTION_RENDER_AS_BADGES);
$isRenderedAsBadge = null !== $badgeSelector && false !== $badgeSelector;

$field->setFormattedValue(implode(', ', $selectedChoices));
$selectedChoices = [];
$flippedChoices = array_flip($choices);
// $value is a scalar for single selections and an array for multiple selections
foreach (array_values((array) $fieldValue) as $selectedValue) {
if (null !== $selectedChoice = $flippedChoices[$selectedValue] ?? null) {
$choiceValue = $this->translator->trans($selectedChoice, $translationParameters);
$selectedChoices[] = $isRenderedAsBadge
? sprintf('<span class="%s">%s</span>', $this->getBadgeCssClass($badgeSelector, $selectedValue, $field), $choiceValue)
: $choiceValue;
}
}
$field->setFormattedValue(implode($isRenderedAsBadge ? '' : ', ', $selectedChoices));
}

$field->setFormTypeOptionIfNotSet('multiple', $field->getCustomOption(ChoiceField::OPTION_ALLOW_MULTIPLE_CHOICES));
$field->setFormTypeOptionIfNotSet('expanded', $field->getCustomOption(ChoiceField::OPTION_RENDER_EXPANDED));
private function getBadgeCssClass($badgeSelector, $value, FieldDto $field): string
{
$commonBadgeCssClass = 'badge badge-pill';

if (true === $field->getCustomOption(ChoiceField::OPTION_AUTOCOMPLETE)) {
$field->setFormTypeOptionIfNotSet('attr.data-widget', 'select2');
if (true === $badgeSelector) {
$badgeType = 'badge-secondary';
} elseif (is_array($badgeSelector)) {
$badgeType = $badgeSelector[$value] ?? 'badge-secondary';
} elseif (is_callable($badgeSelector)) {
$badgeType = $badgeSelector($field);
if (!in_array($badgeType, ChoiceField::VALID_BADGE_TYPES, true)) {
throw new \RuntimeException(sprintf('The value returned by the callable passed to the "renderAsBadges()" method must be one of the following valid badge types: "%s" ("%s" given).', implode(', ', ChoiceField::VALID_BADGE_TYPES), $badgeType));
}
}

$badgeTypeCssClass = empty($badgeType) ? '' : u($badgeType)->ensureStart('badge-');

return $commonBadgeCssClass.' '.$badgeTypeCssClass;
}
}
2 changes: 1 addition & 1 deletion src/Registry/TemplateRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ final class TemplateRegistry
'crud/field/avatar' => '@EasyAdmin/crud/field/avatar.html.twig',
'crud/field/bigint' => '@EasyAdmin/crud/field/bigint.html.twig',
'crud/field/boolean' => '@EasyAdmin/crud/field/boolean.html.twig',
'crud/field/choice' => '@EasyAdmin/crud/field/choice.html.twig',
'crud/field/code_editor' => '@EasyAdmin/crud/field/code_editor.html.twig',
'crud/field/collection' => '@EasyAdmin/crud/field/collection.html.twig',
'crud/field/color' => '@EasyAdmin/crud/field/color.html.twig',
Expand All @@ -44,7 +45,6 @@ final class TemplateRegistry
'crud/field/money' => '@EasyAdmin/crud/field/money.html.twig',
'crud/field/number' => '@EasyAdmin/crud/field/number.html.twig',
'crud/field/percent' => '@EasyAdmin/crud/field/percent.html.twig',
'crud/field/select' => '@EasyAdmin/crud/field/select.html.twig',
'crud/field/raw' => '@EasyAdmin/crud/field/raw.html.twig',
'crud/field/smallint' => '@EasyAdmin/crud/field/smallint.html.twig',
'crud/field/telephone' => '@EasyAdmin/crud/field/telephone.html.twig',
Expand Down
4 changes: 2 additions & 2 deletions src/Resources/public/app.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Resources/public/app.rtl.css

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{{ field.formattedValue }}
{{ field.formattedValue|raw }}
2 changes: 1 addition & 1 deletion src/Resources/views/crud/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
{% set nextSortDirection = isSortingField ? (sort_order == 'DESC' ? 'ASC' : 'DESC') : 'DESC' %}
{% set column_icon = isSortingField ? (nextSortDirection == 'DESC' ? 'fa-arrow-up' : 'fa-arrow-down') : 'fa-sort' %}

<th class="{{ isSortingField ? 'sorted' }} {{ field.isVirtual ? 'field-virtual' }} text-{{ field.textAlign }} {{ field.cssClass }}" dir="{{ ea.i18n.textDirection }}">
<th class="{{ isSortingField ? 'sorted' }} {{ field.isVirtual ? 'field-virtual' }} text-{{ field.textAlign }}" dir="{{ ea.i18n.textDirection }}">
{% if field.isSortable %}
<a href="{{ ea_url({ page: 1, sort: { (field.property): nextSortDirection } }).includeReferrer() }}">
{{ field.label|raw }} <i class="fa fa-fw {{ column_icon }}"></i>
Expand Down

0 comments on commit 2a8c4e1

Please sign in to comment.