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
32 changed files with 1293 additions and 2206 deletions
+2 -8
View File
@@ -102,16 +102,10 @@ Submit a PR!
## Installing/Building ## 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: If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.0.0-beta5.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta5/owl.js) - [owl-1.0.0-beta2.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta2/owl.js)
- [owl-1.0.0-beta5.min.js](https://github.com/odoo/owl/releases/download/v1.0.0-beta5/owl.min.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: Some npm scripts are available:
+9 -9
View File
@@ -56,15 +56,15 @@ useState Link
config RouteComponent config RouteComponent
mode Router mode Router
core tags core tags
EventBus css EventBus xml
Observer xml Observer utils
hooks utils hooks debounce
onWillStart debounce onWillStart escape
onMounted escape onMounted loadJS
onWillUpdateProps loadJS onWillUpdateProps loadFile
onWillPatch loadFile onWillPatch shallowEqual
onPatched shallowEqual onPatched whenReady
onWillUnmount whenReady onWillUnmount
useContext useContext
useState useState
useRef useRef
-4
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`. 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 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 This is the way [hooks](hooks.md) are able to get a reference to the target
+4 -88
View File
@@ -4,19 +4,16 @@
- [Overview](#overview) - [Overview](#overview)
- [`xml` tag](#xml-tag) - [`xml` tag](#xml-tag)
- [`css` tag](#css-tag)
## Overview ## Overview
Tags are very small helpers intended to make it easy to write inline templates Tags are very small helpers to make it easy to write inline templates. There is
or styles. There are currently two tags: `css` and `xml`. With these functions, only one currently available tag: `xml`, but we plan to add other tags later,
it is possible to write [single file components](../tooling.md#single-file-component). such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component).
## XML tag ## XML tag
The `xml` tag is certainly the most useful tag. It is used to define an inline Without tags, creating a standalone component would look like this:
QWeb template for a component. Without tags, creating a standalone component
would look like this:
```js ```js
import { Component } from 'owl' import { Component } from 'owl'
@@ -55,84 +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;
}
```
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;
}
`;
}
```
+9 -20
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 is very useful to group code by feature instead of by type of file. It makes
it easier to scale application to larger size. it easier to scale application to larger size.
To do so, Owl has two small helpers that make it easy to define a To do so, Owl currently has a small helper that makes it easy to define a
template or a stylesheet inside a javascript (or typescript) file: the template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag)
[`xml`](reference/tags.md#xml-tag) and [`css`](reference/tags.md#css-tag) helper. With this, a template is automatically registered to [QWeb](reference/qweb_engine.md).
helper.
This means that the template, the style and the javascript code can be defined in This means that the template and the javascript code can be defined in the same
the same file. For example: file. It is not currently possible to add css to the same file, but Owl may
get a `css` tag helper later.
```js ```js
const { Component } = owl; const { Component } = owl;
const { xml, css } = owl.tags; const { xml } = owl.tags;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// TEMPLATE // TEMPLATE
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const TEMPLATE = xml/* xml */ ` const TEMPLATE = xml/* xml */ `
<div class="main"> <div class="main two-columns">
<Sidebar/> <Sidebar/>
<Content /> <Content />
</div>`; </div>`;
// -----------------------------------------------------------------------------
// STYLE
// -----------------------------------------------------------------------------
const STYLE = css/* css */ `
.main {
display: grid;
grid-template-columns: 200px auto;
}
`;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// CODE // CODE
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
class Main extends Component { class MyComponent extends Component {
static template = TEMPLATE; static template = TEMPLATE;
static style = STYLE;
static components = { Sidebar, Content }; static components = { Sidebar, Content };
// rest of component... // rest of component...
+3 -6
View File
@@ -1,11 +1,8 @@
{ {
"name": "@odoo/owl", "name": "owl-framework",
"version": "1.0.0-beta5", "version": "1.0.0-beta2",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl/index.js", "main": "src/index.ts",
"types": "dist/owl/index.d.ts",
"prepublish": "npm run build",
"files": ["dist/owl/"],
"engines": { "engines": {
"node": ">=10.15.3" "node": ">=10.15.3"
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉 # 🦉 OWL Roadmap 🦉
- Current version: 1.0.0-beta5 - Current version: 1.0.0-beta2
- Status: mostly stable - Status: mostly stable
This roadmap is only an attempt at predicting Owl's future. Everything may This roadmap is only an attempt at predicting Owl's future. Everything may
-18
View File
@@ -6,7 +6,6 @@ import "./directive";
import { Fiber } from "./fiber"; import { Fiber } from "./fiber";
import "./props_validation"; import "./props_validation";
import { Scheduler, scheduler } from "./scheduler"; import { Scheduler, scheduler } from "./scheduler";
import { activateSheet } from "./styles";
/** /**
* Owl Component System * Owl Component System
@@ -200,9 +199,6 @@ export class Component<T extends Env, Props extends {}> {
refs: null, refs: null,
scope: null scope: null
}; };
if (constr.style) {
this.__applyStyles(constr);
}
} }
/** /**
@@ -584,20 +580,6 @@ export class Component<T extends Env, Props extends {}> {
return fiber; 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 { __getTemplate(qweb: QWeb): string {
let p = (<any>this).constructor; let p = (<any>this).constructor;
if (!p.hasOwnProperty("_template")) { if (!p.hasOwnProperty("_template")) {
+1 -1
View File
@@ -211,7 +211,7 @@ QWeb.addDirective({
} else if (!name.startsWith("t-")) { } else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") { if (name !== "class" && name !== "style") {
// this is a prop! // this is a prop!
props[name] = ctx.formatExpression(value) || "undefined"; props[name] = ctx.formatExpression(value);
} }
} }
} }
-56
View File
@@ -1,56 +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 } = {};
function processSheet(str: string): string {
const tokens = str.split(/(\{|\}|;)/).map(s => s.trim());
const selectorStack: string[] = [];
const parts: string[] = [];
let rules: string[] = [];
function generateRules() {
if (rules.length) {
parts.push(selectorStack.join(" ") + " {");
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);
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' // fun fact: typeof null === 'object'
return value; 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); let metadata = this.weakMap.get(value) || this._observe(value, parent);
return metadata.proxy; return metadata.proxy;
} }
+15 -24
View File
@@ -1,7 +1,6 @@
import { CompilationContext } from "./compilation_context"; import { CompilationContext } from "./compilation_context";
import { QWeb } from "./qweb"; import { QWeb } from "./qweb";
import { htmlToVDOM } from "../vdom/html_to_vdom"; import { htmlToVDOM } from "../vdom/html_to_vdom";
import { QWebVar } from "./expression_parser";
/** /**
* Owl QWeb Directives * Owl QWeb Directives
@@ -40,11 +39,11 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
let exprID: string; let exprID: string;
if (typeof value === "string") { if (typeof value === "string") {
exprID = `_${ctx.generateID()}`; exprID = `_${ctx.generateID()}`;
ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`); ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
} else { } else {
exprID = `scope.${value.id}`; exprID = `scope.${value.id}`;
} }
ctx.addIf(`${exprID} != null`); ctx.addIf(`${exprID} || ${exprID} === 0`);
if (ctx.escaping) { if (ctx.escaping) {
let protectID; let protectID;
@@ -62,7 +61,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID; ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID; ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`); ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) { if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`); ctx.addLine(`result = vn${nodeID}`);
} }
@@ -120,7 +119,7 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineScope = true; ctx.rootContext.shouldDefineScope = true;
const variable = node.getAttribute("t-set")!; const variable = node.getAttribute("t-set")!;
let value = node.getAttribute("t-value")!; 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]; let qwebvar = ctx.variables[variable];
const hasBody = node.hasChildNodes(); const hasBody = node.hasChildNodes();
@@ -141,7 +140,7 @@ QWeb.addDirective({
const _parentNode = ctx.parentNode; const _parentNode = ctx.parentNode;
ctx.parentNode = tempParentNodeID; 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; const nodeCopy = node.cloneNode(true) as Element;
for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) { for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) {
nodeCopy.removeAttribute(attr); nodeCopy.removeAttribute(attr);
@@ -216,7 +215,6 @@ QWeb.addDirective({
// Step 1: sanity checks // Step 1: sanity checks
// ------------------------------------------------ // ------------------------------------------------
ctx.rootContext.shouldDefineScope = true; ctx.rootContext.shouldDefineScope = true;
ctx.rootContext.shouldDefineUtils = true;
if (node.nodeName !== "t") { if (node.nodeName !== "t") {
throw new Error("Invalid tag for t-call directive (should be 't')"); throw new Error("Invalid tag for t-call directive (should be 't')");
} }
@@ -230,7 +228,7 @@ QWeb.addDirective({
// ------------------------------------------------ // ------------------------------------------------
if (!qweb.subTemplates[subTemplate]) { if (!qweb.subTemplates[subTemplate]) {
qweb.subTemplates[subTemplate] = true; 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; qweb.subTemplates[subTemplate] = subTemplateFn;
} }
@@ -264,17 +262,17 @@ QWeb.addDirective({
// Step 4: add the appropriate function call to current component // Step 4: add the appropriate function call to current component
// ------------------------------------------------ // ------------------------------------------------
const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)"; const callingScope = hasBody ? "scope" : "Object.assign(Object.create(context), scope)";
const parentComponent = `utils.getComponent(context)`;
const keyCode = ctx.loopNumber || ctx.hasKey0 ? `, key: ${ctx.generateTemplateKey()}` : "";
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}${keyCode}})`;
if (ctx.parentNode) { 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 { } else {
// this is a t-call with no parentnode, we need to extract the result // this is a t-call with no parentnode, we need to extract the result
ctx.rootContext.shouldDefineResult = true; ctx.rootContext.shouldDefineResult = true;
ctx.addLine(`result = []`); 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]`); ctx.addLine(`result = result[0]`);
} }
@@ -303,16 +301,16 @@ QWeb.addDirective({
const elems = node.getAttribute("t-foreach")!; const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!; const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID(); 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')}`); ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
let keysID = ctx.generateID(); let keysID = ctx.generateID();
let valuesID = ctx.generateID(); let valuesID = ctx.generateID();
ctx.addLine(`let _${keysID} = _${valuesID} = _${arrayID};`); ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
ctx.addIf(`!(_${arrayID} instanceof Array)`); ctx.addIf(`!(_${arrayID} instanceof Array)`);
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`); ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`); ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
ctx.closeIf(); ctx.closeIf();
ctx.addLine(`let _length${keysID} = _${keysID}.length;`); ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
let varsID = ctx.startProtectScope(); let varsID = ctx.startProtectScope();
const loopVar = `i${ctx.loopNumber}`; const loopVar = `i${ctx.loopNumber}`;
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`); ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
@@ -334,13 +332,6 @@ QWeb.addDirective({
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')` `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"); nodeCopy.removeAttribute("t-foreach");
qweb._compileNode(nodeCopy, ctx); qweb._compileNode(nodeCopy, ctx);
+16 -12
View File
@@ -26,13 +26,12 @@ export class CompilationContext {
templateName: string; templateName: string;
allowMultipleRoots: boolean = false; allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false; hasParentWidget: boolean = false;
hasKey0: boolean = false; currentKey: string = "";
keyStack: boolean[] = [];
constructor(name?: string) { constructor(name?: string) {
this.rootContext = this; this.rootContext = this;
this.templateName = name || "noname"; this.templateName = name || "noname";
this.addLine("let h = this.h;"); this.addLine("var h = this.h;");
} }
generateID(): number { generateID(): number {
@@ -49,15 +48,20 @@ export class CompilationContext {
*/ */
generateTemplateKey(prefix: string = ""): string { generateTemplateKey(prefix: string = ""): string {
const id = this.generateID(); const id = this.generateID();
if (this.loopNumber === 0 && !this.hasKey0) { if (this.loopNumber === 0 && !this.currentKey) {
return `'${prefix}__${id}__'`; return `'${prefix}__${id}__'`;
} }
let key = `\`${prefix}__${id}__`; let locationExpr = `\`${prefix}__${id}__`;
let start = this.hasKey0 ? 0 : 1; for (let i = 0; i < this.loopNumber - 1; i++) {
for (let i = start; i < this.loopNumber + 1; i++) { locationExpr += `\${i${i + 1}}__`;
key += `\${key${i}}__`; }
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}`; return `k${id}`;
} }
@@ -112,11 +116,11 @@ export class CompilationContext {
} }
indent() { indent() {
this.rootContext.indentLevel++; this.indentLevel++;
} }
dedent() { dedent() {
this.rootContext.indentLevel--; this.indentLevel--;
} }
addLine(line: string): number { addLine(line: string): number {
@@ -176,7 +180,7 @@ export class CompilationContext {
startProtectScope(): number { startProtectScope(): number {
const protectID = this.generateID(); const protectID = this.generateID();
this.rootContext.shouldDefineScope = true; this.rootContext.shouldDefineScope = true;
this.addLine(`let _origScope${protectID} = scope;`); this.addLine(`const _origScope${protectID} = scope;`);
this.addLine(`scope = Object.assign(Object.create(context), scope);`); this.addLine(`scope = Object.assign(Object.create(context), scope);`);
return protectID; return protectID;
} }
+1 -1
View File
@@ -81,7 +81,7 @@ const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
// note that the space after typeof is relevant. It makes sure that the formatted // note that the space after typeof is relevant. It makes sure that the formatted
// expression has a space after typeof // expression has a space after typeof
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;".split(","); const OPERATORS = ".,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;".split(",");
type Tokenizer = (expr: string) => Token | false; type Tokenizer = (expr: string) => Token | false;
+4 -15
View File
@@ -303,20 +303,9 @@ QWeb.addDirective({
QWeb.addDirective({ QWeb.addDirective({
name: "key", name: "key",
priority: 45, priority: 45,
atNodeEncounter({ ctx, value, node }) { atNodeEncounter({ ctx, value }) {
if (ctx.loopNumber === 0) { let id = ctx.generateID();
ctx.keyStack.push(ctx.rootContext.hasKey0); ctx.addLine(`const nodeKey${id} = ${ctx.formatExpression(value)};`);
ctx.rootContext.hasKey0 = true; ctx.currentKey = `nodeKey${id}`;
}
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;
}
} }
}); });
+15 -24
View File
@@ -119,7 +119,7 @@ const UTILS: Utils = {
return vnode.text; return vnode.text;
} }
}) })
.join(""); .join();
}, },
getComponent(obj) { getComponent(obj) {
while (obj && !obj.hasOwnProperty("__owl__")) { while (obj && !obj.hasOwnProperty("__owl__")) {
@@ -383,12 +383,7 @@ export class QWeb extends EventBus {
}); });
} }
_compile( _compile(name: string, elem: Element, parentContext?: CompilationContext): CompiledTemplate {
name: string,
elem: Element,
parentContext?: CompilationContext,
defineKey?: boolean
): CompiledTemplate {
const isDebug = elem.attributes.hasOwnProperty("t-debug"); const isDebug = elem.attributes.hasOwnProperty("t-debug");
const ctx = new CompilationContext(name); const ctx = new CompilationContext(name);
if (elem.tagName !== "t") { if (elem.tagName !== "t") {
@@ -401,10 +396,6 @@ export class QWeb extends EventBus {
ctx.hasParentWidget = true; ctx.hasParentWidget = true;
ctx.shouldDefineResult = false; ctx.shouldDefineResult = false;
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
if (defineKey) {
ctx.addLine(`let key0 = extra.key || "";`);
ctx.hasKey0 = true;
}
} }
this._compileNode(elem, ctx); this._compileNode(elem, ctx);
@@ -475,13 +466,16 @@ export class QWeb extends EventBus {
// this is an unusual situation: this text node is the result of the // this is an unusual situation: this text node is the result of the
// template rendering. // template rendering.
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`); ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.addLine(`result = vn${nodeID};`); ctx.addLine(`result = vn${nodeID};`);
ctx.rootContext.rootNode = nodeID; ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID; ctx.rootContext.parentTextNode = nodeID;
} }
return; return;
} }
if (ctx !== ctx.rootContext) {
ctx = ctx.subContext("currentKey", ctx.currentKey);
}
const firstLetter = node.tagName[0]; const firstLetter = node.tagName[0];
if (firstLetter === firstLetter.toUpperCase()) { if (firstLetter === firstLetter.toUpperCase()) {
@@ -684,7 +678,7 @@ export class QWeb extends EventBus {
ctx.addLine(`let ${classObj} = {${classDef}};`); ctx.addLine(`let ${classObj} = {${classDef}};`);
} }
} else { } else {
ctx.addLine(`let _${attID} = '${value}';`); ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) { if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it // attribute contains 'non letters' => we want to quote it
name = '"' + name + '"'; name = '"' + name + '"';
@@ -720,12 +714,12 @@ export class QWeb extends EventBus {
const attValue = (<Element>node).getAttribute(attName); const attValue = (<Element>node).getAttribute(attName);
if (attValue) { if (attValue) {
const attValueID = ctx.generateID(); const attValueID = ctx.generateID();
ctx.addLine(`let _${attValueID} = ${formattedValue};`); ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":")); const attrIndex = attrs.findIndex(att => att.startsWith(attName + ":"));
attrs.splice(attrIndex, 1); attrs.splice(attrIndex, 1);
} }
ctx.addLine(`let _${attID} = ${formattedValue};`); ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID); handleBooleanProps(attName, attID);
} }
@@ -741,9 +735,9 @@ export class QWeb extends EventBus {
const attID = ctx.generateID(); const attID = ctx.generateID();
let staticVal = (<Element>node).getAttribute(attName); let staticVal = (<Element>node).getAttribute(attName);
if (staticVal) { if (staticVal) {
ctx.addLine(`let _${attID} = '${staticVal} ' + ${formattedExpr};`); ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
} else { } else {
ctx.addLine(`let _${attID} = ${formattedExpr};`); ctx.addLine(`var _${attID} = ${formattedExpr};`);
} }
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
} }
@@ -751,13 +745,13 @@ export class QWeb extends EventBus {
// t-att= attributes // t-att= attributes
if (name === "t-att") { if (name === "t-att") {
let id = ctx.generateID(); let id = ctx.generateID();
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`); ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id); tattrs.push(id);
} }
} }
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
let key = ctx.loopNumber || ctx.hasKey0 ? `\`\${key${ctx.loopNumber}}_${nodeID}\`` : nodeID; let nodeKey = ctx.currentKey || nodeID;
const parts = [`key:${key}`]; const parts = [`key:${nodeKey}`];
if (attrs.length + tattrs.length > 0) { if (attrs.length + tattrs.length > 0) {
parts.push(`attrs:{${attrs.join(",")}}`); parts.push(`attrs:{${attrs.join(",")}}`);
} }
@@ -783,12 +777,9 @@ export class QWeb extends EventBus {
ctx.addLine(`}`); ctx.addLine(`}`);
ctx.closeIf(); 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) { if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); 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; return nodeID;
+1 -4
View File
@@ -132,7 +132,7 @@ export function useStore(selector, options: SelectorOptions = {}): any {
__destroy.call(component, parent); __destroy.call(component, parent);
}; };
if (typeof result !== "object" || result === null) { if (typeof result !== "object") {
return result; return result;
} }
return new Proxy(result, { return new Proxy(result, {
@@ -141,9 +141,6 @@ export function useStore(selector, options: SelectorOptions = {}): any {
}, },
set(target, k, v) { set(target, k, v) {
throw new Error("Store state should only be modified through actions"); 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 { QWeb } from "./qweb/index";
import { registerSheet } from "./component/styles";
/** /**
* Owl Tags * Owl Tags
@@ -26,19 +25,3 @@ export function xml(strings, ...args) {
QWeb.registerTemplate(name, value); QWeb.registerTemplate(name, value);
return name; 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 QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
// Component 'Child' // Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {}; let props2 = {};
@@ -52,9 +52,9 @@ exports[`animations t-transition combined with t-component and t-if 1`] = `
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (scope['state'].display) { if (scope['state'].display) {
// Component 'Child' // Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; 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 QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (scope['state'].flag) { if (scope['state'].flag) {
// Component 'Child' // Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; 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\\" // Template name: \\"test\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
utils.transitionInsert(vn, 'jupiler'); utils.transitionInsert(vn, 'jupiler');
@@ -162,9 +162,9 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); var vn1 = h('span', p1, c1);
p1.hook = { p1.hook = {
insert: vn => { insert: vn => {
utils.transitionInsert(vn, 'chimay'); 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 QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
// Component 'Child' // Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {message:1}; let props2 = {message:1};
+122 -122
View File
@@ -8,9 +8,9 @@ exports[`t-slot directive can define and call slots 1`] = `
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
// Component 'Dialog' // Component 'Dialog'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {}; let props2 = {};
@@ -43,18 +43,18 @@ exports[`t-slot directive can define and call slots 2`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"Dialog\\" // Template name: \\"Dialog\\"
let h = this.h; var h = this.h;
let c6 = [], p6 = {key:6}; let c6 = [], p6 = {key:6};
let vn6 = h('div', p6, c6); var vn6 = h('div', p6, c6);
let c7 = [], p7 = {key:7}; let c7 = [], p7 = {key:7};
let vn7 = h('div', p7, c7); var vn7 = h('div', p7, c7);
c6.push(vn7); c6.push(vn7);
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header']; const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot8) { if (slot8) {
slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context})); slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c7, parent: extra.parent || context}));
} }
let c9 = [], p9 = {key:9}; let c9 = [], p9 = {key:9};
let vn9 = h('div', p9, c9); var vn9 = h('div', p9, c9);
c6.push(vn9); c6.push(vn9);
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer']; const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
if (slot10) { if (slot10) {
@@ -68,10 +68,10 @@ exports[`t-slot directive can define and call slots 3`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_header_template\\" // Template name: \\"slot_header_template\\"
let h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4); var vn4 = h('span', p4, c4);
c1.push(vn4); c1.push(vn4);
c4.push({text: \`header\`}); c4.push({text: \`header\`});
}" }"
@@ -81,10 +81,10 @@ exports[`t-slot directive can define and call slots 4`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_footer_template\\" // Template name: \\"slot_footer_template\\"
let h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5); var vn5 = h('span', p5, c5);
c1.push(vn5); c1.push(vn5);
c5.push({text: \`footer\`}); c5.push({text: \`footer\`});
}" }"
@@ -94,10 +94,10 @@ exports[`t-slot directive content is the default slot 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4); var vn4 = h('span', p4, c4);
c1.push(vn4); c1.push(vn4);
c4.push({text: \`sts rocks\`}); 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 "function anonymous(context, extra
) { ) {
// Template name: \\"__template__1\\" // Template name: \\"__template__1\\"
let h = this.h; var h = this.h;
let c4 = [], p4 = {key:4}; 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']; const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot5) { if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context})); 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 "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
c1.push({text: \`sts rocks\`}); 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 "function anonymous(context, extra
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4); var vn4 = h('span', p4, c4);
c1.push(vn4); c1.push(vn4);
c4.push({text: \`sts\`}); c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5); var vn5 = h('span', p5, c5);
c1.push(vn5); c1.push(vn5);
c5.push({text: \`rocks\`}); 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 "function anonymous(context, extra
) { ) {
// Template name: \\"slot_content_template\\" // Template name: \\"slot_content_template\\"
let h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
let c4 = [], p4 = {key:4}; let c4 = [], p4 = {key:4};
let vn4 = h('span', p4, c4); var vn4 = h('span', p4, c4);
c1.push(vn4); c1.push(vn4);
c4.push({text: \`sts\`}); c4.push({text: \`sts\`});
let c5 = [], p5 = {key:5}; let c5 = [], p5 = {key:5};
let vn5 = h('span', p5, c5); var vn5 = h('span', p5, c5);
c1.push(vn5); c1.push(vn5);
c5.push({text: \`rocks\`}); c5.push({text: \`rocks\`});
}" }"
@@ -168,9 +168,9 @@ exports[`t-slot directive named slots can define a default content 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"__template__1\\" // Template name: \\"__template__1\\"
let h = this.h; var h = this.h;
let c4 = [], p4 = {key:4}; 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']; const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
if (slot5) { if (slot5) {
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context})); 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\\" // Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
context.__owl__.refs = context.__owl__.refs || {}; context.__owl__.refs = context.__owl__.refs || {};
let h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}}; let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8); var vn8 = h('button', p8, c8);
c1.push(vn8); c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);}; 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__']; 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\\" // Template name: \\"slot_footer_template\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
let c8 = [], p8 = {key:8,on:{}}; let c8 = [], p8 = {key:8,on:{}};
let vn8 = h('button', p8, c8); var vn8 = h('button', p8, c8);
c1.push(vn8); c1.push(vn8);
extra.handlers['click__9__'] = extra.handlers['click__9__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);}; 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__']; 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\\" // Template name: \\"Link\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let _11 = scope['props'].to; var _12 = scope['props'].to;
let c12 = [], p12 = {key:12,attrs:{href: _11}}; let c13 = [], p13 = {key:13,attrs:{href: _12}};
let vn12 = h('a', p12, c12); var vn13 = h('a', p13, c13);
const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot13) { if (slot14) {
slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context})); slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context}));
} }
return vn12; return vn13;
}" }"
`; `;
@@ -248,21 +248,21 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
let vn2 = h('u', p2, c2); var vn2 = h('u', p2, c2);
c1.push(vn2); c1.push(vn2);
let _3 = scope['state'].users; var _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')} if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3; var _4 = _5 = _3;
if (!(_3 instanceof Array)) { if (!(_3 instanceof Array)) {
_4 = Object.keys(_3); _4 = Object.keys(_3);
_5 = Object.values(_3); _5 = Object.values(_3);
} }
let _length4 = _4.length; var _length4 = _4.length;
let _origScope6 = scope; const _origScope6 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.assign(Object.create(context), scope);
for (let i1 = 0; i1 < _length4; i1++) { for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0 scope.user_first = i1 === 0
@@ -270,35 +270,35 @@ exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
scope.user_index = i1 scope.user_index = i1
scope.user = _4[i1] scope.user = _4[i1]
scope.user_value = _5[i1] scope.user_value = _5[i1]
let key1 = scope['user'].id; const nodeKey7 = scope['user'].id;
let c7 = [], p7 = {key:\`\${key1}_7\`}; let c8 = [], p8 = {key:nodeKey7};
let vn7 = h('li', p7, c7); var vn8 = h('li', p8, c8);
c2.push(vn7); c2.push(vn8);
// Component 'Link' // Component 'Link'
let k9 = \`__9__\${key1}__\`; let k10 = \`__10__\` + nodeKey7;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false; let w9 = k10 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k10]] : false;
let props8 = {to:'/user/'+scope['user'].id}; let props9 = {to:'/user/'+scope['user'].id};
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) { if (w9 && w9.__owl__.currentFiber && !w9.__owl__.vnode) {
w8.destroy(); w9.destroy();
w8 = false; w9 = false;
} }
if (w8) { if (w9) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope)); w9.__updateProps(props9, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode; let pvnode = w9.__owl__.pvnode;
c7.push(pvnode); c8.push(pvnode);
} else { } else {
let componentKey8 = \`Link\`; let componentKey9 = \`Link\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link']; let W9 = context.constructor.components[componentKey9] || QWeb.components[componentKey9]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')} if (!W9) {throw new Error('Cannot find the definition of component \\"' + componentKey9 + '\\"')}
w8 = new W8(parent, props8); w9 = new W9(parent, props9);
parent.__owl__.cmap[k9] = w8.__owl__.id; parent.__owl__.cmap[k10] = w9.__owl__.id;
w8.__owl__.slotId = 1; w9.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w9.__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();}}}); let pvnode = h('dummy', {key: k10, hook: {remove() {},destroy(vn) {w9.destroy();}}});
c7.push(pvnode); c8.push(pvnode);
w8.__owl__.pvnode = pvnode; w9.__owl__.pvnode = pvnode;
} }
w8.__owl__.parentLastFiberId = extra.fiber.id; w9.__owl__.parentLastFiberId = extra.fiber.id;
} }
scope = _origScope6; scope = _origScope6;
return vn1; return vn1;
@@ -310,12 +310,12 @@ exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c7 = extra.parentNode; let c8 = extra.parentNode;
c7.push({text: \`User \`}); c8.push({text: \`User \`});
let _10 = scope['user'].name; var _11 = scope['user'].name;
if (_10 != null) { if (_11 || _11 === 0) {
c7.push({text: _10}); c8.push({text: _11});
} }
}" }"
`; `;
@@ -325,15 +325,15 @@ exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
) { ) {
// Template name: \\"Link\\" // Template name: \\"Link\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let _10 = scope['props'].to; var _11 = scope['props'].to;
let c11 = [], p11 = {key:11,attrs:{href: _10}}; let c12 = [], p12 = {key:12,attrs:{href: _11}};
let vn11 = h('a', p11, c11); var vn12 = h('a', p12, c12);
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot13 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot12) { if (slot13) {
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context})); slot13.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c12, parent: extra.parent || context}));
} }
return vn11; return vn12;
}" }"
`; `;
@@ -345,21 +345,21 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
let QWeb = this.constructor; let QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let c2 = [], p2 = {key:2}; let c2 = [], p2 = {key:2};
let vn2 = h('u', p2, c2); var vn2 = h('u', p2, c2);
c1.push(vn2); c1.push(vn2);
let _3 = scope['state'].users; var _3 = scope['state'].users;
if (!_3) { throw new Error('QWeb error: Invalid loop expression')} if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
let _4 = _5 = _3; var _4 = _5 = _3;
if (!(_3 instanceof Array)) { if (!(_3 instanceof Array)) {
_4 = Object.keys(_3); _4 = Object.keys(_3);
_5 = Object.values(_3); _5 = Object.values(_3);
} }
let _length4 = _4.length; var _length4 = _4.length;
let _origScope6 = scope; const _origScope6 = scope;
scope = Object.assign(Object.create(context), scope); scope = Object.assign(Object.create(context), scope);
for (let i1 = 0; i1 < _length4; i1++) { for (let i1 = 0; i1 < _length4; i1++) {
scope.user_first = i1 === 0 scope.user_first = i1 === 0
@@ -367,36 +367,36 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
scope.user_index = i1 scope.user_index = i1
scope.user = _4[i1] scope.user = _4[i1]
scope.user_value = _5[i1] scope.user_value = _5[i1]
let key1 = scope['user'].id; const nodeKey7 = scope['user'].id;
let c7 = [], p7 = {key:\`\${key1}_7\`}; let c8 = [], p8 = {key:nodeKey7};
let vn7 = h('li', p7, c7); var vn8 = h('li', p8, c8);
c2.push(vn7); c2.push(vn8);
scope.userdescr = 'User '+scope['user'].name; scope.userdescr = 'User '+scope['user'].name;
// Component 'Link' // Component 'Link'
let k9 = \`__9__\${key1}__\`; let k10 = \`__10__\` + nodeKey7;
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false; let w9 = k10 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k10]] : false;
let props8 = {to:'/user/'+scope['user'].id}; let props9 = {to:'/user/'+scope['user'].id};
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) { if (w9 && w9.__owl__.currentFiber && !w9.__owl__.vnode) {
w8.destroy(); w9.destroy();
w8 = false; w9 = false;
} }
if (w8) { if (w9) {
w8.__updateProps(props8, extra.fiber, Object.assign(Object.create(context), scope)); w9.__updateProps(props9, extra.fiber, Object.assign(Object.create(context), scope));
let pvnode = w8.__owl__.pvnode; let pvnode = w9.__owl__.pvnode;
c7.push(pvnode); c8.push(pvnode);
} else { } else {
let componentKey8 = \`Link\`; let componentKey9 = \`Link\`;
let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| scope['Link']; let W9 = context.constructor.components[componentKey9] || QWeb.components[componentKey9]|| scope['Link'];
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')} if (!W9) {throw new Error('Cannot find the definition of component \\"' + componentKey9 + '\\"')}
w8 = new W8(parent, props8); w9 = new W9(parent, props9);
parent.__owl__.cmap[k9] = w8.__owl__.id; parent.__owl__.cmap[k10] = w9.__owl__.id;
w8.__owl__.slotId = 1; w9.__owl__.slotId = 1;
let fiber = w8.__prepare(extra.fiber, Object.assign(Object.create(context), scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); let fiber = w9.__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();}}}); let pvnode = h('dummy', {key: k10, hook: {remove() {},destroy(vn) {w9.destroy();}}});
c7.push(pvnode); c8.push(pvnode);
w8.__owl__.pvnode = pvnode; w9.__owl__.pvnode = pvnode;
} }
w8.__owl__.parentLastFiberId = extra.fiber.id; w9.__owl__.parentLastFiberId = extra.fiber.id;
} }
scope = _origScope6; scope = _origScope6;
return vn1; return vn1;
@@ -408,10 +408,10 @@ exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
) { ) {
// Template name: \\"slot_default_template\\" // Template name: \\"slot_default_template\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c7 = extra.parentNode; let c8 = extra.parentNode;
if (scope.userdescr != null) { if (scope.userdescr || scope.userdescr === 0) {
c7.push({text: scope.userdescr}); 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 QWeb = this.constructor;
let parent = context; let parent = context;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
scope.userdescr = 'User '+scope['state'].user.name; scope.userdescr = 'User '+scope['state'].user.name;
// Component 'Link' // Component 'Link'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; 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\\" // Template name: \\"slot_default_template\\"
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let c1 = extra.parentNode; let c1 = extra.parentNode;
if (scope.userdescr != null) { if (scope.userdescr || scope.userdescr === 0) {
c1.push({text: scope.userdescr}); c1.push({text: scope.userdescr});
} }
}" }"
@@ -475,7 +475,7 @@ exports[`t-slot directive template can just return a slot 1`] = `
// Template name: \\"__template__2\\" // Template name: \\"__template__2\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let result; let result;
let h = this.h; var h = this.h;
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
if (slot6) { if (slot6) {
let children7= [] let children7= []
+2 -134
View File
@@ -147,21 +147,6 @@ describe("basic widget properties", () => {
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>"); 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 () => { test("cannot be clicked on and updated if not in DOM", async () => {
class Counter extends Component<any, any> { class Counter extends Component<any, any> {
static template = xml` static template = xml`
@@ -337,36 +322,6 @@ describe("basic widget properties", () => {
console.warn = warn; 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 () => { test("same t-keys in two different places", async () => {
class Child extends Component<any, any> { class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.blip"/></span>`; static template = xml`<span><t t-esc="props.blip"/></span>`;
@@ -384,7 +339,6 @@ describe("basic widget properties", () => {
const widget = new Parent(); const widget = new Parent();
await widget.mount(fixture); await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>1</span></div><div><span>2</span></div></div>"); 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 () => { test("t-key on a component with t-if, and a sibling component", async () => {
@@ -4464,28 +4418,6 @@ describe("t-model directive", () => {
expect(comp.state[2].f).toBe(false); expect(comp.state[2].f).toBe(false);
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); 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", () => { describe("environment and plugins", () => {
@@ -4588,7 +4520,7 @@ describe("component error handling (catchError)", () => {
} }
const app = new App(); const app = new App();
await app.mount(fixture); 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; app.state.flag = true;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>"); expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
@@ -4623,7 +4555,7 @@ describe("component error handling (catchError)", () => {
} }
const app = new App(); const app = new App();
await app.mount(fixture); 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; app.state.flag = true;
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
@@ -5645,70 +5577,6 @@ describe("t-call", () => {
expect(env.qweb.subTemplates["sub"].toString()).toMatchSnapshot(); 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 () => { test("handlers with arguments are properly bound through a t-call", async () => {
expect.assertions(3); expect.assertions(3);
env.qweb.addTemplate("sub", `<p t-on-click="update(a)">lucas</p>`); env.qweb.addTemplate("sub", `<p t-on-click="update(a)">lucas</p>`);
+7
View File
@@ -19,6 +19,13 @@ let env: Env;
beforeEach(() => { beforeEach(() => {
fixture = makeTestFixture(); fixture = makeTestFixture();
env = makeTestEnv(); 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; Component.env = env;
}); });
-132
View File
@@ -1,132 +0,0 @@
import { Component, Env } from "../../src/component/component";
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;
}`);
});
});
+40
View File
@@ -429,4 +429,44 @@ describe("observer", () => {
obj.a.push(2); obj.a.push(2);
}).toThrow('Observed state cannot be changed here! (key: "1", val: "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']);
});
}); });
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>`); qweb.addTemplate("test", `<span t-esc="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>nope</span>"); expect(renderToString(qweb, "test")).toBe("<span>nope</span>");
}); });
test("t-esc is escaped", () => { test("t-esc is escaped", () => {
qweb.addTemplate("test", `<div><t t-set="var"><p>escaped</p></t><t t-esc="var"/></div>`); qweb.addTemplate("test", `<div><t t-set="var"><p>escaped</p></t><t t-esc="var"/></div>`);
const domRendered = renderToDOM(qweb, "test"); const domRendered = renderToDOM(qweb, "test");
expect(domRendered.textContent).toBe("<p>escaped</p>"); expect(domRendered.textContent).toBe("<p>escaped</p>");
}); });
test("t-esc=0 is escaped", () => { test("t-esc=0 is escaped", () => {
qweb.addTemplate("test", `<span><t t-esc="0"/></span>`); qweb.addTemplate("test", `<span><t t-esc="0"/></span>`);
qweb.addTemplate("testCaller", `<div><t t-call="test"><p>escaped</p></t></div>`); qweb.addTemplate("testCaller", `<div><t t-call="test"><p>escaped</p></t></div>`);
const domRendered = renderToDOM(qweb, "testCaller") as HTMLElement; const domRendered = renderToDOM(qweb, "testCaller") as HTMLElement;
expect(domRendered.querySelector("span")!.textContent).toBe("<p>escaped</p>"); 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", () => { 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", () => { test("evaluate value expression, part 2", () => {
qweb.addTemplate( qweb.addTemplate(
"test", "test",
-12
View File
@@ -50,12 +50,6 @@ describe("tokenizer", () => {
{ type: "OPERATOR", value: "typeof " }, { type: "OPERATOR", value: "typeof " },
{ type: "SYMBOL", value: "a" } { type: "SYMBOL", value: "a" }
]); ]);
expect(tokenize("a...1")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "OPERATOR", value: "..." },
{ type: "VALUE", value: "1" }
]);
}); });
test("strings", () => { test("strings", () => {
@@ -183,10 +177,4 @@ describe("expression evaluation", () => {
expect(compileExpr("a -= b", {})).toBe("scope['a']-=scope['b']"); expect(compileExpr("a -= b", {})).toBe("scope['a']-=scope['b']");
expect(compileExpr("a.b = !a.b", {})).toBe("scope['a'].b=!scope['a'].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\\" // Template name: \\"__template__1\\"
let utils = this.constructor.utils; let utils = this.constructor.utils;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; var h = this.h;
let _4 = utils.toObj({'router-link-active':scope['isActive']}); 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 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);}; 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__']; p6.on['click'] = extra.handlers['click__7__'];
const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
@@ -9,37 +9,35 @@ exports[`RouteComponent can render simple cases 1`] = `
let parent = context; let parent = context;
let scope = Object.create(context); let scope = Object.create(context);
let result; let result;
let h = this.h; var h = this.h;
if (scope['routeComponent']) { if (scope['routeComponent']) {
{ const nodeKey4 = scope['env'].router.currentRouteName;
let key0 = scope['env'].router.currentRouteName; // Component 'routeComponent'
// Component 'routeComponent' let k6 = \`__6__\` + nodeKey4;
let k5 = \`__5__\${key0}__\`; let w5 = k6 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k6]] : false;
let w4 = k5 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k5]] : false; let vn7 = {};
let vn6 = {}; result = vn7;
result = vn6; let props5 = Object.assign({}, scope['env'].router.currentParams);
let props4 = Object.assign({}, scope['env'].router.currentParams); if (w5 && w5.__owl__.currentFiber && !w5.__owl__.vnode) {
if (w4 && w4.__owl__.currentFiber && !w4.__owl__.vnode) { w5.destroy();
w4.destroy(); w5 = false;
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;
} }
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; 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 () => { test("useStore can observe primitive types and call onUpdate", async () => {
const state = { isBoolean: false, nullValue: null }; const state = { isBoolean: false };
const actions = { const actions = {
setTrue({ state }) { setTrue({ state }) {
state.isBoolean = true; state.isBoolean = true;
},
setNotNull({ state }) {
state.nullValue = "ok";
} }
}; };
const store = new Store({ state, actions }); const store = new Store({ state, actions });
@@ -64,10 +61,8 @@ describe("connecting a component to store", () => {
static template = xml` static template = xml`
<div> <div>
<span t-if="isBoolean">ok</span> <span t-if="isBoolean">ok</span>
<span t-if="nullValue !== null">not null</span>
</div>`; </div>`;
isBoolean: boolean; isBoolean: boolean;
nullValue: string;
constructor() { constructor() {
super(); super();
this.isBoolean = useStore(state => state.isBoolean, { this.isBoolean = useStore(state => state.isBoolean, {
@@ -75,11 +70,6 @@ describe("connecting a component to store", () => {
this.isBoolean = isBoolean; 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"); store.dispatch("setTrue");
await nextTick(); await nextTick();
expect(fixture.innerHTML).toBe("<div><span>ok</span></div>"); 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 () => { test("throw error if no store is found", async () => {