Skip to content

Commit f08ad1b

Browse files
committed
Handle complex expressions in for loops and member expressions
In order to render structured data in Vue templates, we need to be able to destructure more complex Javascript expressions both in the target of 'v-for' statments and in Javascript Member Expressions that use 'Computed' values. Here we add support for data structures that use hypens in key names `<div v-for="item in list['data-values']">`, or numeric key names `<div v-for="item in list[1]">`, or in the case where the lookup key is itself an identifier `{{ data[variable] }}``. Bug: T396098
1 parent 89cd93b commit f08ad1b

File tree

4 files changed

+80
-8
lines changed

4 files changed

+80
-8
lines changed

src/JsParsing/ComputedKey.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare( strict_types = 1 );
4+
5+
namespace WMDE\VueJsTemplating\JsParsing;
6+
7+
class ComputedKey implements ParsedExpression {
8+
9+
private ParsedExpression $expression;
10+
11+
public function __construct( ParsedExpression $expression ) {
12+
$this->expression = $expression;
13+
}
14+
15+
/**
16+
* @param array $data
17+
*
18+
* @return expression as evaluated in the context of the data
19+
*/
20+
public function evaluate( array $data ) {
21+
return $this->expression->evaluate( $data );
22+
}
23+
24+
}

src/JsParsing/PeastExpressionConverter.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,15 @@ protected function convertMemberExpression( MemberExpression $expression ) {
5050
$parts = [];
5151
while ( $expression !== null ) {
5252
if ( get_class( $expression ) === MemberExpression::class ) {
53-
$property = $expression->getProperty()->getName();
54-
array_unshift( $parts, $property );
53+
if ( $expression->getComputed() ) {
54+
array_unshift( $parts, new ComputedKey( $this->convertExpression( $expression->getProperty() ) ) );
55+
} else {
56+
$propertyName = $this->convertKeyToLiteral( $expression->getProperty() );
57+
array_unshift( $parts, new StringLiteral( $propertyName ) );
58+
}
5559
$expression = $expression->getObject();
5660
} elseif ( get_class( $expression ) === Identifier::class ) {
57-
array_unshift( $parts, $expression->getName() );
61+
array_unshift( $parts, new StringLiteral( $expression->getName() ) );
5862
$expression = null;
5963
} else {
6064
throw new RuntimeException(
@@ -85,7 +89,7 @@ public function convertExpression( Expression $expression ) {
8589
UnaryExpression::class => $this->convertUnaryExpression( $expression ),
8690
MemberExpression::class => $this->convertMemberExpression( $expression ),
8791
PeastStringLiteral::class => new StringLiteral( $expression->getValue() ),
88-
Identifier::class => new VariableAccess( [ $expression->getName() ] ),
92+
Identifier::class => new VariableAccess( [ new StringLiteral( $expression->getName() ) ] ),
8993
CallExpression::class => $this->convertCallExpression( $expression ),
9094
ObjectExpression::class => $this->convertObjectExpression( $expression ),
9195
PeastBooleanLiteral::class => new BooleanLiteral( $expression->getValue() ),
@@ -100,6 +104,7 @@ public function convertExpression( Expression $expression ) {
100104
protected function convertKeyToLiteral( $key ) {
101105
return match( get_class( $key ) ) {
102106
PeastStringLiteral::class => $key->getValue(),
107+
PeastNumericLiteral::class => $key->getValue(),
103108
Identifier::class => $key->getName(),
104109
default => throw new RuntimeException(
105110
'Unable to extract name from dictionary key of type ' . get_class( $key )

src/JsParsing/VariableAccess.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
class VariableAccess implements ParsedExpression {
88

99
/**
10-
* @var string[]
10+
* @var ParsedExpression[]
1111
*/
1212
private $pathParts;
1313

@@ -24,11 +24,14 @@ public function __construct( array $pathParts ) {
2424
public function evaluate( array $data ) {
2525
$value = $data;
2626
foreach ( $this->pathParts as $key ) {
27-
if ( !array_key_exists( $key, $value ) ) {
28-
$expression = implode( '.', $this->pathParts );
27+
$keyValue = $key->evaluate( $data );
28+
if ( !array_key_exists( $keyValue, $value ) ) {
29+
$expression = implode( '.', array_map(
30+
static fn ( $part ) => $part->evaluate( $data ), $this->pathParts
31+
) );
2932
throw new RuntimeException( "Undefined variable '{$expression}'" );
3033
}
31-
$value = $value[$key];
34+
$value = $value[$keyValue];
3235
}
3336
return $value;
3437
}

tests/php/TemplatingTest.php

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

258+
public function testTemplateWithForLoopAndMultipleElementsInNestedArrayWithStringKeys_ResolvesVariables() {
259+
$result = $this->createAndRender(
260+
'<p><a v-for="item in list[\'data-values\']"></a></p>',
261+
[ 'list' => [ 'data-values' => [ 1, 2 ] ] ]
262+
);
263+
264+
$this->assertSame( '<p><a></a><a></a></p>', $result );
265+
}
266+
267+
public function testTemplateWithForLoopAndMultipleElementsInNestedIndexedArray_ResolvesVariables() {
268+
$result = $this->createAndRender(
269+
'<p><a v-for="item in list[1]"></a></p>',
270+
[ 'list' => [ [ 3, 4, 5 ], [ 1, 2 ] ] ]
271+
);
272+
273+
$this->assertSame( '<p><a></a><a></a></p>', $result );
274+
}
275+
276+
public function testForVariableIsAvailableForNestedExpressions() {
277+
$result = $this->createAndRender(
278+
'<div><div v-for="index in indexKeys">' .
279+
'<p>{{ data[index] }}</p>' .
280+
'</div></div>',
281+
[ 'indexKeys' => [ 'index1', 'index2' ],
282+
'data' => [ 'index1' => 1, 'index2' => 2 ] ]
283+
);
284+
$this->assertSame( '<div><div><p>1</p></div><div><p>2</p></div></div>', $result );
285+
}
286+
287+
public function testForVariableIsAvailableForNestedExpressions_NestedDataAccess() {
288+
$result = $this->createAndRender(
289+
'<div><div v-for="index in data">' .
290+
'<p>{{ indexKeys[index.key] }}</p>' .
291+
'</div></div>',
292+
[ 'indexKeys' => [ 'value1', 'value2' ],
293+
'data' => [ 'index1' => [ 'key' => 0 ], 'index2' => [ 'key' => 1 ] ] ]
294+
);
295+
$this->assertSame( '<div><div><p>value1</p></div><div><p>value2</p></div></div>', $result );
296+
}
297+
258298
public function testTemplateWithForLoopMustache_RendersCorrectValues() {
259299
$result = $this->createAndRender(
260300
'<p><a v-for="item in list">{{item}}</a></p>',

0 commit comments

Comments
 (0)