From b469f1813bbd039bfb5ba0d2a65b9a4f0eeff224 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 25 Nov 2024 09:59:24 +0000 Subject: [PATCH] feature: allow binding directly to `data-element` (#498) * feature: allow binding directly to `data-element` * tweak: pass empty string rather than bool * tweak: fix php doc * tweak: fix php doc * tweak: fix php doc * tweak: fix php doc * tweak: fix php doc * tweak: fix php doc * tweak: fix php doc * tweak: fix php doc * test: add test from docs --- src/DocumentBinder.php | 19 ++++++--- src/HTMLAttributeBinder.php | 7 ++++ src/HTMLAttributeCollection.php | 2 +- src/ListElement.php | 2 +- src/TableBinder.php | 2 +- test/phpunit/DocumentBinderTest.php | 46 ++++++++++++++++++++- test/phpunit/TestHelper/HTMLPageContent.php | 29 +++++++++++++ 7 files changed, 98 insertions(+), 9 deletions(-) diff --git a/src/DocumentBinder.php b/src/DocumentBinder.php index 881444f..fe7b234 100644 --- a/src/DocumentBinder.php +++ b/src/DocumentBinder.php @@ -159,24 +159,33 @@ public function bindListCallback( } public function cleanupDocument():void { + /** + * @var Attr[] $xpathResult + * @phpstan-ignore varTag.nativeType + */ $xpathResult = $this->document->evaluate( - "//*/@*[starts-with(name(), 'data-bind')] | //*/@*[starts-with(name(), 'data-list')] | //*/@*[starts-with(name(), 'data-template')] | //*/@*[starts-with(name(), 'data-table-key')]" + "//*/@*[starts-with(name(), 'data-bind')] | //*/@*[starts-with(name(), 'data-list')] | //*/@*[starts-with(name(), 'data-template')] | //*/@*[starts-with(name(), 'data-table-key')] | //*/@*[starts-with(name(), 'data-element')]" ); $elementsToRemove = []; - /** @var Attr $item */ foreach($xpathResult as $item) { - if($item->ownerElement->hasAttribute("data-element")) { - array_push($elementsToRemove, $item->ownerElement); + $ownerElement = $item->ownerElement; + if($ownerElement->hasAttribute("data-element")) { + if(!$ownerElement->hasAttribute("data-bound")) { + array_push($elementsToRemove, $ownerElement); + } continue; } - $item->ownerElement->removeAttribute($item->name); + $ownerElement->removeAttribute($item->name); } foreach($this->document->querySelectorAll("[data-element]") as $dataElement) { $dataElement->removeAttribute("data-element"); } + foreach($this->document->querySelectorAll("[data-bound]") as $dataBound) { + $dataBound->removeAttribute("data-bound"); + } foreach($elementsToRemove as $element) { $element->remove(); diff --git a/src/HTMLAttributeBinder.php b/src/HTMLAttributeBinder.php index 64849a9..890647d 100644 --- a/src/HTMLAttributeBinder.php +++ b/src/HTMLAttributeBinder.php @@ -41,6 +41,12 @@ public function bind( foreach($element->attributes as $attrName => $attr) { $attrValue = $attr->value; + if($attrName === "data-element") { + if($attr->value === $key && $value) { + $element->setAttribute("data-bound", ""); + } + } + if(!str_starts_with($attrName, "data-bind")) { continue; } @@ -81,6 +87,7 @@ public function bind( $value, $modifier ); + $element->setAttribute("data-bound", ""); if(!$attr->ownerElement->hasAttribute("data-rebind")) { array_push($attributesToRemove, $attrName); diff --git a/src/HTMLAttributeCollection.php b/src/HTMLAttributeCollection.php index c3dc3df..737b9a3 100644 --- a/src/HTMLAttributeCollection.php +++ b/src/HTMLAttributeCollection.php @@ -7,7 +7,7 @@ class HTMLAttributeCollection { public function find(Element $context):XPathResult { return $context->ownerDocument->evaluate( - "descendant-or-self::*[@*[starts-with(name(), 'data-bind')]]", + "descendant-or-self::*[@*[starts-with(name(), 'data-bind')] or (@data-element and @data-element != '')]", $context ); } diff --git a/src/ListElement.php b/src/ListElement.php index aa06660..d0a08f0 100644 --- a/src/ListElement.php +++ b/src/ListElement.php @@ -10,7 +10,7 @@ class ListElement { const ATTRIBUTE_LIST_PARENT = "data-list-parent"; private string $listItemParentPath; - private null|Node|Element $listItemNextSibling; + private null|Element $listItemNextSibling; private int $insertCount; public function __construct( diff --git a/src/TableBinder.php b/src/TableBinder.php index cac1deb..f4454d8 100644 --- a/src/TableBinder.php +++ b/src/TableBinder.php @@ -187,7 +187,7 @@ public function bindTableData( } } - /** @param array>|array>|array>|array|array> $array */ + /** @param array> | array> | array> | array> | array $array */ public function detectTableDataStructureType(array $array):TableDataStructureType { if(empty($array)) { return TableDataStructureType::NORMALISED; diff --git a/test/phpunit/DocumentBinderTest.php b/test/phpunit/DocumentBinderTest.php index 600f4dc..b126783 100644 --- a/test/phpunit/DocumentBinderTest.php +++ b/test/phpunit/DocumentBinderTest.php @@ -1245,7 +1245,51 @@ public function test_keepsElementWhenBound():void { $sut->setDependencies(...$this->documentBinderDependencies($document)); $sut->bindKeyValue("error", "Example error!"); $sut->cleanupDocument(); - self::assertStringContainsStringIgnoringCase("error", (string)$document); + $errorDiv = $document->querySelector("form>div"); + self::assertNotNull($errorDiv); + self::assertSame("Example error!", $errorDiv->textContent); + } + + public function test_bindElementWithBindValue():void { + $document = new HTMLDocument(HTMLPageContent::HTML_REMOVE_UNBOUND_BIND_VALUE); + $sut = new DocumentBinder($document); + $sut->setDependencies(...$this->documentBinderDependencies($document)); + $sut->bindKeyValue("error", true); + $sut->cleanupDocument(); + $errorDiv = $document->querySelector("form>div"); + self::assertNotNull($errorDiv); + self::assertSame("There has been an error!", $errorDiv->textContent); + } + + public function test_bindElementRemovesMultiple():void { + $document = new HTMLDocument(HTMLPageContent::HTML_ADMIN_PANEL); + $sut = new DocumentBinder($document); + $sut->setDependencies(...$this->documentBinderDependencies($document)); + $sut->bindKeyValue("isAdmin", false); + $sut->cleanupDocument(); + + $panelDiv = $document->querySelector("div.panel"); + self::assertCount(2, $panelDiv->children); + } + + public function test_bindElementRemovesMultiple_doesNotRemoveWithTrue():void { + $document = new HTMLDocument(HTMLPageContent::HTML_ADMIN_PANEL); + $sut = new DocumentBinder($document); + $sut->setDependencies(...$this->documentBinderDependencies($document)); + $sut->bindKeyValue("isAdmin", true); + $sut->cleanupDocument(); + + $panelDiv = $document->querySelector("div.panel"); + self::assertCount(4, $panelDiv->children); + } + + public function test_bindElementIsRemovedWhenNotBound():void { + $document = new HTMLDocument(HTMLPageContent::HTML_REMOVE_UNBOUND_BIND_VALUE); + $sut = new DocumentBinder($document); + $sut->setDependencies(...$this->documentBinderDependencies($document)); + $sut->cleanupDocument(); + $errorDiv = $document->querySelector("form>div"); + self::assertNull($errorDiv); } public function test_bindData_withList_dataBindList():void { diff --git a/test/phpunit/TestHelper/HTMLPageContent.php b/test/phpunit/TestHelper/HTMLPageContent.php index 6ee8104..b395333 100644 --- a/test/phpunit/TestHelper/HTMLPageContent.php +++ b/test/phpunit/TestHelper/HTMLPageContent.php @@ -82,6 +82,16 @@ class HTMLPageContent { HTML; + const HTML_ADMIN_PANEL = << +

You are logged in as username

+

You are an administrator

+ + + +HTML; + + const HTML_DIFFERENT_BIND_PROPERTIES = << Not boundLog in! +HTML; + + const HTML_REMOVE_UNBOUND_BIND_VALUE = << +

Log in to the system

+
+
There has been an error!
+ + + + + +
HTML; const HTML_DATA_BIND_LIST = <<