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';