mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 409431c4ad |
@@ -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
|
||||
|
||||
+4
-10
@@ -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.0";
|
||||
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-07-25T13:13:44.371Z';
|
||||
__info__.hash = '0cde4b8';
|
||||
__info__.date = '2024-06-17T13:31:12.099Z';
|
||||
__info__.hash = 'e7f405c';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
@@ -99,11 +99,6 @@ const SAMPLES = [
|
||||
folder: "todo_app",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
{
|
||||
description: "Tic-Tac-Toe (with reactivity)",
|
||||
folder: "tic_tac_toe",
|
||||
code: ["js", "xml", "css"],
|
||||
},
|
||||
{
|
||||
description: "Responsive app",
|
||||
folder: "responsive_app",
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
.square {
|
||||
background: #fff;
|
||||
border: 1px solid #999;
|
||||
float: left;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 34px;
|
||||
height: 34px;
|
||||
margin-right: -1px;
|
||||
margin-top: -1px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 34px;
|
||||
}
|
||||
|
||||
.board-row:after {
|
||||
clear: both;
|
||||
content: '';
|
||||
display: table;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.game {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.game-info {
|
||||
margin-left: 20px;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// This example is an implementation of the Tic-Tac-Toe game, from
|
||||
// https://react.dev/learn/tutorial-tic-tac-toe. This is an easy application to start learning owl
|
||||
// with some interesting user interactions.
|
||||
//
|
||||
// In this implementation, we use the owl reactivity mechanism.
|
||||
import { Component, useState, mount } from "@odoo/owl";
|
||||
|
||||
|
||||
class Square extends Component {
|
||||
static template = "Square";
|
||||
}
|
||||
|
||||
class Board extends Component {
|
||||
static template = "Board"
|
||||
static components = { Square };
|
||||
|
||||
handleClick(i) {
|
||||
if (this.calculateWinner(this.props.squares) || this.props.squares[i]) {
|
||||
return;
|
||||
}
|
||||
const nextSquares = this.props.squares.slice();
|
||||
if (this.props.xIsNext) {
|
||||
nextSquares[i] = 'X';
|
||||
} else {
|
||||
nextSquares[i] = 'O';
|
||||
}
|
||||
this.props.onPlay(nextSquares);
|
||||
}
|
||||
|
||||
get status(){
|
||||
const winner = this.calculateWinner(this.props.squares);
|
||||
if (winner) {
|
||||
return 'Winner: ' + winner;
|
||||
} else {
|
||||
if (Object.values(this.props.squares).filter((v) => v === null).length > 0)
|
||||
return 'Next player: ' + (this.props.xIsNext ? 'X' : 'O');
|
||||
else
|
||||
return 'Draw';
|
||||
}
|
||||
}
|
||||
|
||||
calculateWinner(squares) {
|
||||
const lines = [
|
||||
[0, 1, 2],
|
||||
[3, 4, 5],
|
||||
[6, 7, 8],
|
||||
[0, 3, 6],
|
||||
[1, 4, 7],
|
||||
[2, 5, 8],
|
||||
[0, 4, 8],
|
||||
[2, 4, 6],
|
||||
];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const [a, b, c] = lines[i];
|
||||
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
|
||||
return squares[a];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class Game extends Component {
|
||||
static template = "Game"
|
||||
static components = { Board };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
currentMove: 0,
|
||||
history: [Array(9).fill(null)],
|
||||
});
|
||||
}
|
||||
|
||||
get currentSquares() {
|
||||
return this.state.history[this.state.currentMove];
|
||||
}
|
||||
|
||||
get xIsNext() {
|
||||
return this.state.currentMove % 2 === 0;
|
||||
}
|
||||
|
||||
jumpTo(nextMove) {
|
||||
this.state.currentMove = nextMove;
|
||||
}
|
||||
|
||||
handlePlay(nextSquares) {
|
||||
const nextHistory = [...this.state.history.slice(0, this.state.currentMove + 1), nextSquares];
|
||||
this.state.history = nextHistory;
|
||||
this.state.currentMove = this.state.history.length - 1;
|
||||
}
|
||||
|
||||
get moves() {
|
||||
return this.state.history.map((_squares, move) => {
|
||||
if (move > 0) {
|
||||
return {id: move, description: 'Go to move #' + move};
|
||||
} else {
|
||||
return {id: move, description: 'Go to game start'};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Application setup
|
||||
mount(Game, document.body, { templates: TEMPLATES, dev: true});
|
||||
@@ -1,43 +0,0 @@
|
||||
<templates>
|
||||
<button t-name="Square" class="square" t-on-click="props.onSquareClick">
|
||||
<t t-esc="props.value"/>
|
||||
</button>
|
||||
|
||||
<t t-name="Board">
|
||||
<div class="status">
|
||||
<t t-esc="status"/>
|
||||
</div>
|
||||
<div class="board-row">
|
||||
<Square value="props.squares[0]" onSquareClick="() => this.handleClick(0)" />
|
||||
<Square value="props.squares[1]" onSquareClick="() => this.handleClick(1)" />
|
||||
<Square value="props.squares[2]" onSquareClick="() => this.handleClick(2)" />
|
||||
</div>
|
||||
<div class="board-row">
|
||||
<Square value="props.squares[3]" onSquareClick="() => this.handleClick(3)" />
|
||||
<Square value="props.squares[4]" onSquareClick="() => this.handleClick(4)" />
|
||||
<Square value="props.squares[5]" onSquareClick="() => this.handleClick(5)" />
|
||||
</div>
|
||||
<div class="board-row">
|
||||
<Square value="props.squares[6]" onSquareClick="() => this.handleClick(6)" />
|
||||
<Square value="props.squares[7]" onSquareClick="() => this.handleClick(7)" />
|
||||
<Square value="props.squares[8]" onSquareClick="() => this.handleClick(8)" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div t-name="Game" class="game">
|
||||
<div class="game-board">
|
||||
<Board xIsNext="xIsNext" squares="currentSquares" onPlay.bind="handlePlay" />
|
||||
</div>
|
||||
<div class="game-info">
|
||||
<ol>
|
||||
<t t-foreach="moves" t-as="move" t-key="move.id">
|
||||
<li>
|
||||
<button t-on-click="() => this.jumpTo(move.id)">
|
||||
<t t-esc="move.description"/>
|
||||
</button>
|
||||
</li>
|
||||
</t>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
Generated
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.3.0",
|
||||
"version": "2.2.11",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -5925,9 +5925,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"xml-name-validator": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.3.0",
|
||||
"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");
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.3.0";
|
||||
export const version = "2.2.11";
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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