Skip to content

Commit 28ce02a

Browse files
committed
Add support for v-for attributes on template tags
Sometimes it is useful to be able to iterate in a template without adding additional levels of HTML nesting to the DOM. Vue supports iterating with `v-for` attributes on `template` elements for this use case. Add support for `v-for` loops with `template` tags. Bug: T396098
1 parent edb6eca commit 28ce02a

File tree

3 files changed

+92
-20
lines changed

3 files changed

+92
-20
lines changed

src/Component.php

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,40 @@ private function convertDataValueToString( $value ) {
9494
return json_encode( $value );
9595
}
9696

97+
private function safeModifyChildren( DOMNode $parent, DOMNode $oldNode, array $newNodes, bool $insert = false ) {
98+
// TODO To work around the double-free, we detach all the children of the parent node and
99+
// re-attach them in the correct sequence, replacing the target node with our newly-imported
100+
// node. Once `mwcli` has moved off this outdated version of PHP (T388411) we should be able
101+
// to remove this workaround. T398821
102+
$children = [];
103+
foreach ( iterator_to_array( $parent->childNodes ) as $child ) {
104+
if ( $child === $oldNode ) {
105+
$children = array_merge( $children, $newNodes );
106+
}
107+
if ( $insert || $child !== $oldNode ) {
108+
$children[] = $child;
109+
}
110+
$child->remove();
111+
}
112+
113+
foreach ( $children as $child ) {
114+
$parent->appendChild( $child );
115+
}
116+
}
117+
118+
private function safeReplaceNode( DOMNode $parent, DOMNode $oldNode, array $newNodes ) {
119+
$this->safeModifyChildren( $parent, $oldNode, $newNodes, false );
120+
}
121+
122+
private function safeInsertBefore( DOMNode $parent, DOMNode $oldNode, array $newNodes ) {
123+
$this->safeModifyChildren( $parent, $oldNode, $newNodes, true );
124+
}
125+
126+
private function replaceNodeWithChildren( DOMNode $node ) {
127+
$children = iterator_to_array( $node->childNodes );
128+
$this->safeReplaceNode( $node->parentNode, $node, $children );
129+
}
130+
97131
/**
98132
* @param DOMNode $node
99133
* @param array $data
@@ -145,24 +179,7 @@ private function handleComponent( DOMElement $node, array $data ): bool {
145179
if ( $node != $importNode ) {
146180
$node->replaceWith( $importNode );
147181
} else {
148-
// TODO To work around the double-free, we detach all the children of the parent node and
149-
// re-attach them in the correct sequence, replacing the target node with our newly-imported
150-
// node. Once `mwcli` has moved off this outdated version of PHP (T388411) we should be able
151-
// to remove this workaround. T398821
152-
$parent = $node->parentNode;
153-
$children = [];
154-
foreach ( iterator_to_array( $parent->childNodes ) as $child ) {
155-
if ( $child !== $node ) {
156-
$children[] = $child;
157-
} else {
158-
$children[] = $importNode;
159-
}
160-
$child->remove();
161-
}
162-
163-
foreach ( $children as $child ) {
164-
$parent->appendChild( $child );
165-
}
182+
$this->safeReplaceNode( $node->parentNode, $node, [ $importNode ] );
166183
}
167184
return true;
168185
}
@@ -286,14 +303,18 @@ private function handleFor( DOMNode $node, array $data ) {
286303

287304
/** @var DOMElement $node */
288305
if ( $node->hasAttribute( 'v-for' ) ) {
306+
$parentNode = $node->parentNode;
289307
list( $itemName, $listName ) = explode( ' in ', $node->getAttribute( 'v-for' ) );
290308
$node->removeAttribute( 'v-for' );
291309
$node->removeAttribute( ':key' );
292310

293311
foreach ( $this->app->evaluateExpression( $listName, $data ) as $item ) {
294312
$newNode = $node->cloneNode( true );
295-
$node->parentNode->insertBefore( $newNode, $node );
313+
$this->safeInsertBefore( $parentNode, $node, [ $newNode ] );
296314
$this->handleNode( $newNode, array_merge( $data, [ $itemName => $item ] ) );
315+
if ( $newNode->tagName === 'template' ) {
316+
$this->replaceNodeWithChildren( $newNode );
317+
}
297318
}
298319

299320
$this->removeNode( $node );
@@ -324,7 +345,9 @@ private function handleRawHtml( DOMNode $node, array $data ) {
324345
}
325346

326347
private function removeNode( DOMElement $node ) {
327-
$node->parentNode->removeChild( $node );
348+
if ( $node->parentNode ) {
349+
$node->parentNode->removeChild( $node );
350+
}
328351
}
329352

330353
/**
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template id="template" type="text/x-template">
2+
<div>
3+
<p></p>
4+
<div v-html="link"></div>
5+
<template v-for="item in items">
6+
<p>{{ item }}</p>
7+
</template>
8+
</div>
9+
</template>
10+
<script id="data" type="application/json">
11+
{"condition":true , "link":"<a href=\"URL\">link</a>", "items": [ 1, 2, 3 ] }
12+
</script>
13+
<div id="result">
14+
<!-- generated by `npm run-script populate-fixtures` -->
15+
<div><p></p><div><a href="URL">link</a></div><!--[--><p>1</p><p>2</p><p>3</p><!--]--></div>
16+
</div>

tests/php/TemplatingTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,39 @@ public function testTemplateWithForLoopAndSingleElementInArrayToIterate_Rendered
255255
$this->assertSame( '<p><a></a></p>', $result );
256256
}
257257

258+
public function testTemplateWithForLoopUsingTemplateElement_DropsTemplateTags() {
259+
$result = $this->createAndRender(
260+
'<p><template v-for="item in list">{{ item }}</template></p>',
261+
[ 'list' => [ 1, 2 ] ]
262+
);
263+
264+
$this->assertSame( '<p>12</p>', $result );
265+
}
266+
267+
public function testTemplateWithNestedForLoopUsingTemplateElement_DropsTemplateTags() {
268+
$result = $this->createAndRender(
269+
'<p><template v-for="sublist in list">' .
270+
'<template v-for="item in sublist">{{item}}</template></template></p>',
271+
[ 'list' => [ [ 1, 2 ], [ 3, 4 ] ] ]
272+
);
273+
274+
$this->assertSame( '<p>1234</p>', $result );
275+
}
276+
277+
public function testTemplateWithForLoopUsingTemplateElement_RendersMultipleChildren() {
278+
$result = $this->createAndRender(
279+
'<dl><template v-for="item in list">' .
280+
'<dt>{{item.dt}}</dt><dd>{{item.dd}}</dd></template></dl>',
281+
[ 'list' => [
282+
[ 'dt' => 'cat', 'dd' => 'a feline animal' ],
283+
[ 'dt' => 'dog', 'dd' => 'a canine animal' ],
284+
] ]
285+
);
286+
287+
$this->assertSame( '<dl><dt>cat</dt><dd>a feline animal</dd>' .
288+
'<dt>dog</dt><dd>a canine animal</dd></dl>', $result );
289+
}
290+
258291
public function testTemplateWithForLoopAndMemberExpressionForData_RenderedOnce() {
259292
$result = $this->createAndRender(
260293
'<p><a v-for="item in list.data"></a></p>',

0 commit comments

Comments
 (0)