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
20 changes: 20 additions & 0 deletions src/ComponentBinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ public function bindList(
return parent::bindList($listData, $context, $templateName);
}

public function bindListCallback(
iterable $listData,
callable $callback,
null|string|Element $context = null,
?string $templateName = null
):int {
if(is_string($context)) {
$context = $this->stringToContext($context);
}

if($context) {
$this->checkElementContainedWithinComponent($context);
}
else {
$context = $this->componentElement;
}

return parent::bindListCallback($listData, $callback, $context, $templateName);
}

protected function bind(
?string $key,
mixed $value,
Expand Down
29 changes: 29 additions & 0 deletions src/ListElementCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use Gt\Dom\Document;
use Gt\Dom\Element;
use Throwable;

class ListElementCollection {
/** @var array<string, ListElement> */
Expand Down Expand Up @@ -69,6 +70,34 @@ private function findMatch(Element $context):ListElement {
$contextPath
);

$matchedElement = null;
$matchedDepth = -1;
foreach($this->elementKVP as $name => $element) {
try {
$listItemParent = $element->getListItemParent();
}
catch(Throwable) {
continue;
}

if(!$listItemParent instanceof Element) {
continue;
}
if($listItemParent !== $context && !$context->contains($listItemParent)) {
continue;
}

$depth = substr_count((string)(new NodePathCalculator($listItemParent)), "/");
if($depth > $matchedDepth) {
$matchedElement = $element;
$matchedDepth = $depth;
}
}

if($matchedElement) {
return $matchedElement;
}

foreach($this->elementKVP as $name => $element) {
if($contextPath === $name) {
continue;
Expand Down
44 changes: 44 additions & 0 deletions test/phpunit/ComponentBinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,50 @@ public function testBindList_stringContext():void {
$sut->bindList(["List", "for", "main component"]);
}

public function testBindListCallback_stringContext():void {
$document = new HTMLDocument(HTMLPageContent::HTML_COMPONENT_WITH_ATTRIBUTE_NESTED);
$componentElement = $document->querySelector("example-component");
$subComponent1 = $document->querySelector("#subcomponent-1");
$subComponent2 = $document->querySelector("#subcomponent-2");

$listBinder = self::createMock(ListBinder::class);
$bindMatcher = self::exactly(3);
$listBinder->expects($bindMatcher)
->method("bindListData")
->willReturnCallback(function(
array $listData,
Element $context,
?string $templateName = null,
?callable $callback = null,
) use($bindMatcher, $componentElement, $subComponent1, $subComponent2):int {
match($bindMatcher->numberOfInvocations()) {
1 => self::assertEquals([["List", "for", "component 2"], $subComponent2], [$listData, $context]),
2 => self::assertEquals([["List", "for", "component 1"], $subComponent1], [$listData, $context]),
3 => self::assertEquals([["List", "for", "main component"], $componentElement], [$listData, $context]),
};
self::assertNull($templateName);
self::assertIsCallable($callback);

return 0;
});

$sut = new ComponentBinder($document);
$sut->setDependencies(
self::createStub(ElementBinder::class),
self::createStub(PlaceholderBinder::class),
self::createStub(TableBinder::class),
$listBinder,
self::createStub(ListElementCollection::class),
self::createStub(BindableCache::class),
);
$sut->setComponentBinderDependencies($componentElement);

$callback = static fn(Element $template, array $row):array => $row;
$sut->bindListCallback(["List", "for", "component 2"], $callback, "#subcomponent-2");
$sut->bindListCallback(["List", "for", "component 1"], $callback, "#subcomponent-1");
$sut->bindListCallback(["List", "for", "main component"], $callback);
}

public function testBindValue_stringContext():void {
$document = new HTMLDocument(HTMLPageContent::HTML_COMPONENT_WITH_ATTRIBUTE_NESTED);
$componentElement = $document->querySelector("example-component");
Expand Down
135 changes: 124 additions & 11 deletions test/phpunit/DocumentBinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Gt\DomTemplate\BindableCache;
use Gt\DomTemplate\BindGetter;
use Gt\DomTemplate\BindValue;
use Gt\DomTemplate\ComponentBinder;
use Gt\DomTemplate\ComponentExpander;
use Gt\DomTemplate\DocumentBinder;
use Gt\DomTemplate\ElementBinder;
use Gt\DomTemplate\HTMLAttributeBinder;
Expand All @@ -20,6 +22,7 @@
use Gt\DomTemplate\InvalidBindPropertyException;
use Gt\DomTemplate\ListBinder;
use Gt\DomTemplate\ListElementCollection;
use Gt\DomTemplate\PartialContent;
use Gt\DomTemplate\PlaceholderBinder;
use Gt\DomTemplate\TableBinder;
use Gt\DomTemplate\TableElementNotFoundInContextException;
Expand All @@ -35,7 +38,7 @@
use Traversable;
use ArrayIterator;

class DocumentBinderTest extends TestCase {
class DocumentBinderTest extends PartialContentTestCase {
/**
* If the developer forgets to add a bind property (the bit after the
* colon in `data-bind:text`, we should let them know with a friendly
Expand Down Expand Up @@ -1823,24 +1826,28 @@ public function testBindList_stringContext():void {

public function testBindListCallback_stringContext():void {
$document = new HTMLDocument(HTMLPageContent::HTML_COMPONENT_WITH_ATTRIBUTE_NESTED);
$documentElement = $document->documentElement;
$subComponent1 = $document->querySelector("#subcomponent-1");
$subComponent2 = $document->querySelector("#subcomponent-2");

$listBinder = self::createMock(ListBinder::class);
$bindMatcher = self::exactly(2);
$bindMatcher = self::exactly(3);
$listBinder->expects($bindMatcher)
->method("bindListData")
->willReturnCallback(function(
array $listData,
Element|Document $context,
?string $templateName,
?callable $callback,
)use($bindMatcher, $subComponent1, $subComponent2):int {
?string $templateName = null,
?callable $callback = null,
) use($bindMatcher, $documentElement, $subComponent1, $subComponent2):int {
match($bindMatcher->numberOfInvocations()) {
1 => self::assertEquals([["A"], $subComponent1], [$listData, $context]),
2 => self::assertEquals([["B"], $subComponent2], [$listData, $context]),
1 => self::assertEquals([["List", "for", "component 2"], $subComponent2], [$listData, $context]),
2 => self::assertEquals([["List", "for", "component 1"], $subComponent1], [$listData, $context]),
3 => self::assertEquals([["List", "for", "main component"], $documentElement], [$listData, $context]),
};
self::assertNotNull($callback);
self::assertNull($templateName);
self::assertIsCallable($callback);

return 0;
});

Expand All @@ -1854,9 +1861,10 @@ public function testBindListCallback_stringContext():void {
self::createStub(BindableCache::class),
);

$callback = fn(Element $template, mixed $listItem, int|string $key):mixed => $listItem;
$sut->bindListCallback(["A"], $callback, "#subcomponent-1");
$sut->bindListCallback(["B"], $callback, $subComponent2);
$callback = static fn(Element $template, array $row):array => $row;
$sut->bindListCallback(["List", "for", "component 2"], $callback, "#subcomponent-2");
$sut->bindListCallback(["List", "for", "component 1"], $callback, "#subcomponent-1");
$sut->bindListCallback(["List", "for", "main component"], $callback);
}

public function testBindValue_stringContext():void {
Expand Down Expand Up @@ -1963,6 +1971,111 @@ private function bindCleanupMixedDebug(DocumentBinder $sut):string {
return "test/phpunit/DocumentBinderTest.php:$line";
}

public function testBindList_multipleComponent():void {
$document = new HTMLDocument(HTMLPageContent::HTML_PAGE_WITH_TWO_LIST_COMPONENTS);
$componentExpander = new ComponentExpander(
$document,
self::mockPartialContent(
"_component",
[
"global-header" => HTMLPageContent::HTML_GLOBAL_HEADER,
"simple-list" => HTMLPageContent::HTML_SIMPLE_LIST,
]
)
);
$componentExpander->expand();
$dependencies = $this->documentBinderDependencies($document);

$sut = new DocumentBinder($document);
$sut->setDependencies(...$dependencies);

$linkList = [
["name" => "First link", "link" => "/1"],
["name" => "Second link", "link" => "/2"],
["name" => "Third link", "link" => "/3"],
];
$peopleList = [
"active" => [
["name" => "Andrew"],
["name" => "Becca"],
],
"inactive" => [
["name" => "Charlie"],
["name" => "Devi"],
["name" => "Elle"],
["name" => "Frankie"],
],
];

$globalHeaderEl = $document->querySelector("global-header");
[$simpleListEl1, $simpleListEl2] = iterator_to_array($document->querySelectorAll("simple-list"));

$globalheaderBinder = new ComponentBinder($document);
$globalheaderBinder->setDependencies(...$dependencies);
$globalheaderBinder->setComponentBinderDependencies($globalHeaderEl);
$globalheaderBinder->bindList($linkList);

$simpleListBinder1 = new ComponentBinder($document);
$simpleListBinder1->setDependencies(...$dependencies);
$simpleListBinder1->setComponentBinderDependencies($simpleListEl1);
$simpleListBinder1->bindList($peopleList["active"]);

$simpleListBinder2 = new ComponentBinder($document);
$simpleListBinder2->setDependencies(...$dependencies);
$simpleListBinder2->setComponentBinderDependencies($simpleListEl2);
$simpleListBinder2->bindList($peopleList["inactive"]);

self::assertCount(3, $globalHeaderEl->querySelectorAll("a"));
self::assertCount(2, $simpleListEl1->querySelectorAll("li"));
self::assertCount(4, $simpleListEl2->querySelectorAll("li"));
}

public function testBindListCallback_multipleComponent():void {
$document = new HTMLDocument(HTMLPageContent::HTML_PAGE_WITH_TWO_LIST_COMPONENTS);
$componentExpander = new ComponentExpander(
$document,
self::mockPartialContent(
"_component",
[
"global-header" => HTMLPageContent::HTML_GLOBAL_HEADER,
"simple-list" => HTMLPageContent::HTML_SIMPLE_LIST,
]
)
);
$componentExpander->expand();
$dependencies = $this->documentBinderDependencies($document);

[$simpleListEl1, $simpleListEl2] = iterator_to_array($document->querySelectorAll("simple-list"));
$callback = fn(Element $template, array $row):array => $row;

$simpleListBinder1 = new ComponentBinder($document);
$simpleListBinder1->setDependencies(...$dependencies);
$simpleListBinder1->setComponentBinderDependencies($simpleListEl1);
$simpleListBinder1->bindListCallback(
[
["name" => "Andrew"],
["name" => "Becca"],
],
$callback,
);

$simpleListBinder2 = new ComponentBinder($document);
$simpleListBinder2->setDependencies(...$dependencies);
$simpleListBinder2->setComponentBinderDependencies($simpleListEl2);
$simpleListBinder2->bindListCallback(
[
["name" => "Charlie"],
["name" => "Devi"],
["name" => "Elle"],
["name" => "Frankie"],
],
$callback,
);

self::assertCount(2, $simpleListEl1->querySelectorAll("li"));
self::assertCount(4, $simpleListEl2->querySelectorAll("li"));
}

private function documentBinderDependencies(HTMLDocument $document, mixed...$otherObjectList):array {
$htmlAttributeBinder = new HTMLAttributeBinder();
$htmlAttributeCollection = new HTMLAttributeCollection();
Expand Down
28 changes: 28 additions & 0 deletions test/phpunit/ListElementCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Gt\DomTemplate\HTMLAttributeBinder;
use Gt\DomTemplate\HTMLAttributeCollection;
use Gt\DomTemplate\ListBinder;
use Gt\DomTemplate\ListElement;
use Gt\DomTemplate\ListElementCollection;
use Gt\DomTemplate\ListElementNotFoundInContextException;
use Gt\DomTemplate\PlaceholderBinder;
Expand Down Expand Up @@ -160,6 +161,33 @@ public function testConstructor_nonListChildrenArePreservedInOrder():void {
self::assertSame("This list item will always show at the end", $ulChildren[3]->textContent);
}

public function testGet_noName_fallsBackToPathMatchingWhenListParentDetached():void {
$document = new HTMLDocument(
"<!doctype html><html><body><section id='target'><ul></ul></section></body></html>"
);
$sut = new ListElementCollection($document);
$nonElementParent = self::createMock(ListElement::class);
$nonElementParent->method("getListItemParent")
->willReturn(self::createStub(\Gt\Dom\Node::class));
$listElement = self::createMock(ListElement::class);
$listElement->method("getListItemParent")
->willThrowException(new \TypeError("Detached"));

$reflection = new \ReflectionProperty($sut, "elementKVP");
$reflection->setValue(
$sut,
[
"/html/body/aside/span" => $nonElementParent,
"/html/body/section[@id='target']/ul" => $listElement,
]
);

self::assertSame(
$listElement,
$sut->get($document->querySelector("#target"))
);
}

private function elementBinderDependencies(HTMLDocument $document, mixed...$otherObjectList):array {
$htmlAttributeBinder = new HTMLAttributeBinder();
$htmlAttributeCollection = new HTMLAttributeCollection();
Expand Down
37 changes: 37 additions & 0 deletions test/phpunit/TestHelper/HTMLPageContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,43 @@ class HTMLPageContent {
</form>
HTML;

const HTML_PAGE_WITH_TWO_LIST_COMPONENTS = <<<HTML
<!doctype html>
<meta charset="utf-8" />
<title>Two components, each with lists</title>

<global-header />
<h1>Test page</h1>

<main>
<p>Simple list 1</p>
<simple-list />

<p>Simple lilst 2</p>
<simple-list />
</main>
HTML;

const HTML_GLOBAL_HEADER = <<<HTML
<nav>
<ul>
<li data-list>
<a href="/" data-bind:text="name" data-bind:href="link">Link title</a>
</li>
</ul>
</nav>
HTML;

const HTML_SIMPLE_LIST = <<<HTML
<ul>
<li data-list>
<p>Name: <span data-bind:text="name">unnamed</span></p>
</li>
</ul>
HTML;



public static function createHTML(string $html = ""):HTMLDocument {
return new HTMLDocument($html);
}
Expand Down
Loading