Compare commits

..

2 Commits

Author SHA1 Message Date
Lucas Perais (lpe) b3fe8d9966 wip 2019-12-12 17:30:22 +01:00
Géry Debongnie f6634df72c wip 2019-12-12 16:40:31 +01:00
46 changed files with 1365 additions and 8678 deletions
+2 -8
View File
@@ -102,16 +102,10 @@ Submit a PR!
## Installing/Building
Owl can be installed with the following command:
```
npm install @odoo/owl
```
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.3.js](https://github.com/odoo/owl/releases/download/v1.0.3/owl.js)
- [owl-1.0.3.min.js](https://github.com/odoo/owl/releases/download/v1.0.3/owl.min.js)
- [owl-1.0.0-beta2.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.js)
- [owl-1.0.0-beta2.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.min.js)
Some npm scripts are available:
+9 -9
View File
@@ -56,15 +56,15 @@ useState Link
config RouteComponent
mode Router
core tags
EventBus css
Observer xml
hooks utils
onWillStart debounce
onMounted escape
onWillUpdateProps loadJS
onWillPatch loadFile
onPatched shallowEqual
onWillUnmount whenReady
EventBus xml
Observer utils
hooks debounce
onWillStart escape
onMounted loadJS
onWillUpdateProps loadFile
onWillPatch shallowEqual
onPatched whenReady
onWillUnmount
useContext
useState
useRef
+1 -5
View File
@@ -225,10 +225,6 @@ to be called in the constructor.
}
```
- **`style`** (string, optional): it should be the return value of the [`css` tag](tags.md#css-tag),
which is used to inject stylesheet whenever the component is visible on the
screen.
There is another static property defined on the `Component` class: `current`.
This property is set to the currently being defined component (in the constructor).
This is the way [hooks](hooks.md) are able to get a reference to the target
@@ -425,7 +421,7 @@ likely via a change in its state/props or environment).
This method is not called on the initial render. It is useful to interact
with the DOM (for example, through an external library) whenever the
component was patched. Note that this hook will not be called if the component is
component was patched. Note that this hook will not be called if the compoent is
not in the DOM.
Updating the component state in this hook is possible, but not encouraged.
+2 -2
View File
@@ -23,7 +23,7 @@ a rendering that is no longer relevant, restart it, reuse it in some cases.
But even though using concurrency is quite simple (and is the default behaviour),
asynchrony is difficult, because it introduces an additional dimension that
vastly increase the complexity of an application. This section will explain
how Owl manages this complexity, how concurrent rendering works in a general way.
how Owl manages this complexity, how concuurent rendering works in a general way.
## Rendering Components
@@ -120,7 +120,7 @@ Here is what Owl will do:
1. hook `willUpdateProps` is called on `D` (async)
2. template `D` is rerendered
- component `F` is created:
1. hook `willStart` is called on `F` (async)
1. hook `willStart` is called on `E` (async)
2. template `F` is rendered
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
+2 -2
View File
@@ -40,8 +40,8 @@ all templates.
Whenever a root component `App` is mounted, Owl will setup a valid environment by
following the next steps:
- take the `env` object defined on `App.env` (if no `env` was explicitly setup,
this will return the empty `env` object defined on `Component`)
- take the `env` object defined on `App.env` (if no `env` was explicitely setup,
this will be return the empty `env` object defined on `Component`)
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
The correct way to customize an environment is to simply set it up on the root
-12
View File
@@ -17,7 +17,6 @@
- [`useContext`](#usecontext)
- [`useRef`](#useref)
- [`useSubEnv`](#usesubenv)
- [`useExternalListener`](#useexternallistener)
- [`useStore`](#usestore)
- [`useDispatch`](#usedispatch)
- [`useGetters`](#usegetters)
@@ -355,17 +354,6 @@ will be added to the parent environment. Note that it will extend, not replace
the parent environment. And of course, the parent environment will not be
affected.
### `useExternalListener`
The `useExternalListener` hook helps solve a very common problem: adding and removing
a listener on some target whenever a component is mounted/unmounted. For example,
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
to be closed:
```js
useExternalListener(window, "click", this.closeMenu);
```
### `useStore`
The `useStore` hook is the entry point for a component to connect to the store.
+4 -134
View File
@@ -4,19 +4,16 @@
- [Overview](#overview)
- [`xml` tag](#xml-tag)
- [`css` tag](#css-tag)
## Overview
Tags are very small helpers intended to make it easy to write inline templates
or styles. There are currently two tags: `css` and `xml`. With these functions,
it is possible to write [single file components](../tooling.md#single-file-component).
Tags are very small helpers to make it easy to write inline templates. There is
only one currently available tag: `xml`, but we plan to add other tags later,
such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component).
## XML tag
The `xml` tag is certainly the most useful tag. It is used to define an inline
QWeb template for a component. Without tags, creating a standalone component
would look like this:
Without tags, creating a standalone component would look like this:
```js
import { Component } from 'owl'
@@ -55,130 +52,3 @@ class MyComponent extends Component {
...
}
```
## CSS tag
The CSS tag is useful to define a css stylesheet in the javascript file:
```js
class MyComponent extends Component {
static template = xml`
<div class="my-component">some template</div>
`;
static css`
.my-component {
color: red;
}
`;
}
```
The `css` tag registers internally the css information. Then, whenever the first
instance of the component is created, will add a `<style>` tag to the document
`<head>`.
Note that to make it more useful, like other css preprocessors, the `css` tag
accepts a small extension of the css specification: css scopes can be nested,
and the rules will then be expanded by the `css` helper:
```scss
.my-component {
display: block;
.sub-component h {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component .sub-component h {
color: red;
}
```
This extension brings another useful feature: the `&` selector which refers to
the parent selector. For example, we want our component to be red when hovered.
We would like to write something like:
```scss
.my-component {
display: block;
:hover {
color: red;
}
}
```
but it will be formatted as:
```css
.my-component {
display: block;
}
.my-component :hover {
color: red;
}
```
The `&` selector can be used to solve this problem:
```scss
.my-component {
display: block;
&:hover {
color: red;
}
}
```
will be formatted as:
```css
.my-component {
display: block;
}
.my-component:hover {
color: red;
}
```
Now, there is no additional processing done by the `css` tag. However, since it
is done in javascript at runtime, we actually have more power. For example:
1. sharing values between javascript and css:
```js
import { theme } from "./theme";
class MyComponent extends Component {
static template = xml`<div class="my-component">...</div>`;
static style = css`
.my-component {
color: ${theme.MAIN_COLOR};
background-color: ${theme.SECONDARY_color};
}
`;
}
```
2. scoping rules to the current component:
```js
import { generateUUID } from "./utils";
const uuid = generateUUID();
class MyComponent extends Component {
static template = xml`<div data-o-${uuid}="">...</div>`;
static style = css`
[data-o-${uuid}] {
color: red;
}
`;
}
```
+10 -21
View File
@@ -43,43 +43,32 @@ useful to compare various performance metrics on some tasks.
It is very useful to group code by feature instead of by type of file. It makes
it easier to scale application to larger size.
To do so, Owl has two small helpers that make it easy to define a
template or a stylesheet inside a javascript (or typescript) file: the
[`xml`](reference/tags.md#xml-tag) and [`css`](reference/tags.md#css-tag)
helper.
To do so, Owl currently has a small helper that makes it easy to define a
template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
helper. With this, a template is automatically registered to [QWeb](reference/qweb_engine.md).
This means that the template, the style and the javascript code can be defined in
the same file. For example:
This means that the template and the javascript code can be defined in the same
file. It is not currently possible to add css to the same file, but Owl may
get a `css` tag helper later.
```js
const { Component } = owl;
const { xml, css } = owl.tags;
const { xml } = owl.tags;
// -----------------------------------------------------------------------------
// TEMPLATE
// -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ `
<div class="main">
<div class="main two-columns">
<Sidebar/>
<Content />
</div>`;
// -----------------------------------------------------------------------------
// STYLE
// -----------------------------------------------------------------------------
const STYLE = css/* css */ `
.main {
display: grid;
grid-template-columns: 200px auto;
}
`;
// -----------------------------------------------------------------------------
// CODE
// -----------------------------------------------------------------------------
class Main extends Component {
class MyComponent extends Component {
static template = TEMPLATE;
static style = STYLE;
static components = { Sidebar, Content };
// rest of component...
@@ -102,7 +91,7 @@ logging useful information is extremely valuable. There is a [javascript file](.
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
```
function debugOwl(t,e){let n,o="[OWL_DEBUG]";function r(t){let e;try{e=JSON.stringify(t||{})}catch(t){e="<JSON error>"}return e.length>200&&(e=e.slice(0,200)+"..."),e}if(Object.defineProperty(t.Component,"current",{get:()=>n,set(s){n=s;const i=s.constructor.name;if(e.componentBlackList&&e.componentBlackList.test(i))return;if(e.componentWhiteList&&!e.componentWhiteList.test(i))return;let l;Object.defineProperty(n,"__owl__",{get:()=>l,set(n){!function(n,s,i){let l=`${s}<id=${i}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!e.methodBlackList||!e.methodBlackList.includes(t))&&!(e.methodWhiteList&&!e.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${r(n.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${r(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=n.__render.bind(n);n.__render=function(...t){c("rendering template"),d(...t)};const h=n.render.bind(n);n.render=function(...t){const e=n.__owl__;let o="render";return e.isMounted||e.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=n.mount.bind(n);n.mount=function(...t){return c("mount"),p(...t)}}(s,i,(l=n).id)}})}}),e.logScheduler){let e=t.Component.scheduler.start,n=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),e.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),n.call(this)}}if(e.logStore){let e=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...n){return console.log(`${o} store: action '${t}' dispatched. Payload: '${r(n)}'`),e.call(this,t,...n)}}}
function debugOwl(t,n){let e,o="[OWL_DEBUG]";function s(t){let n=JSON.stringify(t||{});return n.length>200&&(n=n.slice(0,200)+"..."),n}if(Object.defineProperty(t.Component,"current",{get:()=>e,set(i){e=i;const r=i.constructor.name;if(n.componentBlackList&&n.componentBlackList.test(r))return;if(n.componentWhiteList&&!n.componentWhiteList.test(r))return;let l;Object.defineProperty(e,"__owl__",{get:()=>l,set(e){!function(e,i,r){let l=`${i}<id=${r}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!n.methodBlackList||!n.methodBlackList.includes(t))&&!(n.methodWhiteList&&!n.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${s(e.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${s(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=e.__render.bind(e);e.__render=function(...t){c("rendering template"),d(...t)};const h=e.render.bind(e);e.render=function(...t){const n=e.__owl__;let o="render";return n.isMounted||n.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=e.mount.bind(e);e.mount=function(...t){return c("mount"),p(...t)}}(i,r,(l=e).id)}})}}),n.logScheduler){let n=t.Component.scheduler.start,e=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),n.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),e.call(this)}}if(n.logStore){let n=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...e){return console.log(`${o} store: action '${t}' dispatched. Payload: '${s(e)}'`),n.call(this,t,...e)}}}
debugOwl(owl, {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
+4 -7
View File
@@ -1,10 +1,8 @@
{
"name": "@odoo/owl",
"version": "1.0.3",
"name": "owl-framework",
"version": "1.0.0-beta2",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.js",
"types": "dist/types/index.d.ts",
"files": ["dist/types/", "dist/owl.js"],
"main": "src/index.ts",
"engines": {
"node": ">=10.15.3"
},
@@ -20,8 +18,7 @@
"tools": "npm run build && npm run tools:serve",
"pretools:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"publish": "npm run buildcommonjs && npm publish"
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write"
},
"repository": {
"type": "git",
+2 -2
View File
@@ -1,7 +1,7 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.0.3
- Status: stable
- Current version: 1.0.0-beta2
- Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may
change!
-18
View File
@@ -6,7 +6,6 @@ import "./directive";
import { Fiber } from "./fiber";
import "./props_validation";
import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles";
/**
* Owl Component System
@@ -200,9 +199,6 @@ export class Component<T extends Env, Props extends {}> {
refs: null,
scope: null
};
if (constr.style) {
this.__applyStyles(constr);
}
}
/**
@@ -584,20 +580,6 @@ export class Component<T extends Env, Props extends {}> {
return fiber;
}
/**
* Apply the stylesheets defined by the component. Note that we need to make
* sure all inherited stylesheets are applied as well. We then delete the
* `style` key from the constructor to make sure we do not apply it again.
*/
private __applyStyles(constr) {
while (constr && constr.style) {
if (constr.hasOwnProperty("style")) {
activateSheet(constr.style, constr.name);
delete constr.style;
}
constr = constr.__proto__;
}
}
__getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) {
+1 -1
View File
@@ -211,7 +211,7 @@ QWeb.addDirective({
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
props[name] = ctx.formatExpression(value) || "undefined";
props[name] = ctx.formatExpression(value);
}
}
}
-70
View File
@@ -1,70 +0,0 @@
/**
* Owl Style System
*
* This files contains the Owl code related to processing (extended) css strings
* and creating/adding <style> tags to the document head.
*/
export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {};
export function processSheet(str: string): string {
const tokens = str.split(/(\{|\}|;)/).map(s => s.trim());
const selectorStack: string[][] = [];
const parts: string[] = [];
let rules: string[] = [];
function generateSelector(stackIndex: number, parentSelector?: string) {
const parts: string[] = [];
for (const selector of selectorStack[stackIndex]) {
let part = (parentSelector && parentSelector + " " + selector) || selector;
if (part.includes("&")) {
part = selector.replace(/&/g, parentSelector || "");
}
if (stackIndex < selectorStack.length - 1) {
part = generateSelector(stackIndex + 1, part);
}
parts.push(part);
}
return parts.join(", ");
}
function generateRules() {
if (rules.length) {
parts.push(generateSelector(0) + " {");
parts.push(...rules);
parts.push("}");
rules = [];
}
}
while (tokens.length) {
let token = tokens.shift()!;
if (token === "}") {
generateRules();
selectorStack.pop();
} else {
if (tokens[0] === "{") {
generateRules();
selectorStack.push(token.split(/\s*,\s*/));
tokens.shift();
}
if (tokens[0] === ";") {
rules.push(" " + token + ";");
}
}
}
return parts.join("\n");
}
export function registerSheet(id: string, css: string) {
const sheet = document.createElement("style");
sheet.innerHTML = processSheet(css);
STYLESHEETS[id] = sheet;
}
export function activateSheet(id, name) {
const sheet = STYLESHEETS[id];
if (!sheet) {
throw new Error(
`Invalid css stylesheet for component '${name}'. Did you forget to use the 'css' tag helper?`
);
}
sheet.setAttribute("component", name);
document.head.appendChild(sheet);
}
+8
View File
@@ -29,6 +29,14 @@ export class Observer {
// fun fact: typeof null === 'object'
return value;
}
if (
value instanceof Map ||
value instanceof WeakMap ||
value instanceof Set ||
value instanceof WeakSet
) {
return value;
}
let metadata = this.weakMap.get(value) || this._observe(value, parent);
return metadata.proxy;
}
-24
View File
@@ -131,27 +131,3 @@ export function useSubEnv(nextEnv) {
const component = Component.current!;
component.env = Object.assign(Object.create(component.env), nextEnv);
}
// -----------------------------------------------------------------------------
// useExternalListener
// -----------------------------------------------------------------------------
/**
* When a component needs to listen to DOM Events on element(s) that are not
* part of his hierarchy, we can use the `useExternalListener` hook.
* It will correctly add and remove the event listener, whenever the
* component is mounted and unmounted.
*
* Example:
* a menu needs to listen to the click on window to be closed automatically
*
* Usage:
* in the constructor of the OWL component that needs to be notified,
* `useExternalListener(window, 'click', this._doSomething);`
* */
export function useExternalListener(target: HTMLElement, eventName: string, handler, eventParams?) {
const boundHandler = handler.bind(Component.current);
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
}
+17 -31
View File
@@ -1,7 +1,6 @@
import { CompilationContext } from "./compilation_context";
import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom";
import { QWebVar } from "./expression_parser";
/**
* Owl QWeb Directives
@@ -40,11 +39,11 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
let exprID: string;
if (typeof value === "string") {
exprID = `_${ctx.generateID()}`;
ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`);
ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
} else {
exprID = `scope.${value.id}`;
}
ctx.addIf(`${exprID} != null`);
ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) {
let protectID;
@@ -62,7 +61,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`);
}
@@ -120,7 +119,7 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!;
ctx.variables[variable] = ctx.variables[variable] || ({} as QWebVar);
ctx.variables[variable] = ctx.variables[variable] || {};
let qwebvar = ctx.variables[variable];
const hasBody = node.hasChildNodes();
@@ -128,12 +127,7 @@ QWeb.addDirective({
qwebvar.expr = `scope.${variable}`;
if (value) {
const formattedValue = ctx.formatExpression(value);
let scopeExpr = `scope`;
if (ctx.protectedScopeNumber) {
ctx.rootContext.shouldDefineUtils = true;
scopeExpr = `utils.getScope(scope, '${variable}')`;
}
ctx.addLine(`${scopeExpr}.${variable} = ${formattedValue};`);
ctx.addLine(`${qwebvar.expr} = ${formattedValue};`);
qwebvar.value = formattedValue;
}
@@ -146,7 +140,7 @@ QWeb.addDirective({
const _parentNode = ctx.parentNode;
ctx.parentNode = tempParentNodeID;
ctx.addLine(`let c${tempParentNodeID} = new utils.VDomArray();`);
ctx.addLine(`const c${tempParentNodeID} = new utils.VDomArray();`);
const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) {
nodeCopy.removeAttribute(attr);
@@ -221,7 +215,6 @@ QWeb.addDirective({
// Step 1: sanity checks
// ------------------------------------------------
ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true;
if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')");
}
@@ -235,7 +228,7 @@ QWeb.addDirective({
// ------------------------------------------------
if (!qweb.subTemplates[subTemplate]) {
qweb.subTemplates[subTemplate] = true;
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true);
const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx);
qweb.subTemplates[subTemplate] = subTemplateFn;
}
@@ -269,17 +262,17 @@ QWeb.addDirective({
// Step 4: add the appropriate function call to current component
// ------------------------------------------------
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
const parentComponent = `utils.getComponent(context)`;
const key = ctx.generateTemplateKey();
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
if (ctx.parentNode) {
ctx.addLine(`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, ${extra});`);
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}, ${extra});`);
ctx.addLine(
`this.subTemplates['${subTemplate}'].call(this, ${callingScope}, Object.assign({}, extra, {parentNode: result}));`
);
ctx.addLine(`result = result[0]`);
}
@@ -308,17 +301,17 @@ QWeb.addDirective({
const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID();
ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID();
let valuesID = ctx.generateID();
ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf();
ctx.addLine(`let _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope(true);
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();
@@ -339,13 +332,6 @@ QWeb.addDirective({
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
);
}
if (nodeCopy.hasAttribute("t-key")) {
const expr = ctx.formatExpression(nodeCopy.getAttribute("t-key")!);
ctx.addLine(`let key${ctx.loopNumber} = ${expr};`);
nodeCopy.removeAttribute("t-key");
} else {
ctx.addLine(`let key${ctx.loopNumber} = i${ctx.loopNumber};`);
}
nodeCopy.removeAttribute("t-foreach");
qweb._compileNode(nodeCopy, ctx);
+19 -39
View File
@@ -1,4 +1,4 @@
import { compileExpr, compileExprToArray, QWebVar } from "./expression_parser";
import { compileExpr, QWebVar } from "./expression_parser";
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
//------------------------------------------------------------------------------
@@ -17,7 +17,6 @@ export class CompilationContext {
rootContext: CompilationContext;
shouldDefineParent: boolean = false;
shouldDefineScope: boolean = false;
protectedScopeNumber: number = 0;
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
shouldDefineRefs: boolean = false;
@@ -27,13 +26,12 @@ export class CompilationContext {
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
hasKey0: boolean = false;
keyStack: boolean[] = [];
currentKey: string = "";
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
this.addLine("let h = this.h;");
this.addLine("var h = this.h;");
}
generateID(): number {
@@ -50,15 +48,20 @@ export class CompilationContext {
*/
generateTemplateKey(prefix: string = ""): string {
const id = this.generateID();
if (this.loopNumber === 0 && !this.hasKey0) {
if (this.loopNumber === 0 && !this.currentKey) {
return `'${prefix}__${id}__'`;
}
let key = `\`${prefix}__${id}__`;
let start = this.hasKey0 ? 0 : 1;
for (let i = start; i < this.loopNumber + 1; i++) {
key += `\${key${i}}__`;
let locationExpr = `\`${prefix}__${id}__`;
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};`);
}
this.addLine(`let k${id} = ${key}\`;`);
return `k${id}`;
}
@@ -113,11 +116,11 @@ export class CompilationContext {
}
indent() {
this.rootContext.indentLevel++;
this.indentLevel++;
}
dedent() {
this.rootContext.indentLevel--;
this.indentLevel--;
}
addLine(line: string): number {
@@ -156,24 +159,6 @@ export class CompilationContext {
this.rootContext.shouldDefineScope = true;
return compileExpr(expr, this.variables);
}
captureExpression(expr: string): string {
this.rootContext.shouldDefineScope = true;
const argId = this.generateID();
const tokens = compileExprToArray(expr, this.variables);
const done = new Set();
return tokens
.map(tok => {
if (tok.varName) {
if (!done.has(tok.varName)) {
done.add(tok.varName);
this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`);
}
tok.value = `${tok.varName}_${argId}`;
}
return tok.value;
})
.join("");
}
/**
* Perform string interpolation on the given string. Note that if the whole
@@ -192,19 +177,14 @@ export class CompilationContext {
let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}");
return "`" + r + "`";
}
startProtectScope(codeBlock?: boolean): number {
startProtectScope(): number {
const protectID = this.generateID();
this.rootContext.protectedScopeNumber++;
this.rootContext.shouldDefineScope = true;
const scopeExpr = codeBlock
? `Object.create(scope);`
: `Object.assign(Object.create(context), scope);`;
this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = ${scopeExpr}`);
this.addLine(`const _origScope${protectID} = scope;`);
this.addLine(`scope = Object.assign(Object.create(context), scope);`);
return protectID;
}
stopProtectScope(protectID: number) {
this.rootContext.protectedScopeNumber--;
this.addLine(`scope = _origScope${protectID};`);
}
}
+3 -11
View File
@@ -66,7 +66,6 @@ interface Token {
value: string;
originalValue?: string;
size?: number;
varName?: string;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
@@ -82,7 +81,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
// note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;".split(",");
type Tokenizer = (expr: string) => Token | false;
@@ -235,7 +234,7 @@ export function tokenize(expr: string): Token[] {
* 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
*/
export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] {
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
scope = Object.create(scope);
const tokens = tokenize(expr);
for (let i = 0; i < tokens.length; i++) {
@@ -270,7 +269,6 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
}
if (isVar) {
token.varName = token.value;
if (token.value in scope && "id" in scope[token.value]) {
token.value = scope[token.value].expr!;
} else {
@@ -279,11 +277,5 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
}
}
}
return tokens;
}
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
return compileExprToArray(expr, scope)
.map(t => t.value)
.join("");
return tokens.map(t => t.value).join("");
}
+6 -18
View File
@@ -58,7 +58,7 @@ export function makeHandlerCode(
ctx.rootContext.shouldDefineUtils = true;
const comp = `utils.getComponent(context)`;
if (args) {
const argId = ctx.generateID();
let argId = ctx.generateID();
ctx.addLine(`let args${argId} = [${ctx.formatExpression(args)}];`);
code = `${comp}['${name}'](...args${argId}, e);`;
putInCache = false;
@@ -67,9 +67,8 @@ export function makeHandlerCode(
}
} else {
// if we get here, then it is an expression
// we need to capture every variable in it
putInCache = false;
code = ctx.captureExpression(value);
code = ctx.formatExpression(value);
}
const modCode = mods.map(mod => modcodes[mod]).join("");
let handler = `function (e) {if (!context.__owl__.isMounted){return}${modCode}${code}}`;
@@ -304,20 +303,9 @@ QWeb.addDirective({
QWeb.addDirective({
name: "key",
priority: 45,
atNodeEncounter({ ctx, value, node }) {
if (ctx.loopNumber === 0) {
ctx.keyStack.push(ctx.rootContext.hasKey0);
ctx.rootContext.hasKey0 = true;
}
ctx.addLine("{");
ctx.indent();
ctx.addLine(`let key${ctx.loopNumber} = ${ctx.formatExpression(value)};`);
},
finalize({ ctx }) {
ctx.dedent();
ctx.addLine("}");
if (ctx.loopNumber === 0) {
ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean;
}
atNodeEncounter({ ctx, value }) {
let id = ctx.generateID();
ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.currentKey = `nodeKey${id}`;
}
});
+16 -40
View File
@@ -86,10 +86,6 @@ interface Utils {
[key: string]: any;
}
function isComponent(obj) {
return obj && obj.hasOwnProperty("__owl__");
}
const UTILS: Utils = {
zero: Symbol("zero"),
toObj(expr) {
@@ -123,24 +119,13 @@ const UTILS: Utils = {
return vnode.text;
}
})
.join("");
.join();
},
getComponent(obj) {
while (obj && !isComponent(obj)) {
while (obj && !obj.hasOwnProperty("__owl__")) {
obj = obj.__proto__;
}
return obj;
},
getScope(obj, property: string) {
const obj0 = obj;
while (obj && !obj.hasOwnProperty(property)) {
const newObj = obj.__proto__;
if (!newObj || isComponent(newObj)) {
return obj0;
}
obj = newObj;
}
return obj;
}
};
@@ -398,12 +383,7 @@ export class QWeb extends EventBus {
});
}
_compile(
name: string,
elem: Element,
parentContext?: CompilationContext,
defineKey?: boolean
): CompiledTemplate {
_compile(name: string, elem: Element, parentContext?: CompilationContext): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name);
if (elem.tagName !== "t") {
@@ -416,10 +396,6 @@ export class QWeb extends EventBus {
ctx.hasParentWidget = true;
ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
if (defineKey) {
ctx.addLine(`let key0 = extra.key || "";`);
ctx.hasKey0 = true;
}
}
this._compileNode(elem, ctx);
@@ -490,13 +466,16 @@ export class QWeb extends EventBus {
// this is an unusual situation: this text node is the result of the
// template rendering.
let nodeID = ctx.generateID();
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
}
return;
}
if (ctx !== ctx.rootContext) {
ctx = ctx.subContext("currentKey", ctx.currentKey);
}
const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) {
@@ -699,7 +678,7 @@ export class QWeb extends EventBus {
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
} else {
ctx.addLine(`let _${attID} = '${value}';`);
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
@@ -735,12 +714,12 @@ export class QWeb extends EventBus {
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1);
}
ctx.addLine(`let _${attID} = ${formattedValue};`);
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
@@ -756,9 +735,9 @@ export class QWeb extends EventBus {
const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) {
ctx.addLine(`let _${attID} = '${staticVal} ' + ${formattedExpr};`);
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else {
ctx.addLine(`let _${attID} = ${formattedExpr};`);
ctx.addLine(`var _${attID} = ${formattedExpr};`);
}
attrs.push(`${attName}: _${attID}`);
}
@@ -766,13 +745,13 @@ export class QWeb extends EventBus {
// t-att= attributes
if (name === "t-att") {
let id = ctx.generateID();
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`);
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id);
}
}
let nodeID = ctx.generateID();
let key = ctx.loopNumber || ctx.hasKey0 ? `\`\${key${ctx.loopNumber}}_${nodeID}\`` : nodeID;
const parts = [`key:${key}`];
let nodeKey = ctx.currentKey || nodeID;
const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`);
}
@@ -798,12 +777,9 @@ export class QWeb extends EventBus {
ctx.addLine(`}`);
ctx.closeIf();
}
ctx.addLine(`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
ctx.addLine(`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`);
if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
} else if (ctx.loopNumber || ctx.hasKey0) {
ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = vn${nodeID};`);
}
return nodeID;
+1 -4
View File
@@ -132,7 +132,7 @@ export function useStore(selector, options: SelectorOptions = {}): any {
__destroy.call(component, parent);
};
if (typeof result !== "object" || result === null) {
if (typeof result !== "object") {
return result;
}
return new Proxy(result, {
@@ -141,9 +141,6 @@ export function useStore(selector, options: SelectorOptions = {}): any {
},
set(target, k, v) {
throw new Error("Store state should only be modified through actions");
},
has(target, k) {
return k in result;
}
});
}
-17
View File
@@ -1,5 +1,4 @@
import { QWeb } from "./qweb/index";
import { registerSheet } from "./component/styles";
/**
* Owl Tags
@@ -26,19 +25,3 @@ export function xml(strings, ...args) {
QWeb.registerTemplate(name, value);
return name;
}
/**
* CSS tag helper for defining inline stylesheets. With this, one can simply define
* an inline stylesheet with just the following code:
* ```js
* class A extends Component {
* static style = css`.component-a { color: red; }`;
* }
* ```
*/
export function css(strings, ...args) {
const name = `__sheet__${QWeb.nextId++}`;
const value = String.raw(strings, ...args);
registerSheet(name, value);
return name;
}
+10 -10
View File
@@ -8,9 +8,9 @@ exports[`animations t-transition combined with component 1`] = `
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
var vn1 = h('div', p1, c1);
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
@@ -52,9 +52,9 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
var vn1 = h('div', p1, c1);
if (scope['state'].display) {
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
@@ -98,9 +98,9 @@ exports[`animations t-transition combined with t-component, remove and re-add be
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
var vn1 = h('div', p1, c1);
if (scope['state'].flag) {
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
@@ -141,9 +141,9 @@ exports[`animations t-transition with no delay/duration 1`] = `
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1);
var vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'jupiler');
@@ -162,9 +162,9 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1);
var vn1 = h('span', p1, c1);
p1.hook = {
insert: vn => {
utils.transitionInsert(vn, 'chimay');
File diff suppressed because it is too large Load Diff
@@ -8,9 +8,9 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
var vn1 = h('div', p1, c1);
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {message:1};
+125 -125
View File
@@ -8,9 +8,9 @@ exports[`t-slot directive can define and call slots 1`] = `
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
var vn1 = h('div', p1, c1);
// Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
@@ -43,18 +43,18 @@ exports[`t-slot directive can define and call slots 2`] = `
"function anonymous(context, extra
) {
// Template name: \\"Dialog\\"
let h = this.h;
var h = this.h;
let c6 = [], p6 = {key:6};
let vn6 = h('div', p6, c6);
var vn6 = h('div', p6, c6);
let c7 = [], p7 = {key:7};
let vn7 = h('div', p7, c7);
var vn7 = h('div', p7, c7);
c6.push(vn7);
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
}
let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9);
var vn9 = h('div', p9, c9);
c6.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot10) {
@@ -68,10 +68,10 @@ exports[`t-slot directive can define and call slots 3`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_header_template\\"
let h = this.h;
var h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
var vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`header\`});
}"
@@ -81,10 +81,10 @@ exports[`t-slot directive can define and call slots 4`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_footer_template\\"
let h = this.h;
var h = this.h;
let c1 = extra.parentNode;
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
var vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`footer\`});
}"
@@ -94,10 +94,10 @@ exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
var h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
var vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts rocks\`});
}"
@@ -107,9 +107,9 @@ exports[`t-slot directive dafault slots can define a default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
var h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
var vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
@@ -124,7 +124,7 @@ exports[`t-slot directive default slot work with text nodes 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
var h = this.h;
let c1 = extra.parentNode;
c1.push({text: \`sts rocks\`});
}"
@@ -134,14 +134,14 @@ exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_default_template\\"
let h = this.h;
var h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
var vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
var vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
@@ -151,14 +151,14 @@ exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"slot_content_template\\"
let h = this.h;
var h = this.h;
let c1 = extra.parentNode;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
var vn4 = h('span', p4, c4);
c1.push(vn4);
c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5);
var vn5 = h('span', p5, c5);
c1.push(vn5);
c5.push({text: \`rocks\`});
}"
@@ -168,9 +168,9 @@ exports[`t-slot directive named slots can define a default content 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__1\\"
let h = this.h;
var h = this.h;
let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4);
var vn4 = h('span', p4, c4);
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
@@ -187,10 +187,10 @@ exports[`t-slot directive refs are properly bound in slots 1`] = `
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
context.__owl__.refs = context.__owl__.refs || {};
let h = this.h;
var h = this.h;
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
var vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
@@ -212,10 +212,10 @@ exports[`t-slot directive slots are rendered with proper context 1`] = `
) {
// Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils;
let h = this.h;
var h = this.h;
let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8);
var vn8 = h('button', p8, c8);
c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p8.on['click'] = extra.handlers['click__9__'];
@@ -228,15 +228,15 @@ exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
) {
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _11 = scope['props'].to;
let c12 = [], p12 = {key:12,attrs:{href: _11}};
let vn12 = h('a', p12, c12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
var h = this.h;
var _12 = scope['props'].to;
let c13 = [], p13 = {key:13,attrs:{href: _12}};
var vn13 = h('a', p13, c13);
const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot14) {
slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context}));
}
return vn12;
return vn13;
}"
`;
@@ -248,57 +248,57 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2};
let vn2 = h('u', p2, c2);
var vn2 = h('u', p2, c2);
c1.push(vn2);
let _3 = scope['state'].users;
var _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
var _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
let _length4 = _4.length;
let _origScope6 = scope;
scope = Object.create(scope);
var _length4 = _4.length;
const _origScope6 = scope;
scope = Object.assign(Object.create(context), scope);
for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0
scope.user_last = i1 === _length4 - 1
scope.user_index = i1
scope.user = _4[i1]
scope.user_value = _5[i1]
let key1 = scope['user'].id;
let c7 = [], p7 = {key:\`\${key1}_7\`};
let vn7 = h('li', p7, c7);
c2.push(vn7);
const nodeKey7 = scope['user'].id;
let c8 = [], p8 = {key:nodeKey7};
var vn8 = h('li', p8, c8);
c2.push(vn8);
// Component 'Link'
let k9 = \`__9__\${key1}__\`;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
let props8 = {to:'/user/'+scope['user'].id};
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
w8.destroy();
w8 = false;
let k10 = \`__10__\` + nodeKey7;
let w9 = k10 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k10]] : false;
let props9 = {to:'/user/'+scope['user'].id};
if (w9 && w9.__owl__.currentFiber && !w9.__owl__.vnode) {
w9.destroy();
w9 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
if (w9) {
w9.__updateProps(props9, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w9.__owl__.pvnode;
c8.push(pvnode);
} else {
let componentKey8 = \`Link\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
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 pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
let componentKey9 = \`Link\`;
let W9 = context.constructor.components[componentKey9] || QWeb.components[componentKey9]|| scope['Link'];
if (!W9) {throw new Error('Cannot find the definition of component \\"' + componentKey9 + '\\"')}
w9 = new W9(parent, props9);
parent.__owl__.cmap[k10] = w9.__owl__.id;
w9.__owl__.slotId = 1;
let fiber = w9.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k10, hook: {remove() {},destroy(vn) {w9.destroy();}}});
c8.push(pvnode);
w9.__owl__.pvnode = pvnode;
}
w8.__owl__.parentLastFiberId = extra.fiber.id;
w9.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope6;
return vn1;
@@ -310,12 +310,12 @@ exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c7 = extra.parentNode;
c7.push({text: \`User \`});
let _10 = scope['user'].name;
if (_10 != null) {
c7.push({text: _10});
var h = this.h;
let c8 = extra.parentNode;
c8.push({text: \`User \`});
var _11 = scope['user'].name;
if (_11 || _11 === 0) {
c8.push({text: _11});
}
}"
`;
@@ -325,15 +325,15 @@ exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
) {
// Template name: \\"Link\\"
let scope = Object.create(context);
let h = this.h;
let _10 = scope['props'].to;
let c11 = [], p11 = {key:11,attrs:{href: _10}};
let vn11 = h('a', p11, c11);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot12) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
var h = this.h;
var _11 = scope['props'].to;
let c12 = [], p12 = {key:12,attrs:{href: _11}};
var vn12 = h('a', p12, c12);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot13) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
}
return vn11;
return vn12;
}"
`;
@@ -345,58 +345,58 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2};
let vn2 = h('u', p2, c2);
var vn2 = h('u', p2, c2);
c1.push(vn2);
let _3 = scope['state'].users;
var _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3;
var _4 = _5 = _3;
if (!(_3 instanceof Array)) {
_4 = Object.keys(_3);
_5 = Object.values(_3);
}
let _length4 = _4.length;
let _origScope6 = scope;
scope = Object.create(scope);
var _length4 = _4.length;
const _origScope6 = scope;
scope = Object.assign(Object.create(context), scope);
for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0
scope.user_last = i1 === _length4 - 1
scope.user_index = i1
scope.user = _4[i1]
scope.user_value = _5[i1]
let key1 = scope['user'].id;
let c7 = [], p7 = {key:\`\${key1}_7\`};
let vn7 = h('li', p7, c7);
c2.push(vn7);
utils.getScope(scope, 'userdescr').userdescr = 'User '+scope['user'].name;
const nodeKey7 = scope['user'].id;
let c8 = [], p8 = {key:nodeKey7};
var vn8 = h('li', p8, c8);
c2.push(vn8);
scope.userdescr = 'User '+scope['user'].name;
// Component 'Link'
let k9 = \`__9__\${key1}__\`;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
let props8 = {to:'/user/'+scope['user'].id};
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
w8.destroy();
w8 = false;
let k10 = \`__10__\` + nodeKey7;
let w9 = k10 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k10]] : false;
let props9 = {to:'/user/'+scope['user'].id};
if (w9 && w9.__owl__.currentFiber && !w9.__owl__.vnode) {
w9.destroy();
w9 = false;
}
if (w8) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode;
c7.push(pvnode);
if (w9) {
w9.__updateProps(props9, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w9.__owl__.pvnode;
c8.push(pvnode);
} else {
let componentKey8 = \`Link\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
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 pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
c7.push(pvnode);
w8.__owl__.pvnode = pvnode;
let componentKey9 = \`Link\`;
let W9 = context.constructor.components[componentKey9] || QWeb.components[componentKey9]|| scope['Link'];
if (!W9) {throw new Error('Cannot find the definition of component \\"' + componentKey9 + '\\"')}
w9 = new W9(parent, props9);
parent.__owl__.cmap[k10] = w9.__owl__.id;
w9.__owl__.slotId = 1;
let fiber = w9.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k10, hook: {remove() {},destroy(vn) {w9.destroy();}}});
c8.push(pvnode);
w9.__owl__.pvnode = pvnode;
}
w8.__owl__.parentLastFiberId = extra.fiber.id;
w9.__owl__.parentLastFiberId = extra.fiber.id;
}
scope = _origScope6;
return vn1;
@@ -408,10 +408,10 @@ exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
let c7 = extra.parentNode;
if (scope.userdescr != null) {
c7.push({text: scope.userdescr});
var h = this.h;
let c8 = extra.parentNode;
if (scope.userdescr || scope.userdescr === 0) {
c8.push({text: scope.userdescr});
}
}"
`;
@@ -424,9 +424,9 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
var vn1 = h('div', p1, c1);
scope.userdescr = 'User '+scope['state'].user.name;
// Component 'Link'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
@@ -461,9 +461,9 @@ exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
) {
// Template name: \\"slot_default_template\\"
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let c1 = extra.parentNode;
if (scope.userdescr != null) {
if (scope.userdescr || scope.userdescr === 0) {
c1.push({text: scope.userdescr});
}
}"
@@ -475,7 +475,7 @@ exports[`t-slot directive template can just return a slot 1`] = `
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let result;
let h = this.h;
var h = this.h;
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot6) {
let children7= []
+2 -512
View File
@@ -147,21 +147,6 @@ describe("basic widget properties", () => {
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
});
test("can handle empty props", async () => {
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class Parent extends Component<any, any> {
static template = xml`<div><Child val=""/></div>`;
static components = { Child };
}
const parent = new Parent();
await parent.mount(fixture);
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
expect(fixture.innerHTML).toBe("<div><span></span></div>");
});
test("cannot be clicked on and updated if not in DOM", async () => {
class Counter extends Component<any, any> {
static template = xml`
@@ -337,36 +322,6 @@ describe("basic widget properties", () => {
console.warn = warn;
});
test("reconciliation alg works for t-foreach in t-foreach, 2", async () => {
class Child extends Component<any, any> {
static template = xml`<div><t t-esc="props.row + '_' + props.col"/></div>`;
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<p t-foreach="state.rows" t-as="row" t-key="row">
<p t-foreach="state.cols" t-as="col" t-key="col">
<Child row="row" col="col"/>
</p>
</p>
</div>`;
static components = { Child };
state = useState({ rows: [1, 2], cols: ["a", "b"] });
}
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><p><p><div>1_a</div></p><p><div>1_b</div></p></p><p><p><div>2_a</div></p><p><div>2_b</div></p></p></div>"
);
widget.state.rows = [2, 1];
await nextTick();
expect(fixture.innerHTML).toBe(
"<div><p><p><div>2_a</div></p><p><div>2_b</div></p></p><p><p><div>1_a</div></p><p><div>1_b</div></p></p></div>"
);
});
test("same t-keys in two different places", async () => {
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.blip"/></span>`;
@@ -384,7 +339,6 @@ describe("basic widget properties", () => {
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>1</span></div><div><span>2</span></div></div>");
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
});
test("t-key on a component with t-if, and a sibling component", async () => {
@@ -2670,359 +2624,6 @@ describe("other directives with t-component", () => {
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("<div><span>B0</span><span>B1</span></div>");
});
test("t-set outside modified in t-foreach", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<t t-set="iter" t-value="0"/>
<t t-foreach="state.values" t-as="val" t-key="val">
<p>InLoop: <t t-esc="iter"/></p>
<t t-set="iter" t-value="iter + 1"/>
</t>
<p>EndLoop: <t t-esc="iter"/></p>
</div>`;
state = useState({ values: ["a", "b"] });
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>InLoop: 0</p><p>InLoop: 1</p><p>EndLoop: 2</p></div>");
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set outside modified in t-if", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<t t-set="iter" t-value="0"/>
<t t-set="flag" t-value="state.flag" />
<t t-if="flag === 'if'">
<t t-set="iter" t-value="2"/>
</t>
<t t-elif="flag === 'elif'">
<t t-set="iter" t-value="3"/>
</t>
<t t-else="">
<t t-set="iter" t-value="4"/>
</t>
<p><t t-esc="iter"/></p>
</div>`;
state = { flag: "if" };
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
widget.state.flag = "elif";
await widget.render();
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
widget.state.flag = "false";
await widget.render();
expect(fixture.innerHTML).toBe("<div><p>4</p></div>");
});
test("t-set in t-if", async () => {
// Weird that code block within 'if' leaks outside of it
// Python does the same
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<t t-set="flag" t-value="state.flag" />
<t t-if="flag === 'if'">
<t t-set="iter" t-value="2"/>
</t>
<t t-elif="flag === 'elif'">
<t t-set="iter" t-value="3"/>
</t>
<t t-else="">
<t t-set="iter" t-value="4"/>
</t>
<p><t t-esc="iter"/></p>
</div>`;
state = { flag: "if" };
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
widget.state.flag = "elif";
await widget.render();
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
widget.state.flag = "false";
await widget.render();
expect(fixture.innerHTML).toBe("<div><p>4</p></div>");
});
test("t-set can't alter component", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<p><t t-esc="iter"/></p>
<t t-set="iter" t-value="5"/>
<p><t t-esc="iter"/></p>
</div>`;
iter = 1;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>1</p><p>5</p></div>");
expect(widget.iter).toBe(1);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set can't alter from within callee", async () => {
env.qweb.addTemplate(
"ChildWidget",
`<div><t t-esc="iter"/><t t-set="iter" t-value="'called'"/><t t-esc="iter"/></div>`
);
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<t t-set="iter" t-value="'source'"/>
<p><t t-esc="iter"/></p>
<t t-call="ChildWidget"/>
<p><t t-esc="iter"/></p>
</div>`;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>source</p><div>sourcecalled</div><p>source</p></div>");
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set can't alter in t-call body", async () => {
env.qweb.addTemplate(
"ChildWidget",
`<div><t t-esc="iter"/><t t-set="iter" t-value="'called'"/><t t-esc="iter"/></div>`
);
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<t t-set="iter" t-value="'source'"/>
<p><t t-esc="iter"/></p>
<t t-call="ChildWidget">
<t t-set="iter" t-value="'inCall'"/>
</t>
<p><t t-esc="iter"/></p>
</div>`;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>source</p><div>inCallcalled</div><p>source</p></div>");
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("slot setted value (with t-set) not accessible with t-esc", async () => {
class ChildWidget extends Component<any, any> {
static template = xml`<div><t t-esc="iter"/><t t-set="iter" t-value="'called'"/><t t-esc="iter"/></div>`;
}
class SomeWidget extends Component<any, any> {
static components = { ChildWidget };
static template = xml`
<div>
<t t-set="iter" t-value="'source'"/>
<p><t t-esc="iter"/></p>
<ChildWidget>
<t t-set="iter" t-value="'inCall'"/>
</ChildWidget>
<p><t t-esc="iter"/></p>
</div>`;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>source</p><div>called</div><p>source</p></div>");
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set not altered by child widget", async () => {
let child;
class ChildWidget extends Component<any, any> {
static template = xml`<div><t t-esc="iter"/><t t-set="iter" t-value="'called'"/><t t-esc="iter"/></div>`;
iter = "child";
constructor() {
super(...arguments);
child = this;
}
}
class SomeWidget extends Component<any, any> {
static components = { ChildWidget };
static template = xml`
<div>
<t t-set="iter" t-value="'source'"/>
<p><t t-esc="iter"/></p>
<ChildWidget/>
<p><t t-esc="iter"/></p>
</div>`;
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>source</p><div>childcalled</div><p>source</p></div>");
expect(child.iter).toBe("child");
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-set outside modified in t-foreach increment-after operator", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<t t-set="iter" t-value="0"/>
<t t-foreach="state.values" t-as="val" t-key="val">
<p>InLoop: <t t-esc="iter"/></p>
<t t-set="iter" t-value="iter++"/>
</t>
<p>EndLoop: <t t-esc="iter"/></p>
</div>`;
state = useState({ values: ["a", "b"] });
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>InLoop: 0</p><p>InLoop: 1</p><p>EndLoop: 0</p></div>");
});
test("t-set outside modified in t-foreach increment-before operator", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<t t-set="iter" t-value="0"/>
<t t-foreach="state.values" t-as="val" t-key="val">
<p>InLoop: <t t-esc="iter"/></p>
<t t-set="iter" t-value="++iter"/>
</t>
<p>EndLoop: <t t-esc="iter"/></p>
</div>`;
state = useState({ values: ["a", "b"] });
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><p>InLoop: 0</p><p>InLoop: 1</p><p>EndLoop: 1</p></div>");
});
test("t-on expression in t-foreach", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<div t-foreach="state.values" t-as="val" t-key="val">
<t t-esc="val_index"/>: <t t-esc="val + ''"/>
<button t-on-click="otherState.vals.push(val)">Expr</button>
</div>
</div>`;
state = useState({ values: ["a", "b"] });
otherState = { vals: [] };
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div>0: a<button>Expr</button></div><div>1: b<button>Expr</button></div></div>"
);
expect(widget.otherState.vals).toStrictEqual([]);
const buttons = fixture.querySelectorAll("button");
buttons[0].click();
buttons[1].click();
expect(widget.otherState.vals).toStrictEqual(["a", "b"]);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-on expression in t-foreach with t-set", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<t t-set="bossa" t-value="'nova'"/>
<div t-foreach="state.values" t-as="val" t-key="val">
<t t-set="bossa" t-value="bossa + '_' + val_index" />
<t t-esc="val_index"/>: <t t-esc="val + ''"/>
<button t-on-click="otherState.vals.push(val + '_' + bossa)">Expr</button>
</div>
</div>`;
state = useState({ values: ["a", "b"] });
otherState = { vals: [] };
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div>0: a<button>Expr</button></div><div>1: b<button>Expr</button></div></div>"
);
expect(widget.otherState.vals).toStrictEqual([]);
const buttons = fixture.querySelectorAll("button");
buttons[0].click();
buttons[1].click();
expect(widget.otherState.vals).toStrictEqual(["a_nova_0", "b_nova_0_1"]);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-on method call in t-foreach", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<div t-foreach="state.values" t-as="val" t-key="val">
<t t-esc="val_index"/>: <t t-esc="val + ''"/>
<button t-on-click="addVal(val)">meth call</button>
</div>
</div>`;
state = useState({ values: ["a", "b"] });
otherState = { vals: new Array<string>() };
addVal(val: string) {
this.otherState.vals.push(val);
}
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div>0: a<button>meth call</button></div><div>1: b<button>meth call</button></div></div>"
);
expect(widget.otherState.vals).toStrictEqual([]);
const buttons = fixture.querySelectorAll("button");
buttons[0].click();
buttons[1].click();
expect(widget.otherState.vals).toStrictEqual(["a", "b"]);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
test("t-on expression captured in t-foreach", async () => {
class SomeWidget extends Component<any, any> {
static template = xml`
<div>
<t t-set="iter" t-value="0" />
<div t-foreach="arr" t-as="val" t-key="val">
<button t-on-click="otherState.vals.push(iter + '_' + iter)">expr</button>
<t t-set="iter" t-value="iter + 1" />
</div>
</div>`;
arr = ["a", "b"];
otherState = { vals: new Array<string>() };
}
const widget = new SomeWidget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe(
"<div><div><button>expr</button></div><div><button>expr</button></div></div>"
);
expect(widget.otherState.vals).toStrictEqual([]);
const buttons = fixture.querySelectorAll("button");
buttons[0].click();
buttons[1].click();
expect(widget.otherState.vals).toStrictEqual(["0_0", "1_1"]);
expect(QWeb.TEMPLATES[SomeWidget.template].fn.toString()).toMatchSnapshot();
});
});
describe("random stuff/miscellaneous", () => {
@@ -4817,28 +4418,6 @@ describe("t-model directive", () => {
expect(comp.state[2].f).toBe(false);
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
});
test("two inputs in a div with a t-key", async () => {
class SomeComponent extends Component<any, any> {
static template = xml`
<div t-key="'key'">
<input class="a" t-if="state.flag"/>
<input class="b" t-if="!state.flag"/>
</div>
`;
state = useState({ flag: true });
}
const comp = new SomeComponent();
await comp.mount(fixture);
expect(fixture.innerHTML).toBe('<div><input class="a"></div>');
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
fixture.querySelector("input")!.value = "asdf";
expect(fixture.querySelector("input")!.value).toBe("asdf");
comp.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe('<div><input class="b"></div>');
expect(fixture.querySelector("input")!.value).toBe("");
});
});
describe("environment and plugins", () => {
@@ -4941,7 +4520,7 @@ describe("component error handling (catchError)", () => {
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><div>heyfalse</div></div></div>");
expect(fixture.innerHTML).toBe("<div><div><div>hey</div></div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
@@ -4976,7 +4555,7 @@ describe("component error handling (catchError)", () => {
}
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div>heyfalse</div></div>");
expect(fixture.innerHTML).toBe("<div><div>hey</div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
@@ -5998,70 +5577,6 @@ describe("t-call", () => {
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot();
});
test("parent is set within t-call", async () => {
env.qweb.addTemplate("sub", `<Child/>`);
let child;
class Child extends Component<any, any> {
static template = xml`<span>lucas</span>`;
constructor() {
super(...arguments);
child = this;
}
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`<div><t t-call="sub"/></div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>lucas</span></div>");
expect(child.__owl__.parent).toBe(parent);
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot();
});
test("t-call in t-foreach and children component", async () => {
env.qweb.addTemplate("sub", `<Child val="val"/>`);
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`
<div>
<t t-foreach="['a', 'b', 'c']" t-as="val" t-key="val">
<t t-call="sub"/>
</t>
</div>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>a</span><span>b</span><span>c</span></div>");
});
test("parent is set within t-call with no parentNode", async () => {
env.qweb.addTemplate("sub", `<Child/>`);
let child;
class Child extends Component<any, any> {
constructor() {
super(...arguments);
child = this;
}
static template = xml`<span>lucas</span>`;
}
class Parent extends Component<any, any> {
static components = { Child };
static template = xml`<t t-call="sub"/>`;
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<span>lucas</span>");
expect(child.__owl__.parent).toBe(parent);
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot();
});
test("handlers with arguments are properly bound through a t-call", async () => {
expect.assertions(3);
env.qweb.addTemplate("sub", `<p t-on-click="update(a)">lucas</p>`);
@@ -6079,29 +5594,4 @@ describe("t-call", () => {
fixture.querySelector("p")!.click();
});
test("sub components in two t-calls", async () => {
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
env.qweb.addTemplate("sub", `<Child val="state.val"/>`);
class Parent extends Component<any, any> {
static template = xml`
<div>
<t t-if="state.val===1">
<t t-call="sub"/>
</t>
<div t-else=""><t t-call="sub"/></div>
</div>`;
static components = { Child };
state = useState({ val: 1 });
}
const parent = new Parent();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.state.val = 2;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div><span>2</span></div></div>");
});
});
+7
View File
@@ -19,6 +19,13 @@ let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
env.qweb.addTemplate("Component<any,any>", "<div></div>");
env.qweb.addTemplate(
"Counter",
`<div><t t-esc="state.counter"/><button t-on-click="inc">Inc</button></div>`
);
env.qweb.addTemplate("WidgetA", `<div>Hello<t t-component="b"/></div>`);
env.qweb.addTemplate("WidgetB", `<div>world</div>`);
Component.env = env;
});
-170
View File
@@ -1,170 +0,0 @@
import { Component, Env } from "../../src/component/component";
import { processSheet } from "../../src/component/styles";
import { xml, css } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - fixture: a div, appended to the DOM, intended to be the target of dom
// manipulations. Note that it is removed after each test.
// - env: an Env, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
document.head.innerHTML = "";
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("styles and component", () => {
test("can define an inline stylesheet", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
color: red;
}
`;
}
expect(document.head.innerHTML).toBe("");
const app = new App();
expect(document.head.innerHTML).toBe(`<style component=\"App\">.app {
color: red;
}</style>`);
await app.mount(fixture);
const style = getComputedStyle(app.el!);
expect(style.color).toBe("red");
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
});
test("inherited components properly apply css", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
color: red;
}
`;
}
class SubApp extends App {
static style = css`
.app {
font-weight: bold;
}
`;
}
expect(document.head.innerHTML).toBe("");
const app = new SubApp();
expect(document.head.innerHTML).toBe(`<style component=\"SubApp\">.app {
font-weight: bold;
}</style><style component=\"App\">.app {
color: red;
}</style>`);
await app.mount(fixture);
const style = getComputedStyle(app.el!);
expect(style.color).toBe("red");
expect(style.fontWeight).toBe("bold");
expect(fixture.innerHTML).toBe('<div class="app">text</div>');
});
test("get a meaningful error message if css helper is missing", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = `.app {color: red;}`;
}
let error;
try {
new App();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(
"Invalid css stylesheet for component 'App'. Did you forget to use the 'css' tag helper?"
);
});
test("inline stylesheets are processed", async () => {
class App extends Component<any, any> {
static template = xml`<div class="app">text</div>`;
static style = css`
.app {
color: red;
.some-class {
font-weight: bold;
width: 40px;
}
display: block;
}
`;
}
new App();
expect(document.head.querySelector("style")!.innerHTML).toBe(`.app {
color: red;
}
.app .some-class {
font-weight: bold;
width: 40px;
}
.app {
display: block;
}`);
});
test("properly handle rules with commas", async () => {
const sheet = processSheet(`.parent-a, .parent-b {
.child-a, .child-b {
color: red;
}
}`);
expect(sheet)
.toBe(`.parent-a .child-a, .parent-a .child-b, .parent-b .child-a, .parent-b .child-b {
color: red;
}`);
});
test("handle & selector", async () => {
let sheet = processSheet(`.btn {
&.danger {
color: red;
}
}`);
expect(sheet).toBe(`.btn.danger {
color: red;
}`);
sheet = processSheet(`.some-class {
&.btn {
.other-class ~ & {
color: red;
}
}
}`);
expect(sheet).toBe(`.other-class ~ .some-class.btn {
color: red;
}`);
});
});
+40
View File
@@ -429,4 +429,44 @@ describe("observer", () => {
obj.a.push(2);
}).toThrow('Observed state cannot be changed here! (key: "1", val: "2")');
});
test("functions are not observed, but are properly bound", () => {
const observer = new Observer();
const obj: any = observer.observe({ m: new Map() });
expect(observer.rev).toBe(1);
obj.m.set("a", 1);
expect(obj.m.get("a")).toBe(1);
// should be 2, but we do not support reactivity on maps/weakmaps/set/weakset
expect(observer.rev).toBe(1);
});
test("functions are correctly bound", async () => {
const observer = new Observer();
const obj: any = observer.observe({ set: new Set() });
expect(observer.revNumber(obj)).toBe(1);
obj.set.add('mojo');
expect(observer.revNumber(obj)).toBe(1);
const steps: string[] = [];
obj.set.forEach((elm)=>{steps.push(elm)});
expect(steps).toStrictEqual(['mojo']);
obj.set.clear();
expect(observer.revNumber(obj)).toBe(1);
});
test("functions are correctly bound", async () => {
const observer = new Observer();
const obj: any = observer.observe({ set: [] });
expect(observer.revNumber(obj)).toBe(1);
obj.set.push('mojo');
expect(observer.revNumber(obj)).toBe(2);
const steps: string[] = [];
obj.set.forEach((elm)=>{steps.push(elm)});
expect(steps).toStrictEqual(['mojo']);
});
});
+1 -37
View File
@@ -9,8 +9,7 @@ import {
onWillPatch,
onWillStart,
onWillUpdateProps,
useSubEnv,
useExternalListener
useSubEnv
} from "../src/hooks";
import { xml } from "../src/tags";
@@ -593,39 +592,4 @@ describe("hooks", () => {
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
expect(steps).toEqual(["onWillStart", "onWillUpdateProps"]);
});
test("useExternalListener", async () => {
let n = 0;
class MyComponent extends Component<any, any> {
static template = xml`<span><t t-esc="props.value"/></span>`;
constructor(parent, props) {
super(parent, props);
useExternalListener(window as any, "click", this.increment);
}
increment() {
n++;
}
}
class App extends Component<any, any> {
static template = xml`<div><MyComponent t-if="state.flag"/></div>`;
static components = { MyComponent };
state = useState({ flag: false });
}
const app = new App();
await app.mount(fixture);
expect(n).toBe(0);
window.dispatchEvent(new Event("click"));
expect(n).toBe(0);
app.state.flag = true;
await nextTick();
window.dispatchEvent(new Event("click"));
expect(n).toBe(1);
app.state.flag = false;
await nextTick();
window.dispatchEvent(new Event("click"));
expect(n).toBe(1);
});
});
File diff suppressed because it is too large Load Diff
-44
View File
@@ -141,49 +141,17 @@ describe("t-esc", () => {
qweb.addTemplate("test", `<span t-esc="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>nope</span>");
});
test("t-esc is escaped", () => {
qweb.addTemplate("test", `<div><t t-set="var"><p>escaped</p></t><t t-esc="var"/></div>`);
const domRendered = renderToDOM(qweb, "test");
expect(domRendered.textContent).toBe("<p>escaped</p>");
});
test("t-esc=0 is escaped", () => {
qweb.addTemplate("test", `<span><t t-esc="0"/></span>`);
qweb.addTemplate("testCaller", `<div><t t-call="test"><p>escaped</p></t></div>`);
const domRendered = renderToDOM(qweb, "testCaller") as HTMLElement;
expect(domRendered.querySelector("span")!.textContent).toBe("<p>escaped</p>");
});
test("div with falsy values", () => {
qweb.addTemplate(
"test",
`
<div>
<p t-esc="v1"/>
<p t-esc="v2"/>
<p t-esc="v3"/>
<p t-esc="v4"/>
<p t-esc="v5"/>
</div>`
);
const vals = {
v1: false,
v2: undefined,
v3: null,
v4: 0,
v5: ""
};
expect(renderToString(qweb, "test", vals)).toBe(
"<div><p>false</p><p></p><p></p><p>0</p><p></p></div>"
);
});
test("t-esc work with spread operator", () => {
qweb.addTemplate("test", `<span><t t-esc="[...state.list]"/></span>`);
const result = renderToString(qweb, "test", { state: { list: [1, 2] } });
expect(result).toBe("<span>1,2</span>");
});
});
describe("t-raw", () => {
@@ -290,18 +258,6 @@ describe("t-set", () => {
);
});
test("t-set with content and sub t-esc", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="setvar"><t t-esc="beep"/> boop</t>
<t t-esc="setvar"/>
</div>`
);
expect(renderToString(qweb, "test", { beep: "beep" })).toBe("<div>beep boop</div>");
});
test("evaluate value expression, part 2", () => {
qweb.addTemplate(
"test",
-19
View File
@@ -50,18 +50,6 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" }
]);
expect(tokenize("a...1")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "..." },
{ type: "VALUE", value: "1" }
]);
expect(tokenize("a in b")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "in " },
{ type: "SYMBOL", value: "b" }
]);
});
test("strings", () => {
@@ -140,7 +128,6 @@ describe("expression evaluation", () => {
expect(compileExpr("color === 'black'", {})).toBe("scope['color']==='black'");
expect(compileExpr("'li_'+item", {})).toBe("'li_'+scope['item']");
expect(compileExpr("state.val > 1", {})).toBe("scope['state'].val>1");
expect(compileExpr("a in b", {})).toBe("scope['a']in scope['b']");
});
test("boolean operations", () => {
@@ -190,10 +177,4 @@ describe("expression evaluation", () => {
expect(compileExpr("a -= b", {})).toBe("scope['a']-=scope['b']");
expect(compileExpr("a.b = !a.b", {})).toBe("scope['a'].b=!scope['a'].b");
});
test("spread operator", () => {
expect(compileExpr("[...state.list]", {})).toBe("[...scope['state'].list]");
expect(compileExpr("f(...state.list)", {})).toBe("scope['f'](...scope['state'].list)");
expect(compileExpr("f([...list])", {})).toBe("scope['f']([...scope['list']])");
});
});
+3 -3
View File
@@ -6,11 +6,11 @@ exports[`Link component can render simple cases 1`] = `
// Template name: \\"__template__1\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
var h = this.h;
let _4 = utils.toObj({'router-link-active':scope['isActive']});
let _5 = scope['href'];
var _5 = scope['href'];
let c6 = [], p6 = {key:6,attrs:{href: _5},class:_4,on:{}};
let vn6 = h('a', p6, c6);
var vn6 = h('a', p6, c6);
extra.handlers['click__7__'] = extra.handlers['click__7__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['navigate'](e);};
p6.on['click'] = extra.handlers['click__7__'];
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
@@ -9,37 +9,35 @@ exports[`RouteComponent can render simple cases 1`] = `
let parent = context;
let scope = Object.create(context);
let result;
let h = this.h;
var h = this.h;
if (scope['routeComponent']) {
{
let key0 = scope['env'].router.currentRouteName;
// Component 'routeComponent'
let k5 = \`__5__\${key0}__\`;
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);
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) {
w4.destroy();
w4 = false;
}
if (w4) {
w4.__updateProps(props4, extra.fiber, undefined);
let pvnode = w4.__owl__.pvnode;
utils.defineProxy(vn6, pvnode);
} else {
let componentKey4 = \`routeComponent\`;
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4]|| scope['routeComponent'];
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
w4 = new W4(parent, props4);
parent.__owl__.cmap[k5] = w4.__owl__.id;
let fiber = w4.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k5, hook: {remove() {},destroy(vn) {w4.destroy();}}});
utils.defineProxy(vn6, pvnode);
w4.__owl__.pvnode = pvnode;
}
w4.__owl__.parentLastFiberId = extra.fiber.id;
const nodeKey4 = scope['env'].router.currentRouteName;
// Component 'routeComponent'
let k6 = \`__6__\` + nodeKey4;
let w5 = k6 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k6]] : false;
let vn7 = {};
result = vn7;
let props5 = Object.assign({}, scope['env'].router.currentParams);
if (w5 && w5.__owl__.currentFiber && !w5.__owl__.vnode) {
w5.destroy();
w5 = false;
}
if (w5) {
w5.__updateProps(props5, extra.fiber, undefined);
let pvnode = w5.__owl__.pvnode;
utils.defineProxy(vn7, pvnode);
} else {
let componentKey5 = \`routeComponent\`;
let W5 = context.constructor.components[componentKey5] || QWeb.components[componentKey5]|| scope['routeComponent'];
if (!W5) {throw new Error('Cannot find the definition of component \\"' + componentKey5 + '\\"')}
w5 = new W5(parent, props5);
parent.__owl__.cmap[k6] = w5.__owl__.id;
let fiber = w5.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
let pvnode = h('dummy', {key: k6, hook: {remove() {},destroy(vn) {w5.destroy();}}});
utils.defineProxy(vn7, pvnode);
w5.__owl__.pvnode = pvnode;
}
w5.__owl__.parentLastFiberId = extra.fiber.id;
}
return result;
}"
+1 -47
View File
@@ -49,13 +49,10 @@ describe("connecting a component to store", () => {
});
test("useStore can observe primitive types and call onUpdate", async () => {
const state = { isBoolean: false, nullValue: null };
const state = { isBoolean: false };
const actions = {
setTrue({ state }) {
state.isBoolean = true;
},
setNotNull({ state }) {
state.nullValue = "ok";
}
};
const store = new Store({ state, actions });
@@ -64,10 +61,8 @@ describe("connecting a component to store", () => {
static template = xml`
<div>
<span t-if="isBoolean">ok</span>
<span t-if="nullValue !== null">not null</span>
</div>`;
isBoolean: boolean;
nullValue: string;
constructor() {
super();
this.isBoolean = useStore(state => state.isBoolean, {
@@ -75,11 +70,6 @@ describe("connecting a component to store", () => {
this.isBoolean = isBoolean;
}
});
this.nullValue = useStore(state => state.nullValue, {
onUpdate: nullValue => {
this.nullValue = nullValue;
}
});
}
}
@@ -92,42 +82,6 @@ describe("connecting a component to store", () => {
store.dispatch("setTrue");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>ok</span></div>");
store.dispatch("setNotNull");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>ok</span><span>not null</span></div>");
});
test("map works on the result of useStore when the resulting array changes for a bigger one", async () => {
const state = { smallerArray: [1], biggerArray: [2, 3], useSmallArray: true };
const store = new Store({ state });
class App extends Component<any, any> {
static template = xml`<div t-esc="mapAdd"/>`;
storeProps = {
array: useStore(state => {
if (state.useSmallArray) {
return state.smallerArray;
}
return state.biggerArray;
})
};
get mapAdd() {
return this.storeProps.array.map(a => {
return a + 1;
});
}
}
(<any>env).store = store;
const app = new App();
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
store.state.useSmallArray = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div>3,4</div>");
});
test("throw error if no store is found", async () => {
-57
View File
@@ -1,57 +0,0 @@
/**
* We can only make one test per file, since the debug tool modify in place
* the owl object in a way that is difficult to undo.
*/
import { debugOwl } from "../../tools/debug";
import * as owl from "../../src/index";
import { Component, Env } from "../../src/component/component";
import { xml } from "../../src/tags";
import { makeTestFixture, makeTestEnv } from "../helpers";
let fixture: HTMLElement = makeTestFixture();
let env: Env = makeTestEnv();
Component.env = env;
debugOwl(owl, { logScheduler: true });
test("log a sub component with non stringifiable props", async () => {
const steps: string[] = [];
const log = console.log;
console.log = arg => steps.push(arg);
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.obj.val"/></span>`;
}
const circularObject: any = { val: 1 };
circularObject.circularObject = circularObject;
class Parent extends Component<any, any> {
static template = xml`<div><Child obj="obj"/></div>`;
static components = { Child };
obj = circularObject;
}
const parent = new Parent(null, {});
await parent.mount(fixture);
parent.unmount();
expect(steps).toEqual([
"[OWL_DEBUG] Parent<id=1> constructor, props={}",
"[OWL_DEBUG] Parent<id=1> mount",
"[OWL_DEBUG] Parent<id=1> willStart",
"[OWL_DEBUG] scheduler: start running tasks queue",
"[OWL_DEBUG] Parent<id=1> rendering template",
"[OWL_DEBUG] Child<id=2> constructor, props=<JSON error>",
"[OWL_DEBUG] Child<id=2> willStart",
"[OWL_DEBUG] Child<id=2> rendering template",
"[OWL_DEBUG] Child<id=2> mounted",
"[OWL_DEBUG] Parent<id=1> mounted",
"[OWL_DEBUG] scheduler: stop running tasks queue",
"[OWL_DEBUG] Parent<id=1> willUnmount",
"[OWL_DEBUG] Child<id=2> willUnmount"
]);
console.log = log;
});
-154
View File
@@ -1,154 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = useState({ counter: 0 });
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.logRef.el.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
App.env = {
qweb: new owl.QWeb({ templates })
};
const app = new App();
app.mount(document.body);
}
start();
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL v1.0.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
+1 -6
View File
@@ -35,12 +35,7 @@
});
function toStr(obj) {
let str;
try {
str = JSON.stringify(obj || {});
} catch (e) {
str = "<JSON error>";
}
let str = JSON.stringify(obj || {});
if (str.length > 200) {
str = str.slice(0, 200) + "...";
}
-1
View File
@@ -37,7 +37,6 @@
<li><a href="benchmarks/owl-0.21.0">OWL 0.21.0</a></li>
<li><a href="benchmarks/owl-0.24.0">OWL 0.24.0</a></li>
<li><a href="benchmarks/owl-1.0.0-beta1">OWL 1.0.0-beta1</a></li>
<li><a href="benchmarks/owl-1.0.0">OWL 1.0.0</a></li>
<li><a href="benchmarks/owl-master">OWL Master</a></li>
</ul>
<ul>
+7 -7
View File
@@ -455,7 +455,7 @@ const actions = {
todo.completed = !todo.completed;
},
clearCompleted({ state, dispatch }) {
for (let todo of state.todos.slice()) {
for (let todo of state.todos) {
if (todo.completed) {
dispatch("removeTodo", todo.id);
}
@@ -1089,20 +1089,20 @@ const RESPONSIVE_XML = `<templates>
<button>Button!</button>
</div>
<t t-name="maincontent">
<FormView />
<Chatter />
</t>
<div t-name="App" class="app" t-att-class="{mobile: env.isMobile, desktop: !env.isMobile}">
<t t-set="maincontent">
<FormView />
<Chatter />
</t>
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!env.isMobile">
<div class="content">
<t t-call="maincontent"/>
<t t-raw="maincontent"/>
</div>
</div>
<t t-else="">
<t t-call="maincontent"/>
<t t-raw="maincontent"/>
</t>
</div>
</templates>
+1 -2
View File
@@ -13,8 +13,7 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictPropertyInitialization": true,
"strictNullChecks": true,
"declarationDir": "dist/types"
"strictNullChecks": true
},
"include": ["src/**/*.ts","src/*.ts"]
}