From bf2938bffb5141742cdc3ecd88b525329432a043 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 14 Jun 2019 18:06:37 +0100 Subject: [PATCH 01/18] Begin sketching out functionality for #54 --- src/Bindable.php | 107 ++++++++++++++++++++++++++++++++++------- src/TemplateParent.php | 1 - 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/src/Bindable.php b/src/Bindable.php index 0bc4a31..4e9a4f3 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -4,31 +4,99 @@ use Gt\Dom\Attr; use Gt\Dom\Element as BaseElement; use DOMNode; -use Gt\Dom\HTMLCollection; -use stdClass; - +use StdClass; +use Gt\Dom\HTMLCollection as BaseHTMLCollection; + +/** + * In WebEngine, all Elements in the DOM are Bindable by default. A Bindable + * Element is a ParentNode that can have data injected into it via this Trait's + * bind* functions. + */ trait Bindable { - public function bind($data, string $templateName = null):void { + /** + * Bind a single key-value-pair within $this Element. + * Elements state their bindable key using the data-bind HTML attribute. + * There may be multiple Elements with the matching attribute, in which + * case they will all have their data set. + */ + public function bindKeyValue( + string $key, + string $value + ):void { /** @var BaseElement $element */ $element = $this; if($element instanceof HTMLDocument) { $element = $element->documentElement; } - $this->injectDataIntoAttributeValues($element, $data); + $data = [ + $key => $value, + ]; - $this->bindExisting($element, $data); - $this->bindTemplates( - $element, - $data, - $templateName - ); + $this->injectDataIntoAttributeValues($element, $data); + $this->injectDataIntoBindProperties($element, $data); $this->cleanBindAttributes($element); } - protected function bindExisting( - DOMNode $parent, + /** + * Bind multiple key-value-pairs within $this Element, calling + * bindKeyValue for each key-value-pair in the iterable $kvp object. + * @see self::bindKeyValue + */ + public function bindData( + iterable $kvp + ):void { + foreach($kvp as $key => $value) { + $this->bindKeyValue($key, $value); + } + } + + /** + * $kvpList is a nested iterable object. The outer iterable contains + * zero or more inner iterables. The inner iterables contain data in the + * form of an iterable key-value-pair array (typically an associative + * array or data object). + * + * For each iteration of the outer iterable object, a new clone will be + * made of the template element with the given name. The cloned element + * will have the inner iterable data bound to it before being added into + * the DOM in the position that it was originally extracted. + * + * TODO: Enforce the following: + * When $templateName is not provided, the data within $kvpList will be + * bound to an element that has a data-template attribute with no value. + * If there are multiple un-named template elements, an exception is + * thrown - in this case, you will need to use bindNestedList + * + * @throws TODO: Name an exception + * @see self::bindNestedList + */ + public function bindList( + iterable $kvpList, + string $templateName = null + ):void { + /** @var BaseElement $element */ + $element = $this; + if($element instanceof HTMLDocument) { + $element = $element->documentElement; + } + + // TODO: Do the looping here. + } + + /** + * When data needs binding to a nested DOM structure, a BindIterator is + * necessary to link each child list with the correct template. + * + * TODO: Implement. + */ + public function bindNestedList(BindIterator $iterator):void { + + } + + protected function injectDataIntoBindProperties( + Element $parent, $data ):void { $childrenWithBindAttribute = $this->getChildrenWithBindAttribute($parent); @@ -100,7 +168,7 @@ protected function bindTemplates( } $newNode = $fragment->insertTemplate($insertInto); - $this->bindExisting($newNode, $row); + $this->injectDataIntoBindProperties($newNode, $row); $this->injectDataIntoAttributeValues( $newNode, $row @@ -123,8 +191,11 @@ protected function setData(BaseElement $element, $data):void { foreach($element->attributes as $attr) { $matches = []; - if(!preg_match("/(?:data-bind:)(.+)/", - $attr->name,$matches)) { + if(!preg_match( + "/(?:data-bind:)(.+)/", + $attr->name, + $matches) + ) { continue; } $bindProperty = $matches[1]; @@ -179,7 +250,7 @@ protected function handlePropertyData( } protected function injectDataIntoAttributeValues( - DOMNode $element, + BaseElement $element, $data ):void { if(is_array($data)) { @@ -292,7 +363,7 @@ protected function getTemplateNamesForElement(BaseElement $element):array { return $templateNames; } - protected function getChildrenWithBindAttribute(DOMNode $parent):HTMLCollection { + protected function getChildrenWithBindAttribute(BaseElement $parent):BaseHTMLCollection { return $parent->xPath( "descendant-or-self::*[@*[starts-with(name(), 'data-bind')]]" ); diff --git a/src/TemplateParent.php b/src/TemplateParent.php index b6661cc..23b04fb 100644 --- a/src/TemplateParent.php +++ b/src/TemplateParent.php @@ -6,7 +6,6 @@ use Gt\Dom\Element as BaseElement; trait TemplateParent { - public function extractTemplates():int { $i = null; /** @var HTMLCollection $templateElementList */ From 9481dc8c4984341e93f74f2898603b7ff1dc42a1 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 14 Jun 2019 23:33:09 +0100 Subject: [PATCH 02/18] Begin extracting bind functions for #54 --- src/Attr.php | 16 + src/Bindable.php | 316 +++++++++---- src/HTMLCollection.php | 4 +- src/HTMLDocument.php | 47 ++ src/ParentNode.php | 9 +- test/unit/BindableTest.php | 910 ++++++++++++++++++++----------------- 6 files changed, 794 insertions(+), 508 deletions(-) diff --git a/src/Attr.php b/src/Attr.php index bea4f63..e2e7a7c 100644 --- a/src/Attr.php +++ b/src/Attr.php @@ -1,6 +1,22 @@ documentElement; - } - - $data = [ - $key => $value, - ]; - - $this->injectDataIntoAttributeValues($element, $data); - $this->injectDataIntoBindProperties($element, $data); - - $this->cleanBindAttributes($element); + $this->injectBoundProperty($key, $value); +// $this->injectAttributePlaceholder($key, $value); } /** @@ -95,44 +83,239 @@ public function bindNestedList(BindIterator $iterator):void { } - protected function injectDataIntoBindProperties( - Element $parent, - $data + /** + * Within the current element, iterate all children that have a + * matching data-bind:* attribute, and inject the provided $value + * into the according property value. + */ + protected function injectBoundProperty( + string $key, + string $value ):void { - $childrenWithBindAttribute = $this->getChildrenWithBindAttribute($parent); + $children = $this->getChildrenWithBindAttribute(); + + foreach($children as $child) { + foreach($child->attributes as $attr) { + /** @var Attr $attr */ +// Skip attributes that do not have a bindProperty set (the text that comes after +// the colon in data-bind:* + $matches = []; + if(!preg_match( + "/(?:data-bind:)(?P.+)/", + $attr->name, + $matches + )) { + continue; + } - foreach($childrenWithBindAttribute as $element) { - $this->setData($element, $data); - } + $element = $attr->ownerElement; + $propertyToSet = $this->getPropertyToSet($attr); + $attr->ownerDocument->storeBoundAttribute($attr); + +// Skip attributes whose value does not equal the key that we are setting. + if($propertyToSet !== $key) { + continue; + } - $this->bindAttributes($parent, $data); +// The "class" property behaves differently to others, as it is represented by +// a StringMap rather than a single value. + if($propertyToSet === "class") { + $this->setClassValue( + $element, + $value + ); + } + else { + $this->setPropertyValue( + $element, + $matches["bindProperty"], + $value + ); + } + } + } } - protected function bindAttributes(BaseElement $element, $data):void { - foreach($element->attributes as $attr) { - preg_match( - "/{(.+)}/", - $attr->value, - $matches + /** + * The property that is to be bound can reference another property's + * value, using the @ syntax. For example, data-bind:text="@id" will + * bind the element's text content with the data value with the key + * of the element's id attribute value. + */ + protected function getPropertyToSet( + BaseAttr $attr + ):string { + $propertyToSet = $attr->value; + + if($propertyToSet[0] === "@") { + $lookupAttribute = substr($propertyToSet, 1); + $propertyToSet = $attr->ownerElement->getAttribute( + $lookupAttribute ); - if(empty($matches)) { - continue; + + if(is_null($propertyToSet)) { + throw new BoundAttributeDoesNotExistException( + $lookupAttribute + ); } + } - list($placeholder, $dataKey) = $matches; + return $propertyToSet; + } - if(!isset($data[$dataKey])) { - continue; + protected function injectAttributePlaceholder( + string $key, + string $value + ):void { + /** @var BaseElement $element */ + $element = $this; + if($element instanceof HTMLDocument) { + $element = $element->documentElement; + } + + foreach($element->xPath("//*[@*[contains(.,'{')]]") + as $elementWithBraceInAttributeValue) { + foreach($elementWithBraceInAttributeValue->attributes + as $attr) { + preg_match_all( + "/{([^}]+)}/", + $attr->value, + $matches + ); + + if(empty($matches[0])) { + continue; + } + + foreach($matches[0] as $i => $match) { + $value = str_replace( + $match, + "{$key}", + $value + ); + + $attr->ownerElement->setAttribute( + $attr->name, + $value + ); + } } + } + } - $attr->value = str_replace( - $placeholder, - $data[$dataKey], - $attr->value - ); + protected function setClassValue( + BaseElement $element, +// string $key, // TODO: Do we need this??? I Don't think so. + string $value + ):void { + $classList = explode(" ", $attr->value); + foreach($classList as $class) { + + } + } + + protected function setPropertyValue( + BaseElement $element, + string $bindProperty, + string $value + ):void { + switch($bindProperty) { + case "html": + case "innerhtml": + case "innerHtml": + case "innerHTML": + $element->innerHTML = $value; + break; + + case "text": + case "innertext": + case "innerText": + $element->innerText = $value; + break; + default: + $element->setAttribute($bindProperty, $value); } } +// protected function injectDataIntoBindProperties( +// Element $parent, +// $data +// ):void { +// $childrenWithBindAttribute = $this->getChildrenWithBindAttribute($parent); +// +// foreach($childrenWithBindAttribute as $element) { +// $this->setData($element, $data); +// } +// +// $this->bindAttributes($parent, $data); +// } + +// protected function injectDataIntoAttributeValues( +// BaseElement $element, +// $data +// ):void { +// if(is_array($data)) { +// $data = (object)$data; +// } +// +// foreach($element->xPath("//*[@*[contains(.,'{')]]") +// as $elementWithBraceInAttributeValue) { +// foreach($elementWithBraceInAttributeValue->attributes as $attr) { +// preg_match_all( +// "/{([^}]+)}/", +// $attr->value, +// $matches +// ); +// +// if(empty($matches[0])) { +// continue; +// } +// +// foreach($matches[0] as $i => $match) { +// $key = $matches[1][$i]; +// +// if(!isset($data->{$key})) { +// continue; +// } +// +// +// $value = str_replace( +// $match, +// $data->{$key}, +// $attr->value +// ); +// +// $attr->ownerElement->setAttribute($attr->name, $value); +// } +// } +// } +// } + +// protected function bindAttributes(BaseElement $element, $data):void { +// foreach($element->attributes as $attr) { +// preg_match( +// "/{(.+)}/", +// $attr->value, +// $matches +// ); +// if(empty($matches)) { +// continue; +// } +// +// list($placeholder, $dataKey) = $matches; +// +// if(!isset($data[$dataKey])) { +// continue; +// } +// +// $attr->value = str_replace( +// $placeholder, +// $data[$dataKey], +// $attr->value +// ); +// } +// } + protected function bindTemplates( DOMNode $element, $data, @@ -219,7 +402,7 @@ protected function setData(BaseElement $element, $data):void { } protected function handlePropertyData( - Attr $attr, + BaseAttr $attr, string $bindProperty, BaseElement $element, $data @@ -249,49 +432,8 @@ protected function handlePropertyData( } } - protected function injectDataIntoAttributeValues( - BaseElement $element, - $data - ):void { - if(is_array($data)) { - $data = (object)$data; - } - - foreach($element->xPath("//*[@*[contains(.,'{')]]") - as $elementWithBraceInAttributeValue) { - foreach($elementWithBraceInAttributeValue->attributes as $attr) { - preg_match_all( - "/{([^}]+)}/", - $attr->value, - $matches - ); - - if(empty($matches[0])) { - continue; - } - - foreach($matches[0] as $i => $match) { - $key = $matches[1][$i]; - - if(!isset($data->{$key})) { - continue; - } - - - $value = str_replace( - $match, - $data->{$key}, - $attr->value - ); - - $attr->ownerElement->setAttribute($attr->name, $value); - } - } - } - } - protected function handleClassData( - Attr $attr, + BaseAttr $attr, BaseElement $element, $data ):void { @@ -363,8 +505,14 @@ protected function getTemplateNamesForElement(BaseElement $element):array { return $templateNames; } - protected function getChildrenWithBindAttribute(BaseElement $parent):BaseHTMLCollection { - return $parent->xPath( + protected function getChildrenWithBindAttribute():BaseHTMLCollection { + /** @var BaseElement $element */ + $element = $this; + if($element instanceof HTMLDocument) { + $element = $element->documentElement; + } + + return $element->xPath( "descendant-or-self::*[@*[starts-with(name(), 'data-bind')]]" ); } diff --git a/src/HTMLCollection.php b/src/HTMLCollection.php index c7ae84a..20e3282 100644 --- a/src/HTMLCollection.php +++ b/src/HTMLCollection.php @@ -1,4 +1,6 @@ componentDirectory = $componentDirectory; $this->templateFragmentMap = []; + $this->boundAttributeList = []; } public function getComponentDirectory():string { @@ -104,4 +108,47 @@ public function createTemplateFragment(DOMElement $templateElement):BaseDocument return $fragment; } + + public function storeBoundAttribute(BaseAttr $attr) { + $this->boundAttributeList []= $attr; + } + + public function validateBinds():void { + $allBindableElements = $this->getAllBindableElements(); + + foreach($allBindableElements as $element) { + foreach($element->attributes as $attr) { + if(strpos($attr->name, "data-bind") !== 0) { + continue; + } + + if(in_array($attr, $this->boundAttributeList)) { + throw new BoundDataNotSetException( + $attr->value + ); + } + } + } + } + + public function removeBinds():void { + $allBindableElements = $this->getAllBindableElements(); + + foreach($allBindableElements as $element) { + foreach($element->attributes as $attr) { + /** @var \Gt\Dom\Attr $attr */ + if(strpos($attr->name, "data-bind") !== 0) { + continue; + } + + $attr->remove(); + } + } + } + + protected function getAllBindableElements():BaseHTMLCollection { + return $this->documentElement->xPath( + "descendant-or-self::*[@*[starts-with(name(), 'data-bind')]]" + ); + } } \ No newline at end of file diff --git a/src/ParentNode.php b/src/ParentNode.php index 4848ff5..4105eed 100644 --- a/src/ParentNode.php +++ b/src/ParentNode.php @@ -2,6 +2,7 @@ namespace Gt\DomTemplate; use DOMNode; +use Gt\Dom\HTMLCollection; /** * @property-read HTMLCollection $children A live HTMLCollection containing all @@ -22,9 +23,9 @@ * @method Node|Element replaceChild(DOMNode $newNode, DOMNode $oldNode) * * @method Element|null querySelector(string $selector) - * @method Element[] querySelectorAll(string $selector) - * @method Element[] css(string $selector, string $prefix = "descendant-or-self::") - * @method Element[] xPath(string $selector) - * @method Element[] getElementsByTagName(string $tag) + * @method HTMLCollection querySelectorAll(string $selector) + * @method HTMLCollection css(string $selector, string $prefix = "descendant-or-self::") + * @method HTMLCollection xPath(string $selector) + * @method HTMLCollection getElementsByTagName(string $tag) */ trait ParentNode {} \ No newline at end of file diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index b511a37..6c57e02 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -15,11 +15,35 @@ public function testBindMethodAvailable() { $outputTo = $document->querySelector("dl"); self::assertTrue( - method_exists($document, "bind"), + method_exists($document, "bindKeyValue"), "HTMLDocument is not bindable" ); self::assertTrue( - method_exists($outputTo, "bind"), + method_exists($document, "bindData"), + "HTMLDocument is not bindable" + ); + self::assertTrue( + method_exists($document, "bindList"), + "HTMLDocument is not bindable" + ); + self::assertTrue( + method_exists($document, "bindNestedList"), + "HTMLDocument is not bindable" + ); + self::assertTrue( + method_exists($outputTo, "bindKeyValue"), + "Template container element (dl) is not bindable" + ); + self::assertTrue( + method_exists($outputTo, "bindData"), + "Template container element (dl) is not bindable" + ); + self::assertTrue( + method_exists($outputTo, "bindList"), + "Template container element (dl) is not bindable" + ); + self::assertTrue( + method_exists($outputTo, "bindNestedList"), "Template container element (dl) is not bindable" ); } @@ -30,16 +54,41 @@ public function testBindMethodOnTemplateElement() { $template = $document->getTemplate("title-definition"); self::assertTrue( - method_exists($template, "bind"), + method_exists($template, "bindKeyValue"), "Template element is not bindable" ); + self::assertTrue( + method_exists($template, "bindData"), + "Template element is not bindable" + ); + self::assertTrue( + method_exists($template, "bindList"), + "Template element is not bindable" + ); + self::assertTrue( + method_exists($template, "bindNestedList"), + "Template element is not bindable" + ); + } + + public function testBindKeyValueExistingElements() { + $document = new HTMLDocument(Helper::HTML_NO_TEMPLATES); + $name = "Winston Smith"; + $age = 39; + $document->bindKeyValue("name", $name); + $document->bindKeyValue("age", $age); + + $boundDataTestElement = $document->querySelector(".bound-data-test"); + $spanChildren = $boundDataTestElement->querySelectorAll("span"); + self::assertEquals($name, $spanChildren[0]->innerText); + self::assertEquals($age, $spanChildren[1]->innerText); } - public function testBindExistingElements() { + public function testBindDataExistingElements() { $document = new HTMLDocument(Helper::HTML_NO_TEMPLATES); $name = "Winston Smith"; $age = 39; - $document->bind([ + $document->bindData([ "name" => $name, "age" => $age, ]); @@ -50,11 +99,11 @@ public function testBindExistingElements() { self::assertEquals($age,$spanChildren[1]->innerText); } - public function testBindOnUnknownProperty() { + public function testBindDataOnUnknownProperty() { $document = new HTMLDocument(Helper::HTML_BIND_UNKNOWN_PROPERTY); $name = "Winston Smith"; $age = 39; - $document->bind([ + $document->bindData([ "name" => $name, "age" => $age, ]); @@ -66,11 +115,11 @@ public function testBindOnUnknownProperty() { self::assertEquals($age, $test2->textContent); } - public function testBindAttributeLookup() { + public function testBindDataAttributeLookup() { $document = new HTMLDocument(Helper::HTML_NO_TEMPLATES_BIND_ATTR); $name = "Julia Dixon"; $age = 26; - $document->bind([ + $document->bindData([ "name" => $name, "age" => $age, ]); @@ -81,7 +130,7 @@ public function testBindAttributeLookup() { self::assertEquals($age,$spanChildren[1]->innerText); } - public function testBindAttributeNoMatch() { + public function testBindDataAttributeNoMatch() { self::expectException(BoundAttributeDoesNotExistException::class); $document = new HTMLDocument(Helper::HTML_NO_TEMPLATES_BIND_ATTR); $name = "Julia Dixon"; @@ -90,7 +139,7 @@ public function testBindAttributeNoMatch() { "data-bind:text", "@does-not-exist" ); - $document->bind([ + $document->bindData([ "name" => $name, "age" => $age, ]); @@ -105,424 +154,447 @@ public function testBindDataNoMatch() { "data-bind:text", "nothing" ); - $document->bind([ + $document->bindData([ "name" => $name, "age" => $age, ]); - } - - public function testTemplateTodoList() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST); - $todoData = [ - ["id" => 1, "title" => "Write tests", "complete" => true], - ["id" => 2, "title" => "Implement features", "complete" => false], - ["id" => 3, "title" => "Pass tests", "complete" => false], - ]; - $document->extractTemplates(); - $todoListElement = $document->getElementById("todo-list"); - $todoListElement->bind($todoData); - - $liChildren = $todoListElement->querySelectorAll("li"); - - self::assertCount( - count($todoData), - $liChildren, - "There should be the same amount of li elements as there are rows of data" - ); - - foreach($todoData as $i => $row) { - self::assertStringContainsString( - $row["title"], - $liChildren[$i]->innerHTML - ); - } - } - - public function testBoundTemplatesCleanedUpAfterAdding() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST); - $todoData = [ - ["id" => 1, "title" => "Write tests", "complete" => true], - ["id" => 2, "title" => "Implement features", "complete" => false], - ["id" => 3, "title" => "Pass tests", "complete" => false], - ]; - $document->extractTemplates(); - $todoListElement = $document->getElementById("todo-list"); - $todoListElement->bind($todoData); - - self::assertStringContainsString( - "Implement features", - $todoListElement->innerHTML - ); - self::assertStringNotContainsString( - "data-bind", - $todoListElement->innerHTML - ); - } - - public function testBindWithInlineNamedTemplate() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST_INLINE_NAMED_TEMPLATE); - $todoData = [ - ["id" => 1, "title" => "Write tests", "complete" => true], - ["id" => 2, "title" => "Implement features", "complete" => false], - ["id" => 3, "title" => "Pass tests", "complete" => false], - ]; - $document->extractTemplates(); - $todoListElement = $document->getElementById("todo-list"); - $todoListElement->bind($todoData); - - self::assertStringContainsString( - "Implement features", - $todoListElement->innerHTML - ); - self::assertStringNotContainsString( - "data-bind", - $todoListElement->innerHTML - ); - } - - public function testBindWithInlineNamedTemplateWhenAnotherTemplateExists() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST_INLINE_NAMED_TEMPLATE_DOUBLE); - $todoData = [ - ["id" => 1, "title" => "Write tests", "complete" => true], - ["id" => 2, "title" => "Implement features", "complete" => false], - ["id" => 3, "title" => "Pass tests", "complete" => false], - ]; - $document->extractTemplates(); - - $todoListElement = $document->getElementById("todo-list"); - $todoListElement->bind($todoData); - self::assertStringContainsString( - "Implement features", - $todoListElement->innerHTML - ); - self::assertStringNotContainsString( - "Use the other template instead!", - $todoListElement->innerHTML - ); - - $todoListElement = $document->getElementById("todo-list-2"); - $todoListElement->bind($todoData, "todo-list-item"); - - self::assertStringContainsString( - "Implement features", - $todoListElement->innerHTML - ); - self::assertStringNotContainsString( - "Use the other template instead!", - $todoListElement->innerHTML - ); - } - - public function testBindWithNonOptionalKey() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST); - $todoData = [ - ["title" => "Write tests", "complete" => true], - ["title" => "Implement features", "complete" => false], - ["title" => "Pass tests", "complete" => false], - ]; - $document->extractTemplates(); - $todoListElement = $document->getElementById("todo-list"); - - self::expectException(BoundDataNotSetException::class); - $todoListElement->bind($todoData); - } - - public function testBindWithOptionalKey() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST_OPTIONAL_ID); - $todoData = [ - ["title" => "Write tests", "complete" => true], - ["title" => "Implement features", "complete" => false], - ["title" => "Pass tests", "complete" => false], - ]; - $document->extractTemplates(); - $todoListElement = $document->getElementById("todo-list"); - - $todoListElement->bind($todoData); - $items = $todoListElement->querySelectorAll("li"); - self::assertCount(3, $items); - self::assertEquals( - "Implement features", - $items[1]->querySelector("input[name=title]")->value - ); - self::assertNull( - $items[1]->querySelector("input[name=id]")->value - ); - } - - public function testBindClass() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS); - $todoData = [ - ["id" => 1, "title" => "Write tests", "complete" => true], - ["id" => 2, "title" => "Implement features", "complete" => false], - ["id" => 3, "title" => "Pass tests", "complete" => false], - ]; - $document->extractTemplates(); - $todoListElement = $document->getElementById("todo-list"); - - $todoListElement->bind($todoData); - $items = $todoListElement->querySelectorAll("li"); - - foreach($todoData as $i => $todoDatum) { - self::assertEquals( - $todoDatum["complete"], - $items[$i]->classList->contains("complete") - ); - - self::assertTrue($items[$i]->classList->contains("existing-class")); - } + $document->validateBinds(); } - public function testBindClassColon() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS_COLON); - $todoData = [ - ["id" => 1, "title" => "Write tests", "dateTimeCompleted" => "2018-07-01 19:46:00"], - ["id" => 2, "title" => "Implement features", "dateTimeCompleted" => null], - ["id" => 3, "title" => "Pass tests", "dateTimeCompleted" => "2018-07-01 19:49:00"], - ]; - $document->extractTemplates(); - $todoListElement = $document->getElementById("todo-list"); - - $todoListElement->bind($todoData); - $items = $todoListElement->querySelectorAll("li"); - - foreach($todoData as $i => $todoDatum) { - $completed = (bool)$todoDatum["dateTimeCompleted"]; - self::assertEquals( - $completed, - $items[$i]->classList->contains("complete") - ); - - self::assertTrue($items[$i]->classList->contains("existing-class")); - } - } - - public function testBindClassColonMultiple() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS_COLON_MULTIPLE); - $todoData = [ - [ - "id" => 1, - "title" => "Write tests", - "dateTimeCompleted" => "2018-07-01 19:46:00", - "dateTimeDeleted" => null, - ], - [ - "id" => 2, - "title" => "Implement features", - "dateTimeCompleted" => null, - "dateTimeDeleted" => "2018-07-01 19:54:00", - ], - [ - "id" => 3, - "title" => "Pass tests", - "dateTimeCompleted" => "2018-07-01 19:49:00", - "dateTimeDeleted" => null, - ], - ]; - $document->extractTemplates(); - $todoListElement = $document->getElementById("todo-list"); - - $todoListElement->bind($todoData); - $items = $todoListElement->querySelectorAll("li"); - - foreach($todoData as $i => $todoDatum) { - self::assertTrue($items[$i]->classList->contains("existing-class")); - - $completed = (bool)$todoDatum["dateTimeCompleted"]; - self::assertEquals( - $completed, - $items[$i]->classList->contains("complete") - ); - - $deleted = (bool)$todoDatum["dateTimeDeleted"]; - self::assertEquals( - $deleted, - $items[$i]->classList->contains("deleted") - ); - } - } - - public function testBindWithObjectData() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS_COLON_MULTIPLE); - $todoData = [ - [ - "id" => 1, - "title" => "Write tests", - "dateTimeCompleted" => "2018-07-01 19:46:00", - "dateTimeDeleted" => null, - ], - [ - "id" => 2, - "title" => "Implement features", - "dateTimeCompleted" => null, - "dateTimeDeleted" => "2018-07-01 19:54:00", - ], - [ - "id" => 3, - "title" => "Pass tests", - "dateTimeCompleted" => "2018-07-01 19:49:00", - "dateTimeDeleted" => null, - ], - ]; - - $todoObjData = []; - - foreach($todoData as $todo) { - $obj = new StdClass(); - foreach($todo as $key => $value) { - $obj->$key = $value; - } - - $todoObjData []= $obj; - } - - $document->extractTemplates(); - $todoListElement = $document->getElementById("todo-list"); - - $todoListElement->bind($todoObjData); - $items = $todoListElement->querySelectorAll("li"); - - foreach($todoObjData as $i => $todo) { - self::assertTrue($items[$i]->classList->contains("existing-class")); - - $completed = (bool)$todo->dateTimeCompleted; - self::assertEquals( - $completed, - $items[$i]->classList->contains("complete") - ); - - $deleted = (bool)$todo->dateTimeDeleted; - self::assertEquals( - $deleted, - $items[$i]->classList->contains("deleted") - ); - } - } - -// For issue #52: - public function testBindingDataWithBindableParentElement() { - $document = new HTMLDocument(Helper::HTML_PARENT_HAS_DATA_BIND_ATTR); - $document->extractTemplates(); - - $data = [ - ["example-key" => "example-value-1","target-key" => "target-value-1"], - ["example-key" => "example-value-2","target-key" => "target-value-2"], - ["example-key" => "example-value-3","target-key" => "target-value-3"], - ]; - - $exception = null; - - foreach($data as $row) { - $t = $document->getTemplate("target-template"); - try { - $t->bind($row); - } - catch(DomTemplateException $exception) {} - - $t->insertTemplate(); - } - - self::assertNull($exception); - } - - public function testBindingDataWithBindableParentElementDoesNotAddMoreNodes() { - $document = new HTMLDocument(Helper::HTML_PARENT_HAS_DATA_BIND_ATTR); - $document->extractTemplates(); - - $document->querySelector("label>span")->bind( - ["outside-scope" => "example content"] - ); - - $data = [ - ["example-key" => "example-value-1","target-key" => "target-value-1"], - ["example-key" => "example-value-2","target-key" => "target-value-2"], - ["example-key" => "example-value-3","target-key" => "target-value-3"], - ]; - - $exception = null; - - try { - $document->querySelector("ul")->bind($data); - } - catch(DomTemplateException $exception) {} - self::assertNull($exception); - - self::assertCount(3, $document->querySelectorAll("ul li")); - self::assertEquals( - "example content", - $document->querySelector("label>span")->textContent + public function testBindDataRemoved() { + $document = new HTMLDocument(Helper::HTML_NO_TEMPLATES); + $name = "Julia Dixon"; + $age = 26; + $document->querySelector("span")->setAttribute( + "data-bind:text", + "nothing" ); - } - - public function testMultipleListBindSameDocument() { - $document = new HTMLDocument(Helper::HTML_DOUBLE_BINDABLE_LIST); - $document->extractTemplates(); - - $oneToTen = []; - for($i = 1; $i <= 10; $i++) { - $oneToTen []= [ - "i" => $i, - ]; - } - - $document->querySelector(".area-1 ul")->bind($oneToTen); - $document->querySelector("h1")->bind([ - "name" => "Example Name", - ]); - - $startingNumber = rand(100, 1000); - $document->querySelector(".area-2 p")->bind([ - "start" => $startingNumber, + $document->bindData([ + "name" => $name, + "age" => $age, ]); - for($i = $startingNumber; $i <= $startingNumber + 10; $i++) { - $t = $document->getTemplate("dynamic-list-item"); - $t->bind(["i" => $i]); - $t->insertTemplate(); - } - - foreach($document->querySelectorAll(".area-1 ul li") as $i => $li) { - $number = $i + 1; - self::assertStringContainsString($number, $li->textContent); - } - - foreach($document->querySelectorAll(".area-2 ul li") as $i => $li) { - $number = $i + $startingNumber; - self::assertStringContainsString($number, $li->textContent); - } + $document->removeBinds(); - self::assertStringContainsString("Example Name", $document->querySelector("h1")->textContent); - self::assertStringContainsString($startingNumber, $document->querySelector(".area-2 p")->textContent); + $boundDataTestElement = $document->querySelector(".bound-data-test"); + $spanChildren = $boundDataTestElement->querySelectorAll("span"); + self::assertFalse($spanChildren[0]->hasAttribute("data-bind:text")); + self::assertFalse($spanChildren[1]->hasAttribute("data-bind:text")); } - public function testBindingTodoListFromObject() { - $document = new HTMLDocument(Helper::HTML_TODO_LIST); - $document->extractTemplates(); - - $todoItems = [ - "Go to shops", - "Buy pizza", - "Eat pizza", - "Sleep", - ]; - - $list = new TodoListExampleObject($todoItems); - - $container = $document->getElementById("todo-list"); - $container->bind($list); - - $liNodes = $container->querySelectorAll("li"); - self::assertCount(4, $liNodes); - - foreach($todoItems as $id => $name) { - self::assertEquals( - $id, - $liNodes[$id]->querySelector("[name='id']")->value - ); - self::assertEquals( - $name, - $liNodes[$id]->querySelector("[name='title']")->value - ); - } - } +// public function testBindListTemplateTodoList() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST); +// $todoData = [ +// ["id" => 1, "title" => "Write tests", "complete" => true], +// ["id" => 2, "title" => "Implement features", "complete" => false], +// ["id" => 3, "title" => "Pass tests", "complete" => false], +// ]; +// $document->extractTemplates(); +// $todoListElement = $document->getElementById("todo-list"); +// $todoListElement->bindList($todoData); +// +// $liChildren = $todoListElement->querySelectorAll("li"); +// +// self::assertCount( +// count($todoData), +// $liChildren, +// "There should be the same amount of li elements as there are rows of data" +// ); +// +// foreach($todoData as $i => $row) { +// self::assertStringContainsString( +// $row["title"], +// $liChildren[$i]->innerHTML +// ); +// } +// } +// +// public function testBindListTemplatesCleanedUpAfterAdding() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST); +// $todoData = [ +// ["id" => 1, "title" => "Write tests", "complete" => true], +// ["id" => 2, "title" => "Implement features", "complete" => false], +// ["id" => 3, "title" => "Pass tests", "complete" => false], +// ]; +// $document->extractTemplates(); +// $todoListElement = $document->getElementById("todo-list"); +// $todoListElement->bindList($todoData); +// +// self::assertStringContainsString( +// "Implement features", +// $todoListElement->innerHTML +// ); +// self::assertStringNotContainsString( +// "data-bind", +// $todoListElement->innerHTML +// ); +// } +// +// public function testBindListWithInlineNamedTemplate() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST_INLINE_NAMED_TEMPLATE); +// $todoData = [ +// ["id" => 1, "title" => "Write tests", "complete" => true], +// ["id" => 2, "title" => "Implement features", "complete" => false], +// ["id" => 3, "title" => "Pass tests", "complete" => false], +// ]; +// $document->extractTemplates(); +// $todoListElement = $document->getElementById("todo-list"); +// $todoListElement->bindList($todoData); +// +// self::assertStringContainsString( +// "Implement features", +// $todoListElement->innerHTML +// ); +// self::assertStringNotContainsString( +// "data-bind", +// $todoListElement->innerHTML +// ); +// } + +// public function testBindListWithInlineNamedTemplateWhenAnotherTemplateExists() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST_INLINE_NAMED_TEMPLATE_DOUBLE); +// $todoData = [ +// ["id" => 1, "title" => "Write tests", "complete" => true], +// ["id" => 2, "title" => "Implement features", "complete" => false], +// ["id" => 3, "title" => "Pass tests", "complete" => false], +// ]; +// $document->extractTemplates(); +// +// $todoListElement = $document->getElementById("todo-list"); +// $todoListElement->bindList($todoData); +// self::assertStringContainsString( +// "Implement features", +// $todoListElement->innerHTML +// ); +// self::assertStringNotContainsString( +// "Use the other template instead!", +// $todoListElement->innerHTML +// ); +// +// $todoListElement = $document->getElementById("todo-list-2"); +// $todoListElement->bind($todoData, "todo-list-item"); +// +// self::assertStringContainsString( +// "Implement features", +// $todoListElement->innerHTML +// ); +// self::assertStringNotContainsString( +// "Use the other template instead!", +// $todoListElement->innerHTML +// ); +// } +// +// public function testBindWithNonOptionalKey() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST); +// $todoData = [ +// ["title" => "Write tests", "complete" => true], +// ["title" => "Implement features", "complete" => false], +// ["title" => "Pass tests", "complete" => false], +// ]; +// $document->extractTemplates(); +// $todoListElement = $document->getElementById("todo-list"); +// +// self::expectException(BoundDataNotSetException::class); +// $todoListElement->bind($todoData); +// } +// +// public function testBindWithOptionalKey() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST_OPTIONAL_ID); +// $todoData = [ +// ["title" => "Write tests", "complete" => true], +// ["title" => "Implement features", "complete" => false], +// ["title" => "Pass tests", "complete" => false], +// ]; +// $document->extractTemplates(); +// $todoListElement = $document->getElementById("todo-list"); +// +// $todoListElement->bind($todoData); +// $items = $todoListElement->querySelectorAll("li"); +// self::assertCount(3, $items); +// +// self::assertEquals( +// "Implement features", +// $items[1]->querySelector("input[name=title]")->value +// ); +// self::assertNull( +// $items[1]->querySelector("input[name=id]")->value +// ); +// } +// +// public function testBindClass() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS); +// $todoData = [ +// ["id" => 1, "title" => "Write tests", "complete" => true], +// ["id" => 2, "title" => "Implement features", "complete" => false], +// ["id" => 3, "title" => "Pass tests", "complete" => false], +// ]; +// $document->extractTemplates(); +// $todoListElement = $document->getElementById("todo-list"); +// +// $todoListElement->bind($todoData); +// $items = $todoListElement->querySelectorAll("li"); +// +// foreach($todoData as $i => $todoDatum) { +// self::assertEquals( +// $todoDatum["complete"], +// $items[$i]->classList->contains("complete") +// ); +// +// self::assertTrue($items[$i]->classList->contains("existing-class")); +// } +// } +// +// public function testBindClassColon() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS_COLON); +// $todoData = [ +// ["id" => 1, "title" => "Write tests", "dateTimeCompleted" => "2018-07-01 19:46:00"], +// ["id" => 2, "title" => "Implement features", "dateTimeCompleted" => null], +// ["id" => 3, "title" => "Pass tests", "dateTimeCompleted" => "2018-07-01 19:49:00"], +// ]; +// $document->extractTemplates(); +// $todoListElement = $document->getElementById("todo-list"); +// +// $todoListElement->bind($todoData); +// $items = $todoListElement->querySelectorAll("li"); +// +// foreach($todoData as $i => $todoDatum) { +// $completed = (bool)$todoDatum["dateTimeCompleted"]; +// self::assertEquals( +// $completed, +// $items[$i]->classList->contains("complete") +// ); +// +// self::assertTrue($items[$i]->classList->contains("existing-class")); +// } +// } +// +// public function testBindClassColonMultiple() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS_COLON_MULTIPLE); +// $todoData = [ +// [ +// "id" => 1, +// "title" => "Write tests", +// "dateTimeCompleted" => "2018-07-01 19:46:00", +// "dateTimeDeleted" => null, +// ], +// [ +// "id" => 2, +// "title" => "Implement features", +// "dateTimeCompleted" => null, +// "dateTimeDeleted" => "2018-07-01 19:54:00", +// ], +// [ +// "id" => 3, +// "title" => "Pass tests", +// "dateTimeCompleted" => "2018-07-01 19:49:00", +// "dateTimeDeleted" => null, +// ], +// ]; +// $document->extractTemplates(); +// $todoListElement = $document->getElementById("todo-list"); +// +// $todoListElement->bind($todoData); +// $items = $todoListElement->querySelectorAll("li"); +// +// foreach($todoData as $i => $todoDatum) { +// self::assertTrue($items[$i]->classList->contains("existing-class")); +// +// $completed = (bool)$todoDatum["dateTimeCompleted"]; +// self::assertEquals( +// $completed, +// $items[$i]->classList->contains("complete") +// ); +// +// $deleted = (bool)$todoDatum["dateTimeDeleted"]; +// self::assertEquals( +// $deleted, +// $items[$i]->classList->contains("deleted") +// ); +// } +// } +// +// public function testBindWithObjectData() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS_COLON_MULTIPLE); +// $todoData = [ +// [ +// "id" => 1, +// "title" => "Write tests", +// "dateTimeCompleted" => "2018-07-01 19:46:00", +// "dateTimeDeleted" => null, +// ], +// [ +// "id" => 2, +// "title" => "Implement features", +// "dateTimeCompleted" => null, +// "dateTimeDeleted" => "2018-07-01 19:54:00", +// ], +// [ +// "id" => 3, +// "title" => "Pass tests", +// "dateTimeCompleted" => "2018-07-01 19:49:00", +// "dateTimeDeleted" => null, +// ], +// ]; +// +// $todoObjData = []; +// +// foreach($todoData as $todo) { +// $obj = new StdClass(); +// foreach($todo as $key => $value) { +// $obj->$key = $value; +// } +// +// $todoObjData []= $obj; +// } +// +// $document->extractTemplates(); +// $todoListElement = $document->getElementById("todo-list"); +// +// $todoListElement->bind($todoObjData); +// $items = $todoListElement->querySelectorAll("li"); +// +// foreach($todoObjData as $i => $todo) { +// self::assertTrue($items[$i]->classList->contains("existing-class")); +// +// $completed = (bool)$todo->dateTimeCompleted; +// self::assertEquals( +// $completed, +// $items[$i]->classList->contains("complete") +// ); +// +// $deleted = (bool)$todo->dateTimeDeleted; +// self::assertEquals( +// $deleted, +// $items[$i]->classList->contains("deleted") +// ); +// } +// } +// +//// For issue #52: +// public function testBindingDataWithBindableParentElement() { +// $document = new HTMLDocument(Helper::HTML_PARENT_HAS_DATA_BIND_ATTR); +// $document->extractTemplates(); +// +// $data = [ +// ["example-key" => "example-value-1","target-key" => "target-value-1"], +// ["example-key" => "example-value-2","target-key" => "target-value-2"], +// ["example-key" => "example-value-3","target-key" => "target-value-3"], +// ]; +// +// $exception = null; +// +// foreach($data as $row) { +// $t = $document->getTemplate("target-template"); +// try { +// $t->bind($row); +// } +// catch(DomTemplateException $exception) {} +// +// $t->insertTemplate(); +// } +// +// self::assertNull($exception); +// } +// +// public function testBindingDataWithBindableParentElementDoesNotAddMoreNodes() { +// $document = new HTMLDocument(Helper::HTML_PARENT_HAS_DATA_BIND_ATTR); +// $document->extractTemplates(); +// +// $document->querySelector("label>span")->bind( +// ["outside-scope" => "example content"] +// ); +// +// $data = [ +// ["example-key" => "example-value-1","target-key" => "target-value-1"], +// ["example-key" => "example-value-2","target-key" => "target-value-2"], +// ["example-key" => "example-value-3","target-key" => "target-value-3"], +// ]; +// +// $exception = null; +// +// try { +// $document->querySelector("ul")->bind($data); +// } +// catch(DomTemplateException $exception) {} +// self::assertNull($exception); +// +// self::assertCount(3, $document->querySelectorAll("ul li")); +// self::assertEquals( +// "example content", +// $document->querySelector("label>span")->textContent +// ); +// } +// +// public function testMultipleListBindSameDocument() { +// $document = new HTMLDocument(Helper::HTML_DOUBLE_BINDABLE_LIST); +// $document->extractTemplates(); +// +// $oneToTen = []; +// for($i = 1; $i <= 10; $i++) { +// $oneToTen []= [ +// "i" => $i, +// ]; +// } +// +// $document->querySelector(".area-1 ul")->bind($oneToTen); +// $document->querySelector("h1")->bind([ +// "name" => "Example Name", +// ]); +// +// $startingNumber = rand(100, 1000); +// $document->querySelector(".area-2 p")->bind([ +// "start" => $startingNumber, +// ]); +// +// for($i = $startingNumber; $i <= $startingNumber + 10; $i++) { +// $t = $document->getTemplate("dynamic-list-item"); +// $t->bind(["i" => $i]); +// $t->insertTemplate(); +// } +// +// foreach($document->querySelectorAll(".area-1 ul li") as $i => $li) { +// $number = $i + 1; +// self::assertStringContainsString($number, $li->textContent); +// } +// +// foreach($document->querySelectorAll(".area-2 ul li") as $i => $li) { +// $number = $i + $startingNumber; +// self::assertStringContainsString($number, $li->textContent); +// } +// +// self::assertStringContainsString("Example Name", $document->querySelector("h1")->textContent); +// self::assertStringContainsString($startingNumber, $document->querySelector(".area-2 p")->textContent); +// } +// +// public function testBindingTodoListFromObject() { +// $document = new HTMLDocument(Helper::HTML_TODO_LIST); +// $document->extractTemplates(); +// +// $todoItems = [ +// "Go to shops", +// "Buy pizza", +// "Eat pizza", +// "Sleep", +// ]; +// +// $list = new TodoListExampleObject($todoItems); +// +// $container = $document->getElementById("todo-list"); +// $container->bind($list); +// +// $liNodes = $container->querySelectorAll("li"); +// self::assertCount(4, $liNodes); +// +// foreach($todoItems as $id => $name) { +// self::assertEquals( +// $id, +// $liNodes[$id]->querySelector("[name='id']")->value +// ); +// self::assertEquals( +// $name, +// $liNodes[$id]->querySelector("[name='title']")->value +// ); +// } +// } } \ No newline at end of file From 777863daacc2d834574036032ec2120b281ebfd5 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sat, 15 Jun 2019 14:46:21 +0100 Subject: [PATCH 03/18] Inject attribute placeholders --- src/Bindable.php | 26 ++++++++++++++------------ test/unit/BindableTest.php | 16 ++++++++++++++++ test/unit/Helper/Helper.php | 14 ++++++++++++++ 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/Bindable.php b/src/Bindable.php index ce1fc69..03a08e6 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -24,7 +24,7 @@ public function bindKeyValue( string $value ):void { $this->injectBoundProperty($key, $value); -// $this->injectAttributePlaceholder($key, $value); + $this->injectAttributePlaceholder($key, $value); } /** @@ -177,26 +177,28 @@ protected function injectAttributePlaceholder( as $elementWithBraceInAttributeValue) { foreach($elementWithBraceInAttributeValue->attributes as $attr) { + /** @var Attr $attr */ preg_match_all( - "/{([^}]+)}/", + "/{(?P[^}]+)}/", $attr->value, $matches ); - if(empty($matches[0])) { + $bindProperties = $matches["bindProperties"] ?? null; + + if(!in_array($key, $bindProperties)) { continue; } - foreach($matches[0] as $i => $match) { - $value = str_replace( - $match, - "{$key}", - $value - ); + if(is_null($bindProperties)) { + continue; + } - $attr->ownerElement->setAttribute( - $attr->name, - $value + foreach($bindProperties as $i => $bindProperty) { + $attr->value = str_replace( + "{" . "$key" . "}", + $value, + $attr->value ); } } diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index 6c57e02..11e7168 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -183,6 +183,22 @@ public function testBindDataRemoved() { self::assertFalse($spanChildren[1]->hasAttribute("data-bind:text")); } + public function testInjectAttributePlaceholder() { + $document = new HTMLDocument(Helper::HTML_ATTRIBUTE_PLACEHOLDERS); + $userId = 101; + $username = "thoughtpolice"; + $document->bindKeyValue("userId", $userId); + $document->bindKeyValue("username", $username); + + $link = $document->querySelector("a"); + $img = $document->querySelector("img"); + self::assertEquals("/user/101", $link->href); + self::assertEquals("/img/profile/$userId.jpg", $img->src); + self::assertEquals("thoughtpolice's profile picture", $img->alt); + } + + + // public function testBindListTemplateTodoList() { // $document = new HTMLDocument(Helper::HTML_TODO_LIST); // $todoData = [ diff --git a/test/unit/Helper/Helper.php b/test/unit/Helper/Helper.php index b0fb9a6..33cccb2 100644 --- a/test/unit/Helper/Helper.php +++ b/test/unit/Helper/Helper.php @@ -388,5 +388,19 @@ class Helper { HTML; + const HTML_ATTRIBUTE_PLACEHOLDERS = << + +This document has some elements with attribute placeholders +
+

This is a test!

+

View your account

+ +

You are logged in.

+

This is your profile picture:

+ + {username}'s profile picture +
+HTML; } \ No newline at end of file From e4e3d01bf3d9ac1cabbdb05b5f99ac2351aa8dcd Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sat, 15 Jun 2019 14:47:20 +0100 Subject: [PATCH 04/18] Delete old tests --- test/unit/BindableTest.php | 417 ------------------------------------- 1 file changed, 417 deletions(-) diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index 11e7168..1f1502f 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -196,421 +196,4 @@ public function testInjectAttributePlaceholder() { self::assertEquals("/img/profile/$userId.jpg", $img->src); self::assertEquals("thoughtpolice's profile picture", $img->alt); } - - - -// public function testBindListTemplateTodoList() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST); -// $todoData = [ -// ["id" => 1, "title" => "Write tests", "complete" => true], -// ["id" => 2, "title" => "Implement features", "complete" => false], -// ["id" => 3, "title" => "Pass tests", "complete" => false], -// ]; -// $document->extractTemplates(); -// $todoListElement = $document->getElementById("todo-list"); -// $todoListElement->bindList($todoData); -// -// $liChildren = $todoListElement->querySelectorAll("li"); -// -// self::assertCount( -// count($todoData), -// $liChildren, -// "There should be the same amount of li elements as there are rows of data" -// ); -// -// foreach($todoData as $i => $row) { -// self::assertStringContainsString( -// $row["title"], -// $liChildren[$i]->innerHTML -// ); -// } -// } -// -// public function testBindListTemplatesCleanedUpAfterAdding() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST); -// $todoData = [ -// ["id" => 1, "title" => "Write tests", "complete" => true], -// ["id" => 2, "title" => "Implement features", "complete" => false], -// ["id" => 3, "title" => "Pass tests", "complete" => false], -// ]; -// $document->extractTemplates(); -// $todoListElement = $document->getElementById("todo-list"); -// $todoListElement->bindList($todoData); -// -// self::assertStringContainsString( -// "Implement features", -// $todoListElement->innerHTML -// ); -// self::assertStringNotContainsString( -// "data-bind", -// $todoListElement->innerHTML -// ); -// } -// -// public function testBindListWithInlineNamedTemplate() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST_INLINE_NAMED_TEMPLATE); -// $todoData = [ -// ["id" => 1, "title" => "Write tests", "complete" => true], -// ["id" => 2, "title" => "Implement features", "complete" => false], -// ["id" => 3, "title" => "Pass tests", "complete" => false], -// ]; -// $document->extractTemplates(); -// $todoListElement = $document->getElementById("todo-list"); -// $todoListElement->bindList($todoData); -// -// self::assertStringContainsString( -// "Implement features", -// $todoListElement->innerHTML -// ); -// self::assertStringNotContainsString( -// "data-bind", -// $todoListElement->innerHTML -// ); -// } - -// public function testBindListWithInlineNamedTemplateWhenAnotherTemplateExists() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST_INLINE_NAMED_TEMPLATE_DOUBLE); -// $todoData = [ -// ["id" => 1, "title" => "Write tests", "complete" => true], -// ["id" => 2, "title" => "Implement features", "complete" => false], -// ["id" => 3, "title" => "Pass tests", "complete" => false], -// ]; -// $document->extractTemplates(); -// -// $todoListElement = $document->getElementById("todo-list"); -// $todoListElement->bindList($todoData); -// self::assertStringContainsString( -// "Implement features", -// $todoListElement->innerHTML -// ); -// self::assertStringNotContainsString( -// "Use the other template instead!", -// $todoListElement->innerHTML -// ); -// -// $todoListElement = $document->getElementById("todo-list-2"); -// $todoListElement->bind($todoData, "todo-list-item"); -// -// self::assertStringContainsString( -// "Implement features", -// $todoListElement->innerHTML -// ); -// self::assertStringNotContainsString( -// "Use the other template instead!", -// $todoListElement->innerHTML -// ); -// } -// -// public function testBindWithNonOptionalKey() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST); -// $todoData = [ -// ["title" => "Write tests", "complete" => true], -// ["title" => "Implement features", "complete" => false], -// ["title" => "Pass tests", "complete" => false], -// ]; -// $document->extractTemplates(); -// $todoListElement = $document->getElementById("todo-list"); -// -// self::expectException(BoundDataNotSetException::class); -// $todoListElement->bind($todoData); -// } -// -// public function testBindWithOptionalKey() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST_OPTIONAL_ID); -// $todoData = [ -// ["title" => "Write tests", "complete" => true], -// ["title" => "Implement features", "complete" => false], -// ["title" => "Pass tests", "complete" => false], -// ]; -// $document->extractTemplates(); -// $todoListElement = $document->getElementById("todo-list"); -// -// $todoListElement->bind($todoData); -// $items = $todoListElement->querySelectorAll("li"); -// self::assertCount(3, $items); -// -// self::assertEquals( -// "Implement features", -// $items[1]->querySelector("input[name=title]")->value -// ); -// self::assertNull( -// $items[1]->querySelector("input[name=id]")->value -// ); -// } -// -// public function testBindClass() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS); -// $todoData = [ -// ["id" => 1, "title" => "Write tests", "complete" => true], -// ["id" => 2, "title" => "Implement features", "complete" => false], -// ["id" => 3, "title" => "Pass tests", "complete" => false], -// ]; -// $document->extractTemplates(); -// $todoListElement = $document->getElementById("todo-list"); -// -// $todoListElement->bind($todoData); -// $items = $todoListElement->querySelectorAll("li"); -// -// foreach($todoData as $i => $todoDatum) { -// self::assertEquals( -// $todoDatum["complete"], -// $items[$i]->classList->contains("complete") -// ); -// -// self::assertTrue($items[$i]->classList->contains("existing-class")); -// } -// } -// -// public function testBindClassColon() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS_COLON); -// $todoData = [ -// ["id" => 1, "title" => "Write tests", "dateTimeCompleted" => "2018-07-01 19:46:00"], -// ["id" => 2, "title" => "Implement features", "dateTimeCompleted" => null], -// ["id" => 3, "title" => "Pass tests", "dateTimeCompleted" => "2018-07-01 19:49:00"], -// ]; -// $document->extractTemplates(); -// $todoListElement = $document->getElementById("todo-list"); -// -// $todoListElement->bind($todoData); -// $items = $todoListElement->querySelectorAll("li"); -// -// foreach($todoData as $i => $todoDatum) { -// $completed = (bool)$todoDatum["dateTimeCompleted"]; -// self::assertEquals( -// $completed, -// $items[$i]->classList->contains("complete") -// ); -// -// self::assertTrue($items[$i]->classList->contains("existing-class")); -// } -// } -// -// public function testBindClassColonMultiple() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS_COLON_MULTIPLE); -// $todoData = [ -// [ -// "id" => 1, -// "title" => "Write tests", -// "dateTimeCompleted" => "2018-07-01 19:46:00", -// "dateTimeDeleted" => null, -// ], -// [ -// "id" => 2, -// "title" => "Implement features", -// "dateTimeCompleted" => null, -// "dateTimeDeleted" => "2018-07-01 19:54:00", -// ], -// [ -// "id" => 3, -// "title" => "Pass tests", -// "dateTimeCompleted" => "2018-07-01 19:49:00", -// "dateTimeDeleted" => null, -// ], -// ]; -// $document->extractTemplates(); -// $todoListElement = $document->getElementById("todo-list"); -// -// $todoListElement->bind($todoData); -// $items = $todoListElement->querySelectorAll("li"); -// -// foreach($todoData as $i => $todoDatum) { -// self::assertTrue($items[$i]->classList->contains("existing-class")); -// -// $completed = (bool)$todoDatum["dateTimeCompleted"]; -// self::assertEquals( -// $completed, -// $items[$i]->classList->contains("complete") -// ); -// -// $deleted = (bool)$todoDatum["dateTimeDeleted"]; -// self::assertEquals( -// $deleted, -// $items[$i]->classList->contains("deleted") -// ); -// } -// } -// -// public function testBindWithObjectData() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS_COLON_MULTIPLE); -// $todoData = [ -// [ -// "id" => 1, -// "title" => "Write tests", -// "dateTimeCompleted" => "2018-07-01 19:46:00", -// "dateTimeDeleted" => null, -// ], -// [ -// "id" => 2, -// "title" => "Implement features", -// "dateTimeCompleted" => null, -// "dateTimeDeleted" => "2018-07-01 19:54:00", -// ], -// [ -// "id" => 3, -// "title" => "Pass tests", -// "dateTimeCompleted" => "2018-07-01 19:49:00", -// "dateTimeDeleted" => null, -// ], -// ]; -// -// $todoObjData = []; -// -// foreach($todoData as $todo) { -// $obj = new StdClass(); -// foreach($todo as $key => $value) { -// $obj->$key = $value; -// } -// -// $todoObjData []= $obj; -// } -// -// $document->extractTemplates(); -// $todoListElement = $document->getElementById("todo-list"); -// -// $todoListElement->bind($todoObjData); -// $items = $todoListElement->querySelectorAll("li"); -// -// foreach($todoObjData as $i => $todo) { -// self::assertTrue($items[$i]->classList->contains("existing-class")); -// -// $completed = (bool)$todo->dateTimeCompleted; -// self::assertEquals( -// $completed, -// $items[$i]->classList->contains("complete") -// ); -// -// $deleted = (bool)$todo->dateTimeDeleted; -// self::assertEquals( -// $deleted, -// $items[$i]->classList->contains("deleted") -// ); -// } -// } -// -//// For issue #52: -// public function testBindingDataWithBindableParentElement() { -// $document = new HTMLDocument(Helper::HTML_PARENT_HAS_DATA_BIND_ATTR); -// $document->extractTemplates(); -// -// $data = [ -// ["example-key" => "example-value-1","target-key" => "target-value-1"], -// ["example-key" => "example-value-2","target-key" => "target-value-2"], -// ["example-key" => "example-value-3","target-key" => "target-value-3"], -// ]; -// -// $exception = null; -// -// foreach($data as $row) { -// $t = $document->getTemplate("target-template"); -// try { -// $t->bind($row); -// } -// catch(DomTemplateException $exception) {} -// -// $t->insertTemplate(); -// } -// -// self::assertNull($exception); -// } -// -// public function testBindingDataWithBindableParentElementDoesNotAddMoreNodes() { -// $document = new HTMLDocument(Helper::HTML_PARENT_HAS_DATA_BIND_ATTR); -// $document->extractTemplates(); -// -// $document->querySelector("label>span")->bind( -// ["outside-scope" => "example content"] -// ); -// -// $data = [ -// ["example-key" => "example-value-1","target-key" => "target-value-1"], -// ["example-key" => "example-value-2","target-key" => "target-value-2"], -// ["example-key" => "example-value-3","target-key" => "target-value-3"], -// ]; -// -// $exception = null; -// -// try { -// $document->querySelector("ul")->bind($data); -// } -// catch(DomTemplateException $exception) {} -// self::assertNull($exception); -// -// self::assertCount(3, $document->querySelectorAll("ul li")); -// self::assertEquals( -// "example content", -// $document->querySelector("label>span")->textContent -// ); -// } -// -// public function testMultipleListBindSameDocument() { -// $document = new HTMLDocument(Helper::HTML_DOUBLE_BINDABLE_LIST); -// $document->extractTemplates(); -// -// $oneToTen = []; -// for($i = 1; $i <= 10; $i++) { -// $oneToTen []= [ -// "i" => $i, -// ]; -// } -// -// $document->querySelector(".area-1 ul")->bind($oneToTen); -// $document->querySelector("h1")->bind([ -// "name" => "Example Name", -// ]); -// -// $startingNumber = rand(100, 1000); -// $document->querySelector(".area-2 p")->bind([ -// "start" => $startingNumber, -// ]); -// -// for($i = $startingNumber; $i <= $startingNumber + 10; $i++) { -// $t = $document->getTemplate("dynamic-list-item"); -// $t->bind(["i" => $i]); -// $t->insertTemplate(); -// } -// -// foreach($document->querySelectorAll(".area-1 ul li") as $i => $li) { -// $number = $i + 1; -// self::assertStringContainsString($number, $li->textContent); -// } -// -// foreach($document->querySelectorAll(".area-2 ul li") as $i => $li) { -// $number = $i + $startingNumber; -// self::assertStringContainsString($number, $li->textContent); -// } -// -// self::assertStringContainsString("Example Name", $document->querySelector("h1")->textContent); -// self::assertStringContainsString($startingNumber, $document->querySelector(".area-2 p")->textContent); -// } -// -// public function testBindingTodoListFromObject() { -// $document = new HTMLDocument(Helper::HTML_TODO_LIST); -// $document->extractTemplates(); -// -// $todoItems = [ -// "Go to shops", -// "Buy pizza", -// "Eat pizza", -// "Sleep", -// ]; -// -// $list = new TodoListExampleObject($todoItems); -// -// $container = $document->getElementById("todo-list"); -// $container->bind($list); -// -// $liNodes = $container->querySelectorAll("li"); -// self::assertCount(4, $liNodes); -// -// foreach($todoItems as $id => $name) { -// self::assertEquals( -// $id, -// $liNodes[$id]->querySelector("[name='id']")->value -// ); -// self::assertEquals( -// $name, -// $liNodes[$id]->querySelector("[name='title']")->value -// ); -// } -// } } \ No newline at end of file From 2c098f2ad6f36e1d467bd4d81ecb10b4b5dbaa45 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sat, 15 Jun 2019 15:02:46 +0100 Subject: [PATCH 05/18] Test binding to class attribute --- src/Bindable.php | 48 ++++++++++++++------------------------ test/unit/BindableTest.php | 24 +++++++++++++++++++ 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/Bindable.php b/src/Bindable.php index 03a08e6..df38d51 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -109,26 +109,25 @@ protected function injectBoundProperty( } $element = $attr->ownerElement; - $propertyToSet = $this->getPropertyToSet($attr); + $keyToSet = $this->getKeyToSet($attr); $attr->ownerDocument->storeBoundAttribute($attr); // Skip attributes whose value does not equal the key that we are setting. - if($propertyToSet !== $key) { + if($keyToSet !== $key) { continue; } + $bindProperty = $matches["bindProperty"]; + // The "class" property behaves differently to others, as it is represented by // a StringMap rather than a single value. - if($propertyToSet === "class") { - $this->setClassValue( - $element, - $value - ); + if($bindProperty === "class") { + $element->classList->toggle($value); } else { $this->setPropertyValue( $element, - $matches["bindProperty"], + $bindProperty, $value ); } @@ -137,30 +136,30 @@ protected function injectBoundProperty( } /** - * The property that is to be bound can reference another property's - * value, using the @ syntax. For example, data-bind:text="@id" will - * bind the element's text content with the data value with the key - * of the element's id attribute value. + * The data-bind syntax can reference another attribute's value to use + * as the key to set, using the @ syntax. For example, + * data-bind:text="@id" will bind the element's text content with the + * data value with the key of the element's id attribute value. */ - protected function getPropertyToSet( + protected function getKeyToSet( BaseAttr $attr ):string { - $propertyToSet = $attr->value; + $keyToSet = $attr->value; - if($propertyToSet[0] === "@") { - $lookupAttribute = substr($propertyToSet, 1); - $propertyToSet = $attr->ownerElement->getAttribute( + if($keyToSet[0] === "@") { + $lookupAttribute = substr($keyToSet, 1); + $keyToSet = $attr->ownerElement->getAttribute( $lookupAttribute ); - if(is_null($propertyToSet)) { + if(is_null($keyToSet)) { throw new BoundAttributeDoesNotExistException( $lookupAttribute ); } } - return $propertyToSet; + return $keyToSet; } protected function injectAttributePlaceholder( @@ -205,17 +204,6 @@ protected function injectAttributePlaceholder( } } - protected function setClassValue( - BaseElement $element, -// string $key, // TODO: Do we need this??? I Don't think so. - string $value - ):void { - $classList = explode(" ", $attr->value); - foreach($classList as $class) { - - } - } - protected function setPropertyValue( BaseElement $element, string $bindProperty, diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index 1f1502f..3bce279 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -7,6 +7,7 @@ use Gt\DomTemplate\HTMLDocument; use Gt\DomTemplate\Test\Helper\Helper; use Gt\DomTemplate\Test\Helper\TodoListExampleObject; +use PHPUnit\TextUI\Help; use stdClass; class BindableTest extends TestCase { @@ -196,4 +197,27 @@ public function testInjectAttributePlaceholder() { self::assertEquals("/img/profile/$userId.jpg", $img->src); self::assertEquals("thoughtpolice's profile picture", $img->alt); } + + public function testBindClass() { + $document = new HTMLDocument(Helper::HTML_TODO_LIST_BIND_CLASS); + $isComplete = true; + + $li = $document->querySelector(".existing-class"); + $todoListElement = $document->getElementById("todo-list"); + $todoListElement->bindKeyValue( + "complete", + $isComplete ? "task-complete" : "task-to-do" + ); + + $classList = $li->classList; + self::assertTrue($classList->contains("existing-class")); + self::assertTrue($classList->contains("task-complete")); + +// If there is already a class on the element, binding to it again will remove it. + $todoListElement->bindKeyValue( + "complete", + $isComplete ? "task-complete" : "task-to-do" + ); + self::assertFalse($classList->contains("task-complete")); + } } \ No newline at end of file From 0f510793b3734ad5aac5840605eaff687596a872 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sat, 15 Jun 2019 17:02:24 +0100 Subject: [PATCH 06/18] Test binding lists of data --- src/Bindable.php | 92 +++----------------- src/HTMLDocument.php | 17 ++++ src/NamelessTemplateSpecificityException.php | 4 + test/unit/BindableTest.php | 38 ++++++++ test/unit/Helper/Helper.php | 20 ++++- 5 files changed, 89 insertions(+), 82 deletions(-) create mode 100644 src/NamelessTemplateSpecificityException.php diff --git a/src/Bindable.php b/src/Bindable.php index df38d51..ac607e3 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -69,8 +69,19 @@ public function bindList( if($element instanceof HTMLDocument) { $element = $element->documentElement; } + /** @var HTMLDocument $document */ + $document = $element->ownerDocument; - // TODO: Do the looping here. + foreach($kvpList as $data) { + if(is_null($templateName)) { + $t = $document->getUnnamedTemplate($element); + } + else { + $t = $document->getNamedTemplate($templateName); + } + + $t->insertTemplate(); + } } /** @@ -227,85 +238,6 @@ protected function setPropertyValue( } } -// protected function injectDataIntoBindProperties( -// Element $parent, -// $data -// ):void { -// $childrenWithBindAttribute = $this->getChildrenWithBindAttribute($parent); -// -// foreach($childrenWithBindAttribute as $element) { -// $this->setData($element, $data); -// } -// -// $this->bindAttributes($parent, $data); -// } - -// protected function injectDataIntoAttributeValues( -// BaseElement $element, -// $data -// ):void { -// if(is_array($data)) { -// $data = (object)$data; -// } -// -// foreach($element->xPath("//*[@*[contains(.,'{')]]") -// as $elementWithBraceInAttributeValue) { -// foreach($elementWithBraceInAttributeValue->attributes as $attr) { -// preg_match_all( -// "/{([^}]+)}/", -// $attr->value, -// $matches -// ); -// -// if(empty($matches[0])) { -// continue; -// } -// -// foreach($matches[0] as $i => $match) { -// $key = $matches[1][$i]; -// -// if(!isset($data->{$key})) { -// continue; -// } -// -// -// $value = str_replace( -// $match, -// $data->{$key}, -// $attr->value -// ); -// -// $attr->ownerElement->setAttribute($attr->name, $value); -// } -// } -// } -// } - -// protected function bindAttributes(BaseElement $element, $data):void { -// foreach($element->attributes as $attr) { -// preg_match( -// "/{(.+)}/", -// $attr->value, -// $matches -// ); -// if(empty($matches)) { -// continue; -// } -// -// list($placeholder, $dataKey) = $matches; -// -// if(!isset($data[$dataKey])) { -// continue; -// } -// -// $attr->value = str_replace( -// $placeholder, -// $data[$dataKey], -// $attr->value -// ); -// } -// } - protected function bindTemplates( DOMNode $element, $data, diff --git a/src/HTMLDocument.php b/src/HTMLDocument.php index 1780c9f..1f1959e 100644 --- a/src/HTMLDocument.php +++ b/src/HTMLDocument.php @@ -63,6 +63,23 @@ public function getNamedTemplate(string $name):?DocumentFragment { return null; } + public function getUnnamedTemplate(Element $element):?DocumentFragment { + $path = $element->getNodePath(); + $matches = []; + + foreach($this->templateFragmentMap as $name => $t) { + if(strpos($name, $path) === 0) { + $matches []= $t; + } + } + + if(count($matches) > 1) { + throw new NamelessTemplateSpecificityException(); + } + + return $matches[0] ?? null; + } + /** * @return \Gt\Dom\DocumentFragment[] */ diff --git a/src/NamelessTemplateSpecificityException.php b/src/NamelessTemplateSpecificityException.php new file mode 100644 index 0000000..12955c1 --- /dev/null +++ b/src/NamelessTemplateSpecificityException.php @@ -0,0 +1,4 @@ +contains("task-complete")); } + + public function testBindListMultipleDataTemplateElementsNoName() { + $document = new HTMLDocument(Helper::HTML_DOUBLE_NAMELESS_BIND_LIST); + $document->extractTemplates(); +// This will fail, because I am not passing a template name to bind to, and there +// are more than one nameless template elements in the document. +// Instead, I should either pass a template name, or bind to a more specific element, +// such as the UL itself. + self::expectException(NamelessTemplateSpecificityException::class); +// No need for any actual data during this test. + $document->bindList([[]]); + } + + public function testBindListMultipleDataTemplateElements() { + $document = new HTMLDocument(Helper::HTML_DOUBLE_NAMELESS_BIND_LIST); + $document->extractTemplates(); + $stateList = [ + ["state-name" => "Oceania", "ideology" => "Ingsoc", "main-territory" => "Western Hemisphere"], + ["state-name" => "Eurasia", "ideology" => "Neo-Bolshevism", "main-territory" => "Continental Europe"], + ["state-name" => "Eastasia", "ideology" => "Death Worship", "main-territory" => "China"], + ]; + $ministryList = [ + ["ministry-name" => "Peace", "ministry-id" => 123], + ["ministry-name" => "Plenty", "ministry-id" => 511], + ["ministry-name" => "Truth", "ministry-id" => 141], + ["ministry-name" => "Love", "ministry-id" => 610], + ]; + + $firstList = $document->getElementById("list-1"); + $secondList = $document->getElementById("list-2"); + + $firstList->bindList($stateList); + $secondList->bindList($ministryList); + + self::assertCount(count($stateList), $firstList->children); + self::assertCount(count($ministryList), $secondList->children); + } } \ No newline at end of file diff --git a/test/unit/Helper/Helper.php b/test/unit/Helper/Helper.php index 33cccb2..07d853b 100644 --- a/test/unit/Helper/Helper.php +++ b/test/unit/Helper/Helper.php @@ -371,7 +371,7 @@ class Helper {
  • - N + N
@@ -381,13 +381,29 @@ class Helper {
  • - N + N
HTML; + const HTML_DOUBLE_NAMELESS_BIND_LIST = << +
+

List of totalitarian superstates:

+
    +
  • +
+ +

Ministries of Oceana:

+
    +
  • +
+
+HTML; + + const HTML_ATTRIBUTE_PLACEHOLDERS = << From 0af0a80cb02f4476d91545ae006d8bbe858f61f1 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sat, 15 Jun 2019 17:06:05 +0100 Subject: [PATCH 07/18] Actually bind the data to list elements --- src/Bindable.php | 1 + test/unit/BindableTest.php | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/Bindable.php b/src/Bindable.php index ac607e3..6ebe752 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -80,6 +80,7 @@ public function bindList( $t = $document->getNamedTemplate($templateName); } + $t->bindData($data); $t->insertTemplate(); } } diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index d459a63..8ffc55e 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -257,5 +257,8 @@ public function testBindListMultipleDataTemplateElements() { self::assertCount(count($stateList), $firstList->children); self::assertCount(count($ministryList), $secondList->children); + + self::assertEquals($stateList[1]["state-name"], $firstList->querySelectorAll("li")[1]->innerText); + self::assertEquals($ministryList[2]["ministry-name"], $secondList->querySelectorAll("li")[2]->innerText); } } \ No newline at end of file From be9a7c3371ab8e684a4eeac9876f3a17a0527acf Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sat, 15 Jun 2019 17:06:36 +0100 Subject: [PATCH 08/18] Document exception --- src/Bindable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bindable.php b/src/Bindable.php index 6ebe752..8814b5f 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -57,7 +57,7 @@ public function bindData( * If there are multiple un-named template elements, an exception is * thrown - in this case, you will need to use bindNestedList * - * @throws TODO: Name an exception + * @throws NamelessTemplateSpecificityException * @see self::bindNestedList */ public function bindList( From 176e64738d821fedefc9f1ff56d3dc1afa15fb70 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sat, 15 Jun 2019 17:13:30 +0100 Subject: [PATCH 09/18] Test binding lists to the document with named templates --- test/unit/BindableTest.php | 31 +++++++++++++++++++++++++++++++ test/unit/Helper/Helper.php | 15 +++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index 8ffc55e..63f9843 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -234,6 +234,37 @@ public function testBindListMultipleDataTemplateElementsNoName() { $document->bindList([[]]); } + public function testBindListMultipleDataTemplateElementsWithName() { + $document = new HTMLDocument(Helper::HTML_DOUBLE_NAMES_BIND_LIST); + $document->extractTemplates(); + $stateList = [ + ["state-name" => "Oceania", "ideology" => "Ingsoc", "main-territory" => "Western Hemisphere"], + ["state-name" => "Eurasia", "ideology" => "Neo-Bolshevism", "main-territory" => "Continental Europe"], + ["state-name" => "Eastasia", "ideology" => "Death Worship", "main-territory" => "China"], + ]; + $ministryList = [ + ["ministry-name" => "Peace", "ministry-id" => 123], + ["ministry-name" => "Plenty", "ministry-id" => 511], + ["ministry-name" => "Truth", "ministry-id" => 141], + ["ministry-name" => "Love", "ministry-id" => 610], + ]; + + $firstList = $document->getElementById("list-1"); + $secondList = $document->getElementById("list-2"); + +// Note that the difference here to the test above and below this one is that +// we're actually passing a template name, even though we're still binding to +// the root document node. + $document->bindList($stateList, "state"); + $document->bindList($ministryList, "ministry"); + + self::assertCount(count($stateList), $firstList->children); + self::assertCount(count($ministryList), $secondList->children); + + self::assertEquals($stateList[1]["state-name"], $firstList->querySelectorAll("li")[1]->innerText); + self::assertEquals($ministryList[2]["ministry-name"], $secondList->querySelectorAll("li")[2]->innerText); + } + public function testBindListMultipleDataTemplateElements() { $document = new HTMLDocument(Helper::HTML_DOUBLE_NAMELESS_BIND_LIST); $document->extractTemplates(); diff --git a/test/unit/Helper/Helper.php b/test/unit/Helper/Helper.php index 07d853b..899abaa 100644 --- a/test/unit/Helper/Helper.php +++ b/test/unit/Helper/Helper.php @@ -403,6 +403,21 @@ class Helper { HTML; + const HTML_DOUBLE_NAMES_BIND_LIST = << +
+

List of totalitarian superstates:

+
    +
  • +
+ +

Ministries of Oceana:

+
    +
  • +
+
+HTML; + const HTML_ATTRIBUTE_PLACEHOLDERS = << From c49573e7c66c2868f2ba8951f6f93f02e92a5df4 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sat, 15 Jun 2019 23:12:02 +0100 Subject: [PATCH 10/18] Delete old implementation --- src/Bindable.php | 235 +---------------------------------------------- 1 file changed, 4 insertions(+), 231 deletions(-) diff --git a/src/Bindable.php b/src/Bindable.php index 8814b5f..0180c25 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -3,8 +3,6 @@ use Gt\Dom\Attr as BaseAttr; use Gt\Dom\Element as BaseElement; -use DOMNode; -use StdClass; use Gt\Dom\HTMLCollection as BaseHTMLCollection; /** @@ -51,7 +49,6 @@ public function bindData( * will have the inner iterable data bound to it before being added into * the DOM in the position that it was originally extracted. * - * TODO: Enforce the following: * When $templateName is not provided, the data within $kvpList will be * bound to an element that has a data-template attribute with no value. * If there are multiple un-named template elements, an exception is @@ -86,12 +83,11 @@ public function bindList( } /** - * When data needs binding to a nested DOM structure, a BindIterator is - * necessary to link each child list with the correct template. - * - * TODO: Implement. + * When complex data needs binding to a nested DOM structure, a + * BindIterator is necessary to link each child list with the + * correct template. */ - public function bindNestedList(BindIterator $iterator):void { + public function bindNestedList(iterable $data):void { } @@ -239,195 +235,6 @@ protected function setPropertyValue( } } - protected function bindTemplates( - DOMNode $element, - $data, - string $templateName = null - ):void { - if($element instanceof \DOMDocumentFragment) { - return; - } - - $namesToMatch = []; - - if(is_null($templateName)) { - $namesToMatch []= $element->getNodePath(); - - } - else { - $namesToMatch []= $templateName; - } - - /** @var HTMLDocument $rootDocument */ - $rootDocument = $this->getRootDocument(); - /** @var DocumentFragment[] $templateChildren */ - $templateChildren = $rootDocument->getNamedTemplateChildren( - ...$namesToMatch - ); - - foreach($data as $rowIndex => $row) { - foreach($templateChildren as $childNumber => $fragment) { - $insertInto = null; - - if($fragment->templateParentNode !== $element) { - $insertInto = $element; - } - - $newNode = $fragment->insertTemplate($insertInto); - $this->injectDataIntoBindProperties($newNode, $row); - $this->injectDataIntoAttributeValues( - $newNode, - $row - ); - } - } - - if(is_null($rowIndex)) { - $trimmed = trim($element->innerHTML); - if($trimmed === "") { - $element->innerHTML = ""; - } - } - } - - protected function setData(BaseElement $element, $data):void { - if(is_array($data)) { - $data = $this->convertArrayToObject($data); - } - - foreach($element->attributes as $attr) { - $matches = []; - if(!preg_match( - "/(?:data-bind:)(.+)/", - $attr->name, - $matches) - ) { - continue; - } - $bindProperty = $matches[1]; - - if($bindProperty === "class") { - $this->handleClassData( - $attr, - $element, - $data - ); - } - else { - $this->handlePropertyData( - $attr, - $bindProperty, - $element, - $data - ); - } - } - } - - protected function handlePropertyData( - BaseAttr $attr, - string $bindProperty, - BaseElement $element, - $data - ):void { - $dataKeyMatch = $this->getKeyFromAttribute($element, $attr); - $dataValue = $dataKeyMatch->getValue($data) ?? ""; - - switch($bindProperty) { - case "html": - case "innerhtml": - $element->innerHTML = $dataValue; - break; - - case "text": - case "innertext": - case "textcontent": - $element->innerText = $dataValue; - break; - - case "value": - $element->value = $dataValue; - break; - - default: - $element->setAttribute($bindProperty, $dataValue); - break; - } - } - - protected function handleClassData( - BaseAttr $attr, - BaseElement $element, - $data - ):void { - $classList = explode(" ", $attr->value); - $this->setClassFromData( - $element, - $data, ... - $classList - ); - } - - protected function setClassFromData( - BaseElement $element, - $data, - string...$classList - ):void { - foreach($classList as $class) { - if(!strstr($class, ":")) { - $class = "$class:$class"; - } - - list($keyMatch, $className) = explode(":", $class); - - if(!isset($data->{$keyMatch})) { - continue; - } - - if($data->{$keyMatch}) { - $element->classList->add($className); - } - else { - $element->classList->remove($className); - } - } - } - - protected function getKeyFromAttribute(BaseElement $element, Attr $attr):DataKeyMatch { - $required = true; - $key = $attr->value; - - if($key[0] === "?") { - $required = false; - $key = substr($key, 1); - } - - if($key[0] === "@") { - $key = substr($key, 1); - $attributeValue = $element->getAttribute($key); - if(is_null($attributeValue)) { - throw new BoundAttributeDoesNotExistException($attr->name); - } - - $key = $attributeValue; - } - - return new DataKeyMatch($key, $required); - } - - protected function getTemplateNamesForElement(BaseElement $element):array { - $templateNames = []; - $nodePath = $element->getNodePath(); - - foreach($this->templateFragmentMap as $key => $templateFragment) { - if(strpos($key, $nodePath) === 0) { - $templateNames []= $key; - } - } - - return $templateNames; - } - protected function getChildrenWithBindAttribute():BaseHTMLCollection { /** @var BaseElement $element */ $element = $this; @@ -439,38 +246,4 @@ protected function getChildrenWithBindAttribute():BaseHTMLCollection { "descendant-or-self::*[@*[starts-with(name(), 'data-bind')]]" ); } - - protected function cleanBindAttributes(DOMNode $element):void { - $elementsToClean = [$element]; - $childrenWithBindAttribute = $this->getChildrenWithBindAttribute($element); - foreach($childrenWithBindAttribute as $child) { - $elementsToClean []= $child; - } - - foreach($elementsToClean as $cleanMe) { - if(!$cleanMe->attributes) { - continue; - } - - $attributesToRemove = []; - foreach($cleanMe->attributes as $attr) { - if(strpos($attr->name, "data-bind") === 0) { - $attributesToRemove []= $attr->name; - } - } - - foreach($attributesToRemove as $attrName) { - $cleanMe->removeAttribute($attrName); - } - } - } - - protected function convertArrayToObject(array $array) { - $object = new StdClass(); - foreach($array as $key => $value) { - $object->$key = $value; - } - - return $object; - } } From 9a5a497651998fafcc233bc5884930820f1e5ac9 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sat, 15 Jun 2019 23:13:18 +0100 Subject: [PATCH 11/18] Add tests for nested lists --- src/HTMLDocument.php | 8 +++- src/TemplateParent.php | 1 + test/unit/BindableTest.php | 24 +++++++++++ test/unit/HTMLDocumentTest.php | 2 + test/unit/Helper/Helper.php | 76 ++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/HTMLDocument.php b/src/HTMLDocument.php index 1f1959e..778c775 100644 --- a/src/HTMLDocument.php +++ b/src/HTMLDocument.php @@ -63,7 +63,10 @@ public function getNamedTemplate(string $name):?DocumentFragment { return null; } - public function getUnnamedTemplate(Element $element):?DocumentFragment { + public function getUnnamedTemplate( + Element $element, + bool $throwIfMoreThanOneMatch = true + ):?DocumentFragment { $path = $element->getNodePath(); $matches = []; @@ -73,7 +76,8 @@ public function getUnnamedTemplate(Element $element):?DocumentFragment { } } - if(count($matches) > 1) { + if(count($matches) > 1 + && $throwIfMoreThanOneMatch) { throw new NamelessTemplateSpecificityException(); } diff --git a/src/TemplateParent.php b/src/TemplateParent.php index 23b04fb..5328772 100644 --- a/src/TemplateParent.php +++ b/src/TemplateParent.php @@ -24,6 +24,7 @@ public function extractTemplates():int { $document = ($this instanceof DOMDocument) ? $this : $this->ownerDocument; + /** @var DocumentFragment $fragment */ $fragment = $document->createTemplateFragment( $templateElement diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index 63f9843..07c7a39 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -292,4 +292,28 @@ public function testBindListMultipleDataTemplateElements() { self::assertEquals($stateList[1]["state-name"], $firstList->querySelectorAll("li")[1]->innerText); self::assertEquals($ministryList[2]["ministry-name"], $secondList->querySelectorAll("li")[2]->innerText); } + + public function testBindNestedList() { + $document = new HTMLDocument(Helper::HTML_MUSIC); + $document->extractTemplates(); + $document->bindNestedList(Helper::LIST_MUSIC); + + foreach(Helper::LIST_MUSIC as $artistName => $albumList) { + $domArtist = $document->querySelector("[data-artist-name='$artistName']"); + $h2 = $domArtist->querySelector("h2"); + self::assertEquals($artistName, $h2->innerText); + + foreach($albumList as $albumName => $trackList) { + $domAlbum = $domArtist->querySelector("[data-album-name='$albumName']"); + $h3 = $domAlbum->querySelector("h3"); + self::assertEquals($albumName, $h3->innerText); + + foreach($trackList as $i => $trackName) { + $domTrack = $domAlbum->querySelector("[data-track-name='$trackName']"); + self::assertSame($domTrack, $domAlbum->children[$i]); + self::assertEquals($trackName, $domTrack->innerText); + } + } + } + } } \ No newline at end of file diff --git a/test/unit/HTMLDocumentTest.php b/test/unit/HTMLDocumentTest.php index 0993b92..23b5add 100644 --- a/test/unit/HTMLDocumentTest.php +++ b/test/unit/HTMLDocumentTest.php @@ -12,4 +12,6 @@ public function testOverriddenClasses() { $fragment = $document->createDocumentFragment(); self::assertInstanceOf(DocumentFragment::class, $fragment); } + + } \ No newline at end of file diff --git a/test/unit/Helper/Helper.php b/test/unit/Helper/Helper.php index 899abaa..4e2c950 100644 --- a/test/unit/Helper/Helper.php +++ b/test/unit/Helper/Helper.php @@ -434,4 +434,80 @@ class Helper { HTML; + const HTML_MUSIC = << + +Music list +
+

Music list!

+ +
    +
  • +

    Artist name

    + +
      +
    • +

      Album name

      + +
        +
      1. + Track name +
      2. +
      +
    • +
    +
  • +
+
+HTML; + + const LIST_MUSIC = [ + "A Band From Your Childhood" => [ + "This Album is Good" => [ + "The Best Song You've Ever Heard", + "Another Cracking Tune", + "Top Notch Music Here", + "The Best Is Left 'Till Last", + ], + "Adequate Collection" => [ + "Meh", + "'sok", + "Sounds Like Every Other Song", + ], + ], + "Bongo and The Bronks" => [ + "Salad" => [ + "Tomatoes", + "Song About Cucumber", + "Onions Make Me Cry (but I love them)", + ], + "Meat" => [ + "Steak", + "Is Chicken Really a Meat?", + "Don't Look in the Sausage Factory", + "Stop Horsing Around", + ], + "SnaxX" => [ + "Crispy Potatoes With Salt", + "Pretzel Song", + "Pork Scratchings Are Skin", + "The Peanut Is Not Actually A Nut", + ], + ], + "Crayons" => [ + "Pastel Colours" => [ + "Egg Shell", + "Cotton", + "Frost", + "Periwinkle", + ], + "Different Shades of Blue" => [ + "Cobalt", + "Slate", + "Indigo", + "Teal", + ], + ] + ]; + } \ No newline at end of file From 05fded641d40e8aaeb23cf9146751195fa378d91 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Sun, 16 Jun 2019 23:00:52 +0100 Subject: [PATCH 12/18] Work on nested list output --- src/Bindable.php | 45 ++++++++++++++++++++++++++++++++----- src/HTMLDocument.php | 29 +++++++++++++++++++++++- src/TemplateParent.php | 20 ++++++++++++++--- test/unit/BindableTest.php | 2 ++ test/unit/Helper/Helper.php | 12 +++++----- 5 files changed, 93 insertions(+), 15 deletions(-) diff --git a/src/Bindable.php b/src/Bindable.php index 0180c25..7e3746c 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -18,13 +18,23 @@ trait Bindable { * case they will all have their data set. */ public function bindKeyValue( - string $key, + ?string $key, string $value ):void { $this->injectBoundProperty($key, $value); $this->injectAttributePlaceholder($key, $value); } + /** + * Bind a single value to a data-bind element that has no matching + * attribute vale. For example,

Your text here

+ * does not have an addressable attribute value for data-bind:text. + */ + public function bindValue(string $value):void { + $this->bindKeyValue(null, $value); +// Note, it's impossible to inject attribute placeholders without a key. + } + /** * Bind multiple key-value-pairs within $this Element, calling * bindKeyValue for each key-value-pair in the iterable $kvp object. @@ -88,7 +98,32 @@ public function bindList( * correct template. */ public function bindNestedList(iterable $data):void { + /** @var BaseElement $element */ + $element = $this; + if($element instanceof HTMLDocument) { + $element = $element->documentElement; + } + /** @var HTMLDocument $document */ + $document = $element->ownerDocument; + + $baseListElement = $document->getParentOfUnnamedTemplate($element); + foreach($data as $key => $value) { + $t = $document->getUnnamedTemplate( + $baseListElement, + false + ); + + if(is_string($key)) { + $t->bindValue($key); + } + + $insertedTemplate = $baseListElement->appendChild($t); + + if(is_iterable($value)) { + $insertedTemplate->bindNestedList($value); + } + } } /** @@ -97,7 +132,7 @@ public function bindNestedList(iterable $data):void { * into the according property value. */ protected function injectBoundProperty( - string $key, + ?string $key, string $value ):void { $children = $this->getChildrenWithBindAttribute(); @@ -151,8 +186,8 @@ protected function injectBoundProperty( */ protected function getKeyToSet( BaseAttr $attr - ):string { - $keyToSet = $attr->value; + ):?string { + $keyToSet = $attr->value ?: null; if($keyToSet[0] === "@") { $lookupAttribute = substr($keyToSet, 1); @@ -171,7 +206,7 @@ protected function getKeyToSet( } protected function injectAttributePlaceholder( - string $key, + ?string $key, string $value ):void { /** @var BaseElement $element */ diff --git a/src/HTMLDocument.php b/src/HTMLDocument.php index 778c775..39eb499 100644 --- a/src/HTMLDocument.php +++ b/src/HTMLDocument.php @@ -81,7 +81,34 @@ public function getUnnamedTemplate( throw new NamelessTemplateSpecificityException(); } - return $matches[0] ?? null; + if(!isset($matches[0])) { + return null; + } + + return $matches[0]->cloneNode(true); + } + + public function getParentOfUnnamedTemplate(Element $element):?Element { + $path = $element->getNodePath(); +// Unnamed templates can't have sibling elements of the same path, otherwise +// they would need to be named. Remove any index from the path. + $path = preg_replace("/\[\d+\]/", "", $path); + $matches = []; + + foreach($this->templateFragmentMap as $name => $t) { + if(strpos($name, $path) !== 0) { + continue; + } + + $pathToReturn = substr( + $name, + 0, + strrpos($name, "/") + ); + return $this->xPath($pathToReturn)[0] ?? null; + } + + return null; } /** diff --git a/src/TemplateParent.php b/src/TemplateParent.php index 5328772..e1cd260 100644 --- a/src/TemplateParent.php +++ b/src/TemplateParent.php @@ -6,14 +6,19 @@ use Gt\Dom\Element as BaseElement; trait TemplateParent { - public function extractTemplates():int { + public function extractTemplates(BaseElement $context = null):int { + if(is_null($context)) { + $context = $this; + } + $i = null; /** @var HTMLCollection $templateElementList */ - $templateElementList = $this->querySelectorAll( + $templateElementList = $context->querySelectorAll( "template,[data-template]" ); - foreach($templateElementList as $i => $templateElement) { + for($i = count($templateElementList) - 1; $i >= 0; $i--) { + $templateElement = $templateElementList[$i]; $name = $this->getTemplateNameFromElement($templateElement); $parentNode = $templateElement->parentNode; @@ -21,6 +26,13 @@ public function extractTemplates():int { $previousSibling = $templateElement->previousSibling; $templateNodePath = $templateElement->getNodePath(); + $nestedTemplateElementList = $templateElement->querySelectorAll( + "template,[data-template]" + ); + foreach($nestedTemplateElementList as $nestedTemplateElement) { + $this->extractTemplates($nestedTemplateElement); + } + $document = ($this instanceof DOMDocument) ? $this : $this->ownerDocument; @@ -56,6 +68,8 @@ public function extractTemplates():int { $templateElement->classList->add("t-$name"); } + ksort($this->templateFragmentMap); + if(is_null($i)) { return 0; } diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index 07c7a39..06cdf91 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -298,6 +298,8 @@ public function testBindNestedList() { $document->extractTemplates(); $document->bindNestedList(Helper::LIST_MUSIC); + echo $document; + foreach(Helper::LIST_MUSIC as $artistName => $albumList) { $domArtist = $document->querySelector("[data-artist-name='$artistName']"); $h2 = $domArtist->querySelector("h2"); diff --git a/test/unit/Helper/Helper.php b/test/unit/Helper/Helper.php index 4e2c950..1ff14df 100644 --- a/test/unit/Helper/Helper.php +++ b/test/unit/Helper/Helper.php @@ -442,16 +442,16 @@ class Helper {

Music list!

    -
  • -

    Artist name

    +
  • +

    Artist name

      -
    • -

      Album name

      +
    • +

      Album name

        -
      1. - Track name +
      2. + Track name
    • From 6bf5d241822559d23179aa1cbb57ee20ef63499b Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 17 Jun 2019 10:24:18 +0100 Subject: [PATCH 13/18] Test binding an iterable object --- src/Element.php | 1 + src/HTMLDocument.php | 13 ++++++++++++- src/TemplateParent.php | 12 ++++++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Element.php b/src/Element.php index 70f7313..98f958a 100644 --- a/src/Element.php +++ b/src/Element.php @@ -4,6 +4,7 @@ use DOMAttr; use DOMNode; use Gt\Dom\Element as BaseElement; +use Gt\Dom\PropertyAttribute; /** * @property-read Attr[] $attributes diff --git a/src/HTMLDocument.php b/src/HTMLDocument.php index 39eb499..dd37eb6 100644 --- a/src/HTMLDocument.php +++ b/src/HTMLDocument.php @@ -50,6 +50,7 @@ public function getNamedTemplate(string $name):?DocumentFragment { $fragment = $this->templateFragmentMap[$name] ?? null; if($fragment) { + /** @var DocumentFragment $clone */ $clone = $fragment->cloneNode(true); $clone->setTemplateProperties( $fragment->templateParentNode, @@ -85,7 +86,17 @@ public function getUnnamedTemplate( return null; } - return $matches[0]->cloneNode(true); + /** @var DocumentFragment $fragment */ + $fragment = $matches[0]; + /** @var DocumentFragment $clone */ + $clone = $fragment->cloneNode(true); + $clone->setTemplateProperties( + $fragment->templateParentNode, + $fragment->templateNextSibling, + $fragment->templatePreviousSibling + ); + + return $clone; } public function getParentOfUnnamedTemplate(Element $element):?Element { diff --git a/src/TemplateParent.php b/src/TemplateParent.php index e1cd260..a7a2c53 100644 --- a/src/TemplateParent.php +++ b/src/TemplateParent.php @@ -17,7 +17,9 @@ public function extractTemplates(BaseElement $context = null):int { "template,[data-template]" ); - for($i = count($templateElementList) - 1; $i >= 0; $i--) { + $count = count($templateElementList) - 1; + + for($i = $count; $i >= 0; $i--) { $templateElement = $templateElementList[$i]; $name = $this->getTemplateNameFromElement($templateElement); @@ -68,13 +70,15 @@ public function extractTemplates(BaseElement $context = null):int { $templateElement->classList->add("t-$name"); } - ksort($this->templateFragmentMap); + if($this instanceof HTMLDocument) { + ksort($this->templateFragmentMap); + } - if(is_null($i)) { + if(is_null($count)) { return 0; } - return $i + 1; + return $count + 1; } public function getTemplate( From 6652df33285dcc7202ee6d9cc360a8e8ab1c91c7 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 17 Jun 2019 10:24:56 +0100 Subject: [PATCH 14/18] Pass tests so far --- test/unit/BindableTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index 06cdf91..74e0686 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -3,13 +3,9 @@ use Gt\DomTemplate\BoundAttributeDoesNotExistException; use Gt\DomTemplate\BoundDataNotSetException; -use Gt\DomTemplate\DomTemplateException; use Gt\DomTemplate\HTMLDocument; use Gt\DomTemplate\NamelessTemplateSpecificityException; use Gt\DomTemplate\Test\Helper\Helper; -use Gt\DomTemplate\Test\Helper\TodoListExampleObject; -use PHPUnit\TextUI\Help; -use stdClass; class BindableTest extends TestCase { public function testBindMethodAvailable() { From 0c1127434372b262c9ad1c544afcebd72e8b6bb5 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 17 Jun 2019 14:21:59 +0100 Subject: [PATCH 15/18] Pass all tests --- src/Bindable.php | 38 ++++++++++++++++++++++++++++++------- src/HTMLDocument.php | 32 +++++++++++++++++++++++++++---- test/unit/BindableTest.php | 9 ++++----- test/unit/Helper/Helper.php | 8 ++++---- 4 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/Bindable.php b/src/Bindable.php index 7e3746c..cf36a90 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -81,7 +81,11 @@ public function bindList( foreach($kvpList as $data) { if(is_null($templateName)) { - $t = $document->getUnnamedTemplate($element); + $t = $document->getUnnamedTemplate( + $element, + true, + false + ); } else { $t = $document->getNamedTemplate($templateName); @@ -97,7 +101,10 @@ public function bindList( * BindIterator is necessary to link each child list with the * correct template. */ - public function bindNestedList(iterable $data):void { + public function bindNestedList( + iterable $data, + bool $requireMatchingTemplatePath = false + ):void { /** @var BaseElement $element */ $element = $this; if($element instanceof HTMLDocument) { @@ -106,23 +113,40 @@ public function bindNestedList(iterable $data):void { /** @var HTMLDocument $document */ $document = $element->ownerDocument; - $baseListElement = $document->getParentOfUnnamedTemplate($element); + $templateParent = $document->getParentOfUnnamedTemplate( + $element, + $requireMatchingTemplatePath + ); foreach($data as $key => $value) { $t = $document->getUnnamedTemplate( - $baseListElement, + $templateParent, false ); + if($t) { if(is_string($key)) { $t->bindValue($key); } - $insertedTemplate = $baseListElement->appendChild($t); - if(is_iterable($value)) { - $insertedTemplate->bindNestedList($value); + + if(is_string($value)) { + $t->bindValue($value); + } + + $insertedTemplate = $templateParent->appendChild($t); + + if(is_iterable($value)) { + $insertedTemplate->bindNestedList( + $value, + true + ); + } } + + + } } diff --git a/src/HTMLDocument.php b/src/HTMLDocument.php index dd37eb6..2c67a51 100644 --- a/src/HTMLDocument.php +++ b/src/HTMLDocument.php @@ -66,9 +66,15 @@ public function getNamedTemplate(string $name):?DocumentFragment { public function getUnnamedTemplate( Element $element, - bool $throwIfMoreThanOneMatch = true + bool $throwIfMoreThanOneMatch = true, + bool $stripArraySyntax = true ):?DocumentFragment { $path = $element->getNodePath(); +// Unnamed templates can't have sibling elements of the same path, otherwise +// they would need to be named. Remove any index from the path. + if($stripArraySyntax) { + $path = preg_replace("/\[\d+\]/", "", $path); + } $matches = []; foreach($this->templateFragmentMap as $name => $t) { @@ -99,12 +105,16 @@ public function getUnnamedTemplate( return $clone; } - public function getParentOfUnnamedTemplate(Element $element):?Element { + public function getParentOfUnnamedTemplate( + Element $element, + bool $requireMatchingPath = false + ):?Element { $path = $element->getNodePath(); // Unnamed templates can't have sibling elements of the same path, otherwise // they would need to be named. Remove any index from the path. $path = preg_replace("/\[\d+\]/", "", $path); $matches = []; + $pathToReturn = null; foreach($this->templateFragmentMap as $name => $t) { if(strpos($name, $path) !== 0) { @@ -116,10 +126,24 @@ public function getParentOfUnnamedTemplate(Element $element):?Element { 0, strrpos($name, "/") ); - return $this->xPath($pathToReturn)[0] ?? null; + + if($requireMatchingPath) { + if(strpos($name, $path) === 0 + && $path !== $name) { + break; + } + } + else { + break; + } } - return null; + if(!$pathToReturn) { + return null; + } + + $matchingElements = $this->xPath($pathToReturn); + return $matchingElements[count($matchingElements) - 1]; } /** diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index 74e0686..880b57e 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -279,8 +279,8 @@ public function testBindListMultipleDataTemplateElements() { $firstList = $document->getElementById("list-1"); $secondList = $document->getElementById("list-2"); - $firstList->bindList($stateList); $secondList->bindList($ministryList); + $firstList->bindList($stateList); self::assertCount(count($stateList), $firstList->children); self::assertCount(count($ministryList), $secondList->children); @@ -294,8 +294,6 @@ public function testBindNestedList() { $document->extractTemplates(); $document->bindNestedList(Helper::LIST_MUSIC); - echo $document; - foreach(Helper::LIST_MUSIC as $artistName => $albumList) { $domArtist = $document->querySelector("[data-artist-name='$artistName']"); $h2 = $domArtist->querySelector("h2"); @@ -308,8 +306,9 @@ public function testBindNestedList() { foreach($trackList as $i => $trackName) { $domTrack = $domAlbum->querySelector("[data-track-name='$trackName']"); - self::assertSame($domTrack, $domAlbum->children[$i]); - self::assertEquals($trackName, $domTrack->innerText); + self::assertStringContainsString($trackName, $domTrack->innerText); + $child = $domAlbum->querySelector("ol")->children[$i]; + self::assertSame($domTrack, $child); } } } diff --git a/test/unit/Helper/Helper.php b/test/unit/Helper/Helper.php index 1ff14df..2ae40d2 100644 --- a/test/unit/Helper/Helper.php +++ b/test/unit/Helper/Helper.php @@ -464,14 +464,14 @@ class Helper { const LIST_MUSIC = [ "A Band From Your Childhood" => [ "This Album is Good" => [ - "The Best Song You've Ever Heard", + "The Best Song You‘ve Ever Heard", "Another Cracking Tune", "Top Notch Music Here", - "The Best Is Left 'Till Last", + "The Best Is Left ‘Til Last", ], "Adequate Collection" => [ "Meh", - "'sok", + "‘sok", "Sounds Like Every Other Song", ], ], @@ -484,7 +484,7 @@ class Helper { "Meat" => [ "Steak", "Is Chicken Really a Meat?", - "Don't Look in the Sausage Factory", + "Don‘t Look in the Sausage Factory", "Stop Horsing Around", ], "SnaxX" => [ From 9bc2467552f16c656667d62eea7e704b93a3a1a3 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 17 Jun 2019 14:32:44 +0100 Subject: [PATCH 16/18] Remove dev dependency --- composer.json | 4 +- composer.lock | 140 ++++++++++++++++++++++---------------------------- 2 files changed, 63 insertions(+), 81 deletions(-) diff --git a/composer.json b/composer.json index 83aca01..06d8147 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "license": "MIT", "require": { - "phpgt/dom": "1.*", - "ext-dom": "*" + "ext-dom": "*", + "phpgt/dom": "*" }, "require-dev": { "phpunit/phpunit": "8.*" diff --git a/composer.lock b/composer.lock index e348995..666b7d0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,62 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6f97b94242e6d134e9f931b72f613467", + "content-hash": "fec06242b81ae24129a62e07b56ecde5", "packages": [ + { + "name": "phpgt/cssxpath", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/PhpGt/CssXPath.git", + "reference": "60554671449ab5068e0171116e68d05aec7c0647" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PhpGt/CssXPath/zipball/60554671449ab5068e0171116e68d05aec7c0647", + "reference": "60554671449ab5068e0171116e68d05aec7c0647", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpgt/dom": "*", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gt\\CssXPath\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Convert CSS selectors to XPath queries.", + "time": "2019-06-14T15:09:28+00:00" + }, { "name": "phpgt/dom", - "version": "v1.1.2", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/PhpGt/Dom.git", - "reference": "12011a45fad9930306466dbc5d09da6d90380d1a" + "reference": "4eb31b11e9e3b279068dca3f2efad1e46209ff55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/Dom/zipball/12011a45fad9930306466dbc5d09da6d90380d1a", - "reference": "12011a45fad9930306466dbc5d09da6d90380d1a", + "url": "https://api.github.com/repos/PhpGt/Dom/zipball/4eb31b11e9e3b279068dca3f2efad1e46209ff55", + "reference": "4eb31b11e9e3b279068dca3f2efad1e46209ff55", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-mbstring": "*", - "php": ">=7.2.0", - "symfony/css-selector": "3.2.14" + "php": ">=7.2", + "phpgt/cssxpath": "*" }, "require-dev": { "phpunit/phpunit": "8.*" @@ -83,60 +118,7 @@ } ], "description": "The modern DOM API for PHP 7 projects.", - "time": "2019-06-10T08:13:02+00:00" - }, - { - "name": "symfony/css-selector", - "version": "v3.2.14", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "02983c144038e697c959e6b06ef6666de759ccbc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/02983c144038e697c959e6b06ef6666de759ccbc", - "reference": "02983c144038e697c959e6b06ef6666de759ccbc", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony CssSelector Component", - "homepage": "https://symfony.com", - "time": "2017-05-01T14:55:58+00:00" + "time": "2019-06-17T08:24:24+00:00" } ], "packages-dev": [ @@ -500,16 +482,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.8.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" + "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", - "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76", + "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76", "shasum": "" }, "require": { @@ -530,8 +512,8 @@ } }, "autoload": { - "psr-0": { - "Prophecy\\": "src/" + "psr-4": { + "Prophecy\\": "src/Prophecy" } }, "notification-url": "https://packagist.org/downloads/", @@ -559,7 +541,7 @@ "spy", "stub" ], - "time": "2018-08-05T17:53:17+00:00" + "time": "2019-06-13T12:50:23+00:00" }, { "name": "phpunit/php-code-coverage", @@ -815,16 +797,16 @@ }, { "name": "phpunit/phpunit", - "version": "8.2.1", + "version": "8.2.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "047f771e34dccacb6c432a1a70e9980e087eac92" + "reference": "24b6cfcec34c1167ee1d90b7cb22bee324af319f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/047f771e34dccacb6c432a1a70e9980e087eac92", - "reference": "047f771e34dccacb6c432a1a70e9980e087eac92", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/24b6cfcec34c1167ee1d90b7cb22bee324af319f", + "reference": "24b6cfcec34c1167ee1d90b7cb22bee324af319f", "shasum": "" }, "require": { @@ -839,7 +821,7 @@ "phar-io/manifest": "^1.0.3", "phar-io/version": "^2.0.1", "php": "^7.2", - "phpspec/prophecy": "^1.8.0", + "phpspec/prophecy": "^1.8.1", "phpunit/php-code-coverage": "^7.0.5", "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", @@ -894,7 +876,7 @@ "testing", "xunit" ], - "time": "2019-06-07T14:04:13+00:00" + "time": "2019-06-15T07:25:54+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1571,16 +1553,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "1c42705be2b6c1de5904f8afacef5895cab44bf8" + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/1c42705be2b6c1de5904f8afacef5895cab44bf8", - "reference": "1c42705be2b6c1de5904f8afacef5895cab44bf8", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", "shasum": "" }, "require": { @@ -1607,7 +1589,7 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2019-04-04T09:56:43+00:00" + "time": "2019-06-13T22:48:21+00:00" }, { "name": "webmozart/assert", From fdd17e53b9d3012f00552a649704be81dc439c87 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 17 Jun 2019 20:06:08 +0100 Subject: [PATCH 17/18] Remove obsolete null check --- src/Bindable.php | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/Bindable.php b/src/Bindable.php index cf36a90..0a320ae 100644 --- a/src/Bindable.php +++ b/src/Bindable.php @@ -124,29 +124,22 @@ public function bindNestedList( false ); - if($t) { if(is_string($key)) { $t->bindValue($key); } - - - if(is_string($value)) { - $t->bindValue($value); - } - - $insertedTemplate = $templateParent->appendChild($t); - - if(is_iterable($value)) { - $insertedTemplate->bindNestedList( - $value, - true - ); - } + if(is_string($value)) { + $t->bindValue($value); } + $insertedTemplate = $templateParent->appendChild($t); - + if(is_iterable($value)) { + $insertedTemplate->bindNestedList( + $value, + true + ); + } } } From 74b209988b22a9de1ca128cae8e89b1bcd7a2f6e Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 17 Jun 2019 20:14:17 +0100 Subject: [PATCH 18/18] Test binding non-iterable data, closes #54 --- test/unit/BindableTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/unit/BindableTest.php b/test/unit/BindableTest.php index 880b57e..92cf4fe 100644 --- a/test/unit/BindableTest.php +++ b/test/unit/BindableTest.php @@ -313,4 +313,33 @@ public function testBindNestedList() { } } } + + public function testBindNestedListWithBadData() { + $document = new HTMLDocument(Helper::HTML_MUSIC); + $document->extractTemplates(); + $data = Helper::LIST_MUSIC; + $data["Bongo and The Bronks"] = 123; + $document->bindNestedList($data); + + unset($data["Bongo and The Bronks"]); + + foreach($data as $artistName => $albumList) { + $domArtist = $document->querySelector("[data-artist-name='$artistName']"); + $h2 = $domArtist->querySelector("h2"); + self::assertEquals($artistName, $h2->innerText); + + foreach($albumList as $albumName => $trackList) { + $domAlbum = $domArtist->querySelector("[data-album-name='$albumName']"); + $h3 = $domAlbum->querySelector("h3"); + self::assertEquals($albumName, $h3->innerText); + + foreach($trackList as $i => $trackName) { + $domTrack = $domAlbum->querySelector("[data-track-name='$trackName']"); + self::assertStringContainsString($trackName, $domTrack->innerText); + $child = $domAlbum->querySelector("ol")->children[$i]; + self::assertSame($domTrack, $child); + } + } + } + } } \ No newline at end of file