Skip to content

Commit 8f659b1

Browse files
committed
feat: added parameter-less constructors for element classes, allow cross-document appending, improve test coverage
1 parent 49f18ad commit 8f659b1

12 files changed

Lines changed: 648 additions & 483 deletions

File tree

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,13 @@ composer require --dev vardumper/extended-htmldocument
3232

3333
## TL;DR
3434

35-
This library adds the HTML5 specification to PHP and is fully compatible with `DOM\HTMLDocument`. You can create an `Anchor()` object and append it to any `DOM\Document`.
35+
This library adds the HTML5 specification to PHP and is fully compatible with `DOM\HTMLDocument`. You can now instantiate an `Anchor` without passing a document.
3636

3737
```php
38-
use Html\Delegator\HTMLDocumentDelegator as HTMLDocument;
3938
use Html\Element\Inline\Anchor;
39+
use Html\Enum\RelEnum;
4040

41-
$dom = HTMLDocument::createEmpty()
42-
echo (string) Anchor::create($dom)
41+
echo (new Anchor())
4342
->setClass('secondary')
4443
->setRel(RelEnum::NOFOLLOW)
4544
->setHref('https://google.com')

clover.xml

Lines changed: 496 additions & 441 deletions
Large diffs are not rendered by default.

docs/frameworks/symfony.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ There are several different ways one can use this library in Symfony. One can:
88

99
## Using the HTML5 Element PHP classes and Enums
1010

11-
This library adds the HTML5 specification to PHP and is fully compatible with `DOM\HTMLDocument`. You can create an `Anchor()` object and append it to any `DOM\Document`.
11+
This library adds the HTML5 specification to PHP and is fully compatible with `DOM\HTMLDocument`. You can now instantiate an `Anchor` without passing a document.
1212

1313
```php
14-
use Html\Delegator\HTMLDocumentDelegator as HTMLDocument;
1514
use Html\Element\Inline\Anchor;
15+
use Html\Enum\RelEnum;
1616

17-
$dom = HTMLDocument::createEmpty()
18-
echo (string) Anchor::create($dom)
17+
echo (new Anchor())
1918
->setClass('secondary')
2019
->setRel(RelEnum::NOFOLLOW)
2120
->setHref('https://google.com')

docs/frameworks/yii.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ composer require vardumper/extended-htmldocument
88
## Use in PHP DOM\HTMLDocument
99

1010
```php
11-
use Html\Delegator\HTMLDocumentDelegator as HTMLDocument;
1211
use Html\Element\Inline\Anchor;
12+
use Html\Enum\RelEnum;
1313

14-
$dom = HTMLDocument::createEmpty()
15-
echo (string) Anchor::create($dom)
14+
echo (new Anchor())
1615
->setClass('secondary')
1716
->setRel(RelEnum::NOFOLLOW)
1817
->setHref('https://google.com')

docs/getting-started.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ composer require --dev vardumper/extended-htmldocument
1111

1212
```php [This library]
1313
// Extended HTMLDocument library
14-
$dom = \Html\Delegator\HTMLDocumentDelegator::createEmpty();
15-
echo (string) (\Html\Element\Inline\Anchor::create($dom))
14+
echo (new \Html\Element\Inline\Anchor())
1615
->setHref('https://example.com')
1716
->setTitle('Some info about the link')
1817
->setRel('nofollow')

docs/usage-examples.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ tbd add a gif
1212
This part is currently work in progress. The information below might be outdated, incomplete or incorrect.
1313
:::
1414

15-
### Creation via `create()` method
15+
### Creation via constructor
1616

1717
```php{4}
18-
$dom = Html\Delegator\HTMLDocumentDelegator::createEmpty();
19-
20-
$anchor = Html\Element\Inline\Anchor::create($dom);
18+
$anchor = new Html\Element\Inline\Anchor();
2119
$anchor->textContent = 'This is a test link.';
2220
$anchor->setAttributes([
2321
'href' => 'https://www.example.com',
@@ -28,7 +26,7 @@ echo $anchor;
2826
// or: $anchor->__toString();
2927
```
3028

31-
In the example above, we're passing an instance of the HTMLDocumentDelegator to the create method, which we make use of three new methods HTMLElementDelegator `create()`, `setAttributes()` and `__toString()` as well as BackedEnums for static values.
29+
In the example above, we instantiate the element directly and make use of `setAttributes()` and `__toString()` as well as BackedEnums for static values.
3230

3331
The output will be this:
3432
```html{4}

src/Delegator/HTMLElementDelegator.php

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,24 @@ class HTMLElementDelegator implements HTMLElementDelegatorInterface
5757

5858
public static array $parentOf = []; // Default value, change as needed
5959

60+
public HTMLElement|Element $delegated;
61+
6062
public function __construct(
61-
public readonly HTMLElement|Element $delegated,
63+
HTMLElement|Element|null $delegated = null,
6264
public ?TemplateGeneratorInterface $renderer = null
6365
) {
66+
if ($delegated === null) {
67+
if (! \defined(static::class . '::QUALIFIED_NAME')) {
68+
throw new InvalidArgumentException(
69+
'Cannot instantiate ' . self::class . ' without a delegated node; instantiate a concrete Html\\Element\\* class instead.'
70+
);
71+
}
72+
$document = HTMLDocumentDelegator::createEmpty();
73+
static::$ownerDocument = $document;
74+
$delegated = $document->delegated->createElement(static::getQualifiedName());
75+
}
76+
77+
$this->delegated = $delegated;
6478
if ($renderer !== null && ! $renderer->canRenderElements()) {
6579
throw new InvalidArgumentException('The given renderer cannot render elements.');
6680
}
@@ -132,10 +146,19 @@ public function appendChild(HTMLElementDelegatorInterface|TextDelegator $child):
132146
// }
133147

134148
if ($child->getOwnerDocument() !== $this->getOwnerDocument()) {
135-
/** @todo the child could be imported here */
136-
throw new InvalidArgumentException(
137-
'The child element must belong to the same document as the parent element.'
138-
);
149+
$owner = $this->delegated->ownerDocument;
150+
if (! $owner instanceof \DOM\HTMLDocument) {
151+
throw new RuntimeException('No owner document available for this element.');
152+
}
153+
154+
$imported = $owner->importNode($child->delegated, true);
155+
$this->delegated->appendChild($imported);
156+
157+
if ($child instanceof self && ($imported instanceof \DOM\HTMLElement || $imported instanceof Element)) {
158+
$child->delegated = $imported;
159+
}
160+
161+
return $this;
139162
}
140163

141164
$this->delegated->appendChild($child->delegated);
@@ -144,8 +167,21 @@ public function appendChild(HTMLElementDelegatorInterface|TextDelegator $child):
144167

145168
public function removeChild(HTMLElementDelegatorInterface|TextDelegatorInterface|Text $child): static
146169
{
147-
if (! \property_exists($child, 'ownerDocument')) {
148-
throw new Exception('The child element must be an instance of HTMLElementDelegatorInterface or Text.');
170+
if ($child instanceof Text) {
171+
$owner = $child->ownerDocument;
172+
if (! $owner instanceof \DOM\HTMLDocument) {
173+
throw new RuntimeException('No owner document available for this text node.');
174+
}
175+
176+
if (HTMLDocumentDelegator::getInstance($owner) !== $this->getOwnerDocument()) {
177+
/** @todo the child could be imported here */
178+
throw new InvalidArgumentException(
179+
'The child element must belong to the same document as the parent element.'
180+
);
181+
}
182+
183+
$this->delegated->removeChild($child);
184+
return $this;
149185
}
150186

151187
if ($child->getOwnerDocument() !== $this->getOwnerDocument()) {
@@ -154,15 +190,8 @@ public function removeChild(HTMLElementDelegatorInterface|TextDelegatorInterface
154190
'The child element must belong to the same document as the parent element.'
155191
);
156192
}
157-
if ($child instanceof HTMLElementDelegatorInterface) {
158-
$this->delegated->removeChild($child->delegated);
159-
return $this;
160-
}
161-
if ($child instanceof TextDelegatorInterface) {
162-
$this->delegated->removeChild($child->delegated);
163-
return $this;
164-
}
165-
$this->delegated->removeChild($child);
193+
194+
$this->delegated->removeChild($child->delegated);
166195
return $this;
167196
}
168197

src/Interface/HTMLElementDelegatorInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717
interface HTMLElementDelegatorInterface
1818
{
19-
public function __construct(HTMLElement $delegated, ?TemplateGeneratorInterface $renderer = null);
19+
public function __construct(HTMLElement|\Dom\Element|null $delegated = null, ?TemplateGeneratorInterface $renderer = null);
2020

2121
public function __call($name, $arguments);
2222

tests/Delegator/HTMLElementDelegatorTest.php

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,25 @@
1212
use Html\TemplateGenerator\HTMLGenerator;
1313

1414
beforeEach(function () {
15-
$this->document = HTMLDocumentDelegator::createEmpty();
16-
$this->delegator = Anchor::create($this->document);
15+
$this->delegator = new Anchor();
16+
$this->document = $this->delegator->getOwnerDocument();
1717
});
1818

1919
test('constructor', function () {
2020
expect($this->delegator)->toBeInstanceOf(HTMLElementDelegator::class);
2121
});
2222

23+
test('can be instantiated without providing a document', function () {
24+
$anchor = new Anchor();
25+
26+
expect($anchor)->toBeInstanceOf(Anchor::class);
27+
expect(strtolower($anchor->delegated->tagName))->toBe('a');
28+
expect($anchor->getOwnerDocument())->toBeInstanceOf(HTMLDocumentDelegator::class);
29+
30+
$anchor->setHref('https://example.com');
31+
expect($anchor->getHref())->toBe('https://example.com');
32+
});
33+
2334
test('call get global attribute', function () {
2435
$this->delegator->setAttribute('id', 'test');
2536
expect($this->delegator->getAttribute('id'))
@@ -376,6 +387,27 @@
376387
->toBe(0);
377388
});
378389

390+
test('removeChild can remove TextDelegator', function () {
391+
$text = $this->document->createTextNode('hello');
392+
393+
$this->delegator->appendChild($text);
394+
expect($this->delegator->delegated->textContent)->toBe('hello');
395+
396+
$this->delegator->removeChild($text);
397+
expect($this->delegator->delegated->textContent)->toBe('');
398+
});
399+
400+
test('removeChild can remove native DOM\\Text', function () {
401+
$textDelegator = $this->document->createTextNode('raw');
402+
$nativeText = $textDelegator->delegated;
403+
404+
$this->delegator->delegated->appendChild($nativeText);
405+
expect($this->delegator->delegated->textContent)->toBe('raw');
406+
407+
$this->delegator->removeChild($nativeText);
408+
expect($this->delegator->delegated->textContent)->toBe('');
409+
});
410+
379411
test('replace child', function () {
380412
$child1 = Anchor::create($this->document);
381413
$child1->setTextContent('Original');
@@ -457,14 +489,19 @@ public function render($elementOrDocument): ?string
457489
->toEqual(RelEnum::NOFOLLOW);
458490
});
459491

460-
test('append child throws exception for different document', function () {
492+
test('append child imports node for different document', function () {
461493
$otherDocument = HTMLDocumentDelegator::createEmpty();
462-
$child = Anchor::create($otherDocument);
463-
464-
$this->expectException(InvalidArgumentException::class);
465-
$this->expectExceptionMessage('The child element must belong to the same document as the parent element.');
494+
$child = $otherDocument->createElement('span');
495+
$child->setTextContent('import-me');
466496

467497
$this->delegator->appendChild($child);
498+
499+
expect($child->getOwnerDocument())
500+
->toBe($this->document);
501+
expect($this->delegator->delegated->childNodes->length)
502+
->toBe(1);
503+
expect($this->delegator->delegated->firstChild)
504+
->toBe($child->delegated);
468505
});
469506

470507
test('remove child throws exception for different document', function () {

tests/Element/ClassTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
use Html\Delegator\HTMLDocumentDelegator;
55
use Html\Delegator\HTMLElementDelegator;
66
use Html\Delegator\NodeDelegator;
7+
use Html\Element\Block\ListItem;
8+
use Html\Element\Block\UnorderedList;
79
use Html\Element\Inline\Anchor;
810
use Html\Element\Inline\Input;
911
use Html\Element\InlineElement;
@@ -82,6 +84,36 @@
8284
->toBeInstanceOf(HTMLElement::class);
8385
});
8486

87+
test('appending an element created with its own document to a different document imports the node', function () {
88+
$ul = UnorderedList::create($this->document);
89+
90+
$li = new ListItem();
91+
$li->setTextContent('Item 1');
92+
93+
$ul->appendChild($li);
94+
95+
expect($li->getOwnerDocument())
96+
->toBe($this->document);
97+
expect((string) $ul)
98+
->toBe('<ul><li>Item 1</li></ul>');
99+
});
100+
101+
test('unordered list can accept list items from the same document', function () {
102+
$ul = UnorderedList::create($this->document);
103+
104+
$li1 = ListItem::create($this->document);
105+
$li1->setTextContent('One');
106+
$li2 = ListItem::create($this->document);
107+
$li2->setTextContent('Two');
108+
109+
$ul->appendChild($li1);
110+
$ul->appendChild($li2);
111+
112+
expect($ul->delegated->childNodes->length)->toBe(2);
113+
expect(strtolower($ul->delegated->firstChild->nodeName))->toBe('li');
114+
expect((string) $ul)->toBe('<ul><li>One</li><li>Two</li></ul>');
115+
});
116+
85117
test('setting properties via Setter', function () {
86118
$input = Input::create($this->document);
87119
$input->setType('text');

0 commit comments

Comments
 (0)