diff --git a/owl.js b/owl.js index cc4c64fc..9bd02601 100644 --- a/owl.js +++ b/owl.js @@ -837,7 +837,7 @@ }; // note that the space after typeof is relevant. It makes sure that the formatted // expression has a space after typeof - const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>".split(","); + const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;".split(","); let tokenizeString = function (expr) { let s = expr[0]; let start = s; @@ -1055,7 +1055,7 @@ this.currentKey = ""; this.rootContext = this; this.templateName = name || "noname"; - this.addLine("var h = this.h;"); + this.addLine("let h = this.h;"); } generateID() { return CompilationContext.nextID++; @@ -1068,9 +1068,12 @@ * Such a key is necessary when we need to associate an id to some element * generated by a template (for example, a component) */ - generateTemplateKey() { + generateTemplateKey(prefix = "") { const id = this.generateID(); - let locationExpr = `\`__${this.generateID()}__`; + if (this.loopNumber === 0 && !this.currentKey) { + return `'${prefix}__${id}__'`; + } + let locationExpr = `\`${prefix}__${id}__`; for (let i = 0; i < this.loopNumber - 1; i++) { locationExpr += `\${i${i + 1}}__`; } @@ -1185,7 +1188,7 @@ startProtectScope() { const protectID = this.generateID(); this.rootContext.shouldDefineScope = true; - this.addLine(`const _origScope${protectID} = scope;`); + this.addLine(`let _origScope${protectID} = scope;`); this.addLine(`scope = Object.assign(Object.create(context), scope);`); return protectID; } @@ -1251,12 +1254,9 @@ if (typeof str === "number") { return String(str); } - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, "'") - .replace(/`/g, "`"); + const p = document.createElement("p"); + p.textContent = str; + return p.innerHTML; } /** * Returns a function, that, as long as it continues to be invoked, will not @@ -1352,7 +1352,13 @@ return vnode.text; } }) - .join(); + .join(""); + }, + getComponent(obj) { + while (obj && !obj.hasOwnProperty("__owl__")) { + obj = obj.__proto__; + } + return obj; } }; function parseXML(xml) { @@ -1537,8 +1543,17 @@ return vnode.text; } const node = document.createElement(vnode.sel); - const result = patch(node, vnode); - return result.elm.outerHTML; + const elem = patch(node, vnode).elm; + function escapeTextNodes(node) { + if (node.nodeType === 3) { + node.textContent = escape(node.textContent); + } + for (let n of node.childNodes) { + escapeTextNodes(n); + } + } + escapeTextNodes(elem); + return elem.outerHTML; } /** * Force all widgets connected to this QWeb instance to rerender themselves. @@ -1637,7 +1652,7 @@ // this is an unusual situation: this text node is the result of the // template rendering. let nodeID = ctx.generateID(); - ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`); + ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`); ctx.addLine(`result = vn${nodeID};`); ctx.rootContext.rootNode = nodeID; ctx.rootContext.parentTextNode = nodeID; @@ -1822,7 +1837,7 @@ } } else { - ctx.addLine(`var _${attID} = '${value}';`); + ctx.addLine(`let _${attID} = '${value}';`); if (!name.match(/^[a-zA-Z]+$/)) { // attribute contains 'non letters' => we want to quote it name = '"' + name + '"'; @@ -1858,12 +1873,12 @@ const attValue = node.getAttribute(attName); if (attValue) { const attValueID = ctx.generateID(); - ctx.addLine(`var _${attValueID} = ${formattedValue};`); + ctx.addLine(`let _${attValueID} = ${formattedValue};`); formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); attrs.splice(attrIndex, 1); } - ctx.addLine(`var _${attID} = ${formattedValue};`); + ctx.addLine(`let _${attID} = ${formattedValue};`); attrs.push(`${attName}: _${attID}`); handleBooleanProps(attName, attID); } @@ -1878,17 +1893,17 @@ const attID = ctx.generateID(); let staticVal = node.getAttribute(attName); if (staticVal) { - ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`); + ctx.addLine(`let _${attID} = '${staticVal} ' + ${formattedExpr};`); } else { - ctx.addLine(`var _${attID} = ${formattedExpr};`); + ctx.addLine(`let _${attID} = ${formattedExpr};`); } attrs.push(`${attName}: _${attID}`); } // t-att= attributes if (name === "t-att") { let id = ctx.generateID(); - ctx.addLine(`var _${id} = ${ctx.formatExpression(value)};`); + ctx.addLine(`let _${id} = ${ctx.formatExpression(value)};`); tattrs.push(id); } } @@ -1919,7 +1934,7 @@ ctx.addLine(`}`); ctx.closeIf(); } - ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`); + ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`); if (ctx.parentNode) { ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); } @@ -2009,12 +2024,12 @@ let exprID; if (typeof value === "string") { exprID = `_${ctx.generateID()}`; - ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`); + ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`); } else { exprID = `scope.${value.id}`; } - ctx.addIf(`${exprID} || ${exprID} === 0`); + ctx.addIf(`${exprID} != null`); if (ctx.escaping) { let protectID; if (value.hasBody) { @@ -2031,7 +2046,7 @@ let nodeID = ctx.generateID(); ctx.rootContext.rootNode = nodeID; ctx.rootContext.parentTextNode = nodeID; - ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`); + ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`); if (ctx.rootContext.shouldDefineResult) { ctx.addLine(`result = vn${nodeID}`); } @@ -2103,7 +2118,7 @@ const tempParentNodeID = ctx.generateID(); const _parentNode = ctx.parentNode; ctx.parentNode = tempParentNodeID; - ctx.addLine(`const c${tempParentNodeID} = new utils.VDomArray();`); + ctx.addLine(`let c${tempParentNodeID} = new utils.VDomArray();`); const nodeCopy = node.cloneNode(true); for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) { nodeCopy.removeAttribute(attr); @@ -2170,6 +2185,7 @@ // Step 1: sanity checks // ------------------------------------------------ ctx.rootContext.shouldDefineScope = true; + ctx.rootContext.shouldDefineUtils = true; if (node.nodeName !== "t") { throw new Error("Invalid tag for t-call directive (should be 't')"); } @@ -2195,7 +2211,9 @@ ctx.indent(); protectID = ctx.startProtectScope(); const nodeCopy = node.cloneNode(true); - nodeCopy.removeAttribute("t-call"); + for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) { + nodeCopy.removeAttribute(attr); + } const parentNode = ctx.parentNode; ctx.parentNode = "__0"; // this local scope is intended to trap c__0 @@ -2212,14 +2230,15 @@ // Step 4: add the appropriate function call to current component // ------------------------------------------------ const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)"; + const parentComponent = `utils.getComponent(context)`; if (ctx.parentNode) { - ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: c${ctx.parentNode}}));`); + ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, parent: ${parentComponent}}));`); } else { // this is a t-call with no parentnode, we need to extract the result ctx.rootContext.shouldDefineResult = true; ctx.addLine(`result = []`); - ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: result}));`); + ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: result, parent: ${parentComponent}}));`); ctx.addLine(`result = result[0]`); } // Step 5: restore previous scope @@ -2245,16 +2264,16 @@ const elems = node.getAttribute("t-foreach"); const name = node.getAttribute("t-as"); let arrayID = ctx.generateID(); - ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`); + ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`); ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`); let keysID = ctx.generateID(); let valuesID = ctx.generateID(); - ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`); + ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`); ctx.addIf(`!(_${arrayID} instanceof Array)`); ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`); ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`); ctx.closeIf(); - ctx.addLine(`var _length${keysID} = _${keysID}.length;`); + ctx.addLine(`let _length${keysID} = _${keysID}.length;`); let varsID = ctx.startProtectScope(); const loopVar = `i${ctx.loopNumber}`; ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`); @@ -2325,46 +2344,54 @@ self: "if (e.target !== this.elm) {return}", stop: "e.stopPropagation();" }; + const FNAMEREGEXP = /^[$A-Z_][0-9A-Z_$]*$/i; + function makeHandlerCode(ctx, fullName, value, putInCache, modcodes = MODS_CODE) { + const [event, ...mods] = fullName.slice(5).split("."); + if (!event) { + throw new Error("Missing event name with t-on directive"); + } + let code; + // check if it is a method with no args, a method with args or an expression + let args = ""; + const name = value.replace(/\(.*\)/, function (_args) { + args = _args.slice(1, -1); + return ""; + }); + const isMethodCall = name.match(FNAMEREGEXP); + // then generate code + if (isMethodCall) { + ctx.rootContext.shouldDefineUtils = true; + const comp = `utils.getComponent(context)`; + if (args) { + let argId = ctx.generateID(); + ctx.addLine(`let args${argId} = [${ctx.formatExpression(args)}];`); + code = `${comp}['${name}'](...args${argId}, e);`; + putInCache = false; + } + else { + code = `${comp}['${name}'](e);`; + } + } + else { + // if we get here, then it is an expression + putInCache = false; + code = ctx.formatExpression(value); + } + const modCode = mods.map(mod => modcodes[mod]).join(""); + let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`; + if (putInCache) { + const key = ctx.generateTemplateKey(event); + ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`); + handler = `extra.handlers[${key}]`; + } + return { event, handler }; + } QWeb.addDirective({ name: "on", priority: 90, atNodeCreation({ ctx, fullName, value, nodeID }) { - const [eventName, ...mods] = fullName.slice(5).split("."); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handlerName = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - let params = extraArgs ? `context, ${ctx.formatExpression(extraArgs)}` : "context"; - let handler = `function (e) {if (!context.__owl__.isMounted){return}`; - handler += mods - .map(function (mod) { - return MODS_CODE[mod]; - }) - .join(""); - if (handlerName) { - if (!extraArgs) { - handler += `const fn = context['${handlerName}'];`; - handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerName}; }`; - handler += `}`; - ctx.addLine(`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};`); - ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); - } - else { - const handlerKey = `handler${ctx.generateID()}`; - ctx.addLine(`const ${handlerKey} = context['${handlerName}'] && context['${handlerName}'].bind(${params});`); - handler += `if (${handlerKey}) { ${handlerKey}(e); } else { context.${value}; }`; - handler += `}`; - ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); - } - } - else { - handler += "}"; - ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); - } + const { event, handler } = makeHandlerCode(ctx, fullName, value, true); + ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`); } }); //------------------------------------------------------------------------------ @@ -2781,7 +2808,7 @@ extraNames: ["props"], priority: 100, atNodeEncounter({ ctx, value, node, qweb }) { - ctx.addLine("//COMPONENT"); + ctx.addLine(`// Component '${value}'`); ctx.rootContext.shouldDefineQWeb = true; ctx.rootContext.shouldDefineParent = true; ctx.rootContext.shouldDefineUtils = true; @@ -2796,13 +2823,7 @@ const name = attributes[i].name; const value = attributes[i].textContent; if (name.startsWith("t-on-")) { - const [eventName, ...mods] = name.slice(5).split("."); - let extraArgs; - let handlerValue = value.replace(/\(.*\)/, function (args) { - extraArgs = args.slice(1, -1); - return ""; - }); - events.push([eventName, mods, handlerValue, extraArgs]); + events.push([name, value]); } else if (name === "t-transition") { transition = value; @@ -2875,33 +2896,9 @@ } } let eventsCode = events - .map(function ([eventName, mods, handlerValue, extraArgs]) { - let params = "context"; - if (extraArgs) { - if (ctx.loopNumber) { - let argId = ctx.generateID(); - // we need to evaluate the arguments now, because the handler will - // be set asynchronously later when the widget is ready, and the - // context might be different. - ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`); - params = `context, arg${argId}`; - } - else { - params = `context, ${ctx.formatExpression(extraArgs)}`; - } - } - let handler = `function (e) {if(!context.__owl__.isMounted){return}`; - handler += mods - .map(function (mod) { - return T_COMPONENT_MODS_CODE[mod]; - }) - .join(""); - if (handlerValue) { - handler += `const fn = context['${handlerValue}'];`; - handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerValue}; }`; - } - handler += `}`; - return `vn.elm.addEventListener('${eventName}', ${handler});`; + .map(function ([name, value]) { + const { event, handler } = makeHandlerCode(ctx, name, value, false, T_COMPONENT_MODS_CODE); + return `vn.elm.addEventListener('${event}', ${handler});`; }) .join(""); const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false); @@ -4356,6 +4353,9 @@ }, set(target, k, v) { throw new Error("Store state should only be modified through actions"); + }, + has(target, k) { + return k in result; } }); } @@ -4864,9 +4864,9 @@ exports.useState = useState$1; exports.utils = utils; - exports.__info__.version = '1.0.0-beta2'; - exports.__info__.date = '2019-12-11T09:31:58.227Z'; - exports.__info__.hash = '0762eeb'; + exports.__info__.version = '1.0.0-beta3'; + exports.__info__.date = '2019-12-16T08:57:32.773Z'; + exports.__info__.hash = 'd9bf428'; exports.__info__.url = 'https://github.com/odoo/owl'; }(this.owl = this.owl || {})); diff --git a/playground/samples.js b/playground/samples.js index d02526d6..d01834ad 100644 --- a/playground/samples.js +++ b/playground/samples.js @@ -1101,7 +1101,7 @@ const RESPONSIVE_XML = ` - + @@ -1438,7 +1438,7 @@ const FORM_XML = `
Text:
Other Text:
Number:
-
Boolean: TrueFalse
+
Boolean: TrueFalse
Color: