diff --git a/CHANGELOG.md b/CHANGELOG.md index d099e31..af7895d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve ## 0.5.1 Under development +- fix: make `HasAttributes::attributes()` update existing attributes and add `replaceAttributes()` for full replacement. + ## 0.5.0 April 29, 2026 - fix: remove `ui-awesome/html-interop` from runtime dependencies and keep it as a development-only dependency for local tooling and tests. diff --git a/UPGRADE.md b/UPGRADE.md index 823e1c9..ecfc4f4 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,12 @@ # Upgrade Guide +## 0.5.1 + +### Attribute replacement + +- `HasAttributes::attributes()` now updates existing attributes instead of replacing the full attribute bag. +- Use `replaceAttributes()` when you need to discard previous attributes before applying new ones. + ## 0.5.0 ### Runtime dependencies diff --git a/src/HasAttributes.php b/src/HasAttributes.php index 9a8dfb0..d81bc74 100644 --- a/src/HasAttributes.php +++ b/src/HasAttributes.php @@ -18,9 +18,7 @@ trait HasAttributes { /** - * HTML attributes array used by the implementing class. - * - * @var mixed[] + * @var mixed[] HTML attributes array used by the implementing class. */ protected array $attributes = []; @@ -38,11 +36,11 @@ public function addAttribute(string|UnitEnum $key, mixed $value): static } /** - * Replaces all HTML attributes for the element. + * Adds or updates multiple HTML attributes for the element. * * @param mixed[] $values Associative array of attribute keys and values. * - * @return static New instance with the replaced attributes value. + * @return static New instance with the updated attributes value. */ public function attributes(array $values): static { @@ -88,6 +86,22 @@ public function removeAttribute(string|UnitEnum $key): static return $new; } + /** + * Replaces all HTML attributes for the element. + * + * @param mixed[] $values Associative array of attribute keys and values. + * + * @return static New instance with the replaced attributes value. + */ + public function replaceAttributes(array $values): static + { + $new = clone $this; + + AttributeBag::replace($new->attributes, $values); + + return $new; + } + /** * Sets a single HTML attribute for internal fluent setters. * @@ -107,18 +121,18 @@ private function setAttribute(string|UnitEnum $key, mixed $value, string $prefix } /** - * Replaces all HTML attributes for internal fluent setters. + * Adds or updates multiple HTML attributes for internal fluent setters. * * @param mixed[] $values Associative array of attribute keys and values. * @param string $prefix Prefix to ensure (for example, `aria-`, `data-`, `on`). * - * @return static New instance with the replaced attributes value. + * @return static New instance with the updated attributes value. */ private function setAttributes(array $values, string $prefix = ''): static { $new = clone $this; - AttributeBag::replace($new->attributes, $values, $prefix); + AttributeBag::setMany($new->attributes, $values, $prefix); return $new; } diff --git a/tests/HasAttributesTest.php b/tests/HasAttributesTest.php index 48f9457..9243370 100644 --- a/tests/HasAttributesTest.php +++ b/tests/HasAttributesTest.php @@ -19,8 +19,9 @@ * Test coverage. * - Adds single attributes through the public API. * - Ensures fluent setters return new instances (immutability). - * - Replaces attributes through the public API. + * - Merges attributes through the public API. * - Removes attributes and returns expected values. + * - Replaces attributes through the explicit replacement API. * - Sets prefixed attributes through protected internals exposed by test stubs. * - Throws InvalidArgumentException for empty or unsupported attribute keys. * @@ -99,6 +100,26 @@ public function __toString(): string ); } + public function testAttributesMergeExistingValues(): void + { + $instance = new class { + use HasAttributes; + }; + + $instance = $instance->attributes(['id' => 'my-id']); + $instance = $instance->attributes(['class' => 'my-class']); + $instance = $instance->attributes(['class' => 'new-class']); + + self::assertSame( + [ + 'id' => 'my-id', + 'class' => 'new-class', + ], + $instance->getAttributes(), + 'Should merge new attributes and update existing attributes.', + ); + } + public function testAttributesPrefixSupportThroughProtectedInternals(): void { $instance = new class { @@ -126,9 +147,12 @@ public function testAttributesPrefixSupportThroughProtectedInternals(): void "Should set and read prefixed attributes via internal 'setAttributes()'.", ); self::assertSame( - ['aria-describedby' => 'field-id'], + [ + 'aria-label' => 'label', + 'aria-describedby' => 'field-id', + ], $instance->getAttributes(), - 'Should replace the attribute bag when setting prefixed attributes in bulk.', + 'Should merge prefixed attributes in bulk.', ); $instance = $instance->removeAttribute('aria-describedby'); @@ -139,22 +163,6 @@ public function testAttributesPrefixSupportThroughProtectedInternals(): void ); } - public function testAttributesReplaceExistingValues(): void - { - $instance = new class { - use HasAttributes; - }; - - $instance = $instance->attributes(['id' => 'my-id']); - $instance = $instance->attributes(['class' => 'my-class']); - - self::assertSame( - ['class' => 'my-class'], - $instance->getAttributes(), - 'Should replace existing attributes instead of merging them.', - ); - } - public function testAttributesValue(): void { $instance = new class { @@ -239,6 +247,27 @@ public function testRemoveAttributeValue(): void ); } + public function testReplaceAttributesReplacesExistingValues(): void + { + $instance = new class { + use HasAttributes; + }; + + $original = $instance->attributes(['id' => 'my-id']); + $replacement = $original->replaceAttributes(['class' => 'my-class']); + + self::assertSame( + ['id' => 'my-id'], + $original->getAttributes(), + 'Should keep the original attributes unchanged after explicit replacement.', + ); + self::assertSame( + ['class' => 'my-class'], + $replacement->getAttributes(), + 'Should replace existing attributes through the explicit replacement API.', + ); + } + public function testReturnNewInstanceWhenSettingAttributes(): void { $instance = new class { @@ -261,6 +290,11 @@ public function testReturnNewInstanceWhenSettingAttributes(): void $instance->removeAttribute('tests'), 'Should return a new instance when removing an attribute, ensuring immutability.', ); + self::assertNotSame( + $instance, + $instance->replaceAttributes([]), + 'Should return a new instance when replacing attributes, ensuring immutability.', + ); self::assertNotSame( $instance, $instance->setAttributeForTest('tests', ''),