New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(compiler): Compiler errors for missing keys in iterator #138
Changes from 4 commits
b841982
89645c5
d138d2e
d85123e
ce4b715
12c6567
9d1c60c
3a87e45
222ef58
db5545b
0f76569
0ea3caf
84acc3c
8941ade
cf84fc1
6e215eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,6 @@ import { ComponentConstructor, markComponentAsDirty } from "./component"; | |
|
||
import { VNode, VNodeData, VNodes, VElement, VComment, VText, Hooks } from "../3rdparty/snabbdom/types"; | ||
import { getCustomElementVM } from "./html-element"; | ||
import { unwrap } from "./reactive"; | ||
|
||
export interface RenderAPI { | ||
h(tagName: string, data: VNodeData, children: VNodes): VNode; | ||
|
@@ -255,6 +254,11 @@ export function i(iterable: Iterable<any>, factory: (value: any, index: number, | |
let next = iterator.next(); | ||
let j = 0; | ||
let { value, done: last } = next; | ||
if (process.env.NODE_ENV !== 'production') { | ||
// var is intentional here, function level scoping is required. | ||
var keyMap = {}; | ||
} | ||
|
||
while (last === false) { | ||
// implementing a look-back-approach because we need to know if the element is the last | ||
next = iterator.next(); | ||
|
@@ -271,9 +275,14 @@ export function i(iterable: Iterable<any>, factory: (value: any, index: number, | |
if (process.env.NODE_ENV !== 'production') { | ||
const vnodes = isArray(vnode) ? vnode : [vnode]; | ||
vnodes.forEach((childVnode) => { | ||
if (!isNull(childVnode) && isObject(childVnode) && !isUndefined(childVnode.sel) && childVnode.sel.indexOf('-') > 0 && isUndefined(childVnode.key)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we want the |
||
// TODO - it'd be nice to log the owner component rather than the iteration children | ||
assert.logWarning(`Missing "key" attribute in iteration with child "<${childVnode.sel}>", index ${i}. Instead set a unique "key" attribute value on all iteration children so internal state can be preserved during rehydration.`); | ||
if (!isNull(childVnode) && isObject(childVnode) && !isUndefined(childVnode.sel)) { | ||
if (isUndefined(childVnode.key)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about key being null? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably better to just test for typeof to be number or string |
||
// TODO - it'd be nice to log the owner component rather than the iteration children | ||
assert.logWarning(`Missing "key" attribute in iteration with child "<${childVnode.sel}>", index ${i}. Instead set a unique "key" attribute value on all iteration children so internal state can be preserved during rehydration.`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} else if (keyMap[childVnode.key!] === 1) { | ||
assert.logWarning(`Invalid "key" attribute in iteration with child "<${childVnode.sel}>". Key with value "${childVnode.key}" appears more than once in iteration. Key values must be unique numbers or strings.`); | ||
} | ||
keyMap[childVnode.key!] = 1; | ||
} | ||
}); | ||
} | ||
|
@@ -352,10 +361,7 @@ export function b(fn: EventListener): EventListener { | |
}; | ||
} | ||
|
||
const objToKeyMap: WeakMap<any, number> = new WeakMap(); | ||
let globalKey: number = 0; | ||
|
||
// [k]ind function | ||
// [k]ey function | ||
export function k(compilerKey: number, obj: any): number | string | void { | ||
switch (typeof obj) { | ||
case 'number': | ||
|
@@ -364,17 +370,6 @@ export function k(compilerKey: number, obj: any): number | string | void { | |
case 'string': | ||
return compilerKey + ':' + obj; | ||
case 'object': | ||
if (isNull(obj)) { | ||
return; | ||
} | ||
// Slow path. We get here when element is inside iterator | ||
// but no key is specified. | ||
const unwrapped = unwrap(obj); | ||
let objKey = objToKeyMap.get(unwrapped); | ||
if (isUndefined(objKey)) { | ||
objKey = globalKey++; | ||
objToKeyMap.set(unwrapped, objKey); | ||
} | ||
return compilerKey + ':' + objKey; | ||
throw new Error(`Invalid key value ${obj}. Key must be a string or number.`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Opted for an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you will have to do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this error is only incomplete, we should at least tell them what VM is this, but in prod we don't have toString on VM. my recommendation is to keep it as an assert until the next refactor of the errors. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
<template> | ||
<test-case issue-title="[proxy-compat] Error: Setting property Symbol(Symbol.iterator) during the rendering" issue-id="702"> | ||
<template for:each={state.items} for:item="item"> | ||
<compat-item item={item}></compat-item> | ||
<compat-item key={item.label} item={item}></compat-item> | ||
</template> | ||
</test-case> | ||
</template> | ||
</template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<template> | ||
<template for:each={items} for:item="item"> | ||
<p>1{item}</p> | ||
</template> | ||
</template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"warnings": [{ | ||
"level": "error", | ||
"message": "Missing key for element <p> inside of iterator. Elements within iterators must have a unique, computed key value.", | ||
"start": 67, | ||
"length": 14 | ||
}], | ||
"metadata": { | ||
"templateUsedIds": ["items"] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
<template> | ||
<ul> | ||
<li for:each={items} for:item="item" class={item.x}>{item}</li> | ||
<li key={item.id} for:each={items} for:item="item" class={item.x}>{item}</li> | ||
</ul> | ||
</template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
<template> | ||
<ul> | ||
<li for:each={items} for:item="item" class={item.x}>{item}</li> | ||
<li for:each={items} for:item="item" class={item.x} key={item.id}>{item}</li> | ||
<li>Last</li> | ||
</ul> | ||
</template> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't think of a better way to hoist a map of used keys that will get populated in the while loop. Suggestions happily taken :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this a little bit more, I think this will still get minified if I use
let
outside of the prod check