Skip to content

Commit

Permalink
fix: view component attribute fixes (tempestphp#459)
Browse files Browse the repository at this point in the history
  • Loading branch information
brendt authored Sep 22, 2024
1 parent ee84b94 commit 4622298
Show file tree
Hide file tree
Showing 16 changed files with 154 additions and 28 deletions.
5 changes: 5 additions & 0 deletions src/Tempest/Support/src/StringHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ public function pascal(): self
return new self(implode('', $studlyWords));
}

public function camel(): self
{
return new self(lcfirst((string) $this->pascal()));
}

public function deduplicate(string|array $characters = ' '): self
{
$string = $this->string;
Expand Down
6 changes: 5 additions & 1 deletion src/Tempest/View/src/Elements/ElementFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Tempest\View\Elements;

use DOMAttr;
use DOMElement;
use DOMNode;
use DOMText;
Expand Down Expand Up @@ -48,8 +49,11 @@ private function makeElement(View $view, DOMNode $node, ?Element $parent): ?Elem
} else {
$attributes = [];

/** @var DOMAttr $attribute */
foreach ($node->attributes as $attribute) {
$attributes[$attribute->name] = $attribute->value;
$name = (string) \Tempest\Support\str($attribute->name)->camel();

$attributes[$name] = $attribute->value;
}

$element = new GenericElement(
Expand Down
6 changes: 5 additions & 1 deletion src/Tempest/View/src/Elements/GenericElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,13 @@ public function getSlot(string $name = 'slot'): ?Element

public function getData(?string $key = null): mixed
{
if ($key && $this->hasAttribute($key)) {
return $this->getAttribute($key);
}

$parentData = $this->getParent()?->getData() ?? [];

$data = [...$this->view->getData(), ...$parentData, ...$this->data];
$data = [...$this->attributes, ...$this->view->getData(), ...$parentData, ...$this->data];

if ($key) {
return $data[$key] ?? null;
Expand Down
5 changes: 5 additions & 0 deletions src/Tempest/View/src/IsView.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public function get(string $key): mixed
return $this->{$key} ?? $this->data[$key] ?? null;
}

public function has(string $key): bool
{
return array_key_exists($key, $this->data) || property_exists($this, $key);
}

public function data(mixed ...$params): self
{
$this->rawData = [...$this->rawData, ...$params];
Expand Down
61 changes: 44 additions & 17 deletions src/Tempest/View/src/Renderers/TempestViewRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,19 +135,7 @@ private function resolveContent(View $view): string
$path = $view->getPath();

if (! str_ends_with($path, '.php')) {
ob_start();

try {
// TODO: find a better way of dealing with views that declare strict types
$path = str_replace('declare(strict_types=1);', '', $path);

/** @phpstan-ignore-next-line */
eval('?>' . $path . '<?php');
} catch (ParseError) {
return $path;
}

return ob_get_clean();
return $this->evalContentIsolated($view, $path);
}

$discoveryLocations = $this->kernel->discoveryLocations;
Expand All @@ -161,7 +149,7 @@ private function resolveContent(View $view): string
throw new Exception("View {$path} not found");
}

return $this->resolveContentIsolated($path, $view->getData());
return $this->resolveContentIsolated($view, $path);
}

private function resolveViewComponent(GenericElement $element): ?ViewComponent
Expand Down Expand Up @@ -239,9 +227,9 @@ private function renderCollectionElement(View $view, CollectionElement $collecti
private function renderViewComponent(View $view, ViewComponent $viewComponent, GenericElement $element): string
{
$renderedContent = preg_replace_callback(
pattern: '/<x-slot\s*(name="(?<name>\w+)")?\s*\/>/',
pattern: '/<x-slot\s*(name="(?<name>\w+)")?((\s*\/>)|><\/x-slot>)/',
callback: function ($matches) use ($view, $element) {
$name = $matches['name'] ?? 'slot';
$name = $matches['name'] ?: 'slot';

$slot = $element->getSlot($name);

Expand Down Expand Up @@ -306,14 +294,53 @@ private function renderGenericElement(View $view, GenericElement $element): stri
return "<{$element->getTag()}{$attributes}>{$content}</{$element->getTag()}>";
}

private function resolveContentIsolated(string $_path, array $_data): string
private function resolveContentIsolated(View $_view, string $_path): string
{
ob_start();

$_data = $_view->getData();

extract($_data, flags: EXTR_SKIP);

include $_path;

$content = ob_get_clean();

// If the view defines local variables, we add them here to the view object as well
foreach (get_defined_vars() as $key => $value) {
if (! $_view->has($key)) {
$_view->data(...[$key => $value]);
}
}

return $content;
}

private function evalContentIsolated(View $_view, string $_content): string
{
ob_start();

$_data = $_view->getData();

extract($_data, flags: EXTR_SKIP);

try {
// TODO: find a better way of dealing with views that declare strict types
$_content = str_replace('declare(strict_types=1);', '', $_content);

/** @phpstan-ignore-next-line */
eval('?>' . $_content . '<?php');
} catch (ParseError) {
return $_content;
}

// If the view defines local variables, we add them here to the view object as well
foreach (get_defined_vars() as $key => $value) {
if (! $_view->has($key)) {
$_view->data(...[$key => $value]);
}
}

return ob_get_clean();
}
}
2 changes: 2 additions & 0 deletions src/Tempest/View/src/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public function getRaw(string $key): mixed;

public function get(string $key): mixed;

public function has(string $key): bool;

public function data(...$params): self;

public function raw(string $name): ?string;
Expand Down
15 changes: 6 additions & 9 deletions src/Tempest/View/src/ViewComponentView.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Tempest\View;

use Tempest\View\Elements\GenericElement;

final class ViewComponentView implements View
{
use IsView;
Expand All @@ -18,15 +16,14 @@ public function __construct(
$this->path = $content;
}

public function __get(string $name): mixed
public function getData(): array
{
$value = null;

if ($this->wrappingElement instanceof GenericElement) {
$value = $this->wrappingElement->getAttribute($name);
}
return $this->wrappingElement->getData();
}

return $value ?? $this->wrappingElement->getData($name);
public function __get(string $name): mixed
{
return $this->wrappingElement->getData($name);
}

public function __call(string $name, array $arguments)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<x-component name="x-view-component-attribute-without-this-a">
<?= $var ?? 'nothing' ?>
</x-component>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<x-view-component-attribute-without-this-a var="fromString" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<x-component name="x-view-component-with-camelcase-attribute-a">
<?= $metaType ?? 'nothing' ?>
</x-component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<x-view-component-with-camelcase-attribute-a meta-type="test" />
<x-view-component-with-camelcase-attribute-a meta_type="test" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<x-component name="x-view-component-with-non-self-closing-slot-a">
A: <x-slot name="other"/>
B: <x-slot name="other" />
C: <x-slot name="other"></x-slot>
A: <x-slot/>
B: <x-slot />
C: <x-slot></x-slot>
</x-component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<x-view-component-with-non-self-closing-slot-a>
main slot
<x-slot name="other">other slot</x-slot>
</x-view-component-with-non-self-closing-slot-a>
3 changes: 3 additions & 0 deletions tests/Fixtures/Views/view-defined-local-vars-a.view.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<x-component name="x-view-defined-local-vars-a">
<?= $this->var ?? 'nothing' ?>
</x-component>
7 changes: 7 additions & 0 deletions tests/Fixtures/Views/view-defined-local-vars-b.view.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php
$localVariable = 'fromPHP';
?>

<x-view-defined-local-vars-a :var="$localVariable"></x-view-defined-local-vars-a>
<x-view-defined-local-vars-a var="fromString"></x-view-defined-local-vars-a>
<x-view-defined-local-vars-a/>
50 changes: 50 additions & 0 deletions tests/Integration/View/ViewComponentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,54 @@ public function test_with_passed_variable_within_loop(): void
$rendered
);
}

public function test_inline_view_variables_passed_to_component(): void
{
$html = $this->render(view(__DIR__ . '/../../Fixtures/Views/view-defined-local-vars-b.view.php'));

$this->assertSame(<<<HTML
fromPHP
fromString
nothing
HTML, $html);
}

public function test_view_component_attribute_variables_without_this(): void
{
$html = $this->render(view(__DIR__ . '/../../Fixtures/Views/view-component-attribute-without-this-b.view.php'));

$this->assertSame(<<<HTML
fromString
HTML, $html);
}

public function test_view_component_slots_without_self_closing_tags(): void
{
$html = $this->render(view(__DIR__ . '/../../Fixtures/Views/view-component-with-non-self-closing-slot-b.view.php'));

$this->assertStringEqualsStringIgnoringLineEndings(<<<HTML
A: other slot
B: other slot
C: other slot
A:
main slot
B:
main slot
C:
main slot
HTML, $html);
}

public function test_view_component_with_camelcase_attribute(): void
{
$html = $this->render(view(__DIR__ . '/../../Fixtures/Views/view-component-with-camelcase-attribute-b.view.php'));

$this->assertSame(<<<HTML
test
test
HTML, $html);
}
}

0 comments on commit 4622298

Please sign in to comment.