diff --git a/packages/@lwc/babel-plugin-component/src/__tests__/implicit-explicit.spec.js b/packages/@lwc/babel-plugin-component/src/__tests__/implicit-explicit.spec.js index e2ba7b969f..dcdb4137e3 100644 --- a/packages/@lwc/babel-plugin-component/src/__tests__/implicit-explicit.spec.js +++ b/packages/@lwc/babel-plugin-component/src/__tests__/implicit-explicit.spec.js @@ -130,6 +130,7 @@ describe('Implicit mode', () => { static: { id: 1 }, + hasParams: false, config: function($cmp) { return { id: 1 diff --git a/packages/@lwc/babel-plugin-component/src/__tests__/observed-fields.spec.js b/packages/@lwc/babel-plugin-component/src/__tests__/observed-fields.spec.js index d3624af703..7f7a933acd 100644 --- a/packages/@lwc/babel-plugin-component/src/__tests__/observed-fields.spec.js +++ b/packages/@lwc/babel-plugin-component/src/__tests__/observed-fields.spec.js @@ -61,6 +61,7 @@ describe('observed fields', () => { wire: { wiredProp: { adapter: createElement, + hasParams: false, config: function($cmp) { return {}; } @@ -118,6 +119,7 @@ describe('observed fields', () => { wire: { function: { adapter: createElement, + hasParams: false, config: function($cmp) { return {}; } @@ -310,6 +312,7 @@ describe('observed fields', () => { wire: { wiredProp: { adapter: createElement, + hasParams: false, config: function($cmp) { return {}; } diff --git a/packages/@lwc/babel-plugin-component/src/__tests__/wire-decorator.spec.js b/packages/@lwc/babel-plugin-component/src/__tests__/wire-decorator.spec.js index 7e53ab4715..3bb382471b 100644 --- a/packages/@lwc/babel-plugin-component/src/__tests__/wire-decorator.spec.js +++ b/packages/@lwc/babel-plugin-component/src/__tests__/wire-decorator.spec.js @@ -41,6 +41,7 @@ describe('Transform property', () => { static: { key2: ["fixed", "array"] }, + hasParams: true, config: function($cmp) { return { key2: ["fixed", "array"], @@ -93,6 +94,7 @@ describe('Transform property', () => { static: { key1: importedValue }, + hasParams: false, config: function($cmp) { return { key1: importedValue @@ -145,6 +147,7 @@ describe('Transform property', () => { static: { key2: ["fixed", "array"] }, + hasParams: true, config: function($cmp) { let v1 = $cmp.prop1; let v2 = $cmp.p1; @@ -200,6 +203,7 @@ describe('Transform property', () => { static: { key2: ["fixed", "array"] }, + hasParams: true, config: function($cmp) { let v1 = $cmp.prop1; return { @@ -260,6 +264,7 @@ describe('Transform property', () => { key3: "fixed", key4: ["fixed", "array"] }, + hasParams: true, config: function($cmp) { return { key3: "fixed", @@ -351,6 +356,7 @@ describe('Transform property', () => { adapter: getFoo, params: {}, static: {}, + hasParams: false, config: function($cmp) { return {}; } @@ -395,6 +401,7 @@ describe('Transform property', () => { adapter: Foo.Bar, params: {}, static: {}, + hasParams: false, config: function($cmp) { return {}; } @@ -439,6 +446,7 @@ describe('Transform property', () => { adapter: Foo.Bar, params: {}, static: {}, + hasParams: false, config: function($cmp) { return {}; } @@ -503,6 +511,7 @@ describe('Transform property', () => { wire: { wiredProp: { adapter: getFoo, + hasParams: false, config: function($cmp) { return {}; } @@ -619,6 +628,7 @@ describe('Transform property', () => { key2: "p1.p2" }, static: {}, + hasParams: true, config: function($cmp) { let v1 = $cmp["prop1"]; let v2 = $cmp.p1; @@ -673,6 +683,7 @@ describe('Transform property', () => { static: { key2: ["fixed", "array"] }, + hasParams: true, config: function($cmp) { let v1 = $cmp["prop1"]; return { @@ -777,6 +788,7 @@ describe('Transform property', () => { static: { key2: ["fixed"] }, + hasParams: true, config: function($cmp) { return { key2: ["fixed"], @@ -792,6 +804,7 @@ describe('Transform property', () => { static: { key2: ["array"] }, + hasParams: true, config: function($cmp) { return { key2: ["array"], @@ -845,6 +858,7 @@ describe('Transform method', () => { key2: ["fixed"] }, method: 1, + hasParams: true, config: function($cmp) { return { key2: ["fixed"], diff --git a/packages/@lwc/babel-plugin-component/src/decorators/wire/transform.js b/packages/@lwc/babel-plugin-component/src/decorators/wire/transform.js index 64d52766f3..da5f4f9819 100644 --- a/packages/@lwc/babel-plugin-component/src/decorators/wire/transform.js +++ b/packages/@lwc/babel-plugin-component/src/decorators/wire/transform.js @@ -167,6 +167,12 @@ function buildWireConfigValue(t, wiredValues) { wireConfig.push(t.objectProperty(t.identifier('method'), t.numericLiteral(1))); } + wireConfig.push( + t.objectProperty( + t.identifier('hasParams'), + t.booleanLiteral(!!wiredValue.params && wiredValue.params.length > 0) + ) + ); wireConfig.push(getGeneratedConfig(t, wiredValue)); return t.objectProperty( diff --git a/packages/@lwc/engine/src/framework/decorators/register.ts b/packages/@lwc/engine/src/framework/decorators/register.ts index ffcd37c97a..8fff430725 100644 --- a/packages/@lwc/engine/src/framework/decorators/register.ts +++ b/packages/@lwc/engine/src/framework/decorators/register.ts @@ -47,6 +47,7 @@ interface WireCompilerDef { method?: number; adapter: WireAdapterConstructor; config: ConfigCallback; + hasParams: boolean; } interface RegisterDecoratorMeta { readonly publicMethods?: MethodCompilerMeta; @@ -211,7 +212,7 @@ export function registerDecorators( } if (!isUndefined(wire)) { for (const fieldOrMethodName in wire) { - const { adapter, method, config: configCallback } = wire[fieldOrMethodName]; + const { adapter, method, config: configCallback, hasParams } = wire[fieldOrMethodName]; descriptor = getOwnPropertyDescriptor(proto, fieldOrMethodName); if (method === 1) { if (process.env.NODE_ENV !== 'production') { @@ -225,7 +226,7 @@ export function registerDecorators( throw new Error(); } wiredMethods[fieldOrMethodName] = descriptor; - storeWiredMethodMeta(descriptor, adapter, configCallback); + storeWiredMethodMeta(descriptor, adapter, configCallback, hasParams); } else { if (process.env.NODE_ENV !== 'production') { assert.isTrue( @@ -236,7 +237,7 @@ export function registerDecorators( } descriptor = internalWireFieldDecorator(fieldOrMethodName); wiredFields[fieldOrMethodName] = descriptor; - storeWiredFieldMeta(descriptor, adapter, configCallback); + storeWiredFieldMeta(descriptor, adapter, configCallback, hasParams); defineProperty(proto, fieldOrMethodName, descriptor); } } diff --git a/packages/@lwc/engine/src/framework/wiring.ts b/packages/@lwc/engine/src/framework/wiring.ts index 8aa8d413c5..1209014159 100644 --- a/packages/@lwc/engine/src/framework/wiring.ts +++ b/packages/@lwc/engine/src/framework/wiring.ts @@ -106,7 +106,8 @@ function createContextWatcher( } function createConnector(vm: VM, name: string, wireDef: WireDef): WireAdapter { - const { method, adapter } = wireDef; + const { method, adapter, configCallback, hasParams } = wireDef; + const { component } = vm; const dataCallback = isUndefined(method) ? createFieldDataCallback(vm, name) : createMethodDataCallback(vm, method); @@ -128,7 +129,7 @@ function createConnector(vm: VM, name: string, wireDef: WireDef): WireAdapter { }, noop ); - const computeConfigAndUpdate = createConfigWatcher(vm, wireDef, (config: ConfigValue) => { + const updateConnectorConfig = (config: ConfigValue) => { // every time the config is recomputed due to tracking, // this callback will be invoked with the new computed config runWithBoundaryProtection( @@ -141,9 +142,27 @@ function createConnector(vm: VM, name: string, wireDef: WireDef): WireAdapter { }, noop ); - }); - // computing the initial config (no context at this point because the component is not connected) - computeConfigAndUpdate(); + }; + + // Computes the current wire config and calls the update method on the wire adapter. + // This initial implementation may change depending on the specific wire instance, if it has params, we will need + // to observe changes in the next tick. + let computeConfigAndUpdate = () => { + updateConnectorConfig(configCallback(component)); + }; + + if (hasParams) { + // This wire has dynamic parameters: we wait for the component instance is created and its values set + // in order to call the update(config) method. + Promise.resolve().then(() => { + computeConfigAndUpdate = createConfigWatcher(vm, wireDef, updateConnectorConfig); + + computeConfigAndUpdate(); + }); + } else { + computeConfigAndUpdate(); + } + // if the adapter needs contextualization, we need to watch for new context and push it alongside the config if (!isUndefined(adapter.contextSchema)) { createContextWatcher(vm, wireDef, (newContext: ContextValue) => { @@ -176,6 +195,7 @@ type WireAdapterSchemaValue = 'optional' | 'required'; interface WireDef { method?: (data: any) => void; adapter: WireAdapterConstructor; + hasParams: boolean; configCallback: ConfigCallback; } @@ -208,7 +228,8 @@ export interface WireAdapterConstructor { export function storeWiredMethodMeta( descriptor: PropertyDescriptor, adapter: WireAdapterConstructor, - configCallback: ConfigCallback + configCallback: ConfigCallback, + hasParams: boolean ) { // support for callable adapters if ((adapter as any).adapter) { @@ -219,6 +240,7 @@ export function storeWiredMethodMeta( adapter, method, configCallback, + hasParams, }; WireMetaMap.set(descriptor, def); } @@ -226,7 +248,8 @@ export function storeWiredMethodMeta( export function storeWiredFieldMeta( descriptor: PropertyDescriptor, adapter: WireAdapterConstructor, - configCallback: ConfigCallback + configCallback: ConfigCallback, + hasParams: boolean ) { // support for callable adapters if ((adapter as any).adapter) { @@ -235,6 +258,7 @@ export function storeWiredFieldMeta( const def: WireFieldDef = { adapter, configCallback, + hasParams, }; WireMetaMap.set(descriptor, def); } diff --git a/packages/integration-karma/test/wire/property-trap/index.spec.js b/packages/integration-karma/test/wire/property-trap/index.spec.js index 0d937efe32..bfc6a57527 100644 --- a/packages/integration-karma/test/wire/property-trap/index.spec.js +++ b/packages/integration-karma/test/wire/property-trap/index.spec.js @@ -36,12 +36,12 @@ describe('wire adapter update', () => { return Promise.resolve().then(() => { const actualWiredValues = elm.getWiredProp(); - expect(spy.length).toBe(2); + expect(spy.length).toBe(1); expect(actualWiredValues.data.recordId).toBe('default value'); elm.setWireKeyParameter(wireKey); return Promise.resolve().then(() => { - expect(spy.length).toBe(2); + expect(spy.length).toBe(1); }); }); }); diff --git a/packages/integration-karma/test/wire/wiring/index.spec.js b/packages/integration-karma/test/wire/wiring/index.spec.js index 3bb8f5d627..b48b1a1f3e 100644 --- a/packages/integration-karma/test/wire/wiring/index.spec.js +++ b/packages/integration-karma/test/wire/wiring/index.spec.js @@ -55,25 +55,54 @@ describe('wiring', () => { }); describe('update method on wire adapter', () => { - it('should be called when component is created', () => { + it('should be called in same tick when component with wire no dynamic params is created', () => { + const spy = []; + AdapterId.setSpy(spy); + expect(spy.length).toBe(0); + + createElement('x-echo-adapter-consumer', { is: InheritedMethods }); + + expect(spy.length).toBe(3); + expect(spy[0].method).toBe('update'); // parentMethod + expect(spy[1].method).toBe('update'); // childMethod + expect(spy[2].method).toBe('update'); // overriddenInChild + }); + + it('should be called next tick when component with wire that has dynamic params is created', () => { const spy = []; AdapterId.setSpy(spy); expect(spy.length).toBe(0); createElement('x-echo-adapter-consumer', { is: ComponentClass }); - expect(spy.length).toBe(1); - expect(spy[0].method).toBe('update'); + return Promise.resolve().then(() => { + expect(spy.length).toBe(1); + expect(spy[0].method).toBe('update'); + }); }); - it('should be called only once during multiple renders when the wire config does not change', () => { + it('should call update only once when the component is created and a wire dynamic param is modified in the same tick', () => { const spy = []; AdapterId.setSpy(spy); + expect(spy.length).toBe(0); + const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); + elm.setDynamicParamSource(1); document.body.appendChild(elm); - expect(filterCalls(spy, 'update').length).toBe(1); // update,connected - elm.forceRerender(); + return Promise.resolve().then(() => { + // in the old wire protocol, there is only one call because + // on the same tick the config was modified + const updateCalls = filterCalls(spy, 'update'); + expect(updateCalls.length).toBe(1); + }); + }); + + it('should be called only once during multiple renders when the wire config does not change', () => { + const spy = []; + AdapterId.setSpy(spy); + const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); + document.body.appendChild(elm); return Promise.resolve() .then(() => { @@ -93,15 +122,19 @@ describe('wiring', () => { const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); document.body.appendChild(elm); - expect(filterCalls(spy, 'update').length).toBe(1); - elm.setDynamicParamSource('simpleParam modified'); + return Promise.resolve() + .then(() => { + expect(filterCalls(spy, 'update').length).toBe(1); + elm.setDynamicParamSource('simpleParam modified'); - return Promise.resolve().then(() => { - expect(filterCalls(spy, 'update').length).toBe(2); - const wireResult = elm.getWiredProp(); + return Promise.resolve(); + }) + .then(() => { + expect(filterCalls(spy, 'update').length).toBe(2); + const wireResult = elm.getWiredProp(); - expect(wireResult.simpleParam).toBe('simpleParam modified'); - }); + expect(wireResult.simpleParam).toBe('simpleParam modified'); + }); }); it('should be called for common parameter when shared among wires', () => { @@ -110,18 +143,20 @@ describe('wiring', () => { const elm = createElement('x-bc-consumer', { is: BroadcastConsumer }); document.body.appendChild(elm); - expect(filterCalls(spy, 'update').length).toBe(2); - elm.setCommonParameter('modified'); - return Promise.resolve().then(() => { - expect(filterCalls(spy, 'update').length).toBe(4); - const wireResult1 = elm.getEchoWiredProp1(); - const wireResult2 = elm.getEchoWiredProp2(); - - expect(wireResult1.id).toBe('echoWire1'); - expect(wireResult1.common).toBe('modified'); - expect(wireResult2.id).toBe('echoWire2'); - expect(wireResult2.common).toBe('modified'); + expect(filterCalls(spy, 'update').length).toBe(2); + elm.setCommonParameter('modified'); + + return Promise.resolve().then(() => { + expect(filterCalls(spy, 'update').length).toBe(4); + const wireResult1 = elm.getEchoWiredProp1(); + const wireResult2 = elm.getEchoWiredProp2(); + + expect(wireResult1.id).toBe('echoWire1'); + expect(wireResult1.common).toBe('modified'); + expect(wireResult2.id).toBe('echoWire2'); + expect(wireResult2.common).toBe('modified'); + }); }); }); @@ -153,25 +188,28 @@ describe('wiring', () => { const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); document.body.appendChild(elm); - return Promise.resolve().then(() => { - const staticValue = elm.shadowRoot.querySelector('.static'); - const dynamicValue = elm.shadowRoot.querySelector('.dynamic'); - - expect(staticValue.textContent).toBe('1,2,3'); - expect(dynamicValue.textContent).toBe(''); - - elm.setDynamicParamSource('modified value'); - - setTimeout(() => { + return Promise.resolve() + .then(() => Promise.resolve()) // In this tick, the config is injected. + .then(() => { + // Now the component have re-rendered. const staticValue = elm.shadowRoot.querySelector('.static'); const dynamicValue = elm.shadowRoot.querySelector('.dynamic'); expect(staticValue.textContent).toBe('1,2,3'); - expect(dynamicValue.textContent).toBe('modified value'); + expect(dynamicValue.textContent).toBe(''); - done(); - }, 5); - }); + elm.setDynamicParamSource('modified value'); + + setTimeout(() => { + const staticValue = elm.shadowRoot.querySelector('.static'); + const dynamicValue = elm.shadowRoot.querySelector('.dynamic'); + + expect(staticValue.textContent).toBe('1,2,3'); + expect(dynamicValue.textContent).toBe('modified value'); + + done(); + }, 5); + }); }); }); }); @@ -217,6 +255,7 @@ describe('wired methods', () => { const elm = createElement('x-inherited-methods', { is: InheritedMethods }); document.body.appendChild(elm); + // No need to wait for next tick, the wire only has static config. const calls = filterCalls(spy, 'update'); const getCallByName = name => { return calls.filter(call => name === call.args[0].name)[0]; diff --git a/packages/integration-karma/test/wire/wiring/x/echoAdapter/echoAdapter.js b/packages/integration-karma/test/wire/wiring/x/echoAdapter/echoAdapter.js index a374591585..d95c13bf36 100644 --- a/packages/integration-karma/test/wire/wiring/x/echoAdapter/echoAdapter.js +++ b/packages/integration-karma/test/wire/wiring/x/echoAdapter/echoAdapter.js @@ -9,6 +9,7 @@ export class EchoWireAdapter { constructor(callback) { this.callback = callback; + callback({}); } update(config) {