diff --git a/owl.js b/owl.js
index dfbc49f5..cc4c64fc 100644
--- a/owl.js
+++ b/owl.js
@@ -88,7 +88,6 @@
constructor() {
this.rev = 1;
this.allowMutations = true;
- this.dirty = false;
this.weakMap = new WeakMap();
}
notifyCB() { }
@@ -153,432 +152,6 @@
}
}
- /**
- * Owl QWeb Expression Parser
- *
- * Owl needs in various contexts to be able to understand the structure of a
- * string representing a javascript expression. The usual goal is to be able
- * to rewrite some variables. For example, if a template has
- *
- * ```xml
- * ...
- * ```
- *
- * this needs to be translated in something like this:
- *
- * ```js
- * if (context["computeSomething"]({val: context["state"].val})) { ... }
- * ```
- *
- * This file contains the implementation of an extremely naive tokenizer/parser
- * and evaluator for javascript expressions. The supported grammar is basically
- * only expressive enough to understand the shape of objects, of arrays, and
- * various operators.
- */
- //------------------------------------------------------------------------------
- // Misc types, constants and helpers
- //------------------------------------------------------------------------------
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
- const WORD_REPLACEMENT = {
- and: "&&",
- or: "||",
- gt: ">",
- gte: ">=",
- lt: "<",
- lte: "<="
- };
- const STATIC_TOKEN_MAP = {
- "{": "LEFT_BRACE",
- "}": "RIGHT_BRACE",
- "[": "LEFT_BRACKET",
- "]": "RIGHT_BRACKET",
- ":": "COLON",
- ",": "COMMA",
- "(": "LEFT_PAREN",
- ")": "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 ".split(",");
- let tokenizeString = function (expr) {
- let s = expr[0];
- let start = s;
- if (s !== "'" && s !== '"') {
- return false;
- }
- let i = 1;
- let cur;
- while (expr[i] && expr[i] !== start) {
- cur = expr[i];
- s += cur;
- if (cur === "\\") {
- i++;
- cur = expr[i];
- if (!cur) {
- throw new Error("Invalid expression");
- }
- s += cur;
- }
- i++;
- }
- if (expr[i] !== start) {
- throw new Error("Invalid expression");
- }
- s += start;
- return { type: "VALUE", value: s };
- };
- let tokenizeNumber = function (expr) {
- let s = expr[0];
- if (s && s.match(/[0-9]/)) {
- let i = 1;
- while (expr[i] && expr[i].match(/[0-9]|\./)) {
- s += expr[i];
- i++;
- }
- return { type: "VALUE", value: s };
- }
- else {
- return false;
- }
- };
- let tokenizeSymbol = function (expr) {
- let s = expr[0];
- if (s && s.match(/[a-zA-Z_\$]/)) {
- let i = 1;
- while (expr[i] && expr[i].match(/\w/)) {
- s += expr[i];
- i++;
- }
- if (s in WORD_REPLACEMENT) {
- return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
- }
- return { type: "SYMBOL", value: s };
- }
- else {
- return false;
- }
- };
- const tokenizeStatic = function (expr) {
- const char = expr[0];
- if (char && char in STATIC_TOKEN_MAP) {
- return { type: STATIC_TOKEN_MAP[char], value: char };
- }
- return false;
- };
- const tokenizeOperator = function (expr) {
- for (let op of OPERATORS) {
- if (expr.startsWith(op)) {
- return { type: "OPERATOR", value: op };
- }
- }
- return false;
- };
- const TOKENIZERS = [
- tokenizeString,
- tokenizeNumber,
- tokenizeOperator,
- tokenizeSymbol,
- tokenizeStatic
- ];
- /**
- * Convert a javascript expression (as a string) into a list of tokens. For
- * example: `tokenize("1 + b")` will return:
- * ```js
- * [
- * {type: "VALUE", value: "1"},
- * {type: "OPERATOR", value: "+"},
- * {type: "SYMBOL", value: "b"}
- * ]
- * ```
- */
- function tokenize(expr) {
- const result = [];
- let token = true;
- while (token) {
- expr = expr.trim();
- if (expr) {
- for (let tokenizer of TOKENIZERS) {
- token = tokenizer(expr);
- if (token) {
- result.push(token);
- expr = expr.slice(token.size || token.value.length);
- break;
- }
- }
- }
- else {
- token = false;
- }
- }
- if (expr.length) {
- throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
- }
- return result;
- }
- //------------------------------------------------------------------------------
- // Expression "evaluator"
- //------------------------------------------------------------------------------
- /**
- * This is the main function exported by this file. This is the code that will
- * process an expression (given as a string) and returns another expression with
- * proper lookups in the context.
- *
- * Usually, this kind of code would be very simple to do if we had an AST (so,
- * if we had a javascript parser), since then, we would only need to find the
- * variables and replace them. However, a parser is more complicated, and there
- * are no standard builtin parser API.
- *
- * Since this method is applied to simple javasript expressions, and the work to
- * be done is actually quite simple, we actually can get away with not using a
- * parser, which helps with the code size.
- *
- * Here is the heuristic used by this method to determine if a token is a
- * variable:
- * - by default, all symbols are considered a variable
- * - unless the previous token is a dot (in that case, this is a property: `a.b`)
- * - or if the previous token is a left brace or a comma, and the next token is
- * a colon (in that case, this is an object key: `{a: b}`)
- */
- function compileExpr(expr, vars) {
- const tokens = tokenize(expr);
- let result = "";
- for (let i = 0; i < tokens.length; i++) {
- let token = tokens[i];
- if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
- // we need to find if it is a variable
- let isVar = true;
- let prevToken = tokens[i - 1];
- if (prevToken) {
- if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
- isVar = false;
- }
- else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
- let nextToken = tokens[i + 1];
- if (nextToken && nextToken.type === "COLON") {
- isVar = false;
- }
- }
- }
- if (isVar) {
- if (token.value in vars && "id" in vars[token.value]) {
- token.value = vars[token.value].id;
- }
- else {
- token.value = `context['${token.value}']`;
- }
- }
- }
- result += token.value;
- }
- return result;
- }
-
- const INTERP_REGEXP = /\{\{.*?\}\}/g;
- //------------------------------------------------------------------------------
- // Compilation Context
- //------------------------------------------------------------------------------
- class CompilationContext {
- constructor(name) {
- this.code = [];
- this.variables = {};
- this.escaping = false;
- this.parentNode = null;
- this.parentTextNode = null;
- this.rootNode = null;
- this.indentLevel = 0;
- this.shouldDefineOwner = false;
- this.shouldDefineParent = false;
- this.shouldDefineQWeb = false;
- this.shouldDefineUtils = false;
- this.shouldDefineRefs = false;
- this.shouldDefineResult = true;
- this.shouldProtectContext = false;
- this.shouldTrackScope = false;
- this.loopNumber = 0;
- this.inPreTag = false;
- this.allowMultipleRoots = false;
- this.hasParentWidget = false;
- this.scopeVars = [];
- this.currentKey = "";
- this.templates = {};
- this.callingLevel = 0;
- this.inliningLevel = 0;
- this.rootContext = this;
- this.templateName = name || "noname";
- this.templates[this.templateName] = true;
- this.addLine("var h = this.h;");
- }
- generateID() {
- return CompilationContext.nextID++;
- }
- /**
- * This method generates a "template key", which is basically a unique key
- * which depends on the currently set keys, and on the iteration numbers (if
- * we are in a loop).
- *
- * Such a key is necessary when we need to associate an id to some element
- * generated by a template (for example, a component)
- */
- generateTemplateKey() {
- const id = this.generateID();
- let locationExpr = `\`__${this.generateID()}__`;
- for (let i = 0; i < this.loopNumber - 1; i++) {
- locationExpr += `\${i${i + 1}}__`;
- }
- if (this.currentKey) {
- const k = this.currentKey;
- this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
- }
- else {
- locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
- this.addLine(`let k${id} = ${locationExpr};`);
- }
- return `k${id}`;
- }
- generateCode() {
- const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
- if (shouldTrackScope) {
- // add some vars to scope if needed
- for (let scopeVar of this.scopeVars.reverse()) {
- let { index, key, indent } = scopeVar;
- const prefix = new Array(indent + 2).join(" ");
- this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
- }
- this.code.unshift(" const scope = Object.create(null);");
- }
- if (this.shouldProtectContext) {
- this.code.unshift(" context = Object.create(context);");
- }
- if (this.shouldDefineResult) {
- this.code.unshift(" let result;");
- }
- if (this.shouldDefineRefs) {
- this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
- }
- if (this.shouldDefineOwner) {
- // this is necessary to prevent some directives (t-forach for ex) to
- // pollute the rendering context by adding some keys in it.
- this.code.unshift(" let owner = context;");
- }
- if (this.shouldDefineParent) {
- if (this.hasParentWidget) {
- this.code.unshift(" let parent = extra.parent;");
- }
- else {
- this.code.unshift(" let parent = context;");
- }
- }
- if (this.shouldDefineQWeb) {
- this.code.unshift(" let QWeb = this.constructor;");
- }
- if (this.shouldDefineUtils) {
- this.code.unshift(" let utils = this.constructor.utils;");
- }
- return this.code;
- }
- withParent(node) {
- 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) {
- this.rootContext.rootNode = node;
- }
- if (!this.parentNode && this.rootContext.shouldDefineResult) {
- this.addLine(`result = vn${node};`);
- }
- return this.subContext("parentNode", node);
- }
- subContext(key, value) {
- const newContext = Object.create(this);
- newContext[key] = value;
- if (key === "caller") {
- newContext.callingLevel++;
- newContext.inliningLevel++;
- }
- return newContext;
- }
- indent() {
- this.indentLevel++;
- }
- dedent() {
- this.indentLevel--;
- }
- addLine(line) {
- const prefix = new Array(this.indentLevel + 2).join(" ");
- this.code.push(prefix + line);
- return this.code.length - 1;
- }
- addToScope(key, expr) {
- const index = this.addLine(`context.${key} = ${expr};`);
- this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
- }
- addIf(condition) {
- this.addLine(`if (${condition}) {`);
- this.indent();
- }
- addElse() {
- this.dedent();
- this.addLine("} else {");
- this.indent();
- }
- closeIf() {
- this.dedent();
- this.addLine("}");
- }
- /**
- * Recursively (inverse) fetches the `caller` of a context
- * Useful to determine to which t-call a t-raw="0" refers
- */
- getCaller(targetLevel) {
- if (targetLevel === undefined) {
- targetLevel = this.inliningLevel;
- }
- if (targetLevel === this.callingLevel) {
- return this.caller || null;
- }
- const proto = this.__proto__;
- return proto ? proto.getCaller(targetLevel) : null;
- }
- /**
- * Marks the context with the current recursive level
- * in which we are for inlining archs (t-raw="0")
- */
- getInliningContext() {
- return this.subContext("inliningLevel", this.inliningLevel - 1);
- }
- getValue(val) {
- return val in this.variables ? this.getValue(this.variables[val]) : val;
- }
- /**
- * Prepare an expression for being consumed at render time. Its main job
- * is to
- * - replace unknown variables by a lookup in the context
- * - replace already defined variables by their internal name
- */
- formatExpression(expr) {
- return compileExpr(expr, this.variables);
- }
- /**
- * Perform string interpolation on the given string. Note that if the whole
- * string is an expression, it simply returns it (formatted and enclosed in
- * parentheses).
- * For instance:
- * 'Hello {{x}}!' -> `Hello ${x}`
- * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
- */
- interpolate(s) {
- let matches = s.match(INTERP_REGEXP);
- if (matches && matches[0].length === s.length) {
- return `(${this.formatExpression(s.slice(2, -2))})`;
- }
- let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
- return "`" + r + "`";
- }
- }
- CompilationContext.nextID = 1;
-
//------------------------------------------------------------------------------
// module/props.ts
//------------------------------------------------------------------------------
@@ -771,7 +344,7 @@
klass = klass || {};
elm = vnode.elm;
for (name in oldClass) {
- if (!klass[name]) {
+ if (name && !klass[name]) {
elm.classList.remove(name);
}
}
@@ -1218,6 +791,410 @@
const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
+ /**
+ * Owl QWeb Expression Parser
+ *
+ * Owl needs in various contexts to be able to understand the structure of a
+ * string representing a javascript expression. The usual goal is to be able
+ * to rewrite some variables. For example, if a template has
+ *
+ * ```xml
+ * ...
+ * ```
+ *
+ * this needs to be translated in something like this:
+ *
+ * ```js
+ * if (context["computeSomething"]({val: context["state"].val})) { ... }
+ * ```
+ *
+ * This file contains the implementation of an extremely naive tokenizer/parser
+ * and evaluator for javascript expressions. The supported grammar is basically
+ * only expressive enough to understand the shape of objects, of arrays, and
+ * various operators.
+ */
+ //------------------------------------------------------------------------------
+ // Misc types, constants and helpers
+ //------------------------------------------------------------------------------
+ const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
+ const WORD_REPLACEMENT = {
+ and: "&&",
+ or: "||",
+ gt: ">",
+ gte: ">=",
+ lt: "<",
+ lte: "<="
+ };
+ const STATIC_TOKEN_MAP = {
+ "{": "LEFT_BRACE",
+ "}": "RIGHT_BRACE",
+ "[": "LEFT_BRACKET",
+ "]": "RIGHT_BRACKET",
+ ":": "COLON",
+ ",": "COMMA",
+ "(": "LEFT_PAREN",
+ ")": "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 ,=>".split(",");
+ let tokenizeString = function (expr) {
+ let s = expr[0];
+ let start = s;
+ if (s !== "'" && s !== '"') {
+ return false;
+ }
+ let i = 1;
+ let cur;
+ while (expr[i] && expr[i] !== start) {
+ cur = expr[i];
+ s += cur;
+ if (cur === "\\") {
+ i++;
+ cur = expr[i];
+ if (!cur) {
+ throw new Error("Invalid expression");
+ }
+ s += cur;
+ }
+ i++;
+ }
+ if (expr[i] !== start) {
+ throw new Error("Invalid expression");
+ }
+ s += start;
+ return { type: "VALUE", value: s };
+ };
+ let tokenizeNumber = function (expr) {
+ let s = expr[0];
+ if (s && s.match(/[0-9]/)) {
+ let i = 1;
+ while (expr[i] && expr[i].match(/[0-9]|\./)) {
+ s += expr[i];
+ i++;
+ }
+ return { type: "VALUE", value: s };
+ }
+ else {
+ return false;
+ }
+ };
+ let tokenizeSymbol = function (expr) {
+ let s = expr[0];
+ if (s && s.match(/[a-zA-Z_\$]/)) {
+ let i = 1;
+ while (expr[i] && expr[i].match(/\w/)) {
+ s += expr[i];
+ i++;
+ }
+ if (s in WORD_REPLACEMENT) {
+ return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
+ }
+ return { type: "SYMBOL", value: s };
+ }
+ else {
+ return false;
+ }
+ };
+ const tokenizeStatic = function (expr) {
+ const char = expr[0];
+ if (char && char in STATIC_TOKEN_MAP) {
+ return { type: STATIC_TOKEN_MAP[char], value: char };
+ }
+ return false;
+ };
+ const tokenizeOperator = function (expr) {
+ for (let op of OPERATORS) {
+ if (expr.startsWith(op)) {
+ return { type: "OPERATOR", value: op };
+ }
+ }
+ return false;
+ };
+ const TOKENIZERS = [
+ tokenizeString,
+ tokenizeNumber,
+ tokenizeOperator,
+ tokenizeSymbol,
+ tokenizeStatic
+ ];
+ /**
+ * Convert a javascript expression (as a string) into a list of tokens. For
+ * example: `tokenize("1 + b")` will return:
+ * ```js
+ * [
+ * {type: "VALUE", value: "1"},
+ * {type: "OPERATOR", value: "+"},
+ * {type: "SYMBOL", value: "b"}
+ * ]
+ * ```
+ */
+ function tokenize(expr) {
+ const result = [];
+ let token = true;
+ while (token) {
+ expr = expr.trim();
+ if (expr) {
+ for (let tokenizer of TOKENIZERS) {
+ token = tokenizer(expr);
+ if (token) {
+ result.push(token);
+ expr = expr.slice(token.size || token.value.length);
+ break;
+ }
+ }
+ }
+ else {
+ token = false;
+ }
+ }
+ if (expr.length) {
+ throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
+ }
+ return result;
+ }
+ //------------------------------------------------------------------------------
+ // Expression "evaluator"
+ //------------------------------------------------------------------------------
+ /**
+ * This is the main function exported by this file. This is the code that will
+ * process an expression (given as a string) and returns another expression with
+ * proper lookups in the context.
+ *
+ * Usually, this kind of code would be very simple to do if we had an AST (so,
+ * if we had a javascript parser), since then, we would only need to find the
+ * variables and replace them. However, a parser is more complicated, and there
+ * are no standard builtin parser API.
+ *
+ * Since this method is applied to simple javasript expressions, and the work to
+ * be done is actually quite simple, we actually can get away with not using a
+ * parser, which helps with the code size.
+ *
+ * Here is the heuristic used by this method to determine if a token is a
+ * variable:
+ * - by default, all symbols are considered a variable
+ * - unless the previous token is a dot (in that case, this is a property: `a.b`)
+ * - or if the previous token is a left brace or a comma, and the next token is
+ * a colon (in that case, this is an object key: `{a: b}`)
+ *
+ * Some specific code is also required to support arrow functions. If we detect
+ * the arrow operator, then we add the current (or some previous tokens) token to
+ * the list of variables so it does not get replaced by a lookup in the context
+ */
+ function compileExpr(expr, scope) {
+ scope = Object.create(scope);
+ const tokens = tokenize(expr);
+ for (let i = 0; i < tokens.length; i++) {
+ let token = tokens[i];
+ let prevToken = tokens[i - 1];
+ let nextToken = tokens[i + 1];
+ let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
+ if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
+ if (prevToken) {
+ if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
+ isVar = false;
+ }
+ else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
+ if (nextToken && nextToken.type === "COLON") {
+ isVar = false;
+ }
+ }
+ }
+ }
+ if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
+ if (token.type === "RIGHT_PAREN") {
+ let j = i - 1;
+ while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
+ if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
+ tokens[j].value = tokens[j].originalValue;
+ scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value };
+ }
+ j--;
+ }
+ }
+ else {
+ scope[token.value] = { id: token.value, expr: token.value };
+ }
+ }
+ if (isVar) {
+ if (token.value in scope && "id" in scope[token.value]) {
+ token.value = scope[token.value].expr;
+ }
+ else {
+ token.originalValue = token.value;
+ token.value = `scope['${token.value}']`;
+ }
+ }
+ }
+ return tokens.map(t => t.value).join("");
+ }
+
+ const INTERP_REGEXP = /\{\{.*?\}\}/g;
+ //------------------------------------------------------------------------------
+ // Compilation Context
+ //------------------------------------------------------------------------------
+ class CompilationContext {
+ constructor(name) {
+ this.code = [];
+ this.variables = {};
+ this.escaping = false;
+ this.parentNode = null;
+ this.parentTextNode = null;
+ this.rootNode = null;
+ this.indentLevel = 0;
+ this.shouldDefineParent = false;
+ this.shouldDefineScope = false;
+ this.shouldDefineQWeb = false;
+ this.shouldDefineUtils = false;
+ this.shouldDefineRefs = false;
+ this.shouldDefineResult = true;
+ this.loopNumber = 0;
+ this.inPreTag = false;
+ this.allowMultipleRoots = false;
+ this.hasParentWidget = false;
+ this.currentKey = "";
+ this.rootContext = this;
+ this.templateName = name || "noname";
+ this.addLine("var h = this.h;");
+ }
+ generateID() {
+ return CompilationContext.nextID++;
+ }
+ /**
+ * This method generates a "template key", which is basically a unique key
+ * which depends on the currently set keys, and on the iteration numbers (if
+ * we are in a loop).
+ *
+ * Such a key is necessary when we need to associate an id to some element
+ * generated by a template (for example, a component)
+ */
+ generateTemplateKey() {
+ const id = this.generateID();
+ let locationExpr = `\`__${this.generateID()}__`;
+ for (let i = 0; i < this.loopNumber - 1; i++) {
+ locationExpr += `\${i${i + 1}}__`;
+ }
+ if (this.currentKey) {
+ const k = this.currentKey;
+ this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
+ }
+ else {
+ locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
+ this.addLine(`let k${id} = ${locationExpr};`);
+ }
+ return `k${id}`;
+ }
+ generateCode() {
+ if (this.shouldDefineResult) {
+ this.code.unshift(" let result;");
+ }
+ if (this.shouldDefineScope) {
+ this.code.unshift(" let scope = Object.create(context);");
+ }
+ if (this.shouldDefineRefs) {
+ this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
+ }
+ if (this.shouldDefineParent) {
+ if (this.hasParentWidget) {
+ this.code.unshift(" let parent = extra.parent;");
+ }
+ else {
+ this.code.unshift(" let parent = context;");
+ }
+ }
+ if (this.shouldDefineQWeb) {
+ this.code.unshift(" let QWeb = this.constructor;");
+ }
+ if (this.shouldDefineUtils) {
+ this.code.unshift(" let utils = this.constructor.utils;");
+ }
+ return this.code;
+ }
+ withParent(node) {
+ 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) {
+ this.rootContext.rootNode = node;
+ }
+ if (!this.parentNode && this.rootContext.shouldDefineResult) {
+ this.addLine(`result = vn${node};`);
+ }
+ return this.subContext("parentNode", node);
+ }
+ subContext(key, value) {
+ const newContext = Object.create(this);
+ newContext[key] = value;
+ return newContext;
+ }
+ indent() {
+ this.indentLevel++;
+ }
+ dedent() {
+ this.indentLevel--;
+ }
+ addLine(line) {
+ const prefix = new Array(this.indentLevel + 2).join(" ");
+ this.code.push(prefix + line);
+ return this.code.length - 1;
+ }
+ addIf(condition) {
+ this.addLine(`if (${condition}) {`);
+ this.indent();
+ }
+ addElse() {
+ this.dedent();
+ this.addLine("} else {");
+ this.indent();
+ }
+ closeIf() {
+ this.dedent();
+ this.addLine("}");
+ }
+ getValue(val) {
+ return val in this.variables ? this.getValue(this.variables[val]) : val;
+ }
+ /**
+ * Prepare an expression for being consumed at render time. Its main job
+ * is to
+ * - replace unknown variables by a lookup in the context
+ * - replace already defined variables by their internal name
+ */
+ formatExpression(expr) {
+ this.rootContext.shouldDefineScope = true;
+ return compileExpr(expr, this.variables);
+ }
+ /**
+ * Perform string interpolation on the given string. Note that if the whole
+ * string is an expression, it simply returns it (formatted and enclosed in
+ * parentheses).
+ * For instance:
+ * 'Hello {{x}}!' -> `Hello ${x}`
+ * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
+ */
+ interpolate(s) {
+ let matches = s.match(INTERP_REGEXP);
+ if (matches && matches[0].length === s.length) {
+ return `(${this.formatExpression(s.slice(2, -2))})`;
+ }
+ let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
+ return "`" + r + "`";
+ }
+ startProtectScope() {
+ const protectID = this.generateID();
+ this.rootContext.shouldDefineScope = true;
+ this.addLine(`const _origScope${protectID} = scope;`);
+ this.addLine(`scope = Object.assign(Object.create(context), scope);`);
+ return protectID;
+ }
+ stopProtectScope(protectID) {
+ this.addLine(`scope = _origScope${protectID};`);
+ }
+ }
+ CompilationContext.nextID = 1;
+
/**
* Owl Utils
*
@@ -1341,6 +1318,7 @@
destroy: "()"
};
const UTILS = {
+ zero: Symbol("zero"),
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
@@ -1359,6 +1337,22 @@
shallowEqual,
addNameSpace(vnode) {
addNS(vnode.data, vnode.children, vnode.sel);
+ },
+ VDomArray: class VDomArray extends Array {
+ },
+ vDomToString: function (vdom) {
+ return vdom
+ .map(vnode => {
+ if (vnode.sel) {
+ const node = document.createElement(vnode.sel);
+ const result = patch(node, vnode);
+ return result.elm.outerHTML;
+ }
+ else {
+ return vnode.text;
+ }
+ })
+ .join();
}
};
function parseXML(xml) {
@@ -1399,7 +1393,7 @@
// recursiveTemplates contains sub templates called with t-call, but which
// ends up in recursive situations. This is very similar to the slot situation,
// as in we need to propagate the scope.
- this.recursiveFns = {};
+ this.subTemplates = {};
this.isUpdating = false;
this.templates = Object.create(QWeb.TEMPLATES);
if (config.templates) {
@@ -1568,22 +1562,12 @@
ctx.shouldDefineResult = false;
}
if (parentContext) {
- ctx.templates = Object.create(parentContext.templates);
ctx.variables = Object.create(parentContext.variables);
ctx.parentNode = parentContext.parentNode || ctx.generateID();
ctx.allowMultipleRoots = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
- for (let v in parentContext.variables) {
- let variable = parentContext.variables[v];
- if (variable.id) {
- ctx.addLine(`let ${variable.id} = extra.fiber.vars.${variable.id}`);
- }
- }
- }
- if (parentContext) {
- ctx.addLine(" Object.assign(context, extra.fiber.scope);");
}
this._compileNode(elem, ctx);
if (!parentContext) {
@@ -1598,12 +1582,13 @@
}
}
let code = ctx.generateCode();
+ const templateName = ctx.templateName.replace(/`/g, "'").slice(0, 200);
+ code.unshift(` // Template name: "${templateName}"`);
let template;
try {
- template = new Function("context", "extra", code.join("\n"));
+ template = new Function("context, extra", code.join("\n"));
}
catch (e) {
- const templateName = ctx.templateName.replace(/`/g, "'");
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
console.warn(code.join("\n"));
console.groupEnd();
@@ -1827,13 +1812,14 @@
if (!name.startsWith("t-") && !node.getAttribute("t-attf-" + name)) {
const attID = ctx.generateID();
if (name === "class") {
- let classDef = value
- .trim()
- .split(/\s+/)
- .map(a => `'${a}':true`)
- .join(",");
- classObj = `_${ctx.generateID()}`;
- ctx.addLine(`let ${classObj} = {${classDef}};`);
+ if ((value = value.trim())) {
+ let classDef = value
+ .split(/\s+/)
+ .map(a => `'${a}':true`)
+ .join(",");
+ classObj = `_${ctx.generateID()}`;
+ ctx.addLine(`let ${classObj} = {${classDef}};`);
+ }
}
else {
ctx.addLine(`var _${attID} = '${value}';`);
@@ -1849,7 +1835,7 @@
if (name.startsWith("t-att-")) {
let attName = name.slice(6);
const v = ctx.getValue(value);
- let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : v.id;
+ let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : `scope.${v.id}`;
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toObj(${formattedValue})`;
@@ -2005,23 +1991,18 @@
//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
- QWeb.utils.getFragment = function (str) {
- const temp = document.createElement("template");
- temp.innerHTML = str;
- return temp.content;
- };
QWeb.utils.htmlToVDOM = htmlToVDOM;
function compileValueNode(value, node, qweb, ctx) {
+ ctx.rootContext.shouldDefineScope = true;
if (value === "0") {
- const caller = ctx.getCaller();
- if (caller) {
- qweb._compileNode(caller, ctx.getInliningContext());
- return;
- }
- }
- if (value.xml instanceof NodeList && !value.id) {
- for (let node of Array.from(value.xml)) {
- qweb._compileNode(node, ctx);
+ if (ctx.parentNode) {
+ // the 'zero' magical symbol is where we can find the result of the rendering
+ // of the body of the t-call.
+ ctx.rootContext.shouldDefineUtils = true;
+ const zeroArgs = ctx.escaping
+ ? `{text: utils.vDomToString(scope[utils.zero])}`
+ : `...scope[utils.zero]`;
+ ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`);
}
return;
}
@@ -2031,10 +2012,15 @@
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
}
else {
- exprID = value.id;
+ exprID = `scope.${value.id}`;
}
ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) {
+ let protectID;
+ if (value.hasBody) {
+ protectID = ctx.startProtectScope();
+ ctx.addLine(`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`);
+ }
if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
}
@@ -2050,21 +2036,24 @@
ctx.addLine(`result = vn${nodeID}`);
}
}
+ if (value.hasBody) {
+ ctx.stopProtectScope(protectID);
+ }
}
else {
ctx.rootContext.shouldDefineUtils = true;
- ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
+ if (value.hasBody) {
+ ctx.addLine(`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`);
+ ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
+ }
+ else {
+ ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
+ }
}
if (node.childNodes.length) {
ctx.addElse();
qweb._compileChildren(node, ctx);
}
- if (value.xml instanceof NodeList && value.id) {
- ctx.addElse();
- for (let node of Array.from(value.xml)) {
- qweb._compileNode(node, ctx);
- }
- }
ctx.closeIf();
}
QWeb.addDirective({
@@ -2092,25 +2081,41 @@
name: "set",
extraNames: ["value"],
priority: 60,
- atNodeEncounter({ node, ctx }) {
+ atNodeEncounter({ node, qweb, ctx }) {
+ ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set");
let value = node.getAttribute("t-value");
ctx.variables[variable] = ctx.variables[variable] || {};
let qwebvar = ctx.variables[variable];
+ const hasBody = node.hasChildNodes();
+ qwebvar.id = variable;
+ qwebvar.expr = `scope.${variable}`;
if (value) {
const formattedValue = ctx.formatExpression(value);
- if (ctx.variables.hasOwnProperty(variable) && qwebvar.id) {
- ctx.addLine(`${qwebvar.id} = ${formattedValue}`);
- }
- else {
- const varName = `_${ctx.generateID()}`;
- ctx.addLine(`var ${varName} = ${formattedValue};`);
- qwebvar.id = varName;
- qwebvar.expr = formattedValue;
- }
+ ctx.addLine(`${qwebvar.expr} = ${formattedValue};`);
+ qwebvar.value = formattedValue;
}
- else {
- qwebvar.xml = node.childNodes;
+ if (hasBody) {
+ ctx.rootContext.shouldDefineUtils = true;
+ if (value) {
+ ctx.addIf(`!(${qwebvar.expr})`);
+ }
+ const tempParentNodeID = ctx.generateID();
+ const _parentNode = ctx.parentNode;
+ ctx.parentNode = tempParentNodeID;
+ ctx.addLine(`const 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);
+ }
+ qweb._compileNode(nodeCopy, ctx);
+ ctx.addLine(`${qwebvar.expr} = c${tempParentNodeID}`);
+ qwebvar.value = `c${tempParentNodeID}`;
+ qwebvar.hasBody = true;
+ ctx.parentNode = _parentNode;
+ if (value) {
+ ctx.closeIf();
+ }
}
return true;
}
@@ -2123,7 +2128,7 @@
priority: 20,
atNodeEncounter({ node, ctx }) {
let cond = ctx.getValue(node.getAttribute("t-if"));
- ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : cond.id);
+ ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`);
return false;
},
finalize({ ctx }) {
@@ -2135,7 +2140,7 @@
priority: 30,
atNodeEncounter({ node, ctx }) {
let cond = ctx.getValue(node.getAttribute("t-elif"));
- ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : cond.id}) {`);
+ ctx.addLine(`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`}) {`);
ctx.indent();
return false;
},
@@ -2162,6 +2167,9 @@
name: "call",
priority: 50,
atNodeEncounter({ node, qweb, ctx }) {
+ // Step 1: sanity checks
+ // ------------------------------------------------
+ ctx.rootContext.shouldDefineScope = true;
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
@@ -2170,70 +2178,56 @@
if (!nodeTemplate) {
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
}
- const nodeCopy = node.cloneNode(true);
- nodeCopy.removeAttribute("t-call");
- // extract variables from nodecopy
- const tempCtx = new CompilationContext();
- tempCtx.allowMultipleRoots = true;
- qweb._compileNode(nodeCopy, tempCtx);
- const vars = Object.assign({}, ctx.variables, tempCtx.variables);
- const templateMap = Object.create(ctx.templates);
- // open new scope, if necessary
- const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
- // compile sub template
- let subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));
- subCtx = subCtx.subContext("templates", templateMap);
- if (templateMap[subTemplate]) {
- // OUCH, IT IS A RECURSIVE TEMPLATE SITUATION...
- // This is a tricky situation... We obviously cannot inline the compiled
- // template. So, what we need to do is to compile it, and make sure we
- // properly transfer everything from the current scope to the sub template.
- ctx.rootContext.shouldTrackScope = true;
- ctx.rootContext.shouldDefineOwner = true;
- let subTemplateName;
- if (ctx.hasParentWidget) {
- subTemplateName = ctx.templateName;
- }
- else {
- subTemplateName = `__${ctx.generateID()}`;
- subCtx.variables = {};
- let id = 0;
- for (let v in vars) {
- subCtx.variables[v] = vars[v];
- vars[v].id = `_v${id++}`;
- }
- const subTemplateFn = qweb._compile(subTemplateName, nodeTemplate.elem, subCtx);
- qweb.recursiveFns[subTemplateName] = subTemplateFn;
- }
- let varCode = `{}`;
- if (Object.keys(vars).length) {
- let id = 0;
- const content = Object.values(vars)
- .map((v) => `_v${id++}: ${v.expr}`)
- .join(",");
- varCode = `{${content}}`;
- }
- ctx.addLine(`this.recursiveFns['${subTemplateName}'].call(this, context, Object.assign({}, extra, {parentNode: c${ctx.parentNode}, fiber: {vars: ${varCode}, scope}}));`);
- return true;
+ // Step 2: compile target template in sub templates
+ // ------------------------------------------------
+ if (!qweb.subTemplates[subTemplate]) {
+ qweb.subTemplates[subTemplate] = true;
+ const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx);
+ qweb.subTemplates[subTemplate] = subTemplateFn;
}
- templateMap[subTemplate] = true;
- if (hasNewVariables) {
- ctx.addLine("{");
+ // Step 3: compile t-call body if necessary
+ // ------------------------------------------------
+ let hasBody = node.hasChildNodes();
+ let protectID;
+ if (hasBody) {
+ // we add a sub scope to protect the ambient scope
+ ctx.addLine(`{`);
ctx.indent();
- // add new variables, if any
- for (let key in tempCtx.variables) {
- const v = tempCtx.variables[key];
- if (v.expr) {
- ctx.addLine(`let ${v.id} = ${v.expr};`);
- }
- // todo: handle XML variables...
- }
- }
- qweb._compileNode(nodeTemplate.elem, subCtx);
- // close new scope
- if (hasNewVariables) {
+ protectID = ctx.startProtectScope();
+ const nodeCopy = node.cloneNode(true);
+ nodeCopy.removeAttribute("t-call");
+ const parentNode = ctx.parentNode;
+ ctx.parentNode = "__0";
+ // this local scope is intended to trap c__0
+ ctx.addLine(`{`);
+ ctx.indent();
+ ctx.addLine("let c__0 = [];");
+ qweb._compileNode(nodeCopy, ctx);
+ ctx.rootContext.shouldDefineUtils = true;
+ ctx.addLine("scope[utils.zero] = c__0;");
+ ctx.parentNode = parentNode;
ctx.dedent();
- ctx.addLine("}");
+ ctx.addLine(`}`);
+ }
+ // Step 4: add the appropriate function call to current component
+ // ------------------------------------------------
+ const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
+ if (ctx.parentNode) {
+ ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: c${ctx.parentNode}}));`);
+ }
+ 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(`result = result[0]`);
+ }
+ // Step 5: restore previous scope
+ // ------------------------------------------------
+ if (hasBody) {
+ ctx.stopProtectScope(protectID);
+ ctx.dedent();
+ ctx.addLine(`}`);
}
return true;
}
@@ -2246,7 +2240,7 @@
extraNames: ["as"],
priority: 10,
atNodeEncounter({ node, qweb, ctx }) {
- ctx.rootContext.shouldProtectContext = true;
+ ctx.rootContext.shouldDefineScope = true;
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
const elems = node.getAttribute("t-foreach");
const name = node.getAttribute("t-as");
@@ -2261,14 +2255,15 @@
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
+ let varsID = ctx.startProtectScope();
const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
ctx.indent();
- ctx.addToScope(name + "_first", `${loopVar} === 0`);
- ctx.addToScope(name + "_last", `${loopVar} === _length${keysID} - 1`);
- ctx.addToScope(name + "_index", loopVar);
- ctx.addToScope(name, `_${keysID}[${loopVar}]`);
- ctx.addToScope(name + "_value", `_${valuesID}[${loopVar}]`);
+ ctx.addLine(`scope.${name}_first = ${loopVar} === 0`);
+ ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`);
+ ctx.addLine(`scope.${name}_index = ${loopVar}`);
+ ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`);
+ ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
const nodeCopy = node.cloneNode(true);
let shouldWarn = !nodeCopy.hasAttribute("t-key") &&
node.children.length === 1 &&
@@ -2281,6 +2276,7 @@
qweb._compileNode(nodeCopy, ctx);
ctx.dedent();
ctx.addLine("}");
+ ctx.stopProtectScope(varsID);
return true;
}
});
@@ -2333,7 +2329,6 @@
name: "on",
priority: 90,
atNodeCreation({ ctx, fullName, value, nodeID }) {
- ctx.rootContext.shouldDefineOwner = true;
const [eventName, ...mods] = fullName.slice(5).split(".");
if (!eventName) {
throw new Error("Missing event name with t-on directive");
@@ -2343,8 +2338,8 @@
extraArgs = args.slice(1, -1);
return "";
});
- let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner";
- let handler = `function (e) {`;
+ 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];
@@ -2482,9 +2477,8 @@
QWeb.addDirective({
name: "slot",
priority: 80,
- atNodeEncounter({ ctx, value }) {
+ atNodeEncounter({ ctx, value, node, qweb }) {
const slotKey = ctx.generateID();
- ctx.rootContext.shouldDefineOwner = true;
ctx.addLine(`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + '${value}'];`);
ctx.addIf(`slot${slotKey}`);
let parentNode = `c${ctx.parentNode}`;
@@ -2495,10 +2489,16 @@
ctx.addLine(`let ${parentNode}= []`);
ctx.addLine(`result = {}`);
}
- ctx.addLine(`slot${slotKey}.call(this, context.__owl__.parent, Object.assign({}, extra, {parentNode: ${parentNode}, vars: extra.vars, parent: extra.parent || owner}));`);
+ ctx.addLine(`slot${slotKey}.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));`);
if (!ctx.parentNode) {
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
}
+ if (node.hasChildNodes()) {
+ ctx.addElse();
+ const nodeCopy = node.cloneNode(true);
+ nodeCopy.removeAttribute("t-slot");
+ qweb._compileNode(nodeCopy, ctx);
+ }
ctx.closeIf();
return true;
}
@@ -2782,10 +2782,10 @@
priority: 100,
atNodeEncounter({ ctx, value, node, qweb }) {
ctx.addLine("//COMPONENT");
- ctx.rootContext.shouldDefineOwner = true;
ctx.rootContext.shouldDefineQWeb = true;
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
+ ctx.rootContext.shouldDefineScope = true;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
// t-on- events and t-transition
const events = [];
@@ -2876,7 +2876,7 @@
}
let eventsCode = events
.map(function ([eventName, mods, handlerValue, extraArgs]) {
- let params = "owner";
+ let params = "context";
if (extraArgs) {
if (ctx.loopNumber) {
let argId = ctx.generateID();
@@ -2884,21 +2884,21 @@
// be set asynchronously later when the widget is ready, and the
// context might be different.
ctx.addLine(`let arg${argId} = ${ctx.formatExpression(extraArgs)};`);
- params = `owner, arg${argId}`;
+ params = `context, arg${argId}`;
}
else {
- params = `owner, ${ctx.formatExpression(extraArgs)}`;
+ params = `context, ${ctx.formatExpression(extraArgs)}`;
}
}
- let handler = `function (e) {`;
+ 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 = owner['${handlerValue}'];`;
- handler += `if (fn) { fn.call(${params}, e); } else { owner.${handlerValue}; }`;
+ handler += `const fn = context['${handlerValue}'];`;
+ handler += `if (fn) { fn.call(${params}, e); } else { context.${handlerValue}; }`;
}
handler += `}`;
return `vn.elm.addEventListener('${eventName}', ${handler});`;
@@ -2934,33 +2934,15 @@
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
}
// SLOTS
- const varDefs = [];
const hasSlots = node.childNodes.length;
- if (hasSlots) {
- ctx.rootContext.shouldTrackScope = true;
- for (let v of Object.values(ctx.variables)) {
- if (v["id"]) {
- varDefs.push(v["id"]);
- }
- }
- }
- let scopeVars;
- if (hasSlots) {
- let scope = ctx.scopeVars.length ? `Object.assign({}, scope)` : `{}`;
- let vars = varDefs.length ? `{${varDefs.join(",")}}` : "undefined";
- scopeVars = `${scope}, ${vars}`;
- }
- else {
- scopeVars = "undefined, undefined";
- }
+ let scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined";
ctx.addIf(`w${componentID}`);
// need to update component
let styleCode = "";
if (tattStyle) {
styleCode = `.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};w${componentID}.el.style=${tattStyle};});`;
}
- ctx.addLine(`w${componentID}.__updateProps(props${componentID}, extra.fiber${scopeVars &&
- ", " + scopeVars})${styleCode};`);
+ ctx.addLine(`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`);
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
if (registerCode) {
ctx.addLine(registerCode);
@@ -3009,7 +2991,7 @@
QWeb.slots[`${slotId}_default`] = slotFn;
}
}
- ctx.addLine(`let fiber = w${componentID}.__prepare(extra.fiber, ${scopeVars}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`);
+ ctx.addLine(`let fiber = w${componentID}.__prepare(extra.fiber, ${scope}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
ctx.addLine(`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`);
@@ -3120,7 +3102,7 @@
* states and in general determine the state of the rendering.
*/
class Fiber {
- constructor(parent, component, force, target) {
+ constructor(parent, component, force, inserter) {
this.id = Fiber.nextId++;
// isCompleted means that the rendering corresponding to this fiber's work is
// done, either because the component has been mounted or patched, or because
@@ -3148,10 +3130,9 @@
this.parent = null;
this.component = component;
this.force = force;
- this.target = target;
+ this.inserter = inserter;
const __owl__ = component.__owl__;
this.scope = __owl__.scope;
- this.vars = __owl__.vars;
this.root = parent ? parent.root : this;
this.parent = parent;
let oldFiber = __owl__.currentFiber;
@@ -3257,7 +3238,7 @@
complete() {
let component = this.component;
this.isCompleted = true;
- if (!this.target && !component.__owl__.isMounted) {
+ if (!this.inserter && !component.__owl__.isMounted) {
return;
}
// build patchQueue
@@ -3284,29 +3265,29 @@
const fiber = patchQueue[i];
component = fiber.component;
component.__patch(fiber.vnode);
- if (!fiber.shouldPatch && (!fiber.target || i !== 0)) {
+ if (!fiber.shouldPatch && (!fiber.inserter || i !== 0)) {
component.__owl__.pvnode.elm = component.__owl__.vnode.elm;
}
component.__owl__.currentFiber = null;
}
// insert into the DOM (mount case)
let inDOM = false;
- if (this.target) {
- this.target.appendChild(this.component.el);
- inDOM = document.body.contains(this.target);
+ if (this.inserter) {
+ this.inserter(this.component.el);
+ inDOM = document.body.contains(this.component.el);
this.component.env.qweb.trigger("dom-appended");
}
// call patched/mounted hook on each fiber of (reversed) patchQueue
for (let i = patchLen - 1; i >= 0; i--) {
const fiber = patchQueue[i];
component = fiber.component;
- if (fiber.shouldPatch && !this.target) {
+ if (fiber.shouldPatch && !this.inserter) {
component.patched();
if (component.__owl__.patchedCB) {
component.__owl__.patchedCB();
}
}
- else if (this.target ? inDOM : true) {
+ else if (this.inserter ? inDOM : true) {
component.__callMounted();
}
}
@@ -3554,8 +3535,7 @@
renderFn: qweb.render.bind(qweb, template),
classObj: null,
refs: null,
- scope: null,
- vars: null
+ scope: null
};
}
/**
@@ -3642,7 +3622,8 @@
*
* Note that a component can be mounted an unmounted several times
*/
- async mount(target) {
+ async mount(target, options = {}) {
+ const position = options.position || "last-child";
const __owl__ = this.__owl__;
if (__owl__.isMounted) {
return Promise.resolve();
@@ -3652,7 +3633,15 @@
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
throw new Error(message);
}
- const fiber = new Fiber(null, this, false, target);
+ let inserter = position === "last-child"
+ ? el => target.appendChild(el)
+ : position === "first-child"
+ ? el => target.prepend(el)
+ : el => { };
+ if (position === "self") {
+ this.__target = target;
+ }
+ const fiber = new Fiber(null, this, false, inserter);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber, () => { });
@@ -3838,9 +3827,8 @@
* The __updateProps method is called by the t-component directive whenever
* it updates a component (so, when the parent template is rerendered).
*/
- async __updateProps(nextProps, parentFiber, scope, vars) {
+ async __updateProps(nextProps, parentFiber, scope) {
this.__owl__.scope = scope;
- this.__owl__.vars = vars;
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
if (shouldUpdate) {
const __owl__ = this.__owl__;
@@ -3876,17 +3864,25 @@
*/
__patch(vnode) {
const __owl__ = this.__owl__;
- const target = __owl__.vnode || document.createElement(vnode.sel);
- __owl__.vnode = patch(target, vnode);
+ if (this.__target) {
+ if (this.__target.tagName.toLowerCase() !== vnode.sel) {
+ throw new Error(`Cannot attach '${this.constructor.name}' to target node (not same tag name)`);
+ }
+ __owl__.vnode = patch(this.__target, vnode);
+ delete this.__target;
+ }
+ else {
+ const target = __owl__.vnode || document.createElement(vnode.sel);
+ __owl__.vnode = patch(target, vnode);
+ }
}
/**
* The __prepare method is only called by the t-component directive, when a
- * subcomponent is created. It gets its scope and vars, if any, from the
+ * subcomponent is created. It gets its scope, if any, from the
* parent template.
*/
- __prepare(parentFiber, scope, vars, cb) {
+ __prepare(parentFiber, scope, cb) {
this.__owl__.scope = scope;
- this.__owl__.vars = vars;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
fiber.shouldPatch = false;
if (!parentFiber.child) {
@@ -4463,6 +4459,16 @@
}
});
}
+ /**
+ * Override to revert back to a classic Component's structure
+ *
+ * @override
+ */
+ __callWillUnmount() {
+ super.__callWillUnmount();
+ this.el.appendChild(this.portal.elm);
+ this.doTargetLookUp = true;
+ }
/**
* At each DOM change, we must ensure that the portal contains exactly one
* child
@@ -4531,7 +4537,7 @@
}
}
this.__checkVNodeStructure(vnode);
- const shouldDeploy = !this.portal && !this.doTargetLookUp;
+ const shouldDeploy = (!this.portal || this.el.contains(this.portal.elm)) && !this.doTargetLookUp;
if (!this.doTargetLookUp && !shouldDeploy) {
// Only on pure patching, provided the
// this.target's parent has not been unmounted
@@ -4858,9 +4864,9 @@
exports.useState = useState$1;
exports.utils = utils;
- exports.__info__.version = '1.0.0-beta1';
- exports.__info__.date = '2019-12-03T07:54:22.133Z';
- exports.__info__.hash = '42aa9d3';
+ exports.__info__.version = '1.0.0-beta2';
+ exports.__info__.date = '2019-12-11T09:31:58.227Z';
+ exports.__info__.hash = '0762eeb';
exports.__info__.url = 'https://github.com/odoo/owl';
}(this.owl = this.owl || {}));