Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<table>
<thead>
<template for:each={rows} for:item="row">
<tr key={row.id}>
<th></th>
</tr>
</template>
</thead>
<tbody>
<template for:each={rows} for:item="row">
<tr key={row.id}>
<td></td>
</tr>
</template>
</tbody>
</table>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { registerTemplate } from "lwc";
const stc0 = {
key: 0,
};
const stc1 = {
key: 1,
};
const stc2 = {
key: 3,
};
const stc3 = {
key: 4,
};
const stc4 = {
key: 6,
};
function tmpl($api, $cmp, $slotset, $ctx) {
const { k: api_key, h: api_element, i: api_iterator } = $api;
return [
api_element("table", stc0, [
api_element(
"thead",
stc1,
api_iterator($cmp.rows, function (row) {
return api_element(
"tr",
{
key: api_key(2, row.id),
},
[api_element("th", stc2)]
);
})
),
api_element(
"tbody",
stc3,
api_iterator($cmp.rows, function (row) {
return api_element(
"tr",
{
key: api_key(5, row.id),
},
[api_element("td", stc4)]
);
})
),
]),
];
/*LWC compiler vX.X.X*/
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"warnings": []
}
8 changes: 7 additions & 1 deletion packages/@lwc/template-compiler/src/codegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as astring from 'astring';
import { isBooleanAttribute, SVG_NAMESPACE, LWC_VERSION_COMMENT } from '@lwc/shared';
import { generateCompilerError, TemplateErrors } from '@lwc/errors';

import { isUnsafeTopLevelSerializableElement } from '../parser/tag';
import {
isComment,
isText,
Expand Down Expand Up @@ -71,7 +72,12 @@ function transform(codeGen: CodeGen): t.Expression {
const databag = elementDataBag(element, slotParentName);
let res: t.Expression;

if (codeGen.staticNodes.has(element) && isElement(element)) {
if (
codeGen.staticNodes.has(element) &&
isElement(element) &&
// Tags like <th> and <td> are okay as static fragments, but only if they're not at the top level
!isUnsafeTopLevelSerializableElement(element)
Copy link
Collaborator

@caridy caridy Jun 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of the double negation like this... a more elegant approach is to have something like: isSafeTopLevelSerializableElement(element) instead @nolanlawson

) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually noticed this was a problem because in one of the snapshots we had a fragment like:

<table><tbody><tr><td></td></tr></tbody></table>

We should totally allow these kinds of fragments. There's no problem with them. The only problem is if elements like tbody and td appear at the top level of a fragment.

// do not process children of static nodes.
return codeGen.genHoistedElement(element, slotParentName);
}
Expand Down
32 changes: 32 additions & 0 deletions packages/@lwc/template-compiler/src/parser/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,42 @@ export const KNOWN_HTML_ELEMENTS = new Set(HTML_ELEMENTS.concat(HTML_VOID_ELEMEN
export const HTML_TAG = {
A: 'a',
AREA: 'area',
BODY: 'body',
CAPTION: 'caption',
COL: 'col',
COLGROUP: 'colgroup',
HEAD: 'head',
HTML: 'html',
TBODY: 'tbody',
TD: 'td',
TFOOT: 'tfoot',
TH: 'th',
THEAD: 'thead',
TR: 'tr',
USE: 'use',
};
export const ATTR_NAME = {
HREF: 'href',
XLINK_HREF: 'xlink:href',
};
export const TEMPLATE_DIRECTIVES = [/^key$/, /^lwc:*/, /^if:*/, /^for:*/, /^iterator:*/];

// Tags that cannot be parsed at the top level of a fragment. Generated from
// https://github.com/sindresorhus/html-tags/blob/95dcdd5/index.js
// using the test:
// document.createRange().createContextualFragment(`<${tag}></${tag}>`).firstChild === null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important to call out that we are doing this because we still support IE11. If it wasn't for IE11, we wouldn't need to use Range.createContextFragment and use the <template> element to parse fragments.
Could we add a comment here and in createFragment under @lwc/engine-dom indicating that all of this should be dropped when we are moving away from IE11.

Another option worth calling out would be to disable entirely static VNode hoisting in compat node and transition to <template> element for parsing fragments.
Pros:

  • No need to maintain this special logic in the template compiler
  • Better performance for good citizens
    Cons:
  • Support for 2 template compiler outputs at runtime

At this stage of the release, I would favor the approach with the least amount of changes (the current PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I'll add the comment.

I looked back at the previous perf analysis I did (#2781 (comment)), and it looks like using a <template> and setting innerHTML on it is just as fast as createContextualFragment. So yeah, we could switch to using that, if not for IE11.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #2890

// TODO [#2890]: use <template> instead, drop IE11 support for static optimization
export const TAGS_THAT_CANNOT_BE_PARSED_AS_TOP_LEVEL = new Set([
HTML_TAG.BODY,
HTML_TAG.CAPTION,
HTML_TAG.COL,
HTML_TAG.COLGROUP,
HTML_TAG.HEAD,
HTML_TAG.HTML,
HTML_TAG.TBODY,
HTML_TAG.TD,
HTML_TAG.TFOOT,
HTML_TAG.TH,
HTML_TAG.THEAD,
HTML_TAG.TR,
]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why anybody would put an <html> inside of an LWC template, but we might as well cover every case.

13 changes: 13 additions & 0 deletions packages/@lwc/template-compiler/src/parser/tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { BaseElement } from '../shared/types';
import { TAGS_THAT_CANNOT_BE_PARSED_AS_TOP_LEVEL } from './constants';

export function isUnsafeTopLevelSerializableElement(element: BaseElement) {
return TAGS_THAT_CANNOT_BE_PARSED_AS_TOP_LEVEL.has(element.name);
}
23 changes: 20 additions & 3 deletions packages/integration-karma/test/static-content/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createElement, setFeatureFlagForTest } from 'lwc';
import Container from './x/container/container';
import MultipleStyles from './x/multipleStyles/multipleStyles';
import SvgNs from './x/svgNs/svgNs';
import Container from 'x/container';
import MultipleStyles from 'x/multipleStyles';
import SvgNs from 'x/svgNs';
import Table from 'x/table';

// In compat mode, the component will always render in synthetic mode with the scope attribute
if (!process.env.NATIVE_SHADOW && !process.env.COMPAT) {
Expand Down Expand Up @@ -91,3 +92,19 @@ describe('svg and static content', () => {
});
});
});

describe('tables and static content', () => {
it('should work with a static <td>', () => {
const elm = createElement('x-table', { is: Table });
document.body.appendChild(elm);

expect(elm.shadowRoot.querySelectorAll('td').length).toEqual(0);

elm.addRow();

return Promise.resolve().then(() => {
expect(elm.shadowRoot.querySelectorAll('td').length).toEqual(1);
expect(elm.shadowRoot.querySelector('td').textContent).toEqual('');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<table>
<tbody>
<template for:each={rows} for:item="row">
<tr key={row}>
<td></td>
</tr>
</template>
</tbody>
</table>
</template>
13 changes: 13 additions & 0 deletions packages/integration-karma/test/static-content/x/table/table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { LightningElement, api, track } from 'lwc';

let count = 0;

export default class extends LightningElement {
@track
rows = [];

@api
addRow() {
this.rows.push(++count);
}
}