mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a75e5ec19 | |||
| 3e8682b741 | |||
| fd26775a0f | |||
| a86d0b5b02 | |||
| 7ebea5a486 | |||
| 56be87f26e | |||
| 990a08dc04 | |||
| e7a4ad99de | |||
| d6e61c2203 | |||
| e6b0d06e3e | |||
| 5cd45b59af | |||
| 1e58c7b12b | |||
| afc89e6f66 | |||
| 04679a6d03 | |||
| cb2e76498b | |||
| 7ac4e63e51 | |||
| d804bcf2dd |
@@ -1,4 +1,4 @@
|
||||
# 🦉 Odoo Web Lab 🦉
|
||||
<h1 align="center">🦉 Odoo Web Lab 🦉</h1>
|
||||
|
||||
## Project Overview
|
||||
|
||||
@@ -30,26 +30,16 @@ client will ever be converted to typescript (even though I would really like it)
|
||||
|
||||
## Installing/Building
|
||||
|
||||
To install every dependency needed to play with this code:
|
||||
Some npm scripts are available:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
To build a bundle of this as a library:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
To run tests:
|
||||
|
||||
```
|
||||
npm run test
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
Note that the test scripts also run the example tests suites.
|
||||
| Command | Description |
|
||||
| ------------------ | --------------------------------------------------------- |
|
||||
| npm install | install every dependency required for this project |
|
||||
| npm run build | build a bundle of _owl_ in the _/dist/_ folder |
|
||||
| npm run build:es5 | build a bundle of _owl_ in the _/dist/_ folder (ES5 code) |
|
||||
| npm run minify | minify the prebuilt owl.js file |
|
||||
| npm run test | run all tests |
|
||||
| npm run test:watch | run all tests, and keep a watcher |
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -66,7 +56,7 @@ Here is a minimal Hello World example:
|
||||
|
||||
```javascript
|
||||
class HelloWorld extends owl.core.Component {
|
||||
inlineTemplate = `<div>Hello <t t-esc="state.name"/></div>`;
|
||||
inlineTemplate = `<div>Hello <t t-esc="props.name"/></div>`;
|
||||
}
|
||||
|
||||
const env = {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<!-- Application JS/CSS -->
|
||||
<script src="/core.js"></script>
|
||||
<script src="/owl.js"></script>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<script type="module" src="/main.js"></script>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<!-- Application JS/CSS -->
|
||||
<script src="/core.js"></script>
|
||||
<script src="/owl.js"></script>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<script type="module" src="/main.js"></script>
|
||||
|
||||
|
||||
+7
-5
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "owl",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "Odoo Web Lab",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build:js": "tsc --lib es2017,dom --target esnext --outDir dist/core src/*",
|
||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
|
||||
"build:js:es5": "tsc --target es5 --module es6 --outDir dist/owl src/*",
|
||||
"build:bundle": "rollup -c",
|
||||
"build": "npm run build:js && npm run build:bundle",
|
||||
"minify": "uglifyjs dist/core.js -o dist/core.min.js --compress --mangle",
|
||||
"build:es5": "npm run build:js:es5 && npm run build:bundle",
|
||||
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
|
||||
"dev": "npm-run-all --parallel \"build:* -- --watch\"",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
@@ -19,12 +21,12 @@
|
||||
"example:web:serve": "live-server --entry-file=index.html dist/examples/web/",
|
||||
"preexample:web:dev": "npm run example:web:build",
|
||||
"example:web:dev": "npm-run-all --parallel \"example:web:build:* -- --watch\" example:web:serve",
|
||||
"example:benchmarks:build:core": "npm run build && cpx \"dist/core.js\" dist/examples/benchmarks",
|
||||
"example:benchmarks:build:core": "npm run build && cpx \"dist/owl.js\" dist/examples/benchmarks",
|
||||
"example:benchmarks:build:js": "cpx \"examples/benchmarks/{*.html,*.js,*.css}\" dist/examples/benchmarks",
|
||||
"example:benchmarks:build": "npm-run-all --parallel example:benchmarks:build:*",
|
||||
"example:benchmarks:serve": "live-server --entry-file=index.html dist/examples/benchmarks/",
|
||||
"example:benchmarks:dev": "npm-run-all --parallel \"example:benchmarks:build:* -- --watch\" example:benchmarks:serve",
|
||||
"example:todoapp:build:core": "npm run build && cpx \"dist/core.js\" dist/examples/todoapp",
|
||||
"example:todoapp:build:core": "npm run build && cpx \"dist/owl.js\" dist/examples/todoapp",
|
||||
"example:todoapp:build:js": "cpx \"examples/todoapp/{*.html,**/*.js,*.css,*.xml}\" dist/examples/todoapp",
|
||||
"example:todoapp:build": "npm-run-all --parallel example:todoapp:build:*",
|
||||
"example:todoapp:serve": "live-server --entry-file=index.html dist/examples/todoapp/",
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@ import git from "git-rev-sync";
|
||||
|
||||
// rollup.config.js
|
||||
export default {
|
||||
input: "dist/core/src/index.js",
|
||||
input: "dist/owl/src/index.js",
|
||||
output: {
|
||||
file: "dist/core.js",
|
||||
file: "dist/owl.js",
|
||||
format: "iife",
|
||||
name: "owl",
|
||||
extend: true,
|
||||
|
||||
+16
-3
@@ -149,6 +149,16 @@ export class Component<
|
||||
*/
|
||||
mounted() {}
|
||||
|
||||
/**
|
||||
* This hook is called whenever a component did actually update its props,
|
||||
* state or env.
|
||||
*
|
||||
* This method is not called on the initial render. It is useful to interact
|
||||
* with the DOM (for example, through an external library) whenever the
|
||||
* component was updated.
|
||||
*/
|
||||
componentDidUpdate() {}
|
||||
|
||||
/**
|
||||
* willUnmount is a hook that is called each time a component is detached from
|
||||
* the DOM. This is a good place to remove some listeners, for example.
|
||||
@@ -281,8 +291,9 @@ export class Component<
|
||||
}
|
||||
Object.assign(this.env, nextEnv);
|
||||
if (this.__widget__.isMounted) {
|
||||
return this.render(true);
|
||||
await this.render(true);
|
||||
}
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
async updateProps(
|
||||
@@ -312,8 +323,9 @@ export class Component<
|
||||
}
|
||||
Object.assign(this.state, nextState);
|
||||
if (this.__widget__.isStarted) {
|
||||
return this.render();
|
||||
await this.render();
|
||||
}
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -322,7 +334,8 @@ export class Component<
|
||||
|
||||
async _updateProps(nextProps: Props): Promise<void> {
|
||||
this.props = nextProps;
|
||||
return this.render();
|
||||
await this.render();
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
_patch(vnode) {
|
||||
|
||||
+54
-24
@@ -53,10 +53,12 @@ export class Context {
|
||||
shouldProtectContext: boolean = false;
|
||||
inLoop: boolean = false;
|
||||
inPreTag: boolean = false;
|
||||
templateName: string;
|
||||
|
||||
constructor() {
|
||||
constructor(name?: string) {
|
||||
this.rootContext = this;
|
||||
this.addLine("let h = this.utils.h;");
|
||||
this.templateName = name || "noname";
|
||||
this.addLine("var h = this.utils.h;");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
@@ -182,7 +184,7 @@ export class QWeb {
|
||||
templates: { [name: string]: CompiledTemplate<VNode> } = {};
|
||||
directives: Directive[] = [];
|
||||
|
||||
constructor() {
|
||||
constructor(data?: string) {
|
||||
[
|
||||
forEachDirective,
|
||||
escDirective,
|
||||
@@ -196,6 +198,10 @@ export class QWeb {
|
||||
refDirective,
|
||||
widgetDirective
|
||||
].forEach(d => this.addDirective(d));
|
||||
|
||||
if (data) {
|
||||
this.loadTemplates(data);
|
||||
}
|
||||
}
|
||||
|
||||
utils = {
|
||||
@@ -300,6 +306,9 @@ export class QWeb {
|
||||
loadTemplates(xmlstr: string) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xmlstr, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
throw new Error("Invalid XML in template");
|
||||
}
|
||||
const templates = doc.getElementsByTagName("templates")[0];
|
||||
if (!templates) {
|
||||
return;
|
||||
@@ -330,7 +339,7 @@ export class QWeb {
|
||||
|
||||
const mainNode = this.processedTemplates[name];
|
||||
const isDebug = (<Element>mainNode).attributes.hasOwnProperty("t-debug");
|
||||
const ctx = new Context();
|
||||
const ctx = new Context(name);
|
||||
this._compileNode(mainNode, ctx);
|
||||
|
||||
if (ctx.shouldProtectContext) {
|
||||
@@ -357,7 +366,12 @@ export class QWeb {
|
||||
ctx.code.join("\n")
|
||||
) as CompiledTemplate<VNode>;
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid template (or compiled code): ${e.message}`);
|
||||
throw new Error(
|
||||
`Invalid generated code while compiling template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}': ${e.message}`
|
||||
);
|
||||
}
|
||||
if (isDebug) {
|
||||
console.log(
|
||||
@@ -390,7 +404,7 @@ export class QWeb {
|
||||
// this is an unusual situation: this text node is the result of the
|
||||
// template rendering.
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentNode = nodeID;
|
||||
}
|
||||
@@ -515,9 +529,12 @@ export class QWeb {
|
||||
const value = attributes[i].textContent!;
|
||||
|
||||
// regular attributes
|
||||
if (!name.startsWith("t-")) {
|
||||
if (
|
||||
!name.startsWith("t-") &&
|
||||
!(<Element>node).getAttribute("t-attf-" + name)
|
||||
) {
|
||||
const attID = ctx.generateID();
|
||||
ctx.addLine(`let _${attID} = '${value}';`);
|
||||
ctx.addLine(`var _${attID} = '${value}';`);
|
||||
if (!name.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
name = '"' + name + '"';
|
||||
@@ -546,14 +563,14 @@ export class QWeb {
|
||||
const attValue = (<Element>node).getAttribute(attName);
|
||||
if (attValue) {
|
||||
const attValueID = ctx.generateID();
|
||||
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
|
||||
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
|
||||
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
||||
const attrIndex = attrs.findIndex(att =>
|
||||
att.startsWith(attName + ":")
|
||||
);
|
||||
attrs.splice(attrIndex, 1);
|
||||
}
|
||||
ctx.addLine(`let _${attID} = ${formattedValue};`);
|
||||
ctx.addLine(`var _${attID} = ${formattedValue};`);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
handleBooleanProps(attName, attID);
|
||||
}
|
||||
@@ -569,14 +586,21 @@ export class QWeb {
|
||||
s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}"
|
||||
);
|
||||
const attID = ctx.generateID();
|
||||
ctx.addLine(`let _${attID} = \`${formattedExpr}\`;`);
|
||||
let staticVal = (<Element>node).getAttribute(attName);
|
||||
if (staticVal) {
|
||||
ctx.addLine(
|
||||
`var _${attID} = '${staticVal} ' + \`${formattedExpr}\`;`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(`var _${attID} = \`${formattedExpr}\`;`);
|
||||
}
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
}
|
||||
|
||||
// t-att= attributes
|
||||
if (name === "t-att") {
|
||||
let id = ctx.generateID();
|
||||
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`);
|
||||
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
|
||||
tattrs.push(id);
|
||||
}
|
||||
}
|
||||
@@ -592,7 +616,7 @@ export class QWeb {
|
||||
parts.push(`on:{}`);
|
||||
}
|
||||
|
||||
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
|
||||
ctx.addLine(`var c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
|
||||
for (let id of tattrs) {
|
||||
ctx.addIf(`_${id} instanceof Array`);
|
||||
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
|
||||
@@ -605,7 +629,7 @@ export class QWeb {
|
||||
ctx.closeIf();
|
||||
}
|
||||
ctx.addLine(
|
||||
`let vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
|
||||
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
|
||||
);
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
|
||||
@@ -654,7 +678,7 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
|
||||
if (typeof value === "string") {
|
||||
const exprID = ctx.generateID();
|
||||
ctx.addLine(`let e${exprID} = ${ctx.formatExpression(value)};`);
|
||||
ctx.addLine(`var e${exprID} = ${ctx.formatExpression(value)};`);
|
||||
ctx.addIf(`e${exprID} || e${exprID} === 0`);
|
||||
let text = `e${exprID}`;
|
||||
|
||||
@@ -665,14 +689,14 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${text}});`);
|
||||
} else {
|
||||
let fragID = ctx.generateID();
|
||||
ctx.addLine(`let frag${fragID} = this.utils.getFragment(e${exprID})`);
|
||||
ctx.addLine(`var frag${fragID} = this.utils.getFragment(e${exprID})`);
|
||||
let tempNodeID = ctx.generateID();
|
||||
ctx.addLine(`let p${tempNodeID} = {hook: {`);
|
||||
ctx.addLine(`var p${tempNodeID} = {hook: {`);
|
||||
ctx.addLine(
|
||||
` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`
|
||||
);
|
||||
ctx.addLine(`}};`);
|
||||
ctx.addLine(`let vn${tempNodeID} = h('div', p${tempNodeID})`);
|
||||
ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
|
||||
}
|
||||
if (node.childNodes.length) {
|
||||
@@ -809,7 +833,7 @@ const forEachDirective: Directive = {
|
||||
const elems = node.getAttribute("t-foreach")!;
|
||||
const name = node.getAttribute("t-as")!;
|
||||
let arrayID = ctx.generateID();
|
||||
ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`);
|
||||
ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
|
||||
ctx.addLine(
|
||||
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
|
||||
);
|
||||
@@ -818,11 +842,11 @@ const forEachDirective: Directive = {
|
||||
);
|
||||
let keysID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`let _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
|
||||
`var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
|
||||
);
|
||||
let valuesID = ctx.generateID();
|
||||
ctx.addLine(
|
||||
`let _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
|
||||
`var _${valuesID} = _${arrayID} instanceof Array ? _${arrayID} : Object.values(_${arrayID});`
|
||||
);
|
||||
ctx.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
|
||||
ctx.indent();
|
||||
@@ -852,15 +876,19 @@ const onDirective: Directive = {
|
||||
extraArgs = args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
|
||||
/`/g,
|
||||
"'"
|
||||
)}'\`)})()`;
|
||||
if (extraArgs) {
|
||||
ctx.addLine(
|
||||
`p${nodeID}.on['${eventName}'] = context['${handler}'].bind(owner, ${ctx.formatExpression(
|
||||
`p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
|
||||
extraArgs
|
||||
)});`
|
||||
);
|
||||
} else {
|
||||
ctx.addLine(
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || context['${handler}'].bind(owner);`
|
||||
`extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || (context['${handler}'] || ${error}).bind(owner);`
|
||||
);
|
||||
ctx.addLine(
|
||||
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
|
||||
@@ -931,7 +959,7 @@ const widgetDirective: Directive = {
|
||||
ctx.addLine(
|
||||
`let w${widgetID} = ${templateID} in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[${templateID}]] : false;`
|
||||
);
|
||||
ctx.addLine(`let props${widgetID} = ${props};`);
|
||||
ctx.addLine(`let props${widgetID} = ${props || "{}"};`);
|
||||
ctx.addLine(`let isNew${widgetID} = !w${widgetID};`);
|
||||
|
||||
// check if we can reuse current rendering promise
|
||||
@@ -981,6 +1009,8 @@ const widgetDirective: Directive = {
|
||||
ctx.parentNode
|
||||
}[_${dummyID}_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w${widgetID}._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w${widgetID}.${
|
||||
keepAlive ? "detach" : "destroy"
|
||||
}()},destroy(){w${widgetID}.${
|
||||
keepAlive ? "detach" : "destroy"
|
||||
}()}}; w${widgetID}.__widget__.pvnode = pvnode;});`
|
||||
);
|
||||
ctx.addElse();
|
||||
|
||||
+17
-14
@@ -7,33 +7,36 @@ export function connect(mapStateToProps) {
|
||||
return class extends Comp {
|
||||
constructor(parent, props?: any) {
|
||||
const env = parent instanceof Component ? parent.env : parent;
|
||||
const storeProps = mapStateToProps(env.store.state);
|
||||
props = Object.assign(props || {}, storeProps);
|
||||
super(parent, props);
|
||||
const ownProps = Object.assign({}, props || {});
|
||||
const storeProps = mapStateToProps(env.store.state, ownProps);
|
||||
const mergedProps = Object.assign(props || {}, storeProps);
|
||||
super(parent, mergedProps);
|
||||
this.__widget__.currentStoreProps = storeProps;
|
||||
this.__widget__.ownProps = ownProps;
|
||||
}
|
||||
mounted() {
|
||||
this.env.store.on("update", this, () => {
|
||||
const storeProps = mapStateToProps(this.env.store.state);
|
||||
const ownProps = this.__widget__.ownProps;
|
||||
const storeProps = mapStateToProps(this.env.store.state, ownProps);
|
||||
if (!shallowEqual(storeProps, this.__widget__.currentStoreProps)) {
|
||||
this.__widget__.currentStoreProps = storeProps;
|
||||
// probably not optimal, will do 2 object.assign, one here and
|
||||
// one in updateProps.
|
||||
const nextProps = Object.assign(
|
||||
{},
|
||||
this.props,
|
||||
this.__widget__.currentStoreProps
|
||||
);
|
||||
this.updateProps(nextProps, false);
|
||||
this.updateProps(ownProps, false);
|
||||
}
|
||||
});
|
||||
super.mounted();
|
||||
}
|
||||
willUnmount() {
|
||||
this.env.store.off("update", this);
|
||||
super.willUnmount();
|
||||
}
|
||||
updateProps(nextProps, forceUpdate) {
|
||||
nextProps = Object.assign(nextProps, this.__widget__.currentStoreProps);
|
||||
return super.updateProps(nextProps, forceUpdate);
|
||||
this.__widget__.ownProps = nextProps;
|
||||
const mergedProps = Object.assign(
|
||||
{},
|
||||
nextProps,
|
||||
this.__widget__.currentStoreProps
|
||||
);
|
||||
return super.updateProps(mergedProps, forceUpdate);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,14 +5,14 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
) {
|
||||
let owner = context;
|
||||
context = Object.create(context);
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
let _2 = context['state'].numbers;
|
||||
var h = this.utils.h;
|
||||
var c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = context['state'].numbers;
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
|
||||
let _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
let _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
|
||||
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
|
||||
for (let i = 0; i < _3.length; i++) {
|
||||
context.number_first = i === 0;
|
||||
context.number_last = i === _3.length - 1;
|
||||
@@ -26,7 +26,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
c1.push(null);
|
||||
let def6;
|
||||
let w7 = key8 in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[key8]] : false;
|
||||
let props7 = null;
|
||||
let props7 = {};
|
||||
let isNew7 = !w7;
|
||||
if (w7 && w7.__widget__.renderPromise) {
|
||||
if (w7.__widget__.isStarted) {
|
||||
@@ -51,7 +51,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
||||
}
|
||||
}
|
||||
if (isNew7) {
|
||||
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8});c1[_5_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w7._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w7.destroy()}}; w7.__widget__.pvnode = pvnode;});
|
||||
def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8});c1[_5_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w7._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w7.destroy()},destroy(){w7.destroy()}}; w7.__widget__.pvnode = pvnode;});
|
||||
} else {
|
||||
def6 = def6.then(()=>{if (w7.__widget__.isDestroyed) {return};let vnode;if (!w7.__widget__.vnode){vnode=w7.__widget__.pvnode} else { vnode=h(w7.__widget__.vnode.sel, {key: key8});vnode.elm=w7.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w7.el,a.elm);a.elm=w7.el;w7.__mount();},remove(){w7.destroy()}}}c1[_5_index]=vnode;});
|
||||
}
|
||||
@@ -65,9 +65,9 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
let h = this.utils.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
var h = this.utils.h;
|
||||
var c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let key5 = \\"somestring\\";
|
||||
let _2_index = c1.length;
|
||||
@@ -99,7 +99,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
}
|
||||
}
|
||||
if (isNew4) {
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w4.destroy()}}; w4.__widget__.pvnode = pvnode;});
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__widget__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = def3.then(()=>{if (w4.__widget__.isDestroyed) {return};let vnode;if (!w4.__widget__.vnode){vnode=w4.__widget__.pvnode} else { vnode=h(w4.__widget__.vnode.sel, {key: key5});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}}}c1[_2_index]=vnode;});
|
||||
}
|
||||
@@ -107,3 +107,49 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let owner = context;
|
||||
var h = this.utils.h;
|
||||
var c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
//WIDGET
|
||||
let _2_index = c1.length;
|
||||
c1.push(null);
|
||||
let def3;
|
||||
let w4 = 4 in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[4]] : false;
|
||||
let props4 = {};
|
||||
let isNew4 = !w4;
|
||||
if (w4 && w4.__widget__.renderPromise) {
|
||||
if (w4.__widget__.isStarted) {
|
||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
||||
} else {
|
||||
isNew4 = true
|
||||
if (props4 === w4.__widget__.renderProps) {
|
||||
def3 = w4.__widget__.renderPromise;
|
||||
} else {
|
||||
w4.destroy();
|
||||
w4 = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!def3) {
|
||||
if (w4) {
|
||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
||||
} else {
|
||||
w4 = new context.widgets['child'](owner, props4);
|
||||
context.__widget__.cmap[4] = w4.__widget__.id;
|
||||
def3 = w4._start();
|
||||
}
|
||||
}
|
||||
if (isNew4) {
|
||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4});c1[_2_index]=pvnode;pvnode.data.hook = {insert(vn){let nvn=w4._mount(vnode, vn.elm);pvnode.elm=nvn.elm},remove(){w4.destroy()},destroy(){w4.destroy()}}; w4.__widget__.pvnode = pvnode;});
|
||||
} else {
|
||||
def3 = def3.then(()=>{if (w4.__widget__.isDestroyed) {return};let vnode;if (!w4.__widget__.vnode){vnode=w4.__widget__.pvnode} else { vnode=h(w4.__widget__.vnode.sel, {key: 4});vnode.elm=w4.el;vnode.data.hook = {insert(a){a.elm.parentNode.replaceChild(w4.el,a.elm);a.elm=w4.el;w4.__mount();},remove(){w4.destroy()}}}c1[_2_index]=vnode;});
|
||||
}
|
||||
extra.promises.push(def3);
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -387,6 +387,61 @@ describe("lifecycle hooks", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("componentDidUpdate hook is called after updateState", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
state = { a: 1 };
|
||||
|
||||
componentDidUpdate() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(n).toBe(0);
|
||||
|
||||
await widget.updateState({}); // empty update, should do nothing
|
||||
expect(n).toBe(0);
|
||||
|
||||
await widget.updateState({ a: 3 });
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("componentDidUpdate hook is called after updateProps", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
componentDidUpdate() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
const widget = new TestWidget(env, { a: 1 });
|
||||
await widget.mount(fixture);
|
||||
expect(n).toBe(0);
|
||||
|
||||
await widget.updateProps({ a: 2 });
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("componentDidUpdate hook is called after updateEnv", async () => {
|
||||
let n = 0;
|
||||
|
||||
class TestWidget extends Widget {
|
||||
state = { a: 1 };
|
||||
|
||||
componentDidUpdate() {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
const widget = new TestWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(n).toBe(0);
|
||||
|
||||
await widget.updateEnv({ isMobile: true });
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("shouldUpdate hook prevent rerendering", async () => {
|
||||
let shouldUpdate = false;
|
||||
class TestWidget extends Widget {
|
||||
@@ -404,6 +459,45 @@ describe("lifecycle hooks", () => {
|
||||
await widget.updateProps({ val: 666 });
|
||||
expect(fixture.innerHTML).toBe("<div>666</div>");
|
||||
});
|
||||
|
||||
test("sub widget (inside sub node): hooks are correctly called", async () => {
|
||||
let destroyed = false;
|
||||
let created = false;
|
||||
let mounted = false;
|
||||
class ParentWidget extends Widget {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<t t-if="state.flag">
|
||||
<div><t t-widget="child"/></div>
|
||||
</t>
|
||||
</div>`;
|
||||
widgets = { child: ChildWidget };
|
||||
state = { flag: false };
|
||||
}
|
||||
|
||||
class ChildWidget extends Widget {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
created = true;
|
||||
}
|
||||
mounted() {
|
||||
mounted = true;
|
||||
}
|
||||
destroyed() {
|
||||
destroyed = true;
|
||||
}
|
||||
}
|
||||
const widget = new ParentWidget(env);
|
||||
await widget.mount(fixture);
|
||||
expect(created).toBe(false);
|
||||
expect(mounted).toBe(false);
|
||||
await widget.updateState({ flag: true });
|
||||
expect(mounted).toBe(true);
|
||||
expect(created).toBe(true);
|
||||
expect(destroyed).toBe(false);
|
||||
await widget.updateState({ flag: false });
|
||||
expect(destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("destroy method", () => {
|
||||
@@ -939,6 +1033,22 @@ describe("random stuff/miscellaneous", () => {
|
||||
await widget.mount(fixture);
|
||||
expect(env.qweb.templates.parent.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-props should not be undefined (snapshotting)", async () => {
|
||||
env.qweb.addTemplate("parent", `<div><t t-widget="child"/></div>`);
|
||||
class Parent extends Widget {
|
||||
inlineTemplate = "parent";
|
||||
widgets = { child: Child };
|
||||
}
|
||||
|
||||
class Child extends Widget {
|
||||
inlineTemplate = `<span>abc</span>`;
|
||||
}
|
||||
|
||||
const widget = new Parent(env);
|
||||
await widget.mount(fixture);
|
||||
expect(env.qweb.templates.parent.toString()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("async rendering", () => {
|
||||
|
||||
@@ -101,6 +101,29 @@ describe("error handling", () => {
|
||||
qweb.addTemplate("test", `<t></t>`);
|
||||
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined");
|
||||
});
|
||||
|
||||
test("loadTemplates throw if parser error", () => {
|
||||
expect(() => {
|
||||
qweb.loadTemplates("<templates><abc>></templates>");
|
||||
}).toThrow("Invalid XML in template");
|
||||
});
|
||||
|
||||
test("nice error when t-on-directive is evaluated with a missing handler", () => {
|
||||
qweb.addTemplate("templatename", `<div t-on-click="somemethod"></div>`);
|
||||
expect(() => qweb.render("templatename", {}, { handlers: [] })).toThrow(
|
||||
"Missing handler 'somemethod' when evaluating template 'templatename'"
|
||||
);
|
||||
});
|
||||
|
||||
test("error when compiled code is invalid", () => {
|
||||
qweb.addTemplate(
|
||||
"templatename",
|
||||
`<div t-att-hey="}/^function invalid{{>'"></div>`
|
||||
);
|
||||
expect(() => qweb.render("templatename")).toThrow(
|
||||
"Invalid generated code while compiling template 'templatename': Unexpected token }"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("t-esc", () => {
|
||||
@@ -395,6 +418,12 @@ describe("attributes", () => {
|
||||
expect(result).toBe(`<div foo="bar"></div>`);
|
||||
});
|
||||
|
||||
test("t-attf-class should combine with class", () => {
|
||||
qweb.addTemplate("test", `<div class="hello" t-attf-class="world"/>`);
|
||||
const result = renderToString(qweb, "test");
|
||||
expect(result).toBe(`<div class="hello world"></div>`);
|
||||
});
|
||||
|
||||
test("format value", () => {
|
||||
qweb.addTemplate("test", `<div t-attf-foo="b{{value}}r"/>`);
|
||||
const result = renderToString(qweb, "test", { value: "a" });
|
||||
@@ -872,6 +901,16 @@ describe("t-ref", () => {
|
||||
});
|
||||
|
||||
describe("loading templates", () => {
|
||||
test("can initialize qweb with a string", () => {
|
||||
const data = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<div t-name="hey">jupiler</div>
|
||||
</templates>`;
|
||||
const qweb = new QWeb(data);
|
||||
expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>");
|
||||
});
|
||||
|
||||
test("can load a few templates from a xml string", () => {
|
||||
const data = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
@@ -106,4 +106,91 @@ describe("connecting a component to store", () => {
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("connected child components with custom hooks", async () => {
|
||||
let steps: any = [];
|
||||
class Child extends Component<any, any, any> {
|
||||
inlineTemplate = `<div/>`;
|
||||
mounted() {
|
||||
steps.push("child:mounted");
|
||||
}
|
||||
willUnmount() {
|
||||
steps.push("child:willUnmount");
|
||||
}
|
||||
destroyed() {
|
||||
steps.push("child:destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectedChild = connect(s => s)(Child);
|
||||
|
||||
class Parent extends Component<any, any, any> {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<t t-if="state.child" t-widget="ConnectedChild"/>
|
||||
</div>`;
|
||||
widgets = { ConnectedChild };
|
||||
|
||||
constructor(env: Env) {
|
||||
super(env);
|
||||
this.state = { child: true };
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Store({ state: {} });
|
||||
(<any>env).store = store;
|
||||
const parent = new Parent(env);
|
||||
|
||||
await parent.mount(fixture);
|
||||
expect(steps).toEqual(["child:mounted"]);
|
||||
|
||||
await parent.updateState({ child: false });
|
||||
expect(steps).toEqual([
|
||||
"child:mounted",
|
||||
"child:willUnmount",
|
||||
"child:destroyed"
|
||||
]);
|
||||
});
|
||||
|
||||
test("connect receives ownprops as second argument", async () => {
|
||||
const state = { todos: [{ id: 1, text: "jupiler" }] };
|
||||
let nextId = 2;
|
||||
const mutations = {
|
||||
addTodo(state, text) {
|
||||
state.todos.push({ text, id: nextId++ });
|
||||
}
|
||||
};
|
||||
const store = new Store({ state, mutations });
|
||||
|
||||
class TodoItem extends Component<any, any, any> {
|
||||
inlineTemplate = `<span><t t-esc="props.text"/></span>`;
|
||||
}
|
||||
const ConnectedTodo = connect((state, props) => {
|
||||
const todo = state.todos.find(t => t.id === props.id);
|
||||
return todo;
|
||||
})(TodoItem);
|
||||
|
||||
class TodoList extends Component<any, any, any> {
|
||||
inlineTemplate = `<div>
|
||||
<t t-foreach="props.todos" t-as="todo">
|
||||
<t t-widget="ConnectedTodo" t-props="todo"/>
|
||||
</t>
|
||||
</div>`;
|
||||
widgets = { ConnectedTodo };
|
||||
}
|
||||
|
||||
const ConnectedTodoList = connect(state => state)(TodoList);
|
||||
|
||||
(<any>env).store = store;
|
||||
const app = new ConnectedTodoList(env);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>jupiler</span></div>");
|
||||
|
||||
store.commit("addTodo", "hoegaarden");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
"<div><span>jupiler</span><span>hoegaarden</span></div>"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-2
@@ -3,11 +3,10 @@
|
||||
"module": "commonjs",
|
||||
"preserveConstEnums": true,
|
||||
"noImplicitThis": true,
|
||||
"lib": ["es2015", "dom"],
|
||||
"removeComments": false,
|
||||
"inlineSourceMap": true,
|
||||
"declaration": true,
|
||||
"target": "es6",
|
||||
"target": "es5",
|
||||
"outDir": "dist",
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
|
||||
Reference in New Issue
Block a user