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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 22 additions & 8 deletions src/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand All @@ -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
{
Expand Down Expand Up @@ -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.
*
Expand All @@ -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;
}
Expand Down
72 changes: 53 additions & 19 deletions tests/HasAttributesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.',
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public function testAttributesPrefixSupportThroughProtectedInternals(): void
{
$instance = new class {
Expand Down Expand Up @@ -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');
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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', ''),
Expand Down
Loading