mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] component: support dynamic t-props
It is useful in some rare situations. closes #144
This commit is contained in:
@@ -40,7 +40,7 @@ class Counter extends owl.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class App extends owl.Component {
|
class App extends owl.Component {
|
||||||
static components = { Counter };
|
static components = { Counter };
|
||||||
}
|
}
|
||||||
|
|
||||||
const qweb = new owl.QWeb(TEMPLATES);
|
const qweb = new owl.QWeb(TEMPLATES);
|
||||||
@@ -69,7 +69,7 @@ requirements are common and code needs to be maintained by large teams.
|
|||||||
it is not ok to use standard web tools based on `npm`.
|
it is not ok to use standard web tools based on `npm`.
|
||||||
|
|
||||||
Owl is not designed to be fast nor small (even though it is quite good on those
|
Owl is not designed to be fast nor small (even though it is quite good on those
|
||||||
two topics). It is a no nonsense framework to build applications. There is only
|
two topics). It is a no nonsense framework to build applications. There is only
|
||||||
one way to define components (with classes).
|
one way to define components (with classes).
|
||||||
|
|
||||||
If you are interested in a comparison with React or Vue, you will
|
If you are interested in a comparison with React or Vue, you will
|
||||||
|
|||||||
+4
-4
@@ -31,11 +31,11 @@ than React and Vue. Also, jQuery is not the same kind of framework, but it is in
|
|||||||
|
|
||||||
## Class Based
|
## Class Based
|
||||||
|
|
||||||
Both React and Vue moved away from defining components with classes. They prefer
|
Both React and Vue moved away from defining components with classes. They prefer
|
||||||
a more functional approach, in particular, with the new `hooks` mechanisms.
|
a more functional approach, in particular, with the new `hooks` mechanisms.
|
||||||
|
|
||||||
This has some advantages and disadvantages. But the end result is that React
|
This has some advantages and disadvantages. But the end result is that React
|
||||||
and Vue both offers multiple different ways of defining new components. In
|
and Vue both offers multiple different ways of defining new components. In
|
||||||
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
||||||
components are fast enough for all our usecases, and making it as simple as
|
components are fast enough for all our usecases, and making it as simple as
|
||||||
possible for developers is more valuable (for us).
|
possible for developers is more valuable (for us).
|
||||||
@@ -233,4 +233,4 @@ class Counter extends owl.ConnectedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const counter = new Counter({ store, qweb });
|
const counter = new Counter({ store, qweb });
|
||||||
```
|
```
|
||||||
|
|||||||
+18
-2
@@ -119,7 +119,7 @@ find a template with the component name (or one of its ancestor).
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
class ParentComponent extends owl.Component {
|
class ParentComponent extends owl.Component {
|
||||||
static components = { SubComponent };
|
static components = { SubComponent };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -471,7 +471,8 @@ the sub component will also be updated automatically.
|
|||||||
Note that there are some restrictions on prop names: `class`, `style` and any
|
Note that there are some restrictions on prop names: `class`, `style` and any
|
||||||
string which starts with `t-` are not allowed.
|
string which starts with `t-` are not allowed.
|
||||||
|
|
||||||
The `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
|
It is not common, but sometimes we need a dynamic component name and/or dynamic props. In this case,
|
||||||
|
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb.md#dynamic-attributes) directive):
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<div t-name="ParentComponent">
|
<div t-name="ParentComponent">
|
||||||
@@ -486,6 +487,21 @@ class ParentComponent {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
And the `t-props` directive can be used to specify totally dynamic props:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<Child t-props="some.obj"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent {
|
||||||
|
static components = { Child };
|
||||||
|
some = { obj: { a: 1, b: 2 } };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**CSS and style:** there is some specific support to allow the parent to declare
|
**CSS and style:** there is some specific support to allow the parent to declare
|
||||||
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
||||||
root component element.
|
root component element.
|
||||||
|
|||||||
+11
-11
@@ -65,16 +65,16 @@ We present here a list of all standard QWeb directives:
|
|||||||
The component system in Owl requires additional directives, to express various
|
The component system in Owl requires additional directives, to express various
|
||||||
needs. Here is a list of all Owl specific directives:
|
needs. Here is a list of all Owl specific directives:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
|
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||||
| `t-component`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
| `t-component`, `t-props`, `t-keepalive`, `t-asyncroot` | [Defining a sub component](component.md#composition) |
|
||||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
| `t-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
|
||||||
| `t-on-*` | [Event handling](component.md#event-handling) |
|
| `t-on-*` | [Event handling](component.md#event-handling) |
|
||||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||||
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
| `t-mounted` | [Callback when a node or component is mounted](component.md#t-mounted-directive) |
|
||||||
| `t-slot` | [Rendering a slot](component.md#slots) |
|
| `t-slot` | [Rendering a slot](component.md#slots) |
|
||||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||||
|
|
||||||
## QWeb Engine
|
## QWeb Engine
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ It's API is quite simple:
|
|||||||
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
||||||
```
|
```
|
||||||
|
|
||||||
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
|
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply return whenever a template is added for a second time. Otherwise, `QWeb` will crash.
|
||||||
|
|
||||||
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
||||||
attribute).
|
attribute).
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
## Owl Content
|
## Owl Content
|
||||||
|
|
||||||
Owl is a javascript library that contains some core classes and function to help
|
Owl is a javascript library that contains some core classes and function to help
|
||||||
build applications. Here is a complete representation of its content:
|
build applications. Here is a complete representation of its content:
|
||||||
|
|
||||||
```
|
```
|
||||||
owl
|
owl
|
||||||
@@ -46,4 +46,4 @@ owl
|
|||||||
|
|
||||||
- [Comparison with React/Vue](comparison.md)
|
- [Comparison with React/Vue](comparison.md)
|
||||||
- [Tooling](tooling.md)
|
- [Tooling](tooling.md)
|
||||||
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates)
|
||||||
|
|||||||
+1
-2
@@ -125,7 +125,7 @@ The router also has a `navigate` method, useful to programmatically change the
|
|||||||
application to another state (and the url):
|
application to another state (and the url):
|
||||||
|
|
||||||
```js
|
```js
|
||||||
router.navigate({to: 'USER', params: {id: 51}});
|
router.navigate({ to: "USER", params: { id: 51 } });
|
||||||
```
|
```
|
||||||
|
|
||||||
### Navigation Guards
|
### Navigation Guards
|
||||||
@@ -160,7 +160,6 @@ to the currently active route (if any):
|
|||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### `Link`
|
### `Link`
|
||||||
|
|
||||||
The `Link` component is a Owl component which render as a `<a>` tag with any
|
The `Link` component is a Owl component which render as a `<a>` tag with any
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ Note that templates compiled with the `prod` settings will not be recompiled.
|
|||||||
So, changing this setting is best done at startup.
|
So, changing this setting is best done at startup.
|
||||||
|
|
||||||
An important job done by the `dev` mode is to validate props for each component
|
An important job done by the `dev` mode is to validate props for each component
|
||||||
creation and update. Also, extra props will cause an error.
|
creation and update. Also, extra props will cause an error.
|
||||||
|
|
||||||
## Playground
|
## Playground
|
||||||
|
|
||||||
|
|||||||
+1
-3
@@ -18,9 +18,7 @@ not ready yet, resolved directly otherwise). If called with a callback as
|
|||||||
argument, it executes it as soon as the DOM ready (or directly).
|
argument, it executes it as soon as the DOM ready (or directly).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([
|
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) {
|
||||||
templates
|
|
||||||
]) {
|
|
||||||
const qweb = new owl.QWeb(templates);
|
const qweb = new owl.QWeb(templates);
|
||||||
const app = new App({ qweb });
|
const app = new App({ qweb });
|
||||||
app.mount(document.body);
|
app.mount(document.body);
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ QWeb.addDirective({
|
|||||||
ctx.rootContext.shouldDefineParent = true;
|
ctx.rootContext.shouldDefineParent = true;
|
||||||
ctx.rootContext.shouldDefineUtils = true;
|
ctx.rootContext.shouldDefineUtils = true;
|
||||||
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
|
||||||
|
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
|
||||||
let async = node.getAttribute("t-asyncroot") ? true : false;
|
let async = node.getAttribute("t-asyncroot") ? true : false;
|
||||||
|
|
||||||
// t-on- events and t-transition
|
// t-on- events and t-transition
|
||||||
@@ -372,7 +373,12 @@ QWeb.addDirective({
|
|||||||
ctx.addLine(`result = vn${id};`);
|
ctx.addLine(`result = vn${id};`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
if (hasDynamicProps) {
|
||||||
|
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
|
||||||
|
ctx.addLine(`let props${componentID} = Object.assign({${propStr}}, ${dynamicProp});`);
|
||||||
|
} else {
|
||||||
|
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||||
|
}
|
||||||
ctx.addIf(
|
ctx.addIf(
|
||||||
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
|
`w${componentID} && w${componentID}.__owl__.renderPromise && !w${componentID}.__owl__.vnode`
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ export class Context {
|
|||||||
this.rootContext.rootNode = node;
|
this.rootContext.rootNode = node;
|
||||||
}
|
}
|
||||||
if (!this.parentNode) {
|
if (!this.parentNode) {
|
||||||
this.addLine(`result = vn${node};`);
|
this.addLine(`result = vn${node};`);
|
||||||
}
|
}
|
||||||
return this.subContext("parentNode", node);
|
return this.subContext("parentNode", node);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -657,6 +657,49 @@ exports[`composition t-component with dynamic value 2 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`dynamic t-props basic use 1`] = `
|
||||||
|
"function anonymous(context,extra
|
||||||
|
) {
|
||||||
|
let utils = this.constructor.utils;
|
||||||
|
let QWeb = this.constructor;
|
||||||
|
let parent = context;
|
||||||
|
let owner = context;
|
||||||
|
let result;
|
||||||
|
var h = this.h;
|
||||||
|
let c1 = [], p1 = {key:1};
|
||||||
|
var vn1 = h('div', p1, c1);
|
||||||
|
result = vn1;
|
||||||
|
//COMPONENT
|
||||||
|
let def3;
|
||||||
|
let w4 = 4 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[4]] : false;
|
||||||
|
let _2_index = c1.length;
|
||||||
|
c1.push(null);
|
||||||
|
let props4 = Object.assign({}, context['some'].obj);
|
||||||
|
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||||
|
if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||||
|
def3 = w4.__owl__.renderPromise;
|
||||||
|
} else {
|
||||||
|
w4.destroy();
|
||||||
|
w4 = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!w4) {
|
||||||
|
let componentKey4 = \`Child\`;
|
||||||
|
let W4 = context.constructor.components[componentKey4] || QWeb.components[componentKey4];
|
||||||
|
if (!W4) {throw new Error('Cannot find the definition of component \\"' + componentKey4 + '\\"')}
|
||||||
|
w4 = new W4(parent, props4);
|
||||||
|
parent.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
|
def3 = w4.__prepare();
|
||||||
|
def3 = def3.then(vnode=>{if (w4.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
|
} else {
|
||||||
|
def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
|
}
|
||||||
|
extra.promises.push(def3);
|
||||||
|
return result;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`lifecycle hooks willPatch/patched hook with t-keepalive 1`] = `
|
exports[`lifecycle hooks willPatch/patched hook with t-keepalive 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ class WidgetA extends Widget {
|
|||||||
static components = { b: WidgetB };
|
static components = { b: WidgetB };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -900,7 +899,6 @@ describe("destroy method", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const parent = new Parent(env);
|
const parent = new Parent(env);
|
||||||
await parent.mount(fixture);
|
await parent.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div><span><button>click</button></span></div>");
|
expect(fixture.innerHTML).toBe("<div><span><button>click</button></span></div>");
|
||||||
@@ -2243,7 +2241,6 @@ describe("random stuff/miscellaneous", () => {
|
|||||||
name = "A";
|
name = "A";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const a = new A(env);
|
const a = new A(env);
|
||||||
await a.mount(fixture);
|
await a.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe(`<div>A<div>B</div><div>C<div>D</div><div>E</div></div></div>`);
|
expect(fixture.innerHTML).toBe(`<div>A<div>B</div><div>C<div>D</div><div>E</div></div></div>`);
|
||||||
@@ -4317,7 +4314,7 @@ describe("dynamic root nodes", () => {
|
|||||||
</templates>
|
</templates>
|
||||||
`);
|
`);
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
state = {flag: true};
|
state = { flag: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const widget = new TestWidget(env);
|
const widget = new TestWidget(env);
|
||||||
@@ -4344,8 +4341,8 @@ describe("dynamic root nodes", () => {
|
|||||||
class ChildA extends Widget {}
|
class ChildA extends Widget {}
|
||||||
class ChildB extends Widget {}
|
class ChildB extends Widget {}
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
static components = {ChildA, ChildB};
|
static components = { ChildA, ChildB };
|
||||||
state = {flag: true};
|
state = { flag: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const widget = new TestWidget(env);
|
const widget = new TestWidget(env);
|
||||||
@@ -4357,5 +4354,38 @@ describe("dynamic root nodes", () => {
|
|||||||
|
|
||||||
expect(fixture.innerHTML).toBe("<div>abc</div>");
|
expect(fixture.innerHTML).toBe("<div>abc</div>");
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
describe("dynamic t-props", () => {
|
||||||
|
test("basic use", async () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<span t-name="Child">
|
||||||
|
<t t-esc="props.a + props.b"/>
|
||||||
|
</span>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<Child t-props="some.obj"/>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
class Child extends Widget {
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
expect(props).toEqual({ a: 1, b: 2 });
|
||||||
|
expect(props).not.toBe(widget.some.obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Parent extends Widget {
|
||||||
|
static components = { Child };
|
||||||
|
|
||||||
|
some = { obj: { a: 1, b: 2 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = new Parent(env);
|
||||||
|
await widget.mount(fixture);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div><span>3</span></div>");
|
||||||
|
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -573,7 +573,6 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const state = { current: "a", msg: { a: "a", b: "b" } };
|
const state = { current: "a", msg: { a: "a", b: "b" } };
|
||||||
const actions = {
|
const actions = {
|
||||||
setCurrent({ state }, c) {
|
setCurrent({ state }, c) {
|
||||||
@@ -627,7 +626,6 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const state = { someId: 1, flag: true, messages: { 1: "abc" } };
|
const state = { someId: 1, flag: true, messages: { 1: "abc" } };
|
||||||
const actions = {
|
const actions = {
|
||||||
setFlagToFalse({ state }) {
|
setFlagToFalse({ state }) {
|
||||||
@@ -714,7 +712,6 @@ describe("connecting a component to store", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
(<any>env).store = store;
|
(<any>env).store = store;
|
||||||
const app = new TodoApp(env);
|
const app = new TodoApp(env);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user