Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] fc3ed3c0b5 Bump tough-cookie from 4.1.2 to 4.1.3
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 01:15:54 +00:00
27 changed files with 181 additions and 746 deletions
-1
View File
@@ -451,6 +451,5 @@ console.log(status(component));
// logs either: // logs either:
// - 'new', if the component is new and has not been mounted yet // - 'new', if the component is new and has not been mounted yet
// - 'mounted', if the component is currently mounted // - 'mounted', if the component is currently mounted
// - 'cancelled', if the component has not been mounted yet but will be destroyed soon
// - 'destroyed' if the component is currently destroyed // - 'destroyed' if the component is currently destroyed
``` ```
+7 -8
View File
@@ -376,16 +376,15 @@ An important difference should be made with the usual `QWeb` behaviour: Owl
requires the presence of a `t-key` directive, to be able to properly reconcile requires the presence of a `t-key` directive, to be able to properly reconcile
renderings. renderings.
`t-foreach` can iterate on any iterable, and also has special support for objects `t-foreach` can iterate on an array (the current item will be the current value)
and maps, it will expose the key of the current iteration as the contents of the or an object (the current item will be the current key).
`t-as`, and the corresponding value with the same name and the suffix `_value`.
In addition to the name passed via t-as, `t-foreach` provides a few other useful In addition to the name passed via t-as, `t-foreach` provides a few other
variables (note: `$as` will be replaced with the name passed to `t-as`): variables for various data points (note: `$as` will be replaced with the name
passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for arrays and - `$as_value`: the current iteration value, identical to `$as` for lists and
other iterables, but for objects and maps, it provides the value (where `$as` integers, but for objects, it provides the value (where `$as` provides the key)
provides the key)
- `$as_index`: the current iteration index (the first item of the iteration has index 0) - `$as_index`: the current iteration index (the first item of the iteration has index 0)
- `$as_first`: whether the current item is the first of the iteration - `$as_first`: whether the current item is the first of the iteration
(equivalent to `$as_index == 0`) (equivalent to `$as_index == 0`)
+46 -78
View File
@@ -122,7 +122,6 @@ function handleError(params) {
} }
const node = "node" in params ? params.node : params.fiber.node; const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber; const fiber = "fiber" in params ? params.fiber : node.fiber;
if (fiber) {
// resets the fibers on components if possible. This is important so that // resets the fibers on components if possible. This is important so that
// new renderings can be properly included in the initial one, if any. // new renderings can be properly included in the initial one, if any.
let current = fiber; let current = fiber;
@@ -131,7 +130,6 @@ function handleError(params) {
current = current.parent; current = current.parent;
} while (current); } while (current);
fibersInError.set(fiber.root, error); fibersInError.set(fiber.root, error);
}
const handled = _handleError(node, error); const handled = _handleError(node, error);
if (!handled) { if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`); console.warn(`[Owl] Unhandled error. Destroying the root component`);
@@ -313,13 +311,20 @@ function updateClass(val, oldVal) {
* @returns a batched version of the original callback * @returns a batched version of the original callback
*/ */
function batched(callback) { function batched(callback) {
let scheduled = false; let called = false;
return async (...args) => { return async () => {
if (!scheduled) { // This await blocks all calls to the callback here, then releases them sequentially
scheduled = true; // in the next microtick. This line decides the granularity of the batch.
await Promise.resolve(); await Promise.resolve();
scheduled = false; if (!called) {
callback(...args); called = true;
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback.
// Schedule this before calling the callback so that calls to the batched function
// within the callback will proceed only after resetting called to false, and have
// a chance to execute the callback again
Promise.resolve().then(() => (called = false));
callback();
} }
}; };
} }
@@ -1652,7 +1657,8 @@ function cancelFibers(fibers) {
let node = fiber.node; let node = fiber.node;
fiber.render = throwOnRender; fiber.render = throwOnRender;
if (node.status === 0 /* NEW */) { if (node.status === 0 /* NEW */) {
node.cancel(); node.destroy();
delete node.parent.children[node.parentKey];
} }
node.fiber = null; node.fiber = null;
if (fiber.bdom) { if (fiber.bdom) {
@@ -2379,9 +2385,6 @@ class ComponentNode {
} }
} }
async render(deep) { async render(deep) {
if (this.status >= 2 /* CANCELLED */) {
return;
}
let current = this.fiber; let current = this.fiber;
if (current && (current.root.locked || current.bdom === true)) { if (current && (current.root.locked || current.bdom === true)) {
await Promise.resolve(); await Promise.resolve();
@@ -2407,7 +2410,7 @@ class ComponentNode {
this.fiber = fiber; this.fiber = fiber;
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
await Promise.resolve(); await Promise.resolve();
if (this.status >= 2 /* CANCELLED */) { if (this.status === 2 /* DESTROYED */) {
return; return;
} }
// We only want to actually render the component if the following two // We only want to actually render the component if the following two
@@ -2425,18 +2428,6 @@ class ComponentNode {
fiber.render(); fiber.render();
} }
} }
cancel() {
this._cancel();
delete this.parent.children[this.parentKey];
this.app.scheduler.scheduleDestroy(this);
}
_cancel() {
this.status = 2 /* CANCELLED */;
const children = this.children;
for (let childKey in children) {
children[childKey]._cancel();
}
}
destroy() { destroy() {
let shouldRemove = this.status === 1 /* MOUNTED */; let shouldRemove = this.status === 1 /* MOUNTED */;
this._destroy(); this._destroy();
@@ -2464,7 +2455,7 @@ class ComponentNode {
this.app.handleError({ error: e, node: this }); this.app.handleError({ error: e, node: this });
} }
} }
this.status = 3 /* DESTROYED */; this.status = 2 /* DESTROYED */;
} }
async updateAndRender(props, parentFiber) { async updateAndRender(props, parentFiber) {
this.nextProps = props; this.nextProps = props;
@@ -2997,22 +2988,12 @@ function prepareList(collection) {
keys = collection; keys = collection;
values = collection; values = collection;
} }
else if (collection instanceof Map) { else if (collection) {
keys = [...collection.keys()];
values = [...collection.values()];
}
else if (collection && typeof collection === "object") {
if (Symbol.iterator in collection) {
keys = [...collection];
values = keys;
}
else {
values = Object.keys(collection); values = Object.keys(collection);
keys = Object.values(collection); keys = Object.values(collection);
} }
}
else { else {
throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`); throw new OwlError("Invalid loop expression");
} }
const n = values.length; const n = values.length;
return [keys, values, n, new Array(n)]; return [keys, values, n, new Array(n)];
@@ -4839,10 +4820,10 @@ function parseNode(node, ctx) {
parseTCall(node, ctx) || parseTCall(node, ctx) ||
parseTCallBlock(node) || parseTCallBlock(node) ||
parseTEscNode(node, ctx) || parseTEscNode(node, ctx) ||
parseTOutNode(node, ctx) ||
parseTKey(node, ctx) || parseTKey(node, ctx) ||
parseTTranslation(node, ctx) || parseTTranslation(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseTOutNode(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
parseDOMNode(node, ctx) || parseDOMNode(node, ctx) ||
parseTSetNode(node, ctx) || parseTSetNode(node, ctx) ||
@@ -4955,8 +4936,10 @@ function parseDOMNode(node, ctx) {
const typeAttr = node.getAttribute("type"); const typeAttr = node.getAttribute("type");
const isInput = tagName === "input"; const isInput = tagName === "input";
const isSelect = tagName === "select"; const isSelect = tagName === "select";
const isTextarea = tagName === "textarea";
const isCheckboxInput = isInput && typeAttr === "checkbox"; const isCheckboxInput = isInput && typeAttr === "checkbox";
const isRadioInput = isInput && typeAttr === "radio"; const isRadioInput = isInput && typeAttr === "radio";
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
const hasLazyMod = attr.includes(".lazy"); const hasLazyMod = attr.includes(".lazy");
const hasNumberMod = attr.includes(".number"); const hasNumberMod = attr.includes(".number");
const hasTrimMod = attr.includes(".trim"); const hasTrimMod = attr.includes(".trim");
@@ -4968,8 +4951,8 @@ function parseDOMNode(node, ctx) {
specialInitTargetAttr: isRadioInput ? "checked" : null, specialInitTargetAttr: isRadioInput ? "checked" : null,
eventType, eventType,
hasDynamicChildren: false, hasDynamicChildren: false,
shouldTrim: hasTrimMod, shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
shouldNumberize: hasNumberMod, shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
}; };
if (isSelect) { if (isSelect) {
// don't pollute the original ctx // don't pollute the original ctx
@@ -5032,6 +5015,9 @@ function parseTEscNode(node, ctx) {
content: [tesc], content: [tesc],
}; };
} }
if (ast.type === 11 /* TComponent */) {
throw new OwlError("t-esc is not supported on Component nodes");
}
return tesc; return tesc;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -5473,22 +5459,20 @@ function normalizeTIf(el) {
* *
* @param el the element containing the tree that should be normalized * @param el the element containing the tree that should be normalized
*/ */
function normalizeTEscTOut(el) { function normalizeTEsc(el) {
for (const d of ["t-esc", "t-out"]) { const elements = [...el.querySelectorAll("[t-esc]")].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
const elements = [...el.querySelectorAll(`[${d}]`)].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
for (const el of elements) { for (const el of elements) {
if (el.childNodes.length) { if (el.childNodes.length) {
throw new OwlError(`Cannot have ${d} on a component that already has content`); throw new OwlError("Cannot have t-esc on a component that already has content");
} }
const value = el.getAttribute(d); const value = el.getAttribute("t-esc");
el.removeAttribute(d); el.removeAttribute("t-esc");
const t = el.ownerDocument.createElement("t"); const t = el.ownerDocument.createElement("t");
if (value != null) { if (value != null) {
t.setAttribute(d, value); t.setAttribute("t-esc", value);
} }
el.appendChild(t); el.appendChild(t);
} }
}
} }
/** /**
* Normalizes the tree inside a given element and do some preliminary validation * Normalizes the tree inside a given element and do some preliminary validation
@@ -5498,7 +5482,7 @@ function normalizeTEscTOut(el) {
*/ */
function normalizeXML(el) { function normalizeXML(el) {
normalizeTIf(el); normalizeTIf(el);
normalizeTEscTOut(el); normalizeTEsc(el);
} }
/** /**
* Parses an XML string into an XML document, throwing errors on parser errors * Parses an XML string into an XML document, throwing errors on parser errors
@@ -5551,7 +5535,7 @@ function compile(template, options = {}) {
} }
// do not modify manually. This file is generated by the release script. // do not modify manually. This file is generated by the release script.
const version = "2.2.3"; const version = "2.1.3";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Scheduler // Scheduler
@@ -5561,18 +5545,11 @@ class Scheduler {
this.tasks = new Set(); this.tasks = new Set();
this.frame = 0; this.frame = 0;
this.delayedRenders = []; this.delayedRenders = [];
this.cancelledNodes = new Set();
this.requestAnimationFrame = Scheduler.requestAnimationFrame; this.requestAnimationFrame = Scheduler.requestAnimationFrame;
} }
addFiber(fiber) { addFiber(fiber) {
this.tasks.add(fiber.root); this.tasks.add(fiber.root);
} }
scheduleDestroy(node) {
this.cancelledNodes.add(node);
if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => this.processTasks());
}
}
/** /**
* Process all current tasks. This only applies to the fibers that are ready. * Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged. * Other tasks are left unchanged.
@@ -5582,29 +5559,22 @@ class Scheduler {
let renders = this.delayedRenders; let renders = this.delayedRenders;
this.delayedRenders = []; this.delayedRenders = [];
for (let f of renders) { for (let f of renders) {
if (f.root && f.node.status !== 3 /* DESTROYED */ && f.node.fiber === f) { if (f.root && f.node.status !== 2 /* DESTROYED */ && f.node.fiber === f) {
f.render(); f.render();
} }
} }
} }
if (this.frame === 0) { if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => this.processTasks()); this.frame = this.requestAnimationFrame(() => {
}
}
processTasks() {
this.frame = 0; this.frame = 0;
for (let node of this.cancelledNodes) { this.tasks.forEach((fiber) => this.processFiber(fiber));
node._destroy();
}
this.cancelledNodes.clear();
for (let task of this.tasks) { for (let task of this.tasks) {
this.processFiber(task); if (task.node.status === 2 /* DESTROYED */) {
}
for (let task of this.tasks) {
if (task.node.status === 3 /* DESTROYED */) {
this.tasks.delete(task); this.tasks.delete(task);
} }
} }
});
}
} }
processFiber(fiber) { processFiber(fiber) {
if (fiber.root !== fiber) { if (fiber.root !== fiber) {
@@ -5616,7 +5586,7 @@ class Scheduler {
this.tasks.delete(fiber); this.tasks.delete(fiber);
return; return;
} }
if (fiber.node.status === 3 /* DESTROYED */) { if (fiber.node.status === 2 /* DESTROYED */) {
this.tasks.delete(fiber); this.tasks.delete(fiber);
return; return;
} }
@@ -5708,8 +5678,8 @@ class App extends TemplateSet {
} }
destroy() { destroy() {
if (this.root) { if (this.root) {
this.scheduler.flush();
this.root.destroy(); this.root.destroy();
this.scheduler.processTasks();
} }
window.__OWL_DEVTOOLS__.apps.delete(this); window.__OWL_DEVTOOLS__.apps.delete(this);
} }
@@ -5840,11 +5810,9 @@ function status(component) {
switch (component.__owl__.status) { switch (component.__owl__.status) {
case 0 /* NEW */: case 0 /* NEW */:
return "new"; return "new";
case 2 /* CANCELLED */:
return "cancelled";
case 1 /* MOUNTED */: case 1 /* MOUNTED */:
return "mounted"; return "mounted";
case 3 /* DESTROYED */: case 2 /* DESTROYED */:
return "destroyed"; return "destroyed";
} }
} }
@@ -5984,6 +5952,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml }; export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
__info__.date = '2023-07-20T06:05:29.796Z'; __info__.date = '2023-06-28T09:17:13.630Z';
__info__.hash = 'b1a3b32'; __info__.hash = '432ff44';
__info__.url = 'https://github.com/odoo/owl'; __info__.url = 'https://github.com/odoo/owl';
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.2.3", "version": "2.1.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -5594,9 +5594,9 @@
} }
}, },
"tough-cookie": { "tough-cookie": {
"version": "4.1.2", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
"dev": true, "dev": true,
"requires": { "requires": {
"psl": "^1.1.33", "psl": "^1.1.33",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.2.3", "version": "2.1.4",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"module": "dist/owl.es.js", "module": "dist/owl.es.js",
+15 -12
View File
@@ -235,10 +235,10 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
parseTCall(node, ctx) || parseTCall(node, ctx) ||
parseTCallBlock(node, ctx) || parseTCallBlock(node, ctx) ||
parseTEscNode(node, ctx) || parseTEscNode(node, ctx) ||
parseTOutNode(node, ctx) ||
parseTKey(node, ctx) || parseTKey(node, ctx) ||
parseTTranslation(node, ctx) || parseTTranslation(node, ctx) ||
parseTSlot(node, ctx) || parseTSlot(node, ctx) ||
parseTOutNode(node, ctx) ||
parseComponent(node, ctx) || parseComponent(node, ctx) ||
parseDOMNode(node, ctx) || parseDOMNode(node, ctx) ||
parseTSetNode(node, ctx) || parseTSetNode(node, ctx) ||
@@ -365,8 +365,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const typeAttr = node.getAttribute("type"); const typeAttr = node.getAttribute("type");
const isInput = tagName === "input"; const isInput = tagName === "input";
const isSelect = tagName === "select"; const isSelect = tagName === "select";
const isTextarea = tagName === "textarea";
const isCheckboxInput = isInput && typeAttr === "checkbox"; const isCheckboxInput = isInput && typeAttr === "checkbox";
const isRadioInput = isInput && typeAttr === "radio"; const isRadioInput = isInput && typeAttr === "radio";
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
const hasLazyMod = attr.includes(".lazy"); const hasLazyMod = attr.includes(".lazy");
const hasNumberMod = attr.includes(".number"); const hasNumberMod = attr.includes(".number");
const hasTrimMod = attr.includes(".trim"); const hasTrimMod = attr.includes(".trim");
@@ -379,8 +381,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
specialInitTargetAttr: isRadioInput ? "checked" : null, specialInitTargetAttr: isRadioInput ? "checked" : null,
eventType, eventType,
hasDynamicChildren: false, hasDynamicChildren: false,
shouldTrim: hasTrimMod, shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
shouldNumberize: hasNumberMod, shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
}; };
if (isSelect) { if (isSelect) {
// don't pollute the original ctx // don't pollute the original ctx
@@ -444,6 +446,9 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
content: [tesc], content: [tesc],
}; };
} }
if (ast.type === ASTType.TComponent) {
throw new OwlError("t-esc is not supported on Component nodes");
}
return tesc; return tesc;
} }
@@ -938,24 +943,22 @@ function normalizeTIf(el: Element) {
* *
* @param el the element containing the tree that should be normalized * @param el the element containing the tree that should be normalized
*/ */
function normalizeTEscTOut(el: Element) { function normalizeTEsc(el: Element) {
for (const d of ["t-esc", "t-out"]) { const elements = [...el.querySelectorAll("[t-esc]")].filter(
const elements = [...el.querySelectorAll(`[${d}]`)].filter(
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component") (el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
); );
for (const el of elements) { for (const el of elements) {
if (el.childNodes.length) { if (el.childNodes.length) {
throw new OwlError(`Cannot have ${d} on a component that already has content`); throw new OwlError("Cannot have t-esc on a component that already has content");
} }
const value = el.getAttribute(d); const value = el.getAttribute("t-esc");
el.removeAttribute(d); el.removeAttribute("t-esc");
const t = el.ownerDocument.createElement("t"); const t = el.ownerDocument.createElement("t");
if (value != null) { if (value != null) {
t.setAttribute(d, value); t.setAttribute("t-esc", value);
} }
el.appendChild(t); el.appendChild(t);
} }
}
} }
/** /**
@@ -966,7 +969,7 @@ function normalizeTEscTOut(el: Element) {
*/ */
function normalizeXML(el: Element) { function normalizeXML(el: Element) {
normalizeTIf(el); normalizeTIf(el);
normalizeTEscTOut(el); normalizeTEsc(el);
} }
/** /**
+1 -1
View File
@@ -136,8 +136,8 @@ export class App<
destroy() { destroy() {
if (this.root) { if (this.root) {
this.scheduler.flush();
this.root.destroy(); this.root.destroy();
this.scheduler.processTasks();
} }
window.__OWL_DEVTOOLS__.apps.delete(this); window.__OWL_DEVTOOLS__.apps.delete(this);
} }
+1 -18
View File
@@ -145,9 +145,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
async render(deep: boolean) { async render(deep: boolean) {
if (this.status >= STATUS.CANCELLED) {
return;
}
let current = this.fiber; let current = this.fiber;
if (current && (current.root!.locked || (current as any).bdom === true)) { if (current && (current.root!.locked || (current as any).bdom === true)) {
await Promise.resolve(); await Promise.resolve();
@@ -174,7 +171,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
await Promise.resolve(); await Promise.resolve();
if (this.status >= STATUS.CANCELLED) { if (this.status === STATUS.DESTROYED) {
return; return;
} }
// We only want to actually render the component if the following two // We only want to actually render the component if the following two
@@ -193,20 +190,6 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
} }
cancel() {
this._cancel();
delete this.parent!.children[this.parentKey!];
this.app.scheduler.scheduleDestroy(this);
}
_cancel() {
this.status = STATUS.CANCELLED;
const children = this.children;
for (let childKey in children) {
children[childKey]._cancel();
}
}
destroy() { destroy() {
let shouldRemove = this.status === STATUS.MOUNTED; let shouldRemove = this.status === STATUS.MOUNTED;
this._destroy(); this._destroy();
+1 -3
View File
@@ -51,9 +51,8 @@ export function handleError(params: ErrorParams) {
); );
} }
const node = "node" in params ? params.node : params.fiber.node; const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber; const fiber = "fiber" in params ? params.fiber : node.fiber!;
if (fiber) {
// resets the fibers on components if possible. This is important so that // resets the fibers on components if possible. This is important so that
// new renderings can be properly included in the initial one, if any. // new renderings can be properly included in the initial one, if any.
let current: Fiber | null = fiber; let current: Fiber | null = fiber;
@@ -63,7 +62,6 @@ export function handleError(params: ErrorParams) {
} while (current); } while (current);
fibersInError.set(fiber.root!, error); fibersInError.set(fiber.root!, error);
}
const handled = _handleError(node, error); const handled = _handleError(node, error);
if (!handled) { if (!handled) {
+2 -1
View File
@@ -55,7 +55,8 @@ function cancelFibers(fibers: Fiber[]): number {
let node = fiber.node; let node = fiber.node;
fiber.render = throwOnRender; fiber.render = throwOnRender;
if (node.status === STATUS.NEW) { if (node.status === STATUS.NEW) {
node.cancel(); node.destroy();
delete node.parent!.children[node.parentKey!];
} }
node.fiber = null; node.fiber = null;
if (fiber.bdom) { if (fiber.bdom) {
+4 -21
View File
@@ -1,4 +1,3 @@
import type { ComponentNode } from "./component_node";
import { fibersInError } from "./error_handling"; import { fibersInError } from "./error_handling";
import { Fiber, RootFiber } from "./fibers"; import { Fiber, RootFiber } from "./fibers";
import { STATUS } from "./status"; import { STATUS } from "./status";
@@ -15,7 +14,6 @@ export class Scheduler {
requestAnimationFrame: Window["requestAnimationFrame"]; requestAnimationFrame: Window["requestAnimationFrame"];
frame: number = 0; frame: number = 0;
delayedRenders: Fiber[] = []; delayedRenders: Fiber[] = [];
cancelledNodes: Set<ComponentNode> = new Set();
constructor() { constructor() {
this.requestAnimationFrame = Scheduler.requestAnimationFrame; this.requestAnimationFrame = Scheduler.requestAnimationFrame;
@@ -25,13 +23,6 @@ export class Scheduler {
this.tasks.add(fiber.root!); this.tasks.add(fiber.root!);
} }
scheduleDestroy(node: ComponentNode) {
this.cancelledNodes.add(node);
if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => this.processTasks());
}
}
/** /**
* Process all current tasks. This only applies to the fibers that are ready. * Process all current tasks. This only applies to the fibers that are ready.
* Other tasks are left unchanged. * Other tasks are left unchanged.
@@ -48,24 +39,16 @@ export class Scheduler {
} }
if (this.frame === 0) { if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => this.processTasks()); this.frame = this.requestAnimationFrame(() => {
}
}
processTasks() {
this.frame = 0; this.frame = 0;
for (let node of this.cancelledNodes) { this.tasks.forEach((fiber) => this.processFiber(fiber));
node._destroy();
}
this.cancelledNodes.clear();
for (let task of this.tasks) {
this.processFiber(task);
}
for (let task of this.tasks) { for (let task of this.tasks) {
if (task.node.status === STATUS.DESTROYED) { if (task.node.status === STATUS.DESTROYED) {
this.tasks.delete(task); this.tasks.delete(task);
} }
} }
});
}
} }
processFiber(fiber: RootFiber) { processFiber(fiber: RootFiber) {
+1 -6
View File
@@ -7,20 +7,15 @@ import type { Component } from "./component";
export const enum STATUS { export const enum STATUS {
NEW, NEW,
MOUNTED, // is ready, and in DOM. It has a valid el MOUNTED, // is ready, and in DOM. It has a valid el
// component has been created, but has been replaced by a newer component before being mounted
// it is cancelled until the next animation frame where it will be destroyed
CANCELLED,
DESTROYED, DESTROYED,
} }
type STATUS_DESCR = "new" | "mounted" | "cancelled" | "destroyed"; type STATUS_DESCR = "new" | "mounted" | "destroyed";
export function status(component: Component): STATUS_DESCR { export function status(component: Component): STATUS_DESCR {
switch (component.__owl__.status) { switch (component.__owl__.status) {
case STATUS.NEW: case STATUS.NEW:
return "new"; return "new";
case STATUS.CANCELLED:
return "cancelled";
case STATUS.MOUNTED: case STATUS.MOUNTED:
return "mounted"; return "mounted";
case STATUS.DESTROYED: case STATUS.DESTROYED:
+5 -13
View File
@@ -60,26 +60,18 @@ function withKey(elem: any, k: string) {
return elem; return elem;
} }
function prepareList(collection: unknown): [unknown[], unknown[], number, undefined[]] { function prepareList(collection: any): [any[], any[], number, any[]] {
let keys: unknown[]; let keys: any[];
let values: unknown[]; let values: any[];
if (Array.isArray(collection)) { if (Array.isArray(collection)) {
keys = collection; keys = collection;
values = collection; values = collection;
} else if (collection instanceof Map) { } else if (collection) {
keys = [...collection.keys()];
values = [...collection.values()];
} else if (collection && typeof collection === "object") {
if (Symbol.iterator in collection) {
keys = [...(<Iterable<unknown>>collection)];
values = keys;
} else {
values = Object.keys(collection); values = Object.keys(collection);
keys = Object.values(collection); keys = Object.values(collection);
}
} else { } else {
throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`); throw new OwlError("Invalid loop expression");
} }
const n = values.length; const n = values.length;
return [keys, values, n, new Array(n)]; return [keys, values, n, new Array(n)];
+13 -6
View File
@@ -9,13 +9,20 @@ export type Callback = () => void;
* @returns a batched version of the original callback * @returns a batched version of the original callback
*/ */
export function batched(callback: Callback): Callback { export function batched(callback: Callback): Callback {
let scheduled = false; let called = false;
return async (...args) => { return async () => {
if (!scheduled) { // This await blocks all calls to the callback here, then releases them sequentially
scheduled = true; // in the next microtick. This line decides the granularity of the batch.
await Promise.resolve(); await Promise.resolve();
scheduled = false; if (!called) {
callback(...args); called = true;
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback.
// Schedule this before calling the callback so that calls to the batched function
// within the callback will proceed only after resetting called to false, and have
// a chance to execute the callback again
Promise.resolve().then(() => (called = false));
callback();
} }
}; };
} }
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script. // do not modify manually. This file is generated by the release script.
export const version = "2.2.3"; export const version = "2.1.4";
-28
View File
@@ -15,34 +15,6 @@ exports[`app App supports env with getters/setters 1`] = `
}" }"
`; `;
exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately on destroy 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`B\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`A\`);
if (ctx['state'].value) {
b3 = comp1({}, key + \`__1\`, node, this, null);
}
return multi([b2, b3]);
}
}"
`;
exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately on destroy 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`B\`);
}
}"
`;
exports[`app can configure an app with props 1`] = ` exports[`app can configure an app with props 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
+2 -50
View File
@@ -1,14 +1,6 @@
import { App, Component, mount, onWillStart, useState, xml } from "../../src"; import { App, Component, mount, xml } from "../../src";
import { status } from "../../src/runtime/status"; import { status } from "../../src/runtime/status";
import { import { makeTestFixture, snapshotEverything, nextTick, elem } from "../helpers";
makeTestFixture,
snapshotEverything,
nextTick,
elem,
useLogLifecycle,
makeDeferred,
nextMicroTick,
} from "../helpers";
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -102,44 +94,4 @@ describe("app", () => {
expect(iframeDoc.contains(div)).toBe(false); expect(iframeDoc.contains(div)).toBe(false);
expect(status(comp)).toBe("destroyed"); expect(status(comp)).toBe("destroyed");
}); });
test("app: clear scheduler tasks and destroy cancelled nodes immediately on destroy", async () => {
let def = makeDeferred();
class B extends Component {
static template = xml`B`;
setup() {
useLogLifecycle();
onWillStart(() => def);
}
}
class A extends Component {
static template = xml`A<t t-if="state.value"><B/></t>`;
static components = { B };
state = useState({ value: false });
setup() {
useLogLifecycle();
}
}
const app = new App(A);
const comp = await app.mount(fixture);
expect(["A:setup", "A:willStart", "A:willRender", "A:rendered", "A:mounted"]).toBeLogged();
comp.state.value = true;
await nextTick();
expect(["A:willRender", "B:setup", "B:willStart", "A:rendered"]).toBeLogged();
// rerender to force the instantiation of a new B component (and cancelling the first)
comp.render();
await nextMicroTick();
expect(["A:willRender", "B:setup", "B:willStart", "A:rendered"]).toBeLogged();
app.destroy();
expect([
"A:willUnmount",
"B:willDestroy",
"A:willDestroy",
"B:willDestroy", // make sure the 2 B instances have been destroyed synchronously
]).toBeLogged();
});
}); });
@@ -77,62 +77,6 @@ exports[`t-foreach iterate on items 1`] = `
}" }"
`; `;
exports[`t-foreach iterate, Map param 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['value']);;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
ctx[\`item_index\`] = i1;
ctx[\`item_value\`] = k_block1[i1];
const key1 = ctx['item_index'];
const b3 = text(\` [\`);
const b4 = text(ctx['item_index']);
const b5 = text(\`: \`);
const b6 = text(ctx['item']);
const b7 = text(\` \`);
const b8 = text(ctx['item_value']);
const b9 = text(\`] \`);
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-foreach iterate, Set param 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['value']);;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
ctx[\`item_index\`] = i1;
ctx[\`item_value\`] = k_block1[i1];
const key1 = ctx['item_index'];
const b3 = text(\` [\`);
const b4 = text(ctx['item_index']);
const b5 = text(\`: \`);
const b6 = text(ctx['item']);
const b7 = text(\` \`);
const b8 = text(ctx['item_value']);
const b9 = text(\`] \`);
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-foreach iterate, dict param 1`] = ` exports[`t-foreach iterate, dict param 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -164,62 +108,6 @@ exports[`t-foreach iterate, dict param 1`] = `
}" }"
`; `;
exports[`t-foreach iterate, generator param 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['gen']());;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
ctx[\`item_index\`] = i1;
ctx[\`item_value\`] = k_block1[i1];
const key1 = ctx['item_index'];
const b3 = text(\` [\`);
const b4 = text(ctx['item_index']);
const b5 = text(\`: \`);
const b6 = text(ctx['item']);
const b7 = text(\` \`);
const b8 = text(ctx['item_value']);
const b9 = text(\`] \`);
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-foreach iterate, iterable param 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['map'].values());;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
ctx[\`item_index\`] = i1;
ctx[\`item_value\`] = k_block1[i1];
const key1 = ctx['item_index'];
const b3 = text(\` [\`);
const b4 = text(ctx['item_index']);
const b5 = text(\`: \`);
const b6 = text(ctx['item']);
const b7 = text(\` \`);
const b8 = text(ctx['item_value']);
const b9 = text(\`] \`);
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-foreach iterate, position 1`] = ` exports[`t-foreach iterate, position 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
+2 -8
View File
@@ -1569,12 +1569,6 @@ describe("qweb parser", () => {
); );
}); });
test("component with t-out", async () => {
expect(parse(`<MyComponent t-out="someValue"/>`)).toEqual(
parse(`<MyComponent><t t-out="someValue"/></MyComponent>`)
);
});
test("component with t-esc and content", async () => { test("component with t-esc and content", async () => {
expect(() => parse(`<MyComponent t-esc="someValue">Some content</MyComponent>`)).toThrow( expect(() => parse(`<MyComponent t-esc="someValue">Some content</MyComponent>`)).toThrow(
"Cannot have t-esc on a component that already has content" "Cannot have t-esc on a component that already has content"
@@ -1997,8 +1991,8 @@ describe("qweb parser", () => {
baseExpr: "state", baseExpr: "state",
expr: "'stuff'", expr: "'stuff'",
eventType: "click", eventType: "click",
shouldNumberize: true, shouldNumberize: false,
shouldTrim: true, shouldTrim: false,
targetAttr: "value", targetAttr: "value",
hasDynamicChildren: false, hasDynamicChildren: false,
specialInitTargetAttr: "checked", specialInitTargetAttr: "checked",
+1 -61
View File
@@ -105,64 +105,6 @@ describe("t-foreach", () => {
expect(renderToString(template, context)).toBe(expected); expect(renderToString(template, context)).toBe(expected);
}); });
test("iterate, Map param", () => {
const template = `
<t t-foreach="value" t-as="item" t-key="item_index">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>`;
const expected = ` [0: 1 a] [1: 2 b] [2: 3 c] `;
const context = {
value: new Map([
["a", 1],
["b", 2],
["c", 3],
]),
};
expect(renderToString(template, context)).toBe(expected);
});
test("iterate, Set param", () => {
const template = `
<t t-foreach="value" t-as="item" t-key="item_index">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>`;
const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `;
const context = { value: new Set([1, 2, 3]) };
expect(renderToString(template, context)).toBe(expected);
});
test("iterate, iterable param", () => {
const template = `
<t t-foreach="map.values()" t-as="item" t-key="item_index">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>`;
const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `;
const context = {
map: new Map([
["a", 1],
["b", 2],
["c", 3],
]),
};
expect(renderToString(template, context)).toBe(expected);
});
test("iterate, generator param", () => {
const template = `
<t t-foreach="gen()" t-as="item" t-key="item_index">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>`;
const expected = ` [0: 1 1] [1: 2 2] [2: 3 3] `;
const context = {
*gen() {
yield 1;
yield 2;
yield 3;
},
};
expect(renderToString(template, context)).toBe(expected);
});
test("does not pollute the rendering context", () => { test("does not pollute the rendering context", () => {
const template = ` const template = `
<div> <div>
@@ -251,9 +193,7 @@ describe("t-foreach", () => {
test("throws error if invalid loop expression", () => { test("throws error if invalid loop expression", () => {
const test = `<div><t t-foreach="abc" t-as="item" t-key="item"><span t-key="item_index"/></t></div>`; const test = `<div><t t-foreach="abc" t-as="item" t-key="item"><span t-key="item_index"/></t></div>`;
expect(() => renderToString(test)).toThrow( expect(() => renderToString(test)).toThrow("Invalid loop expression");
'Invalid loop expression: "undefined" is not iterable'
);
}); });
test("t-foreach with t-if inside", () => { test("t-foreach with t-if inside", () => {
@@ -212,73 +212,6 @@ exports[`changing state before first render does not trigger a render 1`] = `
}" }"
`; `;
exports[`component destroyed just after render 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`B\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`component destroyed just after render 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'].value);
return multi([b2, b3]);
}
}"
`;
exports[`components are not destroyed between animation frame 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`B\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`A\`);
if (ctx['state'].flag) {
b3 = comp1({}, key + \`__1\`, node, this, null);
}
return multi([b2, b3]);
}
}"
`;
exports[`components are not destroyed between animation frame 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`C\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`B\`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
}"
`;
exports[`components are not destroyed between animation frame 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[`concurrent renderings scenario 1 1`] = ` exports[`concurrent renderings scenario 1 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -468,36 +468,6 @@ exports[`t-model directive t-model on select with static options 1`] = `
}" }"
`; `;
exports[`t-model directive t-model with dynamic number values on select options in foreach 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { toNumber, prepareList, withKey } = helpers;
let block1 = createBlock(\`<select block-handler-0=\\"change\\"><block-child-0/></select>\`);
let block3 = createBlock(\`<option block-attribute-0=\\"value\\" block-attribute-1=\\"selected\\"><block-text-2/></option>\`);
return function template(ctx, node, key = \\"\\") {
const bExpr1 = ctx['state'];
const expr1 = 'value';
const bValue1 = bExpr1[expr1];
let hdlr1 = [(ev) => { bExpr1[expr1] = toNumber(ev.target.value); }];
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['state'].options);;
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`o\`] = v_block2[i1];
const key1 = ctx['o'].value;
let attr1 = ctx['o'].value;
let attr2 = bValue1 === ctx['o'].value;
let txt1 = ctx['o'].value;
c_block2[i1] = withKey(block3([attr1, attr2, txt1]), key1);
}
const b2 = list(c_block2);
return block1([hdlr1], [b2]);
}
}"
`;
exports[`t-model directive t-model with dynamic values on select options -- 2 1`] = ` exports[`t-model directive t-model with dynamic values on select options -- 2 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
+18 -123
View File
@@ -115,7 +115,13 @@ test("destroying/recreating a subwidget with different props (if start is not ov
await nextMicroTick(); await nextMicroTick();
expect(n).toBe(2); expect(n).toBe(2);
expect(["W:willRender", "Child:setup", "Child:willStart", "W:rendered"]).toBeLogged(); expect([
"Child:willDestroy",
"W:willRender",
"Child:setup",
"Child:willStart",
"W:rendered",
]).toBeLogged();
def.resolve(); def.resolve();
await nextTick(); await nextTick();
@@ -124,7 +130,6 @@ test("destroying/recreating a subwidget with different props (if start is not ov
expect([ expect([
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Child:willDestroy",
"W:willPatch", "W:willPatch",
"Child:mounted", "Child:mounted",
"W:patched", "W:patched",
@@ -173,13 +178,13 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered", "Parent:rendered",
"Child:willDestroy",
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered", "Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Child:willDestroy",
"Parent:willPatch", "Parent:willPatch",
"Child:mounted", "Child:mounted",
"Parent:patched", "Parent:patched",
@@ -246,13 +251,13 @@ test("creating two async components, scenario 1", async () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect([ expect([
"ChildA:willDestroy",
"Parent:willRender", "Parent:willRender",
"ChildA:setup", "ChildA:setup",
"ChildA:willStart", "ChildA:willStart",
"ChildB:setup", "ChildB:setup",
"ChildB:willStart", "ChildB:willStart",
"Parent:rendered", "Parent:rendered",
"ChildA:willDestroy",
]).toBeLogged(); ]).toBeLogged();
defB.resolve(); defB.resolve();
@@ -698,13 +703,13 @@ test("rendering component again in next microtick", async () => {
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered", "Parent:rendered",
"Child:willDestroy",
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Parent:rendered", "Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Child:willDestroy",
"Parent:willPatch", "Parent:willPatch",
"Child:mounted", "Child:mounted",
"Parent:patched", "Parent:patched",
@@ -1727,9 +1732,9 @@ test("concurrent renderings scenario 10", async () => {
expect(fixture.innerHTML).toBe("<div><p></p></div>"); expect(fixture.innerHTML).toBe("<div><p></p></div>");
expect([ expect([
"ComponentA:willRender", "ComponentA:willRender",
"ComponentC:willDestroy",
"ComponentB:willUpdateProps", "ComponentB:willUpdateProps",
"ComponentA:rendered", "ComponentA:rendered",
"ComponentC:willDestroy",
]).toBeLogged(); ]).toBeLogged();
defB.resolve(); defB.resolve();
@@ -2277,6 +2282,7 @@ test("concurrent renderings scenario 16", async () => {
"D:setup", "D:setup",
"D:willStart", "D:willStart",
"C:rendered", "C:rendered",
"D:willDestroy",
"B:willRender", "B:willRender",
"C:willUpdateProps", "C:willUpdateProps",
"B:rendered", "B:rendered",
@@ -2284,7 +2290,6 @@ test("concurrent renderings scenario 16", async () => {
"D:setup", "D:setup",
"D:willStart", "D:willStart",
"C:rendered", "C:rendered",
"D:willDestroy",
]).toBeLogged(); ]).toBeLogged();
// at this point, C rendering is still pending, and nothing should have been // at this point, C rendering is still pending, and nothing should have been
@@ -2992,11 +2997,11 @@ test("t-key on dom node having a component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>"); expect(fixture.innerHTML).toBe("<div>3</div>");
expect([ expect([
"Child (2):willDestroy",
"Child (3):setup", "Child (3):setup",
"Child (3):willStart", "Child (3):willStart",
"Child (3):willRender", "Child (3):willRender",
"Child (3):rendered", "Child (3):rendered",
"Child (2):willDestroy",
"Child (1):willUnmount", "Child (1):willUnmount",
"Child (1):willDestroy", "Child (1):willDestroy",
"Child (3):mounted", "Child (3):mounted",
@@ -3050,11 +3055,11 @@ test("t-key on dynamic async component (toggler is never patched)", async () =>
expect(fixture.innerHTML).toBe("<div>3</div>"); expect(fixture.innerHTML).toBe("<div>3</div>");
expect([ expect([
"Child (2):willDestroy",
"Child (3):setup", "Child (3):setup",
"Child (3):willStart", "Child (3):willStart",
"Child (3):willRender", "Child (3):willRender",
"Child (3):rendered", "Child (3):rendered",
"Child (2):willDestroy",
"Child (1):willUnmount", "Child (1):willUnmount",
"Child (1):willDestroy", "Child (1):willDestroy",
"Child (3):mounted", "Child (3):mounted",
@@ -3109,11 +3114,11 @@ test("t-foreach with dynamic async component", async () => {
expect(fixture.innerHTML).toBe("<div>3</div>"); expect(fixture.innerHTML).toBe("<div>3</div>");
expect([ expect([
"Child (2):willDestroy",
"Child (3):setup", "Child (3):setup",
"Child (3):willStart", "Child (3):willStart",
"Child (3):willRender", "Child (3):willRender",
"Child (3):rendered", "Child (3):rendered",
"Child (2):willDestroy",
"Child (1):willUnmount", "Child (1):willUnmount",
"Child (1):willDestroy", "Child (1):willDestroy",
"Child (3):mounted", "Child (3):mounted",
@@ -3796,7 +3801,7 @@ test("destroyed component causes other soon to be destroyed component to rerende
static template = xml`<t t-esc="state.val + props.value"/>`; static template = xml`<t t-esc="state.val + props.value"/>`;
state = useState({ val: 0 }); state = useState({ val: 0 });
setup() { setup() {
c = c || this; c = this;
useLogLifecycle(); useLogLifecycle();
} }
} }
@@ -3841,6 +3846,8 @@ test("destroyed component causes other soon to be destroyed component to rerende
parent.state.valueB = 2; parent.state.valueB = 2;
await nextTick(); await nextTick();
expect([ expect([
"B:willDestroy",
"C:willDestroy",
"A:willRender", "A:willRender",
"B:setup", "B:setup",
"B:willStart", "B:willStart",
@@ -3851,8 +3858,6 @@ test("destroyed component causes other soon to be destroyed component to rerende
"B:rendered", "B:rendered",
"C:willRender", "C:willRender",
"C:rendered", "C:rendered",
"B:willDestroy",
"C:willDestroy",
"A:willPatch", "A:willPatch",
"C:mounted", "C:mounted",
"B:mounted", "B:mounted",
@@ -4195,116 +4200,6 @@ test("delayed render is not cancelled by upcoming render", async () => {
]).toBeLogged(); ]).toBeLogged();
}); });
test("components are not destroyed between animation frame", async () => {
const def = makeDeferred();
class C extends Component {
static template = xml`C`;
setup() {
useLogLifecycle();
}
}
class B extends Component {
static template = xml`B<C/>`;
static components = { C };
setup() {
useLogLifecycle();
onWillStart(() => {
return def;
});
}
}
class A extends Component {
static template = xml`A<B t-if="state.flag"/>`;
static components = { B };
state = useState({ flag: false });
setup() {
useLogLifecycle();
}
}
const a = await mount(A, fixture);
expect(fixture.innerHTML).toBe("A");
expect(["A:setup", "A:willStart", "A:willRender", "A:rendered", "A:mounted"]).toBeLogged();
// turn the flag on, this will render A and stops at B because of def
a.state.flag = true;
await nextTick();
expect(["A:willRender", "B:setup", "B:willStart", "A:rendered"]).toBeLogged();
// force a render of A
// => owl will need to create a new B component
// => initial B component will be cancelled
a.render();
await nextMicroTick();
expect([
// note that B is not destroyed here. It is cancelled instead
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
]).toBeLogged();
// resolve def, so B render is unblocked
def.resolve();
await nextTick();
expect([
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
// animation frame callback starts here
"B:willDestroy", // B is destroyed here
"A:willPatch",
"C:mounted",
"B:mounted",
"A:patched",
]).toBeLogged();
});
test("component destroyed just after render", async () => {
let stateB: any;
class B extends Component {
static template = xml`B<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
stateB = this.state;
useLogLifecycle();
}
}
class A extends Component {
static template = xml`<B/>`;
static components = { B };
setup() {
useLogLifecycle();
}
}
const a = await mount(A, fixture);
expect(fixture.innerHTML).toBe("B1");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"B:mounted",
"A:mounted",
]).toBeLogged();
stateB!.value++; // force a render of B
await nextMicroTick(); // wait for B render to actually start
a.__owl__.app.destroy();
expect(["A:willUnmount", "B:willUnmount", "B:willDestroy", "A:willDestroy"]).toBeLogged();
await nextTick();
// check that B was not rendered after being destroyed
expect([]).toBeLogged();
});
// test.skip("components with shouldUpdate=false", async () => { // test.skip("components with shouldUpdate=false", async () => {
// const state = { p: 1, cc: 10 }; // const state = { p: 1, cc: 10 };
+2 -6
View File
@@ -1444,7 +1444,6 @@ describe("can catch errors", () => {
"Parent:willPatch", "Parent:willPatch",
"Child:willUnmount", "Child:willUnmount",
"Child:willDestroy", "Child:willDestroy",
"Parent:patched",
"Parent:willRender", "Parent:willRender",
"Parent:rendered", "Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
@@ -1507,15 +1506,12 @@ describe("can catch errors", () => {
parent.state.hasChild = false; parent.state.hasChild = false;
await nextTick(); await nextTick();
expect([ expect([
"Parent:willRender",
"Parent:rendered",
"Child:willDestroy", "Child:willDestroy",
"Parent:willRender", "Parent:willRender",
"Parent:rendered", "Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged(); ]).toBeLogged();
expect(fixture.innerHTML).toBe("1");
await nextTick();
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
expect(fixture.innerHTML).toBe("2"); expect(fixture.innerHTML).toBe("2");
}); });
}); });
-33
View File
@@ -626,39 +626,6 @@ describe("t-model directive", () => {
expect(fixture.querySelector("select")!.value).toEqual("b"); expect(fixture.querySelector("select")!.value).toEqual("b");
}); });
test("t-model with dynamic number values on select options in foreach", async () => {
class Test extends Component {
static template = xml`
<select t-model.number="state.value">
<t t-foreach="state.options" t-as="o" t-key="o.value">
<option t-att-value="o.value" t-esc="o.value"/>
</t>
</select>
`;
state: any;
setup() {
this.state = useState({
value: 2,
options: [{ value: 1 }, { value: 2 }, { value: 3 }],
});
}
}
const comp = await mount(Test, fixture);
// check that we have a value of 2 selected
expect(fixture.querySelector("select")!.value).toEqual("2");
expect(comp.state.value).toBe(2);
// emulate a click on the option=3 element
fixture.querySelectorAll("option")[2].selected = true;
fixture.querySelector("select")!.dispatchEvent(new Event("change"));
await nextTick();
// check that we have now selected the number 3 (and not the string)
expect(fixture.querySelector("select")!.value).toEqual("3");
expect(comp.state.value).toBe(3);
});
test("t-model is applied before t-on-input", async () => { test("t-model is applied before t-on-input", async () => {
expect.assertions(3); expect.assertions(3);
class SomeComponent extends Component { class SomeComponent extends Component {
@@ -1519,9 +1519,8 @@
} }
} }
} }
// If nothing was found, return the path of the first root component found in the apps // If nothing was found, return the first app's root component path
const appIndex = [...this.apps].findIndex((app) => app.root); return ["0", "root"];
return [appIndex.toString(), "root"];
} }
// Returns the tree of components of the inspected page in a parsed format // Returns the tree of components of the inspected page in a parsed format
// Use inspectedPath to specify the path of the selected component // Use inspectedPath to specify the path of the selected component
+7 -6
View File
@@ -82,12 +82,6 @@ async function startRelease() {
return; return;
} }
// ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: updating package.json...`);
await writeFile("package.json", JSON.stringify({...package, version: next}, null, 2) + "\n");
await writeFile("package-lock.json", JSON.stringify({...packageLock, version: next}, null, 2) + "\n");
await writeFile("./src/version.ts", `// do not modify manually. This file is generated by the release script.\nexport const version = "${next}";\n`);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: building owl...`); log(`Step ${step++}/${STEPS}: building owl...`);
await execCommand("rm -rf dist/"); await execCommand("rm -rf dist/");
@@ -114,6 +108,13 @@ async function startRelease() {
await execCommand("cd dist && zip -r owl-devtools.zip devtools-chrome devtools-firefox && cd .."); await execCommand("cd dist && zip -r owl-devtools.zip devtools-chrome devtools-firefox && cd ..");
await execCommand("rm -r dist/devtools-chrome dist/devtools-firefox && rm dist/compiler.js"); await execCommand("rm -r dist/devtools-chrome dist/devtools-firefox && rm dist/compiler.js");
// ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: updating package.json...`);
await writeFile("package.json", JSON.stringify({...package, version: next}, null, 2) + "\n");
await writeFile("package-lock.json", JSON.stringify({...packageLock, version: next}, null, 2) + "\n");
await writeFile("./src/version.ts", `// do not modify manually. This file is generated by the release script.\nexport const version = "${next}";\n`);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: updating owl on github page...`); log(`Step ${step++}/${STEPS}: updating owl on github page...`);
await fs.copyFileSync("dist/owl.es.js", "docs/owl.js"); await fs.copyFileSync("dist/owl.es.js", "docs/owl.js");