diff --git a/packages/@lwc/engine-core/src/framework/modules/props.ts b/packages/@lwc/engine-core/src/framework/modules/props.ts index 27a6096186..ca51eb687c 100644 --- a/packages/@lwc/engine-core/src/framework/modules/props.ts +++ b/packages/@lwc/engine-core/src/framework/modules/props.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { assign, htmlPropertyToAttribute, isNull, isUndefined } from '@lwc/shared'; +import { htmlPropertyToAttribute, isNull, isUndefined } from '@lwc/shared'; import { logWarn } from '../../shared/logger'; import { RendererAPI } from '../renderer'; import { EmptyObject } from '../utils'; @@ -21,33 +21,23 @@ export function patchProps( vnode: VBaseElement, renderer: RendererAPI ) { - let { props } = vnode.data; - const { spread } = vnode.data; + const { props } = vnode.data; - if (isUndefined(props) && isUndefined(spread)) { + if (isUndefined(props)) { return; } let oldProps; if (!isNull(oldVnode)) { oldProps = oldVnode.data.props; - const oldSpread = oldVnode.data.spread; // Props may be the same due to the static content optimization, so we can skip diffing - if (oldProps === props && oldSpread === spread) { + if (oldProps === props) { return; } if (isUndefined(oldProps)) { oldProps = EmptyObject; } - - if (!isUndefined(oldSpread)) { - oldProps = assign({}, oldProps, oldSpread); - } - } - - if (!isUndefined(spread)) { - props = assign({}, props, spread); } const isFirstPatch = isNull(oldVnode); diff --git a/packages/@lwc/engine-core/src/framework/vnodes.ts b/packages/@lwc/engine-core/src/framework/vnodes.ts index 8ab3391d44..c20445a412 100644 --- a/packages/@lwc/engine-core/src/framework/vnodes.ts +++ b/packages/@lwc/engine-core/src/framework/vnodes.ts @@ -119,7 +119,6 @@ export interface VNodeData { readonly on?: Readonly any>>; readonly svg?: boolean; readonly renderer?: RendererAPI; - readonly spread?: Readonly>; } export interface VElementData extends VNodeData { diff --git a/packages/@lwc/integration-karma/test/spread/index.spec.js b/packages/@lwc/integration-karma/test/spread/index.spec.js index 13691bced6..f7f05ab7d0 100644 --- a/packages/@lwc/integration-karma/test/spread/index.spec.js +++ b/packages/@lwc/integration-karma/test/spread/index.spec.js @@ -1,18 +1,37 @@ import { createElement } from 'lwc'; import Test from 'x/test'; +import { getHooks, setHooks } from 'test-utils'; +function setSanitizeHtmlContentHookForTest(impl) { + const { sanitizeHtmlContent } = getHooks(); + + setHooks({ + sanitizeHtmlContent: impl, + }); + + return sanitizeHtmlContent; +} describe('lwc:spread', () => { - let elm, simpleChild, overriddenChild; + let elm, simpleChild, overriddenChild, trackedChild, innerHTMLChild, originalHook; beforeEach(() => { + originalHook = setSanitizeHtmlContentHookForTest((x) => x); elm = createElement('x-test', { is: Test }); document.body.appendChild(elm); simpleChild = elm.shadowRoot.querySelector('.x-child-simple'); overriddenChild = elm.shadowRoot.querySelector('.x-child-overridden'); + trackedChild = elm.shadowRoot.querySelector('.x-child-tracked'); + innerHTMLChild = elm.shadowRoot.querySelector('.div-innerhtml'); spyOn(console, 'log'); }); + afterEach(() => { + setSanitizeHtmlContentHookForTest(originalHook); + }); it('should render basic test', () => { expect(simpleChild.shadowRoot.querySelector('span').textContent).toEqual('Name: LWC'); }); + it('should override innerHTML from inner-html directive', () => { + expect(innerHTMLChild.innerHTML).toEqual('innerHTML from spread'); + }); it('should assign onclick', () => { simpleChild.click(); // eslint-disable-next-line no-console @@ -76,4 +95,13 @@ describe('lwc:spread', () => { .shadowRoot.querySelector('span').textContent ).toEqual('Name: Dynamic'); }); + + it('should rerender when tracked props are assigned', async () => { + expect(trackedChild.shadowRoot.querySelector('span').textContent).toEqual('Name: Tracked'); + elm.modify(function () { + this.trackedProps.name = 'Altered'; + }); + await Promise.resolve(); + expect(trackedChild.shadowRoot.querySelector('span').textContent).toEqual('Name: Altered'); + }); }); diff --git a/packages/@lwc/integration-karma/test/spread/x/test/test.html b/packages/@lwc/integration-karma/test/spread/x/test/test.html index e2a7c68eb6..9a2aa1505f 100644 --- a/packages/@lwc/integration-karma/test/spread/x/test/test.html +++ b/packages/@lwc/integration-karma/test/spread/x/test/test.html @@ -4,4 +4,6 @@ Hello + +
diff --git a/packages/@lwc/integration-karma/test/spread/x/test/test.js b/packages/@lwc/integration-karma/test/spread/x/test/test.js index dc245b8b9f..d5e2c852d7 100644 --- a/packages/@lwc/integration-karma/test/spread/x/test/test.js +++ b/packages/@lwc/integration-karma/test/spread/x/test/test.js @@ -1,4 +1,4 @@ -import { LightningElement, api } from 'lwc'; +import { api, LightningElement, track } from 'lwc'; import Child from 'x/child'; export default class Test extends LightningElement { @@ -7,6 +7,9 @@ export default class Test extends LightningElement { spanProps = { className: 'spanclass' }; dynamicCtor = Child; dynamicProps = { name: 'Dynamic' }; + @track trackedProps = { name: 'Tracked' }; + innerHTMLProps = { innerHTML: 'innerHTML from spread' }; + innerHTML = 'innerHTML from directive'; spreadClick() { // eslint-disable-next-line no-console diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/dynamic-components/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/dynamic-components/expected.js index 8cc59e95e7..e6737f6be7 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/dynamic-components/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/dynamic-components/expected.js @@ -3,7 +3,9 @@ function tmpl($api, $cmp, $slotset, $ctx) { const { dc: api_dynamic_component } = $api; return [ api_dynamic_component($cmp.ctor, { - spread: $cmp.hello, + props: { + ...$cmp.hello, + }, key: 0, }), ]; diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/no-renderer-hook/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/no-renderer-hook/expected.js index e3fe3b5331..f7ef8ef773 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/no-renderer-hook/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/no-renderer-hook/expected.js @@ -4,11 +4,15 @@ function tmpl($api, $cmp, $slotset, $ctx) { $api; return [ api_deprecated_dynamic_component("x-foo", $cmp.dynamicCtor, { - spread: $cmp.dynamicProps, + props: { + ...$cmp.dynamicProps, + }, key: 0, }), api_dynamic_component($cmp.dynamicCtor, { - spread: $cmp.dynamicProps, + props: { + ...$cmp.dynamicProps, + }, key: 1, }), ]; diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/valid/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/valid/expected.js index 651f4aa5dc..803b4ee6ea 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/valid/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-spread/valid/expected.js @@ -3,7 +3,9 @@ function tmpl($api, $cmp, $slotset, $ctx) { const { h: api_element } = $api; return [ api_element("a", { - spread: $cmp.hello, + props: { + ...$cmp.hello, + }, key: 0, }), ]; diff --git a/packages/@lwc/template-compiler/src/codegen/index.ts b/packages/@lwc/template-compiler/src/codegen/index.ts index b43e3c2ed5..a83957caf1 100644 --- a/packages/@lwc/template-compiler/src/codegen/index.ts +++ b/packages/@lwc/template-compiler/src/codegen/index.ts @@ -594,6 +594,12 @@ function transform(codeGen: CodeGen): t.Expression { data.push(codeGen.genRef(ref)); } + // Properties: lwc:spread directive + if (spread) { + // spread goes last, so it can be used to override any other properties + propsObj.properties.push(t.spreadElement(codeGen.bindExpression(spread.value))); + instrumentation?.incrementCounter(CompilerMetrics.LWCSpreadDirective); + } if (propsObj.properties.length) { data.push(t.property(t.identifier('props'), propsObj)); } @@ -609,12 +615,6 @@ function transform(codeGen: CodeGen): t.Expression { data.push(t.property(t.identifier('context'), contextObj)); } - // Spread - if (spread) { - data.push(t.property(t.identifier('spread'), codeGen.bindExpression(spread.value))); - instrumentation?.incrementCounter(CompilerMetrics.LWCSpreadDirective); - } - // Key property on VNode if (forKey) { // If element has user-supplied `key` or is in iterator, call `api.k` diff --git a/packages/@lwc/template-compiler/src/shared/estree.ts b/packages/@lwc/template-compiler/src/shared/estree.ts index 9c6a745a57..493e8d74a5 100644 --- a/packages/@lwc/template-compiler/src/shared/estree.ts +++ b/packages/@lwc/template-compiler/src/shared/estree.ts @@ -193,6 +193,13 @@ export function property( }; } +export function spreadElement(argument: t.Expression): t.SpreadElement { + return { + type: 'SpreadElement', + argument, + }; +} + export function assignmentProperty( key: t.AssignmentProperty['key'], value: t.AssignmentProperty['value'],