From 5382e824b1cbef079db9a6caf4a8f5deb8b892ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sun, 14 Jul 2019 22:18:00 +0200 Subject: [PATCH] [REF] owl: reorganize src code in sub files --- doc/readme.md | 23 + package.json | 6 +- src/{ => component}/component.ts | 10 +- .../directive.ts} | 382 +----------- src/component/props_validation.ts | 96 +++ src/{ => core}/event_bus.ts | 0 src/{ => core}/observer.ts | 0 src/index.ts | 27 +- .../base_directives.ts} | 18 +- src/qweb/context.ts | 167 ++++++ .../expression_parser.ts} | 0 src/qweb/extensions.ts | 284 +++++++++ src/qweb/index.ts | 4 + src/{qweb_core.ts => qweb/qweb.ts} | 182 +----- src/store/connected_component.ts | 139 +++++ src/{ => store}/store.ts | 144 +---- src/vdom/index.ts | 9 + src/vdom/modules.ts | 234 ++++++++ src/{ => vdom}/vdom.ts | 272 +-------- tests/__snapshots__/animations.test.ts.snap | 22 +- tests/animations.test.ts | 4 +- .../__snapshots__/component.test.ts.snap | 130 ++-- .../props_validation.test.ts.snap | 4 +- tests/{ => component}/component.test.ts | 8 +- .../{ => component}/props_validation.test.ts | 22 +- tests/{ => core}/event_bus.test.ts | 2 +- tests/{ => core}/observer.test.ts | 4 +- tests/helpers.ts | 15 +- .../__snapshots__/qweb.test.ts.snap | 223 +++---- tests/{ => qweb}/qweb.test.ts | 4 +- tests/{ => qweb}/qweb_expressions.test.ts | 2 +- .../connected_component.test.ts.snap} | 0 .../connected_component.test.ts} | 556 +----------------- tests/store/store.test.ts | 549 +++++++++++++++++ tests/vdom.test.ts | 3 +- tsconfig.json | 4 +- 36 files changed, 1802 insertions(+), 1747 deletions(-) rename src/{ => component}/component.ts (98%) rename src/{qweb_extensions.ts => component/directive.ts} (59%) create mode 100644 src/component/props_validation.ts rename src/{ => core}/event_bus.ts (100%) rename src/{ => core}/observer.ts (100%) rename src/{qweb_directives.ts => qweb/base_directives.ts} (94%) create mode 100644 src/qweb/context.ts rename src/{qweb_expressions.ts => qweb/expression_parser.ts} (100%) create mode 100644 src/qweb/extensions.ts create mode 100644 src/qweb/index.ts rename src/{qweb_core.ts => qweb/qweb.ts} (80%) create mode 100644 src/store/connected_component.ts rename src/{ => store}/store.ts (55%) create mode 100644 src/vdom/index.ts create mode 100644 src/vdom/modules.ts rename src/{ => vdom}/vdom.ts (73%) rename tests/{ => component}/__snapshots__/component.test.ts.snap (96%) rename tests/{ => component}/__snapshots__/props_validation.test.ts.snap (96%) rename tests/{ => component}/component.test.ts (99%) rename tests/{ => component}/props_validation.test.ts (92%) rename tests/{ => core}/event_bus.test.ts (95%) rename tests/{ => core}/observer.test.ts (99%) rename tests/{ => qweb}/__snapshots__/qweb.test.ts.snap (93%) rename tests/{ => qweb}/qweb.test.ts (99%) rename tests/{ => qweb}/qweb_expressions.test.ts (98%) rename tests/{__snapshots__/store.test.ts.snap => store/__snapshots__/connected_component.test.ts.snap} (100%) rename tests/{store.test.ts => store/connected_component.test.ts} (60%) create mode 100644 tests/store/store.test.ts diff --git a/doc/readme.md b/doc/readme.md index 3dc64b45..9bfa11a9 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -1,5 +1,28 @@ # 🦉 OWL Documentation 🦉 +## Owl Content + +Owl is a javascript library that contains some core classes and function to help +build applications. Here is a complete representation of its content: + +``` +owl + Component + QWeb + core + EventBus + Observer + store + Store + ConnectedComponent + utils + whenReady + loadJS + loadTemplates + escape + debounce +``` + ## Reference - [Animations](animations.md) diff --git a/package.json b/package.json index 0fdd2834..6b08f6fb 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "cpx": "^1.5.0", "git-rev-sync": "^1.12.0", "jest": "^23.6.0", + "jest-environment-jsdom": "^24.7.1", "live-server": "^1.2.1", "npm-run-all": "^4.1.5", "rollup": "^1.6.0", @@ -38,8 +39,7 @@ "source-map-support": "^0.5.10", "ts-jest": "^23.10.5", "typescript": "^3.2.2", - "uglify-es": "^3.3.9", - "jest-environment-jsdom": "^24.7.1" + "uglify-es": "^3.3.9" }, "dependencies": {}, "jest": { @@ -62,6 +62,6 @@ ] }, "prettier": { - "printWidth": 100 + "printWidth": 100 } } diff --git a/src/component.ts b/src/component/component.ts similarity index 98% rename from src/component.ts rename to src/component/component.ts index f12cd5e4..1d48c5f4 100644 --- a/src/component.ts +++ b/src/component/component.ts @@ -1,6 +1,8 @@ -import { Observer } from "./observer"; -import { QWeb, CompiledTemplate, UTILS } from "./qweb_core"; -import { h, patch, VNode } from "./vdom"; +import { Observer } from "../core/observer"; +import { CompiledTemplate, QWeb } from "../qweb/index"; +import { h, patch, VNode } from "../vdom/index"; +import "./directive"; +import "./props_validation"; /** * Owl Component System @@ -135,7 +137,7 @@ export class Component { if (QWeb.dev) { // we only validate props for root widgets here. "Regular" widget // props are validated by the t-component directive - UTILS.validateProps(this.constructor, this.props); + QWeb.utils.validateProps(this.constructor, this.props); } this.env.qweb.on("update", this, () => { if (this.__owl__.isMounted) { diff --git a/src/qweb_extensions.ts b/src/component/directive.ts similarity index 59% rename from src/qweb_extensions.ts rename to src/component/directive.ts index aecb5d14..0e2830ad 100644 --- a/src/qweb_extensions.ts +++ b/src/component/directive.ts @@ -1,186 +1,5 @@ -import { QWeb, UTILS } from "./qweb_core"; -import { VNode } from "./vdom"; - -/** - * Owl QWeb Extensions - * - * This file contains the implementation of non standard QWeb directives, added - * by Owl and that will only work on Owl projects: - * - * - t-on - * - t-ref - * - t-transition - * - t-component/t-keepalive - * - t-mounted - * - t-slot - * - t-model - */ - -//------------------------------------------------------------------------------ -// t-on -//------------------------------------------------------------------------------ -// these are pieces of code that will be injected into the event handler if -// modifiers are specified -const MODS_CODE = { - prevent: "e.preventDefault();", - self: "if (e.target !== this.elm) {return}", - stop: "e.stopPropagation();" -}; - -QWeb.addDirective({ - name: "on", - priority: 90, - atNodeCreation({ ctx, fullName, value, nodeID }) { - ctx.rootContext.shouldDefineOwner = true; - const [eventName, ...mods] = fullName.slice(5).split("."); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handlerName = value.replace(/\(.*\)/, function(args) { - extraArgs = args.slice(1, -1); - return ""; - }); - ctx.addIf(`!context['${handlerName}']`); - ctx.addLine( - `throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace( - /`/g, - "'" - )}'\`)` - ); - ctx.closeIf(); - let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner"; - let handler; - if (mods.length > 0) { - handler = `function (e) {`; - handler += mods - .map(function(mod) { - return MODS_CODE[mod]; - }) - .join(""); - handler += `context['${handlerName}'].call(${params}, e);}`; - } else { - handler = `context['${handlerName}'].bind(${params})`; - } - if (extraArgs) { - ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); - } else { - ctx.addLine( - `extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};` - ); - ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); - } - } -}); - -//------------------------------------------------------------------------------ -// t-ref -//------------------------------------------------------------------------------ -QWeb.addDirective({ - name: "ref", - priority: 95, - atNodeCreation({ ctx, value, addNodeHook }) { - const refKey = `ref${ctx.generateID()}`; - ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); - addNodeHook("create", `context.refs[${refKey}] = n.elm;`); - } -}); - -//------------------------------------------------------------------------------ -// t-transition -//------------------------------------------------------------------------------ -UTILS.nextFrame = function(cb: () => void) { - requestAnimationFrame(() => requestAnimationFrame(cb)); -}; - -UTILS.transitionInsert = function(vn: VNode, name: string) { - const elm = vn.elm; - // remove potential duplicated vnode that is currently being removed, to - // prevent from having twice the same node in the DOM during an animation - const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`); - if (dup) { - dup.remove(); - } - - elm.classList.add(name + "-enter"); - elm.classList.add(name + "-enter-active"); - const finalize = () => { - elm.classList.remove(name + "-enter-active"); - elm.classList.remove(name + "-enter-to"); - }; - this.nextFrame(() => { - elm.classList.remove(name + "-enter"); - elm.classList.add(name + "-enter-to"); - whenTransitionEnd(elm, finalize); - }); -}; - -UTILS.transitionRemove = function(vn: VNode, name: string, rm: () => void) { - const elm = vn.elm; - elm.setAttribute("data-owl-key", vn.key!); - - elm.classList.add(name + "-leave"); - elm.classList.add(name + "-leave-active"); - const finalize = () => { - elm.classList.remove(name + "-leave-active"); - elm.classList.remove(name + "-leave-to"); - rm(); - }; - this.nextFrame(() => { - elm.classList.remove(name + "-leave"); - elm.classList.add(name + "-leave-to"); - whenTransitionEnd(elm, finalize); - }); -}; - -function getTimeout(delays: Array, durations: Array): number { - /* istanbul ignore next */ - while (delays.length < durations.length) { - delays = delays.concat(delays); - } - - return Math.max.apply( - null, - durations.map((d, i) => { - return toMs(d) + toMs(delays[i]); - }) - ); -} - -// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers -// in a locale-dependent way, using a comma instead of a dot. -// If comma is not replaced with a dot, the input will be rounded down (i.e. acting -// as a floor function) causing unexpected behaviors -function toMs(s: string): number { - return Number(s.slice(0, -1).replace(",", ".")) * 1000; -} - -function whenTransitionEnd(elm: HTMLElement, cb) { - const styles = window.getComputedStyle(elm); - const delays: Array = (styles.transitionDelay || "").split(", "); - const durations: Array = (styles.transitionDuration || "").split(", "); - const timeout: number = getTimeout(delays, durations); - if (timeout > 0) { - elm.addEventListener("transitionend", cb, { once: true }); - } else { - cb(); - } -} - -QWeb.addDirective({ - name: "transition", - priority: 96, - atNodeCreation({ value, addNodeHook }) { - let name = value; - const hooks = { - insert: `this.utils.transitionInsert(vn, '${name}');`, - remove: `this.utils.transitionRemove(vn, '${name}', rm);` - }; - for (let hookName in hooks) { - addNodeHook(hookName, hooks[hookName]); - } - } -}); +import { QWeb } from "../qweb/index"; +import { MODS_CODE } from "../qweb/extensions"; //------------------------------------------------------------------------------ // t-component @@ -190,7 +9,7 @@ const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, { self: "if (e.target !== vn.elm) {return}" }); -UTILS.defineProxy = function defineProxy(target, source) { +QWeb.utils.defineProxy = function defineProxy(target, source) { for (let k in source) { Object.defineProperty(target, k, { get() { @@ -673,198 +492,3 @@ QWeb.addDirective({ return true; } }); - -//------------------------------------------------------------------------------ -// Prop validation helper -//------------------------------------------------------------------------------ - -/** - * Validate the component props (or next props) against the (static) props - * description. This is potentially an expensive operation: it may needs to - * visit recursively the props and all the children to check if they are valid. - * This is why it is only done in 'dev' mode. - */ -UTILS.validateProps = function(Widget, props: Object) { - const propsDef = (Widget).props; - if (propsDef instanceof Array) { - // list of strings (prop names) - for (let i = 0, l = propsDef.length; i < l; i++) { - const propName = propsDef[i]; - if (propName[propName.length - 1] === "?") { - // optional prop - break; - } - if (!props[propName]) { - throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`); - } - } - for (let key in props) { - if (!propsDef.includes(key) && !propsDef.includes(key + "?")) { - throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`); - } - } - } else if (propsDef) { - // propsDef is an object now - for (let propName in propsDef) { - if (props[propName] === undefined) { - if (propsDef[propName] && !propsDef[propName].optional) { - throw new Error(`Missing props '${propName}' (component '${Widget.name}')`); - } else { - break; - } - } - let isValid = isValidProp(props[propName], propsDef[propName]); - if (!isValid) { - throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`); - } - } - for (let propName in props) { - if (!(propName in propsDef)) { - throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`); - } - } - } -}; - -/** - * Check if an invidual prop value matches its (static) prop definition - */ -function isValidProp(prop, propDef): boolean { - if (propDef === true) { - return true; - } - if (typeof propDef === "function") { - // Check if a value is constructed by some Constructor. Note that there is a - // slight abuse of language: we want to consider primitive values as well. - // - // So, even though 1 is not an instance of Number, we want to consider that - // it is valid. - if (typeof prop === "object") { - return prop instanceof propDef; - } - return typeof prop === propDef.name.toLowerCase(); - } else if (propDef instanceof Array) { - // If this code is executed, this means that we want to check if a prop - // matches at least one of its descriptor. - let result = false; - for (let i = 0, iLen = propDef.length; i < iLen; i++) { - result = result || isValidProp(prop, propDef[i]); - } - return result; - } - // propsDef is an object - let result = isValidProp(prop, propDef.type); - if (propDef.type === Array) { - for (let i = 0, iLen = prop.length; i < iLen; i++) { - result = result && isValidProp(prop[i], propDef.element); - } - } - if (propDef.type === Object) { - const shape = propDef.shape; - for (let key in shape) { - result = result && isValidProp(prop[key], shape[key]); - } - } - return result; -} - -//------------------------------------------------------------------------------ -// t-mounted -//------------------------------------------------------------------------------ -QWeb.addDirective({ - name: "mounted", - priority: 97, - atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) { - ctx.rootContext.shouldDefineOwner = true; - const eventName = fullName.slice(5); - if (!eventName) { - throw new Error("Missing event name with t-on directive"); - } - let extraArgs; - let handler = value.replace(/\(.*\)/, function(args) { - extraArgs = args.slice(1, -1); - return ""; - }); - let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace( - /`/g, - "'" - )}'\`)})()`; - if (extraArgs) { - ctx.addLine( - `extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression( - extraArgs - )});` - ); - } else { - ctx.addLine( - `extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);` - ); - } - addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`); - } -}); - -//------------------------------------------------------------------------------ -// t-slot -//------------------------------------------------------------------------------ -QWeb.addDirective({ - name: "slot", - priority: 80, - atNodeEncounter({ ctx, value }): boolean { - const slotKey = ctx.generateID(); - ctx.rootContext.shouldDefineOwner = true; - ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`); - ctx.addIf(`slot${slotKey}`); - ctx.addLine( - `slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ - ctx.parentNode - }, vars: extra.vars, parent: owner}));` - ); - ctx.closeIf(); - return true; - } -}); - -//------------------------------------------------------------------------------ -// t-model -//------------------------------------------------------------------------------ -UTILS.toNumber = function(val: string): number | string { - const n = parseFloat(val); - return isNaN(n) ? val : n; -}; - -QWeb.addDirective({ - name: "model", - priority: 42, - atNodeCreation({ ctx, nodeID, value, node, fullName }) { - const type = node.getAttribute("type"); - let handler; - let event = fullName.includes(".lazy") ? "change" : "input"; - if (node.tagName === "select") { - ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`); - event = "change"; - handler = `(ev) => {context.state['${value}'] = ev.target.value}`; - } else if (type === "checkbox") { - ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`); - handler = `(ev) => {context.state['${value}'] = ev.target.checked}`; - } else if (type === "radio") { - const nodeValue = node.getAttribute("value")!; - ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`); - handler = `(ev) => {context.state['${value}'] = ev.target.value}`; - event = "click"; - } else { - ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`); - const trimCode = fullName.includes(".trim") ? ".trim()" : ""; - let valueCode = `ev.target.value${trimCode}`; - if (fullName.includes(".number")) { - ctx.rootContext.shouldDefineUtils = true; - valueCode = `utils.toNumber(${valueCode})`; - } - handler = `(ev) => {context.state['${value}'] = ${valueCode}}`; - } - ctx.addLine( - `extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});` - ); - ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`); - } -}); diff --git a/src/component/props_validation.ts b/src/component/props_validation.ts new file mode 100644 index 00000000..b073a8db --- /dev/null +++ b/src/component/props_validation.ts @@ -0,0 +1,96 @@ +import { QWeb } from "../qweb/index"; + +//------------------------------------------------------------------------------ +// Prop validation helper +//------------------------------------------------------------------------------ + +/** + * Validate the component props (or next props) against the (static) props + * description. This is potentially an expensive operation: it may needs to + * visit recursively the props and all the children to check if they are valid. + * This is why it is only done in 'dev' mode. + */ + +QWeb.utils.validateProps = function(Widget, props: Object) { + const propsDef = (Widget).props; + if (propsDef instanceof Array) { + // list of strings (prop names) + for (let i = 0, l = propsDef.length; i < l; i++) { + const propName = propsDef[i]; + if (propName[propName.length - 1] === "?") { + // optional prop + break; + } + if (!props[propName]) { + throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`); + } + } + for (let key in props) { + if (!propsDef.includes(key) && !propsDef.includes(key + "?")) { + throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`); + } + } + } else if (propsDef) { + // propsDef is an object now + for (let propName in propsDef) { + if (props[propName] === undefined) { + if (propsDef[propName] && !propsDef[propName].optional) { + throw new Error(`Missing props '${propName}' (component '${Widget.name}')`); + } else { + break; + } + } + let isValid = isValidProp(props[propName], propsDef[propName]); + if (!isValid) { + throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`); + } + } + for (let propName in props) { + if (!(propName in propsDef)) { + throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`); + } + } + } +}; + +/** + * Check if an invidual prop value matches its (static) prop definition + */ +function isValidProp(prop, propDef): boolean { + if (propDef === true) { + return true; + } + if (typeof propDef === "function") { + // Check if a value is constructed by some Constructor. Note that there is a + // slight abuse of language: we want to consider primitive values as well. + // + // So, even though 1 is not an instance of Number, we want to consider that + // it is valid. + if (typeof prop === "object") { + return prop instanceof propDef; + } + return typeof prop === propDef.name.toLowerCase(); + } else if (propDef instanceof Array) { + // If this code is executed, this means that we want to check if a prop + // matches at least one of its descriptor. + let result = false; + for (let i = 0, iLen = propDef.length; i < iLen; i++) { + result = result || isValidProp(prop, propDef[i]); + } + return result; + } + // propsDef is an object + let result = isValidProp(prop, propDef.type); + if (propDef.type === Array) { + for (let i = 0, iLen = prop.length; i < iLen; i++) { + result = result && isValidProp(prop[i], propDef.element); + } + } + if (propDef.type === Object) { + const shape = propDef.shape; + for (let key in shape) { + result = result && isValidProp(prop[key], shape[key]); + } + } + return result; +} diff --git a/src/event_bus.ts b/src/core/event_bus.ts similarity index 100% rename from src/event_bus.ts rename to src/core/event_bus.ts diff --git a/src/observer.ts b/src/core/observer.ts similarity index 100% rename from src/observer.ts rename to src/core/observer.ts diff --git a/src/index.ts b/src/index.ts index 4b16f28e..80bb08e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,23 +4,19 @@ * * Note that dynamic values, such as a date or a commit hash are added by rollup */ -export { Component } from "./component"; -import { EventBus } from "./event_bus"; -import { Observer } from "./observer"; - -export const misc = {EventBus, Observer}; -// we need to import manually the extra directives so they can register -// themselves in QWeb, otherwise these files will not even be loaded. -import "./qweb_directives"; -import "./qweb_extensions"; -import { QWeb } from "./qweb_core"; -export { QWeb }; - -import { Store, ConnectedComponent } from "./store"; - -export const store = {Store, ConnectedComponent}; +import { EventBus } from "./core/event_bus"; +import { Observer } from "./core/observer"; +import { QWeb } from "./qweb/index"; +import { ConnectedComponent } from "./store/connected_component"; +import { Store } from "./store/store"; import * as _utils from "./utils"; +export { Component } from "./component/component"; +export { QWeb }; +export const core = { EventBus, Observer }; +export const store = { Store, ConnectedComponent }; +export const utils = _utils; + export const __info__ = {}; Object.defineProperty(__info__, "mode", { @@ -39,4 +35,3 @@ Object.defineProperty(__info__, "mode", { } } }); -export const utils = _utils; diff --git a/src/qweb_directives.ts b/src/qweb/base_directives.ts similarity index 94% rename from src/qweb_directives.ts rename to src/qweb/base_directives.ts index ad2ab4fb..6e303bfa 100644 --- a/src/qweb_directives.ts +++ b/src/qweb/base_directives.ts @@ -1,5 +1,6 @@ -import { Context, QWeb, UTILS } from "./qweb_core"; -import { QWebExprVar } from "./qweb_expressions"; +import { Context } from "./context"; +import { QWebExprVar } from "./expression_parser"; +import { QWeb } from "./qweb"; /** * Owl QWeb Directives @@ -18,7 +19,7 @@ import { QWebExprVar } from "./qweb_expressions"; //------------------------------------------------------------------------------ // t-esc and t-raw //------------------------------------------------------------------------------ -(UTILS).getFragment = function(str: string): DocumentFragment { +QWeb.utils.getFragment = function(str: string): DocumentFragment { const temp = document.createElement("template"); temp.innerHTML = str; return temp.content; @@ -57,7 +58,8 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) { } } else { let fragID = ctx.generateID(); - ctx.addLine(`var frag${fragID} = this.utils.getFragment(${exprID})`); + ctx.rootContext.shouldDefineUtils = true; + ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`); let tempNodeID = ctx.generateID(); ctx.addLine(`var p${tempNodeID} = {hook: {`); ctx.addLine(` insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`); @@ -255,11 +257,11 @@ QWeb.addDirective({ ctx.addLine(`var _length${keysID} = _${keysID}.length;`); ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`); ctx.indent(); - ctx.addToScope(name + '_first', 'i === 0'); - ctx.addToScope(name + '_last', `i === _length${keysID} - 1`); - ctx.addToScope(name + '_index', 'i'); + ctx.addToScope(name + "_first", "i === 0"); + ctx.addToScope(name + "_last", `i === _length${keysID} - 1`); + ctx.addToScope(name + "_index", "i"); ctx.addToScope(name, `_${keysID}[i]`); - ctx.addToScope(name + '_value', `_${valuesID}[i]`); + ctx.addToScope(name + "_value", `_${valuesID}[i]`); const nodeCopy = node.cloneNode(true); let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key"); if (!shouldWarn && node.tagName === "t") { diff --git a/src/qweb/context.ts b/src/qweb/context.ts new file mode 100644 index 00000000..21ced242 --- /dev/null +++ b/src/qweb/context.ts @@ -0,0 +1,167 @@ +import { compileExpr, QWebVar } from "./expression_parser"; + +//------------------------------------------------------------------------------ +// Compilation Context +//------------------------------------------------------------------------------ + +export class Context { + nextID: number = 1; + code: string[] = []; + variables: { [key: string]: QWebVar } = {}; + escaping: boolean = false; + parentNode: number | null = null; + parentTextNode: number | null = null; + rootNode: number | null = null; + indentLevel: number = 0; + rootContext: Context; + caller: Element | undefined; + shouldDefineOwner: boolean = false; + shouldDefineParent: boolean = false; + shouldDefineQWeb: boolean = false; + shouldDefineUtils: boolean = false; + shouldDefineResult: boolean = false; + shouldProtectContext: boolean = false; + shouldTrackScope: boolean = false; + inLoop: boolean = false; + inPreTag: boolean = false; + templateName: string; + allowMultipleRoots: boolean = false; + hasParentWidget: boolean = false; + scopeVars: any[] = []; + + constructor(name?: string) { + this.rootContext = this; + this.templateName = name || "noname"; + this.addLine("var h = this.h;"); + } + + generateID(): number { + const id = this.rootContext.nextID++; + return id; + } + + generateCode(): string[] { + const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length; + if (shouldTrackScope) { + // add some vars to scope if needed + for (let scopeVar of this.scopeVars.reverse()) { + let { index, key, indent } = scopeVar; + const prefix = new Array(indent + 2).join(" "); + this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`); + } + this.code.unshift(" const scope = Object.create(null);"); + } + if (this.shouldProtectContext) { + this.code.unshift(" context = Object.create(context);"); + } + if (this.shouldDefineResult) { + this.code.unshift(" let result;"); + } + if (this.shouldDefineOwner) { + // this is necessary to prevent some directives (t-forach for ex) to + // pollute the rendering context by adding some keys in it. + this.code.unshift(" let owner = context;"); + } + if (this.shouldDefineParent) { + if (this.hasParentWidget) { + this.code.unshift(" let parent = extra.parent;"); + } else { + this.code.unshift(" let parent = context;"); + } + } + if (this.shouldDefineQWeb) { + this.code.unshift(" let QWeb = this.constructor;"); + } + if (this.shouldDefineUtils) { + this.code.unshift(" let utils = this.constructor.utils;"); + } + return this.code; + } + + withParent(node: number): Context { + if ( + !this.allowMultipleRoots && + this === this.rootContext && + (this.parentNode || this.parentTextNode) + ) { + throw new Error("A template should not have more than one root node"); + } + if (!this.rootContext.rootNode) { + this.rootContext.rootNode = node; + } + return this.subContext("parentNode", node); + } + + subContext(key: keyof Context, value: any): Context { + const newContext = Object.create(this); + newContext[key] = value; + return newContext; + } + + indent() { + this.indentLevel++; + } + + dedent() { + this.indentLevel--; + } + + addLine(line: string): number { + const prefix = new Array(this.indentLevel + 2).join(" "); + this.code.push(prefix + line); + return this.code.length - 1; + } + + addToScope(key: string, expr: string) { + const index = this.addLine(`context.${key} = ${expr};`); + this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel }); + } + + addIf(condition: string) { + this.addLine(`if (${condition}) {`); + this.indent(); + } + + addElse() { + this.dedent(); + this.addLine("} else {"); + this.indent(); + } + + closeIf() { + this.dedent(); + this.addLine("}"); + } + + getValue(val: any): any { + return val in this.variables ? this.getValue(this.variables[val]) : val; + } + + /** + * Prepare an expression for being consumed at render time. Its main job + * is to + * - replace unknown variables by a lookup in the context + * - replace already defined variables by their internal name + */ + formatExpression(expr: string): string { + return compileExpr(expr, this.variables); + } + + /** + * Perform string interpolation on the given string. Note that if the whole + * string is an expression, it simply returns it (formatted and enclosed in + * parentheses). + * For instance: + * 'Hello {{x}}!' -> `Hello ${x}` + * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') + */ + interpolate(s: string): string { + let matches = s.match(/\{\{.*?\}\}/g); + if (matches && matches[0].length === s.length) { + return `(${this.formatExpression(s.slice(2, -2))})`; + } + + let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}"); + return "`" + r + "`"; + } +} diff --git a/src/qweb_expressions.ts b/src/qweb/expression_parser.ts similarity index 100% rename from src/qweb_expressions.ts rename to src/qweb/expression_parser.ts diff --git a/src/qweb/extensions.ts b/src/qweb/extensions.ts new file mode 100644 index 00000000..0b28b6e2 --- /dev/null +++ b/src/qweb/extensions.ts @@ -0,0 +1,284 @@ +import { VNode } from "../vdom/index"; +import { QWeb } from "./qweb"; + +/** + * Owl QWeb Extensions + * + * This file contains the implementation of non standard QWeb directives, added + * by Owl and that will only work on Owl projects: + * + * - t-on + * - t-ref + * - t-transition + * - t-mounted + * - t-slot + * - t-model + */ + +//------------------------------------------------------------------------------ +// t-on +//------------------------------------------------------------------------------ +// these are pieces of code that will be injected into the event handler if +// modifiers are specified +export const MODS_CODE = { + prevent: "e.preventDefault();", + self: "if (e.target !== this.elm) {return}", + stop: "e.stopPropagation();" +}; + +QWeb.addDirective({ + name: "on", + priority: 90, + atNodeCreation({ ctx, fullName, value, nodeID }) { + ctx.rootContext.shouldDefineOwner = true; + const [eventName, ...mods] = fullName.slice(5).split("."); + if (!eventName) { + throw new Error("Missing event name with t-on directive"); + } + let extraArgs; + let handlerName = value.replace(/\(.*\)/, function(args) { + extraArgs = args.slice(1, -1); + return ""; + }); + ctx.addIf(`!context['${handlerName}']`); + ctx.addLine( + `throw new Error('Missing handler \\'' + '${handlerName}' + \`\\' when evaluating template '${ctx.templateName.replace( + /`/g, + "'" + )}'\`)` + ); + ctx.closeIf(); + let params = extraArgs ? `owner, ${ctx.formatExpression(extraArgs)}` : "owner"; + let handler; + if (mods.length > 0) { + handler = `function (e) {`; + handler += mods + .map(function(mod) { + return MODS_CODE[mod]; + }) + .join(""); + handler += `context['${handlerName}'].call(${params}, e);}`; + } else { + handler = `context['${handlerName}'].bind(${params})`; + } + if (extraArgs) { + ctx.addLine(`p${nodeID}.on['${eventName}'] = ${handler};`); + } else { + ctx.addLine( + `extra.handlers['${eventName}' + ${nodeID}] = extra.handlers['${eventName}' + ${nodeID}] || ${handler};` + ); + ctx.addLine(`p${nodeID}.on['${eventName}'] = extra.handlers['${eventName}' + ${nodeID}];`); + } + } +}); + +//------------------------------------------------------------------------------ +// t-ref +//------------------------------------------------------------------------------ +QWeb.addDirective({ + name: "ref", + priority: 95, + atNodeCreation({ ctx, value, addNodeHook }) { + const refKey = `ref${ctx.generateID()}`; + ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`); + addNodeHook("create", `context.refs[${refKey}] = n.elm;`); + } +}); + +//------------------------------------------------------------------------------ +// t-transition +//------------------------------------------------------------------------------ +QWeb.utils.nextFrame = function(cb: () => void) { + requestAnimationFrame(() => requestAnimationFrame(cb)); +}; + +QWeb.utils.transitionInsert = function(vn: VNode, name: string) { + const elm = vn.elm; + // remove potential duplicated vnode that is currently being removed, to + // prevent from having twice the same node in the DOM during an animation + const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`); + if (dup) { + dup.remove(); + } + + elm.classList.add(name + "-enter"); + elm.classList.add(name + "-enter-active"); + const finalize = () => { + elm.classList.remove(name + "-enter-active"); + elm.classList.remove(name + "-enter-to"); + }; + this.nextFrame(() => { + elm.classList.remove(name + "-enter"); + elm.classList.add(name + "-enter-to"); + whenTransitionEnd(elm, finalize); + }); +}; + +QWeb.utils.transitionRemove = function(vn: VNode, name: string, rm: () => void) { + const elm = vn.elm; + elm.setAttribute("data-owl-key", vn.key!); + + elm.classList.add(name + "-leave"); + elm.classList.add(name + "-leave-active"); + const finalize = () => { + elm.classList.remove(name + "-leave-active"); + elm.classList.remove(name + "-leave-to"); + rm(); + }; + this.nextFrame(() => { + elm.classList.remove(name + "-leave"); + elm.classList.add(name + "-leave-to"); + whenTransitionEnd(elm, finalize); + }); +}; + +function getTimeout(delays: Array, durations: Array): number { + /* istanbul ignore next */ + while (delays.length < durations.length) { + delays = delays.concat(delays); + } + + return Math.max.apply( + null, + durations.map((d, i) => { + return toMs(d) + toMs(delays[i]); + }) + ); +} + +// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers +// in a locale-dependent way, using a comma instead of a dot. +// If comma is not replaced with a dot, the input will be rounded down (i.e. acting +// as a floor function) causing unexpected behaviors +function toMs(s: string): number { + return Number(s.slice(0, -1).replace(",", ".")) * 1000; +} + +function whenTransitionEnd(elm: HTMLElement, cb) { + const styles = window.getComputedStyle(elm); + const delays: Array = (styles.transitionDelay || "").split(", "); + const durations: Array = (styles.transitionDuration || "").split(", "); + const timeout: number = getTimeout(delays, durations); + if (timeout > 0) { + elm.addEventListener("transitionend", cb, { once: true }); + } else { + cb(); + } +} + +QWeb.addDirective({ + name: "transition", + priority: 96, + atNodeCreation({ ctx, value, addNodeHook }) { + ctx.rootContext.shouldDefineUtils = true; + let name = value; + const hooks = { + insert: `utils.transitionInsert(vn, '${name}');`, + remove: `utils.transitionRemove(vn, '${name}', rm);` + }; + for (let hookName in hooks) { + addNodeHook(hookName, hooks[hookName]); + } + } +}); + +//------------------------------------------------------------------------------ +// t-mounted +//------------------------------------------------------------------------------ +QWeb.addDirective({ + name: "mounted", + priority: 97, + atNodeCreation({ ctx, fullName, value, nodeID, addNodeHook }) { + ctx.rootContext.shouldDefineOwner = true; + const eventName = fullName.slice(5); + if (!eventName) { + throw new Error("Missing event name with t-on directive"); + } + let extraArgs; + let handler = value.replace(/\(.*\)/, function(args) { + extraArgs = args.slice(1, -1); + return ""; + }); + let error = `(function () {throw new Error('Missing handler \\'' + '${handler}' + \`\\' when evaluating template '${ctx.templateName.replace( + /`/g, + "'" + )}'\`)})()`; + if (extraArgs) { + ctx.addLine( + `extra.mountedHandlers[${nodeID}] = (context['${handler}'] || ${error}).bind(owner, ${ctx.formatExpression( + extraArgs + )});` + ); + } else { + ctx.addLine( + `extra.mountedHandlers[${nodeID}] = extra.mountedHandlers[${nodeID}] || (context['${handler}'] || ${error}).bind(owner);` + ); + } + addNodeHook("insert", `if (context.__owl__.isMounted) { extra.mountedHandlers[${nodeID}](); }`); + } +}); + +//------------------------------------------------------------------------------ +// t-slot +//------------------------------------------------------------------------------ +QWeb.addDirective({ + name: "slot", + priority: 80, + atNodeEncounter({ ctx, value }): boolean { + const slotKey = ctx.generateID(); + ctx.rootContext.shouldDefineOwner = true; + ctx.addLine(`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`); + ctx.addIf(`slot${slotKey}`); + ctx.addLine( + `slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${ + ctx.parentNode + }, vars: extra.vars, parent: owner}));` + ); + ctx.closeIf(); + return true; + } +}); + +//------------------------------------------------------------------------------ +// t-model +//------------------------------------------------------------------------------ +QWeb.utils.toNumber = function(val: string): number | string { + const n = parseFloat(val); + return isNaN(n) ? val : n; +}; + +QWeb.addDirective({ + name: "model", + priority: 42, + atNodeCreation({ ctx, nodeID, value, node, fullName }) { + const type = node.getAttribute("type"); + let handler; + let event = fullName.includes(".lazy") ? "change" : "input"; + if (node.tagName === "select") { + ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`); + event = "change"; + handler = `(ev) => {context.state['${value}'] = ev.target.value}`; + } else if (type === "checkbox") { + ctx.addLine(`p${nodeID}.props = {checked: context.state['${value}']};`); + handler = `(ev) => {context.state['${value}'] = ev.target.checked}`; + } else if (type === "radio") { + const nodeValue = node.getAttribute("value")!; + ctx.addLine(`p${nodeID}.props = {checked:context.state['${value}'] === '${nodeValue}'};`); + handler = `(ev) => {context.state['${value}'] = ev.target.value}`; + event = "click"; + } else { + ctx.addLine(`p${nodeID}.props = {value: context.state['${value}']};`); + const trimCode = fullName.includes(".trim") ? ".trim()" : ""; + let valueCode = `ev.target.value${trimCode}`; + if (fullName.includes(".number")) { + ctx.rootContext.shouldDefineUtils = true; + valueCode = `utils.toNumber(${valueCode})`; + } + handler = `(ev) => {context.state['${value}'] = ${valueCode}}`; + } + ctx.addLine( + `extra.handlers['${event}' + ${nodeID}] = extra.handlers['${event}' + ${nodeID}] || (${handler});` + ); + ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers['${event}' + ${nodeID}];`); + } +}); diff --git a/src/qweb/index.ts b/src/qweb/index.ts new file mode 100644 index 00000000..02fe6a10 --- /dev/null +++ b/src/qweb/index.ts @@ -0,0 +1,4 @@ +import "./base_directives"; +import "./extensions"; + +export { CompiledTemplate, QWeb } from "./qweb"; diff --git a/src/qweb_core.ts b/src/qweb/qweb.ts similarity index 80% rename from src/qweb_core.ts rename to src/qweb/qweb.ts index d9448c30..6c102d74 100644 --- a/src/qweb_core.ts +++ b/src/qweb/qweb.ts @@ -1,6 +1,6 @@ -import { VNode, h, patch } from "./vdom"; -import { QWebVar, compileExpr } from "./qweb_expressions"; -import { EventBus } from "./event_bus"; +import { EventBus } from "../core/event_bus"; +import { h, patch, VNode } from "../vdom/index"; +import { Context } from "./context"; /** * Owl QWeb Engine @@ -80,14 +80,12 @@ const NODE_HOOKS_PARAMS = { }; interface Utils { - h: typeof h; toObj(expr: any): Object; shallowEqual(p1: Object, p2: Object): boolean; [key: string]: any; } -export const UTILS: Utils = { - h: h, +const UTILS: Utils = { toObj(expr) { if (typeof expr === "string") { expr = expr.trim(); @@ -149,9 +147,10 @@ let nextID = 1; //------------------------------------------------------------------------------ export class QWeb extends EventBus { templates: { [name: string]: Template } = {}; - utils = UTILS; + static utils = UTILS; static components = Object.create(null); + h = h; // dev mode enables better error messages or more costly validations static dev: boolean = false; @@ -571,7 +570,8 @@ export class QWeb extends EventBus { let formattedValue = v.id || ctx.formatExpression(v); if (attName === "class") { - formattedValue = `this.utils.toObj(${formattedValue})`; + ctx.rootContext.shouldDefineUtils = true; + formattedValue = `utils.toObj(${formattedValue})`; if (classObj) { ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`); } else { @@ -673,169 +673,3 @@ export class QWeb extends EventBus { } } } - -//------------------------------------------------------------------------------ -// Compilation Context -//------------------------------------------------------------------------------ - -export class Context { - nextID: number = 1; - code: string[] = []; - variables: { [key: string]: QWebVar } = {}; - escaping: boolean = false; - parentNode: number | null = null; - parentTextNode: number | null = null; - rootNode: number | null = null; - indentLevel: number = 0; - rootContext: Context; - caller: Element | undefined; - shouldDefineOwner: boolean = false; - shouldDefineParent: boolean = false; - shouldDefineQWeb: boolean = false; - shouldDefineUtils: boolean = false; - shouldDefineResult: boolean = false; - shouldProtectContext: boolean = false; - shouldTrackScope: boolean = false; - inLoop: boolean = false; - inPreTag: boolean = false; - templateName: string; - allowMultipleRoots: boolean = false; - hasParentWidget: boolean = false; - scopeVars: any[] = []; - - constructor(name?: string) { - this.rootContext = this; - this.templateName = name || "noname"; - this.addLine("var h = this.utils.h;"); - } - - generateID(): number { - const id = this.rootContext.nextID++; - return id; - } - - generateCode(): string[] { - const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length; - if (shouldTrackScope) { - // add some vars to scope if needed - for (let scopeVar of this.scopeVars.reverse()) { - let { index, key, indent } = scopeVar; - const prefix = new Array(indent + 2).join(" "); - this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`); - } - this.code.unshift(" const scope = Object.create(null);"); - } - if (this.shouldProtectContext) { - this.code.unshift(" context = Object.create(context);"); - } - if (this.shouldDefineResult) { - this.code.unshift(" let result;"); - } - if (this.shouldDefineOwner) { - // this is necessary to prevent some directives (t-forach for ex) to - // pollute the rendering context by adding some keys in it. - this.code.unshift(" let owner = context;"); - } - if (this.shouldDefineParent) { - if (this.hasParentWidget) { - this.code.unshift(" let parent = extra.parent;"); - } else { - this.code.unshift(" let parent = context;"); - } - } - if (this.shouldDefineQWeb) { - this.code.unshift(" let QWeb = this.constructor;"); - } - if (this.shouldDefineUtils) { - this.code.unshift(" let utils = this.utils;"); - } - return this.code; - } - - withParent(node: number): Context { - if ( - !this.allowMultipleRoots && - this === this.rootContext && - (this.parentNode || this.parentTextNode) - ) { - throw new Error("A template should not have more than one root node"); - } - if (!this.rootContext.rootNode) { - this.rootContext.rootNode = node; - } - return this.subContext("parentNode", node); - } - - subContext(key: keyof Context, value: any): Context { - const newContext = Object.create(this); - newContext[key] = value; - return newContext; - } - - indent() { - this.indentLevel++; - } - - dedent() { - this.indentLevel--; - } - - addLine(line: string): number { - const prefix = new Array(this.indentLevel + 2).join(" "); - this.code.push(prefix + line); - return this.code.length - 1; - } - - addToScope(key: string, expr: string) { - const index = this.addLine(`context.${key} = ${expr};`); - this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel }); - } - - addIf(condition: string) { - this.addLine(`if (${condition}) {`); - this.indent(); - } - - addElse() { - this.dedent(); - this.addLine("} else {"); - this.indent(); - } - - closeIf() { - this.dedent(); - this.addLine("}"); - } - - getValue(val: any): any { - return val in this.variables ? this.getValue(this.variables[val]) : val; - } - - /** - * Prepare an expression for being consumed at render time. Its main job - * is to - * - replace unknown variables by a lookup in the context - * - replace already defined variables by their internal name - */ - formatExpression(expr: string): string { - return compileExpr(expr, this.variables); - } - - /** - * Perform string interpolation on the given string. Note that if the whole - * string is an expression, it simply returns it (formatted and enclosed in - * parentheses). - * For instance: - * 'Hello {{x}}!' -> `Hello ${x}` - * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') - */ - interpolate(s: string): string { - let matches = s.match(/\{\{.*?\}\}/g); - if (matches && matches[0].length === s.length) { - return `(${this.formatExpression(s.slice(2, -2))})`; - } - - let r = s.replace(/\{\{.*?\}\}/g, s => "${" + this.formatExpression(s.slice(2, -2)) + "}"); - return "`" + r + "`"; - } -} diff --git a/src/store/connected_component.ts b/src/store/connected_component.ts new file mode 100644 index 00000000..525a3e1f --- /dev/null +++ b/src/store/connected_component.ts @@ -0,0 +1,139 @@ +import { Component, Env } from "../component/component"; + +//------------------------------------------------------------------------------ +// Connect function +//------------------------------------------------------------------------------ + +function revNumber(o: T): number { + if (o !== null && typeof o === "object" && (o).__owl__) { + return (o).__owl__.rev; + } + return 0; +} + +function deepRevNumber(o: T): number { + if (o !== null && typeof o === "object" && (o).__owl__) { + return (o).__owl__.deepRev; + } + return 0; +} + +type HashFunction = (a: any, b: any) => number; + +export class ConnectedComponent extends Component { + deep: boolean = true; + getStore(env) { + return env.store; + } + + hashFunction: HashFunction = ({ storeProps }, options) => { + let refFunction = this.deep ? deepRevNumber : revNumber; + if ("__owl__" in storeProps) { + return refFunction(storeProps); + } + const { currentStoreProps } = options; + let hash = 0; + for (let key in storeProps) { + const val = storeProps[key]; + const hashVal = refFunction(val); + if (hashVal === 0) { + if (val !== currentStoreProps[key]) { + options.didChange = true; + } + } else { + hash += hashVal; + } + } + return hash; + }; + + static mapStoreToProps(storeState, ownProps, getters) { + return {}; + } + constructor(parent, props?: any) { + super(parent, props); + const store = this.getStore(this.env); + const ownProps = Object.assign({}, props || {}); + const storeProps = (this.constructor).mapStoreToProps( + store.state, + ownProps, + store.getters + ); + const mergedProps = Object.assign({}, props || {}, storeProps); + this.props = mergedProps; + (this.__owl__).ownProps = ownProps; + (this.__owl__).currentStoreProps = storeProps; + (this.__owl__).store = store; + (this.__owl__).storeHash = this.hashFunction( + { + state: store.state, + storeProps: storeProps, + revNumber, + deepRevNumber + }, + { + currentStoreProps: storeProps + } + ); + } + /** + * We do not use the mounted hook here for a subtle reason: we want the + * updates to be called for the parents before the children. However, + * if we use the mounted hook, this will be done in the reverse order. + */ + __callMounted() { + (this.__owl__).store.on("update", this, this.__checkUpdate); + super.__callMounted(); + } + willUnmount() { + (this.__owl__).store.off("update", this); + super.willUnmount(); + } + + async __checkUpdate(updateId) { + if (updateId === (this.__owl__).currentUpdateId) { + return; + } + const ownProps = (this.__owl__).ownProps; + const storeProps = (this.constructor).mapStoreToProps( + (this.__owl__).store.state, + ownProps, + (this.__owl__).store.getters + ); + const options: any = { + currentStoreProps: (this.__owl__).currentStoreProps + }; + const storeHash = this.hashFunction( + { + state: (this.__owl__).store.state, + storeProps: storeProps, + revNumber, + deepRevNumber + }, + options + ); + let didChange = options.didChange; + if (storeHash !== (this.__owl__).storeHash) { + didChange = true; + (this.__owl__).storeHash = storeHash; + } + if (didChange) { + (this.__owl__).currentStoreProps = storeProps; + await this.__updateProps(ownProps, false); + } + } + __updateProps(nextProps, forceUpdate, patchQueue?: any[]) { + const __owl__ = this.__owl__; + __owl__.currentUpdateId = __owl__.store._updateId; + if (__owl__.ownProps !== nextProps) { + __owl__.currentStoreProps = (this.constructor).mapStoreToProps( + __owl__.store.state, + nextProps, + __owl__.store.getters + ); + } + __owl__.ownProps = nextProps; + const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps); + return super.__updateProps(mergedProps, forceUpdate, patchQueue); + } +} diff --git a/src/store.ts b/src/store/store.ts similarity index 55% rename from src/store.ts rename to src/store/store.ts index db39859d..dcd58849 100644 --- a/src/store.ts +++ b/src/store/store.ts @@ -1,6 +1,6 @@ -import { Component, Env } from "./component"; -import { EventBus } from "./event_bus"; -import { Observer } from "./observer"; +import { Env } from "../component/component"; +import { EventBus } from "../core/event_bus"; +import { Observer } from "../core/observer"; /** * Owl Store @@ -166,141 +166,3 @@ export class Store extends EventBus { } } } - -//------------------------------------------------------------------------------ -// Connect function -//------------------------------------------------------------------------------ - -function revNumber(o: T): number { - if (o !== null && typeof o === "object" && (o).__owl__) { - return (o).__owl__.rev; - } - return 0; -} - -function deepRevNumber(o: T): number { - if (o !== null && typeof o === "object" && (o).__owl__) { - return (o).__owl__.deepRev; - } - return 0; -} - -type HashFunction = (a: any, b: any) => number; - -export class ConnectedComponent extends Component { - deep: boolean = true; - getStore(env) { - return env.store; - } - - hashFunction: HashFunction = ({ storeProps }, options) => { - let refFunction = this.deep ? deepRevNumber : revNumber; - if ("__owl__" in storeProps) { - return refFunction(storeProps); - } - const { currentStoreProps } = options; - let hash = 0; - for (let key in storeProps) { - const val = storeProps[key]; - const hashVal = refFunction(val); - if (hashVal === 0) { - if (val !== currentStoreProps[key]) { - options.didChange = true; - } - } else { - hash += hashVal; - } - } - return hash; - }; - - static mapStoreToProps(storeState, ownProps, getters) { - return {}; - } - constructor(parent, props?: any) { - super(parent, props); - const store = this.getStore(this.env); - const ownProps = Object.assign({}, props || {}); - const storeProps = (this.constructor).mapStoreToProps( - store.state, - ownProps, - store.getters - ); - const mergedProps = Object.assign({}, props || {}, storeProps); - this.props = mergedProps; - (this.__owl__).ownProps = ownProps; - (this.__owl__).currentStoreProps = storeProps; - (this.__owl__).store = store; - (this.__owl__).storeHash = this.hashFunction( - { - state: store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, - { - currentStoreProps: storeProps - } - ); - } - /** - * We do not use the mounted hook here for a subtle reason: we want the - * updates to be called for the parents before the children. However, - * if we use the mounted hook, this will be done in the reverse order. - */ - __callMounted() { - (this.__owl__).store.on("update", this, this.__checkUpdate); - super.__callMounted(); - } - willUnmount() { - (this.__owl__).store.off("update", this); - super.willUnmount(); - } - - async __checkUpdate(updateId) { - if (updateId === (this.__owl__).currentUpdateId) { - return; - } - const ownProps = (this.__owl__).ownProps; - const storeProps = (this.constructor).mapStoreToProps( - (this.__owl__).store.state, - ownProps, - (this.__owl__).store.getters - ); - const options: any = { - currentStoreProps: (this.__owl__).currentStoreProps - }; - const storeHash = this.hashFunction( - { - state: (this.__owl__).store.state, - storeProps: storeProps, - revNumber, - deepRevNumber - }, - options - ); - let didChange = options.didChange; - if (storeHash !== (this.__owl__).storeHash) { - didChange = true; - (this.__owl__).storeHash = storeHash; - } - if (didChange) { - (this.__owl__).currentStoreProps = storeProps; - await this.__updateProps(ownProps, false); - } - } - __updateProps(nextProps, forceUpdate, patchQueue?: any[]) { - const __owl__ = this.__owl__; - __owl__.currentUpdateId = __owl__.store._updateId; - if (__owl__.ownProps !== nextProps) { - __owl__.currentStoreProps = (this.constructor).mapStoreToProps( - __owl__.store.state, - nextProps, - __owl__.store.getters - ); - } - __owl__.ownProps = nextProps; - const mergedProps = Object.assign({}, nextProps, __owl__.currentStoreProps); - return super.__updateProps(mergedProps, forceUpdate, patchQueue); - } -} diff --git a/src/vdom/index.ts b/src/vdom/index.ts new file mode 100644 index 00000000..0824704c --- /dev/null +++ b/src/vdom/index.ts @@ -0,0 +1,9 @@ +import { attrsModule, classModule, eventListenersModule, propsModule } from "./modules"; +import { init } from "./vdom"; +//------------------------------------------------------------------------------ +// patch +//------------------------------------------------------------------------------ + +export { h, VNode } from "./vdom"; + +export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]); diff --git a/src/vdom/modules.ts b/src/vdom/modules.ts new file mode 100644 index 00000000..c887bb4e --- /dev/null +++ b/src/vdom/modules.ts @@ -0,0 +1,234 @@ +import { Module, VNode, VNodeData } from "./vdom"; + +//------------------------------------------------------------------------------ +// module/props.ts +//------------------------------------------------------------------------------ + +function updateProps(oldVnode: VNode, vnode: VNode): void { + var key: string, + cur: any, + old: any, + elm = vnode.elm, + oldProps = (oldVnode.data as VNodeData).props, + props = (vnode.data as VNodeData).props; + + if (!oldProps && !props) return; + if (oldProps === props) return; + oldProps = oldProps || {}; + props = props || {}; + + for (key in oldProps) { + if (!props[key]) { + delete (elm as any)[key]; + } + } + for (key in props) { + cur = props[key]; + old = oldProps[key]; + if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) { + (elm as any)[key] = cur; + } + } +} + +export const propsModule = { + create: updateProps, + update: updateProps +} as Module; + +//------------------------------------------------------------------------------ +// module/eventlisteners.ts +//------------------------------------------------------------------------------ + +function invokeHandler(handler: any, vnode?: VNode, event?: Event): void { + if (typeof handler === "function") { + // call function handler + handler.call(vnode, event, vnode); + } else if (typeof handler === "object") { + // call handler with arguments + if (typeof handler[0] === "function") { + // special case for single argument for performance + if (handler.length === 2) { + handler[0].call(vnode, handler[1], event, vnode); + } else { + var args = handler.slice(1); + args.push(event); + args.push(vnode); + handler[0].apply(vnode, args); + } + } else { + // call multiple handlers + for (let i = 0, iLen = handler.length; i < iLen; i++) { + invokeHandler(handler[i], vnode, event); + } + } + } +} + +function handleEvent(event: Event, vnode: VNode) { + var name = event.type, + on = (vnode.data as VNodeData).on; + + // call event handler(s) if exists + if (on && on[name]) { + invokeHandler(on[name], vnode, event); + } +} + +function createListener() { + return function handler(event: Event) { + handleEvent(event, (handler as any).vnode); + }; +} + +function updateEventListeners(oldVnode: VNode, vnode?: VNode): void { + var oldOn = (oldVnode.data as VNodeData).on, + oldListener = (oldVnode as any).listener, + oldElm: Element = oldVnode.elm as Element, + on = vnode && (vnode.data as VNodeData).on, + elm: Element = (vnode && vnode.elm) as Element, + name: string; + + // optimization for reused immutable handlers + if (oldOn === on) { + return; + } + + // remove existing listeners which no longer used + if (oldOn && oldListener) { + // if element changed or deleted we remove all existing listeners unconditionally + if (!on) { + for (name in oldOn) { + // remove listener if element was changed or existing listeners removed + oldElm.removeEventListener(name, oldListener, false); + } + } else { + for (name in oldOn) { + // remove listener if existing listener removed + if (!on[name]) { + oldElm.removeEventListener(name, oldListener, false); + } + } + } + } + + // add new listeners which has not already attached + if (on) { + // reuse existing listener or create new + var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener()); + // update vnode for listener + listener.vnode = vnode; + + // if element changed or added we add all needed listeners unconditionally + if (!oldOn) { + for (name in on) { + // add listener if element was changed or new listeners added + elm.addEventListener(name, listener, false); + } + } else { + for (name in on) { + // add listener if new listener added + if (!oldOn[name]) { + elm.addEventListener(name, listener, false); + } + } + } + } +} + +export const eventListenersModule = { + create: updateEventListeners, + update: updateEventListeners, + destroy: updateEventListeners +} as Module; + +//------------------------------------------------------------------------------ +// attributes.ts +//------------------------------------------------------------------------------ + +const xlinkNS = "http://www.w3.org/1999/xlink"; +const xmlNS = "http://www.w3.org/XML/1998/namespace"; +const colonChar = 58; +const xChar = 120; + +function updateAttrs(oldVnode: VNode, vnode: VNode): void { + var key: string, + elm: Element = vnode.elm as Element, + oldAttrs = (oldVnode.data as VNodeData).attrs, + attrs = (vnode.data as VNodeData).attrs; + + if (!oldAttrs && !attrs) return; + if (oldAttrs === attrs) return; + oldAttrs = oldAttrs || {}; + attrs = attrs || {}; + + // update modified attributes, add new attributes + for (key in attrs) { + const cur = attrs[key]; + const old = oldAttrs[key]; + if (old !== cur) { + if (cur === true) { + elm.setAttribute(key, ""); + } else if (cur === false) { + elm.removeAttribute(key); + } else { + if (key.charCodeAt(0) !== xChar) { + elm.setAttribute(key, cur); + } else if (key.charCodeAt(3) === colonChar) { + // Assume xml namespace + elm.setAttributeNS(xmlNS, key, cur); + } else if (key.charCodeAt(5) === colonChar) { + // Assume xlink namespace + elm.setAttributeNS(xlinkNS, key, cur); + } else { + elm.setAttribute(key, cur); + } + } + } + } + // remove removed attributes + // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) + // the other option is to remove all attributes with value == undefined + for (key in oldAttrs) { + if (!(key in attrs)) { + elm.removeAttribute(key); + } + } +} + +export const attrsModule = { + create: updateAttrs, + update: updateAttrs +} as Module; + +//------------------------------------------------------------------------------ +// class.ts +//------------------------------------------------------------------------------ +function updateClass(oldVnode: VNode, vnode: VNode): void { + var cur: any, + name: string, + elm: Element, + oldClass = (oldVnode.data as VNodeData).class, + klass = (vnode.data as VNodeData).class; + + if (!oldClass && !klass) return; + if (oldClass === klass) return; + oldClass = oldClass || {}; + klass = klass || {}; + + elm = vnode.elm as Element; + + for (name in oldClass) { + if (!klass[name]) { + elm.classList.remove(name); + } + } + for (name in klass) { + cur = klass[name]; + if (cur !== oldClass[name]) { + (elm.classList as any)[cur ? "add" : "remove"](name); + } + } +} + +export const classModule = { create: updateClass, update: updateClass } as Module; diff --git a/src/vdom.ts b/src/vdom/vdom.ts similarity index 73% rename from src/vdom.ts rename to src/vdom/vdom.ts index 4cea3356..0d8b430c 100644 --- a/src/vdom.ts +++ b/src/vdom/vdom.ts @@ -27,6 +27,21 @@ declare global { } } +type Props = Record; +type Attrs = Record; +type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & { + [event: string]: EventListener; +}; + +export interface Module { + pre: PreHook; + create: CreateHook; + update: UpdateHook; + destroy: DestroyHook; + remove: RemoveHook; + post: PostHook; +} + //------------------------------------------------------------------------------ // vnode.ts //------------------------------------------------------------------------------ @@ -41,7 +56,7 @@ export interface VNode { key: Key | undefined; } -interface VNodeData { +export interface VNodeData { props?: Props; attrs?: Attrs; on?: On; @@ -631,258 +646,3 @@ export function h(sel: any, b?: any, c?: any): VNode { } return vnode(sel, data, children, text, undefined); } - -//------------------------------------------------------------------------------ -// module/props.ts -//------------------------------------------------------------------------------ -type Props = Record; - -function updateProps(oldVnode: VNode, vnode: VNode): void { - var key: string, - cur: any, - old: any, - elm = vnode.elm, - oldProps = (oldVnode.data as VNodeData).props, - props = (vnode.data as VNodeData).props; - - if (!oldProps && !props) return; - if (oldProps === props) return; - oldProps = oldProps || {}; - props = props || {}; - - for (key in oldProps) { - if (!props[key]) { - delete (elm as any)[key]; - } - } - for (key in props) { - cur = props[key]; - old = oldProps[key]; - if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) { - (elm as any)[key] = cur; - } - } -} - -export const propsModule = { - create: updateProps, - update: updateProps -} as Module; - -//------------------------------------------------------------------------------ -// module/module.ts -//------------------------------------------------------------------------------ -interface Module { - pre: PreHook; - create: CreateHook; - update: UpdateHook; - destroy: DestroyHook; - remove: RemoveHook; - post: PostHook; -} - -//------------------------------------------------------------------------------ -// module/eventlisteners.ts -//------------------------------------------------------------------------------ -type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & { - [event: string]: EventListener; -}; - -function invokeHandler(handler: any, vnode?: VNode, event?: Event): void { - if (typeof handler === "function") { - // call function handler - handler.call(vnode, event, vnode); - } else if (typeof handler === "object") { - // call handler with arguments - if (typeof handler[0] === "function") { - // special case for single argument for performance - if (handler.length === 2) { - handler[0].call(vnode, handler[1], event, vnode); - } else { - var args = handler.slice(1); - args.push(event); - args.push(vnode); - handler[0].apply(vnode, args); - } - } else { - // call multiple handlers - for (let i = 0, iLen = handler.length; i < iLen; i++) { - invokeHandler(handler[i], vnode, event); - } - } - } -} - -function handleEvent(event: Event, vnode: VNode) { - var name = event.type, - on = (vnode.data as VNodeData).on; - - // call event handler(s) if exists - if (on && on[name]) { - invokeHandler(on[name], vnode, event); - } -} - -function createListener() { - return function handler(event: Event) { - handleEvent(event, (handler as any).vnode); - }; -} - -function updateEventListeners(oldVnode: VNode, vnode?: VNode): void { - var oldOn = (oldVnode.data as VNodeData).on, - oldListener = (oldVnode as any).listener, - oldElm: Element = oldVnode.elm as Element, - on = vnode && (vnode.data as VNodeData).on, - elm: Element = (vnode && vnode.elm) as Element, - name: string; - - // optimization for reused immutable handlers - if (oldOn === on) { - return; - } - - // remove existing listeners which no longer used - if (oldOn && oldListener) { - // if element changed or deleted we remove all existing listeners unconditionally - if (!on) { - for (name in oldOn) { - // remove listener if element was changed or existing listeners removed - oldElm.removeEventListener(name, oldListener, false); - } - } else { - for (name in oldOn) { - // remove listener if existing listener removed - if (!on[name]) { - oldElm.removeEventListener(name, oldListener, false); - } - } - } - } - - // add new listeners which has not already attached - if (on) { - // reuse existing listener or create new - var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener()); - // update vnode for listener - listener.vnode = vnode; - - // if element changed or added we add all needed listeners unconditionally - if (!oldOn) { - for (name in on) { - // add listener if element was changed or new listeners added - elm.addEventListener(name, listener, false); - } - } else { - for (name in on) { - // add listener if new listener added - if (!oldOn[name]) { - elm.addEventListener(name, listener, false); - } - } - } - } -} - -export const eventListenersModule = { - create: updateEventListeners, - update: updateEventListeners, - destroy: updateEventListeners -} as Module; - -//------------------------------------------------------------------------------ -// attributes.ts -//------------------------------------------------------------------------------ -type Attrs = Record; - -const xlinkNS = "http://www.w3.org/1999/xlink"; -const xmlNS = "http://www.w3.org/XML/1998/namespace"; -const colonChar = 58; -const xChar = 120; - -function updateAttrs(oldVnode: VNode, vnode: VNode): void { - var key: string, - elm: Element = vnode.elm as Element, - oldAttrs = (oldVnode.data as VNodeData).attrs, - attrs = (vnode.data as VNodeData).attrs; - - if (!oldAttrs && !attrs) return; - if (oldAttrs === attrs) return; - oldAttrs = oldAttrs || {}; - attrs = attrs || {}; - - // update modified attributes, add new attributes - for (key in attrs) { - const cur = attrs[key]; - const old = oldAttrs[key]; - if (old !== cur) { - if (cur === true) { - elm.setAttribute(key, ""); - } else if (cur === false) { - elm.removeAttribute(key); - } else { - if (key.charCodeAt(0) !== xChar) { - elm.setAttribute(key, cur); - } else if (key.charCodeAt(3) === colonChar) { - // Assume xml namespace - elm.setAttributeNS(xmlNS, key, cur); - } else if (key.charCodeAt(5) === colonChar) { - // Assume xlink namespace - elm.setAttributeNS(xlinkNS, key, cur); - } else { - elm.setAttribute(key, cur); - } - } - } - } - // remove removed attributes - // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) - // the other option is to remove all attributes with value == undefined - for (key in oldAttrs) { - if (!(key in attrs)) { - elm.removeAttribute(key); - } - } -} - -export const attrsModule = { - create: updateAttrs, - update: updateAttrs -} as Module; - -//------------------------------------------------------------------------------ -// class.ts -//------------------------------------------------------------------------------ -function updateClass(oldVnode: VNode, vnode: VNode): void { - var cur: any, - name: string, - elm: Element, - oldClass = (oldVnode.data as VNodeData).class, - klass = (vnode.data as VNodeData).class; - - if (!oldClass && !klass) return; - if (oldClass === klass) return; - oldClass = oldClass || {}; - klass = klass || {}; - - elm = vnode.elm as Element; - - for (name in oldClass) { - if (!klass[name]) { - elm.classList.remove(name); - } - } - for (name in klass) { - cur = klass[name]; - if (cur !== oldClass[name]) { - (elm.classList as any)[cur ? "add" : "remove"](name); - } - } -} - -const classModule = { create: updateClass, update: updateClass } as Module; - -//------------------------------------------------------------------------------ -// patch -//------------------------------------------------------------------------------ -export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]); diff --git a/tests/__snapshots__/animations.test.ts.snap b/tests/__snapshots__/animations.test.ts.snap index 5e0c4cec..328236d3 100644 --- a/tests/__snapshots__/animations.test.ts.snap +++ b/tests/__snapshots__/animations.test.ts.snap @@ -3,11 +3,11 @@ exports[`animations t-transition combined with component 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -47,11 +47,11 @@ exports[`animations t-transition combined with component 1`] = ` exports[`animations t-transition combined with t-component and t-if 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); if (context['state'].display) { @@ -93,15 +93,16 @@ exports[`animations t-transition combined with t-component and t-if 1`] = ` exports[`animations t-transition with no delay/duration 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('span', p1, c1); p1.hook = { insert: vn => { - this.utils.transitionInsert(vn, 'jupiler'); + utils.transitionInsert(vn, 'jupiler'); }, remove: (vn, rm) => { - this.utils.transitionRemove(vn, 'jupiler', rm); + utils.transitionRemove(vn, 'jupiler', rm); }, }; c1.push({text: \`blue\`}); @@ -112,15 +113,16 @@ exports[`animations t-transition with no delay/duration 1`] = ` exports[`animations t-transition, on a simple node (insert) 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('span', p1, c1); p1.hook = { insert: vn => { - this.utils.transitionInsert(vn, 'chimay'); + utils.transitionInsert(vn, 'chimay'); }, remove: (vn, rm) => { - this.utils.transitionRemove(vn, 'chimay', rm); + utils.transitionRemove(vn, 'chimay', rm); }, }; c1.push({text: \`blue\`}); diff --git a/tests/animations.test.ts b/tests/animations.test.ts index e7d792e1..f1ce39d0 100644 --- a/tests/animations.test.ts +++ b/tests/animations.test.ts @@ -1,5 +1,5 @@ -import { Component, Env } from "../src/component"; -import { QWeb } from "../src/qweb_core"; +import { Component, Env } from "../src/component/component"; +import { QWeb } from "../src/qweb/index"; import { makeDeferred, makeTestFixture, diff --git a/tests/__snapshots__/component.test.ts.snap b/tests/component/__snapshots__/component.test.ts.snap similarity index 96% rename from tests/__snapshots__/component.test.ts.snap rename to tests/component/__snapshots__/component.test.ts.snap index 40cf1eb0..88ae919c 100644 --- a/tests/__snapshots__/component.test.ts.snap +++ b/tests/component/__snapshots__/component.test.ts.snap @@ -3,11 +3,11 @@ exports[`async rendering delayed component with t-asyncroot directive 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -85,11 +85,11 @@ exports[`async rendering delayed component with t-asyncroot directive 1`] = ` exports[`async rendering fast component with t-asyncroot directive 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -167,11 +167,11 @@ exports[`async rendering fast component with t-asyncroot directive 1`] = ` exports[`async rendering t-component with t-asyncroot directive: mixed re-renderings 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -249,11 +249,11 @@ exports[`async rendering t-component with t-asyncroot directive: mixed re-render exports[`class and style attributes with t-component dynamic t-att-style is properly added and updated on widget root el 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -291,11 +291,11 @@ exports[`class and style attributes with t-component dynamic t-att-style is prop exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -336,9 +336,10 @@ exports[`class and style attributes with t-component t-att-class is properly add exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 2`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let _2 = {'c':true}; - Object.assign(_2, this.utils.toObj({d:context['state'].d})) + Object.assign(_2, utils.toObj({d:context['state'].d})) let c3 = [], p3 = {key:3,class:_2}; var vn3 = h('span', p3, c3); return vn3; @@ -348,11 +349,11 @@ exports[`class and style attributes with t-component t-att-class is properly add exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -393,9 +394,10 @@ exports[`class and style attributes with t-component t-att-class is properly add exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 2`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let _2 = {'c':true}; - Object.assign(_2, this.utils.toObj(context['state'].d?'d':'')) + Object.assign(_2, utils.toObj(context['state'].d?'d':'')) let c3 = [], p3 = {key:3,class:_2}; var vn3 = h('span', p3, c3); return vn3; @@ -405,11 +407,11 @@ exports[`class and style attributes with t-component t-att-class is properly add exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -448,12 +450,12 @@ exports[`class and style attributes with t-component t-att-class is properly add exports[`composition sub components with some state rendered in a loop 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['state'].numbers; @@ -506,11 +508,11 @@ exports[`composition sub components with some state rendered in a loop 1`] = ` exports[`composition t-component with dynamic value 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -547,11 +549,11 @@ exports[`composition t-component with dynamic value 1`] = ` exports[`composition t-component with dynamic value 2 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -588,11 +590,11 @@ exports[`composition t-component with dynamic value 2 1`] = ` exports[`other directives with t-component t-on with handler bound to argument 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -629,11 +631,11 @@ exports[`other directives with t-component t-on with handler bound to argument 1 exports[`other directives with t-component t-on with handler bound to empty object (with non empty inner string) 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -670,11 +672,11 @@ exports[`other directives with t-component t-on with handler bound to empty obje exports[`other directives with t-component t-on with handler bound to empty object 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -711,11 +713,11 @@ exports[`other directives with t-component t-on with handler bound to empty obje exports[`other directives with t-component t-on with handler bound to object 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -752,11 +754,11 @@ exports[`other directives with t-component t-on with handler bound to object 1`] exports[`other directives with t-component t-on with prevent and self modifiers (order matters) 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -793,11 +795,11 @@ exports[`other directives with t-component t-on with prevent and self modifiers exports[`other directives with t-component t-on with self and prevent modifiers (order matters) 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -834,11 +836,11 @@ exports[`other directives with t-component t-on with self and prevent modifiers exports[`other directives with t-component t-on with self modifier 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -875,11 +877,11 @@ exports[`other directives with t-component t-on with self modifier 1`] = ` exports[`other directives with t-component t-on with stop and/or prevent modifiers 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -916,11 +918,11 @@ exports[`other directives with t-component t-on with stop and/or prevent modifie exports[`random stuff/miscellaneous snapshotting compiled code 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -958,12 +960,12 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = ` exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument on a t-foreach 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['props'].items; @@ -1017,7 +1019,7 @@ exports[`random stuff/miscellaneous t-on with handler bound to dynamic argument exports[`t-model directive .lazy modifier 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -1040,7 +1042,7 @@ exports[`t-model directive .lazy modifier 1`] = ` exports[`t-model directive basic use, on an input 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -1063,7 +1065,7 @@ exports[`t-model directive basic use, on an input 1`] = ` exports[`t-model directive on a select 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -1102,7 +1104,7 @@ exports[`t-model directive on a select 1`] = ` exports[`t-model directive on an input type=radio 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 'radio'; @@ -1138,7 +1140,7 @@ exports[`t-model directive on an input type=radio 1`] = ` exports[`t-model directive on an input, type=checkbox 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 'checkbox'; @@ -1164,11 +1166,11 @@ exports[`t-model directive on an input, type=checkbox 1`] = ` exports[`t-slot directive can define and call slots 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT @@ -1207,7 +1209,7 @@ exports[`t-slot directive can define and call slots 2`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2}; @@ -1232,7 +1234,7 @@ exports[`t-slot directive slots are rendered with proper context, part 2 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; var _1 = context['props'].to; let c2 = [], p2 = {key:2,attrs:{href: _1}}; var vn2 = h('a', p2, c2); @@ -1247,13 +1249,13 @@ exports[`t-slot directive slots are rendered with proper context, part 2 1`] = ` exports[`t-slot directive slots are rendered with proper context, part 2 2`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; context = Object.create(context); const scope = Object.create(null); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2}; @@ -1318,7 +1320,7 @@ exports[`t-slot directive slots are rendered with proper context, part 3 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; var _1 = context['props'].to; let c2 = [], p2 = {key:2,attrs:{href: _1}}; var vn2 = h('a', p2, c2); @@ -1333,13 +1335,13 @@ exports[`t-slot directive slots are rendered with proper context, part 3 1`] = ` exports[`t-slot directive slots are rendered with proper context, part 3 2`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; context = Object.create(context); const scope = Object.create(null); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2}; @@ -1404,11 +1406,11 @@ exports[`t-slot directive slots are rendered with proper context, part 3 2`] = ` exports[`t-slot directive slots are rendered with proper context, part 4 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 'User '+context['state'].user.name; @@ -1447,12 +1449,12 @@ exports[`t-slot directive slots are rendered with proper context, part 4 1`] = ` exports[`top level sub widgets basic use 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; let result; - var h = this.utils.h; + var h = this.h; //COMPONENT let def2; let w3 = 3 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[3]] : false; @@ -1487,12 +1489,12 @@ exports[`top level sub widgets basic use 1`] = ` exports[`top level sub widgets can select a sub widget 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; let result; - var h = this.utils.h; + var h = this.h; if (context['env'].flag) { //COMPONENT let def2; diff --git a/tests/__snapshots__/props_validation.test.ts.snap b/tests/component/__snapshots__/props_validation.test.ts.snap similarity index 96% rename from tests/__snapshots__/props_validation.test.ts.snap rename to tests/component/__snapshots__/props_validation.test.ts.snap index 72c753c0..709338da 100644 --- a/tests/__snapshots__/props_validation.test.ts.snap +++ b/tests/component/__snapshots__/props_validation.test.ts.snap @@ -3,11 +3,11 @@ exports[`props validation props are validated in dev mode (code snapshot) 1`] = ` "function anonymous(context,extra ) { - let utils = this.utils; + let utils = this.constructor.utils; let QWeb = this.constructor; let parent = context; let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); //COMPONENT diff --git a/tests/component.test.ts b/tests/component/component.test.ts similarity index 99% rename from tests/component.test.ts rename to tests/component/component.test.ts index 271e0a2c..ff9e0f4d 100644 --- a/tests/component.test.ts +++ b/tests/component/component.test.ts @@ -1,6 +1,6 @@ -import { Component, Env } from "../src/component"; -import { QWeb } from "../src/qweb_core"; -import { EventBus } from "../src/event_bus"; +import { Component, Env } from "../../src/component/component"; +import { QWeb } from "../../src/qweb/qweb"; +import { EventBus } from "../../src/core/event_bus"; import { makeDeferred, makeTestFixture, @@ -9,7 +9,7 @@ import { nextTick, normalize, editInput -} from "./helpers"; +} from "../helpers"; //------------------------------------------------------------------------------ // Setup and helpers diff --git a/tests/props_validation.test.ts b/tests/component/props_validation.test.ts similarity index 92% rename from tests/props_validation.test.ts rename to tests/component/props_validation.test.ts index 8e7c1e47..939e6976 100644 --- a/tests/props_validation.test.ts +++ b/tests/component/props_validation.test.ts @@ -1,6 +1,6 @@ -import { Component, Env } from "../src/component"; -import { makeTestFixture, makeTestEnv } from "./helpers"; -import { QWeb, UTILS } from "../src/qweb_core"; +import { Component, Env } from "../../src/component/component"; +import { makeTestFixture, makeTestEnv } from "../helpers"; +import { QWeb } from "../../src/qweb"; //------------------------------------------------------------------------------ // Setup and helpers @@ -251,10 +251,10 @@ describe("props validation", () => { } expect(() => { - UTILS.validateProps(TestWidget, { someProp: 1 }); + QWeb.utils.validateProps(TestWidget, { someProp: 1 }); }).toThrow(); expect(() => { - UTILS.validateProps(TestWidget, { message: 1 }); + QWeb.utils.validateProps(TestWidget, { message: 1 }); }).not.toThrow(); }); @@ -264,7 +264,7 @@ describe("props validation", () => { } expect(() => { - UTILS.validateProps(TestWidget, {}); + QWeb.utils.validateProps(TestWidget, {}); }).toThrow(); }); @@ -274,7 +274,7 @@ describe("props validation", () => { } expect(() => { - UTILS.validateProps(TestWidget, { message: 1, flag: true }); + QWeb.utils.validateProps(TestWidget, { message: 1, flag: true }); }).toThrow(); }); @@ -284,7 +284,7 @@ describe("props validation", () => { } expect(() => { - UTILS.validateProps(TestWidget, { message: 1, flag: true }); + QWeb.utils.validateProps(TestWidget, { message: 1, flag: true }); }).toThrow(); }); @@ -294,7 +294,7 @@ describe("props validation", () => { } expect(() => { - UTILS.validateProps(TestWidget, { message: 1}); + QWeb.utils.validateProps(TestWidget, { message: 1}); }).not.toThrow(); }); @@ -304,10 +304,10 @@ describe("props validation", () => { } expect(() => { - UTILS.validateProps(TestWidget, { message: undefined}); + QWeb.utils.validateProps(TestWidget, { message: undefined}); }).not.toThrow(); expect(() => { - UTILS.validateProps(TestWidget, { message: null}); + QWeb.utils.validateProps(TestWidget, { message: null}); }).toThrow(); }); diff --git a/tests/event_bus.test.ts b/tests/core/event_bus.test.ts similarity index 95% rename from tests/event_bus.test.ts rename to tests/core/event_bus.test.ts index 258d0183..c7e80255 100644 --- a/tests/event_bus.test.ts +++ b/tests/core/event_bus.test.ts @@ -1,4 +1,4 @@ -import { EventBus } from "../src/event_bus"; +import { EventBus } from "../../src/core/event_bus"; describe("event bus behaviour", () => { test("can subscribe and be notified", () => { diff --git a/tests/observer.test.ts b/tests/core/observer.test.ts similarity index 99% rename from tests/observer.test.ts rename to tests/core/observer.test.ts index 70b3cdbd..92defaab 100644 --- a/tests/observer.test.ts +++ b/tests/core/observer.test.ts @@ -1,5 +1,5 @@ -import { Observer } from "../src/observer"; -import { nextMicroTick } from "./helpers"; +import { Observer } from "../../src/core/observer"; +import { nextMicroTick } from "../helpers"; describe("observer", () => { test("properly observe objects", () => { diff --git a/tests/helpers.ts b/tests/helpers.ts index 1a783569..df6991e1 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -1,8 +1,9 @@ -import { Env } from "../src/component"; -import { EvalContext, QWeb, UTILS } from "../src/qweb_core"; +import { Env } from "../src/component/component"; +import { EvalContext, QWeb } from "../src/qweb/qweb"; import { patch } from "../src/vdom"; -import "../src/qweb_directives"; -import "../src/qweb_extensions"; +import "../src/qweb/base_directives"; +import "../src/qweb/extensions"; +import "../src/component/directive"; export function nextMicroTick(): Promise { return Promise.resolve(); @@ -90,15 +91,15 @@ export function renderToString(qweb: QWeb, t: string, context: EvalContext = {}) // is useful for animations tests, as we hook before repaints to trigger // animations (thanks to requestAnimationFrame). Patching nextFrame allows to // simulate calls to this hook. One must not forget to unpatch afterwards. -let nextFrame = UTILS.nextFrame; +let nextFrame = QWeb.utils.nextFrame; export function patchNextFrame(f: Function) { - UTILS.nextFrame = (cb: () => void) => { + QWeb.utils.nextFrame = (cb: () => void) => { setTimeout(() => f(cb)); }; } export function unpatchNextFrame() { - UTILS.nextFrame = nextFrame; + QWeb.utils.nextFrame = nextFrame; } export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) { diff --git a/tests/__snapshots__/qweb.test.ts.snap b/tests/qweb/__snapshots__/qweb.test.ts.snap similarity index 93% rename from tests/__snapshots__/qweb.test.ts.snap rename to tests/qweb/__snapshots__/qweb.test.ts.snap index 6f72aab8..d8b54c0a 100644 --- a/tests/__snapshots__/qweb.test.ts.snap +++ b/tests/qweb/__snapshots__/qweb.test.ts.snap @@ -3,7 +3,7 @@ exports[`attributes class and t-attf-class with ternary operation 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = 'hello ' + (context['value']?'world':''); let c2 = [], p2 = {key:2,attrs:{class: _1}}; var vn2 = h('div', p2, c2); @@ -14,7 +14,7 @@ exports[`attributes class and t-attf-class with ternary operation 1`] = ` exports[`attributes dynamic attribute falsy variable 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = context['value']; let c2 = [], p2 = {key:2,attrs:{foo: _1}}; var vn2 = h('div', p2, c2); @@ -25,7 +25,7 @@ exports[`attributes dynamic attribute falsy variable 1`] = ` exports[`attributes dynamic attribute with a dash 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = context['id']; let c2 = [], p2 = {key:2,attrs:{\\"data-action-id\\": _1}}; var vn2 = h('div', p2, c2); @@ -36,7 +36,7 @@ exports[`attributes dynamic attribute with a dash 1`] = ` exports[`attributes dynamic attributes 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = 'bar'; let c2 = [], p2 = {key:2,attrs:{foo: _1}}; var vn2 = h('div', p2, c2); @@ -47,7 +47,7 @@ exports[`attributes dynamic attributes 1`] = ` exports[`attributes dynamic formatted attributes with a dash 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = \`Some text \${context['id']}\`; let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}}; var vn2 = h('div', p2, c2); @@ -58,7 +58,7 @@ exports[`attributes dynamic formatted attributes with a dash 1`] = ` exports[`attributes fixed variable 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = context['value']; let c2 = [], p2 = {key:2,attrs:{foo: _1}}; var vn2 = h('div', p2, c2); @@ -69,7 +69,7 @@ exports[`attributes fixed variable 1`] = ` exports[`attributes format expression 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = (context['value']+37); let c2 = [], p2 = {key:2,attrs:{foo: _1}}; var vn2 = h('div', p2, c2); @@ -80,7 +80,7 @@ exports[`attributes format expression 1`] = ` exports[`attributes format expression, other format 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = (context['value']+37); let c2 = [], p2 = {key:2,attrs:{foo: _1}}; var vn2 = h('div', p2, c2); @@ -91,7 +91,7 @@ exports[`attributes format expression, other format 1`] = ` exports[`attributes format literal 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = \`bar\`; let c2 = [], p2 = {key:2,attrs:{foo: _1}}; var vn2 = h('div', p2, c2); @@ -102,7 +102,7 @@ exports[`attributes format literal 1`] = ` exports[`attributes format multiple 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = \`a \${context['value1']} is \${context['value2']} of \${context['value3']} ]\`; let c2 = [], p2 = {key:2,attrs:{foo: _1}}; var vn2 = h('div', p2, c2); @@ -113,7 +113,7 @@ exports[`attributes format multiple 1`] = ` exports[`attributes format value 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = \`b\${context['value']}r\`; let c2 = [], p2 = {key:2,attrs:{foo: _1}}; var vn2 = h('div', p2, c2); @@ -124,11 +124,12 @@ exports[`attributes format value 1`] = ` exports[`attributes from object variables set previously 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = {a:'b'}; - let _3 = this.utils.toObj(_2.a); + let _3 = utils.toObj(_2.a); let c4 = [], p4 = {key:4,class:_3}; var vn4 = h('span', p4, c4); c1.push(vn4); @@ -139,11 +140,12 @@ exports[`attributes from object variables set previously 1`] = ` exports[`attributes from variables set previously 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 'def'; - let _3 = this.utils.toObj(_2); + let _3 = utils.toObj(_2); let c4 = [], p4 = {key:4,class:_3}; var vn4 = h('span', p4, c4); c1.push(vn4); @@ -154,7 +156,7 @@ exports[`attributes from variables set previously 1`] = ` exports[`attributes object 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = context['value']; let c2 = [], p2 = {key:2,attrs:{}}; if (_1 instanceof Array) { @@ -172,7 +174,7 @@ exports[`attributes object 1`] = ` exports[`attributes static attributes 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = 'a'; var _2 = 'b'; var _3 = 'c'; @@ -185,7 +187,7 @@ exports[`attributes static attributes 1`] = ` exports[`attributes static attributes on void elements 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = '/test.jpg'; var _2 = 'Test'; let c3 = [], p3 = {key:3,attrs:{src: _1,alt: _2}}; @@ -197,7 +199,7 @@ exports[`attributes static attributes on void elements 1`] = ` exports[`attributes static attributes with dashes 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = 'Close'; let c2 = [], p2 = {key:2,attrs:{\\"aria-label\\": _1}}; var vn2 = h('div', p2, c2); @@ -208,9 +210,10 @@ exports[`attributes static attributes with dashes 1`] = ` exports[`attributes t-att-class and class should combine together 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let _2 = {'hello':true}; - Object.assign(_2, this.utils.toObj(context['value'])) + Object.assign(_2, utils.toObj(context['value'])) let c3 = [], p3 = {key:3,class:_2}; var vn3 = h('div', p3, c3); return vn3; @@ -220,9 +223,10 @@ exports[`attributes t-att-class and class should combine together 1`] = ` exports[`attributes t-att-class with object 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let _2 = {'static':true}; - Object.assign(_2, this.utils.toObj({a:context['b'],c:context['d'],e:context['f']})) + Object.assign(_2, utils.toObj({a:context['b'],c:context['d'],e:context['f']})) let c3 = [], p3 = {key:3,class:_2}; var vn3 = h('div', p3, c3); return vn3; @@ -232,7 +236,7 @@ exports[`attributes t-att-class with object 1`] = ` exports[`attributes t-attf-class should combine with class 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = 'hello ' + \`world\`; let c2 = [], p2 = {key:2,attrs:{class: _1}}; var vn2 = h('div', p2, c2); @@ -243,7 +247,7 @@ exports[`attributes t-attf-class should combine with class 1`] = ` exports[`attributes tuple literal 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = ['foo','bar']; let c2 = [], p2 = {key:2,attrs:{}}; if (_1 instanceof Array) { @@ -261,7 +265,7 @@ exports[`attributes tuple literal 1`] = ` exports[`attributes tuple variable 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = context['value']; let c2 = [], p2 = {key:2,attrs:{}}; if (_1 instanceof Array) { @@ -279,7 +283,7 @@ exports[`attributes tuple variable 1`] = ` exports[`debugging t-debug 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; debugger; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); @@ -297,7 +301,7 @@ exports[`debugging t-debug 1`] = ` exports[`debugging t-log 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 42; @@ -310,7 +314,7 @@ exports[`foreach does not pollute the rendering context 1`] = ` "function anonymous(context,extra ) { context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = [1]; @@ -340,7 +344,7 @@ exports[`foreach iterate on items (on a element node) 1`] = ` "function anonymous(context,extra ) { context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = [1,2]; @@ -373,7 +377,7 @@ exports[`foreach iterate on items 1`] = ` "function anonymous(context,extra ) { context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = [3,2,1]; @@ -415,7 +419,7 @@ exports[`foreach iterate, dict param 1`] = ` "function anonymous(context,extra ) { context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['value']; @@ -457,7 +461,7 @@ exports[`foreach iterate, position 1`] = ` "function anonymous(context,extra ) { context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = Array(5); @@ -496,7 +500,7 @@ exports[`foreach warn if no key in some case 1`] = ` "function anonymous(context,extra ) { context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = [1,2]; @@ -528,7 +532,7 @@ exports[`foreach warn if no key in some case 1`] = ` exports[`loading templates can initialize qweb with a string 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); c1.push({text: \`jupiler\`}); @@ -539,7 +543,7 @@ exports[`loading templates can initialize qweb with a string 1`] = ` exports[`loading templates can load a few templates from a xml string 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('ul', p1, c1); let c2 = [], p2 = {key:2}; @@ -557,8 +561,9 @@ exports[`loading templates can load a few templates from a xml string 1`] = ` exports[`misc global 1`] = ` "function anonymous(context,extra ) { + let utils = this.constructor.utils; context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = [4,5,6]; @@ -624,7 +629,7 @@ exports[`misc global 1`] = ` c1.push(vn21); var _22 = context['toto']; if (_22 || _22 === 0) { - var frag23 = this.utils.getFragment(_22) + var frag23 = utils.getFragment(_22) var p24 = {hook: { insert: n => n.elm.parentNode.replaceChild(frag23, n.elm), }}; @@ -640,7 +645,7 @@ exports[`misc global 1`] = ` exports[`special cases for some boolean html attributes/properties input type= checkbox, with t-att-checked 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = 'checkbox'; var _2 = context['flag']; let c3 = [], p3 = {key:3,attrs:{type: _1,checked: _2},props:{checked: _2}}; @@ -652,7 +657,7 @@ exports[`special cases for some boolean html attributes/properties input type= c exports[`special cases for some boolean html attributes/properties various boolean html attributes 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 'checkbox'; @@ -692,7 +697,7 @@ exports[`special cases for some boolean html attributes/properties various boole exports[`static templates div with a span child node 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2}; @@ -706,7 +711,7 @@ exports[`static templates div with a span child node 1`] = ` exports[`static templates div with a text node 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); c1.push({text: \`word\`}); @@ -717,7 +722,7 @@ exports[`static templates div with a text node 1`] = ` exports[`static templates empty div 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); return vn1; @@ -727,7 +732,7 @@ exports[`static templates empty div 1`] = ` exports[`static templates simple dynamic value 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var _1 = context['text']; if (_1 || _1 === 0) { var vn2 = {text: _1}; @@ -739,7 +744,7 @@ exports[`static templates simple dynamic value 1`] = ` exports[`static templates simple string 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var vn1 = {text: \`hello vdom\`}; return vn1; }" @@ -748,7 +753,7 @@ exports[`static templates simple string 1`] = ` exports[`static templates simple string, with some dynamic value 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var vn1 = {text: \`hello \`}; var _2 = context['text']; if (_2 || _2 === 0) { @@ -761,7 +766,7 @@ exports[`static templates simple string, with some dynamic value 1`] = ` exports[`t-call (template calling basic caller 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); c1.push({text: \`ok\`}); @@ -772,7 +777,7 @@ exports[`t-call (template calling basic caller 1`] = ` exports[`t-call (template calling inherit context 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 1; @@ -786,7 +791,7 @@ exports[`t-call (template calling inherit context 1`] = ` exports[`t-call (template calling scoped parameters 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); { @@ -804,7 +809,7 @@ exports[`t-call (template calling scoped parameters 1`] = ` exports[`t-call (template calling with unused body 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c2 = [], p2 = {key:2}; var vn2 = h('div', p2, c2); c2.push({text: \`ok\`}); @@ -815,7 +820,7 @@ exports[`t-call (template calling with unused body 1`] = ` exports[`t-call (template calling with unused setbody 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; { let _1 = 3; let c2 = [], p2 = {key:2}; @@ -829,7 +834,7 @@ exports[`t-call (template calling with unused setbody 1`] = ` exports[`t-call (template calling with used body 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c2 = [], p2 = {key:2}; var vn2 = h('h1', p2, c2); c2.push({text: \`ok\`}); @@ -840,7 +845,7 @@ exports[`t-call (template calling with used body 1`] = ` exports[`t-call (template calling with used set body 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('span', p1, c1); { @@ -856,7 +861,7 @@ exports[`t-call (template calling with used set body 1`] = ` exports[`t-esc escaping on a node 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('span', p1, c1); var _2 = 'ok'; @@ -870,7 +875,7 @@ exports[`t-esc escaping on a node 1`] = ` exports[`t-esc escaping on a node with a body 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('span', p1, c1); var _2 = 'ok'; @@ -886,7 +891,7 @@ exports[`t-esc escaping on a node with a body 1`] = ` exports[`t-esc escaping on a node with a body, as a default 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('span', p1, c1); var _2 = context['var']; @@ -902,7 +907,7 @@ exports[`t-esc escaping on a node with a body, as a default 1`] = ` exports[`t-esc literal 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('span', p1, c1); var _2 = 'ok'; @@ -916,7 +921,7 @@ exports[`t-esc literal 1`] = ` exports[`t-esc variable 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('span', p1, c1); var _2 = context['var']; @@ -930,7 +935,7 @@ exports[`t-esc variable 1`] = ` exports[`t-if boolean value condition elif 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); if (context['color']=='black') { @@ -952,7 +957,7 @@ exports[`t-if boolean value condition elif 1`] = ` exports[`t-if boolean value condition else 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2}; @@ -976,7 +981,7 @@ exports[`t-if boolean value condition else 1`] = ` exports[`t-if boolean value condition false else 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2}; @@ -1000,7 +1005,7 @@ exports[`t-if boolean value condition false else 1`] = ` exports[`t-if boolean value condition missing 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('span', p1, c1); if (context['condition']) { @@ -1013,7 +1018,7 @@ exports[`t-if boolean value condition missing 1`] = ` exports[`t-if boolean value false condition 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); if (context['condition']) { @@ -1026,7 +1031,7 @@ exports[`t-if boolean value false condition 1`] = ` exports[`t-if boolean value true condition 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); if (context['condition']) { @@ -1039,7 +1044,7 @@ exports[`t-if boolean value true condition 1`] = ` exports[`t-if can use some boolean operators in expressions 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); if (context['cond1']&&context['cond2']) { @@ -1073,7 +1078,7 @@ exports[`t-if can use some boolean operators in expressions 1`] = ` exports[`t-key can use t-key directive on a node 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:context['beer'].id}; var vn1 = h('div', p1, c1); var _2 = context['beer'].name; @@ -1088,7 +1093,7 @@ exports[`t-key t-key directive in a list 1`] = ` "function anonymous(context,extra ) { context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('ul', p1, c1); var _2 = context['beers']; @@ -1123,7 +1128,7 @@ exports[`t-on can bind event handler 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('button', p1, c1); if (!context['add']) { @@ -1140,7 +1145,7 @@ exports[`t-on can bind handlers with arguments 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('button', p1, c1); if (!context['add']) { @@ -1156,7 +1161,7 @@ exports[`t-on can bind handlers with empty object (with non empty inner string) "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('button', p1, c1); if (!context['doSomething']) { @@ -1172,7 +1177,7 @@ exports[`t-on can bind handlers with empty object 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('button', p1, c1); if (!context['doSomething']) { @@ -1189,7 +1194,7 @@ exports[`t-on can bind handlers with loop variable as argument 1`] = ` ) { let owner = context; context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('ul', p1, c1); var _2 = ['someval']; @@ -1226,7 +1231,7 @@ exports[`t-on can bind handlers with object arguments 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('button', p1, c1); if (!context['add']) { @@ -1242,7 +1247,7 @@ exports[`t-on can bind two event handlers 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('button', p1, c1); if (!context['handleClick']) { @@ -1264,7 +1269,7 @@ exports[`t-on handler is bound to proper owner 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1,on:{}}; var vn1 = h('button', p1, c1); if (!context['add']) { @@ -1281,7 +1286,7 @@ exports[`t-on t-on with prevent and self modifiers (order matters) 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -1304,7 +1309,7 @@ exports[`t-on t-on with prevent and/or stop modifiers 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -1342,7 +1347,7 @@ exports[`t-on t-on with self and prevent modifiers (order matters) 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -1365,7 +1370,7 @@ exports[`t-on t-on with self modifier 1`] = ` "function anonymous(context,extra ) { let owner = context; - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2,on:{}}; @@ -1399,12 +1404,13 @@ exports[`t-on t-on with self modifier 1`] = ` exports[`t-raw literal 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('span', p1, c1); var _2 = 'ok'; if (_2 || _2 === 0) { - var frag3 = this.utils.getFragment(_2) + var frag3 = utils.getFragment(_2) var p4 = {hook: { insert: n => n.elm.parentNode.replaceChild(frag3, n.elm), }}; @@ -1418,12 +1424,13 @@ exports[`t-raw literal 1`] = ` exports[`t-raw not escaping 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['var']; if (_2 || _2 === 0) { - var frag3 = this.utils.getFragment(_2) + var frag3 = utils.getFragment(_2) var p4 = {hook: { insert: n => n.elm.parentNode.replaceChild(frag3, n.elm), }}; @@ -1437,7 +1444,8 @@ exports[`t-raw not escaping 1`] = ` exports[`t-raw t-raw and another sibling node 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('span', p1, c1); let c2 = [], p2 = {key:2}; @@ -1446,7 +1454,7 @@ exports[`t-raw t-raw and another sibling node 1`] = ` c2.push({text: \`hello\`}); var _3 = context['var']; if (_3 || _3 === 0) { - var frag4 = this.utils.getFragment(_3) + var frag4 = utils.getFragment(_3) var p5 = {hook: { insert: n => n.elm.parentNode.replaceChild(frag4, n.elm), }}; @@ -1460,12 +1468,13 @@ exports[`t-raw t-raw and another sibling node 1`] = ` exports[`t-raw variable 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + let utils = this.constructor.utils; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('span', p1, c1); var _2 = context['var']; if (_2 || _2 === 0) { - var frag3 = this.utils.getFragment(_2) + var frag3 = utils.getFragment(_2) var p4 = {hook: { insert: n => n.elm.parentNode.replaceChild(frag3, n.elm), }}; @@ -1479,7 +1488,7 @@ exports[`t-raw variable 1`] = ` exports[`t-ref can get a dynamic ref on a node 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2}; @@ -1498,7 +1507,7 @@ exports[`t-ref can get a dynamic ref on a node 1`] = ` exports[`t-ref can get a ref on a node 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2}; @@ -1518,7 +1527,7 @@ exports[`t-ref refs in a loop 1`] = ` "function anonymous(context,extra ) { context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['items']; @@ -1556,7 +1565,7 @@ exports[`t-ref refs in a loop 1`] = ` exports[`t-set evaluate value expression 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 1+2; @@ -1570,7 +1579,7 @@ exports[`t-set evaluate value expression 1`] = ` exports[`t-set evaluate value expression, part 2 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['somevariable']+2; @@ -1584,7 +1593,7 @@ exports[`t-set evaluate value expression, part 2 1`] = ` exports[`t-set set from attribute literal 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 'ok'; @@ -1598,7 +1607,7 @@ exports[`t-set set from attribute literal 1`] = ` exports[`t-set set from attribute lookup 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['value']; @@ -1612,7 +1621,7 @@ exports[`t-set set from attribute lookup 1`] = ` exports[`t-set set from body literal 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; var vn1 = {text: \`ok\`}; return vn1; }" @@ -1621,7 +1630,7 @@ exports[`t-set set from body literal 1`] = ` exports[`t-set set from body lookup 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['value']; @@ -1635,7 +1644,7 @@ exports[`t-set set from body lookup 1`] = ` exports[`t-set set from empty body 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); return vn1; @@ -1645,7 +1654,7 @@ exports[`t-set set from empty body 1`] = ` exports[`t-set t-set and t-if 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['value']; @@ -1659,7 +1668,7 @@ exports[`t-set t-set and t-if 1`] = ` exports[`t-set t-set evaluates an expression only once 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = context['value']+' artois'; @@ -1677,7 +1686,7 @@ exports[`t-set t-set should reuse variable if possible 1`] = ` "function anonymous(context,extra ) { context = Object.create(context); - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 1; @@ -1714,7 +1723,7 @@ exports[`t-set t-set should reuse variable if possible 1`] = ` exports[`t-set value priority 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); var _2 = 1; @@ -1728,7 +1737,7 @@ exports[`t-set value priority 1`] = ` exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); c1.push({text: \` abc \`}); @@ -1739,7 +1748,7 @@ exports[`whitespace handling consecutives whitespaces are condensed into a singl exports[`whitespace handling nothing is done in pre tags 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('pre', p1, c1); c1.push({text: \` \`}); @@ -1750,7 +1759,7 @@ exports[`whitespace handling nothing is done in pre tags 1`] = ` exports[`whitespace handling nothing is done in pre tags 2`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('pre', p1, c1); c1.push({text: \` @@ -1763,7 +1772,7 @@ exports[`whitespace handling nothing is done in pre tags 2`] = ` exports[`whitespace handling nothing is done in pre tags 3`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('pre', p1, c1); c1.push({text: \` @@ -1776,7 +1785,7 @@ exports[`whitespace handling nothing is done in pre tags 3`] = ` exports[`whitespace handling white space only text nodes are condensed into a single space 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); c1.push({text: \` \`}); @@ -1787,7 +1796,7 @@ exports[`whitespace handling white space only text nodes are condensed into a si exports[`whitespace handling whitespace only text nodes with newlines are removed 1`] = ` "function anonymous(context,extra ) { - var h = this.utils.h; + var h = this.h; let c1 = [], p1 = {key:1}; var vn1 = h('div', p1, c1); let c2 = [], p2 = {key:2}; diff --git a/tests/qweb.test.ts b/tests/qweb/qweb.test.ts similarity index 99% rename from tests/qweb.test.ts rename to tests/qweb/qweb.test.ts index e116b592..5dc53e7b 100644 --- a/tests/qweb.test.ts +++ b/tests/qweb/qweb.test.ts @@ -1,5 +1,5 @@ -import { QWeb } from "../src/qweb_core"; -import { normalize, renderToDOM, renderToString, trim } from "./helpers"; +import { QWeb } from "../../src/qweb/index"; +import { normalize, renderToDOM, renderToString, trim } from "../helpers"; //------------------------------------------------------------------------------ // Setup and helpers diff --git a/tests/qweb_expressions.test.ts b/tests/qweb/qweb_expressions.test.ts similarity index 98% rename from tests/qweb_expressions.test.ts rename to tests/qweb/qweb_expressions.test.ts index 34052047..e818d44e 100644 --- a/tests/qweb_expressions.test.ts +++ b/tests/qweb/qweb_expressions.test.ts @@ -1,4 +1,4 @@ -import { compileExpr, tokenize } from "../src/qweb_expressions"; +import { compileExpr, tokenize } from "../../src/qweb/expression_parser"; describe("tokenizer", () => { test("simple tokens", () => { diff --git a/tests/__snapshots__/store.test.ts.snap b/tests/store/__snapshots__/connected_component.test.ts.snap similarity index 100% rename from tests/__snapshots__/store.test.ts.snap rename to tests/store/__snapshots__/connected_component.test.ts.snap diff --git a/tests/store.test.ts b/tests/store/connected_component.test.ts similarity index 60% rename from tests/store.test.ts rename to tests/store/connected_component.test.ts index 0a3a5984..eceaf603 100644 --- a/tests/store.test.ts +++ b/tests/store/connected_component.test.ts @@ -1,553 +1,10 @@ -import { Component, Env } from "../src/component"; -import { Store, ConnectedComponent } from "../src/store"; -import { makeTestFixture, makeTestEnv, nextMicroTick, nextTick } from "./helpers"; -import { misc } from "../src"; +import { core } from "../../src"; +import { Component, Env } from "../../src/component/component"; +import { ConnectedComponent } from "../../src/store/connected_component"; +import { Store } from "../../src/store/store"; +import { makeTestEnv, makeTestFixture, nextTick } from "../helpers"; -const Observer = misc.Observer; - -describe("basic use", () => { - test("commit a mutation", () => { - const state = { n: 1 }; - const mutations = { - inc({ state }, delta) { - state.n += delta; - } - }; - const store = new Store({ state, mutations }); - - expect(store.state.n).toBe(1); - store.commit("inc", 14); - expect(store.state.n).toBe(15); - }); - - test("dispatch an action", () => { - const state = { n: 1 }; - const mutations = { - inc({ state }, delta) { - state.n += delta; - } - }; - const actions = { - inc({ commit }, delta) { - commit("inc", delta); - } - }; - const store = new Store({ state, mutations, actions }); - - expect(store.state.n).toBe(1); - store.dispatch("inc", 14); - expect(store.state.n).toBe(15); - }); - - test("dispatch an action + commit a mutation with positional arguments", () => { - const state = { n1: 1, n2: 1, n3: 1 }; - const mutations = { - batchInc({ state }, delta1, delta2, delta3) { - state.n1 += delta1; - state.n2 += delta2; - state.n3 += delta3; - } - }; - const actions = { - batchInc({ commit }, delta1, delta2, delta3) { - commit("batchInc", delta1, delta2, delta3); - } - }; - const store = new Store({ state, mutations, actions }); - - expect(store.state.n1).toBe(1); - expect(store.state.n2).toBe(1); - expect(store.state.n3).toBe(1); - store.dispatch("batchInc", 14, 30, 88); - expect(store.state.n1).toBe(15); - expect(store.state.n2).toBe(31); - expect(store.state.n3).toBe(89); - }); - - test("modifying state outside of mutations trigger error", () => { - const state = { n: 1 }; - const actions = { - inc({ state }) { - state.n++; - } - }; - const store = new Store({ state, mutations: {}, actions }); - - expect(() => store.dispatch("inc")).toThrow(); - expect(() => (store.state.n = 15)).toThrow(); - }); - - test("can dispatch an action in an action", () => { - const state = { n: 1 }; - const mutations = { - inc({ state }, delta) { - state.n += delta; - } - }; - const actions = { - inc({ commit }, delta) { - commit("inc", delta); - }, - inc100({ dispatch }) { - dispatch("inc", 100); - } - }; - const store = new Store({ state, mutations, actions }); - - expect(store.state.n).toBe(1); - store.dispatch("inc100"); - expect(store.state.n).toBe(101); - }); - - test("can commit a mutation in a mutation", () => { - const state = { n: 1 }; - const mutations = { - inc({ state }) { - state.n++; - }, - inc10({ commit }) { - for (let i = 0; i < 10; i++) { - commit("inc"); - } - } - }; - const store = new Store({ state, mutations }); - - expect(store.state.n).toBe(1); - store.commit("inc10"); - expect(store.state.n).toBe(11); - }); - - test("return data from committing a mutation", () => { - const state = { n: 1 }; - const mutations = { - inc({ state }) { - return ++state.n; - } - }; - const store = new Store({ state, mutations }); - - expect(store.state.n).toBe(1); - const res = store.commit("inc"); - expect(store.state.n).toBe(2); - expect(res).toBe(2); - }); - - test("dispatch allow synchronizing between actions", async () => { - const state = { n: 1 }; - const mutations = { - inc({ state }, delta) { - state.n += delta; - }, - setN({ state }, n) { - state.n = n; - } - }; - const actions = { - async dosomething({ commit, dispatch }) { - await dispatch("setTo10"); - commit("inc", 3); - }, - async setTo10({ commit }) { - await Promise.resolve(); - commit("setN", 10); - } - }; - const store = new Store({ state, mutations, actions }); - - expect(store.state.n).toBe(1); - store.dispatch("dosomething"); - expect(store.state.n).toBe(1); - await nextTick(); - expect(store.state.n).toBe(13); - }); - - test("env is given to actions", () => { - expect.assertions(1); - const someEnv = {}; - const actions = { - someaction({ env }) { - expect(env).toBe(someEnv); - } - }; - const store = new Store({ state: {}, actions, env: someEnv }); - - store.dispatch("someaction"); - }); - - test("can have getters from store", async () => { - const state = { - beers: { - 1: { - id: 1, - name: "bertinchamps", - tasterID: 1 - } - }, - tasters: { - 1: { - id: 1, - name: "aaron" - } - } - }; - const getters = { - beerTasterName({ state }, beerID) { - return state.tasters[state.beers[beerID].tasterID].name; - }, - bestBeerName({ state }) { - return state.beers[1].name; - } - }; - const store = new Store({ state, mutations: {}, actions: {}, getters }); - expect(store.getters).toBeDefined(); - expect((store.getters).bestBeerName()).toBe("bertinchamps"); - expect((store.getters).beerTasterName(1)).toBe("aaron"); - }); - - test("getters are memoized", async () => { - const state = { - beers: { - 1: { - id: 1, - name: "bertinchamps", - tasterID: 1 - } - }, - tasters: { - 1: { - id: 1, - name: "aaron" - } - } - }; - let n = 0; - const getters = { - beerTasterName({ state }, beerID) { - n++; - return state.tasters[state.beers[beerID].tasterID].name; - }, - bestBeerName({ state }) { - n++; - return state.beers[1].name; - } - }; - const store = new Store({ state, mutations: {}, actions: {}, getters }); - expect((store.getters).bestBeerName()).toBe("bertinchamps"); - expect((store.getters).beerTasterName(1)).toBe("aaron"); - expect(n).toBe(2); - expect((store.getters).bestBeerName()).toBe("bertinchamps"); - expect((store.getters).beerTasterName(1)).toBe("aaron"); - expect(n).toBe(2); - }); - - test("getters taking Array as argument aren't memoized", async () => { - const state = { - beers: { - 1: { - id: 1, - name: "bertinchamps", - tasterID: 1 - } - } - }; - let n = 0; - const getters = { - getBeerNames({ state }, beerIDs) { - n++; - return beerIDs.map(beerID => { - return state.beers[beerID].name; - }); - } - }; - const store = new Store({ state, mutations: {}, actions: {}, getters }); - expect((store.getters).getBeerNames([1])).toEqual(["bertinchamps"]); - expect(n).toBe(1); - expect((store.getters).getBeerNames([1])).toEqual(["bertinchamps"]); - expect(n).toBe(2); - }); - - test("getters cache is nuked on store changes", async () => { - const state = { - beers: { - 1: { - id: 1, - name: "bertinchamps", - tasterID: 1 - } - }, - tasters: { - 1: { - id: 1, - name: "aaron" - }, - 2: { - id: 2, - name: "gery" - } - } - }; - const mutations = { - changeTaster({ state }, { beerID, tasterID }) { - state.beers[beerID].tasterID = tasterID; - } - }; - let n = 0; - const getters = { - beerTasterName({ state }, beerID) { - n++; - return state.tasters[state.beers[beerID].tasterID].name; - } - }; - const store = new Store({ - state, - mutations: mutations, - actions: {}, - getters - }); - expect((store.getters).beerTasterName(1)).toBe("aaron"); - expect(n).toBe(1); - expect((store.getters).beerTasterName(1)).toBe("aaron"); - expect(n).toBe(1); - - store.commit("changeTaster", { beerID: 1, tasterID: 2 }); - await nextTick(); - - expect((store.getters).beerTasterName(1)).toBe("gery"); - expect(n).toBe(2); - }); - - test("getters cache is disabled during a mutation", async () => { - const state = { - beers: { - 1: { - id: 1, - name: "bertinchamps" - } - } - }; - const mutations = { - renameBeer({ state, getters }, beerID) { - expect(getters.beerName(beerID)).toBe("bertinchamps"); - state.beers[1].name = "chouffe"; - expect(getters.beerName(beerID)).toBe("chouffe"); - } - }; - let n = 0; - const getters = { - beerName({ state }, beerID) { - n++; - return state.beers[beerID].name; - } - }; - const store = new Store({ - state, - mutations: mutations, - actions: {}, - getters - }); - - store.commit("renameBeer", 1); - expect((store.getters).beerName(1)).toBe("chouffe"); - await nextTick(); - - expect(n).toBe(3); - }); - - test("getters given to actions", async () => { - expect.assertions(3); - const state = { - beers: { - 1: { - id: 1, - name: "bertinchamps", - tasterID: 1 - } - }, - tasters: { - 1: { - id: 1, - name: "aaron" - } - } - }; - const getters = { - beerTasterName({ state }, beerID) { - return state.tasters[state.beers[beerID].tasterID].name; - }, - bestBeerName({ state }) { - return state.beers[1].name; - } - }; - const actions = { - action({ getters }) { - expect(getters).toBeDefined(); - expect(getters.bestBeerName()).toBe("bertinchamps"); - expect(getters.beerTasterName(1)).toBe("aaron"); - } - }; - const store = new Store({ state, mutations: {}, actions, getters }); - store.dispatch("action"); - }); - - test("getters given to mutations", async () => { - expect.assertions(3); - const state = { - beers: { - 1: { - id: 1, - name: "bertinchamps", - tasterID: 1 - } - }, - tasters: { - 1: { - id: 1, - name: "aaron" - } - } - }; - const getters = { - beerTasterName({ state }, beerID) { - return state.tasters[state.beers[beerID].tasterID].name; - }, - bestBeerName({ state }) { - return state.beers[1].name; - } - }; - const mutations = { - mutation({ getters }) { - expect(getters).toBeDefined(); - expect(getters.bestBeerName()).toBe("bertinchamps"); - expect(getters.beerTasterName(1)).toBe("aaron"); - } - }; - const store = new Store({ state, mutations, actions: {}, getters }); - store.commit("mutation"); - }); - - test("can use getters inside a getter", () => { - const getters = { - a({ getters }) { - return `${getters.b()}${getters.c(1)}`; - }, - b() { - return "b"; - }, - c({}, i) { - return `c${i}`; - } - }; - const store = new Store({ getters }); - - expect(store.getters.a()).toBe("bc1"); - }); -}); - -describe("advanced state properties", () => { - test("state in the store is reference equal after mutation", async () => { - const state = {}; - const mutations = { - donothing() {} - }; - const store = new Store({ state, mutations }); - expect(store.state).toBe(state); - store.commit("donothing"); - expect(store.state).toBe(state); - }); - - test("can use array properties in mutations", () => { - expect.assertions(3); - const state = { a: [1, 2, 3] }; - const mutations = { - m({ state }) { - expect(state.a.length).toBe(3); - const l = state.a.push(53); - expect(l).toBe(4); - } - }; - const store = new Store({ state, mutations }); - store.commit("m"); - expect(store.state.a).toEqual([1, 2, 3, 53]); - }); - - test("can use object assign in store", async () => { - const mutations = { - dosomething({ state }) { - Object.assign(state.westmalle, { a: 3, b: 4 }); - } - }; - const store = new Store({ - state: { westmalle: { a: 1, b: 2 } }, - mutations - }); - store.commit("dosomething"); - expect(store.state.westmalle).toEqual({ a: 3, b: 4 }); - }); - - test("aku reactive store state 1", async () => { - const mutations = { - inc({ state }) { - state.counter++; - } - }; - const state = { counter: 0 }; - const store = new Store({ state, mutations }); - expect(store.state.counter).toBe(0); - store.commit("inc", {}); - expect(store.state.counter).toBe(1); - }); -}); - -describe("updates triggered by the store", () => { - test("multiple commits trigger one update", async () => { - let updateCounter = 0; - const state = { n: 1 }; - const mutations = { - inc({ state }, delta) { - state.n += delta; - } - }; - const store = new Store({ state, mutations }); - store.on("update", null, () => updateCounter++); - - store.commit("inc", 14); - expect(updateCounter).toBe(0); - store.commit("inc", 50); - expect(updateCounter).toBe(0); - await nextMicroTick(); - expect(updateCounter).toBe(1); - }); - - test("empty commits do not trigger updates", async () => { - let updateCounter = 0; - const state = { n: 1 }; - const mutations = { - inc({ state }, delta) { - state.n += delta; - }, - noop() {}, - noop2({ state }) { - const val = state.n; - state.n = val; - } - }; - const store = new Store({ state, mutations }); - store.on("update", null, () => updateCounter++); - - store.commit("noop"); - await nextMicroTick(); - expect(updateCounter).toBe(0); - - store.commit("inc", 50); - await nextMicroTick(); - expect(updateCounter).toBe(1); - - store.commit("noop2"); - await nextMicroTick(); - expect(updateCounter).toBe(1); - }); -}); +const Observer = core.Observer; describe("connecting a component to store", () => { let fixture: HTMLElement; @@ -1301,5 +758,4 @@ describe("connecting a component to store", () => { expect(fixture.innerHTML).toBe("
b
"); expect(steps).toEqual(["willpatch", "patched"]); }); - }); diff --git a/tests/store/store.test.ts b/tests/store/store.test.ts new file mode 100644 index 00000000..128de616 --- /dev/null +++ b/tests/store/store.test.ts @@ -0,0 +1,549 @@ +import { Env } from "../../src/component/component"; +import { Store } from "../../src/store/store"; +import { nextMicroTick, nextTick } from "../helpers"; + + +describe("basic use", () => { + test("commit a mutation", () => { + const state = { n: 1 }; + const mutations = { + inc({ state }, delta) { + state.n += delta; + } + }; + const store = new Store({ state, mutations }); + + expect(store.state.n).toBe(1); + store.commit("inc", 14); + expect(store.state.n).toBe(15); + }); + + test("dispatch an action", () => { + const state = { n: 1 }; + const mutations = { + inc({ state }, delta) { + state.n += delta; + } + }; + const actions = { + inc({ commit }, delta) { + commit("inc", delta); + } + }; + const store = new Store({ state, mutations, actions }); + + expect(store.state.n).toBe(1); + store.dispatch("inc", 14); + expect(store.state.n).toBe(15); + }); + + test("dispatch an action + commit a mutation with positional arguments", () => { + const state = { n1: 1, n2: 1, n3: 1 }; + const mutations = { + batchInc({ state }, delta1, delta2, delta3) { + state.n1 += delta1; + state.n2 += delta2; + state.n3 += delta3; + } + }; + const actions = { + batchInc({ commit }, delta1, delta2, delta3) { + commit("batchInc", delta1, delta2, delta3); + } + }; + const store = new Store({ state, mutations, actions }); + + expect(store.state.n1).toBe(1); + expect(store.state.n2).toBe(1); + expect(store.state.n3).toBe(1); + store.dispatch("batchInc", 14, 30, 88); + expect(store.state.n1).toBe(15); + expect(store.state.n2).toBe(31); + expect(store.state.n3).toBe(89); + }); + + test("modifying state outside of mutations trigger error", () => { + const state = { n: 1 }; + const actions = { + inc({ state }) { + state.n++; + } + }; + const store = new Store({ state, mutations: {}, actions }); + + expect(() => store.dispatch("inc")).toThrow(); + expect(() => (store.state.n = 15)).toThrow(); + }); + + test("can dispatch an action in an action", () => { + const state = { n: 1 }; + const mutations = { + inc({ state }, delta) { + state.n += delta; + } + }; + const actions = { + inc({ commit }, delta) { + commit("inc", delta); + }, + inc100({ dispatch }) { + dispatch("inc", 100); + } + }; + const store = new Store({ state, mutations, actions }); + + expect(store.state.n).toBe(1); + store.dispatch("inc100"); + expect(store.state.n).toBe(101); + }); + + test("can commit a mutation in a mutation", () => { + const state = { n: 1 }; + const mutations = { + inc({ state }) { + state.n++; + }, + inc10({ commit }) { + for (let i = 0; i < 10; i++) { + commit("inc"); + } + } + }; + const store = new Store({ state, mutations }); + + expect(store.state.n).toBe(1); + store.commit("inc10"); + expect(store.state.n).toBe(11); + }); + + test("return data from committing a mutation", () => { + const state = { n: 1 }; + const mutations = { + inc({ state }) { + return ++state.n; + } + }; + const store = new Store({ state, mutations }); + + expect(store.state.n).toBe(1); + const res = store.commit("inc"); + expect(store.state.n).toBe(2); + expect(res).toBe(2); + }); + + test("dispatch allow synchronizing between actions", async () => { + const state = { n: 1 }; + const mutations = { + inc({ state }, delta) { + state.n += delta; + }, + setN({ state }, n) { + state.n = n; + } + }; + const actions = { + async dosomething({ commit, dispatch }) { + await dispatch("setTo10"); + commit("inc", 3); + }, + async setTo10({ commit }) { + await Promise.resolve(); + commit("setN", 10); + } + }; + const store = new Store({ state, mutations, actions }); + + expect(store.state.n).toBe(1); + store.dispatch("dosomething"); + expect(store.state.n).toBe(1); + await nextTick(); + expect(store.state.n).toBe(13); + }); + + test("env is given to actions", () => { + expect.assertions(1); + const someEnv = {}; + const actions = { + someaction({ env }) { + expect(env).toBe(someEnv); + } + }; + const store = new Store({ state: {}, actions, env: someEnv }); + + store.dispatch("someaction"); + }); + + test("can have getters from store", async () => { + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps", + tasterID: 1 + } + }, + tasters: { + 1: { + id: 1, + name: "aaron" + } + } + }; + const getters = { + beerTasterName({ state }, beerID) { + return state.tasters[state.beers[beerID].tasterID].name; + }, + bestBeerName({ state }) { + return state.beers[1].name; + } + }; + const store = new Store({ state, mutations: {}, actions: {}, getters }); + expect(store.getters).toBeDefined(); + expect((store.getters).bestBeerName()).toBe("bertinchamps"); + expect((store.getters).beerTasterName(1)).toBe("aaron"); + }); + + test("getters are memoized", async () => { + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps", + tasterID: 1 + } + }, + tasters: { + 1: { + id: 1, + name: "aaron" + } + } + }; + let n = 0; + const getters = { + beerTasterName({ state }, beerID) { + n++; + return state.tasters[state.beers[beerID].tasterID].name; + }, + bestBeerName({ state }) { + n++; + return state.beers[1].name; + } + }; + const store = new Store({ state, mutations: {}, actions: {}, getters }); + expect((store.getters).bestBeerName()).toBe("bertinchamps"); + expect((store.getters).beerTasterName(1)).toBe("aaron"); + expect(n).toBe(2); + expect((store.getters).bestBeerName()).toBe("bertinchamps"); + expect((store.getters).beerTasterName(1)).toBe("aaron"); + expect(n).toBe(2); + }); + + test("getters taking Array as argument aren't memoized", async () => { + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps", + tasterID: 1 + } + } + }; + let n = 0; + const getters = { + getBeerNames({ state }, beerIDs) { + n++; + return beerIDs.map(beerID => { + return state.beers[beerID].name; + }); + } + }; + const store = new Store({ state, mutations: {}, actions: {}, getters }); + expect((store.getters).getBeerNames([1])).toEqual(["bertinchamps"]); + expect(n).toBe(1); + expect((store.getters).getBeerNames([1])).toEqual(["bertinchamps"]); + expect(n).toBe(2); + }); + + test("getters cache is nuked on store changes", async () => { + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps", + tasterID: 1 + } + }, + tasters: { + 1: { + id: 1, + name: "aaron" + }, + 2: { + id: 2, + name: "gery" + } + } + }; + const mutations = { + changeTaster({ state }, { beerID, tasterID }) { + state.beers[beerID].tasterID = tasterID; + } + }; + let n = 0; + const getters = { + beerTasterName({ state }, beerID) { + n++; + return state.tasters[state.beers[beerID].tasterID].name; + } + }; + const store = new Store({ + state, + mutations: mutations, + actions: {}, + getters + }); + expect((store.getters).beerTasterName(1)).toBe("aaron"); + expect(n).toBe(1); + expect((store.getters).beerTasterName(1)).toBe("aaron"); + expect(n).toBe(1); + + store.commit("changeTaster", { beerID: 1, tasterID: 2 }); + await nextTick(); + + expect((store.getters).beerTasterName(1)).toBe("gery"); + expect(n).toBe(2); + }); + + test("getters cache is disabled during a mutation", async () => { + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps" + } + } + }; + const mutations = { + renameBeer({ state, getters }, beerID) { + expect(getters.beerName(beerID)).toBe("bertinchamps"); + state.beers[1].name = "chouffe"; + expect(getters.beerName(beerID)).toBe("chouffe"); + } + }; + let n = 0; + const getters = { + beerName({ state }, beerID) { + n++; + return state.beers[beerID].name; + } + }; + const store = new Store({ + state, + mutations: mutations, + actions: {}, + getters + }); + + store.commit("renameBeer", 1); + expect((store.getters).beerName(1)).toBe("chouffe"); + await nextTick(); + + expect(n).toBe(3); + }); + + test("getters given to actions", async () => { + expect.assertions(3); + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps", + tasterID: 1 + } + }, + tasters: { + 1: { + id: 1, + name: "aaron" + } + } + }; + const getters = { + beerTasterName({ state }, beerID) { + return state.tasters[state.beers[beerID].tasterID].name; + }, + bestBeerName({ state }) { + return state.beers[1].name; + } + }; + const actions = { + action({ getters }) { + expect(getters).toBeDefined(); + expect(getters.bestBeerName()).toBe("bertinchamps"); + expect(getters.beerTasterName(1)).toBe("aaron"); + } + }; + const store = new Store({ state, mutations: {}, actions, getters }); + store.dispatch("action"); + }); + + test("getters given to mutations", async () => { + expect.assertions(3); + const state = { + beers: { + 1: { + id: 1, + name: "bertinchamps", + tasterID: 1 + } + }, + tasters: { + 1: { + id: 1, + name: "aaron" + } + } + }; + const getters = { + beerTasterName({ state }, beerID) { + return state.tasters[state.beers[beerID].tasterID].name; + }, + bestBeerName({ state }) { + return state.beers[1].name; + } + }; + const mutations = { + mutation({ getters }) { + expect(getters).toBeDefined(); + expect(getters.bestBeerName()).toBe("bertinchamps"); + expect(getters.beerTasterName(1)).toBe("aaron"); + } + }; + const store = new Store({ state, mutations, actions: {}, getters }); + store.commit("mutation"); + }); + + test("can use getters inside a getter", () => { + const getters = { + a({ getters }) { + return `${getters.b()}${getters.c(1)}`; + }, + b() { + return "b"; + }, + c({}, i) { + return `c${i}`; + } + }; + const store = new Store({ getters }); + + expect(store.getters.a()).toBe("bc1"); + }); +}); + +describe("advanced state properties", () => { + test("state in the store is reference equal after mutation", async () => { + const state = {}; + const mutations = { + donothing() {} + }; + const store = new Store({ state, mutations }); + expect(store.state).toBe(state); + store.commit("donothing"); + expect(store.state).toBe(state); + }); + + test("can use array properties in mutations", () => { + expect.assertions(3); + const state = { a: [1, 2, 3] }; + const mutations = { + m({ state }) { + expect(state.a.length).toBe(3); + const l = state.a.push(53); + expect(l).toBe(4); + } + }; + const store = new Store({ state, mutations }); + store.commit("m"); + expect(store.state.a).toEqual([1, 2, 3, 53]); + }); + + test("can use object assign in store", async () => { + const mutations = { + dosomething({ state }) { + Object.assign(state.westmalle, { a: 3, b: 4 }); + } + }; + const store = new Store({ + state: { westmalle: { a: 1, b: 2 } }, + mutations + }); + store.commit("dosomething"); + expect(store.state.westmalle).toEqual({ a: 3, b: 4 }); + }); + + test("aku reactive store state 1", async () => { + const mutations = { + inc({ state }) { + state.counter++; + } + }; + const state = { counter: 0 }; + const store = new Store({ state, mutations }); + expect(store.state.counter).toBe(0); + store.commit("inc", {}); + expect(store.state.counter).toBe(1); + }); +}); + +describe("updates triggered by the store", () => { + test("multiple commits trigger one update", async () => { + let updateCounter = 0; + const state = { n: 1 }; + const mutations = { + inc({ state }, delta) { + state.n += delta; + } + }; + const store = new Store({ state, mutations }); + store.on("update", null, () => updateCounter++); + + store.commit("inc", 14); + expect(updateCounter).toBe(0); + store.commit("inc", 50); + expect(updateCounter).toBe(0); + await nextMicroTick(); + expect(updateCounter).toBe(1); + }); + + test("empty commits do not trigger updates", async () => { + let updateCounter = 0; + const state = { n: 1 }; + const mutations = { + inc({ state }, delta) { + state.n += delta; + }, + noop() {}, + noop2({ state }) { + const val = state.n; + state.n = val; + } + }; + const store = new Store({ state, mutations }); + store.on("update", null, () => updateCounter++); + + store.commit("noop"); + await nextMicroTick(); + expect(updateCounter).toBe(0); + + store.commit("inc", 50); + await nextMicroTick(); + expect(updateCounter).toBe(1); + + store.commit("noop2"); + await nextMicroTick(); + expect(updateCounter).toBe(1); + }); +}); + diff --git a/tests/vdom.test.ts b/tests/vdom.test.ts index fddbc5fe..ec52b503 100644 --- a/tests/vdom.test.ts +++ b/tests/vdom.test.ts @@ -1,4 +1,5 @@ -import { h, patch, init } from "../src/vdom"; +import { h, patch } from "../src/vdom"; +import { init } from "../src/vdom/vdom"; function map(list, fn) { var ret: any[] = []; diff --git a/tsconfig.json b/tsconfig.json index 43e24aaa..eff00b34 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "noImplicitThis": true, "removeComments": false, "declaration": true, - "target": "es5", + "target": "esnext", "outDir": "dist", "alwaysStrict": true, "noUnusedLocals": true, @@ -15,5 +15,5 @@ "strictPropertyInitialization": true, "strictNullChecks": true }, - "include": ["src/*.ts"] + "include": ["src/**/*.ts","src/*.ts"] }