[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
class ClickCounter extends owl.Component {
inlineTemplate = `
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>`;
state = { value: 0 };
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 });
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.
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
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.
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,
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
@@ -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
with a class object:
```js
```xml
<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.
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).
### 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
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
any. After the willStart method, the `state` property is observed, and each
change will cause the widget to rerender itself.
+21 -34
View File
@@ -43,7 +43,7 @@ export interface Meta<T extends Env, Props> {
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
// qweb dependant, so we need a place to store this information indexed by
// qweb instances.
@@ -61,7 +61,6 @@ export class Component<
> extends EventBus {
readonly __owl__: Meta<Env, Props>;
template?: string;
inlineTemplate?: string;
get el(): HTMLElement | null {
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
@@ -388,41 +387,29 @@ export class Component<
const qweb = this.env.qweb;
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;
let tmap = TEMPLATE_MAP[qweb.id];
if (!tmap) {
tmap = {};
TEMPLATE_MAP[qweb.id] = tmap;
}
let p = (<any>this).constructor;
let name: string = p.name;
let template = tmap[name];
if (template) {
this.template = template;
} else {
let tmap = TEMPLATE_MAP[qweb.id];
if (!tmap) {
tmap = {};
TEMPLATE_MAP[qweb.id] = tmap;
while (
(template = p.name) &&
!(template in qweb.templates) &&
p !== Component
) {
p = p.__proto__;
}
let p = (<any>this).constructor;
let name: string = p.name;
let template = tmap[name];
if (template) {
this.template = template;
if (p === Component) {
this.template = "default";
} else {
while (
(template = p.name) &&
!(template in qweb.templates) &&
p !== Component
) {
p = p.__proto__;
}
if (p === Component) {
this.template = "default";
} else {
tmap[name] = template;
this.template = template;
}
tmap[name] = template;
this.template = template;
}
}
}
+1 -8
View File
@@ -152,14 +152,7 @@ export class QWeb {
* Add a template to the internal template map. Note that it is not
* immediately compiled.
*/
addTemplate(
name: string,
xmlString: string,
allowDuplicates: boolean = false
) {
if (name in this.templates && allowDuplicates) {
return;
}
addTemplate(name: string, xmlString: string) {
const doc = parseXML(xmlString);
if (!doc.firstChild) {
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];
this.getters[name] = payload => {
return func({ state: this.state, getters: this.getters }, payload);
}
};
}
}
@@ -154,6 +154,8 @@ interface EnvWithStore extends Env {
store: Store;
}
let nextID = 1;
export function connect(mapStateToProps, options: any = {}) {
let hashFunction = options.hashFunction || null;
@@ -184,7 +186,7 @@ export function connect(mapStateToProps, options: any = {}) {
return function<E extends EnvWithStore, P, S>(
Comp: Constructor<Component<E, P, S>>
) {
return class extends Comp {
const Result = class extends Comp {
constructor(parent, props?: any) {
const env = parent instanceof Component ? parent.env : parent;
const ownProps = Object.assign({}, props || {});
@@ -269,5 +271,13 @@ export function connect(mapStateToProps, options: any = {}) {
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
+113 -84
View File
@@ -176,15 +176,15 @@ describe("basic use", () => {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1,
},
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: 'aaron',
name: "aaron"
}
},
}
};
const getters = {
beerTasterName({ state }, beerID) {
@@ -207,15 +207,15 @@ describe("basic use", () => {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1,
},
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: 'aaron',
name: "aaron"
}
},
}
};
const getters = {
beerTasterName({ state }, beerID) {
@@ -243,15 +243,15 @@ describe("basic use", () => {
1: {
id: 1,
name: "bertinchamps",
tasterID: 1,
},
tasterID: 1
}
},
tasters: {
1: {
id: 1,
name: 'aaron',
name: "aaron"
}
},
}
};
const getters = {
beerTasterName({ state }, beerID) {
@@ -278,15 +278,15 @@ describe("basic use", () => {
return `${getters.b()}${getters.c(1)}`;
},
b() {
return 'b';
return "b";
},
c({}, i) {
return `c${i}`;
},
}
};
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();
});
class App extends Component<any, any, any> {
inlineTemplate = `
test("connecting a component works", async () => {
env.qweb.addTemplate(
"App",
`
<div>
<t t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-widget="Todo" t-props="todo"/>
</t>
</div>`;
widgets = { Todo };
}
class Todo extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.msg"/></span>`;
}
test("connecting a component works", async () => {
</div>`
);
env.qweb.addTemplate("Todo", `<span><t t-esc="props.msg"/></span>`);
class App extends Component<any, any, any> {
widgets = { Todo };
}
class Todo extends Component<any, any, any> {}
const state = { todos: [] };
const mutations = {
addTodo({ state }, msg) {
@@ -456,14 +457,16 @@ describe("connecting a component to store", () => {
}
const store = new Store({ state, mutations });
class App extends Component<any, any, any> {
inlineTemplate = `
env.qweb.addTemplate(
"App",
`
<div>
<span t-foreach="props.todos" t-as="todo" t-key="todo">
<t t-esc="todo.title"/>
</span>
</div>`;
}
</div>`
);
class App extends Component<any, any, any> {}
const DeepTodoApp = connect(
mapStateToProps,
@@ -493,8 +496,8 @@ describe("connecting a component to store", () => {
test("connected child components with custom hooks", async () => {
let steps: any = [];
env.qweb.addTemplate("Child", `<div/>`);
class Child extends Component<any, any, any> {
inlineTemplate = `<div/>`;
mounted() {
steps.push("child:mounted");
}
@@ -505,11 +508,14 @@ describe("connecting a component to store", () => {
const ConnectedChild = connect(s => s)(Child);
class Parent extends Component<any, any, any> {
inlineTemplate = `
env.qweb.addTemplate(
"Parent",
`
<div>
<t t-if="state.child" t-widget="ConnectedChild"/>
</div>`;
</div>`
);
class Parent extends Component<any, any, any> {
widgets = { ConnectedChild };
constructor(env: Env) {
@@ -540,20 +546,22 @@ describe("connecting a component to store", () => {
};
const store = new Store({ state, mutations });
class TodoItem extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.text"/></span>`;
}
env.qweb.addTemplate("TodoItem", `<span><t t-esc="props.text"/></span>`);
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props) => {
const todo = state.todos.find(t => t.id === props.id);
return todo;
})(TodoItem);
class TodoList extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
</t>
</div>`;
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
}
@@ -578,10 +586,7 @@ describe("connecting a component to store", () => {
test("connect receives store getters as third argument", async () => {
const state = {
importantID: 1,
todos: [
{ id: 1, text: "jupiler" },
{ id: 2, text: "bertinchamps" },
],
todos: [{ id: 1, text: "jupiler" }, { id: 2, text: "bertinchamps" }]
};
const getters = {
importantTodoText({ state }) {
@@ -589,30 +594,35 @@ describe("connecting a component to store", () => {
},
text({ state }, id) {
return state.todos.find(todo => todo.id === id).text;
},
}
};
const store = new Store({ state, getters });
class TodoItem extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"TodoItem",
`<div>
<span><t t-esc="props.activeTodoText"/></span>
<span><t t-esc="props.importantTodoText"/></span>
</div>`;
}
</div>`
);
class TodoItem extends Component<any, any, any> {}
const ConnectedTodo = connect((state, props, getters) => {
const todo = state.todos.find(t => t.id === props.id);
return {
activeTodoText: getters.text(todo.id),
importantTodoText: getters.importantTodoText(),
importantTodoText: getters.importantTodoText()
};
})(TodoItem);
class TodoList extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"TodoList",
`<div>
<t t-foreach="props.todos" t-as="todo">
<t t-widget="ConnectedTodo" t-props="todo"/>
</t>
</div>`;
</div>`
);
class TodoList extends Component<any, any, any> {
widgets = { ConnectedTodo };
}
@@ -625,21 +635,25 @@ describe("connecting a component to store", () => {
const app = new ConnectedTodoList(env);
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 () => {
class Beer extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.name"/></span>`;
}
env.qweb.addTemplate("Beer", `<span><t t-esc="props.name"/></span>`);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => {
return state.beers[props.id];
})(Beer);
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`
);
class App extends Component<any, any, any> {
inlineTemplate = `<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`;
widgets = { ConnectedBeer };
state = { beerId: 1 };
}
@@ -658,12 +672,14 @@ describe("connecting a component to store", () => {
});
test("connected component is updated when store is changed", async () => {
class App extends Component<any, any, any> {
inlineTemplate = `
env.qweb.addTemplate(
"App",
`
<div>
<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 = {
addBeer({ state }, name) {
@@ -692,13 +708,15 @@ describe("connecting a component to store", () => {
});
test("connected component with undefined, null and string props", async () => {
class Beer extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"Beer",
`<div>
<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.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`;
}
</div>`
);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => {
return {
selected: state.beers[props.id],
@@ -707,10 +725,13 @@ describe("connecting a component to store", () => {
};
})(Beer);
class App extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`;
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
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 () => {
class Beer extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"Beer",
`<div>
<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.consumed">consumed:<t t-esc="props.consumed.name"/></span>
</div>`;
}
</div>`
);
class Beer extends Component<any, any, any> {}
const ConnectedBeer = connect((state, props) => {
return {
selected: state.beers[props.id],
@@ -771,10 +794,13 @@ describe("connecting a component to store", () => {
};
})(Beer);
class App extends Component<any, any, any> {
inlineTemplate = `<div>
env.qweb.addTemplate(
"App",
`<div>
<t t-widget="ConnectedBeer" t-props="{id: state.beerId}"/>
</div>`;
</div>`
);
class App extends Component<any, any, any> {
widgets = { ConnectedBeer };
state = { beerId: 0 };
}
@@ -845,12 +871,16 @@ describe("connecting a component to store", () => {
test("correct update order when parent/children are connected", async () => {
const steps: string[] = [];
class Parent extends Component<any, any, any> {
inlineTemplate = `
env.qweb.addTemplate(
"Parent",
`
<div>
<t t-widget="Child" t-ref="'child'" t-props="{key: props.current}"/>
</div>
`;
`
);
class Parent extends Component<any, any, any> {
widgets = { Child: ConnectedChild };
}
const ConnectedParent = connect(function(s) {
@@ -858,9 +888,8 @@ describe("connecting a component to store", () => {
return { current: s.current, isvisible: s.isvisible };
})(Parent);
class Child extends Component<any, any, any> {
inlineTemplate = `<span><t t-esc="props.msg"/></span>`;
}
env.qweb.addTemplate("Child", `<span><t t-esc="props.msg"/></span>`);
class Child extends Component<any, any, any> {}
const ConnectedChild = connect(function(s, props) {
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 () => {
const steps: string[] = [];
env.qweb.addTemplate("App", `<div><t t-esc="props.msg"/></div>`);
class App extends Component<any, any, any> {
inlineTemplate = `<div><t t-esc="props.msg"/></div>`;
willPatch() {
steps.push("willpatch");
}