mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2042f2ec01 | |||
| 37313c47a3 | |||
| 3f563de9c0 | |||
| 97564a7612 | |||
| f1abf7f2ea | |||
| 8464a1b04e | |||
| c0a62dfd05 | |||
| 600f1e35d4 | |||
| b3181f119d | |||
| 1e1e05350a | |||
| b242230e20 | |||
| 9fe2da704e | |||
| 21b1661d39 | |||
| ec05b1f5e3 | |||
| 8083678f03 | |||
| 61c2ec5d83 | |||
| e579a993fd | |||
| ea3c6f7bf0 | |||
| 9cfafc30b5 | |||
| ad42c583c6 |
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [10.x, 12.x, 14.x]
|
node-version: [12.x, 14.x, 16.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
+3
-1
@@ -31,4 +31,6 @@ release-notes.md
|
|||||||
.rpt2_cache
|
.rpt2_cache
|
||||||
|
|
||||||
# useful in some cases
|
# useful in some cases
|
||||||
/temp
|
/temp
|
||||||
|
|
||||||
|
coverage
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1>
|
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1>
|
||||||
|
|
||||||
|
[](https://www.gnu.org/licenses/lgpl-3.0)
|
||||||
|
[](https://badge.fury.io/js/@odoo%2Fowl)
|
||||||
|
[](https://www.npmjs.com/package/@odoo/owl)
|
||||||
|
|
||||||
_Class based components with hooks, reactive state and concurrent mode_
|
_Class based components with hooks, reactive state and concurrent mode_
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
@@ -120,7 +124,7 @@ 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.4.0](https://github.com/odoo/owl/releases/tag/v1.4.0)
|
- [owl-1.4.5](https://github.com/odoo/owl/releases/tag/v1.4.5)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ just put the following code:
|
|||||||
Note that we put everything inside an immediately executed function to avoid leaking
|
Note that we put everything inside an immediately executed function to avoid leaking
|
||||||
anything to the global scope.
|
anything to the global scope.
|
||||||
|
|
||||||
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer).
|
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files
|
||||||
|
are built to run directly on the browser (other files such as `owl.cjs.js` are
|
||||||
|
built to be bundled by other tools).
|
||||||
|
|
||||||
Now, the project should be ready. Loading the `index.html` file into a browser
|
Now, the project should be ready. Loading the `index.html` file into a browser
|
||||||
should show an empty page, with the title `Owl Todo App`, and it should log a
|
should show an empty page, with the title `Owl Todo App`, and it should log a
|
||||||
|
|||||||
+12
-9
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "1.4.0",
|
"version": "1.4.5",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"browser": "dist/owl.iife.js",
|
"browser": "dist/owl.iife.js",
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.15.3"
|
"node": ">=12.18.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:bundle": "rollup -c",
|
"build:bundle": "rollup -c",
|
||||||
@@ -37,28 +37,29 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/odoo/owl#readme",
|
"homepage": "https://github.com/odoo/owl#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^23.3.14",
|
"@types/jest": "^27.0.1",
|
||||||
"@types/node": "^14.11.8",
|
"@types/node": "^14.11.8",
|
||||||
"chalk": "^3.0.0",
|
"chalk": "^3.0.0",
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"current-git-branch": "^1.1.0",
|
"current-git-branch": "^1.1.0",
|
||||||
"git-rev-sync": "^1.12.0",
|
"git-rev-sync": "^1.12.0",
|
||||||
"github-api": "^3.3.0",
|
"github-api": "^3.3.0",
|
||||||
"jest": "^23.6.0",
|
"jest": "^27.1.0",
|
||||||
"jest-environment-jsdom": "^24.7.1",
|
"jest-environment-jsdom": "^27.1.0",
|
||||||
"live-server": "^1.2.1",
|
"live-server": "^1.2.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.0.4",
|
"prettier": "^2.0.4",
|
||||||
"rollup": "^1.6.0",
|
"rollup": "^2.56.3",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"rollup-plugin-typescript2": "^0.27.3",
|
"rollup-plugin-typescript2": "^0.30.0",
|
||||||
"sass": "^1.16.1",
|
"sass": "^1.16.1",
|
||||||
"source-map-support": "^0.5.10",
|
"source-map-support": "^0.5.10",
|
||||||
"ts-jest": "^23.10.5",
|
"ts-jest": "^27.0.5",
|
||||||
"typescript": "^3.7.2",
|
"typescript": "^3.7.2",
|
||||||
"uglify-es": "^3.3.9"
|
"uglify-es": "^3.3.9"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
"roots": [
|
"roots": [
|
||||||
"<rootDir>/src",
|
"<rootDir>/src",
|
||||||
"<rootDir>/tests"
|
"<rootDir>/tests"
|
||||||
@@ -75,7 +76,9 @@
|
|||||||
"jsx",
|
"jsx",
|
||||||
"json",
|
"json",
|
||||||
"node"
|
"node"
|
||||||
]
|
],
|
||||||
|
"collectCoverage": true,
|
||||||
|
"coverageReporters": ["html"]
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# 🦉 OWL Roadmap 🦉
|
# 🦉 OWL Roadmap 🦉
|
||||||
|
|
||||||
- Current version: 1.4.0
|
- Current version: 1.4.5
|
||||||
- Status: stable
|
- Status: 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
|
||||||
|
|||||||
+5
-1
@@ -1,4 +1,5 @@
|
|||||||
import { QWeb } from "./qweb/index";
|
import { QWeb } from "./qweb/index";
|
||||||
|
import { TRANSLATABLE_ATTRS } from "./qweb/qweb";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file creates and exports the OWL 'config' object, with keys:
|
* This file creates and exports the OWL 'config' object, with keys:
|
||||||
@@ -9,9 +10,12 @@ import { QWeb } from "./qweb/index";
|
|||||||
interface Config {
|
interface Config {
|
||||||
mode: string;
|
mode: string;
|
||||||
enableTransitions: boolean;
|
enableTransitions: boolean;
|
||||||
|
translatableAttributes: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {} as Config;
|
export const config = {
|
||||||
|
translatableAttributes: TRANSLATABLE_ATTRS,
|
||||||
|
} as Config;
|
||||||
|
|
||||||
Object.defineProperty(config, "mode", {
|
Object.defineProperty(config, "mode", {
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -257,16 +257,33 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
|
|||||||
const tokens = tokenize(expr);
|
const tokens = tokenize(expr);
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
let stack = []; // to track last opening [ or {
|
||||||
|
|
||||||
while (i < tokens.length) {
|
while (i < tokens.length) {
|
||||||
let token = tokens[i];
|
let token = tokens[i];
|
||||||
let prevToken = tokens[i - 1];
|
let prevToken = tokens[i - 1];
|
||||||
let nextToken = tokens[i + 1];
|
let nextToken = tokens[i + 1];
|
||||||
|
let groupType = stack[stack.length - 1];
|
||||||
|
|
||||||
|
switch (token.type) {
|
||||||
|
case "LEFT_BRACE":
|
||||||
|
case "LEFT_BRACKET":
|
||||||
|
stack.push(token.type);
|
||||||
|
break;
|
||||||
|
case "RIGHT_BRACE":
|
||||||
|
case "RIGHT_BRACKET":
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
|
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
|
||||||
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
|
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
|
||||||
if (prevToken) {
|
if (prevToken) {
|
||||||
// normalize missing tokens: {a} should be equivalent to {a:a}
|
// normalize missing tokens: {a} should be equivalent to {a:a}
|
||||||
if (isLeftSeparator(prevToken) && isRightSeparator(nextToken)) {
|
if (
|
||||||
|
groupType === "LEFT_BRACE" &&
|
||||||
|
isLeftSeparator(prevToken) &&
|
||||||
|
isRightSeparator(nextToken)
|
||||||
|
) {
|
||||||
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
|
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
|
||||||
nextToken = tokens[i + 1];
|
nextToken = tokens[i + 1];
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-8
@@ -66,7 +66,7 @@ interface QWebConfig {
|
|||||||
// Const/global stuff/helpers
|
// Const/global stuff/helpers
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||||
|
|
||||||
const lineBreakRE = /[\r\n]/;
|
const lineBreakRE = /[\r\n]/;
|
||||||
const whitespaceRE = /\s+/g;
|
const whitespaceRE = /\s+/g;
|
||||||
@@ -127,20 +127,35 @@ const UTILS: Utils = {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
// this is already an object, but we may need to split keys:
|
// this is already an object, but we may need to split keys:
|
||||||
// {'a': true, 'b c': true} should become {a: true, b: true, c: true}
|
// {'a b': true, 'a c': false} should become {a: true, b: true, c: false}
|
||||||
for (let key in expr) {
|
for (let key in expr) {
|
||||||
const value = expr[key];
|
const value = expr[key];
|
||||||
const words = key.split(/\s+/);
|
const words = key.split(/\s+/);
|
||||||
for (let word of words) {
|
for (let word of words) {
|
||||||
result[word] = value;
|
result[word] = result[word] || value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* This method combines the current context with the variables defined in a
|
||||||
|
* scope for use in a slot.
|
||||||
|
*
|
||||||
|
* The implementation is kind of tricky because we want to preserve the
|
||||||
|
* prototype chain structure of the cloned result. So we need to traverse the
|
||||||
|
* prototype chain, cloning each level respectively.
|
||||||
|
*/
|
||||||
combine(context, scope) {
|
combine(context, scope) {
|
||||||
const clone = Object.create(context);
|
let clone = context;
|
||||||
for (let k in scope) {
|
const scopeStack = [];
|
||||||
clone[k] = scope[k];
|
while (!isComponent(scope)) {
|
||||||
|
scopeStack.push(scope);
|
||||||
|
scope = scope.__proto__;
|
||||||
|
}
|
||||||
|
while (scopeStack.length) {
|
||||||
|
let scope = scopeStack.pop();
|
||||||
|
clone = Object.create(clone);
|
||||||
|
Object.assign(clone, scope);
|
||||||
}
|
}
|
||||||
return clone;
|
return clone;
|
||||||
},
|
},
|
||||||
@@ -314,6 +329,9 @@ export class QWeb extends EventBus {
|
|||||||
* template, with the name given by the t-name attribute.
|
* template, with the name given by the t-name attribute.
|
||||||
*/
|
*/
|
||||||
addTemplates(xmlstr: string | Document) {
|
addTemplates(xmlstr: string | Document) {
|
||||||
|
if (!xmlstr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
|
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
|
||||||
const templates = doc.getElementsByTagName("templates")[0];
|
const templates = doc.getElementsByTagName("templates")[0];
|
||||||
if (!templates) {
|
if (!templates) {
|
||||||
@@ -542,7 +560,11 @@ export class QWeb extends EventBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
|
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
|
||||||
const tCallNode = document.createElement("t");
|
const tCallNode = document.implementation.createDocument(
|
||||||
|
"http://www.w3.org/1999/xhtml",
|
||||||
|
"t",
|
||||||
|
null
|
||||||
|
).documentElement;
|
||||||
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
|
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
|
||||||
node.removeAttribute("t-call");
|
node.removeAttribute("t-call");
|
||||||
node.prepend(tCallNode);
|
node.prepend(tCallNode);
|
||||||
@@ -578,7 +600,11 @@ export class QWeb extends EventBus {
|
|||||||
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
||||||
}
|
}
|
||||||
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
|
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
|
||||||
const tNode = document.createElement("t");
|
const tNode = document.implementation.createDocument(
|
||||||
|
"http://www.w3.org/1999/xhtml",
|
||||||
|
"t",
|
||||||
|
null
|
||||||
|
).documentElement;
|
||||||
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
|
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
|
||||||
for (let child of Array.from(node.childNodes)) {
|
for (let child of Array.from(node.childNodes)) {
|
||||||
tNode.appendChild(child);
|
tNode.appendChild(child);
|
||||||
|
|||||||
+2
-1
@@ -231,7 +231,8 @@ function updateClass(oldVnode: VNode, vnode: VNode): void {
|
|||||||
elm = vnode.elm as Element;
|
elm = vnode.elm as Element;
|
||||||
|
|
||||||
for (name in oldClass) {
|
for (name in oldClass) {
|
||||||
if (name && !klass[name]) {
|
if (name && !klass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) {
|
||||||
|
// was `true` and now not provided
|
||||||
elm.classList.remove(name);
|
elm.classList.remove(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,7 +265,8 @@ describe("class and style attributes with t-component", () => {
|
|||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
expect(error).toBeDefined();
|
expect(error).toBeDefined();
|
||||||
expect(error.message).toBe("Cannot read property 'crash' of undefined");
|
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
||||||
|
expect(error.message).toMatch(regexp);
|
||||||
expect(fixture.innerHTML).toBe("");
|
expect(fixture.innerHTML).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ describe("component error handling (catchError)", () => {
|
|||||||
try {
|
try {
|
||||||
await super.render();
|
await super.render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe("Cannot read property 'this' of undefined");
|
expect(e.message).toMatch(
|
||||||
|
/Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,7 +361,8 @@ describe("component error handling (catchError)", () => {
|
|||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
expect(error).toBeDefined();
|
expect(error).toBeDefined();
|
||||||
expect(error.message).toBe("Cannot read property 'crash' of undefined");
|
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
||||||
|
expect(error.message).toMatch(regexp);
|
||||||
|
|
||||||
expect(console.error).toBeCalledTimes(0);
|
expect(console.error).toBeCalledTimes(0);
|
||||||
console.error = consoleError;
|
console.error = consoleError;
|
||||||
@@ -469,7 +472,8 @@ describe("component error handling (catchError)", () => {
|
|||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
expect(error).toBeDefined();
|
expect(error).toBeDefined();
|
||||||
expect(error.message).toBe("Cannot read property 'crash' of undefined");
|
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
||||||
|
expect(error.message).toMatch(regexp);
|
||||||
|
|
||||||
expect(console.error).toBeCalledTimes(0);
|
expect(console.error).toBeCalledTimes(0);
|
||||||
console.error = consoleError;
|
console.error = consoleError;
|
||||||
@@ -495,7 +499,8 @@ describe("component error handling (catchError)", () => {
|
|||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
expect(error).toBeDefined();
|
expect(error).toBeDefined();
|
||||||
expect(error.message).toBe("Cannot read property 'crash' of undefined");
|
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
||||||
|
expect(error.message).toMatch(regexp);
|
||||||
|
|
||||||
expect(console.error).toBeCalledTimes(0);
|
expect(console.error).toBeCalledTimes(0);
|
||||||
console.error = consoleError;
|
console.error = consoleError;
|
||||||
@@ -518,7 +523,8 @@ describe("component error handling (catchError)", () => {
|
|||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
expect(error).toBeDefined();
|
expect(error).toBeDefined();
|
||||||
expect(error.message).toBe("Cannot read property 'y' of undefined");
|
const regexp = /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
|
||||||
|
expect(error.message).toMatch(regexp);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("simple catchError", async () => {
|
test("simple catchError", async () => {
|
||||||
|
|||||||
@@ -1257,4 +1257,117 @@ describe("t-slot directive", () => {
|
|||||||
`<div><div class="slotted"><div class="slot"><div class="child"></div></div></div></div>`
|
`<div><div class="slotted"><div class="slot"><div class="child"></div></div></div></div>`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-slot scope context", async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
class Wrapper extends Component {
|
||||||
|
static template = xml`<t t-slot="default"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dialog;
|
||||||
|
|
||||||
|
class Dialog extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<Wrapper>
|
||||||
|
<div t-on-click="onClick">
|
||||||
|
<t t-slot="default" />
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
`;
|
||||||
|
|
||||||
|
static components = { Wrapper };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
dialog = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(ev) {
|
||||||
|
// we do not use expect(this).toBe(dialog) here because if it fails, it
|
||||||
|
// may blow up jest because it then tries to compute a diff, which is
|
||||||
|
// infinite if there is a cycle
|
||||||
|
expect(this === dialog).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<Dialog>
|
||||||
|
<button>The Button</button>
|
||||||
|
</Dialog>`;
|
||||||
|
static components = { Dialog };
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, { target: fixture });
|
||||||
|
|
||||||
|
document.querySelector("button").click();
|
||||||
|
await nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-slot in recursive templates", async () => {
|
||||||
|
QWeb.registerTemplate(
|
||||||
|
"_test_recursive_template",
|
||||||
|
`
|
||||||
|
<Wrapper>
|
||||||
|
<t t-esc="name" />
|
||||||
|
<t t-foreach="items" t-as="item">
|
||||||
|
|
||||||
|
<t t-if="!item.children.length">
|
||||||
|
<t t-esc="item.name" />
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-else="" t-call="_test_recursive_template">
|
||||||
|
<t t-set="name" t-value="item.name" />
|
||||||
|
<t t-set="items" t-value="item.children" />
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</t>
|
||||||
|
</Wrapper>`
|
||||||
|
);
|
||||||
|
|
||||||
|
class Wrapper extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<wrapper>
|
||||||
|
<t t-slot="default"/>
|
||||||
|
</wrapper>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = "_test_recursive_template";
|
||||||
|
static components = { Wrapper };
|
||||||
|
name = "foo";
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
name: "foo-0",
|
||||||
|
children: [
|
||||||
|
{ name: "foo-00", children: [] },
|
||||||
|
{
|
||||||
|
name: "foo-01",
|
||||||
|
children: [
|
||||||
|
{ name: "foo-010", children: [] },
|
||||||
|
{ name: "foo-011", children: [] },
|
||||||
|
{
|
||||||
|
name: "foo-012",
|
||||||
|
children: [
|
||||||
|
{ name: "foo-0120", children: [] },
|
||||||
|
{ name: "foo-0121", children: [] },
|
||||||
|
{ name: "foo-0122", children: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: "foo-02", children: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: "foo-1", children: [] },
|
||||||
|
{ name: "foo-2", children: [] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, { target: fixture });
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
"<wrapper>foo<wrapper>foo-0foo-00<wrapper>foo-01foo-010foo-011<wrapper>foo-012foo-0120foo-0121foo-0122</wrapper></wrapper>foo-02</wrapper>foo-1foo-2</wrapper>"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,8 +75,6 @@ describe("hooks", () => {
|
|||||||
}
|
}
|
||||||
const component = new MyComponent();
|
const component = new MyComponent();
|
||||||
await component.mount(fixture);
|
await component.mount(fixture);
|
||||||
expect(component).not.toHaveProperty("mounted");
|
|
||||||
expect(component).not.toHaveProperty("willUnmount");
|
|
||||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||||
expect(steps).toEqual(["mounted"]);
|
expect(steps).toEqual(["mounted"]);
|
||||||
component.unmount();
|
component.unmount();
|
||||||
@@ -383,8 +381,6 @@ describe("hooks", () => {
|
|||||||
|
|
||||||
const component = new MyComponent();
|
const component = new MyComponent();
|
||||||
await component.mount(fixture);
|
await component.mount(fixture);
|
||||||
expect(component).not.toHaveProperty("patched");
|
|
||||||
expect(component).not.toHaveProperty("willPatch");
|
|
||||||
expect(steps).toEqual([]);
|
expect(steps).toEqual([]);
|
||||||
|
|
||||||
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
expect(fixture.innerHTML).toBe("<div>hey</div>");
|
||||||
@@ -630,8 +626,6 @@ describe("hooks", () => {
|
|||||||
|
|
||||||
const app = new App();
|
const app = new App();
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(app).not.toHaveProperty("willStart");
|
|
||||||
expect(app).not.toHaveProperty("willUpdateProps");
|
|
||||||
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
||||||
|
|
||||||
// NOTE: 'on2ndStart' appears first in the list even though
|
// NOTE: 'on2ndStart' appears first in the list even though
|
||||||
|
|||||||
@@ -546,7 +546,8 @@ describe("Portal: Basic use and DOM placement", () => {
|
|||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
expect(error).toBeDefined();
|
expect(error).toBeDefined();
|
||||||
expect(error.message).toBe("Cannot read property 'crash' of undefined");
|
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
||||||
|
expect(error.message).toMatch(regexp);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("portal manual unmount", async () => {
|
test("portal manual unmount", async () => {
|
||||||
|
|||||||
@@ -3943,6 +3943,20 @@ exports[`t-set value priority 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`translation support can add additional attributes to the list of translatable attributes 1`] = `
|
||||||
|
"function anonymous(context, extra
|
||||||
|
) {
|
||||||
|
// Template name: \\"test\\"
|
||||||
|
let h = this.h;
|
||||||
|
let _1 = 'word';
|
||||||
|
let _2 = 'mot';
|
||||||
|
let c3 = [], p3 = {key:3,attrs:{tomato: _1,potato: _2}};
|
||||||
|
let vn3 = h('div', p3, c3);
|
||||||
|
c3.push({text: \`text\`});
|
||||||
|
return vn3;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`translation support can translate node content 1`] = `
|
exports[`translation support can translate node content 1`] = `
|
||||||
"function anonymous(context, extra
|
"function anonymous(context, extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -27,3 +27,31 @@ exports[`qweb t-att t-att-class with multiple classes 2`] = `
|
|||||||
return vn4;
|
return vn4;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 1`] = `
|
||||||
|
"function anonymous(context, extra
|
||||||
|
) {
|
||||||
|
// Template name: \\"test\\"
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let scope = Object.create(context);
|
||||||
|
let h = this.h;
|
||||||
|
let _1 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
|
||||||
|
let c2 = [], p2 = {key:2,class:_1};
|
||||||
|
let vn2 = h('div', p2, c2);
|
||||||
|
return vn2;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`qweb t-att t-att-class with multiple classes, some of which are duplicate 2`] = `
|
||||||
|
"function anonymous(context, extra
|
||||||
|
) {
|
||||||
|
// Template name: \\"test\\"
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let scope = Object.create(context);
|
||||||
|
let h = this.h;
|
||||||
|
let _3 = utils.toClassObj({'a b c':scope['value'],'a b d':!scope['value']});
|
||||||
|
let c4 = [], p4 = {key:4,class:_3};
|
||||||
|
let vn4 = h('div', p4, c4);
|
||||||
|
return vn4;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|||||||
+15
-4
@@ -1,4 +1,5 @@
|
|||||||
import { QWeb } from "../../src/qweb/index";
|
import { QWeb } from "../../src/qweb/index";
|
||||||
|
import { config } from "../../src/index";
|
||||||
import { nextTick, normalize, renderToDOM, renderToString, trim } from "../helpers";
|
import { nextTick, normalize, renderToDOM, renderToString, trim } from "../helpers";
|
||||||
import { patch } from "../../src/vdom";
|
import { patch } from "../../src/vdom";
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ let qweb: QWeb;
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
QWeb.TEMPLATES = {};
|
QWeb.TEMPLATES = {};
|
||||||
|
QWeb.nextId = 1;
|
||||||
qweb = new QWeb();
|
qweb = new QWeb();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1862,8 +1864,7 @@ describe("t-ref", () => {
|
|||||||
|
|
||||||
describe("loading templates", () => {
|
describe("loading templates", () => {
|
||||||
test("can initialize qweb with a string", () => {
|
test("can initialize qweb with a string", () => {
|
||||||
const templates = `
|
const templates = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<templates id="template" xml:space="preserve">
|
<templates id="template" xml:space="preserve">
|
||||||
<div t-name="hey">jupiler</div>
|
<div t-name="hey">jupiler</div>
|
||||||
</templates>`;
|
</templates>`;
|
||||||
@@ -1872,8 +1873,7 @@ describe("loading templates", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("can load a few templates from a xml string", () => {
|
test("can load a few templates from a xml string", () => {
|
||||||
const data = `
|
const data = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<templates id="template" xml:space="preserve">
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
<t t-name="items"><li>ok</li><li>foo</li></t>
|
<t t-name="items"><li>ok</li><li>foo</li></t>
|
||||||
@@ -2213,6 +2213,17 @@ describe("translation support", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can add additional attributes to the list of translatable attributes", () => {
|
||||||
|
const translations = {
|
||||||
|
word: "mot",
|
||||||
|
};
|
||||||
|
const translateFn = (expr) => translations[expr] || expr;
|
||||||
|
const qweb = new QWeb({ translateFn });
|
||||||
|
config.translatableAttributes.push("potato");
|
||||||
|
qweb.addTemplate("test", `<div tomato="word" potato="word">text</div>`);
|
||||||
|
expect(renderToString(qweb, "test")).toBe('<div tomato="word" potato="mot">text</div>');
|
||||||
|
});
|
||||||
|
|
||||||
test("translation is done on the trimmed text, with extra spaces readded after", () => {
|
test("translation is done on the trimmed text, with extra spaces readded after", () => {
|
||||||
const translations = {
|
const translations = {
|
||||||
word: "mot",
|
word: "mot",
|
||||||
|
|||||||
@@ -207,6 +207,17 @@ describe("expression evaluation", () => {
|
|||||||
expect(compileExpr("{a,b:3,c}", {})).toBe("{a:scope['a'],b:3,c:scope['c']}");
|
expect(compileExpr("{a,b:3,c}", {})).toBe("{a:scope['a'],b:3,c:scope['c']}");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("works with short object description and lists ", () => {
|
||||||
|
expect(compileExpr("[a, b]", {})).toBe("[scope['a'],scope['b']]");
|
||||||
|
expect(compileExpr("[a, b, c]", {})).toBe("[scope['a'],scope['b'],scope['c']]");
|
||||||
|
expect(compileExpr("[a, {b, c},d]", {})).toBe(
|
||||||
|
"[scope['a'],{b:scope['b'],c:scope['c']},scope['d']]"
|
||||||
|
);
|
||||||
|
expect(compileExpr("{a:[b, {c, d: e}]}", {})).toBe(
|
||||||
|
"{a:[scope['b'],{c:scope['c'],d:scope['e']}]}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("template strings", () => {
|
test("template strings", () => {
|
||||||
expect(compileExpr("`hey`", {})).toBe("`hey`");
|
expect(compileExpr("`hey`", {})).toBe("`hey`");
|
||||||
expect(compileExpr("`hey ${you}`", {})).toBe("`hey ${scope['you']}`");
|
expect(compileExpr("`hey ${you}`", {})).toBe("`hey ${scope['you']}`");
|
||||||
|
|||||||
@@ -24,4 +24,13 @@ describe("qweb t-att", () => {
|
|||||||
'<div class="a b c"></div>'
|
'<div class="a b c"></div>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-att-class with multiple classes, some of which are duplicate", () => {
|
||||||
|
expect(render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: true })).toBe(
|
||||||
|
'<div class="a b c"></div>'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
render(`<div t-att-class="{'a b c': value, 'a b d': !value}" />`, { value: false })
|
||||||
|
).toBe('<div class="a b d"></div>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ describe("Link component", () => {
|
|||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
|
|
||||||
expect(window.location.pathname).toBe("/users");
|
expect(window.location.pathname).toBe("/users");
|
||||||
var evt = new MouseEvent("click", {
|
var evt = new MouseEvent("contextmenu", {
|
||||||
button: 1,
|
button: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+40
-1
@@ -62,6 +62,9 @@ async function startRelease() {
|
|||||||
}
|
}
|
||||||
let shouldUploadPlayground = await ask(`Should this release be uploaded on the playground [y/n] ? (y)`);
|
let shouldUploadPlayground = await ask(`Should this release be uploaded on the playground [y/n] ? (y)`);
|
||||||
shouldUploadPlayground = shouldUploadPlayground.toLowerCase() !== 'n';
|
shouldUploadPlayground = shouldUploadPlayground.toLowerCase() !== 'n';
|
||||||
|
|
||||||
|
let shouldUploadPCoverageReport = await ask(`Should this release coverage report be published [y/n] ? (y)`);
|
||||||
|
shouldUploadPCoverageReport = shouldUploadPCoverageReport.toLowerCase() !== 'n';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
log(`Step 2/${STEPS}: running tests...`);
|
log(`Step 2/${STEPS}: running tests...`);
|
||||||
@@ -122,7 +125,7 @@ async function startRelease() {
|
|||||||
if (shouldUploadPlayground) {
|
if (shouldUploadPlayground) {
|
||||||
log(`Bonus step: publishing new release on playground...`);
|
log(`Bonus step: publishing new release on playground...`);
|
||||||
let owl_code = null;
|
let owl_code = null;
|
||||||
status = 0
|
let status = 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
owl_code = await readFile("dist/owl.iife.js");
|
owl_code = await readFile("dist/owl.iife.js");
|
||||||
@@ -153,6 +156,42 @@ async function startRelease() {
|
|||||||
logError("Something went wrong for the playground update.")
|
logError("Something went wrong for the playground update.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldUploadPCoverageReport) {
|
||||||
|
let status = 0
|
||||||
|
log(`Bonus step: publishing new coverage report...`);
|
||||||
|
|
||||||
|
if (!fs.existsSync("./coverage")) {
|
||||||
|
logError("Couldn't update coverage report, there is no coverage folder on master.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status += await execCommand("git stash push -a -- coverage");
|
||||||
|
if (status !== 0) {
|
||||||
|
logError("Couldn't stash coverage")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status += await execCommand("git checkout gh-pages");
|
||||||
|
if (status !== 0) {
|
||||||
|
logError("Couldn't switch to gh-pages branch")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
status += await execCommand("rm -rf coverage");
|
||||||
|
status += await execCommand("git stash pop");
|
||||||
|
status += await execCommand("git add coverage");
|
||||||
|
status += await execCommand(`git commit -m "[IMP] update coverage report for owl v${next}"`);
|
||||||
|
status += await execCommand(`git push origin gh-pages`);
|
||||||
|
status += await execCommand("git checkout -");
|
||||||
|
|
||||||
|
if (status !== 0) {
|
||||||
|
logError("Something went wrong for the coverage report update.")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user