Compare commits

..

2 Commits

Author SHA1 Message Date
Géry Debongnie c085eef441 experiment: add support for attached components 2024-10-31 07:39:24 +01:00
Géry Debongnie b4f84513a6 [REF] app: reorganize startup code
The goal of this refactoring is to delay the instantiation of the
component node until we are ready to mount the component. This is useful
so we know if a component will be attached or mounted (which means that
it needs to have a template, or not)
2024-10-29 13:49:04 +01:00
85 changed files with 1338 additions and 4415 deletions
-3
View File
@@ -66,9 +66,6 @@ The `config` object is an object with some of the following keys:
needs a template. If undefined is returned, owl looks into the app templates.
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
- **`customDirectives (object)`**: if given, the corresponding function on the object will be called
on the template custom directives: `t-custom-*` (see [Custom Directives](templates.md#custom-directives)).
- **`globalValues (object)`**: Global object of elements available at compilations.
## `mount` helper
-22
View File
@@ -320,28 +320,6 @@ class ComponentB extends owl.Component {
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
### `slots` prop
If a component that uses [slots](slots.md) also lists or validates its props, then
you will have to explicitely allow the `slots` prop (with an `Object` type), or
allow extra props using the `*` notation mentioned above. This is because slots
are provided to a component [as props](slots.md#slots-and-props).
For example:
```js
class MyComponent extends Component {
static props = [someProp, slots?];
}
class MyComponentWithValidation extends Component {
static props = {
someProp: {type: Number, optional: true},
slots : {type: Object, optional: true},
}
}
```
## Good Practices
A `props` object is a collection of values that come from the parent. As such,
+11 -53
View File
@@ -18,7 +18,6 @@
- [Sub Templates](#sub-templates)
- [Dynamic Sub Templates](#dynamic-sub-templates)
- [Debugging](#debugging)
- [Custom Directives](#custom-directives)
- [Fragments](#fragments)
- [Inline templates](#inline-templates)
- [Rendering svg](#rendering-svg)
@@ -56,19 +55,17 @@ extensions.
For reference, here is a list of all standard QWeb directives:
| Name | Description |
| ------------------------------ | ----------------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](translations.md) |
| `t-translation-context` | [Context of translations within a node](translations.md) |
| `t-translation-context-*` | [Context of translation for a specific node attribute](translations.md) |
| Name | Description |
| ------------------------------ | --------------------------------------------------------------- |
| `t-esc` | [Outputting safely a value](#outputting-data) |
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
| `t-translation` | [Disabling the translation of a node](translations.md) |
The component system in Owl requires additional directives, to express various
needs. Here is a list of all Owl specific directives:
@@ -83,7 +80,6 @@ needs. Here is a list of all Owl specific directives:
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
| `t-model` | [Form input bindings](input_bindings.md) |
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
| `t-custom-*` | [Rendering nodes with custom directives](#custom-directives) |
## QWeb Template Reference
@@ -193,15 +189,6 @@ The first `t-out` will act as a `t-esc` directive, which means that the content
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
this will be injected as html.
`markup` can also be used as a tag function, allowing the interpolated values to
be safely escaped:
```js
const maliciousInput = "<script>alert('💥💥')</script>";
// <b>&lt;script&gt;alert(&#x27;💥💥&#x27;)&lt;/script&gt;</b>
const value = markup`<b>${maliciousInput}</b>`;
```
### Setting Variables
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
@@ -601,35 +588,6 @@ will stop execution if the browser dev tools are open.
will print 42 to the console.
### Custom Directives
Owl 2 supports the declaration of custom directives. To use them, an Object of functions needs to be configured on the owl APP:
```js
new App(..., {
customDirectives: {
test_directive: function (el, value) {
el.setAttribute("t-on-click", value);
}
}
});
```
The functions will be called when a custom directive with the name of the
function is found. The original element will be replaced with the one
modified by the function.
This :
```xml
<div t-custom-test_directive="click" />
```
will be replaced by :
```xml
<div t-on-click="value"/>
```
## Fragments
Owl 2 supports templates with an arbitrary number of root elements, or even just
+5 -37
View File
@@ -1,28 +1,17 @@
# 🦉 Translations 🦉
If properly setup, Owl can translate all rendered templates. To do
so, it needs a translate function, which takes
- a string (the term to translate)
- a string (the translation context of the term)
and returns a string.
so, it needs a translate function, which takes a string and returns a string.
For example:
```js
const translations = {
fr: {
hello: "bonjour",
yes: "oui",
no: "non",
},
pt: {
hello: "bom dia",
yes: "sim",
no: "não",
},
hello: "bonjour",
yes: "oui",
no: "non",
};
const translateFn = (str, ctx) => translations[ctx]?.[str] || str;
const translateFn = (str) => translations[str] || str;
const app = new App(Root, { templates, tranaslateFn });
// ...
@@ -38,11 +27,6 @@ Once setup, all rendered templates will be translated using `translateFn`:
`placeholder`, `label` and `alt`,
- translating text nodes can be disabled with the special attribute `t-translation`,
if its value is `off`.
- the translate function receives as second parameter a context that can be used
to contextualized the translation. That context can be set globally on a node
and its children by using `t-translation-context`. If a specific node
attribute `x` needs another context, that context can be specified with a
special directive `t-translation-context-x`.
So, with the above `translateFn`, the following templates:
@@ -62,22 +46,6 @@ will be rendered as:
<input placeholder="bonjour" other="yes"/>
```
and the following template:
```xml
<div t-translation-context="fr" title="hello">hello</div>
<div>Are you sure?</div>
<input t-translation-context-placeholder="pt" placeholder="hello" other="yes"/>
```
will be rendered as:
```xml
<div title="bonjour">bonjour</div>
<div>Are you sure?</div>
<input placeholder="bom dia" other="yes"/>
```
Note that the translation is done during the compilation of the template, not
when it is rendered.
-16
View File
@@ -82,22 +82,6 @@ in the sources tab as well.
<img src="screenshots/function_menu.png"/>
Using the right-click context menu on a property also allows to observe variables. Observed variables will
be sent to a dedicated section of the details window and their value will be refreshed every 200ms. These
variables are only shown when they are found and their access path will be kept in memory inside the
browser so that it will always persist until the user decides to stop observing the variable. As in the
browser's devtools, observed objects are displayed in reduced form and cannot be interacted with. It is
still possible to send them to the console or remove them from the list using right-click.
<img src="screenshots/observe_variables.png"/>
The last section of the details window is filled with the component's lifecycle hooks. Using right click on
them allows to place breakpoints inside the hook (either on its instance or class, hooks like mounted and
willStart cannot have instance-based breakpoints because they will never trigger). Conditions in conditional
breakpoints will be evaluated in the context of the component's definition.
<img src="screenshots/hooks.png"/>
There are several icons available to perform several of the actions described before in the components
tree context menu and all these actions are also available by opening the menu by right-clicking on the
component's name. Using the left click on the component's name will focus it in the components tree.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 177 KiB

+74 -340
View File
@@ -276,39 +276,13 @@ function inOwnerDocument(el) {
const rootNode = el.getRootNode();
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
}
/**
* Determine whether the given element is contained in a specific root documnet:
* either directly or with a shadow root in between or in an iframe.
*/
function isAttachedToDocument(element, documentElement) {
let current = element;
const shadowRoot = documentElement.defaultView.ShadowRoot;
while (current) {
if (current === documentElement) {
return true;
}
if (current.parentNode) {
current = current.parentNode;
}
else if (current instanceof shadowRoot && current.host) {
current = current.host;
}
else {
return false;
}
}
return false;
}
function validateTarget(target) {
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
const document = target && target.ownerDocument;
if (document) {
if (!document.defaultView) {
throw new OwlError("Cannot mount a component: the target document is not attached to a window (defaultView is missing)");
}
const HTMLElement = document.defaultView.HTMLElement;
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
if (!isAttachedToDocument(target, document)) {
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
throw new OwlError("Cannot mount a component on a detached dom node");
}
return;
@@ -345,40 +319,12 @@ async function loadFile(url) {
*/
class Markup extends String {
}
function htmlEscape(str) {
if (str instanceof Markup) {
return str;
}
if (str === undefined) {
return markup("");
}
if (typeof str === "number") {
return markup(String(str));
}
[
["&", "&amp;"],
["<", "&lt;"],
[">", "&gt;"],
["'", "&#x27;"],
['"', "&quot;"],
["`", "&#x60;"],
].forEach((pairs) => {
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
});
return markup(str);
}
function markup(valueOrStrings, ...placeholders) {
if (!Array.isArray(valueOrStrings)) {
return new Markup(valueOrStrings);
}
const strings = valueOrStrings;
let acc = "";
let i = 0;
for (; i < placeholders.length; ++i) {
acc += strings[i] + htmlEscape(placeholders[i]);
}
acc += strings[i];
return new Markup(acc);
/*
* Marks a value as safe, that is, a value that can be injected as HTML directly.
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
*/
function markup(value) {
return new Markup(value);
}
function createEventHandler(rawEvent) {
@@ -1679,13 +1625,6 @@ function makeRootFiber(node) {
fibersInError.delete(current);
fibersInError.delete(root);
current.appliedToDom = false;
if (current instanceof RootFiber) {
// it is possible that this fiber is a fiber that crashed while being
// mounted, so the mounted list is possibly corrupted. We restore it to
// its normal initial state (which is empty list or a list with a mount
// fiber.
current.mounted = current instanceof MountFiber ? [current] : [];
}
}
return current;
}
@@ -1802,7 +1741,6 @@ class RootFiber extends Fiber {
const node = this.node;
this.locked = true;
let current = undefined;
let mountedFibers = this.mounted;
try {
// Step 1: calling all willPatch lifecycle hooks
for (current of this.willPatch) {
@@ -1822,6 +1760,7 @@ class RootFiber extends Fiber {
node._patch();
this.locked = false;
// Step 4: calling all mounted lifecycle hooks
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
current = current;
if (current.appliedToDom) {
@@ -1842,15 +1781,6 @@ class RootFiber extends Fiber {
}
}
catch (e) {
// if mountedFibers is not empty, this means that a crash occured while
// calling the mounted hooks of some component. So, there may still be
// some component that have been mounted, but for which the mounted hooks
// have not been called. Here, we remove the willUnmount hooks for these
// specific component to prevent a worse situation (willUnmount being
// called even though mounted has not been called)
for (let fiber of mountedFibers) {
fiber.node.willUnmount = [];
}
this.locked = false;
node.app.handleError({ fiber: current || this, error: e });
}
@@ -2340,12 +2270,6 @@ function collectionsProxyHandler(target, callback, targetRawType) {
}
let currentNode = null;
function saveCurrent() {
let n = currentNode;
return () => {
currentNode = n;
};
}
function getCurrent() {
if (!currentNode) {
throw new OwlError("No active component (a hook function should only be called in 'setup')");
@@ -3297,9 +3221,6 @@ class TemplateSet {
}
}
this.getRawTemplate = config.getTemplate;
this.customDirectives = config.customDirectives || {};
this.runtimeUtils = { ...helpers, __globals__: config.globalValues || {} };
this.hasGlobalValues = Boolean(config.globalValues && Object.keys(config.globalValues).length);
}
static registerTemplate(name, fn) {
globalTemplates[name] = fn;
@@ -3356,7 +3277,7 @@ class TemplateSet {
this.templates[name] = function (context, parent) {
return templates[name].call(this, context, parent);
};
const template = templateFn(this, bdom, this.runtimeUtils);
const template = templateFn(this, bdom, helpers);
this.templates[name] = template;
}
return this.templates[name];
@@ -3407,7 +3328,7 @@ TemplateSet.registerTemplate("__portal__", portalTemplate);
//------------------------------------------------------------------------------
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(",");
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date".split(",");
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
and: "&&",
or: "||",
@@ -3792,7 +3713,6 @@ function createContext(parentCtx, params) {
index: 0,
forceNewBlock: true,
translate: parentCtx.translate,
translationCtx: parentCtx.translationCtx,
tKeyExpr: null,
nameSpace: parentCtx.nameSpace,
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
@@ -3850,16 +3770,7 @@ class CodeTarget {
return key;
}
}
const TRANSLATABLE_ATTRS = [
"alt",
"aria-label",
"aria-placeholder",
"aria-roledescription",
"aria-valuetext",
"label",
"placeholder",
"title",
];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
class CodeGenerator {
constructor(ast, options) {
@@ -3889,9 +3800,6 @@ class CodeGenerator {
this.dev = options.dev || false;
this.ast = ast;
this.templateName = options.name;
if (options.hasGlobalValues) {
this.helpers.add("__globals__");
}
}
generateCode() {
const ast = this.ast;
@@ -3904,7 +3812,6 @@ class CodeGenerator {
forceNewBlock: false,
isLast: true,
translate: true,
translationCtx: "",
tKeyExpr: null,
});
// define blocks and utility functions
@@ -4042,9 +3949,9 @@ class CodeGenerator {
})
.join("");
}
translate(str, translationCtx) {
translate(str) {
const match = translationRE.exec(str);
return match[1] + this.translateFn(match[2], translationCtx) + match[3];
return match[1] + this.translateFn(match[2]) + match[3];
}
/**
* @returns the newly created block name, if any
@@ -4085,9 +3992,7 @@ class CodeGenerator {
return this.compileTSlot(ast, ctx);
case 16 /* TTranslation */:
return this.compileTTranslation(ast, ctx);
case 17 /* TTranslationContext */:
return this.compileTTranslationContext(ast, ctx);
case 18 /* TPortal */:
case 17 /* TPortal */:
return this.compileTPortal(ast, ctx);
}
}
@@ -4125,7 +4030,7 @@ class CodeGenerator {
let { block, forceNewBlock } = ctx;
let value = ast.value;
if (value && ctx.translate !== false) {
value = this.translate(value, ctx.translationCtx);
value = this.translate(value);
}
if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " ");
@@ -4160,7 +4065,6 @@ class CodeGenerator {
return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`;
}
compileTDomNode(ast, ctx) {
var _a;
let { block, forceNewBlock } = ctx;
const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;
let codeIdx = this.target.code.length;
@@ -4216,8 +4120,7 @@ class CodeGenerator {
}
}
else if (this.translatableAttributes.includes(key)) {
const attrTranslationCtx = ((_a = ast.attrsTranslationCtx) === null || _a === void 0 ? void 0 : _a[key]) || ctx.translationCtx;
attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
attrs[key] = this.translateFn(ast.attrs[key]);
}
else {
expr = `"${ast.attrs[key]}"`;
@@ -4539,7 +4442,7 @@ class CodeGenerator {
const isNewBlock = !block || forceNewBlock;
let codeIdx = this.target.code.length;
if (isNewBlock) {
const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
const n = ast.content.filter((c) => c.type !== 6 /* TSet */).length;
let result = null;
if (n <= 1) {
for (let child of ast.content) {
@@ -4553,15 +4456,15 @@ class CodeGenerator {
let index = 0;
for (let i = 0, l = ast.content.length; i < l; i++) {
const child = ast.content[i];
const forceNewBlock = !child.hasNoRepresentation;
const isTSet = child.type === 6 /* TSet */;
const subCtx = createContext(ctx, {
block,
index,
forceNewBlock,
forceNewBlock: !isTSet,
isLast: ctx.isLast && i === l - 1,
});
this.compileAST(child, subCtx);
if (forceNewBlock) {
if (!isTSet) {
index++;
}
}
@@ -4661,7 +4564,7 @@ class CodeGenerator {
else {
let value;
if (ast.defaultValue) {
const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue);
const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue);
if (ast.value) {
value = `withDefault(${expr}, ${defaultValue})`;
}
@@ -4695,10 +4598,9 @@ class CodeGenerator {
* "some-prop" "state" "'some-prop': ctx['state']"
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
*/
formatProp(name, value, attrsTranslationCtx, translationCtx) {
formatProp(name, value) {
if (name.endsWith(".translate")) {
const attrTranslationCtx = (attrsTranslationCtx === null || attrsTranslationCtx === void 0 ? void 0 : attrsTranslationCtx[name]) || translationCtx;
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
value = toStringExpression(this.translateFn(value));
}
else {
value = this.captureExpression(value);
@@ -4714,14 +4616,14 @@ class CodeGenerator {
case "translate":
break;
default:
throw new OwlError(`Invalid prop suffix: ${suffix}`);
throw new OwlError("Invalid prop suffix");
}
}
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
return `${name}: ${value || undefined}`;
}
formatPropObject(obj, attrsTranslationCtx, translationCtx) {
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v, attrsTranslationCtx, translationCtx));
formatPropObject(obj) {
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
}
getPropString(props, dynProps) {
let propString = `{${props.join(",")}}`;
@@ -4734,9 +4636,7 @@ class CodeGenerator {
let { block } = ctx;
// props
const hasSlotsProp = "slots" in (ast.props || {});
const props = ast.props
? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
: [];
const props = ast.props ? this.formatPropObject(ast.props) : [];
// slots
let slotDef = "";
if (ast.slots) {
@@ -4759,7 +4659,7 @@ class CodeGenerator {
params.push(`__scope: "${scope}"`);
}
if (ast.slots[slotName].attrs) {
params.push(...this.formatPropObject(ast.slots[slotName].attrs, ast.slots[slotName].attrsTranslationCtx, ctx.translationCtx));
params.push(...this.formatPropObject(ast.slots[slotName].attrs));
}
const slotInfo = `{${params.join(", ")}}`;
slotStr.push(`'${slotName}': ${slotInfo}`);
@@ -4864,16 +4764,15 @@ class CodeGenerator {
isMultiple = isMultiple || this.slotNames.has(ast.name);
this.slotNames.add(ast.name);
}
const attrs = { ...ast.attrs };
const dynProps = attrs["t-props"];
delete attrs["t-props"];
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
if (ast.attrs) {
delete ast.attrs["t-props"];
}
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
if (isMultiple) {
key = this.generateComponentKey(key);
}
const props = ast.attrs
? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
: [];
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
const scope = this.getPropString(props, dynProps);
if (ast.defaultContent) {
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
@@ -4906,12 +4805,6 @@ class CodeGenerator {
}
return null;
}
compileTTranslationContext(ast, ctx) {
if (ast.content) {
return this.compileAST(ast.content, Object.assign({}, ctx, { translationCtx: ast.translationCtx }));
}
return null;
}
compileTPortal(ast, ctx) {
if (!this.staticDefs.find((d) => d.id === "Portal")) {
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
@@ -4945,43 +4838,38 @@ class CodeGenerator {
// Parser
// -----------------------------------------------------------------------------
const cache = new WeakMap();
function parse(xml, customDir) {
const ctx = {
inPreTag: false,
customDirectives: customDir,
};
function parse(xml) {
if (typeof xml === "string") {
const elem = parseXML(`<t>${xml}</t>`).firstChild;
return _parse(elem, ctx);
return _parse(elem);
}
let ast = cache.get(xml);
if (!ast) {
// we clone here the xml to prevent modifying it in place
ast = _parse(xml.cloneNode(true), ctx);
ast = _parse(xml.cloneNode(true));
cache.set(xml, ast);
}
return ast;
}
function _parse(xml, ctx) {
function _parse(xml) {
normalizeXML(xml);
const ctx = { inPreTag: false };
return parseNode(xml, ctx) || { type: 0 /* Text */, value: "" };
}
function parseNode(node, ctx) {
if (!(node instanceof Element)) {
return parseTextCommentNode(node, ctx);
}
return (parseTCustom(node, ctx) ||
parseTDebugLog(node, ctx) ||
return (parseTDebugLog(node, ctx) ||
parseTForEach(node, ctx) ||
parseTIf(node, ctx) ||
parseTPortal(node, ctx) ||
parseTCall(node, ctx) ||
parseTCallBlock(node) ||
parseTTranslation(node, ctx) ||
parseTTranslationContext(node, ctx) ||
parseTKey(node, ctx) ||
parseTEscNode(node, ctx) ||
parseTOutNode(node, ctx) ||
parseTKey(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTSlot(node, ctx) ||
parseComponent(node, ctx) ||
parseDOMNode(node, ctx) ||
@@ -5014,64 +4902,25 @@ function parseTextCommentNode(node, ctx) {
}
return null;
}
function parseTCustom(node, ctx) {
if (!ctx.customDirectives) {
return null;
}
const nodeAttrsNames = node.getAttributeNames();
for (let attr of nodeAttrsNames) {
if (attr === "t-custom" || attr === "t-custom-") {
throw new OwlError("Missing custom directive name with t-custom directive");
}
if (attr.startsWith("t-custom-")) {
const directiveName = attr.split(".")[0].slice(9);
const customDirective = ctx.customDirectives[directiveName];
if (!customDirective) {
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
}
const value = node.getAttribute(attr);
const modifiers = attr.split(".").slice(1);
node.removeAttribute(attr);
try {
customDirective(node, value, modifiers);
}
catch (error) {
throw new OwlError(`Custom directive "${directiveName}" throw the following error: ${error}`);
}
return parseNode(node, ctx);
}
}
return null;
}
// -----------------------------------------------------------------------------
// debugging
// -----------------------------------------------------------------------------
function parseTDebugLog(node, ctx) {
if (node.hasAttribute("t-debug")) {
node.removeAttribute("t-debug");
const content = parseNode(node, ctx);
const ast = {
return {
type: 12 /* TDebug */,
content,
content: parseNode(node, ctx),
};
if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
if (node.hasAttribute("t-log")) {
const expr = node.getAttribute("t-log");
node.removeAttribute("t-log");
const content = parseNode(node, ctx);
const ast = {
return {
type: 13 /* TLog */,
expr,
content,
content: parseNode(node, ctx),
};
if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
return null;
}
@@ -5100,7 +4949,6 @@ function parseDOMNode(node, ctx) {
node.removeAttribute("t-ref");
const nodeAttrsNames = node.getAttributeNames();
let attrs = null;
let attrsTranslationCtx = null;
let on = null;
let model = null;
for (let attr of nodeAttrsNames) {
@@ -5161,11 +5009,6 @@ function parseDOMNode(node, ctx) {
else if (attr === "xmlns") {
ns = value;
}
else if (attr.startsWith("t-translation-context-")) {
const attrName = attr.slice(22);
attrsTranslationCtx = attrsTranslationCtx || {};
attrsTranslationCtx[attrName] = value;
}
else if (attr !== "t-name") {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
@@ -5187,7 +5030,6 @@ function parseDOMNode(node, ctx) {
tag: tagName,
dynamicTag,
attrs,
attrsTranslationCtx,
on,
ref,
content: children,
@@ -5301,19 +5143,11 @@ function parseTKey(node, ctx) {
}
const key = node.getAttribute("t-key");
node.removeAttribute("t-key");
const content = parseNode(node, ctx);
if (!content) {
const body = parseNode(node, ctx);
if (!body) {
return null;
}
const ast = {
type: 10 /* TKey */,
expr: key,
content,
};
if (content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
return { type: 10 /* TKey */, expr: key, content: body };
}
// -----------------------------------------------------------------------------
// t-call
@@ -5336,15 +5170,7 @@ function parseTCall(node, ctx) {
if (ast && ast.type === 11 /* TComponent */) {
return {
...ast,
slots: {
default: {
content: tcall,
scope: null,
on: null,
attrs: null,
attrsTranslationCtx: null,
},
},
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
};
}
}
@@ -5422,7 +5248,7 @@ function parseTSetNode(node, ctx) {
if (node.textContent !== node.innerHTML) {
body = parseChildren(node, ctx);
}
return { type: 6 /* TSet */, name, value, defaultValue, body, hasNoRepresentation: true };
return { type: 6 /* TSet */, name, value, defaultValue, body };
}
// -----------------------------------------------------------------------------
// Components
@@ -5459,15 +5285,9 @@ function parseComponent(node, ctx) {
node.removeAttribute("t-slot-scope");
let on = null;
let props = null;
let propsTranslationCtx = null;
for (let name of node.getAttributeNames()) {
const value = node.getAttribute(name);
if (name.startsWith("t-translation-context-")) {
const attrName = name.slice(22);
propsTranslationCtx = propsTranslationCtx || {};
propsTranslationCtx[attrName] = value;
}
else if (name.startsWith("t-")) {
if (name.startsWith("t-")) {
if (name.startsWith("t-on-")) {
on = on || {};
on[name.slice(5)] = value;
@@ -5511,7 +5331,6 @@ function parseComponent(node, ctx) {
const slotAst = parseNode(slotNode, ctx);
let on = null;
let attrs = null;
let attrsTranslationCtx = null;
let scope = null;
for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName);
@@ -5519,11 +5338,6 @@ function parseComponent(node, ctx) {
scope = value;
continue;
}
else if (attributeName.startsWith("t-translation-context-")) {
const attrName = attributeName.slice(22);
attrsTranslationCtx = attrsTranslationCtx || {};
attrsTranslationCtx[attrName] = value;
}
else if (attributeName.startsWith("t-on-")) {
on = on || {};
on[attributeName.slice(5)] = value;
@@ -5534,32 +5348,17 @@ function parseComponent(node, ctx) {
}
}
slots = slots || {};
slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
slots[name] = { content: slotAst, on, attrs, scope };
}
// default slot
const defaultContent = parseChildNodes(clone, ctx);
slots = slots || {};
// t-set-slot="default" has priority over content
if (defaultContent && !slots.default) {
slots.default = {
content: defaultContent,
on,
attrs: null,
attrsTranslationCtx: null,
scope: defaultSlotScope,
};
slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
}
}
return {
type: 11 /* TComponent */,
name,
isDynamic,
dynamicProps,
props,
propsTranslationCtx,
slots,
on,
};
return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
}
// -----------------------------------------------------------------------------
// Slots
@@ -5571,7 +5370,6 @@ function parseTSlot(node, ctx) {
const name = node.getAttribute("t-slot");
node.removeAttribute("t-slot");
let attrs = null;
let attrsTranslationCtx = null;
let on = null;
for (let attributeName of node.getAttributeNames()) {
const value = node.getAttribute(attributeName);
@@ -5579,11 +5377,6 @@ function parseTSlot(node, ctx) {
on = on || {};
on[attributeName.slice(5)] = value;
}
else if (attributeName.startsWith("t-translation-context-")) {
const attrName = attributeName.slice(22);
attrsTranslationCtx = attrsTranslationCtx || {};
attrsTranslationCtx[attrName] = value;
}
else {
attrs = attrs || {};
attrs[attributeName] = value;
@@ -5593,59 +5386,19 @@ function parseTSlot(node, ctx) {
type: 14 /* TSlot */,
name,
attrs,
attrsTranslationCtx,
on,
defaultContent: parseChildNodes(node, ctx),
};
}
// -----------------------------------------------------------------------------
// Translation
// -----------------------------------------------------------------------------
function wrapInTTranslationAST(r) {
const ast = { type: 16 /* TTranslation */, content: r };
if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
function parseTTranslation(node, ctx) {
if (node.getAttribute("t-translation") !== "off") {
return null;
}
node.removeAttribute("t-translation");
const result = parseNode(node, ctx);
if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
const children = result.content.map(wrapInTTranslationAST);
return makeASTMulti(children);
}
return wrapInTTranslationAST(result);
}
// -----------------------------------------------------------------------------
// Translation Context
// -----------------------------------------------------------------------------
function wrapInTTranslationContextAST(r, translationCtx) {
const ast = {
type: 17 /* TTranslationContext */,
content: r,
translationCtx,
return {
type: 16 /* TTranslation */,
content: parseNode(node, ctx),
};
if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
function parseTTranslationContext(node, ctx) {
const translationCtx = node.getAttribute("t-translation-context");
if (!translationCtx) {
return null;
}
node.removeAttribute("t-translation-context");
const result = parseNode(node, ctx);
if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
return makeASTMulti(children);
}
return wrapInTTranslationContextAST(result, translationCtx);
}
// -----------------------------------------------------------------------------
// Portal
@@ -5664,7 +5417,7 @@ function parseTPortal(node, ctx) {
};
}
return {
type: 18 /* TPortal */,
type: 17 /* TPortal */,
target,
content,
};
@@ -5690,13 +5443,6 @@ function parseChildren(node, ctx) {
}
return children;
}
function makeASTMulti(children) {
const ast = { type: 3 /* Multi */, content: children };
if (children.every((c) => c.hasNoRepresentation)) {
ast.hasNoRepresentation = true;
}
return ast;
}
/**
* Parse all the child nodes of a given node and return an ast if possible.
* In the case there are multiple children, they are wrapped in a astmulti.
@@ -5709,7 +5455,7 @@ function parseChildNodes(node, ctx) {
case 1:
return children[0];
default:
return makeASTMulti(children);
return { type: 3 /* Multi */, content: children };
}
}
/**
@@ -5787,11 +5533,9 @@ function normalizeXML(el) {
normalizeTEscTOut(el);
}
function compile(template, options = {
hasGlobalValues: false,
}) {
function compile(template, options = {}) {
// parsing
const ast = parse(template, options.customDirectives);
const ast = parse(template);
// some work
const hasSafeContext = template instanceof Node
? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
@@ -5813,7 +5557,7 @@ function compile(template, options = {
}
// do not modify manually. This file is generated by the release script.
const version = "2.8.1";
const version = "2.4.0";
// -----------------------------------------------------------------------------
// Scheduler
@@ -5824,7 +5568,6 @@ class Scheduler {
this.frame = 0;
this.delayedRenders = [];
this.cancelledNodes = new Set();
this.processing = false;
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
}
addFiber(fiber) {
@@ -5855,10 +5598,6 @@ class Scheduler {
}
}
processTasks() {
if (this.processing) {
return;
}
this.processing = true;
this.frame = 0;
for (let node of this.cancelledNodes) {
node._destroy();
@@ -5872,7 +5611,6 @@ class Scheduler {
this.tasks.delete(task);
}
}
this.processing = false;
}
processFiber(fiber) {
if (fiber.root !== fiber) {
@@ -5892,14 +5630,7 @@ class Scheduler {
if (!hasError) {
fiber.complete();
}
// at this point, the fiber should have been applied to the DOM, so we can
// remove it from the task list. If it is not the case, it means that there
// was an error and an error handler triggered a new rendering that recycled
// the fiber, so in that case, we actually want to keep the fiber around,
// otherwise it will just be ignored.
if (fiber.appliedToDom) {
this.tasks.delete(fiber);
}
this.tasks.delete(fiber);
}
}
}
@@ -5908,6 +5639,13 @@ class Scheduler {
Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
let hasBeenLogged = false;
const DEV_MSG = () => {
const hash = window.owl ? window.owl.__info__.hash : "master";
return `Owl is running in 'dev' mode.
This is not suitable for production use.
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
};
const apps = new Set();
window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = { apps, Fiber, RootFiber, toRaw, reactive });
class App extends TemplateSet {
@@ -5924,7 +5662,7 @@ class App extends TemplateSet {
}
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
if (this.dev && !config.test && !hasBeenLogged) {
console.info(`Owl is running in 'dev' mode.`);
console.info(DEV_MSG());
hasBeenLogged = true;
}
const env = config.env || {};
@@ -5948,9 +5686,7 @@ class App extends TemplateSet {
if (config.env) {
this.env = config.env;
}
const restore = saveCurrent();
const node = this.makeNode(Root, props);
restore();
if (config.env) {
this.env = env;
}
@@ -6276,14 +6012,12 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
customDirectives: this.customDirectives,
hasGlobalValues: this.hasGlobalValues,
});
};
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, htmlEscape, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
__info__.date = '2025-09-23T07:17:45.055Z';
__info__.hash = '5211116';
__info__.date = '2024-09-30T08:49:29.420Z';
__info__.hash = 'eb2b32a';
__info__.url = 'https://github.com/odoo/owl';
+212 -827
View File
File diff suppressed because it is too large Load Diff
+2 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.8.1",
"version": "2.4.0",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
@@ -32,10 +32,7 @@
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
"lint": "eslint src/**/*.ts tests/**/*.ts",
"release": "node tools/release.js",
"compile_templates": "node tools/compile_owl_templates.mjs"
},
"bin": {
"compile_owl_templates": "tools/compile_owl_templates.mjs"
"compile_templates": "node tools/compile_xml.js"
},
"repository": {
"type": "git",
@@ -49,7 +46,6 @@
"homepage": "https://github.com/odoo/owl#readme",
"devDependencies": {
"@types/jest": "^27.0.1",
"@types/jsdom": "^21.1.7",
"@types/node": "^14.11.8",
"@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.1",
@@ -101,8 +97,5 @@
"prettier": {
"printWidth": 100,
"endOfLine": "auto"
},
"dependencies": {
"jsdom": "^25.0.1"
}
}
+25 -33
View File
@@ -1,6 +1,6 @@
import pkg from "./package.json";
import git from "git-rev-sync";
import typescript from "rollup-plugin-typescript2";
import typescript from 'rollup-plugin-typescript2';
import { terser } from "rollup-plugin-terser";
import dts from "rollup-plugin-dts";
@@ -12,7 +12,7 @@ const ES_FILENAME = "dist/owl.es.js";
if (pkg.module !== ES_FILENAME || pkg.main !== CJS_FILENAME) {
throw new Error("package.json has been modified. Build script should be updated accordingly");
}
}
const outro = `
__info__.date = '${new Date().toISOString()}';
@@ -21,37 +21,39 @@ __info__.url = 'https://github.com/odoo/owl';
`;
switch (process.argv[4]) {
case "compiler":
(input = "src/compiler/index.ts"),
(output = [getConfigForFormat("cjs", "dist/compiler.js", "")]);
case "compiler":
input = "src/compiler/index.ts",
output = [
getConfigForFormat('cjs', 'dist/compiler.js', ''),
]
break;
case "runtime":
input = "src/runtime/index.ts";
output = [
getConfigForFormat("esm", addSuffix(ES_FILENAME, "runtime"), outro),
getConfigForFormat("cjs", addSuffix(CJS_FILENAME, "runtime"), outro),
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro),
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro, true),
];
getConfigForFormat('esm', addSuffix(ES_FILENAME, 'runtime'), outro),
getConfigForFormat('cjs', addSuffix(CJS_FILENAME, 'runtime'), outro),
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro),
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro, true),
]
break;
default:
(input = "src/index.ts"),
(output = [
getConfigForFormat("esm", ES_FILENAME, outro),
getConfigForFormat("cjs", CJS_FILENAME, outro),
getConfigForFormat("iife", IIFE_FILENAME, outro),
getConfigForFormat("iife", IIFE_FILENAME, outro, true),
]);
}
input = "src/index.ts",
output = [
getConfigForFormat('esm', ES_FILENAME, outro),
getConfigForFormat('cjs', CJS_FILENAME, outro),
getConfigForFormat('iife', IIFE_FILENAME, outro),
getConfigForFormat('iife', IIFE_FILENAME, outro, true),
]
}
/**
* Generate from a string depicting a path a new path for the minified version.
* @param {string} pkgFileName file name
*/
function addSuffix(pkgFileName, suffix) {
const parts = pkgFileName.split(".");
const parts = pkgFileName.split('.');
parts.splice(parts.length - 1, 0, suffix);
return parts.join(".");
return parts.join('.');
}
/**
@@ -69,7 +71,7 @@ function getConfigForFormat(format, generatedFileName, outro, minified = false)
outro: outro,
freeze: false,
plugins: minified ? [terser()] : [],
indent: " ", // indent with 4 spaces
indent: ' ', // indent with 4 spaces
};
}
@@ -79,19 +81,9 @@ export default [
output,
plugins: [
typescript({
useTsconfigDeclarationDir: true,
useTsconfigDeclarationDir: true
}),
],
},
{
input: "src/compiler/standalone/index.ts",
output: [{ file: "dist/compile_templates.mjs", format: "es" }],
external: ["fs", "fs/promises", "path", "jsdom"],
plugins: [
typescript({
useTsconfigDeclarationDir: true,
}),
],
]
},
{
input: "dist/types/index.d.ts",
@@ -1,4 +1,4 @@
import { OwlError } from "../common/owl_error";
import { OwlError } from "./owl_error";
/**
* Owl QWeb Expression Parser
@@ -28,7 +28,7 @@ import { OwlError } from "../common/owl_error";
//------------------------------------------------------------------------------
const RESERVED_WORDS =
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
-4
View File
@@ -1,4 +0,0 @@
export type customDirectives = Record<
string,
(node: Element, value: string, modifier: string[]) => void
>;
+25 -75
View File
@@ -4,7 +4,7 @@ import {
interpolate,
INTERP_REGEXP,
replaceDynamicParts,
} from "./inline_expressions";
} from "../common/inline_expressions";
import {
AST,
ASTComment,
@@ -24,7 +24,6 @@ import {
ASTTOut,
ASTTPortal,
ASTTranslation,
ASTTranslationContext,
ASTTSet,
ASTType,
Attrs,
@@ -36,7 +35,7 @@ type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
const whitespaceRE = /\s+/g;
export interface Config {
translateFn?: (s: string, translationCtx: string) => string;
translateFn?: (s: string) => string;
translatableAttributes?: string[];
dev?: boolean;
}
@@ -44,7 +43,6 @@ export interface Config {
export interface CodeGenOptions extends Config {
hasSafeContext?: boolean;
name?: string;
hasGlobalValues: boolean;
}
// using a non-html document so that <inner/outer>HTML serializes as XML instead
@@ -172,7 +170,6 @@ interface Context {
forceNewBlock: boolean;
isLast?: boolean;
translate: boolean;
translationCtx: string;
tKeyExpr: string | null;
nameSpace?: string;
tModelSelectedExpr?: string;
@@ -187,7 +184,6 @@ function createContext(parentCtx: Context, params?: Partial<Context>): Context {
index: 0,
forceNewBlock: true,
translate: parentCtx.translate,
translationCtx: parentCtx.translationCtx,
tKeyExpr: null,
nameSpace: parentCtx.nameSpace,
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
@@ -254,16 +250,7 @@ class CodeTarget {
}
}
const TRANSLATABLE_ATTRS = [
"alt",
"aria-label",
"aria-placeholder",
"aria-roledescription",
"aria-valuetext",
"label",
"placeholder",
"title",
];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
export class CodeGenerator {
@@ -275,7 +262,7 @@ export class CodeGenerator {
target = new CodeTarget("template");
templateName?: string;
dev: boolean;
translateFn: (s: string, translationCtx: string) => string;
translateFn: (s: string) => string;
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
ast: AST;
staticDefs: { id: string; expr: string }[] = [];
@@ -299,9 +286,6 @@ export class CodeGenerator {
this.dev = options.dev || false;
this.ast = ast;
this.templateName = options.name;
if (options.hasGlobalValues) {
this.helpers.add("__globals__");
}
}
generateCode(): string {
@@ -315,7 +299,6 @@ export class CodeGenerator {
forceNewBlock: false,
isLast: true,
translate: true,
translationCtx: "",
tKeyExpr: null,
});
// define blocks and utility functions
@@ -470,9 +453,9 @@ export class CodeGenerator {
.join("");
}
translate(str: string, translationCtx: string): string {
translate(str: string): string {
const match = translationRE.exec(str) as any;
return match[1] + this.translateFn(match[2], translationCtx) + match[3];
return match[1] + this.translateFn(match[2]) + match[3];
}
/**
@@ -514,8 +497,6 @@ export class CodeGenerator {
return this.compileTSlot(ast, ctx);
case ASTType.TTranslation:
return this.compileTTranslation(ast, ctx);
case ASTType.TTranslationContext:
return this.compileTTranslationContext(ast, ctx);
case ASTType.TPortal:
return this.compileTPortal(ast, ctx);
}
@@ -557,7 +538,7 @@ export class CodeGenerator {
let value = ast.value;
if (value && ctx.translate !== false) {
value = this.translate(value, ctx.translationCtx);
value = this.translate(value);
}
if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " ");
@@ -646,8 +627,7 @@ export class CodeGenerator {
}
}
} else if (this.translatableAttributes.includes(key)) {
const attrTranslationCtx = ast.attrsTranslationCtx?.[key] || ctx.translationCtx;
attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
attrs[key] = this.translateFn(ast.attrs[key]);
} else {
expr = `"${ast.attrs[key]}"`;
attrName = key;
@@ -995,7 +975,7 @@ export class CodeGenerator {
const isNewBlock = !block || forceNewBlock;
let codeIdx = this.target.code.length;
if (isNewBlock) {
const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
const n = ast.content.filter((c) => c.type !== ASTType.TSet).length;
let result: string | null = null;
if (n <= 1) {
for (let child of ast.content) {
@@ -1009,15 +989,15 @@ export class CodeGenerator {
let index = 0;
for (let i = 0, l = ast.content.length; i < l; i++) {
const child = ast.content[i];
const forceNewBlock = !child.hasNoRepresentation;
const isTSet = child.type === ASTType.TSet;
const subCtx = createContext(ctx, {
block,
index,
forceNewBlock,
forceNewBlock: !isTSet,
isLast: ctx.isLast && i === l - 1,
});
this.compileAST(child, subCtx);
if (forceNewBlock) {
if (!isTSet) {
index++;
}
}
@@ -1120,7 +1100,7 @@ export class CodeGenerator {
let value: string;
if (ast.defaultValue) {
const defaultValue = toStringExpression(
ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue
ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue
);
if (ast.value) {
value = `withDefault(${expr}, ${defaultValue})`;
@@ -1155,15 +1135,9 @@ export class CodeGenerator {
* "some-prop" "state" "'some-prop': ctx['state']"
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
*/
formatProp(
name: string,
value: string,
attrsTranslationCtx: { [name: string]: string } | null,
translationCtx: string
): string {
formatProp(name: string, value: string): string {
if (name.endsWith(".translate")) {
const attrTranslationCtx = attrsTranslationCtx?.[name] || translationCtx;
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
value = toStringExpression(this.translateFn(value));
} else {
value = this.captureExpression(value);
}
@@ -1178,21 +1152,15 @@ export class CodeGenerator {
case "translate":
break;
default:
throw new OwlError(`Invalid prop suffix: ${suffix}`);
throw new OwlError("Invalid prop suffix");
}
}
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
return `${name}: ${value || undefined}`;
}
formatPropObject(
obj: { [prop: string]: any },
attrsTranslationCtx: { [name: string]: string } | null,
translationCtx: string
): string[] {
return Object.entries(obj).map(([k, v]) =>
this.formatProp(k, v, attrsTranslationCtx, translationCtx)
);
formatPropObject(obj: { [prop: string]: any }): string[] {
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
}
getPropString(props: string[], dynProps: string | null): string {
@@ -1209,9 +1177,7 @@ export class CodeGenerator {
let { block } = ctx;
// props
const hasSlotsProp = "slots" in (ast.props || {});
const props: string[] = ast.props
? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
: [];
const props: string[] = ast.props ? this.formatPropObject(ast.props) : [];
// slots
let slotDef: string = "";
@@ -1235,13 +1201,7 @@ export class CodeGenerator {
params.push(`__scope: "${scope}"`);
}
if (ast.slots[slotName].attrs) {
params.push(
...this.formatPropObject(
ast.slots[slotName].attrs!,
ast.slots[slotName].attrsTranslationCtx,
ctx.translationCtx
)
);
params.push(...this.formatPropObject(ast.slots[slotName].attrs!));
}
const slotInfo = `{${params.join(", ")}}`;
slotStr.push(`'${slotName}': ${slotInfo}`);
@@ -1359,17 +1319,16 @@ export class CodeGenerator {
isMultiple = isMultiple || this.slotNames.has(ast.name);
this.slotNames.add(ast.name);
}
const attrs = { ...ast.attrs };
const dynProps = attrs["t-props"];
delete attrs["t-props"];
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
if (ast.attrs) {
delete ast.attrs["t-props"];
}
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
if (isMultiple) {
key = this.generateComponentKey(key);
}
const props = ast.attrs
? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
: [];
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
const scope = this.getPropString(props, dynProps);
if (ast.defaultContent) {
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
@@ -1402,15 +1361,6 @@ export class CodeGenerator {
}
return null;
}
compileTTranslationContext(ast: ASTTranslationContext, ctx: Context): string | null {
if (ast.content) {
return this.compileAST(
ast.content,
Object.assign({}, ctx, { translationCtx: ast.translationCtx })
);
}
return null;
}
compileTPortal(ast: ASTTPortal, ctx: Context): string {
if (!this.staticDefs.find((d) => d.id === "Portal")) {
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
+2 -7
View File
@@ -1,4 +1,3 @@
import type { customDirectives } from "../common/types";
import type { TemplateSet } from "../runtime/template_set";
import type { BDom } from "../runtime/blockdom";
import { CodeGenerator, Config } from "./code_generator";
@@ -11,17 +10,13 @@ export type TemplateFunction = (app: TemplateSet, bdom: any, helpers: any) => Te
interface CompileOptions extends Config {
name?: string;
customDirectives?: customDirectives;
hasGlobalValues: boolean;
}
export function compile(
template: string | Element,
options: CompileOptions = {
hasGlobalValues: false,
}
options: CompileOptions = {}
): TemplateFunction {
// parsing
const ast = parse(template, options.customDirectives);
const ast = parse(template);
// some work
const hasSafeContext =
+42 -213
View File
@@ -1,5 +1,4 @@
import { OwlError } from "../common/owl_error";
import type { customDirectives } from "../common/types";
import { parseXML } from "../common/utils";
// -----------------------------------------------------------------------------
@@ -27,21 +26,15 @@ export const enum ASTType {
TSlot,
TCallBlock,
TTranslation,
TTranslationContext,
TPortal,
}
export interface BaseAST {
type: ASTType;
hasNoRepresentation?: true;
}
export interface ASTText extends BaseAST {
export interface ASTText {
type: ASTType.Text;
value: string;
}
export interface ASTComment extends BaseAST {
export interface ASTComment {
type: ASTType.Comment;
value: string;
}
@@ -57,12 +50,11 @@ interface TModelInfo {
specialInitTargetAttr: string | null;
}
export interface ASTDomNode extends BaseAST {
export interface ASTDomNode {
type: ASTType.DomNode;
tag: string;
content: AST[];
attrs: Attrs | null;
attrsTranslationCtx: Attrs | null;
ref: string | null;
on: EventHandlers | null;
model: TModelInfo | null;
@@ -70,24 +62,24 @@ export interface ASTDomNode extends BaseAST {
ns: string | null;
}
export interface ASTMulti extends BaseAST {
export interface ASTMulti {
type: ASTType.Multi;
content: AST[];
}
export interface ASTTEsc extends BaseAST {
export interface ASTTEsc {
type: ASTType.TEsc;
expr: string;
defaultValue: string;
}
export interface ASTTOut extends BaseAST {
export interface ASTTOut {
type: ASTType.TOut;
expr: string;
body: AST[] | null;
}
export interface ASTTif extends BaseAST {
export interface ASTTif {
type: ASTType.TIf;
condition: string;
content: AST;
@@ -95,16 +87,15 @@ export interface ASTTif extends BaseAST {
tElse: AST | null;
}
export interface ASTTSet extends BaseAST {
export interface ASTTSet {
type: ASTType.TSet;
name: string;
value: string | null; // value defined in attribute
defaultValue: string | null; // value defined in body, if text
body: AST[] | null; // content of body if not text
hasNoRepresentation: true;
}
export interface ASTTForEach extends BaseAST {
export interface ASTTForEach {
type: ASTType.TForEach;
collection: string;
elem: string;
@@ -117,13 +108,13 @@ export interface ASTTForEach extends BaseAST {
key: string | null;
}
export interface ASTTKey extends BaseAST {
export interface ASTTKey {
type: ASTType.TKey;
expr: string;
content: AST;
}
export interface ASTTCall extends BaseAST {
export interface ASTTCall {
type: ASTType.TCall;
name: string;
body: AST[] | null;
@@ -135,57 +126,48 @@ interface SlotDefinition {
scope: string | null;
on: EventHandlers | null;
attrs: Attrs | null;
attrsTranslationCtx: Attrs | null;
}
export interface ASTComponent extends BaseAST {
export interface ASTComponent {
type: ASTType.TComponent;
name: string;
isDynamic: boolean;
dynamicProps: string | null;
on: EventHandlers | null;
props: { [name: string]: string } | null;
propsTranslationCtx: { [name: string]: string } | null;
slots: { [name: string]: SlotDefinition } | null;
}
export interface ASTSlot extends BaseAST {
export interface ASTSlot {
type: ASTType.TSlot;
name: string;
attrs: Attrs | null;
attrsTranslationCtx: Attrs | null;
on: EventHandlers | null;
defaultContent: AST | null;
}
export interface ASTTCallBlock extends BaseAST {
export interface ASTTCallBlock {
type: ASTType.TCallBlock;
name: string;
}
export interface ASTDebug extends BaseAST {
export interface ASTDebug {
type: ASTType.TDebug;
content: AST | null;
}
export interface ASTLog extends BaseAST {
export interface ASTLog {
type: ASTType.TLog;
expr: string;
content: AST | null;
}
export interface ASTTranslation extends BaseAST {
export interface ASTTranslation {
type: ASTType.TTranslation;
content: AST | null;
}
export interface ASTTranslationContext extends BaseAST {
type: ASTType.TTranslationContext;
content: AST | null;
translationCtx: string;
}
export interface ASTTPortal extends BaseAST {
export interface ASTTPortal {
type: ASTType.TPortal;
target: string;
content: AST;
@@ -209,7 +191,6 @@ export type AST =
| ASTLog
| ASTDebug
| ASTTranslation
| ASTTranslationContext
| ASTTPortal;
// -----------------------------------------------------------------------------
@@ -217,26 +198,23 @@ export type AST =
// -----------------------------------------------------------------------------
const cache: WeakMap<Element, AST> = new WeakMap();
export function parse(xml: string | Element, customDir?: customDirectives): AST {
const ctx = {
inPreTag: false,
customDirectives: customDir,
};
export function parse(xml: string | Element): AST {
if (typeof xml === "string") {
const elem = parseXML(`<t>${xml}</t>`).firstChild as Element;
return _parse(elem, ctx);
return _parse(elem);
}
let ast = cache.get(xml);
if (!ast) {
// we clone here the xml to prevent modifying it in place
ast = _parse(xml.cloneNode(true) as Element, ctx);
ast = _parse(xml.cloneNode(true) as Element);
cache.set(xml, ast);
}
return ast;
}
function _parse(xml: Element, ctx: ParsingContext): AST {
function _parse(xml: Element): AST {
normalizeXML(xml);
const ctx = { inPreTag: false };
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
}
@@ -244,7 +222,6 @@ interface ParsingContext {
tModelInfo?: TModelInfo | null;
nameSpace?: string;
inPreTag: boolean;
customDirectives?: customDirectives;
}
function parseNode(node: Node, ctx: ParsingContext): AST | null {
@@ -252,18 +229,16 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
return parseTextCommentNode(node, ctx);
}
return (
parseTCustom(node, ctx) ||
parseTDebugLog(node, ctx) ||
parseTForEach(node, ctx) ||
parseTIf(node, ctx) ||
parseTPortal(node, ctx) ||
parseTCall(node, ctx) ||
parseTCallBlock(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTTranslationContext(node, ctx) ||
parseTKey(node, ctx) ||
parseTEscNode(node, ctx) ||
parseTOutNode(node, ctx) ||
parseTKey(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTSlot(node, ctx) ||
parseComponent(node, ctx) ||
parseDOMNode(node, ctx) ||
@@ -302,37 +277,6 @@ function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
return null;
}
function parseTCustom(node: Element, ctx: ParsingContext): AST | null {
if (!ctx.customDirectives) {
return null;
}
const nodeAttrsNames = node.getAttributeNames();
for (let attr of nodeAttrsNames) {
if (attr === "t-custom" || attr === "t-custom-") {
throw new OwlError("Missing custom directive name with t-custom directive");
}
if (attr.startsWith("t-custom-")) {
const directiveName = attr.split(".")[0].slice(9);
const customDirective = ctx.customDirectives[directiveName];
if (!customDirective) {
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
}
const value = node.getAttribute(attr)!;
const modifiers = attr.split(".").slice(1);
node.removeAttribute(attr);
try {
customDirective(node, value, modifiers);
} catch (error) {
throw new OwlError(
`Custom directive "${directiveName}" throw the following error: ${error}`
);
}
return parseNode(node, ctx);
}
}
return null;
}
// -----------------------------------------------------------------------------
// debugging
// -----------------------------------------------------------------------------
@@ -340,30 +284,20 @@ function parseTCustom(node: Element, ctx: ParsingContext): AST | null {
function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
if (node.hasAttribute("t-debug")) {
node.removeAttribute("t-debug");
const content = parseNode(node, ctx);
const ast: ASTDebug = {
return {
type: ASTType.TDebug,
content,
content: parseNode(node, ctx),
};
if (content?.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
if (node.hasAttribute("t-log")) {
const expr = node.getAttribute("t-log")!;
node.removeAttribute("t-log");
const content = parseNode(node, ctx);
const ast: ASTLog = {
return {
type: ASTType.TLog,
expr,
content,
content: parseNode(node, ctx),
};
if (content?.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
return null;
}
@@ -397,7 +331,6 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const nodeAttrsNames = node.getAttributeNames();
let attrs: ASTDomNode["attrs"] = null;
let attrsTranslationCtx: ASTDomNode["attrsTranslationCtx"] = null;
let on: EventHandlers | null = null;
let model: TModelInfo | null = null;
@@ -458,10 +391,6 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
throw new OwlError(`Invalid attribute: '${attr}'`);
} else if (attr === "xmlns") {
ns = value;
} else if (attr.startsWith("t-translation-context-")) {
const attrName = attr.slice(22);
attrsTranslationCtx = attrsTranslationCtx || {};
attrsTranslationCtx[attrName] = value;
} else if (attr !== "t-name") {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
@@ -484,7 +413,6 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
tag: tagName,
dynamicTag,
attrs,
attrsTranslationCtx,
on,
ref,
content: children,
@@ -614,19 +542,11 @@ function parseTKey(node: Element, ctx: ParsingContext): AST | null {
}
const key = node.getAttribute("t-key")!;
node.removeAttribute("t-key");
const content = parseNode(node, ctx);
if (!content) {
const body = parseNode(node, ctx);
if (!body) {
return null;
}
const ast: ASTTKey = {
type: ASTType.TKey,
expr: key,
content,
};
if (content.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
return { type: ASTType.TKey, expr: key, content: body };
}
// -----------------------------------------------------------------------------
@@ -652,15 +572,7 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
if (ast && ast.type === ASTType.TComponent) {
return {
...ast,
slots: {
default: {
content: tcall,
scope: null,
on: null,
attrs: null,
attrsTranslationCtx: null,
},
},
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
};
}
}
@@ -748,7 +660,7 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
if (node.textContent !== node.innerHTML) {
body = parseChildren(node, ctx);
}
return { type: ASTType.TSet, name, value, defaultValue, body, hasNoRepresentation: true };
return { type: ASTType.TSet, name, value, defaultValue, body };
}
// -----------------------------------------------------------------------------
@@ -795,14 +707,9 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
let on: ASTComponent["on"] = null;
let props: ASTComponent["props"] = null;
let propsTranslationCtx: ASTComponent["propsTranslationCtx"] = null;
for (let name of node.getAttributeNames()) {
const value = node.getAttribute(name)!;
if (name.startsWith("t-translation-context-")) {
const attrName = name.slice(22);
propsTranslationCtx = propsTranslationCtx || {};
propsTranslationCtx[attrName] = value;
} else if (name.startsWith("t-")) {
if (name.startsWith("t-")) {
if (name.startsWith("t-on-")) {
on = on || {};
on[name.slice(5)] = value;
@@ -850,17 +757,12 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
const slotAst = parseNode(slotNode, ctx);
let on: SlotDefinition["on"] = null;
let attrs: Attrs | null = null;
let attrsTranslationCtx: Attrs | null = null;
let scope: string | null = null;
for (let attributeName of slotNode.getAttributeNames()) {
const value = slotNode.getAttribute(attributeName)!;
if (attributeName === "t-slot-scope") {
scope = value;
continue;
} else if (attributeName.startsWith("t-translation-context-")) {
const attrName = attributeName.slice(22);
attrsTranslationCtx = attrsTranslationCtx || {};
attrsTranslationCtx[attrName] = value;
} else if (attributeName.startsWith("t-on-")) {
on = on || {};
on[attributeName.slice(5)] = value;
@@ -870,7 +772,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
}
}
slots = slots || {};
slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
slots[name] = { content: slotAst, on, attrs, scope };
}
// default slot
@@ -878,25 +780,10 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
slots = slots || {};
// t-set-slot="default" has priority over content
if (defaultContent && !slots.default) {
slots.default = {
content: defaultContent,
on,
attrs: null,
attrsTranslationCtx: null,
scope: defaultSlotScope,
};
slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
}
}
return {
type: ASTType.TComponent,
name,
isDynamic,
dynamicProps,
props,
propsTranslationCtx,
slots,
on,
};
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots, on };
}
// -----------------------------------------------------------------------------
@@ -910,17 +797,12 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
const name = node.getAttribute("t-slot")!;
node.removeAttribute("t-slot");
let attrs: Attrs | null = null;
let attrsTranslationCtx: Attrs | null = null;
let on: ASTComponent["on"] = null;
for (let attributeName of node.getAttributeNames()) {
const value = node.getAttribute(attributeName)!;
if (attributeName.startsWith("t-on-")) {
on = on || {};
on[attributeName.slice(5)] = value;
} else if (attributeName.startsWith("t-translation-context-")) {
const attrName = attributeName.slice(22);
attrsTranslationCtx = attrsTranslationCtx || {};
attrsTranslationCtx[attrName] = value;
} else {
attrs = attrs || {};
attrs[attributeName] = value;
@@ -930,65 +812,20 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
type: ASTType.TSlot,
name,
attrs,
attrsTranslationCtx,
on,
defaultContent: parseChildNodes(node, ctx),
};
}
// -----------------------------------------------------------------------------
// Translation
// -----------------------------------------------------------------------------
function wrapInTTranslationAST(r: AST | null) {
const ast: ASTTranslation = { type: ASTType.TTranslation, content: r };
if (r?.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
if (node.getAttribute("t-translation") !== "off") {
return null;
}
node.removeAttribute("t-translation");
const result = parseNode(node, ctx);
if (result?.type === ASTType.Multi) {
const children = result.content.map(wrapInTTranslationAST);
return makeASTMulti(children);
}
return wrapInTTranslationAST(result);
}
// -----------------------------------------------------------------------------
// Translation Context
// -----------------------------------------------------------------------------
function wrapInTTranslationContextAST(r: AST | null, translationCtx: string) {
const ast: ASTTranslationContext = {
type: ASTType.TTranslationContext,
content: r,
translationCtx,
return {
type: ASTType.TTranslation,
content: parseNode(node, ctx),
};
if (r?.hasNoRepresentation) {
ast.hasNoRepresentation = true;
}
return ast;
}
function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null {
const translationCtx = node.getAttribute("t-translation-context");
if (!translationCtx) {
return null;
}
node.removeAttribute("t-translation-context");
const result = parseNode(node, ctx);
if (result?.type === ASTType.Multi) {
const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
return makeASTMulti(children);
}
return wrapInTTranslationContextAST(result, translationCtx);
}
// -----------------------------------------------------------------------------
@@ -1037,14 +874,6 @@ function parseChildren(node: Element, ctx: ParsingContext): AST[] {
return children;
}
function makeASTMulti(children: AST[]) {
const ast: ASTMulti = { type: ASTType.Multi, content: children };
if (children.every((c) => c.hasNoRepresentation)) {
ast.hasNoRepresentation = true;
}
return ast;
}
/**
* Parse all the child nodes of a given node and return an ast if possible.
* In the case there are multiple children, they are wrapped in a astmulti.
@@ -1057,7 +886,7 @@ function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
case 1:
return children[0];
default:
return makeASTMulti(children);
return { type: ASTType.Multi, content: children };
}
}
-89
View File
@@ -1,89 +0,0 @@
// -----------------------------------------------------------------------------
// This file exports a function that allows compiling templates ahead of time.
// It is used by the "compile_owl_template" command registered in the "bin"
// section of owl's package.json
// -----------------------------------------------------------------------------
import { readdir, readFile, stat } from "fs/promises";
import path from "path";
import "./setup_jsdom";
// Owl imports must be made after setting up jsdom in the global namespace
import { compile } from "..";
// -----------------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------------
async function getXmlFiles(paths: string[]): Promise<string[]> {
return (
await Promise.all(
paths.map(async (file) => {
const stats = await stat(path.join(file));
if (stats.isDirectory()) {
return await getXmlFiles(
(await readdir(file)).map((fileName) => path.join(file, fileName))
);
}
if (file.endsWith(".xml")) {
return file;
}
return [];
})
)
).flat();
}
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
const a = "·-_,:;";
const p = new RegExp(a.split("").join("|"), "g");
function slugify(str: string) {
return str
.replace(/\//g, "") // remove /
.replace(/\./g, "_") // Replace . with _
.replace(p, (c) => "_") // Replace special characters
.replace(/&/g, "_and_") // Replace & with and
.replace(/[^\w\-]+/g, ""); // Remove all non-word characters
}
// -----------------------------------------------------------------------------
// main
// -----------------------------------------------------------------------------
export async function compileTemplates(paths: string[]) {
const files = await getXmlFiles(paths);
process.stdout.write(`Processing ${files.length} files`);
let xmlStrings = await Promise.all(files.map((file) => readFile(file, "utf8")));
const templates = [];
const errors = [];
for (let i = 0; i < files.length; i++) {
const fileName = files[i];
const fileContent = xmlStrings[i];
process.stdout.write(`.`);
const parser = new DOMParser();
const doc = parser.parseFromString(fileContent, "text/xml");
for (const template of doc.querySelectorAll("[t-name]")) {
const name = template.getAttribute("t-name");
if (template.hasAttribute("owl")) {
template.removeAttribute("owl");
}
const fnName = slugify(name!);
try {
const fn = compile(template).toString().replace("anonymous", fnName);
templates.push(`"${name}": ${fn},\n`);
} catch (e) {
errors.push({ name, fileName, e });
}
}
}
process.stdout.write(`\n`);
for (let { name, fileName, e } of errors) {
console.warn(`Error while compiling '${name}' (in file ${fileName})`);
console.error(e);
}
console.log(`${templates.length} templates compiled`);
return `export const templates = {\n ${templates.join("\n")} \n}`;
}
-13
View File
@@ -1,13 +0,0 @@
import jsdom from "jsdom";
// -----------------------------------------------------------------------------
// add global DOM stuff for compiler. Needs to be in a separate file so rollup
// doesn't hoist the owl imports above this block of code.
// -----------------------------------------------------------------------------
var document = new jsdom.JSDOM("", {});
var window = document.window;
global.document = window.document;
global.window = window as unknown as Window & typeof globalThis;
global.DOMParser = window.DOMParser;
global.Element = window.Element;
global.Node = window.Node;
-2
View File
@@ -12,7 +12,5 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
customDirectives: this.customDirectives,
hasGlobalValues: this.hasGlobalValues,
});
};
+67 -38
View File
@@ -1,14 +1,14 @@
import { OwlError } from "../common/owl_error";
import { version } from "../version";
import { Component, ComponentConstructor, Props } from "./component";
import { ComponentNode, saveCurrent } from "./component_node";
import { nodeErrorHandlers, handleError } from "./error_handling";
import { OwlError } from "../common/owl_error";
import { Fiber, RootFiber, MountOptions } from "./fibers";
import { handleError, nodeErrorHandlers } from "./error_handling";
import { Fiber, MountOptions, RootFiber } from "./fibers";
import { reactive, toRaw } from "./reactivity";
import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers";
import { TemplateSet, TemplateSetConfig } from "./template_set";
import { validateTarget } from "./utils";
import { toRaw, reactive } from "./reactivity";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
@@ -29,6 +29,15 @@ export interface AppConfig<P, E> extends TemplateSetConfig, RootConfig<P, E> {
let hasBeenLogged = false;
export const DEV_MSG = () => {
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
return `Owl is running in 'dev' mode.
This is not suitable for production use.
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
};
const apps = new Set<App>();
declare global {
@@ -44,7 +53,7 @@ declare global {
}
interface Root<P extends Props, E> {
node: ComponentNode<P, E>;
node: ComponentNode<P, E> | null;
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
destroy(): void;
}
@@ -65,9 +74,10 @@ export class App<
props: P;
env: E;
scheduler = new Scheduler();
subRoots: Set<ComponentNode> = new Set();
root: ComponentNode<P, E> | null = null;
subRoots: Set<Root<any, any>> = new Set();
root: Root<P, E> | null = null;
warnIfNoStaticProps: boolean;
_lastRootEl: HTMLElement | ShadowRoot | null = null; // temporary ref to propagate to roots
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
super(config);
@@ -79,7 +89,7 @@ export class App<
}
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
if (this.dev && !config.test && !hasBeenLogged) {
console.info(`Owl is running in 'dev' mode.`);
console.info(DEV_MSG());
hasBeenLogged = true;
}
const env = config.env || {};
@@ -92,10 +102,8 @@ export class App<
target: HTMLElement | ShadowRoot,
options?: MountOptions
): Promise<Component<P, E> & InstanceType<T>> {
const root = this.createRoot(this.Root, { props: this.props });
this.root = root.node;
this.subRoots.delete(root.node);
return root.mount(target, options) as any;
this.root = this.createRoot(this.Root, { props: this.props });
return this.root.mount(target, options) as any;
}
createRoot<Props extends object, SubEnv = any>(
@@ -103,26 +111,38 @@ export class App<
config: RootConfig<Props, SubEnv> = {}
): Root<Props, SubEnv> {
const props = config.props || ({} as Props);
// hack to make sure the sub root get the sub env if necessary. for owl 3,
// would be nice to rethink the initialization process to make sure that
// we can create a ComponentNode and give it explicitely the env, instead
// of looking it up in the app
const env = this.env;
if (config.env) {
this.env = config.env as any;
}
const restore = saveCurrent();
const node = this.makeNode(Root, props);
restore();
if (config.env) {
this.env = env;
}
this.subRoots.add(node);
return {
node,
const root: Root<Props, SubEnv> = {
node: null,
mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => {
App.validateTarget(target);
// hack to make sure the sub root get the sub env if necessary. for owl 3,
// would be nice to rethink the initialization process to make sure that
// we can create a ComponentNode and give it explicitely the env, instead
// of looking it up in the app
if (config.env) {
this.env = config.env as any;
}
const restore = saveCurrent();
if (options?.position === "attach") {
if (Root.template) {
throw new Error("Cannot attach a component with a template");
}
this._lastRootEl = target;
} else {
if (!Root.template) {
// no template => trigger an error
this.getTemplate("");
}
}
const node = this.makeNode(Root, props);
root.node = node;
this._lastRootEl = null;
restore();
if (config.env) {
this.env = env;
}
if (this.dev) {
validateProps(Root, props, { __owl__: { app: this } });
}
@@ -130,11 +150,16 @@ export class App<
return prom;
},
destroy: () => {
this.subRoots.delete(node);
node.destroy();
this.scheduler.processTasks();
this.subRoots.delete(root);
if (root.node) {
root.node?.destroy();
this.scheduler.processTasks();
}
},
};
this.subRoots.add(root);
return root;
}
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
@@ -169,13 +194,17 @@ export class App<
}
destroy() {
if (this.root) {
for (let subroot of this.subRoots) {
subroot.destroy();
}
this.root.destroy();
this.scheduler.processTasks();
const roots = [...this.subRoots].reverse();
for (let root of roots) {
root.destroy();
}
// if (this.root) {
// for (let subroot of this.subRoots) {
// subroot.destroy();
// }
// this.root.destroy();
this.scheduler.processTasks();
// }
apps.delete(this);
}
+1
View File
@@ -9,6 +9,7 @@ export type Props = { [key: string]: any };
interface StaticComponentProperties {
template: string;
dynamicContent?: { [spec: string]: string };
defaultProps?: any;
props?: Schema;
components?: { [componentName: string]: ComponentConstructor };
+95 -2
View File
@@ -6,7 +6,9 @@ import { OwlError } from "../common/owl_error";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
import { STATUS } from "./status";
import { batched, Callback } from "./utils";
import { batched, Callback, Markup } from "./utils";
import { xml } from "./template_set";
import { compileExpr } from "../common/inline_expressions";
let currentNode: ComponentNode | null = null;
@@ -124,11 +126,102 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
this.component = new C(props, env, this);
const ctx = Object.assign(Object.create(this.component), { this: this.component });
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
if (C.template) {
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
} else {
// component will be attached
this.renderFn = app.getTemplate(xml``).bind(this.component, ctx, this);
if (C.dynamicContent) {
this.prepareAttach(app._lastRootEl!, C.dynamicContent, ctx);
}
}
this.component.setup();
currentNode = null;
}
prepareAttach(
el: HTMLElement | ShadowRoot,
dynamicContent: { [spec: string]: string },
ctx: any
) {
const attrs: { selector: string; attr: string; fn: Function }[] = [];
const handlers: { selector: string; event: string; fn: any }[] = [];
const tOuts: { selector: string; fn: Function }[] = [];
for (let key in dynamicContent) {
const value = dynamicContent[key];
const parts = key.split(":");
if (parts[1].startsWith("t-att-")) {
const attr = parts[1].slice(6);
const fn = new Function("ctx", `return ${compileExpr(value)};`);
attrs.push({
selector: parts[0],
attr,
fn,
});
}
if (parts[1].startsWith("t-on-")) {
const event = parts[1].slice(5);
// const fn = new Function("ctx", "ev", `${compileExpr(value)}(ev);`);
const fn = (ev: any) => (this as any).component[value](ev);
handlers.push({
selector: parts[0],
event,
fn,
});
}
if (parts[1] === "t-out") {
const fn = new Function("ctx", `return ${compileExpr(value)};`);
tOuts.push({ selector: parts[0], fn });
}
}
const handleAttrs = () => {
for (let attr of attrs) {
const val = attr.fn.call(this.component, ctx);
// todo: cache the queryselector result?
const target = attr.selector === "root" ? el : (el.querySelector(attr.selector) as any);
if (target) {
target.setAttribute(attr.attr, val);
}
}
};
const handleEvents = () => {
for (let handler of handlers) {
// const val = attr.fn.call(this.component, ctx);
// todo: cache the queryselector result?
const target =
handler.selector === "root" ? el : (el.querySelector(handler.selector) as any);
if (target) {
target.addEventListener(handler.event, handler.fn);
}
}
};
const handleTOuts = () => {
for (let tOut of tOuts) {
const val = tOut.fn.call(this.component, ctx);
// todo: cache the queryselector result?
const target = tOut.selector === "root" ? el : (el.querySelector(tOut.selector) as any);
if (target) {
if (val instanceof Markup) {
target.innerHTML = val as any;
} else {
target.textContent = val;
}
}
}
};
if (attrs.length) {
this.mounted.push(handleAttrs);
this.patched.push(handleAttrs);
}
if (handlers.length) {
this.mounted.push(handleEvents);
}
if (tOuts.length) {
this.mounted.push(handleTOuts);
this.patched.push(handleTOuts);
}
}
mountComponent(target: any, options?: MountOptions) {
const fiber = new MountFiber(this, target, options);
this.app.scheduler.addFiber(fiber);
-1
View File
@@ -55,7 +55,6 @@ export function handleError(params: ErrorParams) {
let current: Fiber | null = fiber;
do {
current.node.fiber = current;
fibersInError.set(current, error);
current = current.parent;
} while (current);
+2 -18
View File
@@ -30,13 +30,6 @@ export function makeRootFiber(node: ComponentNode): Fiber {
fibersInError.delete(current);
fibersInError.delete(root);
current.appliedToDom = false;
if (current instanceof RootFiber) {
// it is possible that this fiber is a fiber that crashed while being
// mounted, so the mounted list is possibly corrupted. We restore it to
// its normal initial state (which is empty list or a list with a mount
// fiber.
current.mounted = current instanceof MountFiber ? [current] : [];
}
}
return current;
}
@@ -159,7 +152,6 @@ export class RootFiber extends Fiber {
const node = this.node;
this.locked = true;
let current: Fiber | undefined = undefined;
let mountedFibers = this.mounted;
try {
// Step 1: calling all willPatch lifecycle hooks
for (current of this.willPatch) {
@@ -181,6 +173,7 @@ export class RootFiber extends Fiber {
this.locked = false;
// Step 4: calling all mounted lifecycle hooks
let mountedFibers = this.mounted;
while ((current = mountedFibers.pop())) {
current = current;
if (current.appliedToDom) {
@@ -201,15 +194,6 @@ export class RootFiber extends Fiber {
}
}
} catch (e) {
// if mountedFibers is not empty, this means that a crash occured while
// calling the mounted hooks of some component. So, there may still be
// some component that have been mounted, but for which the mounted hooks
// have not been called. Here, we remove the willUnmount hooks for these
// specific component to prevent a worse situation (willUnmount being
// called even though mounted has not been called)
for (let fiber of mountedFibers) {
fiber.node.willUnmount = [];
}
this.locked = false;
node.app.handleError({ fiber: current || this, error: e });
}
@@ -223,7 +207,7 @@ export class RootFiber extends Fiber {
}
}
type Position = "first-child" | "last-child";
type Position = "first-child" | "last-child" | "attach";
export interface MountOptions {
position?: Position;
+17
View File
@@ -136,3 +136,20 @@ export function useExternalListener(
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
}
// -----------------------------------------------------------------------------
// useAttachedEl
// -----------------------------------------------------------------------------
/**
* The purpose of this hook is to allow attached components to get a reference to
* the element they have been attached on.
*/
export function useAttachedEl(): HTMLElement {
const node = getCurrent();
const el = node.app._lastRootEl as HTMLElement;
if (!el) {
throw new Error("useAttachedEl can only be called with component that are attached");
}
return el;
}
+1 -1
View File
@@ -41,7 +41,7 @@ export { useComponent, useState } from "./component_node";
export { status } from "./status";
export { reactive, markRaw, toRaw } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
export { batched, EventBus, whenReady, loadFile, markup } from "./utils";
export {
onWillStart,
onMounted,
+1 -8
View File
@@ -93,14 +93,7 @@ export class Scheduler {
if (!hasError) {
fiber.complete();
}
// at this point, the fiber should have been applied to the DOM, so we can
// remove it from the task list. If it is not the case, it means that there
// was an error and an error handler triggered a new rendering that recycled
// the fiber, so in that case, we actually want to keep the fiber around,
// otherwise it will just be ignored.
if (fiber.appliedToDom) {
this.tasks.delete(fiber);
}
this.tasks.delete(fiber);
}
}
}
+3 -12
View File
@@ -5,18 +5,15 @@ import { Portal, portalTemplate } from "./portal";
import { helpers } from "./template_helpers";
import { OwlError } from "../common/owl_error";
import { parseXML } from "../common/utils";
import type { customDirectives } from "../common/types";
const bdom = { text, createBlock, list, multi, html, toggler, comment };
export interface TemplateSetConfig {
dev?: boolean;
translatableAttributes?: string[];
translateFn?: (s: string, translationCtx: string) => string;
translateFn?: (s: string) => string;
templates?: string | Document | Record<string, string>;
getTemplate?: (s: string) => Element | Function | string | void;
customDirectives?: customDirectives;
globalValues?: object;
}
export class TemplateSet {
@@ -27,12 +24,9 @@ export class TemplateSet {
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {};
getRawTemplate?: (s: string) => Element | Function | string | void;
translateFn?: (s: string, translationCtx: string) => string;
translateFn?: (s: string) => string;
translatableAttributes?: string[];
Portal = Portal;
customDirectives: customDirectives;
runtimeUtils: object;
hasGlobalValues: boolean;
constructor(config: TemplateSetConfig = {}) {
this.dev = config.dev || false;
@@ -48,9 +42,6 @@ export class TemplateSet {
}
}
this.getRawTemplate = config.getTemplate;
this.customDirectives = config.customDirectives || {};
this.runtimeUtils = { ...helpers, __globals__: config.globalValues || {} };
this.hasGlobalValues = Boolean(config.globalValues && Object.keys(config.globalValues).length);
}
addTemplate(name: string, template: string | Element) {
@@ -106,7 +97,7 @@ export class TemplateSet {
this.templates[name] = function (context, parent) {
return templates[name].call(this, context, parent);
};
const template = templateFn(this, bdom, this.runtimeUtils);
const template = templateFn(this, bdom, helpers);
this.templates[name] = template;
}
return this.templates[name];
+4 -74
View File
@@ -35,43 +35,13 @@ export function inOwnerDocument(el?: HTMLElement) {
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
}
/**
* Determine whether the given element is contained in a specific root documnet:
* either directly or with a shadow root in between or in an iframe.
*/
function isAttachedToDocument(
element: HTMLElement | ShadowRoot,
documentElement: Document
): boolean {
let current: Node = element;
const shadowRoot = documentElement.defaultView!.ShadowRoot;
while (current) {
if (current === documentElement) {
return true;
}
if (current.parentNode) {
current = current.parentNode;
} else if (current instanceof shadowRoot && current.host) {
current = current.host;
} else {
return false;
}
}
return false;
}
export function validateTarget(target: HTMLElement | ShadowRoot) {
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
const document = target && target.ownerDocument;
if (document) {
if (!document.defaultView) {
throw new OwlError(
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
);
}
const HTMLElement = document.defaultView.HTMLElement;
const HTMLElement = document.defaultView!.HTMLElement;
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
if (!isAttachedToDocument(target, document)) {
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
throw new OwlError("Cannot mount a component on a detached dom node");
}
return;
@@ -111,50 +81,10 @@ export async function loadFile(url: string): Promise<string> {
*/
export class Markup extends String {}
export function htmlEscape(str: any): Markup {
if (str instanceof Markup) {
return str;
}
if (str === undefined) {
return markup("");
}
if (typeof str === "number") {
return markup(String(str));
}
[
["&", "&amp;"],
["<", "&lt;"],
[">", "&gt;"],
["'", "&#x27;"],
['"', "&quot;"],
["`", "&#x60;"],
].forEach((pairs) => {
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
});
return markup(str);
}
/*
* Marks a value as safe, that is, a value that can be injected as HTML directly.
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
*
* If called as a tag function, the interpolated strings are escaped.
*/
export function markup(strings: TemplateStringsArray, ...placeholders: unknown[]): Markup;
export function markup(value: string): Markup;
export function markup(
valueOrStrings: string | TemplateStringsArray,
...placeholders: unknown[]
): Markup {
if (!Array.isArray(valueOrStrings)) {
return new Markup(valueOrStrings);
}
const strings = valueOrStrings;
let acc = "";
let i = 0;
for (; i < placeholders.length; ++i) {
acc += strings[i] + htmlEscape(placeholders[i]);
}
acc += strings[i];
return new Markup(acc);
export function markup(value: any) {
return new Markup(value);
}
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.8.1";
export const version = "2.4.0";
-15
View File
@@ -43,21 +43,6 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
}"
`;
exports[`app can add functions to the bdom 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { __globals__ } = helpers;
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [()=>__globals__.plop('click'), ctx];
return block1([hdlr1]);
}
}"
`;
exports[`app can call processTask twice in a row without crashing 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -94,17 +94,6 @@ exports[`subroot can create a root in a setup function, then use a hook 1`] = `
}"
`;
exports[`subroot can create a root in a setup function, then use a hook 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`c\`);
}
}"
`;
exports[`subroot can mount subroot 1`] = `
"function anonymous(app, bdom, helpers
) {
-18
View File
@@ -201,22 +201,4 @@ describe("app", () => {
await app.mount(fixture);
expect(fixture.innerHTML).toBe("parent<div></div>");
});
test("can add functions to the bdom", async () => {
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`<div t-on-click="() => __globals__.plop('click')" class="my-div"/>`;
}
const app = new App(SomeComponent, {
globalValues: {
plop: (string: any) => {
steps.push(string);
},
},
});
await app.mount(fixture);
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
fixture.querySelector("div")!.click();
expect(steps).toEqual(["click"]);
});
});
@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-custom can use t-custom directive on a node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['click'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-custom can use t-custom directive with modifiers on a node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['click'], ctx];
return block1([hdlr1]);
}
}"
`;
@@ -49,27 +49,6 @@ exports[`debugging t-debug on sub template 2`] = `
}"
`;
exports[`debugging t-debug: interaction with t-set 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
debugger;
setContextValue(ctx, \\"foo\\", 42);
debugger;
setContextValue(ctx, \\"bar\\", 49);
let txt1 = ctx['foo']+ctx['bar'];
return block1([txt1]);
}
}"
`;
exports[`debugging t-log 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -87,24 +66,3 @@ exports[`debugging t-log 1`] = `
}
}"
`;
exports[`debugging t-log: interaction with t-set 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
console.log(ctx['foo']);
setContextValue(ctx, \\"foo\\", 42);
console.log(ctx['bar']);
setContextValue(ctx, \\"bar\\", 49);
let txt1 = ctx['foo']+ctx['bar'];
return block1([txt1]);
}
}"
`;
@@ -103,18 +103,3 @@ exports[`t-key t-key on sub dom node pushes a child block in its parent 2`] = `
}
}"
`;
exports[`t-key t-key: interaction with t-esc 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<p><block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
const tKey_1 = ctx['key'];
let txt1 = ctx['text'];
return toggler(tKey_1, block1([txt1]));
}
}"
`;
@@ -1,13 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-slot compile t-props correctly multiple time 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
return function template(ctx, node, key = \\"\\") {
return callSlot(ctx, node, key, 'default', false, Object.assign({}, {a:1}));
}
}"
`;
@@ -1,144 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`translation context body of t-sets are translated in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", \`traduit\`);
return text(ctx['label']);
}
}"
`;
exports[`translation context default slot params and content translated in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
function defaultContent1(ctx, node, key = \\"\\") {
return text(\` foo \`);
}
return function template(ctx, node, key = \\"\\") {
const b3 = callSlot(ctx, node, key, 'default', false, {param: \`param\`,title: \`título\`}, defaultContent1.bind(this));
return block1([], [b3]);
}
}"
`;
exports[`translation context props with modifier .translate are translated in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`ChildComponent\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({text: \`jeu\`}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`translation context props with modifier .translate are translated in context 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<span><block-text-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['props'].text;
return block1([txt1]);
}
}"
`;
exports[`translation context slot attrs and text contents are translated in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { capture, markRaw } = helpers;
const comp1 = app.createComponent(\`ChildComponent\`, true, true, false, []);
function slot1(ctx, node, key = \\"\\") {
return text(\`jeu\`);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return comp1({slots: markRaw({'a': {__render: slot1.bind(this), __ctx: ctx1, title: \`título\`}})}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`translation context slot attrs and text contents are translated in context 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = callSlot(ctx, node, key, 'a', false, {});
return block1([], [b2]);
}
}"
`;
exports[`translation context t-translation-context with several children 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><div/><div/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (true) {
b2 = text(\`\`);
}
return block1([], [b2]);
}
}"
`;
exports[`translation context translation of attributes in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div title=\\"titre\\" label=\\"game\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`translation context translation of text in context 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block2 = createBlock(\`<div>word</div>\`);
let block3 = createBlock(\`<div>mot</div>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = block2();
const b3 = block3();
return multi([b2, b3]);
}
}"
`;
exports[`translation support body of t-sets are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -169,21 +30,6 @@ exports[`translation support body of t-sets inside translation=off are not trans
}"
`;
exports[`translation support body of t-sets inside translation=off are not translated 2 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", \`untranslated\`);
return text(ctx['label']);
}
}"
`;
exports[`translation support body of t-sets with html content are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -295,23 +141,6 @@ exports[`translation support t-set and falsy t-value: t-body are translated 1`]
}"
`;
exports[`translation support t-translation with several children 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><div/><div/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
if (true) {
b2 = text(\`\`);
}
return block1([], [b2]);
}
}"
`;
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
"function anonymous(app, bdom, helpers
) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { compileExpr, tokenize } from "../../src/compiler/inline_expressions";
import { compileExpr, tokenize } from "../../src/common/inline_expressions";
describe("tokenizer", () => {
test("simple tokens", () => {
+5 -321
View File
@@ -43,7 +43,6 @@ describe("qweb parser", () => {
dynamicTag: null,
content: [],
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -71,7 +70,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -86,7 +84,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -101,7 +98,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -113,7 +109,6 @@ describe("qweb parser", () => {
tag: "span",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -133,7 +128,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -145,7 +139,6 @@ describe("qweb parser", () => {
tag: "span",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -163,7 +156,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -189,7 +181,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -210,7 +201,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -233,7 +223,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -257,7 +246,6 @@ describe("qweb parser", () => {
tag: "span",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -274,7 +262,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: { class: "abc" },
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -293,7 +280,6 @@ describe("qweb parser", () => {
height: "90px",
width: "100px",
},
attrsTranslationCtx: null,
content: [
{
attrs: {
@@ -304,7 +290,6 @@ describe("qweb parser", () => {
stroke: "green",
"stroke-width": "1",
},
attrsTranslationCtx: null,
content: [],
dynamicTag: null,
model: null,
@@ -327,7 +312,6 @@ describe("qweb parser", () => {
parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`)
).toEqual({
attrs: null,
attrsTranslationCtx: null,
content: [
{
attrs: {
@@ -338,7 +322,6 @@ describe("qweb parser", () => {
stroke: "green",
"stroke-width": "1",
},
attrsTranslationCtx: null,
content: [],
dynamicTag: null,
model: null,
@@ -365,7 +348,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
content: [
@@ -374,7 +356,6 @@ describe("qweb parser", () => {
tag: "pre",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
content: [],
@@ -410,7 +391,6 @@ describe("qweb parser", () => {
tag: "span",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -433,7 +413,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -476,7 +455,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -491,7 +469,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -512,7 +489,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -554,7 +530,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -632,7 +607,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -652,7 +626,6 @@ describe("qweb parser", () => {
tag: "h1",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -666,7 +639,6 @@ describe("qweb parser", () => {
tag: "h2",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -692,7 +664,6 @@ describe("qweb parser", () => {
value: "value",
defaultValue: null,
body: null,
hasNoRepresentation: true,
});
});
@@ -703,7 +674,6 @@ describe("qweb parser", () => {
defaultValue: "ok",
value: null,
body: null,
hasNoRepresentation: true,
});
expect(parse(`<t t-set="v"><div>ok</div></t>`)).toEqual({
@@ -715,7 +685,6 @@ describe("qweb parser", () => {
{
type: ASTType.DomNode,
attrs: null,
attrsTranslationCtx: null,
on: null,
tag: "div",
dynamicTag: null,
@@ -725,7 +694,6 @@ describe("qweb parser", () => {
content: [{ type: ASTType.Text, value: "ok" }],
},
],
hasNoRepresentation: true,
});
expect(parse(`<t t-set="v"><div>ok</div>abc</t>`)).toEqual({
@@ -737,7 +705,6 @@ describe("qweb parser", () => {
{
type: ASTType.DomNode,
attrs: null,
attrsTranslationCtx: null,
on: null,
tag: "div",
dynamicTag: null,
@@ -748,7 +715,6 @@ describe("qweb parser", () => {
},
{ type: ASTType.Text, value: "abc" },
],
hasNoRepresentation: true,
});
});
@@ -762,7 +728,6 @@ describe("qweb parser", () => {
defaultValue: "ok",
value: null,
body: null,
hasNoRepresentation: true,
},
tElif: null,
tElse: null,
@@ -777,7 +742,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -788,14 +752,7 @@ describe("qweb parser", () => {
condition: "flag",
content: { type: ASTType.Text, value: "1" },
tElif: null,
tElse: {
type: ASTType.TSet,
name: "ourvar",
value: "0",
defaultValue: null,
body: null,
hasNoRepresentation: true,
},
tElse: { type: ASTType.TSet, name: "ourvar", value: "0", defaultValue: null, body: null },
},
],
});
@@ -854,7 +811,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -897,7 +853,6 @@ describe("qweb parser", () => {
tag: "span",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -932,7 +887,6 @@ describe("qweb parser", () => {
tag: "span",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -966,7 +920,6 @@ describe("qweb parser", () => {
"t-att-selected": "category.id==options.active_category_id",
"t-att-value": "category.id",
},
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -987,7 +940,6 @@ describe("qweb parser", () => {
).toEqual({
type: ASTType.DomNode,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -1035,7 +987,6 @@ describe("qweb parser", () => {
ref: null,
model: null,
attrs: null,
attrsTranslationCtx: null,
ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
},
@@ -1059,7 +1010,6 @@ describe("qweb parser", () => {
name: "Comp",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
slots: null,
on: null,
},
@@ -1149,7 +1099,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -1190,7 +1139,6 @@ describe("qweb parser", () => {
tag: "button",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: { click: "add" },
ref: null,
model: null,
@@ -1227,7 +1175,6 @@ describe("qweb parser", () => {
tag: "select",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
content: [
@@ -1236,7 +1183,6 @@ describe("qweb parser", () => {
tag: "option",
dynamicTag: null,
attrs: { value: "1" },
attrsTranslationCtx: null,
on: null,
ref: null,
content: [],
@@ -1266,7 +1212,6 @@ describe("qweb parser", () => {
tag: "select",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
content: [
@@ -1275,7 +1220,6 @@ describe("qweb parser", () => {
tag: "option",
dynamicTag: null,
attrs: { "t-att-value": "valueVar" },
attrsTranslationCtx: null,
on: null,
ref: null,
content: [],
@@ -1307,7 +1251,6 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
on: null,
slots: null,
isDynamic: false,
@@ -1320,7 +1263,6 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: { a: "1", b: "'b'" },
propsTranslationCtx: null,
isDynamic: false,
on: null,
slots: null,
@@ -1333,7 +1275,6 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: "state",
props: { a: "1" },
propsTranslationCtx: null,
isDynamic: false,
on: null,
slots: null,
@@ -1346,7 +1287,6 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
isDynamic: false,
on: { click: "someMethod" },
slots: null,
@@ -1389,14 +1329,12 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
isDynamic: false,
on: null,
slots: {
default: {
content: { type: ASTType.Text, value: "foo" },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
@@ -1412,14 +1350,12 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
isDynamic: false,
on: null,
slots: {
default: {
content: { type: ASTType.Text, value: "foo" },
attrs: { param: "param" },
attrsTranslationCtx: null,
on: null,
scope: null,
},
@@ -1434,7 +1370,6 @@ describe("qweb parser", () => {
isDynamic: false,
dynamicProps: null,
props: null,
propsTranslationCtx: null,
on: null,
slots: {
default: {
@@ -1446,7 +1381,6 @@ describe("qweb parser", () => {
tag: "span",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
content: [],
ref: null,
model: null,
@@ -1458,7 +1392,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
content: [],
ref: null,
model: null,
@@ -1468,7 +1401,6 @@ describe("qweb parser", () => {
],
},
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
@@ -1483,11 +1415,9 @@ describe("qweb parser", () => {
name: "MyComponent",
on: null,
props: null,
propsTranslationCtx: null,
slots: {
mySlot: {
attrs: null,
attrsTranslationCtx: null,
content: null,
on: null,
scope: null,
@@ -1504,16 +1434,9 @@ describe("qweb parser", () => {
isDynamic: false,
dynamicProps: null,
props: null,
propsTranslationCtx: null,
on: null,
slots: {
name: {
content: { type: ASTType.Text, value: "foo" },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
},
});
});
@@ -1525,13 +1448,11 @@ describe("qweb parser", () => {
isDynamic: false,
dynamicProps: null,
props: null,
propsTranslationCtx: null,
on: null,
slots: {
name: {
content: { type: ASTType.Text, value: "foo" },
attrs: { param: "param" },
attrsTranslationCtx: null,
on: null,
scope: null,
},
@@ -1548,14 +1469,12 @@ describe("qweb parser", () => {
isDynamic: false,
dynamicProps: null,
props: null,
propsTranslationCtx: null,
on: null,
slots: {
name: {
content: { type: ASTType.Text, value: "foo" },
on: { click: "doStuff" },
attrs: null,
attrsTranslationCtx: null,
scope: null,
},
},
@@ -1574,24 +1493,16 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
isDynamic: false,
on: null,
slots: {
default: {
content: { type: ASTType.Text, value: " " },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
name: {
content: { type: ASTType.Text, value: "foo" },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
},
});
});
@@ -1607,24 +1518,11 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
isDynamic: false,
on: null,
slots: {
a: {
content: { type: ASTType.Text, value: "foo" },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
b: {
content: { type: ASTType.Text, value: "bar" },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
a: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
b: { content: { type: ASTType.Text, value: "bar" }, attrs: null, on: null, scope: null },
},
});
});
@@ -1635,7 +1533,6 @@ describe("qweb parser", () => {
name: "myComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
isDynamic: true,
on: null,
slots: null,
@@ -1648,7 +1545,6 @@ describe("qweb parser", () => {
name: "mycomponent",
dynamicProps: null,
props: { a: "1", b: "'b'" },
propsTranslationCtx: null,
isDynamic: true,
on: null,
slots: null,
@@ -1661,7 +1557,6 @@ describe("qweb parser", () => {
name: "mycomponent",
dynamicProps: "state",
props: { a: "1" },
propsTranslationCtx: null,
isDynamic: true,
on: null,
slots: null,
@@ -1692,14 +1587,12 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
isDynamic: false,
on: null,
slots: {
default: {
content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null },
attrs: null,
attrsTranslationCtx: null,
scope: null,
on: null,
},
@@ -1720,13 +1613,11 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
isDynamic: false,
on: null,
slots: {
default: {
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
content: {
@@ -1735,13 +1626,11 @@ describe("qweb parser", () => {
name: "Child",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
on: null,
slots: {
brol: {
content: { type: ASTType.Text, value: "coucou" },
attrs: null,
attrsTranslationCtx: null,
scope: null,
on: null,
},
@@ -1765,13 +1654,11 @@ describe("qweb parser", () => {
name: "MyComponent",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
isDynamic: false,
on: null,
slots: {
default: {
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
content: {
@@ -1780,13 +1667,11 @@ describe("qweb parser", () => {
name: "Child",
dynamicProps: null,
props: null,
propsTranslationCtx: null,
on: null,
slots: {
brol: {
content: { type: ASTType.Text, value: "coucou" },
attrs: null,
attrsTranslationCtx: null,
on: null,
scope: null,
},
@@ -1806,7 +1691,6 @@ describe("qweb parser", () => {
type: ASTType.TSlot,
name: "default",
attrs: null,
attrsTranslationCtx: null,
on: null,
defaultContent: null,
});
@@ -1817,7 +1701,6 @@ describe("qweb parser", () => {
type: ASTType.TSlot,
name: "header",
attrs: null,
attrsTranslationCtx: null,
on: null,
defaultContent: { type: ASTType.Text, value: "default content" },
});
@@ -1828,7 +1711,6 @@ describe("qweb parser", () => {
type: ASTType.TSlot,
name: "default",
attrs: null,
attrsTranslationCtx: null,
on: { "click.prevent": "doSomething" },
defaultContent: null,
});
@@ -1846,7 +1728,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -1865,7 +1746,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: null,
model: null,
@@ -1885,7 +1765,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: "name",
model: null,
@@ -1900,7 +1779,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: "name",
model: null,
@@ -1917,7 +1795,6 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: null,
attrs: null,
attrsTranslationCtx: null,
on: null,
ref: "name",
model: null,
@@ -1954,7 +1831,6 @@ describe("qweb parser", () => {
body: {
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
type: ASTType.Text,
@@ -1983,190 +1859,6 @@ describe("qweb parser", () => {
});
});
test('t-translation="off": interaction with t-esc', async () => {
expect(parse(`<span t-esc="a" t-translation="off"/>`)).toEqual({
type: ASTType.TTranslation,
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
defaultValue: "",
expr: "a",
type: ASTType.TEsc,
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "span",
type: ASTType.DomNode,
},
});
});
test('t-translation="off": interaction with t-out', async () => {
expect(parse(`<span t-out="a" t-translation="off"/>`)).toEqual({
type: ASTType.TTranslation,
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
body: null,
expr: "a",
type: ASTType.TOut,
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "span",
type: ASTType.DomNode,
},
});
});
// ---------------------------------------------------------------------------
// t-translation-context
// ---------------------------------------------------------------------------
test('t-translation-context="fr"', async () => {
expect(parse(`<t t-translation-context="fr">word</t>`)).toEqual({
type: ASTType.TTranslationContext,
content: {
type: ASTType.Text,
value: "word",
},
translationCtx: "fr",
});
expect(parse(`<div t-translation-context="fr">word</div>`)).toEqual({
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
type: 0,
value: "word",
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "div",
type: ASTType.DomNode,
},
translationCtx: "fr",
type: ASTType.TTranslationContext,
});
});
test("t-translation-context: interaction with t-esc", async () => {
expect(parse(`<span t-esc="a" t-translation-context="fr"/>`)).toEqual({
type: ASTType.TTranslationContext,
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
defaultValue: "",
expr: "a",
type: ASTType.TEsc,
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "span",
type: ASTType.DomNode,
},
translationCtx: "fr",
});
});
test("t-translation-context: interaction with t-out", async () => {
expect(parse(`<span t-out="a" t-translation-context="fr"/>`)).toEqual({
type: ASTType.TTranslationContext,
content: {
attrs: null,
attrsTranslationCtx: null,
content: [
{
body: null,
expr: "a",
type: ASTType.TOut,
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "span",
type: ASTType.DomNode,
},
translationCtx: "fr",
});
});
// ---------------------------------------------------------------------------
// t-translation-context-attr
// ---------------------------------------------------------------------------
test('t-translation-context="fr" and t-translation-context-title="pt" for a div attr title', async () => {
expect(
parse(
`<div t-translation-context="fr" title="hello" t-translation-context-title="pt">word</div>`
)
).toEqual({
content: {
attrs: { title: "hello" },
attrsTranslationCtx: { title: "pt" },
content: [
{
type: 0,
value: "word",
},
],
dynamicTag: null,
model: null,
ns: null,
on: null,
ref: null,
tag: "div",
type: ASTType.DomNode,
},
translationCtx: "fr",
type: ASTType.TTranslationContext,
});
});
test('t-translation-context-title="fr" for component prop title', async () => {
expect(parse(`<Comp title="hello" t-translation-context-title="fr" />`)).toEqual({
dynamicProps: null,
isDynamic: false,
name: "Comp",
on: null,
props: {
title: "hello",
},
propsTranslationCtx: {
title: "fr",
},
slots: null,
type: ASTType.TComponent,
});
});
// ---------------------------------------------------------------------------
// t-model
// ---------------------------------------------------------------------------
@@ -2174,7 +1866,6 @@ describe("qweb parser", () => {
expect(parse(`<input t-model="state.stuff" />`)).toEqual({
type: ASTType.DomNode,
attrs: null,
attrsTranslationCtx: null,
content: [],
on: null,
ref: null,
@@ -2195,7 +1886,6 @@ describe("qweb parser", () => {
expect(parse(`<input t-model="state['stuff']" />`)).toEqual({
type: ASTType.DomNode,
attrs: null,
attrsTranslationCtx: null,
content: [],
on: null,
ref: null,
@@ -2216,7 +1906,6 @@ describe("qweb parser", () => {
expect(parse(`<input t-model.lazy.trim.number="state.stuff" />`)).toEqual({
type: ASTType.DomNode,
attrs: null,
attrsTranslationCtx: null,
content: [],
on: null,
ref: null,
@@ -2238,7 +1927,6 @@ describe("qweb parser", () => {
expect(parse(`<textarea t-model="state.stuff" />`)).toEqual({
type: ASTType.DomNode,
attrs: null,
attrsTranslationCtx: null,
content: [],
on: null,
ref: null,
@@ -2259,7 +1947,6 @@ describe("qweb parser", () => {
expect(parse(`<input type="checkbox" t-model="state.stuff" />`)).toEqual({
type: ASTType.DomNode,
attrs: { type: "checkbox" },
attrsTranslationCtx: null,
content: [],
on: null,
ref: null,
@@ -2280,7 +1967,6 @@ describe("qweb parser", () => {
expect(parse(`<input type="radio" t-model="state.stuff" />`)).toEqual({
type: ASTType.DomNode,
attrs: { type: "radio" },
attrsTranslationCtx: null,
content: [],
on: null,
ref: null,
@@ -2301,7 +1987,6 @@ describe("qweb parser", () => {
expect(parse(`<input type="radio" t-model.lazy.trim.number="state.stuff" />`)).toEqual({
type: ASTType.DomNode,
attrs: { type: "radio" },
attrsTranslationCtx: null,
content: [],
on: null,
ref: null,
@@ -2327,7 +2012,6 @@ describe("qweb parser", () => {
expect(parse(`<div t-tag="theTag" />`)).toEqual({
type: ASTType.DomNode,
attrs: null,
attrsTranslationCtx: null,
content: [],
on: null,
ref: null,
-57
View File
@@ -1,57 +0,0 @@
import { App, Component, xml } from "../../src";
import { makeTestFixture, snapshotEverything } from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("t-custom", () => {
test("can use t-custom directive on a node", async () => {
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`<div t-custom-plop="click" class="my-div"/>`;
click() {
steps.push("clicked");
}
}
const app = new App(SomeComponent, {
customDirectives: {
plop: (node, value) => {
node.setAttribute("t-on-click", value);
},
},
});
await app.mount(fixture);
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
fixture.querySelector("div")!.click();
expect(steps).toEqual(["clicked"]);
});
test("can use t-custom directive with modifiers on a node", async () => {
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`<div t-custom-plop.mouse.stop="click" class="my-div"/>`;
click() {
steps.push("clicked");
}
}
const app = new App(SomeComponent, {
customDirectives: {
plop: (node, value, modifiers) => {
node.setAttribute("t-on-click", value);
for (let mod of modifiers) {
steps.push(mod);
}
},
},
});
await app.mount(fixture);
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
fixture.querySelector("div")!.click();
expect(steps).toEqual(["mouse", "stop", "clicked"]);
});
});
-30
View File
@@ -38,34 +38,4 @@ describe("debugging", () => {
expect(console.log).toHaveBeenCalledWith(45);
console.log = consoleLog;
});
test("t-log: interaction with t-set", () => {
const consoleLog = console.log;
console.log = jest.fn();
const template = `
<t>
<t t-log="foo" t-set="foo" t-value="42"/>
<t t-log="bar" t-set="bar" t-value="49"/>
<span t-esc="foo + bar"/>
</t>
`;
snapshotTemplate(template);
renderToString(template);
expect(console.log).toHaveBeenCalledWith(undefined);
expect(console.log).toHaveBeenCalledWith(undefined);
console.log = consoleLog;
});
test("t-debug: interaction with t-set", () => {
const template = `
<t>
<t t-debug="" t-set="foo" t-value="42"/>
<t t-debug="" t-set="bar" t-value="49"/>
<span t-esc="foo + bar"/>
</t>
`;
snapshotTemplate(template);
renderToString(template);
});
});
-6
View File
@@ -63,10 +63,4 @@ describe("t-key", () => {
expect(renderToString(template2, { key: "1" })).toBe("<div><h1></h1></div>");
});
test("t-key: interaction with t-esc", async () => {
const template = `<p t-key="key" t-esc="text"/>`;
expect(renderToString(template, { key: "1", text: "abc" })).toBe("<p>abc</p>");
});
});
-15
View File
@@ -1,15 +0,0 @@
import { parseXML } from "../../src/common/utils";
import { compile } from "../../src/compiler";
describe("t-slot", () => {
test("compile t-props correctly multiple time", () => {
const template = `<t t-slot="default" t-props="{ a: 1 }"/>`;
const parsedTemplate = parseXML(template).firstChild as Element;
const fn1 = compile(parsedTemplate);
expect(fn1.toString()).toMatchSnapshot();
const fn2 = compile(parsedTemplate);
expect(fn2.toString()).toBe(fn1.toString());
});
});
+2 -172
View File
@@ -86,7 +86,7 @@ describe("translation support", () => {
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div> mot </div>");
expect(translateFn).toHaveBeenCalledWith("word", "");
expect(translateFn).toHaveBeenCalledWith("word");
});
test("translation works, even if initial string has inner consecutive white space", async () => {
@@ -97,7 +97,7 @@ describe("translation support", () => {
const translateFn = jest.fn((expr: string) => (expr === "some word" ? "un mot" : expr));
await mount(SomeComponent, fixture, { translateFn });
expect(translateFn).toHaveBeenCalledWith("some word", "");
expect(translateFn).toHaveBeenCalledWith("some word");
expect(fixture.innerHTML).toBe("<div>un mot</div>");
});
@@ -129,21 +129,6 @@ describe("translation support", () => {
expect(fixture.innerHTML).toBe("untranslated");
});
test("body of t-sets inside translation=off are not translated 2", async () => {
class SomeComponent extends Component {
static template = xml`
<t>
<t t-translation="off" t-set="label">untranslated</t>
<t t-esc="label"/>
</t>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("untranslated");
});
test("body of t-sets with html content are translated", async () => {
class SomeComponent extends Component {
static template = xml`
@@ -185,159 +170,4 @@ describe("translation support", () => {
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("translated");
});
test("t-translation with several children", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-translation="off">
<div/>
<div/>
</t>
<t t-if="true"/>
</div>
`;
}
await mount(SomeComponent, fixture);
expect(fixture.outerHTML).toBe("<div><div><div></div><div></div></div></div>");
});
});
describe("translation context", () => {
test("translation of text in context", async () => {
class SomeComponent extends Component {
static template = xml`
<div>word</div>
<div t-translation-context="fr">word</div>
`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? (expr === "word" ? "mot" : expr) : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div>word</div><div>mot</div>");
expect(translateFn).toHaveBeenCalledWith("word", "");
expect(translateFn).toHaveBeenCalledWith("word", "fr");
});
test("translation of attributes in context", async () => {
class SomeComponent extends Component {
static template = xml`
<div t-translation-context="en" t-translation-context-title="fr" title="title" label="game"/>
`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? (expr === "title" ? "titre" : expr) : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe(`<div title="titre" label="game"></div>`);
expect(translateFn).toHaveBeenCalledWith("title", "fr");
expect(translateFn).toHaveBeenCalledWith("game", "en");
});
test("body of t-sets are translated in context", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label" t-translation-context="fr">untranslated</t>
<t t-esc="label"/>`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? "traduit" : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("traduit");
expect(translateFn).toHaveBeenCalledWith("untranslated", "fr");
});
test("props with modifier .translate are translated in context", async () => {
class ChildComponent extends Component {
static props = ["text"];
static template = xml`<span t-esc="props.text"/>`;
}
class SomeComponent extends Component {
static components = { ChildComponent };
static template = xml`
<ChildComponent text.translate="game" t-translation-context-text.translate="fr" />`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? "jeu" : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<span>jeu</span>");
expect(translateFn).toHaveBeenCalledWith("game", "fr");
});
test("slot attrs and text contents are translated in context", async () => {
class ChildComponent extends Component {
static template = xml`
<div t-translation-context="ja">
<t t-slot="a"/>
</div>`;
}
class SomeComponent extends Component {
static components = { ChildComponent };
static template = xml`
<ChildComponent t-translation-context="fr">
<t t-set-slot="a" title.translate="title" t-translation-context-title.translate="pt">game</t>
</ChildComponent>
`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "fr" ? "jeu" : translationCtx === "pt" ? "título" : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div>jeu</div>");
expect(translateFn).toHaveBeenCalledWith("game", "fr");
expect(translateFn).toHaveBeenCalledWith("title", "pt");
});
test("default slot params and content translated in context", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t
t-slot="default"
t-translation-context="fr"
param.translate="param"
title.translate="title"
t-translation-context-title.translate="pt"
>
foo
</t>
</div>`;
}
const translateFn = jest.fn((expr: string, translationCtx: string) =>
translationCtx === "pt" ? "título" : expr
);
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div> foo </div>");
expect(translateFn).toHaveBeenCalledWith("foo", "fr");
expect(translateFn).toHaveBeenCalledWith("param", "fr");
expect(translateFn).toHaveBeenCalledWith("title", "pt");
});
test("t-translation-context with several children", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-translation-context="ctx">
<div/>
<div/>
</t>
<t t-if="true"/>
</div>
`;
}
await mount(SomeComponent, fixture);
expect(fixture.outerHTML).toBe("<div><div><div></div><div></div></div></div>");
});
});
@@ -94,52 +94,6 @@ exports[`basics no component catching error lead to full app destruction 2`] = `
}"
`;
exports[`basics render from above on error -- handler is not a Root or MountFiber 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Parent\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`basics render from above on error -- handler is not a Root or MountFiber 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Boom\`, true, false, false, []);
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2, b3;
if (ctx['error']) {
b2 = text(\`Error\`);
} else {
b3 = comp1({onError: (ctx['handleError']).bind(this)}, key + \`__1\`, node, this, null);
}
return block1([], [b2, b3]);
}
}"
`;
exports[`basics render from above on error -- handler is not a Root or MountFiber 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['a'].b.c;
return block1([txt1]);
}
}"
`;
exports[`basics simple catchError 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -366,50 +320,6 @@ exports[`can catch errors can catch an error in a component render function 3`]
}"
`;
exports[`can catch errors can catch an error in onmounted 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(null, false, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2, b3;
b2 = text(\`Main\`);
if (ctx['state'].ok) {
const Comp1 = ctx['component'];
b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors can catch an error in onmounted 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>Error!!!</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors can catch an error in onmounted 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>perfect</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors can catch an error in the constructor call of a component render function 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1237,135 +1147,6 @@ exports[`can catch errors error in mounted on a component with a sibling (proper
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(null, false, false, false, []);
return function template(ctx, node, key = \\"\\") {
const Comp1 = ctx['component'];
return toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
const comp2 = app.createComponent(\`Boom\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`parent\`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
const b4 = comp2({}, key + \`__2\`, node, this, null);
return multi([b2, b3, b4]);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`abc\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`boom\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`def\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(null, false, false, false, []);
return function template(ctx, node, key = \\"\\") {
let b2, b3;
b2 = text(\`R\`);
if (ctx['state'].gogogo) {
const Comp1 = ctx['component'];
b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
const comp2 = app.createComponent(\`Boom\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`parent\`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
const b4 = comp2({}, key + \`__2\`, node, this, null);
return multi([b2, b3, b4]);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`abc\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`boom\`);
}
}"
`;
exports[`can catch errors error in onMounted, graceful recovery, variation 6`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`def\`);
}
}"
`;
exports[`can catch errors onError in class inheritance is called if rethrown 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -332,6 +332,31 @@ exports[`lifecycle hooks lifecycle semantics, part 3 1`] = `
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 3 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`GrandChild\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 3 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 4 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -0,0 +1,258 @@
import { Component, markup, mount, useState, xml } from "../../src";
import { useAttachedEl } from "../../src/runtime/hooks";
import { makeTestFixture, nextTick, steps, useLogLifecycle } from "../helpers";
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
});
describe("basics", () => {
test("can attach an empty component", async () => {
fixture.innerHTML = "<div>hello</div>";
class Test extends Component {
setup() {
useLogLifecycle();
}
}
await mount(Test, fixture, { position: "attach" });
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Test:setup",
"Test:willStart",
"Test:willRender",
"Test:rendered",
"Test:mounted",
]
`);
expect(fixture.innerHTML).toBe("<div>hello</div>");
});
test("attaching a component with a template throws", async () => {
fixture.innerHTML = "<div>hello</div>";
class Test extends Component {
static template = xml`hello`;
}
let error: Error | null = null;
try {
await mount(Test, fixture, { position: "attach" });
} catch (e: any) {
error = e;
}
expect(error!.message).toBe("Cannot attach a component with a template");
expect(fixture.innerHTML).toBe("<div>hello</div>");
});
test("can attach a component with simple dynamic content", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
class Test extends Component {
static dynamicContent = {
"p:t-att-a": "value",
};
value: string = "";
setup() {
this.value = "b";
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe('<div><p a="b">hello</p></div>');
});
test("useAttachedEl returns the attached element", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
let el: any = null;
class Test extends Component {
setup() {
el = useAttachedEl();
}
}
await mount(Test, fixture, { position: "attach" });
expect(el).toBe(fixture);
});
test("useAttachedEl throws if component is not attached", async () => {
class Test extends Component {
static template = xml`hello`;
setup() {
useAttachedEl();
}
}
let error: any = null;
try {
await mount(Test, fixture);
} catch (_e: any) {
error = _e;
}
expect(error.message).toBe("useAttachedEl can only be called with component that are attached");
});
test("multiple dynamic attribute", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
class Test extends Component {
static dynamicContent = {
"p:t-att-a": "value",
"p:t-att-b": "value + 'coucou'",
};
value: string = "";
setup() {
this.value = "b";
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe('<div><p a="b" b="bcoucou">hello</p></div>');
});
test("attrs can target root", async () => {
fixture.innerHTML = "<p>hello</p>";
class Test extends Component {
static dynamicContent = {
"root:t-att-a": "value",
};
value: string = "";
setup() {
this.value = "b";
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.outerHTML).toBe('<div a="b"><p>hello</p></div>');
});
test("dynamic attribute is updated on rerender", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
class Test extends Component {
static dynamicContent = {
"p:t-att-a": "state.value",
};
state: any;
setup() {
this.state = useState({ value: 1 });
}
}
const test = await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe('<div><p a="1">hello</p></div>');
test.state.value = 2;
await nextTick();
expect(fixture.innerHTML).toBe('<div><p a="2">hello</p></div>');
});
test("t-on-click, basic", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
let ev: Event | null = null;
class Test extends Component {
static dynamicContent = {
"p:t-att-a": "state.value",
"p:t-on-click": "onClick",
};
state: any;
setup() {
this.state = useState({ value: 1 });
}
onClick(_ev: any) {
ev = _ev;
this.state.value = 2;
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe('<div><p a="1">hello</p></div>');
fixture.querySelector("p")!.click();
expect(ev).toBeInstanceOf(Event);
await nextTick();
expect(fixture.innerHTML).toBe('<div><p a="2">hello</p></div>');
});
test("t-on-click, target root element", async () => {
fixture.innerHTML = "hello";
let click = false;
class Test extends Component {
static dynamicContent = {
"root:t-on-click": "onClick",
};
onClick() {
click = true;
}
}
await mount(Test, fixture, { position: "attach" });
fixture.click();
expect(click).toBe(true);
});
test("t-out, basic", async () => {
fixture.innerHTML = "<div><p>hello</p></div>";
class Test extends Component {
static dynamicContent = {
"p:t-out": "state.value",
};
state: any;
setup() {
this.state = useState({ value: 1 });
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
});
test("t-out, on root", async () => {
fixture.innerHTML = "hello";
class Test extends Component {
static dynamicContent = {
"root:t-out": "state.value",
};
state: any;
setup() {
this.state = useState({ value: 1 });
}
}
expect(fixture.outerHTML).toBe("<div>hello</div>");
await mount(Test, fixture, { position: "attach" });
expect(fixture.outerHTML).toBe("<div>1</div>");
});
test("t-out, with markup", async () => {
fixture.innerHTML = `<p class="p1">hello</p><p class="p2">hello</p>`;
class Test extends Component {
static dynamicContent = {
"p.p1:t-out": "value1",
"p.p2:t-out": "value2",
};
value1: any;
value2: any;
setup() {
this.value1 = "<div>value1</div>";
this.value2 = markup("<div>value2</div>");
}
}
await mount(Test, fixture, { position: "attach" });
expect(fixture.innerHTML).toBe(
`<p class="p1">&lt;div&gt;value1&lt;/div&gt;</p><p class="p2"><div>value2</div></p>`
);
});
});
-308
View File
@@ -233,44 +233,6 @@ function(app, bdom, helpers) {
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("render from above on error -- handler is not a Root or MountFiber", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
setup() {
onError((err) => {
this.props.onError(err);
});
}
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Boom onError.bind="handleError"/>
</t>
</div>`;
static components = { Boom };
error: any = false;
handleError(err: Error) {
this.error = err;
this.render();
}
}
class GrandParent extends Component {
static template: string = xml`<Parent />`;
static components = { Parent };
}
await mount(GrandParent, fixture);
expect(fixture.innerHTML).toBe("<div>Error</div>");
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
});
describe("errors and promises", () => {
@@ -602,82 +564,6 @@ describe("can catch errors", () => {
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("can catch an error in onmounted", async () => {
class ErrorComponent extends Component {
static template = xml`<div>Error!!!</div>`;
setup() {
useLogLifecycle();
onMounted(() => {
throw new Error("error");
});
}
}
class PerfectComponent extends Component {
static template = xml`<div>perfect</div>`;
setup() {
useLogLifecycle();
}
}
class Main extends Component {
static template = xml`Main<t t-if="state.ok" t-component="component"/>`;
component: any;
state: any;
setup() {
this.state = useState({ ok: false });
useLogLifecycle();
this.component = ErrorComponent;
onError(() => {
this.component = PerfectComponent;
this.render();
});
}
}
const app = await mount(Main, fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Main:setup",
"Main:willStart",
"Main:willRender",
"Main:rendered",
"Main:mounted",
]
`);
expect(fixture.innerHTML).toBe("Main");
(app as any).state.ok = true;
await nextTick();
expect(fixture.innerHTML).toBe("Main<div>Error!!!</div>");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Main:willRender",
"ErrorComponent:setup",
"ErrorComponent:willStart",
"Main:rendered",
"ErrorComponent:willRender",
"ErrorComponent:rendered",
"Main:willPatch",
"ErrorComponent:mounted",
"Main:willRender",
"PerfectComponent:setup",
"PerfectComponent:willStart",
"Main:rendered",
]
`);
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"PerfectComponent:willRender",
"PerfectComponent:rendered",
"Main:willPatch",
"ErrorComponent:willUnmount",
"ErrorComponent:willDestroy",
"PerfectComponent:mounted",
"Main:patched",
]
`);
expect(fixture.innerHTML).toBe("Main<div>perfect</div>");
});
test("calling a hook outside setup should crash", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
@@ -1716,198 +1602,4 @@ describe("can catch errors", () => {
`);
expect(fixture.innerHTML).toBe("2");
});
test("error in onMounted, graceful recovery", async () => {
class Child extends Component {
static template = xml`abc`;
setup() {
useLogLifecycle();
}
}
class OtherChild extends Component {
static template = xml`def`;
setup() {
useLogLifecycle();
}
}
class Boom extends Component {
static template = xml`boom`;
setup() {
useLogLifecycle();
onMounted(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`parent<Child/><Boom/>`;
static components = { Child, Boom };
setup() {
useLogLifecycle();
}
}
class Root extends Component {
static template = xml`<t t-component="component"/>`;
component: any = Parent;
setup() {
useLogLifecycle();
onError(() => {
logStep("error");
this.component = OtherChild;
this.render();
});
}
}
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("def");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Root:setup",
"Root:willStart",
"Root:willRender",
"Parent:setup",
"Parent:willStart",
"Root:rendered",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Boom:setup",
"Boom:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Boom:willRender",
"Boom:rendered",
"Boom:mounted",
"error",
"Root:willRender",
"OtherChild:setup",
"OtherChild:willStart",
"Root:rendered",
"OtherChild:willRender",
"OtherChild:rendered",
"OtherChild:mounted",
"Root:mounted",
]
`);
});
test("error in onMounted, graceful recovery, variation", async () => {
class Child extends Component {
static template = xml`abc`;
setup() {
useLogLifecycle();
}
}
class OtherChild extends Component {
static template = xml`def`;
setup() {
useLogLifecycle();
}
}
class Boom extends Component {
static template = xml`boom`;
setup() {
useLogLifecycle();
onMounted(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`parent<Child/><Boom/>`;
static components = { Child, Boom };
setup() {
useLogLifecycle();
}
}
class Root extends Component {
static template = xml`R<t t-if="state.gogogo" t-component="component"/>`;
component: any = Parent;
state = useState({ gogogo: false });
setup() {
useLogLifecycle();
onError(() => {
logStep("error");
this.component = OtherChild;
this.render();
});
}
}
const root = await mount(Root, fixture);
expect(fixture.innerHTML).toBe("R");
// standard mounting process
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Root:setup",
"Root:willStart",
"Root:willRender",
"Root:rendered",
"Root:mounted",
]
`);
root.state.gogogo = true;
await nextTick();
expect(fixture.innerHTML).toBe("Rparentabcboom");
// rerender, root creates sub components, it crashes, tries to recover
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Root:willRender",
"Parent:setup",
"Parent:willStart",
"Root:rendered",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Boom:setup",
"Boom:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Boom:willRender",
"Boom:rendered",
"Root:willPatch",
"Boom:mounted",
"error",
"Root:willRender",
"OtherChild:setup",
"OtherChild:willStart",
"Root:rendered",
]
`);
await nextTick();
expect(fixture.innerHTML).toBe("Rdef");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"OtherChild:willRender",
"OtherChild:rendered",
"Root:willPatch",
"Child:willDestroy",
"Boom:willUnmount",
"Boom:willDestroy",
"Parent:willDestroy",
"OtherChild:mounted",
"Root:patched",
]
`);
});
});
+1 -1
View File
@@ -339,7 +339,7 @@ test("throw if prop uses an unknown suffix", async () => {
await expect(async () => {
await mount(Parent, fixture);
}).rejects.toThrowError("Invalid prop suffix: somesuffix");
}).rejects.toThrowError("Invalid prop suffix");
});
test(".alike suffix in a simple case", async () => {
+3 -3
View File
@@ -1,6 +1,6 @@
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
import { Component, onError, xml, mount, OwlError, useState } from "../../src";
import { App } from "../../src/runtime/app";
import { App, DEV_MSG } from "../../src/runtime/app";
import { validateProps } from "../../src/runtime/template_helpers";
import { Schema } from "../../src/runtime/validation";
@@ -13,7 +13,7 @@ let mockConsoleWarn: any;
beforeAll(() => {
console.info = (message: any) => {
if (message === `Owl is running in 'dev' mode.`) {
if (message === DEV_MSG()) {
return;
}
info(message);
@@ -702,7 +702,7 @@ describe("props validation", () => {
const app = new App(Parent, { test: true });
await app.mount(fixture);
expect(fixture.innerHTML).toBe("12");
expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
expect(app.root!.node!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
});
test("props are validated whenever component is updated", async () => {
+2 -1
View File
@@ -11,6 +11,7 @@ import {
useState,
} from "../../src";
import { xml } from "../../src/";
import { DEV_MSG } from "../../src/runtime/app";
import { elem, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
let fixture: HTMLElement;
@@ -29,7 +30,7 @@ snapshotEverything();
beforeAll(() => {
console.info = (message: any) => {
if (message === `Owl is running in 'dev' mode.`) {
if (message === DEV_MSG()) {
return;
}
info(message);
@@ -27,58 +27,6 @@ exports[`shadow_dom can mount app 1`] = `
}"
`;
exports[`shadow_dom can mount app in closed shadow dom 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`shadow_dom can mount app inside a separate HTML document 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`shadow_dom can mount app inside a shadow child element 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`shadow_dom can mount app inside an element in a shadow root inside an iframe 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`shadow_dom useRef hook 1`] = `
"function anonymous(app, bdom, helpers
) {
-87
View File
@@ -29,24 +29,6 @@ describe("shadow_dom", () => {
expect(status(comp)).toBe("destroyed");
});
test("can mount app in closed shadow dom", async () => {
class SomeComponent extends Component {
static template = xml`<div class="my-div"/>`;
}
const container = document.createElement("div");
fixture.appendChild(container);
const shadow = container.attachShadow({ mode: "closed" });
const app = new App(SomeComponent);
const comp = await app.mount(shadow);
const div = shadow.querySelector(".my-div");
expect(div).not.toBe(null);
expect(shadow.contains(div)).toBe(true);
app.destroy();
expect(shadow.contains(div)).toBe(false);
expect(status(comp)).toBe("destroyed");
});
test("can bind event handler", async () => {
let a = 1;
class SomeComponent extends Component {
@@ -82,73 +64,4 @@ describe("shadow_dom", () => {
await mountedProm;
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
});
test("can mount app inside a shadow child element", async () => {
class SomeComponent extends Component {
static template = xml`<div class="my-div"/>`;
}
const shadow = fixture.attachShadow({ mode: "open" });
const shadowDiv = document.createElement("div");
shadow.append(shadowDiv);
const app = new App(SomeComponent);
const comp = await app.mount(shadowDiv);
const div = shadow.querySelector(".my-div");
expect(div).not.toBe(null);
expect(shadow.contains(div)).toBe(true);
app.destroy();
expect(shadow.contains(div)).toBe(false);
expect(status(comp)).toBe("destroyed");
});
test("can mount app inside a separate HTML document", async () => {
class SomeComponent extends Component {
static template = xml`<div class="my-div"/>`;
}
const separateDoc = document.implementation.createHTMLDocument();
const container = separateDoc.createElement("div");
separateDoc.body.appendChild(container);
const app = new App(SomeComponent);
let error: Error;
try {
await app.mount(container);
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
);
});
test("can mount app inside an element in a shadow root inside an iframe", async () => {
class SomeComponent extends Component {
static template = xml`<div class="my-div"/>`;
}
const iframe = document.createElement("iframe");
fixture.appendChild(iframe);
const iframeDoc = iframe.contentDocument!;
const container = iframeDoc.createElement("div");
iframeDoc.body.appendChild(container);
const shadow = container.attachShadow({ mode: "open" });
const shadowTarget = iframeDoc.createElement("div");
shadow.appendChild(shadowTarget);
const app = new App(SomeComponent);
const comp = await app.mount(shadowTarget);
const div = shadow.querySelector(".my-div");
expect(div).not.toBe(null);
expect(shadow.contains(div)).toBe(true);
expect(iframeDoc.body.contains(container)).toBe(true);
app.destroy();
expect(shadow.contains(div)).toBe(false);
expect(status(comp)).toBe("destroyed");
});
});
+1 -92
View File
@@ -1,4 +1,4 @@
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
import { batched, EventBus } from "../src/runtime/utils";
import { nextMicroTick } from "./helpers";
describe("event bus behaviour", () => {
@@ -71,94 +71,3 @@ describe("batched", () => {
expect(n).toBe(2);
});
});
const Markup = markup("").constructor;
describe("markup", () => {
test("string is flagged as safe", () => {
const html = markup("<blink>Hello</blink>");
expect(html).toBeInstanceOf(Markup);
});
describe("htmlEscape", () => {
test("htmlEscape escapes text", () => {
const res = htmlEscape("<p>test</p>");
expect(res.toString()).toBe("&lt;p&gt;test&lt;/p&gt;");
expect(res).toBeInstanceOf(Markup);
});
test("htmlEscape keeps html markup", () => {
const res = htmlEscape(markup("<p>test</p>"));
expect(res.toString()).toBe("<p>test</p>");
expect(res).toBeInstanceOf(Markup);
});
test("htmlEscape produces empty string on undefined", () => {
const res = htmlEscape(undefined);
expect(res.toString()).toBe("");
expect(res).toBeInstanceOf(Markup);
});
test("htmlEscape produces string from number", () => {
const res = htmlEscape(10);
expect(res.toString()).toBe("10");
expect(res).toBeInstanceOf(Markup);
});
test("htmlEscape produces string from boolean", () => {
const res = htmlEscape(false);
expect(res.toString()).toBe("false");
expect(res).toBeInstanceOf(Markup);
});
test("htmlEscape correctly escapes various links", () => {
expect(htmlEscape("<a>this is a link</a>").toString()).toBe(
"&lt;a&gt;this is a link&lt;/a&gt;"
);
expect(htmlEscape(`<a href="https://www.odoo.com">odoo<a>`).toString()).toBe(
`&lt;a href=&quot;https://www.odoo.com&quot;&gt;odoo&lt;a&gt;`
);
expect(htmlEscape(`<a href='https://www.odoo.com'>odoo<a>`).toString()).toBe(
`&lt;a href=&#x27;https://www.odoo.com&#x27;&gt;odoo&lt;a&gt;`
);
expect(htmlEscape("<a href='https://www.odoo.com'>Odoo`s website<a>").toString()).toBe(
`&lt;a href=&#x27;https://www.odoo.com&#x27;&gt;Odoo&#x60;s website&lt;a&gt;`
);
});
test("htmlEscape doesn't escape already escaped content", () => {
const res = htmlEscape("<p>test</p>");
expect(res.toString()).toBe("&lt;p&gt;test&lt;/p&gt;");
expect(res).toBeInstanceOf(Markup);
const res2 = htmlEscape(res);
expect(res2.toString()).toBe("&lt;p&gt;test&lt;/p&gt;");
expect(res2).toBeInstanceOf(Markup);
expect(res2).toBe(res);
});
test("htmlEscape returns markup even for only-safe text", () => {
const res = htmlEscape("safe");
expect(res.toString()).toBe("safe");
expect(res).toBeInstanceOf(Markup);
});
});
describe("tag function", () => {
test("interpolated values are escaped", () => {
const maliciousInput = "<script>alert('💥💥')</script>";
const html = markup`<b>${maliciousInput}</b>`;
expect(html.toString()).toBe("<b>&lt;script&gt;alert(&#x27;💥💥&#x27;)&lt;/script&gt;</b>");
expect(html).toBeInstanceOf(Markup);
});
test("interpolated markups aren't escaped", () => {
const shouldBeEscaped = "<script>alert('should be escaped')</script>";
const shouldnt = markup("<b>this is safe</b>");
const html = markup`<div>${shouldBeEscaped} ${shouldnt}</div>`;
expect(html.toString()).toBe(
"<div>&lt;script&gt;alert(&#x27;should be escaped&#x27;)&lt;/script&gt; <b>this is safe</b></div>"
);
expect(html).toBeInstanceOf(Markup);
});
test("quotes in interpolated values are escaped", () => {
const imgUrl = `lol" onerror="alert('xss')`;
const html = markup`<img src="${imgUrl}">`;
expect(html.toString()).toBe(`<img src="lol&quot; onerror=&quot;alert(&#x27;xss&#x27;)">`);
});
test("already escaped content is not escaped again", () => {
const res = htmlEscape("<p>test</p>");
expect(res.toString()).toBe("&lt;p&gt;test&lt;/p&gt;");
const html = markup`${res}`;
expect(html.toString()).toBe("&lt;p&gt;test&lt;/p&gt;");
});
});
});
-31
View File
@@ -1,31 +0,0 @@
#!/usr/bin/env node
// this is the "compile_owl_templates" command that owl makes available when
// installed as a node_module.
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { dirname } from "path";
import { compileTemplates } from "../dist/compile_templates.mjs";
import { parseArgs } from "util";
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
output: {
type: "string",
short: "o",
default: "templates.js",
},
},
});
if (positionals.length) {
const result = await compileTemplates(positionals);
const outputPath = values.output;
const dir = dirname(outputPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(outputPath, result);
} else {
console.log("Please provide a path");
}
+113
View File
@@ -0,0 +1,113 @@
const fs = require("fs");
const path = require("path");
const jsdom = require("jsdom");
// -----------------------------------------------------------------------------
// add global DOM stuff for compiler
// -----------------------------------------------------------------------------
var document = new jsdom.JSDOM("", {});
var window = document.window;
global.document = window.document;
global.window = window;
global.DOMParser = window.DOMParser;
global.Element = window.Element;
global.Node = window.Node;
// this needs to be below the jsdom stuff
const { compile } = require("../dist/compiler.js");
// -----------------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------------
async function getXmlFiles(dir) {
let xmls = [];
const files = await fs.promises.readdir(dir);
const filesStats = await Promise.all(files.map((file) => fs.promises.stat(path.join(dir, file))));
for (let i in files) {
const name = path.join(dir, files[i]);
if (filesStats[i].isDirectory()) {
xmls = xmls.concat(await getXmlFiles(name));
} else {
if (name.endsWith(".xml")) {
xmls.push(name);
}
}
}
return xmls;
}
function writeToFile(filepath, data) {
if (!fs.existsSync(path.dirname(filepath))) {
fs.mkdirSync(path.dirname(filepath), { recursive: true });
}
fs.writeFile(filepath, data, (err) => {
if (err) {
process.stdout.write(`Error while writing file ${filepath}: ${err}`);
return;
}
});
}
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
const a = "·-_,:;";
const p = new RegExp(a.split("").join("|"), "g");
function slugify(str) {
return str
.replace(/\//g, "") // remove /
.replace(/\./g, "_") // Replace . with _
.replace(p, (c) => '_') // Replace special characters
.replace(/&/g, "_and_") // Replace & with and
.replace(/[^\w\-]+/g, "") // Remove all non-word characters
}
// -----------------------------------------------------------------------------
// main
// -----------------------------------------------------------------------------
async function compileTemplates(files) {
process.stdout.write(`Processing ${files.length} files`);
let xmlStrings = await Promise.all(files.map((file) => fs.promises.readFile(file, "utf8")));
const templates = [];
const errors = [];
for (let i = 0; i < files.length; i++) {
const fileName = files[i];
const fileContent = xmlStrings[i];
process.stdout.write(`.`);
const parser = new DOMParser();
const doc = parser.parseFromString(fileContent, "text/xml");
for (const template of doc.querySelectorAll("[t-name]")) {
const name = template.getAttribute("t-name");
if (template.hasAttribute("owl")) {
template.removeAttribute("owl")
}
const fnName = slugify(name);
try {
const fn = compile(template).toString().replace('anonymous', fnName);
templates.push(`"${name}": ${fn},\n`);
} catch (e) {
errors.push({ name, fileName, e });
}
}
}
process.stdout.write(`\n`);
for (let { name, fileName, e } of errors) {
console.warn(`Error while compiling '${name}' (in file ${fileName})`);
console.error(e);
}
console.log(`${templates.length} templates compiled`);
return `export const templates = {\n ${templates.join("\n")} \n}`;
}
const templatesPath = process.argv[2];
if (templatesPath && templatesPath.length) {
getXmlFiles(templatesPath).then(async (files) => {
const result = await compileTemplates(files);
writeToFile("templates.js", result);
});
} else {
console.log("Please provide a path");
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Owl devtools",
"version": "1.3.0",
"version": "1.2.2",
"manifest_version": 3,
"description": "Chrome devtools extension for Odoo Owl framework",
"icons": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Owl devtools",
"version": "1.1.0",
"version": "1.0.0",
"description": "Firefox devtools extension for Odoo Owl framework",
"manifest_version": 2,
"browser_specific_settings": {
@@ -1,36 +0,0 @@
import { useStore } from "../store/store";
const { Component, useEffect, useRef } = owl;
export class ContextMenu extends Component {
static template = "devtools.ContextMenu";
static props = {
items: Array,
};
setup() {
this.store = useStore();
this.contextMenu = useRef("contextmenu");
useEffect(
(position) => {
const menu = this.contextMenu.el;
const menuWidth = menu.offsetWidth;
const menuHeight = menu.offsetHeight;
let { x, y } = position;
if (x + menuWidth > window.innerWidth) {
x = window.innerWidth - menuWidth;
}
if (y + menuHeight > window.innerHeight) {
y = window.innerHeight - menuHeight;
}
menu.style.left = x + "px";
// Need 25px offset because of the main navbar from the browser devtools
menu.style.top = y + "px";
},
() => [this.store.contextMenu?.position]
);
}
onClickItem(action) {
action();
this.store.contextMenu = null;
}
}
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="devtools.ContextMenu">
<div class="custom-menu" t-ref="contextmenu">
<ul class="my-1">
<li t-foreach="props.items" t-as="item" t-key="item_index" t-if="item.show" t-esc="item.title" t-on-click.stop="() => this.onClickItem(item.action)" class="custom-menu-item py-1 px-4"/>
</ul>
</div>
</t>
</templates>
@@ -1,4 +1,4 @@
const { Component } = owl;
const { Component, useRef, useEffect } = owl;
import { useStore } from "../../../store/store";
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
@@ -7,64 +7,23 @@ export class DetailsWindow extends Component {
static components = { ObjectTreeElement };
setup() {
this.store = useStore();
}
get contextMenuItems() {
return [
{
title: "Inspect source code",
show: true,
action: () => this.store.inspectComponent("source", this.store.activeComponent.path),
this.contextMenu = useRef("contextmenu");
this.contextMenuId = this.store.contextMenu.id++;
this.contextMenuEvent;
// Open the context menu when the ids match
useEffect(
(menuId) => {
if (menuId === this.contextMenuId) {
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
}
},
{
title: "Store as global variable",
show: this.store.activeComponent.path.length !== 1,
action: () =>
this.store.logObjectInConsole([
...this.store.activeComponent.path,
{ type: "item", value: "component" },
]),
},
{
title: "Inspect in Elements tab",
show: this.store.activeComponent.path.length !== 1,
action: () => this.store.inspectComponent("DOM", this.store.activeComponent.path),
},
{
title: "Force rerender",
show: this.store.activeComponent.path.length !== 1,
action: () => this.store.refreshComponent(this.store.activeComponent.path),
},
{
title: "Store observed states as global variable",
show: this.store.activeComponent.path.length !== 1,
action: () =>
this.store.logObjectInConsole([
...this.store.activeComponent.path,
{ type: "item", value: "subscriptions" },
]),
},
{
title: "Inspect compiled template",
show: this.store.activeComponent.path.length !== 1,
action: () =>
this.store.inspectComponent("compiled template", this.store.activeComponent.path),
},
{
title: "Log raw template",
show: this.store.activeComponent.path.length !== 1,
action: () => this.store.inspectComponent("raw template", this.store.activeComponent.path),
},
{
title: "Store as global variable",
show: this.store.activeComponent.path.length === 1,
action: () => this.store.logObjectInConsole([...this.store.activeComponent.path]),
},
];
() => [this.store.contextMenu.activeMenu]
);
}
openMenu(ev) {
this.store.openContextMenu(ev, this.contextMenuItems);
this.contextMenuEvent = ev;
this.store.contextMenu.activeMenu = this.contextMenuId;
}
toggleCategory(ev, category) {
@@ -20,17 +20,6 @@
</t>
</div>
<div class="details-container">
<div t-if="store.observedVariables.length and store.observedVariables.some((v) => v.visible)" id="observedVariables" class="details-panel ps-2 py-1">
<div class="d-flex mb-2">
<div class="w-100">
<b class="ps-2">observed variables</b>
</div>
<i title="Remove observed variables" class="fa fa-times utility-icon p-1" t-on-click.stop="() => this.store.clearObservedVariable()"></i>
</div>
<t t-foreach="store.observedVariables" t-as="observed" t-key="observed_index" t-if="observed.visible">
<ObjectTreeElement object="observed" index="observed_index"/>
</t>
</div>
<div t-if="store.activeComponent.env.children.length > 0" id="env" class="details-panel ps-2 py-1">
<div class="d-flex mb-2">
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'env')">
@@ -70,7 +59,7 @@
</t>
</div>
</div>
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
<div class="d-flex mb-2">
<div class="w-100 text-truncate" t-on-click.stop="(ev) => this.toggleCategory(ev, 'instance')">
<i class="fa mx-1 pointer-icon"
@@ -86,19 +75,22 @@
<ObjectTreeElement object="instance"/>
</t>
</div>
<div t-if="store.activeComponent.hooks?.children.length > 0" id="hooks" class="details-panel ps-2 py-1">
<div class="d-flex mb-2">
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'hooks')">
<i class="fa mx-1 pointer-icon"
t-att-class="{'fa-caret-right': !store.activeComponent.hooks.toggled, 'fa-caret-down': store.activeComponent.hooks.toggled}"
/><b>hooks</b>
</div>
<i title="Remove breakpoints" class="fa fa-times utility-icon p-1" t-on-click.stop="() => this.store.removeBreakpoints()"></i>
</div>
<t t-if="store.activeComponent.hooks.toggled" t-foreach="store.activeComponent.hooks.children" t-as="hook" t-key="hook_index">
<ObjectTreeElement object="hook"/>
</div>
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.inspectComponent('source', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
<t t-if="store.activeComponent.path.length !== 1">
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
<li t-on-click.stop="() => this.store.inspectComponent('DOM', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
<li t-on-click.stop="() => this.store.refreshComponent(store.activeComponent.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
<li t-on-click.stop="() => this.store.inspectComponent('raw template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
</t>
</div>
<t t-else="">
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
</t>
</ul>
</div>
</t>
</templates>
</templates>
@@ -13,8 +13,19 @@ export class ObjectTreeElement extends Component {
menuTop: 0,
menuLeft: 0,
});
this.contextMenu = useRef("contextmenu");
const inputRef = useRef("input");
this.store = useStore();
this.contextMenuId = this.store.contextMenu.id++;
this.contextMenuEvent,
useEffect(
(menuId) => {
if (menuId === this.contextMenuId) {
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
}
},
() => [this.store.contextMenu.activeMenu]
);
useEffect(
(editMode) => {
// Focus on the input when it is created
@@ -52,76 +63,16 @@ export class ObjectTreeElement extends Component {
return this.props.object.depth * 0.8 + 0.3;
}
get contextMenuItems() {
return [
{
title: "Store as global variable",
show: true,
action: () => this.store.logObjectInConsole(this.props.object.path),
},
{
title: "Inspect function source code",
show: this.props.object.contentType === "function",
action: () => this.store.inspectFunctionSource(this.props.object.path),
},
{
title: "Observe variable",
show: this.props.object.objectType !== "observed",
action: () => this.store.observeVariable(this.props.object.path),
},
{
title: "Unobserve variable",
show: this.props.object.objectType === "observed",
action: () => this.store.clearObservedVariable(this.props.index),
},
{
title: "Inject breakpoint on component",
show: this.props.object.contentType === "array" && this.props.object.objectType === "hook",
action: () =>
this.store.injectBreakpoint(this.props.object.name, this.store.activeComponent.path),
},
{
title: "Inject conditional breakpoint on component",
show: this.props.object.contentType === "array" && this.props.object.objectType === "hook",
action: () => {
const condition = window.prompt("Enter the condition");
if (condition) {
this.store.injectBreakpoint(
this.props.object.name,
this.store.activeComponent.path,
false,
condition
);
}
},
},
{
title: "Inject breakpoint on instance",
show:
this.props.object.contentType === "array" &&
this.props.object.objectType === "hook" &&
!["mounted", "willStart"].includes(this.props.object.name),
action: () =>
this.store.injectBreakpoint(
this.props.object.name,
this.store.activeComponent.path,
true
),
},
];
}
openMenu(ev) {
this.store.openContextMenu(ev, this.contextMenuItems);
this.contextMenuEvent = ev;
this.store.contextMenu.activeMenu = this.contextMenuId;
}
setupEditMode() {
if (
!this.state.editMode &&
!this.props.object.hasChildren &&
!(this.props.object.objectType === "observed")
) {
this.state.editMode = true;
if (!this.state.editMode) {
if (!this.props.object.hasChildren) {
this.state.editMode = true;
}
}
}
@@ -4,7 +4,7 @@
<div class="m-0 p-0 text-nowrap w-100 object-line"
t-att-class="props.class + (props.object.hasChildren ? ' bg-feedback' : '')"
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
t-on-contextmenu.prevent="openMenu"
t-on-contextmenu.prevent="openMenu"
>
<div t-attf-style="padding-left: {{objectPadding}}rem">
<i class="fa px-1 pointer-icon caret"
@@ -12,7 +12,7 @@
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
/>
<t t-esc="props.object.name"/>
<t t-if="props.object.content.length > 0">: </t>
<t t-if="props.object.content.length > 0">: </t>
<t t-if="props.object.contentType == 'getter'">
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
<t t-esc="props.object.content"/>
@@ -20,17 +20,25 @@
</t>
<t t-else="">
<span class="object-content" t-att-class="objectLineClass" t-on-dblclick.stop="setupEditMode">
<t t-if="state.editMode">
<t t-if="state.editMode">
<input t-attf-id="objectEditionInput/{{pathAsString}}" t-ref="input" type="text" placeholder="" t-att-value="props.object.content" t-on-keydown.stop="editObject"/>
</t>
<t t-else="">
<t t-esc="props.object.content"/>
</t>
</span>
</span>
</t>
<span t-if="keyChanges" class="key-changes ms-1 badge p-1" title="Key additions/deletions are observed">+/-</span>
</div>
</div>
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
<ul class="my-1">
<li t-on-click="() => this.store.logObjectInConsole(this.props.object.path)" class="custom-menu-item py-1 px-4 text-nowrap">Store as global variable</li>
<t t-if='props.object.contentType == "function"'>
<li t-on-click="() => this.store.inspectFunctionSource(this.props.object.path)" class="custom-menu-item py-1 px-4 text-nowrap">Inspect function source code</li>
</t>
</ul>
</div>
<t t-if="props.object.toggled" t-key="contextMenuId">
<t t-foreach="props.object.children" t-as="child" t-key="child_index">
<ObjectTreeElement object="child" class="this.classFor(child)"/>
@@ -16,7 +16,10 @@ export class TreeElement extends Component {
searched: false,
});
this.store = useStore();
this.contextMenu = useRef("contextmenu");
this.element = useRef("element");
this.contextMenuId = this.store.contextMenu.id++;
this.contextMenuEvent;
this.stringifiedPath = JSON.stringify(this.props.component.path);
// Scroll to the selected element when it changes
onMounted(() => {
@@ -35,6 +38,15 @@ export class TreeElement extends Component {
},
() => [this.props.component.selected]
);
// Open the context menu when the ids match
useEffect(
(menuId) => {
if (menuId === this.contextMenuId) {
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
}
},
() => [this.store.contextMenu.activeMenu]
);
// Effect to apply a short highlight effect to the component when it is rendered
useEffect(
() => {
@@ -74,86 +86,9 @@ export class TreeElement extends Component {
return minimizeKey(this.props.component.key);
}
get contextMenuItems() {
return [
{
title: "Expand children",
show: true,
action: () => this.store.toggleComponentAndChildren(this.props.component, true),
},
{
title: "Fold all children",
show: true,
action: () => this.store.toggleComponentAndChildren(this.props.component, false),
},
{
title: "Fold direct children",
show: true,
action: () => this.store.foldDirectChildren(this.props.component),
},
{
title: "Inspect source code",
show: true,
action: () => this.store.inspectComponent("source", this.props.component.path),
},
{
title: "Store as global variable",
show: this.props.component.path.length !== 1,
action: () =>
this.store.logObjectInConsole([
...this.props.component.path,
{ type: "item", value: "component" },
]),
},
{
title: "Inspect in Elements tab",
show: this.props.component.path.length !== 1,
action: () => this.store.inspectComponent("DOM", this.props.component.path),
},
{
title: "Force rerender",
show: this.props.component.path.length !== 1,
action: () => this.store.refreshComponent(this.props.component.path),
},
{
title: "Store observed states as global variable",
show: this.props.component.path.length !== 1,
action: () =>
this.store.logObjectInConsole([
...this.props.component.path,
{ type: "item", value: "subscriptions" },
]),
},
{
title: "Inspect compiled template",
show: this.props.component.path.length !== 1,
action: () => this.store.inspectComponent("compiled template", this.props.component.path),
},
{
title: "Log raw template",
show: this.props.component.path.length !== 1,
action: () => this.store.inspectComponent("raw template", this.props.component.path),
},
{
title: "Store as global variable",
show: this.props.component.path.length === 1,
action: () => this.store.logObjectInConsole([...this.props.component.path]),
},
{
title: "Don't fold component by default",
show: this.store.settings.componentsToggleBlacklist.has(this.props.component.name),
action: () => this.toggleComponentToBlacklist(),
},
{
title: "Fold component by default",
show: !this.store.settings.componentsToggleBlacklist.has(this.props.component.name),
action: () => this.toggleComponentToBlacklist(),
},
];
}
openMenu(ev) {
this.store.openContextMenu(ev, this.contextMenuItems);
this.contextMenuEvent = ev;
this.store.contextMenu.activeMenu = this.contextMenuId;
}
// Expand/fold the component node
@@ -2,15 +2,15 @@
<templates xml:space="preserve">
<t t-name="devtools.TreeElement" owl="1">
<div t-ref="element"
t-att-class="{'component-selected': props.component.selected,'component-highlighted': props.component.highlighted}"
class="tree-component m-0 p-0 w-100 text-nowrap user-select-none"
t-on-contextmenu.prevent="openMenu"
t-on-mouseover.stop="() => this.store.highlightComponent(props.component.path)"
t-att-class="{'component-selected': props.component.selected,'component-highlighted': props.component.highlighted}"
class="tree-component m-0 p-0 w-100 text-nowrap user-select-none"
t-on-contextmenu.prevent="openMenu"
t-on-mouseover.stop="() => this.store.highlightComponent(props.component.path)"
t-on-click.stop="toggleComponent"
>
<div class="component-wrapper" t-attf-style="padding-left: {{componentPadding}}rem">
<i class="fa px-1 pointer-icon caret"
t-att-class="{'fa-caret-right': !props.component.toggled, 'fa-caret-down': props.component.toggled}"
t-att-class="{'fa-caret-right': !props.component.toggled, 'fa-caret-down': props.component.toggled}"
t-on-click.stop="toggleDisplay"
t-attf-style="{{props.component.children.length > 0 ? '' : 'visibility: hidden;'}}"
/>
@@ -26,6 +26,29 @@
<span t-if="props.component.depth">&gt;</span>
<span class="version" t-else="">owl=<t t-esc="props.component.version"/></span>
</div>
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.toggleComponentAndChildren(props.component, true)" class="custom-menu-item py-1 px-4">Expand children</li>
<li t-on-click.stop="() => this.store.toggleComponentAndChildren(props.component, false)" class="custom-menu-item py-1 px-4">Fold all children</li>
<li t-on-click.stop="() => this.store.foldDirectChildren(props.component)" class="custom-menu-item py-1 px-4">Fold direct children</li>
<li t-on-click.stop="() => this.store.inspectComponent('source', props.component.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
<t t-if="props.component.path.length !== 1">
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.component.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
<li t-on-click.stop="() => this.store.refreshComponent(props.component.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.component.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.component.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
</t>
<t t-else="">
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
</t>
<li t-on-click.stop="() => this.toggleComponentToBlacklist()" class="custom-menu-item py-1 px-4">
<t t-if="store.settings.componentsToggleBlacklist.has(props.component.name)">Don't fold component by default</t>
<t t-else="">Fold component by default</t>
</li>
</ul>
</div>
</div>
<t t-if="props.component.toggled">
<t t-foreach="props.component.children" t-as="child" t-key="child.key">
@@ -1,14 +1,13 @@
const { Component } = owl;
import { ContextMenu } from "../context_menu/context_menu";
import { useStore } from "../store/store";
import { ComponentsTab } from "./components_tab/components_tab";
import { ProfilerTab } from "./profiler_tab/profiler_tab";
import { Tab } from "./tab/tab";
import { ProfilerTab } from "./profiler_tab/profiler_tab";
import { useStore } from "../store/store";
export class DevtoolsWindow extends Component {
static props = [];
static template = "devtools.DevtoolsWindow";
static components = { ComponentsTab, Tab, ProfilerTab, ContextMenu };
static components = { ComponentsTab, Tab, ProfilerTab };
setup() {
this.store = useStore();
}
@@ -28,7 +28,6 @@
Owl is not loaded on this page.
</div>
</t>
<ContextMenu t-if="store.contextMenu" items="store.contextMenu.items"/>
</div>
</t>
</templates>
@@ -1,13 +1,24 @@
import { minimizeKey } from "../../../../utils";
import { useStore } from "../../../store/store";
const { Component } = owl;
const { Component, useEffect, useRef } = owl;
export class Event extends Component {
static template = "devtools.Event";
setup() {
this.store = useStore();
this.componentContextMenu = useRef("componentContextmenu");
this.componentContextMenuId = this.store.contextMenu.id++;
this.contextMenuEvent,
useEffect(
(menuId) => {
if (menuId === this.componentContextMenuId) {
this.store.contextMenu.open(this.contextMenuEvent, this.componentContextMenu.el);
}
},
() => [this.store.contextMenu.activeMenu]
);
}
// Formatting for displaying the key of the component
@@ -43,65 +54,13 @@ export class Event extends Component {
}
}
get contextMenuItems() {
return [
{
title: "Inspect source code",
show: true,
action: () => this.store.inspectComponent("source", this.props.event.path),
},
{
title: "Store as global variable",
show: this.props.event.path.length !== 1,
action: () =>
this.store.logObjectInConsole([
...this.props.event.path,
{ type: "item", value: "component" },
]),
},
{
title: "Inspect in Elements tab",
show: this.props.event.path.length !== 1,
action: () => this.store.inspectComponent("DOM", this.props.event.path),
},
{
title: "Force rerender",
show: this.props.event.path.length !== 1,
action: () => this.store.refreshComponent(this.props.event.path),
},
{
title: "Store observed states as global variable",
show: this.props.event.path.length !== 1,
action: () =>
this.store.logObjectInConsole([
...this.props.event.path,
{ type: "item", value: "subscriptions" },
]),
},
{
title: "Inspect compiled template",
show: this.props.event.path.length !== 1,
action: () => this.store.inspectComponent("compiled template", this.props.event.path),
},
{
title: "Log raw template",
show: this.props.event.path.length !== 1,
action: () => this.store.inspectComponent("raw template", this.props.event.path),
},
{
title: "Store as global variable",
show: this.props.event.path.length === 1,
action: () => this.store.logObjectInConsole([...this.props.event.path]),
},
];
}
openMenu(ev) {
openComponentMenu(ev) {
if (this.props.event.type === "destroy") {
return;
} else {
ev.preventDefault();
this.store.openContextMenu(ev, this.contextMenuItems);
this.contextMenuEvent = ev;
this.store.contextMenu.activeMenu = this.componentContextMenuId;
}
}
}
@@ -8,11 +8,11 @@
t-att-class="{'fa-caret-right': !props.event.toggled, 'fa-caret-down': props.event.toggled}"
t-attf-style="visibility: {{props.event.origin ? '' : 'hidden'}};"
/>
<t t-esc="props.event.type"/>:
<t t-esc="props.event.type"/>:
&lt;<span style="cursor:pointer; color: var(--component-color);"
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
t-on-contextmenu="openMenu"
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
t-on-contextmenu="openComponentMenu"
t-esc="props.event.component"
/>
<t t-if="minimizedKey.length > 0">
@@ -29,10 +29,10 @@
<div class="my-0 pt-1 object-line">
<i class="fa fa-caret-right mx-1 pe-2" style="visibility: hidden;"></i>
<span>
origin:
&lt;<span style="cursor:pointer; color: var(--component-color);"
t-on-click.stop="() => this.store.selectComponent(props.event.origin.path)"
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.origin.path)"
origin:
&lt;<span style="cursor:pointer; color: var(--component-color);"
t-on-click.stop="() => this.store.selectComponent(props.event.origin.path)"
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.origin.path)"
t-esc="props.event.origin.component"
/>
<t t-if="originMinimizedKey.length > 0">
@@ -43,6 +43,22 @@
</span>
</div>
</t>
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-ref="componentContextmenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.inspectComponent('source', props.event.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
<t t-if="props.event.path.length !== 1">
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.event.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
<li t-on-click.stop="() => this.store.refreshComponent(props.event.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.event.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.event.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
</t>
<t t-else="">
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
</t>
</ul>
</div>
</div>
</t>
</templates>
@@ -1,7 +1,7 @@
import { minimizeKey } from "../../../../utils";
import { useStore } from "../../../store/store";
const { Component } = owl;
const { Component, useRef, useEffect } = owl;
export class EventNode extends Component {
static template = "devtools.EventNode";
@@ -10,89 +10,33 @@ export class EventNode extends Component {
setup() {
this.store = useStore();
this.nodeContextMenu = useRef("nodeContextMenu");
this.nodeContextMenuId = this.store.contextMenu.id++;
this.componentContextMenu = useRef("componentContextmenu");
this.componentContextMenuId = this.store.contextMenu.id++;
this.contextMenuEvent,
useEffect(
(menuId) => {
if (menuId === this.nodeContextMenuId) {
this.store.contextMenu.open(this.contextMenuEvent, this.nodeContextMenu.el);
}
if (menuId === this.componentContextMenuId) {
this.store.contextMenu.open(this.contextMenuEvent, this.componentContextMenu.el);
}
},
() => [this.store.contextMenu.activeMenu]
);
}
get eventPadding() {
return this.props.event.depth * 0.8 + 0.3;
}
get nodeContextMenuItems() {
return [
{
title: "Expand children",
show: true,
action: () => this.store.toggleEventAndChildren(this.props.event, true),
},
{
title: "Fold all children",
show: true,
action: () => this.store.toggleEventAndChildren(this.props.event, false),
},
{
title: "Fold direct children",
show: true,
action: () => this.store.foldDirectChildren(this.props.event),
},
];
}
get componentContextMenuItems() {
return [
{
title: "Inspect source code",
show: true,
action: () => this.store.inspectComponent("source", this.props.event.path),
},
{
title: "Store as global variable",
show: this.props.event.path.length !== 1,
action: () =>
this.store.logObjectInConsole([
...this.props.event.path,
{ type: "item", value: "component" },
]),
},
{
title: "Inspect in Elements tab",
show: this.props.event.path.length !== 1,
action: () => this.store.inspectComponent("DOM", this.props.event.path),
},
{
title: "Force rerender",
show: this.props.event.path.length !== 1,
action: () => this.store.refreshComponent(this.props.event.path),
},
{
title: "Store observed states as global variable",
show: this.props.event.path.length !== 1,
action: () =>
this.store.logObjectInConsole([
...this.props.event.path,
{ type: "item", value: "subscriptions" },
]),
},
{
title: "Inspect compiled template",
show: this.props.event.path.length !== 1,
action: () => this.store.inspectComponent("compiled template", this.props.event.path),
},
{
title: "Log raw template",
show: this.props.event.path.length !== 1,
action: () => this.store.inspectComponent("raw template", this.props.event.path),
},
{
title: "Store as global variable",
show: this.props.event.path.length === 1,
action: () => this.store.logObjectInConsole([...this.props.event.path]),
},
];
}
openNodeMenu(ev) {
if (this.props.event.children.length) {
ev.preventDefault();
this.store.openContextMenu(ev, this.nodeContextMenuItems);
this.contextMenuEvent = ev;
this.store.contextMenu.activeMenu = this.nodeContextMenuId;
}
}
@@ -101,7 +45,8 @@ export class EventNode extends Component {
return;
} else {
ev.preventDefault();
this.store.openContextMenu(ev, this.componentContextMenuItems);
this.contextMenuEvent = ev;
this.store.contextMenu.activeMenu = this.componentContextMenuId;
}
}
@@ -2,8 +2,8 @@
<templates xml:space="preserve">
<t t-name="devtools.EventNode" owl="1">
<div class="my-0 p-0 object-line"
t-on-click.stop="toggleDisplay"
t-on-contextmenu="openNodeMenu"
t-on-click.stop="toggleDisplay"
t-on-contextmenu="openNodeMenu"
>
<div class="text-nowrap" t-attf-style="padding-left: {{eventPadding}}rem">
<i class="fa px-1 pointer-icon caret"
@@ -11,11 +11,11 @@
t-attf-style="visibility: {{props.event.children.length > 0 ? '' : 'hidden'}};"
/>
<span>
<t t-esc="props.event.type"/>:
&lt;<span style="cursor:pointer; color: var(--component-color);"
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
t-on-contextmenu.stop="openComponentMenu"
<t t-esc="props.event.type"/>:
&lt;<span style="cursor:pointer; color: var(--component-color);"
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
t-on-contextmenu.stop="openComponentMenu"
t-esc="props.event.component"/>
<t t-if="minimizedKey.length > 0">
<span t-if="minimizedKey.length > 0" style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
@@ -28,6 +28,29 @@
</span>
</div>
</div>
<div t-if="store.contextMenu.activeMenu === nodeContextMenuId" class="custom-menu" t-ref="nodeContextMenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.toggleEventAndChildren(props.event, true)" class="custom-menu-item py-1 px-4">Expand children</li>
<li t-on-click.stop="() => this.store.toggleEventAndChildren(props.event, false)" class="custom-menu-item py-1 px-4">Fold all children</li>
<li t-on-click.stop="() => this.store.foldDirectChildren(props.event)" class="custom-menu-item py-1 px-4">Fold direct children</li>
</ul>
</div>
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-ref="componentContextmenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.inspectComponent('source', props.event.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
<t t-if="props.event.path.length !== 1">
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.event.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
<li t-on-click.stop="() => this.store.refreshComponent(props.event.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.event.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.event.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
</t>
<t t-else="">
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
</t>
</ul>
</div>
<t t-if="props.event.toggled">
<t t-foreach="props.event.children" t-as="child" t-key="child.id">
<EventNode event="child"/>
+26 -72
View File
@@ -11,7 +11,30 @@ export const store = reactive({
darkmode: false,
componentsToggleBlacklist: new Set(),
},
contextMenu: null,
contextMenu: {
id: 0,
activeMenu: -1,
// Opens the context menu corresponding with the given menu html element
open(event, menu) {
const menuWidth = menu.offsetWidth;
const menuHeight = menu.offsetHeight;
let x = event.clientX;
let y = event.clientY;
if (x + menuWidth > window.innerWidth) {
x = window.innerWidth - menuWidth;
}
if (y + menuHeight > window.innerHeight) {
y = window.innerHeight - menuHeight;
}
menu.style.left = x + "px";
// Need 25px offset because of the main navbar from the browser devtools
menu.style.top = y - 25 + "px";
},
// Close the currently displayed context menu
close() {
this.activeMenu = -1;
},
},
isFirefox: IS_FIREFOX,
frameUrls: ["top"],
activeFrame: "top",
@@ -33,11 +56,9 @@ export const store = reactive({
props: { toggled: true, children: [] },
env: { toggled: false, children: [] },
instance: { toggled: true, children: [] },
hooks: { toggled: true, children: [] },
version: "1.0",
},
selectedElement: null,
observedVariables: [],
componentSearch: {
search: "",
searchResults: [],
@@ -72,16 +93,6 @@ export const store = reactive({
evalFunctionInWindow("disableHTMLSelector", [], this.activeFrame);
},
openContextMenu(event, items) {
this.contextMenu = {
position: {
x: event.clientX,
y: event.clientY,
},
items,
};
},
// Load all data related to the components tree using the global hook loaded on the page
// Use fromOld to specify if we want to keep most of the toggled/selected data of the old tree
// when generating the new one
@@ -566,42 +577,6 @@ export const store = reactive({
}
},
async injectBreakpoint(hook, path, instanceOnly = false, condition = "1") {
path = [...path];
await evalFunctionInWindow(
"injectBreakpoint",
[hook, path, instanceOnly, condition],
this.activeFrame
);
await this.loadComponentsTree(true);
},
async removeBreakpoints() {
await evalFunctionInWindow("removeBreakpoints", [], this.activeFrame);
await this.loadComponentsTree(true);
},
async observeVariable(path) {
this.observedVariables.push({ path: [...path], visible: false });
this.observedVariables = await evalFunctionInWindow(
"getObservedVariables",
[store.observedVariables],
store.activeFrame
);
await browserInstance.storage.local.set({
observedVariables: toRaw(this.observedVariables).map((o) => o.path),
});
},
clearObservedVariable(index) {
if (index !== undefined) {
this.observedVariables.splice(index, 1);
} else {
this.observedVariables = [];
}
browserInstance.storage.local.set({ observedVariables: toRaw(this.observedVariables) });
},
// Trigger the highlight on the component in the page
highlightComponent(path) {
evalFunctionInWindow("highlightComponent", [path], this.activeFrame);
@@ -671,9 +646,8 @@ async function init() {
store.updateIFrameList();
// Global listeners to close the currently shown context menu when the user clicks or opens another
document.addEventListener("click", () => (store.contextMenu = null), { capture: true });
document.addEventListener("contextmenu", () => (store.contextMenu = null), { capture: true });
window.addEventListener("blur", () => (store.contextMenu = null), { capture: true });
document.addEventListener("click", () => store.contextMenu.close(), { capture: true });
document.addEventListener("contextmenu", () => store.contextMenu.close(), { capture: true });
// Make sure the events recorder is at its initial state in every frame
for (const frame of store.frameUrls) {
@@ -692,16 +666,6 @@ async function init() {
}
}
}, 500);
// Refresh observed variables values every 200 ms
setInterval(async () => {
if (store.owlStatus) {
store.observedVariables = await evalFunctionInWindow(
"getObservedVariables",
[[...store.observedVariables]],
store.activeFrame
);
}
}, 200);
}
let rootRendersTimeout = false;
@@ -796,16 +760,6 @@ async function loadSettings() {
storage.owlDevtoolsComponentsToggleBlacklist
);
}
// Observed variables
if (storage.observedVariables) {
store.observedVariables = [];
for (const path of storage.observedVariables) {
store.observedVariables.push({
path: [...path],
visible: false,
});
}
}
}
// Function to handle and store a batch of events coming from the page
@@ -51,8 +51,6 @@
this.requestedFrame = false;
this.enabledSelector = false;
this.eventsBatch = [];
this.breakpointsClassMap = new Map();
this.breakpointsHookMap = new Map();
// Object which defines how different types of data should be displayed when passed to the devtools
this.serializer = {
// Defines how leaf object nodes should be displayed in the extension when inside a bigger structure
@@ -60,16 +58,15 @@
// The asConstructorName parameter can be passed to change the display of objects and functions to
// be their constructor name (useful for prototype, map and set display)
serializeItem(value, asConstructorName = false) {
if (typeof value === "object") {
if (typeof value === "array") {
return "Array(" + value.length + ")";
} else if (typeof value === "object") {
if (value == null) {
return "null";
}
if (asConstructorName) {
return value.constructor.name;
}
if (value instanceof Array) {
return "Array(" + value.length + ")";
}
return "{...}";
} else if (typeof value === "undefined") {
return "undefined";
@@ -119,17 +116,12 @@
} else if (obj instanceof Number) {
result[0] = obj.toString();
} else {
for (const key of Reflect.ownKeys(obj)) {
for (const [key, value] of Object.entries(obj)) {
if (length > 25) {
result.push("...");
break;
}
let element;
if (Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")) {
element = key.toString() + ": (...)";
} else {
element = key.toString() + ": " + this.serializeItem(obj[key]);
}
const element = key + ": " + this.serializeItem(value);
length += element.length;
result.push(element);
}
@@ -143,7 +135,7 @@
result.push(element);
}
}
if (obj.constructor && obj.constructor.name !== "Object") {
if (obj.constructor.name !== "Object") {
return obj.constructor.name + " {" + result.join(", ") + "}";
}
return "{" + result.join(", ") + "}";
@@ -703,7 +695,7 @@
try {
obj = Object.getOwnPropertyDescriptor(obj, key.value).get.call(obj);
} catch (e) {
obj = "Exception: " + e.toString();
obj = e.toString();
}
break;
case "prototype getter":
@@ -721,13 +713,13 @@
try {
obj = Object.getOwnPropertyDescriptor(obj, key.value).get;
} catch (e) {
obj = "Exception: " + e.toString();
obj = e.toString();
}
} else {
try {
obj = obj[key.value];
} catch (e) {
obj = "Exception: " + e.toString();
obj = e.toString();
}
}
}
@@ -740,20 +732,15 @@
// Returns the asked property given its global path
getObjectProperty(path) {
// Just return the corresponding app if path is of length 1 and not simplified
if (path.length === 1 && !isNaN(path[0])) {
// Just return the corresponding app if path is of length 1
if (path.length === 1) {
return [...this.apps][path[0]];
}
// Path to the component node is only strings, becomes objects for properties
const index = path.findIndex((key) => typeof key !== "string");
if (index === -1) {
return this.getComponentNode(path);
}
const componentNode = this.getComponentNode(index === 1 ? [path[0]] : path.slice(0, index));
if (componentNode) {
return this.getObject(componentNode, path.slice(index));
}
return "Exception: component not found";
const componentNode = this.getComponentNode(path.slice(0, index));
const obj = this.getObject(componentNode, path.slice(index));
return obj;
}
// Returns a modified version of an object node that has compatible format with the devtools ObjectTreeElement component
@@ -801,11 +788,6 @@
child.name = "value";
obj = parentObj[1];
break;
case "getter":
child.name = key.value;
obj = parentObj[key.value];
break;
case "prototype getter":
case "set entry":
case "map entry":
case "item":
@@ -821,8 +803,6 @@
parentObj
).findIndex((sym) => sym === key.value);
child.path[child.path.length - 1].value = key.value.toString();
} else if (key.hasOwnProperty("symbolIndex")) {
obj = parentObj[Object.getOwnPropertySymbols(parentObj)[key.symbolIndex]];
} else {
obj = parentObj[key.value];
}
@@ -854,7 +834,7 @@
child.contentType = "set";
child.hasChildren = true;
break;
case obj.constructor?.name === "Array":
case obj.constructor.name === "Array":
child.contentType = "array";
child.hasChildren = obj.length > 0;
break;
@@ -862,11 +842,11 @@
child.contentType = "function";
child.hasChildren = true;
break;
case typeof obj === "object":
case obj instanceof Object:
child.contentType = "object";
child.hasChildren =
Object.keys(obj).length > 0 ||
Object.getOwnPropertySymbols(obj).length > 0 ||
Object.keys(obj).length ||
Object.getOwnPropertySymbols(obj).length ||
obj.constructor.name !== "Object";
break;
default:
@@ -910,15 +890,12 @@
path.shift();
}
// the value is either "props" or "env" here
if (objType !== "instance" && objType !== "hook") {
if (objType !== "instance") {
obj = oldTree[path[0].value].children;
path.shift();
// there is nothing otherwise but extension side it is in instance
} else if (objType === "instance") {
obj = oldTree.instance.children;
} else {
obj = oldTree.hooks.children;
path.shift();
obj = oldTree.instance.children;
}
// the first element here is directly in an array instead of a children array
obj = obj[path[0].childIndex];
@@ -1049,40 +1026,36 @@
break;
case "object":
case "function":
Reflect.ownKeys(obj)
.sort(compareKeys)
.forEach((key) => {
if (
key !== "__proto__" &&
Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
) {
let child = {
name: key,
depth: depth,
toggled: false,
objectType: objType,
path: [...path, { type: "getter", value: key, childIndex: children.length }],
contentType: "getter",
content: "(...)",
hasChildren: false,
children: [],
};
children.push(child);
}
const child = this.serializeObjectChild(
obj,
{ type: "item", value: key, childIndex: children.length },
depth,
objType,
path,
oldBranch?.children[index],
oldTree
);
if (child) {
children.push(child);
}
index++;
});
Reflect.ownKeys(obj).forEach((key) => {
if (
key !== "__proto__" &&
Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
) {
let child = {
name: key,
depth: depth,
toggled: false,
objectType: objType,
path: [...path, { type: "getter", value: key, childIndex: children.length }],
contentType: "getter",
content: "(...)",
hasChildren: false,
children: [],
};
children.push(child);
}
const child = this.serializeObjectChild(
obj,
{ type: "item", value: key, childIndex: children.length },
depth,
objType,
path,
oldBranch?.children[index],
oldTree
);
if (child) children.push(child);
index++;
});
}
let proto = Object.getPrototypeOf(obj);
while (proto) {
@@ -1126,44 +1099,25 @@
}
// Returns the Component node given its path and the root component node
getComponentNode(path) {
// All paths that consists in an array containing a single stringified number lead to an app
if (path.length === 1 && !isNaN(path[0])) {
// The node is an app and not a component
if (path.length === 1) {
return [...this.apps][path[0]];
}
let node;
// If the path is longer and its first item is indeed an app number, it is a regular path
if (!isNaN(path[0])) {
// The second element in the path will always be the root of the app
node = [...this.apps][path[0]]?.root;
if (!node) {
// The second element in the path will always be the root of the app
let node = [...this.apps][path[0]]?.root;
if (!node) {
return null;
}
for (let i = 2; i < path.length; i++) {
// From this point onwards, it is an object path inside the component node
if (typeof path[i] !== "string") {
break;
}
if (node.children.hasOwnProperty(path[i])) {
node = node.children[path[i]];
} else {
return null;
}
for (let i = 2; i < path.length; i++) {
// From this point onwards, it is an object path inside the component node
if (typeof path[i] !== "string") {
break;
}
if (node.children.hasOwnProperty(path[i])) {
node = node.children[path[i]];
} else {
return null;
}
}
// If the first path item is a more complex string, it is a simplified path where elements
// are a series of component names and indexes separated by slashes
} else {
const simplifiedPathArray = path[0].split("/");
node = [...this.apps][simplifiedPathArray[0]]?.root;
if (node.name !== simplifiedPathArray[1]) {
return null;
}
for (let i = 2; i < simplifiedPathArray.length; i += 2) {
const key = Reflect.ownKeys(node.children)[simplifiedPathArray[i]];
node = node.children[key];
if (node.name !== simplifiedPathArray[i + 1]) {
return null;
}
}
}
return node;
}
@@ -1201,46 +1155,42 @@
const propsPath = isApp
? [...path, { type: "item", value: "props" }]
: [...path, { type: "item", value: "component" }, { type: "item", value: "props" }];
Reflect.ownKeys(props)
.sort(compareKeys)
.forEach((key) => {
let oldBranch = oldTree?.props.children[component.props.children.length];
const property = this.serializeObjectChild(
props,
{ type: "item", value: key, childIndex: component.props.children.length },
0,
"props",
propsPath,
oldBranch,
oldTree
);
if (property) {
component.props.children.push(property);
}
});
Reflect.ownKeys(props).forEach((key) => {
let oldBranch = oldTree?.props.children[component.props.children.length];
const property = this.serializeObjectChild(
props,
{ type: "item", value: key, childIndex: component.props.children.length },
0,
"props",
propsPath,
oldBranch,
oldTree
);
if (property) {
component.props.children.push(property);
}
});
// Load env of the component
const env = isApp ? node.env : node.component.env;
component.env = { toggled: oldTree ? oldTree.env.toggled : false, children: [] };
const envPath = isApp
? [...path, { type: "item", value: "env" }]
: [...path, { type: "item", value: "component" }, { type: "item", value: "env" }];
Reflect.ownKeys(env)
.sort(compareKeys)
.forEach((key) => {
let oldBranch = oldTree?.env.children[component.env.children.length];
const envElement = this.serializeObjectChild(
env,
{ type: "item", value: key, childIndex: component.env.children.length },
0,
"env",
envPath,
oldBranch,
oldTree
);
if (envElement) {
component.env.children.push(envElement);
}
});
Reflect.ownKeys(env).forEach((key) => {
let oldBranch = oldTree?.env.children[component.env.children.length];
const envElement = this.serializeObjectChild(
env,
{ type: "item", value: key, childIndex: component.env.children.length },
0,
"env",
envPath,
oldBranch,
oldTree
);
if (envElement) {
component.env.children.push(envElement);
}
});
// Load env getters
let obj = Object.getPrototypeOf(env);
Reflect.ownKeys(obj).forEach((key) => {
@@ -1279,25 +1229,23 @@
const instance = isApp ? node : node.component;
component.instance = { toggled: oldTree ? oldTree.instance.toggled : true, children: [] };
const instancePath = isApp ? path : [...path, { type: "item", value: "component" }];
Reflect.ownKeys(instance)
.sort(compareKeys)
.forEach((key) => {
if (!["env", "props"].includes(key)) {
let oldBranch = oldTree?.instance.children[component.instance.children.length];
const instanceElement = this.serializeObjectChild(
instance,
{ type: "item", value: key, childIndex: component.instance.children.length },
0,
"instance",
instancePath,
oldBranch,
oldTree
);
if (instanceElement) {
component.instance.children.push(instanceElement);
}
Reflect.ownKeys(instance).forEach((key) => {
if (!["env", "props"].includes(key)) {
let oldBranch = oldTree?.instance.children[component.instance.children.length];
const instanceElement = this.serializeObjectChild(
instance,
{ type: "item", value: key, childIndex: component.instance.children.length },
0,
"instance",
instancePath,
oldBranch,
oldTree
);
if (instanceElement) {
component.instance.children.push(instanceElement);
}
});
}
});
// Load instance getters
obj = Object.getPrototypeOf(instance);
while (obj) {
@@ -1413,39 +1361,6 @@
component.subscriptions.children.push(subscription);
});
}
// Load hooks of the component
if (!isApp) {
component.hooks = { toggled: oldTree ? oldTree.hooks.toggled : true, children: [] };
const hooksList = [
"mounted",
"patched",
"willDestroy",
"willPatch",
"willStart",
"willUnmount",
"willUpdateProps",
];
const hooksPath = [...instancePath, { type: "item", value: "__owl__" }];
Reflect.ownKeys(instance.__owl__)
.sort(compareKeys)
.forEach((key) => {
if (hooksList.includes(key)) {
let oldBranch = oldTree?.hooks.children[component.hooks.children.length];
const property = this.serializeObjectChild(
instance.__owl__,
{ type: "item", value: key, childIndex: component.hooks.children.length },
0,
"hook",
hooksPath,
oldBranch,
oldTree
);
if (property) {
component.hooks.children.push(property);
}
}
});
}
return component;
}
// Replace the content of a parsed getter object with the result of the corresponding get method
@@ -1720,37 +1635,6 @@
}
return children;
}
getObservedVariables(current) {
const res = [...current];
for (let i = 0; i < current.length; i++) {
const path = current[i].path;
const parent = this.getObjectProperty(path.slice(0, path.length - 1));
if (parent && !(typeof parent === "string" && parent.startsWith("Exception: "))) {
const result = this.serializeObjectChild(
parent,
path.at(-1),
0,
"observed",
path.slice(0, path.length - 1),
{},
{}
);
result.hasChildren = false;
result.visible = true;
const index = path.findIndex((key) => typeof key !== "string");
if (index > 1) {
const componentNode = this.getComponentNode(path.slice(0, index));
result.path = [this.getComponentSimplifiedPath(componentNode)].concat(
path.slice(index)
);
}
res[i] = result;
} else {
res[i].visible = false;
}
}
return res;
}
// Returns the path of the given component node
getComponentPath(componentNode) {
let path = [];
@@ -1767,23 +1651,6 @@
path.unshift(index.toString());
return path;
}
// Returns the simplified path of the given component node (using component names and indexes)
getComponentSimplifiedPath(componentNode) {
let path = componentNode.name;
if (componentNode.parentKey) {
while (componentNode.parent) {
const previousKey = componentNode.parentKey;
componentNode = componentNode.parent;
path = `${componentNode.name}/${Reflect.ownKeys(componentNode.children).indexOf(
previousKey
)}/${path}`;
}
}
const appsArray = [...this.apps];
let index = appsArray.findIndex((app) => app === componentNode.app);
path = index.toString() + (path.length ? `/${path}` : "");
return path;
}
// Store the object into a temp window variable and log it to the console
sendObjectToConsole(path) {
const obj = this.getObjectProperty(path);
@@ -1831,48 +1698,6 @@
}
}
injectBreakpoint(hook, path, instanceOnly, condition) {
const componentNode = this.getObjectProperty(path);
const injectFunctionInHook = (comp, hook, fn) => {
comp[hook].push(fn);
};
const originalHook = [...componentNode.component.__owl__[hook]];
injectFunctionInHook(componentNode.component.__owl__, hook, () => {
debugger;
});
if (!this.breakpointsHookMap.get([componentNode.component.__owl__, hook])) {
this.breakpointsHookMap.set([componentNode.component.__owl__, hook], originalHook);
}
if (!instanceOnly) {
const componentClass = componentNode.component.constructor;
const originalSetup = componentClass.prototype.setup;
if (!this.breakpointsClassMap.get(componentClass)) {
this.breakpointsClassMap.set(componentClass, originalSetup);
}
componentClass.prototype.setup = function () {
const debuggerFunc = () => {
if (eval(condition)) {
this;
debugger;
}
};
injectFunctionInHook(this.__owl__, hook, debuggerFunc);
originalSetup.call(this, ...arguments);
};
}
}
removeBreakpoints() {
for (const [component, setup] of this.breakpointsClassMap) {
component.prototype.setup = setup;
}
this.breakpointsClassMap.clear();
for (const [ref, originalHook] of this.breakpointsHookMap) {
ref[0][ref[1]] = originalHook;
}
this.breakpointsHookMap.clear();
}
targetName(target, node) {
// check on component
const { component } = node;
@@ -1922,19 +1747,6 @@
}
}
function compareKeys(a, b) {
const isSymbolA = typeof a === "symbol";
const isSymbolB = typeof b === "symbol";
if (isSymbolA && !isSymbolB) {
return 1; // Place Symbols at the end
} else if (!isSymbolA && isSymbolB) {
return -1; // Place non-Symbols at the beginning
} else {
return String(a).localeCompare(String(b), undefined, { numeric: true }); // Sort other keys alphabetically
}
}
function checkOwlStatus() {
let owlStatus = 2;
if (!window.__OWL__DEVTOOLS_GLOBAL_HOOK__) {
+1
View File
@@ -140,6 +140,7 @@ async function startRelease() {
}
// ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: Creating the release...`);
const relaseResult = await execCommand(`gh release create v${next} dist/*.js dist/*.zip ${draft} -F ${file}`);
if (relaseResult !== 0) {