[IMP] component: remove support for inlineTemplate

closes #93
This commit is contained in:
Géry Debongnie
2019-05-15 11:15:08 +02:00
parent 9c1d369e94
commit 0a10bbc023
7 changed files with 473 additions and 344 deletions
+6 -6
View File
@@ -36,11 +36,6 @@ Here is a short example to illustrate interactive widgets:
```javascript ```javascript
class ClickCounter extends owl.Component { class ClickCounter extends owl.Component {
inlineTemplate = `
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 }; state = { value: 0 };
increment() { increment() {
@@ -48,7 +43,12 @@ class ClickCounter extends owl.Component {
} }
} }
const qweb = new owl.QWeb(); const TEMPLATES = `
<button t-name="ClickCounter" t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
const qweb = new owl.QWeb(TEMPLATES);
const counter = new ClickCounter({ qweb }); const counter = new ClickCounter({ qweb });
counter.mount(document.body); counter.mount(document.body);
``` ```
+4 -7
View File
@@ -30,7 +30,7 @@ OWL components are the building blocks for user interface. They are designed to
and follow the QWeb specification. This is a requirement for Odoo. and follow the QWeb specification. This is a requirement for Odoo.
OWL components are defined as a subclass of Component. The rendering is OWL components are defined as a subclass of Component. The rendering is
exclusively done by a [QWeb](qweb.md) template (either defined inline or preloaded in QWeb). exclusively done by a [QWeb](qweb.md) template (which needs to be preloaded in QWeb).
Rendering a component generates a virtual dom representation Rendering a component generates a virtual dom representation
of the widget, which is then patched to the DOM, in order to apply the changes in an efficient way. of the widget, which is then patched to the DOM, in order to apply the changes in an efficient way.
@@ -61,7 +61,7 @@ Note that this code is written in ESNext style, so it will only run on the
latest browsers without a transpilation step. latest browsers without a transpilation step.
This example show how a component should be defined: it simply subclasses the This example show how a component should be defined: it simply subclasses the
Component class. If no `template` key (or `inlineTemplate`), is defined, then Component class. If no `template` key is defined, then
Owl will use the component's name as template name. Here, Owl will use the component's name as template name. Here,
a state object is defined. It is not mandatory to use the state object, but it a state object is defined. It is not mandatory to use the state object, but it
is certainly encouraged. The state object is [observed](observer.md), and any is certainly encouraged. The state object is [observed](observer.md), and any
@@ -112,7 +112,7 @@ between a valid css class added by the component, or some custom code, and a
class that need to be removed. This is why we only support the explicit syntax class that need to be removed. This is why we only support the explicit syntax
with a class object: with a class object:
```js ```xml
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" /> <t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
``` ```
@@ -124,7 +124,7 @@ parent to its children. The environment needs to have a QWeb instance, which
will be used to render the component template. will be used to render the component template.
Be aware that the name of the component may be significant: if a component does Be aware that the name of the component may be significant: if a component does
not define a `template` or `inlineTemplate` key, then Owl will lookup in QWeb to not define a `template` key, then Owl will lookup in QWeb to
find a template with the component name (or one of its ancestor). find a template with the component name (or one of its ancestor).
### Properties ### Properties
@@ -137,9 +137,6 @@ find a template with the component name (or one of its ancestor).
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render - **`template`** (string, optional): if given, this is the name of the QWeb template that will render
the component. the component.
- **`inlineTemplate`** (string, optional): a string that represents a xml template. If set,
this will be loaded into QWeb and used instead of the `template` property.
- **`state`** (Object): this is the location of the component's state, if there is - **`state`** (Object): this is the location of the component's state, if there is
any. After the willStart method, the `state` property is observed, and each any. After the willStart method, the `state` property is observed, and each
change will cause the widget to rerender itself. change will cause the widget to rerender itself.
+1 -14
View File
@@ -43,7 +43,7 @@ export interface Meta<T extends Env, Props> {
mountedHandlers: { [key: number]: Function }; mountedHandlers: { [key: number]: Function };
} }
// If a component does not define explicitely a template (or inlineTemplate) // If a component does not define explicitely a template
// key, it needs to find a template with its name (or a parent's). This is // key, it needs to find a template with its name (or a parent's). This is
// qweb dependant, so we need a place to store this information indexed by // qweb dependant, so we need a place to store this information indexed by
// qweb instances. // qweb instances.
@@ -61,7 +61,6 @@ export class Component<
> extends EventBus { > extends EventBus {
readonly __owl__: Meta<Env, Props>; readonly __owl__: Meta<Env, Props>;
template?: string; template?: string;
inlineTemplate?: string;
get el(): HTMLElement | null { get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null; return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
@@ -388,17 +387,6 @@ export class Component<
const qweb = this.env.qweb; const qweb = this.env.qweb;
if (!this.template) { if (!this.template) {
if (this.inlineTemplate) {
this.env.qweb.addTemplate(
this.inlineTemplate,
this.inlineTemplate,
true
);
// we write on the proto, so any new component of this class will get
// automatically the template key properly setup.
(<any>this).__proto__.template = this.inlineTemplate;
} else {
let tmap = TEMPLATE_MAP[qweb.id]; let tmap = TEMPLATE_MAP[qweb.id];
if (!tmap) { if (!tmap) {
tmap = {}; tmap = {};
@@ -425,7 +413,6 @@ export class Component<
} }
} }
} }
}
this.__owl__.render = qweb.render.bind(qweb, this.template); this.__owl__.render = qweb.render.bind(qweb, this.template);
this._observeState(); this._observeState();
return this._render(); return this._render();
+1 -8
View File
@@ -152,14 +152,7 @@ export class QWeb {
* Add a template to the internal template map. Note that it is not * Add a template to the internal template map. Note that it is not
* immediately compiled. * immediately compiled.
*/ */
addTemplate( addTemplate(name: string, xmlString: string) {
name: string,
xmlString: string,
allowDuplicates: boolean = false
) {
if (name in this.templates && allowDuplicates) {
return;
}
const doc = parseXML(xmlString); const doc = parseXML(xmlString);
if (!doc.firstChild) { if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)"); throw new Error("Invalid template (should not be empty)");
+12 -2
View File
@@ -72,7 +72,7 @@ export class Store extends EventBus {
const func: (...any) => any = entry[1]; const func: (...any) => any = entry[1];
this.getters[name] = payload => { this.getters[name] = payload => {
return func({ state: this.state, getters: this.getters }, payload); return func({ state: this.state, getters: this.getters }, payload);
} };
} }
} }
@@ -154,6 +154,8 @@ interface EnvWithStore extends Env {
store: Store; store: Store;
} }
let nextID = 1;
export function connect(mapStateToProps, options: any = {}) { export function connect(mapStateToProps, options: any = {}) {
let hashFunction = options.hashFunction || null; let hashFunction = options.hashFunction || null;
@@ -184,7 +186,7 @@ export function connect(mapStateToProps, options: any = {}) {
return function<E extends EnvWithStore, P, S>( return function<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>> Comp: Constructor<Component<E, P, S>>
) { ) {
return class extends Comp { const Result = class extends Comp {
constructor(parent, props?: any) { constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent; const env = parent instanceof Component ? parent.env : parent;
const ownProps = Object.assign({}, props || {}); const ownProps = Object.assign({}, props || {});
@@ -269,5 +271,13 @@ export function connect(mapStateToProps, options: any = {}) {
return super._updateProps(mergedProps, forceUpdate, patchQueue); return super._updateProps(mergedProps, forceUpdate, patchQueue);
} }
}; };
// we assign here a unique name to the resulting anonymous class.
// this is necessary for Owl to be able to properly deduce templates.
// Otherwise, all connected components would have the same name, and then
// each component after the first will necessarily have the same template.
let name = `ConnectedComponent${nextID++}`;
Object.defineProperty(Result, "name", { value: name });
return Result;
}; };
} }
+316 -203
View File
File diff suppressed because it is too large Load Diff
+111 -82
View File
@@ -176,15 +176,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1, tasterID: 1
}, }
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: 'aaron', name: "aaron"
}
} }
},
}; };
const getters = { const getters = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -207,15 +207,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1, tasterID: 1
}, }
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: 'aaron', name: "aaron"
}
} }
},
}; };
const getters = { const getters = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -243,15 +243,15 @@ describe("basic use", () => {
1: { 1: {
id: 1, id: 1,
name: "bertinchamps", name: "bertinchamps",
tasterID: 1, tasterID: 1
}, }
}, },
tasters: { tasters: {
1: { 1: {
id: 1, id: 1,
name: 'aaron', name: "aaron"
}
} }
},
}; };
const getters = { const getters = {
beerTasterName({ state }, beerID) { beerTasterName({ state }, beerID) {
@@ -278,15 +278,15 @@ describe("basic use", () => {
return `${getters.b()}${getters.c(1)}`; return `${getters.b()}${getters.c(1)}`;
}, },
b() { b() {
return 'b'; return "b";
}, },
c({}, i) { c({}, i) {
return `c${i}`; return `c${i}`;
}, }
}; };
const store = new Store({ getters }); const store = new Store({ getters });
expect(store.getters.a()).toBe('bc1'); expect(store.getters.a()).toBe("bc1");
}); });
}); });
@@ -408,20 +408,21 @@ describe("connecting a component to store", () => {
fixture.remove(); fixture.remove();
}); });
class App extends Component<any, any, any> { test("connecting a component works", async () => {
inlineTemplate = ` env.qweb.addTemplate(
"App",
`
<div> <div>
<t t-foreach="props.todos" t-as="todo" t-key="todo"> <t t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-widget="Todo" t-props="todo"/> <t t-widget="Todo" t-props="todo"/>
</t> </t>
</div>`; </div>`
);
env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`);
class App extends Component<any, any, any> {
widgets = { Todo }; widgets = { Todo };
} }
class Todo extends Component<any, any, any> { class Todo extends Component<any, any, any> {}
inlineTemplate = `<span><t t-esc="props.msg"/></span>`;
}
test("connecting a component works", async () => {
const state = { todos: [] }; const state = { todos: [] };
const mutations = { const mutations = {
addTodo({ state }, msg) { addTodo({ state }, msg) {
@@ -456,14 +457,16 @@ describe("connecting a component to store", () => {
} }
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
class App extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = ` "App",
`
<div> <div>
<span t-foreach="props.todos" t-as="todo" t-key="todo"> <span t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-esc="todo.title"/> <t t-esc="todo.title"/>
</span> </span>
</div>`; </div>`
} );
class App extends Component<any, any, any> {}
const DeepTodoApp = connect( const DeepTodoApp = connect(
mapStateToProps, mapStateToProps,
@@ -493,8 +496,8 @@ describe("connecting a component to store", () => {
test("connected child components with custom hooks", async () => { test("connected child components with custom hooks", async () => {
let steps: any = []; let steps: any = [];
env.qweb.addTemplate("Child", `<div/>`);
class Child extends Component<any, any, any> { class Child extends Component<any, any, any> {
inlineTemplate = `<div/>`;
mounted() { mounted() {
steps.push("child:mounted"); steps.push("child:mounted");
} }
@@ -505,11 +508,14 @@ describe("connecting a component to store", () => {
const ConnectedChild = connect(s => s)(Child); const ConnectedChild = connect(s => s)(Child);
class Parent extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = ` "Parent",
`
<div> <div>
<t t-if="state.child" t-widget="ConnectedChild"/> <t t-if="state.child" t-widget="ConnectedChild"/>
</div>`; </div>`
);
class Parent extends Component<any, any, any> {
widgets = { ConnectedChild }; widgets = { ConnectedChild };
constructor(env: Env) { constructor(env: Env) {
@@ -540,20 +546,22 @@ describe("connecting a component to store", () => {
}; };
const store = new Store({ state, mutations }); const store = new Store({ state, mutations });
class TodoItem extends Component<any, any, any> { env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`);
inlineTemplate = `<span><t t-esc="props.text"/></span>`; class TodoItem extends Component<any, any, any> {}
}
const ConnectedTodo = connect((state, props) => { const ConnectedTodo = connect((state, props) => {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return todo; return todo;
})(TodoItem); })(TodoItem);
class TodoList extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo"> <t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/> <t t-widget="ConnectedTodo" t-props="todo"/>
</t> </t>
</div>`; </div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo }; widgets = { ConnectedTodo };
} }
@@ -578,10 +586,7 @@ describe("connecting a component to store", () => {
test("connect receives store getters as third argument", async () => { test("connect receives store getters as third argument", async () => {
const state = { const state = {
importantID: 1, importantID: 1,
todos: [ todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
{ id: 1, text: "jupiler" },
{ id: 2, text: "bertinchamps" },
],
}; };
const getters = { const getters = {
importantTodoText({ state }) { importantTodoText({ state }) {
@@ -589,30 +594,35 @@ describe("connecting a component to store", () => {
}, },
text({ state }, id) { text({ state }, id) {
return state.todos.find(todo => todo.id === id).text; return state.todos.find(todo => todo.id === id).text;
}, }
}; };
const store = new Store({ state, getters }); const store = new Store({ state, getters });
class TodoItem extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "TodoItem",
`<div>
<span><t t-esc="props.activeTodoText"/></span> <span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span> <span><t t-esc="props.importantTodoText"/></span>
</div>`; </div>`
} );
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props, getters) => { const ConnectedTodo = connect((state, props, getters) => {
const todo = state.todos.find(t => t.id === props.id); const todo = state.todos.find(t => t.id === props.id);
return { return {
activeTodoText: getters.text(todo.id), activeTodoText: getters.text(todo.id),
importantTodoText: getters.importantTodoText(), importantTodoText: getters.importantTodoText()
}; };
})(TodoItem); })(TodoItem);
class TodoList extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo"> <t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/> <t t-widget="ConnectedTodo" t-props="todo"/>
</t> </t>
</div>`; </div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo }; widgets = { ConnectedTodo };
} }
@@ -625,21 +635,25 @@ describe("connecting a component to store", () => {
const app = new ConnectedTodoList(env); const app = new ConnectedTodoList(env);
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>"); expect(fixture.innerHTML).toBe(
"<div><div><span>jupiler</span><span>jupiler</span></div><div><span>bertinchamps</span><span>jupiler</span></div></div>"
);
}); });
test("connected component is updated when props are updated", async () => { test("connected component is updated when props are updated", async () => {
class Beer extends Component<any, any, any> { env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`);
inlineTemplate = `<span><t t-esc="props.name"/></span>`; class Beer extends Component<any, any, any> {}
}
const ConnectedBeer = connect((state, props) => { const ConnectedBeer = connect((state, props) => {
return state.beers[props.id]; return state.beers[props.id];
})(Beer); })(Beer);
class App extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/> <t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`; </div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer }; widgets = { ConnectedBeer };
state = { beerId: 1 }; state = { beerId: 1 };
} }
@@ -658,12 +672,14 @@ describe("connecting a component to store", () => {
}); });
test("connected component is updated when store is changed", async () => { test("connected component is updated when store is changed", async () => {
class App extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = ` "App",
`
<div> <div>
<span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span> <span t-foreach="props.beers" t-as="beer" t-key="beer.name"><t t-esc="beer.name"/></span>
</div>`; </div>`
} );
class App extends Component<any, any, any> {}
const mutations = { const mutations = {
addBeer({ state }, name) { addBeer({ state }, name) {
@@ -692,13 +708,15 @@ describe("connecting a component to store", () => {
}); });
test("connected component with undefined, null and string props", async () => { test("connected component with undefined, null and string props", async () => {
class Beer extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span> <span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span> <span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span> <span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`; </div>`
} );
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => { const ConnectedBeer = connect((state, props) => {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
@@ -707,10 +725,13 @@ describe("connecting a component to store", () => {
}; };
})(Beer); })(Beer);
class App extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/> <t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`; </div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer }; widgets = { ConnectedBeer };
state = { beerId: 0 }; state = { beerId: 0 };
} }
@@ -756,13 +777,15 @@ describe("connecting a component to store", () => {
}); });
test("connected component deeply reactive with undefined, null and string props", async () => { test("connected component deeply reactive with undefined, null and string props", async () => {
class Beer extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "Beer",
`<div>
<span>taster:<t t-esc="props.taster"/></span> <span>taster:<t t-esc="props.taster"/></span>
<span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span> <span t-if="props.selected">selected:<t t-esc="props.selected.name"/></span>
<span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span> <span t-if="props.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`; </div>`
} );
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => { const ConnectedBeer = connect((state, props) => {
return { return {
selected: state.beers[props.id], selected: state.beers[props.id],
@@ -771,10 +794,13 @@ describe("connecting a component to store", () => {
}; };
})(Beer); })(Beer);
class App extends Component<any, any, any> { env.qweb.addTemplate(
inlineTemplate = `<div> "App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/> <t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`; </div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer }; widgets = { ConnectedBeer };
state = { beerId: 0 }; state = { beerId: 0 };
} }
@@ -845,12 +871,16 @@ describe("connecting a component to store", () => {
test("correct update order when parent/children are connected", async () => { test("correct update order when parent/children are connected", async () => {
const steps: string[] = []; const steps: string[] = [];
class Parent extends Component<any, any, any> {
inlineTemplate = ` env.qweb.addTemplate(
"Parent",
`
<div> <div>
<t t-widget="Child" t-ref="'child'" t-props="{key: props.current}"/> <t t-widget="Child" t-ref="'child'" t-props="{key: props.current}"/>
</div> </div>
`; `
);
class Parent extends Component<any, any, any> {
widgets = { Child: ConnectedChild }; widgets = { Child: ConnectedChild };
} }
const ConnectedParent = connect(function(s) { const ConnectedParent = connect(function(s) {
@@ -858,9 +888,8 @@ describe("connecting a component to store", () => {
return { current: s.current, isvisible: s.isvisible }; return { current: s.current, isvisible: s.isvisible };
})(Parent); })(Parent);
class Child extends Component<any, any, any> { env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`);
inlineTemplate = `<span><t t-esc="props.msg"/></span>`; class Child extends Component<any, any, any> {}
}
const ConnectedChild = connect(function(s, props) { const ConnectedChild = connect(function(s, props) {
steps.push("child"); steps.push("child");
@@ -889,8 +918,8 @@ describe("connecting a component to store", () => {
test("connected component willpatch/patch hooks are called on store updates", async () => { test("connected component willpatch/patch hooks are called on store updates", async () => {
const steps: string[] = []; const steps: string[] = [];
env.qweb.addTemplate("App", `<div><t t-esc="props.msg"/></div>`);
class App extends Component<any, any, any> { class App extends Component<any, any, any> {
inlineTemplate = `<div><t t-esc="props.msg"/></div>`;
willPatch() { willPatch() {
steps.push("willpatch"); steps.push("willpatch");
} }