[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 {
static components = { Counter };
static components = { Counter };
}
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`.
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).
If you are interested in a comparison with React or Vue, you will
+4 -4
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
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.
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
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
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
possible for developers is more valuable (for us).
@@ -233,4 +233,4 @@ class Counter extends owl.ConnectedComponent {
}
const counter = new Counter({ store, qweb });
```
```
+18 -2
View File
@@ -119,7 +119,7 @@ find a template with the component name (or one of its ancestor).
```js
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
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
<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
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.
+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
needs. Here is a list of all Owl specific directives:
| Name | Description |
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
| `t-component`, `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-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `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-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
| Name | Description |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `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-key` | [Defining a key (to help virtual dom reconciliation)](component.md#t-key-directive) |
| `t-on-*` | [Event handling](component.md#event-handling) |
| `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-slot` | [Rendering a slot](component.md#slots) |
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
## QWeb Engine
@@ -101,7 +101,7 @@ It's API is quite simple:
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`
attribute).
+2 -2
View File
@@ -3,7 +3,7 @@
## Owl Content
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
@@ -46,4 +46,4 @@ owl
- [Comparison with React/Vue](comparison.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
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):
```js
router.navigate({to: 'USER', params: {id: 51}});
router.navigate({ to: "USER", params: { id: 51 } });
```
### Navigation Guards
@@ -160,7 +160,6 @@ to the currently active route (if any):
</div>
```
### `Link`
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.
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
+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).
```js
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([
templates
]) {
Promise.all([loadTemplates(), owl.utils.whenReady()]).then(function([templates]) {
const qweb = new owl.QWeb(templates);
const app = new App({ qweb });
app.mount(document.body);
+7 -1
View File
@@ -194,6 +194,7 @@ QWeb.addDirective({
ctx.rootContext.shouldDefineParent = true;
ctx.rootContext.shouldDefineUtils = true;
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
let async = node.getAttribute("t-asyncroot") ? true : false;
// t-on- events and t-transition
@@ -372,7 +373,12 @@ QWeb.addDirective({
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(
`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;
}
if (!this.parentNode) {
this.addLine(`result = vn${node};`);
this.addLine(`result = vn${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`] = `
"function anonymous(context,extra
) {
+37 -7
View File
@@ -63,7 +63,6 @@ class WidgetA extends Widget {
static components = { b: WidgetB };
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
@@ -900,7 +899,6 @@ describe("destroy method", () => {
}
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span><button>click</button></span></div>");
@@ -2243,7 +2241,6 @@ describe("random stuff/miscellaneous", () => {
name = "A";
}
const a = new A(env);
await a.mount(fixture);
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>
`);
class TestWidget extends Widget {
state = {flag: true};
state = { flag: true };
}
const widget = new TestWidget(env);
@@ -4344,8 +4341,8 @@ describe("dynamic root nodes", () => {
class ChildA extends Widget {}
class ChildB extends Widget {}
class TestWidget extends Widget {
static components = {ChildA, ChildB};
state = {flag: true};
static components = { ChildA, ChildB };
state = { flag: true };
}
const widget = new TestWidget(env);
@@ -4357,5 +4354,38 @@ describe("dynamic root nodes", () => {
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 actions = {
setCurrent({ state }, c) {
@@ -627,7 +626,6 @@ describe("connecting a component to store", () => {
}
}
const state = { someId: 1, flag: true, messages: { 1: "abc" } };
const actions = {
setFlagToFalse({ state }) {
@@ -714,7 +712,6 @@ describe("connecting a component to store", () => {
}
}
(<any>env).store = store;
const app = new TodoApp(env);