Skip to content
Home » My Blog Tutorial » OWL Tutorial: TodoApp-Part2

OWL Tutorial: TodoApp-Part2

owl

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

  1. Adding Tasks (Part 1)
  2. Adding Tasks (Part 2)
  3. Toggling Tasks
  4. Deleting Tasks
  5. Using a Store

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

  1. 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.
  1. How do I mark tasks as completed?
  • Click the checkbox next to the task to mark it as completed.
  1. How do I delete tasks?
  • Click the trash icon next to the task to delete it from the list.
  1. 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.
  1. Where can I learn more about the Owl framework?

Discover more from teguhteja.id

Subscribe to get the latest posts sent to your email.

1 thought on “OWL Tutorial: TodoApp-Part2”

  1. Pingback: Owl: Odoo Framework 101 - teguhteja.id

Leave a Reply

Optimized by Optimole
WP Twitter Auto Publish Powered By : XYZScripts.com

Discover more from teguhteja.id

Subscribe now to keep reading and get access to the full archive.

Continue reading