Owl Tutorial – Welcome to the second part of building a TodoApp using the OWL Framework. In the first part, we covered setting up the project, adding the first component, displaying a list of tasks, and basic CSS layout. In this section, we will continue by adding features to create new tasks, mark tasks as completed, and delete tasks. We will also refactor the code by using a store for better task management.
Content
6. Adding Tasks (Part 1)
Owl Tutorial – We still use a list of hardcoded tasks. Now, it’s really time to give the user a way to add tasks himself. The first step is to add an input to the Root
component. This input will be outside the task list, so we need to adapt the Root
template, JavaScript, and CSS:
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask"/>
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>
addTask(ev) {
if (ev.keyCode === 13) {
const text = ev.target.value.trim();
ev.target.value = "";
console.log('adding task', text);
// todo
}
}
.todo-app {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.todo-app > input {
display: block;
margin: auto;
}
.task-list {
margin-top: 8px;
}
Now, we have a working input that logs to the console whenever the user adds a task. Notice that when you load the page, the input is not focused. However, adding tasks is a core feature of a task list, so let’s make it as fast as possible by focusing the input.
We need to execute code when the Root
component is ready (mounted). Let’s do that using the onMounted
hook. We will also need to get a reference to the input by using the t-ref
directive with the useRef
hook:
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
const { Component, mount, xml, useRef, onMounted } = owl;
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
}
This is a common situation: whenever we need to perform actions depending on the lifecycle of a component, we need to do it in the setup
method, using a lifecycle hook. Here, we first get a reference to the inputRef
, then in the onMounted
hook, we simply focus the HTML element.
7. Adding Tasks (Part 2)
Owl Tutorial – In the previous section, we did everything except implement the code that actually creates tasks! So, let’s do that now.
We need a way to generate unique id
numbers. To do that, we will simply add a nextId
number in App
. At the same time, let’s remove the demo tasks in App
:
nextId = 1;
tasks = [];
Now, the addTask
method can be implemented:
addTask(ev) {
if (ev.keyCode === 13) {
const text = ev.target.value.trim();
ev.target.value = "";
if (text) {
const newTask = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(newTask);
}
}
}
This almost works, but if you test it, you will notice that no new task is ever displayed when the user presses Enter
. However, if you add a debugger
or a console.log
statement, you will see that the code is actually running as expected. The problem is that Owl has no way of knowing that it needs to rerender the user interface. We can fix the issue by making tasks
reactive with the useState
hook:
const { Component, mount, xml, useRef, onMounted, useState } = owl;
tasks = useState([]);
It now works as expected!
8. Toggling Tasks
Owl Tutorial – If you tried to mark a task as completed, you may have noticed that the text did not change in opacity. This is because there is no code to modify the isCompleted
flag.
Now, this is an interesting situation: the task is displayed by the Task
component, but it is not the owner of its state, so ideally, it should not modify it. However, for now, that’s what we will do. In Task
, change the input
to:
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
and add the toggleTask
method:
toggleTask() {
this.props.task.isCompleted = !this.props.task.isCompleted;
}
9. Deleting Tasks
Let us now add the possibility to delete tasks. This is different from the previous feature: deleting a task has to be done on the task itself, but the actual operation needs to be done on the task list. So, we need to communicate the request to the Root
component. This is usually done by providing a callback in a prop.
First, let us update the Task
template, CSS, and JS:
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
<span><t t-esc="props.task.text"/></span>
<span class="delete" t-on-click="deleteTask">Delete</span>
</div>
.task {
font-size: 18px;
color: #111111;
display: grid;
grid-template-columns: 30px auto 30px;
}
.task > input {
margin: auto;
}
.delete {
opacity: 0;
cursor: pointer;
text-align: center;
}
.task:hover .delete {
opacity: 1;
}
static props = ["task", "onDelete"];
deleteTask() {
this.props.onDelete(this.props.task);
}
Now, we need to provide the onDelete
callback to each task in the Root
component:
<Task task="task" onDelete.bind="deleteTask"/>
deleteTask(task) {
const index = this.tasks.findIndex(t => t.id === task.id);
this.tasks.splice(index, 1);
}
Notice that the onDelete
prop is defined with a .bind
suffix: this is a special suffix that makes sure the function callback is bound to the component.
Notice also that we have two functions named deleteTask
. The one in the Task
component just delegates the work to the Root
component that owns the task list via the onDelete
property.
10. Using a Store
Looking at the code, it is apparent that all the code handling tasks is scattered around the application. Also, it mixes UI code and business logic code. Owl does not provide any high-level abstraction to manage business logic, but it is easy to do it with the basic reactivity primitives (useState
and reactive
).
Let us use it in our application to implement a central store. This is a pretty large refactoring (for our application), since it involves extracting all task-related code out of the components. Here is the new content of the app.js
file:
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
function useStore() {
const env = useEnv();
return useState(env.store);
}
// -------------------------------------------------------------------------
// TaskList
// -------------------------------------------------------------------------
class TaskList {
nextId = 1;
tasks = [];
addTask(text) {
text = text.trim();
if (text) {
const task = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(task) {
task.isCompleted = !task.isCompleted;
}
deleteTask(task) {
const index = this.tasks.findIndex((t) => t.id === task.id);
this.tasks.splice(index, 1);
}
}
function createTaskStore() {
return reactive(new TaskList());
}
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
class Task extends Component {
static template = xml`
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="() => store.toggle
Task(props.task)"/>
<span><t t-esc="props.task.text"/></span>
<span class="delete" t-on-click="() => store.deleteTask(props.task)">Delete</span>
</div>`;
static props = ["task"];
setup() {
this.store = useStore();
}
}
// -------------------------------------------------------------------------
// Root Component
// -------------------------------------------------------------------------
class Root extends Component {
static template = xml`
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="store.tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>`;
static components = { Task };
setup() {
const inputRef = useRef("add-input");
onMounted(() => inputRef.el.focus());
this.store = useStore();
}
addTask(ev) {
if (ev.keyCode === 13) {
this.store.addTask(ev.target.value);
ev.target.value = "";
}
}
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
const env = {
store: createTaskStore(),
};
mount(Root, document.body, { dev: true, env });
With this refactoring, we have a cleaner codebase where UI logic and business logic are separated. The TaskList
class handles the tasks’ logic, and the components interact with it through the store
.
By following these steps, we have built a fully functional Todo app using the Owl framework, showcasing its powerful reactivity system and component-based architecture. You can read more about Owl’s capabilities and features in the Owl Framework Documentation.
FAQs
- How do I add tasks in the Todo app?
- Type your task in the input field and press Enter to add it to the list.
- How do I mark tasks as completed?
- Click the checkbox next to the task to mark it as completed.
- How do I delete tasks?
- Click the trash icon next to the task to delete it from the list.
- What is the purpose of using a store in Owl?
- The store manages the application’s state and business logic, keeping the UI code clean and separated.
- Where can I learn more about the Owl framework?
- Visit the Owl Framework Documentation for more details and advanced features.
Discover more from teguhteja.id
Subscribe to get the latest posts sent to your email.
Pingback: Owl: Odoo Framework 101 - teguhteja.id