Compare commits

...

36 Commits

Author SHA1 Message Date
Géry Debongnie b3181f119d [REL] v1.4.4
# v1.4.4

fix: overlapping multi-class in t-att-class
2021-09-03 09:57:00 +02:00
Géry Debongnie 1e1e05350a [REF] run prettier on codebase 2021-09-03 09:56:09 +02:00
Samuel Degueldre b242230e20 [FIX] qweb: fix overlapping multi-class in t-att-class
Previously, if two attributes in t-att-class shared some classes, their
presence would be determined by the last attribute declared, instead of
being present if any attribute containing it evaluates to true. This
commit fixes that.
2021-09-01 11:58:45 +02:00
Géry Debongnie 9fe2da704e [REL] v1.4.3
# v1.4.3

- fix: another scoping issue with t-slots
2021-07-08 11:54:32 +02:00
Géry Debongnie 21b1661d39 [CLEANUP] run prettier on codebase 2021-07-08 11:53:48 +02:00
Géry Debongnie ec05b1f5e3 [FIX] slots: fix bad interaction between t-call and slots
The previous changes in the combine method (used to copy all the
variables defined in the current scope for use in a slot) had the effect
of squashing the prototype chain: instead of `Component -> Obj1 ->
Obj2 -> Obj3`, the combined scope had: `Component -> Obj1'`.

This has an unfortunate interaction with the way t-call is implemented,
which uses the fact that we are in a subscope to add a own
__access_mode__. It depends specifially on the prototype chain, and that
the parent scope may have a different value for that property. But with
the way combine was implemented, we lost all that subtlety since
everything is squashed.

In this commit, we reimplement that function in a way to make sure we
keep the prototype chain structure
2021-07-08 11:49:25 +02:00
Géry Debongnie 8083678f03 [REL] v1.4.2
# 1.4.2

- qweb: properly handle inline expressions with lists such as '[a,b,c]'
2021-07-07 14:56:46 +02:00
Géry Debongnie 61c2ec5d83 [FIX] qweb: properly handle lists in inline expressions
Before this commit, Owl inline expressions with a list with multiple
elements such as [a,b,c] was transformed into

[scope['a'], b: scope['b'], scope['c']]

instead of

[scope['a'], scope['b'], scope['c']]

This is due to a previous commit adding support for short object
descriptions such as {a,b}.

To fix this means that we have to keep track of the current group type
for the expression, which is done by using a stack.
2021-07-07 14:50:27 +02:00
Géry Debongnie e579a993fd [REL] v1.4.1
# 1.4.1

This release brings in two small fixes:

- vdom: a performance improvement to reduce number of calls to classList.remove
- slots: fix subtle issue with wrong context used in event handlers when
  multiple slots are involved
2021-07-07 11:10:05 +02:00
Géry Debongnie ea3c6f7bf0 [FIX] slots: make sure handlers are properly bound to component
In some situations (a slot inside a slot), the combine method was
wrongly copying all properties of the scope into the context, which
caused the event handling system to wrongly use a subobject as component
(since it detects the fact that __owl__ is a own property_).
Consequently, we could have very subtle issue with some properties being
shadowed by a sub object.
2021-07-07 11:04:25 +02:00
Pierre Paridans 9cfafc30b5 [IMP] vdom: performance improvement
Port from original snabbdom project: snabbdom/snabbdom#634

The issue is that before this commit, the removeClass method was
sometimes called even if it is not useful. See this comment for more
detail: https://github.com/snabbdom/snabbdom/issues/633#issue-618706258
2021-07-06 14:14:40 +02:00
Aaron Bohy ad42c583c6 [FIX] package.json: bump node version to v12
Node 10.x is no longer maintained, and since a recent commit [1],
a test fails with that version.

[1] 88fd1cf483
2021-07-06 13:30:17 +02:00
Géry Debongnie 6faaa6c361 [REL] v1.4.0
# v1.4.0

This release is larger than usual: lot of small fixes, and a few improvements as
well.

- config: change debug mode warning to info
- fix: add support for callable expression in event handler (for example, `t-on-click="some.function"`)
- fix: allow specific props to override generic props (in `t-props`)
- fix: add support for short object description in inline expressions: `{ hey }`
- fix: add support for t-att-value on `<textarea/>` and `<select/>`
- fix: slots: issue with scoping in nested loops
- fix: slots: default slot is not lost in some cases
- fix: do not shadow an error in case the finalizing code fails
- imp: qweb: allow multiple class in class attribute object notation: `t-att-class="{'a b': value}"`
- imp: better detection for dynamic component change (less need for `t-key`)
- fix: router: allow querystrings in path
- fix: qweb: add support for template strings
- fix: qweb: make sure nodes with t-transition are removed in all cases
2021-07-06 10:27:55 +02:00
Géry Debongnie bff539cabf [FIX] tooling: make sure we escape content in release script 2021-07-06 10:27:30 +02:00
Joseph Caburnay 88fd1cf483 [FIX] extensions: guarantee to call transitionend callback
There is a possibility that the transitionend event of an element/component
with t-transition directive won't trigger. Though this situation is
difficult to assert, it was observe in odoo runbot for the pos ui.

When the transitionend event is not fired, the callback that removes
the element from the dom won't be called, resulting to a corrupted view.
An example of which is the following:

```html
<div t-if="show" t-transition="fade">Hello</div>
```

If `show` is set to false by some ui action and by any chance the
transitionend event is not fired (perhaps because the transition didn't
actually start or because of completely unknown reason), the div element
will remain in the view -- and this is not desirable.

This commit patches this situation such that if after 50ms that the
transitionend event is supposed to be fired but the event isn't fired, we
force the callback using a setTimeout. This guarantees the call of the
callback that is suppose to remove the element from the view.
2021-07-06 09:11:12 +02:00
Russell Briggs f0b75b6890 [IMP] router: allow query string in paths 2021-07-06 09:05:58 +02:00
Géry Debongnie a1552117f9 [IMP] qweb: add support for template strings
in inline expressions, such as t-esc or t-value.

closes #746
2021-07-05 10:09:23 +02:00
Géry Debongnie 17ae1d06c4 [IMP] component: better detection for dynamic component change
Before this commit, Owl could not detect that the underlying component
in a template such as <t t-component="{{state.child}}"/> was changing,
if the two components have the same tag as root element.

This is because the reconciliation is done at the vdom level, which does
not know about components.  To solve this, one could use a t-key to make
sure owl can make the difference.

With this commit, we can simply use our knowledge of the fact that we are
dealing with a dynamic component and autogenerate a suitable key.

closes #623
2021-07-05 10:09:23 +02:00
Géry Debongnie 27629cedfa [IMP] qweb: allow multiclasses in t-att-class object form
the low level method htmlelement.classList.add does not accept multiple
classes in one string, which is why, in owl, the expression

`<div t-att-class="{'a b c': value}" />`

did not work as one might expect. It is however very convenient in real
life templates, so this commit improve owl by adding support for this
feature.

closes #813
2021-07-05 10:09:23 +02:00
Géry Debongnie 3a93370ab6 [FIX] component: do not shadow the initial error in some cases
Before this commit, the error handling code simply destroyed the
application whenever an unhandled error occured in the owl rendering
process. This is perfectly fine, except that since the application is
potentially corrupted, the destroy code may crash as well. We simply
catch those errors to avoid shadowing the main issue.

closes #866
2021-07-05 10:09:23 +02:00
Géry Debongnie a6bb4d8ee1 [FIX] component: make sure default slot is applied
Before this commit, owl was erroneously defining default slots in most
cases, even though they are empty. The problem occurs when the content
of a component slots is a t-set-slot, and we remove that, then use the
rest as default slots, even though it is only reduced to a set of text
nodes.

With this commit, we only consider the content as default content if it
is not only a sequence of text nodes containing only spaces.

closes #882
2021-07-05 10:09:23 +02:00
Géry Debongnie caf842c482 [FIX] component: fix scoping issue in nested loops
The templates contained in a slot have to be rendered with the current
scope, but during the rendering of the sub component, which happens
later (after the willStart for the sub component). Therefore, we need to
save the scope that should be used for the slots, so we can access the
proper variables. This was done using an Object.assign() statement,
which is actually only a shallowclone for the own properties.

In this commit, we properly copy all the properties for the current
scope, even those contained in the prototype.

closes #855
2021-07-05 10:09:23 +02:00
Géry Debongnie 1b513a1637 [FIX] qweb: add support for t-att-value on <select> tags
Before this commit, it was not possible to set the value of a select tag
by using the t-att-value attribute.

Doing so is not actually trivial because of the way the vdom works: it
processes the node attributes before its children are created, which
means that the vdom code tries to set the initial value of the select
before its children are created, which means that it is ignored. To make
it work, I added a node create hook which is called after the children
are completely processed.

closes #873
2021-07-05 10:09:23 +02:00
Géry Debongnie 52a200878a [FIX] qweb: add support for t-att-value on textareas
The code did not treat textareas in the same way as inputs. As a result,
using t-att-value on a textarea did not work.

closes #872
2021-07-05 10:09:23 +02:00
Géry Debongnie f4994a20d8 [MISC] owl: add temp/ in gitignore
I sometimes use the temp folder to keep stuff in while working
on refactorings/switching branches.
2021-07-05 10:09:23 +02:00
Géry Debongnie 7419c1982a [DOC] tags: add missing information
The example given in the tags page was misleading.

closes #884
2021-07-05 10:09:23 +02:00
Bishal Pun a8c8f6affe Update comparison.md 2021-07-03 07:27:47 +02:00
Géry Debongnie dabe7f9bfa [FIX] qweb: properly parse short object description
Before this commit, using an expression such as "{machin}" was compiled
in qweb into "{scope['machin']}" which is not valid.

With this commit, we instead transform it into "{machin:
scope['machin']}".

closes #885
2021-07-02 14:20:13 +02:00
Michael Mattiello (mcm) d08ea63565 [IMP] directive: make props override t-props
Before this commit, props and t-props were computed like
`Object.assign(props, t_props)`.
Now, it computes like `Object.assign(t_props, props)` so props will
override t-props.

closes https://github.com/odoo/owl/issues/886
2021-07-02 11:50:23 +02:00
Sébastien Theys 7ebe0da962 [IMP] component: add support for callable expression in event handler 2021-07-01 12:26:23 +02:00
Xavier Morel 3e0e8475a6 [CHG] config: flip debug mode warning to info
Backport of odoo/odoo@e4834fce69

There's no way to easily disable / silence this warning, because
multiple tours explicitly opt into debug mode (with good reasons). So
it's not enough to bypass the `mode` setter in the test setup helpers
and set `QWeb.dev` directly (which works for test_main in POS and
main_tests in web), there would also need to be special workarounds in
the 4 modules which set `owl.config.mode` based on the session's debug
mode.

I'm not sure how useful this warning is: it defaults to `false` so the
only situation in which this would be relevant would be for a
third-party to use Owl *and* explicitly enable the debug mode
in-source *and* forget to remove it when deploying to production *and*
look at their console.
2021-06-29 08:43:20 +02:00
Géry Debongnie 2e83c739b9 [REL] v1.3.2
# v1.3.2

Fixes

- components: correctly propagate errors to parent
2021-06-18 10:41:51 +02:00
Géry Debongnie 922eb7cd98 [FIX] component: propagate errors to parent
Before this commit, the error handling process was too naive: once an
error occurs in a rendering, owl catches it, looks for a component that
implements the catchError method, then calls it.

However, in real life, we sometimes need to rethrow that error (or
another one) to propagate the error to some parent handler. This error
needs to be handled by the closest parent component that implements
catchError.

This is what this commit implements: it wraps the catchError call in a
try/catch, then in case of errors, try to handle it by a parent.
2021-06-18 10:34:09 +02:00
Géry Debongnie d12115554c [REL] v1.3.1
# v1.3.1

Fixes

- components: fix issue with t-call in nested t-slot (parent component was incorrect)
2021-06-10 10:36:33 +02:00
Géry Debongnie 1a6b26c8c6 [REF] cleanup: prettier 2021-06-10 10:23:48 +02:00
Lucas Perais (lpe) d67d295eaa [FIX] qweb, t-call: t-call nested in t-slot
Have a t-call within a t-set-slot of a component.
The called template has a t-component directive.

It should be like:

```xml
<t t-name="Zero">
  <Slotted>
    <t t-call="someTemplate" />
  </Slotted>
</t>

<t t-name="someTemplate">
  <SomeComponent />
</t>
```

Before this commit, the parent of SomeComponent was the Zero component

After this commit, the parent of SomeComponent is the Slotted Component as it should be

closes #862
2021-06-10 10:00:22 +02:00
33 changed files with 1179 additions and 139 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
node-version: [12.x, 14.x]
steps:
- uses: actions/checkout@v2
+4 -1
View File
@@ -28,4 +28,7 @@ node_modules
release-notes.md
.rpt2_cache
.rpt2_cache
# useful in some cases
/temp
+1 -1
View File
@@ -120,7 +120,7 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.3.0](https://github.com/odoo/owl/releases/tag/v1.3.0)
- [owl-1.4.4](https://github.com/odoo/owl/releases/tag/v1.4.4)
## License
+2 -2
View File
@@ -4,7 +4,7 @@ OWL, React and Vue have the same main feature: they allow developers to build
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
In this page, we try to highlight some of these differences. Obviously, a lot of
effort was done to be fair. However, if you disagree with some of the points
effort was put to be fair. However, if you disagree with some of the points
discussed, feel free to open an issue/submit a PR to correct this text.
## Content
@@ -47,7 +47,7 @@ components are fast enough for all our usecases, and making it as simple as
possible for developers is more valuable (for us).
Also, functions or class based components are more than just syntax. Functions
comes with a mindset of composition and class are about inheritance. Clearly,
come with a mindset of composition and class are about inheritance. Clearly,
both of these are important mechanisms for reusing code. Also, one does not
exclude the other.
+19
View File
@@ -13,6 +13,7 @@
- [Setting Variables](#setting-variables)
- [Conditionals](#conditionals)
- [Dynamic Attributes](#dynamic-attributes)
- [Dynamic Class Attribute](#dynamic-class-attribute)
- [Dynamic Tag Names](#dynamic-tag-names)
- [Loops](#loops)
- [Rendering Sub Templates](#rendering-sub-templates)
@@ -326,6 +327,24 @@ values) or a pair `[key, value]`. For example:
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
```
### Dynamic class attribute
For convenience, Owl supports a special case for the `t-att-class` case: one can
use an object with keys describing the classes, and values boolean value denoting
if the class is or is not present:
```xml
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
```
Note that it can be combined with normal class attribute:
```xml
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
```
### Dynamic tag names
When writing generic components or templates, the specific concrete tag for an
+6 -6
View File
@@ -62,14 +62,14 @@ The CSS tag is useful to define a css stylesheet in the javascript file:
```js
class MyComponent extends Component {
static template = xml`
static template = xml`
<div class="my-component">some template</div>
`;
static css`
.my-component {
color: red;
}
`;
static style = css`
.my-component {
color: red;
}
`;
}
```
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "1.3.0",
"version": "1.4.4",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
@@ -10,7 +10,7 @@
"dist"
],
"engines": {
"node": ">=10.15.3"
"node": ">=12.18.3"
},
"scripts": {
"build:bundle": "rollup -c",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.3.0
- Current version: 1.4.4
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+30 -8
View File
@@ -245,7 +245,18 @@ QWeb.addDirective({
.join(",");
let componentID = ctx.generateID();
const templateKey = ctx.generateTemplateKey();
let hasDefinedKey = false;
let templateKey;
if (node.tagName === "t" && !node.hasAttribute("t-key") && value.match(INTERP_REGEXP)) {
defineComponentKey();
const id = ctx.generateID();
// the ___ is to make sure we have no possible conflict with normal
// template keys
ctx.addLine(`let k${id} = '___' + componentKey${componentID}`);
templateKey = `k${id}`;
} else {
templateKey = ctx.generateTemplateKey();
}
let ref = node.getAttribute("t-ref");
let refExpr = "";
let refKey: string = "";
@@ -291,7 +302,7 @@ QWeb.addDirective({
if (tattClass) {
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `utils.toObj(${tattExpr})`;
tattExpr = `utils.toClassObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
@@ -336,7 +347,7 @@ QWeb.addDirective({
}
if (hasDynamicProps) {
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
ctx.addLine(`let props${componentID} = Object.assign({}, ${dynamicProp}, {${propStr}});`);
} else {
ctx.addLine(`let props${componentID} = {${propStr}};`);
}
@@ -355,7 +366,7 @@ QWeb.addDirective({
// SLOTS
const hasSlots = node.childNodes.length;
let scope = hasSlots ? `Object.assign(Object.create(context), scope)` : "undefined";
let scope = hasSlots ? `utils.combine(context, scope)` : "undefined";
ctx.addIf(`w${componentID}`);
@@ -378,9 +389,15 @@ QWeb.addDirective({
ctx.addElse();
// new component
function defineComponentKey() {
if (!hasDefinedKey) {
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
hasDefinedKey = true;
}
}
defineComponentKey();
const contextualValue = value.match(INTERP_REGEXP) ? "false" : ctx.formatExpression(value);
const interpValue = ctx.interpolate(value);
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
ctx.addLine(
`let W${componentID} = ${contextualValue} || context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
);
@@ -452,12 +469,17 @@ QWeb.addDirective({
}
}
if (clone.childNodes.length) {
let hasContent = false;
const t = clone.ownerDocument!.createElement("t");
for (let child of Object.values(clone.childNodes)) {
hasContent =
hasContent || (child instanceof Text ? Boolean(child.textContent.trim().length) : true);
t.appendChild(child);
}
const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true });
QWeb.slots[`${slotId}_default`] = slotFn;
if (hasContent) {
const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true });
QWeb.slots[`${slotId}_default`] = slotFn;
}
}
}
+31 -10
View File
@@ -326,22 +326,43 @@ export class Fiber {
const qweb = component.env.qweb;
let root = component;
let canCatch = false;
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
qweb.trigger("error", error);
if (canCatch) {
component.catchError!(error);
} else {
function handle(error) {
let canCatch = false;
qweb.trigger("error", error);
while (component && !(canCatch = !!component.catchError)) {
root = component;
component = component.__owl__.parent!;
}
if (canCatch) {
try {
component.catchError!(error);
} catch (e) {
root = component;
component = component.__owl__.parent!;
return handle(e);
}
return true;
}
return false;
}
let isHandled = handle(error);
if (!isHandled) {
// the 3 next lines aim to mark the root fiber as being in error, and
// to force it to end, without waiting for its children
this.root.counter = 0;
this.root.error = error;
scheduler.flush();
root.destroy();
// at this point, the state of the application is corrupted and we could
// have a lot of issues or crashes. So we destroy the application in a try
// catch and swallow these errors because the fiber is already in error,
// and this is the actual issue that needs to be solved, not those followup
// errors.
try {
root.destroy();
} catch (e) {}
}
}
}
+4 -4
View File
@@ -20,10 +20,10 @@ Object.defineProperty(config, "mode", {
set(mode: string) {
QWeb.dev = mode === "dev";
if (QWeb.dev) {
const url = `https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode`;
console.warn(
`Owl is running in 'dev' mode. This is not suitable for production use. See ${url} for more information.`
);
console.info(`Owl is running in 'dev' mode.
This is not suitable for production use.
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
+3 -1
View File
@@ -281,7 +281,9 @@ QWeb.addDirective({
// Step 4: add the appropriate function call to current component
// ------------------------------------------------
const parentComponent = `utils.getComponent(context)`;
const parentComponent = ctx.rootContext.shouldDefineParent
? `parent`
: `utils.getComponent(context)`;
const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
+49 -2
View File
@@ -57,6 +57,7 @@ type TKind =
| "RIGHT_PAREN"
| "COMMA"
| "VALUE"
| "TEMPLATE_STRING"
| "SYMBOL"
| "OPERATOR"
| "COLON";
@@ -67,6 +68,7 @@ interface Token {
originalValue?: string;
size?: number;
varName?: string;
replace?: Function;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
@@ -89,7 +91,7 @@ type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function (expr) {
let s = expr[0];
let start = s;
if (s !== "'" && s !== '"') {
if (s !== "'" && s !== '"' && s !== "`") {
return false;
}
let i = 1;
@@ -111,6 +113,17 @@ let tokenizeString: Tokenizer = function (expr) {
throw new Error("Invalid expression");
}
s += start;
if (start === "`") {
return {
type: "TEMPLATE_STRING",
value: s,
replace(replacer) {
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
return "${" + replacer(group) + "}";
});
},
};
}
return { type: "VALUE", value: s };
};
@@ -210,6 +223,10 @@ export function tokenize(expr: string): Token[] {
// Expression "evaluator"
//------------------------------------------------------------------------------
const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
const isRightSeparator = (token) =>
token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
/**
* 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
@@ -238,13 +255,39 @@ export function tokenize(expr: string): Token[] {
export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] {
scope = Object.create(scope);
const tokens = tokenize(expr);
for (let i = 0; i < tokens.length; i++) {
let i = 0;
let stack = []; // to track last opening [ or {
while (i < tokens.length) {
let token = tokens[i];
let prevToken = tokens[i - 1];
let nextToken = tokens[i + 1];
let groupType = stack[stack.length - 1];
switch (token.type) {
case "LEFT_BRACE":
case "LEFT_BRACKET":
stack.push(token.type);
break;
case "RIGHT_BRACE":
case "RIGHT_BRACKET":
stack.pop();
}
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
if (prevToken) {
// normalize missing tokens: {a} should be equivalent to {a:a}
if (
groupType === "LEFT_BRACE" &&
isLeftSeparator(prevToken) &&
isRightSeparator(nextToken)
) {
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
nextToken = tokens[i + 1];
}
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
@@ -254,6 +297,9 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
}
}
}
if (token.type === "TEMPLATE_STRING") {
token.value = token.replace((expr) => compileExpr(expr, scope));
}
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
if (token.type === "RIGHT_PAREN") {
let j = i - 1;
@@ -278,6 +324,7 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
token.value = `scope['${token.value}']`;
}
}
i++;
}
return tokens;
}
+10 -1
View File
@@ -2,6 +2,7 @@ import { STATUS } from "../component/component";
import { VNode } from "../vdom/index";
import { INTERP_REGEXP } from "./compilation_context";
import { QWeb } from "./qweb";
import { browser } from "../browser";
/**
* Owl QWeb Extensions
@@ -75,6 +76,7 @@ export function makeHandlerCode(
// we need to capture every variable in it
putInCache = false;
code = ctx.captureExpression(value);
code = `const res = (() => { return ${code} })(); if (typeof res === 'function') { res(e) }`;
}
const modCode = mods.map((mod) => modcodes[mod]).join("");
let handler = `function (e) {if (context.__owl__.status === ${STATUS.DESTROYED}){return}${modCode}${code}}`;
@@ -197,7 +199,14 @@ function whenTransitionEnd(elm: HTMLElement, cb) {
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
const timeout: number = getTimeout(delays, durations);
if (timeout > 0) {
elm.addEventListener("transitionend", cb, { once: true });
const transitionEndCB = () => {
if (!elm.parentNode) return;
cb();
browser.clearTimeout(fallbackTimeout);
elm.removeEventListener("transitionend", transitionEndCB);
};
elm.addEventListener("transitionend", transitionEndCB, { once: true });
const fallbackTimeout = browser.setTimeout(transitionEndCB, timeout + 1);
} else {
cb();
}
+63 -14
View File
@@ -80,7 +80,7 @@ const NODE_HOOKS_PARAMS = {
};
interface Utils {
toObj(expr: any): Object;
toClassObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
@@ -111,20 +111,53 @@ function vDomToString(vdom: VNode[]): string {
const UTILS: Utils = {
zero: Symbol("zero"),
toObj(expr) {
toClassObj(expr) {
const result = {};
if (typeof expr === "string") {
// we transform here a list of classes into an object:
// 'hey you' becomes {hey: true, you: true}
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 expr;
// this is already an object, but we may need to split keys:
// {'a b': true, 'a c': false} should become {a: true, b: true, c: false}
for (let key in expr) {
const value = expr[key];
const words = key.split(/\s+/);
for (let word of words) {
result[word] = result[word] || value;
}
}
return result;
},
/**
* This method combines the current context with the variables defined in a
* scope for use in a slot.
*
* The implementation is kind of tricky because we want to preserve the
* prototype chain structure of the cloned result. So we need to traverse the
* prototype chain, cloning each level respectively.
*/
combine(context, scope) {
let clone = context;
const scopeStack = [];
while (!isComponent(scope)) {
scopeStack.push(scope);
scope = scope.__proto__;
}
while (scopeStack.length) {
let scope = scopeStack.pop();
clone = Object.create(clone);
Object.assign(clone, scope);
}
return clone;
},
shallowEqual,
addNameSpace(vnode) {
@@ -436,6 +469,7 @@ export class QWeb extends EventBus {
ctx.variables = Object.create(null);
ctx.parentNode = ctx.generateID();
ctx.allowMultipleRoots = true;
ctx.shouldDefineParent = true;
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
@@ -616,13 +650,21 @@ export class QWeb extends EventBus {
}
if (node.nodeName !== "t" || node.hasAttribute("t-tag")) {
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
let nodeHooks = {};
let addNodeHook = function (hook, handler) {
nodeHooks[hook] = nodeHooks[hook] || [];
nodeHooks[hook].push(handler);
};
if (node.tagName === "select" && node.hasAttribute("t-att-value")) {
const value = node.getAttribute("t-att-value");
let exprId = ctx.generateID();
ctx.addLine(`let expr${exprId} = ${ctx.formatExpression(value)};`);
let expr = `expr${exprId}`;
node.setAttribute("t-att-value", expr);
addNodeHook("create", `n.elm.value=${expr};`);
}
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
ctx = ctx.withParent(nodeID);
for (let { directive, value, fullName } of validDirectives) {
if (directive.atNodeCreation) {
@@ -704,16 +746,18 @@ export class QWeb extends EventBus {
isProp = key === "selected" || key === "disabled";
break;
case "textarea":
isProp = key === "readonly" || key === "disabled";
isProp = key === "readonly" || key === "disabled" || key === "value";
break;
case "select":
isProp = key === "disabled" || key === "value";
break;
case "button":
case "select":
case "optgroup":
isProp = key === "disabled";
break;
}
if (isProp) {
props.push(`${key}: _${val}`);
props.push(`${key}: ${val}`);
}
}
let classObj = "";
@@ -749,7 +793,7 @@ export class QWeb extends EventBus {
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleProperties(name, attID);
handleProperties(name, `_${attID}`);
}
}
@@ -761,7 +805,7 @@ export class QWeb extends EventBus {
if (attName === "class") {
ctx.rootContext.shouldDefineUtils = true;
formattedValue = `utils.toObj(${formattedValue})`;
formattedValue = `utils.toClassObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
@@ -784,9 +828,14 @@ export class QWeb extends EventBus {
const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleProperties(attName, attID);
if (node.nodeName === "select" && attName === "value") {
attrs.push(`${attName}: ${v}`);
handleProperties(attName, v);
} else {
ctx.addLine(`let _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleProperties(attName, "_" + attID);
}
}
}
+7 -1
View File
@@ -125,7 +125,10 @@ export class Router {
const initialParams = this.currentParams;
const result = await this.matchAndApplyRules(path);
if (result.type === "match") {
const finalPath = this.routeToPath(result.route, result.params);
let finalPath = this.routeToPath(result.route, result.params);
if (path.indexOf("?") > -1) {
finalPath += "?" + path.split("?")[1];
}
const isPopStateEvent = ev && ev instanceof PopStateEvent;
if (!isPopStateEvent) {
this.setUrlFromPath(finalPath);
@@ -239,6 +242,9 @@ export class Router {
if (route.path === "*") {
return {};
}
if (path.indexOf("?") > -1) {
path = path.split("?")[0];
}
if (path.startsWith("#")) {
path = path.slice(1);
}
+2 -1
View File
@@ -231,7 +231,8 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
elm = vnode.elm as Element;
for (name in oldClass) {
if (name && !klass[name]) {
if (name && !klass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) {
// was `true` and now not provided
elm.classList.remove(name);
}
}
@@ -90,7 +90,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj({d:scope['state'].d}))
Object.assign(_7, utils.toClassObj({d:scope['state'].d}))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
@@ -112,7 +112,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
// Component 'Child'
const ref4 = \`child\`;
let _5 = {'a':true};
Object.assign(_5, utils.toObj(scope['state'].b?'b':''))
Object.assign(_5, utils.toClassObj(scope['state'].b?'b':''))
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
@@ -148,7 +148,7 @@ exports[`class and style attributes with t-component t-att-class is properly add
let scope = Object.create(context);
let h = this.h;
let _7 = {'c':true};
Object.assign(_7, utils.toObj(scope['state'].d?'d':''))
Object.assign(_7, utils.toClassObj(scope['state'].d?'d':''))
let c8 = [], p8 = {key:8,class:_7};
let vn8 = h('span', p8, c8);
return vn8;
@@ -257,6 +257,45 @@ exports[`basic widget properties t-key on a component with t-if, and a sibling c
}"
`;
exports[`composition can switch between dynamic components without the need for a t-key 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__3\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component '{{state.child}}'
let componentKey2 = (scope['state'].child);
let k3 = '___' + componentKey2
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let W2 = false || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`composition sub components with some state rendered in a loop 1`] = `
"function anonymous(context, extra
) {
@@ -330,7 +369,9 @@ exports[`composition t-component with dynamic value 1`] = `
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component '{{state.widget}}'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let componentKey2 = (scope['state'].widget);
let k3 = '___' + componentKey2
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
@@ -341,13 +382,12 @@ exports[`composition t-component with dynamic value 1`] = `
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = (scope['state'].widget);
let W2 = false || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
@@ -368,7 +408,9 @@ exports[`composition t-component with dynamic value 2 1`] = `
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Widget{{state.widget}}'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let componentKey2 = \`Widget\${scope['state'].widget}\`;
let k3 = '___' + componentKey2
let w2 = k3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k3]] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
@@ -379,13 +421,12 @@ exports[`composition t-component with dynamic value 2 1`] = `
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Widget\${scope['state'].widget}\`;
let W2 = false || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
parent.__owl__.cmap[k3] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
let pvnode = h('dummy', {key: k3, hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
@@ -467,7 +508,7 @@ exports[`dynamic t-props basic use 1`] = `
let vn1 = h('div', p1, c1);
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = Object.assign({}, scope['some'].obj);
let props2 = Object.assign({}, scope['some'].obj, {});
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
@@ -518,7 +559,7 @@ exports[`other directives with t-component slot setted value (with t-set) not ac
w3 = false;
}
if (w3) {
w3.__updateProps(props3, extra.fiber, Object.assign(Object.create(context), scope));
w3.__updateProps(props3, extra.fiber, utils.combine(context, scope));
let pvnode = w3.__owl__.pvnode;
c1.push(pvnode);
} else {
@@ -528,7 +569,7 @@ exports[`other directives with t-component slot setted value (with t-set) not ac
w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id;
w3.__owl__.slotId = 1;
let fiber = w3.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w3.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
@@ -579,7 +620,7 @@ exports[`other directives with t-component t-on expression captured in t-foreach
c6.push(vn7);
const otherState_8 = scope['otherState'];
const iter_8 = scope.iter;
p7.on['click'] = function (e) {if (context.__owl__.status === 5){return}otherState_8.vals.push(iter_8+'_'+iter_8)};
p7.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return otherState_8.vals.push(iter_8+'_'+iter_8) })(); if (typeof res === 'function') { res(e) }};
c7.push({text: \`expr\`});
utils.getScope(scope, 'iter').iter = scope.iter+1;
}
@@ -630,7 +671,7 @@ exports[`other directives with t-component t-on expression in t-foreach 1`] = `
c6.push(vn9);
const otherState_10 = scope['otherState'];
const val_10 = scope['val'];
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}otherState_10.vals.push(val_10)};
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return otherState_10.vals.push(val_10) })(); if (typeof res === 'function') { res(e) }};
c9.push({text: \`Expr\`});
}
scope = _origScope5;
@@ -684,7 +725,7 @@ exports[`other directives with t-component t-on expression in t-foreach with t-s
const otherState_10 = scope['otherState'];
const val_10 = scope['val'];
const bossa_10 = scope.bossa;
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}otherState_10.vals.push(val_10+'_'+bossa_10)};
p9.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return otherState_10.vals.push(val_10+'_'+bossa_10) })(); if (typeof res === 'function') { res(e) }};
c9.push({text: \`Expr\`});
}
scope = _origScope5;
@@ -1011,7 +1052,7 @@ exports[`other directives with t-component t-on with inline statement 1`] = `
if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')}
w3 = new W3(parent, props3);
parent.__owl__.cmap['__4__'] = w3.__owl__.id;
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}state_5.counter++});}});});
let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('ev', function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_5.counter++ })(); if (typeof res === 'function') { res(e) }});}});});
let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}});
c1.push(pvnode);
w3.__owl__.pvnode = pvnode;
@@ -1573,6 +1614,7 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
) {
// Template name: \\"sub\\"
let utils = this.constructor.utils;
let parent = extra.parent;
let h = this.h;
let c2 = extra.parentNode;
let key0 = extra.key || \\"\\";
@@ -1591,6 +1633,7 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
) {
// Template name: \\"sub\\"
let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c2 = extra.parentNode;
+184 -44
View File
@@ -19,7 +19,7 @@ exports[`t-slot directive can define and call slots 1`] = `
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
@@ -29,7 +29,7 @@ exports[`t-slot directive can define and call slots 1`] = `
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -44,23 +44,23 @@ exports[`t-slot directive can define and call slots 2`] = `
) {
// Template name: \\"Dialog\\"
let h = this.h;
let c8 = [], p8 = {key:8};
let vn8 = h('div', p8, c8);
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
let c10 = [], p10 = {key:10};
let vn10 = h('div', p10, c10);
c9.push(vn10);
const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
c8.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot10) {
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
let c12 = [], p12 = {key:12};
let vn12 = h('div', p12, c12);
c9.push(vn12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
let c11 = [], p11 = {key:11};
let vn11 = h('div', p11, c11);
c8.push(vn11);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
}
return vn9;
return vn8;
}"
`;
@@ -68,6 +68,7 @@ exports[`t-slot directive can define and call slots 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c5 = [], p5 = {key:5};
@@ -81,6 +82,7 @@ exports[`t-slot directive can define and call slots 4`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let parent = extra.parent;
let h = this.h;
let c6 = extra.parentNode;
let c7 = [], p7 = {key:7};
@@ -109,7 +111,7 @@ exports[`t-slot directive can define and call slots using old t-set keyword 1`]
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
@@ -119,7 +121,7 @@ exports[`t-slot directive can define and call slots using old t-set keyword 1`]
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -134,23 +136,23 @@ exports[`t-slot directive can define and call slots using old t-set keyword 2`]
) {
// Template name: \\"__template__1\\"
let h = this.h;
let c8 = [], p8 = {key:8};
let vn8 = h('div', p8, c8);
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
let c10 = [], p10 = {key:10};
let vn10 = h('div', p10, c10);
c9.push(vn10);
const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
c8.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot10) {
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
let c12 = [], p12 = {key:12};
let vn12 = h('div', p12, c12);
c9.push(vn12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
let c11 = [], p11 = {key:11};
let vn11 = h('div', p11, c11);
c8.push(vn11);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
}
return vn9;
return vn8;
}"
`;
@@ -158,6 +160,7 @@ exports[`t-slot directive can define and call slots using old t-set keyword 3`]
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c5 = [], p5 = {key:5};
@@ -171,6 +174,7 @@ exports[`t-slot directive can define and call slots using old t-set keyword 4`]
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let parent = extra.parent;
let h = this.h;
let c6 = extra.parentNode;
let c7 = [], p7 = {key:7};
@@ -184,6 +188,7 @@ exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c5 = [], p5 = {key:5};
@@ -210,10 +215,50 @@ exports[`t-slot directive dafault slots can define a default content 1`] = `
}"
`;
exports[`t-slot directive default slot next to named slot, with default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Dialog\`;
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`t-slot directive default slot work with text nodes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
c4.push({text: \`sts rocks\`});
@@ -227,15 +272,15 @@ exports[`t-slot directive dynamic t-slot call 1`] = `
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c10 = [], p10 = {key:10,on:{}};
let vn10 = h('button', p10, c10);
extra.handlers['click__11__'] = extra.handlers['click__11__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['toggle'](e);};
p10.on['click'] = extra.handlers['click__11__'];
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c10, parent: extra.parent || context}));
let c9 = [], p9 = {key:9,on:{}};
let vn9 = h('button', p9, c9);
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['toggle'](e);};
p9.on['click'] = extra.handlers['click__10__'];
const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
if (slot11) {
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
}
return vn10;
return vn9;
}"
`;
@@ -243,6 +288,7 @@ exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c5 = [], p5 = {key:5};
@@ -260,6 +306,7 @@ exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_content_template\\"
let parent = extra.parent;
let h = this.h;
let c4 = extra.parentNode;
let c5 = [], p5 = {key:5};
@@ -295,6 +342,7 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
let parent = extra.parent;
context.__owl__.refs = context.__owl__.refs || {};
let h = this.h;
let c8 = extra.parentNode;
@@ -321,6 +369,7 @@ exports[`t-slot directive slots are rendered with proper context 1`] = `
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
let parent = extra.parent;
let h = this.h;
let c8 = extra.parentNode;
let c9 = [], p9 = {key:9,on:{}};
@@ -392,7 +441,7 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
w8 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
w8.__updateProps(props8, extra.fiber, utils.combine(context, scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
} else {
@@ -402,7 +451,7 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w8.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
@@ -418,6 +467,7 @@ exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c10 = extra.parentNode;
@@ -490,7 +540,7 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
w8 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
w8.__updateProps(props8, extra.fiber, utils.combine(context, scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
} else {
@@ -500,7 +550,7 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
w8 = new W8(parent, props8);
parent.__owl__.cmap[k9] = w8.__owl__.id;
w8.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w8.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
@@ -516,6 +566,7 @@ exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c10 = extra.parentNode;
@@ -546,7 +597,7 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, Object.assign(Object.create(context), scope));
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
@@ -556,7 +607,7 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
w2.__owl__.slotId = 1;
let fiber = w2.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
@@ -570,6 +621,7 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c4 = extra.parentNode;
@@ -580,6 +632,94 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
}"
`;
exports[`t-slot directive slots in t-foreach in t-foreach 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _2 = scope['tree'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
let _3 = _4 = _2;
if (!(_2 instanceof Array)) {
_3 = Object.keys(_2);
_4 = Object.values(_2);
}
let _length3 = _3.length;
let _origScope5 = scope;
scope = Object.create(scope);
for (let i1 = 0; i1 < _length3; i1++) {
scope.node1_first = i1 === 0
scope.node1_last = i1 === _length3 - 1
scope.node1_index = i1
scope.node1 = _3[i1]
scope.node1_value = _4[i1]
let key1 = scope['node1'].key;
let c6 = [], p6 = {key:\`\${key1}_6\`};
let vn6 = h('div', p6, c6);
c1.push(vn6);
let _7 = scope['node1'].value;
if (_7 != null) {
c6.push({text: _7});
}
let c8 = [], p8 = {key:\`\${key1}_8\`};
let vn8 = h('ul', p8, c8);
c1.push(vn8);
let _9 = scope['node1'].nodes;
if (!_9) { throw new Error('QWeb error: Invalid loop expression')}
let _10 = _11 = _9;
if (!(_9 instanceof Array)) {
_10 = Object.keys(_9);
_11 = Object.values(_9);
}
let _length10 = _10.length;
let _origScope12 = scope;
scope = Object.create(scope);
for (let i2 = 0; i2 < _length10; i2++) {
scope.node2_first = i2 === 0
scope.node2_last = i2 === _length10 - 1
scope.node2_index = i2
scope.node2 = _10[i2]
scope.node2_value = _11[i2]
let key2 = scope['node2'].key;
// Component 'Child'
let k14 = \`__14__\${key1}__\${key2}__\`;
let w13 = k14 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k14]] : false;
let props13 = {};
if (w13 && w13.__owl__.currentFiber && !w13.__owl__.vnode) {
w13.destroy();
w13 = false;
}
if (w13) {
w13.__updateProps(props13, extra.fiber, utils.combine(context, scope));
let pvnode = w13.__owl__.pvnode;
c8.push(pvnode);
} else {
let componentKey13 = \`Child\`;
let W13 = scope['Child'] || context.constructor.components[componentKey13] || QWeb.components[componentKey13];
if (!W13) {throw new Error('Cannot find the definition of component \\"' + componentKey13 + '\\"')}
w13 = new W13(parent, props13);
parent.__owl__.cmap[k14] = w13.__owl__.id;
w13.__owl__.slotId = 1;
let fiber = w13.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k14, hook: {remove() {},destroy(vn) {w13.destroy();}}});
c8.push(pvnode);
w13.__owl__.pvnode = pvnode;
}
w13.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope12;
}
scope = _origScope5;
return vn1;
}"
`;
exports[`t-slot directive t-set t-value in a slot 1`] = `
"function anonymous(context, extra
) {
+66
View File
@@ -149,6 +149,24 @@ describe("basic widget properties", () => {
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
});
test("support for callable expression in event handler", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.value"/><input type="text" t-on-input="obj.onInput"/></div>`;
state = useState({ value: "" });
obj = { onInput: (ev) => (this.state.value = ev.target.value) };
}
const counter = await mount(Counter, { target: fixture });
await nextTick();
expect(fixture.innerHTML).toBe(`<div><input type="text"></div>`);
const input = (<HTMLElement>counter.el).getElementsByTagName("input")[0];
input.value = "test";
input.dispatchEvent(new Event("input"));
await nextTick();
expect(fixture.innerHTML).toBe(`<div>test<input type="text"></div>`);
});
test("can handle empty props", async () => {
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
@@ -1228,6 +1246,30 @@ describe("composition", () => {
expect(fixture.innerHTML).toBe("<div>child b</div>");
});
test("can switch between dynamic components without the need for a t-key", async () => {
class A extends Component {
static template = xml`<span>child a</span>`;
}
class B extends Component {
static template = xml`<span>child b</span>`;
}
class App extends Component {
static template = xml`
<div>
<t t-component="{{state.child}}"/>
</div>`;
static components = { A, B };
state = useState({ child: "A" });
}
const app = await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe("<div><span>child a</span></div>");
app.state.child = "B";
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>child b</span></div>");
expect(QWeb.TEMPLATES[App.template].fn.toString()).toMatchSnapshot();
});
test("don't fallback to global registry if widget defined locally", async () => {
QWeb.registerComponent("WidgetB", WidgetB); // should not use this widget
env.qweb.addTemplate("ParentWidget", `<div><t t-component="WidgetB"/></div>`);
@@ -4022,6 +4064,30 @@ describe("dynamic t-props", () => {
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("t-props with props", async () => {
expect.assertions(1);
class Child extends Component {
static template = xml`<div />`;
setup() {
expect(this.props).toEqual({ a: 1, b: 2, c: "c" });
}
}
class Parent extends Component {
static template = xml`
<div>
<Child t-props="props" a="1" b="2" />
</div>
`;
static components = { Child };
props = { a: "a", c: "c" };
}
const widget = new Parent();
await widget.mount(fixture);
});
});
describe("support svg components", () => {
+88 -1
View File
@@ -1,4 +1,4 @@
import { Component, Env, STATUS } from "../../src/component/component";
import { Component, Env, mount, STATUS } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
@@ -520,4 +520,91 @@ describe("component error handling (catchError)", () => {
expect(error).toBeDefined();
expect(error.message).toBe("Cannot read property 'y' of undefined");
});
test("simple catchError", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Boom />
</t>
</div>`;
static components = { Boom };
error = false;
catchError(error) {
this.error = error;
this.render();
}
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div>Error</div>");
});
test("catchError in catchError", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
}
class Child extends Component {
static template = xml`
<div>
<Boom />
</div>`;
static components = { Boom };
catchError(error) {
throw error;
}
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Child />
</t>
</div>`;
static components = { Child };
error = false;
catchError(error) {
this.error = error;
this.render();
}
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe("<div>Error</div>");
});
test("errors in mounted and in willUnmount", async () => {
expect.assertions(1);
class Example extends Component {
static template = xml`<div/>`;
val;
mounted() {
throw new Error("Error in mounted");
this.val = { foo: "bar" };
}
willUnmount() {
console.log(this.val.foo);
}
}
try {
await mount(Example, { target: fixture });
} catch (e) {
expect(e.message).toBe("Error in mounted");
}
});
});
+252
View File
@@ -325,6 +325,70 @@ describe("t-slot directive", () => {
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("slots in t-foreach in t-foreach", async () => {
class Child extends Component {
static template = xml`
<div><t t-slot="default" /></div>
`;
}
class App extends Component {
static template = xml`
<div>
<t t-foreach="tree" t-as="node1" t-key="node1.key">
<div t-esc="node1.value" />
<ul>
<t t-foreach="node1.nodes" t-as="node2" t-key="node2.key">
<Child>
<li t-esc="node1.value" />
</Child>
</t>
</ul>
</t>
</div>`;
static components = { Child };
tree = [
{
key: "a",
value: "A",
nodes: [
{
key: "1",
value: "A-1",
},
{
key: "2",
value: "A-2",
},
],
},
{
key: "b",
value: "B",
nodes: [
{
key: "1",
value: "B-1",
},
{
key: "2",
value: "B-2",
},
],
},
];
}
await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe(
"<div><div>A</div><ul><div><li>A</li></div><div><li>A</li></div></ul><div>B</div><ul><div><li>B</li></div><div><li>B</li></div></ul></div>"
);
expect(env.qweb.templates[App.template].fn.toString()).toMatchSnapshot();
});
test("refs are properly bound in slots", async () => {
class Dialog extends Component {
static template = xml`<span><t t-slot="footer"/></span>`;
@@ -403,6 +467,48 @@ describe("t-slot directive", () => {
expect(QWeb.slots["1_default"].toString()).toMatchSnapshot();
});
test("default slot next to named slot, with default content", async () => {
class Dialog extends Component {
// We're using 2 slots here: a "default" one and a "footer",
// both having default children nodes.
static template = xml`
<div class="Dialog">
<div class="content">
<t t-slot="default">
Default content
</t>
</div>
<div class="footer">
<t t-slot="footer">
Default footer
</t>
</div>
</div>
`;
}
class App extends Component {
// Here we're trying to assign the "footer" slot with some content
static components = { Dialog };
static template = xml`
<div>
<Dialog>
<t t-set-slot="footer">
Overridden footer
</t>
</Dialog>
</div>
`;
}
await mount(App, { target: fixture });
expect(fixture.innerHTML).toBe(
'<div><div class="Dialog"><div class="content"> Default content </div><div class="footer"> Overridden footer </div></div></div>'
);
expect(QWeb.TEMPLATES[App.template].fn.toString()).toMatchSnapshot();
});
test("multiple roots are allowed in a named slot", async () => {
env.qweb.addTemplates(`
<templates>
@@ -1118,4 +1224,150 @@ describe("t-slot directive", () => {
expect(env.qweb.templates[Toggler.template].fn.toString()).toMatchSnapshot();
});
test("t-slot within dynamic t-call", async () => {
let child;
class Child extends Component {
static template = xml`<div class="child"/>`;
constructor(...args) {
super(...args);
child = this;
}
}
class Slotted extends Component {
static template = xml`<div class="slotted"><t t-slot="default" /></div>`;
}
class UsingTcallInSlotted extends Component {
tcallTemplate = xml`<div class="slot"><Child/></div>`;
static template = xml`
<div>
<Slotted>
<t t-call="{{ tcallTemplate }}"/>
</Slotted>
</div>`;
static components = { Slotted, Child };
}
await mount(UsingTcallInSlotted, { target: fixture });
expect(child.__owl__.parent).toBeInstanceOf(Slotted);
expect(fixture.innerHTML).toBe(
`<div><div class="slotted"><div class="slot"><div class="child"></div></div></div></div>`
);
});
test("t-slot scope context", async () => {
expect.assertions(1);
class Wrapper extends Component {
static template = xml`<t t-slot="default"/>`;
}
let dialog;
class Dialog extends Component {
static template = xml`
<Wrapper>
<div t-on-click="onClick">
<t t-slot="default" />
</div>
</Wrapper>
`;
static components = { Wrapper };
setup() {
dialog = this;
}
onClick(ev) {
// we do not use expect(this).toBe(dialog) here because if it fails, it
// may blow up jest because it then tries to compute a diff, which is
// infinite if there is a cycle
expect(this === dialog).toBe(true);
}
}
class Parent extends Component {
static template = xml`
<Dialog>
<button>The Button</button>
</Dialog>`;
static components = { Dialog };
}
await mount(Parent, { target: fixture });
document.querySelector("button").click();
await nextTick();
});
test("t-slot in recursive templates", async () => {
QWeb.registerTemplate(
"_test_recursive_template",
`
<Wrapper>
<t t-esc="name" />
<t t-foreach="items" t-as="item">
<t t-if="!item.children.length">
<t t-esc="item.name" />
</t>
<t t-else="" t-call="_test_recursive_template">
<t t-set="name" t-value="item.name" />
<t t-set="items" t-value="item.children" />
</t>
</t>
</Wrapper>`
);
class Wrapper extends Component {
static template = xml`
<wrapper>
<t t-slot="default"/>
</wrapper>`;
}
class Parent extends Component {
static template = "_test_recursive_template";
static components = { Wrapper };
name = "foo";
items = [
{
name: "foo-0",
children: [
{ name: "foo-00", children: [] },
{
name: "foo-01",
children: [
{ name: "foo-010", children: [] },
{ name: "foo-011", children: [] },
{
name: "foo-012",
children: [
{ name: "foo-0120", children: [] },
{ name: "foo-0121", children: [] },
{ name: "foo-0122", children: [] },
],
},
],
},
{ name: "foo-02", children: [] },
],
},
{ name: "foo-1", children: [] },
{ name: "foo-2", children: [] },
];
}
await mount(Parent, { target: fixture });
expect(fixture.innerHTML).toBe(
"<wrapper>foo<wrapper>foo-0foo-00<wrapper>foo-01foo-010foo-011<wrapper>foo-012foo-0120foo-0121foo-0122</wrapper></wrapper>foo-02</wrapper>foo-1foo-2</wrapper>"
);
});
});
+102 -16
View File
@@ -7,7 +7,7 @@ exports[`attributes class and t-att-class should combine together 1`] = `
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toObj(scope['value']);
let _1 = utils.toClassObj(scope['value']);
Object.assign(_1, {'hello':true})
let c3 = [], p3 = {key:3,class:_1};
let vn3 = h('div', p3, c3);
@@ -74,7 +74,7 @@ exports[`attributes dynamic class attribute 1`] = `
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toObj(scope['c']);
let _1 = utils.toClassObj(scope['c']);
let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2);
return vn2;
@@ -88,7 +88,7 @@ exports[`attributes dynamic empty class attribute 1`] = `
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toObj(scope['c']);
let _1 = utils.toClassObj(scope['c']);
let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2);
return vn2;
@@ -195,7 +195,7 @@ exports[`attributes from object variables set previously 1`] = `
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
scope.o = {a:'b'};
let _2 = utils.toObj(scope.o.a);
let _2 = utils.toClassObj(scope.o.a);
let c3 = [], p3 = {key:3,class:_2};
let vn3 = h('span', p3, c3);
c1.push(vn3);
@@ -213,7 +213,7 @@ exports[`attributes from variables set previously 1`] = `
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
scope.abc = 'def';
let _2 = utils.toObj(scope.abc);
let _2 = utils.toClassObj(scope.abc);
let c3 = [], p3 = {key:3,class:_2};
let vn3 = h('span', p3, c3);
c1.push(vn3);
@@ -288,7 +288,7 @@ exports[`attributes t-att-class and class should combine together 1`] = `
let scope = Object.create(context);
let h = this.h;
let _2 = {'hello':true};
Object.assign(_2, utils.toObj(scope['value']))
Object.assign(_2, utils.toClassObj(scope['value']))
let c3 = [], p3 = {key:3,class:_2};
let vn3 = h('div', p3, c3);
return vn3;
@@ -303,7 +303,7 @@ exports[`attributes t-att-class with object 1`] = `
let scope = Object.create(context);
let h = this.h;
let _2 = {'static':true};
Object.assign(_2, utils.toObj({a:scope['b'],c:scope['d'],e:scope['f']}))
Object.assign(_2, utils.toClassObj({a:scope['b'],c:scope['d'],e:scope['f']}))
let c3 = [], p3 = {key:3,class:_2};
let vn3 = h('div', p3, c3);
return vn3;
@@ -1073,6 +1073,39 @@ exports[`special cases for some specific html attributes/properties input type=
}"
`;
exports[`special cases for some specific html attributes/properties select with t-att-value 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let h = this.h;
let expr1 = scope['value'];
let c3 = [], p3 = {key:3,attrs:{value: expr1},props:{value: expr1}};
let vn3 = h('select', p3, c3);
p3.hook = {
create: (_, n) => {
n.elm.value=expr1;
},
};
let _4 = 'potato';
let c5 = [], p5 = {key:5,attrs:{value: _4}};
let vn5 = h('option', p5, c5);
c3.push(vn5);
c5.push({text: \`Potato\`});
let _6 = 'tomato';
let c7 = [], p7 = {key:7,attrs:{value: _6}};
let vn7 = h('option', p7, c7);
c3.push(vn7);
c7.push({text: \`Tomato\`});
let _8 = 'onion';
let c9 = [], p9 = {key:9,attrs:{value: _8}};
let vn9 = h('option', p9, c9);
c3.push(vn9);
c9.push({text: \`Onion\`});
return vn3;
}"
`;
exports[`special cases for some specific html attributes/properties various boolean html attributes 1`] = `
"function anonymous(context, extra
) {
@@ -1203,6 +1236,55 @@ exports[`static templates empty div 1`] = `
}"
`;
exports[`static templates inline template string in t-esc 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let _1 = \`text\`;
if (_1 != null) {
let vn2 = {text: _1};
result = vn2
}
return result;
}"
`;
exports[`static templates inline template string with content in t-esc 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
scope.v = 1;
let _1 = \`text\${scope.v}\`;
if (_1 != null) {
let vn2 = {text: _1};
result = vn2
}
return result;
}"
`;
exports[`static templates inline template string with variable in context 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let _1 = \`text \${scope['v']}\`;
if (_1 != null) {
let vn2 = {text: _1};
result = vn2
}
return result;
}"
`;
exports[`static templates properly handle comments 1`] = `
"function anonymous(context, extra
) {
@@ -1514,6 +1596,7 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
) {
// Template name: \\"recursive\\"
let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c3 = extra.parentNode;
@@ -1530,7 +1613,7 @@ exports[`t-call (template calling recursive template, part 1 2`] = `
scope = Object.create(scope);
scope.__access_mode__ = 'ro';
let k7 = \`__7__\${key0}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c4, parent: utils.getComponent(context), key: k7}));
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c4, parent: parent, key: k7}));
scope = _origScope6;
}
}"
@@ -1566,6 +1649,7 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
) {
// Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c2 = extra.parentNode;
@@ -1607,7 +1691,7 @@ exports[`t-call (template calling recursive template, part 2 2`] = `
scope[utils.zero] = c__0;
}
let k11 = \`__11__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k11}));
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: parent, key: k11}));
}
scope = _origScope10;
}
@@ -1645,6 +1729,7 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
) {
// Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c2 = extra.parentNode;
@@ -1686,7 +1771,7 @@ exports[`t-call (template calling recursive template, part 3 2`] = `
scope[utils.zero] = c__0;
}
let k11 = \`__11__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k11}));
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: parent, key: k11}));
}
scope = _origScope10;
}
@@ -1725,6 +1810,7 @@ exports[`t-call (template calling recursive template, part 4: with t-set recursi
) {
// Template name: \\"nodeTemplate\\"
let utils = this.constructor.utils;
let parent = extra.parent;
let scope = Object.create(context);
let h = this.h;
let c2 = extra.parentNode;
@@ -1771,7 +1857,7 @@ exports[`t-call (template calling recursive template, part 4: with t-set recursi
scope[utils.zero] = c__0;
}
let k11 = \`__11__\${key0}__\${key1}__\`;
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: utils.getComponent(context), key: k11}));
this.constructor.subTemplates['1'].call(this, scope, Object.assign({}, extra, {parentNode: c3, parent: parent, key: k11}));
}
scope = _origScope10;
}
@@ -2946,7 +3032,7 @@ exports[`t-on t-on with empty handler (only modifiers) 1`] = `
let c2 = [], p2 = {key:2,on:{}};
let vn2 = h('button', p2, c2);
c1.push(vn2);
p2.on['click'] = function (e) {if (context.__owl__.status === 5){return}e.preventDefault();};
p2.on['click'] = function (e) {if (context.__owl__.status === 5){return}e.preventDefault();const res = (() => { return })(); if (typeof res === 'function') { res(e) }};
c2.push({text: \`Button\`});
return vn1;
}"
@@ -2961,7 +3047,7 @@ exports[`t-on t-on with inline statement (function call) 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
const state_2 = scope['state'];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}state_2.incrementCounter(2)};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.incrementCounter(2) })(); if (typeof res === 'function') { res(e) }};
c1.push({text: \`Click\`});
return vn1;
}"
@@ -2976,7 +3062,7 @@ exports[`t-on t-on with inline statement 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
const state_2 = scope['state'];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}state_2.counter++};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.counter++ })(); if (typeof res === 'function') { res(e) }};
c1.push({text: \`Click\`});
return vn1;
}"
@@ -2991,7 +3077,7 @@ exports[`t-on t-on with inline statement, part 2 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
const state_2 = scope['state'];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}state_2.flag=!state_2.flag};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.flag=!state_2.flag })(); if (typeof res === 'function') { res(e) }};
c1.push({text: \`Toggle\`});
return vn1;
}"
@@ -3007,7 +3093,7 @@ exports[`t-on t-on with inline statement, part 3 1`] = `
let vn1 = h('button', p1, c1);
const state_2 = scope['state'];
const someFunction_2 = scope['someFunction'];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}state_2.n=someFunction_2(3)};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.n=someFunction_2(3) })(); if (typeof res === 'function') { res(e) }};
c1.push({text: \`Toggle\`});
return vn1;
}"
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`qweb t-att t-att-class with multiple classes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toClassObj({'a b c':scope['value']});
let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2);
return vn2;
}"
`;
exports[`qweb t-att t-att-class with multiple classes 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _3 = utils.toClassObj({['a b c']:scope['value']});
let c4 = [], p4 = {key:4,class:_3};
let vn4 = h('div', p4, c4);
return vn4;
}"
`;
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _1 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
let c2 = [], p2 = {key:2,class:_1};
let vn2 = h('div', p2, c2);
return vn2;
}"
`;
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _3 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
let c4 = [], p4 = {key:4,class:_3};
let vn4 = h('div', p4, c4);
return vn4;
}"
`;
+58
View File
@@ -31,6 +31,21 @@ describe("static templates", () => {
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
});
test("inline template string in t-esc", () => {
qweb.addTemplate("test", '<t><t t-esc="`text`"/></t>');
expect(renderToString(qweb, "test")).toBe("text");
});
test("inline template string with content in t-esc", () => {
qweb.addTemplate("test", '<t><t t-set="v" t-value="1"/><t t-esc="`text${v}`"/></t>');
expect(renderToString(qweb, "test")).toBe("text1");
});
test("inline template string with variable in context", () => {
qweb.addTemplate("test", '<t><t t-esc="`text ${v}`"/></t>');
expect(renderToString(qweb, "test", { v: "from context" })).toBe("text from context");
});
test("simple string, with some dynamic value", () => {
qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
@@ -1931,6 +1946,49 @@ describe("special cases for some specific html attributes/properties", () => {
let elm = vnode2.elm as HTMLInputElement;
expect(elm.indeterminate).toBe(true);
});
test("textarea with t-att-value", () => {
// render input with initial value
qweb.addTemplate("test", `<textarea t-att-value="v"/>`);
const vnode1 = qweb.render("test", { v: "zucchini" });
const vnode2 = patch(document.createElement("textarea"), vnode1);
let elm = vnode2.elm as HTMLInputElement;
expect(elm.value).toBe("zucchini");
// change value manually in textarea, to simulate user textarea
elm.value = "tomato";
expect(elm.value).toBe("tomato");
// rerender with a different value, and patch actual dom, to check that
// textarea value was properly reset by owl
const vnode3 = qweb.render("test", { v: "potato" });
patch(vnode2, vnode3);
expect(elm.value).toBe("potato");
});
test("select with t-att-value", () => {
const template = `
<select t-att-value="value">
<option value="potato">Potato</option>
<option value="tomato">Tomato</option>
<option value="onion">Onion</option>
</select>`;
qweb.addTemplate("test", template);
const vnode1 = qweb.render("test", { value: "tomato" });
const vnode2 = patch(document.createElement("select"), vnode1);
let elm = vnode2.elm as HTMLSelectElement;
expect(elm.value).toBe("tomato");
elm.value = "potato";
expect(elm.value).toBe("potato");
// rerender with a different value, and patch actual dom, to check that
// select value was properly reset by owl
const vnode3 = qweb.render("test", { value: "onion" });
patch(vnode2, vnode3);
expect(elm.value).toBe("onion");
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
});
});
describe("whitespace handling", () => {
+23
View File
@@ -200,4 +200,27 @@ describe("expression evaluation", () => {
test("works with builtin properties", () => {
expect(compileExpr("state.constructor.name", {})).toBe("scope['state'].constructor.name");
});
test("works with shortcut object key description", () => {
expect(compileExpr("{a}", {})).toBe("{a:scope['a']}");
expect(compileExpr("{a,b}", {})).toBe("{a:scope['a'],b:scope['b']}");
expect(compileExpr("{a,b:3,c}", {})).toBe("{a:scope['a'],b:3,c:scope['c']}");
});
test("works with short object description and lists ", () => {
expect(compileExpr("[a, b]", {})).toBe("[scope['a'],scope['b']]");
expect(compileExpr("[a, b, c]", {})).toBe("[scope['a'],scope['b'],scope['c']]");
expect(compileExpr("[a, {b, c},d]", {})).toBe(
"[scope['a'],{b:scope['b'],c:scope['c']},scope['d']]"
);
expect(compileExpr("{a:[b, {c, d: e}]}", {})).toBe(
"{a:[scope['b'],{c:scope['c'],d:scope['e']}]}"
);
});
test("template strings", () => {
expect(compileExpr("`hey`", {})).toBe("`hey`");
expect(compileExpr("`hey ${you}`", {})).toBe("`hey ${scope['you']}`");
expect(compileExpr("`hey ${1 + 2}`", {})).toBe("`hey ${1+2}`");
});
});
+36
View File
@@ -0,0 +1,36 @@
import { QWeb } from "../../src/qweb/index";
import { renderToString } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
function render(template, context = {}) {
const qweb = new QWeb();
qweb.addTemplate("test", template);
return renderToString(qweb, "test", context);
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("qweb t-att", () => {
test("t-att-class with multiple classes", () => {
expect(render(`<div t-att-class="{'a b c': value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
expect(render(`<div t-att-class="{['a b c']: value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
});
test("t-att-class with multiple classes, some of which are duplicate", () => {
expect(render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: true })).toBe(
'<div class="a b c"></div>'
);
expect(
render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: false })
).toBe('<div class="a b d"></div>');
});
});
+1 -1
View File
@@ -7,7 +7,7 @@ exports[`Link component can render simple cases 1`] = `
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let _5 = utils.toObj({'router-link-active':scope['isActive']});
let _5 = utils.toClassObj({'router-link-active':scope['isActive']});
let _6 = scope['href'];
let c7 = [], p7 = {key:7,attrs:{href: _6},class:_5,on:{}};
let vn7 = h('a', p7, c7);
@@ -18,7 +18,7 @@ exports[`RouteComponent can render simple cases 1`] = `
let w4 = k5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k5]] : false;
let vn6 = {};
result = vn6;
let props4 = Object.assign({}, scope['env'].router.currentParams);
let props4 = Object.assign({}, scope['env'].router.currentParams, {});
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
w4.destroy();
w4 = false;
+12
View File
@@ -60,6 +60,13 @@ describe("router miscellaneous", () => {
await router.navigate({ to: "users", params: { id: 3 } });
expect(window.location.href).toBe("http://localhost/test.html#/users/3");
});
test("navigate using path and query string should preserve query string", async () => {
router = new TestRouter(env, [{ name: "users", path: "/users/{{id}}" }]);
await router.navigate({ path: "/users/3?test=1" });
expect(window.location.pathname).toBe("/users/3");
expect(window.location.search).toBe("?test=1");
});
});
describe("routeToPath", () => {
@@ -117,6 +124,11 @@ describe("getRouteParams", () => {
expect(getRouteParams({ path: "*" }, "somepath")).toEqual({});
});
test("properly match routes with query params", () => {
expect(getRouteParams({ path: "/home" }, "/home?test=1")).toEqual({});
expect(getRouteParams({ path: "/home" }, "/home?test1=1&test2=2")).toEqual({});
});
test("properly match simple routes, mode hash", () => {
// simple route
expect(getRouteParams({ path: "/home" }, "#/home")).toEqual({});
+1 -1
View File
@@ -72,7 +72,7 @@ if __name__ == "__main__":
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`');
// create iframe
+2 -1
View File
@@ -79,7 +79,8 @@ async function startRelease() {
// ---------------------------------------------------------------------------
log(`Step 4/${STEPS}: creating git commit...`);
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${content}"`);
const escapedContent = content.replace(/\"/g, '\\\"').replace(/\`/g, '\\\`');
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${escapedContent}"`);
if (gitResult !== 0) {
logError("Git commit failed. Aborting.");
return;