Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance select element #88

Merged
merged 13 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/Common/MultipleAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace ipl\Html\Common;

use ipl\Html\Attributes;
use ipl\Html\Contract\FormElement;

/**
* Trait for form elements that can have the `multiple` attribute
*
* **Example usage:**
*
* ```
* namespace ipl\Html\FormElement;
*
* use ipl\Html\Common\MultipleAttribute;
*
* class SelectElement extends BaseFormElement
* {
* protected function registerAttributeCallbacks(Attributes $attributes)
* {
* // ...
* $this->registerMultipleAttributeCallback($attributes);
* }
* }
* ```
*/
trait MultipleAttribute
{
/** @var bool Whether the attribute `multiple` is set to `true` */
protected $multiple = false;

/**
* Get whether the attribute `multiple` is set to `true`
*
* @return bool
*/
public function isMultiple(): bool
{
return $this->multiple;
}

/**
* Set the `multiple` attribute
*
* @param bool $multiple
*
* @return $this
*/
public function setMultiple(bool $multiple): self
{
$this->multiple = $multiple;

return $this;
}

/**
* Register the callback for `multiple` Attribute
*
* @param Attributes $attributes
*/
protected function registerMultipleAttributeCallback(Attributes $attributes): void
{
$attributes->registerAttributeCallback(
'multiple',
[$this, 'isMultiple'],
[$this, 'setMultiple']
);
}
}
253 changes: 174 additions & 79 deletions src/FormElement/SelectElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,114 +2,128 @@

namespace ipl\Html\FormElement;

use InvalidArgumentException;
use ipl\Html\Attributes;
use ipl\Html\Common\MultipleAttribute;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Validator\DeferredInArrayValidator;
use ipl\Validator\ValidatorChain;
use UnexpectedValueException;

class SelectElement extends BaseFormElement
{
use MultipleAttribute;

protected $tag = 'select';

/** @var SelectOption[] */
protected $options = [];

/** @var SelectOption[]|HtmlElement[] */
protected $optionContent = [];

public function __construct($name, $attributes = null)
{
$this->getAttributes()->registerAttributeCallback(
'options',
null,
[$this, 'setOptions']
);
// ZF1 compatibility:
$this->getAttributes()->registerAttributeCallback(
'multiOptions',
null,
[$this, 'setOptions']
);
/** @var array Disabled select options */
protected $disabledOptions = [];
sukhwinder33445 marked this conversation as resolved.
Show resolved Hide resolved

parent::__construct($name, $attributes);
}
/** @var array|string|null */
protected $value;

public function hasOption($value)
/**
* Get the option with specified value
*
* @param string|int|null $value
*
* @return ?SelectOption
*/
public function getOption($value): ?SelectOption
{
return isset($this->options[$value]);
return $this->options[$value] ?? null;
}

public function validate()
/**
* Set the options from specified values
*
* @param array $options
*
* @return $this
*/
public function setOptions(array $options): self
{
$value = $this->getValue();
if (
$value !== null && (
! ($option = $this->getOption($value))
|| $option->getAttributes()->has('disabled')
)
) {
$this->addMessage("'$value' is not allowed here");
} elseif ($this->isRequired() && $value === null) {
$this->addMessage('This field is required');
} else {
return parent::validate();
$this->options = [];
$this->optionContent = [];
foreach ($options as $value => $label) {
$this->optionContent[$value] = $this->makeOption($value, $label);
}

return false;
return $this;
}

public function deselect()
/**
* Set the specified options as disable
*
* @param array $disabledOptions
*
* @return $this
*/
public function setDisabledOptions(array $disabledOptions): self
sukhwinder33445 marked this conversation as resolved.
Show resolved Hide resolved
{
$this->setValue(null);
if (! empty($this->options)) {
foreach ($this->options as $option) {
$optionValue = $option->getValue();

return $this;
}
$option->setAttribute(
'disabled',
in_array($optionValue, $disabledOptions, ! is_int($optionValue))
|| ($optionValue === null && in_array('', $disabledOptions, true))
);
}

public function disableOption($value)
sukhwinder33445 marked this conversation as resolved.
Show resolved Hide resolved
{
if ($option = $this->getOption($value)) {
$option->getAttributes()->add('disabled', true);
}
if ($this->getValue() == $value) {
$this->addMessage("'$value' is not allowed here");
$this->disabledOptions = [];
} else {
$this->disabledOptions = $disabledOptions;
}

return $this;
}

public function disableOptions($values)
/**
* Get the value of the element
*
* Returns `array` when the attribute `multiple` is set to `true`, `string` or `null` otherwise
*
* @return array|string|null
*/
public function getValue()
{
foreach ($values as $value) {
$this->disableOption($value);
if ($this->isMultiple()) {
return parent::getValue() ?? [];
}

return $this;
return parent::getValue();
}

/**
* @param $value
* @return SelectOption|null
*/
public function getOption($value)
public function getValueAttribute()
{
if ($this->hasOption($value)) {
return $this->options[$value];
} else {
return null;
}
// select elements don't have a value attribute
return null;
}

/**
* @param array $options
* @return $this
*/
public function setOptions(array $options)
public function getNameAttribute()
{
$this->options = [];
$this->optionContent = [];
foreach ($options as $value => $label) {
$this->optionContent[$value] = $this->makeOption($value, $label);
}
$name = $this->getName();

return $this;
return $this->isMultiple() ? ($name . '[]') : $name;
}

/**
* Make the selectOption for the specified value and the label
*
* @param string|int|null $value Value of the option
* @param string|array $label Label of the option
*
* @return SelectOption|HtmlElement
*/
protected function makeOption($value, $label)
{
if (is_array($label)) {
Expand All @@ -119,25 +133,106 @@ protected function makeOption($value, $label)
}

return $grp;
} else {
$option = new SelectOption($value, $label);
$option->getAttributes()->registerAttributeCallback('selected', function () use ($option) {
$optionValue = $option->getValue();
}

$option = (new SelectOption($value, $label))
->setAttribute('disabled', in_array($value, $this->disabledOptions, ! is_int($value)));

$option->getAttributes()->registerAttributeCallback('selected', function () use ($option) {
return $this->isSelectedOption($option->getValue());
});

return is_int($optionValue)
// The loose comparison is required because PHP casts
// numeric strings to integers if used as array keys
? $this->getValue() == $optionValue
: $this->getValue() === $optionValue;
});
$this->options[$value] = $option;
$this->options[$value] = $option;

return $this->options[$value];
return $this->options[$value];
}

/**
* Get whether the given option is selected
*
* @param int|string|null $optionValue
*
* @return bool
*/
protected function isSelectedOption($optionValue): bool
{
$value = $this->getValue();

if ($optionValue === '') {
$optionValue = null;
}

if ($this->isMultiple()) {
if (! is_array($value)) {
throw new UnexpectedValueException(
'Value must be an array when the `multiple` attribute is set to `true`'
);
}

return in_array($optionValue, $this->getValue(), ! is_int($optionValue))
|| ($optionValue === null && in_array('', $this->getValue(), true));
}

if (is_array($value)) {
throw new UnexpectedValueException(
'Value cannot be an array without setting the `multiple` attribute to `true`'
);
}

return is_int($optionValue)
// The loose comparison is required because PHP casts
// numeric strings to integers if used as array keys
? $value == $optionValue
: $value === $optionValue;
}

protected function addDefaultValidators(ValidatorChain $chain): void
{
$chain->add(
new DeferredInArrayValidator(function (): array {
$possibleValues = [];

foreach ($this->options as $option) {
if ($option->getAttributes()->get('disabled')->getValue()) {
continue;
}

$possibleValues[] = $option->getValue();
}

return $possibleValues;
})
);
}

protected function assemble()
{
$this->addHtml(...array_values($this->optionContent));
}

protected function registerAttributeCallbacks(Attributes $attributes)
{
parent::registerAttributeCallbacks($attributes);

$attributes->registerAttributeCallback(
'options',
null,
[$this, 'setOptions']
);

$attributes->registerAttributeCallback(
'disabledOptions',
null,
[$this, 'setDisabledOptions']
);

// ZF1 compatibility:
$this->getAttributes()->registerAttributeCallback(
'multiOptions',
null,
[$this, 'setOptions']
);

$this->registerMultipleAttributeCallback($attributes);
}
}
2 changes: 1 addition & 1 deletion src/FormElement/SelectOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class SelectOption extends BaseHtmlElement
*/
public function __construct($value, string $label)
{
$this->value = $value;
$this->value = $value === '' ? null : $value;
$this->label = $label;

$this->getAttributes()->registerAttributeCallback('value', [$this, 'getValue']);
Expand Down
Loading