From c7e51eab009fea310ee8fb8e799e2ca391de609b Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Sun, 29 Mar 2026 19:57:51 +0200 Subject: [PATCH 1/2] Add fast paths for class/style string arguments in BlazeAttributeBag When merge(), class(), or style() receive a simple string argument (the most common usage), skip the general-purpose loops and handle the merge directly. The shared class-merging logic is extracted into withMergedClass() to avoid duplication between merge() and class(). Uses array union to place the merged key first, matching the attribute ordering of the general merge() path's array_merge($defaults, $attrs). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Runtime/BlazeAttributeBag.php | 43 +++++++++++++++++++ tests/ComparisonTest.php | 5 +++ .../views/components/merge-class.blade.php | 3 ++ 3 files changed, 51 insertions(+) create mode 100644 tests/fixtures/views/components/merge-class.blade.php diff --git a/src/Runtime/BlazeAttributeBag.php b/src/Runtime/BlazeAttributeBag.php index fb7c0fe..641650b 100644 --- a/src/Runtime/BlazeAttributeBag.php +++ b/src/Runtime/BlazeAttributeBag.php @@ -49,6 +49,11 @@ public static function make(array $attributes, array $boundKeys = [], array $ori /** {@inheritdoc} */ public function merge(array $attributeDefaults = [], $escape = true): static { + // Fast path: merge(['class' => 'string']) — the most common pattern. + if (count($attributeDefaults) === 1 && isset($attributeDefaults['class']) && is_string($attributeDefaults['class'])) { + return $this->withMergedClass($escape ? e($attributeDefaults['class']) : $attributeDefaults['class']); + } + if ($escape) { foreach ($attributeDefaults as $key => $value) { if ($this->shouldEscapeAttributeValue($escape, $value)) { @@ -103,6 +108,10 @@ public function merge(array $attributeDefaults = [], $escape = true): static /** {@inheritdoc} */ public function class($classList): static { + if (is_string($classList)) { + return $this->withMergedClass(e($classList)); + } + $classes = $this->toCssClasses(Arr::wrap($classList)); return $this->merge(['class' => $classes]); @@ -111,11 +120,45 @@ public function class($classList): static /** {@inheritdoc} */ public function style($styleList): static { + if (is_string($styleList)) { + $default = e(rtrim($styleList, ';').';'); + $current = $this->attributes['style'] ?? ''; + + if ($current) { + $current = rtrim((string) $current, ';').';'; + $style = $current === $default ? $default : $default.' '.$current; + } else { + $style = $default; + } + + return new static(['style' => $style] + $this->attributes); + } + $styles = $this->toCssStyles((array) $styleList); return $this->merge(['style' => $styles]); } + /** + * Return a new bag with the given class default merged into the current class attribute. + */ + protected function withMergedClass(string $default): static + { + $current = $this->attributes['class'] ?? ''; + + if (! $current || $current === $default) { + $class = $default; + } elseif (! $default) { + $class = $current ?: ''; + } else { + $class = $default.' '.$current; + } + + // Array union places class first, matching merge()'s + // array_merge($attributeDefaults, $attributes) key ordering. + return new static(['class' => $class] + $this->attributes); + } + /** * Convert class list to CSS classes string. */ diff --git a/tests/ComparisonTest.php b/tests/ComparisonTest.php index 165b09c..14f776a 100644 --- a/tests/ComparisonTest.php +++ b/tests/ComparisonTest.php @@ -95,6 +95,11 @@ BLADE )); +test('merge preserves attribute ordering', fn () => compare(<<<'BLADE' + + BLADE +)); + test('deeply nested same component with different components interleaved', fn () => compare(<<<'BLADE' diff --git a/tests/fixtures/views/components/merge-class.blade.php b/tests/fixtures/views/components/merge-class.blade.php new file mode 100644 index 0000000..dba7365 --- /dev/null +++ b/tests/fixtures/views/components/merge-class.blade.php @@ -0,0 +1,3 @@ +@blaze + +
merge(['class' => 'default-class']) }}>
\ No newline at end of file From 1e154c0fe341dd410f79e0b87b9e5aa50a94a258 Mon Sep 17 00:00:00 2001 From: Caleb Porzio Date: Sat, 4 Jul 2026 08:07:00 -0400 Subject: [PATCH 2/2] Fall back to full merge when the other appendable attribute is present The fast paths skipped merge()'s style normalization (trailing semicolon) and appendable-first key ordering whenever the bag carried the other appendable attribute, e.g.: Blade: class="..." style="color:blue;" wire:model="data" Blaze: class="..." wire:model="data" style="color:blue" Guard the class fast paths on the absence of a style attribute (and vice versa for style()), and handle falsy current style values ('' / '0') the way merge() does. Co-Authored-By: Claude Fable 5 --- src/Runtime/BlazeAttributeBag.php | 13 +++++++------ tests/ComparisonTest.php | 13 +++++++++++++ .../views/components/class-style-string.blade.php | 4 ++++ 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/views/components/class-style-string.blade.php diff --git a/src/Runtime/BlazeAttributeBag.php b/src/Runtime/BlazeAttributeBag.php index 641650b..24d80c0 100644 --- a/src/Runtime/BlazeAttributeBag.php +++ b/src/Runtime/BlazeAttributeBag.php @@ -49,8 +49,9 @@ public static function make(array $attributes, array $boundKeys = [], array $ori /** {@inheritdoc} */ public function merge(array $attributeDefaults = [], $escape = true): static { - // Fast path: merge(['class' => 'string']) — the most common pattern. - if (count($attributeDefaults) === 1 && isset($attributeDefaults['class']) && is_string($attributeDefaults['class'])) { + // Fast path: merge(['class' => 'string']) — the most common pattern. Skipped when the + // bag contains a style attribute, which merge() normalizes and reorders alongside class. + if (count($attributeDefaults) === 1 && isset($attributeDefaults['class']) && is_string($attributeDefaults['class']) && ! isset($this->attributes['style'])) { return $this->withMergedClass($escape ? e($attributeDefaults['class']) : $attributeDefaults['class']); } @@ -108,7 +109,7 @@ public function merge(array $attributeDefaults = [], $escape = true): static /** {@inheritdoc} */ public function class($classList): static { - if (is_string($classList)) { + if (is_string($classList) && ! isset($this->attributes['style'])) { return $this->withMergedClass(e($classList)); } @@ -120,11 +121,11 @@ public function class($classList): static /** {@inheritdoc} */ public function style($styleList): static { - if (is_string($styleList)) { + if (is_string($styleList) && ! isset($this->attributes['class'])) { $default = e(rtrim($styleList, ';').';'); - $current = $this->attributes['style'] ?? ''; + $current = $this->attributes['style'] ?? null; - if ($current) { + if ($current !== null) { $current = rtrim((string) $current, ';').';'; $style = $current === $default ? $default : $default.' '.$current; } else { diff --git a/tests/ComparisonTest.php b/tests/ComparisonTest.php index 14f776a..af0ac45 100644 --- a/tests/ComparisonTest.php +++ b/tests/ComparisonTest.php @@ -100,6 +100,19 @@ BLADE )); +test('merge class with style attribute present', fn () => compare(<<<'BLADE' + + BLADE +)); + +test('class and style string arguments', fn () => compare(<<<'BLADE' + + + + + BLADE +)); + test('deeply nested same component with different components interleaved', fn () => compare(<<<'BLADE' diff --git a/tests/fixtures/views/components/class-style-string.blade.php b/tests/fixtures/views/components/class-style-string.blade.php new file mode 100644 index 0000000..89c3f09 --- /dev/null +++ b/tests/fixtures/views/components/class-style-string.blade.php @@ -0,0 +1,4 @@ +@blaze + +
class('base') }}>
+
style('color: red') }}>
\ No newline at end of file