[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
+17 -1
View File
@@ -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.
+2 -2
View File
@@ -66,8 +66,8 @@ 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) |
+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 -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);
+6
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};`);
} }
} }
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.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`
); );
@@ -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);