diff --git a/docs/1-essentials/02-views.md b/docs/1-essentials/02-views.md
index 17b6536ace..3db0f453cd 100644
--- a/docs/1-essentials/02-views.md
+++ b/docs/1-essentials/02-views.md
@@ -246,6 +246,39 @@ The example above will only render the child `div` elements:
Post C
```
+### Tag override with the `as` prop
+
+The `as` attribute allows you to transform the rendered tag of one element into another. This takes place on an instance of `GenericElement`, so for example this code:
+```html
+My Link
+```
+Would render
+```html
+
+```
+The power behind this is when you use an `Expression` to determine the element.
+
+Say for example, you wish to have a `` component which renders as an `` when the `$href` attribute is provided. In your view, use the component like so:
+```html
+Click to go to an awesome website
+
+This is just a button
+```
+In your `` component, define:
+```html
+
+```
+Your page will render two links, as follows
+```html
+Click to go to an awesome website
+
+
+```
+
+#### Where this can and cannot be used
+
+You can't use the `as` Attribute on things like ``, ``, etc, as these do not themselves render any HTML. They are placeholders in the page. Nor will placing it on a view component itself inherently do anything. The `as` attribute CAN be passed to a ViewComponent as shown in the example above, but by itself it will actually do nothing, unless you specifically provide logic to place it where you want it.
+
## View components
Components allow for splitting the user interface into independent and reusable pieces.
diff --git a/packages/view/src/Attributes/AsAttribute.php b/packages/view/src/Attributes/AsAttribute.php
new file mode 100644
index 0000000000..68a62f0212
--- /dev/null
+++ b/packages/view/src/Attributes/AsAttribute.php
@@ -0,0 +1,47 @@
+consumeAttribute($this->name) ?? '');
+
+ if ($value->isEmpty()) {
+ return $element;
+ }
+
+ $generic = $element->unwrap(GenericElement::class);
+
+ if ($generic === null) {
+ return $element;
+ }
+
+ // :as="expression" — follows the ExpressionAttribute convention
+ if (str($this->name)->startsWith(':')) {
+ if ($value->startsWith(['{{', '{!!', ...TempestViewCompiler::PHP_TOKENS])) {
+ throw new ExpressionAttributeWasInvalid($value);
+ }
+
+ return $generic->withTagExpression($value->toString());
+ }
+
+ // as="literal-tag"
+ return $generic->withTag($value->toString());
+ }
+}
diff --git a/packages/view/src/Attributes/AttributeFactory.php b/packages/view/src/Attributes/AttributeFactory.php
index a62a2cbb14..ce1f9eced5 100644
--- a/packages/view/src/Attributes/AttributeFactory.php
+++ b/packages/view/src/Attributes/AttributeFactory.php
@@ -17,6 +17,8 @@ public function make(string $attributeName): Attribute
$attributeName === ':else' => new ElseAttribute(),
$attributeName === ':foreach' => new ForeachAttribute(),
$attributeName === ':forelse' => new ForelseAttribute(),
+ $attributeName === 'as' => new AsAttribute('as'),
+ $attributeName === ':as' => new AsAttribute(':as'),
str_starts_with($attributeName, '::') => new EscapedExpressionAttribute($attributeName),
str_starts_with($attributeName, ':') => new ExpressionAttribute($attributeName),
default => new DataAttribute($attributeName),
diff --git a/packages/view/src/Elements/GenericElement.php b/packages/view/src/Elements/GenericElement.php
index b0266a9b70..fdb9849242 100644
--- a/packages/view/src/Elements/GenericElement.php
+++ b/packages/view/src/Elements/GenericElement.php
@@ -14,9 +14,11 @@ final class GenericElement implements Element, WithToken
{
use IsElement;
+ private ?string $tagExpression = null;
+
public function __construct(
public readonly Token $token,
- private readonly string $tag,
+ private string $tag,
private readonly bool $isHtml,
array $attributes,
) {
@@ -28,6 +30,22 @@ public function getTag(): string
return $this->tag;
}
+ public function withTag(string $tag): self
+ {
+ $clone = clone $this;
+ $clone->tag = $tag;
+
+ return $clone;
+ }
+
+ public function withTagExpression(string $expression): self
+ {
+ $clone = clone $this;
+ $clone->tagExpression = $expression;
+
+ return $clone;
+ }
+
public function compile(): string
{
$content = [];
@@ -54,6 +72,16 @@ public function compile(): string
$attributes = ' ' . $attributes;
}
+ // When a PHP expression drives the tag, we cannot know the resolved
+ // name at compile time, so void-element detection is skipped and we
+ // always emit a full open/close pair.
+ if ($this->tagExpression !== null) {
+ $open = "= {$this->tagExpression} ?>";
+ $close = "= {$this->tagExpression} ?>";
+
+ return "<{$open}{$attributes}>{$content}{$close}>";
+ }
+
// Void elements
if (is_void_tag($this->tag)) {
if ($this->isHtml) {
diff --git a/packages/view/tests/AsAttributeTest.php b/packages/view/tests/AsAttributeTest.php
new file mode 100644
index 0000000000..0e6ebfca72
--- /dev/null
+++ b/packages/view/tests/AsAttributeTest.php
@@ -0,0 +1,167 @@
+render(
+ 'Test',
+ );
+
+ $this->assertSnippetsMatch('', $html);
+ }
+
+ #[Test]
+ public function generic_element_expression_as_overrides_tag(): void
+ {
+ $html = TempestViewRenderer::make()->render(
+ view('Test')->data(tag: 'button'),
+ );
+
+ $this->assertSnippetsMatch('', $html);
+ }
+
+ #[Test]
+ public function generic_element_static_as_on_nested_element(): void
+ {
+ $html = TempestViewRenderer::make()->render(
+ '