Compare commits

..

17 Commits

Author SHA1 Message Date
Géry Debongnie 5a75e5ec19 bump to v0.5.0 2019-04-04 14:19:40 +02:00
Géry Debongnie 3e8682b741 ref: adapt qweb to output code closer to es5 2019-04-04 14:14:02 +02:00
Géry Debongnie fd26775a0f imp: adapt build scripts to allow ES5 output 2019-04-04 13:55:46 +02:00
Géry Debongnie a86d0b5b02 imp: store connect: mapStateToProps receive ownProps 2019-04-02 14:23:40 +02:00
Géry Debongnie 7ebea5a486 imp: better error message when evaluating/compiling templates
fixes #3
2019-04-02 11:03:04 +02:00
Géry Debongnie 56be87f26e fix: allow t-attf- attributes to combine with regular attrs
fixes #10
2019-04-02 09:22:37 +02:00
Géry Debongnie 990a08dc04 imp: add componentDidUpdate hook 2019-04-01 16:00:57 +02:00
Alexandre Kühn e7a4ad99de [FIX] connected child components with custom hooks 2019-04-01 14:44:53 +02:00
Géry Debongnie d6e61c2203 imp: qweb engine can be initialized with templates 2019-03-29 16:40:59 +01:00
Géry Debongnie e6b0d06e3e fix: make sure qweb throw error if invalid templates 2019-03-29 15:37:42 +01:00
Géry Debongnie 5cd45b59af fix: properly destroy/detach widgets in some cases
Sometimes, widgets were not properly destroyed/detached. This was caused
by the fact that we used the hook remove, when we sometimes need to use
the hook destroy as well.
2019-03-29 12:50:45 +01:00
Géry Debongnie 1e58c7b12b fix: update component snapshots 2019-03-29 12:39:00 +01:00
Géry Debongnie afc89e6f66 fix: if t-props is not defined, should fallback on {} 2019-03-28 17:35:08 +01:00
Géry Debongnie 04679a6d03 fix: hello world example was wrong :( 2019-03-28 14:03:41 +01:00
Géry Debongnie cb2e76498b fix: update build scripts: core.js -> owl.js 2019-03-28 10:57:12 +01:00
Géry Debongnie 7ac4e63e51 doc: improve readme.md 2019-03-28 10:52:07 +01:00
Géry Debongnie d804bcf2dd doc: improve main page style 2019-03-28 10:42:12 +01:00
14 changed files with 839 additions and 498 deletions
+11 -21
View File
@@ -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 = {
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
};
};
+58 -12
View File
@@ -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
+110
View File
@@ -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", () => {
+39
View File
@@ -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"?>
+87
View File
@@ -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
View File
@@ -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,