From d1a7a2770358405f1bd37d24d7d0a36bfef37829 Mon Sep 17 00:00:00 2001 From: Jose David Rodriguez Velasco Date: Mon, 20 Jan 2020 13:40:01 -0800 Subject: [PATCH 1/2] refactor(wire-service): wire decorator reform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "Revert "feat: add wire config as function (#1455)" (#1601)" This reverts commit 722979e4b4c411e06e8404931a484d4573ef77b3. rebase from master Squashed commit of the following: commit 0f00e08d99227f79c7bd00ba8a154fdd459eef1f Author: Jose David Rodriguez Date: Thu Sep 19 23:13:15 2019 -0700 refactor(wire-service): wire decorator reform Squashed commit of the following: commit fb136b7d82118cb1b3a7ae4213efcb6e84b2a1e9 Author: Caridy Patiño Date: Thu Aug 29 23:59:16 2019 -0400 fix(wire-service): does not accept adapter id to be a symbol commit 91c2d973eb3467ac56f4c1132a9febe4458c0189 Author: Caridy Patiño Date: Thu Aug 29 15:04:19 2019 -0400 fix(engine): tests and karma tests fixes commit 719c41e45d0e08f131d3f65eb39da7e0fd82e593 Merge: 3cdf6508 8e5035ed Author: Caridy Patiño Date: Sat Aug 31 14:31:07 2019 -0400 Merge branch 'master' into caridy/wire-reform-2 commit 8e5035edb48c9001fdc47d0bdd4b809da1871791 Author: Caridy Patiño Date: Fri Aug 30 22:25:49 2019 -0400 refactor: hidden fields instead of internal fields (2) (#1485) * chore: package-unique keys for engine and synthetic shadow * refactor: remove internal fields in favor of hidden fields * fix: avoid returning boolean false when field undefined * test: use hidden fields instead of internal fields * refactor(synthetic-shadow): hidden fields instead of internal fields * chore(synthetic-shadow): "unique" string keys * fix(engine): issue 1299 second attempt * fix(engine): removing hasOwnProperty check * chore: revert changes for vm access by getComponentConstructor commit 9a7f8224d52bd6e46144e5cf75acbb28e8787a07 Author: Ravi Jayaramappa Date: Thu Aug 29 14:05:57 2019 -0700 fix: cloneNode() default behavior should match spec (#1480) commit 3cdf65080fe70a75ce406ad483f47a3dfe18f689 Author: Caridy Patiño Date: Thu Aug 29 05:40:52 2019 -0400 fix(engine): context to work as expected commit 2224bd221ba37f881af57c53ede7d16e1a517d48 Author: Caridy Patiño Date: Thu Aug 29 04:38:45 2019 -0400 fix(engine): karma tests for context providers commit 8b6c978f0e1f0d6126f15ee336b30184bc82b0f3 Merge: d02243bc 5d5f7afd Author: Caridy Patiño Date: Thu Aug 29 04:33:58 2019 -0400 Merge branch 'caridy/wire-reform-2' of github.com:salesforce/lwc into caridy/wire-reform-2 commit 5d5f7afd3097a279c75c61ca064477099af475af Author: Jose David Rodriguez Date: Fri Aug 30 13:30:34 2019 -0700 fix: observable-fields commit a901a6403cdb671a359af600febc4f19925080e9 Author: Jose David Rodriguez Date: Thu Aug 29 22:41:50 2019 -0700 wip: wire register is broken it was making karma fail commit d02243bc7abf1cbd2e83ee9c3228972de103bbe2 Author: Caridy Patiño Date: Wed Aug 28 07:46:56 2019 -0400 fix(engine): remove unnecessary comment commit c9ad2c54f60b8a3371576cf5388d301a5691631b Author: Caridy Patiño Date: Wed Aug 28 02:59:29 2019 -0400 refactor(engine): context provider options commit 6bcf0be094b23ae16b3d9947deb85058c7a842ff Author: Caridy Patiño Date: Tue Jul 16 23:03:56 2019 -0400 fix(engine): @wire() protocol reform RFC fix: rebase issues fix: wire-reform tests (#1524) * fix: invalid syntax error on invalid wire param value When a wire parameter has invalid value (eg: foo..bar, foo.bar[3]) it should (ideally) resolve in a compilation error during validation phase. This is not possible due that platform components may have a param definition which is invalid but passes compilation, and throwing at compile time would break such components. In such cases where the param does not have proper notation, the config generated will use the bracket notation to match the current behavior (that most likely end up resolving that param as undefined). * fix: tests for wire reactive parameters * fix: unit test register and WireAdapter * fix: backward compability when adding config listener * fix: adding general tests for wire adapter * wip: address pr comments and lint errors fix: reactivity of wire params (#1526) * fix: reactivity of wire params * wip: address review comments and run performance * wip: trigger perf measures * fix: remove valueMutated pruning condition * fix: add componentValueObserved for consistency * fix: remove tf because of wired properties being track before the wire reform. * fix: ensure component rerender in compat * fix: tf because registerDecorators happens before registerAdapter * Revert "fix: tf because registerDecorators happens before registerAdapter" This reverts commit b2d1a895ebe5fc42c3471edaadeaa6be8f647cc1. * fix: wire integration tests fix: rebase issues fix: remove tests because wire defs is not part of the public api of getComponentDef fix: add test for inherited methods fix: context in targets hierarchy fix: add registerWireService for backward compability it is a noop. fix(engine): remove assertions added by mistake on rebase fix: rebase conflicts fix: lint errors todos errors. fix: add support for adapters that use wirecontextevent fix: prefix wired element key with deprecated and remove redundant options in descriptor fix: relax adapterId validation to isExtensible instead of instanceof Object. In order to be able to register an adapterId, the only precondition needed is that adapterId is extensible. this commit changes the old validation that was doing instanceof Object. fix: tf because of error message changed fix: debounce config call when creating component with wire also, adds an extra data for the wire decorator, indicating if it has dynamic parameters. fix: incorrect api field config returned from getComponentDef fix: address pr comments fix: include 33b91a2 in the wire reform fix: —strictFunctionTypes issues plus rebase leftovers --- .../src/__tests__/implicit-explicit.spec.js | 6 + .../src/__tests__/observed-fields.spec.js | 18 +- .../src/__tests__/wire-decorator.spec.js | 341 ++++++++++- .../babel-plugin-component/src/constants.js | 1 + .../src/decorators/wire/transform.js | 112 ++++ .../__tests__/error-boundary.spec.ts | 2 +- .../src/framework/base-lightning-element.ts | 35 +- .../@lwc/engine/src/framework/component.ts | 24 - .../engine/src/framework/context-provider.ts | 63 +++ .../decorators/__tests__/wire.spec.ts | 235 +------- .../engine/src/framework/decorators/api.ts | 85 +-- .../src/framework/decorators/decorate.ts | 48 -- .../src/framework/decorators/register.ts | 382 +++++++++---- .../engine/src/framework/decorators/track.ts | 68 +-- .../engine/src/framework/decorators/wire.ts | 75 ++- packages/@lwc/engine/src/framework/def.ts | 176 +++--- packages/@lwc/engine/src/framework/hooks.ts | 4 +- packages/@lwc/engine/src/framework/main.ts | 4 +- .../@lwc/engine/src/framework/membrane.ts | 2 +- .../engine/src/framework/mutation-tracker.ts | 18 + .../engine/src/framework/observed-fields.ts | 31 +- .../framework/{decorators => }/readonly.ts | 4 +- .../@lwc/engine/src/framework/services.ts | 4 +- packages/@lwc/engine/src/framework/upgrade.ts | 4 +- packages/@lwc/engine/src/framework/utils.ts | 10 + packages/@lwc/engine/src/framework/vm.ts | 30 +- packages/@lwc/engine/src/framework/wc.ts | 23 +- packages/@lwc/engine/src/framework/wiring.ts | 333 +++++++++++ packages/@lwc/shared/src/language.ts | 2 +- .../wire-service/src/__tests__/index.spec.ts | 530 +++++++++--------- .../src/__tests__/property-trap.spec.ts | 304 ---------- .../wire-service/src/__tests__/wiring.spec.ts | 522 ----------------- packages/@lwc/wire-service/src/constants.ts | 21 - packages/@lwc/wire-service/src/engine.ts | 23 - packages/@lwc/wire-service/src/index.ts | 325 ++++++----- .../wire-service/src/link-context-event.ts | 22 - .../@lwc/wire-service/src/property-trap.ts | 200 ------- packages/@lwc/wire-service/src/wiring.ts | 281 ---------- .../test/api/getComponentDef/index.spec.js | 152 ++--- .../x/publicPropertiesInheritance/base.js | 6 +- .../x/wireAdapter/wireAdapter.js | 1 - .../x/wireMethods/wireMethods.js | 8 - .../x/wireMethodsInheritance/base.js | 7 - .../wireMethodsInheritance.js | 9 - .../x/wireProperties/wireProperties.js | 8 - .../x/wirePropertiesInheritance/base.js | 7 - .../wirePropertiesInheritance.js | 9 - .../test/context/advanced-context.spec.js | 4 +- .../test/context/simple-context.spec.js | 21 +- .../x/advancedConsumer/advancedConsumer.js | 4 +- .../x/advancedProvider/advancedProvider.js | 119 ++-- .../x/simpleConsumer/simpleConsumer.js | 4 +- .../x/simpleProvider/simpleProvider.js | 130 ++--- .../test/wire/property-trap/index.spec.js | 175 ++++++ .../x/echoAdapter/echoAdapter.js | 29 + .../echoAdapterConsumer.html | 3 + .../echoAdapterConsumer.js | 48 ++ .../wirecontextevent-legacy/index.spec.js | 18 + .../wireContextAdapter/wireContextAdapter.js | 20 + .../wireContextConsumer.html | 3 + .../wireContextConsumer.js | 6 + .../wireContextProvider.html | 5 + .../wireContextProvider.js | 9 + .../test/wire/wiring/index.spec.js | 270 +++++++++ .../x/adapterConsumer/adapterConsumer.html | 5 + .../x/adapterConsumer/adapterConsumer.js | 31 + .../test/wire/wiring/x/base/base.js | 0 .../x/broadcastAdapter/broadcastAdapter.js | 26 + .../broadcastConsumer/broadcastConsumer.html | 3 + .../x/broadcastConsumer/broadcastConsumer.js | 57 ++ .../wire/wiring/x/echoAdapter/echoAdapter.js | 34 ++ .../x/inheritedMethods/inheritedMethods.html | 2 + .../x/inheritedMethods/inheritedMethods.js | 12 + packages/integration-tests/scripts/build.js | 6 +- .../integration-tests/src/shared/templates.js | 39 +- packages/integration-tests/src/shared/todo.js | 177 +++--- 76 files changed, 2881 insertions(+), 2954 deletions(-) create mode 100644 packages/@lwc/engine/src/framework/context-provider.ts delete mode 100644 packages/@lwc/engine/src/framework/decorators/decorate.ts create mode 100644 packages/@lwc/engine/src/framework/mutation-tracker.ts rename packages/@lwc/engine/src/framework/{decorators => }/readonly.ts (89%) create mode 100644 packages/@lwc/engine/src/framework/wiring.ts delete mode 100644 packages/@lwc/wire-service/src/__tests__/property-trap.spec.ts delete mode 100644 packages/@lwc/wire-service/src/__tests__/wiring.spec.ts delete mode 100644 packages/@lwc/wire-service/src/constants.ts delete mode 100644 packages/@lwc/wire-service/src/engine.ts delete mode 100644 packages/@lwc/wire-service/src/link-context-event.ts delete mode 100644 packages/@lwc/wire-service/src/property-trap.ts delete mode 100644 packages/@lwc/wire-service/src/wiring.ts delete mode 100644 packages/integration-karma/test/api/getComponentDef/x/wireAdapter/wireAdapter.js delete mode 100644 packages/integration-karma/test/api/getComponentDef/x/wireMethods/wireMethods.js delete mode 100644 packages/integration-karma/test/api/getComponentDef/x/wireMethodsInheritance/base.js delete mode 100644 packages/integration-karma/test/api/getComponentDef/x/wireMethodsInheritance/wireMethodsInheritance.js delete mode 100644 packages/integration-karma/test/api/getComponentDef/x/wireProperties/wireProperties.js delete mode 100644 packages/integration-karma/test/api/getComponentDef/x/wirePropertiesInheritance/base.js delete mode 100644 packages/integration-karma/test/api/getComponentDef/x/wirePropertiesInheritance/wirePropertiesInheritance.js create mode 100644 packages/integration-karma/test/wire/property-trap/index.spec.js create mode 100644 packages/integration-karma/test/wire/property-trap/x/echoAdapter/echoAdapter.js create mode 100644 packages/integration-karma/test/wire/property-trap/x/echoAdapterConsumer/echoAdapterConsumer.html create mode 100644 packages/integration-karma/test/wire/property-trap/x/echoAdapterConsumer/echoAdapterConsumer.js create mode 100644 packages/integration-karma/test/wire/wirecontextevent-legacy/index.spec.js create mode 100644 packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextAdapter/wireContextAdapter.js create mode 100644 packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextConsumer/wireContextConsumer.html create mode 100644 packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextConsumer/wireContextConsumer.js create mode 100644 packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextProvider/wireContextProvider.html create mode 100644 packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextProvider/wireContextProvider.js create mode 100644 packages/integration-karma/test/wire/wiring/index.spec.js create mode 100644 packages/integration-karma/test/wire/wiring/x/adapterConsumer/adapterConsumer.html create mode 100644 packages/integration-karma/test/wire/wiring/x/adapterConsumer/adapterConsumer.js create mode 100644 packages/integration-karma/test/wire/wiring/x/base/base.js create mode 100644 packages/integration-karma/test/wire/wiring/x/broadcastAdapter/broadcastAdapter.js create mode 100644 packages/integration-karma/test/wire/wiring/x/broadcastConsumer/broadcastConsumer.html create mode 100644 packages/integration-karma/test/wire/wiring/x/broadcastConsumer/broadcastConsumer.js create mode 100644 packages/integration-karma/test/wire/wiring/x/echoAdapter/echoAdapter.js create mode 100644 packages/integration-karma/test/wire/wiring/x/inheritedMethods/inheritedMethods.html create mode 100644 packages/integration-karma/test/wire/wiring/x/inheritedMethods/inheritedMethods.js 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 68816d909b..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 @@ -129,6 +129,12 @@ describe('Implicit mode', () => { params: {}, 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 37eddbac0d..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 @@ -60,7 +60,11 @@ describe('observed fields', () => { publicMethods: ["someMethod"], wire: { wiredProp: { - adapter: createElement + adapter: createElement, + hasParams: false, + config: function($cmp) { + return {}; + } } }, track: { @@ -114,7 +118,11 @@ describe('observed fields', () => { }, wire: { function: { - adapter: createElement + adapter: createElement, + hasParams: false, + config: function($cmp) { + return {}; + } } }, track: { @@ -303,7 +311,11 @@ describe('observed fields', () => { publicMethods: ["someMethod"], wire: { wiredProp: { - adapter: createElement + adapter: createElement, + hasParams: false, + config: function($cmp) { + return {}; + } } }, track: { 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 70229e0ec3..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 @@ -40,6 +40,181 @@ describe('Transform property', () => { }, static: { key2: ["fixed", "array"] + }, + hasParams: true, + config: function($cmp) { + return { + key2: ["fixed", "array"], + key1: $cmp.prop1 + }; + } + } + } + }); + + export default _registerComponent(Test, { + tmpl: _tmpl + }); +`, + }, + } + ); + + pluginTest( + 'transforms named imports from static imports', + ` + import { wire } from 'lwc'; + import importedValue from "ns/module"; + import { getFoo } from 'data-service'; + export default class Test { + @wire(getFoo, { key1: importedValue }) + wiredProp; + } + `, + { + output: { + code: ` + import { registerDecorators as _registerDecorators } from "lwc"; + import _tmpl from "./test.html"; + import { registerComponent as _registerComponent } from "lwc"; + import importedValue from "ns/module"; + import { getFoo } from "data-service"; + + class Test { + constructor() { + this.wiredProp = void 0; + } + } + + _registerDecorators(Test, { + wire: { + wiredProp: { + adapter: getFoo, + params: {}, + static: { + key1: importedValue + }, + hasParams: false, + config: function($cmp) { + return { + key1: importedValue + }; + } + } + } + }); + + export default _registerComponent(Test, { + tmpl: _tmpl + }); +`, + }, + } + ); + + pluginTest( + 'transforms parameters with 2 levels deep (foo.bar)', + ` + import { wire } from 'lwc'; + import { getFoo } from 'data-service'; + export default class Test { + @wire(getFoo, { key1: "$prop1.prop2", key2: ["fixed", 'array'], key3: "$p1.p2" }) + wiredProp; + } + `, + { + output: { + code: ` + import { registerDecorators as _registerDecorators } from "lwc"; + import _tmpl from "./test.html"; + import { registerComponent as _registerComponent } from "lwc"; + import { getFoo } from "data-service"; + + class Test { + constructor() { + this.wiredProp = void 0; + } + } + + _registerDecorators(Test, { + wire: { + wiredProp: { + adapter: getFoo, + params: { + key1: "prop1.prop2", + key3: "p1.p2" + }, + static: { + key2: ["fixed", "array"] + }, + hasParams: true, + config: function($cmp) { + let v1 = $cmp.prop1; + let v2 = $cmp.p1; + return { + key2: ["fixed", "array"], + key1: v1 != null ? v1.prop2 : undefined, + key3: v2 != null ? v2.p2 : undefined + }; + } + } + } + }); + + export default _registerComponent(Test, { + tmpl: _tmpl + }); +`, + }, + } + ); + + pluginTest( + 'transforms parameters with multiple levels deep', + ` + import { wire } from 'lwc'; + import { getFoo } from 'data-service'; + export default class Test { + @wire(getFoo, { key1: "$prop1.prop2.prop3.prop4", key2: ["fixed", 'array']}) + wiredProp; + } + `, + { + output: { + code: ` + import { registerDecorators as _registerDecorators } from "lwc"; + import _tmpl from "./test.html"; + import { registerComponent as _registerComponent } from "lwc"; + import { getFoo } from "data-service"; + + class Test { + constructor() { + this.wiredProp = void 0; + } + } + + _registerDecorators(Test, { + wire: { + wiredProp: { + adapter: getFoo, + params: { + key1: "prop1.prop2.prop3.prop4" + }, + static: { + key2: ["fixed", "array"] + }, + hasParams: true, + config: function($cmp) { + let v1 = $cmp.prop1; + return { + key2: ["fixed", "array"], + key1: + v1 != null && + (v1 = v1.prop2) != null && + (v1 = v1.prop3) != null + ? v1.prop4 + : undefined + }; } } } @@ -88,6 +263,15 @@ describe('Transform property', () => { static: { key3: "fixed", key4: ["fixed", "array"] + }, + hasParams: true, + config: function($cmp) { + return { + key3: "fixed", + key4: ["fixed", "array"], + key1: $cmp.prop, + key2: $cmp.prop + }; } } } @@ -171,7 +355,11 @@ describe('Transform property', () => { wiredProp: { adapter: getFoo, params: {}, - static: {} + static: {}, + hasParams: false, + config: function($cmp) { + return {}; + } } } }); @@ -185,7 +373,7 @@ describe('Transform property', () => { ); pluginTest( - 'decorator accepts a member epxression', + 'decorator accepts a member expression', ` import { wire } from 'lwc'; import { Foo } from 'data-service'; @@ -212,7 +400,11 @@ describe('Transform property', () => { wiredProp: { adapter: Foo.Bar, params: {}, - static: {} + static: {}, + hasParams: false, + config: function($cmp) { + return {}; + } } } }); @@ -253,7 +445,11 @@ describe('Transform property', () => { wiredProp: { adapter: Foo.Bar, params: {}, - static: {} + static: {}, + hasParams: false, + config: function($cmp) { + return {}; + } } } }); @@ -314,7 +510,11 @@ describe('Transform property', () => { _registerDecorators(Test, { wire: { wiredProp: { - adapter: getFoo + adapter: getFoo, + hasParams: false, + config: function($cmp) { + return {}; + } } } }); @@ -395,6 +595,114 @@ describe('Transform property', () => { } ); + pluginTest( + "config function should use bracket notation for param when it's definition has invalid identifier as segment", + ` + import { api, wire } from 'lwc'; + import { getFoo } from 'data-service'; + export default class Test { + @wire(getFoo, { key1: "$prop1.a b", key2: "$p1.p2" }) + wiredProp; + } + `, + { + output: { + code: ` + import { registerDecorators as _registerDecorators } from "lwc"; + import _tmpl from "./test.html"; + import { registerComponent as _registerComponent } from "lwc"; + import { getFoo } from "data-service"; + + class Test { + constructor() { + this.wiredProp = void 0; + } + } + + _registerDecorators(Test, { + wire: { + wiredProp: { + adapter: getFoo, + params: { + key1: "prop1.a b", + key2: "p1.p2" + }, + static: {}, + hasParams: true, + config: function($cmp) { + let v1 = $cmp["prop1"]; + let v2 = $cmp.p1; + return { + key1: v1 != null ? v1["a b"] : undefined, + key2: v2 != null ? v2.p2 : undefined + }; + } + } + } + }); + + export default _registerComponent(Test, { + tmpl: _tmpl + }); +`, + }, + } + ); + + pluginTest( + 'config function should use bracket notation when param definition has empty segment', + ` + import { api, wire } from 'lwc'; + import { getFoo } from 'data-service'; + export default class Test { + @wire(getFoo, { key1: "$prop1..prop2", key2: ["fixed", 'array']}) + wiredProp; + } + `, + { + output: { + code: ` + import { registerDecorators as _registerDecorators } from "lwc"; + import _tmpl from "./test.html"; + import { registerComponent as _registerComponent } from "lwc"; + import { getFoo } from "data-service"; + + class Test { + constructor() { + this.wiredProp = void 0; + } + } + + _registerDecorators(Test, { + wire: { + wiredProp: { + adapter: getFoo, + params: { + key1: "prop1..prop2" + }, + static: { + key2: ["fixed", "array"] + }, + hasParams: true, + config: function($cmp) { + let v1 = $cmp["prop1"]; + return { + key2: ["fixed", "array"], + key1: v1 != null && (v1 = v1[""]) != null ? v1["prop2"] : undefined + }; + } + } + } + }); + + export default _registerComponent(Test, { + tmpl: _tmpl + }); +`, + }, + } + ); + pluginTest( 'throws when wired property is combined with @track', ` @@ -479,6 +787,13 @@ describe('Transform property', () => { }, static: { key2: ["fixed"] + }, + hasParams: true, + config: function($cmp) { + return { + key2: ["fixed"], + key1: $cmp.prop1 + }; } }, wired2: { @@ -488,6 +803,13 @@ describe('Transform property', () => { }, static: { key2: ["array"] + }, + hasParams: true, + config: function($cmp) { + return { + key2: ["array"], + key1: $cmp.prop1 + }; } } } @@ -535,7 +857,14 @@ describe('Transform method', () => { static: { key2: ["fixed"] }, - method: 1 + method: 1, + hasParams: true, + config: function($cmp) { + return { + key2: ["fixed"], + key1: $cmp.prop1 + }; + } } } }); diff --git a/packages/@lwc/babel-plugin-component/src/constants.js b/packages/@lwc/babel-plugin-component/src/constants.js index bad385ef8a..9c28e96e55 100644 --- a/packages/@lwc/babel-plugin-component/src/constants.js +++ b/packages/@lwc/babel-plugin-component/src/constants.js @@ -40,6 +40,7 @@ const LWC_SUPPORTED_APIS = new Set([ 'getComponentDef', 'getComponentConstructor', 'isComponentConstructor', + 'createContextProvider', 'readonly', 'register', 'setFeatureFlagForTest', 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 0d9e3a6d3b..da5f4f9819 100644 --- a/packages/@lwc/babel-plugin-component/src/decorators/wire/transform.js +++ b/packages/@lwc/babel-plugin-component/src/decorators/wire/transform.js @@ -9,6 +9,7 @@ const { staticClassProperty, markAsLWCNode } = require('../../utils'); const { LWC_COMPONENT_PROPERTIES } = require('../../constants'); const WIRE_PARAM_PREFIX = '$'; +const WIRE_CONFIG_ARG_NAME = '$cmp'; function isObservedProperty(configProperty) { const propertyValue = configProperty.get('value'); @@ -37,6 +38,109 @@ function getWiredParams(t, wireConfig) { }); } +function getGeneratedConfig(t, wiredValue) { + let counter = 0; + const configBlockBody = []; + const configProps = []; + const generateParameterConfigValue = memberExprPaths => { + // Note: When memberExprPaths ($foo.bar) has an invalid identifier (eg: foo..bar, foo.bar[3]) + // it should (ideally) resolve in a compilation error during validation phase. + // This is not possible due that platform components may have a param definition which is invalid + // but passes compilation, and throwing at compile time would break such components. + // In such cases where the param does not have proper notation, the config generated will use the bracket + // notation to match the current behavior (that most likely end up resolving that param as undefined). + const isInvalidMemberExpr = memberExprPaths.some( + maybeIdentifier => !t.isValidES3Identifier(maybeIdentifier) + ); + const memberExprPropertyGen = !isInvalidMemberExpr ? t.identifier : t.StringLiteral; + + if (memberExprPaths.length === 1) { + return { + configValueExpression: t.memberExpression( + t.identifier(WIRE_CONFIG_ARG_NAME), + memberExprPropertyGen(memberExprPaths[0]) + ), + }; + } + + const varName = 'v' + ++counter; + const varDeclaration = t.variableDeclaration('let', [ + t.variableDeclarator( + t.identifier(varName), + t.memberExpression( + t.identifier(WIRE_CONFIG_ARG_NAME), + memberExprPropertyGen(memberExprPaths[0]), + isInvalidMemberExpr + ) + ), + ]); + + // Results in: v != null && ... (v = v.i) != null && ... (v = v.(n-1)) != null + let conditionTest = t.binaryExpression('!=', t.identifier(varName), t.nullLiteral()); + + for (let i = 1, n = memberExprPaths.length; i < n - 1; i++) { + const nextPropValue = t.assignmentExpression( + '=', + t.identifier(varName), + t.memberExpression( + t.identifier(varName), + memberExprPropertyGen(memberExprPaths[i]), + isInvalidMemberExpr + ) + ); + + conditionTest = t.logicalExpression( + '&&', + conditionTest, + t.binaryExpression('!=', nextPropValue, t.nullLiteral()) + ); + } + + // conditionTest ? v.n : undefined + const configValueExpression = t.conditionalExpression( + conditionTest, + t.memberExpression( + t.identifier(varName), + memberExprPropertyGen(memberExprPaths[memberExprPaths.length - 1]), + isInvalidMemberExpr + ), + t.identifier('undefined') + ); + + return { + varDeclaration, + configValueExpression, + }; + }; + + if (wiredValue.static) { + Array.prototype.push.apply(configProps, wiredValue.static); + } + + if (wiredValue.params) { + wiredValue.params.forEach(param => { + const memberExprPaths = param.value.value.split('.'); + const paramConfigValue = generateParameterConfigValue(memberExprPaths); + + configProps.push(t.objectProperty(param.key, paramConfigValue.configValueExpression)); + + if (paramConfigValue.varDeclaration) { + configBlockBody.push(paramConfigValue.varDeclaration); + } + }); + } + + configBlockBody.push(t.returnStatement(t.objectExpression(configProps))); + + const fnExpression = t.functionExpression( + null, + [t.identifier(WIRE_CONFIG_ARG_NAME)], + t.blockStatement(configBlockBody) + ); + + return t.objectProperty(t.identifier('config'), fnExpression); +} + function buildWireConfigValue(t, wiredValues) { return t.objectExpression( wiredValues.map(wiredValue => { @@ -63,6 +167,14 @@ 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( t.identifier(wiredValue.propertyName), t.objectExpression(wireConfig) diff --git a/packages/@lwc/engine/src/framework/__tests__/error-boundary.spec.ts b/packages/@lwc/engine/src/framework/__tests__/error-boundary.spec.ts index 7ff0a5d3c6..705c64b2b9 100644 --- a/packages/@lwc/engine/src/framework/__tests__/error-boundary.spec.ts +++ b/packages/@lwc/engine/src/framework/__tests__/error-boundary.spec.ts @@ -979,7 +979,7 @@ describe('error boundary component', () => { } } registerDecorators(PreErrorChildContent, { - publicProps: { foo: { config: 1 } }, + publicProps: { foo: { config: 3 } }, }); const baseTmpl = compileTemplate( ` diff --git a/packages/@lwc/engine/src/framework/base-lightning-element.ts b/packages/@lwc/engine/src/framework/base-lightning-element.ts index 297a476878..09525f9110 100644 --- a/packages/@lwc/engine/src/framework/base-lightning-element.ts +++ b/packages/@lwc/engine/src/framework/base-lightning-element.ts @@ -13,19 +13,16 @@ * shape of a component. It is also used internally to apply extra optimizations. */ import { - ArrayReduce, assert, create, defineProperties, freeze, - getOwnPropertyNames, - isFalse, isFunction, isNull, - isObject, seal, defineProperty, isUndefined, + isObject, } from '@lwc/shared'; import { HTMLElementOriginalDescriptors } from './html-properties'; import { @@ -36,7 +33,7 @@ import { } from './component'; import { vmBeingConstructed, isBeingConstructed, isInvokingRender } from './invoker'; import { associateVM, getAssociatedVM, VM } from './vm'; -import { valueObserved, valueMutated } from '../libs/mutation-tracker'; +import { componentValueMutated, componentValueObserved } from './mutation-tracker'; import { dispatchEvent } from '../env/dom'; import { patchComponentWithRestrictions, @@ -93,7 +90,7 @@ function createBridgeToElementDescriptor( } return; } - valueObserved(this, propName); + componentValueObserved(vm, propName); return get.call(vm.elm); }, set(this: ComponentInterface, newValue: any) { @@ -122,10 +119,8 @@ function createBridgeToElementDescriptor( if (newValue !== vm.cmpProps[propName]) { vm.cmpProps[propName] = newValue; - if (isFalse(vm.isDirty)) { - // perf optimization to skip this step if not in the DOM - valueMutated(this, propName); - } + + componentValueMutated(vm, propName); } return set.call(vm.elm, newValue); }, @@ -535,19 +530,15 @@ BaseLightningElementConstructor.prototype = { }, }; -const baseDescriptors = ArrayReduce.call( - getOwnPropertyNames(HTMLElementOriginalDescriptors), - (descriptors, propName) => { - (descriptors as PropertyDescriptorMap)[propName] = createBridgeToElementDescriptor( - propName, - HTMLElementOriginalDescriptors[propName] - ); - return descriptors; - }, - create(null) -) as PropertyDescriptorMap; +export const lightningBasedDescriptors: PropertyDescriptorMap = create(null); +for (const propName in HTMLElementOriginalDescriptors) { + lightningBasedDescriptors[propName] = createBridgeToElementDescriptor( + propName, + HTMLElementOriginalDescriptors[propName] + ); +} -defineProperties(BaseLightningElementConstructor.prototype, baseDescriptors); +defineProperties(BaseLightningElementConstructor.prototype, lightningBasedDescriptors); const ComponentConstructorAsCustomElementConstructorMap = new Map< ComponentConstructor, diff --git a/packages/@lwc/engine/src/framework/component.ts b/packages/@lwc/engine/src/framework/component.ts index eb66671d2e..8a13ece6ee 100644 --- a/packages/@lwc/engine/src/framework/component.ts +++ b/packages/@lwc/engine/src/framework/component.ts @@ -11,7 +11,6 @@ import { isInvokingRender, invokeEventListener, } from './invoker'; -import { invokeServiceHook, Services } from './services'; import { VM, UninitializedVM, scheduleRehydration } from './vm'; import { VNodes } from '../3rdparty/snabbdom/types'; import { ReactiveObserver } from '../libs/mutation-tracker'; @@ -67,31 +66,8 @@ export function createComponent(uninitializedVm: UninitializedVM, Ctor: Componen } } -export function linkComponent(vm: VM) { - const { - def: { wire }, - } = vm; - - if (!isUndefined(wire)) { - const { wiring } = Services; - if (wiring) { - invokeServiceHook(vm, wiring); - } - } -} - export function getTemplateReactiveObserver(vm: VM): ReactiveObserver { return new ReactiveObserver(() => { - if (process.env.NODE_ENV !== 'production') { - assert.invariant( - !isInvokingRender, - `Mutating property is not allowed during the rendering life-cycle of ${getVMBeingRendered()}.` - ); - assert.invariant( - !isUpdatingTemplate, - `Mutating property is not allowed while updating template of ${getVMBeingRendered()}.` - ); - } const { isDirty } = vm; if (isFalse(isDirty)) { markComponentAsDirty(vm); diff --git a/packages/@lwc/engine/src/framework/context-provider.ts b/packages/@lwc/engine/src/framework/context-provider.ts new file mode 100644 index 0000000000..d16c71736d --- /dev/null +++ b/packages/@lwc/engine/src/framework/context-provider.ts @@ -0,0 +1,63 @@ +/* + * 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 { isUndefined, ArrayIndexOf } from '@lwc/shared'; +import { guid } from './utils'; +import { WireAdapterConstructor, ContextValue, getAdapterToken, setAdapterToken } from './wiring'; + +type WireContextDisconnectCallback = () => void; +type WireContextInternalProtocolCallback = ( + newContext: ContextValue, + disconnectCallback: WireContextDisconnectCallback +) => void; +interface ContextConsumer { + provide(newContext: ContextValue): void; +} + +interface WireContextEvent extends CustomEvent {} + +interface ContextProviderOptions { + consumerConnectedCallback: (consumer: ContextConsumer) => void; + consumerDisconnectedCallback?: (consumer: ContextConsumer) => void; +} + +// this is lwc internal implementation +export function createContextProvider(adapter: WireAdapterConstructor) { + let adapterContextToken = getAdapterToken(adapter); + if (!isUndefined(adapterContextToken)) { + throw new Error(`Adapter already have a context provider.`); + } + adapterContextToken = guid(); + setAdapterToken(adapter, adapterContextToken); + const providers: EventTarget[] = []; + + return (elm: EventTarget, options: ContextProviderOptions) => { + if (ArrayIndexOf.call(providers, elm) !== -1) { + throw new Error(`Adapter was already installed on ${elm}.`); + } + providers.push(elm); + + const { consumerConnectedCallback, consumerDisconnectedCallback } = options; + elm.addEventListener( + adapterContextToken as string, + ((evt: WireContextEvent) => { + const { detail } = evt; + const consumer: ContextConsumer = { + provide(newContext) { + detail(newContext, disconnectCallback); + }, + }; + const disconnectCallback = () => { + if (!isUndefined(consumerDisconnectedCallback)) { + consumerDisconnectedCallback(consumer); + } + }; + consumerConnectedCallback(consumer); + evt.stopImmediatePropagation(); + }) as EventListener + ); + }; +} diff --git a/packages/@lwc/engine/src/framework/decorators/__tests__/wire.spec.ts b/packages/@lwc/engine/src/framework/decorators/__tests__/wire.spec.ts index 9e6703508b..81c22708d8 100644 --- a/packages/@lwc/engine/src/framework/decorators/__tests__/wire.spec.ts +++ b/packages/@lwc/engine/src/framework/decorators/__tests__/wire.spec.ts @@ -4,233 +4,10 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { compileTemplate } from 'test-utils'; import { createElement, LightningElement, registerDecorators } from '../../main'; import wire from '../wire'; -const emptyTemplate = compileTemplate(``); - describe('wire.ts', () => { - describe('integration', () => { - it('should support setting a wired property in constructor', () => { - expect.assertions(3); - - const o = { x: 1 }; - class MyComponent extends LightningElement { - constructor() { - super(); - expect('foo' in this).toBe(true); - this.foo = o; - - expect(this.foo).toEqual(o); - expect(this.foo).not.toBe(o); - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - }); - - const elm = createElement('x-foo', { is: MyComponent }); - document.body.appendChild(elm); - }); - - it('should support wired properties', () => { - expect.assertions(2); - - const o = { x: 1 }; - class MyComponent extends LightningElement { - injectFoo(v) { - this.foo = v; - expect(this.foo).toEqual(o); - expect(this.foo).not.toBe(o); - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - publicMethods: ['injectFoo'], - }); - - const elm = createElement('x-foo', { is: MyComponent }); - document.body.appendChild(elm); - elm.injectFoo(o); - }); - - it('should make wired properties reactive', () => { - let counter = 0; - class MyComponent extends LightningElement { - injectFoo(v) { - this.foo = v; - } - constructor() { - super(); - this.foo = { x: 1 }; - } - render() { - counter++; - this.foo.x; - return emptyTemplate; - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - publicMethods: ['injectFoo'], - }); - - const elm = createElement('x-foo', { is: MyComponent }); - document.body.appendChild(elm); - elm.injectFoo({ x: 2 }); - - return Promise.resolve().then(() => { - expect(counter).toBe(2); - }); - }); - - it('should make properties of a wired object property reactive', () => { - let counter = 0; - class MyComponent extends LightningElement { - injectFooDotX(x) { - this.foo.x = x; - } - constructor() { - super(); - this.foo = { x: 1 }; - } - render() { - counter++; - this.foo.x; - return emptyTemplate; - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - publicMethods: ['injectFooDotX'], - }); - - const elm = createElement('x-foo', { is: MyComponent }); - document.body.appendChild(elm); - elm.injectFooDotX(2); - - return Promise.resolve().then(() => { - expect(counter).toBe(2); - }); - }); - - it('should not proxify primitive value', function() { - expect.assertions(1); - - class MyComponent extends LightningElement { - injectFoo(v) { - this.foo = v; - expect(this.foo).toBe(1); - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - publicMethods: ['injectFoo'], - }); - - const elm = createElement('x-foo', { is: MyComponent }); - document.body.appendChild(elm); - elm.injectFoo(1); - }); - - it('should proxify plain arrays', function() { - expect.assertions(2); - - const a = []; - class MyComponent extends LightningElement { - injectFoo(v) { - this.foo = v; - expect(this.foo).toEqual(a); - expect(this.foo).not.toBe(a); - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - publicMethods: ['injectFoo'], - }); - - const elm = createElement('x-foo', { is: MyComponent }); - document.body.appendChild(elm); - elm.injectFoo(a); - }); - - it('should not proxify exotic objects', function() { - expect.assertions(1); - - class MyComponent extends LightningElement { - injectFoo(v) { - this.foo = v; - expect(this.foo).toBe(d); - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - publicMethods: ['injectFoo'], - }); - - const elm = createElement('x-foo', { is: MyComponent }); - document.body.appendChild(elm); - - const d = new Date(); - elm.injectFoo(d); - }); - - it('should not proxify non-observable object', function() { - expect.assertions(1); - - class MyComponent extends LightningElement { - injectFoo(v) { - this.foo = v; - expect(this.foo).toBe(o); - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - publicMethods: ['injectFoo'], - }); - - const elm = createElement('x-foo', { is: MyComponent }); - document.body.appendChild(elm); - - const o = Object.create({}); - elm.injectFoo(o); - }); - - it('should not throw an error if wire is observable object', function() { - class MyComponent extends LightningElement { - injectFoo(v) { - this.foo = v; - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - publicMethods: ['injectFoo'], - }); - const elm = createElement('x-foo', { is: MyComponent }); - document.body.appendChild(elm); - expect(() => { - elm.injectFoo({}); - }).not.toThrow(); - }); - - it('should throw a wire property is mutated during rendering', function() { - class MyComponent extends LightningElement { - render() { - this.foo = 1; - return emptyTemplate; - } - } - registerDecorators(MyComponent, { - wire: { foo: {} }, - }); - const elm = createElement('x-foo', { is: MyComponent }); - expect(() => { - document.body.appendChild(elm); - }).toThrow(); - }); - }); - describe('@wire misuse', () => { it('should throw when invoking wire without adapter', () => { class MyComponent extends LightningElement { @@ -243,5 +20,17 @@ describe('wire.ts', () => { createElement('x-foo', { is: MyComponent }); }).toThrow('@wire(adapter, config?) may only be used as a decorator.'); }); + + it('should throw if wire adapter is not truthy', () => { + class MyComponent extends LightningElement {} + + expect(() => { + registerDecorators(MyComponent, { + wire: { + foo: {}, + }, + }); + }).toThrow('Assert Violation: @wire on field "foo": adapter id must be truthy.'); + }); }); }); diff --git a/packages/@lwc/engine/src/framework/decorators/api.ts b/packages/@lwc/engine/src/framework/decorators/api.ts index 14fbd13c5e..a9b478db62 100644 --- a/packages/@lwc/engine/src/framework/decorators/api.ts +++ b/packages/@lwc/engine/src/framework/decorators/api.ts @@ -5,13 +5,16 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import features from '@lwc/features'; -import { assert, isFalse, isFunction, isObject, isTrue, isUndefined, toString } from '@lwc/shared'; +import { assert, isFalse, isFunction, isTrue, isUndefined, toString } from '@lwc/shared'; import { logError } from '../../shared/logger'; import { isInvokingRender, isBeingConstructed } from '../invoker'; -import { valueObserved, valueMutated, ReactiveObserver } from '../../libs/mutation-tracker'; -import { ComponentInterface, ComponentConstructor } from '../component'; +import { + componentValueObserved, + componentValueMutated, + ReactiveObserver, +} from '../mutation-tracker'; +import { ComponentInterface } from '../component'; import { getAssociatedVM, rerenderVM, VM } from '../vm'; -import { getDecoratorsRegisteredMeta } from './register'; import { addCallbackToNextTick } from '../utils'; import { isUpdatingTemplate, getVMBeingRendered } from '../template'; @@ -20,51 +23,15 @@ import { isUpdatingTemplate, getVMBeingRendered } from '../template'; * LWC Components. This function implements the internals of this * decorator. */ -export default function api( - target: ComponentConstructor, - propName: PropertyKey, - descriptor: PropertyDescriptor | undefined -): PropertyDescriptor { +export default function api(target: any, propertyKey: string, descriptor: PropertyDescriptor): void; +export default function api() { if (process.env.NODE_ENV !== 'production') { - if (arguments.length !== 3) { - assert.fail(`@api decorator can only be used as a decorator function.`); - } - } - if (process.env.NODE_ENV !== 'production') { - assert.invariant( - !descriptor || isFunction(descriptor.get) || isFunction(descriptor.set), - `Invalid property ${toString( - propName - )} definition in ${target}, it cannot be a prototype definition if it is a public property. Instead use the constructor to define it.` - ); - if (isObject(descriptor) && isFunction(descriptor.set)) { - assert.isTrue( - isObject(descriptor) && isFunction(descriptor.get), - `Missing getter for property ${toString( - propName - )} decorated with @api in ${target}. You cannot have a setter without the corresponding getter.` - ); - } - } - const meta = getDecoratorsRegisteredMeta(target); - // initializing getters and setters for each public prop on the target prototype - if (isObject(descriptor) && (isFunction(descriptor.get) || isFunction(descriptor.set))) { - // if it is configured as an accessor it must have a descriptor - // @ts-ignore it must always be set before calling this method - meta.props[propName].config = isFunction(descriptor.set) ? 3 : 1; - return createPublicAccessorDescriptor(target, propName, descriptor); - } else { - // @ts-ignore it must always be set before calling this method - meta.props[propName].config = 0; - return createPublicPropertyDescriptor(target, propName, descriptor); + assert.fail(`@api decorator can only be used as a decorator function.`); } + throw new Error(); } -function createPublicPropertyDescriptor( - proto: ComponentConstructor, - key: PropertyKey, - descriptor: PropertyDescriptor | undefined -): PropertyDescriptor { +export function createPublicPropertyDescriptor(key: string): PropertyDescriptor { return { get(this: ComponentInterface): any { const vm = getAssociatedVM(this); @@ -80,7 +47,7 @@ function createPublicPropertyDescriptor( } return; } - valueObserved(this, key); + componentValueObserved(vm, key); return vm.cmpProps[key]; }, set(this: ComponentInterface, newValue: any) { @@ -102,13 +69,10 @@ function createPublicPropertyDescriptor( } vm.cmpProps[key] = newValue; - // avoid notification of observability if the instance is already dirty - if (isFalse(vm.isDirty)) { - // perf optimization to skip this step if the component is dirty already. - valueMutated(this, key); - } + componentValueMutated(vm, key); }, - enumerable: isUndefined(descriptor) ? true : descriptor.enumerable, + enumerable: true, + configurable: true, }; } @@ -148,23 +112,19 @@ class AccessorReactiveObserver extends ReactiveObserver { } } -function createPublicAccessorDescriptor( - Ctor: ComponentConstructor, +export function createPublicAccessorDescriptor( key: PropertyKey, descriptor: PropertyDescriptor ): PropertyDescriptor { - const { get, set, enumerable } = descriptor; + const { get, set, enumerable, configurable } = descriptor; if (!isFunction(get)) { if (process.env.NODE_ENV !== 'production') { - assert.fail( - `Invalid attempt to create public property descriptor ${toString( - key - )} in ${Ctor}. It is missing the getter declaration with @api get ${toString( - key - )}() {} syntax.` + assert.invariant( + isFunction(get), + `Invalid compiler output for public accessor ${toString(key)} decorated with @api` ); } - throw new TypeError(); + throw new Error(); } return { get(this: ComponentInterface): any { @@ -217,5 +177,6 @@ function createPublicAccessorDescriptor( } }, enumerable, + configurable, }; } diff --git a/packages/@lwc/engine/src/framework/decorators/decorate.ts b/packages/@lwc/engine/src/framework/decorators/decorate.ts deleted file mode 100644 index 7e3ceceff3..0000000000 --- a/packages/@lwc/engine/src/framework/decorators/decorate.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { - defineProperty, - getOwnPropertyDescriptor, - getOwnPropertyNames, - isFunction, - isUndefined, -} from '@lwc/shared'; - -export type DecoratorFunction = ( - Ctor: any, - key: PropertyKey, - descriptor: PropertyDescriptor | undefined -) => PropertyDescriptor; -export type DecoratorMap = Record; - -/** - * EXPERIMENTAL: This function allows for the registration of "services" in - * LWC by exposing hooks into the component life-cycle. This API is subject - * to change or being removed. - */ -export default function decorate(Ctor: any, decorators: DecoratorMap): any { - // intentionally comparing decorators with null and undefined - if (!isFunction(Ctor) || decorators == null) { - throw new TypeError(); - } - const props = getOwnPropertyNames(decorators); - // intentionally allowing decoration of classes only for now - const target = Ctor.prototype; - for (let i = 0, len = props.length; i < len; i += 1) { - const propName = props[i]; - const decorator = decorators[propName]; - if (!isFunction(decorator)) { - throw new TypeError(); - } - const originalDescriptor = getOwnPropertyDescriptor(target, propName); - const descriptor = decorator(Ctor, propName, originalDescriptor); - if (!isUndefined(descriptor)) { - defineProperty(target, propName, descriptor); - } - } - return Ctor; // chaining -} diff --git a/packages/@lwc/engine/src/framework/decorators/register.ts b/packages/@lwc/engine/src/framework/decorators/register.ts index d42de095d1..3c6b59e7ec 100644 --- a/packages/@lwc/engine/src/framework/decorators/register.ts +++ b/packages/@lwc/engine/src/framework/decorators/register.ts @@ -4,163 +4,305 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { assert, assign, create, getOwnPropertyNames, isFunction, isUndefined } from '@lwc/shared'; +import { + assert, + create, + isFunction, + isUndefined, + forEach, + defineProperty, + getOwnPropertyDescriptor, + toString, + isFalse, +} from '@lwc/shared'; import { ComponentConstructor } from '../component'; -import wireDecorator from './wire'; -import trackDecorator from './track'; -import apiDecorator from './api'; +import { internalWireFieldDecorator } from './wire'; +import { internalTrackDecorator } from './track'; +import { createPublicPropertyDescriptor, createPublicAccessorDescriptor } from './api'; +import { + WireAdapterConstructor, + storeWiredMethodMeta, + storeWiredFieldMeta, + ConfigCallback, +} from '../wiring'; import { EmptyObject } from '../utils'; -import { getAttrNameFromPropName } from '../attributes'; -import decorate, { DecoratorMap } from './decorate'; +import { createObservedFieldPropertyDescriptor } from '../observed-fields'; -export interface PropDef { - config: number; +// data produced by compiler +type WireCompilerMeta = Record; +type TrackCompilerMeta = Record; +type MethodCompilerMeta = string[]; +type PropCompilerMeta = Record; +export enum PropType { + Field = 0, + Set = 1, + Get = 2, + GetSet = 3, +} + +interface PropCompilerDef { + config: PropType; // 0 m type: string; // TODO [#1301]: make this an enum - attr: string; } -export interface WireDef { +interface WireCompilerDef { method?: number; - [key: string]: any; + adapter: WireAdapterConstructor; + config: ConfigCallback; + hasParams: boolean; } -export interface PropsDef { - [key: string]: PropDef; +interface RegisterDecoratorMeta { + readonly publicMethods?: MethodCompilerMeta; + readonly publicProps?: PropCompilerMeta; + readonly track?: TrackCompilerMeta; + readonly wire?: WireCompilerMeta; + readonly fields?: string[]; } -export interface TrackDef { - [key: string]: 1; + +function validateObservedField( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { + if (process.env.NODE_ENV !== 'production') { + if (!isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid field ${fieldName} declaration.`); + } + } } -type PublicMethod = (...args: any[]) => any; -export interface MethodDef { - [key: string]: PublicMethod; + +function validateFieldDecoratedWithTrack( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { + if (process.env.NODE_ENV !== 'production') { + if (!isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid @track ${fieldName} declaration.`); + } + } } -export interface WireHash { - [key: string]: WireDef; + +function validateFieldDecoratedWithWire( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { + if (process.env.NODE_ENV !== 'production') { + if (!isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid @wire(...) ${fieldName} field declaration.`); + } + } } -export interface RegisterDecoratorMeta { - readonly publicMethods?: string[]; - readonly publicProps?: PropsDef; - readonly track?: TrackDef; - readonly wire?: WireHash; - readonly fields?: string[]; +function validateMethodDecoratedWithWire( + Ctor: ComponentConstructor, + methodName: string, + descriptor: PropertyDescriptor | undefined +) { + if (process.env.NODE_ENV !== 'production') { + if ( + isUndefined(descriptor) || + !isFunction(descriptor.value) || + isFalse(descriptor.writable) + ) { + assert.fail(`Compiler Error: Invalid @wire(...) ${methodName} method declaration.`); + } + } } -export interface DecoratorMeta { - wire: WireHash | undefined; - track: TrackDef; - props: PropsDef; - methods: MethodDef; - fields?: string[]; +function validateFieldDecoratedWithApi( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { + if (process.env.NODE_ENV !== 'production') { + if (!isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid @api ${fieldName} field declaration.`); + } + } } -const signedDecoratorToMetaMap: Map = new Map(); +function validateAccessorDecoratedWithApi( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { + if (process.env.NODE_ENV !== 'production') { + if (isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid @api get ${fieldName} accessor declaration.`); + } else if (isFunction(descriptor.set)) { + assert.isTrue( + isFunction(descriptor.get), + `Compiler Error: Missing getter for property ${toString( + fieldName + )} decorated with @api in ${Ctor}. You cannot have a setter without the corresponding getter.` + ); + } else if (!isFunction(descriptor.get)) { + assert.fail(`Compiler Error: Missing @api get ${fieldName} accessor declaration.`); + } + } +} + +function validateMethodDecoratedWithApi( + Ctor: ComponentConstructor, + methodName: string, + descriptor: PropertyDescriptor | undefined +) { + if (process.env.NODE_ENV !== 'production') { + if ( + isUndefined(descriptor) || + !isFunction(descriptor.value) || + isFalse(descriptor.writable) + ) { + assert.fail(`Compiler Error: Invalid @api ${methodName} method declaration.`); + } + } +} /** * INTERNAL: This function can only be invoked by compiled code. The compiler - * will prevent this function from being imported by userland code. + * will prevent this function from being imported by user-land code. */ export function registerDecorators( Ctor: ComponentConstructor, meta: RegisterDecoratorMeta ): ComponentConstructor { - const decoratorMap: DecoratorMap = create(null); - const props = getPublicPropertiesHash(Ctor, meta.publicProps); - const methods = getPublicMethodsHash(Ctor, meta.publicMethods); - const wire = getWireHash(Ctor, meta.wire); - const track = getTrackHash(Ctor, meta.track); - const fields = meta.fields; - signedDecoratorToMetaMap.set(Ctor, { - props, - methods, - wire, - track, - fields, - }); - for (const propName in props) { - decoratorMap[propName] = apiDecorator; - } - if (wire) { - for (const propName in wire) { - const wireDef = wire[propName]; - if (wireDef.method) { - // for decorated methods we need to do nothing - continue; + const proto = Ctor.prototype; + const { publicProps, publicMethods, wire, track, fields } = meta; + const apiMethods: PropertyDescriptorMap = create(null); + const apiFields: PropertyDescriptorMap = create(null); + const wiredMethods: PropertyDescriptorMap = create(null); + const wiredFields: PropertyDescriptorMap = create(null); + const observedFields: PropertyDescriptorMap = create(null); + const apiFieldsConfig: Record = create(null); + let descriptor: PropertyDescriptor | undefined; + if (!isUndefined(publicProps)) { + for (const fieldName in publicProps) { + const propConfig = publicProps[fieldName]; + apiFieldsConfig[fieldName] = propConfig.config; + + descriptor = getOwnPropertyDescriptor(proto, fieldName); + if (propConfig.config > 0) { + // accessor declaration + if (process.env.NODE_ENV !== 'production') { + validateAccessorDecoratedWithApi(Ctor, fieldName, descriptor); + } + if (isUndefined(descriptor)) { + throw new Error(); + } + descriptor = createPublicAccessorDescriptor(fieldName, descriptor); + } else { + // field declaration + if (process.env.NODE_ENV !== 'production') { + validateFieldDecoratedWithApi(Ctor, fieldName, descriptor); + } + descriptor = createPublicPropertyDescriptor(fieldName); } - decoratorMap[propName] = wireDecorator(wireDef.adapter, wireDef.params); + apiFields[fieldName] = descriptor; + defineProperty(proto, fieldName, descriptor); } } - if (track) { - for (const propName in track) { - decoratorMap[propName] = trackDecorator; + if (!isUndefined(publicMethods)) { + forEach.call(publicMethods, methodName => { + descriptor = getOwnPropertyDescriptor(proto, methodName); + if (process.env.NODE_ENV !== 'production') { + validateMethodDecoratedWithApi(Ctor, methodName, descriptor); + } + if (isUndefined(descriptor)) { + throw new Error(); + } + apiMethods[methodName] = descriptor; + }); + } + if (!isUndefined(wire)) { + for (const fieldOrMethodName in wire) { + const { adapter, method, config: configCallback, hasParams } = wire[fieldOrMethodName]; + descriptor = getOwnPropertyDescriptor(proto, fieldOrMethodName); + if (method === 1) { + if (process.env.NODE_ENV !== 'production') { + assert.isTrue( + adapter, + `@wire on method "${fieldOrMethodName}": adapter id must be truthy.` + ); + validateMethodDecoratedWithWire(Ctor, fieldOrMethodName, descriptor); + } + if (isUndefined(descriptor)) { + throw new Error(); + } + wiredMethods[fieldOrMethodName] = descriptor; + storeWiredMethodMeta(descriptor, adapter, configCallback, hasParams); + } else { + if (process.env.NODE_ENV !== 'production') { + assert.isTrue( + adapter, + `@wire on field "${fieldOrMethodName}": adapter id must be truthy.` + ); + validateFieldDecoratedWithWire(Ctor, fieldOrMethodName, descriptor); + } + descriptor = internalWireFieldDecorator(fieldOrMethodName); + wiredFields[fieldOrMethodName] = descriptor; + storeWiredFieldMeta(descriptor, adapter, configCallback, hasParams); + defineProperty(proto, fieldOrMethodName, descriptor); + } } } - decorate(Ctor, decoratorMap); - return Ctor; -} -export function getDecoratorsRegisteredMeta(Ctor: ComponentConstructor): DecoratorMeta | undefined { - return signedDecoratorToMetaMap.get(Ctor); -} - -function getTrackHash(target: ComponentConstructor, track: TrackDef | undefined): TrackDef { - if (isUndefined(track) || getOwnPropertyNames(track).length === 0) { - return EmptyObject; + if (!isUndefined(track)) { + for (const fieldName in track) { + descriptor = getOwnPropertyDescriptor(proto, fieldName); + if (process.env.NODE_ENV !== 'production') { + validateFieldDecoratedWithTrack(Ctor, fieldName, descriptor); + } + descriptor = internalTrackDecorator(fieldName); + defineProperty(proto, fieldName, descriptor); + } } - - // TODO [#1302]: check that anything in `track` is correctly defined in the prototype - return assign(create(null), track); + if (!isUndefined(fields)) { + for (let i = 0, n = fields.length; i < n; i++) { + const fieldName = fields[i]; + descriptor = getOwnPropertyDescriptor(proto, fieldName); + if (process.env.NODE_ENV !== 'production') { + validateObservedField(Ctor, fieldName, descriptor); + } + observedFields[fieldName] = createObservedFieldPropertyDescriptor(fieldName); + } + } + setDecoratorsMeta(Ctor, { + apiMethods, + apiFields, + apiFieldsConfig, + wiredMethods, + wiredFields, + observedFields, + }); + return Ctor; } -function getWireHash( - target: ComponentConstructor, - wire: WireHash | undefined -): WireHash | undefined { - if (isUndefined(wire) || getOwnPropertyNames(wire).length === 0) { - return; - } +const signedDecoratorToMetaMap: Map = new Map(); - // TODO [#1302]: check that anything in `wire` is correctly defined in the prototype - return assign(create(null), wire); +interface DecoratorMeta { + readonly apiMethods: PropertyDescriptorMap; + readonly apiFields: PropertyDescriptorMap; + readonly apiFieldsConfig: Record; + readonly wiredMethods: PropertyDescriptorMap; + readonly wiredFields: PropertyDescriptorMap; + readonly observedFields: PropertyDescriptorMap; } -function getPublicPropertiesHash( - target: ComponentConstructor, - props: PropsDef | undefined -): PropsDef { - if (isUndefined(props) || getOwnPropertyNames(props).length === 0) { - return EmptyObject; - } - return getOwnPropertyNames(props).reduce((propsHash: PropsDef, propName: string): PropsDef => { - const attr = getAttrNameFromPropName(propName); - propsHash[propName] = assign( - { - config: 0, - type: 'any', - attr, - }, - props[propName] - ); - return propsHash; - }, create(null)); +function setDecoratorsMeta(Ctor: ComponentConstructor, meta: DecoratorMeta) { + signedDecoratorToMetaMap.set(Ctor, meta); } -function getPublicMethodsHash( - target: ComponentConstructor, - publicMethods: string[] | undefined -): MethodDef { - if (isUndefined(publicMethods) || publicMethods.length === 0) { - return EmptyObject; - } - return publicMethods.reduce((methodsHash: MethodDef, methodName: string): MethodDef => { - const method = (target.prototype as any)[methodName]; - - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - isFunction(method), - `Component "${target.name}" should have a method \`${methodName}\` instead of ${method}.` - ); - } +const defaultMeta: DecoratorMeta = { + apiMethods: EmptyObject, + apiFields: EmptyObject, + apiFieldsConfig: EmptyObject, + wiredMethods: EmptyObject, + wiredFields: EmptyObject, + observedFields: EmptyObject, +}; - methodsHash[methodName] = method; - return methodsHash; - }, create(null)); +export function getDecoratorsMeta(Ctor: ComponentConstructor): DecoratorMeta { + const meta = signedDecoratorToMetaMap.get(Ctor); + return isUndefined(meta) ? defaultMeta : meta; } diff --git a/packages/@lwc/engine/src/framework/decorators/track.ts b/packages/@lwc/engine/src/framework/decorators/track.ts index fcc6fdf94e..0612e9a86b 100644 --- a/packages/@lwc/engine/src/framework/decorators/track.ts +++ b/packages/@lwc/engine/src/framework/decorators/track.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { assert, isFalse, isUndefined, toString } from '@lwc/shared'; -import { valueObserved, valueMutated } from '../../libs/mutation-tracker'; +import { assert, toString } from '@lwc/shared'; +import { componentValueObserved, componentValueMutated } from '../mutation-tracker'; import { isInvokingRender } from '../invoker'; import { getAssociatedVM } from '../vm'; import { reactiveMembrane } from '../membrane'; @@ -13,57 +13,33 @@ import { ComponentInterface } from '../component'; import { isUpdatingTemplate, getVMBeingRendered } from '../template'; /** - * @track decorator to mark fields as reactive in - * LWC Components. This function implements the internals of this - * decorator. + * @track decorator function to mark field value as reactive in + * LWC Components. This function can also be invoked directly + * with any value to obtain the trackable version of the value. */ export default function track( target: any, - prop: PropertyKey, - descriptor?: PropertyDescriptor -): any { + propertyKey: string, + descriptor: PropertyDescriptor +): any; +export default function track(target: any): any { if (arguments.length === 1) { return reactiveMembrane.getProxy(target); } if (process.env.NODE_ENV !== 'production') { - if (arguments.length !== 3) { - assert.fail( - `@track decorator can only be used with one argument to return a trackable object, or as a decorator function.` - ); - } - if (!isUndefined(descriptor)) { - const { get, set, configurable, writable } = descriptor; - assert.isTrue( - !get && !set, - `Compiler Error: A @track decorator can only be applied to a public field.` - ); - assert.isTrue( - configurable !== false, - `Compiler Error: A @track decorator can only be applied to a configurable property.` - ); - assert.isTrue( - writable !== false, - `Compiler Error: A @track decorator can only be applied to a writable property.` - ); - } + assert.fail( + `@track decorator can only be used with one argument to return a trackable object, or as a decorator function.` + ); } - return createTrackedPropertyDescriptor( - target, - prop, - isUndefined(descriptor) ? true : descriptor.enumerable === true - ); + throw new Error(); } -export function createTrackedPropertyDescriptor( - Ctor: any, - key: PropertyKey, - enumerable: boolean -): PropertyDescriptor { +export function internalTrackDecorator(key: string): PropertyDescriptor { return { get(this: ComponentInterface): any { const vm = getAssociatedVM(this); - valueObserved(this, key); - return vm.cmpTrack[key]; + componentValueObserved(vm, key); + return vm.cmpFields[key]; }, set(this: ComponentInterface, newValue: any) { const vm = getAssociatedVM(this); @@ -83,15 +59,13 @@ export function createTrackedPropertyDescriptor( ); } const reactiveOrAnyValue = reactiveMembrane.getProxy(newValue); - if (reactiveOrAnyValue !== vm.cmpTrack[key]) { - vm.cmpTrack[key] = reactiveOrAnyValue; - if (isFalse(vm.isDirty)) { - // perf optimization to skip this step if the track property is on a component that is already dirty - valueMutated(this, key); - } + if (reactiveOrAnyValue !== vm.cmpFields[key]) { + vm.cmpFields[key] = reactiveOrAnyValue; + + componentValueMutated(vm, key); } }, - enumerable, + enumerable: true, configurable: true, }; } diff --git a/packages/@lwc/engine/src/framework/decorators/wire.ts b/packages/@lwc/engine/src/framework/decorators/wire.ts index 4e4c52ef1e..cec802ddb8 100644 --- a/packages/@lwc/engine/src/framework/decorators/wire.ts +++ b/packages/@lwc/engine/src/framework/decorators/wire.ts @@ -4,53 +4,44 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { assert, isObject, isUndefined } from '@lwc/shared'; -import { createTrackedPropertyDescriptor } from './track'; -import { DecoratorFunction } from './decorate'; -import { ComponentConstructor } from '../component'; - -function wireDecorator( - target: ComponentConstructor, - prop: PropertyKey, - descriptor: PropertyDescriptor | undefined -): PropertyDescriptor | any { - if (process.env.NODE_ENV !== 'production') { - if (!isUndefined(descriptor)) { - const { get, set, configurable, writable } = descriptor; - assert.isTrue( - !get && !set, - `Compiler Error: A @wire decorator can only be applied to a public field.` - ); - assert.isTrue( - configurable !== false, - `Compiler Error: A @wire decorator can only be applied to a configurable property.` - ); - assert.isTrue( - writable !== false, - `Compiler Error: A @wire decorator can only be applied to a writable property.` - ); - } - } - return createTrackedPropertyDescriptor( - target, - prop, - isObject(descriptor) ? descriptor.enumerable === true : true - ); -} +import { assert } from '@lwc/shared'; +import { ComponentInterface } from '../component'; +import { componentValueObserved } from '../mutation-tracker'; +import { getAssociatedVM } from '../vm'; +import { WireAdapterConstructor } from '../wiring'; /** * @wire decorator to wire fields and methods to a wire adapter in * LWC Components. This function implements the internals of this * decorator. */ -export default function wire(_adapter: any, _config: any): DecoratorFunction { - const len = arguments.length; - if (len > 0 && len < 3) { - return wireDecorator; - } else { - if (process.env.NODE_ENV !== 'production') { - assert.fail('@wire(adapter, config?) may only be used as a decorator.'); - } - throw new TypeError(); +export default function wire( + _adapter: WireAdapterConstructor, + _config?: Record +): PropertyDecorator | MethodDecorator { + if (process.env.NODE_ENV !== 'production') { + assert.fail('@wire(adapter, config?) may only be used as a decorator.'); } + throw new Error(); +} + +export function internalWireFieldDecorator(key: string): PropertyDescriptor { + return { + get(this: ComponentInterface): any { + const vm = getAssociatedVM(this); + componentValueObserved(vm, key); + return vm.cmpFields[key]; + }, + set(this: ComponentInterface, value: any) { + const vm = getAssociatedVM(this); + /** + * intentionally ignoring the reactivity here since this is just + * letting the author to do the wrong thing, but it will keep our + * system to be backward compatible. + */ + vm.cmpFields[key] = value; + }, + enumerable: true, + configurable: true, + }; } diff --git a/packages/@lwc/engine/src/framework/def.ts b/packages/@lwc/engine/src/framework/def.ts index b5450bec7a..0b2a2af818 100644 --- a/packages/@lwc/engine/src/framework/def.ts +++ b/packages/@lwc/engine/src/framework/def.ts @@ -14,60 +14,56 @@ */ import { - ArrayReduce, assert, assign, create, defineProperties, freeze, - getOwnPropertyNames, getPrototypeOf, isFunction, isNull, isUndefined, setPrototypeOf, + keys, } from '@lwc/shared'; import { getAttrNameFromPropName } from './attributes'; +import { EmptyObject } from './utils'; import { ComponentConstructor, ErrorCallback, ComponentMeta, getComponentRegisteredMeta, } from './component'; -import { createObservedFieldsDescriptorMap } from './observed-fields'; import { Template } from './template'; -import { HTMLElementOriginalDescriptors } from './html-properties'; -import { BaseLightningElement } from './base-lightning-element'; +import { BaseLightningElement, lightningBasedDescriptors } from './base-lightning-element'; +import { PropType, getDecoratorsMeta } from './decorators/register'; +import { defaultEmptyTemplate } from './secure-template'; + import { BaseBridgeElement, HTMLBridgeElementFactory, HTMLElementConstructor, } from './base-bridge-element'; -import { - getDecoratorsRegisteredMeta, - DecoratorMeta, - PropsDef, - WireHash, - MethodDef, - TrackDef, -} from './decorators/register'; -import { defaultEmptyTemplate } from './secure-template'; import { getAssociatedVMIfPresent } from './vm'; import { isCircularModuleDependency, resolveCircularModuleDependency, } from '../shared/circular-module-dependencies'; -export interface ComponentDef extends DecoratorMeta { +export interface ComponentDef { name: string; + wire: PropertyDescriptorMap | undefined; + props: PropertyDescriptorMap; + propsConfig: Record; + methods: PropertyDescriptorMap; template: Template; ctor: ComponentConstructor; bridge: HTMLElementConstructor; connectedCallback?: () => void; disconnectedCallback?: () => void; renderedCallback?: () => void; - render: () => Template; errorCallback?: ErrorCallback; + render: () => Template; } const CtorToDefMap: WeakMap = new WeakMap(); @@ -116,19 +112,15 @@ function createComponentDef( const { name } = meta; let { template } = meta; - const decoratorsMeta = getDecoratorsRegisteredMeta(Ctor); - let props: PropsDef = {}; - let methods: MethodDef = {}; - let wire: WireHash | undefined; - let track: TrackDef = {}; - let fields: string[] | undefined; - if (!isUndefined(decoratorsMeta)) { - props = decoratorsMeta.props; - methods = decoratorsMeta.methods; - wire = decoratorsMeta.wire; - track = decoratorsMeta.track; - fields = decoratorsMeta.fields; - } + const decoratorsMeta = getDecoratorsMeta(Ctor); + const { + apiFields, + apiFieldsConfig, + apiMethods, + wiredFields, + wiredMethods, + observedFields, + } = decoratorsMeta; const proto = Ctor.prototype; let { @@ -139,45 +131,37 @@ function createComponentDef( render, } = proto; const superProto = getCtorProto(Ctor, subclassComponentName); - const superDef = - superProto !== BaseLightningElement - ? getComponentDef(superProto, subclassComponentName) - : null; + const superDef: ComponentDef = + (superProto as any) !== BaseLightningElement + ? getComponentInternalDef(superProto, subclassComponentName) + : lightingElementDef; const SuperBridge = isNull(superDef) ? BaseBridgeElement : superDef.bridge; - const bridge = HTMLBridgeElementFactory( - SuperBridge, - getOwnPropertyNames(props), - getOwnPropertyNames(methods) + const bridge = HTMLBridgeElementFactory(SuperBridge, keys(apiFields), keys(apiMethods)); + const props: PropertyDescriptorMap = assign(create(null), superDef.props, apiFields); + const propsConfig = assign(create(null), superDef.propsConfig, apiFieldsConfig); + const methods: PropertyDescriptorMap = assign(create(null), superDef.methods, apiMethods); + const wire: PropertyDescriptorMap = assign( + create(null), + superDef.wire, + wiredFields, + wiredMethods ); - if (!isNull(superDef)) { - props = assign(create(null), superDef.props, props); - methods = assign(create(null), superDef.methods, methods); - wire = superDef.wire || wire ? assign(create(null), superDef.wire, wire) : undefined; - track = assign(create(null), superDef.track, track); - connectedCallback = connectedCallback || superDef.connectedCallback; - disconnectedCallback = disconnectedCallback || superDef.disconnectedCallback; - renderedCallback = renderedCallback || superDef.renderedCallback; - errorCallback = errorCallback || superDef.errorCallback; - render = render || superDef.render; - template = template || superDef.template; - } - props = assign(create(null), HTML_PROPS, props); + connectedCallback = connectedCallback || superDef.connectedCallback; + disconnectedCallback = disconnectedCallback || superDef.disconnectedCallback; + renderedCallback = renderedCallback || superDef.renderedCallback; + errorCallback = errorCallback || superDef.errorCallback; + render = render || superDef.render; + template = template || superDef.template; - if (!isUndefined(fields)) { - defineProperties(proto, createObservedFieldsDescriptorMap(fields)); - } - - if (isUndefined(template)) { - // default template - template = defaultEmptyTemplate; - } + // installing observed fields into the prototype. + defineProperties(proto, observedFields); const def: ComponentDef = { ctor: Ctor, name, wire, - track, props, + propsConfig, methods, bridge, template, @@ -234,11 +218,7 @@ export function isComponentConstructor(ctor: unknown): ctor is ComponentConstruc return false; } -/** - * EXPERIMENTAL: This function allows for the collection of internal component metadata. This API is - * subject to change or being removed. - */ -export function getComponentDef(Ctor: unknown, name?: string): ComponentDef { +export function getComponentInternalDef(Ctor: unknown, name?: string): ComponentDef { let def = CtorToDefMap.get(Ctor); if (isUndefined(def)) { @@ -292,16 +272,60 @@ export function setElementProto(elm: Element, def: ComponentDef) { setPrototypeOf(elm, def.bridge.prototype); } -const HTML_PROPS = ArrayReduce.call( - getOwnPropertyNames(HTMLElementOriginalDescriptors), - (props, propName) => { - const attrName = getAttrNameFromPropName(propName); - (props as PropsDef)[propName] = { - config: 3, - type: 'any', - attr: attrName, +const lightingElementDef: ComponentDef = { + ctor: BaseLightningElement, + name: BaseLightningElement.name, + props: lightningBasedDescriptors, + propsConfig: EmptyObject, + methods: EmptyObject, + wire: EmptyObject, + bridge: BaseBridgeElement, + template: defaultEmptyTemplate, + render: BaseLightningElement.prototype.render, +}; + +interface PropDef { + config: number; + type: string; + attr: string; +} +type PublicMethod = (...args: any[]) => any; +interface PublicComponentDef { + name: string; + props: Record; + methods: Record; + ctor: ComponentConstructor; +} + +/** + * EXPERIMENTAL: This function allows for the collection of internal component metadata. This API is + * subject to change or being removed. + */ +export function getComponentDef(Ctor: any, subclassComponentName?: string): PublicComponentDef { + const def = getComponentInternalDef(Ctor, subclassComponentName); + // From the internal def object, we need to extract the info that is useful + // for some external services, e.g.: Locker Service, usually, all they care + // is about the shape of the constructor, the internals of it are not relevant + // because they don't have a way to mess with that. + const { ctor, name, props, propsConfig, methods } = def; + const publicProps: Record = {}; + for (const key in props) { + // avoid leaking the reference to the public props descriptors + publicProps[key] = { + config: propsConfig[key] || 0, // a property by default + type: 'any', // no type inference for public services + attr: getAttrNameFromPropName(key), }; - return props; - }, - create(null) -) as PropsDef; + } + const publicMethods: Record = {}; + for (const key in methods) { + // avoid leaking the reference to the public method descriptors + publicMethods[key] = methods[key].value as (...args: any[]) => any; + } + return { + ctor, + name, + props: publicProps, + methods: publicMethods, + }; +} diff --git a/packages/@lwc/engine/src/framework/hooks.ts b/packages/@lwc/engine/src/framework/hooks.ts index 3564755e38..2023195feb 100644 --- a/packages/@lwc/engine/src/framework/hooks.ts +++ b/packages/@lwc/engine/src/framework/hooks.ts @@ -26,7 +26,7 @@ import modStaticClassName from './modules/static-class-attr'; import modStaticStyle from './modules/static-style-attr'; import { updateDynamicChildren, updateStaticChildren } from '../3rdparty/snabbdom/snabbdom'; import { patchElementWithRestrictions, unlockDomMutation, lockDomMutation } from './restrictions'; -import { getComponentDef, setElementProto } from './def'; +import { getComponentInternalDef, setElementProto } from './def'; const noop = () => void 0; @@ -186,7 +186,7 @@ export function createViewModelHook(vnode: VCustomElement) { return; } const { mode, ctor, owner } = vnode; - const def = getComponentDef(ctor); + const def = getComponentInternalDef(ctor); setElementProto(elm, def); if (isTrue(useSyntheticShadow)) { const { shadowAttribute } = owner.context; diff --git a/packages/@lwc/engine/src/framework/main.ts b/packages/@lwc/engine/src/framework/main.ts index 179b69e01e..79be67a6e5 100644 --- a/packages/@lwc/engine/src/framework/main.ts +++ b/packages/@lwc/engine/src/framework/main.ts @@ -11,6 +11,7 @@ import '../polyfills/aria-properties/main'; // TODO [#1296]: Revisit these exports and figure out a better separation export { createElement } from './upgrade'; +export { createContextProvider } from './context-provider'; export { getComponentDef, isComponentConstructor, getComponentConstructor } from './def'; export { BaseLightningElement as LightningElement } from './base-lightning-element'; export { register } from './services'; @@ -22,9 +23,8 @@ export { isNodeFromTemplate } from './vm'; export { default as api } from './decorators/api'; export { default as track } from './decorators/track'; -export { default as readonly } from './decorators/readonly'; export { default as wire } from './decorators/wire'; -export { default as decorate } from './decorators/decorate'; +export { readonly } from './readonly'; export { deprecatedBuildCustomElementConstructor as buildCustomElementConstructor } from './wc'; export { setFeatureFlag, setFeatureFlagForTest } from '@lwc/features'; diff --git a/packages/@lwc/engine/src/framework/membrane.ts b/packages/@lwc/engine/src/framework/membrane.ts index dbdc1a244a..53ed51aa99 100644 --- a/packages/@lwc/engine/src/framework/membrane.ts +++ b/packages/@lwc/engine/src/framework/membrane.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import ObservableMembrane from 'observable-membrane'; -import { valueObserved, valueMutated } from '../libs/mutation-tracker'; +import { valueObserved, valueMutated } from './mutation-tracker'; function valueDistortion(value: any) { return value; diff --git a/packages/@lwc/engine/src/framework/mutation-tracker.ts b/packages/@lwc/engine/src/framework/mutation-tracker.ts new file mode 100644 index 0000000000..2cb6457927 --- /dev/null +++ b/packages/@lwc/engine/src/framework/mutation-tracker.ts @@ -0,0 +1,18 @@ +/* + * 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 { valueMutated, valueObserved } from '../libs/mutation-tracker'; +import { VM } from './vm'; + +export function componentValueMutated(vm: VM, key: PropertyKey) { + valueMutated(vm.component, key); +} + +export function componentValueObserved(vm: VM, key: PropertyKey) { + valueObserved(vm.component, key); +} + +export * from '../libs/mutation-tracker'; diff --git a/packages/@lwc/engine/src/framework/observed-fields.ts b/packages/@lwc/engine/src/framework/observed-fields.ts index 87d2ba8ee5..baac137ab6 100644 --- a/packages/@lwc/engine/src/framework/observed-fields.ts +++ b/packages/@lwc/engine/src/framework/observed-fields.ts @@ -4,37 +4,24 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { ArrayReduce, isFalse } from '@lwc/shared'; import { ComponentInterface } from './component'; import { getAssociatedVM } from './vm'; -import { valueMutated, valueObserved } from '../libs/mutation-tracker'; +import { componentValueMutated, componentValueObserved } from './mutation-tracker'; -export function createObservedFieldsDescriptorMap(fields: PropertyKey[]): PropertyDescriptorMap { - return ArrayReduce.call( - fields, - (acc, field) => { - (acc as PropertyDescriptorMap)[field] = createObservedFieldPropertyDescriptor(field); - - return acc; - }, - {} - ) as PropertyDescriptorMap; -} - -function createObservedFieldPropertyDescriptor(key: PropertyKey): PropertyDescriptor { +export function createObservedFieldPropertyDescriptor(key: string): PropertyDescriptor { return { get(this: ComponentInterface): any { const vm = getAssociatedVM(this); - valueObserved(this, key); - return vm.cmpTrack[key]; + componentValueObserved(vm, key); + return vm.cmpFields[key]; }, set(this: ComponentInterface, newValue: any) { const vm = getAssociatedVM(this); - if (newValue !== vm.cmpTrack[key]) { - vm.cmpTrack[key] = newValue; - if (isFalse(vm.isDirty)) { - valueMutated(this, key); - } + + if (newValue !== vm.cmpFields[key]) { + vm.cmpFields[key] = newValue; + + componentValueMutated(vm, key); } }, enumerable: true, diff --git a/packages/@lwc/engine/src/framework/decorators/readonly.ts b/packages/@lwc/engine/src/framework/readonly.ts similarity index 89% rename from packages/@lwc/engine/src/framework/decorators/readonly.ts rename to packages/@lwc/engine/src/framework/readonly.ts index 37b366ffe9..8cbe99d652 100644 --- a/packages/@lwc/engine/src/framework/decorators/readonly.ts +++ b/packages/@lwc/engine/src/framework/readonly.ts @@ -5,14 +5,14 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { assert } from '@lwc/shared'; -import { reactiveMembrane } from '../membrane'; +import { reactiveMembrane } from './membrane'; /** * EXPERIMENTAL: This function allows you to create a reactive readonly * membrane around any object value. This API is subject to change or * being removed. */ -export default function readonly(obj: any): any { +export function readonly(obj: any): any { if (process.env.NODE_ENV !== 'production') { // TODO [#1292]: Remove the readonly decorator if (arguments.length !== 1) { diff --git a/packages/@lwc/engine/src/framework/services.ts b/packages/@lwc/engine/src/framework/services.ts index 540b5aef8f..cc325bf568 100644 --- a/packages/@lwc/engine/src/framework/services.ts +++ b/packages/@lwc/engine/src/framework/services.ts @@ -18,20 +18,18 @@ type ServiceCallback = ( context: Context ) => void; interface ServiceDef { - wiring?: ServiceCallback; connected?: ServiceCallback; disconnected?: ServiceCallback; rendered?: ServiceCallback; } export const Services: { - wiring?: ServiceCallback[]; connected?: ServiceCallback[]; disconnected?: ServiceCallback[]; rendered?: ServiceCallback[]; } = create(null); -const hooks: Array = ['wiring', 'rendered', 'connected', 'disconnected']; +const hooks: Array = ['rendered', 'connected', 'disconnected']; /** * EXPERIMENTAL: This function allows for the registration of "services" diff --git a/packages/@lwc/engine/src/framework/upgrade.ts b/packages/@lwc/engine/src/framework/upgrade.ts index 549073149e..d64ff3d0d2 100644 --- a/packages/@lwc/engine/src/framework/upgrade.ts +++ b/packages/@lwc/engine/src/framework/upgrade.ts @@ -27,7 +27,7 @@ import { disconnectedRootElement, } from './vm'; import { ComponentConstructor } from './component'; -import { getComponentDef, setElementProto } from './def'; +import { getComponentInternalDef, setElementProto } from './def'; type NodeSlotCallback = (element: Node) => {}; @@ -112,7 +112,7 @@ export function createElement( return element; } - const def = getComponentDef(Ctor); + const def = getComponentInternalDef(Ctor); setElementProto(element, def); createVM(element, def.ctor, { diff --git a/packages/@lwc/engine/src/framework/utils.ts b/packages/@lwc/engine/src/framework/utils.ts index fb9811a6e7..4b6573c419 100644 --- a/packages/@lwc/engine/src/framework/utils.ts +++ b/packages/@lwc/engine/src/framework/utils.ts @@ -44,3 +44,13 @@ export function addCallbackToNextTick(callback: Callback) { } export const useSyntheticShadow = hasOwnProperty.call(Element.prototype, '$shadowToken$'); + +export function guid(): string { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +} diff --git a/packages/@lwc/engine/src/framework/vm.ts b/packages/@lwc/engine/src/framework/vm.ts index 63179a624c..17e619eed1 100644 --- a/packages/@lwc/engine/src/framework/vm.ts +++ b/packages/@lwc/engine/src/framework/vm.ts @@ -20,11 +20,11 @@ import { createHiddenField, getHiddenField, setHiddenField, + getOwnPropertyNames, } from '@lwc/shared'; -import { getComponentDef } from './def'; +import { getComponentInternalDef } from './def'; import { createComponent, - linkComponent, renderComponent, ComponentConstructor, markComponentAsDirty, @@ -48,9 +48,10 @@ import { } from './performance-timing'; import { updateDynamicChildren, updateStaticChildren } from '../3rdparty/snabbdom/snabbdom'; import { hasDynamicChildren } from './hooks'; -import { ReactiveObserver } from '../libs/mutation-tracker'; +import { ReactiveObserver } from './mutation-tracker'; import { LightningElement } from './base-lightning-element'; import { getErrorComponentStack } from '../shared/format'; +import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from './wiring'; export interface SlotSet { [key: string]: VNodes; @@ -81,9 +82,9 @@ export interface UninitializedVM { /** Adopted Children List */ aChildren: VNodes; velements: VCustomElement[]; - cmpProps: any; + cmpProps: Record; cmpSlots: SlotSet; - cmpTrack: any; + cmpFields: Record; callHook: ( cmp: ComponentInterface | undefined, fn: (...args: any[]) => any, @@ -214,7 +215,7 @@ export function createVM( `VM creation requires a DOM element instead of ${elm}.` ); } - const def = getComponentDef(Ctor); + const def = getComponentInternalDef(Ctor); const { isRoot, mode, owner } = options; idx += 1; const uninitializedVm: UninitializedVM = { @@ -232,7 +233,7 @@ export function createVM( data: EmptyObject, context: create(null), cmpProps: create(null), - cmpTrack: create(null), + cmpFields: create(null), cmpSlots: useSyntheticShadow ? create(null) : undefined, callHook, setHook, @@ -259,7 +260,10 @@ export function createVM( // link component to the wire service const initializedVm = uninitializedVm as VM; - linkComponent(initializedVm); + // initializing the wire decorator per instance only when really needed + if (hasWireAdapters(initializedVm)) { + installWireAdapters(initializedVm); + } return initializedVm; } @@ -404,6 +408,9 @@ export function runConnectedCallback(vm: VM) { if (connected) { invokeServiceHook(vm, connected); } + if (hasWireAdapters(vm)) { + connectWireAdapters(vm); + } const { connectedCallback } = vm.def; if (!isUndefined(connectedCallback)) { if (process.env.NODE_ENV !== 'production') { @@ -418,6 +425,10 @@ export function runConnectedCallback(vm: VM) { } } +function hasWireAdapters(vm: VM): boolean { + return getOwnPropertyNames(vm.def.wire).length > 0; +} + function runDisconnectedCallback(vm: VM) { if (process.env.NODE_ENV !== 'production') { assert.isTrue(vm.state !== VMState.disconnected, `${vm} must be inserted.`); @@ -435,6 +446,9 @@ function runDisconnectedCallback(vm: VM) { if (disconnected) { invokeServiceHook(vm, disconnected); } + if (hasWireAdapters(vm)) { + disconnectWireAdapters(vm); + } const { disconnectedCallback } = vm.def; if (!isUndefined(disconnectedCallback)) { if (process.env.NODE_ENV !== 'production') { diff --git a/packages/@lwc/engine/src/framework/wc.ts b/packages/@lwc/engine/src/framework/wc.ts index 0dc7e4ad3b..1765892acf 100644 --- a/packages/@lwc/engine/src/framework/wc.ts +++ b/packages/@lwc/engine/src/framework/wc.ts @@ -4,11 +4,11 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { ArrayMap, getOwnPropertyNames, isUndefined } from '@lwc/shared'; +import { isUndefined, keys } from '@lwc/shared'; import { ComponentConstructor } from './component'; import { createVM, connectRootElement, disconnectedRootElement } from './vm'; -import { getComponentDef } from './def'; -import { getPropNameFromAttrName, isAttributeLocked } from './attributes'; +import { getAttrNameFromPropName, isAttributeLocked } from './attributes'; +import { getComponentInternalDef } from './def'; import { HTMLElementConstructor } from './base-bridge-element'; /** @@ -41,8 +41,14 @@ export function deprecatedBuildCustomElementConstructor( } export function buildCustomElementConstructor(Ctor: ComponentConstructor): HTMLElementConstructor { - const { props, bridge: BaseElement } = getComponentDef(Ctor); + const { props, bridge: BaseElement } = getComponentInternalDef(Ctor); + // generating the hash table for attributes to avoid duplicate fields + // and facilitate validation and false positives in case of inheritance. + const attributeToPropMap: Record = {}; + for (const propName in props) { + attributeToPropMap[getAttrNameFromPropName(propName)] = propName; + } return class extends BaseElement { constructor() { super(); @@ -63,8 +69,8 @@ export function buildCustomElementConstructor(Ctor: ComponentConstructor): HTMLE // ignoring similar values for better perf return; } - const propName = getPropNameFromAttrName(attrName); - if (isUndefined(props[propName])) { + const propName = attributeToPropMap[attrName]; + if (isUndefined(propName)) { // ignoring unknown attributes return; } @@ -82,9 +88,6 @@ export function buildCustomElementConstructor(Ctor: ComponentConstructor): HTMLE } // collecting all attribute names from all public props to apply // the reflection from attributes to props via attributeChangedCallback. - static observedAttributes = ArrayMap.call( - getOwnPropertyNames(props), - propName => props[propName].attr - ); + static observedAttributes = keys(attributeToPropMap); }; } diff --git a/packages/@lwc/engine/src/framework/wiring.ts b/packages/@lwc/engine/src/framework/wiring.ts new file mode 100644 index 0000000000..6deb03c536 --- /dev/null +++ b/packages/@lwc/engine/src/framework/wiring.ts @@ -0,0 +1,333 @@ +/* + * 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 { assert, isUndefined, ArrayPush, getOwnPropertyNames, defineProperty } from '@lwc/shared'; +import { ComponentInterface } from './component'; +import { componentValueMutated, ReactiveObserver } from './mutation-tracker'; +import { VM, runWithBoundaryProtection } from './vm'; +import { invokeComponentCallback } from './invoker'; +import { dispatchEvent } from '../env/dom'; + +const DeprecatedWiredElementHost = '$$DeprecatedWiredElementHostKey$$'; + +const WireMetaMap: Map = new Map(); +function noop(): void {} + +function createFieldDataCallback(vm: VM, name: string) { + const { cmpFields } = vm; + return (value: any) => { + if (value !== vm.cmpFields[name]) { + // storing the value in the underlying storage + cmpFields[name] = value; + + componentValueMutated(vm, name); + } + }; +} + +function createMethodDataCallback(vm: VM, method: (data: any) => any) { + return (value: any) => { + // dispatching new value into the wired method + invokeComponentCallback(vm, method, [value]); + }; +} + +function createConfigWatcher( + vm: VM, + wireDef: WireDef, + callbackWhenConfigIsReady: (newConfig: ConfigValue) => void +) { + const { component } = vm; + const { configCallback } = wireDef; + let hasPendingConfig: boolean = false; + // creating the reactive observer for reactive params when needed + const ro = new ReactiveObserver(() => { + if (hasPendingConfig === false) { + hasPendingConfig = true; + // collect new config in the micro-task + Promise.resolve().then(() => { + hasPendingConfig = false; + // resetting current reactive params + ro.reset(); + // dispatching a new config due to a change in the configuration + callback(); + }); + } + }); + const callback = () => { + let config: ConfigValue; + ro.observe(() => (config = configCallback(component))); + // eslint-disable-next-line lwc-internal/no-invalid-todo + // TODO: dev-mode validation of config based on the adapter.configSchema + // @ts-ignore it is assigned in the observe() callback + callbackWhenConfigIsReady(config); + }; + return callback; +} + +function createContextWatcher( + vm: VM, + wireDef: WireDef, + callbackWhenContextIsReady: (newContext: ContextValue) => void +) { + const { adapter } = wireDef; + const adapterContextToken = getAdapterToken(adapter); + if (isUndefined(adapterContextToken)) { + return; // no provider found, nothing to be done + } + const { + elm, + context: { wiredConnecting, wiredDisconnecting }, + } = vm; + // waiting for the component to be connected to formally request the context via the token + ArrayPush.call(wiredConnecting, () => { + // This event is responsible for connecting the host element with another + // element in the composed path that is providing contextual data. The provider + // must be listening for a special dom event with the name corresponding to the value of + // `adapterContextToken`, which will remain secret and internal to this file only to + // guarantee that the linkage can be forged. + const internalDomEvent = new CustomEvent(adapterContextToken, { + bubbles: true, + composed: true, + detail(newContext: ContextValue, disconnectCallback: () => void) { + // adds this callback into the disconnect bucket so it gets disconnected from parent + // the the element hosting the wire is disconnected + ArrayPush.call(wiredDisconnecting, disconnectCallback); + // eslint-disable-next-line lwc-internal/no-invalid-todo + // TODO: dev-mode validation of config based on the adapter.contextSchema + callbackWhenContextIsReady(newContext); + }, + }); + dispatchEvent.call(elm, internalDomEvent); + }); +} + +function createConnector(vm: VM, name: string, wireDef: WireDef): WireAdapter { + const { method, adapter, configCallback, hasParams } = wireDef; + const { component } = vm; + const dataCallback = isUndefined(method) + ? createFieldDataCallback(vm, name) + : createMethodDataCallback(vm, method); + let context: ContextValue | undefined; + let connector: WireAdapter; + + // Workaround to pass the component element associated to this wire adapter instance. + defineProperty(dataCallback, DeprecatedWiredElementHost, { + value: vm.elm, + }); + + runWithBoundaryProtection( + vm, + vm, + noop, + () => { + // job + connector = new adapter(dataCallback); + }, + noop + ); + const updateConnectorConfig = (config: ConfigValue) => { + // every time the config is recomputed due to tracking, + // this callback will be invoked with the new computed config + runWithBoundaryProtection( + vm, + vm, + noop, + () => { + // job + connector.update(config, context); + }, + noop + ); + }; + + // 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) => { + // every time the context is pushed into this component, + // this callback will be invoked with the new computed context + if (context !== newContext) { + context = newContext; + // Note: when new context arrives, the config will be recomputed and pushed along side the new + // context, this is to preserve the identity characteristics, config should not have identity + // (ever), while context can have identity + computeConfigAndUpdate(); + } + }); + } + // @ts-ignore the boundary protection executes sync, connector is always defined + return connector; +} + +type DataCallback = (value: any) => void; +type ConfigValue = Record; + +interface WireAdapter { + update(config: ConfigValue, context?: ContextValue): void; + connect(): void; + disconnect(): void; +} + +type WireAdapterSchemaValue = 'optional' | 'required'; + +interface WireDef { + method?: (data: any) => void; + adapter: WireAdapterConstructor; + hasParams: boolean; + configCallback: ConfigCallback; +} + +interface WireMethodDef extends WireDef { + method: (data: any) => void; +} + +interface WireFieldDef extends WireDef { + method?: undefined; +} + +const AdapterToTokenMap: Map = new Map(); + +export function getAdapterToken(adapter: WireAdapterConstructor): string | undefined { + return AdapterToTokenMap.get(adapter); +} + +export function setAdapterToken(adapter: WireAdapterConstructor, token: string) { + AdapterToTokenMap.set(adapter, token); +} + +export type ContextValue = Record; +export type ConfigCallback = (component: ComponentInterface) => ConfigValue; +export interface WireAdapterConstructor { + new (callback: DataCallback): WireAdapter; + configSchema?: Record; + contextSchema?: Record; +} + +export function storeWiredMethodMeta( + descriptor: PropertyDescriptor, + adapter: WireAdapterConstructor, + configCallback: ConfigCallback, + hasParams: boolean +) { + // support for callable adapters + if ((adapter as any).adapter) { + adapter = (adapter as any).adapter; + } + const method = descriptor.value; + const def: WireMethodDef = { + adapter, + method, + configCallback, + hasParams, + }; + WireMetaMap.set(descriptor, def); +} + +export function storeWiredFieldMeta( + descriptor: PropertyDescriptor, + adapter: WireAdapterConstructor, + configCallback: ConfigCallback, + hasParams: boolean +) { + // support for callable adapters + if ((adapter as any).adapter) { + adapter = (adapter as any).adapter; + } + const def: WireFieldDef = { + adapter, + configCallback, + hasParams, + }; + WireMetaMap.set(descriptor, def); +} + +export function installWireAdapters(vm: VM) { + const { + def: { wire }, + } = vm; + if (getOwnPropertyNames(wire).length === 0) { + if (process.env.NODE_ENV !== 'production') { + assert.fail( + `Internal Error: wire adapters should only be installed in instances with at least one wire declaration.` + ); + } + } else { + const connect = (vm.context.wiredConnecting = []); + const disconnect = (vm.context.wiredDisconnecting = []); + for (const fieldNameOrMethod in wire) { + const descriptor = wire[fieldNameOrMethod]; + const wireDef = WireMetaMap.get(descriptor); + if (process.env.NODE_ENV !== 'production') { + assert.invariant(wireDef, `Internal Error: invalid wire definition found.`); + } + if (!isUndefined(wireDef)) { + const adapterInstance = createConnector(vm, fieldNameOrMethod, wireDef); + ArrayPush.call(connect, () => adapterInstance.connect()); + ArrayPush.call(disconnect, () => adapterInstance.disconnect()); + } + } + } +} + +export function connectWireAdapters(vm: VM) { + const { + context: { wiredConnecting }, + } = vm; + if (isUndefined(wiredConnecting)) { + if (process.env.NODE_ENV !== 'production') { + assert.fail( + `Internal Error: wire adapters must be installed in instances with at least one wire declaration.` + ); + } + } + for (let i = 0, len = wiredConnecting.length; i < len; i += 1) { + wiredConnecting[i](); + } +} + +export function disconnectWireAdapters(vm: VM) { + const { + context: { wiredDisconnecting }, + } = vm; + if (isUndefined(wiredDisconnecting)) { + if (process.env.NODE_ENV !== 'production') { + assert.fail( + `Internal Error: wire adapters must be installed in instances with at least one wire declaration.` + ); + } + } + runWithBoundaryProtection( + vm, + vm, + noop, + () => { + // job + for (let i = 0, len = wiredDisconnecting.length; i < len; i += 1) { + wiredDisconnecting[i](); + } + }, + noop + ); +} diff --git a/packages/@lwc/shared/src/language.ts b/packages/@lwc/shared/src/language.ts index 76d480fbb5..5483a5d2e4 100644 --- a/packages/@lwc/shared/src/language.ts +++ b/packages/@lwc/shared/src/language.ts @@ -24,7 +24,6 @@ const { isArray } = Array; const { filter: ArrayFilter, find: ArrayFind, - forEach, indexOf: ArrayIndexOf, join: ArrayJoin, map: ArrayMap, @@ -34,6 +33,7 @@ const { slice: ArraySlice, splice: ArraySplice, unshift: ArrayUnshift, + forEach, } = Array.prototype; const { diff --git a/packages/@lwc/wire-service/src/__tests__/index.spec.ts b/packages/@lwc/wire-service/src/__tests__/index.spec.ts index 65486731e3..3e13607df2 100644 --- a/packages/@lwc/wire-service/src/__tests__/index.spec.ts +++ b/packages/@lwc/wire-service/src/__tests__/index.spec.ts @@ -4,288 +4,256 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { registerWireService, register } from '../index'; - -import { Element, ElementDef } from '../engine'; - -import { Context, ConfigContext } from '../wiring'; -import { CONTEXT_ID, CONTEXT_CONNECTED, CONTEXT_DISCONNECTED, CONTEXT_UPDATED } from '../constants'; - -describe('wire service', () => { - describe('registers the service with engine', () => { - it('uses wiring hook', () => { - const mockEngineRegister = jest.fn(); - registerWireService(mockEngineRegister); - expect(mockEngineRegister).toHaveBeenCalledWith( - expect.objectContaining({ - wiring: expect.any(Function), - }) - ); - }); - it('uses connected hook', () => { - const mockEngineRegister = jest.fn(); - registerWireService(mockEngineRegister); - expect(mockEngineRegister).toHaveBeenCalledWith( - expect.objectContaining({ - connected: expect.any(Function), - }) - ); - }); - it('uses disconnected hook', () => { - const mockEngineRegister = jest.fn(); - registerWireService(mockEngineRegister); - expect(mockEngineRegister).toHaveBeenCalledWith( - expect.objectContaining({ - disconnected: expect.any(Function), - }) - ); +import { register, WireEventTarget, ValueChangedEvent } from '../index'; + +describe('WireEventTarget from register', () => { + describe('connected', () => { + it('should invoke connected listeners', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + const adapter = new adapterId.adapter(() => {}); + + const listener1 = jest.fn(); + const listener2 = jest.fn(); + wireEventTarget!.addEventListener('connect', listener1); + wireEventTarget!.addEventListener('connect', listener2); + + adapter.connect(); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); }); }); - describe('wiring process', () => { - it('invokes adapter factory once per wire', () => { - let wireService; - registerWireService(svc => { - wireService = svc; - }); - const adapterId = () => { - /**/ - }; - const adapterFactory = jest.fn(); + + describe('disconnected', () => { + it('should invoke disconnected listeners', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const dataCallback = jest.fn(); + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + register(adapterId, adapterFactory); - const mockDef: ElementDef = { - wire: { - targetFunction: { - adapter: adapterId, - method: 1, - }, - targetProperty: { - adapter: adapterId, - }, - }, - }; + const adapter = new adapterId.adapter(dataCallback); - wireService.wiring({} as Element, {}, mockDef, {} as Context); - expect(adapterFactory).toHaveBeenCalledTimes(2); - }); - it('throws when adapter id is not truthy', () => { - let wireService; - registerWireService(svc => { - wireService = svc; - }); - const mockDef: ElementDef = { - wire: { - target: { - adapter: undefined, - method: 1, - }, - }, - }; - expect(() => - wireService.wiring({} as Element, {}, mockDef, {} as Context) - ).toThrowError('@wire on "target": adapter id must be truthy'); + const listener1 = jest.fn(); + const listener2 = jest.fn(); + wireEventTarget!.addEventListener('disconnect', listener1); + wireEventTarget!.addEventListener('disconnect', listener2); + + adapter.disconnect(); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); }); - it('throws when adapter factory is not found', () => { - let wireService; - registerWireService(svc => { - wireService = svc; - }); - const mockDef: ElementDef = { - wire: { - target: { - adapter: () => { - /**/ - }, - method: 1, - }, - }, - }; - expect(() => - wireService.wiring({} as Element, {}, mockDef, {} as Context) - ).toThrowError('@wire on "target": unknown adapter id: '); + }); + + describe('config', () => { + it('should invoke config listeners', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const dataCallback = jest.fn(); + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + const adapter = new adapterId.adapter(dataCallback); + + const listener1 = jest.fn(); + const listener2 = jest.fn(); + wireEventTarget!.addEventListener('config', listener1); + wireEventTarget!.addEventListener('config', listener2); + + const config = {}; + adapter.update(config); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener1.mock.calls[0][0]).toBe(config); + + expect(listener2).toHaveBeenCalledTimes(1); + expect(listener2.mock.calls[0][0]).toBe(config); }); - it('throws when dot-notation reactive parameter refers to non-@wire target', () => { - let wireService; - registerWireService(svc => { - wireService = svc; - }); - const adapterId = () => { - /**/ - }; - register(adapterId, adapterId); - const mockDef: ElementDef = { - wire: { - target: { - adapter: adapterId, - params: { p1: 'x.y' }, - }, - }, - }; - expect(() => - wireService.wiring({} as Element, {}, mockDef, {} as Context) - ).toThrowError( - '@wire on "target": dot-notation reactive parameter "x.y" must refer to a @wire property' - ); + + it('should immediately fires CONFIG when there is a config ready in the adapter instance', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + const adapter = new adapterId.adapter(jest.fn()); + const expectedConfig = {}; + + adapter.update(expectedConfig); + + const listener = jest.fn(); + wireEventTarget!.addEventListener('config', listener); + expect(listener).toHaveBeenCalledTimes(1); + expect(listener.mock.calls[0][0]).toBe(expectedConfig); }); - it('throws when dot-notation reactive parameter refers to @wired method', () => { - let wireService; - registerWireService(svc => { - wireService = svc; - }); - const adapterId = () => { - /**/ - }; - register(adapterId, adapterId); - const mockDef: ElementDef = { - wire: { - x: { - adapter: adapterId, - method: 1, - }, - target: { - adapter: adapterId, - params: { p1: 'x.y' }, - }, - }, - }; - expect(() => - wireService.wiring({} as Element, {}, mockDef, {} as Context) - ).toThrowError( - '@wire on "target": dot-notation reactive parameter "x.y" must refer to a @wire property' - ); + + it('should enqueue listener to be called when config is ready', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + const adapter = new adapterId.adapter(jest.fn()); + const listener = jest.fn(); + wireEventTarget!.addEventListener('config', listener); + + expect(listener).not.toBeCalled(); + + const expectedConfig = {}; + adapter.update(expectedConfig); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener.mock.calls[0][0]).toBe(expectedConfig); }); - it('throws when reactive parameter refers to own wire target', () => { - let wireService; - registerWireService(svc => { - wireService = svc; - }); - const adapterId = () => { - /**/ - }; - register(adapterId, adapterId); - const mockDef: ElementDef = { - wire: { - target: { - adapter: adapterId, - params: { p1: 'target' }, - }, - }, - }; - expect(() => - wireService.wiring({} as Element, {}, mockDef, {} as Context) - ).toThrowError('@wire on "target": reactive parameter "target" must not refer to self'); + }); + + describe('dispatchEvent', () => { + it('should invoke data callback when dispatchEvent', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const dataCallback = jest.fn(); + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + new adapterId.adapter(dataCallback); + + const expected = 'changed value'; + wireEventTarget!.dispatchEvent(new ValueChangedEvent(expected)); + + expect(dataCallback).toHaveBeenCalledTimes(1); + expect(dataCallback.mock.calls[0][0]).toBe(expected); }); - it('throws when reactive parameter is empty', () => { - let wireService; - registerWireService(svc => { - wireService = svc; - }); - const adapterId = () => { - /**/ + + it('should dispatchEvent in wiredComponent when dispatching event with type wirecontextevent', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const dataCallback = jest.fn(); + const wiredElementMock = { + dispatchEvent: jest.fn(), }; - register(adapterId, adapterId); - const mockDef: ElementDef = { - wire: { - target: { - adapter: adapterId, - params: { p1: '' }, - }, + dataCallback.$$DeprecatedWiredElementHostKey$$ = wiredElementMock; + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + new adapterId.adapter(dataCallback); + + const wireContextEventInLowercase = new CustomEvent('wirecontextevent', { + detail: { + foo: 'bar', }, - }; - expect(() => - wireService.wiring({} as Element, {}, mockDef, {} as Context) - ).toThrowError('@wire on "target": reactive parameters must not be empty'); - }); - it('throws when reactive parameter contains empty segment', () => { - let wireService; - registerWireService(svc => { - wireService = svc; }); - const adapterId = () => { - /**/ - }; - register(adapterId, adapterId); - const mockDef: ElementDef = { - wire: { - target: { - adapter: adapterId, - params: { p1: 'a..b' }, - }, - }, - }; - expect(() => - wireService.wiring({} as Element, {}, mockDef, {} as Context) - ).toThrowError('@wire on "target": reactive parameters must not be empty'); + + wireEventTarget!.dispatchEvent(wireContextEventInLowercase); + + expect(wiredElementMock.dispatchEvent).toHaveBeenCalledTimes(1); + expect(wiredElementMock.dispatchEvent.mock.calls[0][0]).toBe( + wireContextEventInLowercase + ); }); - it('throws when reactive parameter ends with empty segment', () => { - let wireService; - registerWireService(svc => { - wireService = svc; - }); - const adapterId = () => { - /**/ - }; - register(adapterId, adapterId); - const mockDef: ElementDef = { - wire: { - target: { - adapter: adapterId, - params: { p1: 'a.b.' }, - }, - }, - }; - expect(() => - wireService.wiring({} as Element, {}, mockDef, {} as Context) - ).toThrowError('@wire on "target": reactive parameters must not be empty'); + + it('should throw on non-ValueChangedEvent', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const dataCallback = jest.fn(); + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + new adapterId.adapter(dataCallback); + + expect(() => { + const testEvent = 'test' as any; + wireEventTarget.dispatchEvent(testEvent); + }).toThrowError('Invalid event type undefined.'); + + expect(() => { + const testEvent = new CustomEvent('test') as any; + wireEventTarget.dispatchEvent(testEvent as ValueChangedEvent); + }).toThrowError('Invalid event type test.'); }); }); - describe('connected handling', () => { - const def: ElementDef = { - wire: { - target: { adapter: true }, - }, - }; - it('invokes connected listeners', () => { - let wireService; - registerWireService(svc => { - wireService = svc; - }); - const listener = jest.fn(); - const context: Context = { - [CONTEXT_ID]: { - [CONTEXT_CONNECTED]: [listener], - [CONTEXT_DISCONNECTED]: [], - [CONTEXT_UPDATED]: {} as ConfigContext, - }, - }; - wireService.connected({} as Element, {}, def, context); - expect(listener).toHaveBeenCalledTimes(1); + describe('addEventListener', () => { + it('should throw when adding unknown event listener type', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const dataCallback = jest.fn(); + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + new adapterId.adapter(dataCallback); + + expect(() => { + wireEventTarget!.addEventListener('invalidEventType', () => {}); + }).toThrow('Invalid event type invalidEventType.'); }); }); - describe('disconnected handling', () => { - const def: ElementDef = { - wire: { - target: { adapter: true }, - }, - }; - it('invokes connected listeners', () => { - let wireService; - registerWireService(svc => { - wireService = svc; + + describe('removeEventListener', () => { + ['connect', 'disconnect', 'config'].forEach(eventType => { + it(`should remove listener from the queue for ${eventType} event`, () => { + const eventToAdapterMethod = { + connect: 'connect', + disconnect: 'disconnect', + config: 'update', + }; + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + const adapter = new adapterId.adapter(() => {}); + + const listener = jest.fn(); + wireEventTarget.addEventListener(eventType, listener); + adapter[eventToAdapterMethod[eventType]](); + + expect(listener).toHaveBeenCalledTimes(1); + + wireEventTarget.removeEventListener(eventType, listener); + adapter[eventToAdapterMethod[eventType]](); + expect(listener).toHaveBeenCalledTimes(1); }); - const listener = jest.fn(); - const context: Context = { - [CONTEXT_ID]: { - [CONTEXT_CONNECTED]: [], - [CONTEXT_DISCONNECTED]: [listener], - [CONTEXT_UPDATED]: {} as ConfigContext, - }, - }; + }); - wireService.disconnected({} as Element, {}, def, context); - expect(listener).toHaveBeenCalledTimes(1); + it('should throw when event type is not supported', () => { + const adapterId = {}; + let wireEventTarget: WireEventTarget; + const adapterFactory = (wireEvtTarget: WireEventTarget) => + (wireEventTarget = wireEvtTarget); + + register(adapterId, adapterFactory); + new adapterId.adapter(() => {}); + + expect(() => { + const testEvent = 'test' as any; + wireEventTarget.removeEventListener(testEvent, jest.fn()); + }).toThrowError('Invalid event type test.'); }); }); + + it('should invoke adapter factory once per wire', () => { + const adapterId = {}; + const dataCallback = jest.fn(); + const adapterFactory = jest.fn(); + + register(adapterId, adapterFactory); + new adapterId.adapter(dataCallback); + new adapterId.adapter(dataCallback); + + expect(adapterFactory).toHaveBeenCalledTimes(2); + }); }); describe('register', () => { @@ -295,16 +263,46 @@ describe('register', () => { function adapterFactory() {} register(adapterId, adapterFactory); }); - it('accepts symbol as adapter id', () => { - const adapterId = Symbol(); - function adapterFactory() {} - register(adapterId, adapterFactory); - }); + it('throws when adapter id is not truthy', () => { function adapterFactory() {} - expect(() => register(undefined, adapterFactory)).toThrowError('adapter id must be truthy'); + expect(() => register(undefined, adapterFactory)).toThrowError( + 'adapter id must be extensible' + ); }); + it('throws when adapter factory is not a function', () => { expect(() => register({}, {} as any)).toThrowError('adapter factory must be a callable'); }); + + it('should throw when adapter id is already associated to an adapter factory', () => { + const adapterId = { adapter: {} }; + expect(() => register(adapterId, () => {})).toThrowError( + 'adapter id is already associated to an adapter factory' + ); + }); + + it('should freeze adapter property', () => { + const adapterId = {}; + function adapterFactory() {} + register(adapterId, adapterFactory); + + expect(() => { + adapterId.adapter = 'modified'; + }).toThrow("Cannot assign to read only property 'adapter'"); + }); + + it('should freeze adapter class', () => { + const adapterId = {}; + function adapterFactory() {} + register(adapterId, adapterFactory); + + expect(() => { + adapterId.adapter.prototype.update = 'modified'; + }).toThrow('Cannot add property update, object is not extensible'); + + expect(() => { + adapterId.adapter.update = 'modified'; + }).toThrow('Cannot add property update, object is not extensible'); + }); }); diff --git a/packages/@lwc/wire-service/src/__tests__/property-trap.spec.ts b/packages/@lwc/wire-service/src/__tests__/property-trap.spec.ts deleted file mode 100644 index c50a6894be..0000000000 --- a/packages/@lwc/wire-service/src/__tests__/property-trap.spec.ts +++ /dev/null @@ -1,304 +0,0 @@ -/* - * 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 { installTrap, findDescriptor, getReactiveParameterValue, updated } from '../property-trap'; -import { ConfigContext, ReactiveParameter } from '../wiring'; - -describe('findDescriptor', () => { - it('detects circular prototype chains', () => { - function A() { - /**/ - } - function B() { - /**/ - } - B.prototype = Object.create(A.prototype); - A.prototype = Object.create(B.prototype); - const actual = findDescriptor(B, 'target'); - expect(actual).toBe(null); - }); - - it('finds descriptor on super with prototype setting', () => { - function A() { - /**/ - } - A.prototype.target = 'target'; - function B() { - /**/ - } - B.prototype = Object.create(A.prototype); - expect(findDescriptor(B, 'target')).toBe(null); - expect(findDescriptor(new B(), 'target')).not.toBe(null); - }); - - it('finds descriptor on super with classes', () => { - class A { - target: any; - constructor() { - this.target = 'target'; - } - } - class B extends A {} - expect(findDescriptor(B, 'target')).toBe(null); - expect(findDescriptor(new B(), 'target')).not.toBe(null); - }); -}); - -describe('installTrap', () => { - const context: ConfigContext = { - listeners: { - prop1: [], - }, - values: { - prop1: '', - }, - }; - const reactiveParametersGroupByHead: Array = [ - { - reference: 'prop1', - head: 'prop1', - }, - ]; - - it('defaults to original value when setter installed', () => { - class Target { - prop1 = 'initial'; - } - const cmp = new Target(); - installTrap(cmp, 'prop1', reactiveParametersGroupByHead, context); - expect(cmp.prop1).toBe('initial'); - }); - - it('updates original property when installed setter invoked', () => { - const expected = 'expected'; - class Target { - prop1; - } - const cmp = new Target(); - installTrap(cmp, 'prop1', reactiveParametersGroupByHead, context); - cmp.prop1 = expected; - expect(cmp.prop1).toBe(expected); - }); - - it('installs setter on cmp for property', () => { - class Target { - set prop1(value) { - /**/ - } - } - const original = Object.getOwnPropertyDescriptor(Target.prototype, 'prop1'); - const cmp = new Target(); - installTrap(cmp, 'prop1', reactiveParametersGroupByHead, context); - const descriptor = Object.getOwnPropertyDescriptor(cmp, 'prop1'); - expect(descriptor!.set).not.toBe(original!.set); - }); - - it('invokes original setter when installed setter invoked', () => { - const setter = jest.fn(); - const expected = 'expected'; - class Target { - set prop1(value) { - setter(value); - } - get prop1() { - return ''; - } - } - const cmp = new Target(); - installTrap(cmp, 'prop1', reactiveParametersGroupByHead, context); - cmp.prop1 = expected; - expect(setter).toHaveBeenCalledTimes(1); - expect(setter).toHaveBeenCalledWith(expected); - }); - - it('installs setter on cmp only for reactiveParameter.root', () => { - const dotNotationReactiveParameters: Array = [ - { - reference: 'prop1.x.y', - head: 'prop1', - }, - ]; - class Target { - set prop1(value) { - /**/ - } - } - const original = Object.getOwnPropertyDescriptor(Target.prototype, 'prop1'); - const cmp = new Target(); - installTrap(cmp, 'prop1', dotNotationReactiveParameters, context); - const descriptor = Object.getOwnPropertyDescriptor(cmp, 'prop1'); - expect(descriptor!.set).not.toBe(original!.set); - expect(Object.getOwnPropertyDescriptor(Target.prototype, 'prop1.x.y')).toBeUndefined(); - }); -}); - -describe('invokeConfigListeners', () => { - const reactiveParametersGroupByHead: Array = [ - { - reference: 'prop1', - head: 'prop1', - }, - ]; - - it('invokes listener with reactive parameter default value', () => { - const expected = 'expected'; - const listener = jest.fn(); - const context: ConfigContext = { - listeners: { - prop1: [{ listener, reactives: { param1: 'prop1' } }], - }, - values: { - // initial state is empty - }, - }; - class Target { - prop1 = expected; - } - const cmp = new Target(); - installTrap(cmp, 'prop1', reactiveParametersGroupByHead, context); - updated(cmp, reactiveParametersGroupByHead, context); - return Promise.resolve().then(() => { - expect(listener).toHaveBeenCalledTimes(1); - expect(listener.mock.calls[0][0]).toEqual({ param1: expected }); - }); - }); - - it('invokes listener with new value once', () => { - const expected = 'expected'; - const listener = jest.fn(); - const context: ConfigContext = { - listeners: { - prop1: [{ listener, reactives: { param1: 'prop1' } }], - }, - values: {}, - }; - class Target { - prop1; - } - const cmp = new Target(); - installTrap(cmp, 'prop1', reactiveParametersGroupByHead, context); - updated(cmp, reactiveParametersGroupByHead, context); - cmp.prop1 = expected; - return Promise.resolve().then(() => { - expect(listener).toHaveBeenCalledTimes(1); - expect(listener.mock.calls[0][0]).toEqual({ param1: expected }); - }); - }); - - it('does not invoke listener if param value is unchanged', () => { - const expected = 'expected'; - const listener = jest.fn(); - const context: ConfigContext = { - listeners: { - prop1: [{ listener, reactives: { param1: 'prop1' } }], - }, - values: { - prop1: 'expected', - }, - }; - class Target { - prop1; - } - const cmp = new Target(); - installTrap(cmp, 'prop1', reactiveParametersGroupByHead, context); - cmp.prop1 = expected; - return Promise.resolve().then(() => { - expect(listener).toHaveBeenCalledTimes(0); - }); - }); - - it('invokes listener with getter value', () => { - const expected = 'expected'; - const listener = jest.fn(); - const context: ConfigContext = { - listeners: { - prop1: [{ listener, reactives: { param1: 'prop1' } }], - }, - values: { - prop1: '', - }, - }; - class Target { - set prop1(value) { - /**/ - } - get prop1() { - return expected; - } - } - const cmp = new Target(); - installTrap(cmp, 'prop1', reactiveParametersGroupByHead, context); - cmp.prop1 = 'unexpected'; - return Promise.resolve().then(() => { - expect(listener).toHaveBeenCalledTimes(1); - expect(listener.mock.calls[0][0]).toEqual({ param1: expected }); - }); - }); -}); - -describe('getReactiveParameterValue', () => { - it('returns leaf in object graph', () => { - const expected = 'expected'; - const reactiveParameter: ReactiveParameter = { - reference: 'a.b.c.d', - head: 'a', - tail: ['b', 'c', 'd'], - }; - class Target { - a = { b: { c: { d: expected } } }; - } - expect(getReactiveParameterValue(new Target(), reactiveParameter)).toBe(expected); - }); - - it('returns tree in object graph', () => { - const expected = { e: { f: 'expected' } }; - const reactiveParameter: ReactiveParameter = { - reference: 'a.b.c.d', - head: 'a', - tail: ['b', 'c', 'd'], - }; - class Target { - a = { b: { c: { d: expected } } }; - } - expect(getReactiveParameterValue(new Target(), reactiveParameter)).toBe(expected); - }); - - it('returns undefined if root is undefined', () => { - const reactiveParameter: ReactiveParameter = { - reference: 'a.b.c.d', - head: 'a', - tail: ['b', 'c', 'd'], - }; - class Target { - // a does not exist - } - expect(getReactiveParameterValue(new Target(), reactiveParameter)).toBeUndefined(); - }); - - it('returns undefined if a segment is undefined', () => { - const reactiveParameter: ReactiveParameter = { - reference: 'a.b.c.d', - head: 'a', - tail: ['b', 'c', 'd'], - }; - class Target { - a = { b: undefined }; - } - expect(getReactiveParameterValue(new Target(), reactiveParameter)).toBeUndefined(); - }); - - it('returns undefined if a segment is not found', () => { - const reactiveParameter: ReactiveParameter = { - reference: 'a.b.c.d', - head: 'a', - tail: ['b', 'c', 'd'], - }; - class Target { - a = { b: {} }; - } - expect(getReactiveParameterValue(new Target(), reactiveParameter)).toBeUndefined(); - }); -}); diff --git a/packages/@lwc/wire-service/src/__tests__/wiring.spec.ts b/packages/@lwc/wire-service/src/__tests__/wiring.spec.ts deleted file mode 100644 index f22ec12bb0..0000000000 --- a/packages/@lwc/wire-service/src/__tests__/wiring.spec.ts +++ /dev/null @@ -1,522 +0,0 @@ -/* - * 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 * as target from '../wiring'; -import { ValueChangedEvent } from '../value-changed-event'; -import { LinkContextEvent } from '../link-context-event'; -import { - CONTEXT_ID, - CONTEXT_CONNECTED, - CONNECT, - CONTEXT_DISCONNECTED, - DISCONNECT, - CONTEXT_UPDATED, - CONFIG, -} from '../constants'; -import { ElementDef, WireDef } from '../engine'; -import * as dependency from '../property-trap'; - -describe('WireEventTarget', () => { - describe('addEventListener', () => { - describe('connect event', () => { - it('throws on duplicate listener', () => { - function dupeListener() { - /**/ - } - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = Object.create(null); - mockContext[CONTEXT_ID][CONTEXT_CONNECTED] = [dupeListener]; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - {} as WireDef, - 'test' - ); - expect(() => { - wireEventTarget.addEventListener(CONNECT, dupeListener); - }).toThrowError('must not call addEventListener("connect") with the same listener'); - }); - - it('adds listener to the queue', () => { - function listener() { - /**/ - } - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = Object.create(null); - mockContext[CONTEXT_ID][CONTEXT_CONNECTED] = []; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - {} as WireDef, - 'test' - ); - wireEventTarget.addEventListener(CONNECT, listener); - const actual = mockContext[CONTEXT_ID][CONTEXT_CONNECTED]; - expect(actual).toHaveLength(1); - expect(actual[0]).toBe(listener); - }); - }); - - describe('disconnect event', () => { - it('throws on duplicate listener', () => { - function dupeListener() { - /**/ - } - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = Object.create(null); - mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED] = [dupeListener]; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - {} as WireDef, - 'test' - ); - expect(() => { - wireEventTarget.addEventListener(DISCONNECT, dupeListener); - }).toThrowError( - 'must not call addEventListener("disconnect") with the same listener' - ); - }); - - it('adds listener to the queue', () => { - function listener() { - /**/ - } - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = Object.create(null); - mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED] = []; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - {} as WireDef, - 'test' - ); - wireEventTarget.addEventListener(DISCONNECT, listener); - const actual = mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED]; - expect(actual).toHaveLength(1); - expect(actual[0]).toBe(listener); - }); - }); - - describe('config event', () => { - it('immediately fires when no static or dynamic parameters', () => { - const listener = jest.fn(); - const mockWireDef: WireDef = { - adapter: {}, - }; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - {} as target.Context, - mockWireDef, - 'test' - ); - wireEventTarget.addEventListener(CONFIG, listener); - expect(listener).toHaveBeenCalledTimes(1); - }); - it('immediately fires when config is statics only', () => { - const listener = jest.fn(); - const mockWireDef: WireDef = { - adapter: {}, - params: {}, - static: { - test: ['fixed', 'array'], - }, - }; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - {} as target.Context, - mockWireDef, - 'test' - ); - wireEventTarget.addEventListener(CONFIG, listener); - expect(listener).toHaveBeenCalledTimes(1); - }); - it('does not install traps or enqueue for default values when no dynamic parameters', () => { - const mockWireDef: WireDef = { - adapter: {}, - params: {}, - static: { - test: ['fixed', 'array'], - }, - }; - const { installTrap, updated } = dependency; - (dependency as any).installTrap = jest.fn(); - (dependency as any).updated = jest.fn(); - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - {} as target.Context, - mockWireDef, - 'test' - ); - wireEventTarget.addEventListener(CONFIG, () => { - /**/ - }); - expect(dependency.installTrap).toHaveBeenCalledTimes(0); - expect(dependency.updated).toHaveBeenCalledTimes(0); - (dependency as any).installTrap = installTrap; - (dependency as any).updated = updated; - }); - it('enqueues for default values of dynamic parameters', () => { - const wireContext = Object.create(null); - wireContext[CONTEXT_UPDATED] = { listeners: {}, values: {} }; - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = wireContext; - const mockWireDef: WireDef = { - adapter: {}, - params: { - key: 'prop', - }, - }; - const { updated } = dependency; - (dependency as any).updated = jest.fn(); - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - mockWireDef, - 'test' - ); - wireEventTarget.addEventListener(CONFIG, () => { - /**/ - }); - expect(dependency.updated).toHaveBeenCalledTimes(1); - (dependency as any).updated = updated; - }); - it('creates one trap per property for multiple listeners', () => { - const wireContext = Object.create(null); - wireContext[CONTEXT_UPDATED] = { listeners: {}, values: {} }; - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = wireContext; - const mockWireDef: WireDef = { - adapter: {}, - params: { - key: 'prop', - }, - }; - const { installTrap } = dependency; - (dependency as any).installTrap = jest.fn(); - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - mockWireDef, - 'test' - ); - wireEventTarget.addEventListener(CONFIG, () => { - /**/ - }); - expect(dependency.installTrap).toHaveBeenCalled(); - const wireEventTarget1 = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - mockWireDef, - 'test1' - ); - wireEventTarget1.addEventListener(CONFIG, () => { - /**/ - }); - expect(dependency.installTrap).toHaveBeenCalledTimes(1); - (dependency as any).installTrap = installTrap; - }); - it('creates one trap for root property for multiple listeners to dot-notation parameters', () => { - const wireContext = Object.create(null); - wireContext[CONTEXT_UPDATED] = { listeners: {}, values: {} }; - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = wireContext; - const mockWireDef: WireDef = { - adapter: {}, - params: { - key: 'a.b.c.d', - other: 'a.x.y', - }, - }; - const { installTrap } = dependency; - (dependency as any).installTrap = jest.fn(); - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - mockWireDef, - 'test' - ); - wireEventTarget.addEventListener(CONFIG, () => { - /**/ - }); - expect(dependency.installTrap).toHaveBeenCalledTimes(1); - const wireEventTarget1 = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - mockWireDef, - 'test1' - ); - wireEventTarget1.addEventListener(CONFIG, () => { - /**/ - }); - expect(dependency.installTrap).toHaveBeenCalledTimes(1); - (dependency as any).installTrap = installTrap; - }); - it('creates one trap per property per component', () => { - const wireContext1 = Object.create(null); - wireContext1[CONTEXT_UPDATED] = { listeners: {}, values: {} }; - const mockContext1 = Object.create(null); - mockContext1[CONTEXT_ID] = wireContext1; - - const wireContext2 = Object.create(null); - wireContext2[CONTEXT_UPDATED] = { listeners: {}, values: {} }; - const mockContext2 = Object.create(null); - mockContext2[CONTEXT_ID] = wireContext2; - - const mockWireDef: WireDef = { - adapter: {}, - params: { - key: 'prop', - }, - }; - - const { installTrap } = dependency; - (dependency as any).installTrap = jest.fn(); - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext1, - mockWireDef, - 'test' - ); - wireEventTarget.addEventListener(CONFIG, () => { - /**/ - }); - expect(dependency.installTrap).toHaveBeenCalled(); - const wireEventTarget1 = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext2, - mockWireDef, - 'test' - ); - wireEventTarget1.addEventListener(CONFIG, () => { - /**/ - }); - expect(dependency.installTrap).toHaveBeenCalledTimes(2); - (dependency as any).installTrap = installTrap; - }); - }); - - it('throws when event type is not supported', () => { - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - {} as target.Context, - {} as WireDef, - 'test' - ); - expect(() => { - wireEventTarget.addEventListener('test', () => { - /**/ - }); - }).toThrowError('unsupported event type test'); - }); - }); - - describe('removeEventListener', () => { - it('removes listener from the queue for connect event', () => { - function listener() { - /**/ - } - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = Object.create(null); - mockContext[CONTEXT_ID][CONTEXT_CONNECTED] = [listener]; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - {} as WireDef, - 'test' - ); - wireEventTarget.removeEventListener(CONNECT, listener); - expect(mockContext[CONTEXT_ID][CONTEXT_CONNECTED]).toHaveLength(0); - }); - it('removes listener from the queue for disconnect event', () => { - function listener() { - /**/ - } - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = Object.create(null); - mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED] = [listener]; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - {} as WireDef, - 'test' - ); - wireEventTarget.removeEventListener(DISCONNECT, listener); - expect(mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED]).toHaveLength(0); - }); - it('removes listenerMetadata from the queue for config event for non-dot-notation reactive parameter', () => { - function listener() { - /**/ - } - const mockConfigListenerMetadata = { listener }; - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = Object.create(null); - mockContext[CONTEXT_ID][CONTEXT_UPDATED] = { - listeners: { prop: [mockConfigListenerMetadata] }, - }; - const mockWireDef: WireDef = { - adapter: {}, - params: { - test: 'prop', - }, - }; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - mockWireDef, - 'test' - ); - wireEventTarget.removeEventListener(CONFIG, listener); - expect(mockContext[CONTEXT_ID][CONTEXT_UPDATED].listeners.prop).toHaveLength(0); - }); - it('removes listenerMetadata from the queue for config event for dot-notation reactive parameter', () => { - function listener() { - /**/ - } - const mockConfigListenerMetadata = { listener }; - const mockContext = Object.create(null); - mockContext[CONTEXT_ID] = Object.create(null); - mockContext[CONTEXT_ID][CONTEXT_UPDATED] = { - listeners: { x: [mockConfigListenerMetadata] }, - }; - const mockWireDef: WireDef = { - adapter: {}, - params: { - test: 'x.y.z', - }, - }; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - mockContext, - mockWireDef, - 'test' - ); - wireEventTarget.removeEventListener(CONFIG, listener); - expect(mockContext[CONTEXT_ID][CONTEXT_UPDATED].listeners.x).toHaveLength(0); - }); - it('throws when event type is not supported', () => { - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - {} as target.Context, - {} as WireDef, - 'test' - ); - expect(() => { - wireEventTarget.removeEventListener('test', () => { - /**/ - }); - }).toThrowError('unsupported event type test'); - }); - }); - - describe('dispatchEvent', () => { - it('updates wired property when ValueChangeEvent received', () => { - const mockCmp = { - test: undefined, - }; - const wireEventTarget = new target.WireEventTarget( - mockCmp as any, - {} as ElementDef, - {} as target.Context, - {} as WireDef, - 'test' - ); - wireEventTarget.dispatchEvent(new ValueChangedEvent('value')); - expect(mockCmp.test).toBe('value'); - }); - it('invokes wired method when ValueChangedEvent received', () => { - let actual; - const mockCmp = { - test: value => { - actual = value; - }, - }; - const wireEventTarget = new target.WireEventTarget( - mockCmp as any, - {} as ElementDef, - {} as target.Context, - { method: 1 } as WireDef, - 'test' - ); - wireEventTarget.dispatchEvent(new ValueChangedEvent('value')); - expect(actual).toBe('value'); - }); - it('invokes dispatch method on element when LinkContextEvent received', () => { - expect.assertions(5); - function callback(data, disconnect) { - expect(data).toBe(1); - expect(disconnect).toBe(2); - } - const mockCmp = { - dispatchEvent(evt) { - expect(evt.type).toBe('foo'); - expect(typeof evt.detail).toBe('function'); - expect(evt.detail).not.toBe(callback); // avoid side-channeling by not leaking the original callback - evt.detail(1, 2); - }, - }; - const wireEventTarget = new target.WireEventTarget( - mockCmp as any, - {} as ElementDef, - {} as target.Context, - { method: 1 } as WireDef, - 'test' - ); - wireEventTarget.dispatchEvent(new LinkContextEvent('foo', callback)); - }); - it('invokes dispatch method on element when wirecontextevent received', () => { - expect.assertions(1); - const event = { type: 'wirecontextevent' }; - const mockCmp = { - dispatchEvent(evt) { - expect(evt.type).toBe('wirecontextevent'); - }, - }; - const wireEventTarget = new target.WireEventTarget( - mockCmp as any, - {} as ElementDef, - {} as target.Context, - { method: 1 } as WireDef, - 'test' - ); - wireEventTarget.dispatchEvent(event as Event); - }); - it('throws on non-ValueChangedEvent', () => { - const test = {}; - test.toString = () => 'test'; - const wireEventTarget = new target.WireEventTarget( - {} as EventTarget, - {} as ElementDef, - {} as target.Context, - {} as WireDef, - 'test' - ); - expect(() => { - wireEventTarget.dispatchEvent(test as ValueChangedEvent); - }).toThrowError('Invalid event test.'); - }); - }); -}); diff --git a/packages/@lwc/wire-service/src/constants.ts b/packages/@lwc/wire-service/src/constants.ts deleted file mode 100644 index 9ddbb8d3e5..0000000000 --- a/packages/@lwc/wire-service/src/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 - */ -// key in engine service context for wire service context -export const CONTEXT_ID = '@wire'; -// key in wire service context for updated listener metadata -export const CONTEXT_UPDATED = 'updated'; -// key in wire service context for connected listener metadata -export const CONTEXT_CONNECTED = 'connected'; -// key in wire service context for disconnected listener metadata -export const CONTEXT_DISCONNECTED = 'disconnected'; - -// wire event target life cycle connectedCallback hook event type -export const CONNECT = 'connect'; -// wire event target life cycle disconnectedCallback hook event type -export const DISCONNECT = 'disconnect'; -// wire event target life cycle config changed hook event type -export const CONFIG = 'config'; diff --git a/packages/@lwc/wire-service/src/engine.ts b/packages/@lwc/wire-service/src/engine.ts deleted file mode 100644 index 9116f69e9e..0000000000 --- a/packages/@lwc/wire-service/src/engine.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 - */ -// subtypes from lwc -export interface WireDef { - params?: { - [key: string]: string; - }; - static?: { - [key: string]: any; - }; - adapter: any; - method?: 1; -} -export interface ElementDef { - // wire is optional on ElementDef but the lwc guarantees it before invoking wiring service hook - wire: { - [key: string]: WireDef; - }; -} diff --git a/packages/@lwc/wire-service/src/index.ts b/packages/@lwc/wire-service/src/index.ts index 04ce41b9fe..8f6ee07c0b 100644 --- a/packages/@lwc/wire-service/src/index.ts +++ b/packages/@lwc/wire-service/src/index.ts @@ -4,176 +4,195 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ +import { ValueChangedEvent } from './value-changed-event'; + +const { freeze, defineProperty, isExtensible } = Object; + +// This value needs to be in sync with wiring.ts from @lwc/engine +const DeprecatedWiredElementHost = '$$DeprecatedWiredElementHostKey$$'; + /** - * The @wire service. - * - * Provides data binding between wire adapters and LWC components decorated with @wire. - * Register wire adapters with `register(adapterId: any, adapterFactory: WireAdapterFactory)`. + * Registers a wire adapter factory for Lightning Platform. + * @deprecated */ +export function register( + adapterId: any, + adapterEventTargetCallback: (eventTarget: WireEventTarget) => void +) { + if (adapterId == null || !isExtensible(adapterId)) { + throw new TypeError('adapter id must be extensible'); + } + if (typeof adapterEventTargetCallback !== 'function') { + throw new TypeError('adapter factory must be a callable'); + } + if ('adapter' in adapterId) { + throw new TypeError('adapter id is already associated to an adapter factory'); + } -import { assert } from '@lwc/shared'; -import { CONTEXT_ID, CONTEXT_CONNECTED, CONTEXT_DISCONNECTED, CONTEXT_UPDATED } from './constants'; -import { ElementDef } from './engine'; -import { - WireEventTargetListener, - Context, - WireContext, - WireEventTarget as WireServiceTargetConstructor, -} from './wiring'; -import { ValueChangedEvent } from './value-changed-event'; -import { LinkContextEvent } from './link-context-event'; + const AdapterClass = class extends WireAdapter { + constructor(dataCallback: dataCallback) { + super(dataCallback); + adapterEventTargetCallback(this.eventTarget); + } + }; -interface Service { - wiring(cmp: EventTarget, data: object, def: ElementDef, context: Context): void; - connected(cmp: EventTarget, data: object, def: ElementDef, context: Context): void; - disconnected(cmp: EventTarget, data: object, def: ElementDef, context: Context): void; + freeze(AdapterClass); + freeze(AdapterClass.prototype); + + defineProperty(adapterId, 'adapter', { + writable: false, + configurable: false, + value: AdapterClass, + }); } -export interface WireEventTarget { - dispatchEvent(evt: ValueChangedEvent): boolean; - addEventListener(type: string, listener: WireEventTargetListener): void; - removeEventListener(type: string, listener: WireEventTargetListener): void; +/** + * Registers the wire service. noop + * @deprecated + */ +export function registerWireService() {} + +const { forEach, splice: ArraySplice, indexOf: ArrayIndexOf } = Array.prototype; + +// wire event target life cycle connectedCallback hook event type +const CONNECT = 'connect'; +// wire event target life cycle disconnectedCallback hook event type +const DISCONNECT = 'disconnect'; +// wire event target life cycle config changed hook event type +const CONFIG = 'config'; + +type NoArgumentListener = () => void; +interface ConfigListenerArgument { + [key: string]: any; } +type ConfigListener = (config: ConfigListenerArgument) => void; -export type WireAdapterFactory = (eventTarget: WireEventTarget) => void; +type WireEventTargetListener = NoArgumentListener | ConfigListener; -// wire adapters: wire adapter id => adapter ctor -const adapterFactories: Map = new Map(); +export interface WireEventTarget { + addEventListener: (type: string, listener: WireEventTargetListener) => void; + removeEventListener: (type: string, listener: WireEventTargetListener) => void; + dispatchEvent: (evt: ValueChangedEvent) => boolean; +} -/** - * Invokes the specified callbacks. - * @param listeners functions to call - */ -function invokeListener(listeners: WireEventTargetListener[]): void { - for (let i = 0, len = listeners.length; i < len; ++i) { - listeners[i].call(undefined); +function removeListener(listeners: WireEventTargetListener[], toRemove: WireEventTargetListener) { + const idx = ArrayIndexOf.call(listeners, toRemove); + if (idx > -1) { + ArraySplice.call(listeners, idx, 1); } } -/** - * The wire service. - * - * This service is registered with the engine's service API. It connects service - * callbacks to wire adapter lifecycle events. - */ -const wireService: Service = { - wiring: (cmp, data, def, context) => { - const wireContext: WireContext = (context[CONTEXT_ID] = Object.create(null)); - wireContext[CONTEXT_CONNECTED] = []; - wireContext[CONTEXT_DISCONNECTED] = []; - wireContext[CONTEXT_UPDATED] = { listeners: {}, values: {} }; - - // engine guarantees invocation only if def.wire is defined - const wireStaticDef = def.wire; - const wireTargets = Object.keys(wireStaticDef); - for (let i = 0, len = wireTargets.length; i < len; i++) { - const wireTarget = wireTargets[i]; - const wireDef = wireStaticDef[wireTarget]; - const adapterFactory = adapterFactories.get(wireDef.adapter); - - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - wireDef.adapter, - `@wire on "${wireTarget}": adapter id must be truthy` - ); - assert.isTrue( - adapterFactory, - `@wire on "${wireTarget}": unknown adapter id: ${String(wireDef.adapter)}` - ); - - // enforce restrictions of reactive parameters - if (wireDef.params) { - Object.keys(wireDef.params).forEach(param => { - const prop = wireDef.params![param]; - const segments = prop.split('.'); - segments.forEach(segment => { - assert.isTrue( - segment.length > 0, - `@wire on "${wireTarget}": reactive parameters must not be empty` - ); - }); - assert.isTrue( - segments[0] !== wireTarget, - `@wire on "${wireTarget}": reactive parameter "${segments[0]}" must not refer to self` - ); - // restriction for dot-notation reactive parameters - if (segments.length > 1) { - // @wire emits a stream of immutable values. an emit sets the target property; it does not mutate a previously emitted value. - // restricting dot-notation reactive parameters to reference other @wire targets makes trapping the 'head' of the parameter - // sufficient to observe the value change. - assert.isTrue( - wireTargets.includes(segments[0]) && - wireStaticDef[segments[0]].method !== 1, - `@wire on "${wireTarget}": dot-notation reactive parameter "${prop}" must refer to a @wire property` - ); +interface dataCallback { + (value: any): void; + [DeprecatedWiredElementHost]: any; +} +export interface WireAdapterConstructor { + new (callback: dataCallback): WireAdapter; +} + +export class WireAdapter { + private callback: dataCallback; + private readonly wiredElementHost: EventTarget; + + private connecting: NoArgumentListener[] = []; + private disconnecting: NoArgumentListener[] = []; + private configuring: ConfigListener[] = []; + + /** + * Attaching a config listener. + * + * The old behavior for attaching a config listener depended on these 3 cases: + * 1- The wire instance does have any arguments. + * 2- The wire instance have only static arguments. + * 3- The wire instance have at least one dynamic argument. + * + * In case 1 and 2, the listener should be called immediately. + * In case 3, the listener needs to wait for the value of the dynamic argument to be updated by the engine. + * + * In order to match the above logic, we need to save the last config available: + * if is undefined, the engine hasn't set it yet, we treat it as case 3. Note: the current logic does not make a distinction between dynamic and static config. + * if is defined, it means that for the component instance, and this adapter instance, the currentConfig is the proper one + * and the listener will be called immediately. + * + */ + private currentConfig?: ConfigListenerArgument; + + constructor(callback: dataCallback) { + this.callback = callback; + this.wiredElementHost = callback[DeprecatedWiredElementHost]; + this.eventTarget = { + addEventListener: (type: string, listener: WireEventTargetListener): void => { + switch (type) { + case CONNECT: { + this.connecting.push(listener as NoArgumentListener); + break; + } + case DISCONNECT: { + this.disconnecting.push(listener as NoArgumentListener); + break; + } + case CONFIG: { + this.configuring.push(listener as ConfigListener); + + if (this.currentConfig !== undefined) { + (listener as ConfigListener).call(undefined, this.currentConfig); } - }); + break; + } + default: + throw new Error(`Invalid event type ${type}.`); } - } - - if (adapterFactory) { - const wireEventTarget = new WireServiceTargetConstructor( - cmp, - def, - context, - wireDef, - wireTarget - ); - - adapterFactory({ - dispatchEvent: wireEventTarget.dispatchEvent.bind(wireEventTarget), - addEventListener: wireEventTarget.addEventListener.bind(wireEventTarget), - removeEventListener: wireEventTarget.removeEventListener.bind(wireEventTarget), - }); - } - } - }, - - connected: (cmp, data, def, context) => { - let listeners: WireEventTargetListener[]; - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - !def.wire || context[CONTEXT_ID], - 'wire service was not initialized prior to component creation: "connected" service hook invoked without necessary context' - ); - } - if (!def.wire || !(listeners = context[CONTEXT_ID][CONTEXT_CONNECTED])) { - return; - } - invokeListener(listeners); - }, - - disconnected: (cmp, data, def, context) => { - let listeners: WireEventTargetListener[]; - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - !def.wire || context[CONTEXT_ID], - 'wire service was not initialized prior to component creation: "disconnected" service hook invoked without necessary context' - ); - } - if (!def.wire || !(listeners = context[CONTEXT_ID][CONTEXT_DISCONNECTED])) { - return; - } - invokeListener(listeners); - }, -}; + }, + removeEventListener: (type: string, listener: WireEventTargetListener): void => { + switch (type) { + case CONNECT: { + removeListener(this.connecting, listener); + break; + } + case DISCONNECT: { + removeListener(this.disconnecting, listener); + break; + } + case CONFIG: { + removeListener(this.configuring, listener); + break; + } + default: + throw new Error(`Invalid event type ${type}.`); + } + }, + dispatchEvent: (evt: ValueChangedEvent | Event): boolean => { + if (evt instanceof ValueChangedEvent) { + const value = evt.value; + this.callback(value); + } else if (evt.type === 'wirecontextevent') { + // TODO [#1357]: remove this branch + return this.wiredElementHost.dispatchEvent(evt); + } else { + throw new Error(`Invalid event type ${(evt as any).type}.`); + } + return false; // canceling signal since we don't want this to propagate + }, + }; + } -/** - * Registers the wire service. - */ -export function registerWireService(registerService: (service: Service) => void): void { - registerService(wireService); -} + protected eventTarget: WireEventTarget; -/** - * Registers a wire adapter. - */ -export function register(adapterId: any, adapterFactory: WireAdapterFactory): void { - if (process.env.NODE_ENV !== 'production') { - assert.isTrue(adapterId, 'adapter id must be truthy'); - assert.isTrue(typeof adapterFactory === 'function', 'adapter factory must be a callable'); + update(config: Record) { + this.currentConfig = config; + forEach.call(this.configuring, listener => { + listener.call(undefined, config); + }); + } + + connect() { + forEach.call(this.connecting, listener => listener.call(undefined)); + } + + disconnect() { + forEach.call(this.disconnecting, listener => listener.call(undefined)); } - adapterFactories.set(adapterId, adapterFactory); } -export { ValueChangedEvent, LinkContextEvent }; +// re-exporting event constructors +export { ValueChangedEvent }; diff --git a/packages/@lwc/wire-service/src/link-context-event.ts b/packages/@lwc/wire-service/src/link-context-event.ts deleted file mode 100644 index 3196513b08..0000000000 --- a/packages/@lwc/wire-service/src/link-context-event.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 - */ - -const LinkContextEventType = 'LinkContextEvent'; - -/** - * Event fired by wire adapters to link to a context provider - */ -export class LinkContextEvent { - type: string; - uid: string; - callback: (...args: any[]) => void; - constructor(uid: string, callback: (...args: any[]) => void) { - this.type = LinkContextEventType; - this.uid = uid; - this.callback = callback; - } -} diff --git a/packages/@lwc/wire-service/src/property-trap.ts b/packages/@lwc/wire-service/src/property-trap.ts deleted file mode 100644 index 62f575ed38..0000000000 --- a/packages/@lwc/wire-service/src/property-trap.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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 - */ -/* - * Detects property changes by installing setter/getter overrides on the component - * instance. - */ - -import { ConfigListenerMetadata, ConfigContext, ReactiveParameter } from './wiring'; - -/** - * Invokes the provided change listeners with the resolved component properties. - * @param configListenerMetadatas List of config listener metadata (config listeners and their context) - * @param paramValues Values for all wire adapter config params - */ -function invokeConfigListeners( - configListenerMetadatas: Set, - paramValues: any -) { - configListenerMetadatas.forEach(metadata => { - const { listener, statics, reactives } = metadata; - - const reactiveValues = Object.create(null); - if (reactives) { - const keys = Object.keys(reactives); - for (let j = 0, jlen = keys.length; j < jlen; j++) { - const key = keys[j]; - const value = paramValues[reactives[key]]; - reactiveValues[key] = value; - } - } - - // TODO [#1634]: consider read-only membrane to enforce invariant of immutable config - const config = Object.assign({}, statics, reactiveValues); - listener.call(undefined, config); - }); -} - -/** - * Marks a reactive parameter as having changed. - * @param cmp The component - * @param reactiveParameters Reactive parameters that has changed - * @param configContext The service context - */ -export function updated( - cmp: EventTarget, - reactiveParameters: Array, - configContext: ConfigContext -) { - if (!configContext.mutated) { - configContext.mutated = new Set(reactiveParameters); - // collect all prop changes via a microtask - Promise.resolve().then(updatedFuture.bind(undefined, cmp, configContext)); - } else { - for (let i = 0, n = reactiveParameters.length; i < n; i++) { - configContext.mutated.add(reactiveParameters[i]); - } - } -} - -function updatedFuture(cmp: EventTarget, configContext: ConfigContext) { - const uniqueListeners = new Set(); - - // configContext.mutated must be set prior to invoking this function - const mutated = configContext.mutated as Set; - delete configContext.mutated; - - mutated.forEach(reactiveParameter => { - const value = getReactiveParameterValue(cmp, reactiveParameter); - if (configContext.values[reactiveParameter.reference] === value) { - return; - } - configContext.values[reactiveParameter.reference] = value; - - const listeners = configContext.listeners[reactiveParameter.head]; - for (let i = 0, len = listeners.length; i < len; i++) { - uniqueListeners.add(listeners[i]); - } - }); - - invokeConfigListeners(uniqueListeners, configContext.values); -} - -/** - * Gets the value of an @wire reactive parameter. - * @param cmp The component - * @param reactiveParameter The parameter to get - */ -export function getReactiveParameterValue( - cmp: EventTarget, - reactiveParameter: ReactiveParameter -): any { - let value = (cmp as any)[reactiveParameter.head]; - if (!reactiveParameter.tail) { - return value; - } - - const segments = reactiveParameter.tail; - for (let i = 0, len = segments.length; i < len && value != null; i++) { - const segment = segments[i]; - if (typeof value !== 'object' || !(segment in value)) { - return undefined; - } - value = value[segment]; - } - return value; -} - -/** - * Installs setter override to trap changes to a property, triggering the config listeners. - * @param cmp The component - * @param reactiveParametersHead The common head of the reactiveParameters - * @param reactiveParameters Reactive parameters with the same head, that defines the property to monitor - * @param configContext The service context - */ -export function installTrap( - cmp: EventTarget, - reactiveParametersHead: string, - reactiveParameters: Array, - configContext: ConfigContext -) { - const callback = updated.bind(undefined, cmp, reactiveParameters, configContext); - const newDescriptor = getOverrideDescriptor(cmp, reactiveParametersHead, callback); - Object.defineProperty(cmp, reactiveParametersHead, newDescriptor); -} - -/** - * Finds the descriptor of the named property on the prototype chain - * @param target The target instance/constructor function - * @param propName Name of property to find - * @param protoSet Prototypes searched (to avoid circular prototype chains) - */ -export function findDescriptor( - target: any, - propName: PropertyKey, - protoSet?: any[] -): PropertyDescriptor | null { - protoSet = protoSet || []; - if (!target || protoSet.indexOf(target) > -1) { - return null; // null, undefined, or circular prototype definition - } - const descriptor = Object.getOwnPropertyDescriptor(target, propName); - if (descriptor) { - return descriptor; - } - const proto = Object.getPrototypeOf(target); - if (!proto) { - return null; - } - protoSet.push(target); - return findDescriptor(proto, propName, protoSet); -} - -/** - * Gets a property descriptor that monitors the provided property for changes - * @param cmp The component - * @param prop The name of the property to be monitored - * @param callback A function to invoke when the prop's value changes - * @return A property descriptor - */ -function getOverrideDescriptor(cmp: EventTarget, prop: string, callback: () => void) { - const descriptor = findDescriptor(cmp, prop); - let enumerable; - let get; - let set; - // This does not cover the override of existing descriptors at the instance level - // and that's ok because eventually we will not need to do any of these :) - if (descriptor === null || (descriptor.get === undefined && descriptor.set === undefined)) { - let value = (cmp as any)[prop]; - enumerable = true; - get = function() { - return value; - }; - set = function(newValue: any) { - value = newValue; - callback(); - }; - } else { - const { set: originalSet, get: originalGet } = descriptor; - enumerable = descriptor.enumerable; - set = function(newValue: any) { - if (originalSet) { - originalSet.call(cmp, newValue); - } - callback(); - }; - get = function() { - return originalGet ? originalGet.call(cmp) : undefined; - }; - } - return { - set, - get, - enumerable, - configurable: true, - }; -} diff --git a/packages/@lwc/wire-service/src/wiring.ts b/packages/@lwc/wire-service/src/wiring.ts deleted file mode 100644 index 17e5c7f50f..0000000000 --- a/packages/@lwc/wire-service/src/wiring.ts +++ /dev/null @@ -1,281 +0,0 @@ -/* - * 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 { assert, isUndefined } from '@lwc/shared'; -import { - CONTEXT_ID, - CONTEXT_CONNECTED, - CONTEXT_DISCONNECTED, - CONTEXT_UPDATED, - CONNECT, - DISCONNECT, - CONFIG, -} from './constants'; -import { ElementDef, WireDef } from './engine'; -import { installTrap, updated } from './property-trap'; -import { ValueChangedEvent } from './value-changed-event'; -import { LinkContextEvent } from './link-context-event'; - -export type WireEventTargetListener = (config?: any) => void; - -// a reactive parameter (WireDef.params.key) may be a dot-notation string to traverse into another @wire's target -export interface ReactiveParameter { - reference: string; // the complete parameter (aka original WireDef.params.key) - head: string; // head of the parameter - tail?: string[]; // remaining tail of the parameter, present if it's dot-notation -} - -export interface ConfigListenerMetadata { - listener: WireEventTargetListener; - statics?: { - [key: string]: any; - }; - reactives?: { - [key: string]: string; - }; -} -export interface ConfigContext { - // map of reactive parameters to list of config listeners - // when a reactive parameter changes it's a O(1) lookup to the list of config listeners to notify - listeners: { - [key: string]: ConfigListenerMetadata[]; - }; - // map of param values - values: { - [key: string]: any; - }; - // mutated reactive parameters (debounced then cleared) - mutated?: Set; -} - -export interface WireContext { - [CONTEXT_CONNECTED]: WireEventTargetListener[]; - [CONTEXT_DISCONNECTED]: WireEventTargetListener[]; - [CONTEXT_UPDATED]: ConfigContext; -} - -export interface Context { - [CONTEXT_ID]: WireContext; -} - -function removeListener(listeners: WireEventTargetListener[], toRemove: WireEventTargetListener) { - const idx = listeners.indexOf(toRemove); - if (idx > -1) { - listeners.splice(idx, 1); - } -} - -function removeConfigListener( - configListenerMetadatas: ConfigListenerMetadata[], - toRemove: WireEventTargetListener -) { - for (let i = 0, len = configListenerMetadatas.length; i < len; i++) { - if (configListenerMetadatas[i].listener === toRemove) { - configListenerMetadatas.splice(i, 1); - return; - } - } -} - -function buildReactiveParameter(reference: string): ReactiveParameter { - if (!reference.includes('.')) { - return { - reference, - head: reference, - }; - } - const segments = reference.split('.'); - return { - reference, - head: segments.shift() as string, - tail: segments, - }; -} - -export class WireEventTarget { - _cmp: EventTarget; - _def: ElementDef; - _context: Context; - _wireDef: WireDef; - _wireTarget: string; - - constructor( - cmp: EventTarget, - def: ElementDef, - context: Context, - wireDef: WireDef, - wireTarget: string - ) { - this._cmp = cmp; - this._def = def; - this._context = context; - this._wireDef = wireDef; - this._wireTarget = wireTarget; - } - - addEventListener(type: string, listener: WireEventTargetListener): void { - switch (type) { - case CONNECT: { - const connectedListeners = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; - if (process.env.NODE_ENV !== 'production') { - assert.isFalse( - connectedListeners.includes(listener), - 'must not call addEventListener("connect") with the same listener' - ); - } - connectedListeners.push(listener); - break; - } - - case DISCONNECT: { - const disconnectedListeners = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; - if (process.env.NODE_ENV !== 'production') { - assert.isFalse( - disconnectedListeners.includes(listener), - 'must not call addEventListener("disconnect") with the same listener' - ); - } - disconnectedListeners.push(listener); - break; - } - - case CONFIG: { - const reactives = this._wireDef.params; - const statics = this._wireDef.static; - let reactiveKeys: string[]; - - // no reactive parameters. fire config once with static parameters (if present). - if (!reactives || (reactiveKeys = Object.keys(reactives)).length === 0) { - const config = statics || Object.create(null); - listener.call(undefined, config); - return; - } - - const configListenerMetadata: ConfigListenerMetadata = { - listener, - statics, - reactives, - }; - - // setup listeners for all reactive parameters - const configContext = this._context[CONTEXT_ID][CONTEXT_UPDATED]; - const reactiveParametersGroupByHead: Record> = {}; - - reactiveKeys.forEach(key => { - const reactiveParameter = buildReactiveParameter(reactives[key]); - const reactiveParameterHead = reactiveParameter.head; - let configListenerMetadatas = configContext.listeners[reactiveParameterHead]; - - let reactiveParametersWithSameHead = - reactiveParametersGroupByHead[reactiveParameterHead]; - - if (isUndefined(reactiveParametersWithSameHead)) { - reactiveParametersWithSameHead = []; - reactiveParametersGroupByHead[ - reactiveParameterHead - ] = reactiveParametersWithSameHead; - } - - reactiveParametersWithSameHead.push(reactiveParameter); - - if (!configListenerMetadatas) { - configListenerMetadatas = [configListenerMetadata]; - configContext.listeners[reactiveParameterHead] = configListenerMetadatas; - installTrap( - this._cmp, - reactiveParameterHead, - reactiveParametersWithSameHead, - configContext - ); - } else { - configListenerMetadatas.push(configListenerMetadata); - } - }); - - // enqueue to pickup default values - Object.keys(reactiveParametersGroupByHead).forEach(head => { - updated(this._cmp, reactiveParametersGroupByHead[head], configContext); - }); - - break; - } - - default: - throw new Error(`unsupported event type ${type}`); - } - } - - removeEventListener(type: string, listener: WireEventTargetListener): void { - switch (type) { - case CONNECT: { - const connectedListeners = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; - removeListener(connectedListeners, listener); - break; - } - - case DISCONNECT: { - const disconnectedListeners = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; - removeListener(disconnectedListeners, listener); - break; - } - - case CONFIG: { - const paramToConfigListenerMetadata = this._context[CONTEXT_ID][CONTEXT_UPDATED] - .listeners; - const reactives = this._wireDef.params; - if (reactives) { - Object.keys(reactives).forEach(key => { - const reactiveParameter = buildReactiveParameter(reactives[key]); - const configListenerMetadatas = - paramToConfigListenerMetadata[reactiveParameter.head]; - if (configListenerMetadatas) { - removeConfigListener(configListenerMetadatas, listener); - } - }); - } - break; - } - - default: - throw new Error(`unsupported event type ${type}`); - } - } - - dispatchEvent(evt: ValueChangedEvent | LinkContextEvent | Event): boolean { - if (evt instanceof ValueChangedEvent) { - const value = evt.value; - if (this._wireDef.method) { - (this._cmp as any)[this._wireTarget](value); - } else { - (this._cmp as any)[this._wireTarget] = value; - } - return false; // canceling signal since we don't want this to propagate - } else if (evt instanceof LinkContextEvent) { - const { uid, callback } = evt; - // This event is responsible for connecting the host element with another - // element in the composed path that is providing contextual data. The provider - // must be listening for a special dom event with the name corresponding to `uid`, - // which must remain secret, to guarantee that the linkage is only possible via - // the corresponding wire adapter. - const internalDomEvent = new CustomEvent(uid, { - bubbles: true, - composed: true, - // avoid leaking the callback function directly to prevent a side channel - // during the linking phase to the context provider. - detail(...args: any[]) { - callback(...args); - }, - }); - this._cmp.dispatchEvent(internalDomEvent); - return false; // canceling signal since we don't want this to propagate - } else if (evt.type === 'wirecontextevent') { - // TODO [#1357]: remove this branch - return this._cmp.dispatchEvent(evt); - } else { - throw new Error(`Invalid event ${evt}.`); - } - } -} diff --git a/packages/integration-karma/test/api/getComponentDef/index.spec.js b/packages/integration-karma/test/api/getComponentDef/index.spec.js index 2003c24872..496d574a56 100644 --- a/packages/integration-karma/test/api/getComponentDef/index.spec.js +++ b/packages/integration-karma/test/api/getComponentDef/index.spec.js @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { LightningElement, api, getComponentDef } from 'lwc'; import PublicProperties from 'x/publicProperties'; @@ -6,13 +7,6 @@ import PublicMethods from 'x/publicMethods'; import PublicPropertiesInheritance from 'x/publicPropertiesInheritance'; import PublicMethodsInheritance from 'x/publicMethodsInheritance'; -import WireProperties from 'x/wireProperties'; -import WireMethods from 'x/wireMethods'; -import WirePropertiesInheritance from 'x/wirePropertiesInheritance'; -import WireMethodsInheritance from 'x/wireMethodsInheritance'; - -import wireAdapter from 'x/wireAdapter'; - function testInvalidComponentConstructor(name, ctor) { it(`should throw for ${name}`, () => { expect(() => getComponentDef(ctor)).toThrowError( @@ -21,6 +15,36 @@ function testInvalidComponentConstructor(name, ctor) { }); } +beforeAll(function() { + const getNormalizedFunctionAsString = fn => fn.toString().replace(/(\s|\n)/g, ''); + + jasmine.addMatchers({ + toEqualWireSettings: function() { + return { + compare: function(actual, expected) { + Object.keys(actual).forEach(currentKey => { + const normalizedActual = Object.assign({}, actual[currentKey], { + config: getNormalizedFunctionAsString(actual[currentKey].config), + }); + + const normalizedExpected = Object.assign({}, expected[currentKey], { + config: getNormalizedFunctionAsString( + expected[currentKey].config || function() {} + ), + }); + + expect(normalizedActual).toEqual(normalizedExpected); + }); + + return { + pass: true, + }; + }, + }; + }, + }); +}); + testInvalidComponentConstructor('null', null); testInvalidComponentConstructor('undefined', undefined); testInvalidComponentConstructor('String', 'component'); @@ -145,7 +169,7 @@ describe('@api', () => { expect(props).toEqual( jasmine.objectContaining({ parentProp: { - config: 0, + config: 3, type: 'any', attr: 'parent-prop', }, @@ -173,118 +197,6 @@ describe('@api', () => { }); }); -describe('@wire', () => { - it('should return the wired properties in wire object', () => { - const { wire } = getComponentDef(WireProperties); - expect(wire).toEqual({ - foo: { - adapter: wireAdapter, - }, - bar: { - adapter: wireAdapter, - static: { - a: true, - }, - params: {}, - }, - baz: { - adapter: wireAdapter, - static: { - b: true, - }, - params: { - c: 'foo', - }, - }, - }); - }); - - it('should return the wired methods in the wire object with a method flag', () => { - const { wire } = getComponentDef(WireMethods); - expect(wire).toEqual({ - foo: { - adapter: wireAdapter, - method: 1, - }, - bar: { - adapter: wireAdapter, - static: { - a: true, - }, - params: {}, - method: 1, - }, - baz: { - adapter: wireAdapter, - static: { - b: true, - }, - params: { - c: 'foo', - }, - method: 1, - }, - }); - }); - - it('should inherit wire properties from the base class', () => { - const { wire } = getComponentDef(WirePropertiesInheritance); - expect(wire).toEqual({ - parentProp: { - adapter: wireAdapter, - static: { - parent: true, - }, - params: {}, - }, - overriddenInChild: { - adapter: wireAdapter, - static: { - child: true, - }, - params: {}, - }, - childProp: { - adapter: wireAdapter, - static: { - child: true, - }, - params: {}, - }, - }); - }); - - it('should inherit the wire methods from the case class', () => { - const { wire } = getComponentDef(WireMethodsInheritance); - expect(wire).toEqual({ - parentMethod: { - adapter: wireAdapter, - static: { - parent: true, - }, - params: {}, - method: 1, - }, - overriddenInChild: { - adapter: wireAdapter, - static: { - child: true, - }, - params: {}, - method: 1, - }, - childMethod: { - adapter: wireAdapter, - static: { - child: true, - }, - params: {}, - method: 1, - }, - }); - }); -}); - describe('circular dependencies', () => { // Emulates an AMD module with circular dependency. function circularDependency(klass) { diff --git a/packages/integration-karma/test/api/getComponentDef/x/publicPropertiesInheritance/base.js b/packages/integration-karma/test/api/getComponentDef/x/publicPropertiesInheritance/base.js index 8ea2934e57..151d34981b 100644 --- a/packages/integration-karma/test/api/getComponentDef/x/publicPropertiesInheritance/base.js +++ b/packages/integration-karma/test/api/getComponentDef/x/publicPropertiesInheritance/base.js @@ -1,6 +1,10 @@ import { LightningElement, api } from 'lwc'; export default class Base extends LightningElement { - @api parentProp; + @api get parentProp() { + return undefined; + } + set parentProp(v) {} + @api overriddenInChild; } diff --git a/packages/integration-karma/test/api/getComponentDef/x/wireAdapter/wireAdapter.js b/packages/integration-karma/test/api/getComponentDef/x/wireAdapter/wireAdapter.js deleted file mode 100644 index f2672e14de..0000000000 --- a/packages/integration-karma/test/api/getComponentDef/x/wireAdapter/wireAdapter.js +++ /dev/null @@ -1 +0,0 @@ -export default function wireAdapter() {} diff --git a/packages/integration-karma/test/api/getComponentDef/x/wireMethods/wireMethods.js b/packages/integration-karma/test/api/getComponentDef/x/wireMethods/wireMethods.js deleted file mode 100644 index 04d60e9f76..0000000000 --- a/packages/integration-karma/test/api/getComponentDef/x/wireMethods/wireMethods.js +++ /dev/null @@ -1,8 +0,0 @@ -import { LightningElement, wire } from 'lwc'; -import wireAdapter from 'x/wireAdapter'; - -export default class WireMethods extends LightningElement { - @wire(wireAdapter) foo() {} - @wire(wireAdapter, { a: true }) bar() {} - @wire(wireAdapter, { b: true, c: '$foo' }) baz() {} -} diff --git a/packages/integration-karma/test/api/getComponentDef/x/wireMethodsInheritance/base.js b/packages/integration-karma/test/api/getComponentDef/x/wireMethodsInheritance/base.js deleted file mode 100644 index 9d6c243dbf..0000000000 --- a/packages/integration-karma/test/api/getComponentDef/x/wireMethodsInheritance/base.js +++ /dev/null @@ -1,7 +0,0 @@ -import { LightningElement, wire } from 'lwc'; -import wireAdapter from 'x/wireAdapter'; - -export default class Base extends LightningElement { - @wire(wireAdapter, { parent: true }) parentMethod() {} - @wire(wireAdapter, { parent: true }) overriddenInChild() {} -} diff --git a/packages/integration-karma/test/api/getComponentDef/x/wireMethodsInheritance/wireMethodsInheritance.js b/packages/integration-karma/test/api/getComponentDef/x/wireMethodsInheritance/wireMethodsInheritance.js deleted file mode 100644 index 9fc83603c0..0000000000 --- a/packages/integration-karma/test/api/getComponentDef/x/wireMethodsInheritance/wireMethodsInheritance.js +++ /dev/null @@ -1,9 +0,0 @@ -import { wire } from 'lwc'; -import wireAdapter from 'x/wireAdapter'; - -import Base from './base'; - -export default class WireMethodsInheritance extends Base { - @wire(wireAdapter, { child: true }) childMethod() {} - @wire(wireAdapter, { child: true }) overriddenInChild() {} -} diff --git a/packages/integration-karma/test/api/getComponentDef/x/wireProperties/wireProperties.js b/packages/integration-karma/test/api/getComponentDef/x/wireProperties/wireProperties.js deleted file mode 100644 index 98c24dac7e..0000000000 --- a/packages/integration-karma/test/api/getComponentDef/x/wireProperties/wireProperties.js +++ /dev/null @@ -1,8 +0,0 @@ -import { LightningElement, wire } from 'lwc'; -import wireAdapter from 'x/wireAdapter'; - -export default class WireProperties extends LightningElement { - @wire(wireAdapter) foo; - @wire(wireAdapter, { a: true }) bar; - @wire(wireAdapter, { b: true, c: '$foo' }) baz; -} diff --git a/packages/integration-karma/test/api/getComponentDef/x/wirePropertiesInheritance/base.js b/packages/integration-karma/test/api/getComponentDef/x/wirePropertiesInheritance/base.js deleted file mode 100644 index efe484f59e..0000000000 --- a/packages/integration-karma/test/api/getComponentDef/x/wirePropertiesInheritance/base.js +++ /dev/null @@ -1,7 +0,0 @@ -import { LightningElement, wire } from 'lwc'; -import wireAdapter from 'x/wireAdapter'; - -export default class Base extends LightningElement { - @wire(wireAdapter, { parent: true }) parentProp; - @wire(wireAdapter, { parent: true }) overriddenInChild; -} diff --git a/packages/integration-karma/test/api/getComponentDef/x/wirePropertiesInheritance/wirePropertiesInheritance.js b/packages/integration-karma/test/api/getComponentDef/x/wirePropertiesInheritance/wirePropertiesInheritance.js deleted file mode 100644 index 39f6d4946d..0000000000 --- a/packages/integration-karma/test/api/getComponentDef/x/wirePropertiesInheritance/wirePropertiesInheritance.js +++ /dev/null @@ -1,9 +0,0 @@ -import { wire } from 'lwc'; -import wireAdapter from 'x/wireAdapter'; - -import Base from './base'; - -export default class WirePropertiesInheritance extends Base { - @wire(wireAdapter, { child: true }) childProp; - @wire(wireAdapter, { child: true }) overriddenInChild; -} diff --git a/packages/integration-karma/test/context/advanced-context.spec.js b/packages/integration-karma/test/context/advanced-context.spec.js index ac883638ec..2e5e6ec655 100644 --- a/packages/integration-karma/test/context/advanced-context.spec.js +++ b/packages/integration-karma/test/context/advanced-context.spec.js @@ -1,6 +1,4 @@ -import { createElement, register } from 'lwc'; -import { registerWireService } from 'wire-service'; -registerWireService(register); +import { createElement } from 'lwc'; import { installCustomContext, getValueForIdentity } from 'x/advancedProvider'; import Consumer from 'x/advancedConsumer'; import { setValueForIdentity } from './x/advancedProvider/advancedProvider'; diff --git a/packages/integration-karma/test/context/simple-context.spec.js b/packages/integration-karma/test/context/simple-context.spec.js index f3f08cb008..b1b630cb29 100644 --- a/packages/integration-karma/test/context/simple-context.spec.js +++ b/packages/integration-karma/test/context/simple-context.spec.js @@ -1,6 +1,4 @@ -import { createElement, register } from 'lwc'; -import { registerWireService } from 'wire-service'; -registerWireService(register); +import { createElement } from 'lwc'; import { installCustomContext, setCustomContext } from 'x/simpleProvider'; import Consumer from 'x/simpleConsumer'; @@ -29,4 +27,21 @@ describe('Simple Custom Context Provider', () => { div.appendChild(elm); expect(elm.shadowRoot.textContent).toBe('pending'); }); + it('should use closest context when installed in a hierarchy of targets', function() { + const div = document.createElement('div'); + div.innerHTML = '
'; + const elm = createElement('x-consumer', { is: Consumer }); + const elm2 = createElement('x-consumer', { is: Consumer }); + const childTarget = div.querySelector('.child-ctx'); + + document.body.appendChild(div); + installCustomContext(div); + installCustomContext(childTarget); + setCustomContext(div, 'parent'); + setCustomContext(childTarget, 'child'); + div.appendChild(elm); + childTarget.appendChild(elm2); + expect(elm.shadowRoot.textContent).toBe('parent'); + expect(elm2.shadowRoot.textContent).toBe('child'); + }); }); diff --git a/packages/integration-karma/test/context/x/advancedConsumer/advancedConsumer.js b/packages/integration-karma/test/context/x/advancedConsumer/advancedConsumer.js index 57c244f94a..f5e30e7cc8 100644 --- a/packages/integration-karma/test/context/x/advancedConsumer/advancedConsumer.js +++ b/packages/integration-karma/test/context/x/advancedConsumer/advancedConsumer.js @@ -1,8 +1,8 @@ import { LightningElement, wire, api } from 'lwc'; -import { Provider } from 'x/advancedProvider'; +import { WireAdapter } from 'x/advancedProvider'; export default class ConsumerElement extends LightningElement { - @wire(Provider) context; + @wire(WireAdapter) context; @api getIdentity() { return this.context; diff --git a/packages/integration-karma/test/context/x/advancedProvider/advancedProvider.js b/packages/integration-karma/test/context/x/advancedProvider/advancedProvider.js index 5cbd8608a1..849fa6d20e 100644 --- a/packages/integration-karma/test/context/x/advancedProvider/advancedProvider.js +++ b/packages/integration-karma/test/context/x/advancedProvider/advancedProvider.js @@ -6,50 +6,43 @@ * This provide is sharing the same value with every child, and the * identity of the consumer is not tracked. */ - -import { register, ValueChangedEvent, LinkContextEvent } from 'wire-service'; - -const { addEventListener } = Document.prototype; +import { createContextProvider } from 'lwc'; const IdentityMetaMap = new WeakMap(); -const UniqueEventName = `advanced_context_event_${guid()}`; -const Provider = Symbol('SimpleContextProvider'); +const ConsumerMetaMap = new WeakMap(); -function guid() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); -} +const { hasOwnProperty } = Object.prototype; -register(Provider, eventTarget => { - let unsubscribeCallback; +export class WireAdapter { + // no provider was found, in which case the default + // context should be set. + contextValue = null; - function callback(data, unsubscribe) { - eventTarget.dispatchEvent(new ValueChangedEvent(data)); - unsubscribeCallback = unsubscribe; + constructor(dataCallback) { + this._dataCallback = dataCallback; + // Note: you might also use a global identity in constructors + this._dataCallback(this.contextValue); } - - eventTarget.addEventListener('connect', () => { - const event = new LinkContextEvent(UniqueEventName, callback); - eventTarget.dispatchEvent(event); - if (unsubscribeCallback === undefined) { - // no provider was found, in which case the default - // context should be set. - const defaultContext = null; - // Note: you might decide to use a global identity instead - eventTarget.dispatchEvent(new ValueChangedEvent(defaultContext)); + update(_config, context) { + if (context) { + // we only care about the context, no config is expected or used + if (!hasOwnProperty.call(context, 'value')) { + throw new Error(`Invalid context provided`); + } + this.contextValue = context.value; + this._dataCallback(this.contextValue); } - }); - - eventTarget.addEventListener('disconnect', () => { - if (unsubscribeCallback !== undefined) { - unsubscribeCallback(); - unsubscribeCallback = undefined; // resetting it to support reinsertion - } - }); -}); + } + connect() { + // noop + } + disconnect() { + // noop + } + static contextSchema = { value: 'required' /* could be 'optional' */ }; +} -function createNewConsumerMeta(provider, callback) { +function createNewConsumerMeta(consumer) { // identity must be an object that can't be proxified otherwise we // loose the identity when tracking the value. const identity = Object.freeze(_ => { @@ -60,42 +53,44 @@ function createNewConsumerMeta(provider, callback) { // this object is what we can get to via the weak map by using the identity as a key const meta = { identity, - callback, - provider, + consumer, value, }; // storing identity into the map IdentityMetaMap.set(identity, meta); + ConsumerMetaMap.set(consumer, meta); return meta; } -function disconnectConsumer(eventTarget, consumerMeta) { - const meta = IdentityMetaMap.get(consumerMeta.identity); - if (meta !== undefined) { - // take care of disconnecting everything for this consumer - // ... - // then remove the identity from the map - IdentityMetaMap.delete(consumerMeta.identity); - } else { - throw new TypeError(`Invalid context operation in ${eventTarget}.`); +function decommissionConsumer(consumer) { + const meta = ConsumerMetaMap.get(consumer); + if (meta === undefined) { + // this should never happen unless you decommission consumers + // manually without waiting for the disconnect to occur. + throw new TypeError(`Invalid context operation.`); } + // take care of disconnecting everything for this consumer + // ... + // then remove the identity and consumer from maps + IdentityMetaMap.delete(meta.identity); + ConsumerMetaMap.delete(consumer.identity); } -function setupNewContextProvider(eventTarget) { - addEventListener.call(eventTarget, UniqueEventName, event => { - // this event must have a full stop when it is intercepted by a provider - event.stopImmediatePropagation(); - // the new child provides a callback as a communication channel - const { detail: callback } = event; - // create consumer metadata as soon as it is connected - const consumerMeta = createNewConsumerMeta(eventTarget, callback); - // emit the identity value and provide disconnect callback - callback(consumerMeta.identity, () => disconnectConsumer(eventTarget, consumerMeta)); - }); -} +const contextualizer = createContextProvider(WireAdapter); -export function installCustomContext(elm) { - setupNewContextProvider(elm); +export function installCustomContext(target) { + // Note: the identity of the consumer is already bound to the target. + contextualizer(target, { + consumerConnectedCallback(consumer) { + // create consumer metadata as soon as it is connected + const consumerMeta = createNewConsumerMeta(target, consumer); + // emit the identity value + consumer.provide({ value: consumerMeta.identity }); + }, + consumerDisconnectedCallback(consumer) { + decommissionConsumer(consumer); + }, + }); } export function setValueForIdentity(identity, value) { @@ -111,5 +106,3 @@ export function getValueForIdentity(identity) { const meta = IdentityMetaMap.get(identity); return meta.value; } - -export { Provider }; diff --git a/packages/integration-karma/test/context/x/simpleConsumer/simpleConsumer.js b/packages/integration-karma/test/context/x/simpleConsumer/simpleConsumer.js index 4541f65eed..93b1bacc70 100644 --- a/packages/integration-karma/test/context/x/simpleConsumer/simpleConsumer.js +++ b/packages/integration-karma/test/context/x/simpleConsumer/simpleConsumer.js @@ -1,6 +1,6 @@ import { LightningElement, wire } from 'lwc'; -import { Provider } from 'x/simpleProvider'; +import { WireAdapter } from 'x/simpleProvider'; export default class ConsumerElement extends LightningElement { - @wire(Provider) context; + @wire(WireAdapter) context; } diff --git a/packages/integration-karma/test/context/x/simpleProvider/simpleProvider.js b/packages/integration-karma/test/context/x/simpleProvider/simpleProvider.js index 785bb1472c..5bfe75e4b3 100644 --- a/packages/integration-karma/test/context/x/simpleProvider/simpleProvider.js +++ b/packages/integration-karma/test/context/x/simpleProvider/simpleProvider.js @@ -6,21 +6,10 @@ * This provide is sharing the same value with every child, and the * identity of the consumer is not tracked. */ - -// Per Context Component Instance, track the current context data -import { register, ValueChangedEvent, LinkContextEvent } from 'wire-service'; - -const { addEventListener } = Document.prototype; +import { createContextProvider } from 'lwc'; const ContextValueMap = new WeakMap(); -const UniqueEventName = `simple_context_event_${guid()}`; -const Provider = Symbol('SimpleContextProvider'); - -function guid() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); -} +const { hasOwnProperty } = Object.prototype; function getDefaultContext() { return 'missing'; @@ -37,39 +26,39 @@ function createContextPayload(value) { return value; } -register(Provider, eventTarget => { - let unsubscribeCallback; - - function callback(value, unsubscribe) { - eventTarget.dispatchEvent(new ValueChangedEvent(createContextPayload(value))); - unsubscribeCallback = unsubscribe; +export class WireAdapter { + contextValue = getDefaultContext(); + constructor(dataCallback) { + this._dataCallback = dataCallback; + // provides the default wired value based on the default context value + this._dataCallback(createContextPayload(this.contextValue)); } - - eventTarget.addEventListener('connect', () => { - const event = new LinkContextEvent(UniqueEventName, callback); - eventTarget.dispatchEvent(event); - if (unsubscribeCallback === undefined) { - // no provider was found, in which case the default - // context should be set. - const defaultContext = getDefaultContext(); - eventTarget.dispatchEvent(new ValueChangedEvent(createContextPayload(defaultContext))); + update(_config, context) { + if (context) { + // we only care about the context, no config is expected or used + if (!hasOwnProperty.call(context, 'value')) { + throw new Error(`Invalid context provided`); + } + this.contextValue = context.value; + this._dataCallback(createContextPayload(this.contextValue)); } - }); - - eventTarget.addEventListener('disconnect', () => { - if (unsubscribeCallback !== undefined) { - unsubscribeCallback(); - unsubscribeCallback = undefined; // resetting it to support reinsertion - } - }); -}); + } + connect() { + // noop + } + disconnect() { + // noop + } + static configSchema = {}; + static contextSchema = { value: 'required' /* could be 'optional' */ }; +} function getContextData(eventTarget) { let contextData = ContextValueMap.get(eventTarget); if (contextData === undefined) { - // collection of consumers' callbacks and default context value per provider instance + // collection of consumers and default context value per provider instance contextData = { - listeners: [], + consumers: [], value: getInitialContext(), // initial value for an installed provider }; ContextValueMap.set(eventTarget, contextData); @@ -77,48 +66,33 @@ function getContextData(eventTarget) { return contextData; } -function disconnectConsumer(eventTarget, contextData, callback) { - const i = contextData.listeners.indexOf(callback); - if (i >= 0) { - contextData.listeners.splice(i, 1); - } else { - throw new TypeError(`Invalid context operation in ${eventTarget}.`); - } -} - -function setupNewContextProvider(eventTarget) { - let contextData; // lazy initialization - addEventListener.call(eventTarget, UniqueEventName, event => { - // this event must have a full stop when it is intercepted by a provider - event.stopImmediatePropagation(); - // the new child provides a callback as a communication channel - const { detail: callback } = event; - // once the first consumer gets connected, then we create the contextData object - if (contextData === undefined) { - contextData = getContextData(eventTarget); - } - // registering the new callback - contextData.listeners.push(callback); - // emit the current value and provide disconnect callback - callback(contextData.value, () => disconnectConsumer(eventTarget, contextData, callback)); +const contextualizer = createContextProvider(WireAdapter); + +export function installCustomContext(target) { + contextualizer(target, { + consumerConnectedCallback(consumer) { + // once the first consumer gets connected, then we create the contextData object + const contextData = getContextData(target); + // registering the new consumer + contextData.consumers.push(consumer); + // push the current value + consumer.provide({ value: contextData.value }); + }, + consumerDisconnectedCallback(consumer) { + const contextData = getContextData(target); + const i = contextData.consumers.indexOf(consumer); + if (i >= 0) { + contextData.consumers.splice(i, 1); + } else { + throw new TypeError(`Invalid context operation in ${target}.`); + } + }, }); } -function emitNewContextValue(eventTarget, newValue) { - const contextData = getContextData(eventTarget); +export function setCustomContext(target, newValue) { + const contextData = getContextData(target); // in this example, all consumers get the same context value contextData.value = newValue; - contextData.listeners.forEach(callback => - callback(newValue, () => disconnectConsumer(eventTarget, contextData, callback)) - ); + contextData.consumers.forEach(consumer => consumer.provide({ value: newValue })); } - -export function installCustomContext(node) { - setupNewContextProvider(node); -} - -export function setCustomContext(node, newValue) { - emitNewContextValue(node, newValue); -} - -export { Provider }; diff --git a/packages/integration-karma/test/wire/property-trap/index.spec.js b/packages/integration-karma/test/wire/property-trap/index.spec.js new file mode 100644 index 0000000000..bfc6a57527 --- /dev/null +++ b/packages/integration-karma/test/wire/property-trap/index.spec.js @@ -0,0 +1,175 @@ +import { createElement } from 'lwc'; + +import EchoAdapterConsumer from 'x/echoAdapterConsumer'; +import { EchoWireAdapter } from 'x/echoAdapter'; + +describe('wire adapter update', () => { + it('should invoke listener with reactive parameter default value', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + document.body.appendChild(elm); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.recordId).toBe('default value'); + }); + }); + + xit('should invoke listener with reactive parameter is an expando and change', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + document.body.appendChild(elm); + + elm.setExpandoValue('expando modified value'); + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.expando).toBe('expando modified value'); + }); + }); + + it('should not invoke update when parameter value is unchanged', () => { + const spy = []; + EchoWireAdapter.setSpy(spy); + const wireKey = { b: { c: { d: 'a.b.c.d value' } } }; + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + + document.body.appendChild(elm); + elm.setWireKeyParameter(wireKey); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(spy.length).toBe(1); + expect(actualWiredValues.data.recordId).toBe('default value'); + elm.setWireKeyParameter(wireKey); + + return Promise.resolve().then(() => { + expect(spy.length).toBe(1); + }); + }); + }); + + it('should invoke listener with getter value', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + document.body.appendChild(elm); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.getterValue).toBe('getterValue'); + }); + }); + + it('should react invoke listener with getter value when dependant value is updated', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + document.body.appendChild(elm); + + elm.setMutatedGetterValue(' mutated'); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.getterValue).toBe('getterValue mutated'); + }); + }); + + // all currently failing + describe('reactivity when vm.isDirty === true', () => { + it('should call update with value set before connected (using observed fields)', () => { + const wireKey = { b: { c: { d: 'expected' } } }; + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + elm.setWireKeyParameter(wireKey); + + document.body.appendChild(elm); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.keyVal).toBe('expected'); + }); + }); + + it('should call update when value set in setter (using @track) that makes dirty the vm', () => { + const wireKey = { b: { c: { d: 'a.b.c.d value' } } }; + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + + document.body.appendChild(elm); + elm.setTrackedPropAndWireKeyParameter(wireKey); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.keyVal).toBe('a.b.c.d value'); + }); + }); + + it('should call update when value set in setter (using @api) that makes dirty the vm', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + // done before connected, so we ensure the component is dirty. + elm.recordId = 'modified Value'; + + document.body.appendChild(elm); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.recordId).toBe('modified Value'); + }); + }); + }); +}); + +describe('reactive parameter', () => { + it('should return value when multiple levels deep', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + document.body.appendChild(elm); + + elm.setWireKeyParameter({ b: { c: { d: 'a.b.c.d value' } } }); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.keyVal).toBe('a.b.c.d value'); + }); + }); + + it('should return object value', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + document.body.appendChild(elm); + + const expected = { e: { f: 'expected' } }; + elm.setWireKeyParameter({ b: { c: { d: expected } } }); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.keyVal).toBe(expected); + }); + }); + + it('should return undefined when root is undefined', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + document.body.appendChild(elm); + + elm.setWireKeyParameter(undefined); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.keyVal).toBe(undefined); + }); + }); + + it('should return undefined when part of the value is not defined', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + document.body.appendChild(elm); + + elm.setWireKeyParameter({ b: undefined }); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.keyVal).toBe(undefined); + }); + }); + + it('should return undefined when a segment is not found', () => { + const elm = createElement('x-echo-adapter-consumer', { is: EchoAdapterConsumer }); + document.body.appendChild(elm); + + elm.setWireKeyParameter({ b: { fooNotC: 'a.b.c.d value' } }); + + return Promise.resolve().then(() => { + const actualWiredValues = elm.getWiredProp(); + expect(actualWiredValues.data.keyVal).toBe(undefined); + }); + }); +}); diff --git a/packages/integration-karma/test/wire/property-trap/x/echoAdapter/echoAdapter.js b/packages/integration-karma/test/wire/property-trap/x/echoAdapter/echoAdapter.js new file mode 100644 index 0000000000..38e8fdce25 --- /dev/null +++ b/packages/integration-karma/test/wire/property-trap/x/echoAdapter/echoAdapter.js @@ -0,0 +1,29 @@ +let adapterSpy; + +export class EchoWireAdapter { + callback; + + static setSpy(spy) { + adapterSpy = spy; + } + + constructor(callback) { + this.callback = callback; + } + + update(config) { + // it passes as value the config + this.log('update', arguments); + this.callback({ data: config, error: undefined }); + } + + connect() {} + + disconnect() {} + + log(method, args) { + if (adapterSpy) { + adapterSpy.push({ method, args }); + } + } +} diff --git a/packages/integration-karma/test/wire/property-trap/x/echoAdapterConsumer/echoAdapterConsumer.html b/packages/integration-karma/test/wire/property-trap/x/echoAdapterConsumer/echoAdapterConsumer.html new file mode 100644 index 0000000000..b1e80b33a8 --- /dev/null +++ b/packages/integration-karma/test/wire/property-trap/x/echoAdapterConsumer/echoAdapterConsumer.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/integration-karma/test/wire/property-trap/x/echoAdapterConsumer/echoAdapterConsumer.js b/packages/integration-karma/test/wire/property-trap/x/echoAdapterConsumer/echoAdapterConsumer.js new file mode 100644 index 0000000000..26300c61f4 --- /dev/null +++ b/packages/integration-karma/test/wire/property-trap/x/echoAdapterConsumer/echoAdapterConsumer.js @@ -0,0 +1,48 @@ +import { LightningElement, wire, api, track } from 'lwc'; +import { EchoWireAdapter } from 'x/echoAdapter'; + +export default class EchoAdapterConsumer extends LightningElement { + @api recordId = 'default value'; + @wire(EchoWireAdapter, { + recordId: '$recordId', + keyVal: '$a.b.c.d', + getterValue: '$getterValue', + expandoValue: '$expando', + }) + wiredProp; + + @track trackedProp; + + a = {}; + mutatedGetterValue = ''; + + @api + getWiredProp() { + return this.wiredProp; + } + + @api + setWireKeyParameter(newValue) { + this.a = newValue; + } + + @api + setTrackedPropAndWireKeyParameter(newValue) { + this.trackedProp = Math.random(); + this.a = newValue; + } + + @api + setMutatedGetterValue(newValue) { + this.mutatedGetterValue = newValue; + } + + @api + setExpandoValue(newValue) { + this.expando = newValue; + } + + get getterValue() { + return 'getterValue' + this.mutatedGetterValue; + } +} diff --git a/packages/integration-karma/test/wire/wirecontextevent-legacy/index.spec.js b/packages/integration-karma/test/wire/wirecontextevent-legacy/index.spec.js new file mode 100644 index 0000000000..c534986247 --- /dev/null +++ b/packages/integration-karma/test/wire/wirecontextevent-legacy/index.spec.js @@ -0,0 +1,18 @@ +import { createElement } from 'lwc'; + +import WireContextProvider from 'x/wireContextProvider'; + +describe('wirecontextevent', () => { + it('should dispatchEvent on custom element when adapter dispatch an event of type wirecontextevent on the wireEventTarget', () => { + const elm = createElement('x-wirecontext-provider', { is: WireContextProvider }); + elm.context = 'test value'; + + document.body.appendChild(elm); + + return Promise.resolve().then(() => { + const consumer = elm.shadowRoot.querySelector('.consumer'); + + expect(consumer.shadowRoot.textContent).toBe('test value'); + }); + }); +}); diff --git a/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextAdapter/wireContextAdapter.js b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextAdapter/wireContextAdapter.js new file mode 100644 index 0000000000..b4a4bf776d --- /dev/null +++ b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextAdapter/wireContextAdapter.js @@ -0,0 +1,20 @@ +import { register, ValueChangedEvent } from 'wire-service'; + +export const wireContextAdapter = () => {}; + +register(wireContextAdapter, wiredEventTarget => { + wiredEventTarget.addEventListener('connect', () => { + const srEvent = new CustomEvent('wirecontextevent', { + bubbles: true, + cancelable: true, + composed: true, + detail: { + callback: contextValue => { + wiredEventTarget.dispatchEvent(new ValueChangedEvent(contextValue)); + }, + }, + }); + + wiredEventTarget.dispatchEvent(srEvent); + }); +}); diff --git a/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextConsumer/wireContextConsumer.html b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextConsumer/wireContextConsumer.html new file mode 100644 index 0000000000..772a25ab50 --- /dev/null +++ b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextConsumer/wireContextConsumer.html @@ -0,0 +1,3 @@ + diff --git a/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextConsumer/wireContextConsumer.js b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextConsumer/wireContextConsumer.js new file mode 100644 index 0000000000..fdc660a46d --- /dev/null +++ b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextConsumer/wireContextConsumer.js @@ -0,0 +1,6 @@ +import { LightningElement, wire } from 'lwc'; +import { wireContextAdapter } from 'x/wireContextAdapter'; + +export default class WireContextConsumer extends LightningElement { + @wire(wireContextAdapter) contextValue; +} diff --git a/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextProvider/wireContextProvider.html b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextProvider/wireContextProvider.html new file mode 100644 index 0000000000..81c0740295 --- /dev/null +++ b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextProvider/wireContextProvider.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextProvider/wireContextProvider.js b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextProvider/wireContextProvider.js new file mode 100644 index 0000000000..cac37bd9ea --- /dev/null +++ b/packages/integration-karma/test/wire/wirecontextevent-legacy/x/wireContextProvider/wireContextProvider.js @@ -0,0 +1,9 @@ +import { LightningElement, api } from 'lwc'; + +export default class WireContextAdapter extends LightningElement { + @api context; + + provideContextValue(contextEvent) { + contextEvent.detail.callback(this.context); + } +} diff --git a/packages/integration-karma/test/wire/wiring/index.spec.js b/packages/integration-karma/test/wire/wiring/index.spec.js new file mode 100644 index 0000000000..b48b1a1f3e --- /dev/null +++ b/packages/integration-karma/test/wire/wiring/index.spec.js @@ -0,0 +1,270 @@ +import { createElement } from 'lwc'; + +import AdapterConsumer from 'x/adapterConsumer'; +import { EchoWireAdapter } from 'x/echoAdapter'; + +import BroadcastConsumer from 'x/broadcastConsumer'; +import { BroadcastAdapter } from 'x/broadcastAdapter'; + +import InheritedMethods from 'x/inheritedMethods'; + +const ComponentClass = AdapterConsumer; +const AdapterId = EchoWireAdapter; + +function filterCalls(echoAdapterSpy, methodType) { + return echoAdapterSpy.filter(call => call.method === methodType); +} + +describe('wiring', () => { + describe('component lifecycle and wire adapter', () => { + it('should call a connect when component is connected', () => { + const spy = []; + const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); + AdapterId.setSpy(spy); + document.body.appendChild(elm); + + expect(filterCalls(spy, 'connect').length).toBe(1); + }); + + it('should call a disconnect when component is disconnected', () => { + const spy = []; + AdapterId.setSpy(spy); + const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); + + document.body.appendChild(elm); + document.body.removeChild(elm); + expect(filterCalls(spy, 'disconnect').length).toBe(1); + }); + + it('should call a connect and disconnect when component is connected, disconnected twice', () => { + const spy = []; + const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); + AdapterId.setSpy(spy); + + document.body.appendChild(elm); + document.body.removeChild(elm); + document.body.appendChild(elm); + document.body.removeChild(elm); + + const connectCalls = filterCalls(spy, 'connect'); + const disconnectCalls = filterCalls(spy, 'disconnect'); + + expect(connectCalls.length).toBe(2); + expect(disconnectCalls.length).toBe(2); + }); + }); + + describe('update method on wire adapter', () => { + 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 }); + + return Promise.resolve().then(() => { + expect(spy.length).toBe(1); + expect(spy[0].method).toBe('update'); + }); + }); + + 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); + + 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(() => { + expect(filterCalls(spy, 'update').length).toBe(1); + elm.forceRerender(); + + return Promise.resolve(); + }) + .then(() => { + expect(filterCalls(spy, 'update').length).toBe(1); + }); + }); + + it('should be called when the wire parameters change its value.', () => { + const spy = []; + AdapterId.setSpy(spy); + const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); + document.body.appendChild(elm); + + 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(); + + expect(wireResult.simpleParam).toBe('simpleParam modified'); + }); + }); + + it('should be called for common parameter when shared among wires', () => { + const spy = []; + AdapterId.setSpy(spy); + const elm = createElement('x-bc-consumer', { is: BroadcastConsumer }); + document.body.appendChild(elm); + + return Promise.resolve().then(() => { + 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'); + }); + }); + }); + + it('should not update when setting parameter with same value', () => { + const spy = []; + const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); + document.body.appendChild(elm); + + AdapterId.setSpy(spy); + const expected = 'expected value'; + elm.setDynamicParamSource(expected); + + return Promise.resolve() + .then(() => { + expect(spy.length).toBe(1); // update,connected + const wireResult = elm.getWiredProp(); + expect(wireResult.simpleParam).toBe(expected); + + elm.setDynamicParamSource(expected); + + return Promise.resolve(); + }) + .then(() => { + expect(spy.length).toBe(1); // update,connected + }); + }); + + it('should trigger component rerender when field is updated', done => { + const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass }); + document.body.appendChild(elm); + + 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(''); + + 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); + }); + }); + }); +}); + +describe('wired fields', () => { + it('should rerender component when adapter pushes data', () => { + BroadcastAdapter.clearInstances(); + const elm = createElement('x-bc-consumer', { is: BroadcastConsumer }); + document.body.appendChild(elm); + BroadcastAdapter.broadcastData('expected value'); + + return Promise.resolve() + .then(() => { + const staticValue = elm.shadowRoot.querySelector('span'); + expect(staticValue.textContent).toBe('expected value'); + BroadcastAdapter.broadcastData('modified value'); + + return Promise.resolve(); + }) + .then(() => { + const staticValue = elm.shadowRoot.querySelector('span'); + expect(staticValue.textContent).toBe('modified value'); + }); + }); +}); + +describe('wired methods', () => { + it('should call component method when wired to a method', () => { + BroadcastAdapter.clearInstances(); + const elm = createElement('x-bc-consumer', { is: BroadcastConsumer }); + document.body.appendChild(elm); + BroadcastAdapter.broadcastData('expected value'); + + return Promise.resolve().then(() => { + const actual = elm.getWiredMethodArgument(); + expect(actual).toBe('expected value'); + }); + }); + + it('should support method override', () => { + const spy = []; + EchoWireAdapter.setSpy(spy); + 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]; + }; + + expect(calls.length).toBe(3); + + expect(getCallByName('overriddenInChild').args[0].child).toBe(true); + expect(getCallByName('childMethod').args[0].child).toBe(true); + expect(getCallByName('parentMethod').args[0].parent).toBe(true); + }); +}); diff --git a/packages/integration-karma/test/wire/wiring/x/adapterConsumer/adapterConsumer.html b/packages/integration-karma/test/wire/wiring/x/adapterConsumer/adapterConsumer.html new file mode 100644 index 0000000000..8e4d12a862 --- /dev/null +++ b/packages/integration-karma/test/wire/wiring/x/adapterConsumer/adapterConsumer.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/integration-karma/test/wire/wiring/x/adapterConsumer/adapterConsumer.js b/packages/integration-karma/test/wire/wiring/x/adapterConsumer/adapterConsumer.js new file mode 100644 index 0000000000..01da3de996 --- /dev/null +++ b/packages/integration-karma/test/wire/wiring/x/adapterConsumer/adapterConsumer.js @@ -0,0 +1,31 @@ +import { LightningElement, wire, api } from 'lwc'; +import { EchoWireAdapter } from 'x/echoAdapter'; + +export default class AdapterConsumer extends LightningElement { + renderId; + simpleParam; + @wire(EchoWireAdapter, { simpleParam: '$simpleParam', staticParam: [1, 2, 3] }) wireConnected; + + @api + getWiredProp() { + return this.wireConnected; + } + + @api + setDynamicParamSource(newValue) { + this.simpleParam = newValue; + } + + @api + forceRerender() { + this.renderId = Math.random() + Math.random(); + } + + get staticParamValue() { + return this.wireConnected.staticParam && this.wireConnected.staticParam.join(','); + } + + get simpleParamValue() { + return this.wireConnected.simpleParam; + } +} diff --git a/packages/integration-karma/test/wire/wiring/x/base/base.js b/packages/integration-karma/test/wire/wiring/x/base/base.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/integration-karma/test/wire/wiring/x/broadcastAdapter/broadcastAdapter.js b/packages/integration-karma/test/wire/wiring/x/broadcastAdapter/broadcastAdapter.js new file mode 100644 index 0000000000..dceb831f88 --- /dev/null +++ b/packages/integration-karma/test/wire/wiring/x/broadcastAdapter/broadcastAdapter.js @@ -0,0 +1,26 @@ +let instances = []; + +export class BroadcastAdapter { + callback; + + static clearInstances() { + instances = []; + } + + static broadcastData(data) { + instances.forEach(instance => { + instance.callback(data); + }); + } + + constructor(callback) { + this.callback = callback; + instances.push(this); + } + + update() {} + + connect() {} + + disconnect() {} +} diff --git a/packages/integration-karma/test/wire/wiring/x/broadcastConsumer/broadcastConsumer.html b/packages/integration-karma/test/wire/wiring/x/broadcastConsumer/broadcastConsumer.html new file mode 100644 index 0000000000..90deceaeaf --- /dev/null +++ b/packages/integration-karma/test/wire/wiring/x/broadcastConsumer/broadcastConsumer.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/integration-karma/test/wire/wiring/x/broadcastConsumer/broadcastConsumer.js b/packages/integration-karma/test/wire/wiring/x/broadcastConsumer/broadcastConsumer.js new file mode 100644 index 0000000000..a213bc490d --- /dev/null +++ b/packages/integration-karma/test/wire/wiring/x/broadcastConsumer/broadcastConsumer.js @@ -0,0 +1,57 @@ +import { LightningElement, wire, api } from 'lwc'; +import { BroadcastAdapter } from 'x/broadcastAdapter'; +import { EchoWireAdapter } from 'x/echoAdapter'; + +export default class BroadcastConsumer extends LightningElement { + @wire(BroadcastAdapter) wiredProp; + + @wire(BroadcastAdapter) + setWirePropertyInMethod(data) { + this.methodArgument = data; + } + + commonParameter; + + @wire(EchoWireAdapter, { id: 'echoWire1', common: '$commonParameter' }) echoWiredProp1; + @wire(EchoWireAdapter, { id: 'echoWire2', common: '$commonParameter' }) echoWiredProp2; + + @api + getEchoWiredProp1() { + return this.echoWiredProp1; + } + + @api + getEchoWiredProp2() { + return this.echoWiredProp2; + } + + @api + setCommonParameter(value) { + this.commonParameter = value; + } + + @api + getWiredProp() { + return this.wiredProp; + } + + @api + getWiredMethodArgument() { + return this.methodArgument; + } + + @api + setWiredPropData(newValue) { + this.wiredProp.data = newValue; + } + + get WiredPropValue() { + const propValue = this.wiredProp || ''; + + if (propValue.data) { + return propValue.data; + } + + return propValue; + } +} diff --git a/packages/integration-karma/test/wire/wiring/x/echoAdapter/echoAdapter.js b/packages/integration-karma/test/wire/wiring/x/echoAdapter/echoAdapter.js new file mode 100644 index 0000000000..d95c13bf36 --- /dev/null +++ b/packages/integration-karma/test/wire/wiring/x/echoAdapter/echoAdapter.js @@ -0,0 +1,34 @@ +let adapterSpy; + +export class EchoWireAdapter { + callback; + + static setSpy(spy) { + adapterSpy = spy; + } + + constructor(callback) { + this.callback = callback; + callback({}); + } + + update(config) { + // it passes as value the config + this.log('update', arguments); + this.callback(config); + } + + connect() { + this.log('connect', arguments); + } + + disconnect() { + this.log('disconnect', arguments); + } + + log(method, args) { + if (adapterSpy) { + adapterSpy.push({ method, args }); + } + } +} diff --git a/packages/integration-karma/test/wire/wiring/x/inheritedMethods/inheritedMethods.html b/packages/integration-karma/test/wire/wiring/x/inheritedMethods/inheritedMethods.html new file mode 100644 index 0000000000..6beff5199f --- /dev/null +++ b/packages/integration-karma/test/wire/wiring/x/inheritedMethods/inheritedMethods.html @@ -0,0 +1,2 @@ + diff --git a/packages/integration-karma/test/wire/wiring/x/inheritedMethods/inheritedMethods.js b/packages/integration-karma/test/wire/wiring/x/inheritedMethods/inheritedMethods.js new file mode 100644 index 0000000000..b80b191427 --- /dev/null +++ b/packages/integration-karma/test/wire/wiring/x/inheritedMethods/inheritedMethods.js @@ -0,0 +1,12 @@ +import { LightningElement, wire } from 'lwc'; +import { EchoWireAdapter } from 'x/echoAdapter'; + +class Base extends LightningElement { + @wire(EchoWireAdapter, { name: 'parentMethod', parent: true }) parentMethod() {} + @wire(EchoWireAdapter, { name: 'overriddenInChild', parent: true }) overriddenInChild() {} +} + +export default class InheritedMethods extends Base { + @wire(EchoWireAdapter, { name: 'childMethod', child: true }) childMethod() {} + @wire(EchoWireAdapter, { name: 'overriddenInChild', child: true }) overriddenInChild() {} +} diff --git a/packages/integration-tests/scripts/build.js b/packages/integration-tests/scripts/build.js index caa726fc15..be23d123b3 100644 --- a/packages/integration-tests/scripts/build.js +++ b/packages/integration-tests/scripts/build.js @@ -39,6 +39,7 @@ const wireServicePath = getModulePath( isProd ? 'prod' : 'dev' ); const todoPath = path.join(require.resolve('../src/shared/todo.js')); +const todoContent = fs.readFileSync(todoPath).toString(); const testSufix = '.test.js'; const testPrefix = 'test-'; @@ -86,6 +87,8 @@ function entryPointResolverPlugin() { resolveId(id) { if (id.includes(testSufix)) { return id; + } else if (id === 'todo') { + return 'todo.js'; } }, load(id) { @@ -94,6 +97,8 @@ function entryPointResolverPlugin() { return testBundle.startsWith('wired-') ? getTodoApp(testBundle) : templates.app(testBundle); + } else if (id === 'todo.js') { + return todoContent; } }, }; @@ -106,7 +111,6 @@ const globalModules = { 'compat-polyfills/polyfills': 'window', lwc: 'LWC', 'wire-service': 'WireService', - todo: 'Todo', }; function createRollupInputConfig() { diff --git a/packages/integration-tests/src/shared/templates.js b/packages/integration-tests/src/shared/templates.js index 74ef187c1a..287d1391a6 100644 --- a/packages/integration-tests/src/shared/templates.js +++ b/packages/integration-tests/src/shared/templates.js @@ -13,45 +13,8 @@ exports.app = function(cmpName) { exports.todoApp = function(cmpName) { return ` - import { registerWireService, register as registerAdapter, ValueChangedEvent } from 'wire-service'; - import { createElement, register } from 'lwc'; + import { createElement } from 'lwc'; import Cmp from 'integration/${cmpName}'; - import { getTodo, getObservable } from 'todo'; - - registerWireService(register); - - // Register the wire adapter for @wire(getTodo). - registerAdapter(getTodo, function getTodoWireAdapter(wiredEventTarget) { - var subscription; - var config; - wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error: undefined })); - var observer = { - next: function(data) { wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: data, error: undefined })); }, - error: function(error) { wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error: error })); } - }; - wiredEventTarget.addEventListener('connect', function() { - var observable = getObservable(config); - if (observable) { - subscription = observable.subscribe(observer); - return; - } - }); - wiredEventTarget.addEventListener('disconnect', function() { - subscription.unsubscribe(); - }); - wiredEventTarget.addEventListener('config', function(newConfig) { - config = newConfig; - if (subscription) { - subscription.unsubscribe(); - subscription = undefined; - } - var observable = getObservable(config); - if (observable) { - subscription = observable.subscribe(observer); - return; - } - }); - }); var element = createElement('integration-${cmpName}', { is: Cmp }); document.body.appendChild(element); diff --git a/packages/integration-tests/src/shared/todo.js b/packages/integration-tests/src/shared/todo.js index 28cbf05b6c..41c158134c 100644 --- a/packages/integration-tests/src/shared/todo.js +++ b/packages/integration-tests/src/shared/todo.js @@ -1,89 +1,116 @@ -(function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' - ? factory(exports) - : typeof define === 'function' && window.define.amd - ? window.define(['exports'], factory) - : factory((global.Todo = {})); -})(this, function(exports) { - 'use strict'; +import { register, ValueChangedEvent } from 'wire-service'; - function getSubject(initialValue, initialError) { - var observer; +function getSubject(initialValue, initialError) { + var observer; - function next(value) { - observer.next(value); - } - - function error(err) { - observer.error(err); - } - - function complete() { - observer.complete(); - } - - var observable = { - subscribe: function(obs) { - observer = obs; - if (initialValue) { - next(initialValue); - } - if (initialError) { - error(initialError); - } - return { - unsubscribe: function() {}, - }; - }, - }; + function next(value) { + observer.next(value); + } - return { - next: next, - error: error, - complete: complete, - observable: observable, - }; + function error(err) { + observer.error(err); } - function generateTodo(id, completed) { - return { - id: id, - title: 'task ' + id, - completed: completed, - }; + function complete() { + observer.complete(); } - var TODO = [ - generateTodo(0, true), - generateTodo(1, false), - // intentionally skip 2 - generateTodo(3, true), - generateTodo(4, true), - // intentionally skip 5 - generateTodo(6, false), - generateTodo(7, false), - ].reduce(function(acc, value) { - acc[value.id] = value; - return acc; - }, {}); + var observable = { + subscribe: function(obs) { + observer = obs; + if (initialValue) { + next(initialValue); + } + if (initialError) { + error(initialError); + } + return { + unsubscribe: function() {}, + }; + }, + }; - function getObservable(config) { - if (!config || !('id' in config)) { - return undefined; - } + return { + next: next, + error: error, + complete: complete, + observable: observable, + }; +} - var todo = TODO[config.id]; - if (!todo) { - var subject = getSubject(undefined, { message: 'Todo not found' }); - return subject.observable; - } +function generateTodo(id, completed) { + return { + id: id, + title: 'task ' + id, + completed: completed, + }; +} - return getSubject(todo).observable; +var TODO = [ + generateTodo(0, true), + generateTodo(1, false), + // intentionally skip 2 + generateTodo(3, true), + generateTodo(4, true), + // intentionally skip 5 + generateTodo(6, false), + generateTodo(7, false), +].reduce(function(acc, value) { + acc[value.id] = value; + return acc; +}, {}); + +function getObservable(config) { + if (!config || !('id' in config)) { + return undefined; + } + + var todo = TODO[config.id]; + if (!todo) { + var subject = getSubject(undefined, { message: 'Todo not found' }); + return subject.observable; } - const getTodo = Symbol('getTodo'); + return getSubject(todo).observable; +} + +const getTodo = function() {}; - exports.getTodo = getTodo; - exports.getObservable = getObservable; - Object.defineProperty(exports, '__esModule', { value: true }); +register(getTodo, function getTodoWireAdapter(wiredEventTarget) { + var subscription; + var config; + wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error: undefined })); + var observer = { + next: function(data) { + wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: data, error: undefined })); + }, + error: function(error) { + wiredEventTarget.dispatchEvent( + new ValueChangedEvent({ data: undefined, error: error }) + ); + }, + }; + wiredEventTarget.addEventListener('connect', function() { + var observable = getObservable(config); + if (observable) { + subscription = observable.subscribe(observer); + } + }); + wiredEventTarget.addEventListener('disconnect', function() { + subscription.unsubscribe(); + }); + wiredEventTarget.addEventListener('config', function(newConfig) { + config = newConfig; + if (subscription) { + subscription.unsubscribe(); + subscription = undefined; + } + var observable = getObservable(config); + if (observable) { + subscription = observable.subscribe(observer); + } + }); }); + +export { getTodo }; +export { getObservable }; From 825e67130717033a16a869be392b0a6f95206aaf Mon Sep 17 00:00:00 2001 From: Jose David Rodriguez Velasco Date: Thu, 9 Apr 2020 20:41:44 -0700 Subject: [PATCH 2/2] fix: with babel upgrade t.isValidES3Identifier of empty string returns true --- .../babel-plugin-component/src/decorators/wire/transform.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 da5f4f9819..680e5104e5 100644 --- a/packages/@lwc/babel-plugin-component/src/decorators/wire/transform.js +++ b/packages/@lwc/babel-plugin-component/src/decorators/wire/transform.js @@ -50,7 +50,8 @@ function getGeneratedConfig(t, wiredValue) { // In such cases where the param does not have proper notation, the config generated will use the bracket // notation to match the current behavior (that most likely end up resolving that param as undefined). const isInvalidMemberExpr = memberExprPaths.some( - maybeIdentifier => !t.isValidES3Identifier(maybeIdentifier) + maybeIdentifier => + !(t.isValidES3Identifier(maybeIdentifier) && maybeIdentifier.length > 0) ); const memberExprPropertyGen = !isInvalidMemberExpr ? t.identifier : t.StringLiteral;