diff --git a/doc/reference/props.md b/doc/reference/props.md index 4fcfbd9b..7d673339 100644 --- a/doc/reference/props.md +++ b/doc/reference/props.md @@ -4,6 +4,7 @@ - [Overview](#overview) - [Definition](#definition) +- [Props comparison](#props-comparison) - [Binding function props](#binding-function-props) - [Dynamic Props](#dynamic-props) - [Default Props](#default-props) @@ -56,6 +57,47 @@ the `props` object contains the following keys: - for `ComponentA`: `a` and `b`, - for `ComponentB`: `model`, +## Props comparison + +Whenever Owl encounters a subcomponent in a template, it performs a shallow +comparison of all props. If they are all referentially equal, then the subcomponent +will not even be updated. Otherwise, if at least one props has changed, then +Owl will update it. + +However, in some cases, we know that two values are different, but they have the +same effect, and should not be considered different by Owl. For example, anonymous +functions in a template are always different, but most of them should not be +considered different: + +```xml + + + +``` + +In that case, one can use the `.alike` suffix: + +```xml + + + +``` + +This tells Owl that this specific prop should always be considered equivalent +(or, in other words, should be removed from the list of comparable props). + +Note that even if most anonymous functions should probably be considered `alike`, +it is not necessarily true in all cases. It depends on what values are captured +by the anonymous function. The following example shows a case where it is probably +wrong to use `.alike`. + +```xml + + + + +``` + ## Binding function props It is common to have the need to pass a callback as a prop. Since Owl components @@ -95,6 +137,9 @@ class SomeComponent extends Component { } ``` +The `.bind` suffix also implies `.alike`, so these props will not cause additional +renderings. + ## Dynamic Props The `t-props` directive can be used to specify totally dynamic props: diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 626b8e26..18b99314 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -1132,12 +1132,15 @@ export class CodeGenerator { value = this.captureExpression(value); if (name.includes(".")) { let [_name, suffix] = name.split("."); - if (suffix === "bind") { - this.helpers.add("bind"); - name = _name; - value = `bind(this, ${value || undefined})`; - } else { - throw new OwlError("Invalid prop suffix"); + name = _name; + switch (suffix) { + case "bind": + value = `${value}.bind(this)`; + break; + case "alike": + break; + default: + throw new OwlError("Invalid prop suffix"); } } name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`; @@ -1237,13 +1240,18 @@ export class CodeGenerator { keyArg = `${ctx.tKeyExpr} + ${keyArg}`; } let id = generateId("comp"); + const propList: string[] = []; + for (let p in ast.props || {}) { + let [name, suffix] = p.split("."); + if (!suffix) { + propList.push(`"${name}"`); + } + } this.staticDefs.push({ id, expr: `app.createComponent(${ ast.isDynamic ? null : expr - }, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, ${ - !ast.props && !ast.dynamicProps - })`, + }, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`, }); if (ast.isDynamic) { diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 1b4b1946..caca43f5 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -139,22 +139,35 @@ export class App< isStatic: boolean, hasSlotsProp: boolean, hasDynamicPropList: boolean, - hasNoProp: boolean + propList: string[] ) { const isDynamic = !isStatic; - function _arePropsDifferent(props1: Props, props2: Props): boolean { - for (let k in props1) { - if (props1[k] !== props2[k]) { - return true; + let arePropsDifferent: (p1: Object, p2: Object) => boolean; + const hasNoProp = propList.length === 0; + if (hasSlotsProp) { + arePropsDifferent = (_1, _2) => true; + } else if (hasDynamicPropList) { + arePropsDifferent = function (props1: Props, props2: Props) { + for (let k in props1) { + if (props1[k] !== props2[k]) { + return true; + } } - } - return hasDynamicPropList && Object.keys(props1).length !== Object.keys(props2).length; + return Object.keys(props1).length !== Object.keys(props2).length; + }; + } else if (hasNoProp) { + arePropsDifferent = (_1: any, _2: any) => false; + } else { + arePropsDifferent = function (props1: Props, props2: Props) { + for (let p of propList) { + if (props1[p] !== props2[p]) { + return true; + } + } + return false; + }; } - const arePropsDifferent = hasSlotsProp - ? (_1: any, _2: any) => true - : hasNoProp - ? (_1: any, _2: any) => false - : _arePropsDifferent; + const updateAndRender = ComponentNode.prototype.updateAndRender; const initiateRender = ComponentNode.prototype.initiateRender; diff --git a/src/runtime/template_helpers.ts b/src/runtime/template_helpers.ts index f472a6d5..e786f223 100644 --- a/src/runtime/template_helpers.ts +++ b/src/runtime/template_helpers.ts @@ -167,24 +167,6 @@ export function safeOutput(value: any, defaultValue?: any): ReturnType\`); @@ -82,7 +82,7 @@ exports[`Reactivity: useState destroyed component is inactive 1`] = ` "function anonymous(app, bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; - const comp1 = app.createComponent(\`Child\`, true, false, false, true); + const comp1 = app.createComponent(\`Child\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -129,7 +129,7 @@ exports[`Reactivity: useState parent and children subscribed to same context 1`] "function anonymous(app, bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; - const comp1 = app.createComponent(\`Child\`, true, false, false, true); + const comp1 = app.createComponent(\`Child\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -222,8 +222,8 @@ exports[`Reactivity: useState two components are updated in parallel 1`] = ` "function anonymous(app, bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; - const comp1 = app.createComponent(\`Child\`, true, false, false, true); - const comp2 = app.createComponent(\`Child\`, true, false, false, true); + const comp1 = app.createComponent(\`Child\`, true, false, false, []); + const comp2 = app.createComponent(\`Child\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -253,8 +253,8 @@ exports[`Reactivity: useState two components can subscribe to same context 1`] = "function anonymous(app, bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; - const comp1 = app.createComponent(\`Child\`, true, false, false, true); - const comp2 = app.createComponent(\`Child\`, true, false, false, true); + const comp1 = app.createComponent(\`Child\`, true, false, false, []); + const comp2 = app.createComponent(\`Child\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -284,8 +284,8 @@ exports[`Reactivity: useState two independent components on different levels are "function anonymous(app, bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; - const comp1 = app.createComponent(\`Child\`, true, false, false, true); - const comp2 = app.createComponent(\`Parent\`, true, false, false, true); + const comp1 = app.createComponent(\`Child\`, true, false, false, []); + const comp2 = app.createComponent(\`Parent\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -315,7 +315,7 @@ exports[`Reactivity: useState two independent components on different levels are "function anonymous(app, bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; - const comp1 = app.createComponent(\`Child\`, true, false, false, true); + const comp1 = app.createComponent(\`Child\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -345,7 +345,7 @@ exports[`Reactivity: useState useless atoms should be deleted 1`] = ` ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { prepareList, withKey } = helpers; - const comp1 = app.createComponent(\`Quantity\`, true, false, false, false); + const comp1 = app.createComponent(\`Quantity\`, true, false, false, [\\"id\\"]); let block1 = createBlock(\`
Total: Count:
\`); diff --git a/tests/compiler/__snapshots__/misc.test.ts.snap b/tests/compiler/__snapshots__/misc.test.ts.snap index 2d3e8f81..2d4aa3bf 100644 --- a/tests/compiler/__snapshots__/misc.test.ts.snap +++ b/tests/compiler/__snapshots__/misc.test.ts.snap @@ -5,7 +5,7 @@ exports[`misc complex template 1`] = ` ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { prepareList, withKey } = helpers; - const comp1 = app.createComponent(\`SlotButton\`, true, false, false, false); + const comp1 = app.createComponent(\`SlotButton\`, true, false, false, [\\"class\\",\\"slot\\"]); let block1 = createBlock(\`
\`); let block2 = createBlock(\`\`); @@ -184,8 +184,8 @@ exports[`misc other complex template 1`] = ` let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { prepareList, withKey } = helpers; const callTemplate_1 = app.getTemplate(\`LOAD_INFOS_TEMPLATE\`); - const comp1 = app.createComponent(\`BundlesList\`, true, false, false, false); - const comp2 = app.createComponent(\`BundlesList\`, true, false, false, false); + const comp1 = app.createComponent(\`BundlesList\`, true, false, false, [\\"bundles\\",\\"category_custom_views\\",\\"search\\"]); + const comp2 = app.createComponent(\`BundlesList\`, true, false, false, [\\"bundles\\",\\"search\\"]); let block1 = createBlock(\`