Skip to content

Commit

Permalink
Improvements to bind list context (#275)
Browse files Browse the repository at this point in the history
* test: isolate bug for #274

* feature: improve list binding so unnamed sibling lists can coexist
fixes #274

* stan: improve types
  • Loading branch information
g105b authored Oct 13, 2021
1 parent eaba552 commit ce56d65
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 21 deletions.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/vendor
/test/unit/_coverage
/.idea
/test/phpunit/_coverage
/.idea
.phpunit.result.cache
39 changes: 33 additions & 6 deletions src/NodePathCalculator.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<?php
namespace Gt\DomTemplate;

use Gt\Dom\Element;
use Gt\Dom\HTMLElement\HTMLElement;
use Gt\Dom\Node;
use Gt\Dom\Facade\NodeClass\DOMElementFacade;
use ReflectionObject;
use Stringable;

class NodePathCalculator implements Stringable {
Expand All @@ -12,11 +15,35 @@ public function __construct(
}

public function __toString():string {
$refObj = new \ReflectionObject($this->element);
$refProp = $refObj->getProperty("domNode");
$refProp->setAccessible(true);
/** @var DOMElementFacade $nativeDomNode */
$nativeDomNode = $refProp->getValue($this->element);
return $nativeDomNode->getNodePath();
$path = "";
/** @var Element $context */
$context = $this->element;

do {
$contextPath = strtolower($context->tagName);

if($context->id || $context->className) {
$attrPath = "";
if($id = $context->id) {
$attrPath .= "@id='$id'";
}

foreach($context->classList as $class) {
if(strlen($attrPath) !== 0) {
$attrPath .= " and ";
}

$attrPath .= "contains(concat(' ',normalize-space(@class),' '),' $class ')";
}

$contextPath .= "[$attrPath]";
}

$path = "/" . $contextPath . $path;
$context = $context->parentElement;
}
while($context && $context instanceof Element);

return $path;
}
}
25 changes: 16 additions & 9 deletions src/TemplateCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,33 @@ public function get(
private function extractTemplates(Document $document):void {
$dataTemplateArray = [];
foreach($document->querySelectorAll("[data-template]") as $element) {
$nodePath = new NodePathCalculator($element);
$dataTemplateArray[(string)$nodePath] = $element;
/** @var Element $element */
$nodePath = (string)(new NodePathCalculator($element));
$templateElement = new TemplateElement($element);
$key = $templateElement->getTemplateName() ?? $nodePath;
$dataTemplateArray[$key] = $templateElement;
}

uksort($dataTemplateArray,
fn(string $a, string $b):int => (
(strlen($a) > strlen($b))
(substr_count($a, "/") > substr_count($b, "/"))
? -1
: 1
)
);

foreach($dataTemplateArray as $nodePath => $element) {
/** @var Element $element */
$templateElement = new TemplateElement($element);
$name = $templateElement->getTemplateName() ?? $nodePath;
$this->elementKVP[$name] = $templateElement;
foreach($dataTemplateArray as $template) {
$template->removeOriginalElement();
}

$this->elementKVP = array_reverse($this->elementKVP, true);
// foreach($dataTemplateArray as $nodePath => $element) {
// /** @var Element $element */
// $templateElement = new TemplateElement($element);
// $name = $templateElement->getTemplateName() ?? $nodePath;
// $this->elementKVP[$name] = $templateElement;
// }

$this->elementKVP = array_reverse($dataTemplateArray, true);
}

private function findMatch(Element $context):TemplateElement {
Expand Down
9 changes: 5 additions & 4 deletions src/TemplateElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@ public function __construct(
$this->templateParentPath = new NodePathCalculator($this->originalElement->parentElement);

$siblingContext = $this->originalElement;
while($siblingContext = $siblingContext->nextSibling) {
while($siblingContext = $siblingContext->nextElementSibling) {
/** @var Element|Text $siblingContext */
if($siblingContext instanceof Text
|| !$siblingContext->hasAttribute("data-template")) {
if(!$siblingContext->hasAttribute("data-template")) {
break;
}
}
$this->templateNextSiblingPath =
is_null($siblingContext)
? null
: new NodePathCalculator($this->originalElement->nextSibling);
: new NodePathCalculator($this->originalElement->nextElementSibling);
}

public function removeOriginalElement():void {
$this->originalElement->remove();
}

Expand Down
41 changes: 41 additions & 0 deletions test/phpunit/DocumentBinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Gt\DomTemplate\ListBinder;
use Gt\DomTemplate\TableElementNotFoundInContextException;
use Gt\DomTemplate\TemplateCollection;
use Gt\DomTemplate\TemplateElement;
use Gt\DomTemplate\Test\TestFactory\DocumentTestFactory;
use PHPUnit\Framework\TestCase;
use stdClass;
Expand Down Expand Up @@ -616,6 +617,10 @@ public function testBindList_complexHTML():void {
"time" => "10:01",
"location" => "Mansfield",
],
"mnn209" => [
"time" => "10:24",
"location" => "Kirkby in Ashfield",
],
"c0353" => [
"time" => "10:31",
"location" => "Greendale Crescent",
Expand Down Expand Up @@ -746,4 +751,40 @@ public function testCleanBindAttributes_dataTemplate():void {
$document->documentElement->innerHTML
);
}

public function testBindListData_twoListsDifferentContexts():void {
$document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_TWO_LISTS_WITH_UNNAMED_TEMPLATES);
$sut = new DocumentBinder($document);

$progLangData = ["PHP", "HTML", "bash"];
$sut->bindList($progLangData, $document->getElementById("prog-lang-list"));
$gameData = ["Pac Man", "Mega Man", "Tetris"];
$sut->bindList($gameData, $document->getElementById("game-list"));

foreach($progLangData as $i => $progLang) {
self::assertSame($progLang, $document->querySelectorAll("#prog-lang-list li")[$i]->textContent);
}

foreach($gameData as $i => $game) {
self::assertSame($game, $document->querySelectorAll("#game-list li")[$i]->textContent);
}
}

public function testBindListData_twoListsDifferentContexts_withHtmlParents():void {
$document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_TWO_LISTS_WITH_UNNAMED_TEMPLATES_CLASS_PARENTS);
$sut = new DocumentBinder($document);

$progLangData = ["PHP", "HTML", "bash"];
$sut->bindList($progLangData, $document->querySelector(".favourite-list.prog-lang"));
$gameData = ["Pac Man", "Mega Man", "Tetris"];
$sut->bindList($gameData, $document->querySelector(".favourite-list.game"));

foreach($progLangData as $i => $progLang) {
self::assertSame($progLang, $document->querySelectorAll(".prog-lang li")[$i]->textContent);
}

foreach($gameData as $i => $game) {
self::assertSame($game, $document->querySelectorAll(".game li")[$i]->textContent);
}
}
}
73 changes: 73 additions & 0 deletions test/phpunit/ListBinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public function testBindList_simpleList():void {
->with($document->documentElement, null)
->willReturn($templateElement);

$templateElement->removeOriginalElement();

$ul = $document->querySelector("ul");
self::assertCount(
0,
Expand Down Expand Up @@ -132,6 +134,8 @@ public function testBindListData_twoLists():void {
$progLangData = ["PHP", "HTML", "bash"];
$sut->bindListData($progLangData, $document, "prog-lang");
$gameData = ["Pac Man", "Mega Man", "Tetris"];
$templateElementProgLang->removeOriginalElement();
$templateElementGame->removeOriginalElement();
$sut->bindListData($gameData, $document, "game");

foreach($progLangData as $i => $progLang) {
Expand All @@ -143,12 +147,53 @@ public function testBindListData_twoLists():void {
}
}

/**
* This is a slightly different test to above, where the context will
* be provided to specify the containing <UL> nodes, because the <LI>
* elements do not identify their own template name.
*/
public function testBindListData_twoListsDifferentContexts():void {
$document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_TWO_LISTS_WITH_UNNAMED_TEMPLATES);
$templateElementProgLang = new TemplateElement(
$document->querySelector("#prog-lang-list li[data-template]")
);
$templateElementGame = new TemplateElement(
$document->querySelector("#game-list li[data-template]")
);

$templateCollection = self::createMock(TemplateCollection::class);
$templateCollection->expects(self::exactly(2))
->method("get")
->withConsecutive(
[$document->getElementById("prog-lang-list")],
[$document->getElementById("game-list")]
)
->willReturnOnConsecutiveCalls($templateElementProgLang, $templateElementGame);

$sut = new ListBinder($templateCollection);
$progLangData = ["PHP", "HTML", "bash"];
$sut->bindListData($progLangData, $document->getElementById("prog-lang-list"));
$gameData = ["Pac Man", "Mega Man", "Tetris"];
$templateElementProgLang->removeOriginalElement();
$templateElementGame->removeOriginalElement();
$sut->bindListData($gameData, $document->getElementById("game-list"));

foreach($progLangData as $i => $progLang) {
self::assertSame($progLang, $document->querySelectorAll("#prog-lang-list li")[$i]->textContent);
}

foreach($gameData as $i => $game) {
self::assertSame($game, $document->querySelectorAll("#game-list li")[$i]->textContent);
}
}

public function testBindListData_empty_parentShouldBeEmpty():void {
$document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_LIST_TEMPLATE);
$templateElement = new TemplateElement($document->querySelector("li[data-template]"));
$templateCollection = self::createMock(TemplateCollection::class);
$templateCollection->method("get")
->willReturn($templateElement);
$templateElement->removeOriginalElement();

$sut = new ListBinder($templateCollection);
$sut->bindListData([], $document);
Expand All @@ -169,6 +214,7 @@ public function testBindListData_kvpList_array():void {
$templateCollection = self::createMock(TemplateCollection::class);
$templateCollection->method("get")
->willReturn($templateElement);
$templateElement->removeOriginalElement();

$sut = new ListBinder($templateCollection);
$sut->bindListData($kvpList, $orderList);
Expand All @@ -194,6 +240,7 @@ public function testBindListData_kvpList_object():void {
$templateCollection = self::createMock(TemplateCollection::class);
$templateCollection->method("get")
->willReturn($templateElement);
$templateElement->removeOriginalElement();

$sut = new ListBinder($templateCollection);
$sut->bindListData($kvpList, $orderList);
Expand All @@ -219,6 +266,7 @@ public function testBindListData_kvpList_instanceObject():void {
$templateCollection = self::createMock(TemplateCollection::class);
$templateCollection->method("get")
->willReturn($templateElement);
$templateElement->removeOriginalElement();

$sut = new ListBinder($templateCollection);
$sut->bindListData($kvpList, $orderList);
Expand Down Expand Up @@ -287,6 +335,7 @@ public function getTotalOrders():int {
$templateCollection = self::createMock(TemplateCollection::class);
$templateCollection->method("get")
->willReturn($templateElement);
$templateElement->removeOriginalElement();

$sut = new ListBinder($templateCollection);
$sut->bindListData($kvpList, $orderList);
Expand Down Expand Up @@ -342,6 +391,7 @@ public function testBindListData_kvpList_instanceObjectWithBindAttributeProperti
$templateCollection = self::createMock(TemplateCollection::class);
$templateCollection->method("get")
->willReturn($templateElement);
$templateElement->removeOriginalElement();

$sut = new ListBinder($templateCollection);
$sut->bindListData($kvpList, $orderList);
Expand Down Expand Up @@ -543,4 +593,27 @@ public function testBindListData_callback():void {
self::assertEquals($profitValue, $li->querySelector(".profit span")->textContent);
}
}

public function testBindList_twoListsSeparatedByElement():void {
$blueShades = ["Periwinkle", "Ultramarine", "Liberty", "Navy", "Blurple"];
$redShades = ["Brink pink", "Crimson", "Vermilion", "Scarlet"];
$document = DocumentTestFactory::createHTML(DocumentTestFactory::HTML_TWO_SUB_LISTS_SEPARATED_BY_ELEMENT);
$templateCollection = new TemplateCollection($document);
$sut = new ListBinder($templateCollection);
$sut->bindListData($redShades, $document, "red");
$sut->bindListData($blueShades, $document, "blue");

$dtElements = $document->querySelectorAll("dt");
$context = $dtElements[0]->nextElementSibling;
for($i = 0; $i < count($blueShades); $i++) {
self::assertEquals($blueShades[$i], $context->textContent);
$context = $context->nextElementSibling;
}

$context = $dtElements[1]->nextElementSibling;
for($i = 0; $i < count($redShades); $i++) {
self::assertEquals($redShades[$i], $context->textContent);
$context = $context->nextElementSibling;
}
}
}
43 changes: 43 additions & 0 deletions test/phpunit/TestFactory/DocumentTestFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,37 @@ class DocumentTestFactory {
</ul>
</div>
HTML;

const HTML_TWO_LISTS_WITH_UNNAMED_TEMPLATES = <<<HTML
<!doctype html>
<div id="favourites">
<h1>My favourite programming languages</h1>
<ul id="prog-lang-list">
<li data-template data-bind:text>Programming language goes here</li>
</ul>
<h1>My favourite video games</h1>
<ul id="game-list">
<li data-template data-bind:text>Video game goes here</li>
</ul>
</div>
HTML;

const HTML_TWO_LISTS_WITH_UNNAMED_TEMPLATES_CLASS_PARENTS = <<<HTML
<!doctype html>
<div id="favourites">
<h1>My favourite programming languages</h1>
<ul class="favourite-list prog-lang">
<li data-template data-bind:text>Programming language goes here</li>
</ul>
<h1>My favourite video games</h1>
<ul class="favourite-list game">
<li data-template data-bind:text>Video game goes here</li>
</ul>
</div>
HTML;

const HTML_USER_ORDER_LIST = <<<HTML
<!doctype html>
<div id="orders">
Expand Down Expand Up @@ -561,6 +592,18 @@ class DocumentTestFactory {
</ul>
HTML;

const HTML_TWO_SUB_LISTS_SEPARATED_BY_ELEMENT = <<<HTML
<!doctype html>
<main>
<dl>
<dt class="blue">Shades of blue</dt>
<dd data-template="blue" data-bind:text>Blue</dd>
<dt class="red">Shades of red</dt>
<dd data-template="red" data-bind:text>Red</dd>
</dl>
</main>
HTML;


public static function createHTML(string $html = ""):HTMLDocument {
Expand Down

0 comments on commit ce56d65

Please sign in to comment.