diff --git a/packages/@lwc/babel-plugin-component/src/__tests__/api-decorator.spec.js b/packages/@lwc/babel-plugin-component/src/__tests__/api-decorator.spec.js index a8057c6de7..5952a2cd9e 100644 --- a/packages/@lwc/babel-plugin-component/src/__tests__/api-decorator.spec.js +++ b/packages/@lwc/babel-plugin-component/src/__tests__/api-decorator.spec.js @@ -88,7 +88,8 @@ describe('Transform property', () => { outer: { config: 0 } - } + }, + fields: ["a"] }); export default _registerComponent(Outer, { @@ -333,7 +334,8 @@ describe('Transform property', () => { b: { config: 3 } - } + }, + fields: ["_a", "_b"] }); export default _registerComponent(Test, { @@ -405,7 +407,8 @@ describe('Transform property', () => { config: 3 } }, - publicMethods: ["m1"] + publicMethods: ["m1"], + fields: ["privateProp", "ctor"] }); export default _registerComponent(Text, { diff --git a/packages/@lwc/babel-plugin-component/src/__tests__/observable-fields.spec.js b/packages/@lwc/babel-plugin-component/src/__tests__/observable-fields.spec.js new file mode 100644 index 0000000000..ba62fcbdf6 --- /dev/null +++ b/packages/@lwc/babel-plugin-component/src/__tests__/observable-fields.spec.js @@ -0,0 +1,193 @@ +/* + * 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 pluginTest = require('./utils/test-transform').pluginTest(require('../index')); + +describe('observable fields', () => { + pluginTest( + 'should be added to the registerComponentCall when a field is not decorated with @api, @track or @wire', + ` + import { api, wire, track, createElement } from 'lwc'; + export default class Test { + state; + @track foo; + @track bar; + + @api label; + + record = { + value: 'test' + }; + + @api + someMethod() {} + + @wire(createElement) wiredProp; + } + `, + { + output: { + code: ` + import { registerDecorators as _registerDecorators } from "lwc"; + import _tmpl from "./test.html"; + import { registerComponent as _registerComponent } from "lwc"; + import { createElement } from "lwc"; + + class Test { + constructor() { + this.state = void 0; + this.foo = void 0; + this.bar = void 0; + this.label = void 0; + this.record = { + value: "test" + }; + this.wiredProp = void 0; + } + + someMethod() {} + } + + _registerDecorators(Test, { + publicProps: { + label: { + config: 0 + } + }, + publicMethods: ["someMethod"], + wire: { + wiredProp: { + adapter: createElement + } + }, + track: { + foo: 1, + bar: 1 + }, + fields: ["state", "record"] + }); + + export default _registerComponent(Test, { + tmpl: _tmpl + }); + `, + }, + } + ); + + pluginTest( + 'should transform export default that is not a class', + ` + const DATA_FROM_NETWORK = [ + { + id: '1', + }, + { + id: '2', + }, + ]; + + export default DATA_FROM_NETWORK; + `, + { + output: { + code: ` + import _tmpl from "./test.html"; + import { registerComponent as _registerComponent } from "lwc"; + const DATA_FROM_NETWORK = [ + { + id: "1" + }, + { + id: "2" + } + ]; + export default _registerComponent(DATA_FROM_NETWORK, { + tmpl: _tmpl + }); + `, + }, + } + ); + + pluginTest( + 'should add observed fields in class expression', + ` + import { api, wire, track, createElement } from 'lwc'; + + const Test = class { + state; + @track foo; + @track bar; + + @api label; + + record = { + value: 'test' + }; + + @api + someMethod() {} + + @wire(createElement) wiredProp; + } + + const foo = Test; + + export default foo; + `, + { + output: { + code: ` + import _tmpl from "./test.html"; + import { registerComponent as _registerComponent } from "lwc"; + import { registerDecorators as _registerDecorators } from "lwc"; + import { createElement } from "lwc"; + + const Test = _registerDecorators( + class { + constructor() { + this.state = void 0; + this.foo = void 0; + this.bar = void 0; + this.label = void 0; + this.record = { + value: "test" + }; + this.wiredProp = void 0; + } + + someMethod() {} + }, + { + publicProps: { + label: { + config: 0 + } + }, + publicMethods: ["someMethod"], + wire: { + wiredProp: { + adapter: createElement + } + }, + track: { + foo: 1, + bar: 1 + }, + fields: ["state", "record"] + } + ); + + const foo = Test; + export default _registerComponent(foo, { + tmpl: _tmpl + }); + `, + }, + } + ); +}); diff --git a/packages/@lwc/babel-plugin-component/src/post-process/transform.js b/packages/@lwc/babel-plugin-component/src/post-process/transform.js index 4261b7edb3..224f34b5ba 100644 --- a/packages/@lwc/babel-plugin-component/src/post-process/transform.js +++ b/packages/@lwc/babel-plugin-component/src/post-process/transform.js @@ -33,6 +33,42 @@ module.exports = function postProcess({ types: t }) { return metaPropertyList; } + function collectObservedFields(body, decoratedProperties) { + const mappers = { + ObjectExpression: ({ properties }) => properties.map(({ key: { name } }) => name), + ArrayExpression: ({ elements }) => elements.map(({ value }) => value), + }; + + const decoratedIdentifiers = decoratedProperties + .map(({ value }) => mappers[value.type](value)) + .reduce((acc, identifiers) => acc.concat(identifiers), []); + + const nonDecoratedFields = body + .get('body') + .filter( + path => + t.isClassProperty(path.node) && + !isLWCNode(path.node) && + !(decoratedIdentifiers.indexOf(path.node.key.name) >= 0) + ) + .map(path => path.node.key.name); + + return nonDecoratedFields.length + ? t.objectProperty(t.identifier('fields'), t.valueToNode(nonDecoratedFields)) + : null; + } + + function collectMetaPropertyList(klassBody) { + const metaPropertyList = collectDecoratedProperties(klassBody); + const observableFields = collectObservedFields(klassBody, metaPropertyList); + + if (observableFields) { + metaPropertyList.push(observableFields); + } + + return metaPropertyList; + } + function createRegisterDecoratorsCall(path, klass, props) { const id = moduleImports.addNamed(path, REGISTER_DECORATORS_ID, 'lwc'); @@ -100,8 +136,8 @@ module.exports = function postProcess({ types: t }) { ClassExpression(path) { const { node } = path; if (!node[LWC_POST_PROCCESED]) { - const body = path.get('body'); - const metaPropertyList = collectDecoratedProperties(body); + const metaPropertyList = collectMetaPropertyList(path.get('body')); + if (metaPropertyList.length) { path.replaceWith(createRegisterDecoratorsCall(path, node, metaPropertyList)); } @@ -111,8 +147,7 @@ module.exports = function postProcess({ types: t }) { // Decorator collector for class declarations ClassDeclaration(path) { const { node } = path; - const body = path.get('body'); - const metaPropertyList = collectDecoratedProperties(body); + const metaPropertyList = collectMetaPropertyList(path.get('body')); if (metaPropertyList.length) { const statementPath = path.getStatementParent(); diff --git a/packages/@lwc/compiler/src/__tests__/fixtures/expected-babel-compat.js b/packages/@lwc/compiler/src/__tests__/fixtures/expected-babel-compat.js index 7a1498feea..7ecf384da3 100755 --- a/packages/@lwc/compiler/src/__tests__/fixtures/expected-babel-compat.js +++ b/packages/@lwc/compiler/src/__tests__/fixtures/expected-babel-compat.js @@ -18,6 +18,7 @@ import _classCallCheck from '@babel/runtime/helpers/classCallCheck'; import _possibleConstructorReturn from '@babel/runtime/helpers/possibleConstructorReturn'; import _getPrototypeOf from '@babel/runtime/helpers/getPrototypeOf'; import _inherits from '@babel/runtime/helpers/inherits'; +import { registerDecorators } from 'lwc'; function _templateObject3() { var data = _taggedTemplateLiteral(["wow\naB", " ", ""], ["wow\\naB", " ", ""]); _templateObject3 = function _templateObject3() { @@ -212,4 +213,7 @@ var Bar = function Bar() { _classCallCheck(this, Bar); __setKey(this, "bar", "foo"); }; +registerDecorators(Bar, { + fields: ["bar"] +}); export { Bar, Test, literal, obj1, obj2, t, test }; diff --git a/packages/@lwc/compiler/src/__tests__/fixtures/expected-babel.js b/packages/@lwc/compiler/src/__tests__/fixtures/expected-babel.js index cf08e7073c..6fb4756fa1 100755 --- a/packages/@lwc/compiler/src/__tests__/fixtures/expected-babel.js +++ b/packages/@lwc/compiler/src/__tests__/fixtures/expected-babel.js @@ -1,3 +1,4 @@ +import { registerDecorators } from 'lwc'; function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } // babel-plugin-check-es2015-constants @@ -109,6 +110,8 @@ class Bar { } } - +registerDecorators(Bar, { + fields: ["bar"] +}); export { Bar, Test, literal, obj1, obj2, t, test }; diff --git a/packages/@lwc/compiler/src/__tests__/fixtures/expected-prod-mode.js b/packages/@lwc/compiler/src/__tests__/fixtures/expected-prod-mode.js index f278799310..eb6f79b993 100755 --- a/packages/@lwc/compiler/src/__tests__/fixtures/expected-prod-mode.js +++ b/packages/@lwc/compiler/src/__tests__/fixtures/expected-prod-mode.js @@ -1 +1 @@ -define("x/class_and_template",["lwc"],function(t){function e(t,e,s,n){const{h:a}=t;return[a("section",{key:0},[])]}var s=t.registerTemplate(e);e.stylesheets=[],e.stylesheetTokens={hostAttribute:"x-class_and_template_class_and_template-host",shadowAttribute:"x-class_and_template_class_and_template"};const n=1;return t.registerComponent(class extends t.LightningElement{constructor(){super(),this.t=n,this.counter=0}},{tmpl:s})}); +define("x/class_and_template",["lwc"],function(t){function e(t,e,s,n){const{h:a}=t;return[a("section",{key:0},[])]}var s=t.registerTemplate(e);e.stylesheets=[],e.stylesheetTokens={hostAttribute:"x-class_and_template_class_and_template-host",shadowAttribute:"x-class_and_template_class_and_template"};const n=1;class a extends t.LightningElement{constructor(){super(),this.t=n,this.counter=0}}return t.registerDecorators(a,{fields:["t"]}),t.registerComponent(a,{tmpl:s})}); \ No newline at end of file diff --git a/packages/@lwc/compiler/src/__tests__/fixtures/expected-typescript-extension.js b/packages/@lwc/compiler/src/__tests__/fixtures/expected-typescript-extension.js index d31f565727..7d6ef7e9ad 100644 --- a/packages/@lwc/compiler/src/__tests__/fixtures/expected-typescript-extension.js +++ b/packages/@lwc/compiler/src/__tests__/fixtures/expected-typescript-extension.js @@ -1,4 +1,4 @@ -import { registerTemplate, registerComponent, LightningElement } from 'lwc'; +import { registerTemplate, registerComponent, LightningElement, registerDecorators } from 'lwc'; function tmpl($api, $cmp, $slotset, $ctx) { const { @@ -21,6 +21,9 @@ class ClassAndTemplate extends LightningElement { this.counter = 0; } } +registerDecorators(ClassAndTemplate, { + fields: ["t"] +}); var typescript = registerComponent(ClassAndTemplate, { tmpl: _tmpl }); diff --git a/packages/@lwc/engine/src/framework/decorators/register.ts b/packages/@lwc/engine/src/framework/decorators/register.ts index d84a17f49f..b0be3fe53e 100644 --- a/packages/@lwc/engine/src/framework/decorators/register.ts +++ b/packages/@lwc/engine/src/framework/decorators/register.ts @@ -48,6 +48,7 @@ export interface RegisterDecoratorMeta { readonly publicProps?: PropsDef; readonly track?: TrackDef; readonly wire?: WireHash; + readonly fields?: string[]; } export interface DecoratorMeta { @@ -55,6 +56,7 @@ export interface DecoratorMeta { track: TrackDef; props: PropsDef; methods: MethodDef; + fields: string[] | undefined; } const signedDecoratorToMetaMap: Map = new Map(); @@ -72,11 +74,13 @@ export function registerDecorators( 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; diff --git a/packages/@lwc/engine/src/framework/def.ts b/packages/@lwc/engine/src/framework/def.ts index 38e44e1cbb..0d0197e3c1 100644 --- a/packages/@lwc/engine/src/framework/def.ts +++ b/packages/@lwc/engine/src/framework/def.ts @@ -39,6 +39,7 @@ import { ComponentMeta, getComponentRegisteredMeta, } from './component'; +import { observeFields } from './observable-fields'; import { Template } from './template'; export interface ComponentDef extends DecoratorMeta { @@ -104,11 +105,13 @@ function createComponentDef( 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 proto = Ctor.prototype; @@ -143,6 +146,7 @@ function createComponentDef( template = template || superDef.template; } props = assign(create(null), HTML_PROPS, props); + observeFields(Ctor, fields); if (isUndefined(template)) { // default template @@ -154,6 +158,7 @@ function createComponentDef( name, wire, track, + fields, props, methods, bridge, diff --git a/packages/@lwc/engine/src/framework/observable-fields.ts b/packages/@lwc/engine/src/framework/observable-fields.ts new file mode 100644 index 0000000000..0f9e7a88d2 --- /dev/null +++ b/packages/@lwc/engine/src/framework/observable-fields.ts @@ -0,0 +1,51 @@ +import { ComponentConstructor, ComponentInterface } from './component'; +import { getComponentVM } from './vm'; +import assert from '../shared/assert'; +import { valueMutated, valueObserved } from '../libs/mutation-tracker'; +import { isRendering, vmBeingRendered } from './invoker'; +import { defineProperty, isFalse } from '../shared/language'; + +export function observeFields(Ctor: ComponentConstructor, fields: string[] | undefined) { + if (fields) { + const proto = Ctor.prototype; + + for (let i = 0, len = fields.length; i < len; i += 1) { + const fieldDescriptor = createObservedPropertyDescriptor(fields[i]); + defineProperty(proto, fields[i], fieldDescriptor); + } + } +} + +function createObservedPropertyDescriptor(key: PropertyKey): PropertyDescriptor { + return { + get(this: ComponentInterface): any { + const vm = getComponentVM(this); + if (process.env.NODE_ENV !== 'production') { + assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a valid vm.`); + } + valueObserved(this, key); + return vm.cmpFields[key]; + }, + set(this: ComponentInterface, newValue: any) { + const vm = getComponentVM(this); + if (process.env.NODE_ENV !== 'production') { + assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a valid vm.`); + assert.invariant( + !isRendering, + `${vmBeingRendered}.render() method has side effects on the state of ${vm}.${String( + key + )}` + ); + } + + if (newValue !== vm.cmpFields[key]) { + vm.cmpFields[key] = newValue; + if (isFalse(vm.isDirty)) { + valueMutated(this, key); + } + } + }, + enumerable: true, + configurable: true, + }; +} diff --git a/packages/@lwc/engine/src/framework/vm.ts b/packages/@lwc/engine/src/framework/vm.ts index 5037182df1..5b344f731c 100644 --- a/packages/@lwc/engine/src/framework/vm.ts +++ b/packages/@lwc/engine/src/framework/vm.ts @@ -89,6 +89,7 @@ export interface UninitializedVM { cmpProps: any; cmpSlots: SlotSet; cmpTrack: any; + cmpFields: any; callHook: ( cmp: ComponentInterface | undefined, fn: (...args: any[]) => any, @@ -231,6 +232,7 @@ export function createVM(elm: HTMLElement, Ctor: ComponentConstructor, options: context: create(null), cmpProps: create(null), cmpTrack: create(null), + cmpFields: create(null), cmpSlots: useSyntheticShadow ? create(null) : undefined, callHook, setHook, diff --git a/packages/integration-karma/test/component/decorators/api/x/reactivity/reactivity.js b/packages/integration-karma/test/component/decorators/api/x/reactivity/reactivity.js index 26b32db949..a585d08467 100644 --- a/packages/integration-karma/test/component/decorators/api/x/reactivity/reactivity.js +++ b/packages/integration-karma/test/component/decorators/api/x/reactivity/reactivity.js @@ -1,5 +1,4 @@ import { LightningElement, api } from 'lwc'; -import tmpl from './reactivity.html'; export default class Reactivity extends LightningElement { @api nonReactive; @@ -12,8 +11,7 @@ export default class Reactivity extends LightningElement { return this.renderCount; } - render() { + renderedCallback() { this.renderCount++; - return tmpl; } }