[IMP] component: support dynamic t-props

It is useful in some rare situations.

closes #144
This commit is contained in:
Géry Debongnie
2019-09-11 14:01:49 +02:00
parent ffb3263c79
commit 4fc3423682
13 changed files with 128 additions and 39 deletions
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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).
+18 -2
View File
@@ -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
View File
@@ -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).
+1 -1
View File
@@ -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
+1 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+7 -1
View File
@@ -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
View File
@@ -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
) { ) {
+37 -7
View File
@@ -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();
});
}); });
-3
View File
@@ -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);