mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6768501cd | |||
| 6b2486473f | |||
| 7e687234bf | |||
| 968e96ad08 | |||
| 26c7856d5d | |||
| b8d09e523d | |||
| 04c2808701 | |||
| 15c2604df1 | |||
| 3e11fe6b12 | |||
| 20c6cacb4e | |||
| eb2b32ab60 | |||
| 2a223288d4 | |||
| 1272278225 | |||
| f502dd732e | |||
| 9c2d957525 | |||
| f8bb86820e | |||
| 0cde4b8737 | |||
| 66a801393f | |||
| e7f405cc97 |
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
node-version: [20.x, 22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- [API](#api)
|
||||
- [Configuration](#configuration)
|
||||
- [`mount` helper](#mount-helper)
|
||||
- [Roots](#roots)
|
||||
- [Loading templates](#loading-templates)
|
||||
|
||||
## Overview
|
||||
@@ -65,6 +66,9 @@ 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
|
||||
|
||||
@@ -92,6 +96,33 @@ Most of the time, the `mount` helper is more convenient, but whenever one needs
|
||||
a reference to the actual Owl App, then using the `App` class directly is
|
||||
possible.
|
||||
|
||||
## Roots
|
||||
|
||||
An application can have multiple roots. It is sometimes useful to instantiate
|
||||
sub components in places that are not managed by Owl, such as an html editor
|
||||
with dynamic content (the Knowledge application in Odoo).
|
||||
|
||||
To create a root, one can use the `createRoot` method, which takes two arguments:
|
||||
|
||||
- **`Component`**: a component class (Root component of the app)
|
||||
- **`config (optional)`**: a config object that may contain a `props` object or a
|
||||
`env` object.
|
||||
|
||||
The `createRoot` method returns an object with a `mount` method (same API as
|
||||
the `App.mount` method), and a `destroy` method.
|
||||
|
||||
```js
|
||||
const root = app.createRoot(MyComponent, { props: { someProps: true } });
|
||||
await root.mount(targetElement);
|
||||
|
||||
// later
|
||||
root.destroy();
|
||||
```
|
||||
|
||||
Note that, like with owl `App`, it is the responsibility of the code that created
|
||||
the root to properly destroy it (before it has been removed from the DOM!). Owl
|
||||
has no way of doing it itself.
|
||||
|
||||
## Loading templates
|
||||
|
||||
Most applications will need to load templates whenever they start. Here is
|
||||
|
||||
@@ -140,6 +140,28 @@ class SomeComponent extends Component {
|
||||
The `.bind` suffix also implies `.alike`, so these props will not cause additional
|
||||
renderings.
|
||||
|
||||
## Translatable props
|
||||
|
||||
When you need to pass a user-facing string to a subcomponent, you likely want it
|
||||
to be translated. Unfortunately, because props are arbitrary expressions, it wouldn't
|
||||
be practical for Owl to find out which parts of the expression are strings and translate
|
||||
them, and it also makes it difficult for tooling to extract these strings to generate
|
||||
terms to translate. While you can work around this issue by doing the translation in
|
||||
JavaScript, or by using `t-set` with a body (the body of `t-set` is translated),
|
||||
and passing the variable as a prop, this is a sufficiently common use case that Owl
|
||||
provides a suffix for this purpose: `.translate`.
|
||||
|
||||
```xml
|
||||
<t t-name="ParentComponent">
|
||||
<Child someProp.translate="some message"/>
|
||||
</t>
|
||||
```
|
||||
|
||||
Note that the content of this attribute is _NOT_ treated as a JavaScript expression:
|
||||
it is treated as a string, as if it was an attribute on an HTML element, and translated
|
||||
before being passed to the component. If you need to interpolate some data into the
|
||||
string, you will still have to do this in JavaScript.
|
||||
|
||||
## Dynamic Props
|
||||
|
||||
The `t-props` directive can be used to specify totally dynamic props:
|
||||
|
||||
@@ -201,16 +201,17 @@ use this `Notebook` component:
|
||||
|
||||
```xml
|
||||
<Notebook>
|
||||
<t t-set-slot="page1" title="'Page 1'">
|
||||
<t t-set-slot="page1" title.translate="Page 1">
|
||||
<div>this is in the page 1</div>
|
||||
</t>
|
||||
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
|
||||
<t t-set-slot="page2" title.translate="Page 2" hidden="somevalue">
|
||||
<div>this is in the page 2</div>
|
||||
</t>
|
||||
</Notebook>
|
||||
```
|
||||
|
||||
Slot params works like normal props, so one can use the `.bind` suffix to
|
||||
Slot params works like normal props, so one can use suffixes like `.translate`
|
||||
when a prop is a user facing string and should be translated, or `.bind` to
|
||||
bind a function if needed.
|
||||
|
||||
## Slot scopes
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
- [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)
|
||||
@@ -80,6 +81,7 @@ 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
|
||||
|
||||
@@ -588,6 +590,35 @@ 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
|
||||
|
||||
+183
-68
@@ -2270,6 +2270,12 @@ 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')");
|
||||
@@ -2598,42 +2604,47 @@ class ComponentNode {
|
||||
}
|
||||
|
||||
const TIMEOUT = Symbol("timeout");
|
||||
const HOOK_TIMEOUT = {
|
||||
onWillStart: 3000,
|
||||
onWillUpdateProps: 3000,
|
||||
};
|
||||
function wrapError(fn, hookName) {
|
||||
const error = new OwlError(`The following error occurred in ${hookName}: `);
|
||||
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
|
||||
const error = new OwlError();
|
||||
const timeoutError = new OwlError();
|
||||
const node = getCurrent();
|
||||
return (...args) => {
|
||||
const onError = (cause) => {
|
||||
error.cause = cause;
|
||||
if (cause instanceof Error) {
|
||||
error.message += `"${cause.message}"`;
|
||||
}
|
||||
else {
|
||||
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||
}
|
||||
error.message =
|
||||
cause instanceof Error
|
||||
? `The following error occurred in ${hookName}: "${cause.message}"`
|
||||
: `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||
throw error;
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
const result = fn(...args);
|
||||
if (result instanceof Promise) {
|
||||
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
|
||||
const fiber = node.fiber;
|
||||
Promise.race([
|
||||
result.catch(() => { }),
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber) {
|
||||
console.warn(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.catch(onError);
|
||||
}
|
||||
return result;
|
||||
result = fn(...args);
|
||||
}
|
||||
catch (cause) {
|
||||
onError(cause);
|
||||
}
|
||||
if (!(result instanceof Promise)) {
|
||||
return result;
|
||||
}
|
||||
const timeout = HOOK_TIMEOUT[hookName];
|
||||
if (timeout) {
|
||||
const fiber = node.fiber;
|
||||
Promise.race([
|
||||
result.catch(() => { }),
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||
timeoutError.message = `${hookName}'s promise hasn't resolved after ${timeout / 1000} seconds`;
|
||||
console.log(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.catch(onError);
|
||||
};
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -3216,6 +3227,9 @@ 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;
|
||||
@@ -3272,7 +3286,7 @@ class TemplateSet {
|
||||
this.templates[name] = function (context, parent) {
|
||||
return templates[name].call(this, context, parent);
|
||||
};
|
||||
const template = templateFn(this, bdom, helpers);
|
||||
const template = templateFn(this, bdom, this.runtimeUtils);
|
||||
this.templates[name] = template;
|
||||
}
|
||||
return this.templates[name];
|
||||
@@ -3323,7 +3337,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".split(",");
|
||||
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 WORD_REPLACEMENT = Object.assign(Object.create(null), {
|
||||
and: "&&",
|
||||
or: "||",
|
||||
@@ -3512,7 +3526,7 @@ function compileExprToArray(expr) {
|
||||
const localVars = new Set();
|
||||
const tokens = tokenize(expr);
|
||||
let i = 0;
|
||||
let stack = []; // to track last opening [ or {
|
||||
let stack = []; // to track last opening (, [ or {
|
||||
while (i < tokens.length) {
|
||||
let token = tokens[i];
|
||||
let prevToken = tokens[i - 1];
|
||||
@@ -3521,10 +3535,12 @@ function compileExprToArray(expr) {
|
||||
switch (token.type) {
|
||||
case "LEFT_BRACE":
|
||||
case "LEFT_BRACKET":
|
||||
case "LEFT_PAREN":
|
||||
stack.push(token.type);
|
||||
break;
|
||||
case "RIGHT_BRACE":
|
||||
case "RIGHT_BRACKET":
|
||||
case "RIGHT_PAREN":
|
||||
stack.pop();
|
||||
}
|
||||
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
|
||||
@@ -3636,6 +3652,13 @@ function isProp(tag, key) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Returns a template literal that evaluates to str. You can add interpolation
|
||||
* sigils into the string if required
|
||||
*/
|
||||
function toStringExpression(str) {
|
||||
return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// BlockDescription
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -3786,6 +3809,9 @@ 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;
|
||||
@@ -3816,15 +3842,14 @@ class CodeGenerator {
|
||||
mainCode.push(``);
|
||||
for (let block of this.blocks) {
|
||||
if (block.dom) {
|
||||
let xmlString = block.asXmlString();
|
||||
xmlString = xmlString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
||||
let xmlString = toStringExpression(block.asXmlString());
|
||||
if (block.dynamicTagName) {
|
||||
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
|
||||
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
|
||||
mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
|
||||
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
||||
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
||||
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
||||
}
|
||||
else {
|
||||
mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
|
||||
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4002,7 +4027,7 @@ class CodeGenerator {
|
||||
const isNewBlock = !block || forceNewBlock;
|
||||
if (isNewBlock) {
|
||||
block = this.createBlock(block, "comment", ctx);
|
||||
this.insertBlock(`comment(\`${ast.value}\`)`, block, {
|
||||
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
@@ -4024,7 +4049,7 @@ class CodeGenerator {
|
||||
}
|
||||
if (!block || forceNewBlock) {
|
||||
block = this.createBlock(block, "text", ctx);
|
||||
this.insertBlock(`text(\`${value}\`)`, block, {
|
||||
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
@@ -4245,7 +4270,8 @@ class CodeGenerator {
|
||||
expr = compileExpr(ast.expr);
|
||||
if (ast.defaultValue) {
|
||||
this.helpers.add("withDefault");
|
||||
expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
// FIXME: defaultValue is not translated
|
||||
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
||||
}
|
||||
}
|
||||
if (!block || forceNewBlock) {
|
||||
@@ -4498,7 +4524,7 @@ class CodeGenerator {
|
||||
this.addLine(`${ctxVar}[zero] = ${bl};`);
|
||||
}
|
||||
}
|
||||
const key = `key + \`${this.generateComponentKey()}\``;
|
||||
const key = this.generateComponentKey();
|
||||
if (isDynamic) {
|
||||
const templateVar = generateId("template");
|
||||
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||||
@@ -4550,12 +4576,12 @@ class CodeGenerator {
|
||||
else {
|
||||
let value;
|
||||
if (ast.defaultValue) {
|
||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
||||
const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue);
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
||||
value = `withDefault(${expr}, ${defaultValue})`;
|
||||
}
|
||||
else {
|
||||
value = `\`${defaultValue}\``;
|
||||
value = defaultValue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -4566,12 +4592,12 @@ class CodeGenerator {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
generateComponentKey() {
|
||||
generateComponentKey(currentKey = "key") {
|
||||
const parts = [generateId("__")];
|
||||
for (let i = 0; i < this.target.loopLevel; i++) {
|
||||
parts.push(`\${key${i + 1}}`);
|
||||
}
|
||||
return parts.join("__");
|
||||
return `${currentKey} + \`${parts.join("__")}\``;
|
||||
}
|
||||
/**
|
||||
* Formats a prop name and value into a string suitable to be inserted in the
|
||||
@@ -4585,7 +4611,12 @@ class CodeGenerator {
|
||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||
*/
|
||||
formatProp(name, value) {
|
||||
value = this.captureExpression(value);
|
||||
if (name.endsWith(".translate")) {
|
||||
value = toStringExpression(this.translateFn(value));
|
||||
}
|
||||
else {
|
||||
value = this.captureExpression(value);
|
||||
}
|
||||
if (name.includes(".")) {
|
||||
let [_name, suffix] = name.split(".");
|
||||
name = _name;
|
||||
@@ -4594,6 +4625,7 @@ class CodeGenerator {
|
||||
value = `(${value}).bind(this)`;
|
||||
break;
|
||||
case "alike":
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
@@ -4662,7 +4694,6 @@ class CodeGenerator {
|
||||
this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
|
||||
}
|
||||
// cmap key
|
||||
const key = this.generateComponentKey();
|
||||
let expr;
|
||||
if (ast.isDynamic) {
|
||||
expr = generateId("Comp");
|
||||
@@ -4678,7 +4709,7 @@ class CodeGenerator {
|
||||
// todo: check the forcenewblock condition
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
let keyArg = `key + \`${key}\``;
|
||||
let keyArg = this.generateComponentKey();
|
||||
if (ctx.tKeyExpr) {
|
||||
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||||
}
|
||||
@@ -4751,7 +4782,7 @@ class CodeGenerator {
|
||||
}
|
||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||
if (isMultiple) {
|
||||
key = `${key} + \`${this.generateComponentKey()}\``;
|
||||
key = this.generateComponentKey(key);
|
||||
}
|
||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
||||
const scope = this.getPropString(props, dynProps);
|
||||
@@ -4792,7 +4823,6 @@ class CodeGenerator {
|
||||
}
|
||||
let { block } = ctx;
|
||||
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
||||
const key = this.generateComponentKey();
|
||||
let ctxStr = "ctx";
|
||||
if (this.target.loopLevel || !this.hasSafeContext) {
|
||||
ctxStr = generateId("ctx");
|
||||
@@ -4805,7 +4835,8 @@ class CodeGenerator {
|
||||
expr: `app.createComponent(null, false, true, false, false)`,
|
||||
});
|
||||
const target = compileExpr(ast.target);
|
||||
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
|
||||
const key = this.generateComponentKey();
|
||||
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;
|
||||
if (block) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
@@ -4819,29 +4850,33 @@ class CodeGenerator {
|
||||
// Parser
|
||||
// -----------------------------------------------------------------------------
|
||||
const cache = new WeakMap();
|
||||
function parse(xml) {
|
||||
function parse(xml, customDir) {
|
||||
const ctx = {
|
||||
inPreTag: false,
|
||||
customDirectives: customDir,
|
||||
};
|
||||
if (typeof xml === "string") {
|
||||
const elem = parseXML(`<t>${xml}</t>`).firstChild;
|
||||
return _parse(elem);
|
||||
return _parse(elem, ctx);
|
||||
}
|
||||
let ast = cache.get(xml);
|
||||
if (!ast) {
|
||||
// we clone here the xml to prevent modifying it in place
|
||||
ast = _parse(xml.cloneNode(true));
|
||||
ast = _parse(xml.cloneNode(true), ctx);
|
||||
cache.set(xml, ast);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
function _parse(xml) {
|
||||
function _parse(xml, ctx) {
|
||||
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 (parseTDebugLog(node, ctx) ||
|
||||
return (parseTCustom(node, ctx) ||
|
||||
parseTDebugLog(node, ctx) ||
|
||||
parseTForEach(node, ctx) ||
|
||||
parseTIf(node, ctx) ||
|
||||
parseTPortal(node, ctx) ||
|
||||
@@ -4883,6 +4918,35 @@ 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 modifier = attr.split(".").length > 1 ? attr.split(".")[1] : undefined;
|
||||
node.removeAttribute(attr);
|
||||
try {
|
||||
customDirective(node, value, modifier);
|
||||
}
|
||||
catch (error) {
|
||||
throw new OwlError(`Custom directive "${directiveName}" throw the following error: ${error}`);
|
||||
}
|
||||
return parseNode(node, ctx);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// debugging
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -5514,9 +5578,11 @@ function normalizeXML(el) {
|
||||
normalizeTEscTOut(el);
|
||||
}
|
||||
|
||||
function compile(template, options = {}) {
|
||||
function compile(template, options = {
|
||||
hasGlobalValues: false,
|
||||
}) {
|
||||
// parsing
|
||||
const ast = parse(template);
|
||||
const ast = parse(template, options.customDirectives);
|
||||
// some work
|
||||
const hasSafeContext = template instanceof Node
|
||||
? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
|
||||
@@ -5538,7 +5604,7 @@ function compile(template, options = {}) {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.2.10";
|
||||
const version = "2.5.0";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -5549,6 +5615,7 @@ class Scheduler {
|
||||
this.frame = 0;
|
||||
this.delayedRenders = [];
|
||||
this.cancelledNodes = new Set();
|
||||
this.processing = false;
|
||||
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||||
}
|
||||
addFiber(fiber) {
|
||||
@@ -5579,6 +5646,10 @@ class Scheduler {
|
||||
}
|
||||
}
|
||||
processTasks() {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
this.processing = true;
|
||||
this.frame = 0;
|
||||
for (let node of this.cancelledNodes) {
|
||||
node._destroy();
|
||||
@@ -5592,6 +5663,7 @@ class Scheduler {
|
||||
this.tasks.delete(task);
|
||||
}
|
||||
}
|
||||
this.processing = false;
|
||||
}
|
||||
processFiber(fiber) {
|
||||
if (fiber.root !== fiber) {
|
||||
@@ -5611,7 +5683,14 @@ class Scheduler {
|
||||
if (!hasError) {
|
||||
fiber.complete();
|
||||
}
|
||||
this.tasks.delete(fiber);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5633,6 +5712,7 @@ class App extends TemplateSet {
|
||||
constructor(Root, config = {}) {
|
||||
super(config);
|
||||
this.scheduler = new Scheduler();
|
||||
this.subRoots = new Set();
|
||||
this.root = null;
|
||||
this.name = config.name || "";
|
||||
this.Root = Root;
|
||||
@@ -5651,14 +5731,44 @@ class App extends TemplateSet {
|
||||
this.props = config.props || {};
|
||||
}
|
||||
mount(target, options) {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
||||
const root = this.createRoot(this.Root, { props: this.props });
|
||||
this.root = root.node;
|
||||
this.subRoots.delete(root.node);
|
||||
return root.mount(target, options);
|
||||
}
|
||||
createRoot(Root, config = {}) {
|
||||
const props = config.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;
|
||||
}
|
||||
const node = this.makeNode(this.Root, this.props);
|
||||
const prom = this.mountNode(node, target, options);
|
||||
this.root = node;
|
||||
return prom;
|
||||
const restore = saveCurrent();
|
||||
const node = this.makeNode(Root, props);
|
||||
restore();
|
||||
if (config.env) {
|
||||
this.env = env;
|
||||
}
|
||||
this.subRoots.add(node);
|
||||
return {
|
||||
node,
|
||||
mount: (target, options) => {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(Root, props, { __owl__: { app: this } });
|
||||
}
|
||||
const prom = this.mountNode(node, target, options);
|
||||
return prom;
|
||||
},
|
||||
destroy: () => {
|
||||
this.subRoots.delete(node);
|
||||
node.destroy();
|
||||
this.scheduler.processTasks();
|
||||
},
|
||||
};
|
||||
}
|
||||
makeNode(Component, props) {
|
||||
return new ComponentNode(Component, props, this, null, null);
|
||||
@@ -5690,6 +5800,9 @@ class App extends TemplateSet {
|
||||
}
|
||||
destroy() {
|
||||
if (this.root) {
|
||||
for (let subroot of this.subRoots) {
|
||||
subroot.destroy();
|
||||
}
|
||||
this.root.destroy();
|
||||
this.scheduler.processTasks();
|
||||
}
|
||||
@@ -5961,12 +6074,14 @@ 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__, 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 };
|
||||
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 = '2024-04-02T10:25:32.577Z';
|
||||
__info__.hash = '97b69f1';
|
||||
__info__.date = '2024-11-25T09:30:45.930Z';
|
||||
__info__.hash = '6b24864';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.10",
|
||||
"version": "2.5.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.10",
|
||||
"version": "2.5.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
@@ -9,7 +9,7 @@
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.18.3"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build:bundle": "rollup -c --failAfterWarnings",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export type customDirectives = Record<
|
||||
string,
|
||||
(node: Element, value: string, modifier?: string) => void
|
||||
>;
|
||||
@@ -43,6 +43,7 @@ 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
|
||||
@@ -286,6 +287,9 @@ export class CodeGenerator {
|
||||
this.dev = options.dev || false;
|
||||
this.ast = ast;
|
||||
this.templateName = options.name;
|
||||
if (options.hasGlobalValues) {
|
||||
this.helpers.add("__globals__");
|
||||
}
|
||||
}
|
||||
|
||||
generateCode(): string {
|
||||
@@ -1136,7 +1140,11 @@ export class CodeGenerator {
|
||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||
*/
|
||||
formatProp(name: string, value: string): string {
|
||||
value = this.captureExpression(value);
|
||||
if (name.endsWith(".translate")) {
|
||||
value = toStringExpression(this.translateFn(value));
|
||||
} else {
|
||||
value = this.captureExpression(value);
|
||||
}
|
||||
if (name.includes(".")) {
|
||||
let [_name, suffix] = name.split(".");
|
||||
name = _name;
|
||||
@@ -1145,6 +1153,7 @@ export class CodeGenerator {
|
||||
value = `(${value}).bind(this)`;
|
||||
break;
|
||||
case "alike":
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -10,13 +11,17 @@ 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 = {}
|
||||
options: CompileOptions = {
|
||||
hasGlobalValues: false,
|
||||
}
|
||||
): TemplateFunction {
|
||||
// parsing
|
||||
const ast = parse(template);
|
||||
const ast = parse(template, options.customDirectives);
|
||||
|
||||
// some work
|
||||
const hasSafeContext =
|
||||
|
||||
@@ -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".split(
|
||||
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(
|
||||
","
|
||||
);
|
||||
|
||||
@@ -268,7 +268,7 @@ export function compileExprToArray(expr: string): Token[] {
|
||||
const localVars = new Set<string>();
|
||||
const tokens = tokenize(expr);
|
||||
let i = 0;
|
||||
let stack = []; // to track last opening [ or {
|
||||
let stack = []; // to track last opening (, [ or {
|
||||
|
||||
while (i < tokens.length) {
|
||||
let token = tokens[i];
|
||||
@@ -279,10 +279,12 @@ export function compileExprToArray(expr: string): Token[] {
|
||||
switch (token.type) {
|
||||
case "LEFT_BRACE":
|
||||
case "LEFT_BRACKET":
|
||||
case "LEFT_PAREN":
|
||||
stack.push(token.type);
|
||||
break;
|
||||
case "RIGHT_BRACE":
|
||||
case "RIGHT_BRACKET":
|
||||
case "RIGHT_PAREN":
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
|
||||
+42
-5
@@ -1,4 +1,5 @@
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import type { customDirectives } from "../common/types";
|
||||
import { parseXML } from "../common/utils";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -198,23 +199,26 @@ export type AST =
|
||||
// -----------------------------------------------------------------------------
|
||||
const cache: WeakMap<Element, AST> = new WeakMap();
|
||||
|
||||
export function parse(xml: string | Element): AST {
|
||||
export function parse(xml: string | Element, customDir?: customDirectives): AST {
|
||||
const ctx = {
|
||||
inPreTag: false,
|
||||
customDirectives: customDir,
|
||||
};
|
||||
if (typeof xml === "string") {
|
||||
const elem = parseXML(`<t>${xml}</t>`).firstChild as Element;
|
||||
return _parse(elem);
|
||||
return _parse(elem, ctx);
|
||||
}
|
||||
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);
|
||||
ast = _parse(xml.cloneNode(true) as Element, ctx);
|
||||
cache.set(xml, ast);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
function _parse(xml: Element): AST {
|
||||
function _parse(xml: Element, ctx: ParsingContext): AST {
|
||||
normalizeXML(xml);
|
||||
const ctx = { inPreTag: false };
|
||||
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
|
||||
}
|
||||
|
||||
@@ -222,6 +226,7 @@ interface ParsingContext {
|
||||
tModelInfo?: TModelInfo | null;
|
||||
nameSpace?: string;
|
||||
inPreTag: boolean;
|
||||
customDirectives?: customDirectives;
|
||||
}
|
||||
|
||||
function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||
@@ -229,6 +234,7 @@ 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) ||
|
||||
@@ -277,6 +283,37 @@ 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 modifier = attr.split(".").length > 1 ? attr.split(".")[1] : undefined;
|
||||
node.removeAttribute(attr);
|
||||
try {
|
||||
customDirective(node, value, modifier);
|
||||
} catch (error) {
|
||||
throw new OwlError(
|
||||
`Custom directive "${directiveName}" throw the following error: ${error}`
|
||||
);
|
||||
}
|
||||
return parseNode(node, ctx);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// debugging
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -12,5 +12,7 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(
|
||||
dev: this.dev,
|
||||
translateFn: this.translateFn,
|
||||
translatableAttributes: this.translatableAttributes,
|
||||
customDirectives: this.customDirectives,
|
||||
hasGlobalValues: this.hasGlobalValues,
|
||||
});
|
||||
};
|
||||
|
||||
+58
-10
@@ -1,6 +1,6 @@
|
||||
import { version } from "../version";
|
||||
import { Component, ComponentConstructor, Props } from "./component";
|
||||
import { ComponentNode } from "./component_node";
|
||||
import { ComponentNode, saveCurrent } from "./component_node";
|
||||
import { nodeErrorHandlers, handleError } from "./error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { Fiber, RootFiber, MountOptions } from "./fibers";
|
||||
@@ -16,10 +16,13 @@ export interface Env {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface AppConfig<P, E> extends TemplateSetConfig {
|
||||
name?: string;
|
||||
export interface RootConfig<P, E> {
|
||||
props?: P;
|
||||
env?: E;
|
||||
}
|
||||
|
||||
export interface AppConfig<P, E> extends TemplateSetConfig, RootConfig<P, E> {
|
||||
name?: string;
|
||||
test?: boolean;
|
||||
warnIfNoStaticProps?: boolean;
|
||||
}
|
||||
@@ -49,6 +52,12 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
interface Root<P extends Props, E> {
|
||||
node: ComponentNode<P, E>;
|
||||
mount(target: HTMLElement | ShadowRoot, options?: MountOptions): Promise<Component<P, E>>;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
window.__OWL_DEVTOOLS__ ||= { apps, Fiber, RootFiber, toRaw, reactive };
|
||||
|
||||
export class App<
|
||||
@@ -65,6 +74,7 @@ export class App<
|
||||
props: P;
|
||||
env: E;
|
||||
scheduler = new Scheduler();
|
||||
subRoots: Set<ComponentNode> = new Set();
|
||||
root: ComponentNode<P, E> | null = null;
|
||||
warnIfNoStaticProps: boolean;
|
||||
|
||||
@@ -91,14 +101,49 @@ export class App<
|
||||
target: HTMLElement | ShadowRoot,
|
||||
options?: MountOptions
|
||||
): Promise<Component<P, E> & InstanceType<T>> {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
||||
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;
|
||||
}
|
||||
|
||||
createRoot<Props extends object, SubEnv = any>(
|
||||
Root: ComponentConstructor<Props, E>,
|
||||
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 node = this.makeNode(this.Root, this.props);
|
||||
const prom = this.mountNode(node, target, options);
|
||||
this.root = node;
|
||||
return prom;
|
||||
|
||||
const restore = saveCurrent();
|
||||
const node = this.makeNode(Root, props);
|
||||
restore();
|
||||
if (config.env) {
|
||||
this.env = env;
|
||||
}
|
||||
this.subRoots.add(node);
|
||||
return {
|
||||
node,
|
||||
mount: (target: HTMLElement | ShadowRoot, options?: MountOptions) => {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(Root, props, { __owl__: { app: this } });
|
||||
}
|
||||
const prom = this.mountNode(node, target, options);
|
||||
return prom;
|
||||
},
|
||||
destroy: () => {
|
||||
this.subRoots.delete(node);
|
||||
node.destroy();
|
||||
this.scheduler.processTasks();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
||||
@@ -134,6 +179,9 @@ export class App<
|
||||
|
||||
destroy() {
|
||||
if (this.root) {
|
||||
for (let subroot of this.subRoots) {
|
||||
subroot.destroy();
|
||||
}
|
||||
this.root.destroy();
|
||||
this.scheduler.processTasks();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ import { batched, Callback } from "./utils";
|
||||
|
||||
let currentNode: ComponentNode | null = null;
|
||||
|
||||
export function saveCurrent() {
|
||||
let n = currentNode;
|
||||
return () => {
|
||||
currentNode = n;
|
||||
};
|
||||
}
|
||||
|
||||
export function getCurrent(): ComponentNode {
|
||||
if (!currentNode) {
|
||||
throw new OwlError("No active component (a hook function should only be called in 'setup')");
|
||||
|
||||
@@ -3,42 +3,50 @@ import { nodeErrorHandlers } from "./error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
|
||||
const TIMEOUT = Symbol("timeout");
|
||||
const HOOK_TIMEOUT: { [key: string]: number } = {
|
||||
onWillStart: 3000,
|
||||
onWillUpdateProps: 3000,
|
||||
};
|
||||
function wrapError(fn: (...args: any[]) => any, hookName: string) {
|
||||
const error = new OwlError(`The following error occurred in ${hookName}: `) as Error & {
|
||||
const error = new OwlError() as Error & {
|
||||
cause: any;
|
||||
};
|
||||
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
|
||||
const timeoutError = new OwlError();
|
||||
const node = getCurrent();
|
||||
return (...args: any[]) => {
|
||||
const onError = (cause: any) => {
|
||||
error.cause = cause;
|
||||
if (cause instanceof Error) {
|
||||
error.message += `"${cause.message}"`;
|
||||
} else {
|
||||
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||
}
|
||||
error.message =
|
||||
cause instanceof Error
|
||||
? `The following error occurred in ${hookName}: "${cause.message}"`
|
||||
: `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
|
||||
throw error;
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
const result = fn(...args);
|
||||
if (result instanceof Promise) {
|
||||
if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
|
||||
const fiber = node.fiber;
|
||||
Promise.race([
|
||||
result.catch(() => {}),
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||
console.warn(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.catch(onError);
|
||||
}
|
||||
return result;
|
||||
result = fn(...args);
|
||||
} catch (cause) {
|
||||
onError(cause);
|
||||
}
|
||||
if (!(result instanceof Promise)) {
|
||||
return result;
|
||||
}
|
||||
const timeout = HOOK_TIMEOUT[hookName];
|
||||
if (timeout) {
|
||||
const fiber = node.fiber;
|
||||
Promise.race([
|
||||
result.catch(() => {}),
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||
timeoutError.message = `${hookName}'s promise hasn't resolved after ${
|
||||
timeout / 1000
|
||||
} seconds`;
|
||||
console.log(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.catch(onError);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export class Scheduler {
|
||||
frame: number = 0;
|
||||
delayedRenders: Fiber[] = [];
|
||||
cancelledNodes: Set<ComponentNode> = new Set();
|
||||
processing = false;
|
||||
|
||||
constructor() {
|
||||
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||||
@@ -53,6 +54,10 @@ export class Scheduler {
|
||||
}
|
||||
|
||||
processTasks() {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
this.processing = true;
|
||||
this.frame = 0;
|
||||
for (let node of this.cancelledNodes) {
|
||||
node._destroy();
|
||||
@@ -66,6 +71,7 @@ export class Scheduler {
|
||||
this.tasks.delete(task);
|
||||
}
|
||||
}
|
||||
this.processing = false;
|
||||
}
|
||||
|
||||
processFiber(fiber: RootFiber) {
|
||||
@@ -87,7 +93,14 @@ export class Scheduler {
|
||||
if (!hasError) {
|
||||
fiber.complete();
|
||||
}
|
||||
this.tasks.delete(fiber);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 };
|
||||
|
||||
@@ -14,6 +15,8 @@ export interface TemplateSetConfig {
|
||||
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,6 +30,9 @@ export class TemplateSet {
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
Portal = Portal;
|
||||
customDirectives: customDirectives;
|
||||
runtimeUtils: object;
|
||||
hasGlobalValues: boolean;
|
||||
|
||||
constructor(config: TemplateSetConfig = {}) {
|
||||
this.dev = config.dev || false;
|
||||
@@ -42,6 +48,9 @@ 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) {
|
||||
@@ -97,7 +106,7 @@ export class TemplateSet {
|
||||
this.templates[name] = function (context, parent) {
|
||||
return templates[name].call(this, context, parent);
|
||||
};
|
||||
const template = templateFn(this, bdom, helpers);
|
||||
const template = templateFn(this, bdom, this.runtimeUtils);
|
||||
this.templates[name] = template;
|
||||
}
|
||||
return this.templates[name];
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.2.10";
|
||||
export const version = "2.5.0";
|
||||
|
||||
@@ -43,6 +43,48 @@ 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
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`parent\`);
|
||||
const b3 = comp1({}, key + \`__1\`, node, this, null);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`app can call processTask twice in a row without crashing 2`] = `
|
||||
"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[`app can configure an app with props 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`destroy a subroot while another component is mounted in main app 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`ChildB\`, true, false, false, []);
|
||||
const comp2 = app.createComponent(\`ChildA\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2, b3;
|
||||
if (ctx['state'].flag) {
|
||||
b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||
} else {
|
||||
b3 = comp2({}, key + \`__2\`, node, this, null);
|
||||
}
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroy a subroot while another component is mounted in main app 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block3 = createBlock(\`<div block-ref=\\"0\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`a\`);
|
||||
let ref1 = (el) => this.__owl__.setRef((\`elem\`), el);
|
||||
const b3 = block3([ref1]);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroy a subroot while another component is mounted in main app 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`c\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`destroy a subroot while another component is mounted in main app 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`b\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot by default, env is the same in sub root 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>main app</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot by default, env is the same in sub root 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot can create a root in a setup function, then use a hook 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`a\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
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
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>main app</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot can mount subroot 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot can mount subroot inside own dom 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>main app</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot can mount subroot inside own dom 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot env can be specified for sub roots 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>main app</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot env can be specified for sub roots 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot subcomponents can be destroyed, and it properly cleanup the subroots 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>main app</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`subroot subcomponents can be destroyed, and it properly cleanup the subroots 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>sub root</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
+36
-1
@@ -1,4 +1,4 @@
|
||||
import { App, Component, mount, onWillStart, useState, xml } from "../../src";
|
||||
import { App, Component, mount, onWillPatch, onWillStart, useState, xml } from "../../src";
|
||||
import { status } from "../../src/runtime/status";
|
||||
import {
|
||||
makeTestFixture,
|
||||
@@ -184,4 +184,39 @@ describe("app", () => {
|
||||
expect(Object.keys(app.templates)).toEqual(["hello"]);
|
||||
expect(Object.keys(app.rawTemplates)).toEqual(["hello", "world"]);
|
||||
});
|
||||
|
||||
test("can call processTask twice in a row without crashing", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<div/>`;
|
||||
setup() {
|
||||
onWillPatch(() => app.scheduler.processTasks());
|
||||
}
|
||||
}
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`parent<Child/>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
const app = new App(SomeComponent);
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { App, Component, onMounted, onWillDestroy, useRef, useState, xml } from "../../src";
|
||||
import { status } from "../../src/runtime/status";
|
||||
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
||||
|
||||
let fixture: HTMLElement;
|
||||
|
||||
snapshotEverything();
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div>main app</div>`;
|
||||
}
|
||||
|
||||
class SubComponent extends Component {
|
||||
static template = xml`<div>sub root</div>`;
|
||||
}
|
||||
|
||||
describe("subroot", () => {
|
||||
test("can mount subroot", async () => {
|
||||
const app = new App(SomeComponent);
|
||||
const comp = await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>main app</div>");
|
||||
const subRoot = app.createRoot(SubComponent);
|
||||
const subcomp = await subRoot.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>main app</div><div>sub root</div>");
|
||||
|
||||
app.destroy();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
expect(status(comp)).toBe("destroyed");
|
||||
expect(status(subcomp)).toBe("destroyed");
|
||||
});
|
||||
|
||||
test("can mount subroot inside own dom", async () => {
|
||||
const app = new App(SomeComponent);
|
||||
const comp = await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>main app</div>");
|
||||
const subRoot = app.createRoot(SubComponent);
|
||||
const subcomp = await subRoot.mount(fixture.querySelector("div")!);
|
||||
expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");
|
||||
|
||||
app.destroy();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
expect(status(comp)).toBe("destroyed");
|
||||
expect(status(subcomp)).toBe("destroyed");
|
||||
});
|
||||
|
||||
test("by default, env is the same in sub root", async () => {
|
||||
let env, subenv;
|
||||
class SC extends SomeComponent {
|
||||
setup() {
|
||||
env = this.env;
|
||||
}
|
||||
}
|
||||
class Sub extends SubComponent {
|
||||
setup() {
|
||||
subenv = this.env;
|
||||
}
|
||||
}
|
||||
|
||||
const app = new App(SC);
|
||||
await app.mount(fixture);
|
||||
const subRoot = app.createRoot(Sub);
|
||||
await subRoot.mount(fixture);
|
||||
|
||||
expect(env).toBeDefined();
|
||||
expect(subenv).toBeDefined();
|
||||
expect(env).toBe(subenv);
|
||||
});
|
||||
|
||||
test("env can be specified for sub roots", async () => {
|
||||
const env1 = { env1: true };
|
||||
const env2 = {};
|
||||
let someComponentEnv: any, subComponentEnv: any;
|
||||
class SC extends SomeComponent {
|
||||
setup() {
|
||||
someComponentEnv = this.env;
|
||||
}
|
||||
}
|
||||
class Sub extends SubComponent {
|
||||
setup() {
|
||||
subComponentEnv = this.env;
|
||||
}
|
||||
}
|
||||
|
||||
const app = new App(SC, { env: env1 });
|
||||
await app.mount(fixture);
|
||||
const subRoot = app.createRoot(Sub, { env: env2 });
|
||||
await subRoot.mount(fixture);
|
||||
|
||||
// because env is different in app => it is given a sub object, frozen and all
|
||||
// not sure it is a good idea, but it's the way owl 2 works. maybe we should
|
||||
// avoid doing anything with the main env and let user code do it if they
|
||||
// want. in that case, we can change the test here to assert that they are equal
|
||||
expect(someComponentEnv).not.toBe(env1);
|
||||
expect(someComponentEnv!.env1).toBe(true);
|
||||
expect(subComponentEnv).toBe(env2);
|
||||
});
|
||||
|
||||
test("subcomponents can be destroyed, and it properly cleanup the subroots", async () => {
|
||||
const app = new App(SomeComponent);
|
||||
const comp = await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>main app</div>");
|
||||
const root = app.createRoot(SubComponent);
|
||||
const subcomp = await root.mount(fixture.querySelector("div")!);
|
||||
expect(fixture.innerHTML).toBe("<div>main app<div>sub root</div></div>");
|
||||
|
||||
root.destroy();
|
||||
expect(fixture.innerHTML).toBe("<div>main app</div>");
|
||||
expect(status(comp)).not.toBe("destroyed");
|
||||
expect(status(subcomp)).toBe("destroyed");
|
||||
});
|
||||
|
||||
test("can create a root in a setup function, then use a hook", async () => {
|
||||
class C extends Component {
|
||||
static template = xml`c`;
|
||||
}
|
||||
|
||||
class A extends Component {
|
||||
static template = xml`a`;
|
||||
state: any;
|
||||
setup() {
|
||||
app.createRoot(C);
|
||||
this.state = useState({ value: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
const app = new App(A);
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("a");
|
||||
});
|
||||
});
|
||||
|
||||
test("destroy a subroot while another component is mounted in main app", async () => {
|
||||
class C extends Component {
|
||||
static template = xml`c`;
|
||||
}
|
||||
|
||||
class ChildA extends Component {
|
||||
static template = xml`a<div t-ref="elem"></div>`;
|
||||
ref: any;
|
||||
setup() {
|
||||
this.ref = useRef("elem");
|
||||
let root = app.createRoot(C);
|
||||
onMounted(() => {
|
||||
root.mount(this.ref.el);
|
||||
});
|
||||
onWillDestroy(() => {
|
||||
root.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
class ChildB extends Component {
|
||||
static template = xml`b`;
|
||||
}
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-if="state.flag"><ChildB/></t>
|
||||
<t t-else=""><ChildA/></t>
|
||||
`;
|
||||
static components = { ChildA, ChildB };
|
||||
state = useState({ flag: false });
|
||||
}
|
||||
|
||||
const app = new App(SomeComponent);
|
||||
const comp = await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("a<div></div>");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("a<div>c</div>");
|
||||
comp.state.flag = true;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("b");
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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 modifier 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]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -174,6 +174,9 @@ describe("expression evaluation", () => {
|
||||
expect(compileExpr("list.data.map((data) => data)")).toBe(
|
||||
"ctx['list'].data.map((_data)=>_data)"
|
||||
);
|
||||
expect(compileExpr("(ev) => { myFunc(v1, v2, ev.target.value); }")).toBe(
|
||||
"(_ev)=>{ctx['myFunc'](ctx['v1'],ctx['v2'],_ev.target.value);}"
|
||||
);
|
||||
});
|
||||
test.skip("arrow functions: not yet supported", () => {
|
||||
// e is added to localvars in inline_expression but not removed after the arrow func body
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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 modifier on a node", async () => {
|
||||
const steps: string[] = [];
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div t-custom-plop.mouse="click" class="my-div"/>`;
|
||||
click() {
|
||||
steps.push("clicked");
|
||||
}
|
||||
}
|
||||
const app = new App(SomeComponent, {
|
||||
customDirectives: {
|
||||
plop: (node, value, modifier) => {
|
||||
node.setAttribute("t-on-click", value);
|
||||
steps.push(modifier || "");
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
|
||||
fixture.querySelector("div")!.click();
|
||||
expect(steps).toEqual(["mouse", "clicked"]);
|
||||
});
|
||||
});
|
||||
@@ -48,7 +48,7 @@ describe("basic validation", () => {
|
||||
test("compilation error", () => {
|
||||
const template = `<div t-att-class="a b">test</div>`;
|
||||
expect(() => renderToString(template))
|
||||
.toThrow(`Failed to compile anonymous template: Unexpected identifier
|
||||
.toThrow(`Failed to compile anonymous template: Unexpected identifier 'ctx'
|
||||
|
||||
generated code:
|
||||
function(app, bdom, helpers) {
|
||||
|
||||
@@ -97,6 +97,19 @@ exports[`basics a component cannot be mounted in a detached node (even if node i
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics a component cannot be mounted in a detached node 1`] = `
|
||||
"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[`basics a component inside a component 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -261,6 +274,19 @@ exports[`basics can mount a simple component with props 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics cannot mount on a documentFragment 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>content</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics child can be updated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -1002,6 +1028,19 @@ exports[`basics three level of components with collapsing root nodes 3`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics throws if mounting on target=null 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span>simple vnode</span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics two child components 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -320,6 +320,50 @@ 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
|
||||
) {
|
||||
|
||||
@@ -683,7 +683,7 @@ exports[`lifecycle hooks sub widget (inside sub node): hooks are correctly calle
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillStart doesn't emit a warning if app is destroyed 1`] = `
|
||||
exports[`lifecycle hooks timeout in onWillStart doesn't emit a console log if app is destroyed 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -696,7 +696,7 @@ exports[`lifecycle hooks timeout in onWillStart doesn't emit a warning if app is
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
|
||||
exports[`lifecycle hooks timeout in onWillStart emits a console log 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -709,7 +709,7 @@ exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 1`] = `
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a console log 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -723,7 +723,7 @@ exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 2`] = `
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a console log 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
@@ -66,6 +66,29 @@ exports[`.alike suffix in a simple case 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`.translate props are translated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({message: \`translated message\`}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`.translate props are translated 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['props'].message);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`basics accept ES6-like syntax for props (with getters) 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -412,6 +435,29 @@ exports[`can bind function prop with bind suffix 2`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can use .translate suffix 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({message: \`some message\`}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can use .translate suffix 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['props'].message);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`do not crash when binding anonymous function prop with bind suffix 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -924,6 +924,20 @@ exports[`props validation props: list of strings 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`props validation validate props for root component 1`] = `
|
||||
"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['message'];
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`props validation validate simple types 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`slots .translate slot props are translated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { capture, markRaw } = helpers;
|
||||
const comp1 = app.createComponent(\`Child\`, true, true, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const ctx1 = capture(ctx);
|
||||
return comp1({slots: markRaw({'default': {message: \`translated message\`}})}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots .translate slot props are translated 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['props'].slots.default.message);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots can define a default content 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
@@ -201,6 +226,31 @@ exports[`slots can render only empty slot 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots can use .translate suffix on slot props 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { capture, markRaw } = helpers;
|
||||
const comp1 = app.createComponent(\`Child\`, true, true, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const ctx1 = capture(ctx);
|
||||
return comp1({slots: markRaw({'default': {message: \`some message\`}})}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots can use .translate suffix on slot props 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(ctx['props'].slots.default.message);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots can use component in default-content of t-slot 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("basics", () => {
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier
|
||||
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier 'ctx'
|
||||
|
||||
generated code:
|
||||
function(app, bdom, helpers) {
|
||||
@@ -182,7 +182,7 @@ function(app, bdom, helpers) {
|
||||
static components = { Child };
|
||||
static template = xml`<Child/>`;
|
||||
}
|
||||
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier
|
||||
const expectedErrorMessage = `Failed to compile anonymous template: Unexpected identifier 'ctx'
|
||||
|
||||
generated code:
|
||||
function(app, bdom, helpers) {
|
||||
@@ -564,6 +564,82 @@ 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"/>`;
|
||||
|
||||
@@ -112,10 +112,10 @@ describe("lifecycle hooks", () => {
|
||||
await mount(Test, fixture);
|
||||
});
|
||||
|
||||
test("timeout in onWillStart emits a warning", async () => {
|
||||
const { warn } = console;
|
||||
let warnArgs: any[];
|
||||
console.warn = jest.fn((...args) => (warnArgs = args));
|
||||
test("timeout in onWillStart emits a console log", async () => {
|
||||
const { log } = console;
|
||||
let logArgs: any[];
|
||||
console.log = jest.fn((...args) => (logArgs = args));
|
||||
const { setTimeout } = window;
|
||||
let timeoutCbs: any = {};
|
||||
let timeoutId = 0;
|
||||
@@ -138,17 +138,17 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(warnArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(logArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
||||
} finally {
|
||||
console.warn = warn;
|
||||
console.log = log;
|
||||
window.setTimeout = setTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
test("timeout in onWillStart doesn't emit a warning if app is destroyed", async () => {
|
||||
const { warn } = console;
|
||||
console.warn = jest.fn();
|
||||
test("timeout in onWillStart doesn't emit a console log if app is destroyed", async () => {
|
||||
const { log } = console;
|
||||
console.log = jest.fn();
|
||||
const { setTimeout } = window;
|
||||
let timeoutCbs: any = {};
|
||||
let timeoutId = 0;
|
||||
@@ -172,14 +172,14 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(console.warn).toHaveBeenCalledTimes(0);
|
||||
expect(console.log).toHaveBeenCalledTimes(0);
|
||||
} finally {
|
||||
console.warn = warn;
|
||||
console.log = log;
|
||||
window.setTimeout = setTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
test("timeout in onWillUpdateProps emits a warning", async () => {
|
||||
test("timeout in onWillUpdateProps emits a console log", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml``;
|
||||
setup() {
|
||||
@@ -193,9 +193,9 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
const parent = await mount(Parent, fixture, { test: true });
|
||||
|
||||
const { warn } = console;
|
||||
let warnArgs: any[];
|
||||
console.warn = jest.fn((...args) => (warnArgs = args));
|
||||
const { log } = console;
|
||||
let logArgs: any[];
|
||||
console.log = jest.fn((...args) => (logArgs = args));
|
||||
const { setTimeout } = window;
|
||||
let timeoutCbs: any = {};
|
||||
let timeoutId = 0;
|
||||
@@ -218,12 +218,12 @@ describe("lifecycle hooks", () => {
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await tick;
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(warnArgs![0]!.message).toBe(
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(logArgs![0]!.message).toBe(
|
||||
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
|
||||
);
|
||||
} finally {
|
||||
console.warn = warn;
|
||||
console.log = log;
|
||||
window.setTimeout = setTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -299,6 +299,34 @@ test("bound functions are considered 'alike'", async () => {
|
||||
expect(fixture.innerHTML).toBe("3child");
|
||||
});
|
||||
|
||||
test("can use .translate suffix", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.message"/>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child message.translate="some message"/>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("some message");
|
||||
});
|
||||
|
||||
test(".translate props are translated", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.message"/>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child message.translate="some message"/>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
await mount(Parent, fixture, { translateFn: () => "translated message" });
|
||||
expect(fixture.innerHTML).toBe("translated message");
|
||||
});
|
||||
|
||||
test("throw if prop uses an unknown suffix", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.val"/>`;
|
||||
|
||||
@@ -179,6 +179,34 @@ describe("slots", () => {
|
||||
expect(fixture.innerHTML).toBe("<span>default empty</span>");
|
||||
});
|
||||
|
||||
test("can use .translate suffix on slot props", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.slots.default.message"/>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child><t t-set-slot="default" message.translate="some message"/></Child>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
await mount(Parent, fixture);
|
||||
expect(fixture.innerHTML).toBe("some message");
|
||||
});
|
||||
|
||||
test(".translate slot props are translated", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-esc="props.slots.default.message"/>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child><t t-set-slot="default" message.translate="some message"/></Child>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
await mount(Parent, fixture, { translateFn: () => "translated message" });
|
||||
expect(fixture.innerHTML).toBe("translated message");
|
||||
});
|
||||
|
||||
test("default slot with slot scope: shorthand syntax", async () => {
|
||||
let child: any;
|
||||
class Child extends Component {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"default_popup": "popup_app/popup.html"
|
||||
},
|
||||
"permissions": ["scripting", "storage"],
|
||||
"host_permissions": ["http://*/*", "https://*/*"],
|
||||
"host_permissions": ["http://*/*", "https://*/*", "file://*"],
|
||||
"content_security_policy": {
|
||||
"script-src": "self",
|
||||
"object-src": "self"
|
||||
|
||||
@@ -109,23 +109,34 @@
|
||||
object(obj) {
|
||||
const result = [];
|
||||
let length = 0;
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
if (obj instanceof String) {
|
||||
result[0] = `'${obj.toString()}'`;
|
||||
} else if (obj instanceof Array) {
|
||||
return `${obj.constructor.name} ${this.array([...obj])}`;
|
||||
} else if (obj instanceof Number) {
|
||||
result[0] = obj.toString();
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
}
|
||||
const element = key + ": " + this.serializeItem(value);
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
}
|
||||
for (const key of Object.getOwnPropertySymbols(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
}
|
||||
const element = key.toString() + ": " + this.serializeItem(obj[key]);
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
}
|
||||
const element = key + ": " + this.serializeItem(value);
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
}
|
||||
for (const key of Object.getOwnPropertySymbols(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
}
|
||||
const element = key.toString() + ": " + this.serializeItem(obj[key]);
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
if (obj.constructor.name !== "Object") {
|
||||
return obj.constructor.name + " {" + result.join(", ") + "}";
|
||||
}
|
||||
return "{" + result.join(", ") + "}";
|
||||
},
|
||||
@@ -823,7 +834,7 @@
|
||||
child.contentType = "set";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj instanceof Array:
|
||||
case obj.constructor.name === "Array":
|
||||
child.contentType = "array";
|
||||
child.hasChildren = obj.length > 0;
|
||||
break;
|
||||
@@ -834,7 +845,9 @@
|
||||
case obj instanceof Object:
|
||||
child.contentType = "object";
|
||||
child.hasChildren =
|
||||
Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length;
|
||||
Object.keys(obj).length ||
|
||||
Object.getOwnPropertySymbols(obj).length ||
|
||||
obj.constructor.name !== "Object";
|
||||
break;
|
||||
default:
|
||||
child.contentType = typeof obj;
|
||||
|
||||
@@ -140,7 +140,6 @@ 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) {
|
||||
|
||||
Reference in New Issue
Block a user