Skip to content

Commit 3665f31

Browse files
authored
fix(engine-core): fix warnings for invalid keys (#4097)
1 parent 2870fc3 commit 3665f31

File tree

8 files changed

+86
-16
lines changed

8 files changed

+86
-16
lines changed

packages/@lwc/engine-core/src/framework/api.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
isTrue,
2323
isUndefined,
2424
StringReplace,
25+
StringToLowerCase,
2526
toString,
2627
} from '@lwc/shared';
2728

@@ -35,7 +36,9 @@ import { RenderMode, ShadowMode, SlotSet, VM } from './vm';
3536
import { LightningElementConstructor } from './base-lightning-element';
3637
import { markAsDynamicChildren } from './rendering';
3738
import {
39+
isVBaseElement,
3840
isVScopedSlotFragment,
41+
isVStatic,
3942
Key,
4043
VComment,
4144
VCustomElement,
@@ -47,11 +50,9 @@ import {
4750
VNodeType,
4851
VScopedSlotFragment,
4952
VStatic,
50-
VText,
5153
VStaticPart,
5254
VStaticPartData,
53-
isVBaseElement,
54-
isVStatic,
55+
VText,
5556
} from './vnodes';
5657
import { getComponentRegisteredName } from './component';
5758

@@ -432,15 +433,18 @@ function i(
432433
if (process.env.NODE_ENV !== 'production') {
433434
const vnodes = isArray(vnode) ? vnode : [vnode];
434435
forEach.call(vnodes, (childVnode: VNode | null) => {
435-
if (!isNull(childVnode) && isObject(childVnode) && !isUndefined(childVnode.sel)) {
436+
// Check that the child vnode is either an element or VStatic
437+
if (!isNull(childVnode) && (isVBaseElement(childVnode) || isVStatic(childVnode))) {
436438
const { key } = childVnode;
439+
const tagName =
440+
childVnode.sel ?? StringToLowerCase.call(childVnode.fragment.tagName);
437441
if (isString(key) || isNumber(key)) {
438442
if (keyMap[key] === 1 && isUndefined(iterationError)) {
439-
iterationError = `Duplicated "key" attribute value for "<${childVnode.sel}>" in ${vmBeingRendered} for item number ${j}. A key with value "${childVnode.key}" appears more than once in the iteration. Key values must be unique numbers or strings.`;
443+
iterationError = `Duplicated "key" attribute value for "<${tagName}>" in ${vmBeingRendered} for item number ${j}. A key with value "${childVnode.key}" appears more than once in the iteration. Key values must be unique numbers or strings.`;
440444
}
441445
keyMap[key] = 1;
442446
} else if (isUndefined(iterationError)) {
443-
iterationError = `Invalid "key" attribute value in "<${childVnode.sel}>" in ${vmBeingRendered} for item number ${j}. Set a unique "key" value on all iterated child elements.`;
447+
iterationError = `Invalid "key" attribute value in "<${tagName}>" in ${vmBeingRendered} for item number ${j}. Set a unique "key" value on all iterated child elements.`;
444448
}
445449
}
446450
});

packages/@lwc/integration-karma/test/template/directive-for-each/index.spec.js

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { createElement } from 'lwc';
22
import XTest from 'x/test';
3+
import XTestStatic from 'x/testStatic';
4+
import XTestCustomElement from 'x/testCustomElement';
35
import ArrayNullPrototype from 'x/arrayNullPrototype';
46

57
function testForEach(type, obj) {
@@ -64,7 +66,7 @@ it('should throw an error when the passing a non iterable', () => {
6466

6567
// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
6668
expect(() => document.body.appendChild(elm)).toThrowCallbackReactionError(
67-
/Invalid template iteration for value `\[object (ProxyObject|Object)\]` in \[object:vm Test \(\d+\)\]. It must be an array-like object and not `null` nor `undefined`.|is not a function/
69+
/Invalid template iteration for value `\[object (ProxyObject|Object)]` in \[object:vm Test \(\d+\)]\. It must be an array-like object and not `null` nor `undefined`\.|is not a function/
6870
);
6971
});
7072

@@ -75,13 +77,47 @@ it('should render an array of objects with null prototype', () => {
7577
expect(elm.shadowRoot.querySelector('span').textContent).toBe('text');
7678
});
7779

78-
it('logs an error when passing an invalid key', () => {
79-
const elm = createElement('x-test', { is: XTest });
80-
elm.items = [{ key: null, value: 'one' }];
80+
const scenarios = [
81+
{
82+
testName: 'dynamic text node',
83+
Ctor: XTest,
84+
tagName: 'x-test',
85+
},
86+
{
87+
testName: 'static text node',
88+
Ctor: XTestStatic,
89+
tagName: 'x-test-static',
90+
},
91+
{
92+
testName: 'custom element',
93+
Ctor: XTestCustomElement,
94+
tagName: 'x-test-custom-element',
95+
},
96+
];
97+
scenarios.forEach(({ testName, Ctor, tagName }) => {
98+
describe(testName, () => {
99+
it('logs an error when passing an invalid key', () => {
100+
const elm = createElement(tagName, { is: Ctor });
101+
elm.items = [{ key: null, value: 'one' }];
81102

82-
// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
83-
expect(() => document.body.appendChild(elm)).toLogErrorDev([
84-
/Invalid key value "null" in \[object:vm Test \(\d+\)\]. Key must be a string or number./,
85-
/Invalid "key" attribute value in "<li>"/,
86-
]);
103+
// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
104+
expect(() => document.body.appendChild(elm)).toLogErrorDev([
105+
/Invalid key value "null" in \[object:vm (TestStatic|TestCustomElement|Test) \(\d+\)]. Key must be a string or number\./,
106+
/Invalid "key" attribute value in "<(li|x-custom)>"/,
107+
]);
108+
});
109+
110+
it('logs an error when passing a duplicate key', () => {
111+
const elm = createElement(tagName, { is: Ctor });
112+
elm.items = [
113+
{ key: 'xyz', value: 'one' },
114+
{ key: 'xyz', value: 'two' },
115+
];
116+
117+
// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
118+
expect(() => document.body.appendChild(elm)).toLogErrorDev(
119+
/Duplicated "key" attribute value for "<(li|x-custom)>" in \[object:vm (TestStatic|TestCustomElement|Test) \(\d+\)] for item number 1\. A key with value "\d:xyz" appears more than once in the iteration\. Key values must be unique numbers or strings\./
120+
);
121+
});
122+
});
87123
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class Custom extends LightningElement {}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
<template>
22
<ul>
33
<template for:each={items} for:item="item">
4-
<li key={item.key}>{item.value}</li>
4+
<li key={item.key}>
5+
<!-- putting an lwc:if in here so that the LI will always be VElement instead of VStatic -->
6+
<template lwc:if={item.value}>{item.value}</template>
7+
</li>
58
</template>
69
</ul>
710
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<template>
2+
<ul>
3+
<template for:each={items} for:item="item">
4+
<x-custom key={item.key}></x-custom>
5+
</template>
6+
</ul>
7+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { LightningElement, api } from 'lwc';
2+
3+
export default class TestCustomElement extends LightningElement {
4+
@api items = [];
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<template>
2+
<ul>
3+
<template for:each={items} for:item="item">
4+
<li key={item.key}></li>
5+
</template>
6+
</ul>
7+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { LightningElement, api } from 'lwc';
2+
3+
export default class TestStatic extends LightningElement {
4+
@api items = [];
5+
}

0 commit comments

Comments
 (0)