diff --git a/README.md b/README.md index 3cdad433..ec8499c2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/comparison.md b/doc/comparison.md index c247565e..1cee3115 100644 --- a/doc/comparison.md +++ b/doc/comparison.md @@ -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 }); -``` \ No newline at end of file +``` diff --git a/doc/component.md b/doc/component.md index 77c6c3cd..1746b2dd 100644 --- a/doc/component.md +++ b/doc/component.md @@ -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
@@ -486,6 +487,21 @@ class ParentComponent { } ``` +And the `t-props` directive can be used to specify totally dynamic props: + +```xml +
+ +
+``` + +```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. diff --git a/doc/qweb.md b/doc/qweb.md index 9e0787dc..b9f5650a 100644 --- a/doc/qweb.md +++ b/doc/qweb.md @@ -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", "
hello
"); ``` - 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). diff --git a/doc/readme.md b/doc/readme.md index 5440caee..3e189226 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -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) \ No newline at end of file +- [Templates to start Owl applications (external link)](https://github.com/ged-odoo/owl-templates) diff --git a/doc/router.md b/doc/router.md index 142eb53b..e9202a71 100644 --- a/doc/router.md +++ b/doc/router.md @@ -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):
``` - ### `Link` The `Link` component is a Owl component which render as a `` tag with any diff --git a/doc/tooling.md b/doc/tooling.md index 3c17e7b1..d2c7befd 100644 --- a/doc/tooling.md +++ b/doc/tooling.md @@ -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 diff --git a/doc/utils.md b/doc/utils.md index 05a51494..c7ad213c 100644 --- a/doc/utils.md +++ b/doc/utils.md @@ -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); diff --git a/src/component/directive.ts b/src/component/directive.ts index b4f36b53..43ffcc29 100644 --- a/src/component/directive.ts +++ b/src/component/directive.ts @@ -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` ); diff --git a/src/qweb/context.ts b/src/qweb/context.ts index 872c929f..448db444 100644 --- a/src/qweb/context.ts +++ b/src/qweb/context.ts @@ -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); } diff --git a/tests/component/__snapshots__/component.test.ts.snap b/tests/component/__snapshots__/component.test.ts.snap index 0ec816db..487aabe7 100644 --- a/tests/component/__snapshots__/component.test.ts.snap +++ b/tests/component/__snapshots__/component.test.ts.snap @@ -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 ) { diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index 67e9db18..eb33e032 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -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("
"); @@ -2243,7 +2241,6 @@ describe("random stuff/miscellaneous", () => { name = "A"; } - const a = new A(env); await a.mount(fixture); expect(fixture.innerHTML).toBe(`
A
B
C
D
E
`); @@ -4317,7 +4314,7 @@ describe("dynamic root nodes", () => { `); 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("
abc
"); }); +}); -}); \ No newline at end of file +describe("dynamic t-props", () => { + test("basic use", async () => { + expect.assertions(4); + env.qweb.addTemplates(` + + + + +
+ +
+
+ `); + 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("
3
"); + expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); + }); +}); diff --git a/tests/store/connected_component.test.ts b/tests/store/connected_component.test.ts index 97ae7a81..0bf28dc9 100644 --- a/tests/store/connected_component.test.ts +++ b/tests/store/connected_component.test.ts @@ -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", () => { } } - (env).store = store; const app = new TodoApp(env);