odoo / owl Goto Github PK
View Code? Open in Web Editor NEWOWL: A web framework for structured, dynamic and maintainable applications
Home Page: https://odoo.github.io/owl/
License: Other
OWL: A web framework for structured, dynamic and maintainable applications
Home Page: https://odoo.github.io/owl/
License: Other
TODO app with following mapStateToProps
:
function mapStateToProps(state) {
return { todos: state.todos.sort((t1, t2) => t1.title > t2.title ? -1 : 1) };
}
add an item => infinite loop
maybe prevent mutating state inside mapStateToProps
?
e.g.
class Component {
constructor(parent, props) {
// ...
this.set = (...args) => this.__owl__.observer.set(...args);
}
}
Maybe we should disallow a missing t-key. Without a t-key, subtle bugs could happen.
would be nice that people have access to the docs from the playground
a t t-widget
with props such as: t-props="(display: ['js','xml','css']}"
will crash. Other simple props will crash as well: t-props="{a: {b: 1}}"
The reason is that we use a adhoc expression parser, which tries to identifies keywords/strings/variables and replace variables x
with a lookup in the context: context['x']
. This "expression parser" is quite naive.
Write a very minimalist js tokenizer/parser/evaluator. We can only allow a subset of expressions, and have a very simple grammar. For example, we do not need to be able to handle class
, function () {}
or lambda functions.
However, we want to allow this:
1, true, new Date(), ...
1 + 3
{a: 1}, {a: [b, {c: [d, e]}]}, ...
Its job is basically to understand a js expression as well as possible, and:
x
by context['x']
and
, or
, gt
, ... to help with writing expression in xmlMaybe we want to 'hide' all the crap in widget properly.
The examples code should be moved to gh-pages.
Reason:
Some accessibility issues may be discovered by Owl. We are not good at accessibility, and it is difficult to test, but it would be simple to give some warning whenever we have a clear case of a missing accessibility attribute.
In QWeb, whenever we compile a generic dom node, and only if we are in dev
mode, call a _validateAccessibility
method, which would do the following:
alt
attribute, generate a warning (unless it has a aria-labelledby
attribute)when a template has a t-on with a non existing method, we get this error message:
Uncaught (in promise) TypeError: Cannot read property 'bind' of undefined
The error should at least mention the template, and the method name
Would be nice to have a hook that is called before the DOM is patched on component re-render.
This is especially useful when dealing with scrollbar position.
Would be nice to define t-ref
dynamically, in particular when using t-foreach
with t-widget
.
Here's an example which would display "Loading..." until a thread is loaded with messages, and when so it would scroll to the last message in the thread:
const { Component, QWeb } = owl.core;
const { connect, Store } = owl.extras;
class Message extends Component {
template = "Message";
get message() {
return this.props.message;
}
scrollToVisibleBottom() {
this.el.scrollIntoView({
behavior: "smooth",
block: "end",
inline: "nearest",
});
}
}
class Thread extends Component {
template = "Thread";
widgets = { Message };
get messages() {
return this.thread.messages;
}
get thread() {
return this.props.thread;
}
componentDidUpdate() {
if (this.thread.isLoaded) {
// scroll to last message
const lastMessageID = Math.max(...this.messages.map(msg => msg.id));
this.refs[`message_${lastMessageID}`].scrollToVisibleBottom();
}
}
_onLoad() {
this.env.store.commit('load');
}
}
const mutations = {
load(state) {
Object.assign(state.thread, {
isLoaded: true,
messages: [{ id: 1, content: 'hello' }, { id: 2, content: 'world' }],
});
}
};
const state = {
thread: {
isLoaded: false,
messages: [],
},
};
const store = new Store({ state, mutations });
const ConnectedThread = connect(s => s)(Thread);
const env = {
qweb: new QWeb(TEMPLATES),
store,
};
const thread = new ConnectedThread(env);
thread.mount(document.body);
// click on "Load" button to load messages
<templates>
<div t-name="Thread">
<t t-if="!thread.isLoaded">
<span>Loading...</span>
<button t-on-click="_onLoad">Load</button>
</t>
<t t-else="">
<t t-foreach="messages" t-as="message" t-widget="Message" t-props="{message}" t-att-ref="'message_'+message.id"/>
</t>
</div>
<div t-name="Message">
<t t-esc="message.content"/>
</div>
</templates>
probably a hook that should be propagated after initial rerender
Example:
<button t-on="toggleFilter">Toggle</button>
It does nothing
<div class="hello" t-attf-class="'world'"/>
<div class="hello world"/>
<div class="world"/>
Use case: some widget that needs to setup something in its mounted hook depending on props (for example, playground app tabbededitor widget with ace editor).
In that case, we want to update the ace editor value when props are changed. However, if we use the updateProps method as a hook, we need to remember to return a value, otherwise, we will get a cryptic error.
and react to each state change.
With that, we can remove updateState method
In some way, a key is not really an attribute. It is just a way to give an identity to sub entities.
Should we, for symetry reasons, do as we did for t-ref and make the directive dynamic? always using t-key
instead of t-att-key
.
Not sure if this is the right place
The motivation is that we want to be able to do something with refs after a state change, but it may not be available yet. So, we wait for a tick.
Problem: this may not exactly be what we need. We actually want to wait for the next patched event of our component
or we will have memory leaks
const {Component, QWeb} = owl.core;
class HelloWorld extends Component {
constructor() {
super(...arguments);
this.list = ['a', 'b']
this.template = "demo.hello";
}
}
const qweb = new QWeb(TEMPLATES);
const hello = new HelloWorld({qweb}, { name: "World" });
hello.mount(document.body);
<templates>
<div t-name="demo.hello" t-debug="">
<t t-set="v" t-value="1"/>
<div t-foreach="list" t-as="elem">
<t t-esc="elem"/>
<span>v<t t-esc="v"/></span>
<t t-set="v" t-value="elem"/>
</div>
</div>
</templates>
Result: av1bv1
Expected: avbva
const {Component, QWeb} = owl.core;
class Child extends Component {
constructor() {
super(...arguments);
this.template = "child"
}
}
class App extends Component {
constructor() {
super(...arguments);
this.template = "app";
this.widgets = { Child };
}
}
const qweb = new QWeb(TEMPLATES);
const app = new App({qweb});
app.mount(document.body);
<templates>
<div t-name="app">
<div t-if="0"/>
<t t-else="" t-widget="Child" t-props="{ test: 0 }"/>
</div>
<div t-name="child">
<t t-esc="props.test"/>
</div>
</templates>
Crashes with the following error:
Invalid generated code while compiling template 'app': Unexpected token )
It currently says "cannot read property MyWidget of undefined. Would be better to have some message such as 'Cannot find the definition of the widget MyWidget', or something like that
class modules?
For ex, <t t-ess="something"/>
should crash, so we know that there is a type in the directive name
With the benchmarks, if we add 10k widgets a few times (without refreshing), we get:
it looks like adding 10k widgets is an operation that can be done in a roughly constant time. Maybe snabbdom has an issue.
We currently have:
What about this instead:
Or maybe this:
With this code
class Slow extends owl.core.Component {
template = "Slow"
willStart() {
const delay = this.props.delay;
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve();
}, delay);
});
}
async render() {
const delay = this.props.delay;
await new Promise(function(resolve, reject) {
setTimeout(function() {
resolve();
}, delay);});
return super.render()
}
}
class B extends owl.core.Component {
template = "B";
widgets = { Slow };
}
class C extends owl.core.Component {
template = "C";
widgets = { Slow };
}
class A extends owl.core.Component {
template = "A";
widgets = {B, C};
state={b: 1000, c: 2000};
updateB() {
this.updateState({b: this.state.b + 1200});
}
updateC() {
this.updateState({c: this.state.c + 1200});
}
updateBC() {
this.updateState({b: this.state.b + 1200, c: this.state.c + 1200});
}
}
const env = {
qweb: new owl.core.QWeb(TEMPLATES)
};
const app = new A(env);
app.mount(document.body);
<templates>
<div t-name="Slow">Slow: <t t-esc="props.delay"/></div>
<div t-name="B">Widget B: <t t-esc="props.value"/><t t-widget="Slow" t-props="{delay: props.value}"/></div>
<div t-name="C">Widget C: <t t-esc="props.value"/><t t-widget="Slow" t-props="{delay: props.value}"/></div>
<div t-name="A">
<t t-widget="B" t-props="{value: state.b}"/>
<div><button t-on-click="updateB">Update B</button></div>
<div><button t-on-click="updateC">Update C</button></div>
<div><button t-on-click="updateBC">Update BC</button></div>
<t t-widget="C" t-props="{value: state.c}"/>
</div>
</templates>
we observe a (maybe) problematic situation: updating both B and C widgets will be done in two steps. Need to think if that is ok
I think that we want an indent size of 4 for JS/css and 2 for xml
add a section to compare: owl/react/vue on various aspects:
The code is written in typescript. But the exported file should (?) be compiled to ES5 code. Also, I think that qweb should be adapted to output ES5 compatible code. I think that it will basically only need to change let
into var
need to remove xml comments
We can optimise the code compiled by qweb/t-widget to be smaller and faster in some frequent cases: if a given widget does not implement a specific hook, then it does not need to be set in the output of the t-widget directive.
For example, the willPatch hook is likely to be quite rarely used.
The main problem with this is to determine at compile-time what hooks are implemented. This is currently impossible, given that the actual widget is not given to the template compiler. However, we can do that. I suppose that the compiled template can depend on the widget class (but this raises the question: what if two widgets share the same template? should we authorize that? Or maybe we can have a special keyword to enable/disable widget-dependant template compilation)
It is currently just not done...
Currently, the store only tracks changes to a given object/array. However, it is convenient, in many cases, to be able to connect a component to the store with a simple mapStateToProps which observer for example a list of objects, and be able to rerender the full list if a key inside an object changed.
To enable this in a 'performant' way is possible, but requires changes to the store: each object/array should keep track of more metadata.
It currently only know about the current rev number, but it would need more info: the combined rev number of its children. To be able to do that, it will therefore need a reference to is 'parent' data element.
Notice that:
<t t-set="v" t-value="value + 'str'"/>
<t t-esc="v"/>
<t t-esc="v"/>
is compiled into:
var e4 = context['value'] + 'str';
if (e4 || e4 === 0) {
c2.push({text: e4});
}
var e5 = context['value'] + 'str';
if (e5 || e5 === 0) {
c2.push({text: e5});
}
Notice that the t-value expression is evaluated twice. This is most likely not what most people would expect when they use t-set. We should make sure it only evaluates the expression once.
and use it from node_modules (?)
Currently, owl exports this:
export const core = {
QWeb,
EventBus,
Component,
PureComponent,
utils
};
export const extras = {
Store,
connect,
Registry
};
Maybe we should simplify by doing this instead:
export default {
QWeb,
EventBus,
Component,
PureComponent,
utils,
Store,
connect,
Registry
};
This means that it is slightly easier to use some part of owl:
class SomeComponent extends owl.Component {
...
}
const {Component, QWeb} = owl;
class Item extends Component {
template = "item";
}
class App extends Component {
template = "app";
widgets = { Item };
constructor(...args) {
super(...args);
this.state = {
list: [],
next: 1,
};
}
willPatch() {
console.log('willPatch:', this.state.list.length);
console.log('willPatch:', Object.keys(this.refs).length);
}
patched() {
console.log('patched:', this.state.list.length);
console.log('patched:', Object.keys(this.refs).length);
}
add() {
this.state.list.push(this.state.next);
this.state.next++;
}
}
const qweb = new QWeb(TEMPLATES);
const app = new App({qweb});
app.mount(document.body);
<templates>
<li t-name="item">
<t t-esc="props.item"/>
</li>
<div t-name="app" class="hello">
<ul>
<!--<li t-foreach="state.list" t-as="item" t-key="item" t-ref="item"><t t-esc="item"/></li>-->
<t t-foreach="state.list" t-as="item" t-widget="Item" t-props="{ item }" t-key="item" t-ref="item"/>
</ul>
<button t-on-click="add">Add</button>
</div>
</templates>
Click on button "Add".
willPatch: 1
willPatch: 0
patched: 1
patched: 1
willPatch: 1
willPatch: 1
patched: 1
patched: 1
The version without t-widget
works fine:
<div t-name="app">
<ul>
<li t-foreach="state.list" t-as="item" t-key="item" t-ref="item"><t t-esc="item"/></li>
<!--<t t-foreach="state.list" t-as="item" t-widget="Item" t-props="{ item }" t-key="item" t-ref="item"/>-->
</ul>
<button t-on-click="add">Add</button>
</div>
In a thread of messages, on re-render, willPatch()
can check whether the last message is visible, and auto-scroll to it if that is the case in patched()
.
We can use this.el.getElementByClassName('o_message')
as a workaround, but I feel like using refs
should be the recommended way to do it.
At least, there are inconsistencies with and without t-widget
.
const { Component, QWeb } = owl.core;
//------------------------------------------------------------------------------
// TodoList root widget
//------------------------------------------------------------------------------
class TodoList extends Component {
template = "todoapp";
state = {filter: 'all', nextId: 1, todos: []};
addTodo(ev) {
if (ev.keyCode === 13) {
const title = ev.target.value;
if (title.trim()) {
const id = this.state.nextId++;
const todo = { id, title, completed: false };
const todos = this.state.todos.slice();
todos.push(todo);
this.updateState({todos})
}
ev.target.value = "";
}
}
toggleTodo({id}) {
const todo = this.state.todos.find(t => t.id === id);
todo.completed = !todo.completed;
this.updateState({todos: this.state.todos});
}
toggleFilter() {
const newFilter = this.state.filter === 'all' ? 'active' : 'all';
this.updateState({filter: newFilter});
}
get todos() {
let todos = this.state.todos;
if (this.state.filter === 'active') {
todos = todos.filter(t => !t.completed)
}
return todos
}
}
//------------------------------------------------------------------------------
// App Initialization
//------------------------------------------------------------------------------
const qweb = new QWeb(TEMPLATES);
const env = {
qweb,
};
const app = new TodoList(env);
app.mount(document.body);
<templates>
<div t-name="todoapp">
<input autofocus="true" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
<div>
<span>Filter: <t t-esc="state.filter"/></span>
<button t-on-click="toggleFilter">Toggle</button>
</div>
<ul>
<li class="todo" t-foreach="todos" t-as="todo">
<input type="checkbox" t-att-checked="todo.completed" t-on-change="toggleTodo(todo)"/>
<span t-att-class="{completed: todo.completed}"><t t-esc="todo.id"/>. <t t-esc="todo.title"/></span>
</li>
</ul>
</div>
</templates>
Result: it actually disappear, but the item B checkbox is now checked. However, the item B internal state is correct
Here's an app that displays todos and group them by date:
const { Component, QWeb } = owl.core;
class App extends Component {
constructor() {
super(...arguments);
this.template = "app";
}
get todos() {
return [
{ content: 'todo1', date: '10-01-2019' },
{ content: 'todo2', date: '11-01-2019' },
{ content: 'todo3', date: '12-01-2019' }
];
}
dateDay(todo) {
if (todo.date.substring(0,2) === '12') {
return "Today";
} else if (todo.date.substring(0, 2) === '11') {
return "Yesterday";
}
return todo.date;
}
}
const qweb = new QWeb(TEMPLATES);
const app = new App({ qweb });
app.mount(document.body);
<templates>
<div t-name="app">
<t t-set="current_date" t-value="0"/>
<t t-foreach="todos" t-as="todo">
<t t-set="todo_date" t-value="dateDay(todo)"/>
<div t-if="current_date !== todo_date" class="o_date_separator">
<span><t t-esc="todo_date"/></span>
<t t-set="current_date" t-value="todo_date"/>
</div>
</t>
</div>
</templates>
It produces the following template and crashes because of it:
(function anonymous(context,extra
) {
context = Object.create(context);
var h = this.utils.h;
var c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var _2 = context['todos'];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
if (typeof _2 === 'number') { _2 = Array.from(Array(_2).keys())}
var _3 = _2 instanceof Array ? _2 : Object.keys(_2);
var _4 = _2 instanceof Array ? _2 : Object.values(_2);
for (let i = 0; i < _3.length; i++) {
context.todo_first = i === 0;
context.todo_last = i === _3.length - 1;
context.todo_parity = i % 2 === 0 ? 'even' : 'odd';
context.todo_index = i;
context.todo = _3[i];
context.todo_value = _4[i];
if (0 !== dateDay(todo)) {
var _5 = 'o_date_separator';
var c6 = [], p6 = {key:6,attrs:{class: _5}};
var vn6 = h('div', p6, c6);
c1.push(vn6);
var c7 = [], p7 = {key:7};
var vn7 = h('span', p7, c7);
c6.push(vn7);
var e8 = context['dateDay'](context['todo']);
if (e8 || e8 === 0) {
c7.push({text: e8});
}
}
}
return vn1;
})
Looks like the compiled template has t-set
inside t-if
, although the uncompiled template does the opposite.
The template above works correctly with non-owl qweb, and dateDay()
may compute momentjs
objects, which are heavy thus would be nice to use t-set
in the template.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.