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 ## Project Overview
@@ -30,26 +30,16 @@ client will ever be converted to typescript (even though I would really like it)
## Installing/Building ## Installing/Building
To install every dependency needed to play with this code: Some npm scripts are available:
``` | Command | Description |
npm install | ------------------ | --------------------------------------------------------- |
``` | npm install | install every dependency required for this project |
| npm run build | build a bundle of _owl_ in the _/dist/_ folder |
To build a bundle of this as a library: | 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 build | npm run test:watch | run all tests, and keep a watcher |
```
To run tests:
```
npm run test
npm run test:watch
```
Note that the test scripts also run the example tests suites.
## Documentation ## Documentation
@@ -66,7 +56,7 @@ Here is a minimal Hello World example:
```javascript ```javascript
class HelloWorld extends owl.core.Component { 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 = { const env = {
+1 -1
View File
@@ -6,7 +6,7 @@
<link rel="icon" href="data:,"> <link rel="icon" href="data:,">
<!-- Application JS/CSS --> <!-- Application JS/CSS -->
<script src="/core.js"></script> <script src="/owl.js"></script>
<link rel="stylesheet" href="/app.css"> <link rel="stylesheet" href="/app.css">
<script type="module" src="/main.js"></script> <script type="module" src="/main.js"></script>
+1 -1
View File
@@ -6,7 +6,7 @@
<link rel="icon" href="data:,"> <link rel="icon" href="data:,">
<!-- Application JS/CSS --> <!-- Application JS/CSS -->
<script src="/core.js"></script> <script src="/owl.js"></script>
<link rel="stylesheet" href="/app.css"> <link rel="stylesheet" href="/app.css">
<script type="module" src="/main.js"></script> <script type="module" src="/main.js"></script>
+7 -5
View File
@@ -1,13 +1,15 @@
{ {
"name": "owl", "name": "owl",
"version": "0.4.0", "version": "0.5.0",
"description": "Odoo Web Lab", "description": "Odoo Web Lab",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "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:bundle": "rollup -c",
"build": "npm run build:js && npm run build:bundle", "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\"", "dev": "npm-run-all --parallel \"build:* -- --watch\"",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
@@ -19,12 +21,12 @@
"example:web:serve": "live-server --entry-file=index.html dist/examples/web/", "example:web:serve": "live-server --entry-file=index.html dist/examples/web/",
"preexample:web:dev": "npm run example:web:build", "preexample:web:dev": "npm run example:web:build",
"example:web:dev": "npm-run-all --parallel \"example:web:build:* -- --watch\" example:web:serve", "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:js": "cpx \"examples/benchmarks/{*.html,*.js,*.css}\" dist/examples/benchmarks",
"example:benchmarks:build": "npm-run-all --parallel example:benchmarks:build:*", "example:benchmarks:build": "npm-run-all --parallel example:benchmarks:build:*",
"example:benchmarks:serve": "live-server --entry-file=index.html dist/examples/benchmarks/", "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: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:js": "cpx \"examples/todoapp/{*.html,**/*.js,*.css,*.xml}\" dist/examples/todoapp",
"example:todoapp:build": "npm-run-all --parallel example:todoapp:build:*", "example:todoapp:build": "npm-run-all --parallel example:todoapp:build:*",
"example:todoapp:serve": "live-server --entry-file=index.html dist/examples/todoapp/", "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 // rollup.config.js
export default { export default {
input: "dist/core/src/index.js", input: "dist/owl/src/index.js",
output: { output: {
file: "dist/core.js", file: "dist/owl.js",
format: "iife", format: "iife",
name: "owl", name: "owl",
extend: true, extend: true,
+16 -3
View File
@@ -149,6 +149,16 @@ export class Component<
*/ */
mounted() {} 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 * 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. * 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); Object.assign(this.env, nextEnv);
if (this.__widget__.isMounted) { if (this.__widget__.isMounted) {
return this.render(true); await this.render(true);
} }
this.componentDidUpdate();
} }
async updateProps( async updateProps(
@@ -312,8 +323,9 @@ export class Component<
} }
Object.assign(this.state, nextState); Object.assign(this.state, nextState);
if (this.__widget__.isStarted) { 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> { async _updateProps(nextProps: Props): Promise<void> {
this.props = nextProps; this.props = nextProps;
return this.render(); await this.render();
this.componentDidUpdate();
} }
_patch(vnode) { _patch(vnode) {
+54 -24
View File
@@ -53,10 +53,12 @@ export class Context {
shouldProtectContext: boolean = false; shouldProtectContext: boolean = false;
inLoop: boolean = false; inLoop: boolean = false;
inPreTag: boolean = false; inPreTag: boolean = false;
templateName: string;
constructor() { constructor(name?: string) {
this.rootContext = this; this.rootContext = this;
this.addLine("let h = this.utils.h;"); this.templateName = name || "noname";
this.addLine("var h = this.utils.h;");
} }
generateID(): number { generateID(): number {
@@ -182,7 +184,7 @@ export class QWeb {
templates: { [name: string]: CompiledTemplate<VNode> } = {}; templates: { [name: string]: CompiledTemplate<VNode> } = {};
directives: Directive[] = []; directives: Directive[] = [];
constructor() { constructor(data?: string) {
[ [
forEachDirective, forEachDirective,
escDirective, escDirective,
@@ -196,6 +198,10 @@ export class QWeb {
refDirective, refDirective,
widgetDirective widgetDirective
].forEach(d => this.addDirective(d)); ].forEach(d => this.addDirective(d));
if (data) {
this.loadTemplates(data);
}
} }
utils = { utils = {
@@ -300,6 +306,9 @@ export class QWeb {
loadTemplates(xmlstr: string) { loadTemplates(xmlstr: string) {
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(xmlstr, "text/xml"); 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]; const templates = doc.getElementsByTagName("templates")[0];
if (!templates) { if (!templates) {
return; return;
@@ -330,7 +339,7 @@ export class QWeb {
const mainNode = this.processedTemplates[name]; const mainNode = this.processedTemplates[name];
const isDebug = (<Element>mainNode).attributes.hasOwnProperty("t-debug"); const isDebug = (<Element>mainNode).attributes.hasOwnProperty("t-debug");
const ctx = new Context(); const ctx = new Context(name);
this._compileNode(mainNode, ctx); this._compileNode(mainNode, ctx);
if (ctx.shouldProtectContext) { if (ctx.shouldProtectContext) {
@@ -357,7 +366,12 @@ export class QWeb {
ctx.code.join("\n") ctx.code.join("\n")
) as CompiledTemplate<VNode>; ) as CompiledTemplate<VNode>;
} catch (e) { } 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) { if (isDebug) {
console.log( console.log(
@@ -390,7 +404,7 @@ export class QWeb {
// this is an unusual situation: this text node is the result of the // this is an unusual situation: this text node is the result of the
// template rendering. // template rendering.
let nodeID = ctx.generateID(); let nodeID = ctx.generateID();
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`); ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
ctx.rootContext.rootNode = nodeID; ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentNode = nodeID; ctx.rootContext.parentNode = nodeID;
} }
@@ -515,9 +529,12 @@ export class QWeb {
const value = attributes[i].textContent!; const value = attributes[i].textContent!;
// regular attributes // regular attributes
if (!name.startsWith("t-")) { if (
!name.startsWith("t-") &&
!(<Element>node).getAttribute("t-attf-" + name)
) {
const attID = ctx.generateID(); const attID = ctx.generateID();
ctx.addLine(`let _${attID} = '${value}';`); ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) { if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it // attribute contains 'non letters' => we want to quote it
name = '"' + name + '"'; name = '"' + name + '"';
@@ -546,14 +563,14 @@ export class QWeb {
const attValue = (<Element>node).getAttribute(attName); const attValue = (<Element>node).getAttribute(attName);
if (attValue) { if (attValue) {
const attValueID = ctx.generateID(); const attValueID = ctx.generateID();
ctx.addLine(`let _${attValueID} = ${formattedValue};`); ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`; formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att => const attrIndex = attrs.findIndex(att =>
att.startsWith(attName + ":") att.startsWith(attName + ":")
); );
attrs.splice(attrIndex, 1); attrs.splice(attrIndex, 1);
} }
ctx.addLine(`let _${attID} = ${formattedValue};`); ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`); attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID); handleBooleanProps(attName, attID);
} }
@@ -569,14 +586,21 @@ export class QWeb {
s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}" s => "${" + ctx.formatExpression(s.slice(2, -2)) + "}"
); );
const attID = ctx.generateID(); 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}`); attrs.push(`${attName}: _${attID}`);
} }
// t-att= attributes // t-att= attributes
if (name === "t-att") { if (name === "t-att") {
let id = ctx.generateID(); let id = ctx.generateID();
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`); ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
tattrs.push(id); tattrs.push(id);
} }
} }
@@ -592,7 +616,7 @@ export class QWeb {
parts.push(`on:{}`); 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) { for (let id of tattrs) {
ctx.addIf(`_${id} instanceof Array`); ctx.addIf(`_${id} instanceof Array`);
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`); ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
@@ -605,7 +629,7 @@ export class QWeb {
ctx.closeIf(); ctx.closeIf();
} }
ctx.addLine( 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) { if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`); 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") { if (typeof value === "string") {
const exprID = ctx.generateID(); 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`); ctx.addIf(`e${exprID} || e${exprID} === 0`);
let text = `e${exprID}`; 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}});`); ctx.addLine(`c${ctx.parentNode}.push({text: ${text}});`);
} else { } else {
let fragID = ctx.generateID(); 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(); let tempNodeID = ctx.generateID();
ctx.addLine(`let p${tempNodeID} = {hook: {`); ctx.addLine(`var p${tempNodeID} = {hook: {`);
ctx.addLine( ctx.addLine(
` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),` ` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`
); );
ctx.addLine(`}};`); 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});`); ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
} }
if (node.childNodes.length) { if (node.childNodes.length) {
@@ -809,7 +833,7 @@ const forEachDirective: Directive = {
const elems = node.getAttribute("t-foreach")!; const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!; const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID(); let arrayID = ctx.generateID();
ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`); ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
ctx.addLine( ctx.addLine(
`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}` `if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`
); );
@@ -818,11 +842,11 @@ const forEachDirective: Directive = {
); );
let keysID = ctx.generateID(); let keysID = ctx.generateID();
ctx.addLine( ctx.addLine(
`let _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});` `var _${keysID} = _${arrayID} instanceof Array ? _${arrayID} : Object.keys(_${arrayID});`
); );
let valuesID = ctx.generateID(); let valuesID = ctx.generateID();
ctx.addLine( 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.addLine(`for (let i = 0; i < _${keysID}.length; i++) {`);
ctx.indent(); ctx.indent();
@@ -852,15 +876,19 @@ const onDirective: Directive = {
extraArgs = args.slice(1, -1); extraArgs = args.slice(1, -1);
return ""; return "";
}); });
let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace(
/`/g,
"'"
)}'\`)})()`;
if (extraArgs) { if (extraArgs) {
ctx.addLine( ctx.addLine(
`p${nodeID}.on['${eventName}'] = context['${handler}'].bind(owner, ${ctx.formatExpression( `p${nodeID}.on['${eventName}'] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression(
extraArgs extraArgs
)});` )});`
); );
} else { } else {
ctx.addLine( 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( ctx.addLine(
`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];` `p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`
@@ -931,7 +959,7 @@ const widgetDirective: Directive = {
ctx.addLine( ctx.addLine(
`let w${widgetID} = ${templateID} in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[${templateID}]] : false;` `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};`); ctx.addLine(`let isNew${widgetID} = !w${widgetID};`);
// check if we can reuse current rendering promise // check if we can reuse current rendering promise
@@ -981,6 +1009,8 @@ const widgetDirective: Directive = {
ctx.parentNode 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}.${ }[_${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" keepAlive ? "detach" : "destroy"
}()},destroy(){w${widgetID}.${
keepAlive ? "detach" : "destroy"
}()}}; w${widgetID}.__widget__.pvnode = pvnode;});` }()}}; w${widgetID}.__widget__.pvnode = pvnode;});`
); );
ctx.addElse(); ctx.addElse();
+17 -14
View File
@@ -7,33 +7,36 @@ export function connect(mapStateToProps) {
return class extends Comp { return class extends Comp {
constructor(parent, props?: any) { constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent; const env = parent instanceof Component ? parent.env : parent;
const storeProps = mapStateToProps(env.store.state); const ownProps = Object.assign({}, props || {});
props = Object.assign(props || {}, storeProps); const storeProps = mapStateToProps(env.store.state, ownProps);
super(parent, props); const mergedProps = Object.assign(props || {}, storeProps);
super(parent, mergedProps);
this.__widget__.currentStoreProps = storeProps; this.__widget__.currentStoreProps = storeProps;
this.__widget__.ownProps = ownProps;
} }
mounted() { mounted() {
this.env.store.on("update", this, () => { 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)) { if (!shallowEqual(storeProps, this.__widget__.currentStoreProps)) {
this.__widget__.currentStoreProps = storeProps; this.__widget__.currentStoreProps = storeProps;
// probably not optimal, will do 2 object.assign, one here and this.updateProps(ownProps, false);
// one in updateProps.
const nextProps = Object.assign(
{},
this.props,
this.__widget__.currentStoreProps
);
this.updateProps(nextProps, false);
} }
}); });
super.mounted();
} }
willUnmount() { willUnmount() {
this.env.store.off("update", this); this.env.store.off("update", this);
super.willUnmount();
} }
updateProps(nextProps, forceUpdate) { updateProps(nextProps, forceUpdate) {
nextProps = Object.assign(nextProps, this.__widget__.currentStoreProps); this.__widget__.ownProps = nextProps;
return super.updateProps(nextProps, forceUpdate); 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; let owner = context;
context = Object.create(context); context = Object.create(context);
let h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; var c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
let _2 = context['state'].numbers; var _2 = context['state'].numbers;
if (!_2) { throw new Error('QWeb error: Invalid loop expression')} if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())} if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
let _3 = _2 instanceof Array ? _2 : Object.keys(_2); var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
let _4 = _2 instanceof Array ? _2 : Object.values(_2); var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) { for (let i = 0; i < _3.length; i++) {
context.number_first = i === 0; context.number_first = i === 0;
context.number_last = i === _3.length - 1; 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); c1.push(null);
let def6; let def6;
let w7 = key8 in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[key8]] : false; let w7 = key8 in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[key8]] : false;
let props7 = null; let props7 = {};
let isNew7 = !w7; let isNew7 = !w7;
if (w7 && w7.__widget__.renderPromise) { if (w7 && w7.__widget__.renderPromise) {
if (w7.__widget__.isStarted) { if (w7.__widget__.isStarted) {
@@ -51,7 +51,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
} }
} }
if (isNew7) { 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 { } 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;}); 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 "function anonymous(context,extra
) { ) {
let owner = context; let owner = context;
let h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; var c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
//WIDGET //WIDGET
let key5 = \\"somestring\\"; let key5 = \\"somestring\\";
let _2_index = c1.length; let _2_index = c1.length;
@@ -99,7 +99,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
} }
} }
if (isNew4) { 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 { } 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;}); 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; 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 () => { test("shouldUpdate hook prevent rerendering", async () => {
let shouldUpdate = false; let shouldUpdate = false;
class TestWidget extends Widget { class TestWidget extends Widget {
@@ -404,6 +459,45 @@ describe("lifecycle hooks", () => {
await widget.updateProps({ val: 666 }); await widget.updateProps({ val: 666 });
expect(fixture.innerHTML).toBe("<div>666</div>"); 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", () => { describe("destroy method", () => {
@@ -939,6 +1033,22 @@ describe("random stuff/miscellaneous", () => {
await widget.mount(fixture); await widget.mount(fixture);
expect(env.qweb.templates.parent.toString()).toMatchSnapshot(); 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", () => { describe("async rendering", () => {
+39
View File
@@ -101,6 +101,29 @@ describe("error handling", () => {
qweb.addTemplate("test", `<t></t>`); qweb.addTemplate("test", `<t></t>`);
expect(() => qweb.addTemplate("test", "<div/>")).toThrow("already defined"); 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", () => { describe("t-esc", () => {
@@ -395,6 +418,12 @@ describe("attributes", () => {
expect(result).toBe(`<div foo="bar"></div>`); 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", () => { test("format value", () => {
qweb.addTemplate("test", `<div t-attf-foo="b{{value}}r"/>`); qweb.addTemplate("test", `<div t-attf-foo="b{{value}}r"/>`);
const result = renderToString(qweb, "test", { value: "a" }); const result = renderToString(qweb, "test", { value: "a" });
@@ -872,6 +901,16 @@ describe("t-ref", () => {
}); });
describe("loading templates", () => { 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", () => { 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"?>
+87
View File
@@ -106,4 +106,91 @@ describe("connecting a component to store", () => {
await nextTick(); await nextTick();
expect(fixture.innerHTML).toMatchSnapshot(); 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", "module": "commonjs",
"preserveConstEnums": true, "preserveConstEnums": true,
"noImplicitThis": true, "noImplicitThis": true,
"lib": ["es2015", "dom"],
"removeComments": false, "removeComments": false,
"inlineSourceMap": true, "inlineSourceMap": true,
"declaration": true, "declaration": true,
"target": "es6", "target": "es5",
"outDir": "dist", "outDir": "dist",
"alwaysStrict": true, "alwaysStrict": true,
"noUnusedLocals": true, "noUnusedLocals": true,