13. 1. Setting up the projecthooks, slots, store and more
hooks, slots, store and more
github.com/ged-odoo/owl-todolist
14. 2. Adding the first
component
const { Component } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
// Owl Components
class App extends Component {
static template = xml`<div>todo app</div>`;
}
// Setup code
function setup() {
const app = new App();
app.mount(document.body);
}
whenReady(setup);
➔ Components are class, inherit from
Component
➔ An Owl application is a (dynamic) tree
of components, with a root
➔ It is mounted with the mount method
➔ The mount operation completes
when all the sub component tree (if
any) are ready
➔ Templates are inlined here
17. 5. Extracting task as a
subcomponent
➔ Most UI are divided into sub
components
➔ Components are described by xml
elements with a capital first letter
➔ Components attributes are given to
actual sub component instance
(props)
const TASK_TEMPLATE = xml`
<div class="task"
t-att-class="props.task.isCompleted ? 'done':''">
<input type="checkbox"
t-att-checked="props.task.isCompleted"/>
<span><t t-esc="props.task.title"/></span>
</div>`;
class Task extends Component {
static template = TASK_TEMPLATE;
}
const APP_TEMPLATE = xml`
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>`;
class App extends Component {
static template = APP_TEMPLATE;
static components = { Task };
tasks = TASKS;
}
18. UI as a tree of components
Root State
A B C
State
State
Props
D EProps
State
Props
19. State
class App extends Component {
tasks = TASKS;
...
}
➔ Owned by a component
➔ May be reactive or not
➔ Only owner can update it
➔ Can give reference to other
components (as props)
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
Props
➔ Can be accessed in this.props
➔ Owned by someone else
➔ Props cannot be updated by
child component
20. 6. Adding tasks
<div class="todo-app">
<input
placeholder="Enter a new task"
t-on-keyup="addTask"/>
<div class="task-list">
...
</div>
</div>
➔ Can add event handler with t-on-
directive
➔ Handler receive event as first
argument
➔ Handler need to be a function in the
rendering context (the component
instance)
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const title =
ev.target.value.trim();
ev.target.value = "";
console.log('adding task', title);
// todo
}
}
21. 6. Adding tasks
<input
placeholder="Enter a new task"
t-on-keyup="addTask"
t-ref="add-input"/>
➔ Get reference with t-ref directive
➔ Can use mounted lifecycle method
➔ UseRef is our first hook
const { useRef } = owl.hooks;
class App extends Component {
inputRef = useRef("add-input");
mounted() {
this.inputRef.el.focus();
}
...
}
23. 6. Adding tasks
class App extends Component {
...
nextId = 1;
tasks = [];
...
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const title =
ev.target.value.trim();
ev.target.value = "";
if (title) {
const newTask = {
id: this.nextId++,
title: title,
isCompleted: false,
};
this.tasks.push(newTask);
}
}
}
}
➔ Generate some id number
➔ Add the task info to the task list
But… It does not work!
25. 6. Adding tasks
// on top of the file
const { useRef, useState } = owl.hooks;
class App extends Component {
tasks = useState([]);
...
}
➔ Make the state reactive with
useState hook
➔ It works with arrays and objects
(even nested)
27. Communication
➔ Propagate events from
bottom to top
➔ Use trigger method to
generate native event
➔ Use t-on- directive to catch
them
➔ Event payload is available
in the event.detail key
App
Task 2Task 1
28. 7. Toggling tasks class Task extends Component {
toggleTask() {
this.trigger('toggle-task', {
id: this.props.task.id
});
}
}
// In App
<div class="task-list" t-on-toggle-
task="toggleTask">
class App extends Component {
toggleTask(ev) {
const task = this.tasks.find(t => t.id
=== ev.detail.id);
task.isCompleted = !task.isCompleted;
}
}
➔ Trigger event in Task
➔ Catch it in App
➔ Modify state in App
➔ Notice the event payload is actually
set in ev.detail
29. 8. Deleting Tasks
<div class="task" ...>
...
<span class="delete" t-on-click="deleteTask"> </span>
</div>
class Task extends Component {
deleteTask() {
this.trigger('delete-task', {id: ...});
}
}
<div class="task-list"
t-on-toggle-task="toggleTask"
t-on-delete-task="deleteTask">
class App extends Component {
deleteTask(ev) {
const index = this.tasks.findIndex(t => t.id ===
ev.detail.id);
this.tasks.splice(index, 1);
}
}
➔ Add event handler in some
component
➔ Trigger event with proper payload
➔ Catch event in some parent
component
➔ Properly update state
Very common workflow:
30. 9. Saving state to local
storage
class App extends Component {
constructor() {
super(...arguments);
const tasks = localStorage.getItem("todoapp");
if (tasks) {
for (let task of JSON.parse(tasks)) {
this.tasks.push(task);
this.nextId = Math.max(this.nextId, task.id + 1);
}
}
}
addTask(ev) {
...
localStorage.setItem("todoapp",
JSON.stringify(this.tasks));
}
toggleTask(ev) {
...
localStorage.setItem("todoapp",
JSON.stringify(this.tasks));
}
deleteTask(ev) {
...
localStorage.setItem("todoapp",
JSON.stringify(this.tasks));
}
}
➔ Need to load tasks in constructor
➔ And update localstorage whenever
an update is done
➔ Duplication is bad. We’ll solve that
later
31. What if we need
to test?
The App component uses localStorage.
Difficult to test.
33. 10. Dependency Injection
class App extends Component {
constructor(localStorage) {
super();
this.localStorage = localStorage;
// replace all uses of localStorage by
this.localStorage
...
}
}
const app = new App(window.localStorage);
app.mount(document.body);
➔ Give localStorage instance in the
constructor
➔ Use it inside the App component
But, it only works with the root
component!
34. Owl solution: the env
App env
A
env
B
env
C
env
D
env
props props
props props
35. 10. Dependency Injection
class App extends Component {
constructor() {
super();
const tasks =
this.env.localStorage.getItem("todoapp");
...
}
// replace all uses of this.localStorage
by this.env.localStorage
}
App.env.localStorage = window.localStorage;
const app = new App();
app.mount(document.body);
➔ Setup the App env before creating it
➔ It is now available in this.env for the
whole application
36. Issues:
● Some code duplication (with localstorage)
● App component does too much
● State management is all over the place
● Localstorage code is all over the place
37. 11. Extracting concerns
We want components to be mostly
about UI (look and feel + event handling)
So, we want to extract:
➔ Task state management
➔ Persistence (local storage)
env
StoredTaskModel
TaskModel
App env
Task 1 env Task 2 env
39. 11. Extracting concerns
class App extends Component {
constructor() {
super();
const model = new StoredTaskModel(
this.env.localStorage
);
model.on('update', this, this.render);
useSubEnv({model});
}
// access model with this.env.model (or
env.model in template)
}
class Task extends Component {
static template = TASK_TEMPLATE;
toggleTask() {
this.env.model.toggleTask(this.props.task.id);
}
deleteTask() {
this.env.model.deleteTask(this.props.task.id);
}
}
40. ● Use composition (for StoredTaskmodel)
● Inject model in App
● Use a Store
● Make local storage key configurable
● Add filters
● Allow edition
github.com/ged-odoo/owl-todolist
Two years ago, I was working on the Odoo javascript codebase. I was sad, depressed, hopeless, lonely, cold. Why was it so frustrating to combine widgets together? To build an user interface. To add event listeners and properly clean them up? My life was dark and empty. No joy, nothing. But then, we imagined what the perfect developer experience should be. And so was born Owl, in an explosion of light…
Ok, I am lying. But the point is that working with Odoo javascript has been a mixed experience in the past. Full of joy, but sometimes, frustrating. This is why we introduced last year a new javascript framework, named Owl, as a first step to solve our issues.
For a little less than one year, we used Owl for all our new javascript code. For example, the new spreadsheet component in v14 is built entirely with owl. So far, it looks like the technology is really solid and is a huge improvement compared to what we had before.
My name is Géry Debongnie, I am a lead developer at Odoo, working mostly on javascript projects, and today, for this technical talk, I will introduce the fundamentals of Owl.
Like all modern applications, Odoo has a need for a good UI building block system, or framework. It is clear that the state of the art moved toward declarative programming, with good reasons.
All modern frameworks are declarative at their core. But the Odoo widget system was designed a long time ago, and we feel the pain of maintaining a complex application. So,
it is clear that odoo needs a better UI framework.
And then, Odoo decided to make Owl. I think it is worth taking a few minutes explaining why. We are aware that creating a new framework from scratch seems like a terrible idea. Why not use React, or Vue? This is clearly
very true in 99% of the time an existing framework fits your needs. But then
it sometimes happens that we have different needs.
Let me highlight 3 good reasons why we could not use one of these well-known, well maintained existing frameworks.
blabla
This is a really unusual requirement in most web applications, but it is very important for Odoo.
This is more a self-imposed constraint, because we could actually change the way we manage assets. It is however not trivial, since Odoo is modular, and a lot of work is required to make it work in a natural/efficient way
Owl is a powerful tool. It solves all our main needs. But as with all tools, it is designed to solve some kind of problems, and may not be appropriate for other situations.
So, let us discuss what Owl is designed for, and what it is not...
Owl is not designed to solve all your architectural issues. It is not angular. Owl is designed to be kind of low level abstraction. It wants to stay out of your way. You can design an application the way you like, Owl will just give you some building blocks, and go along with your design.
It is not intended to solve all problems.
Finally, Owl provides powerful abstractions, such as declarative UI, hooks, slots, store and more. It takes the best ideas from all other frameworks and blend them together in a coherent experience. I can tell you that learning and using Owl was quite easy for all our developers which had some React or Vue experience.
very proud of this… really powerful, implemented with a concept coming from react: the fiber. probably the most technically difficult part of owl.
and the best part of it is that it is mostly transparent for developers. If some component chose to implement one of the asynchronous lifecycle method, then all its parents will properly coordinate with it. No code change is required. The asynchronous behaviour will propagate automatically to all parent components.
Now, enough theory. Let us do something with our hands… It’s time to actually use Owl out there in the real world! We will build a real application to discuss some interesting owl concepts and ideas
we are going to make a simple todo list… This component will manage of list of tasks, and allow the user to add/toggle and delete tasks. this is just going to be an excuse to introduce various ideas and concepts defined by Owl to build an interface.
The code will be a little bit over engineered, but this is for the greater good
not very interesting nor owl specific. The kind of setup you will use in practice depends on your specific needs. For this talk, I will simply use a single html/js/css file structure.
The repo here contains all major steps for this talk as different commit, in case you are interested in playing with the code
Templates are inlined in this talk for
improve template style (t-key on same line, 2 space indent, …)
Not everything in an owl application need to be a componenet.
This StoredTaskModel uses inheritance. Maybe we should use composition instead. or mixins or whatever you prefer!
File is currently 130 lines (so, JS + templates)
As you can see, there are a lot of improvements that we can still do. Such is life, always imperfect. In this talk, we explored a lot of interesting ideas that you can use while working with Owl. I hope that this talk was useful to you and that you now feel empowered to make great things with the Owl framework.
Thank you for your attention!
Oh, if you are interested in the challenges of working with owl inside odoo, there is a talk on that topic coming up right now.
Thank you again!