Compare commits

..

8 Commits

Author SHA1 Message Date
Géry Debongnie 276c8a0295 [REL] v2.0.7
# v2.0.7

 - [FIX] compiler: t-key and t-ref together
2023-02-20 09:44:36 +01:00
Lucas Perais 0717fe241d [FIX] compiler: t-key and t-ref together
Have a t-ref on a DOM node with a t-key.
Change the t-key.

Before this commit, the old block removed its element from the component's refs *after*
the new block had been mounted, meaning that in effect, the resulting
ref at the end of the whole patch was null.

After this commit, we only remove an element from the component's ref if that very same
element was indeed the ref. (otherwise it means someone else has changed the ref.)
2023-02-20 09:42:16 +01:00
Géry Debongnie 1291f1f175 [REL] v2.0.6
# v2.0.6

 - [IMP] devtools: provide access to Fiber and RootFiber
 - Bump shelljs and git-rev-sync
 - [FIX] props validation: do not subscribe to props keys
 - [FIX] reactivity: only show key in subscription if observed by callback
 - [FIX] components: stop rendering stale t-component when delayed
2023-02-17 14:31:18 +01:00
Géry Debongnie 6c0a3525c8 [IMP] devtools: provide access to Fiber and RootFiber
The devtools extension needs to hook itself into some internal functions of
owl, and to do that, it needs a reference to Fiber and RootFiber
classes.
2023-02-17 14:20:03 +01:00
dependabot[bot] 13241422e9 Bump shelljs and git-rev-sync
Bumps [shelljs](https://github.com/shelljs/shelljs) to 0.8.5 and updates ancestor dependency [git-rev-sync](https://github.com/kurttheviking/git-rev-sync-js). These dependencies need to be updated together.


Updates `shelljs` from 0.7.7 to 0.8.5
- [Release notes](https://github.com/shelljs/shelljs/releases)
- [Changelog](https://github.com/shelljs/shelljs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/shelljs/shelljs/compare/v0.7.7...v0.8.5)

Updates `git-rev-sync` from 1.12.0 to 3.0.2
- [Release notes](https://github.com/kurttheviking/git-rev-sync-js/releases)
- [Commits](https://github.com/kurttheviking/git-rev-sync-js/compare/v1.12.0...v3.0.2)

---
updated-dependencies:
- dependency-name: shelljs
  dependency-type: indirect
- dependency-name: git-rev-sync
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-17 11:42:38 +01:00
Géry Debongnie 8f2c7f24d7 [FIX] props validation: do not subscribe to props keys
In dev mode, owl inserts an additional props validation step. Before
this commit, the validation would iterate on all key/value pairs of each
props. If the props is reactive, this has the unfortunate side effect of
subscribing the component to each of the keys, even though it should not
be.

This commit avoids the issue by only validating the raw object.
2023-02-09 13:34:07 +01:00
Samuel Degueldre fed9cc467f [FIX] reactivity: only show key in subscription if observed by callback
Previously, the getSubscriptions debugging utility returned all observed
keys for a given target, instead of only the keys observed by the
callback it received as argument. This commit fixes that issues.
2023-02-09 10:13:41 +01:00
Samuel Degueldre 33174b301b [FIX] components: stop rendering stale t-component when delayed
Previously, if the value of a t-component directive changed, but there
was already a scheduled render for the component before that change, the
rendering of the existing component would get delayed (as it should),
but after the parent's rendering was complete, the old component which
will be destroyed during the patch would still get rendered, despite
being stale. This can cause crashes if that component relies on state
that no longer exists.

This was caused by the fact that when rendering, we check the parent
chain for an active render, and also check that the component still
exists in the parent's fiber childrenMap (ie, we know that the upcoming
patch is not about to destroy the component). To do this, we use the
component's parentKey, but in the case of t-component, the parentKey for
both possible components is the same, causing the stale component to
incorrectly believe that it's not about to be destroyed, by finding the
next component in the childrenMap under the same key.

This commit fixes that by prepending the component's name to the
parentKey, meaning that if the value of the t-component changes, so will
the key, and the render will be further delayed until the new state is
patched, at which point the stale component will be destroyed and the
following attempt to render will be cancelled as expected.

The choice to use the component's name is to simplify the
implementation, this means that if using t-component and switching
between components that have the same class name, this protection will
not work. The alternative would be to generate a unique id for the
component class, eg with a WeakMap, but this both complicates the
implementation and adds runtime overhead, for a case that is likely
extremely rare. We are open to refine the implementation should this
problem occur in practice.
2023-02-06 11:11:42 +01:00
23 changed files with 382 additions and 63 deletions
+11 -11
View File
@@ -2481,14 +2481,14 @@
}
},
"git-rev-sync": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/git-rev-sync/-/git-rev-sync-1.12.0.tgz",
"integrity": "sha512-LAeWoK54irAVyq/dHjkq13bYw8vsItGVCgZZbFFJv256DIK+VkLqXBjVvwtTgVWegOy7JVwD+w2uk63x+iLB6g==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/git-rev-sync/-/git-rev-sync-3.0.2.tgz",
"integrity": "sha512-Nd5RiYpyncjLv0j6IONy0lGzAqdRXUaBctuGBbrEA2m6Bn4iDrN/9MeQTXuiquw8AEKL9D2BW0nw5m/lQvxqnQ==",
"dev": true,
"requires": {
"escape-string-regexp": "1.0.5",
"graceful-fs": "4.1.11",
"shelljs": "0.7.7"
"graceful-fs": "4.1.15",
"shelljs": "0.8.5"
},
"dependencies": {
"escape-string-regexp": {
@@ -2498,9 +2498,9 @@
"dev": true
},
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha512-9x6DLUuW+ROFdMTII9ec9t/FK8va6kYcC8/LggumssLM8kNv7IdFl3VrNUqgir2tJuBVxBga1QBoRziZacO5Zg==",
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
"dev": true
}
}
@@ -5017,9 +5017,9 @@
"dev": true
},
"shelljs": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.7.tgz",
"integrity": "sha512-5ZXTlakejjdxXAnFl23pgPDzCcyPoshqMVWYqMH8HiP1R+i4auEKHabljL6XQlhQV58jkSRTR33Fq7OlxyLLTg==",
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
"integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
"dev": true,
"requires": {
"glob": "^7.0.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.0.5",
"version": "2.0.7",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
@@ -48,7 +48,7 @@
"chalk": "^3.0.0",
"current-git-branch": "^1.1.0",
"eslint": "8.31.0",
"git-rev-sync": "^1.12.0",
"git-rev-sync": "^3.0.2",
"github-api": "^3.3.0",
"jest": "^27.1.0",
"jest-diff": "^27.3.1",
+11 -2
View File
@@ -672,8 +672,9 @@ export class CodeGenerator {
this.target.hasRef = true;
const isDynamic = INTERP_REGEXP.test(ast.ref);
if (isDynamic) {
this.helpers.add("singleRefSetter");
const str = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
const idx = block!.insertData(`(el) => refs[${str}] = el`, "ref");
const idx = block!.insertData(`singleRefSetter(refs, ${str})`, "ref");
attrs["block-ref"] = String(idx);
} else {
let name = ast.ref;
@@ -686,7 +687,8 @@ export class CodeGenerator {
info[1] = `multiRefSetter(refs, \`${name}\`)`;
} else {
let id = generateId("ref");
this.target.refInfo[name] = [id, `(el) => refs[\`${name}\`] = el`];
this.helpers.add("singleRefSetter");
this.target.refInfo[name] = [id, `singleRefSetter(refs, \`${name}\`)`];
const index = block!.data.push(id) - 1;
attrs["block-ref"] = String(index);
}
@@ -1223,6 +1225,13 @@ export class CodeGenerator {
})`,
});
if (ast.isDynamic) {
// If the component class changes, this can cause delayed renders to go
// through if the key doesn't change. Use the component name for now.
// This means that two component classes with the same name isn't supported
// in t-component. We can generate a unique id per class later if needed.
keyArg = `(${expr}).name + ${keyArg}`;
}
let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;
if (ast.isDynamic) {
blockExpr = `toggler(${expr}, ${blockExpr})`;
+5 -1
View File
@@ -1,7 +1,7 @@
import { Component, ComponentConstructor, Props } from "./component";
import { ComponentNode } from "./component_node";
import { nodeErrorHandlers, OwlError, handleError } from "./error_handling";
import { Fiber, MountOptions } from "./fibers";
import { Fiber, RootFiber, MountOptions } from "./fibers";
import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers";
import { TemplateSet, TemplateSetConfig } from "./template_set";
@@ -35,12 +35,16 @@ declare global {
interface Window {
__OWL_DEVTOOLS__: {
apps: Set<App>;
Fiber: typeof Fiber;
RootFiber: typeof RootFiber;
};
}
}
window.__OWL_DEVTOOLS__ ||= {
apps: new Set<App>(),
Fiber: Fiber,
RootFiber: RootFiber,
};
export class App<
+9 -4
View File
@@ -162,10 +162,15 @@ export function getSubscriptions(callback: Callback) {
const targets = callbacksToTargets.get(callback) || [];
return [...targets].map((target) => {
const keysToCallbacks = targetToKeysToCallbacks.get(target);
return {
target,
keys: keysToCallbacks ? [...keysToCallbacks.keys()] : [],
};
let keys = [];
if (keysToCallbacks) {
for (const [key, cbs] of keysToCallbacks) {
if (cbs.has(callback)) {
keys.push(key);
}
}
}
return { target, keys };
});
}
// Maps reactive objects to the underlying target
+11
View File
@@ -202,6 +202,16 @@ function multiRefSetter(refs: RefMap, name: string): RefSetter {
};
}
function singleRefSetter(refs: RefMap, name: string): RefSetter {
let _el: HTMLElement | null = null;
return (el) => {
if (el || refs[name] === _el) {
refs[name] = el;
_el = el;
}
};
}
/**
* Validate the component props (or next props) against the (static) props
* description. This is potentially an expensive operation: it may needs to
@@ -260,6 +270,7 @@ export const helpers = {
prepareList,
setContextValue,
multiRefSetter,
singleRefSetter,
shallowEqual,
toNumber,
validateProps,
+2
View File
@@ -1,4 +1,5 @@
import { OwlError } from "./error_handling";
import { toRaw } from "./reactivity";
type BaseType =
| typeof String
@@ -84,6 +85,7 @@ export function validateSchema(obj: { [key: string]: any }, schema: Schema): str
if (Array.isArray(schema)) {
schema = toSchema(schema);
}
obj = toRaw(obj);
let errors = [];
// check if each value in obj has correct shape
for (let key in obj) {
@@ -182,7 +182,7 @@ exports[`misc other complex template 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
let { prepareList, withKey, singleRefSetter } = 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);
@@ -218,8 +218,8 @@ exports[`misc other complex template 1`] = `
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`search_input\`] = el;
const ref2 = (el) => refs[\`settings_menu\`] = el;
const ref1 = singleRefSetter(refs, \`search_input\`);
const ref2 = singleRefSetter(refs, \`settings_menu\`);
let b2,b4,b14,b17,b22,b23,b24,b25;
let attr1 = \`/runbot/\${ctx['project'].slug}\`;
let txt1 = ctx['project'].name;
@@ -4,13 +4,14 @@ exports[`t-ref can get a dynamic ref on a node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div><span block-ref=\\"0\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const v1 = ctx['id'];
let ref1 = (el) => refs[\`myspan\${v1}\`] = el;
let ref1 = singleRefSetter(refs, \`myspan\${v1}\`);
return block1([ref1]);
}
}"
@@ -20,13 +21,14 @@ exports[`t-ref can get a dynamic ref on a node, alternate syntax 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div><span block-ref=\\"0\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const v1 = ctx['id'];
let ref1 = (el) => refs[\`myspan\${v1}\`] = el;
let ref1 = singleRefSetter(refs, \`myspan\${v1}\`);
return block1([ref1]);
}
}"
@@ -36,12 +38,13 @@ exports[`t-ref can get a ref on a node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div><span block-ref=\\"0\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`myspan\`] = el;
const ref1 = singleRefSetter(refs, \`myspan\`);
return block1([ref1]);
}
}"
@@ -66,12 +69,13 @@ exports[`t-ref ref in a t-call 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div>1<span block-ref=\\"0\\"/>2</div>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`name\`] = el;
const ref1 = singleRefSetter(refs, \`name\`);
return block1([ref1]);
}
}"
@@ -81,13 +85,14 @@ exports[`t-ref ref in a t-if 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<span block-ref=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`name\`] = el;
const ref1 = singleRefSetter(refs, \`name\`);
let b2;
if (ctx['condition']) {
b2 = block2([ref1]);
@@ -101,7 +106,7 @@ exports[`t-ref refs in a loop 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
let { prepareList, singleRefSetter, withKey } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<div block-ref=\\"0\\"><block-text-1/></div>\`);
@@ -115,7 +120,7 @@ exports[`t-ref refs in a loop 1`] = `
const key1 = ctx['item'];
const tKey_1 = ctx['item'];
const v1 = ctx['item'];
let ref1 = (el) => refs[(v1)] = el;
let ref1 = singleRefSetter(refs, (v1));
let txt1 = ctx['item'];
c_block2[i1] = withKey(block3([ref1, txt1]), tKey_1 + key1);
}
@@ -129,14 +134,15 @@ exports[`t-ref two refs, one in a t-if 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div><block-child-0/><p block-ref=\\"0\\"/></div>\`);
let block2 = createBlock(\`<span block-ref=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`name\`] = el;
const ref2 = (el) => refs[\`p\`] = el;
const ref1 = singleRefSetter(refs, \`name\`);
const ref2 = singleRefSetter(refs, \`p\`);
let b2;
if (ctx['condition']) {
b2 = block2([ref1]);
@@ -8,7 +8,7 @@ exports[`basics GrandChild display is controlled by its GrandParent 1`] = `
return function template(ctx, node, key = \\"\\") {
const Comp1 = ctx['myComp'];
return toggler(Comp1, comp1({displayGrandChild: ctx['displayGrandChild']}, key + \`__1\`, node, this, Comp1));
return toggler(Comp1, comp1({displayGrandChild: ctx['displayGrandChild']}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
}"
`;
@@ -1213,6 +1213,45 @@ exports[`delayed fiber does not get rendered if it was cancelled 4`] = `
}"
`;
exports[`delayed render does not go through when t-component value changed 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(null, false, false, false, true);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`A\`);
const Comp1 = ctx['state'].component;
const b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
return multi([b2, b3]);
}
}"
`;
exports[`delayed render does not go through when t-component value changed 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`B\`);
const b3 = text(ctx['state'].val);
return multi([b2, b3]);
}
}"
`;
exports[`delayed render does not go through when t-component value changed 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`C\`);
}
}"
`;
exports[`delayed rendering, but then initial rendering is cancelled by yet another render 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1776,7 +1815,7 @@ exports[`t-foreach with dynamic async component 1`] = `
let b3;
if (ctx['arr']) {
const Comp1 = ctx['myComp'];
b3 = toggler(Comp1, comp1({key: ctx['arr'][0]}, key + \`__1__\${key1}\`, node, this, Comp1));
b3 = toggler(Comp1, comp1({key: ctx['arr'][0]}, (Comp1).name + key + \`__1__\${key1}\`, node, this, Comp1));
}
c_block1[i1] = withKey(multi([b3]), key1);
}
@@ -1810,7 +1849,7 @@ exports[`t-key on dom node having a component 1`] = `
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['key'];
const Comp1 = ctx['myComp'];
const b2 = toggler(tKey_1, toggler(Comp1, comp1({key: ctx['key']}, tKey_1 + key + \`__1\`, node, this, Comp1)));
const b2 = toggler(tKey_1, toggler(Comp1, comp1({key: ctx['key']}, (Comp1).name + tKey_1 + key + \`__1\`, node, this, Comp1)));
return toggler(tKey_1, block1([], [b2]));
}
}"
@@ -1836,7 +1875,7 @@ exports[`t-key on dynamic async component (toggler is never patched) 1`] = `
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['key'];
const Comp1 = ctx['myComp'];
return toggler(tKey_1, toggler(Comp1, comp1({key: ctx['key']}, tKey_1 + key + \`__1\`, node, this, Comp1)));
return toggler(tKey_1, toggler(Comp1, comp1({key: ctx['key']}, (Comp1).name + tKey_1 + key + \`__1\`, node, this, Comp1)));
}
}"
`;
@@ -931,7 +931,7 @@ exports[`can catch errors catching error, rethrow, render parent -- a main comp
function slot1(ctx, node, key = \\"\\") {
const Comp1 = ctx['cp'].Comp;
return toggler(Comp1, comp1({}, key + \`__1\`, node, this, Comp1));
return toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
return function template(ctx, node, key = \\"\\") {
@@ -1010,7 +1010,7 @@ exports[`can catch errors catching in child makes parent render 1`] = `
function slot1(ctx, node, key = \\"\\") {
const Comp1 = ctx['elem'][1];
return toggler(Comp1, comp1({id: ctx['elem'][0]}, key + \`__1\`, node, this, Comp1));
return toggler(Comp1, comp1({id: ctx['elem'][0]}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
return function template(ctx, node, key = \\"\\") {
@@ -4,14 +4,15 @@ exports[`hooks autofocus hook input in a t-if 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div><input block-ref=\\"0\\"/><block-child-0/></div>\`);
let block2 = createBlock(\`<input block-ref=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`input1\`] = el;
const ref2 = (el) => refs[\`input2\`] = el;
const ref1 = singleRefSetter(refs, \`input1\`);
const ref2 = singleRefSetter(refs, \`input2\`);
let b2;
if (ctx['state'].flag) {
b2 = block2([ref2]);
@@ -25,13 +26,14 @@ exports[`hooks autofocus hook simple input 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div><input block-ref=\\"0\\"/><input block-ref=\\"1\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`input1\`] = el;
const ref2 = (el) => refs[\`input2\`] = el;
const ref1 = singleRefSetter(refs, \`input1\`);
const ref2 = singleRefSetter(refs, \`input2\`);
return block1([ref1, ref2]);
}
}"
@@ -264,12 +266,13 @@ exports[`hooks useEffect hook effect can depend on stuff in dom 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block2 = createBlock(\`<div block-ref=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`div\`] = el;
const ref1 = singleRefSetter(refs, \`div\`);
let b2;
if (ctx['state'].value) {
b2 = block2([ref1]);
@@ -353,12 +356,13 @@ exports[`hooks useRef hook: basic use 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div><button block-ref=\\"0\\"><block-text-1/></button></div>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`button\`] = el;
const ref1 = singleRefSetter(refs, \`button\`);
let txt1 = ctx['value'];
return block1([ref1, txt1]);
}
@@ -841,6 +841,33 @@ exports[`props validation props are validated whenever component is updated 2`]
}"
`;
exports[`props validation props validation does not cause additional subscription 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, false);
return function template(ctx, node, key = \\"\\") {
const props1 = {obj: ctx['obj']};
helpers.validateProps(\`Child\`, props1, this);
const b2 = comp1(props1, key + \`__1\`, node, this, null);
const b3 = text(ctx['obj'].otherValue);
return multi([b2, b3]);
}
}"
`;
exports[`props validation props validation does not cause additional subscription 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].obj.value);
}
}"
`;
exports[`props validation props: list of strings 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -140,3 +140,39 @@ exports[`reactivity in lifecycle state changes in willUnmount do not trigger rer
}
}"
`;
exports[`subscriptions subscriptions returns the keys and targets observed by the component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].a);
}
}"
`;
exports[`subscriptions subscriptions returns the keys observed by the component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, false);
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['state'].a);
const b3 = comp1({state: ctx['state']}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
}"
`;
exports[`subscriptions subscriptions returns the keys observed by the component 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].state.b);
}
}"
`;
@@ -4,12 +4,13 @@ exports[`refs basic use 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block1 = createBlock(\`<div block-ref=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`div\`] = el;
const ref1 = singleRefSetter(refs, \`div\`);
return block1([ref1]);
}
}"
@@ -19,7 +20,7 @@ exports[`refs can use 2 refs with same name in a t-if/t-else situation 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { multiRefSetter } = helpers;
let { singleRefSetter, multiRefSetter } = helpers;
let block2 = createBlock(\`<div block-ref=\\"0\\"/>\`);
let block3 = createBlock(\`<span block-ref=\\"0\\"/>\`);
@@ -42,13 +43,14 @@ exports[`refs refs and recursive templates 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
const comp1 = app.createComponent(\`Test\`, true, false, false, false);
let block1 = createBlock(\`<p block-ref=\\"0\\"><block-text-1/><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`root\`] = el;
const ref1 = singleRefSetter(refs, \`root\`);
let b2;
let txt1 = ctx['props'].tree.value;
if (ctx['props'].tree.child) {
@@ -59,11 +61,33 @@ exports[`refs refs and recursive templates 1`] = `
}"
`;
exports[`refs refs and t-key 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { singleRefSetter } = helpers;
let block2 = createBlock(\`<button block-handler-0=\\"click\\"/>\`);
let block3 = createBlock(\`<p block-ref=\\"0\\"/>\`);
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = singleRefSetter(refs, \`root\`);
const v1 = ctx['state'];
let hdlr1 = [()=>v1.renderId++, ctx];
const b2 = block2([hdlr1]);
const tKey_1 = ctx['state'].renderId;
const b3 = toggler(tKey_1, block3([ref1]));
return multi([b2, b3]);
}
}"
`;
exports[`refs refs are properly bound in slots 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { capture, markRaw } = helpers;
let { capture, singleRefSetter, markRaw } = helpers;
const comp1 = app.createComponent(\`Dialog\`, true, true, false, true);
let block1 = createBlock(\`<div><span class=\\"counter\\"><block-text-0/></span><block-child-0/></div>\`);
@@ -71,7 +95,7 @@ exports[`refs refs are properly bound in slots 1`] = `
function slot1(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`myButton\`] = el;
const ref1 = singleRefSetter(refs, \`myButton\`);
let hdlr1 = [ctx['doSomething'], ctx];
return block2([hdlr1, ref1]);
}
@@ -104,7 +128,7 @@ exports[`refs throws if there are 2 same refs at the same time 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { multiRefSetter } = helpers;
let { singleRefSetter, multiRefSetter } = helpers;
let block2 = createBlock(\`<div block-ref=\\"0\\"/>\`);
let block3 = createBlock(\`<span block-ref=\\"0\\"/>\`);
@@ -158,7 +158,7 @@ exports[`slots can render node with t-ref and Component in same slot 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { markRaw } = helpers;
let { singleRefSetter, markRaw } = helpers;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp2 = app.createComponent(\`Child\`, true, true, false, true);
@@ -166,7 +166,7 @@ exports[`slots can render node with t-ref and Component in same slot 1`] = `
function slot1(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`div\`] = el;
const ref1 = singleRefSetter(refs, \`div\`);
const b2 = block2([ref1]);
const b3 = comp1({}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
@@ -534,7 +534,7 @@ exports[`t-call t-call-context: ComponentNode is not looked up in the context 2`
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { bind, capture, isBoundary, withDefault, setContextValue, markRaw } = helpers;
let { singleRefSetter, bind, capture, isBoundary, withDefault, setContextValue, markRaw } = helpers;
const comp1 = app.createComponent(\`Child\`, true, true, false, false);
let block2 = createBlock(\`<div block-ref=\\"0\\">outside slot</div>\`);
@@ -543,7 +543,7 @@ exports[`t-call t-call-context: ComponentNode is not looked up in the context 2`
function slot1(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref2 = (el) => refs[\`myRef2\`] = el;
const ref2 = singleRefSetter(refs, \`myRef2\`);
ctx = Object.create(ctx);
ctx[isBoundary] = 1
const b4 = block4([ref2]);
@@ -555,7 +555,7 @@ exports[`t-call t-call-context: ComponentNode is not looked up in the context 2`
return function template(ctx, node, key = \\"\\") {
const refs = this.__owl__.refs;
const ref1 = (el) => refs[\`myRef\`] = el;
const ref1 = singleRefSetter(refs, \`myRef\`);
const b2 = block2([ref1]);
const ctx1 = capture(ctx);
const b6 = comp1({prop: bind(this, ctx['method']),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__1\`, node, this, null);
@@ -10,7 +10,7 @@ exports[`t-component can switch between dynamic components without the need for
return function template(ctx, node, key = \\"\\") {
const Comp1 = ctx['constructor'].components[ctx['state'].child];
const b2 = toggler(Comp1, comp1({}, key + \`__1\`, node, this, Comp1));
const b2 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
return block1([], [b2]);
}
}"
@@ -51,7 +51,7 @@ exports[`t-component can use dynamic components (the class) if given (with diffe
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['state'].child;
const Comp1 = ctx['myComponent'];
return toggler(tKey_1, toggler(Comp1, comp1({}, tKey_1 + key + \`__1\`, node, this, Comp1)));
return toggler(tKey_1, toggler(Comp1, comp1({}, (Comp1).name + tKey_1 + key + \`__1\`, node, this, Comp1)));
}
}"
`;
@@ -91,7 +91,7 @@ exports[`t-component can use dynamic components (the class) if given 1`] = `
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['state'].child;
const Comp1 = ctx['myComponent'];
return toggler(tKey_1, toggler(Comp1, comp1({}, tKey_1 + key + \`__1\`, node, this, Comp1)));
return toggler(tKey_1, toggler(Comp1, comp1({}, (Comp1).name + tKey_1 + key + \`__1\`, node, this, Comp1)));
}
}"
`;
@@ -132,7 +132,7 @@ exports[`t-component modifying a sub widget 1`] = `
return function template(ctx, node, key = \\"\\") {
const Comp1 = ctx['Counter'];
const b2 = toggler(Comp1, comp1({}, key + \`__1\`, node, this, Comp1));
const b2 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
return block1([], [b2]);
}
}"
@@ -162,7 +162,7 @@ exports[`t-component switching dynamic component 1`] = `
return function template(ctx, node, key = \\"\\") {
const Comp1 = ctx['Child'];
return toggler(Comp1, comp1({}, key + \`__1\`, node, this, Comp1));
return toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
}"
`;
@@ -199,7 +199,7 @@ exports[`t-component t-component works in simple case 1`] = `
return function template(ctx, node, key = \\"\\") {
const Comp1 = ctx['Child'];
return toggler(Comp1, comp1({}, key + \`__1\`, node, this, Comp1));
return toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
}"
`;
+59
View File
@@ -1,6 +1,7 @@
import {
App,
Component,
ComponentConstructor,
mount,
onMounted,
onRendered,
@@ -4067,6 +4068,64 @@ test("renderings, destruction, patch, stuff, ... yet another variation", async (
expect(fixture.innerHTML).toBe("ABD<p>2</p>");
});
test("delayed render does not go through when t-component value changed", async () => {
class C extends Component {
static template = xml`C`;
setup() {
useLogLifecycle("", true);
}
}
class B extends Component {
static template = xml`B<t t-esc="state.val"/>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle("", true);
b = this;
}
}
let b: B;
class A extends Component {
static template = xml`A<t t-component="state.component"/>`;
state: { component: ComponentConstructor } = useState({ component: B });
setup() {
useLogLifecycle("", true);
}
}
const a = await mount(A, fixture);
expect(fixture.innerHTML).toBe("AB1");
expect([
"A:setup",
"A:willRender",
"B:setup",
"A:rendered",
"B:willRender",
"B:rendered",
"B:mounted",
"A:mounted",
]).toBeLogged();
// start a render in B
b!.state.val = 2;
// start a render in A, invalidating the scheduled render of B, which could crash if executed.
a.state.component = C;
await nextTick();
expect(fixture.innerHTML).toBe("AC");
expect([
"A:willRender",
"C:setup",
"A:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"B:willUnmount",
"B:willDestroy",
"C:mounted",
"A:patched",
]).toBeLogged();
});
// test.skip("components with shouldUpdate=false", async () => {
// const state = { p: 1, cc: 10 };
+24 -1
View File
@@ -1,5 +1,5 @@
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
import { Component, onError, xml, mount, OwlError } from "../../src";
import { Component, onError, xml, mount, OwlError, useState } from "../../src";
import { App, DEV_MSG } from "../../src/runtime/app";
import { validateProps } from "../../src/runtime/template_helpers";
import { Schema } from "../../src/runtime/validation";
@@ -682,6 +682,29 @@ describe("props validation", () => {
expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is missing");
});
test("props validation does not cause additional subscription", async () => {
let obj = {
value: 1,
otherValue: 2,
};
class Child extends Component {
static props = {
obj: { type: Object, shape: { value: Number, otherValue: Number } },
};
static template = xml`<t t-esc="props.obj.value"/>`;
}
class Parent extends Component {
static template = xml`<Child obj="obj"/><t t-esc="obj.otherValue"/>`;
static components = { Child };
obj = useState(obj);
}
const app = new App(Parent, { test: true });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("12");
expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
});
test("props are validated whenever component is updated", async () => {
let error: Error;
class SubComp extends Component {
+32
View File
@@ -7,6 +7,7 @@ import {
onWillUnmount,
useState,
xml,
toRaw,
} from "../../src";
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
@@ -232,3 +233,34 @@ describe("reactivity in lifecycle", () => {
expect(fixture.innerHTML).toBe("34");
});
});
describe("subscriptions", () => {
test("subscriptions returns the keys and targets observed by the component", async () => {
class Comp extends Component {
static template = xml`<t t-esc="state.a"/>`;
state = useState({ a: 1, b: 2 });
}
const comp = await mount(Comp, fixture);
expect(fixture.innerHTML).toBe("1");
expect(comp.__owl__.subscriptions).toEqual([{ keys: ["a"], target: toRaw(comp.state) }]);
});
test("subscriptions returns the keys observed by the component", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.state.b"/>`;
setup() {
child = this;
}
}
let child: Child;
class Parent extends Component {
static template = xml`<t t-esc="state.a"/><Child state="state"/>`;
static components = { Child };
state = useState({ a: 1, b: 2 });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("12");
expect(parent.__owl__.subscriptions).toEqual([{ keys: ["a"], target: toRaw(parent.state) }]);
expect(child!.__owl__.subscriptions).toEqual([{ keys: ["b"], target: toRaw(parent.state) }]);
});
});
+39 -1
View File
@@ -1,4 +1,13 @@
import { App, Component, mount, onMounted, useRef, useState, xml } from "../../src/index";
import {
App,
Component,
mount,
onMounted,
onPatched,
useRef,
useState,
xml,
} from "../../src/index";
import { logStep, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
snapshotEverything();
@@ -127,4 +136,33 @@ describe("refs", () => {
expect(fixture.innerHTML).toBe("<p>a<p>b</p></p>");
expect(["<p>b</p>", "<p>a<p>b</p></p>"]).toBeLogged();
});
test("refs and t-key", async () => {
let el;
class Test extends Component {
static components = {};
static template = xml`
<button t-on-click="() => state.renderId++" />
<p t-ref="root" t-key="state.renderId"/>`;
root = useRef("root");
state = useState({ renderId: 1 });
setup() {
onMounted(() => {
el = this.root.el;
});
onPatched(() => {
el = this.root.el;
});
}
}
await mount(Test, fixture);
expect(el).toBe(fixture.querySelector("p"));
const _el = el;
fixture.querySelector("button")!.click();
await nextTick();
expect(el).not.toBe(_el);
expect(el).toBe(fixture.querySelector("p"));
});
});