diff --git a/owl.js b/owl.js
index 3da6d7fe..d9972462 100644
--- a/owl.js
+++ b/owl.js
@@ -1290,29 +1290,35 @@
}
function createCatcher(eventsSpec) {
- let setupFns = [];
- let removeFns = [];
- for (let name in eventsSpec) {
- let index = eventsSpec[name];
- let { setup, remove } = createEventHandler(name);
- setupFns[index] = setup;
- removeFns[index] = remove;
- }
- let n = setupFns.length;
+ const n = Object.keys(eventsSpec).length;
class VCatcher {
constructor(child, handlers) {
+ this.handlerFns = [];
this.afterNode = null;
this.child = child;
- this.handlers = handlers;
+ this.handlerData = handlers;
}
mount(parent, afterNode) {
this.parentEl = parent;
- this.afterNode = afterNode;
this.child.mount(parent, afterNode);
+ this.afterNode = document.createTextNode("");
+ parent.insertBefore(this.afterNode, afterNode);
+ this.wrapHandlerData();
+ for (let name in eventsSpec) {
+ const index = eventsSpec[name];
+ const handler = createEventHandler(name);
+ this.handlerFns[index] = handler;
+ handler.setup.call(parent, this.handlerData[index]);
+ }
+ }
+ wrapHandlerData() {
for (let i = 0; i < n; i++) {
- let origFn = this.handlers[i][0];
+ let handler = this.handlerData[i];
+ // handler = [...mods, fn, comp], so we need to replace second to last elem
+ let idx = handler.length - 2;
+ let origFn = handler[idx];
const self = this;
- this.handlers[i][0] = function (ev) {
+ handler[idx] = function (ev) {
const target = ev.target;
let currentNode = self.child.firstNode();
const afterNode = self.afterNode;
@@ -1323,18 +1329,21 @@
currentNode = currentNode.nextSibling;
}
};
- setupFns[i].call(parent, this.handlers[i]);
}
}
moveBefore(other, afterNode) {
- this.afterNode = null;
this.child.moveBefore(other ? other.child : null, afterNode);
+ this.parentEl.insertBefore(this.afterNode, afterNode);
}
patch(other, withBeforeRemove) {
if (this === other) {
return;
}
- this.handlers = other.handlers;
+ this.handlerData = other.handlerData;
+ this.wrapHandlerData();
+ for (let i = 0; i < n; i++) {
+ this.handlerFns[i].update.call(this.parentEl, this.handlerData[i]);
+ }
this.child.patch(other.child, withBeforeRemove);
}
beforeRemove() {
@@ -1342,9 +1351,10 @@
}
remove() {
for (let i = 0; i < n; i++) {
- removeFns[i].call(this.parentEl);
+ this.handlerFns[i].remove.call(this.parentEl);
}
this.child.remove();
+ this.afterNode.remove();
}
firstNode() {
return this.child.firstNode();
@@ -1371,50 +1381,301 @@
vnode.remove();
}
- const mainEventHandler = (data, ev, currentTarget) => {
- const { data: _data, modifiers } = filterOutModifiersFromData(data);
- data = _data;
- let stopped = false;
- if (modifiers.length) {
- let selfMode = false;
- const isSelf = ev.target === currentTarget;
- for (const mod of modifiers) {
- switch (mod) {
- case "self":
- selfMode = true;
- if (isSelf) {
- continue;
- }
- else {
- return stopped;
- }
- case "prevent":
- if ((selfMode && isSelf) || !selfMode)
- ev.preventDefault();
- continue;
- case "stop":
- if ((selfMode && isSelf) || !selfMode)
- ev.stopPropagation();
- stopped = true;
- continue;
+ // Maps fibers to thrown errors
+ const fibersInError = new WeakMap();
+ const nodeErrorHandlers = new WeakMap();
+ function _handleError(node, error) {
+ if (!node) {
+ return false;
+ }
+ const fiber = node.fiber;
+ if (fiber) {
+ fibersInError.set(fiber, error);
+ }
+ const errorHandlers = nodeErrorHandlers.get(node);
+ if (errorHandlers) {
+ let handled = false;
+ // execute in the opposite order
+ for (let i = errorHandlers.length - 1; i >= 0; i--) {
+ try {
+ errorHandlers[i](error);
+ handled = true;
+ break;
+ }
+ catch (e) {
+ error = e;
}
}
- }
- // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
- // We check this rather than data[0] being truthy (or typeof function) so that it crashes
- // as expected when there is a handler expression that evaluates to a falsy value
- if (Object.hasOwnProperty.call(data, 0)) {
- const handler = data[0];
- if (typeof handler !== "function") {
- throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
- }
- let node = data[1] ? data[1].__owl__ : null;
- if (node ? node.status === 1 /* MOUNTED */ : true) {
- handler.call(node ? node.component : null, ev);
+ if (handled) {
+ return true;
}
}
- return stopped;
- };
+ return _handleError(node.parent, error);
+ }
+ function handleError(params) {
+ const error = params.error;
+ const node = "node" in params ? params.node : params.fiber.node;
+ const fiber = "fiber" in params ? params.fiber : node.fiber;
+ // resets the fibers on components if possible. This is important so that
+ // new renderings can be properly included in the initial one, if any.
+ let current = fiber;
+ do {
+ current.node.fiber = current;
+ current = current.parent;
+ } while (current);
+ fibersInError.set(fiber.root, error);
+ const handled = _handleError(node, error);
+ if (!handled) {
+ console.warn(`[Owl] Unhandled error. Destroying the root component`);
+ try {
+ node.app.destroy();
+ }
+ catch (e) {
+ console.error(e);
+ }
+ }
+ }
+
+ function makeChildFiber(node, parent) {
+ let current = node.fiber;
+ if (current) {
+ cancelFibers(current.children);
+ current.root = null;
+ }
+ return new Fiber(node, parent);
+ }
+ function makeRootFiber(node) {
+ let current = node.fiber;
+ if (current) {
+ let root = current.root;
+ // lock root fiber because canceling children fibers may destroy components,
+ // which means any arbitrary code can be run in onWillDestroy, which may
+ // trigger new renderings
+ root.locked = true;
+ root.setCounter(root.counter + 1 - cancelFibers(current.children));
+ root.locked = false;
+ current.children = [];
+ current.childrenMap = {};
+ current.bdom = null;
+ if (fibersInError.has(current)) {
+ fibersInError.delete(current);
+ fibersInError.delete(root);
+ current.appliedToDom = false;
+ }
+ return current;
+ }
+ const fiber = new RootFiber(node, null);
+ if (node.willPatch.length) {
+ fiber.willPatch.push(fiber);
+ }
+ if (node.patched.length) {
+ fiber.patched.push(fiber);
+ }
+ return fiber;
+ }
+ function throwOnRender() {
+ throw new Error("Attempted to render cancelled fiber");
+ }
+ /**
+ * @returns number of not-yet rendered fibers cancelled
+ */
+ function cancelFibers(fibers) {
+ let result = 0;
+ for (let fiber of fibers) {
+ let node = fiber.node;
+ fiber.render = throwOnRender;
+ if (node.status === 0 /* NEW */) {
+ node.destroy();
+ }
+ node.fiber = null;
+ if (fiber.bdom) {
+ // if fiber has been rendered, this means that the component props have
+ // been updated. however, this fiber will not be patched to the dom, so
+ // it could happen that the next render compare the current props with
+ // the same props, and skip the render completely. With the next line,
+ // we kindly request the component code to force a render, so it works as
+ // expected.
+ node.forceNextRender = true;
+ }
+ else {
+ result++;
+ }
+ result += cancelFibers(fiber.children);
+ }
+ return result;
+ }
+ class Fiber {
+ constructor(node, parent) {
+ this.bdom = null;
+ this.children = [];
+ this.appliedToDom = false;
+ this.deep = false;
+ this.childrenMap = {};
+ this.node = node;
+ this.parent = parent;
+ if (parent) {
+ this.deep = parent.deep;
+ const root = parent.root;
+ root.setCounter(root.counter + 1);
+ this.root = root;
+ parent.children.push(this);
+ }
+ else {
+ this.root = this;
+ }
+ }
+ render() {
+ // if some parent has a fiber => register in followup
+ let prev = this.root.node;
+ let scheduler = prev.app.scheduler;
+ let current = prev.parent;
+ while (current) {
+ if (current.fiber) {
+ let root = current.fiber.root;
+ if (root.counter === 0 && prev.parentKey in current.fiber.childrenMap) {
+ current = root.node;
+ }
+ else {
+ scheduler.delayedRenders.push(this);
+ return;
+ }
+ }
+ prev = current;
+ current = current.parent;
+ }
+ // there are no current rendering from above => we can render
+ this._render();
+ }
+ _render() {
+ const node = this.node;
+ const root = this.root;
+ if (root) {
+ try {
+ this.bdom = true;
+ this.bdom = node.renderFn();
+ }
+ catch (e) {
+ handleError({ node, error: e });
+ }
+ root.setCounter(root.counter - 1);
+ }
+ }
+ }
+ class RootFiber extends Fiber {
+ constructor() {
+ super(...arguments);
+ this.counter = 1;
+ // only add stuff in this if they have registered some hooks
+ this.willPatch = [];
+ this.patched = [];
+ this.mounted = [];
+ // A fiber is typically locked when it is completing and the patch has not, or is being applied.
+ // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
+ this.locked = false;
+ }
+ complete() {
+ const node = this.node;
+ this.locked = true;
+ let current = undefined;
+ try {
+ // Step 1: calling all willPatch lifecycle hooks
+ for (current of this.willPatch) {
+ // because of the asynchronous nature of the rendering, some parts of the
+ // UI may have been rendered, then deleted in a followup rendering, and we
+ // do not want to call onWillPatch in that case.
+ let node = current.node;
+ if (node.fiber === current) {
+ const component = node.component;
+ for (let cb of node.willPatch) {
+ cb.call(component);
+ }
+ }
+ }
+ current = undefined;
+ // Step 2: patching the dom
+ node._patch();
+ this.locked = false;
+ // Step 4: calling all mounted lifecycle hooks
+ let mountedFibers = this.mounted;
+ while ((current = mountedFibers.pop())) {
+ current = current;
+ if (current.appliedToDom) {
+ for (let cb of current.node.mounted) {
+ cb();
+ }
+ }
+ }
+ // Step 5: calling all patched hooks
+ let patchedFibers = this.patched;
+ while ((current = patchedFibers.pop())) {
+ current = current;
+ if (current.appliedToDom) {
+ for (let cb of current.node.patched) {
+ cb();
+ }
+ }
+ }
+ }
+ catch (e) {
+ this.locked = false;
+ handleError({ fiber: current || this, error: e });
+ }
+ }
+ setCounter(newValue) {
+ this.counter = newValue;
+ if (newValue === 0) {
+ this.node.app.scheduler.flush();
+ }
+ }
+ }
+ class MountFiber extends RootFiber {
+ constructor(node, target, options = {}) {
+ super(node, null);
+ this.target = target;
+ this.position = options.position || "last-child";
+ }
+ complete() {
+ let current = this;
+ try {
+ const node = this.node;
+ node.children = this.childrenMap;
+ node.app.constructor.validateTarget(this.target);
+ if (node.bdom) {
+ // this is a complicated situation: if we mount a fiber with an existing
+ // bdom, this means that this same fiber was already completed, mounted,
+ // but a crash occurred in some mounted hook. Then, it was handled and
+ // the new rendering is being applied.
+ node.updateDom();
+ }
+ else {
+ node.bdom = this.bdom;
+ if (this.position === "last-child" || this.target.childNodes.length === 0) {
+ mount$1(node.bdom, this.target);
+ }
+ else {
+ const firstChild = this.target.childNodes[0];
+ mount$1(node.bdom, this.target, firstChild);
+ }
+ }
+ // unregistering the fiber before mounted since it can do another render
+ // and that the current rendering is obviously completed
+ node.fiber = null;
+ node.status = 1 /* MOUNTED */;
+ this.appliedToDom = true;
+ let mountedFibers = this.mounted;
+ while ((current = mountedFibers.pop())) {
+ if (current.appliedToDom) {
+ for (let cb of current.node.mounted) {
+ cb();
+ }
+ }
+ }
+ }
+ catch (e) {
+ handleError({ fiber: current, error: e });
+ }
+ }
+ }
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
const TARGET = Symbol("Target");
@@ -1599,7 +1860,7 @@
return reactive(originalTarget, callback);
}
if (!reactiveCache.has(target)) {
- reactiveCache.set(target, new Map());
+ reactiveCache.set(target, new WeakMap());
}
const reactivesForTarget = reactiveCache.get(target);
if (!reactivesForTarget.has(callback)) {
@@ -1899,452 +2160,6 @@
*/
function markup(value) {
return new Markup(value);
- }
- // -----------------------------------------------------------------------------
- // xml tag helper
- // -----------------------------------------------------------------------------
- const globalTemplates = {};
- function xml(...args) {
- const name = `__template__${xml.nextId++}`;
- const value = String.raw(...args);
- globalTemplates[name] = value;
- return name;
- }
- xml.nextId = 1;
-
- // Maps fibers to thrown errors
- const fibersInError = new WeakMap();
- const nodeErrorHandlers = new WeakMap();
- function _handleError(node, error) {
- if (!node) {
- return false;
- }
- const fiber = node.fiber;
- if (fiber) {
- fibersInError.set(fiber, error);
- }
- const errorHandlers = nodeErrorHandlers.get(node);
- if (errorHandlers) {
- let handled = false;
- // execute in the opposite order
- for (let i = errorHandlers.length - 1; i >= 0; i--) {
- try {
- errorHandlers[i](error);
- handled = true;
- break;
- }
- catch (e) {
- error = e;
- }
- }
- if (handled) {
- return true;
- }
- }
- return _handleError(node.parent, error);
- }
- function handleError(params) {
- const error = params.error;
- const node = "node" in params ? params.node : params.fiber.node;
- const fiber = "fiber" in params ? params.fiber : node.fiber;
- // resets the fibers on components if possible. This is important so that
- // new renderings can be properly included in the initial one, if any.
- let current = fiber;
- do {
- current.node.fiber = current;
- current = current.parent;
- } while (current);
- fibersInError.set(fiber.root, error);
- const handled = _handleError(node, error);
- if (!handled) {
- console.warn(`[Owl] Unhandled error. Destroying the root component`);
- try {
- node.app.destroy();
- }
- catch (e) {
- console.error(e);
- }
- }
- }
-
- function makeChildFiber(node, parent) {
- let current = node.fiber;
- if (current) {
- cancelFibers(current.children);
- current.root = null;
- }
- return new Fiber(node, parent);
- }
- function makeRootFiber(node) {
- let current = node.fiber;
- if (current) {
- let root = current.root;
- // lock root fiber because canceling children fibers may destroy components,
- // which means any arbitrary code can be run in onWillDestroy, which may
- // trigger new renderings
- root.locked = true;
- root.setCounter(root.counter + 1 - cancelFibers(current.children));
- root.locked = false;
- current.children = [];
- current.childrenMap = {};
- current.bdom = null;
- if (fibersInError.has(current)) {
- fibersInError.delete(current);
- fibersInError.delete(root);
- current.appliedToDom = false;
- }
- return current;
- }
- const fiber = new RootFiber(node, null);
- if (node.willPatch.length) {
- fiber.willPatch.push(fiber);
- }
- if (node.patched.length) {
- fiber.patched.push(fiber);
- }
- return fiber;
- }
- function throwOnRender() {
- throw new Error("Attempted to render cancelled fiber");
- }
- /**
- * @returns number of not-yet rendered fibers cancelled
- */
- function cancelFibers(fibers) {
- let result = 0;
- for (let fiber of fibers) {
- let node = fiber.node;
- fiber.render = throwOnRender;
- if (node.status === 0 /* NEW */) {
- node.destroy();
- }
- node.fiber = null;
- if (fiber.bdom) {
- // if fiber has been rendered, this means that the component props have
- // been updated. however, this fiber will not be patched to the dom, so
- // it could happen that the next render compare the current props with
- // the same props, and skip the render completely. With the next line,
- // we kindly request the component code to force a render, so it works as
- // expected.
- node.forceNextRender = true;
- }
- else {
- result++;
- }
- result += cancelFibers(fiber.children);
- }
- return result;
- }
- class Fiber {
- constructor(node, parent) {
- this.bdom = null;
- this.children = [];
- this.appliedToDom = false;
- this.deep = false;
- this.childrenMap = {};
- this.node = node;
- this.parent = parent;
- if (parent) {
- this.deep = parent.deep;
- const root = parent.root;
- root.setCounter(root.counter + 1);
- this.root = root;
- parent.children.push(this);
- }
- else {
- this.root = this;
- }
- }
- render() {
- // if some parent has a fiber => register in followup
- let prev = this.root.node;
- let scheduler = prev.app.scheduler;
- let current = prev.parent;
- while (current) {
- if (current.fiber) {
- let root = current.fiber.root;
- if (root.counter === 0 && prev.parentKey in current.fiber.childrenMap) {
- current = root.node;
- }
- else {
- scheduler.delayedRenders.push(this);
- return;
- }
- }
- prev = current;
- current = current.parent;
- }
- // there are no current rendering from above => we can render
- this._render();
- }
- _render() {
- const node = this.node;
- const root = this.root;
- if (root) {
- try {
- this.bdom = true;
- this.bdom = node.renderFn();
- }
- catch (e) {
- handleError({ node, error: e });
- }
- root.setCounter(root.counter - 1);
- }
- }
- }
- class RootFiber extends Fiber {
- constructor() {
- super(...arguments);
- this.counter = 1;
- // only add stuff in this if they have registered some hooks
- this.willPatch = [];
- this.patched = [];
- this.mounted = [];
- // A fiber is typically locked when it is completing and the patch has not, or is being applied.
- // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
- this.locked = false;
- }
- complete() {
- const node = this.node;
- this.locked = true;
- let current = undefined;
- try {
- // Step 1: calling all willPatch lifecycle hooks
- for (current of this.willPatch) {
- // because of the asynchronous nature of the rendering, some parts of the
- // UI may have been rendered, then deleted in a followup rendering, and we
- // do not want to call onWillPatch in that case.
- let node = current.node;
- if (node.fiber === current) {
- const component = node.component;
- for (let cb of node.willPatch) {
- cb.call(component);
- }
- }
- }
- current = undefined;
- // Step 2: patching the dom
- node._patch();
- this.locked = false;
- // Step 4: calling all mounted lifecycle hooks
- let mountedFibers = this.mounted;
- while ((current = mountedFibers.pop())) {
- current = current;
- if (current.appliedToDom) {
- for (let cb of current.node.mounted) {
- cb();
- }
- }
- }
- // Step 5: calling all patched hooks
- let patchedFibers = this.patched;
- while ((current = patchedFibers.pop())) {
- current = current;
- if (current.appliedToDom) {
- for (let cb of current.node.patched) {
- cb();
- }
- }
- }
- }
- catch (e) {
- this.locked = false;
- handleError({ fiber: current || this, error: e });
- }
- }
- setCounter(newValue) {
- this.counter = newValue;
- if (newValue === 0) {
- this.node.app.scheduler.flush();
- }
- }
- }
- class MountFiber extends RootFiber {
- constructor(node, target, options = {}) {
- super(node, null);
- this.target = target;
- this.position = options.position || "last-child";
- }
- complete() {
- let current = this;
- try {
- const node = this.node;
- node.children = this.childrenMap;
- node.app.constructor.validateTarget(this.target);
- if (node.bdom) {
- // this is a complicated situation: if we mount a fiber with an existing
- // bdom, this means that this same fiber was already completed, mounted,
- // but a crash occurred in some mounted hook. Then, it was handled and
- // the new rendering is being applied.
- node.updateDom();
- }
- else {
- node.bdom = this.bdom;
- if (this.position === "last-child" || this.target.childNodes.length === 0) {
- mount$1(node.bdom, this.target);
- }
- else {
- const firstChild = this.target.childNodes[0];
- mount$1(node.bdom, this.target, firstChild);
- }
- }
- // unregistering the fiber before mounted since it can do another render
- // and that the current rendering is obviously completed
- node.fiber = null;
- node.status = 1 /* MOUNTED */;
- this.appliedToDom = true;
- let mountedFibers = this.mounted;
- while ((current = mountedFibers.pop())) {
- if (current.appliedToDom) {
- for (let cb of current.node.mounted) {
- cb();
- }
- }
- }
- }
- catch (e) {
- handleError({ fiber: current, error: e });
- }
- }
- }
-
- /**
- * Apply default props (only top level).
- *
- * Note that this method does modify in place the props
- */
- function applyDefaultProps(props, ComponentClass) {
- const defaultProps = ComponentClass.defaultProps;
- if (defaultProps) {
- for (let propName in defaultProps) {
- if (props[propName] === undefined) {
- props[propName] = defaultProps[propName];
- }
- }
- }
- }
- //------------------------------------------------------------------------------
- // Prop validation helper
- //------------------------------------------------------------------------------
- function getPropDescription(staticProps) {
- if (staticProps instanceof Array) {
- return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true])));
- }
- return staticProps || { "*": true };
- }
- /**
- * Validate the component props (or next props) against the (static) props
- * description. This is potentially an expensive operation: it may needs to
- * visit recursively the props and all the children to check if they are valid.
- * This is why it is only done in 'dev' mode.
- */
- function validateProps(name, props, parent) {
- const ComponentClass = typeof name !== "string"
- ? name
- : parent.constructor.components[name];
- if (!ComponentClass) {
- // this is an error, wrong component. We silently return here instead so the
- // error is triggered by the usual path ('component' function)
- return;
- }
- applyDefaultProps(props, ComponentClass);
- const defaultProps = ComponentClass.defaultProps || {};
- let propsDef = getPropDescription(ComponentClass.props);
- const allowAdditionalProps = "*" in propsDef;
- for (let propName in propsDef) {
- if (propName === "*") {
- continue;
- }
- const propDef = propsDef[propName];
- let isMandatory = !!propDef;
- if (typeof propDef === "object" && "optional" in propDef) {
- isMandatory = !propDef.optional;
- }
- if (isMandatory && propName in defaultProps) {
- throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`);
- }
- if (props[propName] === undefined) {
- if (isMandatory) {
- throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
- }
- else {
- continue;
- }
- }
- let isValid;
- try {
- isValid = isValidProp(props[propName], propDef);
- }
- catch (e) {
- e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
- throw e;
- }
- if (!isValid) {
- throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
- }
- }
- if (!allowAdditionalProps) {
- for (let propName in props) {
- if (!(propName in propsDef)) {
- throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
- }
- }
- }
- }
- /**
- * Check if an invidual prop value matches its (static) prop definition
- */
- function isValidProp(prop, propDef) {
- if (propDef === true) {
- return true;
- }
- if (typeof propDef === "function") {
- // Check if a value is constructed by some Constructor. Note that there is a
- // slight abuse of language: we want to consider primitive values as well.
- //
- // So, even though 1 is not an instance of Number, we want to consider that
- // it is valid.
- if (typeof prop === "object") {
- return prop instanceof propDef;
- }
- return typeof prop === propDef.name.toLowerCase();
- }
- else if (propDef instanceof Array) {
- // If this code is executed, this means that we want to check if a prop
- // matches at least one of its descriptor.
- let result = false;
- for (let i = 0, iLen = propDef.length; i < iLen; i++) {
- result = result || isValidProp(prop, propDef[i]);
- }
- return result;
- }
- // propsDef is an object
- if (propDef.optional && prop === undefined) {
- return true;
- }
- let result = propDef.type ? isValidProp(prop, propDef.type) : true;
- if (propDef.validate) {
- result = result && propDef.validate(prop);
- }
- if (propDef.type === Array && propDef.element) {
- for (let i = 0, iLen = prop.length; i < iLen; i++) {
- result = result && isValidProp(prop[i], propDef.element);
- }
- }
- if (propDef.type === Object && propDef.shape) {
- const shape = propDef.shape;
- for (let key in shape) {
- result = result && isValidProp(prop[key], shape[key]);
- }
- if (result) {
- for (let propName in prop) {
- if (!(propName in shape)) {
- throw new Error(`unknown prop '${propName}'`);
- }
- }
- }
- }
- return result;
}
let currentNode = null;
@@ -2357,6 +2172,16 @@
function useComponent() {
return currentNode.component;
}
+ /**
+ * Apply default props (only top level).
+ */
+ function applyDefaultProps(props, defaultProps) {
+ for (let propName in defaultProps) {
+ if (props[propName] === undefined) {
+ props[propName] = defaultProps[propName];
+ }
+ }
+ }
// -----------------------------------------------------------------------------
// Integration with reactivity system (useState)
// -----------------------------------------------------------------------------
@@ -2382,56 +2207,6 @@
}
return reactive(state, render);
}
- function arePropsDifferent(props1, props2) {
- for (let k in props1) {
- if (props1[k] !== props2[k]) {
- return true;
- }
- }
- return Object.keys(props1).length !== Object.keys(props2).length;
- }
- function component(name, props, key, ctx, parent) {
- let node = ctx.children[key];
- let isDynamic = typeof name !== "string";
- if (node && node.status === 2 /* DESTROYED */) {
- node = undefined;
- }
- if (isDynamic && node && node.component.constructor !== name) {
- node = undefined;
- }
- const parentFiber = ctx.fiber;
- if (node) {
- let shouldRender = node.forceNextRender;
- if (shouldRender) {
- node.forceNextRender = false;
- }
- else {
- const currentProps = node.component.props[TARGET];
- shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
- }
- if (shouldRender) {
- node.updateAndRender(props, parentFiber);
- }
- }
- else {
- // new component
- let C;
- if (isDynamic) {
- C = name;
- }
- else {
- C = parent.constructor.components[name];
- if (!C) {
- throw new Error(`Cannot find the definition of component "${name}"`);
- }
- }
- node = new ComponentNode(C, props, ctx.app, ctx, key);
- ctx.children[key] = node;
- node.initiateRender(new Fiber(node, parentFiber));
- }
- parentFiber.childrenMap[key] = node;
- return node;
- }
class ComponentNode {
constructor(C, props, app, parent, parentKey) {
this.fiber = null;
@@ -2450,12 +2225,22 @@
currentNode = this;
this.app = app;
this.parent = parent;
+ this.props = props;
this.parentKey = parentKey;
this.level = parent ? parent.level + 1 : 0;
- applyDefaultProps(props, C);
+ const defaultProps = C.defaultProps;
+ props = Object.assign({}, props);
+ if (defaultProps) {
+ applyDefaultProps(props, defaultProps);
+ }
const env = (parent && parent.childEnv) || app.env;
this.childEnv = env;
- props = useState(props);
+ for (const key in props) {
+ const prop = props[key];
+ if (prop && typeof prop === "object" && prop[TARGET]) {
+ props[key] = useState(prop);
+ }
+ }
this.component = new C(props, env, this);
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
this.component.setup();
@@ -2557,13 +2342,23 @@
this.status = 2 /* DESTROYED */;
}
async updateAndRender(props, parentFiber) {
+ const rawProps = props;
+ props = Object.assign({}, props);
// update
const fiber = makeChildFiber(this, parentFiber);
this.fiber = fiber;
const component = this.component;
- applyDefaultProps(props, component.constructor);
+ const defaultProps = component.constructor.defaultProps;
+ if (defaultProps) {
+ applyDefaultProps(props, defaultProps);
+ }
currentNode = this;
- props = useState(props);
+ for (const key in props) {
+ const prop = props[key];
+ if (prop && typeof prop === "object" && prop[TARGET]) {
+ props[key] = useState(prop);
+ }
+ }
currentNode = null;
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
@@ -2571,6 +2366,7 @@
return;
}
component.props = props;
+ this.props = rawProps;
fiber.render();
const parentRoot = parentFiber.root;
if (this.willPatch.length) {
@@ -2633,10 +2429,15 @@
}
}
_patch() {
- const hasChildren = Object.keys(this.children).length > 0;
- this.children = this.fiber.childrenMap;
- this.bdom.patch(this.fiber.bdom, hasChildren);
- this.fiber.appliedToDom = true;
+ let hasChildren = false;
+ for (let _k in this.children) {
+ hasChildren = true;
+ break;
+ }
+ const fiber = this.fiber;
+ this.children = fiber.childrenMap;
+ this.bdom.patch(fiber.bdom, hasChildren);
+ fiber.appliedToDom = true;
this.fiber = null;
}
beforeRemove() {
@@ -2657,70 +2458,678 @@
}
}
- // -----------------------------------------------------------------------------
- // Scheduler
- // -----------------------------------------------------------------------------
- class Scheduler {
- constructor() {
- this.tasks = new Set();
- this.frame = 0;
- this.delayedRenders = [];
- this.requestAnimationFrame = Scheduler.requestAnimationFrame;
- }
- addFiber(fiber) {
- this.tasks.add(fiber.root);
- }
- /**
- * Process all current tasks. This only applies to the fibers that are ready.
- * Other tasks are left unchanged.
- */
- flush() {
- if (this.delayedRenders.length) {
- let renders = this.delayedRenders;
- this.delayedRenders = [];
- for (let f of renders) {
- if (f.root && f.node.status !== 2 /* DESTROYED */ && f.node.fiber === f) {
- f.render();
+ const TIMEOUT = Symbol("timeout");
+ function wrapError(fn, hookName) {
+ const error = new Error(`The following error occurred in ${hookName}: `);
+ const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
+ const node = getCurrent();
+ return (...args) => {
+ try {
+ const result = fn(...args);
+ if (result instanceof Promise) {
+ if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
+ const fiber = node.fiber;
+ Promise.race([
+ result,
+ new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
+ ]).then((res) => {
+ if (res === TIMEOUT && node.fiber === fiber) {
+ console.warn(timeoutError);
+ }
+ });
}
- }
- }
- if (this.frame === 0) {
- this.frame = this.requestAnimationFrame(() => {
- this.frame = 0;
- this.tasks.forEach((fiber) => this.processFiber(fiber));
- for (let task of this.tasks) {
- if (task.node.status === 2 /* DESTROYED */) {
- this.tasks.delete(task);
+ return result.catch((cause) => {
+ error.cause = cause;
+ if (cause instanceof Error) {
+ error.message += `"${cause.message}"`;
}
- }
- });
+ throw error;
+ });
+ }
+ return result;
+ }
+ catch (cause) {
+ if (cause instanceof Error) {
+ error.message += `"${cause.message}"`;
+ }
+ throw error;
+ }
+ };
+ }
+ // -----------------------------------------------------------------------------
+ // hooks
+ // -----------------------------------------------------------------------------
+ function onWillStart(fn) {
+ const node = getCurrent();
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
+ node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
+ }
+ function onWillUpdateProps(fn) {
+ const node = getCurrent();
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
+ node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
+ }
+ function onMounted(fn) {
+ const node = getCurrent();
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
+ node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
+ }
+ function onWillPatch(fn) {
+ const node = getCurrent();
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
+ node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
+ }
+ function onPatched(fn) {
+ const node = getCurrent();
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
+ node.patched.push(decorate(fn.bind(node.component), "onPatched"));
+ }
+ function onWillUnmount(fn) {
+ const node = getCurrent();
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
+ node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
+ }
+ function onWillDestroy(fn) {
+ const node = getCurrent();
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
+ node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
+ }
+ function onWillRender(fn) {
+ const node = getCurrent();
+ const renderFn = node.renderFn;
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
+ fn = decorate(fn.bind(node.component), "onWillRender");
+ node.renderFn = () => {
+ fn();
+ return renderFn();
+ };
+ }
+ function onRendered(fn) {
+ const node = getCurrent();
+ const renderFn = node.renderFn;
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
+ fn = decorate(fn.bind(node.component), "onRendered");
+ node.renderFn = () => {
+ const result = renderFn();
+ fn();
+ return result;
+ };
+ }
+ function onError(callback) {
+ const node = getCurrent();
+ let handlers = nodeErrorHandlers.get(node);
+ if (!handlers) {
+ handlers = [];
+ nodeErrorHandlers.set(node, handlers);
+ }
+ handlers.push(callback.bind(node.component));
+ }
+
+ class Component {
+ constructor(props, env, node) {
+ this.props = props;
+ this.env = env;
+ this.__owl__ = node;
+ }
+ setup() { }
+ render(deep = false) {
+ this.__owl__.render(deep === true);
+ }
+ }
+ Component.template = "";
+
+ const VText = text("").constructor;
+ class VPortal extends VText {
+ constructor(selector, realBDom) {
+ super("");
+ this.target = null;
+ this.selector = selector;
+ this.realBDom = realBDom;
+ }
+ mount(parent, anchor) {
+ super.mount(parent, anchor);
+ this.target = document.querySelector(this.selector);
+ if (!this.target) {
+ let el = this.el;
+ while (el && el.parentElement instanceof HTMLElement) {
+ el = el.parentElement;
+ }
+ this.target = el && el.querySelector(this.selector);
+ if (!this.target) {
+ throw new Error("invalid portal target");
+ }
+ }
+ this.realBDom.mount(this.target, null);
+ }
+ beforeRemove() {
+ this.realBDom.beforeRemove();
+ }
+ remove() {
+ if (this.realBDom) {
+ super.remove();
+ this.realBDom.remove();
+ this.realBDom = null;
}
}
- processFiber(fiber) {
- if (fiber.root !== fiber) {
- this.tasks.delete(fiber);
- return;
+ patch(other) {
+ super.patch(other);
+ if (this.realBDom) {
+ this.realBDom.patch(other.realBDom, true);
}
- const hasError = fibersInError.has(fiber);
- if (hasError && fiber.counter !== 0) {
- this.tasks.delete(fiber);
- return;
- }
- if (fiber.node.status === 2 /* DESTROYED */) {
- this.tasks.delete(fiber);
- return;
- }
- if (fiber.counter === 0) {
- if (!hasError) {
- fiber.complete();
- }
- this.tasks.delete(fiber);
+ else {
+ this.realBDom = other.realBDom;
+ this.realBDom.mount(this.target, null);
}
}
}
- // capture the value of requestAnimationFrame as soon as possible, to avoid
- // interactions with other code, such as test frameworks that override them
- Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
+ /**
+ *
+ */
+ function portalTemplate(app, bdom, helpers) {
+ let { callSlot } = helpers;
+ return function template(ctx, node, key = "") {
+ return callSlot(ctx, node, key, "default", false, null);
+ };
+ }
+ class Portal extends Component {
+ setup() {
+ const node = this.__owl__;
+ const renderFn = node.renderFn;
+ node.renderFn = () => new VPortal(this.props.target, renderFn());
+ onWillUnmount(() => {
+ if (node.bdom) {
+ node.bdom.remove();
+ }
+ });
+ }
+ }
+ Portal.template = "__portal__";
+ Portal.props = {
+ target: {
+ type: String,
+ },
+ slots: true,
+ };
+
+ // -----------------------------------------------------------------------------
+ // helpers
+ // -----------------------------------------------------------------------------
+ const isUnionType = (t) => Array.isArray(t);
+ const isBaseType = (t) => typeof t !== "object";
+ const isValueType = (t) => typeof t === "object" && t && "value" in t;
+ function isOptional(t) {
+ return typeof t === "object" && "optional" in t ? t.optional || false : false;
+ }
+ function describeType(type) {
+ return type === "*" || type === true ? "value" : type.name.toLowerCase();
+ }
+ function describe(info) {
+ if (isBaseType(info)) {
+ return describeType(info);
+ }
+ else if (isUnionType(info)) {
+ return info.map(describe).join(" or ");
+ }
+ else if (isValueType(info)) {
+ return String(info.value);
+ }
+ if ("element" in info) {
+ return `list of ${describe({ type: info.element, optional: false })}s`;
+ }
+ if ("shape" in info) {
+ return `object`;
+ }
+ return describe(info.type || "*");
+ }
+ function toSchema(spec) {
+ return Object.fromEntries(spec.map((e) => e.endsWith("?") ? [e.slice(0, -1), { optional: true }] : [e, { type: "*", optional: false }]));
+ }
+ /**
+ * Main validate function
+ */
+ function validate(obj, spec) {
+ let errors = validateSchema(obj, spec);
+ if (errors.length) {
+ throw new Error("Invalid object: " + errors.join(", "));
+ }
+ }
+ /**
+ * Helper validate function, to get the list of errors. useful if one want to
+ * manipulate the errors without parsing an error object
+ */
+ function validateSchema(obj, schema) {
+ if (Array.isArray(schema)) {
+ schema = toSchema(schema);
+ }
+ let errors = [];
+ // check if each value in obj has correct shape
+ for (let key in obj) {
+ if (key in schema) {
+ let result = validateType(key, obj[key], schema[key]);
+ if (result) {
+ errors.push(result);
+ }
+ }
+ else if (!("*" in schema)) {
+ errors.push(`unknown key '${key}'`);
+ }
+ }
+ // check that all specified keys are defined in obj
+ for (let key in schema) {
+ const spec = schema[key];
+ if (key !== "*" && !isOptional(spec) && !(key in obj)) {
+ const isObj = typeof spec === "object" && !Array.isArray(spec);
+ const isAny = spec === "*" || (isObj && "type" in spec ? spec.type === "*" : isObj);
+ let detail = isAny ? "" : ` (should be a ${describe(spec)})`;
+ errors.push(`'${key}' is missing${detail}`);
+ }
+ }
+ return errors;
+ }
+ function validateBaseType(key, value, type) {
+ if (typeof type === "function") {
+ if (typeof value === "object") {
+ if (!(value instanceof type)) {
+ return `'${key}' is not a ${describeType(type)}`;
+ }
+ }
+ else if (typeof value !== type.name.toLowerCase()) {
+ return `'${key}' is not a ${describeType(type)}`;
+ }
+ }
+ return null;
+ }
+ function validateArrayType(key, value, descr) {
+ if (!Array.isArray(value)) {
+ return `'${key}' is not a list of ${describe(descr)}s`;
+ }
+ for (let i = 0; i < value.length; i++) {
+ const error = validateType(`${key}[${i}]`, value[i], descr);
+ if (error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ function validateType(key, value, descr) {
+ if (value === undefined) {
+ return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
+ }
+ else if (isBaseType(descr)) {
+ return validateBaseType(key, value, descr);
+ }
+ else if (isValueType(descr)) {
+ return value === descr.value ? null : `'${key}' is not equal to '${descr.value}'`;
+ }
+ else if (isUnionType(descr)) {
+ let validDescr = descr.find((p) => !validateType(key, value, p));
+ return validDescr ? null : `'${key}' is not a ${describe(descr)}`;
+ }
+ let result = null;
+ if ("element" in descr) {
+ result = validateArrayType(key, value, descr.element);
+ }
+ else if ("shape" in descr && !result) {
+ if (typeof value !== "object" || Array.isArray(value)) {
+ result = `'${key}' is not an object`;
+ }
+ else {
+ const errors = validateSchema(value, descr.shape);
+ if (errors.length) {
+ result = `'${key}' has not the correct shape (${errors.join(", ")})`;
+ }
+ }
+ }
+ if ("type" in descr && !result) {
+ result = validateType(key, value, descr.type);
+ }
+ if ("validate" in descr && !result) {
+ result = !descr.validate(value) ? `'${key}' is not valid` : null;
+ }
+ return result;
+ }
+
+ const ObjectCreate = Object.create;
+ /**
+ * This file contains utility functions that will be injected in each template,
+ * to perform various useful tasks in the compiled code.
+ */
+ function withDefault(value, defaultValue) {
+ return value === undefined || value === null || value === false ? defaultValue : value;
+ }
+ function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
+ key = key + "__slot_" + name;
+ const slots = ctx.props.slots || {};
+ const { __render, __ctx, __scope } = slots[name] || {};
+ const slotScope = ObjectCreate(__ctx || {});
+ if (__scope) {
+ slotScope[__scope] = extra;
+ }
+ const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
+ if (defaultContent) {
+ let child1 = undefined;
+ let child2 = undefined;
+ if (slotBDom) {
+ child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
+ }
+ else {
+ child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
+ }
+ return multi([child1, child2]);
+ }
+ return slotBDom || text("");
+ }
+ function capture(ctx) {
+ const component = ctx.__owl__.component;
+ const result = ObjectCreate(component);
+ for (let k in ctx) {
+ result[k] = ctx[k];
+ }
+ return result;
+ }
+ function withKey(elem, k) {
+ elem.key = k;
+ return elem;
+ }
+ function prepareList(collection) {
+ let keys;
+ let values;
+ if (Array.isArray(collection)) {
+ keys = collection;
+ values = collection;
+ }
+ else if (collection) {
+ values = Object.keys(collection);
+ keys = Object.values(collection);
+ }
+ else {
+ throw new Error("Invalid loop expression");
+ }
+ const n = values.length;
+ return [keys, values, n, new Array(n)];
+ }
+ const isBoundary = Symbol("isBoundary");
+ function setContextValue(ctx, key, value) {
+ const ctx0 = ctx;
+ while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
+ const newCtx = ctx.__proto__;
+ if (!newCtx) {
+ ctx = ctx0;
+ break;
+ }
+ ctx = newCtx;
+ }
+ ctx[key] = value;
+ }
+ function toNumber(val) {
+ const n = parseFloat(val);
+ return isNaN(n) ? val : n;
+ }
+ function shallowEqual(l1, l2) {
+ for (let i = 0, l = l1.length; i < l; i++) {
+ if (l1[i] !== l2[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ class LazyValue {
+ constructor(fn, ctx, node) {
+ this.fn = fn;
+ this.ctx = capture(ctx);
+ this.node = node;
+ }
+ evaluate() {
+ return this.fn(this.ctx, this.node);
+ }
+ toString() {
+ return this.evaluate().toString();
+ }
+ }
+ /*
+ * Safely outputs `value` as a block depending on the nature of `value`
+ */
+ function safeOutput(value) {
+ if (!value) {
+ return value;
+ }
+ let safeKey;
+ let block;
+ switch (typeof value) {
+ case "object":
+ if (value instanceof Markup) {
+ safeKey = `string_safe`;
+ block = html(value);
+ }
+ else if (value instanceof LazyValue) {
+ safeKey = `lazy_value`;
+ block = value.evaluate();
+ }
+ else if (value instanceof String) {
+ safeKey = "string_unsafe";
+ block = text(value);
+ }
+ else {
+ // Assuming it is a block
+ safeKey = "block_safe";
+ block = value;
+ }
+ break;
+ case "string":
+ safeKey = "string_unsafe";
+ block = text(value);
+ break;
+ default:
+ safeKey = "string_unsafe";
+ block = text(String(value));
+ }
+ return toggler(safeKey, block);
+ }
+ let boundFunctions = new WeakMap();
+ const WeakMapGet = WeakMap.prototype.get;
+ const WeakMapSet = WeakMap.prototype.set;
+ function bind(ctx, fn) {
+ let component = ctx.__owl__.component;
+ let boundFnMap = WeakMapGet.call(boundFunctions, component);
+ if (!boundFnMap) {
+ boundFnMap = new WeakMap();
+ WeakMapSet.call(boundFunctions, component, boundFnMap);
+ }
+ let boundFn = WeakMapGet.call(boundFnMap, fn);
+ if (!boundFn) {
+ boundFn = fn.bind(component);
+ WeakMapSet.call(boundFnMap, fn, boundFn);
+ }
+ return boundFn;
+ }
+ function multiRefSetter(refs, name) {
+ let count = 0;
+ return (el) => {
+ if (el) {
+ count++;
+ if (count > 1) {
+ throw new Error("Cannot have 2 elements with same ref name at the same time");
+ }
+ }
+ if (count === 0 || el) {
+ refs[name] = el;
+ }
+ };
+ }
+ /**
+ * Validate the component props (or next props) against the (static) props
+ * description. This is potentially an expensive operation: it may needs to
+ * visit recursively the props and all the children to check if they are valid.
+ * This is why it is only done in 'dev' mode.
+ */
+ function validateProps(name, props, parent) {
+ const ComponentClass = typeof name !== "string"
+ ? name
+ : parent.constructor.components[name];
+ if (!ComponentClass) {
+ // this is an error, wrong component. We silently return here instead so the
+ // error is triggered by the usual path ('component' function)
+ return;
+ }
+ const schema = ComponentClass.props;
+ if (!schema) {
+ if (parent.__owl__.app.warnIfNoStaticProps) {
+ console.warn(`Component '${ComponentClass.name}' does not have a static props description`);
+ }
+ return;
+ }
+ const defaultProps = ComponentClass.defaultProps;
+ if (defaultProps) {
+ let isMandatory = (name) => Array.isArray(schema)
+ ? schema.includes(name)
+ : name in schema && !("*" in schema) && !isOptional(schema[name]);
+ for (let p in defaultProps) {
+ if (isMandatory(p)) {
+ throw new Error(`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`);
+ }
+ }
+ }
+ const errors = validateSchema(props, schema);
+ if (errors.length) {
+ throw new Error(`Invalid props for component '${ComponentClass.name}': ` + errors.join(", "));
+ }
+ }
+ const helpers = {
+ withDefault,
+ zero: Symbol("zero"),
+ isBoundary,
+ callSlot,
+ capture,
+ withKey,
+ prepareList,
+ setContextValue,
+ multiRefSetter,
+ shallowEqual,
+ toNumber,
+ validateProps,
+ LazyValue,
+ safeOutput,
+ bind,
+ createCatcher,
+ markRaw,
+ };
+
+ const bdom = { text, createBlock, list, multi, html, toggler, comment };
+ function parseXML$1(xml) {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(xml, "text/xml");
+ if (doc.getElementsByTagName("parsererror").length) {
+ let msg = "Invalid XML in template.";
+ const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
+ if (parsererrorText) {
+ msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
+ const re = /\d+/g;
+ const firstMatch = re.exec(parsererrorText);
+ if (firstMatch) {
+ const lineNumber = Number(firstMatch[0]);
+ const line = xml.split("\n")[lineNumber - 1];
+ const secondMatch = re.exec(parsererrorText);
+ if (line && secondMatch) {
+ const columnIndex = Number(secondMatch[0]) - 1;
+ if (line[columnIndex]) {
+ msg +=
+ `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
+ `${line}\n${"-".repeat(columnIndex - 1)}^`;
+ }
+ }
+ }
+ }
+ throw new Error(msg);
+ }
+ return doc;
+ }
+ class TemplateSet {
+ constructor(config = {}) {
+ this.rawTemplates = Object.create(globalTemplates);
+ this.templates = {};
+ this.Portal = Portal;
+ this.dev = config.dev || false;
+ this.translateFn = config.translateFn;
+ this.translatableAttributes = config.translatableAttributes;
+ if (config.templates) {
+ this.addTemplates(config.templates);
+ }
+ }
+ static registerTemplate(name, fn) {
+ globalTemplates[name] = fn;
+ }
+ addTemplate(name, template) {
+ if (name in this.rawTemplates) {
+ const rawTemplate = this.rawTemplates[name];
+ const currentAsString = typeof rawTemplate === "string"
+ ? rawTemplate
+ : rawTemplate instanceof Element
+ ? rawTemplate.outerHTML
+ : rawTemplate.toString();
+ const newAsString = typeof template === "string" ? template : template.outerHTML;
+ if (currentAsString === newAsString) {
+ return;
+ }
+ throw new Error(`Template ${name} already defined with different content`);
+ }
+ this.rawTemplates[name] = template;
+ }
+ addTemplates(xml) {
+ if (!xml) {
+ // empty string
+ return;
+ }
+ xml = xml instanceof Document ? xml : parseXML$1(xml);
+ for (const template of xml.querySelectorAll("[t-name]")) {
+ const name = template.getAttribute("t-name");
+ this.addTemplate(name, template);
+ }
+ }
+ getTemplate(name) {
+ if (!(name in this.templates)) {
+ const rawTemplate = this.rawTemplates[name];
+ if (rawTemplate === undefined) {
+ let extraInfo = "";
+ try {
+ const componentName = getCurrent().component.constructor.name;
+ extraInfo = ` (for component "${componentName}")`;
+ }
+ catch { }
+ throw new Error(`Missing template: "${name}"${extraInfo}`);
+ }
+ const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
+ const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
+ // first add a function to lazily get the template, in case there is a
+ // recursive call to the template name
+ const templates = this.templates;
+ this.templates[name] = function (context, parent) {
+ return templates[name].call(this, context, parent);
+ };
+ const template = templateFn(this, bdom, helpers);
+ this.templates[name] = template;
+ }
+ return this.templates[name];
+ }
+ _compileTemplate(name, template) {
+ throw new Error(`Unable to compile a template. Please use owl full build instead`);
+ }
+ callTemplate(owner, subTemplate, ctx, parent, key) {
+ const template = this.getTemplate(subTemplate);
+ return toggler(subTemplate, template.call(owner, ctx, parent, key));
+ }
+ }
+ // -----------------------------------------------------------------------------
+ // xml tag helper
+ // -----------------------------------------------------------------------------
+ const globalTemplates = {};
+ function xml(...args) {
+ const name = `__template__${xml.nextId++}`;
+ const value = String.raw(...args);
+ globalTemplates[name] = value;
+ return name;
+ }
+ xml.nextId = 1;
+ TemplateSet.registerTemplate("__portal__", portalTemplate);
/**
* Owl QWeb Expression Parser
@@ -2767,8 +3176,8 @@
")": "RIGHT_PAREN",
});
// note that the space after typeof is relevant. It makes sure that the formatted
- // expression has a space after typeof
- const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
+ // expression has a space after typeof. Currently we don't support delete and void
+ const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ".split(",");
let tokenizeString = function (expr) {
let s = expr[0];
let start = s;
@@ -3009,26 +3418,35 @@
}
return tokens;
}
+ // Leading spaces are trimmed during tokenization, so they need to be added back for some values
+ const paddedValues = new Map([["in ", " in "]]);
function compileExpr(expr) {
return compileExprToArray(expr)
- .map((t) => t.value)
+ .map((t) => paddedValues.get(t.value) || t.value)
.join("");
}
- const INTERP_REGEXP = /\{\{.*?\}\}/g;
- const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g;
- function interpolate(s) {
+ const INTERP_REGEXP = /\{\{.*?\}\}|\#\{.*?\}/g;
+ function replaceDynamicParts(s, replacer) {
let matches = s.match(INTERP_REGEXP);
if (matches && matches[0].length === s.length) {
- return `(${compileExpr(s.slice(2, -2))})`;
+ return `(${replacer(s.slice(2, matches[0][0] === "{" ? -2 : -1))})`;
}
- let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}");
+ let r = s.replace(INTERP_REGEXP, (s) => "${" + replacer(s.slice(2, s[0] === "{" ? -2 : -1)) + "}");
return "`" + r + "`";
+ }
+ function interpolate(s) {
+ return replaceDynamicParts(s, compileExpr);
}
// using a non-html document so that HTML serializes as XML instead
// of HTML (as we will parse it as xml later)
const xmlDoc = document.implementation.createDocument(null, null, null);
const MODS = new Set(["stop", "capture", "prevent", "self", "synthetic"]);
+ let nextDataIds = {};
+ function generateId(prefix = "") {
+ nextDataIds[prefix] = (nextDataIds[prefix] || 0) + 1;
+ return prefix + nextDataIds[prefix];
+ }
// -----------------------------------------------------------------------------
// BlockDescription
// -----------------------------------------------------------------------------
@@ -3047,12 +3465,8 @@
this.target = target;
this.type = type;
}
- static generateId(prefix) {
- this.nextDataIds[prefix] = (this.nextDataIds[prefix] || 0) + 1;
- return prefix + this.nextDataIds[prefix];
- }
insertData(str, prefix = "d") {
- const id = BlockDescription.generateId(prefix);
+ const id = generateId(prefix);
this.target.addLine(`let ${id} = ${str};`);
return this.data.push(id) - 1;
}
@@ -3090,7 +3504,6 @@
}
}
BlockDescription.nextBlockId = 1;
- BlockDescription.nextDataIds = {};
function createContext(parentCtx, params) {
return Object.assign({
block: null,
@@ -3158,7 +3571,6 @@
class CodeGenerator {
constructor(ast, options) {
this.blocks = [];
- this.ids = {};
this.nextBlockId = 1;
this.isDebug = false;
this.targets = [];
@@ -3188,7 +3600,7 @@
const ast = this.ast;
this.isDebug = ast.type === 12 /* TDebug */;
BlockDescription.nextBlockId = 1;
- BlockDescription.nextDataIds = {};
+ nextDataIds = {};
this.compileAST(ast, {
block: null,
index: 0,
@@ -3198,9 +3610,7 @@
tKeyExpr: null,
});
// define blocks and utility functions
- let mainCode = [
- ` let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;`,
- ];
+ let mainCode = [` let { text, createBlock, list, multi, html, toggler, comment } = bdom;`];
if (this.helpers.size) {
mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`);
}
@@ -3216,6 +3626,7 @@
for (let block of this.blocks) {
if (block.dom) {
let xmlString = block.asXmlString();
+ xmlString = xmlString.replace(/`/g, "\\`");
if (block.dynamicTagName) {
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
@@ -3245,7 +3656,7 @@
return code;
}
compileInNewTarget(prefix, ast, ctx, on) {
- const name = this.generateId(prefix);
+ const name = generateId(prefix);
const initialTarget = this.target;
const target = new CodeTarget(name, on);
this.targets.push(target);
@@ -3260,10 +3671,6 @@
define(varName, expr) {
this.addLine(`const ${varName} = ${expr};`);
}
- generateId(prefix = "") {
- this.ids[prefix] = (this.ids[prefix] || 0) + 1;
- return prefix + this.ids[prefix];
- }
insertAnchor(block) {
const tag = `block-child-${block.children.length}`;
const anchor = xmlDoc.createElement(tag);
@@ -3332,7 +3739,7 @@
.map((tok) => {
if (tok.varName && !tok.isLocal) {
if (!mapping.has(tok.varName)) {
- const varId = this.generateId("v");
+ const varId = generateId("v");
mapping.set(tok.varName, varId);
this.define(varId, tok.value);
}
@@ -3472,7 +3879,7 @@
block = this.createBlock(block, "block", ctx);
this.blocks.push(block);
if (ast.dynamicTag) {
- const tagExpr = this.generateId("tag");
+ const tagExpr = generateId("tag");
this.define(tagExpr, compileExpr(ast.dynamicTag));
block.dynamicTagName = tagExpr;
}
@@ -3527,8 +3934,8 @@
this.target.hasRef = true;
const isDynamic = INTERP_REGEXP.test(ast.ref);
if (isDynamic) {
- const str = ast.ref.replace(INTERP_REGEXP, (expr) => "${" + this.captureExpression(expr.slice(2, -2), true) + "}");
- const idx = block.insertData(`(el) => refs[\`${str}\`] = el`, "ref");
+ const str = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
+ const idx = block.insertData(`(el) => refs[${str}] = el`, "ref");
attrs["block-ref"] = String(idx);
}
else {
@@ -3542,7 +3949,7 @@
info[1] = `multiRefSetter(refs, \`${name}\`)`;
}
else {
- let id = this.generateId("ref");
+ let id = generateId("ref");
this.target.refInfo[name] = [id, `(el) => refs[\`${name}\`] = el`];
const index = block.data.push(id) - 1;
attrs["block-ref"] = String(index);
@@ -3554,10 +3961,10 @@
if (ast.model) {
const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;
const baseExpression = compileExpr(baseExpr);
- const bExprId = this.generateId("bExpr");
+ const bExprId = generateId("bExpr");
this.define(bExprId, baseExpression);
const expression = compileExpr(expr);
- const exprId = this.generateId("expr");
+ const exprId = generateId("expr");
this.define(exprId, expression);
const fullExpression = `${bExprId}[${exprId}]`;
let idx;
@@ -3566,7 +3973,7 @@
attrs[`block-attribute-${idx}`] = specialInitTargetAttr;
}
else if (hasDynamicChildren) {
- const bValueId = this.generateId("bValue");
+ const bValueId = generateId("bValue");
tModelSelectedExpr = `${bValueId}`;
this.define(tModelSelectedExpr, fullExpression);
}
@@ -3768,7 +4175,7 @@
let id;
if (ast.memo) {
this.target.hasCache = true;
- id = this.generateId();
+ id = generateId();
this.define(`memo${id}`, compileExpr(ast.memo));
this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);
this.addLine(`if (vnode${id}) {`);
@@ -3797,7 +4204,7 @@
this.insertBlock("l", block, ctx);
}
compileTKey(ast, ctx) {
- const tKeyExpr = this.generateId("tKey_");
+ const tKeyExpr = generateId("tKey_");
this.define(tKeyExpr, compileExpr(ast.expr));
ctx = createContext(ctx, {
tKeyExpr,
@@ -3880,19 +4287,20 @@
}
const key = `key + \`${this.generateComponentKey()}\``;
if (isDynamic) {
- const templateVar = this.generateId("template");
+ const templateVar = generateId("template");
+ if (!this.staticDefs.find((d) => d.id === "call")) {
+ this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
+ }
this.define(templateVar, subTemplate);
block = this.createBlock(block, "multi", ctx);
- this.helpers.add("call");
this.insertBlock(`call(this, ${templateVar}, ctx, node, ${key})`, block, {
...ctx,
forceNewBlock: !block,
});
}
else {
- const id = this.generateId(`callTemplate_`);
- this.helpers.add("getTemplate");
- this.staticDefs.push({ id, expr: `getTemplate(${subTemplate})` });
+ const id = generateId(`callTemplate_`);
+ this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
block = this.createBlock(block, "multi", ctx);
this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block, {
...ctx,
@@ -3943,7 +4351,7 @@
}
}
generateComponentKey() {
- const parts = [this.generateId("__")];
+ const parts = [generateId("__")];
for (let i = 0; i < this.target.loopLevel; i++) {
parts.push(`\${key${i + 1}}`);
}
@@ -3977,27 +4385,26 @@
return `${name}: ${value || undefined}`;
}
formatPropObject(obj) {
- const params = [];
- for (const [n, v] of Object.entries(obj)) {
- params.push(this.formatProp(n, v));
+ return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
+ }
+ getPropString(props, dynProps) {
+ let propString = `{${props.join(",")}}`;
+ if (dynProps) {
+ propString = `Object.assign({}, ${compileExpr(dynProps)}${props.length ? ", " + propString : ""})`;
}
- return params.join(", ");
+ return propString;
}
compileComponent(ast, ctx) {
let { block } = ctx;
// props
const hasSlotsProp = "slots" in (ast.props || {});
- const props = [];
- const propExpr = this.formatPropObject(ast.props || {});
- if (propExpr) {
- props.push(propExpr);
- }
+ const props = ast.props ? this.formatPropObject(ast.props) : [];
// slots
let slotDef = "";
if (ast.slots) {
let ctxStr = "ctx";
if (this.target.loopLevel || !this.hasSafeContext) {
- ctxStr = this.generateId("ctx");
+ ctxStr = generateId("ctx");
this.helpers.add("capture");
this.define(ctxStr, `capture(ctx)`);
}
@@ -4014,7 +4421,7 @@
params.push(`__scope: "${scope}"`);
}
if (ast.slots[slotName].attrs) {
- params.push(this.formatPropObject(ast.slots[slotName].attrs));
+ params.push(...this.formatPropObject(ast.slots[slotName].attrs));
}
const slotInfo = `{${params.join(", ")}}`;
slotStr.push(`'${slotName}': ${slotInfo}`);
@@ -4025,14 +4432,10 @@
this.helpers.add("markRaw");
props.push(`slots: markRaw(${slotDef})`);
}
- const propStr = `{${props.join(",")}}`;
- let propString = propStr;
- if (ast.dynamicProps) {
- propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${props.length ? ", " + propStr : ""})`;
- }
+ let propString = this.getPropString(props, ast.dynamicProps);
let propVar;
if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
- propVar = this.generateId("props");
+ propVar = generateId("props");
this.define(propVar, propString);
propString = propVar;
}
@@ -4044,7 +4447,7 @@
const key = this.generateComponentKey();
let expr;
if (ast.isDynamic) {
- expr = this.generateId("Comp");
+ expr = generateId("Comp");
this.define(expr, compileExpr(ast.name));
}
else {
@@ -4061,8 +4464,12 @@
if (ctx.tKeyExpr) {
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
}
- const blockArgs = `${expr}, ${propString}, ${keyArg}, node, ctx`;
- let blockExpr = `component(${blockArgs})`;
+ let id = generateId("comp");
+ this.staticDefs.push({
+ id,
+ expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, ${!ast.props && !ast.dynamicProps})`,
+ });
+ let blockExpr = `${id}(${propString}, ${keyArg}, node, ctx, ${ast.isDynamic ? expr : null})`;
if (ast.isDynamic) {
blockExpr = `toggler(${expr}, ${blockExpr})`;
}
@@ -4075,11 +4482,11 @@
}
wrapWithEventCatcher(expr, on) {
this.helpers.add("createCatcher");
- let name = this.generateId("catcher");
+ let name = generateId("catcher");
let spec = {};
let handlers = [];
for (let ev in on) {
- let handlerId = this.generateId("hdlr");
+ let handlerId = generateId("hdlr");
let idx = handlers.push(handlerId) - 1;
spec[ev] = idx;
const handler = this.generateHandlerCode(ev, on[ev]);
@@ -4101,14 +4508,19 @@
else {
slotName = "'" + ast.name + "'";
}
- const scope = ast.attrs ? `{${this.formatPropObject(ast.attrs)}}` : null;
+ const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
+ if (ast.attrs) {
+ delete ast.attrs["t-props"];
+ }
+ const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
+ const scope = this.getPropString(props, dynProps);
if (ast.defaultContent) {
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`;
}
else {
if (dynamic) {
- let name = this.generateId("slot");
+ let name = generateId("slot");
this.define(name, slotName);
blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}, ${dynamic}, ${scope}))`;
}
@@ -4132,17 +4544,25 @@
}
}
compileTPortal(ast, ctx) {
- this.helpers.add("Portal");
+ if (!this.staticDefs.find((d) => d.id === "Portal")) {
+ this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
+ }
let { block } = ctx;
const name = this.compileInNewTarget("slot", ast.content, ctx);
const key = this.generateComponentKey();
let ctxStr = "ctx";
if (this.target.loopLevel || !this.hasSafeContext) {
- ctxStr = this.generateId("ctx");
+ ctxStr = generateId("ctx");
this.helpers.add("capture");
- this.define(ctxStr, `capture(ctx);`);
+ this.define(ctxStr, `capture(ctx)`);
}
- const blockString = `component(Portal, {target: ${ast.target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`;
+ let id = generateId("comp");
+ this.staticDefs.push({
+ id,
+ expr: `app.createComponent(null, false, true, false, false)`,
+ });
+ const target = compileExpr(ast.target);
+ const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
if (block) {
this.insertAnchor(block);
}
@@ -4160,7 +4580,7 @@
const cache = new WeakMap();
function parse(xml) {
if (typeof xml === "string") {
- const elem = parseXML$1(`${xml}`).firstChild;
+ const elem = parseXML(`${xml}`).firstChild;
return _parse(elem);
}
let ast = cache.get(xml);
@@ -4858,7 +5278,7 @@
* @param xml the string to parse
* @returns an XML document corresponding to the content of the string
*/
- function parseXML$1(xml) {
+ function parseXML(xml) {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
@@ -4898,477 +5318,118 @@
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
const code = codeGenerator.generateCode();
// template function
- return new Function("bdom, helpers", code);
+ return new Function("app, bdom, helpers", code);
}
- const TIMEOUT = Symbol("timeout");
- function wrapError(fn, hookName) {
- const error = new Error(`The following error occurred in ${hookName}: `);
- const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
- const node = getCurrent();
- return (...args) => {
- try {
- const result = fn(...args);
- if (result instanceof Promise) {
- if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
- const fiber = node.fiber;
- Promise.race([
- result,
- new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
- ]).then((res) => {
- if (res === TIMEOUT && node.fiber === fiber) {
- console.warn(timeoutError);
- }
- });
- }
- return result.catch((cause) => {
- error.cause = cause;
- if (cause instanceof Error) {
- error.message += `"${cause.message}"`;
+ const mainEventHandler = (data, ev, currentTarget) => {
+ const { data: _data, modifiers } = filterOutModifiersFromData(data);
+ data = _data;
+ let stopped = false;
+ if (modifiers.length) {
+ let selfMode = false;
+ const isSelf = ev.target === currentTarget;
+ for (const mod of modifiers) {
+ switch (mod) {
+ case "self":
+ selfMode = true;
+ if (isSelf) {
+ continue;
}
- throw error;
- });
- }
- return result;
- }
- catch (cause) {
- if (cause instanceof Error) {
- error.message += `"${cause.message}"`;
- }
- throw error;
- }
- };
- }
- // -----------------------------------------------------------------------------
- // hooks
- // -----------------------------------------------------------------------------
- function onWillStart(fn) {
- const node = getCurrent();
- const decorate = node.app.dev ? wrapError : (fn) => fn;
- node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
- }
- function onWillUpdateProps(fn) {
- const node = getCurrent();
- const decorate = node.app.dev ? wrapError : (fn) => fn;
- node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
- }
- function onMounted(fn) {
- const node = getCurrent();
- const decorate = node.app.dev ? wrapError : (fn) => fn;
- node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
- }
- function onWillPatch(fn) {
- const node = getCurrent();
- const decorate = node.app.dev ? wrapError : (fn) => fn;
- node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
- }
- function onPatched(fn) {
- const node = getCurrent();
- const decorate = node.app.dev ? wrapError : (fn) => fn;
- node.patched.push(decorate(fn.bind(node.component), "onPatched"));
- }
- function onWillUnmount(fn) {
- const node = getCurrent();
- const decorate = node.app.dev ? wrapError : (fn) => fn;
- node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
- }
- function onWillDestroy(fn) {
- const node = getCurrent();
- const decorate = node.app.dev ? wrapError : (fn) => fn;
- node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
- }
- function onWillRender(fn) {
- const node = getCurrent();
- const renderFn = node.renderFn;
- const decorate = node.app.dev ? wrapError : (fn) => fn;
- fn = decorate(fn.bind(node.component), "onWillRender");
- node.renderFn = () => {
- fn();
- return renderFn();
- };
- }
- function onRendered(fn) {
- const node = getCurrent();
- const renderFn = node.renderFn;
- const decorate = node.app.dev ? wrapError : (fn) => fn;
- fn = decorate(fn.bind(node.component), "onRendered");
- node.renderFn = () => {
- const result = renderFn();
- fn();
- return result;
- };
- }
- function onError(callback) {
- const node = getCurrent();
- let handlers = nodeErrorHandlers.get(node);
- if (!handlers) {
- handlers = [];
- nodeErrorHandlers.set(node, handlers);
- }
- handlers.push(callback.bind(node.component));
- }
-
- class Component {
- constructor(props, env, node) {
- this.props = props;
- this.env = env;
- this.__owl__ = node;
- }
- setup() { }
- render(deep = false) {
- this.__owl__.render(deep === true);
- }
- }
- Component.template = "";
-
- const VText = text("").constructor;
- class VPortal extends VText {
- constructor(selector, realBDom) {
- super("");
- this.target = null;
- this.selector = selector;
- this.realBDom = realBDom;
- }
- mount(parent, anchor) {
- super.mount(parent, anchor);
- this.target = document.querySelector(this.selector);
- if (!this.target) {
- let el = this.el;
- while (el && el.parentElement instanceof HTMLElement) {
- el = el.parentElement;
- }
- this.target = el && el.querySelector(this.selector);
- if (!this.target) {
- throw new Error("invalid portal target");
+ else {
+ return stopped;
+ }
+ case "prevent":
+ if ((selfMode && isSelf) || !selfMode)
+ ev.preventDefault();
+ continue;
+ case "stop":
+ if ((selfMode && isSelf) || !selfMode)
+ ev.stopPropagation();
+ stopped = true;
+ continue;
}
}
- this.realBDom.mount(this.target, null);
}
- beforeRemove() {
- this.realBDom.beforeRemove();
- }
- remove() {
- if (this.realBDom) {
- super.remove();
- this.realBDom.remove();
- this.realBDom = null;
+ // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
+ // We check this rather than data[0] being truthy (or typeof function) so that it crashes
+ // as expected when there is a handler expression that evaluates to a falsy value
+ if (Object.hasOwnProperty.call(data, 0)) {
+ const handler = data[0];
+ if (typeof handler !== "function") {
+ throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
+ }
+ let node = data[1] ? data[1].__owl__ : null;
+ if (node ? node.status === 1 /* MOUNTED */ : true) {
+ handler.call(node ? node.component : null, ev);
}
}
- patch(other) {
- super.patch(other);
- if (this.realBDom) {
- this.realBDom.patch(other.realBDom, true);
- }
- else {
- this.realBDom = other.realBDom;
- this.realBDom.mount(this.target, null);
- }
- }
- }
- class Portal extends Component {
- setup() {
- const node = this.__owl__;
- const renderFn = node.renderFn;
- node.renderFn = () => new VPortal(this.props.target, renderFn());
- onWillUnmount(() => {
- if (node.bdom) {
- node.bdom.remove();
- }
- });
- }
- }
- Portal.template = xml ``;
- Portal.props = {
- target: {
- type: String,
- },
- slots: true,
+ return stopped;
};
- /**
- * This file contains utility functions that will be injected in each template,
- * to perform various useful tasks in the compiled code.
- */
- function withDefault(value, defaultValue) {
- return value === undefined || value === null || value === false ? defaultValue : value;
- }
- function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
- key = key + "__slot_" + name;
- const slots = ctx.props[TARGET].slots || {};
- const { __render, __ctx, __scope } = slots[name] || {};
- const slotScope = Object.create(__ctx || {});
- if (__scope) {
- slotScope[__scope] = extra;
+ // -----------------------------------------------------------------------------
+ // Scheduler
+ // -----------------------------------------------------------------------------
+ class Scheduler {
+ constructor() {
+ this.tasks = new Set();
+ this.frame = 0;
+ this.delayedRenders = [];
+ this.requestAnimationFrame = Scheduler.requestAnimationFrame;
}
- const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
- if (defaultContent) {
- let child1 = undefined;
- let child2 = undefined;
- if (slotBDom) {
- child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
- }
- else {
- child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
- }
- return multi([child1, child2]);
+ addFiber(fiber) {
+ this.tasks.add(fiber.root);
}
- return slotBDom || text("");
- }
- function capture(ctx) {
- const component = ctx.__owl__.component;
- const result = Object.create(component);
- for (let k in ctx) {
- result[k] = ctx[k];
- }
- return result;
- }
- function withKey(elem, k) {
- elem.key = k;
- return elem;
- }
- function prepareList(collection) {
- let keys;
- let values;
- if (Array.isArray(collection)) {
- keys = collection;
- values = collection;
- }
- else if (collection) {
- values = Object.keys(collection);
- keys = Object.values(collection);
- }
- else {
- throw new Error("Invalid loop expression");
- }
- const n = values.length;
- return [keys, values, n, new Array(n)];
- }
- const isBoundary = Symbol("isBoundary");
- function setContextValue(ctx, key, value) {
- const ctx0 = ctx;
- while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
- const newCtx = ctx.__proto__;
- if (!newCtx) {
- ctx = ctx0;
- break;
- }
- ctx = newCtx;
- }
- ctx[key] = value;
- }
- function toNumber(val) {
- const n = parseFloat(val);
- return isNaN(n) ? val : n;
- }
- function shallowEqual(l1, l2) {
- for (let i = 0, l = l1.length; i < l; i++) {
- if (l1[i] !== l2[i]) {
- return false;
- }
- }
- return true;
- }
- class LazyValue {
- constructor(fn, ctx, node) {
- this.fn = fn;
- this.ctx = capture(ctx);
- this.node = node;
- }
- evaluate() {
- return this.fn(this.ctx, this.node);
- }
- toString() {
- return this.evaluate().toString();
- }
- }
- /*
- * Safely outputs `value` as a block depending on the nature of `value`
- */
- function safeOutput(value) {
- if (!value) {
- return value;
- }
- let safeKey;
- let block;
- if (value instanceof Markup) {
- safeKey = `string_safe`;
- block = html(value);
- }
- else if (value instanceof LazyValue) {
- safeKey = `lazy_value`;
- block = value.evaluate();
- }
- else if (value instanceof String || typeof value === "string") {
- safeKey = "string_unsafe";
- block = text(value);
- }
- else {
- // Assuming it is a block
- safeKey = "block_safe";
- block = value;
- }
- return toggler(safeKey, block);
- }
- let boundFunctions = new WeakMap();
- function bind(ctx, fn) {
- let component = ctx.__owl__.component;
- let boundFnMap = boundFunctions.get(component);
- if (!boundFnMap) {
- boundFnMap = new WeakMap();
- boundFunctions.set(component, boundFnMap);
- }
- let boundFn = boundFnMap.get(fn);
- if (!boundFn) {
- boundFn = fn.bind(component);
- boundFnMap.set(fn, boundFn);
- }
- return boundFn;
- }
- function multiRefSetter(refs, name) {
- let count = 0;
- return (el) => {
- if (el) {
- count++;
- if (count > 1) {
- throw new Error("Cannot have 2 elements with same ref name at the same time");
- }
- }
- if (count === 0 || el) {
- refs[name] = el;
- }
- };
- }
- const helpers = {
- withDefault,
- zero: Symbol("zero"),
- isBoundary,
- callSlot,
- capture,
- withKey,
- prepareList,
- setContextValue,
- multiRefSetter,
- shallowEqual,
- toNumber,
- validateProps,
- LazyValue,
- safeOutput,
- bind,
- createCatcher,
- };
-
- const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
- function parseXML(xml) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(xml, "text/xml");
- if (doc.getElementsByTagName("parsererror").length) {
- let msg = "Invalid XML in template.";
- const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
- if (parsererrorText) {
- msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
- const re = /\d+/g;
- const firstMatch = re.exec(parsererrorText);
- if (firstMatch) {
- const lineNumber = Number(firstMatch[0]);
- const line = xml.split("\n")[lineNumber - 1];
- const secondMatch = re.exec(parsererrorText);
- if (line && secondMatch) {
- const columnIndex = Number(secondMatch[0]) - 1;
- if (line[columnIndex]) {
- msg +=
- `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
- `${line}\n${"-".repeat(columnIndex - 1)}^`;
- }
+ /**
+ * Process all current tasks. This only applies to the fibers that are ready.
+ * Other tasks are left unchanged.
+ */
+ flush() {
+ if (this.delayedRenders.length) {
+ let renders = this.delayedRenders;
+ this.delayedRenders = [];
+ for (let f of renders) {
+ if (f.root && f.node.status !== 2 /* DESTROYED */ && f.node.fiber === f) {
+ f.render();
}
}
}
- throw new Error(msg);
- }
- return doc;
- }
- /**
- * Returns the helpers object that will be injected in each template closure
- * function
- */
- function makeHelpers(getTemplate) {
- return Object.assign({}, helpers, {
- Portal,
- markRaw,
- getTemplate,
- call: (owner, subTemplate, ctx, parent, key) => {
- const template = getTemplate(subTemplate);
- return toggler(subTemplate, template.call(owner, ctx, parent, key));
- },
- });
- }
- class TemplateSet {
- constructor(config = {}) {
- this.rawTemplates = Object.create(globalTemplates);
- this.templates = {};
- this.dev = config.dev || false;
- this.translateFn = config.translateFn;
- this.translatableAttributes = config.translatableAttributes;
- if (config.templates) {
- this.addTemplates(config.templates);
+ if (this.frame === 0) {
+ this.frame = this.requestAnimationFrame(() => {
+ this.frame = 0;
+ this.tasks.forEach((fiber) => this.processFiber(fiber));
+ for (let task of this.tasks) {
+ if (task.node.status === 2 /* DESTROYED */) {
+ this.tasks.delete(task);
+ }
+ }
+ });
}
- this.helpers = makeHelpers(this.getTemplate.bind(this));
}
- addTemplate(name, template) {
- if (name in this.rawTemplates) {
- const rawTemplate = this.rawTemplates[name];
- const currentAsString = typeof rawTemplate === "string" ? rawTemplate : rawTemplate.outerHTML;
- const newAsString = typeof template === "string" ? template : template.outerHTML;
- if (currentAsString === newAsString) {
- return;
- }
- throw new Error(`Template ${name} already defined with different content`);
- }
- this.rawTemplates[name] = template;
- }
- addTemplates(xml) {
- if (!xml) {
- // empty string
+ processFiber(fiber) {
+ if (fiber.root !== fiber) {
+ this.tasks.delete(fiber);
return;
}
- xml = xml instanceof Document ? xml : parseXML(xml);
- for (const template of xml.querySelectorAll("[t-name]")) {
- const name = template.getAttribute("t-name");
- this.addTemplate(name, template);
+ const hasError = fibersInError.has(fiber);
+ if (hasError && fiber.counter !== 0) {
+ this.tasks.delete(fiber);
+ return;
}
- }
- getTemplate(name) {
- if (!(name in this.templates)) {
- const rawTemplate = this.rawTemplates[name];
- if (rawTemplate === undefined) {
- let extraInfo = "";
- try {
- const componentName = getCurrent().component.constructor.name;
- extraInfo = ` (for component "${componentName}")`;
- }
- catch { }
- throw new Error(`Missing template: "${name}"${extraInfo}`);
+ if (fiber.node.status === 2 /* DESTROYED */) {
+ this.tasks.delete(fiber);
+ return;
+ }
+ if (fiber.counter === 0) {
+ if (!hasError) {
+ fiber.complete();
}
- const templateFn = this._compileTemplate(name, rawTemplate);
- // first add a function to lazily get the template, in case there is a
- // recursive call to the template name
- const templates = this.templates;
- this.templates[name] = function (context, parent) {
- return templates[name].call(this, context, parent);
- };
- const template = templateFn(bdom, this.helpers);
- this.templates[name] = template;
+ this.tasks.delete(fiber);
}
- return this.templates[name];
}
- _compileTemplate(name, template) {
- return compile(template, {
- name,
- dev: this.dev,
- translateFn: this.translateFn,
- translatableAttributes: this.translatableAttributes,
- });
- }
- }
+ }
+ // capture the value of requestAnimationFrame as soon as possible, to avoid
+ // interactions with other code, such as test frameworks that override them
+ Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
let hasBeenLogged = false;
const DEV_MSG = () => {
@@ -5387,6 +5448,7 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
if (config.test) {
this.dev = true;
}
+ this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
if (this.dev && !config.test && !hasBeenLogged) {
console.info(DEV_MSG());
hasBeenLogged = true;
@@ -5398,6 +5460,9 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
}
mount(target, options) {
App.validateTarget(target);
+ if (this.dev) {
+ validateProps(this.Root, this.props, { __owl__: { app: this } });
+ }
const node = this.makeNode(this.Root, this.props);
const prom = this.mountNode(node, target, options);
this.root = node;
@@ -5440,6 +5505,56 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
this.root.destroy();
}
}
+ createComponent(name, isStatic, hasSlotsProp, hasDynamicPropList, hasNoProp) {
+ const isDynamic = !isStatic;
+ function _arePropsDifferent(props1, props2) {
+ for (let k in props1) {
+ if (props1[k] !== props2[k]) {
+ return true;
+ }
+ }
+ return hasDynamicPropList && Object.keys(props1).length !== Object.keys(props2).length;
+ }
+ const arePropsDifferent = hasSlotsProp
+ ? (_1, _2) => true
+ : hasNoProp
+ ? (_1, _2) => false
+ : _arePropsDifferent;
+ const updateAndRender = ComponentNode.prototype.updateAndRender;
+ const initiateRender = ComponentNode.prototype.initiateRender;
+ return (props, key, ctx, parent, C) => {
+ let children = ctx.children;
+ let node = children[key];
+ if (node &&
+ (node.status === 2 /* DESTROYED */ || (isDynamic && node.component.constructor !== C))) {
+ node = undefined;
+ }
+ const parentFiber = ctx.fiber;
+ if (node) {
+ if (arePropsDifferent(node.props, props) || parentFiber.deep || node.forceNextRender) {
+ node.forceNextRender = false;
+ updateAndRender.call(node, props, parentFiber);
+ }
+ }
+ else {
+ // new component
+ if (isStatic) {
+ C = parent.constructor.components[name];
+ if (!C) {
+ throw new Error(`Cannot find the definition of component "${name}"`);
+ }
+ else if (!(C.prototype instanceof Component)) {
+ throw new Error(`"${name}" is not a Component. It must inherit from the Component class`);
+ }
+ }
+ node = new ComponentNode(C, props, this, ctx, key);
+ children[key] = node;
+ initiateRender.call(node, new Fiber(node, parentFiber));
+ }
+ parentFiber.childrenMap[key] = node;
+ return node;
+ };
+ }
}
App.validateTarget = validateTarget;
async function mount(C, target, config = {}) {
@@ -5576,6 +5691,15 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
};
const __info__ = {};
+ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, template) {
+ return compile(template, {
+ name,
+ dev: this.dev,
+ translateFn: this.translateFn,
+ translatableAttributes: this.translatableAttributes,
+ });
+ };
+
exports.App = App;
exports.Component = Component;
exports.EventBus = EventBus;
@@ -5606,15 +5730,16 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
exports.useRef = useRef;
exports.useState = useState;
exports.useSubEnv = useSubEnv;
+ exports.validate = validate;
exports.whenReady = whenReady;
exports.xml = xml;
Object.defineProperty(exports, '__esModule', { value: true });
- __info__.version = '2.0.0-beta-7';
- __info__.date = '2022-04-27T09:08:50.320Z';
- __info__.hash = '0cd66c8';
+ __info__.version = '2.0.0-beta-10';
+ __info__.date = '2022-06-22T08:33:34.385Z';
+ __info__.hash = '2e03332';
__info__.url = 'https://github.com/odoo/owl';