Skip to content

Commit a039aac

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 a039aac

File tree

3 files changed

+73
-20
lines changed

3 files changed

+73
-20
lines changed

src/Component.php

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

97+
private function safeModifyChildren( DOMNode $parent, DOMNode $oldNode, DOMNode $newNode, 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[] = $newNode;
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, DOMNode $newNode ) {
119+
$this->safeModifyChildren( $parent, $oldNode, $newNode, false );
120+
}
121+
122+
private function safeInsertBefore( DOMNode $parent, DOMNode $oldNode, DOMNode $newNode ) {
123+
$this->safeModifyChildren( $parent, $oldNode, $newNode, true );
124+
}
125+
97126
/**
98127
* @param DOMNode $node
99128
* @param array $data
@@ -145,24 +174,7 @@ private function handleComponent( DOMElement $node, array $data ): bool {
145174
if ( $node != $importNode ) {
146175
$node->replaceWith( $importNode );
147176
} 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-
}
177+
$this->safeReplaceNode( $node->parentNode, $node, $importNode );
166178
}
167179
return true;
168180
}
@@ -286,13 +298,17 @@ private function handleFor( DOMNode $node, array $data ) {
286298

287299
/** @var DOMElement $node */
288300
if ( $node->hasAttribute( 'v-for' ) ) {
301+
$parentNode = $node->parentNode;
289302
list( $itemName, $listName ) = explode( ' in ', $node->getAttribute( 'v-for' ) );
290303
$node->removeAttribute( 'v-for' );
291304
$node->removeAttribute( ':key' );
292305

293306
foreach ( $this->app->evaluateExpression( $listName, $data ) as $item ) {
294307
$newNode = $node->cloneNode( true );
295-
$node->parentNode->insertBefore( $newNode, $node );
308+
if ( $newNode->tagName === 'template' ) {
309+
$newNode = $newNode->firstChild->cloneNode( true );
310+
}
311+
$this->safeInsertBefore( $parentNode, $node, $newNode );
296312
$this->handleNode( $newNode, array_merge( $data, [ $itemName => $item ] ) );
297313
}
298314

@@ -324,7 +340,9 @@ private function handleRawHtml( DOMNode $node, array $data ) {
324340
}
325341

326342
private function removeNode( DOMElement $node ) {
327-
$node->parentNode->removeChild( $node );
343+
if ( $node->parentNode ) {
344+
$node->parentNode->removeChild( $node );
345+
}
328346
}
329347

330348
/**
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,25 @@ 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+
258277
public function testTemplateWithForLoopAndMemberExpressionForData_RenderedOnce() {
259278
$result = $this->createAndRender(
260279
'<p><a v-for="item in list.data"></a></p>',

0 commit comments

Comments
 (0)