Skip to content
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
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ field name exactly. For PHP array submissions, supply a name such as
`categories[]` yourself. Checkbox groups retain the existing convention of
appending `[]` unless the name already ends with it.

`wrapperProps()` targets the dropdown's `<div>` or the group's `<fieldset>`.
`containerProps()` targets the dropdown's `<div>` or the group's `<fieldset>`.
For native group-wide disabling, use
`->checkbox()->wrapperProps('disabled disabled')` (or `radio()`); the browser
`->checkbox()->containerProps('disabled disabled')` (or `radio()`); the browser
disables descendants through fieldset semantics, without rewriting the inputs.
Disabling a dropdown instead requires `inputProps('disabled disabled')`.
`labelProps()` targets the dropdown's label or the group's legend.
Expand All @@ -175,6 +175,14 @@ argument and explicit mode methods instead. A warning forces
warning, caller-supplied guidance and invalid-state properties remain unchanged.
Other repeated properties use the element builder's normal last-value behavior.

### Migrating from wrapperProps

In 5.0, use `containerProps()` instead of `wrapperProps()` on all form
components. This is a rename, not a change in property handling or target
elements. `wrapperProps()` remains a forwarding alias throughout 5.x and is
marked with PHP's native `#[Deprecated]` attribute (supported since PHP 8.4).
Calling it emits `E_USER_DEPRECATED`; removal is deferred until at least 6.0.

## Performance

The `/comparisons` folder can be used to run performance tests locally to compare the same output using:
Expand Down
14 changes: 10 additions & 4 deletions src/Forms/EmailInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class EmailInput implements Stringable
/**
* @var list<string>
*/
private array $wrapperProperties = [];
private array $containerProperties = [];

/**
* @var list<string>
Expand Down Expand Up @@ -59,12 +59,18 @@ public function id(string $id): self
/**
* Sets properties on this component's outermost div, not the input.
*/
public function wrapperProps(string ...$properties): self
public function containerProps(string ...$properties): self
{
$this->wrapperProperties = array_values($properties);
$this->containerProperties = array_values($properties);
return $this;
}

#[\Deprecated(message: 'Use containerProps() instead.', since: '5.0')]
public function wrapperProps(string ...$properties): self
{
return $this->containerProps(...$properties);
}

public function labelProps(string ...$properties): self
{
$this->labelProperties = array_values($properties);
Expand Down Expand Up @@ -135,6 +141,6 @@ public function __toString(): string
),
$input,
...[...$this->content, $warning]
)->props(...$this->wrapperProperties);
)->props(...$this->containerProperties);
}
}
14 changes: 10 additions & 4 deletions src/Forms/Fieldset.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Fieldset implements Stringable
/**
* @var string[]
*/
private array $wrapperProperties = [];
private array $containerProperties = [];

/**
* @var string[]
Expand Down Expand Up @@ -61,12 +61,18 @@ public function elements(string|Stringable ...$elements): self
/**
* Sets properties on this component's outermost element: the fieldset.
*/
public function wrapperProps(string ...$properties): self
public function containerProps(string ...$properties): self
{
$this->wrapperProperties = $properties;
$this->containerProperties = $properties;
return $this;
}

#[\Deprecated(message: 'Use containerProps() instead.', since: '5.0')]
public function wrapperProps(string ...$properties): self
{
return $this->containerProps(...$properties);
}

public function labelProps(string ...$properties): self
{
$this->labelProperties = $properties;
Expand Down Expand Up @@ -96,6 +102,6 @@ public function __toString(): string
return (string) Element::fieldset(
Element::legend($this->label)->props(...$this->labelProperties),
...$elements
)->props(...$this->wrapperProperties);
)->props(...$this->containerProperties);
}
}
14 changes: 10 additions & 4 deletions src/Forms/Password.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Password implements Stringable
/**
* @var list<string>
*/
private array $wrapperProperties = [];
private array $containerProperties = [];

/**
* @var list<string>
Expand Down Expand Up @@ -61,12 +61,18 @@ public function id(string $id): self
/**
* Sets properties on this component's outermost div, not the input.
*/
public function wrapperProps(string ...$properties): self
public function containerProps(string ...$properties): self
{
$this->wrapperProperties = array_values($properties);
$this->containerProperties = array_values($properties);
return $this;
}

#[\Deprecated(message: 'Use containerProps() instead.', since: '5.0')]
public function wrapperProps(string ...$properties): self
{
return $this->containerProps(...$properties);
}

public function labelProps(string ...$properties): self
{
$this->labelProperties = array_values($properties);
Expand Down Expand Up @@ -153,6 +159,6 @@ public function __toString(): string
]),
$input,
...[...$this->content, $warning]
)->props(...$this->wrapperProperties);
)->props(...$this->containerProperties);
}
}
16 changes: 11 additions & 5 deletions src/Forms/Select.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Select implements Stringable
/**
* @var list<string>
*/
private array $wrapperProperties = [];
private array $containerProperties = [];

/**
* @var list<string>
Expand Down Expand Up @@ -76,12 +76,18 @@ public function id(string $id): self
* Sets properties on the outermost div in dropdown/multiple modes,
* or the fieldset in radio/checkbox modes, not on member controls.
*/
public function wrapperProps(string ...$properties): self
public function containerProps(string ...$properties): self
{
$this->wrapperProperties = array_values($properties);
$this->containerProperties = array_values($properties);
return $this;
}

#[\Deprecated(message: 'Use containerProps() instead.', since: '5.0')]
public function wrapperProps(string ...$properties): self
{
return $this->containerProps(...$properties);
}

public function labelProps(string ...$properties): self
{
$this->labelProperties = array_values($properties);
Expand Down Expand Up @@ -239,7 +245,7 @@ private function selectDropdown(
]),
$select,
...[...$this->content, $warning]
)->props(...$this->wrapperProperties);
)->props(...$this->containerProperties);
}

/**
Expand Down Expand Up @@ -288,6 +294,6 @@ private function selectGroup(
return Element::fieldset(
Element::legend($this->label)->props(...$this->labelProperties),
...[...$members, ...$this->content, $warning]
)->props(...$this->wrapperProperties);
)->props(...$this->containerProperties);
}
}
14 changes: 10 additions & 4 deletions src/Forms/TextArea.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TextArea implements Stringable
/**
* @var list<string>
*/
private array $wrapperProperties = [];
private array $containerProperties = [];

/**
* @var list<string>
Expand Down Expand Up @@ -62,12 +62,18 @@ public function id(string $id): self
/**
* Sets properties on this component's outermost div, not the textarea.
*/
public function wrapperProps(string ...$properties): self
public function containerProps(string ...$properties): self
{
$this->wrapperProperties = array_values($properties);
$this->containerProperties = array_values($properties);
return $this;
}

#[\Deprecated(message: 'Use containerProps() instead.', since: '5.0')]
public function wrapperProps(string ...$properties): self
{
return $this->containerProps(...$properties);
}

public function labelProps(string ...$properties): self
{
$this->labelProperties = array_values($properties);
Expand Down Expand Up @@ -136,6 +142,6 @@ public function __toString(): string
]),
$textarea,
...[...$this->content, $warning]
)->props(...$this->wrapperProperties);
)->props(...$this->containerProperties);
}
}
14 changes: 10 additions & 4 deletions src/Forms/TextInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TextInput implements Stringable
/**
* @var list<string>
*/
private array $wrapperProperties = [];
private array $containerProperties = [];

/**
* @var list<string>
Expand Down Expand Up @@ -59,12 +59,18 @@ public function id(string $id): self
/**
* Sets properties on this component's outermost div, not the input.
*/
public function wrapperProps(string ...$properties): self
public function containerProps(string ...$properties): self
{
$this->wrapperProperties = array_values($properties);
$this->containerProperties = array_values($properties);
return $this;
}

#[\Deprecated(message: 'Use containerProps() instead.', since: '5.0')]
public function wrapperProps(string ...$properties): self
{
return $this->containerProps(...$properties);
}

public function labelProps(string ...$properties): self
{
$this->labelProperties = array_values($properties);
Expand Down Expand Up @@ -133,6 +139,6 @@ public function __toString(): string
]),
$input,
...[...$this->content, $warning]
)->props(...$this->wrapperProperties);
)->props(...$this->containerProperties);
}
}
68 changes: 68 additions & 0 deletions tests/Forms/ContainerPropsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);

namespace Eightfold\HtmlBuilder\Tests;

use Deprecated;
use ReflectionMethod;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

use Eightfold\HtmlBuilder\Forms\EmailInput;
use Eightfold\HtmlBuilder\Forms\Fieldset;
use Eightfold\HtmlBuilder\Forms\Password;
use Eightfold\HtmlBuilder\Forms\Select;
use Eightfold\HtmlBuilder\Forms\TextArea;
use Eightfold\HtmlBuilder\Forms\TextInput;

final class ContainerPropsTest extends TestCase
{
/** @return iterable<string, array{string}> */
public static function components(): iterable
{
foreach (['text', 'email', 'password', 'textarea', 'fieldset', 'dropdown', 'multiple', 'radio', 'checkbox'] as $mode) {
yield $mode => [$mode];
}
}

#[Test]
#[DataProvider('components')]
public function deprecated_alias_preserves_fluent_return_and_rendered_output(string $mode): void
{
$component = match ($mode) {
'text' => TextInput::create('Field', 'field'),
'email' => EmailInput::create('Field', 'field'),
'password' => Password::create('Field', 'field'),
'textarea' => TextArea::create('Field', 'field'),
'fieldset' => Fieldset::create('Field', 'field'),
default => Select::create('Field', 'field', ['a' => 'A'])->$mode()
};
$properties = ['class container', 'data-example preserved'];
$modern = clone $component;
parent::assertSame($modern, $modern->containerProps(...$properties));
$attribute = new ReflectionMethod($component, 'wrapperProps')
->getAttributes(Deprecated::class)[0]->newInstance();
parent::assertSame('Use containerProps() instead.', $attribute->message);
parent::assertSame('5.0', $attribute->since);

$deprecations = [];
set_error_handler(static function (int $severity, string $message) use (&$deprecations): bool {
if ($severity !== E_USER_DEPRECATED) {
return false;
}
$deprecations[] = $message;
return true;
});
try {
$result = $component->wrapperProps(...$properties);
} finally {
restore_error_handler();
}

parent::assertSame($component, $result, 'The deprecated alias must return containerProps() results.');
parent::assertSame((string) $modern, (string) $component, 'Renaming must not change markup.');
parent::assertCount(1, $deprecations, 'The native attribute must emit one deprecation per call.');
parent::assertStringContainsString('Use containerProps() instead.', $deprecations[0]);
}
}
2 changes: 1 addition & 1 deletion tests/Forms/EmailInputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function can_set_custom_id(): void // phpcs:ignore
warningMessage: 'Invalid value'
)->id('email-input-id')->content(
Element::p('Helpful text')->props('id email-input-id-help')
)->wrapperProps('is form-control');
)->containerProps('is form-control');

parent::assertSame($expected, $result);
}
Expand Down
6 changes: 3 additions & 3 deletions tests/Forms/SelectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public function native_attributes_target_controls_and_preserve_generated_identit
'aria-describedby category-help',
...($isGroup ? [] : ['size 4'])
)->content(Element::p('Help')->props('id category-help'))
->wrapperProps('class wrapper')->labelProps('class caption')
->containerProps('class wrapper')->labelProps('class caption')
);
$controls = $xpath->query('//*[local-name()="input" or local-name()="select"]');
parent::assertNotFalse($controls);
Expand Down Expand Up @@ -193,7 +193,7 @@ public function group_disabling_and_required_attributes_follow_native_semantics(
{
$control = Select::create('Categories', 'categories', ['a' => 'A', 'b' => 'B']);
$xpath = self::controlXpath(
$control->$mode()->wrapperProps('disabled disabled')->inputProps('required required')
$control->$mode()->containerProps('disabled disabled')->inputProps('required required')
);
$isGroup = in_array($mode, ['radio', 'checkbox'], true);
parent::assertSame($isGroup ? 1 : 0, $xpath->query('//*[local-name()="fieldset" and @disabled]')->length);
Expand Down Expand Up @@ -437,7 +437,7 @@ public function can_add_properties_to_wrapper(): void // phpcs: ignore
[
'value' => 'display'
]
)->wrapperProps('id some-id', 'class some-token');
)->containerProps('id some-id', 'class some-token');

parent::assertSame($expected, $result);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Forms/TextAreaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public function native_properties_and_custom_identity_target_the_textarea(): voi
'required required',
'readonly readonly',
'disabled disabled'
)->wrapperProps('class notes-wrapper')->labelProps('class notes-label');
)->containerProps('class notes-wrapper')->labelProps('class notes-label');
$xpath = self::xpath($html);
$textarea = $xpath->query('//*[local-name()="textarea"]')->item(0);
parent::assertInstanceOf(DomElement::class, $textarea);
Expand Down
2 changes: 1 addition & 1 deletion tests/Forms/TextInputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function can_set_custom_id(): void // phpcs:ignore
warningMessage: 'Invalid value'
)->id('text-input-id')->content(
Element::p('Helpful text')->props('id text-input-id-help')
)->wrapperProps('is form-control');
)->containerProps('is form-control');

parent::assertSame($expected, $result);
}
Expand Down
Loading