diff --git a/packages/view/src/Elements/IsElement.php b/packages/view/src/Elements/IsElement.php index 5e31977e8..839b7944a 100644 --- a/packages/view/src/Elements/IsElement.php +++ b/packages/view/src/Elements/IsElement.php @@ -60,11 +60,7 @@ public function getAttribute(string $name): ?string { $attributes = $this->getAttributes(); - $originalName = $name; - - $name = ltrim($name, ':'); - - return $attributes[$originalName] ?? $this->attributes[":{$name}"] ?? $this->attributes[$name] ?? null; + return $attributes[$name] ?? null; } public function setAttribute(string $name, string $value): self diff --git a/packages/view/src/Elements/ViewComponentElement.php b/packages/view/src/Elements/ViewComponentElement.php index 85f97735e..92a8e3410 100644 --- a/packages/view/src/Elements/ViewComponentElement.php +++ b/packages/view/src/Elements/ViewComponentElement.php @@ -40,7 +40,9 @@ public function __construct( array $attributes, ) { $this->attributes = $attributes; - $this->viewComponentAttributes = arr($attributes); + + $this->viewComponentAttributes = arr($attributes) + ->mapWithKeys(fn (string $value, string $key) => yield str($key)->ltrim(':')->toString() => $value); $this->dataAttributes = arr($attributes) ->filter(fn (string $_, string $key) => ! str_starts_with($key, ':')) @@ -96,50 +98,10 @@ public function compile(): string $compiled = str($this->viewComponent->contents); - // Fallthrough attributes - $compiled = $compiled - ->replaceRegex( - regex: '/^<(?[\w-]+)(.*?["\s])?>/', // Match the very first opening tag, this will never fail. - replace: function ($matches) { - /** @var \Tempest\View\Parser\Token $token */ - $token = TempestViewParser::ast($matches[0])[0]; - - $attributes = arr($token->htmlAttributes)->map(fn (string $value) => new MutableString($value)); - - foreach (['class', 'style', 'id'] as $attributeName) { - if (! isset($this->dataAttributes[$attributeName])) { - continue; - } - - $attributes[$attributeName] ??= new MutableString(); - - if ($attributeName === 'id') { - $attributes[$attributeName] = new MutableString(' ' . $this->dataAttributes[$attributeName]); - } else { - $attributes[$attributeName]->append(' ' . $this->dataAttributes[$attributeName]); - } - } + $compiled = $this->applyFallthroughAttributes($compiled); - return sprintf( - '<%s%s>', - $matches['tag'], - $attributes - ->map(function (MutableString $value, string $key) { - return sprintf('%s="%s"', $key, $value->trim()); - }) - ->implode(' ') - ->when( - fn (ImmutableString $string) => $string->isNotEmpty(), - fn (ImmutableString $string) => $string->prepend(' '), - ), - ); - }, - ); - - // Add scoped variables $compiled = $compiled ->prepend( - // Open the current scope sprintf( '', $this->dataAttributes->isNotEmpty() ? ', ' . $this->dataAttributes->map(fn (string $_value, string $key) => "\${$key}")->implode(', ') : '', @@ -148,10 +110,9 @@ public function compile(): string ), ) ->append( - // Close and call the current scope sprintf( 'currentView?->data ?? []) %s %s) ?>', - ViewObjectExporter::export($this->viewComponentAttributes), + $this->exportAttributesArray(), ViewObjectExporter::export($slots), $this->scopedVariables->isNotEmpty() ? $this->scopedVariables->map(fn (string $name) => "'{$name}' => \${$name}")->implode(', ') @@ -165,7 +126,6 @@ public function compile(): string ), ); - // Compile slots $compiled = $compiled->replaceRegex( regex: '/[\w-]+)")?((\s*\/>)|>(?(.|\n)*?)<\/x-slot>)/', replace: function ($matches) use ($slots) { @@ -222,4 +182,77 @@ private function getSlotElement(string $name): SlotElement|CollectionElement|nul return null; } + + private function applyFallthroughAttributes(ImmutableString $compiled): ImmutableString + { + return $compiled->replaceRegex( + regex: '/^<(?[\w-]+)(.*?["\s])?>/', + replace: function (array $matches): string { + /** @var Token $token */ + $token = TempestViewParser::ast($matches[0])[0]; + + $attributes = arr($token->htmlAttributes) + ->map(fn (string $value) => new MutableString($value)); + + foreach (['class', 'style', 'id'] as $name) { + $attributes = $this->applyFallthroughAttribute($attributes, $name); + } + + $attributeString = $attributes + ->map(fn (MutableString $value, string $key) => sprintf('%s="%s"', $key, $value->trim())) + ->implode(' ') + ->when( + fn (ImmutableString $s) => $s->isNotEmpty(), + fn (ImmutableString $s) => $s->prepend(' '), + ); + + return sprintf('<%s%s>', $matches['tag'], $attributeString); + }, + ); + } + + private function applyFallthroughAttribute(ImmutableArray $attributes, string $name): ImmutableArray + { + $hasDataAttribute = isset($this->dataAttributes[$name]); + $hasExpressionAttribute = isset($this->expressionAttributes[$name]); + + if (! $hasDataAttribute && ! $hasExpressionAttribute) { + return $attributes; + } + + $attributes[$name] ??= new MutableString(); + + if ($name === 'id') { + if ($hasDataAttribute) { + $attributes[$name] = new MutableString($this->dataAttributes[$name]); + } elseif ($hasExpressionAttribute) { + $attributes[$name] = new MutableString(sprintf('', $name)); + } + } else { + if ($hasDataAttribute) { + $attributes[$name]->append(' ' . $this->dataAttributes[$name]); + } + if ($hasExpressionAttribute) { + $attributes[$name]->append(sprintf(' ', $name)); + } + } + + return $attributes; + } + + private function exportAttributesArray(): string + { + $entries = []; + + foreach ($this->viewComponentAttributes as $key => $value) { + $camelKey = str($key)->camel()->toString(); + $isExpression = isset($this->expressionAttributes[$camelKey]); + + $entries[] = $isExpression + ? sprintf("'%s' => %s", $key, $value) + : sprintf("'%s' => %s", $key, ViewObjectExporter::exportValue($value)); + } + + return sprintf('new \%s([%s])', ImmutableArray::class, implode(', ', $entries)); + } } diff --git a/packages/view/tests/FallthroughAttributesTest.php b/packages/view/tests/FallthroughAttributesTest.php new file mode 100644 index 000000000..f68f7a25e --- /dev/null +++ b/packages/view/tests/FallthroughAttributesTest.php @@ -0,0 +1,38 @@ +addViewComponents( + __DIR__ . '/Fixtures/x-fallthrough-test.view.php', + __DIR__ . '/Fixtures/x-fallthrough-dynamic-test.view.php', + ); + + $renderer = + TempestViewRenderer::make( + viewConfig: $viewConfig, + ); + + $html = $renderer->render( + view(__DIR__ . '/Fixtures/fallthrough.view.php'), + ); + + $this->assertEquals(<<<'HTML' +
+
+
+
+ HTML, $html); + } +} diff --git a/packages/view/tests/Fixtures/fallthrough.view.php b/packages/view/tests/Fixtures/fallthrough.view.php new file mode 100644 index 000000000..bdb7a7794 --- /dev/null +++ b/packages/view/tests/Fixtures/fallthrough.view.php @@ -0,0 +1,7 @@ + + + + diff --git a/packages/view/tests/Fixtures/x-fallthrough-dynamic-test.view.php b/packages/view/tests/Fixtures/x-fallthrough-dynamic-test.view.php new file mode 100644 index 000000000..1ccf85821 --- /dev/null +++ b/packages/view/tests/Fixtures/x-fallthrough-dynamic-test.view.php @@ -0,0 +1 @@ +
diff --git a/packages/view/tests/Fixtures/x-fallthrough-test.view.php b/packages/view/tests/Fixtures/x-fallthrough-test.view.php new file mode 100644 index 000000000..58d8adfe0 --- /dev/null +++ b/packages/view/tests/Fixtures/x-fallthrough-test.view.php @@ -0,0 +1 @@ +