[REL] v2.2

# v2.2

 - [IMP] runtime: add support for Map and other iterables in t-foreach
 - [IMP] runtime: only destroy component in raf callback
 - [REF] runtime: simplify implementation of batched
 - [FIX] compiler: allow t-model.number to work with select
 - [FIX] devtools: Fix crash when no root node
 - [FIX] devtools: fix/imp env display
 - [FIX] devtools: fix symbols handling and display
This commit is contained in:
Géry Debongnie
2023-07-18 16:07:37 +02:00
parent 836e12b1c5
commit 0d9d21a5c1
4 changed files with 85 additions and 52 deletions
+82 -49
View File
@@ -122,14 +122,16 @@ 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;
// resets the fibers on components if possible. This is important so that if (fiber) {
// new renderings can be properly included in the initial one, if any. // resets the fibers on components if possible. This is important so that
let current = fiber; // new renderings can be properly included in the initial one, if any.
do { let current = fiber;
current.node.fiber = current; do {
current = current.parent; current.node.fiber = current;
} while (current); current = current.parent;
fibersInError.set(fiber.root, error); } while (current);
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`);
@@ -311,20 +313,13 @@ 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 called = false; let scheduled = false;
return async () => { return async (...args) => {
// This await blocks all calls to the callback here, then releases them sequentially if (!scheduled) {
// in the next microtick. This line decides the granularity of the batch. scheduled = true;
await Promise.resolve(); await Promise.resolve();
if (!called) { scheduled = false;
called = true; callback(...args);
// 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();
} }
}; };
} }
@@ -1657,8 +1652,7 @@ 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.destroy(); node.cancel();
delete node.parent.children[node.parentKey];
} }
node.fiber = null; node.fiber = null;
if (fiber.bdom) { if (fiber.bdom) {
@@ -2385,6 +2379,9 @@ 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();
@@ -2410,7 +2407,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 /* DESTROYED */) { if (this.status >= 2 /* CANCELLED */) {
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
@@ -2428,6 +2425,18 @@ 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();
@@ -2455,7 +2464,7 @@ class ComponentNode {
this.app.handleError({ error: e, node: this }); this.app.handleError({ error: e, node: this });
} }
} }
this.status = 2 /* DESTROYED */; this.status = 3 /* DESTROYED */;
} }
async updateAndRender(props, parentFiber) { async updateAndRender(props, parentFiber) {
this.nextProps = props; this.nextProps = props;
@@ -2988,12 +2997,22 @@ function prepareList(collection) {
keys = collection; keys = collection;
values = collection; values = collection;
} }
else if (collection) { else if (collection instanceof Map) {
values = Object.keys(collection); keys = [...collection.keys()];
keys = Object.values(collection); values = [...collection.values()];
}
else if (collection && typeof collection === "object") {
if (Symbol.iterator in collection) {
keys = [...collection];
values = keys;
}
else {
values = Object.keys(collection);
keys = Object.values(collection);
}
} }
else { else {
throw new OwlError("Invalid loop expression"); throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`);
} }
const n = values.length; const n = values.length;
return [keys, values, n, new Array(n)]; return [keys, values, n, new Array(n)];
@@ -4936,10 +4955,8 @@ 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");
@@ -4951,8 +4968,8 @@ function parseDOMNode(node, ctx) {
specialInitTargetAttr: isRadioInput ? "checked" : null, specialInitTargetAttr: isRadioInput ? "checked" : null,
eventType, eventType,
hasDynamicChildren: false, hasDynamicChildren: false,
shouldTrim: hasTrimMod && (isOtherInput || isTextarea), shouldTrim: hasTrimMod,
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea), shouldNumberize: hasNumberMod,
}; };
if (isSelect) { if (isSelect) {
// don't pollute the original ctx // don't pollute the original ctx
@@ -5535,7 +5552,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.1.3"; const version = "2.1.4";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Scheduler // Scheduler
@@ -5545,11 +5562,18 @@ 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.
@@ -5559,21 +5583,28 @@ 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 !== 2 /* DESTROYED */ && f.node.fiber === f) { if (f.root && f.node.status !== 3 /* DESTROYED */ && f.node.fiber === f) {
f.render(); f.render();
} }
} }
} }
if (this.frame === 0) { if (this.frame === 0) {
this.frame = this.requestAnimationFrame(() => { this.frame = this.requestAnimationFrame(() => this.processTasks());
this.frame = 0; }
this.tasks.forEach((fiber) => this.processFiber(fiber)); }
for (let task of this.tasks) { processTasks() {
if (task.node.status === 2 /* DESTROYED */) { this.frame = 0;
this.tasks.delete(task); for (let node of this.cancelledNodes) {
} node._destroy();
} }
}); this.cancelledNodes.clear();
for (let task of this.tasks) {
this.processFiber(task);
}
for (let task of this.tasks) {
if (task.node.status === 3 /* DESTROYED */) {
this.tasks.delete(task);
}
} }
} }
processFiber(fiber) { processFiber(fiber) {
@@ -5586,7 +5617,7 @@ class Scheduler {
this.tasks.delete(fiber); this.tasks.delete(fiber);
return; return;
} }
if (fiber.node.status === 2 /* DESTROYED */) { if (fiber.node.status === 3 /* DESTROYED */) {
this.tasks.delete(fiber); this.tasks.delete(fiber);
return; return;
} }
@@ -5810,9 +5841,11 @@ 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 2 /* DESTROYED */: case 3 /* DESTROYED */:
return "destroyed"; return "destroyed";
} }
} }
@@ -5952,6 +5985,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-06-28T09:17:13.630Z'; __info__.date = '2023-07-18T14:07:26.565Z';
__info__.hash = '432ff44'; __info__.hash = '836e12b';
__info__.url = 'https://github.com/odoo/owl'; __info__.url = 'https://github.com/odoo/owl';
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.1.4", "version": "2.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.1.4", "version": "2.2",
"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",
+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.1.4"; export const version = "2.2";