Compare commits

..

4 Commits

Author SHA1 Message Date
Géry Debongnie 875ebdcfb0 [REL] v2.2.1
# v2.2.1

 - [FIX] compiler: allow t-out on component tag
2023-07-19 15:22:35 +02:00
Géry Debongnie 2e07799250 [FIX] compiler: allow t-out on component tag
Before this commit, the template parser would allow using t-esc on a
component tag (<MyComponent t-esc="expr"/>) but would incorrectly ignore
the component when parsing a t-out: <MyComponent t-out="expr"/> would be
parsed as <t t-out="expr"/>

This commit solves the issue, and also, moves the `t-out` parsing code
next to `t-esc` so they have the same priority relatively to other
directives.

closes #1483
2023-07-19 15:16:26 +02:00
Géry Debongnie 0d9d21a5c1 [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
2023-07-18 16:07:37 +02:00
Samuel Degueldre 836e12b1c5 [IMP] runtime: add support for Map and other iterables in t-foreach
closes: #1352
2023-07-18 13:41:07 +02:00
15 changed files with 358 additions and 180 deletions
+8 -7
View File
@@ -376,15 +376,16 @@ 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 an array (the current item will be the current value) `t-foreach` can iterate on any iterable, and also has special support for objects
or an object (the current item will be the current key). and maps, it will expose the key of the current iteration as the contents of the
`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 In addition to the name passed via t-as, `t-foreach` provides a few other useful
variables for various data points (note: `$as` will be replaced with the name variables (note: `$as` will be replaced with the name passed to `t-as`):
passed to `t-as`):
- `$as_value`: the current iteration value, identical to `$as` for lists and - `$as_value`: the current iteration value, identical to `$as` for arrays and
integers, but for objects, it provides the value (where `$as` provides the key) other iterables, but for objects and maps, it provides the value (where `$as`
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`)
+98 -66
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)];
@@ -4820,10 +4839,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) ||
@@ -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
@@ -5015,9 +5032,6 @@ 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;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -5459,19 +5473,21 @@ 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 normalizeTEsc(el) { function normalizeTEscTOut(el) {
const elements = [...el.querySelectorAll("[t-esc]")].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")); for (const d of ["t-esc", "t-out"]) {
for (const el of elements) { const elements = [...el.querySelectorAll(`[${d}]`)].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
if (el.childNodes.length) { for (const el of elements) {
throw new OwlError("Cannot have t-esc on a component that already has content"); if (el.childNodes.length) {
throw new OwlError(`Cannot have ${d} on a component that already has content`);
}
const value = el.getAttribute(d);
el.removeAttribute(d);
const t = el.ownerDocument.createElement("t");
if (value != null) {
t.setAttribute(d, value);
}
el.appendChild(t);
} }
const value = el.getAttribute("t-esc");
el.removeAttribute("t-esc");
const t = el.ownerDocument.createElement("t");
if (value != null) {
t.setAttribute("t-esc", value);
}
el.appendChild(t);
} }
} }
/** /**
@@ -5482,7 +5498,7 @@ function normalizeTEsc(el) {
*/ */
function normalizeXML(el) { function normalizeXML(el) {
normalizeTIf(el); normalizeTIf(el);
normalizeTEsc(el); normalizeTEscTOut(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
@@ -5535,7 +5551,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.2";
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Scheduler // Scheduler
@@ -5545,11 +5561,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 +5582,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 +5616,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 +5840,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 +5984,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-19T13:22:24.480Z';
__info__.hash = '432ff44'; __info__.hash = '2e07799';
__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.1",
"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.1",
"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",
+18 -19
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) ||
@@ -444,9 +444,6 @@ 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;
} }
@@ -941,21 +938,23 @@ 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 normalizeTEsc(el: Element) { function normalizeTEscTOut(el: Element) {
const elements = [...el.querySelectorAll("[t-esc]")].filter( for (const d of ["t-esc", "t-out"]) {
(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) { );
if (el.childNodes.length) { for (const el of elements) {
throw new OwlError("Cannot have t-esc on a component that already has content"); if (el.childNodes.length) {
throw new OwlError(`Cannot have ${d} on a component that already has content`);
}
const value = el.getAttribute(d);
el.removeAttribute(d);
const t = el.ownerDocument.createElement("t");
if (value != null) {
t.setAttribute(d, value);
}
el.appendChild(t);
} }
const value = el.getAttribute("t-esc");
el.removeAttribute("t-esc");
const t = el.ownerDocument.createElement("t");
if (value != null) {
t.setAttribute("t-esc", value);
}
el.appendChild(t);
} }
} }
@@ -967,7 +966,7 @@ function normalizeTEsc(el: Element) {
*/ */
function normalizeXML(el: Element) { function normalizeXML(el: Element) {
normalizeTIf(el); normalizeTIf(el);
normalizeTEsc(el); normalizeTEscTOut(el);
} }
/** /**
+19 -59
View File
@@ -93,17 +93,6 @@ function normalizeNode(node: HTMLElement | Text) {
} }
} }
/**
* Encode 2 numbers and 1 boolean in a number, using 31 bits:
* n1 => encoded in 16 most significant bits
* n2 => encoded in 15 next bits
* boolean => encoded in last significant bit.
* This code assumes that n1 and n2 are small enough to fit in that number of bits
*/
function encodeValue(n1: number, n2: number, b: boolean): number {
return (((n1 << 15) | n2) << 1) | (b ? 1 : 0);
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// building a intermediate tree // building a intermediate tree
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -317,7 +306,7 @@ interface IndexedLocation extends Location {
interface Child { interface Child {
parentRefIdx: number; parentRefIdx: number;
afterRefIdx: number; afterRefIdx?: number;
isOnlyChild?: boolean; isOnlyChild?: boolean;
} }
@@ -385,7 +374,6 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
// tree is the parentnode here // tree is the parentnode here
ctx.children[info.idx] = { ctx.children[info.idx] = {
parentRefIdx: info.refIdx!, parentRefIdx: info.refIdx!,
afterRefIdx: 0,
isOnlyChild: true, isOnlyChild: true,
}; };
} else { } else {
@@ -513,6 +501,7 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
})); }));
const locN = locations.length; const locN = locations.length;
const childN = children.length; const childN = children.length;
const childrenLocs = children;
const isDynamic = refN > 0; const isDynamic = refN > 0;
// these values are defined here to make them faster to lookup in the class // these values are defined here to make them faster to lookup in the class
@@ -567,19 +556,6 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
} }
if (isDynamic) { if (isDynamic) {
const nextSibling = nodeGetNextSibling;
const firstChild = nodeGetFirstChild;
const bitPackedCollectors = new Uint32Array(
collectors.map((c) => {
return encodeValue(c.idx, c.prevIdx, c.getVal === nextSibling);
})
);
const childrenLocs = new Uint32Array(
children.map((c) => {
return encodeValue(c.afterRefIdx, c.parentRefIdx, Boolean(c.isOnlyChild));
})
);
Block.prototype.mount = function mount(parent: HTMLElement, afterNode: Node | null) { Block.prototype.mount = function mount(parent: HTMLElement, afterNode: Node | null) {
const el = nodeCloneNode.call(template, true); const el = nodeCloneNode.call(template, true);
// collecting references // collecting references
@@ -587,17 +563,12 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
this.refs = refs; this.refs = refs;
refs[0] = el; refs[0] = el;
for (let i = 0; i < colN; i++) { for (let i = 0; i < colN; i++) {
let info = bitPackedCollectors[i]; const w = collectors[i];
// decode info refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
const fn = (info & 1) === 1 ? nextSibling : firstChild;
info = info >> 1;
const prevIdx = info & 0b111111111111111;
const idx = info >> 15;
refs[idx] = fn.call(refs[prevIdx]);
} }
// applying data to all update points // applying data to all update points
if (locN !== 0) { if (locN) {
const data = this.data!; const data = this.data!;
for (let i = 0; i < locN; i++) { for (let i = 0; i < locN; i++) {
const loc = locations[i]; const loc = locations[i];
@@ -608,21 +579,15 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
nodeInsertBefore.call(parent, el, afterNode); nodeInsertBefore.call(parent, el, afterNode);
// preparing all children // preparing all children
if (childN !== 0) { if (childN) {
const children = this.children; const children = this.children;
for (let i = 0; i < childN; i++) { for (let i = 0; i < childN; i++) {
const child = children![i]; const child = children![i];
if (child !== undefined) { if (child) {
let info = childrenLocs[i]; const loc = childrenLocs[i];
// decode info const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
const isOnlyChild = info & 1; child.isOnlyChild = loc.isOnlyChild;
info = info >> 1; child.mount(refs[loc.parentRefIdx] as any, afterNode);
const parentRefIdx = info & 0b111111111111111;
const afterRefIdx = info >> 15;
const afterNode = afterRefIdx !== 0 ? refs[afterRefIdx] : null;
child.isOnlyChild = isOnlyChild as any;
child.mount(refs[parentRefIdx] as any, afterNode);
} }
} }
} }
@@ -636,7 +601,7 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
} }
const refs = this.refs!; const refs = this.refs!;
// update texts/attributes/ // update texts/attributes/
if (locN !== 0) { if (locN) {
const data1 = this.data!; const data1 = this.data!;
const data2 = other.data!; const data2 = other.data!;
for (let i = 0; i < locN; i++) { for (let i = 0; i < locN; i++) {
@@ -651,14 +616,14 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
} }
// update children // update children
if (childN !== 0) { if (childN) {
let children1 = this.children; let children1 = this.children;
const children2 = other.children; const children2 = other.children;
for (let i = 0; i < childN; i++) { for (let i = 0; i < childN; i++) {
const child1 = children1![i]; const child1 = children1![i];
const child2 = children2![i]; const child2 = children2![i];
if (child1 !== undefined) { if (child1) {
if (child2 !== undefined) { if (child2) {
child1.patch(child2, withBeforeRemove); child1.patch(child2, withBeforeRemove);
} else { } else {
if (withBeforeRemove) { if (withBeforeRemove) {
@@ -667,15 +632,10 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
child1.remove(); child1.remove();
children1![i] = undefined; children1![i] = undefined;
} }
} else if (child2 !== undefined) { } else if (child2) {
let info = childrenLocs[i]; const loc = childrenLocs[i];
// decode info const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
info = info >> 1; child2.mount(refs[loc.parentRefIdx] as any, afterNode);
const parentRefIdx = info & 0b111111111111111;
const afterRefIdx = info >> 15;
const afterNode = afterRefIdx !== 0 ? refs[afterRefIdx] : null;
child2.mount(refs[parentRefIdx] as any, afterNode);
children1![i] = child2; children1![i] = child2;
} }
} }
+1 -1
View File
@@ -41,7 +41,7 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
} }
function remove(this: HTMLElement) { function remove(this: HTMLElement) {
(this as any)[eventKey] = false; delete (this as any)[eventKey];
this.removeEventListener(evName, listener, { capture }); this.removeEventListener(evName, listener, { capture });
} }
function update(this: HTMLElement, data: any) { function update(this: HTMLElement, data: any) {
+5 -5
View File
@@ -28,7 +28,7 @@ class VList {
this.anchor = _anchor; this.anchor = _anchor;
nodeInsertBefore.call(parent, _anchor, afterNode); nodeInsertBefore.call(parent, _anchor, afterNode);
const l = children.length; const l = children.length;
if (l !== 0) { if (l) {
const mount = children[0].mount; const mount = children[0].mount;
for (let i = 0; i < l; i++) { for (let i = 0; i < l; i++) {
mount.call(children[i], parent, _anchor); mount.call(children[i], parent, _anchor);
@@ -186,7 +186,7 @@ class VList {
} else { } else {
for (let i = startIdx1; i <= endIdx1; i++) { for (let i = startIdx1; i <= endIdx1; i++) {
let ch = ch1[i]; let ch = ch1[i];
if (ch !== null) { if (ch) {
if (withBeforeRemove) { if (withBeforeRemove) {
beforeRemove.call(ch); beforeRemove.call(ch);
} }
@@ -200,7 +200,7 @@ class VList {
beforeRemove() { beforeRemove() {
const children = this.children; const children = this.children;
const l = children.length; const l = children.length;
if (l !== 0) { if (l) {
const beforeRemove = children[0].beforeRemove; const beforeRemove = children[0].beforeRemove;
for (let i = 0; i < l; i++) { for (let i = 0; i < l; i++) {
beforeRemove.call(children[i]); beforeRemove.call(children[i]);
@@ -215,7 +215,7 @@ class VList {
} else { } else {
const children = this.children; const children = this.children;
const l = children.length; const l = children.length;
if (l !== 0) { if (l) {
const remove = children[0].remove; const remove = children[0].remove;
for (let i = 0; i < l; i++) { for (let i = 0; i < l; i++) {
remove.call(children[i]); remove.call(children[i]);
@@ -240,7 +240,7 @@ export function list(children: VNode[]): VNode<VList> {
} }
function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } { function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } {
const mapping: any = {}; let mapping: any = {};
for (let i = startIdx1; i <= endIdx2; i++) { for (let i = startIdx1; i <= endIdx2; i++) {
mapping[ch1[i].key] = i; mapping[ch1[i].key] = i;
} }
+10 -10
View File
@@ -26,7 +26,7 @@ export class VMulti {
const anchors = new Array(l); const anchors = new Array(l);
for (let i = 0; i < l; i++) { for (let i = 0; i < l; i++) {
let child = children[i]; let child = children[i];
if (child !== undefined) { if (child) {
child.mount(parent, afterNode); child.mount(parent, afterNode);
} else { } else {
const childAnchor = document.createTextNode(""); const childAnchor = document.createTextNode("");
@@ -44,7 +44,7 @@ export class VMulti {
const anchors = this.anchors; const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) { for (let i = 0, l = children.length; i < l; i++) {
let child = children[i]; let child = children[i];
if (child !== undefined) { if (child) {
child.moveBeforeDOMNode(node, parent); child.moveBeforeDOMNode(node, parent);
} else { } else {
const anchor = anchors![i]; const anchor = anchors![i];
@@ -56,14 +56,14 @@ export class VMulti {
moveBeforeVNode(other: VMulti | null, afterNode: Node | null) { moveBeforeVNode(other: VMulti | null, afterNode: Node | null) {
if (other) { if (other) {
const next = other!.children[0]; const next = other!.children[0];
afterNode = (next !== undefined ? next.firstNode() : other!.anchors![0]) || null; afterNode = (next ? next.firstNode() : other!.anchors![0]) || null;
} }
const children = this.children; const children = this.children;
const parent = this.parentEl; const parent = this.parentEl;
const anchors = this.anchors; const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) { for (let i = 0, l = children.length; i < l; i++) {
let child = children[i]; let child = children[i];
if (child !== undefined) { if (child) {
child.moveBeforeVNode(null, afterNode); child.moveBeforeVNode(null, afterNode);
} else { } else {
const anchor = anchors![i]; const anchor = anchors![i];
@@ -83,8 +83,8 @@ export class VMulti {
for (let i = 0, l = children1.length; i < l; i++) { for (let i = 0, l = children1.length; i < l; i++) {
const vn1 = children1[i]; const vn1 = children1[i];
const vn2 = children2[i]; const vn2 = children2[i];
if (vn1 !== undefined) { if (vn1) {
if (vn2 !== undefined) { if (vn2) {
vn1.patch(vn2, withBeforeRemove); vn1.patch(vn2, withBeforeRemove);
} else { } else {
const afterNode = vn1.firstNode()!; const afterNode = vn1.firstNode()!;
@@ -97,7 +97,7 @@ export class VMulti {
vn1.remove(); vn1.remove();
children1[i] = undefined; children1[i] = undefined;
} }
} else if (vn2 !== undefined) { } else if (vn2) {
children1[i] = vn2; children1[i] = vn2;
const anchor = anchors[i]; const anchor = anchors[i];
vn2.mount(parentEl, anchor); vn2.mount(parentEl, anchor);
@@ -110,7 +110,7 @@ export class VMulti {
const children = this.children; const children = this.children;
for (let i = 0, l = children.length; i < l; i++) { for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]; const child = children[i];
if (child !== undefined) { if (child) {
child.beforeRemove(); child.beforeRemove();
} }
} }
@@ -125,7 +125,7 @@ export class VMulti {
const anchors = this.anchors; const anchors = this.anchors;
for (let i = 0, l = children.length; i < l; i++) { for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]; const child = children[i];
if (child !== undefined) { if (child) {
child.remove(); child.remove();
} else { } else {
nodeRemoveChild.call(parentEl, anchors![i]); nodeRemoveChild.call(parentEl, anchors![i]);
@@ -136,7 +136,7 @@ export class VMulti {
firstNode(): Node | undefined { firstNode(): Node | undefined {
const child = this.children[0]; const child = this.children[0];
return child !== undefined ? child.firstNode() : this.anchors![0]; return child ? child.firstNode() : this.anchors![0];
} }
toString(): string { toString(): string {
+16 -8
View File
@@ -30,7 +30,7 @@ function callSlot(
const slots = ctx.props.slots || {}; const slots = ctx.props.slots || {};
const { __render, __ctx, __scope } = slots[name] || {}; const { __render, __ctx, __scope } = slots[name] || {};
const slotScope = ObjectCreate(__ctx || {}); const slotScope = ObjectCreate(__ctx || {});
if (__scope !== undefined) { if (__scope) {
slotScope[__scope] = extra; slotScope[__scope] = extra;
} }
const slotBDom = __render ? __render(slotScope, parent, key) : null; const slotBDom = __render ? __render(slotScope, parent, key) : null;
@@ -60,18 +60,26 @@ function withKey(elem: any, k: string) {
return elem; return elem;
} }
function prepareList(collection: any): [any[], any[], number, any[]] { function prepareList(collection: unknown): [unknown[], unknown[], number, undefined[]] {
let keys: any[]; let keys: unknown[];
let values: any[]; let values: unknown[];
if (Array.isArray(collection)) { if (Array.isArray(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 = [...(<Iterable<unknown>>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)];
+1 -1
View File
@@ -11,7 +11,7 @@ export type Callback = () => void;
export function batched(callback: Callback): Callback { export function batched(callback: Callback): Callback {
let scheduled = false; let scheduled = false;
return async (...args) => { return async (...args) => {
if (scheduled === false) { if (!scheduled) {
scheduled = true; scheduled = true;
await Promise.resolve(); await Promise.resolve();
scheduled = false; scheduled = false;
+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.1";
@@ -77,6 +77,62 @@ 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
) { ) {
@@ -108,6 +164,62 @@ 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
) { ) {
+6
View File
@@ -1569,6 +1569,12 @@ 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"
+61 -1
View File
@@ -105,6 +105,64 @@ 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>
@@ -193,7 +251,9 @@ 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("Invalid loop expression"); expect(() => renderToString(test)).toThrow(
'Invalid loop expression: "undefined" is not iterable'
);
}); });
test("t-foreach with t-if inside", () => { test("t-foreach with t-if inside", () => {