Compare commits

..

26 Commits

Author SHA1 Message Date
Lucas Perais (lpe) 21b07afb60 [WIP] transitions in user space: playground 2021-11-25 18:04:54 +01:00
Géry Debongnie 13c3178760 [FIX] slots: properly bind this in t-on arrow functions 2021-11-25 14:45:30 +01:00
Géry Debongnie 2b565ce1b4 [REF] component: small cleanup
This commit makes it simpler to understand the way fibers are assigned
to nodes.
2021-11-25 11:34:23 +01:00
Géry Debongnie cfcf2c6714 [FIX] component: concurrency issue
When a parent and a child were rendered at the same time, it was
possible for the 2 renders to decrement the same fiber internal
counter, which meant that the render was stalled.
2021-11-25 11:12:31 +01:00
Samuel Degueldre 92174559a1 [FIX] slots: allow t-call and components in slot default content 2021-11-24 15:47:51 +01:00
Bruno Boi 840348892f [IMP] owl: upgrade rollup-plugin-typescript2 to version 0.31.1 2021-11-24 11:45:12 +01:00
Samuel Degueldre b988a68b6f [IMP] parser: normalize document before parsing 2021-11-24 11:19:45 +01:00
Samuel Degueldre 7869a1f2c6 [IMP] components: add test for template string in props 2021-11-24 10:09:52 +01:00
Samuel Degueldre 9e81d14d50 [IMP] parser: throw when using unsupported directive on component 2021-11-24 10:09:52 +01:00
Samuel Degueldre 0e4a55ba09 [FIX] components: allow prop names that are not valid bare property name 2021-11-24 10:09:52 +01:00
Mathieu Duckerts-Antoine 6e9b68dafa [FIX] props: prop names can contain - 2021-11-23 10:20:45 +01:00
Bruno Boi ba7c9063c0 Update CHANGELOG.md 2021-11-23 10:09:39 +01:00
Mathieu Duckerts-Antoine 3a361876ad [IMP] slots: via prop 'slots'
The slot inner working has been reworked. A prop "slots" is now passed
explicitely to the component. It looks like

{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }

with the objects slotInfo_i with mandatory keys "__render", "__ctx",
and optional key "__scope" and possibly others.

Here is how a slotInfo object can be created:
A slotInfo object is normally created by setting in a template something
like

<div>
    <t t-set-slot="foo" t-set-scope="scope" param_1="var" param_2="3">
        content
        <t t-esc="scope.bool"/>
        <t t-esc="scope.num"/>
    </t>
</div>

and it will be used somewhere like

<div>
    <t t-esc="props.slots.foo.param_1"/>
    <t t-slot="foo" bool="other_var" num="5">
</div>

In the above example, the function "__render" produces the block dom
element for the content of the t-set-slot.
The context "__ctx" will have a key "scope" with value { bool: ..., num: 5 }
and "__scope" will be set to "scope".
2021-11-22 16:36:43 +01:00
Samuel Degueldre 58b8572f0a [FIX] components: capture context in prop expressions 2021-11-22 13:18:17 +01:00
Samuel Degueldre c23637e6d8 [FIX] components: throw on duplicate t-key instead of hanging the app 2021-11-22 11:11:49 +01:00
Géry Debongnie 4a4b1fbba5 [IMP] app: add templates in app config
Also, improve the parsing code
2021-11-22 10:56:16 +01:00
Géry Debongnie 536d9e1762 [REM] tools: remove benchmarks/debug script
They are either no longer relevant, or less useful than some
alternatives (such as the js framework benchmark project)
2021-11-22 10:56:16 +01:00
Lucas Perais (lpe) 6ed68372a6 [FIX] package: bump owl version to 2.0.0-alpha1 2021-11-22 10:49:51 +01:00
Lucas Perais (lpe) efd934d2b1 [FIX] tools: adapt playground to owl 2 2021-11-19 16:11:28 +01:00
Lucas Perais (lpe) dabc24cee3 [FIX] index, reactivity: export reactive function in index 2021-11-19 16:11:28 +01:00
Samuel Degueldre 7f49796a07 [IMP] misc: update typescript to 4.5.2 2021-11-19 14:42:37 +01:00
Géry Debongnie 757dffefac [IMP] components: rename onRender->onWillRender, add onRendered 2021-11-19 13:26:44 +01:00
Samuel Degueldre bdfe058279 [IMP] reactivity: overhaul reactivity system
This commit makes the reactivity system more fine grained and makes it
more eager to stop observing keys or objects when they are modified,
this results in fewer "false positive" notifications.
2021-11-19 13:20:37 +01:00
Bruno Boi 345c44b952 fixup! [IMP] svg namespace support 2021-11-19 12:47:51 +01:00
Bruno Boi ea74739d46 [IMP] svg namespace support 2021-11-19 11:55:18 +01:00
Géry Debongnie 93b53d8017 [FIX] remove cyclic dependency, improve error typing (#982) 2021-11-18 10:44:49 +01:00
138 changed files with 4384 additions and 68814 deletions
+5 -3
View File
@@ -14,7 +14,7 @@ removed after.
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content)) - components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
- new `useEffect` hook - new `useEffect` hook
- new `onDestroyed` and `onRender` hooks - new `onDestroyed`, `onWillRender` and `onRendered` hooks
- breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed)) - breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed))
- breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element)) - breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element))
- breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler)) - breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler))
@@ -447,8 +447,10 @@ bus.addEventListener('event-name', callback);
Rationale: it makes it easier to have just one interface to remember, it makes Rationale: it makes it easier to have just one interface to remember, it makes
the code simpler the code simpler
Migration: most bus methods need to be adapted. So, `bus.on(...)` has to be Migration: most bus methods need to be adapted. So, `bus.on("event-type", owner, (info) => {...})` has to be
rewritten like this: `bus.addEventListener(...)`. rewritten like this: `bus.addEventListener("event-type", (({detail: info}) => {...}).bind(owner))`.
Do not forget to similarly replace `bus.off(...)` by `bus.removeEventListener(...)`
### 22. `Store` is removed ### 22. `Store` is removed
-43
View File
@@ -1,43 +0,0 @@
# 🦉 How to debug Owl applications 🦉
Non trivial applications become quickly more difficult to understand. It is then
useful to have a solid understanding of what is going on. To help with that,
logging useful information is extremely valuable. There is a [javascript file](../../tools/debug.js) which can be evaluated in an application.
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
```
function debugOwl(t,e){let n,o="[OWL_DEBUG]";function r(t){let e;try{e=JSON.stringify(t||{})}catch(t){e="<JSON error>"}return e.length>200&&(e=e.slice(0,200)+"..."),e}if(Object.defineProperty(t.Component,"current",{get:()=>n,set(s){n=s;const i=s.constructor.name;if(e.componentBlackList&&e.componentBlackList.test(i))return;if(e.componentWhiteList&&!e.componentWhiteList.test(i))return;let l;Object.defineProperty(n,"__owl__",{get:()=>l,set(n){!function(n,s,i){let l=`${s}<id=${i}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!e.methodBlackList||!e.methodBlackList.includes(t))&&!(e.methodWhiteList&&!e.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${r(n.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${r(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=n.__render.bind(n);n.__render=function(...t){c("rendering template"),d(...t)};const h=n.render.bind(n);n.render=function(...t){const e=n.__owl__;let o="render";return e.isMounted||e.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=n.mount.bind(n);n.mount=function(...t){return c("mount"),p(...t)}}(s,i,(l=n).id)}})}}),e.logScheduler){let e=t.Component.scheduler.start,n=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),e.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),n.call(this)}}if(e.logStore){let e=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...n){return console.log(`${o} store: action '${t}' dispatched. Payload: '${r(n)}'`),e.call(this,t,...n)}}}
debugOwl(owl, {
// componentBlackList: /App/, // regexp
// componentWhiteList: /SomeComponent/, // regexp
// methodBlackList: ["mounted"], // list of method names
// methodWhiteList: ["willStart"], // list of method names
logScheduler: false, // display/mute scheduler logs
logStore: true, // display/mute store logs
});
```
The above code, once pasted somewhere in the main javascript file of an owl
application, will log information looking like this:
```
[OWL_DEBUG] TodoApp<id=1> constructor, props={}
[OWL_DEBUG] TodoApp<id=1> mount
[OWL_DEBUG] TodoApp<id=1> willStart
[OWL_DEBUG] TodoApp<id=1> rendering template
[OWL_DEBUG] TodoItem<id=2> constructor, props={"id":2,"completed":false,"title":"hey"}
[OWL_DEBUG] TodoItem<id=2> willStart
[OWL_DEBUG] TodoItem<id=3> constructor, props={"id":4,"completed":false,"title":"aaa"}
[OWL_DEBUG] TodoItem<id=3> willStart
[OWL_DEBUG] TodoItem<id=2> rendering template
[OWL_DEBUG] TodoItem<id=3> rendering template
[OWL_DEBUG] TodoItem<id=3> mounted
[OWL_DEBUG] TodoItem<id=2> mounted
[OWL_DEBUG] TodoApp<id=1> mounted
```
Each component has an internal `id`, which is very useful when debugging.
Note that it is certainly useful to run this code at some point in an application,
just to get a feel of what each user action implies, for the framework.
-1
View File
@@ -9,7 +9,6 @@ Are you new to Owl? This is the place to start!
- [How to start an Owl project](learning/quick_start.md) - [How to start an Owl project](learning/quick_start.md)
- [How to test Components](learning/how_to_test.md) - [How to test Components](learning/how_to_test.md)
- [How to write Single File Components](learning/how_to_write_sfc.md) - [How to write Single File Components](learning/how_to_write_sfc.md)
- [How to write debug Owl applications](learning/how_to_debug.md)
## Reference ## Reference
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "1.4.7", "version": "2.0.0-alpha1",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js", "browser": "dist/owl.iife.js",
@@ -18,10 +18,10 @@
"test": "jest", "test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch", "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"tools:serve": "python3 tools/server.py || python tools/server.py", "playground:serve": "python3 tools/server.py || python tools/server.py",
"tools": "npm run build && npm run tools:serve", "playground": "npm run build && npm run playground:serve",
"pretools:watch": "npm run build", "preplayground:watch": "npm run build",
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"", "playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write", "prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check", "check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
"publish": "npm run build && npm publish", "publish": "npm run build && npm publish",
@@ -52,11 +52,11 @@
"prettier": "2.4.1", "prettier": "2.4.1",
"rollup": "^2.56.3", "rollup": "^2.56.3",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.30.0", "rollup-plugin-typescript2": "^0.31.1",
"sass": "^1.16.1", "sass": "^1.16.1",
"source-map-support": "^0.5.10", "source-map-support": "^0.5.10",
"ts-jest": "^27.0.5", "ts-jest": "^27.0.5",
"typescript": "3.9.6", "typescript": "4.5.2",
"uglify-es": "^3.3.9" "uglify-es": "^3.3.9"
}, },
"jest": { "jest": {
+4
View File
@@ -15,6 +15,7 @@ export interface AppConfig {
env?: Env; env?: Env;
translatableAttributes?: string[]; translatableAttributes?: string[];
translateFn?: (s: string) => string; translateFn?: (s: string) => string;
templates?: string | Document;
} }
export const DEV_MSG = `Owl is running in 'dev' mode. export const DEV_MSG = `Owl is running in 'dev' mode.
@@ -49,6 +50,9 @@ export class App<T extends typeof Component = any> extends TemplateSet {
if (config.translatableAttributes) { if (config.translatableAttributes) {
this.translatableAttributes = config.translatableAttributes; this.translatableAttributes = config.translatableAttributes;
} }
if (config.templates) {
this.addTemplates(config.templates);
}
return this; return this;
} }
+12 -7
View File
@@ -17,19 +17,24 @@ function callSlot(
parent: any, parent: any,
key: string, key: string,
name: string, name: string,
defaultSlot?: (ctx: any, key: string) => BDom, dynamic: boolean,
dynamic?: boolean extra: any,
defaultContent?: (ctx: any, node: any, key: string) => BDom
): BDom { ): BDom {
const slots = ctx.__owl__.slots; const slots = (ctx.props && ctx.props.slots) || {};
const slotFn = slots[name]; const { __render, __ctx, __scope } = slots[name] || {};
const slotBDom = slotFn ? slotFn(parent, key) : null; const slotScope = Object.create(__ctx || {});
if (defaultSlot) { if (__scope) {
slotScope[__scope] = extra || {};
}
const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
if (defaultContent) {
let child1: BDom | undefined = undefined; let child1: BDom | undefined = undefined;
let child2: BDom | undefined = undefined; let child2: BDom | undefined = undefined;
if (slotBDom) { if (slotBDom) {
child1 = dynamic ? toggler(name, slotBDom) : slotBDom; child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
} else { } else {
child2 = defaultSlot(parent, key); child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
} }
return multi([child1, child2]); return multi([child1, child2]);
} }
+35 -1
View File
@@ -7,6 +7,36 @@ const bdom = { text, createBlock, list, multi, html, toggler, component };
export const globalTemplates: { [key: string]: string | Node } = {}; export const globalTemplates: { [key: string]: string | Node } = {};
function parseXML(xml: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template.";
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
if (parsererrorText) {
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
const re = /\d+/g;
const firstMatch = re.exec(parsererrorText);
if (firstMatch) {
const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
}
return doc;
}
export class TemplateSet { export class TemplateSet {
rawTemplates: typeof globalTemplates = Object.create(globalTemplates); rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {}; templates: { [name: string]: Template } = {};
@@ -33,7 +63,11 @@ export class TemplateSet {
} }
addTemplates(xml: string | Document, options: { allowDuplicate?: boolean } = {}) { addTemplates(xml: string | Document, options: { allowDuplicate?: boolean } = {}) {
xml = xml instanceof Document ? xml : new DOMParser().parseFromString(xml, "text/xml"); if (!xml) {
// empty string
return;
}
xml = xml instanceof Document ? xml : parseXML(xml);
for (const template of xml.querySelectorAll("[t-name]")) { for (const template of xml.querySelectorAll("[t-name]")) {
const name = template.getAttribute("t-name")!; const name = template.getAttribute("t-name")!;
template.removeAttribute("t-name"); template.removeAttribute("t-name");
+20 -9
View File
@@ -116,6 +116,7 @@ interface IntermediateTree {
forceRef?: boolean; forceRef?: boolean;
refIdx?: number; refIdx?: number;
refN: number; refN: number;
currentNS: string | null;
} }
function buildTree( function buildTree(
@@ -124,9 +125,10 @@ function buildTree(
domParentTree: IntermediateTree | null = null domParentTree: IntermediateTree | null = null
): IntermediateTree { ): IntermediateTree {
switch (node.nodeType) { switch (node.nodeType) {
case 1: { case Node.ELEMENT_NODE: {
// HTMLElement // HTMLElement
let isActive = false; let isActive = false;
let currentNS = parent && parent.currentNS;
const tagName = (node as Element).tagName; const tagName = (node as Element).tagName;
let el: Node | undefined = undefined; let el: Node | undefined = undefined;
const info: DynamicInfo[] = []; const info: DynamicInfo[] = [];
@@ -143,11 +145,18 @@ function buildTree(
el = document.createTextNode(""); el = document.createTextNode("");
isActive = true; isActive = true;
} }
if (!el) {
el = document.createElement(tagName);
}
if (el instanceof HTMLElement) {
const attrs = (node as Element).attributes; const attrs = (node as Element).attributes;
const ns = attrs.getNamedItem("block-ns");
if (ns) {
attrs.removeNamedItem("block-ns");
currentNS = ns.value;
}
if (!el) {
el = currentNS
? document.createElementNS(currentNS, tagName)
: document.createElement(tagName);
}
if (el instanceof Element) {
for (let i = 0; i < attrs.length; i++) { for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i].name; const attrName = attrs[i].name;
const attrValue = attrs[i].value; const attrValue = attrs[i].value;
@@ -193,13 +202,14 @@ function buildTree(
el, el,
info, info,
refN: isActive ? 1 : 0, refN: isActive ? 1 : 0,
currentNS,
}; };
if (node.firstChild) { if (node.firstChild) {
const childNode = node.childNodes[0]; const childNode = node.childNodes[0];
if ( if (
node.childNodes.length === 1 && node.childNodes.length === 1 &&
childNode.nodeType === 1 && childNode.nodeType === Node.ELEMENT_NODE &&
(childNode as Element).tagName.startsWith("block-child-") (childNode as Element).tagName.startsWith("block-child-")
) { ) {
const tagName = (childNode as Element).tagName; const tagName = (childNode as Element).tagName;
@@ -227,11 +237,11 @@ function buildTree(
} }
return tree; return tree;
} }
case 3: case Node.TEXT_NODE:
case 8: { case Node.COMMENT_NODE: {
// text node or comment node // text node or comment node
const el = const el =
node.nodeType === 3 node.nodeType === Node.TEXT_NODE
? document.createTextNode(node.textContent!) ? document.createTextNode(node.textContent!)
: document.createComment(node.textContent!); : document.createComment(node.textContent!);
return { return {
@@ -241,6 +251,7 @@ function buildTree(
el, el,
info: [], info: [],
refN: 0, refN: 0,
currentNS: null,
}; };
} }
} }
+111 -52
View File
@@ -137,7 +137,6 @@ function createContext(parentCtx: Context, params?: Partial<Context>) {
class CodeTarget { class CodeTarget {
name: string; name: string;
signature: string = "";
indentLevel = 0; indentLevel = 0;
loopLevel = 0; loopLevel = 0;
code: string[] = []; code: string[] = [];
@@ -334,7 +333,7 @@ export class CodeGenerator {
generateFunctions(fn: CodeTarget) { generateFunctions(fn: CodeTarget) {
this.addLine(""); this.addLine("");
this.addLine(`const ${fn.name} = ${fn.signature}`); this.addLine(`function ${fn.name}(ctx, node, key) {`);
if (fn.hasCache) { if (fn.hasCache) {
this.addLine(`let cache = ctx.cache || {};`); this.addLine(`let cache = ctx.cache || {};`);
this.addLine(`let nextCache = ctx.cache = {};`); this.addLine(`let nextCache = ctx.cache = {};`);
@@ -344,8 +343,23 @@ export class CodeGenerator {
} }
this.addLine(`}`); this.addLine(`}`);
} }
/**
captureExpression(expr: string): string { * Captures variables that are used inside of an expression. This is useful
* because in compiled code, almost all variables are accessed through the ctx
* object. In the case of functions, that lookup in the context can be delayed
* which can cause issues if the value has changed since the function was
* defined.
*
* @param expr the expression to capture
* @param forceCapture whether the expression should capture its scope even if
* it doesn't contain a function. Useful when the expression will be used as
* a function body.
* @returns a new expression that uses the captured values
*/
captureExpression(expr: string, forceCapture: boolean = false): string {
if (!forceCapture && !expr.includes("=>")) {
return compileExpr(expr);
}
const tokens = compileExprToArray(expr); const tokens = compileExprToArray(expr);
const mapping = new Map<string, string>(); const mapping = new Map<string, string>();
return tokens return tokens
@@ -497,6 +511,10 @@ export class CodeGenerator {
} }
// attributes // attributes
const attrs: { [key: string]: string } = {}; const attrs: { [key: string]: string } = {};
if (ast.ns) {
// specific namespace uri
attrs["block-ns"] = ast.ns;
}
for (let key in ast.attrs) { for (let key in ast.attrs) {
if (key.startsWith("t-attf")) { if (key.startsWith("t-attf")) {
let expr = interpolate(ast.attrs[key]); let expr = interpolate(ast.attrs[key]);
@@ -531,7 +549,7 @@ export class CodeGenerator {
if (isDynamic) { if (isDynamic) {
const str = ast.ref.replace( const str = ast.ref.replace(
INTERP_REGEXP, INTERP_REGEXP,
(expr) => "${" + this.captureExpression(expr.slice(2, -2)) + "}" (expr) => "${" + this.captureExpression(expr.slice(2, -2), true) + "}"
); );
const idx = block!.insertData(`(el) => refs[\`${str}\`] = el`); const idx = block!.insertData(`(el) => refs[\`${str}\`] = el`);
attrs["block-ref"] = String(idx); attrs["block-ref"] = String(idx);
@@ -730,6 +748,10 @@ export class CodeGenerator {
this.addLine( this.addLine(
`const [${keys}, ${vals}, ${l}, ${c}] = prepareList(${compileExpr(ast.collection)});` `const [${keys}, ${vals}, ${l}, ${c}] = prepareList(${compileExpr(ast.collection)});`
); );
// Throw errors on duplicate keys in dev mode
if (this.dev) {
this.addLine(`const keys${block.id} = new Set();`);
}
this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`); this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
this.target.indentLevel++; this.target.indentLevel++;
this.addLine(`ctx[\`${ast.elem}\`] = ${vals}[${loopVar}];`); this.addLine(`ctx[\`${ast.elem}\`] = ${vals}[${loopVar}];`);
@@ -746,6 +768,13 @@ export class CodeGenerator {
this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`); this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`);
} }
this.addLine(`let key${this.target.loopLevel} = ${ast.key ? compileExpr(ast.key) : loopVar};`); this.addLine(`let key${this.target.loopLevel} = ${ast.key ? compileExpr(ast.key) : loopVar};`);
if (this.dev) {
// Throw error on duplicate keys in dev mode
this.addLine(
`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new Error(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`
);
this.addLine(`keys${block.id}.add(key${this.target.loopLevel});`);
}
let id: string; let id: string;
if (ast.memo) { if (ast.memo) {
this.target.hasCache = true; this.target.hasCache = true;
@@ -932,13 +961,62 @@ export class CodeGenerator {
compileComponent(ast: ASTComponent, ctx: Context) { compileComponent(ast: ASTComponent, ctx: Context) {
let { block } = ctx; let { block } = ctx;
let extraArgs: { [key: string]: string } = {};
// props // props
const props: string[] = []; const props: string[] = [];
let hasSlotsProp = false;
for (let p in ast.props) { for (let p in ast.props) {
props.push(`${p}: ${compileExpr(ast.props[p]) || undefined}`); const propName = /^[a-z_]+$/i.test(p) ? p : `'${p}'`;
props.push(`${propName}: ${this.captureExpression(ast.props[p]) || undefined}`);
if (p === "slots") {
hasSlotsProp = true;
} }
}
// slots
const hasSlot = !!Object.keys(ast.slots).length;
let slotDef: string = "";
if (hasSlot) {
let ctxStr = "ctx";
if (this.target.loopLevel || !this.hasSafeContext) {
ctxStr = this.generateId("ctx");
this.addLine(`const ${ctxStr} = capture(ctx);`);
}
let slotStr: string[] = [];
const initialTarget = this.target;
for (let slotName in ast.slots) {
let name = this.generateId("slot");
const slot = new CodeTarget(name);
this.functions.push(slot);
this.target = slot;
const subCtx: Context = createContext(ctx);
this.compileAST(ast.slots[slotName].content, subCtx);
const params = [`__render: ${name}, __ctx: ${ctxStr}`];
const scope = ast.slots[slotName].scope;
if (scope) {
params.push(`__scope: "${scope}"`);
}
if (ast.slots[slotName].attrs) {
for (const [n, v] of Object.entries(ast.slots[slotName].attrs!)) {
params.push(`${n}: ${compileExpr(v) || undefined}`);
}
}
const slotInfo = `{${params.join(", ")}}`;
if (this.hasRef) {
slot.code.unshift(` const refs = ctx.__owl__.refs`);
slotStr.push(`'${slotName}': ${slotInfo}`);
} else {
slotStr.push(`'${slotName}': ${slotInfo}`);
}
}
this.target = initialTarget;
slotDef = `{${slotStr.join(", ")}}`;
}
if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
props.push(`slots: ${slotDef}`);
}
const propStr = `{${props.join(",")}}`; const propStr = `{${props.join(",")}}`;
let propString = propStr; let propString = propStr;
@@ -950,6 +1028,17 @@ export class CodeGenerator {
} }
} }
let propVar: string;
if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
propVar = this.generateId("props");
this.addLine(`const ${propVar!} = ${propString}`);
propString = propVar!;
}
if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
this.addLine(`${propVar!}.slots = Object.assign(${slotDef}, ${propVar!}.slots)`);
}
// cmap key // cmap key
const key = this.generateComponentKey(); const key = this.generateComponentKey();
let expr: string; let expr: string;
@@ -961,41 +1050,7 @@ export class CodeGenerator {
} }
if (this.dev) { if (this.dev) {
const propVar = this.generateId("props"); this.addLine(`helpers.validateProps(${expr}, ${propVar!}, ctx)`);
this.addLine(`const ${propVar} = ${propString}`);
this.addLine(`helpers.validateProps(${expr}, ${propVar}, ctx)`);
propString = propVar;
}
// slots
const hasSlot = !!Object.keys(ast.slots).length;
let slotDef: string;
if (hasSlot) {
let ctxStr = "ctx";
if (this.target.loopLevel || !this.hasSafeContext) {
ctxStr = this.generateId("ctx");
this.addLine(`const ${ctxStr} = capture(ctx);`);
}
let slotStr: string[] = [];
const initialTarget = this.target;
for (let slotName in ast.slots) {
let name = this.generateId("slot");
const slot = new CodeTarget(name);
slot.signature = "ctx => (node, key) => {";
this.functions.push(slot);
this.target = slot;
const subCtx: Context = createContext(ctx);
this.compileAST(ast.slots[slotName], subCtx);
if (this.hasRef) {
slot.code.unshift(` const refs = ctx.__owl__.refs`);
slotStr.push(`'${slotName}': ${name}(${ctxStr})`);
} else {
slotStr.push(`'${slotName}': ${name}(${ctxStr})`);
}
}
this.target = initialTarget;
slotDef = `{${slotStr.join(", ")}}`;
extraArgs.slots = slotDef;
} }
if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) { if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) {
@@ -1009,11 +1064,6 @@ export class CodeGenerator {
} }
const blockArgs = `${expr}, ${propString}, ${keyArg}, node, ctx`; const blockArgs = `${expr}, ${propString}, ${keyArg}, node, ctx`;
let blockExpr = `component(${blockArgs})`; let blockExpr = `component(${blockArgs})`;
if (Object.keys(extraArgs).length) {
this.shouldDefineAssign = true;
const content = Object.keys(extraArgs).map((k) => `${k}: ${extraArgs[k]}`);
blockExpr = `assign(${blockExpr}, {${content.join(", ")}})`;
}
if (ast.isDynamic) { if (ast.isDynamic) {
blockExpr = `toggler(${expr}, ${blockExpr})`; blockExpr = `toggler(${expr}, ${blockExpr})`;
} }
@@ -1032,24 +1082,33 @@ export class CodeGenerator {
} else { } else {
slotName = "'" + ast.name + "'"; slotName = "'" + ast.name + "'";
} }
let scope = null;
if (ast.attrs) {
const params = [];
for (const [n, v] of Object.entries(ast.attrs!)) {
params.push(`${n}: ${compileExpr(v) || undefined}`);
}
scope = `{${params.join(", ")}}`;
}
if (ast.defaultContent) { if (ast.defaultContent) {
let name = this.generateId("defaultSlot"); let name = this.generateId("defaultContent");
const slot = new CodeTarget(name); const slot = new CodeTarget(name);
slot.signature = "ctx => {";
this.functions.push(slot); this.functions.push(slot);
const initialTarget = this.target; const initialTarget = this.target;
const subCtx: Context = createContext(ctx); const subCtx: Context = createContext(ctx);
this.target = slot; this.target = slot;
this.compileAST(ast.defaultContent, subCtx); this.compileAST(ast.defaultContent, subCtx);
this.target = initialTarget; this.target = initialTarget;
blockString = `callSlot(ctx, node, key, ${slotName}, ${name}, ${dynamic})`; blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`;
} else { } else {
if (dynamic) { if (dynamic) {
let name = this.generateId("slot"); let name = this.generateId("slot");
this.addLine(`const ${name} = ${slotName};`); this.addLine(`const ${name} = ${slotName};`);
blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}))`; blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}), ${dynamic}, ${scope})`;
} else { } else {
blockString = `callSlot(ctx, node, key, ${slotName})`; blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`;
} }
} }
if (block) { if (block) {
+144 -45
View File
@@ -49,6 +49,7 @@ export interface ASTDomNode {
shouldTrim: boolean; shouldTrim: boolean;
shouldNumberize: boolean; shouldNumberize: boolean;
} | null; } | null;
ns: string | null;
} }
export interface ASTMulti { export interface ASTMulti {
@@ -117,12 +118,13 @@ export interface ASTComponent {
isDynamic: boolean; isDynamic: boolean;
dynamicProps: string | null; dynamicProps: string | null;
props: { [name: string]: string }; props: { [name: string]: string };
slots: { [name: string]: AST }; slots: { [name: string]: { content: AST; attrs?: { [key: string]: string }; scope?: string } };
} }
export interface ASTSlot { export interface ASTSlot {
type: ASTType.TSlot; type: ASTType.TSlot;
name: string; name: string;
attrs: { [key: string]: string };
defaultContent: AST | null; defaultContent: AST | null;
} }
@@ -171,11 +173,13 @@ export type AST =
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
interface ParsingContext { interface ParsingContext {
inPreTag: boolean; inPreTag: boolean;
inSVG: boolean;
} }
export function parse(xml: string | Node): AST { export function parse(xml: string | Node): AST {
const node = xml instanceof Element ? xml : parseXML(`<t>${xml}</t>`).firstChild!; const node = xml instanceof Element ? xml : (parseXML(`<t>${xml}</t>`).firstChild! as Element);
const ctx = { inPreTag: false }; normalizeXML(node);
const ctx = { inPreTag: false, inSVG: false };
const ast = parseNode(node, ctx); const ast = parseNode(node, ctx);
if (!ast) { if (!ast) {
return { type: ASTType.Text, value: "" }; return { type: ASTType.Text, value: "" };
@@ -240,7 +244,7 @@ const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g; const whitespaceRE = /\s+/g;
function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null { function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null {
if (node.nodeType === 3) { if (node.nodeType === Node.TEXT_NODE) {
let value = node.textContent || ""; let value = node.textContent || "";
if (!ctx.inPreTag) { if (!ctx.inPreTag) {
if (lineBreakRE.test(value) && !value.trim()) { if (lineBreakRE.test(value) && !value.trim()) {
@@ -250,7 +254,7 @@ function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null
} }
return { type: ASTType.Text, value }; return { type: ASTType.Text, value };
} else if (node.nodeType === 8) { } else if (node.nodeType === Node.COMMENT_NODE) {
return { type: ASTType.Comment, value: node.textContent || "" }; return { type: ASTType.Comment, value: node.textContent || "" };
} }
return null; return null;
@@ -289,18 +293,18 @@ const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const { tagName } = node; const { tagName } = node;
let dynamicTag = null; const dynamicTag = node.getAttribute("t-tag");
if (node.hasAttribute("t-tag")) {
dynamicTag = node.getAttribute("t-tag");
node.removeAttribute("t-tag"); node.removeAttribute("t-tag");
}
if (tagName === "t" && !dynamicTag) { if (tagName === "t" && !dynamicTag) {
return null; return null;
} }
const children: AST[] = []; const children: AST[] = [];
if (tagName === "pre") { if (tagName === "pre") {
ctx = { inPreTag: true }; ctx.inPreTag = true;
} }
const shouldAddSVGNS = tagName === "svg" || (tagName === "g" && !ctx.inSVG);
ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
const ref = node.getAttribute("t-ref"); const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref"); node.removeAttribute("t-ref");
@@ -381,6 +385,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
ref, ref,
content: children, content: children,
model, model,
ns,
}; };
} }
@@ -590,7 +595,7 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
if (ast && ast.type === ASTType.TComponent) { if (ast && ast.type === ASTType.TComponent) {
return { return {
...ast, ...ast,
slots: { default: tcall }, slots: { default: { content: tcall } },
}; };
} }
} }
@@ -699,6 +704,20 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
// Components // Components
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Error messages when trying to use an unsupported directive on a component
const directiveErrorMap = new Map([
["t-on", "t-on is no longer supported on components. Consider passing a callback in props."],
[
"t-ref",
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
],
["t-att", "t-att makes no sense on component: props are already treated as expressions"],
[
"t-attf",
"t-attf is not supported on components: use template strings for string interpolation in props",
],
]);
function parseComponent(node: Element, ctx: ParsingContext): AST | null { function parseComponent(node: Element, ctx: ParsingContext): AST | null {
let name = node.tagName; let name = node.tagName;
const firstLetter = name[0]; const firstLetter = name[0];
@@ -722,10 +741,9 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
const props: ASTComponent["props"] = {}; const props: ASTComponent["props"] = {};
for (let name of node.getAttributeNames()) { for (let name of node.getAttributeNames()) {
const value = node.getAttribute(name)!; const value = node.getAttribute(name)!;
if (name.startsWith("t-on-")) { if (name.startsWith("t-")) {
throw new Error( const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
"t-on is no longer supported on Component node. Consider passing a callback in props." throw new Error(message || `unsupported directive on Component: ${name}`);
);
} else { } else {
props[name] = value; props[name] = value;
} }
@@ -738,6 +756,11 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
// named slots // named slots
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]")); const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
for (let slotNode of slotNodes) { for (let slotNode of slotNodes) {
if (slotNode.tagName !== "t") {
throw new Error(
`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`
);
}
const name = slotNode.getAttribute("t-set-slot")!; const name = slotNode.getAttribute("t-set-slot")!;
// check if this is defined in a sub component (in which case it should // check if this is defined in a sub component (in which case it should
@@ -759,14 +782,27 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
slotNode.remove(); slotNode.remove();
const slotAst = parseNode(slotNode, ctx); const slotAst = parseNode(slotNode, ctx);
if (slotAst) { if (slotAst) {
slots[name] = slotAst; const slotInfo: any = { content: slotAst };
const attrs: { [key: string]: string } = {};
for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName)!;
if (attributeName === "t-slot-scope") {
slotInfo.scope = value;
continue;
}
attrs[attributeName] = value;
}
if (Object.keys(attrs).length) {
slotInfo.attrs = attrs;
}
slots[name] = slotInfo;
} }
} }
// default slot // default slot
const defaultContent = parseChildNodes(clone, ctx); const defaultContent = parseChildNodes(clone, ctx);
if (defaultContent) { if (defaultContent) {
slots.default = defaultContent; slots.default = { content: defaultContent };
} }
} }
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots }; return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots };
@@ -780,9 +816,17 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-slot")) { if (!node.hasAttribute("t-slot")) {
return null; return null;
} }
const name = node.getAttribute("t-slot")!;
node.removeAttribute("t-slot");
const attrs: { [key: string]: string } = {};
for (let attributeName of node.getAttributeNames()) {
const value = node.getAttribute(attributeName)!;
attrs[attributeName] = value;
}
return { return {
type: ASTType.TSlot, type: ASTType.TSlot,
name: node.getAttribute("t-slot")!, name,
attrs,
defaultContent: parseChildNodes(node, ctx), defaultContent: parseChildNodes(node, ctx),
}; };
} }
@@ -819,34 +863,17 @@ function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
return { type: ASTType.Multi, content: children }; return { type: ASTType.Multi, content: children };
} }
} }
function parseXML(xml: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml"); /**
if (doc.getElementsByTagName("parsererror").length) { * Normalizes the content of an Element so that t-if/t-elif/t-else directives
let msg = "Invalid XML in template."; * immediately follow one another (by removing empty text nodes or comments).
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent; * Throws an error when a conditional branching statement is malformed. This
if (parsererrorText) { * function modifies the Element in place.
msg += "\nThe parser has produced the following error message:\n" + parsererrorText; *
const re = /\d+/g; * @param el the element containing the tree that should be normalized
const firstMatch = re.exec(parsererrorText); */
if (firstMatch) { function normalizeTIf(el: Element) {
const lineNumber = Number(firstMatch[0]); let tbranch = el.querySelectorAll("[t-elif], [t-else]");
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
}
let tbranch = doc.querySelectorAll("[t-elif], [t-else]");
for (let i = 0, ilen = tbranch.length; i < ilen; i++) { for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
let node = tbranch[i]; let node = tbranch[i];
let prevElem = node.previousElementSibling!; let prevElem = node.previousElementSibling!;
@@ -880,6 +907,78 @@ function parseXML(xml: string): Document {
); );
} }
} }
}
/**
* Normalizes the content of an Element so that t-esc directives on components
* are removed and instead places a <t t-esc=""> as the default slot of the
* component. Also throws if the component already has content. This function
* modifies the Element in place.
*
* @param el the element containing the tree that should be normalized
*/
function normalizeTEsc(el: Element) {
const elements = [...el.querySelectorAll("[t-esc]")].filter(
(el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component")
);
for (const el of elements) {
if (el.childNodes.length) {
throw new Error("Cannot have t-esc on a component that already has content");
}
const value = el.getAttribute("t-esc");
el.removeAttribute("t-esc");
const t = el.ownerDocument.createElement("t");
if (value != null) {
t.setAttribute("t-esc", value);
}
el.appendChild(t);
}
}
/**
* Normalizes the tree inside a given element and do some preliminary validation
* on it. This function modifies the Element in place.
*
* @param el the element containing the tree that should be normalized
*/
function normalizeXML(el: Element) {
normalizeTIf(el);
normalizeTEsc(el);
}
/**
* Parses an XML string into an XML document, throwing errors on parser errors
* instead of returning an XML document containing the parseerror.
*
* @param xml the string to parse
* @returns an XML document corresponding to the content of the string
*/
function parseXML(xml: string): XMLDocument {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "text/xml");
if (doc.getElementsByTagName("parsererror").length) {
let msg = "Invalid XML in template.";
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
if (parsererrorText) {
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
const re = /\d+/g;
const firstMatch = re.exec(parsererrorText);
if (firstMatch) {
const lineNumber = Number(firstMatch[0]);
const line = xml.split("\n")[lineNumber - 1];
const secondMatch = re.exec(parsererrorText);
if (line && secondMatch) {
const columnIndex = Number(secondMatch[0]) - 1;
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
}
throw new Error(msg);
}
return doc; return doc;
} }
+1
View File
@@ -8,6 +8,7 @@ import type { ComponentNode } from "./component_node";
export class Component { export class Component {
static template: string = ""; static template: string = "";
static style: string = ""; static style: string = "";
static props?: any;
props: any; props: any;
env: Env; env: Env;
+25 -10
View File
@@ -72,7 +72,9 @@ export function getCurrent(): ComponentNode | null {
type LifecycleHook = Function; type LifecycleHook = Function;
export class ComponentNode<T extends typeof Component = any> implements VNode<ComponentNode> { export class ComponentNode<T extends typeof Component = typeof Component>
implements VNode<ComponentNode>
{
el?: HTMLElement | Text | undefined; el?: HTMLElement | Text | undefined;
app: App; app: App;
fiber: Fiber | null = null; fiber: Fiber | null = null;
@@ -85,7 +87,6 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
level: number; level: number;
childEnv: Env; childEnv: Env;
children: { [key: string]: ComponentNode } = Object.create(null); children: { [key: string]: ComponentNode } = Object.create(null);
slots: any = {};
refs: any = {}; refs: any = {};
willStart: LifecycleHook[] = []; willStart: LifecycleHook[] = [];
@@ -120,6 +121,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
} }
async initiateRender(fiber: Fiber | MountFiber) { async initiateRender(fiber: Fiber | MountFiber) {
this.fiber = fiber;
if (this.mounted.length) { if (this.mounted.length) {
fiber.root.mounted.push(fiber); fiber.root.mounted.push(fiber);
} }
@@ -127,7 +129,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
try { try {
await Promise.all(this.willStart.map((f) => f.call(component))); await Promise.all(this.willStart.map((f) => f.call(component)));
} catch (e) { } catch (e) {
handleError(this, e as Error); handleError({ node: this, error: e });
return; return;
} }
if (this.status === STATUS.NEW && this.fiber === fiber) { if (this.status === STATUS.NEW && this.fiber === fiber) {
@@ -136,21 +138,33 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
} }
async render() { async render() {
let fiber = this.fiber; const current = this.fiber;
if (fiber && !fiber.bdom && !fibersInError.has(fiber)) { if (current && !current.bdom && !fibersInError.has(current)) {
return fiber.root.promise; return current.root.promise;
} }
if (!this.bdom && !fiber) { if (!this.bdom && !current) {
// should find a way to return the future mounting promise // should find a way to return the future mounting promise
return; return;
} }
fiber = makeRootFiber(this); const fiber = makeRootFiber(this);
this.fiber = fiber;
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
await Promise.resolve(); await Promise.resolve();
if (this.status === STATUS.DESTROYED) { if (this.status === STATUS.DESTROYED) {
return; return;
} }
if (this.fiber === fiber) { // We only want to actually render the component if the following two
// conditions are true:
// * this.fiber: it could be null, in which case the render has been cancelled
// * (current || !fiber.parent): if current is not null, this means that the
// render function was called when a render was already occurring. In this
// case, the pending rendering was cancelled, and the fiber needs to be
// rendered to complete the work. If current is null, we check that the
// fiber has no parent. If that is the case, the fiber was downgraded from
// a root fiber to a child fiber in the previous microtick, because it was
// embedded in a rendering coming from above, so the fiber will be rendered
// in the next microtick anyway, so we should not render it again.
if (this.fiber && (current || !fiber.parent)) {
this._render(fiber); this._render(fiber);
} }
return fiber.root.promise; return fiber.root.promise;
@@ -161,7 +175,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
fiber.bdom = this.renderFn(); fiber.bdom = this.renderFn();
fiber.root.counter--; fiber.root.counter--;
} catch (e) { } catch (e) {
handleError(this, e as Error); handleError({ node: this, error: e });
} }
} }
@@ -199,6 +213,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
async updateAndRender(props: any, parentFiber: Fiber) { async updateAndRender(props: any, parentFiber: Fiber) {
// update // update
const fiber = makeChildFiber(this, parentFiber); const fiber = makeChildFiber(this, parentFiber);
this.fiber = fiber;
if (this.willPatch.length) { if (this.willPatch.length) {
parentFiber.root.willPatch.push(fiber); parentFiber.root.willPatch.push(fiber);
} }
+11 -15
View File
@@ -1,10 +1,11 @@
import type { ComponentNode } from "./component_node"; import type { ComponentNode } from "./component_node";
import type { Fiber } from "./fibers"; import type { Fiber } from "./fibers";
export const fibersInError: WeakMap<Fiber, Error> = new WeakMap(); // Maps fibers to thrown errors
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: Error) => void)[]> = new WeakMap(); export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
function _handleError(node: ComponentNode | null, error: Error, isFirstRound = false): boolean { function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean {
if (!node) { if (!node) {
return false; return false;
} }
@@ -25,7 +26,7 @@ function _handleError(node: ComponentNode | null, error: Error, isFirstRound = f
h(error); h(error);
propagate = false; propagate = false;
} catch (e) { } catch (e) {
error = e as Error; error = e;
} }
} }
@@ -38,17 +39,12 @@ function _handleError(node: ComponentNode | null, error: Error, isFirstRound = f
} }
} }
export function handleError(entity: ComponentNode | Fiber, error: Error) { type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
let node: ComponentNode; export function handleError(params: ErrorParams) {
let fiber: Fiber; const error = params.error;
// soft type check on Fiber const node = "node" in params ? params.node : params.fiber.node;
if ("node" in entity) { const fiber = "fiber" in params ? params.fiber : node.fiber!;
fiber = entity;
node = entity.node;
} else {
node = entity;
fiber = entity.fiber!;
}
fibersInError.set(fiber.root, error); fibersInError.set(fiber.root, error);
const handled = _handleError(node, error, true); const handled = _handleError(node, error, true);
+2 -3
View File
@@ -70,7 +70,6 @@ export class Fiber {
constructor(node: ComponentNode, parent: Fiber | null) { constructor(node: ComponentNode, parent: Fiber | null) {
this.node = node; this.node = node;
node.fiber = this;
this.parent = parent; this.parent = parent;
if (parent) { if (parent) {
const root = parent.root; const root = parent.root;
@@ -159,7 +158,7 @@ export class RootFiber extends Fiber {
// unregistering the fiber // unregistering the fiber
node.fiber = null; node.fiber = null;
} catch (e) { } catch (e) {
if (!handleError(current || this, e)) { if (!handleError({ fiber: current || this, error: e })) {
this.reject(e); this.reject(e);
} }
} }
@@ -206,7 +205,7 @@ export class MountFiber extends RootFiber {
} }
node.fiber = null; node.fiber = null;
} catch (e) { } catch (e) {
if (!handleError(current as Fiber, e)) { if (!handleError({ fiber: current as Fiber, error: e })) {
this.reject(e); this.reject(e);
} }
} }
+11 -1
View File
@@ -40,7 +40,7 @@ export function onDestroyed(fn: () => Promise<void> | void | any) {
node.destroyed.push(fn); node.destroyed.push(fn);
} }
export function onRender(fn: () => void | any) { export function onWillRender(fn: () => void | any) {
const node = getCurrent()!; const node = getCurrent()!;
const renderFn = node.renderFn; const renderFn = node.renderFn;
node.renderFn = () => { node.renderFn = () => {
@@ -49,6 +49,16 @@ export function onRender(fn: () => void | any) {
}; };
} }
export function onRendered(fn: () => void | any) {
const node = getCurrent()!;
const renderFn = node.renderFn;
node.renderFn = () => {
const result = renderFn();
fn();
return result;
};
}
export function onError(fn: (error: Error) => void | any) { export function onError(fn: (error: Error) => void | any) {
const node = getCurrent()!; const node = getCurrent()!;
let handlers = nodeErrorHandlers.get(node); let handlers = nodeErrorHandlers.get(node);
+9 -10
View File
@@ -26,26 +26,23 @@ export function applyDefaultProps(props: { [key: string]: any }, ComponentClass:
* visit recursively the props and all the children to check if they are valid. * visit recursively the props and all the children to check if they are valid.
* This is why it is only done in 'dev' mode. * This is why it is only done in 'dev' mode.
*/ */
// message: { type: String, default: "hello" };
export const validateProps = function (name: string | typeof Component, props: any, parent?: any) { export const validateProps = function (name: string | typeof Component, props: any, parent?: any) {
const ComponentClass = const ComponentClass = (
typeof name !== "string" ? name : parent.constructor.components[name as string]; typeof name !== "string" ? name : parent.constructor.components[name]
) as typeof Component;
applyDefaultProps(props, ComponentClass); applyDefaultProps(props, ComponentClass);
const propsDef = (<any>ComponentClass).props; const propsDef = ComponentClass.props;
if (propsDef instanceof Array) { if (propsDef instanceof Array) {
// list of strings (prop names) // list of strings (prop names)
for (let i = 0, l = propsDef.length; i < l; i++) { for (const propName of propsDef) {
const propName = propsDef[i];
if (propName[propName.length - 1] === "?") { if (propName[propName.length - 1] === "?") {
// optional prop // optional prop
break; break;
} }
if (!(propName in props)) { if (!(propName in props)) {
throw new Error(`Missing props '${propsDef[i]}' (component '${ComponentClass.name}')`); throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
} }
} }
for (let key in props) { for (let key in props) {
@@ -67,7 +64,9 @@ export const validateProps = function (name: string | typeof Component, props: a
try { try {
isValid = isValidProp(props[propName], propsDef[propName]); isValid = isValidProp(props[propName], propsDef[propName]);
} catch (e) { } catch (e) {
e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`; (e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
(e as Error).message
})`;
throw e; throw e;
} }
if (!isValid) { if (!isValid) {
+3 -2
View File
@@ -55,7 +55,7 @@ export { status } from "./component/status";
export { Portal } from "./portal"; export { Portal } from "./portal";
export { Memo } from "./memo"; export { Memo } from "./memo";
export { css, xml } from "./tags"; export { css, xml } from "./tags";
export { useState } from "./reactivity"; export { useState, reactive } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks"; export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
export { EventBus, whenReady, loadFile, markup } from "./utils"; export { EventBus, whenReady, loadFile, markup } from "./utils";
@@ -66,7 +66,8 @@ export {
onWillUpdateProps, onWillUpdateProps,
onWillPatch, onWillPatch,
onPatched, onPatched,
onRender, onWillRender,
onRendered,
onDestroyed, onDestroyed,
onError, onError,
} from "./component/lifecycle_hooks"; } from "./component/lifecycle_hooks";
+1 -1
View File
@@ -39,7 +39,7 @@ export class Memo extends Component {
*/ */
function shallowEqual(p1: any, p2: any): boolean { function shallowEqual(p1: any, p2: any): boolean {
for (let k in p1) { for (let k in p1) {
if (p1[k] !== p2[k]) { if (k !== "slots" && p1[k] !== p2[k]) {
return false; return false;
} }
} }
+219 -256
View File
@@ -1,132 +1,28 @@
import { ComponentNode, getCurrent } from "./component/component_node";
import { onWillUnmount } from "./component/lifecycle_hooks"; import { onWillUnmount } from "./component/lifecycle_hooks";
import { ComponentNode, getCurrent } from "./component/component_node";
type Observer = ComponentNode | Function; // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
type Atom = any; // proxy linked to a unique observer and source const TARGET = Symbol("Target");
type Source = any; // trackable that is not an atom // Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes");
const sourceAtoms: WeakMap<Source, Map<any, ObserverSet>> = new WeakMap(); type ObjectKey = string | number | symbol;
const observerSourceAtom: WeakMap<Observer, Map<any, Atom>> = new WeakMap(); type Target = object;
type Callback = () => void;
type Reactive<T extends Target = Target> = T & {
[TARGET]: any;
};
const SOURCE = Symbol("source"); /**
const OBSERVER = Symbol("observer"); * Checks whether a given value can be made into a reactive object.
const KEYS = Symbol("keys"); *
const ROOT = Symbol("root"); * @param value the value to check
const SEED = Symbol("Seed"); * @returns whether the value can be made reactive
*/
export function observe(source: any, observer: Observer): [Atom, Function] { function canBeMadeReactive(value: any): boolean {
if (!isTrackable(source)) {
throw new Error("First argument is not trackable");
}
const unregisterObserver = registerObserver(observer);
const newAtom = atom(source, observer);
return [newAtom, unregisterObserver];
}
function atom(source: any, observer: Observer) {
return _atom(source, observer, true);
}
function _atom(source: any, observer: Observer, seed = false) {
if (isTrackable(source) && observerSourceAtom.has(observer)) {
source = (source as any)[SOURCE] || source;
const oldAtom = observerSourceAtom.get(observer)!.get(source);
if (oldAtom) {
if (seed) {
oldAtom[SEED] = true;
}
return oldAtom;
}
if (!sourceAtoms.get(source)) {
sourceAtoms.set(source, new Map([[ROOT, new ObserverSet()]]));
}
const newAtom = createAtom(source, observer);
if (seed) {
newAtom[SEED] = true;
}
observerSourceAtom.get(observer)!.set(source, newAtom);
sourceAtoms.get(source)!.get(ROOT)!.add(newAtom);
return newAtom;
}
return source;
}
function createAtom(source: Source, observer: Observer): Atom {
const keys: Set<any> = new Set();
let seed: boolean = false;
const newAtom: Atom = new Proxy(source as any, {
set(target: any, key: any, value: any): boolean {
if (key === SEED) {
seed = value;
return true;
}
if (!(key in target)) {
target[key] = value;
notify(sourceAtoms.get(source)!.get(ROOT)!);
return true;
}
const current = target[key];
if (current !== value) {
target[key] = value;
const observerSet = sourceAtoms.get(source)!.get(key);
if (observerSet) {
const clean = isTrackable(current);
notify(observerSet, clean);
}
}
return true;
},
deleteProperty(target: any, key: string): boolean {
if (key in target) {
const current = target[key];
delete target[key];
// notify source observers
const clean = isTrackable(current);
notify(sourceAtoms.get(source)!.get(ROOT)!, clean);
const atoms = sourceAtoms.get(source)!;
if (atoms.has(key)) {
// clear source-key observers
for (const atom of atoms.get(key)!) {
atom[KEYS].delete(key);
}
atoms.delete(key);
}
}
return true;
},
get(target: any, key: any, proxy: any): any {
switch (key) {
case OBSERVER:
return observer;
case SOURCE:
return source;
case KEYS:
return keys;
case SEED:
return seed;
default:
const value = target[key];
// register observer to source-key
if (!keys.has(key) && observerSourceAtom.has(observer)) {
const atoms = sourceAtoms.get(source)!;
if (!atoms.has(key)) {
atoms.set(key, new ObserverSet());
}
atoms.get(key)!.add(newAtom);
keys.add(key);
}
//
return _atom(value, observer);
}
},
});
return newAtom;
}
function isTrackable(value: any): boolean {
return ( return (
value !== null &&
typeof value === "object" && typeof value === "object" &&
value !== null &&
!(value instanceof Date) && !(value instanceof Date) &&
!(value instanceof Promise) && !(value instanceof Promise) &&
!(value instanceof String) && !(value instanceof String) &&
@@ -134,144 +30,211 @@ function isTrackable(value: any): boolean {
); );
} }
function registerObserver(observer: Observer) { const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
if (!observerSourceAtom.get(observer)) { /**
observerSourceAtom.set(observer, new Map()); * Observes a given key on a target with an callback. The callback will be
* called when the given key changes on the target.
*
* @param target the target whose key should be observed
* @param key the key to observe (or Symbol(KEYCHANGES) for key creation
* or deletion)
* @param callback the function to call when the key changes
*/
function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): void {
if (!targetToKeysToCallbacks.get(target)) {
targetToKeysToCallbacks.set(target, new Map());
} }
return unregisterObserverAtoms.bind(null, observer, false); const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
if (!keyToCallbacks.get(key)) {
keyToCallbacks.set(key, new Set());
} }
keyToCallbacks.get(key)!.add(callback);
function unregisterObserverAtoms(observer: Observer, keepSeeds: boolean) {
const observerAtoms = observerSourceAtom.get(observer)!;
for (const [source, atom] of observerAtoms) {
if (keepSeeds && atom[SEED]) {
continue;
} }
observerAtoms.delete(source); /**
const atoms = sourceAtoms.get(source)!; * Notify Reactives that are observing a given target that a key has changed on
atoms.get(ROOT)!.delete(atom); * the target.
for (const key of atom[KEYS]) { *
atoms.get(key)!.delete(atom); * @param target target whose Reactives should be notified that the target was
} * changed.
} * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
if (!keepSeeds) { * or deleted)
observerSourceAtom.delete(observer); */
} function notifyReactives(target: Target, key: ObjectKey): void {
} const keyToCallbacks = targetToKeysToCallbacks.get(target);
if (!keyToCallbacks) {
export function useState(state: any): Atom {
const node = getCurrent()!;
const [newAtom, unregisterObserver] = observe(state, node);
onWillUnmount(() => unregisterObserver());
return newAtom;
}
class ObserverSet {
nodeAntichain: Antichain = new Antichain();
callbackSet: Set<Atom> = new Set();
add(atom: Atom) {
if (atom[OBSERVER] instanceof ComponentNode) {
this.nodeAntichain.add(atom);
} else {
this.callbackSet.add(atom);
}
return this;
}
delete(atom: Atom) {
if (atom[OBSERVER] instanceof ComponentNode) {
return this.nodeAntichain.delete(atom);
} else {
return this.callbackSet.delete(atom);
}
}
union(other: ObserverSet) {
for (const atom of other.nodeAntichain) {
this.nodeAntichain.add(atom);
}
for (const callback of other.callbackSet) {
this.callbackSet.add(callback);
}
}
notify() {
for (const atom of this.nodeAntichain) {
// an observer cannot be found twice here: <= in add is based on observers
atom[OBSERVER].render();
}
const called = new Set();
for (const atom of this.callbackSet) {
const observer = atom[OBSERVER];
if (!called.has(observer)) {
observer();
}
called.add(observer);
}
}
*[Symbol.iterator]() {
yield* this.nodeAntichain;
yield* this.callbackSet;
}
}
// Need to optimize this!
function isLessOrEqual(node1: ComponentNode, node2: ComponentNode) {
let current: any = node1;
if (current.level <= node2.level) {
return false;
}
do {
if (current === node2) {
return true;
}
current = current.parent;
} while (current);
return false;
}
// set of atoms linked to observers of type ComponentNode
class Antichain extends Set<Atom> {
level?: number;
add(atom: Atom) {
const node = atom[OBSERVER];
if (this.level === node.level || this.level === undefined) {
super.add(atom);
this.level = node.level;
return this;
}
let willAdd = false;
for (const atom2 of this) {
const node2 = atom2[OBSERVER];
if (!willAdd && isLessOrEqual(node, node2)) {
return this;
} else if (isLessOrEqual(node2, node)) {
super.delete(atom2);
willAdd = true;
}
}
super.add(atom);
this.level = NaN;
return this;
}
}
let toNotify: ObserverSet | null = null;
let toClean: Set<Observer> = new Set();
async function notify(observers: ObserverSet, clean = false) {
if (clean) {
for (const atom of observers) {
toClean.add(atom[OBSERVER]);
}
}
if (toNotify) {
toNotify.union(observers);
return; return;
} }
toNotify = new ObserverSet(); const callbacks = keyToCallbacks.get(key);
toNotify.union(observers); if (!callbacks) {
return;
}
// Loop on copy because clearReactivesForCallback will modify the set in place
for (const callback of [...callbacks]) {
clearReactivesForCallback(callback);
callback();
}
}
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
/**
* Clears all subscriptions of the Reactives associated with a given callback.
*
* @param callback the callback for which the reactives need to be cleared
*/
function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) {
return;
}
for (const target of targetsToClear) {
const observedKeys = targetToKeysToCallbacks.get(target);
if (!observedKeys) {
continue;
}
for (const callbacks of observedKeys.values()) {
callbacks.delete(callback);
}
}
}
const reactiveCache = new WeakMap<Target, Map<Callback, Reactive>>();
/**
* Creates a reactive proxy for an object. Reading data on the reactive object
* subscribes to changes to the data. Writing data on the object will cause the
* notify callback to be called if there are suscriptions to that data. Nested
* objects and arrays are automatically made reactive as well.
*
* Whenever you are notified of a change, all subscriptions are cleared, and if
* you would like to be notified of any further changes, you should go read
* the underlying data again. We assume that if you don't go read it again after
* being notified, it means that you are no longer interested in that data.
*
* Subscriptions:
* + Reading a property on an object will subscribe you to changes in the value
* of that property.
* + Accessing an object keys (eg with Object.keys or with `for..in`) will
* subscribe you to the creation/deletion of keys. Checking the presence of a
* key on the object with 'in' has the same effect.
* - getOwnPropertyDescriptor does not currently subscribe you to the property.
* This is a choice that was made because changing a key's value will trigger
* this trap and we do not want to subscribe by writes. This also means that
* Object.hasOwnProperty doesn't subscribe as it goes through this trap.
*
* @param target the object for which to create a reactive proxy
* @param callback the function to call when an observed property of the
* reactive has changed
* @returns a proxy that tracks changes to it
*/
export function reactive<T extends Target>(target: T, callback: Callback): Reactive<T> {
if (!canBeMadeReactive(target)) {
throw new Error(`Cannot make the given value reactive`);
}
const originalTarget = (target as Reactive)[TARGET];
if (originalTarget) {
return reactive(originalTarget, callback);
}
if (!reactiveCache.has(target)) {
reactiveCache.set(target, new Map());
}
const reactivesForTarget = reactiveCache.get(target)!;
if (!reactivesForTarget.has(callback)) {
const proxy = new Proxy(target, {
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
if (key === TARGET) {
return target;
}
observeTargetKey(target, key, callback);
const value = Reflect.get(target, key, proxy);
if (!canBeMadeReactive(value)) {
return value;
}
return reactive(value, callback);
},
set(target, key, value, proxy) {
const isNewKey = !Object.hasOwnProperty.call(target, key);
const originalValue = Reflect.get(target, key, proxy);
const ret = Reflect.set(target, key, value, proxy);
if (isNewKey) {
notifyReactives(target, KEYCHANGES);
}
// While Array length may trigger the set trap, it's not actually set by this
// method but is updated behind the scenes, and the trap is not called with the
// new value. We disable the "same-value-optimization" for it because of that.
if (originalValue !== value || (Array.isArray(target) && key === "length")) {
notifyReactives(target, key);
}
return ret;
},
deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key);
notifyReactives(target, KEYCHANGES);
notifyReactives(target, key);
return ret;
},
ownKeys(target) {
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.ownKeys(target);
},
has(target, key) {
// TODO: this observes all key changes instead of only the presence of the argument key
observeTargetKey(target, KEYCHANGES, callback);
return Reflect.has(target, key);
},
});
reactivesForTarget.set(callback, proxy);
if (!callbacksToTargets.has(callback)) {
callbacksToTargets.set(callback, new Set());
}
callbacksToTargets.get(callback)!.add(target);
}
return reactivesForTarget.get(callback) as Reactive<T>;
}
/**
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
*
* @param callback the callback to batch
* @returns a batched version of the original callback
*/
export function batched(callback: Callback): Callback {
let called = false;
return async () => {
// This await blocks all calls to the callback here, then releases them sequentially
// in the next microtick. This line decides the granularity of the batch.
await Promise.resolve(); await Promise.resolve();
for (const observer of toClean) { if (!called) {
unregisterObserverAtoms(observer, true); called = true;
callback();
// wait for all calls in this microtick to fall through before resetting "called"
// so that only the first call to the batched function calls the original callback
await Promise.resolve();
called = false;
} }
toClean.clear(); };
toNotify.notify(); }
toNotify = null;
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/**
* Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the
* component to subscribe to that data and be rerendered when it changes.
*
* @param state the state to observe
* @returns a reactive object that will cause the component to re-render on
* relevant changes
* @see reactive
*/
export function useState<T extends object>(state: T): Reactive<T> {
const node = getCurrent()!;
if (!batchedRenderFunctions.has(node)) {
batchedRenderFunctions.set(
node,
batched(() => node.render())
);
}
const render = batchedRenderFunctions.get(node)!;
const reactiveState = reactive(state, render);
onWillUnmount(() => clearReactivesForCallback(render));
return reactiveState;
} }
+2 -2
View File
@@ -5,9 +5,9 @@ import { globalTemplates } from "./app/template_set";
// Global templates // Global templates
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export function xml(strings: TemplateStringsArray, ...args: any[]) { export function xml(...args: Parameters<typeof String.raw>) {
const name = `__template__${xml.nextId++}`; const name = `__template__${xml.nextId++}`;
const value = String.raw(strings, ...args); const value = String.raw(...args);
globalTemplates[name] = value; globalTemplates[name] = value;
return name; return name;
} }
@@ -1,5 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reactivity: useState concurrent renderings 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<span><block-text-0/><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['context'][ctx['props'].key].n;
let d2 = ctx['state'].x;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentC\`, {key: ctx['props'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentB\`, {key: ctx['context'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = ` exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
+72
View File
@@ -0,0 +1,72 @@
import { createBlock, mount } from "../../src/blockdom";
import { makeTestFixture } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
const XHTML_URI = "http://www.w3.org/1999/xhtml";
const SVG_URI = "http://www.w3.org/2000/svg";
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("namespace", () => {
test("default namespace is xhtml", () => {
const block = createBlock(`<tag/>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tag></tag>");
expect(fixture.firstElementChild!.namespaceURI).toBe(XHTML_URI);
});
test("namespace can be changed with block-ns", () => {
const block = createBlock(`<tag block-ns="${SVG_URI}"/>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tag></tag>");
expect(fixture.firstElementChild!.namespaceURI).toBe(SVG_URI);
});
test("namespace is kept for children", () => {
const block = createBlock(
`<parent block-ns="${SVG_URI}"><child><subchild/></child><child/></parent>`
);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe(
"<parent><child><subchild></subchild></child><child></child></parent>"
);
const parent = fixture.firstElementChild!;
const child1 = parent.firstElementChild!;
const subchild = child1.firstElementChild!;
const child2 = child1.nextElementSibling!;
expect(parent.namespaceURI).toBe(SVG_URI);
expect(child1.namespaceURI).toBe(SVG_URI);
expect(child2.namespaceURI).toBe(SVG_URI);
expect(subchild.namespaceURI).toBe(SVG_URI);
});
test("various namespaces in same block", () => {
const block = createBlock(`<none><one block-ns="one"/><two block-ns="two"/></none>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<none><one></one><two></two></none>");
const none = fixture.firstElementChild!;
const one = none.firstElementChild!;
const two = one.nextElementSibling!;
expect(none.namespaceURI).toBe(XHTML_URI);
expect(one.namespaceURI).toBe("one");
expect(two.namespaceURI).toBe("two");
});
});
@@ -9,8 +9,7 @@ exports[`t-on can bind event handler 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`); let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add']; let d1 = [ctx['add'], ctx];
let d1 = [v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -116,10 +115,8 @@ exports[`t-on can bind two event handlers 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\" block-handler-1=\\"dblclick\\">Click</button>\`); let block1 = createBlock(\`<button block-handler-0=\\"click\\" block-handler-1=\\"dblclick\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['handleClick']; let d1 = [ctx['handleClick'], ctx];
let d1 = [v1, ctx]; let d2 = [ctx['handleDblClick'], ctx];
const v2 = ctx['handleDblClick'];
let d2 = [v2, ctx];
return block1([d1, d2]); return block1([d1, d2]);
} }
}" }"
@@ -134,8 +131,7 @@ exports[`t-on handler is bound to proper owner 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`); let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add']; let d1 = [ctx['add'], ctx];
let d1 = [v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -155,8 +151,7 @@ exports[`t-on handler is bound to proper owner, part 2 1`] = `
for (let i1 = 0; i1 < l_block1; i1++) { for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`value\`] = v_block1[i1]; ctx[\`value\`] = v_block1[i1];
let key1 = ctx['value']; let key1 = ctx['value'];
const v1 = ctx['add']; let d1 = [ctx['add'], ctx];
let d1 = [v1, ctx];
c_block1[i1] = withKey(block2([d1]), key1); c_block1[i1] = withKey(block2([d1]), key1);
} }
return list(c_block1); return list(c_block1);
@@ -173,8 +168,7 @@ exports[`t-on handler is bound to proper owner, part 3 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`); let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add']; let d1 = [ctx['add'], ctx];
let d1 = [v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -202,8 +196,7 @@ exports[`t-on handler is bound to proper owner, part 4 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`); let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add']; let d1 = [ctx['add'], ctx];
let d1 = [v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -242,8 +235,7 @@ exports[`t-on receive event in first argument 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`); let block1 = createBlock(\`<button block-handler-0=\\"click\\">Click</button>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['add']; let d1 = [ctx['add'], ctx];
let d1 = [v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -258,10 +250,8 @@ exports[`t-on t-on modifiers (native listener) basic support for native listener
let block1 = createBlock(\`<div class=\\"myClass\\" block-handler-0=\\"click\\"><button block-handler-1=\\"click\\">Button</button></div>\`); let block1 = createBlock(\`<div class=\\"myClass\\" block-handler-0=\\"click\\"><button block-handler-1=\\"click\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['divClicked']; let d1 = [ctx['divClicked'], ctx];
let d1 = [v1, ctx]; let d2 = [ctx['btnClicked'], ctx];
const v2 = ctx['btnClicked'];
let d2 = [v2, ctx];
return block1([d1, d2]); return block1([d1, d2]);
} }
}" }"
@@ -276,8 +266,7 @@ exports[`t-on t-on modifiers (native listener) t-on combined with t-esc 1`] = `
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><block-text-1/></button></div>\`); let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><block-text-1/></button></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick']; let d1 = [ctx['onClick'], ctx];
let d1 = [v1, ctx];
let d2 = ctx['text']; let d2 = ctx['text'];
return block1([d1, d2]); return block1([d1, d2]);
} }
@@ -293,8 +282,7 @@ exports[`t-on t-on modifiers (native listener) t-on combined with t-out 1`] = `
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><block-child-0/></button></div>\`); let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><block-child-0/></button></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick']; let d1 = [ctx['onClick'], ctx];
let d1 = [v1, ctx];
let b2 = safeOutput(ctx['html']); let b2 = safeOutput(ctx['html']);
return block1([d1], [b2]); return block1([d1], [b2]);
} }
@@ -310,10 +298,8 @@ exports[`t-on t-on modifiers (native listener) t-on with .capture modifier 1`] =
let block1 = createBlock(\`<div block-handler-0=\\"click.capture\\"><button block-handler-1=\\"click\\">Button</button></div>\`); let block1 = createBlock(\`<div block-handler-0=\\"click.capture\\"><button block-handler-1=\\"click\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onCapture']; let d1 = [\\"capture\\", ctx['onCapture'], ctx];
let d1 = [\\"capture\\", v1, ctx]; let d2 = [ctx['doSomething'], ctx];
const v2 = ctx['doSomething'];
let d2 = [v2, ctx];
return block1([d1, d2]); return block1([d1, d2]);
} }
}" }"
@@ -343,8 +329,7 @@ exports[`t-on t-on modifiers (native listener) t-on with prevent and self modifi
let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent.self\\"><span>Button</span></button></div>\`); let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent.self\\"><span>Button</span></button></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick']; let d1 = [\\"prevent\\",\\"self\\", ctx['onClick'], ctx];
let d1 = [\\"prevent\\",\\"self\\", v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -359,12 +344,9 @@ exports[`t-on t-on modifiers (native listener) t-on with prevent and/or stop mod
let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent\\">Button 1</button><button block-handler-1=\\"click.stop\\">Button 2</button><button block-handler-2=\\"click.prevent.stop\\">Button 3</button></div>\`); let block1 = createBlock(\`<div><button block-handler-0=\\"click.prevent\\">Button 1</button><button block-handler-1=\\"click.stop\\">Button 2</button><button block-handler-2=\\"click.prevent.stop\\">Button 3</button></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClickPrevented']; let d1 = [\\"prevent\\", ctx['onClickPrevented'], ctx];
let d1 = [\\"prevent\\", v1, ctx]; let d2 = [\\"stop\\", ctx['onClickStopped'], ctx];
const v2 = ctx['onClickStopped']; let d3 = [\\"prevent\\",\\"stop\\", ctx['onClickPreventedAndStopped'], ctx];
let d2 = [\\"stop\\", v2, ctx];
const v3 = ctx['onClickPreventedAndStopped'];
let d3 = [\\"prevent\\",\\"stop\\", v3, ctx];
return block1([d1, d2, d3]); return block1([d1, d2, d3]);
} }
}" }"
@@ -406,8 +388,7 @@ exports[`t-on t-on modifiers (native listener) t-on with self and prevent modifi
let block1 = createBlock(\`<div><button block-handler-0=\\"click.self.prevent\\"><span>Button</span></button></div>\`); let block1 = createBlock(\`<div><button block-handler-0=\\"click.self.prevent\\"><span>Button</span></button></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick']; let d1 = [\\"self\\",\\"prevent\\", ctx['onClick'], ctx];
let d1 = [\\"self\\",\\"prevent\\", v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -422,10 +403,8 @@ exports[`t-on t-on modifiers (native listener) t-on with self modifier 1`] = `
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><span>Button</span></button><button block-handler-1=\\"click.self\\"><span>Button</span></button></div>\`); let block1 = createBlock(\`<div><button block-handler-0=\\"click\\"><span>Button</span></button><button block-handler-1=\\"click.self\\"><span>Button</span></button></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick']; let d1 = [ctx['onClick'], ctx];
let d1 = [v1, ctx]; let d2 = [\\"self\\", ctx['onClickSelf'], ctx];
const v2 = ctx['onClickSelf'];
let d2 = [\\"self\\", v2, ctx];
return block1([d1, d2]); return block1([d1, d2]);
} }
}" }"
@@ -440,10 +419,8 @@ exports[`t-on t-on modifiers (synthetic listener) basic support for synthetic 1`
let block1 = createBlock(\`<div block-handler-0=\\"click.synthetic\\"><button block-handler-1=\\"click.synthetic\\">Button</button></div>\`); let block1 = createBlock(\`<div block-handler-0=\\"click.synthetic\\"><button block-handler-1=\\"click.synthetic\\">Button</button></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['divClicked']; let d1 = [\\"synthetic\\", ctx['divClicked'], ctx];
let d1 = [\\"synthetic\\", v1, ctx]; let d2 = [\\"synthetic\\", ctx['btnClicked'], ctx];
const v2 = ctx['btnClicked'];
let d2 = [\\"synthetic\\", v2, ctx];
return block1([d1, d2]); return block1([d1, d2]);
} }
}" }"
@@ -523,8 +500,7 @@ exports[`t-on t-on with t-call 1`] = `
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`); let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['update']; let d1 = [ctx['update'], ctx];
let d1 = [v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
+15 -27
View File
@@ -182,7 +182,7 @@ exports[`misc other complex template 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
const callTemplate_14 = getTemplate(\`LOAD_INFOS_TEMPLATE\`); const callTemplate_2 = getTemplate(\`LOAD_INFOS_TEMPLATE\`);
let block1 = createBlock(\`<div><header><nav class=\\"navbar navbar-expand-md navbar-light bg-light\\"><a block-attribute-0=\\"href\\"><b style=\\"color:#777;\\"><block-text-1/></b></a><button type=\\"button\\" class=\\"navbar-toggler\\" data-toggle=\\"collapse\\" data-target=\\"#top_menu_collapse\\"><span class=\\"navbar-toggler-icon\\"/></button><div class=\\"collapse navbar-collapse\\" id=\\"top_menu_collapse\\" aria-expanded=\\"false\\"><ul class=\\"nav navbar-nav ml-auto text-right\\" id=\\"top_menu\\"><block-child-0/><li class=\\"nav-item divider\\"/><block-child-1/></ul><div><div class=\\"input-group input-group-sm\\"><div class=\\"input-group-prepend input-group-sm\\"><button class=\\"btn btn-default fa fa-cog\\" title=\\"Settings\\" block-handler-2=\\"click\\"/><button class=\\"btn btn-default\\" block-handler-3=\\"click\\"> More </button><block-child-2/></div><input class=\\"form-control\\" type=\\"text\\" placeholder=\\"Search\\" aria-label=\\"Search\\" name=\\"search\\" block-attribute-4=\\"value\\" block-handler-5=\\"keyup\\" block-handler-6=\\"change\\" block-ref=\\"7\\"/><div class=\\"input-group-append\\"><button class=\\"btn btn-default fa fa-eraser\\" block-handler-8=\\"click\\"/></div></div></div></div></nav></header><div class=\\"container-fluid\\" block-ref=\\"9\\"><div class=\\"row\\"><!--div class=\\"form-group col-md-6\\"> let block1 = createBlock(\`<div><header><nav class=\\"navbar navbar-expand-md navbar-light bg-light\\"><a block-attribute-0=\\"href\\"><b style=\\"color:#777;\\"><block-text-1/></b></a><button type=\\"button\\" class=\\"navbar-toggler\\" data-toggle=\\"collapse\\" data-target=\\"#top_menu_collapse\\"><span class=\\"navbar-toggler-icon\\"/></button><div class=\\"collapse navbar-collapse\\" id=\\"top_menu_collapse\\" aria-expanded=\\"false\\"><ul class=\\"nav navbar-nav ml-auto text-right\\" id=\\"top_menu\\"><block-child-0/><li class=\\"nav-item divider\\"/><block-child-1/></ul><div><div class=\\"input-group input-group-sm\\"><div class=\\"input-group-prepend input-group-sm\\"><button class=\\"btn btn-default fa fa-cog\\" title=\\"Settings\\" block-handler-2=\\"click\\"/><button class=\\"btn btn-default\\" block-handler-3=\\"click\\"> More </button><block-child-2/></div><input class=\\"form-control\\" type=\\"text\\" placeholder=\\"Search\\" aria-label=\\"Search\\" name=\\"search\\" block-attribute-4=\\"value\\" block-handler-5=\\"keyup\\" block-handler-6=\\"change\\" block-ref=\\"7\\"/><div class=\\"input-group-append\\"><button class=\\"btn btn-default fa fa-eraser\\" block-handler-8=\\"click\\"/></div></div></div></div></nav></header><div class=\\"container-fluid\\" block-ref=\\"9\\"><div class=\\"row\\"><!--div class=\\"form-group col-md-6\\">
<h5>Search options</h5> <h5>Search options</h5>
@@ -223,9 +223,7 @@ exports[`misc other complex template 1`] = `
for (let i1 = 0; i1 < l_block2; i1++) { for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`project\`] = v_block2[i1]; ctx[\`project\`] = v_block2[i1];
let key1 = ctx['project'].id; let key1 = ctx['project'].id;
const v1 = ctx['selectProject']; let d3 = [ctx['selectProject'](ctx['project']), ctx];
const v2 = ctx['project'];
let d3 = [v1(v2), ctx];
let d4 = ctx['project'].name; let d4 = ctx['project'].name;
c_block2[i1] = withKey(block3([d3, d4]), key1); c_block2[i1] = withKey(block3([d3, d4]), key1);
} }
@@ -257,10 +255,8 @@ exports[`misc other complex template 1`] = `
} }
b4 = multi([b5, b6]); b4 = multi([b5, b6]);
} }
const v3 = ctx['toggleSettingsMenu']; let d11 = [ctx['toggleSettingsMenu'], ctx];
let d11 = [v3, ctx]; let d12 = [ctx['toggleMore'], ctx];
const v4 = ctx['toggleMore'];
let d12 = [v4, ctx];
if (ctx['categories']&&ctx['categories'].length>1) { if (ctx['categories']&&ctx['categories'].length>1) {
ctx = Object.create(ctx); ctx = Object.create(ctx);
const [k_block15, v_block15, l_block15, c_block15] = prepareList(ctx['categories']); const [k_block15, v_block15, l_block15, c_block15] = prepareList(ctx['categories']);
@@ -277,13 +273,10 @@ exports[`misc other complex template 1`] = `
b14 = block14([], [b15]); b14 = block14([], [b15]);
} }
let d16 = ctx['search'].value; let d16 = ctx['search'].value;
const v5 = ctx['updateFilter']; let d17 = [ctx['updateFilter'], ctx];
let d17 = [v5, ctx]; let d18 = [ctx['updateFilter'], ctx];
const v6 = ctx['updateFilter'];
let d18 = [v6, ctx];
let d19 = (el) => refs[\`search_input\`] = el; let d19 = (el) => refs[\`search_input\`] = el;
const v7 = ctx['clearSearch']; let d20 = [ctx['clearSearch'], ctx];
let d20 = [v7, ctx];
let d21 = (el) => refs[\`settings_menu\`] = el; let d21 = (el) => refs[\`settings_menu\`] = el;
if (ctx['triggers']) { if (ctx['triggers']) {
ctx = Object.create(ctx); ctx = Object.create(ctx);
@@ -297,8 +290,7 @@ exports[`misc other complex template 1`] = `
let d23 = \`trigger_\${ctx['trigger'].id}\`; let d23 = \`trigger_\${ctx['trigger'].id}\`;
let d24 = ctx['options'].trigger_display[ctx['trigger'].id]; let d24 = ctx['options'].trigger_display[ctx['trigger'].id];
let d25 = ctx['trigger'].id; let d25 = ctx['trigger'].id;
const v8 = ctx['updateTriggerDisplay']; let d26 = [ctx['updateTriggerDisplay'], ctx];
let d26 = [v8, ctx];
let d27 = \`trigger_\${ctx['trigger'].id}\`; let d27 = \`trigger_\${ctx['trigger'].id}\`;
let d28 = ctx['trigger'].name; let d28 = ctx['trigger'].name;
b20 = block20([d22, d23, d24, d25, d26, d27, d28]); b20 = block20([d22, d23, d24, d25, d26, d27, d28]);
@@ -307,19 +299,15 @@ exports[`misc other complex template 1`] = `
} }
ctx = ctx.__proto__; ctx = ctx.__proto__;
let b18 = list(c_block18); let b18 = list(c_block18);
const v9 = ctx['triggerAll']; let d29 = [ctx['triggerAll'], ctx];
let d29 = [v9, ctx]; let d30 = [ctx['triggerNone'], ctx];
const v10 = ctx['triggerNone']; let d31 = [ctx['triggerDefault'], ctx];
let d30 = [v10, ctx]; let d32 = [ctx['toggleSettingsMenu'], ctx];
const v11 = ctx['triggerDefault'];
let d31 = [v11, ctx];
const v12 = ctx['toggleSettingsMenu'];
let d32 = [v12, ctx];
let b21 = block21([d29, d30, d31, d32]); let b21 = block21([d29, d30, d31, d32]);
b17 = multi([b18, b21]); b17 = multi([b18, b21]);
} }
if (ctx['load_infos']) { if (ctx['load_infos']) {
b22 = callTemplate_14.call(this, ctx, node, key + \`__13\`); b22 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
} }
if (ctx['message']) { if (ctx['message']) {
let d33 = ctx['message']; let d33 = ctx['message'];
@@ -328,8 +316,8 @@ exports[`misc other complex template 1`] = `
if (!ctx['project']) { if (!ctx['project']) {
b24 = block24(); b24 = block24();
} else { } else {
let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky,category_custom_views: ctx['category_custom_views'],search: ctx['search']}, key + \`__15\`, node, ctx); let b26 = component(\`BundlesList\`, {bundles: ctx['bundles'].sticky,category_custom_views: ctx['category_custom_views'],search: ctx['search']}, key + \`__3\`, node, ctx);
let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev,search: ctx['search']}, key + \`__16\`, node, ctx); let b27 = component(\`BundlesList\`, {bundles: ctx['bundles'].dev,search: ctx['search']}, key + \`__4\`, node, ctx);
b25 = block25([], [b26, b27]); b25 = block25([], [b26, b27]);
} }
return block1([d1, d2, d11, d12, d16, d17, d18, d19, d20, d21], [b2, b4, b14, b17, b22, b23, b24, b25]); return block1([d1, d2, d11, d12, d16, d17, d18, d19, d20, d21], [b2, b4, b14, b17, b22, b23, b24, b25]);
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`properly support svg add proper namespace to g tags 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<g block-ns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </g>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg add proper namespace to svg 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\" width=\\"100px\\" height=\\"90px\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg namespace to g tags not added if already in svg namespace 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><g/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`properly support svg namespace to svg tags added even if already in svg namespace 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><svg block-ns=\\"http://www.w3.org/2000/svg\\"/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
+6 -6
View File
@@ -21,12 +21,12 @@ describe("error handling", () => {
expect(() => context.addTemplate("test", "<div/>")).toThrow("already defined"); expect(() => context.addTemplate("test", "<div/>")).toThrow("already defined");
}); });
// test("addTemplates throw if parser error", () => { test("addTemplates throw if parser error", () => {
// const context = new TestContext(); const context = new TestContext();
// expect(() => { expect(() => {
// context.addTemplates("<templates><abc>></templates>"); context.addTemplates("<templates><abc>></templates>");
// }).toThrow("Invalid XML in template"); }).toThrow("Invalid XML in template");
// }); });
test("nice error when t-on is evaluated with a missing event", () => { test("nice error when t-on is evaluated with a missing event", () => {
expect(() => renderToString(`<div t-on="somemethod"></div>`)).toThrow( expect(() => renderToString(`<div t-on="somemethod"></div>`)).toThrow(
+201 -14
View File
@@ -46,6 +46,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
}); });
}); });
@@ -73,6 +74,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
model: null, model: null,
content: [], content: [],
ns: null,
}); });
}); });
@@ -86,6 +88,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
model: null, model: null,
content: [{ type: ASTType.Text, value: "some text" }], content: [{ type: ASTType.Text, value: "some text" }],
ns: null,
}); });
}); });
@@ -98,6 +101,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [ content: [
{ type: ASTType.Text, value: "some text" }, { type: ASTType.Text, value: "some text" },
{ {
@@ -108,6 +112,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "inside" }], content: [{ type: ASTType.Text, value: "inside" }],
}, },
], ],
@@ -126,6 +131,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [], content: [],
}, },
{ {
@@ -136,6 +142,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [], content: [],
}, },
], ],
@@ -155,6 +162,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [], content: [],
}, },
], ],
@@ -170,10 +178,83 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "foo" }], content: [{ type: ASTType.Text, value: "foo" }],
}); });
}); });
test("svg dom node", async () => {
expect(
parse(
`<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></svg>`
)
).toEqual({
attrs: {
height: "90px",
width: "100px",
},
content: [
{
attrs: {
cx: "50",
cy: "50",
fill: "yellow",
r: "4",
stroke: "green",
"stroke-width": "1",
},
content: [],
dynamicTag: null,
model: null,
ns: null,
on: {},
ref: null,
tag: "circle",
type: 2,
},
],
dynamicTag: null,
model: null,
ns: "http://www.w3.org/2000/svg",
on: {},
ref: null,
tag: "svg",
type: 2,
});
expect(
parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`)
).toEqual({
attrs: {},
content: [
{
attrs: {
cx: "50",
cy: "50",
fill: "yellow",
r: "4",
stroke: "green",
"stroke-width": "1",
},
content: [],
dynamicTag: null,
model: null,
ns: null,
on: {},
ref: null,
tag: "circle",
type: 2,
},
],
dynamicTag: null,
model: null,
ns: "http://www.w3.org/2000/svg",
on: {},
ref: null,
tag: "g",
type: 2,
});
});
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// t-esc // t-esc
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -200,6 +281,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "" }], content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "" }],
}); });
}); });
@@ -221,6 +303,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "hey" }], content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "hey" }],
}); });
}); });
@@ -262,6 +345,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.TOut, expr: "text", body: null }], content: [{ type: ASTType.TOut, expr: "text", body: null }],
}); });
}); });
@@ -275,6 +359,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [ content: [
{ type: ASTType.TOut, expr: "text", body: [{ type: ASTType.Text, value: "body" }] }, { type: ASTType.TOut, expr: "text", body: [{ type: ASTType.Text, value: "body" }] },
], ],
@@ -294,6 +379,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [ content: [
{ {
type: ASTType.TIf, type: ASTType.TIf,
@@ -321,6 +407,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "hey" }], content: [{ type: ASTType.Text, value: "hey" }],
}, },
tElif: null, tElif: null,
@@ -397,6 +484,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [ content: [
{ {
type: ASTType.Text, type: ASTType.Text,
@@ -415,6 +503,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "elif" }], content: [{ type: ASTType.Text, value: "elif" }],
}, },
}, },
@@ -427,6 +516,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [ content: [
{ {
type: ASTType.Text, type: ASTType.Text,
@@ -474,6 +564,7 @@ describe("qweb parser", () => {
dynamicTag: null, dynamicTag: null,
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "ok" }], content: [{ type: ASTType.Text, value: "ok" }],
}, },
], ],
@@ -493,6 +584,7 @@ describe("qweb parser", () => {
dynamicTag: null, dynamicTag: null,
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "ok" }], content: [{ type: ASTType.Text, value: "ok" }],
}, },
{ type: ASTType.Text, value: "abc" }, { type: ASTType.Text, value: "abc" },
@@ -527,6 +619,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [ content: [
{ {
type: ASTType.TIf, type: ASTType.TIf,
@@ -601,6 +694,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }], content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
}, },
memo: "", memo: "",
@@ -646,6 +740,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }], content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
}, },
memo: "", memo: "",
@@ -681,6 +776,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }], content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
}, },
}, },
@@ -715,6 +811,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "category.name", defaultValue: "" }], content: [{ type: ASTType.TEsc, expr: "category.name", defaultValue: "" }],
}, },
memo: "", memo: "",
@@ -736,6 +833,7 @@ describe("qweb parser", () => {
model: null, model: null,
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
ns: null,
content: [ content: [
{ {
type: ASTType.TForEach, type: ASTType.TForEach,
@@ -781,6 +879,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
model: null, model: null,
attrs: {}, attrs: {},
ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }], content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
}, },
memo: "", memo: "",
@@ -888,6 +987,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [ content: [
{ {
type: ASTType.TCall, type: ASTType.TCall,
@@ -925,6 +1025,7 @@ describe("qweb parser", () => {
on: { click: "add" }, on: { click: "add" },
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "Click" }], content: [{ type: ASTType.Text, value: "Click" }],
}); });
}); });
@@ -968,7 +1069,31 @@ describe("qweb parser", () => {
test("component with event handler", async () => { test("component with event handler", async () => {
expect(() => parse(`<MyComponent t-on-click="someMethod"/>`)).toThrow( expect(() => parse(`<MyComponent t-on-click="someMethod"/>`)).toThrow(
"t-on is no longer supported on Component node. Consider passing a callback in props." "t-on is no longer supported on components. Consider passing a callback in props."
);
});
test("component with t-ref", async () => {
expect(() => parse(`<MyComponent t-ref="something"/>`)).toThrow(
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop."
);
});
test("component with t-att", async () => {
expect(() => parse(`<MyComponent t-att="something"/>`)).toThrow(
"t-att makes no sense on component: props are already treated as expressions"
);
});
test("component with t-attf", async () => {
expect(() => parse(`<MyComponent t-attf="something"/>`)).toThrow(
"t-attf is not supported on components: use template strings for string interpolation in props"
);
});
test("component with other unsupported directive", async () => {
expect(() => parse(`<MyComponent t-something="5"/>`)).toThrow(
"unsupported directive on Component: t-something"
); );
}); });
@@ -979,7 +1104,22 @@ describe("qweb parser", () => {
dynamicProps: null, dynamicProps: null,
props: {}, props: {},
isDynamic: false, isDynamic: false,
slots: { default: { type: ASTType.Text, value: "foo" } }, slots: { default: { content: { type: ASTType.Text, value: "foo" } } },
});
});
test("a component with a default slot with attributes", async () => {
expect(
parse(`<MyComponent><t t-set-slot="default" param="param">foo</t></MyComponent>`)
).toEqual({
type: ASTType.TComponent,
name: "MyComponent",
dynamicProps: null,
props: {},
isDynamic: false,
slots: {
default: { content: { type: ASTType.Text, value: "foo" }, attrs: { param: "param" } },
},
}); });
}); });
@@ -992,6 +1132,7 @@ describe("qweb parser", () => {
props: {}, props: {},
slots: { slots: {
default: { default: {
content: {
type: ASTType.Multi, type: ASTType.Multi,
content: [ content: [
{ {
@@ -1003,6 +1144,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
model: null, model: null,
on: {}, on: {},
ns: null,
}, },
{ {
type: ASTType.DomNode, type: ASTType.DomNode,
@@ -1013,10 +1155,12 @@ describe("qweb parser", () => {
ref: null, ref: null,
model: null, model: null,
on: {}, on: {},
ns: null,
}, },
], ],
}, },
}, },
},
}); });
}); });
@@ -1027,10 +1171,27 @@ describe("qweb parser", () => {
isDynamic: false, isDynamic: false,
dynamicProps: null, dynamicProps: null,
props: {}, props: {},
slots: { name: { type: ASTType.Text, value: "foo" } }, slots: { name: { content: { type: ASTType.Text, value: "foo" } } },
}); });
}); });
test("a component with a named slot with attributes", async () => {
expect(parse(`<MyComponent><t t-set-slot="name" param="param">foo</t></MyComponent>`)).toEqual({
type: ASTType.TComponent,
name: "MyComponent",
isDynamic: false,
dynamicProps: null,
props: {},
slots: { name: { content: { type: ASTType.Text, value: "foo" }, attrs: { param: "param" } } },
});
});
test("a component with a named slot with div tag", async () => {
expect(() =>
parse(`<MyComponent><div t-set-slot="name">foo</div></MyComponent>`)
).toThrowError();
});
test("a component with a named slot and some white space", async () => { test("a component with a named slot and some white space", async () => {
expect(parse(`<MyComponent><t t-set-slot="name">foo</t> </MyComponent>`)).toEqual({ expect(parse(`<MyComponent><t t-set-slot="name">foo</t> </MyComponent>`)).toEqual({
type: ASTType.TComponent, type: ASTType.TComponent,
@@ -1039,8 +1200,8 @@ describe("qweb parser", () => {
props: {}, props: {},
isDynamic: false, isDynamic: false,
slots: { slots: {
default: { type: ASTType.Text, value: " " }, default: { content: { type: ASTType.Text, value: " " } },
name: { type: ASTType.Text, value: "foo" }, name: { content: { type: ASTType.Text, value: "foo" } },
}, },
}); });
}); });
@@ -1058,8 +1219,8 @@ describe("qweb parser", () => {
props: {}, props: {},
isDynamic: false, isDynamic: false,
slots: { slots: {
a: { type: ASTType.Text, value: "foo" }, a: { content: { type: ASTType.Text, value: "foo" } },
b: { type: ASTType.Text, value: "bar" }, b: { content: { type: ASTType.Text, value: "bar" } },
}, },
}); });
}); });
@@ -1098,8 +1259,14 @@ describe("qweb parser", () => {
}); });
test("component with t-esc", async () => { test("component with t-esc", async () => {
expect(() => parse(`<MyComponent t-esc="someValue"/>`)).toThrow( expect(parse(`<MyComponent t-esc="someValue"/>`)).toEqual(
"t-esc is not supported on Component nodes" parse(`<MyComponent><t t-esc="someValue"/></MyComponent>`)
);
});
test("component with t-esc and content", async () => {
expect(() => parse(`<MyComponent t-esc="someValue">Some content</MyComponent>`)).toThrow(
"Cannot have t-esc on a component that already has content"
); );
}); });
@@ -1110,7 +1277,7 @@ describe("qweb parser", () => {
dynamicProps: null, dynamicProps: null,
props: {}, props: {},
isDynamic: false, isDynamic: false,
slots: { default: { body: null, name: "subTemplate", type: ASTType.TCall } }, slots: { default: { content: { body: null, name: "subTemplate", type: ASTType.TCall } } },
}); });
}); });
@@ -1130,12 +1297,14 @@ describe("qweb parser", () => {
isDynamic: false, isDynamic: false,
slots: { slots: {
default: { default: {
content: {
type: ASTType.TComponent, type: ASTType.TComponent,
isDynamic: false, isDynamic: false,
name: "Child", name: "Child",
dynamicProps: null, dynamicProps: null,
props: {}, props: {},
slots: { brol: { type: ASTType.Text, value: "coucou" } }, slots: { brol: { content: { type: ASTType.Text, value: "coucou" } } },
},
}, },
}, },
}); });
@@ -1145,7 +1314,7 @@ describe("qweb parser", () => {
const template = ` const template = `
<MyComponent> <MyComponent>
<Child> <Child>
<t><t t-set-slot="brol">coucou</t></t> <t t-set-slot="brol">coucou</t>
</Child> </Child>
</MyComponent> </MyComponent>
`; `;
@@ -1157,12 +1326,14 @@ describe("qweb parser", () => {
isDynamic: false, isDynamic: false,
slots: { slots: {
default: { default: {
content: {
type: ASTType.TComponent, type: ASTType.TComponent,
isDynamic: false, isDynamic: false,
name: "Child", name: "Child",
dynamicProps: null, dynamicProps: null,
props: {}, props: {},
slots: { brol: { type: ASTType.Text, value: "coucou" } }, slots: { brol: { content: { type: ASTType.Text, value: "coucou" } } },
},
}, },
}, },
}); });
@@ -1176,6 +1347,7 @@ describe("qweb parser", () => {
expect(parse(`<t t-slot="default"/>`)).toEqual({ expect(parse(`<t t-slot="default"/>`)).toEqual({
type: ASTType.TSlot, type: ASTType.TSlot,
name: "default", name: "default",
attrs: {},
defaultContent: null, defaultContent: null,
}); });
}); });
@@ -1184,6 +1356,7 @@ describe("qweb parser", () => {
expect(parse(`<t t-slot="header">default content</t>`)).toEqual({ expect(parse(`<t t-slot="header">default content</t>`)).toEqual({
type: ASTType.TSlot, type: ASTType.TSlot,
name: "header", name: "header",
attrs: {},
defaultContent: { type: ASTType.Text, value: "default content" }, defaultContent: { type: ASTType.Text, value: "default content" },
}); });
}); });
@@ -1203,6 +1376,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "hey" }], content: [{ type: ASTType.Text, value: "hey" }],
}, },
}); });
@@ -1220,6 +1394,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: null, ref: null,
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "hey" }], content: [{ type: ASTType.Text, value: "hey" }],
}, },
}); });
@@ -1238,6 +1413,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: "name", ref: "name",
model: null, model: null,
ns: null,
content: [{ type: ASTType.Text, value: "hey" }], content: [{ type: ASTType.Text, value: "hey" }],
}); });
}); });
@@ -1251,6 +1427,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: "name", ref: "name",
model: null, model: null,
ns: null,
content: [ content: [
{ type: ASTType.TOut, expr: "text", body: [{ type: ASTType.Text, value: "body" }] }, { type: ASTType.TOut, expr: "text", body: [{ type: ASTType.Text, value: "body" }] },
], ],
@@ -1266,6 +1443,7 @@ describe("qweb parser", () => {
on: {}, on: {},
ref: "name", ref: "name",
model: null, model: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "body" }], content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "body" }],
}); });
}); });
@@ -1309,7 +1487,8 @@ describe("qweb parser", () => {
model: null, model: null,
tag: "div", tag: "div",
dynamicTag: null, dynamicTag: null,
type: 2, type: ASTType.DomNode,
ns: null,
}, },
type: 16, type: 16,
}, },
@@ -1339,6 +1518,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
tag: "input", tag: "input",
dynamicTag: null, dynamicTag: null,
ns: null,
model: { model: {
baseExpr: "state", baseExpr: "state",
expr: "'stuff'", expr: "'stuff'",
@@ -1357,6 +1537,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
tag: "input", tag: "input",
dynamicTag: null, dynamicTag: null,
ns: null,
model: { model: {
baseExpr: "state", baseExpr: "state",
expr: "'stuff'", expr: "'stuff'",
@@ -1375,6 +1556,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
tag: "input", tag: "input",
dynamicTag: null, dynamicTag: null,
ns: null,
model: { model: {
baseExpr: "state", baseExpr: "state",
expr: "'stuff'", expr: "'stuff'",
@@ -1394,6 +1576,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
tag: "textarea", tag: "textarea",
dynamicTag: null, dynamicTag: null,
ns: null,
model: { model: {
baseExpr: "state", baseExpr: "state",
expr: "'stuff'", expr: "'stuff'",
@@ -1412,6 +1595,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
tag: "input", tag: "input",
dynamicTag: null, dynamicTag: null,
ns: null,
model: { model: {
baseExpr: "state", baseExpr: "state",
expr: "'stuff'", expr: "'stuff'",
@@ -1430,6 +1614,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
tag: "input", tag: "input",
dynamicTag: null, dynamicTag: null,
ns: null,
model: { model: {
baseExpr: "state", baseExpr: "state",
expr: "'stuff'", expr: "'stuff'",
@@ -1448,6 +1633,7 @@ describe("qweb parser", () => {
ref: null, ref: null,
tag: "input", tag: "input",
dynamicTag: null, dynamicTag: null,
ns: null,
model: { model: {
baseExpr: "state", baseExpr: "state",
expr: "'stuff'", expr: "'stuff'",
@@ -1472,6 +1658,7 @@ describe("qweb parser", () => {
tag: "div", tag: "div",
dynamicTag: "theTag", dynamicTag: "theTag",
model: null, model: null,
ns: null,
}); });
}); });
}); });
+25 -17
View File
@@ -1,22 +1,30 @@
import { renderToString, snapshotEverything } from "../helpers";
// NB: check the snapshots to see where the SVG namespaces are added
snapshotEverything();
describe("properly support svg", () => { describe("properly support svg", () => {
test.skip("add proper namespace to svg", () => { test("add proper namespace to svg", () => {
// qweb.addTemplate( const template = `<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`;
// "test", expect(renderToString(template)).toBe(
// `<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>` `<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
// ); );
// expect(renderToString(qweb, "test")).toBe(
// `<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
// );
}); });
test.skip("add proper namespace to g tags", () => { test("add proper namespace to g tags", () => {
// this is necessary if one wants to use components in a svg const template = `<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`;
// qweb.addTemplate( expect(renderToString(template)).toBe(
// "test", `<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
// `<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>` );
// ); });
// expect(renderToString(qweb, "test")).toBe(
// `<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>` test("namespace to g tags not added if already in svg namespace", () => {
// ); const template = `<svg><g/></svg>`;
expect(renderToString(template)).toBe(`<svg><g></g></svg>`);
});
test("namespace to svg tags added even if already in svg namespace", () => {
const template = `<svg><svg/></svg>`;
expect(renderToString(template)).toBe(`<svg><svg></svg></svg>`);
}); });
}); });
@@ -1192,7 +1192,7 @@ exports[`support svg components add proper namespace to svg 1`] = `
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<g><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/></g>\`); let block1 = createBlock(\`<g block-ns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/></g>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
return block1(); return block1();
@@ -1206,7 +1206,7 @@ exports[`support svg components add proper namespace to svg 2`] = `
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<svg><block-child-0/></svg>\`); let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><block-child-0/></svg>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2 = component(\`GComp\`, {}, key + \`__1\`, node, ctx); let b2 = component(\`GComp\`, {}, key + \`__1\`, node, ctx);
@@ -1044,6 +1044,30 @@ exports[`destroying/recreating a subwidget with different props (if start is not
}" }"
`; `;
exports[`parent and child rendered at exact same time 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].value);
}
}"
`;
exports[`parent and child rendered at exact same time 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`properly behave when destroyed/unmounted while rendering 1`] = ` exports[`properly behave when destroyed/unmounted while rendering 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -1130,10 +1154,9 @@ exports[`rendering component again in next microtick 2`] = `
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2; let b2;
const v1 = ctx['onClick']; let d1 = [ctx['onClick'], ctx];
let d1 = [v1, ctx];
if (ctx['env'].config.flag) { if (ctx['env'].config.flag) {
b2 = component(\`Child\`, {}, key + \`__2\`, node, ctx); b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
} }
return block1([d1], [b2]); return block1([d1], [b2]);
} }
@@ -105,7 +105,7 @@ exports[`can catch errors can catch an error in a component render function 2`]
if (ctx['state'].error) { if (ctx['state'].error) {
b2 = text(\`Error handled\`); b2 = text(\`Error handled\`);
} else { } else {
b3 = callSlot(ctx, node, key, 'default'); b3 = callSlot(ctx, node, key, 'default', false, {});
} }
return block1([], [b2, b3]); return block1([], [b2, b3]);
} }
@@ -117,16 +117,15 @@ exports[`can catch errors can catch an error in a component render function 3`]
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`ErrorComponent\`, {flag: ctx['state'].flag}, key + \`__3\`, node, ctx); return component(\`ErrorComponent\`, {flag: ctx['state'].flag}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -159,7 +158,7 @@ exports[`can catch errors can catch an error in the constructor call of a compon
if (ctx['state'].error) { if (ctx['state'].error) {
b2 = text(\`Error handled\`); b2 = text(\`Error handled\`);
} else { } else {
b3 = callSlot(ctx, node, key, 'default'); b3 = callSlot(ctx, node, key, 'default', false, {});
} }
return block1([], [b2, b3]); return block1([], [b2, b3]);
} }
@@ -207,7 +206,7 @@ exports[`can catch errors can catch an error in the constructor call of a compon
if (ctx['state'].error) { if (ctx['state'].error) {
b2 = text(\`Error handled\`); b2 = text(\`Error handled\`);
} else { } else {
b3 = callSlot(ctx, node, key, 'default'); b3 = callSlot(ctx, node, key, 'default', false, {});
} }
return block1([], [b2, b3]); return block1([], [b2, b3]);
} }
@@ -219,18 +218,17 @@ exports[`can catch errors can catch an error in the constructor call of a compon
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
let b3 = component(\`ClassicCompoent\`, {}, key + \`__3\`, node, ctx); let b3 = component(\`ClassicCompoent\`, {}, key + \`__2\`, node, ctx);
let b4 = component(\`ErrorComponent\`, {}, key + \`__4\`, node, ctx); let b4 = component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx);
return multi([b3, b4]); return multi([b3, b4]);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b5 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b5 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__4\`, node, ctx);
return block1([], [b5]); return block1([], [b5]);
} }
}" }"
@@ -241,16 +239,15 @@ exports[`can catch errors can catch an error in the constructor call of a compon
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx); return component(\`ErrorComponent\`, {}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -284,7 +281,7 @@ exports[`can catch errors can catch an error in the initial call of a component
if (ctx['state'].error) { if (ctx['state'].error) {
b2 = text(\`Error handled\`); b2 = text(\`Error handled\`);
} else { } else {
b3 = callSlot(ctx, node, key, 'default'); b3 = callSlot(ctx, node, key, 'default', false, {});
} }
return block1([], [b2, b3]); return block1([], [b2, b3]);
} }
@@ -296,16 +293,15 @@ exports[`can catch errors can catch an error in the initial call of a component
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx); return component(\`ErrorComponent\`, {}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -339,7 +335,7 @@ exports[`can catch errors can catch an error in the initial call of a component
if (ctx['state'].error) { if (ctx['state'].error) {
b2 = text(\`Error handled\`); b2 = text(\`Error handled\`);
} else { } else {
b3 = callSlot(ctx, node, key, 'default'); b3 = callSlot(ctx, node, key, 'default', false, {});
} }
return block1([], [b2, b3]); return block1([], [b2, b3]);
} }
@@ -351,18 +347,17 @@ exports[`can catch errors can catch an error in the initial call of a component
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx); return component(\`ErrorComponent\`, {}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3; let b3;
if (ctx['state'].flag) { if (ctx['state'].flag) {
b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
} }
return block1([], [b3]); return block1([], [b3]);
} }
@@ -396,7 +391,7 @@ exports[`can catch errors can catch an error in the mounted call 2`] = `
if (ctx['state'].error) { if (ctx['state'].error) {
b2 = text(\`Error handled\`); b2 = text(\`Error handled\`);
} else { } else {
b3 = callSlot(ctx, node, key, 'default'); b3 = callSlot(ctx, node, key, 'default', false, {});
} }
return block1([], [b2, b3]); return block1([], [b2, b3]);
} }
@@ -408,16 +403,15 @@ exports[`can catch errors can catch an error in the mounted call 3`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx); return component(\`ErrorComponent\`, {}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -451,7 +445,7 @@ exports[`can catch errors can catch an error in the willPatch call 2`] = `
if (ctx['state'].error) { if (ctx['state'].error) {
b2 = text(\`Error handled\`); b2 = text(\`Error handled\`);
} else { } else {
b3 = callSlot(ctx, node, key, 'default'); b3 = callSlot(ctx, node, key, 'default', false, {});
} }
return block1([], [b2, b3]); return block1([], [b2, b3]);
} }
@@ -463,17 +457,16 @@ exports[`can catch errors can catch an error in the willPatch call 3`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><span><block-text-0/></span><block-child-0/></div>\`); let block1 = createBlock(\`<div><span><block-text-0/></span><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`ErrorComponent\`, {message: ctx['state'].message}, key + \`__3\`, node, ctx); return component(\`ErrorComponent\`, {message: ctx['state'].message}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].message; let d1 = ctx['state'].message;
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([d1], [b3]); return block1([d1], [b3]);
} }
}" }"
@@ -506,7 +499,7 @@ exports[`can catch errors can catch an error in the willStart call 2`] = `
if (ctx['state'].error) { if (ctx['state'].error) {
b2 = text(\`Error handled\`); b2 = text(\`Error handled\`);
} else { } else {
b3 = callSlot(ctx, node, key, 'default'); b3 = callSlot(ctx, node, key, 'default', false, {});
} }
return block1([], [b2, b3]); return block1([], [b2, b3]);
} }
@@ -518,16 +511,15 @@ exports[`can catch errors can catch an error in the willStart call 3`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx); return component(\`ErrorComponent\`, {}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -574,7 +566,7 @@ exports[`can catch errors can catch an error origination from a child's willStar
if (ctx['state'].error) { if (ctx['state'].error) {
b2 = text(\`Error handled\`); b2 = text(\`Error handled\`);
} else { } else {
b3 = callSlot(ctx, node, key, 'default'); b3 = callSlot(ctx, node, key, 'default', false, {});
} }
return block1([], [b2, b3]); return block1([], [b2, b3]);
} }
@@ -586,18 +578,17 @@ exports[`can catch errors can catch an error origination from a child's willStar
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
let b3 = component(\`ClassicCompoent\`, {}, key + \`__3\`, node, ctx); let b3 = component(\`ClassicCompoent\`, {}, key + \`__2\`, node, ctx);
let b4 = component(\`ErrorComponent\`, {}, key + \`__4\`, node, ctx); let b4 = component(\`ErrorComponent\`, {}, key + \`__3\`, node, ctx);
return multi([b3, b4]); return multi([b3, b4]);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b5 = assign(component(\`ErrorBoundary\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b5 = component(\`ErrorBoundary\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__4\`, node, ctx);
return block1([], [b5]); return block1([], [b5]);
} }
}" }"
@@ -23,15 +23,40 @@ exports[`event handling handler receive the event as argument 2`] = `
let block1 = createBlock(\`<span block-handler-0=\\"click\\"><block-child-0/><block-text-1/></span>\`); let block1 = createBlock(\`<span block-handler-0=\\"click\\"><block-child-0/><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['inc']; let d1 = [ctx['inc'], ctx];
let d1 = [v1, ctx]; let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
let b2 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
let d2 = ctx['state'].value; let d2 = ctx['state'].value;
return block1([d1, d2], [b2]); return block1([d1, d2], [b2]);
} }
}" }"
`; `;
exports[`event handling objects from scope are properly captured by t-on 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<div class=\\"item\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['items']);
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`item\`] = v_block2[i1];
let key1 = ctx['item'];
const v1 = ctx['onClick'];
const v2 = ctx['item'];
let d1 = [ev=>v1(v2.val,ev), ctx];
c_block2[i1] = withKey(block3([d1]), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
}
}"
`;
exports[`event handling support for callable expression in event handler 1`] = ` exports[`event handling support for callable expression in event handler 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -42,8 +67,7 @@ exports[`event handling support for callable expression in event handler 1`] = `
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].value; let d1 = ctx['state'].value;
const v1 = ctx['obj']; let d2 = [ctx['obj'].onInput, ctx];
let d2 = [v1.onInput, ctx];
return block1([d1, d2]); return block1([d1, d2]);
} }
}" }"
@@ -130,8 +130,7 @@ exports[`basics sub widget is interactive 1`] = `
let block1 = createBlock(\`<span><button block-handler-0=\\"click\\">click</button>child<block-text-1/></span>\`); let block1 = createBlock(\`<span><button block-handler-0=\\"click\\">click</button>child<block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['inc']; let d1 = [ctx['inc'], ctx];
let d1 = [v1, ctx];
let d2 = ctx['state'].val; let d2 = ctx['state'].val;
return block1([d1, d2]); return block1([d1, d2]);
} }
@@ -531,7 +531,7 @@ exports[`lifecycle hooks mounted hook is called on subsubcomponents, in proper o
}" }"
`; `;
exports[`lifecycle hooks onRender 1`] = ` exports[`lifecycle hooks onWillRender 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
@@ -540,15 +540,14 @@ exports[`lifecycle hooks onRender 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`); let block1 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['increment']; let d1 = [ctx['increment'], ctx];
let d1 = [v1, ctx];
let d2 = ctx['state'].value; let d2 = ctx['state'].value;
return block1([d1, d2]); return block1([d1, d2]);
} }
}" }"
`; `;
exports[`lifecycle hooks onRender 2`] = ` exports[`lifecycle hooks onWillRender 2`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
@@ -30,6 +30,42 @@ exports[`basics accept ES6-like syntax for props (with getters) 2`] = `
}" }"
`; `;
exports[`basics arrow functions as prop correctly capture their scope 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<button block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx['props'].onClick, ctx];
return block1([d1]);
}
}"
`;
exports[`basics arrow functions as prop correctly capture their scope 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['items']);
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
let key1 = ctx['item'].val;
const v1 = ctx['onClick'];
const v2 = ctx['item'];
c_block1[i1] = withKey(component(\`Child\`, {onClick: ev=>v1(v2.val,ev)}, key + \`__3__\${key1}\`, node, ctx), key1);
}
return list(c_block1);
}
}"
`;
exports[`basics explicit object prop 1`] = ` exports[`basics explicit object prop 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -60,6 +96,60 @@ exports[`basics explicit object prop 2`] = `
}" }"
`; `;
exports[`basics prop names can contain - 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['props']['prop-name'];
return block1([d1]);
}
}"
`;
exports[`basics prop names can contain - 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {'prop-name': 7}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`basics support prop names that aren't valid bare object property names 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<button block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx['props'].onClick, ctx];
return block1([d1]);
}
}"
`;
exports[`basics support prop names that aren't valid bare object property names 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {'some-dashed-prop': 5,'a.b': 'keyword prop'}, key + \`__1\`, node, ctx);
}
}"
`;
exports[`basics t-set with a body expression can be passed in props, and then t-out 1`] = ` exports[`basics t-set with a body expression can be passed in props, and then t-out 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -161,3 +251,27 @@ exports[`basics t-set works 2`] = `
} }
}" }"
`; `;
exports[`basics template string in prop 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return text(\`\`);
}
}"
`;
exports[`basics template string in prop 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return component(\`Child\`, {propName: \`1\${ctx['someVal']}3\`}, key + \`__1\`, node, ctx);
}
}"
`;
@@ -9,9 +9,9 @@ exports[`default props can set default required boolean values 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {} const props1 = {}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -26,9 +26,9 @@ exports[`default props can set default values 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {} const props1 = {}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -43,9 +43,9 @@ exports[`default props default values are also set whenever component is updated
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['state'].p} const props1 = {p: ctx['state'].p}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -60,9 +60,9 @@ exports[`props validation can validate a prop with multiple types 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -77,9 +77,9 @@ exports[`props validation can validate a prop with multiple types 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -94,9 +94,9 @@ exports[`props validation can validate an array with given primitive type 1`] =
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -111,9 +111,9 @@ exports[`props validation can validate an array with given primitive type 2`] =
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -128,9 +128,9 @@ exports[`props validation can validate an array with multiple sub element types
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -145,9 +145,9 @@ exports[`props validation can validate an array with multiple sub element types
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -162,9 +162,9 @@ exports[`props validation can validate an array with multiple sub element types
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -179,9 +179,9 @@ exports[`props validation can validate an object with simple shape 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -196,9 +196,9 @@ exports[`props validation can validate an optional props 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -213,9 +213,9 @@ exports[`props validation can validate an optional props 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -230,9 +230,9 @@ exports[`props validation can validate recursively complicated prop def 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -247,9 +247,9 @@ exports[`props validation can validate recursively complicated prop def 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -264,9 +264,9 @@ exports[`props validation default values are applied before validating props at
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['state'].p} const props1 = {p: ctx['state'].p}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -281,9 +281,9 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] =
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {message: 1} const props1 = {message: 1}
helpers.validateProps(\`Child\`, props2, ctx) helpers.validateProps(\`Child\`, props1, ctx)
let b2 = component(\`Child\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`Child\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -298,9 +298,9 @@ exports[`props validation props are validated whenever component is updated 1`]
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['state'].p} const props1 = {p: ctx['state'].p}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -315,9 +315,9 @@ exports[`props validation validate simple types 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -332,9 +332,9 @@ exports[`props validation validate simple types 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -349,9 +349,9 @@ exports[`props validation validate simple types 3`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -366,9 +366,9 @@ exports[`props validation validate simple types 4`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -383,9 +383,9 @@ exports[`props validation validate simple types 5`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -400,9 +400,9 @@ exports[`props validation validate simple types 6`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -417,9 +417,9 @@ exports[`props validation validate simple types, alternate form 1`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -434,9 +434,9 @@ exports[`props validation validate simple types, alternate form 2`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -451,9 +451,9 @@ exports[`props validation validate simple types, alternate form 3`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -468,9 +468,9 @@ exports[`props validation validate simple types, alternate form 4`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -485,9 +485,9 @@ exports[`props validation validate simple types, alternate form 5`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -502,9 +502,9 @@ exports[`props validation validate simple types, alternate form 6`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['p']} const props1 = {p: ctx['p']}
helpers.validateProps(\`SubComp\`, props2, ctx) helpers.validateProps(\`SubComp\`, props1, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); let b2 = component(\`SubComp\`, props1, key + \`__2\`, node, ctx);
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -25,7 +25,7 @@ exports[`refs refs are properly bound in slots 1`] = `
let block1 = createBlock(\`<span><block-child-0/></span>\`); let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2 = callSlot(ctx, node, key, 'footer'); let b2 = callSlot(ctx, node, key, 'footer', false, {});
return block1([], [b2]); return block1([], [b2]);
} }
}" }"
@@ -36,15 +36,13 @@ exports[`refs refs are properly bound in slots 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><span class=\\"counter\\"><block-text-0/></span><block-child-0/></div>\`); let block1 = createBlock(\`<div><span class=\\"counter\\"><block-text-0/></span><block-child-0/></div>\`);
let block2 = createBlock(\`<button block-handler-0=\\"click\\" block-ref=\\"1\\">do something</button>\`); let block2 = createBlock(\`<button block-handler-0=\\"click\\" block-ref=\\"1\\">do something</button>\`);
const slot3 = ctx => (node, key) => { function slot2(ctx, node, key) {
const refs = ctx.__owl__.refs const refs = ctx.__owl__.refs
const v4 = ctx['doSomething']; let d2 = [ctx['doSomething'], ctx];
let d2 = [v4, ctx];
let d3 = (el) => refs[\`myButton\`] = el; let d3 = (el) => refs[\`myButton\`] = el;
return block2([d2, d3]); return block2([d2, d3]);
} }
@@ -52,8 +50,8 @@ exports[`refs refs are properly bound in slots 2`] = `
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const refs = ctx.__owl__.refs; const refs = ctx.__owl__.refs;
let d1 = ctx['state'].val; let d1 = ctx['state'].val;
const ctx2 = capture(ctx); const ctx1 = capture(ctx);
let b3 = assign(component(\`Dialog\`, {}, key + \`__1\`, node, ctx), {slots: {'footer': slot3(ctx2)}}); let b3 = component(\`Dialog\`, {slots: {'footer': {__render: slot2, __ctx: ctx1}}}, key + \`__3\`, node, ctx);
return block1([d1], [b3]); return block1([d1], [b3]);
} }
}" }"
File diff suppressed because it is too large Load Diff
@@ -52,8 +52,7 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`); let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['update']; let d1 = [ctx['update'], ctx];
let d1 = [v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -39,6 +39,61 @@ exports[`list of components components in a node in a t-foreach 2`] = `
}" }"
`; `;
exports[`list of components crash on duplicate key in dev mode 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList([1,2]);
const keys1 = new Set();
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
let key1 = 'child';
if (keys1.has(key1)) { throw new Error(\`Got duplicate key in t-foreach: \${key1}\`)}
keys1.add(key1);
const props1 = {}
helpers.validateProps(\`Child\`, props1, ctx)
c_block1[i1] = withKey(component(\`Child\`, props1, key + \`__2__\${key1}\`, node, ctx), key1);
}
return list(c_block1);
}
}"
`;
exports[`list of components crash on duplicate key in dev mode 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return text(\`\`);
}
}"
`;
exports[`list of components crash on duplicate key in dev mode 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList([1,2]);
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
let key1 = 'child';
c_block1[i1] = withKey(component(\`Child\`, {}, key + \`__1__\${key1}\`, node, ctx), key1);
}
return list(c_block1);
}
}"
`;
exports[`list of components list of sub components inside other nodes 1`] = ` exports[`list of components list of sub components inside other nodes 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -117,11 +117,10 @@ exports[`t-model directive can also define t-on directive on same event, part 1
let block1 = createBlock(\`<div><input block-handler-0=\\"input\\" block-attribute-1=\\"value\\" block-handler-2=\\"input\\"/></div>\`); let block1 = createBlock(\`<div><input block-handler-0=\\"input\\" block-attribute-1=\\"value\\" block-handler-2=\\"input\\"/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onInput']; let d1 = [ctx['onInput'], ctx];
let d1 = [v1, ctx]; const bExpr1 = ctx['state'];
const bExpr2 = ctx['state'];
let d2 = ctx['state']['text']; let d2 = ctx['state']['text'];
let d3 = [(ev) => { bExpr2['text'] = ev.target.value; }]; let d3 = [(ev) => { bExpr1['text'] = ev.target.value; }];
return block1([d1, d2, d3]); return block1([d1, d2, d3]);
} }
}" }"
@@ -136,21 +135,18 @@ exports[`t-model directive can also define t-on directive on same event, part 2
let block1 = createBlock(\`<div><input type=\\"radio\\" id=\\"one\\" value=\\"One\\" block-handler-0=\\"click\\" block-attribute-1=\\"checked\\" block-handler-2=\\"click\\"/><input type=\\"radio\\" id=\\"two\\" value=\\"Two\\" block-handler-3=\\"click\\" block-attribute-4=\\"checked\\" block-handler-5=\\"click\\"/><input type=\\"radio\\" id=\\"three\\" value=\\"Three\\" block-handler-6=\\"click\\" block-attribute-7=\\"checked\\" block-handler-8=\\"click\\"/></div>\`); let block1 = createBlock(\`<div><input type=\\"radio\\" id=\\"one\\" value=\\"One\\" block-handler-0=\\"click\\" block-attribute-1=\\"checked\\" block-handler-2=\\"click\\"/><input type=\\"radio\\" id=\\"two\\" value=\\"Two\\" block-handler-3=\\"click\\" block-attribute-4=\\"checked\\" block-handler-5=\\"click\\"/><input type=\\"radio\\" id=\\"three\\" value=\\"Three\\" block-handler-6=\\"click\\" block-attribute-7=\\"checked\\" block-handler-8=\\"click\\"/></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick']; let d1 = [ctx['onClick'], ctx];
let d1 = [v1, ctx]; const bExpr1 = ctx['state'];
const bExpr2 = ctx['state'];
let d2 = ctx['state']['choice'] === 'One'; let d2 = ctx['state']['choice'] === 'One';
let d3 = [(ev) => { bExpr2['choice'] = ev.target.value; }]; let d3 = [(ev) => { bExpr1['choice'] = ev.target.value; }];
const v3 = ctx['onClick']; let d4 = [ctx['onClick'], ctx];
let d4 = [v3, ctx]; const bExpr2 = ctx['state'];
const bExpr4 = ctx['state'];
let d5 = ctx['state']['choice'] === 'Two'; let d5 = ctx['state']['choice'] === 'Two';
let d6 = [(ev) => { bExpr4['choice'] = ev.target.value; }]; let d6 = [(ev) => { bExpr2['choice'] = ev.target.value; }];
const v5 = ctx['onClick']; let d7 = [ctx['onClick'], ctx];
let d7 = [v5, ctx]; const bExpr3 = ctx['state'];
const bExpr6 = ctx['state'];
let d8 = ctx['state']['choice'] === 'Three'; let d8 = ctx['state']['choice'] === 'Three';
let d9 = [(ev) => { bExpr6['choice'] = ev.target.value; }]; let d9 = [(ev) => { bExpr3['choice'] = ev.target.value; }];
return block1([d1, d2, d3, d4, d5, d6, d7, d8, d9]); return block1([d1, d2, d3, d4, d5, d6, d7, d8, d9]);
} }
}" }"
@@ -130,8 +130,7 @@ exports[`t-on t-on on destroyed components 1`] = `
let block1 = createBlock(\`<div block-handler-0=\\"click\\"/>\`); let block1 = createBlock(\`<div block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onClick']; let d1 = [ctx['onClick'], ctx];
let d1 = [v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -24,11 +24,10 @@ exports[`t-set slot setted value (with t-set) not accessible with t-esc 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><p><block-text-0/></p><block-child-0/><p><block-text-1/></p></div>\`); let block1 = createBlock(\`<div><p><block-text-0/></p><block-child-0/><p><block-text-1/></p></div>\`);
const slot3 = ctx => (node, key) => { function slot2(ctx, node, key) {
setContextValue(ctx, \\"iter\\", 'inCall'); setContextValue(ctx, \\"iter\\", 'inCall');
} }
@@ -37,8 +36,8 @@ exports[`t-set slot setted value (with t-set) not accessible with t-esc 2`] = `
ctx[isBoundary] = 1 ctx[isBoundary] = 1
setContextValue(ctx, \\"iter\\", 'source'); setContextValue(ctx, \\"iter\\", 'source');
let d1 = ctx['iter']; let d1 = ctx['iter'];
const ctx2 = capture(ctx); const ctx1 = capture(ctx);
let b2 = assign(component(\`Childcomp\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot3(ctx2)}}); let b2 = component(\`Childcomp\`, {slots: {'default': {__render: slot2, __ctx: ctx1}}}, key + \`__3\`, node, ctx);
let d2 = ctx['iter']; let d2 = ctx['iter'];
return block1([d1, d2], [b2]); return block1([d1, d2], [b2]);
} }
+16 -16
View File
@@ -40,14 +40,14 @@ describe("basics", () => {
class SomeWidget extends Component { class SomeWidget extends Component {
static template = xml`<div>content</div>`; static template = xml`<div>content</div>`;
} }
let error; let error: Error;
try { try {
await mount(SomeWidget, document.createDocumentFragment() as any); await mount(SomeWidget, document.createDocumentFragment() as any);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Cannot mount component: the target is not a valid DOM element"); expect(error!.message).toBe("Cannot mount component: the target is not a valid DOM element");
}); });
test("can mount a simple component with props", async () => { test("can mount a simple component with props", async () => {
@@ -160,28 +160,28 @@ describe("basics", () => {
static template = xml`<span>simple vnode</span>`; static template = xml`<span>simple vnode</span>`;
} }
let error; let error: Error;
try { try {
await mount(Test, null as any); await mount(Test, null as any);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Cannot mount component: the target is not a valid DOM element"); expect(error!.message).toBe("Cannot mount component: the target is not a valid DOM element");
}); });
test("a component cannot be mounted in a detached node", async () => { test("a component cannot be mounted in a detached node", async () => {
class Test extends Component { class Test extends Component {
static template = xml`<div/>`; static template = xml`<div/>`;
} }
let error; let error: Error;
try { try {
await mount(Test, document.createElement("div")); await mount(Test, document.createElement("div"));
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Cannot mount a component on a detached dom node"); expect(error!.message).toBe("Cannot mount a component on a detached dom node");
}); });
test("crashes if it cannot find a template", async () => { test("crashes if it cannot find a template", async () => {
@@ -189,14 +189,14 @@ describe("basics", () => {
static template = "wrongtemplate"; static template = "wrongtemplate";
} }
let error; let error: Error;
try { try {
await mount(Test, fixture); await mount(Test, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe('Missing template: "wrongtemplate"'); expect(error!.message).toBe('Missing template: "wrongtemplate"');
}); });
test("class component with dynamic text", async () => { test("class component with dynamic text", async () => {
File diff suppressed because it is too large Load Diff
+38 -38
View File
@@ -35,18 +35,18 @@ describe("basics", () => {
expect(fixture.innerHTML).toBe("<div><div>heyfalse</div></div>"); expect(fixture.innerHTML).toBe("<div><div>heyfalse</div></div>");
parent.state.flag = true; parent.state.flag = true;
let error; let error: Error;
try { try {
await parent.render(); await parent.render();
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(status(parent)).toBe("destroyed"); expect(status(parent)).toBe("destroyed");
expect(error).toBeDefined(); expect(error!).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/g; /Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/g;
expect(error.message).toMatch(regexp); expect(error!.message).toMatch(regexp);
}); });
test("display a nice error if it cannot find component", async () => { test("display a nice error if it cannot find component", async () => {
@@ -58,14 +58,14 @@ describe("basics", () => {
static template = xml`<SomeMispelledComponent />`; static template = xml`<SomeMispelledComponent />`;
static components = { SomeComponent }; static components = { SomeComponent };
} }
let error; let error: Error;
try { try {
await mount(Parent, fixture); await mount(Parent, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe('Cannot find the definition of component "SomeMispelledComponent"'); expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"');
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
console.error = consoleError; console.error = consoleError;
}); });
@@ -108,16 +108,16 @@ describe("errors and promises", () => {
static template = xml`<div><t t-esc="this.will.crash"/></div>`; static template = xml`<div><t t-esc="this.will.crash"/></div>`;
} }
let error; let error: Error;
try { try {
await mount(App, fixture); await mount(App, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp); expect(error!.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
console.error = consoleError; console.error = consoleError;
@@ -136,14 +136,14 @@ describe("errors and promises", () => {
} }
} }
let error; let error: Error;
try { try {
await mount(App, fixture); await mount(App, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("boom"); expect(error!.message).toBe("boom");
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
@@ -166,14 +166,14 @@ describe("errors and promises", () => {
const app = await mount(App, fixture); const app = await mount(App, fixture);
app.val = 4; app.val = 4;
let error; let error: Error;
try { try {
await app.render(); await app.render();
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("boom"); expect(error!.message).toBe("boom");
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
@@ -196,14 +196,14 @@ describe("errors and promises", () => {
const app = await mount(App, fixture); const app = await mount(App, fixture);
app.val = 4; app.val = 4;
let error; let error: Error;
try { try {
await app.render(); await app.render();
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("boom"); expect(error!.message).toBe("boom");
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
@@ -222,16 +222,16 @@ describe("errors and promises", () => {
static components = { Child }; static components = { Child };
} }
let error; let error: Error;
try { try {
await mount(App, fixture); await mount(App, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp); expect(error!.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
console.error = consoleError; console.error = consoleError;
@@ -249,16 +249,16 @@ describe("errors and promises", () => {
const app = await mount(App, fixture); const app = await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div></div>"); expect(fixture.innerHTML).toBe("<div></div>");
app.flag = true; app.flag = true;
let error; let error: Error;
try { try {
await app.render(); await app.render();
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp); expect(error!.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0); expect(console.error).toBeCalledTimes(0);
console.error = consoleError; console.error = consoleError;
@@ -273,20 +273,20 @@ describe("errors and promises", () => {
static components = { Child }; static components = { Child };
} }
let error; let error: Error;
try { try {
await mount(Parent, fixture); await mount(Parent, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g; /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
expect(error.message).toMatch(regexp); expect(error!.message).toMatch(regexp);
}); });
test("errors in mounted and in willUnmount", async () => { test("errors in mounted and in willUnmount", async () => {
expect.assertions(2); // apparently this expect count in assertions... expect.assertions(2);
class Example extends Component { class Example extends Component {
static template = xml`<div/>`; static template = xml`<div/>`;
val: any; val: any;
@@ -305,7 +305,7 @@ describe("errors and promises", () => {
try { try {
await mount(Example, fixture); await mount(Example, fixture);
} catch (e) { } catch (e) {
expect(e.message).toBe("Error in mounted"); expect((e as Error).message).toBe("Error in mounted");
} }
}); });
}); });
+21
View File
@@ -72,4 +72,25 @@ describe("event handling", () => {
expect(onClickArgs![0]).toBe(1); expect(onClickArgs![0]).toBe(1);
expect(onClickArgs![1]).toBeInstanceOf(MouseEvent); expect(onClickArgs![1]).toBeInstanceOf(MouseEvent);
}); });
test("objects from scope are properly captured by t-on", async () => {
let onClickArgs: [number, MouseEvent] | null = null;
class Parent extends Component {
static template = xml`
<div>
<t t-foreach="items" t-as="item" t-key="item">
<div class="item" t-on-click="ev => onClick(item.val, ev)"/>
</t>
</div>`;
items = [{ val: 1 }, { val: 2 }, { val: 3 }, { val: 4 }];
onClick(n: number, ev: MouseEvent) {
onClickArgs = [n, ev];
}
}
await mount(Parent, fixture);
expect(onClickArgs).toBeNull();
(<HTMLElement>fixture.querySelector(".item")).click();
expect(onClickArgs![0]).toBe(1);
expect(onClickArgs![1]).toBeInstanceOf(MouseEvent);
});
}); });
+96 -42
View File
@@ -4,7 +4,7 @@ import {
onWillUnmount, onWillUnmount,
onPatched, onPatched,
onWillUpdateProps, onWillUpdateProps,
onRender, onWillRender,
} from "../../src/component/lifecycle_hooks"; } from "../../src/component/lifecycle_hooks";
import { status } from "../../src/component/status"; import { status } from "../../src/component/status";
import { xml } from "../../src/tags"; import { xml } from "../../src/tags";
@@ -385,20 +385,25 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
"Parent:render", "Parent:willRender",
"Child:willUpdateProps", "Child:willUpdateProps",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willPatch", "Child:willPatch",
"Child:patched", "Child:patched",
"Parent:patched", "Parent:patched",
"Parent:render", "Parent:willRender",
"Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willUnmount", "Child:willUnmount",
"Child:destroyed", "Child:destroyed",
@@ -561,10 +566,12 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]); ]);
@@ -609,20 +616,29 @@ describe("lifecycle hooks", () => {
const app = new App(Parent); const app = new App(Parent);
const parent = await app.mount(fixture); const parent = await app.mount(fixture);
expect(steps).toEqual(["Parent:setup", "Parent:willStart", "Parent:render", "Parent:mounted"]); expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]);
steps.splice(0); steps.splice(0);
parent.state.hasChild = true; parent.state.hasChild = true;
await nextTick(); await nextTick();
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"GrandChild:setup", "GrandChild:setup",
"GrandChild:willStart", "GrandChild:willStart",
"GrandChild:render", "Child:rendered",
"GrandChild:willRender",
"GrandChild:rendered",
"Parent:willPatch", "Parent:willPatch",
"GrandChild:mounted", "GrandChild:mounted",
"Child:mounted", "Child:mounted",
@@ -672,7 +688,13 @@ describe("lifecycle hooks", () => {
const app = new App(Parent); const app = new App(Parent);
const parent = await app.mount(fixture); const parent = await app.mount(fixture);
expect(steps).toEqual(["Parent:setup", "Parent:willStart", "Parent:render", "Parent:mounted"]); expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]);
steps.splice(0); steps.splice(0);
@@ -717,7 +739,13 @@ describe("lifecycle hooks", () => {
const app = new App(Parent); const app = new App(Parent);
const parent = await app.mount(fixture); const parent = await app.mount(fixture);
expect(steps).toEqual(["Parent:setup", "Parent:willStart", "Parent:render", "Parent:mounted"]); expect(steps).toEqual([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]);
steps.splice(0); steps.splice(0);
@@ -725,12 +753,14 @@ describe("lifecycle hooks", () => {
await nextTick(); await nextTick();
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"GrandChild:setup", "GrandChild:setup",
"GrandChild:willStart", "GrandChild:willStart",
"Child:rendered",
]); ]);
steps.splice(0); steps.splice(0);
@@ -769,10 +799,12 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]); ]);
@@ -783,7 +815,8 @@ describe("lifecycle hooks", () => {
await nextTick(); await nextTick();
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:render", "Parent:willRender",
"Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willUnmount", "Child:willUnmount",
"Child:destroyed", "Child:destroyed",
@@ -816,10 +849,12 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]); ]);
@@ -830,9 +865,11 @@ describe("lifecycle hooks", () => {
await nextTick(); await nextTick();
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:render", "Parent:willRender",
"Child:willUpdateProps", "Child:willUpdateProps",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willPatch", "Child:willPatch",
"Child:patched", "Child:patched",
@@ -841,7 +878,7 @@ describe("lifecycle hooks", () => {
Object.freeze(steps); Object.freeze(steps);
}); });
test("onRender", async () => { test("onWillRender", async () => {
let steps: string[] = []; let steps: string[] = [];
const def = makeDeferred(); const def = makeDeferred();
@@ -852,7 +889,7 @@ describe("lifecycle hooks", () => {
setup() { setup() {
useLogLifecycle(steps); useLogLifecycle(steps);
onWillUpdateProps(() => def); onWillUpdateProps(() => def);
onRender(() => (this.visibleState = this.state.value)); onWillRender(() => (this.visibleState = this.state.value));
} }
increment() { increment() {
this.state.value++; this.state.value++;
@@ -891,17 +928,21 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
"Parent:render", "Parent:willRender",
"Child:willUpdateProps", "Child:willUpdateProps",
"Parent:rendered",
"inc:1", "inc:1",
"inc:1", "inc:1",
"Child:render", "Child:willRender",
"Child:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willPatch", "Child:willPatch",
"Child:patched", "Child:patched",
@@ -993,19 +1034,24 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"A:setup", "A:setup",
"A:willStart", "A:willStart",
"A:render", "A:willRender",
"B:setup", "B:setup",
"B:willStart", "B:willStart",
"C:setup", "C:setup",
"C:willStart", "C:willStart",
"B:render", "A:rendered",
"C:render", "B:willRender",
"B:rendered",
"C:willRender",
"D:setup", "D:setup",
"D:willStart", "D:willStart",
"E:setup", "E:setup",
"E:willStart", "E:willStart",
"D:render", "C:rendered",
"E:render", "D:willRender",
"D:rendered",
"E:willRender",
"E:rendered",
"E:mounted", "E:mounted",
"D:mounted", "D:mounted",
"C:mounted", "C:mounted",
@@ -1018,12 +1064,15 @@ describe("lifecycle hooks", () => {
c!.state.flag = false; c!.state.flag = false;
await nextTick(); await nextTick();
expect(steps).toEqual([ expect(steps).toEqual([
"C:render", "C:willRender",
"D:willUpdateProps", "D:willUpdateProps",
"F:setup", "F:setup",
"F:willStart", "F:willStart",
"D:render", "C:rendered",
"F:render", "D:willRender",
"D:rendered",
"F:willRender",
"F:rendered",
"C:willPatch", "C:willPatch",
"D:willPatch", "D:willPatch",
"E:willUnmount", "E:willUnmount",
@@ -1062,21 +1111,26 @@ describe("lifecycle hooks", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
"Parent:render", "Parent:willRender",
"Parent:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:willUnmount", "Child:willUnmount",
"Child:destroyed", "Child:destroyed",
"Parent:patched", "Parent:patched",
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch", "Parent:willPatch",
"Child:mounted", "Child:mounted",
"Parent:patched", "Parent:patched",
+73
View File
@@ -30,6 +30,20 @@ describe("basics", () => {
expect(fixture.innerHTML).toBe("<div><span>42</span></div>"); expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
}); });
test("prop names can contain -", async () => {
class Child extends Component {
static template = xml`<div><t t-esc="props['prop-name']"/></div>`;
}
class Parent extends Component {
static template = xml`<Child prop-name="7"/>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>7</div>");
});
test("accept ES6-like syntax for props (with getters)", async () => { test("accept ES6-like syntax for props (with getters)", async () => {
class Child extends Component { class Child extends Component {
static template = xml`<span><t t-esc="props.greetings"/></span>`; static template = xml`<span><t t-esc="props.greetings"/></span>`;
@@ -102,4 +116,63 @@ describe("basics", () => {
await mount(Parent, fixture); await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><span>&lt;p&gt;43&lt;/p&gt;<p>43</p></span></div>"); expect(fixture.innerHTML).toBe("<div><span>&lt;p&gt;43&lt;/p&gt;<p>43</p></span></div>");
}); });
test("arrow functions as prop correctly capture their scope", async () => {
class Child extends Component {
static template = xml`<button t-on-click="props.onClick"/>`;
}
let onClickArgs: [number, MouseEvent] | null = null;
class Parent extends Component {
static template = xml`
<t t-foreach="items" t-as="item" t-key="item.val">
<Child onClick="ev => onClick(item.val, ev)"/>
</t>
`;
static components = { Child };
items = [{ val: 1 }, { val: 2 }, { val: 3 }, { val: 4 }];
onClick(n: number, ev: MouseEvent) {
onClickArgs = [n, ev];
}
}
await mount(Parent, fixture);
expect(onClickArgs).toBeNull();
(<HTMLElement>fixture.querySelector("button")).click();
expect(onClickArgs![0]).toBe(1);
expect(onClickArgs![1]).toBeInstanceOf(MouseEvent);
});
test("support prop names that aren't valid bare object property names", async () => {
expect.assertions(4);
class Child extends Component {
static template = xml`<button t-on-click="props.onClick"/>`;
setup() {
expect(this.props["some-dashed-prop"]).toBe(5);
expect(this.props["a.b"]).toBe("keyword prop");
}
}
class Parent extends Component {
static template = xml`<Child some-dashed-prop="5" a.b="'keyword prop'"/>`;
static components = { Child };
}
await mount(Parent, fixture);
});
test("template string in prop", async () => {
expect.assertions(3);
class Child extends Component {
static template = xml``;
setup() {
expect(this.props.propName).toBe("123");
}
}
class Parent extends Component {
static template = xml({ raw: ['<Child propName="`1${someVal}3`"/>'] });
static components = { Child };
someVal = 2;
}
await mount(Parent, fixture);
});
}); });
+116 -116
View File
@@ -46,23 +46,23 @@ describe("props validation", () => {
static components = { SubComp }; static components = { SubComp };
static template = xml`<div><SubComp /></div>`; static template = xml`<div><SubComp /></div>`;
} }
let error; let error: Error | undefined;
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe(`Missing props 'message' (component 'SubComp')`); expect(error!.message).toBe(`Missing props 'message' (component 'SubComp')`);
error = undefined; error = undefined;
try { try {
await mountApp(Parent, false); await mountApp(Parent, false);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
}); });
test("props: list of strings", async () => { test("props: list of strings", async () => {
@@ -75,14 +75,14 @@ describe("props validation", () => {
static template = xml`<div><SubComp /></div>`; static template = xml`<div><SubComp /></div>`;
} }
let error; let error: Error;
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe(`Missing props 'message' (component 'SubComp')`); expect(error!.message).toBe(`Missing props 'message' (component 'SubComp')`);
}); });
test("validate simple types", async () => { test("validate simple types", async () => {
@@ -108,32 +108,32 @@ describe("props validation", () => {
}; };
(Parent as any).components = { SubComp }; (Parent as any).components = { SubComp };
let error; let error: Error | undefined;
props = {}; props = {};
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe(`Missing props 'p' (component '_a')`); expect(error!.message).toBe(`Missing props 'p' (component '_a')`);
error = undefined; error = undefined;
props = { p: test.ok }; props = { p: test.ok };
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
props = { p: test.ko }; props = { p: test.ko };
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component '_a'"); expect(error!.message).toBe("Invalid Prop 'p' in component '_a'");
} }
}); });
@@ -159,31 +159,31 @@ describe("props validation", () => {
static template = xml`<div>hey</div>`; static template = xml`<div>hey</div>`;
}; };
(Parent as any).components = { SubComp }; (Parent as any).components = { SubComp };
let error; let error: Error | undefined;
props = {}; props = {};
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe(`Missing props 'p' (component '_a')`); expect(error!.message).toBe(`Missing props 'p' (component '_a')`);
error = undefined; error = undefined;
props = { p: test.ok }; props = { p: test.ok };
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
props = { p: test.ko }; props = { p: test.ko };
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component '_a'"); expect(error!.message).toBe("Invalid Prop 'p' in component '_a'");
} }
}); });
@@ -199,30 +199,30 @@ describe("props validation", () => {
return props.p; return props.p;
} }
} }
let error; let error: Error;
let props: { p?: any }; let props: { p?: any };
try { try {
props = { p: "string" }; props = { p: "string" };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: true }; props = { p: true };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: 1 }; props = { p: 1 };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
}); });
test("can validate an optional props", async () => { test("can validate an optional props", async () => {
@@ -237,30 +237,30 @@ describe("props validation", () => {
return props.p; return props.p;
} }
} }
let error; let error: Error;
let props: { p?: any }; let props: { p?: any };
try { try {
props = { p: "key" }; props = { p: "key" };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = {}; props = {};
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: 1 }; props = { p: 1 };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
}); });
test("can validate an array with given primitive type", async () => { test("can validate an array with given primitive type", async () => {
@@ -275,35 +275,35 @@ describe("props validation", () => {
return props.p; return props.p;
} }
} }
let error; let error: Error | undefined;
let props: { p?: any }; let props: { p?: any };
try { try {
props = { p: [] }; props = { p: [] };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: ["string"] }; props = { p: ["string"] };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: [1] }; props = { p: [1] };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
error = undefined; error = undefined;
try { try {
props = { p: ["string", 1] }; props = { p: ["string", 1] };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
}); });
@@ -319,37 +319,37 @@ describe("props validation", () => {
return props.p; return props.p;
} }
} }
let error; let error: Error;
let props: { p?: any }; let props: { p?: any };
try { try {
props = { p: [] }; props = { p: [] };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: ["string"] }; props = { p: ["string"] };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: [false, true, "string"] }; props = { p: [false, true, "string"] };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: [true, 1] }; props = { p: [true, 1] };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
}); });
test("can validate an object with simple shape", async () => { test("can validate an object with simple shape", async () => {
@@ -366,40 +366,40 @@ describe("props validation", () => {
return props.p; return props.p;
} }
} }
let error; let error: Error | undefined;
let props: { p?: any }; let props: { p?: any };
try { try {
props = { p: { id: 1, url: "url" } }; props = { p: { id: 1, url: "url" } };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: { id: 1, url: "url", extra: true } }; props = { p: { id: 1, url: "url", extra: true } };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid prop 'p' in component SubComp (unknown prop 'extra')"); expect(error!.message).toBe("Invalid prop 'p' in component SubComp (unknown prop 'extra')");
try { try {
props = { p: { id: "1", url: "url" } }; props = { p: { id: "1", url: "url" } };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
error = undefined; error = undefined;
try { try {
props = { p: { id: 1 } }; props = { p: { id: 1 } };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
}); });
test("can validate recursively complicated prop def", async () => { test("can validate recursively complicated prop def", async () => {
@@ -422,30 +422,30 @@ describe("props validation", () => {
return props.p; return props.p;
} }
} }
let error; let error: Error;
let props: { p?: any }; let props: { p?: any };
try { try {
props = { p: { id: 1, url: true } }; props = { p: { id: 1, url: true } };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: { id: 1, url: [12] } }; props = { p: { id: 1, url: [12] } };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
props = { p: { id: 1, url: [12, true] } }; props = { p: { id: 1, url: [12, true] } };
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'");
}); });
test("can validate optional attributes in nested sub props", () => { test("can validate optional attributes in nested sub props", () => {
@@ -462,20 +462,20 @@ describe("props validation", () => {
}, },
}; };
} }
let error; let error: Error;
try { try {
validateProps(TestComponent as any, { myprop: [{}] }); validateProps(TestComponent as any, { myprop: [{}] });
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
validateProps(TestComponent as any, { myprop: [{ a: 1 }] }); validateProps(TestComponent as any, { myprop: [{ a: 1 }] });
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe( expect(error!.message).toBe(
"Invalid prop 'myprop' in component TestComponent (unknown prop 'a')" "Invalid prop 'myprop' in component TestComponent (unknown prop 'a')"
); );
}); });
@@ -488,20 +488,20 @@ describe("props validation", () => {
}, },
}; };
} }
let error; let error: Error;
try { try {
validateProps(TestComponent as any, { size: "small" }); validateProps(TestComponent as any, { size: "small" });
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
try { try {
validateProps(TestComponent as any, { size: "abcdef" }); validateProps(TestComponent as any, { size: "abcdef" });
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'"); expect(error!.message).toBe("Invalid Prop 'size' in component 'TestComponent'");
}); });
test("can validate with a custom validator, and a type", () => { test("can validate with a custom validator, and a type", () => {
@@ -514,30 +514,30 @@ describe("props validation", () => {
}, },
}; };
} }
let error; let error: Error | undefined;
try { try {
validateProps(TestComponent as any, { n: 3 }); validateProps(TestComponent as any, { n: 3 });
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeUndefined(); expect(error!).toBeUndefined();
expect(validator).toBeCalledTimes(1); expect(validator).toBeCalledTimes(1);
try { try {
validateProps(TestComponent as any, { n: "str" }); validateProps(TestComponent as any, { n: "str" });
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); expect(error!.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(validator).toBeCalledTimes(1); expect(validator).toBeCalledTimes(1);
error = null; error = undefined;
try { try {
validateProps(TestComponent as any, { n: 100 }); validateProps(TestComponent as any, { n: 100 });
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); expect(error!.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(validator).toBeCalledTimes(2); expect(validator).toBeCalledTimes(2);
}); });
@@ -647,18 +647,18 @@ describe("props validation", () => {
static template = xml`<div><SubComp/></div>`; static template = xml`<div><SubComp/></div>`;
static components = { SubComp }; static components = { SubComp };
} }
let error; let error: Error;
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Missing props 'p' (component 'SubComp')"); expect(error!.message).toBe("Missing props 'p' (component 'SubComp')");
}); });
test("props are validated whenever component is updated", async () => { test("props are validated whenever component is updated", async () => {
let error; let error: Error;
class SubComp extends Component { class SubComp extends Component {
static props = { p: { type: Number } }; static props = { p: { type: Number } };
static template = xml`<div><t t-esc="props.p"/></div>`; static template = xml`<div><t t-esc="props.p"/></div>`;
@@ -674,10 +674,10 @@ describe("props validation", () => {
(app as any).root.component.state.p = undefined; (app as any).root.component.state.p = undefined;
await (app as any).root.component.render(); await (app as any).root.component.render();
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Missing props 'p' (component 'SubComp')"); expect(error!.message).toBe("Missing props 'p' (component 'SubComp')");
}); });
test("default values are applied before validating props at update", async () => { test("default values are applied before validating props at update", async () => {
@@ -712,14 +712,14 @@ describe("props validation", () => {
static components = { Child }; static components = { Child };
static template = xml`<div><Child/></div>`; static template = xml`<div><Child/></div>`;
} }
let error; let error: Error;
try { try {
await mountApp(Parent); await mountApp(Parent);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("Missing props 'mandatory' (component 'Child')"); expect(error!.message).toBe("Missing props 'mandatory' (component 'Child')");
}); });
}); });
+4 -4
View File
@@ -2,7 +2,7 @@ import {
Component, Component,
mount, mount,
onPatched, onPatched,
onRender, onWillRender,
onWillPatch, onWillPatch,
onWillUnmount, onWillUnmount,
useState, useState,
@@ -37,7 +37,7 @@ describe("reactivity in lifecycle", () => {
static template = xml`<div><t t-esc="state.a"/></div>`; static template = xml`<div><t t-esc="state.a"/></div>`;
state = useState({ a: 5, b: 7 }); state = useState({ a: 5, b: 7 });
setup() { setup() {
onRender(() => n++); onWillRender(() => n++);
} }
} }
const comp = await mount(Comp, fixture); const comp = await mount(Comp, fixture);
@@ -61,7 +61,7 @@ describe("reactivity in lifecycle", () => {
`; `;
state = useState({ n: 2 }); state = useState({ n: 2 });
setup() { setup() {
onRender(() => { onWillRender(() => {
steps.push("render"); steps.push("render");
}); });
onWillPatch(() => { onWillPatch(() => {
@@ -132,7 +132,7 @@ describe("reactivity in lifecycle", () => {
state = useState({ val: 1 }); state = useState({ val: 1 });
setup() { setup() {
STATE = this.state; STATE = this.state;
onRender(() => { onWillRender(() => {
steps.push(this.state.val); steps.push(this.state.val);
}); });
} }
+175 -1
View File
@@ -37,6 +37,59 @@ describe("slots", () => {
expect(fixture.innerHTML).toBe("some text"); expect(fixture.innerHTML).toBe("some text");
}); });
test("simple default slot with params", async () => {
let child: any;
class Child extends Component {
static template = xml`<span><t t-slot="default" bool="state.bool"/></span>`;
state = useState({ bool: true });
setup() {
child = this;
}
}
class Parent extends Component {
static template = xml`
<Child>
<t t-set-slot="default" t-slot-scope="slotScope">
<t t-if="slotScope.bool">some text</t>
<t t-else="slotScope.bool">other text</t>
</t>
</Child>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>some text</span>");
child.state.bool = false;
await nextTick();
expect(fixture.innerHTML).toBe("<span>other text</span>");
});
test("simple default slot with params", async () => {
class Child extends Component {
static template = xml`<span><t t-slot="default" bool="state.bool"/></span>`;
state = useState({ bool: true });
}
class Parent extends Component {
static template = xml`
<Child>
<t t-if="slotScope.bool">some text</t>
<t t-else="slotScope.bool">other text</t>
</Child>`;
static components = { Child };
}
let error = null;
try {
await mount(Parent, fixture);
} catch (e) {
error = e;
}
expect(error).not.toBeNull();
});
test("fun: two calls to the same slot", async () => { test("fun: two calls to the same slot", async () => {
class Child extends Component { class Child extends Component {
static template = xml`<t t-slot="default"/><t t-slot="default"/>`; static template = xml`<t t-slot="default"/><t t-slot="default"/>`;
@@ -71,6 +124,33 @@ describe("slots", () => {
expect(parent.state.value).toBe(1); expect(parent.state.value).toBe(1);
}); });
test("slot content is bound to caller (variation)", async () => {
class Child extends Component {
static template = xml`<span><t t-slot="default"/></span>`;
}
class Parent extends Component {
// t-set t-value in template is to force compiler to protect the scope
// which in turns means that the ctx propagated to the slot is a sub object
static template = xml`
<Child>
<t t-set="var" t-value="1"/>
<button t-on-click="() => this.inc()">some text</button>
</Child>`;
static components = { Child };
state = useState({ value: 0 });
inc() {
expect(this).toBe(parent);
this.state.value++;
}
}
const parent = await mount(Parent, fixture);
expect(parent.state.value).toBe(0);
fixture.querySelector("button")!.click();
expect(parent.state.value).toBe(1);
});
test("can define and call slots", async () => { test("can define and call slots", async () => {
class Dialog extends Component { class Dialog extends Component {
static template = xml` static template = xml`
@@ -97,6 +177,35 @@ describe("slots", () => {
); );
}); });
test("can define and call slots with params", async () => {
class Dialog extends Component {
static template = xml`
<div>
<t t-esc="props.slots['header'].param"/>
<div><t t-slot="header"/></div>
<t t-esc="props.slots['footer'].param"/>
<div><t t-slot="footer"/></div>
</div>`;
}
class Parent extends Component {
static components = { Dialog };
static template = xml`
<div>
<Dialog>
<t t-set-slot="header" param="var"><span>header</span></t>
<t t-set-slot="footer" param="'5'"><span>footer</span></t>
</Dialog>
</div>`;
var = 3;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
"<div><div>3<div><span>header</span></div>5<div><span>footer</span></div></div></div>"
);
});
test("no named slot content => just no children", async () => { test("no named slot content => just no children", async () => {
class Dialog extends Component { class Dialog extends Component {
static template = xml`<span><t t-slot="header"/></span>`; static template = xml`<span><t t-slot="header"/></span>`;
@@ -126,7 +235,7 @@ describe("slots", () => {
expect(fixture.innerHTML).toBe("<div><span>default content</span></div>"); expect(fixture.innerHTML).toBe("<div><span>default content</span></div>");
}); });
test("dafault slots can define a default content", async () => { test("can define a default content", async () => {
class Dialog extends Component { class Dialog extends Component {
static template = xml` static template = xml`
<span> <span>
@@ -174,6 +283,42 @@ describe("slots", () => {
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>"); expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
}); });
test("slots are properly bound to correct component", async () => {
let child: any = null;
class Child extends Component {
// t-set t-value in template is to force compiler to protect the scope
// which in turns means that the ctx propagated to the slot is a sub object
static template = xml`
<t t-slot="default">
<t t-set="var" t-value="1"/>
<button t-on-click="() => this.increment()">
<t t-esc="state.value"/>
</button>
</t>`;
state = useState({ value: 1 });
setup() {
child = this;
}
increment() {
expect(this).toBe(child);
this.state.value++;
}
}
class Parent extends Component {
static template = xml`<Child/>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<button>1</button>");
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.innerHTML).toBe("<button>2</button>");
});
test("slots are rendered with proper context", async () => { test("slots are rendered with proper context", async () => {
class Dialog extends Component { class Dialog extends Component {
static template = xml`<span><t t-slot="footer"/></span>`; static template = xml`<span><t t-slot="footer"/></span>`;
@@ -1352,4 +1497,33 @@ describe("slots", () => {
} }
expect(error).toBeNull(); expect(error).toBeNull();
}); });
test("can use t-call in default-content of t-slot", async () => {
const template = xml``;
class Child extends Component {
static template = xml`<t t-slot="default"><t t-call="${template}"/></t>`;
}
class Parent extends Component {
static template = xml`<Child/>`;
static components = { Child };
}
await mount(Parent, fixture);
});
test("can use component in default-content of t-slot", async () => {
class GrandChild extends Component {
static template = xml``;
}
class Child extends Component {
static template = xml`<t t-slot="default"><GrandChild/></t>`;
static components = { GrandChild };
}
class Parent extends Component {
static template = xml`<Child/>`;
static components = { Child };
}
await mount(Parent, fixture);
});
}); });
+4 -4
View File
@@ -338,16 +338,16 @@ describe("style and class handling", () => {
static template = xml`<Child class="'a'"/>`; static template = xml`<Child class="'a'"/>`;
static components = { Child }; static components = { Child };
} }
let error; let error: Error;
try { try {
await mount(ParentWidget, fixture); await mount(ParentWidget, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp); expect(error!.message).toMatch(regexp);
expect(fixture.innerHTML).toBe(""); expect(fixture.innerHTML).toBe("");
}); });
}); });
+4 -4
View File
@@ -87,14 +87,14 @@ describe("styles and component", () => {
static template = xml`<div class="app">text</div>`; static template = xml`<div class="app">text</div>`;
static style = `.app {color: red;}`; static style = `.app {color: red;}`;
} }
let error; let error: Error;
try { try {
await mount(App, fixture); await mount(App, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe( expect(error!.message).toBe(
"Invalid css stylesheet for component 'App'. Did you forget to use the 'css' tag helper?" "Invalid css stylesheet for component 'App'. Did you forget to use the 'css' tag helper?"
); );
}); });
+16 -10
View File
@@ -35,10 +35,12 @@ describe("t-component", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
]); ]);
@@ -80,16 +82,20 @@ describe("t-component", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"Parent:render", "Parent:willRender",
"ChildA:setup", "ChildA:setup",
"ChildA:willStart", "ChildA:willStart",
"ChildA:render", "Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted", "ChildA:mounted",
"Parent:mounted", "Parent:mounted",
"Parent:render", "Parent:willRender",
"ChildB:setup", "ChildB:setup",
"ChildB:willStart", "ChildB:willStart",
"ChildB:render", "Parent:rendered",
"ChildB:willRender",
"ChildB:rendered",
"Parent:willPatch", "Parent:willPatch",
"ChildA:willUnmount", "ChildA:willUnmount",
"ChildA:destroyed", "ChildA:destroyed",
@@ -199,14 +205,14 @@ describe("t-component", () => {
static components = { Child }; static components = { Child };
static template = xml`<div><div t-component="Child"/></div>`; static template = xml`<div><div t-component="Child"/></div>`;
} }
let error; let error: Error;
try { try {
await mount(Parent, fixture); await mount(Parent, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe( expect(error!.message).toBe(
`Directive 't-component' can only be used on <t> nodes (used on a <div>)` `Directive 't-component' can only be used on <t> nodes (used on a <div>)`
); );
}); });
+38 -5
View File
@@ -1,5 +1,11 @@
import { Component, mount, onMounted, useState, xml } from "../../src/index"; import { App, Component, mount, onMounted, useState, xml } from "../../src/index";
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers"; import {
makeTestFixture,
nextTick,
snapshotApp,
snapshotEverything,
useLogLifecycle,
} from "../helpers";
snapshotEverything(); snapshotEverything();
@@ -81,13 +87,16 @@ describe("list of components", () => {
expect(steps).toEqual([ expect(steps).toEqual([
"Parent:setup", "Parent:setup",
"Parent:willStart", "Parent:willStart",
"Parent:render", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
"Child:render", "Parent:rendered",
"Child:render", "Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted", "Child:mounted",
"Child:mounted", "Child:mounted",
"Parent:mounted", "Parent:mounted",
@@ -288,4 +297,28 @@ describe("list of components", () => {
expect((parent.el as HTMLElement).innerHTML).toBe("<div>2</div><div>1</div>"); expect((parent.el as HTMLElement).innerHTML).toBe("<div>2</div><div>1</div>");
expect(childInstances.length).toBe(2); expect(childInstances.length).toBe(2);
}); });
test("crash on duplicate key in dev mode", async () => {
const consoleInfo = console.info;
console.info = jest.fn();
class Child extends Component {
static template = xml``;
}
class Parent extends Component {
static template = xml`
<t t-foreach="[1, 2]" t-as="item" t-key="'child'">
<Child/>
</t>
`;
static components = { Child };
}
const app = new App(Parent);
app.configure({ dev: true });
await expect(async () => {
await app.mount(fixture);
}).rejects.toThrowError("Got duplicate key in t-foreach: child");
snapshotApp(app);
console.info = consoleInfo;
});
}); });
+4 -4
View File
@@ -57,14 +57,14 @@ describe("t-model directive", () => {
</div>`; </div>`;
state = useState({ text: "" }); state = useState({ text: "" });
} }
let error; let error: Error;
try { try {
await mount(SomeComponent, fixture); await mount(SomeComponent, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`); expect(error!.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
}); });
test("basic use, on another key in component", async () => { test("basic use, on another key in component", async () => {
+1 -1
View File
@@ -42,7 +42,7 @@ describe("t-props", () => {
a: "first", a: "first",
b: "second", b: "second",
}); });
a = "third"; a? = "third";
} }
const parent = await mount(Parent, fixture); const parent = await mount(Parent, fixture);
expect(fixture.textContent).toBe("thirdsecond"); expect(fixture.textContent).toBe("thirdsecond");
+8 -3
View File
@@ -4,13 +4,14 @@ import {
onDestroyed, onDestroyed,
onMounted, onMounted,
onPatched, onPatched,
onRender, onWillRender,
onWillPatch, onWillPatch,
onWillStart, onWillStart,
onWillUnmount, onWillUnmount,
onWillUpdateProps, onWillUpdateProps,
status, status,
useComponent, useComponent,
onRendered,
} from "../src"; } from "../src";
import { BDom } from "../src/blockdom"; import { BDom } from "../src/blockdom";
import { blockDom } from "../src"; import { blockDom } from "../src";
@@ -181,8 +182,12 @@ export function useLogLifecycle(steps: string[]) {
steps.push(`${name}:willUpdateProps`); steps.push(`${name}:willUpdateProps`);
}); });
onRender(() => { onWillRender(() => {
steps.push(`${name}:render`); steps.push(`${name}:willRender`);
});
onRendered(() => {
steps.push(`${name}:rendered`);
}); });
onWillPatch(() => { onWillPatch(() => {
+7 -10
View File
@@ -17,9 +17,8 @@ exports[`Memo if no prop change, prevent renderings from above 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
let b6 = text(ctx['state'].a); let b6 = text(ctx['state'].a);
let b7 = text(ctx['state'].b); let b7 = text(ctx['state'].b);
let b8 = text(ctx['state'].c); let b8 = text(ctx['state'].c);
@@ -30,7 +29,7 @@ exports[`Memo if no prop change, prevent renderings from above 2`] = `
let b2 = text(ctx['state'].a); let b2 = text(ctx['state'].a);
let b3 = text(ctx['state'].b); let b3 = text(ctx['state'].b);
let b4 = text(ctx['state'].c); let b4 = text(ctx['state'].c);
let b9 = assign(component(\`Memo\`, {a: ctx['state'].a,b: ctx['state'].b}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b9 = component(\`Memo\`, {a: ctx['state'].a,b: ctx['state'].b,slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return multi([b2, b3, b4, b9]); return multi([b2, b3, b4, b9]);
} }
}" }"
@@ -53,15 +52,14 @@ exports[`Memo if no props, prevent renderings from above 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
const slot3 = ctx => (node, key) => { function slot2(ctx, node, key) {
return component(\`Child\`, {value: ctx['state'].value}, key + \`__4\`, node, ctx); return component(\`Child\`, {value: ctx['state'].value}, key + \`__3\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2 = component(\`Child\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx); let b2 = component(\`Child\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
let b4 = assign(component(\`Memo\`, {}, key + \`__2\`, node, ctx), {slots: {'default': slot3(ctx)}}); let b4 = component(\`Memo\`, {slots: {'default': {__render: slot2, __ctx: ctx}}}, key + \`__4\`, node, ctx);
return multi([b2, b4]); return multi([b2, b4]);
} }
}" }"
@@ -72,15 +70,14 @@ exports[`Memo if no props, prevent renderings from above (work with simple html)
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return text(ctx['state'].value); return text(ctx['state'].value);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2 = text(ctx['state'].value); let b2 = text(ctx['state'].value);
let b4 = assign(component(\`Memo\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b4 = component(\`Memo\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return multi([b2, b4]); return multi([b2, b4]);
} }
}" }"
+49 -70
View File
@@ -9,8 +9,7 @@ exports[`Portal Portal composed with t-slot 1`] = `
let block1 = createBlock(\`<div block-handler-0=\\"custom\\"><span id=\\"childSpan\\">child2</span></div>\`); let block1 = createBlock(\`<div block-handler-0=\\"custom\\"><span id=\\"childSpan\\">child2</span></div>\`);
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
const v1 = ctx['onCustom']; let d1 = [ctx['onCustom'], ctx];
let d1 = [v1, ctx];
return block1([d1]); return block1([d1]);
} }
}" }"
@@ -21,14 +20,13 @@ exports[`Portal Portal composed with t-slot 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return callSlot(ctx, node, key, 'default'); return callSlot(ctx, node, key, 'default', false, {});
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
return assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); return component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
} }
}" }"
`; `;
@@ -38,16 +36,15 @@ exports[`Portal Portal composed with t-slot 3`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`Child2\`, {customHandler: ctx['_handled']}, key + \`__3\`, node, ctx); return component(\`Child2\`, {customHandler: ctx['_handled']}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Child\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -58,17 +55,16 @@ exports[`Portal basic use of portal 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><span>1</span><block-child-0/></div>\`); let block1 = createBlock(\`<div><span>1</span><block-child-0/></div>\`);
let block2 = createBlock(\`<p>2</p>\`); let block2 = createBlock(\`<p>2</p>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return block2(); return block2();
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -94,19 +90,18 @@ exports[`Portal conditional use of Portal (with sub Component) 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block2 = createBlock(\`<span>1</span>\`); let block2 = createBlock(\`<span>1</span>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`Child\`, {val: ctx['state'].val}, key + \`__3\`, node, ctx); return component(\`Child\`, {val: ctx['state'].val}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b2,b4; let b2,b4;
b2 = block2(); b2 = block2();
if (ctx['state'].hasPortal) { if (ctx['state'].hasPortal) {
b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); b4 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
} }
return multi([b2, b4]); return multi([b2, b4]);
} }
@@ -118,12 +113,11 @@ exports[`Portal conditional use of Portal 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block2 = createBlock(\`<span>1</span>\`); let block2 = createBlock(\`<span>1</span>\`);
let block3 = createBlock(\`<p>2</p>\`); let block3 = createBlock(\`<p>2</p>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return block3(); return block3();
} }
@@ -131,7 +125,7 @@ exports[`Portal conditional use of Portal 1`] = `
let b2,b4; let b2,b4;
b2 = block2(); b2 = block2();
if (ctx['state'].hasPortal) { if (ctx['state'].hasPortal) {
b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); b4 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
} }
return multi([b2, b4]); return multi([b2, b4]);
} }
@@ -158,18 +152,17 @@ exports[`Portal lifecycle hooks of portal sub component are properly called 2`]
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`Child\`, {val: ctx['state'].val}, key + \`__3\`, node, ctx); return component(\`Child\`, {val: ctx['state'].val}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3; let b3;
if (ctx['state'].hasChild) { if (ctx['state'].hasChild) {
b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
} }
return block1([], [b3]); return block1([], [b3]);
} }
@@ -181,12 +174,11 @@ exports[`Portal portal could have dynamically no content 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<span><block-text-0/></span>\`); let block3 = createBlock(\`<span><block-text-0/></span>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
let b3; let b3;
if (ctx['state'].val) { if (ctx['state'].val) {
let d1 = ctx['state'].val; let d1 = ctx['state'].val;
@@ -196,7 +188,7 @@ exports[`Portal portal could have dynamically no content 1`] = `
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b4 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b4]); return block1([], [b4]);
} }
}" }"
@@ -222,16 +214,15 @@ exports[`Portal portal destroys on crash 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`Child\`, {error: ctx['state'].error}, key + \`__3\`, node, ctx); return component(\`Child\`, {error: ctx['state'].error}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -257,16 +248,15 @@ exports[`Portal portal with child and props 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`Child\`, {val: ctx['state'].val}, key + \`__3\`, node, ctx); return component(\`Child\`, {val: ctx['state'].val}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -277,13 +267,12 @@ exports[`Portal portal with dynamic body 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<span><block-text-0/></span>\`); let block3 = createBlock(\`<span><block-text-0/></span>\`);
let block4 = createBlock(\`<div/>\`); let block4 = createBlock(\`<div/>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
let b3,b4; let b3,b4;
if (ctx['state'].val) { if (ctx['state'].val) {
let d1 = ctx['state'].val; let d1 = ctx['state'].val;
@@ -295,7 +284,7 @@ exports[`Portal portal with dynamic body 1`] = `
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b5 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b5 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b5]); return block1([], [b5]);
} }
}" }"
@@ -306,20 +295,19 @@ exports[`Portal portal with many children 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block3 = createBlock(\`<div>1</div>\`); let block3 = createBlock(\`<div>1</div>\`);
let block4 = createBlock(\`<p>2</p>\`); let block4 = createBlock(\`<p>2</p>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
let b3 = block3(); let b3 = block3();
let b4 = block4(); let b4 = block4();
return multi([b3, b4]); return multi([b3, b4]);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b5 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b5 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b5]); return block1([], [b5]);
} }
}" }"
@@ -330,11 +318,10 @@ exports[`Portal portal with no content 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
let b3; let b3;
if (false) { if (false) {
b3 = text('ABC'); b3 = text('ABC');
@@ -343,7 +330,7 @@ exports[`Portal portal with no content 1`] = `
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b4 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b4 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b4]); return block1([], [b4]);
} }
}" }"
@@ -354,16 +341,15 @@ exports[`Portal portal with only text as content 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return text('only text'); return text('only text');
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -374,17 +360,16 @@ exports[`Portal portal with target not in dom 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<div>2</div>\`); let block2 = createBlock(\`<div>2</div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return block2(); return block2();
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#does-not-exist'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: '#does-not-exist',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -409,16 +394,15 @@ exports[`Portal portal's parent's env is not polluted 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`Child\`, {}, key + \`__3\`, node, ctx); return component(\`Child\`, {}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -429,17 +413,16 @@ exports[`Portal with target in template (after portal) 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><span>1</span><block-child-0/><div id=\\"local-target\\"/></div>\`); let block1 = createBlock(\`<div><span>1</span><block-child-0/><div id=\\"local-target\\"/></div>\`);
let block2 = createBlock(\`<p>2</p>\`); let block2 = createBlock(\`<p>2</p>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return block2(); return block2();
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#local-target'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: '#local-target',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -450,17 +433,16 @@ exports[`Portal with target in template (before portal) 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><div id=\\"local-target\\"/><span>1</span><block-child-0/></div>\`); let block1 = createBlock(\`<div><div id=\\"local-target\\"/><span>1</span><block-child-0/></div>\`);
let block2 = createBlock(\`<p>2</p>\`); let block2 = createBlock(\`<p>2</p>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return block2(); return block2();
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#local-target'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: '#local-target',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -471,17 +453,16 @@ exports[`Portal: Props validation target is mandatory 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<div>2</div>\`); let block2 = createBlock(\`<div>2</div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return block2(); return block2();
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -492,17 +473,16 @@ exports[`Portal: Props validation target is not list 1`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<div>2</div>\`); let block2 = createBlock(\`<div>2</div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return block2(); return block2();
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: ['body']}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: ['body'],slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__2\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
@@ -528,16 +508,15 @@ exports[`Portal: UI/UX focus is kept across re-renders 2`] = `
) { ) {
let { text, createBlock, list, multi, html, toggler, component } = bdom; let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let assign = Object.assign;
let block1 = createBlock(\`<div><block-child-0/></div>\`); let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => { function slot1(ctx, node, key) {
return component(\`Child\`, {val: ctx['state'].val}, key + \`__3\`, node, ctx); return component(\`Child\`, {val: ctx['state'].val}, key + \`__2\`, node, ctx);
} }
return function template(ctx, node, key = \\"\\") { return function template(ctx, node, key = \\"\\") {
let b3 = assign(component(\`Portal\`, {target: '#outside'}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}}); let b3 = component(\`Portal\`, {target: '#outside',slots: {'default': {__render: slot1, __ctx: ctx}}}, key + \`__3\`, node, ctx);
return block1([], [b3]); return block1([], [b3]);
} }
}" }"
+16 -16
View File
@@ -160,15 +160,15 @@ describe("Portal", () => {
</div>`; </div>`;
} }
let error; let error: Error;
try { try {
await mount(Parent, fixture); await mount(Parent, fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe("invalid portal target"); expect(error!.message).toBe("invalid portal target");
expect(fixture.innerHTML).toBe(`<div></div>`); expect(fixture.innerHTML).toBe(`<div></div>`);
}); });
@@ -396,16 +396,16 @@ describe("Portal", () => {
const parent = await mount(Parent, fixture); const parent = await mount(Parent, fixture);
parent.state.error = true; parent.state.error = true;
let error; let error: Error;
try { try {
await parent.render(); await parent.render();
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
const regexp = const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp); expect(error!.message).toMatch(regexp);
}); });
test("portal's parent's env is not polluted", async () => { test("portal's parent's env is not polluted", async () => {
@@ -519,16 +519,16 @@ describe("Portal: Props validation", () => {
</Portal> </Portal>
</div>`; </div>`;
} }
let error; let error: Error;
let app = new App(Parent); let app = new App(Parent);
app.configure({ dev: true }); app.configure({ dev: true });
try { try {
await app.mount(fixture); await app.mount(fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe(`Missing props 'target' (component 'Portal')`); expect(error!.message).toBe(`Missing props 'target' (component 'Portal')`);
console.info = consoleInfo; console.info = consoleInfo;
}); });
@@ -544,16 +544,16 @@ describe("Portal: Props validation", () => {
</Portal> </Portal>
</div>`; </div>`;
} }
let error; let error: Error;
let app = new App(Parent); let app = new App(Parent);
app.configure({ dev: true }); app.configure({ dev: true });
try { try {
await app.mount(fixture); await app.mount(fixture);
} catch (e) { } catch (e) {
error = e; error = e as Error;
} }
expect(error).toBeDefined(); expect(error!).toBeDefined();
expect(error.message).toBe(`Invalid Prop 'target' in component 'Portal'`); expect(error!.message).toBe(`Invalid Prop 'target' in component 'Portal'`);
console.info = consoleInfo; console.info = consoleInfo;
}); });
}); });
+496 -459
View File
File diff suppressed because it is too large Load Diff
-301
View File
@@ -1,301 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
odoo.define("app", function(require) {
const Widget = require("web.Widget");
var core = require("web.core");
var dom = require("web.dom");
//----------------------------------------------------------------------------
// Likes Counter Widget
//----------------------------------------------------------------------------
var Counter = Widget.extend({
template: "counter",
events: {
"click .o_increment": "_onIncrement"
},
init: function(parent) {
this._super(parent);
this.value = 0;
},
start: function() {
this.updateCounter();
},
on_attach_callback: function() {},
on_detach_callback: function() {},
updateCounter: function() {
this.$(".o_increment").html("Value: " + this.value);
},
_onIncrement: function(ev) {
ev.stopPropagation();
this.value += 1;
this.updateCounter();
}
});
//----------------------------------------------------------------------------
// Message Widget
//----------------------------------------------------------------------------
var Message = Widget.extend({
template: "message",
events: {
"click .remove": "_onRemove"
},
init: function(parent, message) {
this._super(parent);
this.author = message.author;
this.msg = message.msg;
this.id = message.id;
},
willStart: function() {
this.counter = new Counter(this);
return this.counter.appendTo($("<div>"));
},
start: function() {
this.$msg = this.$(".msg");
dom.append(this.$el, this.counter.$el, {
in_DOM: this.isInDom,
callbacks: [{ widget: this.counter }]
});
},
on_attach_callback: function() {
this.isInDom = true;
if (this.counter) {
this.counter.on_attach_callback();
}
},
on_detach_callback: function() {
this.isInDom = true;
if (this.counter) {
this.counter.on_detach_callback();
}
},
_onRemove: function() {
this.trigger_up("remove_message", { id: this.id });
},
update: function() {
this.msg += "!!!";
this.$msg.text(this.msg);
}
});
//----------------------------------------------------------------------------
// Root Widget
//----------------------------------------------------------------------------
var App = Widget.extend({
template: "root",
events: {
"click .o_btn_msg.100": function() {
this.addMessages(100);
},
"click .o_btn_msg.1000": function() {
this.addMessages(1000);
},
"click .o_btn_msg.10000": function() {
this.addMessages(10000);
},
"click .o_btn_msg.30000": function() {
this.addMessages(30000);
},
"click .updateSomeMessages": function() {
this.updateSomeMessages();
},
"click .clear": function() {
this.clear();
},
"click .o_multiple": function() {
this.multipleFlag = !this.multipleFlag;
},
"click .o_clear": function() {
this.clearFlag = !this.clearFlag;
},
"click .clear-log": function() {
this.$log[0].innerHTML = "";
}
},
custom_events: {
remove_message: "_onRemoveMessage"
},
init: function(parent) {
this._super(parent);
this.widgets = {};
this.isInDom = false;
this.messageCount = 0;
this.multipleFlag = false;
this.clearFlag = false;
},
start: function() {
this.$content = this.$(".content");
this.$msgCount = this.$(".message_count");
this.$log = this.$(".log-content");
this.log("Benchmarking odoo widgets, 12.0");
},
on_attach_callback: function() {
this.isInDom = true;
for (let widget of Object.values(this.widgets)) {
if (widget.on_attach_callback) {
widget.on_attach_callback();
}
}
},
on_detach_callback: function() {
this.isInDom = true;
for (let widget of Object.values(this.widgets)) {
if (widget.on_detach_callback) {
widget.on_detach_callback();
}
}
},
updateMessageCount() {
this.$msgCount.text("Number of msg: " + this.messageCount);
},
addMessages: function(n) {
const self = this;
this.benchmark("add " + n, () => {
const defs = [];
const messages = buildData(n);
for (let message of messages) {
const widget = new Message(this, message);
this.widgets[message.id] = widget;
defs.push(widget.appendTo("<div>"));
}
return $.when
.apply($, defs)
.then(function() {
for (let message of messages) {
let widget = self.widgets[message.id];
dom.append(self.$content, widget.$el, {
in_DOM: this.isInDom,
callbacks: [{ widget: widget }]
});
}
})
.then(function() {
self.messageCount += n;
self.updateMessageCount();
});
});
},
clear: function() {
startMeasure("clear");
this._clear();
stopMeasure(info => {
this.log(info.msg);
});
},
_clear: function() {
this.$content.empty();
for (let key in this.widgets) {
this.widgets[key].destroy();
delete this.widgets[key];
}
this.messageCount = 0;
this.updateMessageCount();
},
updateSomeMessages: function() {
this.benchmark("update every 10th", () => {
const widgets = Object.values(this.widgets);
for (let i = 0; i < widgets.length; i += 10) {
widgets[i].update();
}
});
},
_onRemoveMessage: function(ev) {
startMeasure("remove message");
ev.target.destroy();
delete this.widgets[ev.data.id];
this.messageCount--;
this.updateMessageCount();
stopMeasure();
},
benchmark: function(message, fn, callback) {
if (this.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.clearFlag) {
this._benchmark("clear", this._clear.bind(this), finalize, false);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
},
_benchmark: function(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
const benchmark = fn();
(benchmark && benchmark.then ? benchmark : $.when()).then(() => {
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
});
},
log: function(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.$log[0].appendChild(div);
this.$log[0].scrollTop = this.$log[0].scrollHeight;
}
});
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
// prepare QWeb
const templates = await fetch("templates.xml");
let strTemplates = await templates.text();
strTemplates = strTemplates.replace(/<!--[\s\S]*?-->/g, "");
core.qweb.add_template(strTemplates);
// prepare app
const app = new App();
await app.appendTo(document.body);
}
start();
});
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Odoo Widget Benchmark (12.0)</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='web.assets_common.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
@@ -1,47 +0,0 @@
<templates>
<div class="main" t-name="root">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button class="o_btn_msg 100">Add 100 messages</button>
<button class="o_btn_msg 1000">Add 1k messages</button>
<button class="o_btn_msg 10000">Add 10k messages</button>
<button class="o_btn_msg 30000">Add 30k messages</button>
<button class="updateSomeMessages">Update every 10th message</button>
<button class="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" class="o_multiple" id="multipleflag" />
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" class="o_clear" id="clearFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info message_count">
Number of messages: 0
</div>
<div class="title">Log <span class="clear-log">(clear)</span></div>
<div class="log">
<div class="log-content"/>
</div>
</div>
<div class="right-thing">
<div class="content">
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="widget.author"/></span>
<span class="msg"><t t-esc="widget.msg"/></span>
<button class="remove">Remove</button>
</div>
<div t-name="counter">
<button class="o_increment"></button>
</div>
</templates>
File diff suppressed because one or more lines are too long
-301
View File
@@ -1,301 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
odoo.define("app", function(require) {
const Widget = require("web.Widget");
var core = require("web.core");
var dom = require("web.dom");
//----------------------------------------------------------------------------
// Likes Counter Widget
//----------------------------------------------------------------------------
var Counter = Widget.extend({
template: "counter",
events: {
"click .o_increment": "_onIncrement"
},
init: function(parent) {
this._super(parent);
this.value = 0;
},
start: function() {
this.updateCounter();
},
on_attach_callback: function() {},
on_detach_callback: function() {},
updateCounter: function() {
this.$(".o_increment").html("Value: " + this.value);
},
_onIncrement: function(ev) {
ev.stopPropagation();
this.value += 1;
this.updateCounter();
}
});
//----------------------------------------------------------------------------
// Message Widget
//----------------------------------------------------------------------------
var Message = Widget.extend({
template: "message",
events: {
"click .remove": "_onRemove"
},
init: function(parent, message) {
this._super(parent);
this.author = message.author;
this.msg = message.msg;
this.id = message.id;
},
willStart: function() {
this.counter = new Counter(this);
return this.counter.appendTo($("<div>"));
},
start: function() {
this.$msg = this.$(".msg");
dom.append(this.$el, this.counter.$el, {
in_DOM: this.isInDom,
callbacks: [{ widget: this.counter }]
});
},
on_attach_callback: function() {
this.isInDom = true;
if (this.counter) {
this.counter.on_attach_callback();
}
},
on_detach_callback: function() {
this.isInDom = true;
if (this.counter) {
this.counter.on_detach_callback();
}
},
_onRemove: function() {
this.trigger_up("remove_message", { id: this.id });
},
update: function() {
this.msg += "!!!";
this.$msg.text(this.msg);
}
});
//----------------------------------------------------------------------------
// Root Widget
//----------------------------------------------------------------------------
var App = Widget.extend({
template: "root",
events: {
"click .o_btn_msg.100": function() {
this.addMessages(100);
},
"click .o_btn_msg.1000": function() {
this.addMessages(1000);
},
"click .o_btn_msg.10000": function() {
this.addMessages(10000);
},
"click .o_btn_msg.30000": function() {
this.addMessages(30000);
},
"click .updateSomeMessages": function() {
this.updateSomeMessages();
},
"click .clear": function() {
this.clear();
},
"click .o_multiple": function() {
this.multipleFlag = !this.multipleFlag;
},
"click .o_clear": function() {
this.clearFlag = !this.clearFlag;
},
"click .clear-log": function() {
this.$log[0].innerHTML = "";
}
},
custom_events: {
remove_message: "_onRemoveMessage"
},
init: function(parent) {
this._super(parent);
this.widgets = {};
this.isInDom = false;
this.messageCount = 0;
this.multipleFlag = false;
this.clearFlag = false;
},
start: function() {
this.$content = this.$(".content");
this.$msgCount = this.$(".message_count");
this.$log = this.$(".log-content");
this.log("Benchmarking odoo widgets, 12.3");
},
on_attach_callback: function() {
this.isInDom = true;
for (let widget of Object.values(this.widgets)) {
if (widget.on_attach_callback) {
widget.on_attach_callback();
}
}
},
on_detach_callback: function() {
this.isInDom = true;
for (let widget of Object.values(this.widgets)) {
if (widget.on_detach_callback) {
widget.on_detach_callback();
}
}
},
updateMessageCount() {
this.$msgCount.text("Number of msg: " + this.messageCount);
},
addMessages: function(n) {
const self = this;
this.benchmark("add " + n, () => {
const defs = [];
const messages = buildData(n);
for (let message of messages) {
const widget = new Message(this, message);
this.widgets[message.id] = widget;
defs.push(widget.appendTo("<div>"));
}
return $.when
.apply($, defs)
.then(function() {
for (let message of messages) {
let widget = self.widgets[message.id];
dom.append(self.$content, widget.$el, {
in_DOM: this.isInDom,
callbacks: [{ widget: widget }]
});
}
})
.then(function() {
self.messageCount += n;
self.updateMessageCount();
});
});
},
clear: function() {
startMeasure("clear");
this._clear();
stopMeasure(info => {
this.log(info.msg);
});
},
_clear: function() {
this.$content.empty();
for (let key in this.widgets) {
this.widgets[key].destroy();
delete this.widgets[key];
}
this.messageCount = 0;
this.updateMessageCount();
},
updateSomeMessages: function() {
this.benchmark("update every 10th", () => {
const widgets = Object.values(this.widgets);
for (let i = 0; i < widgets.length; i += 10) {
widgets[i].update();
}
});
},
_onRemoveMessage: function(ev) {
startMeasure("remove message");
ev.target.destroy();
delete this.widgets[ev.data.id];
this.messageCount--;
this.updateMessageCount();
stopMeasure();
},
benchmark: function(message, fn, callback) {
if (this.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.clearFlag) {
this._benchmark("clear", this._clear.bind(this), finalize, false);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
},
_benchmark: function(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
const benchmark = fn();
(benchmark && benchmark.then ? benchmark : $.when()).then(() => {
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
});
},
log: function(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.$log[0].appendChild(div);
this.$log[0].scrollTop = this.$log[0].scrollHeight;
}
});
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
// prepare QWeb
const templates = await fetch("templates.xml");
let strTemplates = await templates.text();
strTemplates = strTemplates.replace(/<!--[\s\S]*?-->/g, "");
core.qweb.add_template(strTemplates);
// prepare app
const app = new App();
await app.appendTo(document.body);
}
start();
});
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Odoo Widget Benchmark (12.3)</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='web.assets_common.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
@@ -1,47 +0,0 @@
<templates>
<div class="main" t-name="root">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button class="o_btn_msg 100">Add 100 messages</button>
<button class="o_btn_msg 1000">Add 1k messages</button>
<button class="o_btn_msg 10000">Add 10k messages</button>
<button class="o_btn_msg 30000">Add 30k messages</button>
<button class="updateSomeMessages">Update every 10th message</button>
<button class="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" class="o_multiple" id="multipleflag" />
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" class="o_clear" id="clearFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info message_count">
Number of messages: 0
</div>
<div class="title">Log <span class="clear-log">(clear)</span></div>
<div class="log">
<div class="log-content"/>
</div>
</div>
<div class="right-thing">
<div class="content">
</div>
</div>
</div>
<div t-name="message" class="message">
<span class="author"><t t-esc="widget.author"/></span>
<span class="msg"><t t-esc="widget.msg"/></span>
<button class="remove">Remove</button>
</div>
<div t-name="counter">
<button class="o_increment"></button>
</div>
</templates>
File diff suppressed because one or more lines are too long
-170
View File
@@ -1,170 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.11.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="'log'"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.author"/></span>
<span class="msg"><t t-esc="props.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-161
View File
@@ -1,161 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.13.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-161
View File
@@ -1,161 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.15.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<t t-widget="Message" t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<t t-widget="Counter"/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-161
View File
@@ -1,161 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
owl.Observer.set(messages, i, msg);
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.17.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-161
View File
@@ -1,161 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
components = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.18.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<div id='main'></div>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-on-change="toggleMultiple" t-att-checked="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-on-change="toggleClear" t-att-checked="state.clearAfterFlag"/>
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-151
View File
@@ -1,151 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
Message.components = { Counter };
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
}
App.components = { Message };
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL 0.21.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-model="multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-155
View File
@@ -1,155 +0,0 @@
import { buildData, startMeasure, stopMeasure, formatNumber } from "../shared/utils.js";
const { useState, useRef } = owl.hooks;
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = useState({ counter: 0 });
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
static components = { Counter };
shouldUpdate(nextProps) {
return nextProps.message !== this.props.message;
}
removeMessage() {
this.trigger("remove-message", {
id: this.props.message.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
static components = { Message };
state = useState({ messages: [], multipleFlag: false, clearAfterFlag: false });
logRef = useRef("log");
mounted() {
this.log(`Benchmarking Owl v${owl.__info__.version} (build date: ${owl.__info__.date})`);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
messages[i] = msg;
}
});
}
removeMessage(event) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === event.detail.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.logRef.el.appendChild(div);
this.logRef.el.scrollTop = this.logRef.el.scrollHeight;
}
clearLog() {
this.logRef.el.innerHTML = "";
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadFile("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OWL v0.24.0 Benchmark</title>
<link href="../shared/main.css" rel="stylesheet"/>
<script src='owl.js'></script>
</head>
<body>
<script src='app.js' type="module"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
<templates>
<div t-name="App" class="main">
<div class="left-thing">
<div class="title">Actions</div>
<div class="panel">
<button t-on-click="addMessages(100)">Add 100 messages</button>
<button t-on-click="addMessages(1000)">Add 1k messages</button>
<button t-on-click="addMessages(10000)">Add 10k messages</button>
<button t-on-click="addMessages(30000)">Add 30k messages</button>
<button t-on-click="updateSomeMessages">Update every 10th messages</button>
<button t-on-click="clear">Clear</button>
</div>
<div class="flags">
<div>
<input type="checkbox" id="multipleflag" t-model="state.multipleFlag"/>
<label for="multipleflag">Do it 20x</label>
</div>
<div>
<input type="checkbox" id="clearFlag" t-model="state.clearAfterFlag" />
<label for="clearFlag">Clear after</label>
</div>
</div>
<div class="info">Number of messages: <t t-esc="state.messages.length"/></div>
<hr/>
<div class="title">Log <span class="clear-log" t-on-click="clearLog">(clear)</span></div>
<div class="log">
<div class="log-content" t-ref="log"/>
</div>
</div>
<div class="right-thing">
<div class="content" t-on-remove-message="removeMessage">
<t t-foreach="state.messages" t-as="message">
<Message t-key="message.id" message="message"/>
</t>
</div>
</div>
</div>
<div t-name="Message" class="message">
<span class="author"><t t-esc="props.message.author"/></span>
<span class="msg"><t t-esc="props.message.msg"/></span>
<button class="remove" t-on-click="removeMessage">Remove</button>
<Counter/>
</div>
<div t-name="Counter">
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
</div>
</templates>
-173
View File
@@ -1,173 +0,0 @@
import {
buildData,
startMeasure,
stopMeasure,
formatNumber
} from "../shared/utils.js";
//------------------------------------------------------------------------------
// Likes Counter Widget
//------------------------------------------------------------------------------
class Counter extends owl.Component {
state = { counter: 0 };
template = "Counter";
increment() {
this.state.counter++;
}
}
//------------------------------------------------------------------------------
// Message Widget
//------------------------------------------------------------------------------
class Message extends owl.Component {
widgets = { Counter };
template = "Message";
shouldUpdate(nextProps) {
return nextProps !== this.props;
}
removeMessage() {
this.trigger("remove_message", {
id: this.props.id
});
}
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
class App extends owl.Component {
widgets = { Message };
state = { messages: [], multipleFlag: false, clearAfterFlag: false };
template = "App";
mounted() {
this.log(
`Benchmarking Owl v${owl._version} (build date: ${
owl._date
})`
);
}
benchmark(message, fn, callback) {
if (this.state.multipleFlag) {
const N = 20;
let n = N;
let total = 0;
let cb = info => {
let finalize = () => {
n--;
total += info.delta;
if (n === 0) {
const avg = total / N;
this.log(`Average: ${formatNumber(avg)}ms`, true);
if (callback) {
callback();
}
} else {
this._benchmark(message, fn, cb);
}
};
if (this.state.clearAfterFlag) {
this._benchmark(
"clear",
() => {
this.state.messages = [];
},
finalize,
false
);
} else {
finalize();
}
};
this._benchmark(message, fn, cb);
} else {
this._benchmark(message, fn, callback);
}
}
_benchmark(message, fn, cb, log = true) {
setTimeout(() => {
startMeasure(message);
fn();
stopMeasure(info => {
if (log) {
this.log(info.msg);
}
if (cb) {
cb(info);
}
});
}, 10);
}
addMessages(n) {
this.benchmark("add " + n, () => {
const newMessages = buildData(n);
this.state.messages.push.apply(this.state.messages, newMessages);
});
}
clear() {
this._benchmark("clear", () => {
this.state.messages = [];
});
}
updateSomeMessages() {
this.benchmark("update every 10th", () => {
const messages = this.state.messages;
for (let i = 0; i < this.state.messages.length; i += 10) {
const msg = Object.assign({}, messages[i]);
msg.author += "!!!";
this.set(messages, i, msg);
}
});
}
removeMessage(data) {
this.benchmark("remove message", () => {
const index = this.state.messages.findIndex(m => m.id === data.id);
this.state.messages.splice(index, 1);
});
}
log(str, isBold) {
const div = document.createElement("div");
if (isBold) {
div.classList.add("bold");
}
div.textContent = `> ${str}`;
this.refs.log.appendChild(div);
this.refs.log.scrollTop = this.refs.log.scrollHeight;
}
clearLog() {
this.refs.log.innerHTML = "";
}
toggleMultiple() {
this.state.multipleFlag = !this.state.multipleFlag;
}
toggleClear() {
this.state.clearAfterFlag = !this.state.clearAfterFlag;
}
}
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
const templates = await owl.utils.loadTemplates("templates.xml");
const env = {
qweb: new owl.QWeb(templates)
};
const app = new App(env);
app.mount(document.body);
}
start();

Some files were not shown because too many files have changed in this diff Show More