diff --git a/owl.js b/owl.js
index fb1bbce6..fd8d41ed 100644
--- a/owl.js
+++ b/owl.js
@@ -22,15 +22,7 @@
//------------------------------------------------------------------------------
// we define here a new modified Array prototype, which basically override all
// Array methods that change some state to be able to track their changes
- const methodsToPatch = [
- "push",
- "pop",
- "shift",
- "unshift",
- "splice",
- "sort",
- "reverse"
- ];
+ const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
const methodLen = methodsToPatch.length;
const ArrayProto = Array.prototype;
const ModifiedArrayProto = Object.create(ArrayProto);
@@ -118,8 +110,7 @@
}
}
set(target, key, value) {
- let alreadyDefined = key in target &&
- Object.getOwnPropertyDescriptor(target, key).configurable === false;
+ let alreadyDefined = key in target && Object.getOwnPropertyDescriptor(target, key).configurable === false;
if (alreadyDefined) {
target[key] = value;
}
@@ -240,14 +231,7 @@
}
return map;
}
- const hooks = [
- "create",
- "update",
- "remove",
- "destroy",
- "pre",
- "post"
- ];
+ const hooks = ["create", "update", "remove", "destroy", "pre", "post"];
function init(modules, domApi) {
let i, j, cbs = {};
const api = domApi !== undefined ? domApi : htmlDomApi;
@@ -294,9 +278,7 @@
const dotIdx = sel.indexOf(".", hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
- const tag = hashIdx !== -1 || dotIdx !== -1
- ? sel.slice(0, Math.min(hash, dot))
- : sel;
+ const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
const elm = (vnode.elm =
isDef(data) && isDef((i = data.ns))
? api.createElementNS(i, tag)
@@ -366,9 +348,7 @@
rm = createRmCb(ch.elm, listeners);
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i)
cbs.remove[i](ch, rm);
- if (isDef((i = ch.data)) &&
- isDef((i = i.hook)) &&
- isDef((i = i.remove))) {
+ if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) {
i(ch, rm);
}
else {
@@ -467,9 +447,7 @@
}
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
let i, iLen, hook;
- if (isDef((i = vnode.data)) &&
- isDef((hook = i.hook)) &&
- isDef((i = hook.prepatch))) {
+ if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
i(oldVnode, vnode);
}
const elm = (vnode.elm = oldVnode.elm);
@@ -752,8 +730,7 @@
// add new listeners which has not already attached
if (on) {
// reuse existing listener or create new
- var listener = (vnode.listener =
- oldVnode.listener || createListener());
+ var listener = (vnode.listener = oldVnode.listener || createListener());
// update vnode for listener
listener.vnode = vnode;
// if element changed or added we add all needed listeners unconditionally
@@ -832,7 +809,35 @@
create: updateAttrs,
update: updateAttrs
};
- const patch = init([eventListenersModule, attrsModule, propsModule]);
+ //------------------------------------------------------------------------------
+ // class.ts
+ //------------------------------------------------------------------------------
+ function updateClass(oldVnode, vnode) {
+ var cur, name, elm, oldClass = oldVnode.data.class, klass = vnode.data.class;
+ if (!oldClass && !klass)
+ return;
+ if (oldClass === klass)
+ return;
+ oldClass = oldClass || {};
+ klass = klass || {};
+ elm = vnode.elm;
+ for (name in oldClass) {
+ if (!klass[name]) {
+ elm.classList.remove(name);
+ }
+ }
+ for (name in klass) {
+ cur = klass[name];
+ if (cur !== oldClass[name]) {
+ elm.classList[cur ? "add" : "remove"](name);
+ }
+ }
+ }
+ const classModule = { create: updateClass, update: updateClass };
+ //------------------------------------------------------------------------------
+ // patch
+ //------------------------------------------------------------------------------
+ const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
/**
* Owl QWeb Expression Parser
@@ -878,7 +883,7 @@
"(": "LEFT_PAREN",
")": "RIGHT_PAREN"
};
- const OPERATORS = ".,===,==,+,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(',');
+ const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%".split(",");
let tokenizeString = function (expr) {
let s = expr[0];
let start = s;
@@ -1031,8 +1036,7 @@
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
}
- else if (prevToken.type === "LEFT_BRACE" ||
- prevToken.type === "COMMA") {
+ else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") {
isVar = false;
@@ -1053,17 +1057,75 @@
return result;
}
+ /**
+ * We define here a simple event bus: it can
+ * - emit events
+ * - add/remove listeners.
+ *
+ * This is a useful pattern of communication in many cases. For OWL, each
+ * components and stores are event buses.
+ */
+ //------------------------------------------------------------------------------
+ // EventBus
+ //------------------------------------------------------------------------------
+ class EventBus {
+ constructor() {
+ this.subscriptions = {};
+ }
+ /**
+ * Add a listener for the 'eventType' events.
+ *
+ * Note that the 'owner' of this event can be anything, but will more likely
+ * be a component or a class. The idea is that the callback will be called with
+ * the proper owner bound.
+ *
+ * Also, the owner should be kind of unique. This will be used to remove the
+ * listener.
+ */
+ on(eventType, owner, callback) {
+ if (!callback) {
+ throw new Error("Missing callback");
+ }
+ if (!this.subscriptions[eventType]) {
+ this.subscriptions[eventType] = [];
+ }
+ this.subscriptions[eventType].push({
+ owner,
+ callback
+ });
+ }
+ /**
+ * Remove a listener
+ */
+ off(eventType, owner) {
+ const subs = this.subscriptions[eventType];
+ if (subs) {
+ this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
+ }
+ }
+ /**
+ * Emit an event of type 'eventType'. Any extra arguments will be passed to
+ * the listeners callback.
+ */
+ trigger(eventType, ...args) {
+ const subs = this.subscriptions[eventType] || [];
+ for (let i = 0, iLen = subs.length; i < iLen; i++) {
+ const sub = subs[i];
+ sub.callback.call(sub.owner, ...args);
+ }
+ }
+ /**
+ * Remove all subscriptions.
+ */
+ clear() {
+ this.subscriptions = {};
+ }
+ }
+
//------------------------------------------------------------------------------
// Const/global stuff/helpers
//------------------------------------------------------------------------------
- const DISABLED_TAGS = [
- "input",
- "textarea",
- "button",
- "select",
- "option",
- "optgroup"
- ];
+ const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
const DIRECTIVE_NAMES = {
@@ -1080,14 +1142,20 @@
};
const UTILS = {
h: h,
- objectToAttrString(obj) {
- let classes = [];
- for (let k in obj) {
- if (obj[k]) {
- classes.push(k);
+ toObj(expr) {
+ if (typeof expr === "string") {
+ expr = expr.trim();
+ if (!expr) {
+ return {};
}
+ let words = expr.split(/\s+/);
+ let result = {};
+ for (let i = 0; i < words.length; i++) {
+ result[words[i]] = true;
+ }
+ return result;
}
- return classes.join(" ");
+ return expr;
},
shallowEqual(p1, p2) {
for (let k in p1) {
@@ -1110,8 +1178,9 @@
//------------------------------------------------------------------------------
// QWeb rendering engine
//------------------------------------------------------------------------------
- class QWeb {
+ class QWeb extends EventBus {
constructor(data) {
+ super();
this.templates = {};
this.utils = UTILS;
// the id field is useful to be able to hash qweb instances. The current
@@ -1144,7 +1213,10 @@
* Add a template to the internal template map. Note that it is not
* immediately compiled.
*/
- addTemplate(name, xmlString) {
+ addTemplate(name, xmlString, allowDuplicate) {
+ if (allowDuplicate && name in this.templates) {
+ return;
+ }
const doc = parseXML(xmlString);
if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)");
@@ -1228,9 +1300,15 @@
}
return template.fn.call(this, context, extra);
}
- _compile(name, elem) {
+ _compile(name, elem, parentNode) {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new Context(name);
+ if (parentNode) {
+ ctx.nextID = parentNode + 1;
+ ctx.parentNode = parentNode;
+ ctx.allowMultipleRoots = true;
+ ctx.addLine(`let c${parentNode} = extra.parentNode;`);
+ }
this._compileNode(elem, ctx);
if (ctx.shouldProtectContext) {
ctx.code.unshift(" context = Object.create(context);");
@@ -1246,10 +1324,12 @@
if (ctx.shouldDefineUtils) {
ctx.code.unshift(" let utils = this.utils;");
}
- if (!ctx.rootNode) {
- throw new Error("A template should have one root node");
+ if (!parentNode) {
+ if (!ctx.rootNode) {
+ throw new Error("A template should have one root node");
+ }
+ ctx.addLine(`return vn${ctx.rootNode};`);
}
- ctx.addLine(`return vn${ctx.rootNode};`);
let template;
try {
template = new Function("context", "extra", ctx.code.join("\n"));
@@ -1262,8 +1342,11 @@
throw new Error(`Invalid generated code while compiling template '${templateName}': ${e.message}`);
}
if (isDebug) {
- console.log(`Template: ${this.templates[name].elem.outerHTML}\nCompiled code:\n` +
- template.toString());
+ const tpl = this.templates[name];
+ if (tpl) {
+ const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
+ console.log(msg);
+ }
}
return template;
}
@@ -1301,8 +1384,7 @@
if (firstLetter === firstLetter.toUpperCase()) {
// this is a component, we modify in place the xml document to change
// to
- node.setAttribute('t-component', node.tagName);
- node.nodeValue = 't';
+ node.setAttribute("t-component", node.tagName);
}
const attributes = node.attributes;
const validDirectives = [];
@@ -1332,7 +1414,7 @@
fullName = name;
value = attributes[j].textContent;
validDirectives.push({ directive, value, fullName });
- if (directive.name === "on" || directive.name === 'model') {
+ if (directive.name === "on" || directive.name === "model") {
withHandlers = true;
}
}
@@ -1418,56 +1500,74 @@
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
isProp = true;
}
- if ((key === "readonly" && node.nodeName === "input") ||
- node.nodeName === "textarea") {
+ if ((key === "readonly" && node.nodeName === "input") || node.nodeName === "textarea") {
isProp = true;
}
if (isProp) {
props.push(`${key}: _${val}`);
}
}
+ let classObj = "";
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
const value = attributes[i].textContent;
// regular attributes
- if (!name.startsWith("t-") &&
- !node.getAttribute("t-attf-" + name)) {
+ if (!name.startsWith("t-") && !node.getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
- ctx.addLine(`var _${attID} = '${value}';`);
- if (!name.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- name = '"' + name + '"';
+ if (name === "class") {
+ let classDef = value
+ .trim()
+ .split(/\s+/)
+ .map(a => `'${a}':true`)
+ .join(",");
+ classObj = `_${ctx.generateID()}`;
+ ctx.addLine(`let ${classObj} = {${classDef}};`);
+ }
+ else {
+ ctx.addLine(`var _${attID} = '${value}';`);
+ if (!name.match(/^[a-zA-Z]+$/)) {
+ // attribute contains 'non letters' => we want to quote it
+ name = '"' + name + '"';
+ }
+ attrs.push(`${name}: _${attID}`);
+ handleBooleanProps(name, attID);
}
- attrs.push(`${name}: _${attID}`);
- handleBooleanProps(name, attID);
}
// dynamic attributes
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = v.id || ctx.formatExpression(v);
- if (formattedValue[0] === "{" &&
- formattedValue[formattedValue.length - 1] === "}") {
- formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
+ if (attName === "class") {
+ formattedValue = `this.utils.toObj(${formattedValue})`;
+ if (classObj) {
+ ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
+ }
+ else {
+ classObj = `_${ctx.generateID()}`;
+ ctx.addLine(`let ${classObj} = ${formattedValue};`);
+ }
}
- const attID = ctx.generateID();
- if (!attName.match(/^[a-zA-Z]+$/)) {
- // attribute contains 'non letters' => we want to quote it
- attName = '"' + attName + '"';
+ else {
+ const attID = ctx.generateID();
+ if (!attName.match(/^[a-zA-Z]+$/)) {
+ // attribute contains 'non letters' => we want to quote it
+ attName = '"' + attName + '"';
+ }
+ // we need to combine dynamic with non dynamic attributes:
+ // class="a" t-att-class="'yop'" should be rendered as class="a yop"
+ const attValue = node.getAttribute(attName);
+ if (attValue) {
+ const attValueID = ctx.generateID();
+ ctx.addLine(`var _${attValueID} = ${formattedValue};`);
+ formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
+ const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
+ attrs.splice(attrIndex, 1);
+ }
+ ctx.addLine(`var _${attID} = ${formattedValue};`);
+ attrs.push(`${attName}: _${attID}`);
+ handleBooleanProps(attName, attID);
}
- // we need to combine dynamic with non dynamic attributes:
- // class="a" t-att-class="'yop'" should be rendered as class="a yop"
- const attValue = node.getAttribute(attName);
- if (attValue) {
- const attValueID = ctx.generateID();
- ctx.addLine(`var _${attValueID} = ${formattedValue};`);
- formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
- const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
- attrs.splice(attrIndex, 1);
- }
- ctx.addLine(`var _${attID} = ${formattedValue};`);
- attrs.push(`${attName}: _${attID}`);
- handleBooleanProps(attName, attID);
}
if (name.startsWith("t-attf-")) {
let attName = name.slice(7);
@@ -1508,6 +1608,9 @@
if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`);
}
+ if (classObj) {
+ parts.push(`class:${classObj}`);
+ }
if (withHandlers) {
parts.push(`on:{}`);
}
@@ -1559,6 +1662,7 @@
this.shouldProtectContext = false;
this.inLoop = false;
this.inPreTag = false;
+ this.allowMultipleRoots = false;
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("var h = this.utils.h;");
@@ -1568,7 +1672,9 @@
return id;
}
withParent(node) {
- if (this === this.rootContext && (this.parentNode || this.parentTextNode)) {
+ if (!this.allowMultipleRoots &&
+ this === this.rootContext &&
+ (this.parentNode || this.parentTextNode)) {
throw new Error("A template should not have more than one root node");
}
if (!this.rootContext.rootNode) {
@@ -1670,10 +1776,10 @@
this.refs = {};
const defaultProps = this.constructor.defaultProps;
if (defaultProps) {
- props = this._applyDefaultProps(props, defaultProps);
+ props = this.__applyDefaultProps(props, defaultProps);
}
if (QWeb.dev) {
- this._validateProps(props || {});
+ this.__validateProps(props || {});
}
// is this a good idea?
// Pro: if props is empty, we can create easily a component
@@ -1689,6 +1795,19 @@
}
else {
this.env = parent;
+ this.env.qweb.on("update", this, () => {
+ if (this.__owl__.isMounted) {
+ this.render(true);
+ }
+ if (this.__owl__.isDestroyed) {
+ // this is unlikely to happen, but if a root widget is destroyed,
+ // we want to remove our subscription. The usual way to do that
+ // would be to perform some check in the destroy method, but since
+ // it is very performance sensitive, and since this is a rare event,
+ // we simply do it lazily
+ this.env.qweb.off("update", this);
+ }
+ });
}
this.__owl__ = {
id: id,
@@ -1792,20 +1911,20 @@
* created declaratively in templates are managed by the Owl system.
*/
async mount(target) {
- const vnode = await this._prepare();
+ const vnode = await this.__prepare();
if (this.__owl__.isDestroyed) {
// component was destroyed before we get here...
return;
}
- this._patch(vnode);
+ this.__patch(vnode);
target.appendChild(this.el);
if (document.body.contains(target)) {
- this._callMounted();
+ this.__callMounted();
}
}
unmount() {
if (this.__owl__.isMounted) {
- this._callWillUnmount();
+ this.__callWillUnmount();
this.el.remove();
}
}
@@ -1818,13 +1937,13 @@
if (shouldPatch) {
patchQueue = [];
}
- const renderVDom = this._render(force, patchQueue);
+ const renderVDom = this.__render(force, patchQueue);
const renderId = __owl__.renderId;
await renderVDom;
if (shouldPatch && __owl__.isMounted && renderId === __owl__.renderId) {
// we only update the vnode and the actual DOM if no other rendering
// occurred between now and when the render method was initially called.
- this._applyPatchQueue(patchQueue);
+ this.__applyPatchQueue(patchQueue);
}
}
/**
@@ -1840,7 +1959,7 @@
const __owl__ = this.__owl__;
if (!__owl__.isDestroyed) {
const el = this.el;
- this._destroy(__owl__.parent);
+ this.__destroy(__owl__.parent);
if (el) {
el.remove();
}
@@ -1892,7 +2011,7 @@
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
- _destroy(parent) {
+ __destroy(parent) {
const __owl__ = this.__owl__;
const isMounted = __owl__.isMounted;
if (isMounted) {
@@ -1901,7 +2020,7 @@
}
const children = __owl__.children;
for (let key in children) {
- children[key]._destroy(this);
+ children[key].__destroy(this);
}
if (parent) {
let id = __owl__.id;
@@ -1911,13 +2030,13 @@
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
- _callMounted() {
+ __callMounted() {
const __owl__ = this.__owl__;
const children = __owl__.children;
for (let id in children) {
const comp = children[id];
if (!comp.__owl__.isMounted && this.el.contains(comp.el)) {
- comp._callMounted();
+ comp.__callMounted();
}
}
__owl__.isMounted = true;
@@ -1927,7 +2046,7 @@
}
this.mounted();
}
- _callWillUnmount() {
+ __callWillUnmount() {
this.willUnmount();
const __owl__ = this.__owl__;
__owl__.isMounted = false;
@@ -1935,38 +2054,41 @@
for (let id in children) {
const comp = children[id];
if (comp.__owl__.isMounted) {
- comp._callWillUnmount();
+ comp.__callWillUnmount();
}
}
}
- async _updateProps(nextProps, forceUpdate = false, patchQueue) {
+ async __updateProps(nextProps, forceUpdate = false, patchQueue) {
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const defaultProps = this.constructor.defaultProps;
if (defaultProps) {
- nextProps = this._applyDefaultProps(nextProps, defaultProps);
+ nextProps = this.__applyDefaultProps(nextProps, defaultProps);
}
if (QWeb.dev) {
- this._validateProps(nextProps);
+ this.__validateProps(nextProps);
}
await this.willUpdateProps(nextProps);
this.props = nextProps;
await this.render(forceUpdate, patchQueue);
}
}
- _patch(vnode) {
+ __patch(vnode) {
const __owl__ = this.__owl__;
__owl__.renderPromise = null;
const target = __owl__.vnode || document.createElement(vnode.sel);
+ if (this.__owl__.classObj) {
+ vnode.data.class = Object.assign(vnode.data.class || {}, this.__owl__.classObj);
+ }
__owl__.vnode = patch(target, vnode);
}
- _prepare() {
+ __prepare() {
const __owl__ = this.__owl__;
__owl__.renderProps = this.props;
- __owl__.renderPromise = this._prepareAndRender();
+ __owl__.renderPromise = this.__prepareAndRender();
return __owl__.renderPromise;
}
- async _prepareAndRender() {
+ async __prepareAndRender() {
await this.willStart();
const __owl__ = this.__owl__;
if (__owl__.isDestroyed) {
@@ -1986,9 +2108,7 @@
this.template = template;
}
else {
- while ((template = p.name) &&
- !(template in qweb.templates) &&
- p !== Component) {
+ while ((template = p.name) && !(template in qweb.templates) && p !== Component) {
p = p.__proto__;
}
if (p === Component) {
@@ -2001,10 +2121,10 @@
}
}
__owl__.render = qweb.render.bind(qweb, this.template);
- this._observeState();
- return this._render();
+ this.__observeState();
+ return this.__render();
}
- async _render(force = false, patchQueue = []) {
+ async __render(force = false, patchQueue = []) {
const __owl__ = this.__owl__;
__owl__.renderId++;
const promises = [];
@@ -2039,25 +2159,28 @@
/**
* Only called by qweb t-component directive
*/
- _mount(vnode, elm) {
+ __mount(vnode, elm) {
const __owl__ = this.__owl__;
+ if (__owl__.classObj) {
+ vnode.data.class = Object.assign(vnode.data.class || {}, __owl__.classObj);
+ }
__owl__.vnode = patch(elm, vnode);
if (__owl__.parent.__owl__.isMounted && !__owl__.isMounted) {
- this._callMounted();
+ this.__callMounted();
}
return __owl__.vnode;
}
/**
* Only called by qweb t-component directive (when t-keepalive is set)
*/
- _remount() {
+ __remount() {
const __owl__ = this.__owl__;
if (!__owl__.isMounted) {
__owl__.isMounted = true;
this.mounted();
}
}
- _observeState() {
+ __observeState() {
if (this.state) {
const __owl__ = this.__owl__;
__owl__.observer = new Observer();
@@ -2071,7 +2194,7 @@
* Note that this method does not modify in place the props, it returns a new
* prop object
*/
- _applyDefaultProps(props, defaultProps) {
+ __applyDefaultProps(props, defaultProps) {
props = props ? Object.create(props) : {};
for (let propName in defaultProps) {
if (props[propName] === undefined) {
@@ -2084,10 +2207,10 @@
* Apply the given patch queue. A patch is a pair [c, vn], where c is a
* Component instance and vn a VNode.
* 1) Call 'willPatch' on the component of each patch
- * 2) Call '_patch' on the component of each patch
+ * 2) Call '__patch' on the component of each patch
* 3) Call 'patched' on the component of each patch, in inverse order
*/
- _applyPatchQueue(patchQueue) {
+ __applyPatchQueue(patchQueue) {
const patchLen = patchQueue.length;
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
@@ -2095,7 +2218,7 @@
}
for (let i = 0; i < patchLen; i++) {
const patch = patchQueue[i];
- patch[0]._patch(patch[1]);
+ patch[0].__patch(patch[1]);
}
for (let i = patchLen - 1; i >= 0; i--) {
const patch = patchQueue[i];
@@ -2108,7 +2231,7 @@
* visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode.
*/
- _validateProps(props) {
+ __validateProps(props) {
const propsDef = this.constructor.props;
if (propsDef instanceof Array) {
// list of strings (prop names)
@@ -2180,71 +2303,6 @@
return result;
}
- /**
- * We define here a simple event bus: it can
- * - emit events
- * - add/remove listeners.
- *
- * This is a useful pattern of communication in many cases. For OWL, each
- * components and stores are event buses.
- */
- //------------------------------------------------------------------------------
- // EventBus
- //------------------------------------------------------------------------------
- class EventBus {
- constructor() {
- this.subscriptions = {};
- }
- /**
- * Add a listener for the 'eventType' events.
- *
- * Note that the 'owner' of this event can be anything, but will more likely
- * be a component or a class. The idea is that the callback will be called with
- * the proper owner bound.
- *
- * Also, the owner should be kind of unique. This will be used to remove the
- * listener.
- */
- on(eventType, owner, callback) {
- if (!callback) {
- throw new Error("Missing callback");
- }
- if (!this.subscriptions[eventType]) {
- this.subscriptions[eventType] = [];
- }
- this.subscriptions[eventType].push({
- owner,
- callback
- });
- }
- /**
- * Remove a listener
- */
- off(eventType, owner) {
- const subs = this.subscriptions[eventType];
- if (subs) {
- this.subscriptions[eventType] = subs.filter(s => s.owner !== owner);
- }
- }
- /**
- * Emit an event of type 'eventType'. Any extra arguments will be passed to
- * the listeners callback.
- */
- trigger(eventType, ...args) {
- const subs = this.subscriptions[eventType] || [];
- for (let i = 0, iLen = subs.length; i < iLen; i++) {
- const sub = subs[i];
- sub.callback.call(sub.owner, ...args);
- }
- }
- /**
- * Remove all subscriptions.
- */
- clear() {
- this.subscriptions = {};
- }
- }
-
/**
* Owl QWeb Directives
*
@@ -2452,9 +2510,7 @@
}
}
// compile sub template
- const subCtx = ctx
- .subContext("caller", nodeCopy)
- .subContext("variables", Object.create(vars));
+ const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
qweb._compileNode(nodeTemplate.elem, subCtx);
// close new scope
if (hasNewVariables) {
@@ -2522,7 +2578,7 @@
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "debug",
- priority: 99,
+ priority: 1,
atNodeEncounter({ ctx }) {
ctx.addLine("debugger;");
}
@@ -2532,7 +2588,7 @@
//------------------------------------------------------------------------------
QWeb.addDirective({
name: "log",
- priority: 99,
+ priority: 1,
atNodeEncounter({ ctx, value }) {
const expr = ctx.formatExpression(value);
ctx.addLine(`console.log(${expr})`);
@@ -2580,9 +2636,7 @@
ctx.addIf(`!context['${handlerName}']`);
ctx.addLine(`throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace(/`/g, "'")}'\`)`);
ctx.closeIf();
- let params = extraArgs
- ? `owner, ${ctx.formatExpression(extraArgs)}`
- : "owner";
+ let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
let handler;
if (mods.length > 0) {
handler = `function (e) {`;
@@ -2627,8 +2681,7 @@
const elm = vn.elm;
// remove potential duplicated vnode that is currently being removed, to
// prevent from having twice the same node in the DOM during an animation
- const dup = elm.parentElement &&
- elm.parentElement.querySelector(`*[data-owl-key='${vn.key}']`);
+ const dup = elm.parentElement && elm.parentElement.querySelector(`*[data-owl-key='${vn.key}']`);
if (dup) {
dup.remove();
}
@@ -2795,9 +2848,9 @@
* // perspective.
* context.__owl__.cmap[key5] = w4.__owl__.id;
*
- * // _prepare is called, to basically call willStart, then render the
+ * // __prepare is called, to basically call willStart, then render the
* // component
- * def3 = w4._prepare();
+ * def3 = w4.__prepare();
*
* def3 = def3.then(vnode => {
* // we create here a virtual node for the parent (NOT the component). This
@@ -2811,11 +2864,11 @@
* // component at the proper time
* pvnode.data.hook = {
* insert(vn) {
- * // the _mount method will patch the component vdom into the elm vn.elm,
+ * // the __mount method will patch the component vdom into the elm vn.elm,
* // then call the mounted hooks. However, suprisingly, the snabbdom
* // patch method actually replace the elm by a new elm, so we need
* // to synchronise the pvnode elm with the resulting elm
- * let nvn = w4._mount(vnode, vn.elm);
+ * let nvn = w4.__mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
* // what follows is only present if there are animations on the component
* utils.transitionInsert(vn, "fade");
@@ -2843,10 +2896,10 @@
* });
* } else {
* // this is the 'update' path of the directive.
- * // the call to _updateProps is the actual component update
+ * // the call to __updateProps is the actual component update
* // Note that we only update the props if we cannot reuse the previous
* // rendering work (in the case it was rendered with the same props)
- * def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
+ * def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
* def3 = def3.then(() => {
* // if component was destroyed in the meantime, we do nothing (so, this
* // means that the parent's element children list will have a null in
@@ -2929,6 +2982,17 @@
: ctx.inLoop
? `String(-${componentID} - i)`
: String(componentID);
+ if (ctx.allowMultipleRoots) {
+ // necessary to prevent collisions
+ if (!key && ctx.inLoop) {
+ let id = ctx.generateID();
+ ctx.addLine(`let template${id} = "_slot_" + String(-${componentID} - i)`);
+ templateID = `template${id}`;
+ }
+ else {
+ templateID = `"_slot_${templateID}"`;
+ }
+ }
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey = "";
@@ -2961,25 +3025,29 @@
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar;
}
- let updateClassCode = "";
+ let classObj = "";
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
- let classCode = "";
if (classAttr) {
- classCode =
- classAttr
- .split(" ")
- .map(c => `vn.elm.classList.add('${c}')`)
- .join(";") + ";";
+ let classDef = classAttr
+ .trim()
+ .split(/\s+/)
+ .map(a => `'${a}':true`)
+ .join(",");
+ classObj = `_${ctx.generateID()}`;
+ ctx.addLine(`let ${classObj} = {${classDef}};`);
}
if (tattClass) {
- const attVar = `_${ctx.generateID()}`;
- ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`);
- classCode = `for (let k in ${attVar}) {
- if (${attVar}[k]) {
- vn.elm.classList.add(k);
- }
- }`;
- updateClassCode = `let cl=w${componentID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
+ let tattExpr = ctx.formatExpression(tattClass);
+ if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
+ tattExpr = `this.utils.toObj(${tattExpr})`;
+ }
+ if (classAttr) {
+ ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
+ }
+ else {
+ classObj = `_${ctx.generateID()}`;
+ ctx.addLine(`let ${classObj} = ${tattExpr};`);
+ }
}
let eventsCode = events
.map(function ([eventName, mods, handlerName, extraArgs]) {
@@ -3015,7 +3083,7 @@
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
- createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`;
+ createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
}
ctx.addLine(`let w${componentID} = ${templateID} in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[${templateID}]] : false;`);
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
@@ -3044,7 +3112,7 @@
ctx.addLine(`w${componentID} = new W${componentID}(owner, props${componentID});`);
ctx.addLine(`context.__owl__.cmap[${templateID}] = w${componentID}.__owl__.id;`);
// SLOTS
- if (node.childElementCount) {
+ if (node.childNodes.length) {
const clone = node.cloneNode(true);
const slotNodes = clone.querySelectorAll("[t-set]");
const slotId = qweb.nextSlotId++;
@@ -3055,40 +3123,42 @@
slotNode.parentElement.removeChild(slotNode);
const key = slotNode.getAttribute("t-set");
slotNode.removeAttribute("t-set");
- const slotFn = qweb._compile(`slot_${key}_template`, slotNode);
+ const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx.parentNode);
qweb.slots[`${slotId}_${key}`] = slotFn.bind(qweb);
}
}
- if (clone.childElementCount) {
- const content = clone.children[0];
- const slotFn = qweb._compile(`slot_default_template`, content);
+ if (clone.childNodes.length) {
+ const t = clone.ownerDocument.createElement("t");
+ for (let child of Object.values(clone.childNodes)) {
+ t.appendChild(child);
+ }
+ const slotFn = qweb._compile(`slot_default_template`, t, ctx.parentNode);
qweb.slots[`${slotId}_default`] = slotFn.bind(qweb);
}
}
- ctx.addLine(`def${defID} = w${componentID}._prepare();`);
+ ctx.addLine(`def${defID} = w${componentID}.__prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
- ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`);
+ ctx.addLine(`def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${componentID}.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeComponentCode}}}});c${ctx.parentNode}[_${dummyID}_index]=pvnode;w${componentID}.__owl__.pvnode = pvnode;});`);
ctx.addElse();
// need to update component
- const patchQueueCode = async
- ? `patchQueue${componentID}`
- : "extra.patchQueue";
- ctx.addLine(`def${defID} = def${defID} || w${componentID}._updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`);
+ const patchQueueCode = async ? `patchQueue${componentID}` : "extra.patchQueue";
+ ctx.addLine(`def${defID} = def${defID} || w${componentID}.__updateProps(props${componentID}, extra.forceUpdate, ${patchQueueCode});`);
let keepAliveCode = "";
if (keepAlive) {
- keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}._remount();};`;
+ keepAliveCode = `pvnode.data.hook.insert = vn => {vn.elm.parentNode.replaceChild(w${componentID}.el,vn.elm);vn.elm=w${componentID}.el;w${componentID}.__remount();};`;
}
- ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""}${updateClassCode}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`);
+ ctx.addLine(`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${ctx.parentNode}[_${dummyID}_index]=pvnode;});`);
ctx.closeIf();
+ if (classObj) {
+ ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
+ }
if (async) {
- ctx.addLine(`def${defID}.then(w${componentID}._applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`);
+ ctx.addLine(`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`);
}
else {
ctx.addLine(`extra.promises.push(def${defID});`);
}
- if (node.hasAttribute("t-if") ||
- node.hasAttribute("t-else") ||
- node.hasAttribute("t-elif")) {
+ if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
ctx.closeIf();
}
return true;
@@ -3130,7 +3200,9 @@
atNodeEncounter({ ctx, value }) {
const slotKey = ctx.generateID();
ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`);
- ctx.addLine(`c${ctx.parentNode}.push(slot${slotKey}(context.__owl__.parent, extra));`);
+ ctx.addIf(`slot${slotKey}`);
+ ctx.addLine(`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ctx.parentNode}}));`);
+ ctx.closeIf();
return true;
}
});
@@ -3183,16 +3255,14 @@
super();
this._commitLevel = 0;
this.history = [];
+ this._updateId = 1;
this.debug = options.debug || false;
this.state = config.state || {};
this.actions = config.actions;
this.mutations = config.mutations;
this.env = config.env;
this.observer = new Observer();
- this.observer.notifyCB = () => {
- this._gettersCache = {};
- this.trigger("update");
- };
+ this.observer.notifyCB = this.__notifyComponents.bind(this);
this.observer.allowMutations = false;
this.observer.observe(this.state);
this.getters = {};
@@ -3216,7 +3286,7 @@
};
}
}
- dispatch(action, payload) {
+ dispatch(action, ...payload) {
if (!this.actions[action]) {
throw new Error(`[Error] action ${action} is undefined`);
}
@@ -3226,7 +3296,7 @@
env: this.env,
state: this.state,
getters: this.getters
- }, payload);
+ }, ...payload);
if (result instanceof Promise) {
return new Promise((resolve, reject) => {
result.then(() => resolve());
@@ -3234,7 +3304,7 @@
});
}
}
- commit(type, payload) {
+ commit(type, ...payload) {
if (!this.mutations[type]) {
throw new Error(`[Error] mutation ${type} is undefined`);
}
@@ -3244,20 +3314,47 @@
commit: this.commit.bind(this),
state: this.state,
getters: this.getters
- }, payload);
+ }, ...payload);
if (this._commitLevel === 1) {
this.observer.allowMutations = false;
if (this.debug) {
this.history.push({
state: this.state,
mutation: type,
- payload: payload
+ payload: [...payload]
});
}
}
this._commitLevel--;
return res;
}
+ /**
+ * Instead of using trigger to emit an update event, we actually implement
+ * our own function to do that. The reason is that we need to be smarter than
+ * a simple trigger function: we need to wait for parent components to be
+ * done before doing children components. The reason is that if an update
+ * as an effect of destroying a children, we do not want to call the
+ * mapStoreToProps function of the child, nor rendering it.
+ *
+ * This method is not optimal if we have a bunch of asynchronous components:
+ * we wait sequentially for each component to be completed before updating the
+ * next. However, the only things that matters is that children are updated
+ * after their parents. So, this could be optimized by being smarter, and
+ * updating all widgets concurrently, except for parents/children.
+ */
+ async __notifyComponents() {
+ this._updateId++;
+ const current = this._updateId;
+ this._gettersCache = {};
+ const subs = this.subscriptions.update || [];
+ for (let i = 0, iLen = subs.length; i < iLen; i++) {
+ const sub = subs[i];
+ const shouldCallback = sub.owner ? sub.owner.__owl__.isMounted : true;
+ if (shouldCallback) {
+ await sub.callback.call(sub.owner, current);
+ }
+ }
+ }
}
//------------------------------------------------------------------------------
// Connect function
@@ -3326,15 +3423,18 @@
* updates to be called for the parents before the children. However,
* if we use the mounted hook, this will be done in the reverse order.
*/
- _callMounted() {
- this.__owl__.store.on("update", this, this._checkUpdate);
- super._callMounted();
+ __callMounted() {
+ this.__owl__.store.on("update", this, this.__checkUpdate);
+ super.__callMounted();
}
willUnmount() {
this.__owl__.store.off("update", this);
super.willUnmount();
}
- _checkUpdate() {
+ async __checkUpdate(updateId) {
+ if (updateId === this.__owl__.currentUpdateId) {
+ return;
+ }
const ownProps = this.__owl__.ownProps;
const storeProps = mapStoreToProps(this.__owl__.store.state, ownProps, this.__owl__.store.getters);
const options = {
@@ -3353,16 +3453,18 @@
}
if (didChange) {
this.__owl__.currentStoreProps = storeProps;
- this._updateProps(ownProps, false);
+ await this.__updateProps(ownProps, false);
}
}
- _updateProps(nextProps, forceUpdate, patchQueue) {
- if (this.__owl__.ownProps !== nextProps) {
- this.__owl__.currentStoreProps = mapStoreToProps(this.__owl__.store.state, nextProps, this.__owl__.store.getters);
+ __updateProps(nextProps, forceUpdate, patchQueue) {
+ const __owl__ = this.__owl__;
+ __owl__.currentUpdateId = __owl__.store._updateId;
+ if (__owl__.ownProps !== nextProps) {
+ __owl__.currentStoreProps = mapStoreToProps(__owl__.store.state, nextProps, __owl__.store.getters);
}
- this.__owl__.ownProps = nextProps;
- const mergedProps = Object.assign({}, nextProps, this.__owl__.currentStoreProps);
- return super._updateProps(mergedProps, forceUpdate, patchQueue);
+ __owl__.ownProps = nextProps;
+ const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps);
+ return super.__updateProps(mergedProps, forceUpdate, patchQueue);
}
};
// we assign here a unique name to the resulting anonymous class.
@@ -3508,9 +3610,9 @@
exports.connect = connect;
exports.utils = utils;
- exports.__info__.version = '0.15.0';
- exports.__info__.date = '2019-06-21T19:47:14.131Z';
- exports.__info__.hash = '63a8fcd';
+ exports.__info__.version = '0.16.0';
+ exports.__info__.date = '2019-06-28T09:20:47.478Z';
+ exports.__info__.hash = 'd381e85';
exports.__info__.url = 'https://github.com/odoo/owl';
}(this.owl = this.owl || {}));
diff --git a/playground/app.js b/playground/app.js
index 49a5e87d..629c7051 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -97,10 +97,7 @@ function makeCodeIframe(js, css, xml, errorHandler) {
setTimeout(function() {
if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener("error", errorHandler);
- iframe.contentWindow.removeEventListener(
- "unhandledrejection",
- errorHandler
- );
+ iframe.contentWindow.removeEventListener("unhandledrejection", errorHandler);
}
}, 200);
doc.body.appendChild(script);
@@ -200,6 +197,8 @@ class App extends owl.Component {
} catch (e) {
//probably problem with the templates
error = e;
+ // we still log the error, always useful to have it available
+ console.error(e);
}
if (error) {
this.displayError(error.message);
@@ -284,15 +283,7 @@ class TabbedEditor extends owl.Component {
this.setTab = owl.utils.debounce(this.setTab, 250, true);
this.sessions = {};
- for (let tab of ["js", "xml", "css"]) {
- if (props[tab]) {
- this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
- this.sessions[tab].setOption("useWorker", false);
- const tabSize = tab === "xml" ? 2 : 4;
- this.sessions[tab].setOption("tabSize", tabSize);
- this.sessions[tab].setUndoManager(new ace.UndoManager());
- }
- }
+ this._setupSessions(props);
this.editor = null;
}
@@ -317,9 +308,19 @@ class TabbedEditor extends owl.Component {
});
}
+ willUpdateProps(nextProps) {
+ this._setupSessions(nextProps);
+ }
+
patched() {
const session = this.sessions[this.state.currentTab];
- session.setValue(this.props[this.state.currentTab], -1);
+ let content = this.props[this.state.currentTab];
+ if (content === false) {
+ const tab = this.props.js ? "js" : this.props.xml ? "xml" : "css";
+ content = this.props[tab];
+ this.state.currentTab = tab;
+ }
+ session.setValue(content, -1);
this.editor.setSession(session);
this.editor.resize();
}
@@ -347,6 +348,18 @@ class TabbedEditor extends owl.Component {
});
}
}
+
+ _setupSessions(props) {
+ for (let tab of ["js", "xml", "css"]) {
+ if (props[tab] && !this.sessions[tab]) {
+ this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
+ this.sessions[tab].setOption("useWorker", false);
+ const tabSize = tab === "xml" ? 2 : 4;
+ this.sessions[tab].setOption("tabSize", tabSize);
+ this.sessions[tab].setUndoManager(new ace.UndoManager());
+ }
+ }
+ }
}
//------------------------------------------------------------------------------
diff --git a/playground/samples.js b/playground/samples.js
index e037e42e..68980b28 100644
--- a/playground/samples.js
+++ b/playground/samples.js
@@ -942,31 +942,30 @@ class App extends owl.Component {
}
//------------------------------------------------------------------------------
-// Application Startup
+// Responsive plugin
//------------------------------------------------------------------------------
-function isMobile() {
- return window.innerWidth <= 768;
+function setupResponsivePlugin(env) {
+ const isMobile = () => window.innerWidth <= 768;
+ env.isMobile = isMobile();
+ const updateEnv = owl.utils.debounce(() => {
+ if (env.isMobile !== isMobile()) {
+ env.isMobile = !env.isMobile;
+ env.qweb.trigger('update');
+ }
+ }, 15);
+ window.addEventListener("resize", updateEnv);
}
+//------------------------------------------------------------------------------
+// Application Startup
+//------------------------------------------------------------------------------
const env = {
qweb: new owl.QWeb(TEMPLATES),
- isMobile: isMobile()
};
-
+setupResponsivePlugin(env);
const app = new App(env);
app.mount(document.body);
-
-function updateEnv() {
- const _isMobile = isMobile();
- if (_isMobile !== env.isMobile) {
- app.updateEnv({
- isMobile: _isMobile
- });
- }
-}
-
-window.addEventListener("resize", owl.utils.debounce(updateEnv, 20));
`;
const RESPONSIVE_XML = `
@@ -1133,10 +1132,10 @@ const SLOTS_XML = `
-
+
@@ -1352,7 +1351,7 @@ export const SAMPLES = [
description: "Lifecycle demo",
code: LIFECYCLE_DEMO,
xml: LIFECYCLE_DEMO_XML,
- css: LIFECYCLE_CSS,
+ css: LIFECYCLE_CSS
},
{
description: "Todo List App (with store)",
diff --git a/playground/templates.xml b/playground/templates.xml
index 1c67cc6d..ffeb442d 100644
--- a/playground/templates.xml
+++ b/playground/templates.xml
@@ -27,7 +27,7 @@