Skip to content

Commit

Permalink
fix(engine-core): fix warnings for invalid keys (#4097)
Browse files Browse the repository at this point in the history
  • Loading branch information
nolanlawson committed Mar 26, 2024
1 parent 2870fc3 commit 3665f31
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 16 deletions.
16 changes: 10 additions & 6 deletions packages/@lwc/engine-core/src/framework/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
isTrue,
isUndefined,
StringReplace,
StringToLowerCase,
toString,
} from '@lwc/shared';

Expand All @@ -35,7 +36,9 @@ import { RenderMode, ShadowMode, SlotSet, VM } from './vm';
import { LightningElementConstructor } from './base-lightning-element';
import { markAsDynamicChildren } from './rendering';
import {
isVBaseElement,
isVScopedSlotFragment,
isVStatic,
Key,
VComment,
VCustomElement,
Expand All @@ -47,11 +50,9 @@ import {
VNodeType,
VScopedSlotFragment,
VStatic,
VText,
VStaticPart,
VStaticPartData,
isVBaseElement,
isVStatic,
VText,
} from './vnodes';
import { getComponentRegisteredName } from './component';

Expand Down Expand Up @@ -432,15 +433,18 @@ function i(
if (process.env.NODE_ENV !== 'production') {
const vnodes = isArray(vnode) ? vnode : [vnode];
forEach.call(vnodes, (childVnode: VNode | null) => {
if (!isNull(childVnode) && isObject(childVnode) && !isUndefined(childVnode.sel)) {
// Check that the child vnode is either an element or VStatic
if (!isNull(childVnode) && (isVBaseElement(childVnode) || isVStatic(childVnode))) {
const { key } = childVnode;
const tagName =
childVnode.sel ?? StringToLowerCase.call(childVnode.fragment.tagName);
if (isString(key) || isNumber(key)) {
if (keyMap[key] === 1 && isUndefined(iterationError)) {
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.`;
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.`;
}
keyMap[key] = 1;
} else if (isUndefined(iterationError)) {
iterationError = `Invalid "key" attribute value in "<${childVnode.sel}>" in ${vmBeingRendered} for item number ${j}. Set a unique "key" value on all iterated child elements.`;
iterationError = `Invalid "key" attribute value in "<${tagName}>" in ${vmBeingRendered} for item number ${j}. Set a unique "key" value on all iterated child elements.`;
}
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createElement } from 'lwc';
import XTest from 'x/test';
import XTestStatic from 'x/testStatic';
import XTestCustomElement from 'x/testCustomElement';
import ArrayNullPrototype from 'x/arrayNullPrototype';

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

// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
expect(() => document.body.appendChild(elm)).toThrowCallbackReactionError(
/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/
/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/
);
});

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

it('logs an error when passing an invalid key', () => {
const elm = createElement('x-test', { is: XTest });
elm.items = [{ key: null, value: 'one' }];
const scenarios = [
{
testName: 'dynamic text node',
Ctor: XTest,
tagName: 'x-test',
},
{
testName: 'static text node',
Ctor: XTestStatic,
tagName: 'x-test-static',
},
{
testName: 'custom element',
Ctor: XTestCustomElement,
tagName: 'x-test-custom-element',
},
];
scenarios.forEach(({ testName, Ctor, tagName }) => {
describe(testName, () => {
it('logs an error when passing an invalid key', () => {
const elm = createElement(tagName, { is: Ctor });
elm.items = [{ key: null, value: 'one' }];

// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
expect(() => document.body.appendChild(elm)).toLogErrorDev([
/Invalid key value "null" in \[object:vm Test \(\d+\)\]. Key must be a string or number./,
/Invalid "key" attribute value in "<li>"/,
]);
// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
expect(() => document.body.appendChild(elm)).toLogErrorDev([
/Invalid key value "null" in \[object:vm (TestStatic|TestCustomElement|Test) \(\d+\)]. Key must be a string or number\./,
/Invalid "key" attribute value in "<(li|x-custom)>"/,
]);
});

it('logs an error when passing a duplicate key', () => {
const elm = createElement(tagName, { is: Ctor });
elm.items = [
{ key: 'xyz', value: 'one' },
{ key: 'xyz', value: 'two' },
];

// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
expect(() => document.body.appendChild(elm)).toLogErrorDev(
/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\./
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { LightningElement } from 'lwc';

export default class Custom extends LightningElement {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<template>
<ul>
<template for:each={items} for:item="item">
<li key={item.key}>{item.value}</li>
<li key={item.key}>
<!-- putting an lwc:if in here so that the LI will always be VElement instead of VStatic -->
<template lwc:if={item.value}>{item.value}</template>
</li>
</template>
</ul>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<ul>
<template for:each={items} for:item="item">
<x-custom key={item.key}></x-custom>
</template>
</ul>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LightningElement, api } from 'lwc';

export default class TestCustomElement extends LightningElement {
@api items = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<ul>
<template for:each={items} for:item="item">
<li key={item.key}></li>
</template>
</ul>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LightningElement, api } from 'lwc';

export default class TestStatic extends LightningElement {
@api items = [];
}

0 comments on commit 3665f31

Please sign in to comment.