mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5329558a1d |
@@ -140,28 +140,6 @@ class SomeComponent extends Component {
|
||||
The `.bind` suffix also implies `.alike`, so these props will not cause additional
|
||||
renderings.
|
||||
|
||||
## Translatable props
|
||||
|
||||
When you need to pass a user-facing string to a subcomponent, you likely want it
|
||||
to be translated. Unfortunately, because props are arbitrary expressions, it wouldn't
|
||||
be practical for Owl to find out which parts of the expression are strings and translate
|
||||
them, and it also makes it difficult for tooling to extract these strings to generate
|
||||
terms to translate. While you can work around this issue by doing the translation in
|
||||
JavaScript, or by using `t-set` with a body (the body of `t-set` is translated),
|
||||
and passing the variable as a prop, this is a sufficiently common use case that Owl
|
||||
provides a suffix for this purpose: `.translate`.
|
||||
|
||||
```xml
|
||||
<t t-name="ParentComponent">
|
||||
<Child someProp.translate="some message"/>
|
||||
</t>
|
||||
```
|
||||
|
||||
Note that the content of this attribute is _NOT_ treated as a JavaScript expression:
|
||||
it is treated as a string, as if it was an attribute on an HTML element, and translated
|
||||
before being passed to the component. If you need to interpolate some data into the
|
||||
string, you will still have to do this in JavaScript.
|
||||
|
||||
## Dynamic Props
|
||||
|
||||
The `t-props` directive can be used to specify totally dynamic props:
|
||||
|
||||
@@ -201,17 +201,16 @@ use this `Notebook` component:
|
||||
|
||||
```xml
|
||||
<Notebook>
|
||||
<t t-set-slot="page1" title.translate="Page 1">
|
||||
<t t-set-slot="page1" title="'Page 1'">
|
||||
<div>this is in the page 1</div>
|
||||
</t>
|
||||
<t t-set-slot="page2" title.translate="Page 2" hidden="somevalue">
|
||||
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
|
||||
<div>this is in the page 2</div>
|
||||
</t>
|
||||
</Notebook>
|
||||
```
|
||||
|
||||
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
|
||||
Slot params works like normal props, so one can use the `.bind` suffix to
|
||||
bind a function if needed.
|
||||
|
||||
## Slot scopes
|
||||
|
||||
+5
-11
@@ -2623,7 +2623,7 @@ function wrapError(fn, hookName) {
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||
console.log(timeoutError);
|
||||
console.warn(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4594,12 +4594,7 @@ class CodeGenerator {
|
||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||
*/
|
||||
formatProp(name, value) {
|
||||
if (name.endsWith(".translate")) {
|
||||
value = toStringExpression(this.translateFn(value));
|
||||
}
|
||||
else {
|
||||
value = this.captureExpression(value);
|
||||
}
|
||||
value = this.captureExpression(value);
|
||||
if (name.includes(".")) {
|
||||
let [_name, suffix] = name.split(".");
|
||||
name = _name;
|
||||
@@ -4608,7 +4603,6 @@ class CodeGenerator {
|
||||
value = `(${value}).bind(this)`;
|
||||
break;
|
||||
case "alike":
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
@@ -5552,7 +5546,7 @@ function compile(template, options = {}) {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.3.1";
|
||||
const version = "2.2.11";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -5981,6 +5975,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
||||
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-08-14T14:25:23.038Z';
|
||||
__info__.hash = '9c2d957';
|
||||
__info__.date = '2024-06-17T13:31:12.099Z';
|
||||
__info__.hash = 'e7f405c';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
Generated
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.3.1",
|
||||
"version": "2.2.11",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -2484,9 +2484,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true
|
||||
},
|
||||
"for-each": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.3.1",
|
||||
"version": "2.2.11",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
|
||||
@@ -1136,11 +1136,7 @@ export class CodeGenerator {
|
||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||
*/
|
||||
formatProp(name: string, value: string): string {
|
||||
if (name.endsWith(".translate")) {
|
||||
value = toStringExpression(this.translateFn(value));
|
||||
} else {
|
||||
value = this.captureExpression(value);
|
||||
}
|
||||
value = this.captureExpression(value);
|
||||
if (name.includes(".")) {
|
||||
let [_name, suffix] = name.split(".");
|
||||
name = _name;
|
||||
@@ -1149,7 +1145,6 @@ export class CodeGenerator {
|
||||
value = `(${value}).bind(this)`;
|
||||
break;
|
||||
case "alike":
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
|
||||
@@ -29,7 +29,7 @@ function wrapError(fn: (...args: any[]) => any, hookName: string) {
|
||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||
]).then((res) => {
|
||||
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||
console.log(timeoutError);
|
||||
console.warn(timeoutError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.3.1";
|
||||
export const version = "2.2.11";
|
||||
|
||||
@@ -683,7 +683,7 @@ exports[`lifecycle hooks sub widget (inside sub node): hooks are correctly calle
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillStart doesn't emit a console log if app is destroyed 1`] = `
|
||||
exports[`lifecycle hooks timeout in onWillStart doesn't emit a warning if app is destroyed 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -696,7 +696,7 @@ exports[`lifecycle hooks timeout in onWillStart doesn't emit a console log if ap
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillStart emits a console log 1`] = `
|
||||
exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -709,7 +709,7 @@ exports[`lifecycle hooks timeout in onWillStart emits a console log 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a console log 1`] = `
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
@@ -723,7 +723,7 @@ exports[`lifecycle hooks timeout in onWillUpdateProps emits a console log 1`] =
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a console log 2`] = `
|
||||
exports[`lifecycle hooks timeout in onWillUpdateProps emits a warning 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
@@ -66,29 +66,6 @@ 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
|
||||
) {
|
||||
@@ -435,29 +412,6 @@ 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
|
||||
) {
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
// 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
|
||||
) {
|
||||
@@ -226,31 +201,6 @@ 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
|
||||
) {
|
||||
|
||||
@@ -112,10 +112,10 @@ describe("lifecycle hooks", () => {
|
||||
await mount(Test, fixture);
|
||||
});
|
||||
|
||||
test("timeout in onWillStart emits a console log", async () => {
|
||||
const { log } = console;
|
||||
let logArgs: any[];
|
||||
console.log = jest.fn((...args) => (logArgs = args));
|
||||
test("timeout in onWillStart emits a warning", async () => {
|
||||
const { warn } = console;
|
||||
let warnArgs: any[];
|
||||
console.warn = jest.fn((...args) => (warnArgs = args));
|
||||
const { setTimeout } = window;
|
||||
let timeoutCbs: any = {};
|
||||
let timeoutId = 0;
|
||||
@@ -138,17 +138,17 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(logArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(warnArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
||||
} finally {
|
||||
console.log = log;
|
||||
console.warn = warn;
|
||||
window.setTimeout = setTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
test("timeout in onWillStart doesn't emit a console log if app is destroyed", async () => {
|
||||
const { log } = console;
|
||||
console.log = jest.fn();
|
||||
test("timeout in onWillStart doesn't emit a warning if app is destroyed", async () => {
|
||||
const { warn } = console;
|
||||
console.warn = jest.fn();
|
||||
const { setTimeout } = window;
|
||||
let timeoutCbs: any = {};
|
||||
let timeoutId = 0;
|
||||
@@ -172,14 +172,14 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
await nextMicroTick();
|
||||
await nextMicroTick();
|
||||
expect(console.log).toHaveBeenCalledTimes(0);
|
||||
expect(console.warn).toHaveBeenCalledTimes(0);
|
||||
} finally {
|
||||
console.log = log;
|
||||
console.warn = warn;
|
||||
window.setTimeout = setTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
test("timeout in onWillUpdateProps emits a console log", async () => {
|
||||
test("timeout in onWillUpdateProps emits a warning", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml``;
|
||||
setup() {
|
||||
@@ -193,9 +193,9 @@ describe("lifecycle hooks", () => {
|
||||
}
|
||||
const parent = await mount(Parent, fixture, { test: true });
|
||||
|
||||
const { log } = console;
|
||||
let logArgs: any[];
|
||||
console.log = jest.fn((...args) => (logArgs = args));
|
||||
const { warn } = console;
|
||||
let warnArgs: any[];
|
||||
console.warn = jest.fn((...args) => (warnArgs = args));
|
||||
const { setTimeout } = window;
|
||||
let timeoutCbs: any = {};
|
||||
let timeoutId = 0;
|
||||
@@ -218,12 +218,12 @@ describe("lifecycle hooks", () => {
|
||||
delete timeoutCbs[id];
|
||||
}
|
||||
await tick;
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(logArgs![0]!.message).toBe(
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(warnArgs![0]!.message).toBe(
|
||||
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
|
||||
);
|
||||
} finally {
|
||||
console.log = log;
|
||||
console.warn = warn;
|
||||
window.setTimeout = setTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -299,34 +299,6 @@ 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,34 +179,6 @@ 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 {
|
||||
|
||||
Reference in New Issue
Block a user