mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26c7856d5d | |||
| b8d09e523d | |||
| 04c2808701 | |||
| 15c2604df1 | |||
| 3e11fe6b12 | |||
| 20c6cacb4e | |||
| eb2b32ab60 | |||
| 2a223288d4 | |||
| 1272278225 | |||
| f502dd732e | |||
| 9c2d957525 | |||
| f8bb86820e | |||
| 0cde4b8737 | |||
| 66a801393f | |||
| e7f405cc97 | |||
| 55c48b2b12 | |||
| 2eb151c92d | |||
| 7952f31e63 | |||
| 11e4e67599 | |||
| 7b7a6de373 | |||
| b63d1e28b2 | |||
| c0667a11c6 | |||
| fddb1ec924 |
@@ -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
|
||||
|
||||
@@ -47,3 +47,4 @@ Utility/helpers:
|
||||
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
||||
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
||||
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
||||
- [`batched`](reference/utils.md#batched): utility function to batch function calls
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- [API](#api)
|
||||
- [Configuration](#configuration)
|
||||
- [`mount` helper](#mount-helper)
|
||||
- [Roots](#roots)
|
||||
- [Loading templates](#loading-templates)
|
||||
|
||||
## Overview
|
||||
@@ -92,6 +93,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
|
||||
|
||||
+25
-2
@@ -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:
|
||||
@@ -238,7 +260,7 @@ class ComponentB extends owl.Component {
|
||||
count: {type: Number},
|
||||
messages: {
|
||||
type: Array,
|
||||
element: {type: Object, shape: {id: Boolean, text: String }
|
||||
element: {type: Object, shape: {id: Boolean, text: String }}
|
||||
},
|
||||
date: Date,
|
||||
combinedVal: [Number, Boolean],
|
||||
@@ -276,7 +298,8 @@ class ComponentB extends owl.Component {
|
||||
id: Number,
|
||||
name: {type: String, optional: true},
|
||||
url: String
|
||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
}
|
||||
}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
someObj3: {
|
||||
type: Object,
|
||||
values: { type: Array, element: String },
|
||||
|
||||
@@ -133,7 +133,7 @@ Slots can define a default content, in case the parent did not define them:
|
||||
## Dynamic Slots
|
||||
|
||||
The `t-slot` directive is actually able to use any expressions, using string
|
||||
interplolation:
|
||||
interpolation:
|
||||
|
||||
```xml
|
||||
<t t-slot="{{current}}" />
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@ functions are all available in the `owl.utils` namespace.
|
||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||
- [`EventBus`](#eventbus): a simple EventBus
|
||||
- [`validate`](#validate): a validation function
|
||||
- [`batched`](#batched): batch function calls
|
||||
|
||||
## `whenReady`
|
||||
|
||||
@@ -78,3 +79,22 @@ validate(
|
||||
// - 'id' is missing (should be a number),
|
||||
// - 'url' is missing (should be a boolean or list of numbers),
|
||||
```
|
||||
|
||||
## `batched`
|
||||
|
||||
The `batched` function creates a batched version of a callback so that multiple calls to it within the same microtick will only result in a single invocation of the original callback.
|
||||
|
||||
```js
|
||||
function hello() {
|
||||
console.log("hello");
|
||||
}
|
||||
|
||||
const batchedHello = batched(hello);
|
||||
batchedHello();
|
||||
// Nothing is logged
|
||||
batchedHello();
|
||||
// Still not logged
|
||||
|
||||
await Promise.resolve(); // Await the next microtick
|
||||
// "hello" is logged only once
|
||||
```
|
||||
|
||||
+122
-57
@@ -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);
|
||||
};
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -3512,7 +3523,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 +3532,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 +3649,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
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -3816,15 +3836,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 +4021,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 +4043,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 +4264,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 +4518,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 +4570,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 +4586,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 +4605,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 +4619,7 @@ class CodeGenerator {
|
||||
value = `(${value}).bind(this)`;
|
||||
break;
|
||||
case "alike":
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
@@ -4662,7 +4688,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 +4703,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 +4776,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 +4817,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 +4829,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);
|
||||
}
|
||||
@@ -5538,7 +5563,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.4.1";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -5549,6 +5574,7 @@ class Scheduler {
|
||||
this.frame = 0;
|
||||
this.delayedRenders = [];
|
||||
this.cancelledNodes = new Set();
|
||||
this.processing = false;
|
||||
this.requestAnimationFrame = Scheduler.requestAnimationFrame;
|
||||
}
|
||||
addFiber(fiber) {
|
||||
@@ -5579,6 +5605,10 @@ class Scheduler {
|
||||
}
|
||||
}
|
||||
processTasks() {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
this.processing = true;
|
||||
this.frame = 0;
|
||||
for (let node of this.cancelledNodes) {
|
||||
node._destroy();
|
||||
@@ -5592,6 +5622,7 @@ class Scheduler {
|
||||
this.tasks.delete(task);
|
||||
}
|
||||
}
|
||||
this.processing = false;
|
||||
}
|
||||
processFiber(fiber) {
|
||||
if (fiber.root !== fiber) {
|
||||
@@ -5633,6 +5664,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 +5683,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 +5752,9 @@ class App extends TemplateSet {
|
||||
}
|
||||
destroy() {
|
||||
if (this.root) {
|
||||
for (let subroot of this.subRoots) {
|
||||
subroot.destroy();
|
||||
}
|
||||
this.root.destroy();
|
||||
this.scheduler.processTasks();
|
||||
}
|
||||
@@ -5964,9 +6029,9 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
||||
});
|
||||
};
|
||||
|
||||
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-10-31T09:42:30.824Z';
|
||||
__info__.hash = 'b8d09e5';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
@@ -41,9 +41,6 @@ const loadFile = (path) => {
|
||||
* Make an iframe, with all the js, css and xml properly injected.
|
||||
*/
|
||||
function makeCodeIframe(js, css, xml) {
|
||||
// escape backticks in the xml so they don't close the template string
|
||||
const escapedXml = xml.replace(/`/g, '\\\`');
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.onload = () => {
|
||||
const doc = iframe.contentDocument;
|
||||
@@ -55,6 +52,8 @@ function makeCodeIframe(js, css, xml) {
|
||||
|
||||
const script = doc.createElement("script");
|
||||
script.type = "module";
|
||||
// escape characters with special meaning in template literals
|
||||
const escapedXml = xml.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${");
|
||||
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
|
||||
doc.body.appendChild(script);
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.10",
|
||||
"version": "2.4.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.10",
|
||||
"version": "2.4.1",
|
||||
"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",
|
||||
|
||||
@@ -82,6 +82,14 @@ function isProp(tag: string, key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a template literal that evaluates to str. You can add interpolation
|
||||
* sigils into the string if required
|
||||
*/
|
||||
function toStringExpression(str: string) {
|
||||
return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// BlockDescription
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -311,14 +319,13 @@ export 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});`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,7 +522,7 @@ export 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,
|
||||
});
|
||||
@@ -539,7 +546,7 @@ export 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,
|
||||
});
|
||||
@@ -774,7 +781,8 @@ export 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) {
|
||||
@@ -1039,7 +1047,7 @@ export class CodeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
const key = `key + \`${this.generateComponentKey()}\``;
|
||||
const key = this.generateComponentKey();
|
||||
if (isDynamic) {
|
||||
const templateVar = generateId("template");
|
||||
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||||
@@ -1091,11 +1099,13 @@ export class CodeGenerator {
|
||||
} else {
|
||||
let value: string;
|
||||
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 {
|
||||
value = expr;
|
||||
@@ -1106,12 +1116,12 @@ export class CodeGenerator {
|
||||
return null;
|
||||
}
|
||||
|
||||
generateComponentKey() {
|
||||
generateComponentKey(currentKey: string = "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("__")}\``;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1126,7 +1136,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;
|
||||
@@ -1135,6 +1149,7 @@ export class CodeGenerator {
|
||||
value = `(${value}).bind(this)`;
|
||||
break;
|
||||
case "alike":
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
@@ -1214,7 +1229,6 @@ export class CodeGenerator {
|
||||
}
|
||||
|
||||
// cmap key
|
||||
const key = this.generateComponentKey();
|
||||
let expr: string;
|
||||
if (ast.isDynamic) {
|
||||
expr = generateId("Comp");
|
||||
@@ -1232,7 +1246,7 @@ export class CodeGenerator {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
|
||||
let keyArg = `key + \`${key}\``;
|
||||
let keyArg = this.generateComponentKey();
|
||||
if (ctx.tKeyExpr) {
|
||||
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||||
}
|
||||
@@ -1311,7 +1325,7 @@ export 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) : [];
|
||||
@@ -1354,7 +1368,6 @@ export 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");
|
||||
@@ -1368,7 +1381,8 @@ export class CodeGenerator {
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+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')");
|
||||
|
||||
@@ -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 { EventBus, whenReady, loadFile, markup } from "./utils";
|
||||
export { batched, EventBus, whenReady, loadFile, markup } from "./utils";
|
||||
export {
|
||||
onWillStart,
|
||||
onMounted,
|
||||
|
||||
@@ -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) {
|
||||
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) {
|
||||
|
||||
+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.4.1";
|
||||
|
||||
@@ -43,6 +43,33 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
|
||||
}"
|
||||
`;
|
||||
|
||||
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();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
+18
-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,21 @@ 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>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -1,5 +1,38 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`comments comment node with backslash at top level 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comment(\` \\\\\\\\ \`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`comments comment node with backtick at top-level 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comment(\` \\\\\` \`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`comments comment node with interpolation sigil at top level 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comment(\` \\\\\${very cool} \`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`comments only a comment 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -341,6 +341,39 @@ exports[`simple templates, mostly static template with t tag with multiple conte
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static text node with backslash at top level 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`\\\\\\\\\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static text node with backtick at top-level 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`\\\\\`\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static text node with interpolation sigil at top level 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`\\\\\${very cool}\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`simple templates, mostly static two t-escs next to each other 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`t-esc default with backslash at top level 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { withDefault } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(withDefault(undefined, \`\\\\\\\\\`));
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-esc default with backtick at top-level 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { withDefault } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(withDefault(undefined, \`\\\\\`\`));
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-esc default with interpolation sigil at top level 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { withDefault } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(withDefault(undefined, \`\\\\\${very cool}\`));
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-esc div with falsy values 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`t-set body with backslash at top level 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, \\"value\\", \`\\\\\\\\\`);
|
||||
return text(ctx['value']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-set body with backtick at top-level 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, \\"value\\", \`\\\\\`\`);
|
||||
return text(ctx['value']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-set body with interpolation sigil at top level 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, \\"value\\", \`\\\\\${very cool}\`);
|
||||
return text(ctx['value']);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-set evaluate value expression 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -26,4 +26,19 @@ describe("comments", () => {
|
||||
</div>`;
|
||||
expect(renderToString(template)).toBe("<div><span>true</span></div>");
|
||||
});
|
||||
|
||||
test("comment node with backslash at top level", () => {
|
||||
const template = "<!-- \\ -->";
|
||||
expect(renderToString(template)).toBe("<!-- \\ -->");
|
||||
});
|
||||
|
||||
test("comment node with backtick at top-level", () => {
|
||||
const template = "<!-- ` -->";
|
||||
expect(renderToString(template)).toBe("<!-- ` -->");
|
||||
});
|
||||
|
||||
test("comment node with interpolation sigil at top level", () => {
|
||||
const template = "<!-- ${very cool} -->";
|
||||
expect(renderToString(template)).toBe("<!-- ${very cool} -->");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -154,4 +154,19 @@ describe("simple templates, mostly static", () => {
|
||||
</div>`;
|
||||
expect(renderToString(template, { a: "a", b: "b", c: "c" })).toBe("<div>abLoadingc</div>");
|
||||
});
|
||||
|
||||
test("text node with backslash at top level", () => {
|
||||
const template = "\\";
|
||||
expect(renderToString(template)).toBe("\\");
|
||||
});
|
||||
|
||||
test("text node with backtick at top-level", () => {
|
||||
const template = "`";
|
||||
expect(renderToString(template)).toBe("`");
|
||||
});
|
||||
|
||||
test("text node with interpolation sigil at top level", () => {
|
||||
const template = "${very cool}";
|
||||
expect(renderToString(template)).toBe("${very cool}");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,4 +121,19 @@ describe("t-esc", () => {
|
||||
mount(bdom, fixture);
|
||||
expect(fixture.querySelector("span")!.textContent).toBe("<p>escaped</p>");
|
||||
});
|
||||
|
||||
test("default with backslash at top level", () => {
|
||||
const template = '<t t-esc="undefined">\\</t>';
|
||||
expect(renderToString(template)).toBe("\\");
|
||||
});
|
||||
|
||||
test("default with backtick at top-level", () => {
|
||||
const template = '<t t-esc="undefined">`</t>';
|
||||
expect(renderToString(template)).toBe("`");
|
||||
});
|
||||
|
||||
test("default with interpolation sigil at top level", () => {
|
||||
const template = '<t t-esc="undefined">${very cool}</t>';
|
||||
expect(renderToString(template)).toBe("${very cool}");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,6 +54,21 @@ describe("t-set", () => {
|
||||
expect(renderToString(template)).toBe("ok");
|
||||
});
|
||||
|
||||
test("body with backslash at top level", () => {
|
||||
const template = '<t t-set="value">\\</t><t t-esc="value"/>';
|
||||
expect(renderToString(template)).toBe("\\");
|
||||
});
|
||||
|
||||
test("body with backtick at top-level", () => {
|
||||
const template = '<t t-set="value">`</t><t t-esc="value"/>';
|
||||
expect(renderToString(template)).toBe("`");
|
||||
});
|
||||
|
||||
test("body with interpolation sigil at top level", () => {
|
||||
const template = '<t t-set="value">${very cool}</t><t t-esc="value"/>';
|
||||
expect(renderToString(template)).toBe("${very cool}");
|
||||
});
|
||||
|
||||
test("set from body literal (with t-if/t-else", () => {
|
||||
const template = `
|
||||
<t>
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -683,7 +683,7 @@ exports[`lifecycle hooks sub widget (inside sub node): hooks are correctly calle
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillStart emits a warning 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,20 @@ 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 onWillStart emits a console log 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
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;
|
||||
@@ -710,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) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { App, Component, mount, onMounted, onWillStart, useState, xml } from "../../src";
|
||||
import {
|
||||
App,
|
||||
Component,
|
||||
mount,
|
||||
useState,
|
||||
xml,
|
||||
onWillPatch,
|
||||
onWillUnmount,
|
||||
onPatched,
|
||||
@@ -7,7 +11,9 @@ import {
|
||||
onWillRender,
|
||||
onWillDestroy,
|
||||
onRendered,
|
||||
} from "../../src/runtime/lifecycle_hooks";
|
||||
onMounted,
|
||||
onWillStart,
|
||||
} from "../../src";
|
||||
import { status } from "../../src/runtime/status";
|
||||
import {
|
||||
elem,
|
||||
@@ -106,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;
|
||||
@@ -117,27 +123,63 @@ describe("lifecycle hooks", () => {
|
||||
timeoutCbs[++timeoutId] = cb;
|
||||
return timeoutId;
|
||||
}) as any;
|
||||
class Test extends Component {
|
||||
static template = xml`<span/>`;
|
||||
setup() {
|
||||
onWillStart(() => new Promise(() => {}));
|
||||
try {
|
||||
class Test extends Component {
|
||||
static template = xml`<span/>`;
|
||||
setup() {
|
||||
onWillStart(() => new Promise(() => {}));
|
||||
}
|
||||
}
|
||||
mount(Test, fixture, { test: true });
|
||||
nextTick();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(logArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
||||
} finally {
|
||||
console.log = log;
|
||||
window.setTimeout = setTimeout;
|
||||
}
|
||||
mount(Test, fixture, { test: true });
|
||||
nextTick();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(warnArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
||||
console.warn = warn;
|
||||
window.setTimeout = setTimeout;
|
||||
});
|
||||
|
||||
test("timeout in onWillUpdateProps emits a warning", async () => {
|
||||
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;
|
||||
window.setTimeout = ((cb: any) => {
|
||||
timeoutCbs[++timeoutId] = cb;
|
||||
return timeoutId;
|
||||
}) as any;
|
||||
try {
|
||||
class Test extends Component {
|
||||
static template = xml`<span/>`;
|
||||
setup() {
|
||||
onWillStart(() => new Promise(() => {}));
|
||||
}
|
||||
}
|
||||
const app = new App(Test, { test: true });
|
||||
app.mount(fixture);
|
||||
app.destroy();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(console.log).toHaveBeenCalledTimes(0);
|
||||
} finally {
|
||||
console.log = log;
|
||||
window.setTimeout = setTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
test("timeout in onWillUpdateProps emits a console log", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml``;
|
||||
setup() {
|
||||
@@ -151,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;
|
||||
@@ -162,25 +204,28 @@ describe("lifecycle hooks", () => {
|
||||
return timeoutId;
|
||||
}) as any;
|
||||
|
||||
parent.state.prop = 2;
|
||||
let tick = nextTick();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
try {
|
||||
parent.state.prop = 2;
|
||||
let tick = nextTick();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await tick;
|
||||
tick = nextTick();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await tick;
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(logArgs![0]!.message).toBe(
|
||||
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
|
||||
);
|
||||
} finally {
|
||||
console.log = log;
|
||||
window.setTimeout = setTimeout;
|
||||
}
|
||||
await tick;
|
||||
tick = nextTick();
|
||||
for (const id in timeoutCbs) {
|
||||
timeoutCbs[id]();
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await tick;
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(warnArgs![0]!.message).toBe(
|
||||
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
|
||||
);
|
||||
console.warn = warn;
|
||||
window.setTimeout = setTimeout;
|
||||
});
|
||||
|
||||
test("mounted hook is called if mounted in DOM", async () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,6 +4,28 @@ All notable changes to the "owl-vision" extension will be documented in this fil
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
|
||||
## [0.1.0] - 2024-11-06
|
||||
|
||||
### Added
|
||||
|
||||
- Basic autocomplete in xml files. This includes autocompletion for elements, components,
|
||||
props, attributes, and javascript expressions.
|
||||
|
||||
The current implementation, while relatively simple, has a couple of drawbacks:
|
||||
- Javascript imports are not resolved by the xml autocomplete, this means that it does not
|
||||
understand the types of imported functions or objects. That said, I've added custom support
|
||||
for frequently used Owl imports, namely `useState` and `useRef`. You can add more in
|
||||
the settings if needed.
|
||||
- The autocomplete is limited to templates directly linked to components, sub-templates
|
||||
used via t-call will not get autocompletion as no component/context can be bound to them.
|
||||
|
||||
- "Go To Definition" support for props and javascript expressions in xml
|
||||
- Support for the following directives: t-att, t-model, t-tag, t-debug, t-log
|
||||
|
||||
### Fixed
|
||||
|
||||
- Changed t-else syntax highlight from dynamic to static attribute
|
||||
|
||||
## [0.0.2] - 2023-2-11
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+5557
-5527
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "Owl framework extension that highlights templates and ease navigation between components and templates.",
|
||||
"publisher": "Odoo",
|
||||
"license": "LGPL-3.0-only",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/odoo/owl/tree/master/tools/owl-vision"
|
||||
@@ -63,6 +63,13 @@
|
||||
"type": "string",
|
||||
"default": "**/node_modules/**,**/lib/**,**/tests/**",
|
||||
"description": "Files to exclude in search"
|
||||
},
|
||||
"owl-vision.autocomplete-mocks": {
|
||||
"order": 2,
|
||||
"type": "string",
|
||||
"editPresentation": "multilineText",
|
||||
"default": "/**\n* @template T\n* @param {T} obj\n* @returns {T}\n*/\nfunction useState(obj) {}\n\n/**\n* @typedef {Object} Ref\n* @property {HTMLElement} el\n*/\n/**\n* @returns {Ref}\n*/\nfunction useRef(name) {}",
|
||||
"description": "Mocks for functions or object that are imported but not resolved by the autcomplete. Add docstring comments for them to work properly."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -128,6 +135,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "20.2.5",
|
||||
"@types/vscode": "^1.73.0",
|
||||
"@types/xmldoc": "^1.1.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.59.8",
|
||||
"@vscode/test-electron": "^2.3.2",
|
||||
@@ -135,5 +143,8 @@
|
||||
"esbuild": "^0.19.5",
|
||||
"eslint": "^8.41.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"xmldoc": "^1.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ export const propsAttributes = createAttributePatterns("props-attributes", {
|
||||
export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dynamic", {
|
||||
match: [
|
||||
"t-if",
|
||||
"t-else",
|
||||
"t-elif",
|
||||
"t-foreach",
|
||||
"t-as",
|
||||
@@ -75,6 +74,10 @@ export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dyna
|
||||
"t-value",
|
||||
"t-portal",
|
||||
"t-slot-scope",
|
||||
"t-att",
|
||||
"t-tag",
|
||||
"t-log",
|
||||
"t-model",
|
||||
"t-att-[a-z_:.-]+",
|
||||
"t-on-[a-z_:.-]+"
|
||||
].join("|"),
|
||||
@@ -86,12 +89,13 @@ export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dyna
|
||||
export const owlAttributesStatic = createAttributePatterns("owl-attributes-static", {
|
||||
match: [
|
||||
"t-name",
|
||||
"t-else",
|
||||
"t-ref",
|
||||
"t-set-slot",
|
||||
"t-model",
|
||||
"t-inherit",
|
||||
"t-inherit-mode",
|
||||
"t-translation"
|
||||
"t-translation",
|
||||
"t-debug",
|
||||
].join("|"),
|
||||
attributeName: "owl.attribute owl.attribute.static",
|
||||
});
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { getSelectedText, showStatusMessage, hideStatusMessage } from './utils';
|
||||
import { Search } from './search';
|
||||
|
||||
export class ComponentDefinitionProvider implements vscode.DefinitionProvider {
|
||||
|
||||
search: Search;
|
||||
|
||||
constructor(search: Search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface implementation to provide definition when ctrl+click on Component
|
||||
* tag in template.
|
||||
*/
|
||||
async provideDefinition(document: vscode.TextDocument, position: vscode.Position) {
|
||||
const currentWord = getSelectedText(/<\/?[A-Z][a-zA-Z]+/, document, position);
|
||||
if (!currentWord) {
|
||||
return;
|
||||
}
|
||||
const componentName = currentWord.replace(/[\/<]/g, "").trim();
|
||||
|
||||
showStatusMessage(`Searching for component "${componentName}"`);
|
||||
const result = await this.search.findComponent(componentName);
|
||||
hideStatusMessage();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Search } from './search';
|
||||
import { ComponentDefinitionProvider } from './definiton_providers';
|
||||
import { OpenDirection } from './utils';
|
||||
import { OwlLanguageFeaturesProvider } from './language_features/language_features_provider';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const search = new Search();
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch', () => search.switch()));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-besides', () => search.switch(OpenDirection.Besides)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-below', () => search.switch(OpenDirection.Below)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-component', () => search.findComponentCommand()));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-template', () => search.findTemplateCommand()));
|
||||
|
||||
const componentDefProvider = new ComponentDefinitionProvider(search);
|
||||
context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'xml' }, componentDefProvider));
|
||||
context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'javascript' }, componentDefProvider));
|
||||
const languageFeaturesProvider = new OwlLanguageFeaturesProvider(search);
|
||||
context.subscriptions.push(vscode.languages.registerCompletionItemProvider({ language: 'xml', scheme: 'file' }, languageFeaturesProvider, '.', '<'));
|
||||
context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'xml', scheme: 'file' }, languageFeaturesProvider));
|
||||
}
|
||||
|
||||
export function deactivate() { }
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
import { CompletionItemKind } from "vscode"
|
||||
|
||||
export const owlComponentAttributes = [
|
||||
"t-if",
|
||||
"t-else",
|
||||
"t-elif",
|
||||
"t-foreach",
|
||||
"t-as",
|
||||
"t-key",
|
||||
"t-esc",
|
||||
"t-out",
|
||||
"t-props",
|
||||
"t-set",
|
||||
"t-value",
|
||||
"t-portal",
|
||||
"t-slot-scope",
|
||||
"t-log",
|
||||
].map(label => ({
|
||||
label: label,
|
||||
insertText: label + '=""',
|
||||
kind: CompletionItemKind.Property,
|
||||
}));
|
||||
|
||||
export const owlElementAttributes = [
|
||||
"t-component",
|
||||
"t-att",
|
||||
"t-tag",
|
||||
"t-model",
|
||||
].map(label => ({
|
||||
label: label,
|
||||
insertText: label + '=""',
|
||||
kind: CompletionItemKind.Property,
|
||||
}));
|
||||
|
||||
owlElementAttributes.push(...owlComponentAttributes);
|
||||
|
||||
/**
|
||||
To generate this list, run the following snippet on https://developer.mozilla.org/fr/docs/Web/Events
|
||||
|
||||
(function () {
|
||||
const events = [...document.querySelectorAll(".section-content li a")]
|
||||
.map(e => e.childNodes[0])
|
||||
.filter(n => n.nodeType === 3 && n.textContent.toLowerCase() == n.textContent)
|
||||
.map(n => "t-on-" + n.textContent.trim());
|
||||
return JSON.stringify([...new Set(events)], null, 2);
|
||||
}())
|
||||
*/
|
||||
export const events = [
|
||||
"t-on-abort",
|
||||
"t-on-ended",
|
||||
"t-on-addtrack",
|
||||
"t-on-change",
|
||||
"t-on-removetrack",
|
||||
"t-on-messageerror",
|
||||
"t-on-message",
|
||||
"t-on-animationcancel",
|
||||
"t-on-animationend",
|
||||
"t-on-animationiteration",
|
||||
"t-on-animationstart",
|
||||
"t-on-copy",
|
||||
"t-on-cut",
|
||||
"t-on-dragend",
|
||||
"t-on-dragenter",
|
||||
"t-on-dragleave",
|
||||
"t-on-dragover",
|
||||
"t-on-dragstart",
|
||||
"t-on-drag",
|
||||
"t-on-drop",
|
||||
"t-on-fullscreenchange",
|
||||
"t-on-fullscreenerror",
|
||||
"t-on-gotpointercapture",
|
||||
"t-on-keydown",
|
||||
"t-on-keypress",
|
||||
"t-on-keyup",
|
||||
"t-on-lostpointercapture",
|
||||
"t-on-paste",
|
||||
"t-on-pointercancel",
|
||||
"t-on-pointerdown",
|
||||
"t-on-pointerenter",
|
||||
"t-on-pointerleave",
|
||||
"t-on-pointerlockchange",
|
||||
"t-on-pointerlockerror",
|
||||
"t-on-pointermove",
|
||||
"t-on-pointerout",
|
||||
"t-on-pointerover",
|
||||
"t-on-pointerup",
|
||||
"t-on-readystatechange",
|
||||
"t-on-scroll",
|
||||
"t-on-selectionchange",
|
||||
"t-on-selectstart",
|
||||
"t-on-touchcancel",
|
||||
"t-on-touchend",
|
||||
"t-on-touchmove",
|
||||
"t-on-touchstart",
|
||||
"t-on-transitioncancel",
|
||||
"t-on-transitionend",
|
||||
"t-on-transitionrun",
|
||||
"t-on-transitionstart",
|
||||
"t-on-visibilitychange",
|
||||
"t-on-wheel",
|
||||
"t-on-afterscriptexecute",
|
||||
"t-on-auxclick",
|
||||
"t-on-beforescriptexecute",
|
||||
"t-on-blur",
|
||||
"t-on-click",
|
||||
"t-on-compositionend",
|
||||
"t-on-compositionstart",
|
||||
"t-on-compositionupdate",
|
||||
"t-on-contextmenu",
|
||||
"t-on-dblclick",
|
||||
"t-on-error",
|
||||
"t-on-focusin",
|
||||
"t-on-focusout",
|
||||
"t-on-focus",
|
||||
"t-on-gesturechange",
|
||||
"t-on-gestureend",
|
||||
"t-on-gesturestart",
|
||||
"t-on-mousedown",
|
||||
"t-on-mouseenter",
|
||||
"t-on-mouseleave",
|
||||
"t-on-mousemove",
|
||||
"t-on-mouseout",
|
||||
"t-on-mouseover",
|
||||
"t-on-mouseup",
|
||||
"t-on-mousewheel",
|
||||
"t-on-overflow",
|
||||
"t-on-select",
|
||||
"t-on-show",
|
||||
"t-on-underflow",
|
||||
"t-on-webkitmouseforcechanged",
|
||||
"t-on-webkitmouseforcedown",
|
||||
"t-on-webkitmouseforceup",
|
||||
"t-on-webkitmouseforcewillbegin",
|
||||
"t-on-open",
|
||||
"t-on-loadend",
|
||||
"t-on-loadstart",
|
||||
"t-on-load",
|
||||
"t-on-progress",
|
||||
"t-on-webglcontextcreationerror",
|
||||
"t-on-webglcontextlost",
|
||||
"t-on-webglcontextrestored",
|
||||
"t-on-toggle",
|
||||
"t-on-cancel",
|
||||
"t-on-close",
|
||||
"t-on-beforeinput",
|
||||
"t-on-input",
|
||||
"t-on-formdata",
|
||||
"t-on-reset",
|
||||
"t-on-submit",
|
||||
"t-on-invalid",
|
||||
"t-on-search",
|
||||
"t-on-canplaythrough",
|
||||
"t-on-canplay",
|
||||
"t-on-durationchange",
|
||||
"t-on-emptied",
|
||||
"t-on-loadeddata",
|
||||
"t-on-loadedmetadata",
|
||||
"t-on-pause",
|
||||
"t-on-playing",
|
||||
"t-on-play",
|
||||
"t-on-ratechange",
|
||||
"t-on-seeked",
|
||||
"t-on-seeking",
|
||||
"t-on-stalled",
|
||||
"t-on-suspend",
|
||||
"t-on-timeupdate",
|
||||
"t-on-volumechange",
|
||||
"t-on-waiting",
|
||||
"t-on-slotchange",
|
||||
"t-on-cuechange",
|
||||
"t-on-enterpictureinpicture",
|
||||
"t-on-leavepictureinpicture",
|
||||
"t-on-versionchange",
|
||||
"t-on-blocked",
|
||||
"t-on-upgradeneeded",
|
||||
"t-on-success",
|
||||
"t-on-complete",
|
||||
"t-on-devicechange",
|
||||
"t-on-mute",
|
||||
"t-on-unmute",
|
||||
"t-on-merchantvalidation",
|
||||
"t-on-paymentmethodchange",
|
||||
"t-on-shippingaddresschange",
|
||||
"t-on-shippingoptionchange",
|
||||
"t-on-payerdetailchange",
|
||||
"t-on-resourcetimingbufferfull",
|
||||
"t-on-resize",
|
||||
"t-on-bufferedamountlow",
|
||||
"t-on-closing",
|
||||
"t-on-tonechange",
|
||||
"t-on-gatheringstatechange",
|
||||
"t-on-selectedcandidatepairchange",
|
||||
"t-on-statechange",
|
||||
"t-on-addstream",
|
||||
"t-on-connectionstatechange",
|
||||
"t-on-datachannel",
|
||||
"t-on-icecandidateerror",
|
||||
"t-on-icecandidate",
|
||||
"t-on-iceconnectionstatechange",
|
||||
"t-on-icegatheringstatechange",
|
||||
"t-on-negotiationneeded",
|
||||
"t-on-removestream",
|
||||
"t-on-signalingstatechange",
|
||||
"t-on-track",
|
||||
"t-on-audioprocess",
|
||||
"t-on-activate",
|
||||
"t-on-contentdelete",
|
||||
"t-on-install",
|
||||
"t-on-notificationclick",
|
||||
"t-on-pushsubscriptionchange",
|
||||
"t-on-push",
|
||||
"t-on-connect",
|
||||
"t-on-audioend",
|
||||
"t-on-audiostart",
|
||||
"t-on-end",
|
||||
"t-on-nomatch",
|
||||
"t-on-result",
|
||||
"t-on-soundend",
|
||||
"t-on-soundstart",
|
||||
"t-on-speechend",
|
||||
"t-on-speechstart",
|
||||
"t-on-start",
|
||||
"t-on-voiceschanged",
|
||||
"t-on-boundary",
|
||||
"t-on-mark",
|
||||
"t-on-resume",
|
||||
"t-on-unload",
|
||||
"t-on-afterprint",
|
||||
"t-on-appinstalled",
|
||||
"t-on-beforeprint",
|
||||
"t-on-beforeunload",
|
||||
"t-on-devicemotion",
|
||||
"t-on-deviceorientation",
|
||||
"t-on-gamepadconnected",
|
||||
"t-on-gamepaddisconnected",
|
||||
"t-on-hashchange",
|
||||
"t-on-languagechange",
|
||||
"t-on-offline",
|
||||
"t-on-online",
|
||||
"t-on-orientationchange",
|
||||
"t-on-pagehide",
|
||||
"t-on-pageshow",
|
||||
"t-on-popstate",
|
||||
"t-on-rejectionhandled",
|
||||
"t-on-storage",
|
||||
"t-on-unhandledrejection",
|
||||
"t-on-vrdisplayactivate",
|
||||
"t-on-vrdisplayblur",
|
||||
"t-on-vrdisplayconnect",
|
||||
"t-on-vrdisplaydeactivate",
|
||||
"t-on-vrdisplaydisconnect",
|
||||
"t-on-vrdisplayfocus",
|
||||
"t-on-vrdisplaypointerrestricted",
|
||||
"t-on-vrdisplaypointerunrestricted",
|
||||
"t-on-vrdisplaypresentchange",
|
||||
"t-on-timeout",
|
||||
"t-on-inputsourceschange",
|
||||
"t-on-selectend",
|
||||
"t-on-squeezeend",
|
||||
"t-on-squeezestart",
|
||||
"t-on-squeeze"
|
||||
].map(label => ({
|
||||
label: label,
|
||||
insertText: label + '=""',
|
||||
kind: CompletionItemKind.Property,
|
||||
}))
|
||||
|
||||
|
||||
/**
|
||||
To generate the elements list, run the following snippet on https://developer.mozilla.org/en-US/docs/Web/HTML/Element
|
||||
|
||||
(function () {
|
||||
const names = [...document.querySelectorAll("section:not([aria-labelledby='obsolete_and_deprecated_elements']) td:nth-child(1)")]
|
||||
.flatMap(n => n.innerText.split(","))
|
||||
.map(n => n.trim().replace("<", "").replace(">", ""))
|
||||
return JSON.stringify([...new Set(names)], null, 2);
|
||||
})()
|
||||
*/
|
||||
export const elements = [
|
||||
"t",
|
||||
"link",
|
||||
"meta",
|
||||
"style",
|
||||
"title",
|
||||
"body",
|
||||
"address",
|
||||
"article",
|
||||
"aside",
|
||||
"footer",
|
||||
"header",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"hgroup",
|
||||
"main",
|
||||
"nav",
|
||||
"section",
|
||||
"search",
|
||||
"blockquote",
|
||||
"dd",
|
||||
"div",
|
||||
"dl",
|
||||
"dt",
|
||||
"figcaption",
|
||||
"figure",
|
||||
"hr",
|
||||
"li",
|
||||
"menu",
|
||||
"ol",
|
||||
"p",
|
||||
"pre",
|
||||
"ul",
|
||||
"a",
|
||||
"abbr",
|
||||
"b",
|
||||
"bdi",
|
||||
"bdo",
|
||||
"br",
|
||||
"cite",
|
||||
"code",
|
||||
"data",
|
||||
"dfn",
|
||||
"em",
|
||||
"i",
|
||||
"kbd",
|
||||
"mark",
|
||||
"q",
|
||||
"rp",
|
||||
"rt",
|
||||
"ruby",
|
||||
"s",
|
||||
"samp",
|
||||
"small",
|
||||
"span",
|
||||
"strong",
|
||||
"sub",
|
||||
"sup",
|
||||
"time",
|
||||
"u",
|
||||
"var",
|
||||
"wbr",
|
||||
"area",
|
||||
"audio",
|
||||
"img",
|
||||
"map",
|
||||
"track",
|
||||
"video",
|
||||
"embed",
|
||||
"iframe",
|
||||
"object",
|
||||
"picture",
|
||||
"portal",
|
||||
"source",
|
||||
"svg",
|
||||
"math",
|
||||
"canvas",
|
||||
"noscript",
|
||||
"script",
|
||||
"del",
|
||||
"ins",
|
||||
"caption",
|
||||
"col",
|
||||
"colgroup",
|
||||
"table",
|
||||
"tbody",
|
||||
"td",
|
||||
"tfoot",
|
||||
"th",
|
||||
"thead",
|
||||
"tr",
|
||||
"button",
|
||||
"datalist",
|
||||
"fieldset",
|
||||
"form",
|
||||
"input",
|
||||
"label",
|
||||
"legend",
|
||||
"meter",
|
||||
"optgroup",
|
||||
"option",
|
||||
"output",
|
||||
"progress",
|
||||
"select",
|
||||
"textarea",
|
||||
"details",
|
||||
"dialog",
|
||||
"summary",
|
||||
].map(label => ({
|
||||
label: label,
|
||||
insertText: label,
|
||||
kind: CompletionItemKind.Property,
|
||||
}));
|
||||
@@ -0,0 +1,422 @@
|
||||
import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, DefinitionProvider, Location, Position, Range, TextDocument, TextDocumentContentProvider, Uri, commands, workspace } from "vscode";
|
||||
import { Search } from "../search";
|
||||
import { getSelectedText, hash, readFile } from "../utils";
|
||||
import { elements, events, owlComponentAttributes, owlElementAttributes } from "./items";
|
||||
import { ParseResultType, ParseResult, parse, getNodePath, parseXml } from "./parser";
|
||||
|
||||
/**
|
||||
* Commands return basic js object which needs to be converted
|
||||
* to actual CompletionItem instances, this methods streamlines
|
||||
* this process.
|
||||
*/
|
||||
function mapCompletionItems(items: any): CompletionItem[] {
|
||||
return items.map((i: any) => {
|
||||
const item = new CompletionItem(i.label, i.kind);
|
||||
item.sortText = i.sortText;
|
||||
item.detail = i.detail;
|
||||
item.filterText = i.filterText;
|
||||
item.insertText = i.insertText?.startsWith?.(".") ? i.insertText.substring(1) : i.insertText;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
function filterComponentItems(items: CompletionItem[], excludedLabels: string[] = []): CompletionItem[] {
|
||||
return items.filter((item) => {
|
||||
return !excludedLabels.includes(item.label.toString()) && [
|
||||
CompletionItemKind.Field,
|
||||
CompletionItemKind.Method,
|
||||
CompletionItemKind.Variable,
|
||||
CompletionItemKind.Property,
|
||||
].includes(item.kind as number);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds "this." in front of the expression if needed and increments
|
||||
* the expression offset accordingly.
|
||||
*/
|
||||
function contextualize(properties: string[], expression: string, expressionOffset = 0) {
|
||||
const match = expression.match(/^([a-zA-Z_]+)\b/);
|
||||
if (!expression.startsWith("this.") && ((match && properties.includes(match[1])) || expression.match(/^\s*$/))) {
|
||||
expression = "this." + expression;
|
||||
expressionOffset += 5;
|
||||
}
|
||||
return { expression, expressionOffset };
|
||||
};
|
||||
|
||||
const Commands = {
|
||||
Completion: "vscode.executeCompletionItemProvider",
|
||||
Definition: "vscode.executeDefinitionProvider",
|
||||
}
|
||||
|
||||
export class OwlLanguageFeaturesProvider implements CompletionItemProvider, TextDocumentContentProvider, DefinitionProvider {
|
||||
|
||||
virtualDocuments = new Map();
|
||||
componentProperties = new Map();
|
||||
search: Search;
|
||||
|
||||
constructor(search: Search) {
|
||||
this.search = search;
|
||||
workspace.registerTextDocumentContentProvider("owl", this);
|
||||
}
|
||||
|
||||
/**
|
||||
* TextDocumentContentProvider interface implementation to provide
|
||||
* virtual documents to vscode commands.
|
||||
*/
|
||||
async provideTextDocumentContent(uri: Uri) {
|
||||
const id = uri.toString(true);
|
||||
return this.virtualDocuments.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* DefinitionProvider interface implementation.
|
||||
*
|
||||
* - If the target is a js expression, will try to find the definition
|
||||
* inside the current component.
|
||||
* - If the target is a component element, will try to find the definition
|
||||
* of the component.
|
||||
*/
|
||||
async provideDefinition(document: TextDocument, position: Position) {
|
||||
let offset = document.offsetAt(position);
|
||||
const documentText = document.getText();
|
||||
const parseResult = await parse(documentText, offset);
|
||||
|
||||
if (parseResult.type === ParseResultType.Expression) {
|
||||
const component = await this.search.getCurrentComponent();
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { xmlDocument, xmlNode } = parseXml(documentText, offset);
|
||||
const componentText = await readFile(component.uri);
|
||||
const virtualDocument = await this.getVirtualJsDocument(document.uri, component.componentName, componentText, xmlDocument, xmlNode, parseResult);
|
||||
|
||||
const definitions: any = await this.executeCommand(
|
||||
Commands.Definition,
|
||||
document.uri,
|
||||
virtualDocument.content,
|
||||
virtualDocument.offset
|
||||
);
|
||||
|
||||
if (definitions.length > 0) {
|
||||
const selectionRange = definitions[0].targetSelectionRange;
|
||||
const range = new Range(
|
||||
new Position(selectionRange.start.line, selectionRange.start.character),
|
||||
new Position(selectionRange.end.line, selectionRange.end.character),
|
||||
)
|
||||
return new Location(component.uri, range);
|
||||
}
|
||||
} else if (parseResult.type === ParseResultType.Attribute) {
|
||||
const { xmlNode } = parseXml(documentText, offset);
|
||||
|
||||
const childComponent = await this.search.findComponent(xmlNode.name);
|
||||
if (!childComponent) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const modifiersRegex = new RegExp([
|
||||
"\\.bind",
|
||||
"\\.stop",
|
||||
"\\.prevent",
|
||||
"\\.self",
|
||||
"\\.capture",
|
||||
"\\.sythetic",
|
||||
].join("|"), "g");
|
||||
|
||||
let attributeName = getSelectedText(/\b[a-zA-Z0-9_\-.]+\b/, document, position)
|
||||
attributeName = attributeName?.replace(modifiersRegex, "") ?? "";
|
||||
|
||||
const componentText = await readFile(childComponent.uri);
|
||||
const content = `${componentText}\n${xmlNode.name}.props.${attributeName}`;
|
||||
const definitions: any = await this.executeCommand(Commands.Definition, document.uri, content);
|
||||
|
||||
if (definitions.length > 0) {
|
||||
const selectionRange = definitions[0].targetSelectionRange;
|
||||
const range = new Range(
|
||||
new Position(selectionRange.start.line, selectionRange.start.character),
|
||||
new Position(selectionRange.end.line, selectionRange.end.character),
|
||||
)
|
||||
return new Location(childComponent.uri, range);
|
||||
}
|
||||
} else if (parseResult.type === ParseResultType.Element) {
|
||||
const currentWord = getSelectedText(/<\/?[A-Z][a-zA-Z]+/, document, position);
|
||||
if (!currentWord) {
|
||||
return;
|
||||
}
|
||||
const componentName = currentWord.replace(/[\/<]/g, "").trim();
|
||||
return await this.search.findComponent(componentName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CompletionItemProvider interface implementation
|
||||
*
|
||||
* See {@link provideElementItems}, {@link provideAttributeItems} and {@link provideExpressionItems}
|
||||
* for further details.
|
||||
*/
|
||||
async provideCompletionItems(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
token: CancellationToken,
|
||||
context: CompletionContext
|
||||
): Promise<CompletionItem[]> {
|
||||
const component = await this.search.getCurrentComponent();
|
||||
if (!component || token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const offset = document.offsetAt(position);
|
||||
const documentText = document.getText();
|
||||
|
||||
const parseResult = await parse(documentText, offset);
|
||||
const { xmlDocument, xmlNode } = parseXml(documentText, offset);
|
||||
|
||||
if (parseResult.type === ParseResultType.Expression) {
|
||||
return this.provideExpressionItems(document.uri, component.uri, component.componentName, xmlDocument, xmlNode, parseResult);
|
||||
} else if (parseResult.type === ParseResultType.Attribute) {
|
||||
return this.provideAttributeItems(document.uri, xmlNode);
|
||||
} else if (parseResult.type === ParseResultType.Element) {
|
||||
return this.provideElementItems(document.uri, component.uri, component.componentName, parseResult);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the completion items for attributes.
|
||||
* - Returns props if the element is a component
|
||||
* - Returns the owl directives based on the element type
|
||||
*/
|
||||
private async provideAttributeItems(
|
||||
documentUri: Uri,
|
||||
xmlNode: any,
|
||||
): Promise<CompletionItem[]> {
|
||||
if (!xmlNode || xmlNode.name === xmlNode.name.toLowerCase()) {
|
||||
return [...owlElementAttributes, ...events];
|
||||
}
|
||||
|
||||
const childComponent = await this.search.findComponent(xmlNode.name);
|
||||
if (!childComponent) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const componentText = await readFile(childComponent.uri);
|
||||
const content = `${componentText}\n${xmlNode.name}.props.`;
|
||||
const list = await this.executeCommand(Commands.Completion, documentUri, content) as CompletionList;
|
||||
|
||||
const modifiersRegex = new RegExp([
|
||||
"\\.bind",
|
||||
"\\.stop",
|
||||
"\\.prevent",
|
||||
"\\.self",
|
||||
"\\.capture",
|
||||
"\\.sythetic",
|
||||
].join("|"), "g");
|
||||
|
||||
const excludedAttrs = [
|
||||
"slots",
|
||||
...Object.keys(xmlNode.attr).map(attr => attr.replace(modifiersRegex, ""))
|
||||
];
|
||||
|
||||
return mapCompletionItems(filterComponentItems([
|
||||
...owlComponentAttributes,
|
||||
...list.items
|
||||
], excludedAttrs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the completion items for elements, this includes
|
||||
* components, "t" and html elements.
|
||||
*/
|
||||
private async provideElementItems(
|
||||
documentUri: Uri,
|
||||
componentUri: Uri,
|
||||
componentName: string,
|
||||
parseResult: any,
|
||||
): Promise<CompletionItem[]> {
|
||||
const componentText = await readFile(componentUri);
|
||||
|
||||
const content = `${componentText}\n${componentName}.components.${parseResult.expression}`;
|
||||
const list = await this.executeCommand(Commands.Completion, documentUri, content) as CompletionList;
|
||||
|
||||
return mapCompletionItems([
|
||||
...elements,
|
||||
...filterComponentItems(list.items),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the completion items for a js expression
|
||||
*/
|
||||
private async provideExpressionItems(
|
||||
documentUri: Uri,
|
||||
componentUri: Uri,
|
||||
componentName: string,
|
||||
xmlDocument: any,
|
||||
xmlNode: any,
|
||||
parseResult: ParseResult,
|
||||
): Promise<CompletionItem[]> {
|
||||
let { attributeName } = parseResult;
|
||||
|
||||
const dynamicAttributeRegex = new RegExp([
|
||||
"t-if",
|
||||
"t-elif",
|
||||
"t-foreach",
|
||||
"t-as",
|
||||
"t-key",
|
||||
"t-esc",
|
||||
"t-out",
|
||||
"t-props",
|
||||
"t-component",
|
||||
"t-set",
|
||||
"t-value",
|
||||
"t-portal",
|
||||
"t-slot-scope",
|
||||
"t-att",
|
||||
"t-tag",
|
||||
"t-log",
|
||||
"t-model",
|
||||
"t-att-[a-z_:.-]+",
|
||||
"t-on-[a-z_:.-]+"
|
||||
].join("|"));
|
||||
|
||||
if (xmlNode.name === xmlNode.name.toLowerCase() && !dynamicAttributeRegex.test(attributeName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const componentText = await readFile(componentUri);
|
||||
const virtualDocument = await this.getVirtualJsDocument(documentUri, componentName, componentText, xmlDocument, xmlNode, parseResult);
|
||||
|
||||
const completionList = await this.executeCommand(
|
||||
Commands.Completion,
|
||||
documentUri,
|
||||
virtualDocument.content,
|
||||
virtualDocument.offset
|
||||
) as CompletionList;
|
||||
|
||||
let items = filterComponentItems(completionList.items, ["__VIRTUAL__", "setup"]);
|
||||
|
||||
if (!/\bthis\./.test(parseResult.expression)) {
|
||||
items = items.map(item => {
|
||||
let insertText = item.insertText as string;
|
||||
if (/\bthis\./.test(insertText)) {
|
||||
item.insertText = insertText.replace(/\bthis\./, "");
|
||||
}
|
||||
return item;
|
||||
})
|
||||
}
|
||||
|
||||
return mapCompletionItems([...items]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a virtual document to provide the appropriate
|
||||
* completion items for a parsed js expression.
|
||||
*
|
||||
* This method:
|
||||
* - Adds default owl variables such as env and props
|
||||
* - Adds mocks for frequently used Owl imports (which cannot be resolved using commands)
|
||||
* - Adds local variables generated from Owl xml directives such as t-for or t-set
|
||||
* - Will try to add a "this." in front of the js expression if it was omitted so it can
|
||||
* be understood by vscode typescript server.
|
||||
*
|
||||
* The expression offset is also modified accordingly.
|
||||
*/
|
||||
async getVirtualJsDocument(
|
||||
documentUri: Uri,
|
||||
componentName: string,
|
||||
componentText: string,
|
||||
xmlDocument: any,
|
||||
xmlNode: any,
|
||||
parseResult: ParseResult,
|
||||
) {
|
||||
const properties = await this.getComponentProperties(documentUri, componentName, componentText);
|
||||
|
||||
// As imports do not work, use mocks for frequently used owl functions.
|
||||
let importReplacements = workspace.getConfiguration().get(`owl-vision.autocomplete-mocks`);
|
||||
|
||||
// As imports do not work, manually add "env" and "props"
|
||||
// to the current component instance.
|
||||
const localVariables = [
|
||||
"let env = {};",
|
||||
"this.env = env;",
|
||||
`let props = ${componentName}.props;`,
|
||||
`this.props = props;`,
|
||||
];
|
||||
|
||||
// Adds local variables generated based on the template
|
||||
const path = getNodePath(xmlDocument, xmlNode);
|
||||
for (const node of path) {
|
||||
if (node.attr["t-foreach"]) {
|
||||
let array = contextualize(properties, node.attr["t-foreach"]).expression;
|
||||
localVariables.push(`const ${node.attr["t-as"]} = ${array}[0];`);
|
||||
localVariables.push(`const ${node.attr["t-as"]}_index = 0;`);
|
||||
localVariables.push(`const ${node.attr["t-as"]}_first = ${array}[0];`);
|
||||
localVariables.push(`const ${node.attr["t-as"]}_last = ${array}.at(-1);`);
|
||||
localVariables.push(`const ${node.attr["t-as"]}_value = {};`);
|
||||
} else if (node.attr["t-set"]) {
|
||||
localVariables.push(`const ${node.attr["t-set"]} = ${node.attr["t-value"]};`);
|
||||
}
|
||||
}
|
||||
|
||||
const { expression, expressionOffset } = contextualize(properties, parseResult.expression, parseResult.expressionOffset);
|
||||
|
||||
return {
|
||||
offset: expressionOffset,
|
||||
content: `${componentText}
|
||||
${importReplacements}
|
||||
class __VIRTUAL__ extends ${componentName} { __VIRTUAL__() {
|
||||
${localVariables.join("\n")}
|
||||
${expression} }}`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of properties for a given component class.
|
||||
* The result in cached in `componentProperties`.
|
||||
*
|
||||
* @param documentUri
|
||||
* @param componentName
|
||||
* @param componentText
|
||||
* @returns
|
||||
*/
|
||||
async getComponentProperties(
|
||||
documentUri: Uri,
|
||||
componentName: string,
|
||||
componentText: string,
|
||||
): Promise<string[]> {
|
||||
const check = hash(componentText);
|
||||
|
||||
let cached = this.componentProperties.get(componentName);
|
||||
if (!cached || cached.check !== check) {
|
||||
const contextExpression = `${componentText}\nclass __VIRTUAL__ extends ${componentName} { __VIRTUAL__() { \nthis. }}`;
|
||||
const contextItemsCompletion = await this.executeCommand(Commands.Completion, documentUri, contextExpression, 5) as CompletionList;
|
||||
|
||||
const items = filterComponentItems(contextItemsCompletion.items, ["__VIRTUAL__", "setup"]).map(item => item.label);
|
||||
|
||||
cached = { check, items };
|
||||
this.componentProperties.set(componentName, cached);
|
||||
}
|
||||
|
||||
return cached.items;
|
||||
}
|
||||
|
||||
private async executeCommand(commandId: string, uri: Uri, content: string, offset: any = undefined) {
|
||||
const lines = content.split(/\r\n|\r|\n/);
|
||||
const _offset = offset !== undefined ? offset : (lines.at(-1)?.length ?? 0);
|
||||
const position = new Position(lines.length - 1, _offset);
|
||||
|
||||
const originalUri = uri.toString(true);
|
||||
const hashValue = hash(content);
|
||||
|
||||
const id = `owl://js/${originalUri}_${hashValue}.js`;
|
||||
this.virtualDocuments.set(id, content);
|
||||
|
||||
return await commands.executeCommand(
|
||||
commandId,
|
||||
Uri.parse(`owl://js/${encodeURIComponent(originalUri)}_${hashValue}.js`),
|
||||
position
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { XmlDocument, XmlElement, XmlNode } from "xmldoc";
|
||||
|
||||
export enum ParseResultType {
|
||||
Expression,
|
||||
Attribute,
|
||||
Element
|
||||
}
|
||||
|
||||
export interface ParseResult {
|
||||
type: ParseResultType;
|
||||
expression: string;
|
||||
expressionOffset: number;
|
||||
attributeName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Will parse the document to find the selected expression based on an offset.
|
||||
* The result can be on of three types:
|
||||
*
|
||||
* Element: The offset is on a element tag name, the expression is the current
|
||||
* tagname or and empty string if it's just a opening tag.
|
||||
*
|
||||
* Attribute: The offset is inside the element but not in an attribute value,
|
||||
* the expression is the current attribute name if any.
|
||||
*
|
||||
* Expression: The offset is inside an attribute value, the expression is the value.
|
||||
*/
|
||||
export async function parse(
|
||||
documentText: string,
|
||||
offset: number,
|
||||
): Promise<ParseResult> {
|
||||
|
||||
// Check if the offset is preceded by "<xyz", if true returns a type Element
|
||||
// with the current name.
|
||||
const elementMatch = documentText.substring(0, offset).match(/<([a-zA-Z\-._]*)$/);
|
||||
if (elementMatch) {
|
||||
return {
|
||||
type: ParseResultType.Element,
|
||||
expression: elementMatch[1] || "",
|
||||
expressionOffset: elementMatch[1].length || 0,
|
||||
attributeName: "",
|
||||
};
|
||||
}
|
||||
|
||||
let {
|
||||
value: expression,
|
||||
offset: expressionOffset,
|
||||
from,
|
||||
} = getSection(documentText, offset, '="', '"');
|
||||
|
||||
// If the expression contains '"', it means we aren't inside an attribute
|
||||
// value.
|
||||
if (expression.includes('"')) {
|
||||
const attributeMatch = documentText.substring(0, offset).match(/\s([a-zA-Z\-._]*)$/);
|
||||
if (attributeMatch) {
|
||||
return {
|
||||
type: ParseResultType.Attribute,
|
||||
expression: attributeMatch[1] || "",
|
||||
expressionOffset: attributeMatch[1].length || 0,
|
||||
attributeName: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let attributeName = "";
|
||||
let i = from - 2;
|
||||
while (/\S/.test(documentText[i])) {
|
||||
attributeName = documentText[i] + attributeName;
|
||||
i--;
|
||||
}
|
||||
|
||||
return {
|
||||
type: ParseResultType.Expression,
|
||||
expression,
|
||||
expressionOffset,
|
||||
attributeName,
|
||||
};
|
||||
}
|
||||
|
||||
export function getSection(text: string, offset: number, prefix: string, postfix: string) {
|
||||
const beforeText = text.substring(0, offset);
|
||||
let from = beforeText.lastIndexOf(prefix);
|
||||
const afterText = text.substring(offset);
|
||||
const to = beforeText.length + afterText.indexOf(postfix);
|
||||
|
||||
from = from + (prefix.length);
|
||||
|
||||
return {
|
||||
value: text.substring(from, to),
|
||||
offset: offset - from,
|
||||
from: from,
|
||||
to,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 2 xml nodes:
|
||||
* xmlNode: Tries to create the current element based on a string offset,
|
||||
* even if the node is invalid.
|
||||
* xmlDocument: The document root element, only works if the document is
|
||||
* valid xml.
|
||||
*/
|
||||
export function parseXml(text: string, offset: number): any {
|
||||
let xmlDocument = undefined;
|
||||
try {
|
||||
xmlDocument = new XmlDocument(text);
|
||||
} catch (error) { }
|
||||
|
||||
let i = 0;
|
||||
while (text[offset + i] !== "<") {
|
||||
i--;
|
||||
}
|
||||
|
||||
let node = "";
|
||||
while (text[offset + i] !== ">" || text[offset + i - 1] === "=") {
|
||||
node += text[offset + i];
|
||||
i++;
|
||||
}
|
||||
|
||||
let xmlNode = undefined;
|
||||
try {
|
||||
xmlNode = new XmlDocument(`${node}${node.endsWith("/") ? '' : '/'}>`);
|
||||
} catch (error) { }
|
||||
|
||||
return { xmlDocument, xmlNode };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array representing the elements order from the document's
|
||||
* root to the specified element.
|
||||
*/
|
||||
export function getNodePath(xmlDocument: XmlDocument, xmlNode: any): Array<XmlElement> {
|
||||
let path: Array<XmlElement> = [];
|
||||
|
||||
const traverse = (node: XmlElement, currentPath: Array<XmlElement>) => {
|
||||
if (node.name === xmlNode.name && JSON.stringify(node.attr) === JSON.stringify(xmlNode.attr)) {
|
||||
path = currentPath;
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
if (child instanceof XmlElement && child.name) {
|
||||
traverse(child, currentPath.concat(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(xmlDocument, []);
|
||||
|
||||
return path;
|
||||
}
|
||||
@@ -94,6 +94,31 @@ export class Search {
|
||||
return await this.find(templateName, query, "xml");
|
||||
}
|
||||
|
||||
public async getCurrentComponent(): Promise<any | undefined> {
|
||||
if (!this.currentDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = this.currentDocument.getText();
|
||||
const templateName = this.getTemplateName(text, false);
|
||||
|
||||
if (templateName) {
|
||||
const component = await this.findComponentFromTemplateName(templateName);
|
||||
|
||||
if (component) {
|
||||
const componentFile = await workspace.fs.readFile(component.uri);
|
||||
const componentText = Buffer.from(componentFile).toString('utf8');
|
||||
const componentName = this.getComponentName(componentText, templateName);
|
||||
|
||||
return {
|
||||
uri: component.uri,
|
||||
templateName,
|
||||
componentName,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private findComponentFromTemplateName(templateName: string): Promise<Location | undefined> {
|
||||
const query = this.buildQuery(`template\\s*=\\s*["']`, templateName, `["']`);
|
||||
return this.find(templateName, query, "js");
|
||||
@@ -107,6 +132,27 @@ export class Search {
|
||||
}
|
||||
}
|
||||
|
||||
private getComponentName(str: string, templateName: string): string {
|
||||
const templateNameRegex = new RegExp(`template\\s*=\\s*["'](${templateName})["']`, 'g');
|
||||
const templateIndex = [...str.matchAll(templateNameRegex)][0]?.index ?? 0;
|
||||
|
||||
const matches = [...str.matchAll(new RegExp(`class\\s+([A-Za-z_]+)\\sextends\\s+[A-Za-z_]+`, 'g'))];
|
||||
let result = "";
|
||||
let currentIndex = -1;
|
||||
for (const match of matches) {
|
||||
if (match.index > templateIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match.index > currentIndex) {
|
||||
result = match[1];
|
||||
currentIndex = match.index;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async find(
|
||||
name: string,
|
||||
searchQuery: string,
|
||||
|
||||
@@ -102,3 +102,20 @@ export async function showResult(result: vscode.Location, openDirection: OpenDir
|
||||
editor.revealRange(result.range);
|
||||
editor.selection = new vscode.Selection(result.range.start, result.range.end);
|
||||
}
|
||||
|
||||
export async function readFile(uri: vscode.Uri): Promise<string> {
|
||||
const data = await vscode.workspace.fs.readFile(uri);
|
||||
return Buffer.from(data).toString('utf8');
|
||||
}
|
||||
|
||||
export function hash(str: string) {
|
||||
var hash = 0,
|
||||
i, chr;
|
||||
if (str.length === 0) return hash;
|
||||
for (i = 0; i < str.length; i++) {
|
||||
chr = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
"patterns": [
|
||||
{
|
||||
"contentName": "meta.embedded.block.javascript string.quoted.double.xml",
|
||||
"begin": "(\\s*)(t-if|t-else|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(\")",
|
||||
"begin": "(\\s*)(t-if|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att|t-tag|t-log|t-model|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(\")",
|
||||
"beginCaptures": {
|
||||
"2": {
|
||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
||||
@@ -183,7 +183,7 @@
|
||||
},
|
||||
{
|
||||
"contentName": "meta.embedded.block.javascript string.quoted.single.xml",
|
||||
"begin": "(\\s*)(t-if|t-else|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(')",
|
||||
"begin": "(\\s*)(t-if|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att|t-tag|t-log|t-model|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(')",
|
||||
"beginCaptures": {
|
||||
"2": {
|
||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
||||
@@ -210,7 +210,7 @@
|
||||
"patterns": [
|
||||
{
|
||||
"contentName": "string.quoted.double.xml",
|
||||
"begin": "(\\s*)(t-name|t-ref|t-set-slot|t-model|t-inherit|t-inherit-mode|t-translation)(=)(\")",
|
||||
"begin": "(\\s*)(t-name|t-else|t-ref|t-set-slot|t-inherit|t-inherit-mode|t-translation|t-debug)(=)(\")",
|
||||
"beginCaptures": {
|
||||
"2": {
|
||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"contentName": "string.quoted.single.xml",
|
||||
"begin": "(\\s*)(t-name|t-ref|t-set-slot|t-model|t-inherit|t-inherit-mode|t-translation)(=)(')",
|
||||
"begin": "(\\s*)(t-name|t-else|t-ref|t-set-slot|t-inherit|t-inherit-mode|t-translation|t-debug)(=)(')",
|
||||
"beginCaptures": {
|
||||
"2": {
|
||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
||||
|
||||
@@ -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