Розділ 1: Компоненти Owl

У цьому розділі представлено фреймворк Owl, спеціально розроблену систему компонентів для Odoo. Основними структурними блоками OWL є компоненти та шаблони.

В Owl кожна частина користувацького інтерфейсу керується компонентом: вони зберігають логіку та визначають шаблони, що використовуються для відображення користувацького інтерфейсу. На практиці компонент представлений невеликим класом JavaScript, що є підкласом класу Component.

Щоб розпочати, вам знадобиться запущений сервер Odoo та налаштоване середовище розробки. Перш ніж розпочати вправи, переконайтеся, що ви виконали всі кроки, описані в цьому вступ до посібника.

Порада

Якщо ви використовуєте Chrome як веббраузер, ви можете встановити розширення Owl Devtools. Це розширення надає багато функцій, які допоможуть вам зрозуміти та створити профіль будь-якої програми Owl.

Відео: Як використовувати DevTools

У цьому розділі ми використовуємо додаток awesome_owl, який забезпечує спрощене середовище, що містить лише Owl та кілька інших файлів. Мета полягає в тому, щоб вивчити сам Owl, не покладаючись на код веб-клієнта Odoo.

Рішення для кожної вправи цього розділу розміщені в офіційний репозиторій навчальних посібників Odoo. Рекомендується спробувати спочатку вирішити їх, не дивлячись на розв’язок!

Приклад: компонент Counter

Спочатку розглянемо простий приклад. Компонент Counter, показаний нижче, - це компонент, який зберігає внутрішнє числове значення, відображає його та оновлює щоразу, коли користувач натискає кнопку.

/** @odoo-module **/

import { Component, useState } from "@odoo/owl";

export class Counter extends Component {
    static template = "my_module.Counter";

    setup() {
        this.state = useState({ value: 0 });
    }

    increment() {
        this.state.value++;
    }
}

Компонент Counter визначає назву шаблону, що представляє його html. Він написаний у XML мовою QWeb:

<templates xml:space="preserve">
   <t t-name="my_module.Counter">
      <p>Counter: <t t-esc="state.value"/></p>
      <button class="btn btn-primary" t-on-click="increment">Increment</button>
   </t>
</templates>

1. Відображення лічильника

../../../_images/counter.png

Як першу вправу, давайте змінимо компонент Playground, розташований у awesome_owl/static/src/, щоб перетворити його на лічильник. Щоб побачити результат, ви можете перейти за маршрутом /awesome_owl у вашому браузері.

  1. Змініть playground.js так, щоб він діяв як лічильник, як у наведеному вище прикладі. Залиште Playground як назву класу. Вам потрібно буде використовувати хук useState, щоб компонент повторно відображався щоразу, коли змінюється будь-яка частина об’єкта стану, який був прочитаний цим компонентом.

  2. У тому ж компоненті створіть метод increment.

  3. Змініть шаблон у playground.xml так, щоб він відображав вашу змінну лічильника. Використовуйте t-esc для виведення даних.

  4. Додайте кнопку в шаблон і вкажіть атрибут t-on-click в кнопці, щоб запускати метод increment щоразу, коли на кнопці натискається кнопка.

Важливо

Не забудьте /** @odoo-module **/ у ваших JavaScript-файлах. Більше інформації про це можна знайти тут.

Порада

JavaScript-файли Odoo, завантажені браузером, мініфіковані. Для цілей налагодження простіше, коли файли не мініфіковані. Перейдіть у режим налагодження з активами, щоб файли не мініфікувалися.

Ця вправа демонструє важливу особливість Owl: система реактивності. Функція useState обгортає значення в проксі, щоб Owl міг відстежувати, який компонент потребує яку частину стану, і таким чином його можна оновлювати щоразу, коли значення змінюється. Спробуйте видалити функцію useState і подивіться, що станеться.

2. Вилучити Counter у підкомпоненті

Наразі у нас є логіка лічильника в компоненті Playground, але він не може бути використаний повторно. Давайте подивимося, як створити з нього підкомпонент:

  1. Витягніть код лічильника з компонента Playground у новий компонент Counter.

  2. Спочатку ви можете зробити це в тому ж файлі, але після цього оновіть свій код, щоб перемістити Counter в окрему папку та файл. Імпортуйте його відносно з ./counter/counter. Переконайтеся, що шаблон знаходиться в окремому файлі з тою самою назвою.

  3. Використайте <Counter/> у шаблоні компонента Playground, щоб додати два лічильники на ваш ігровий майданчик.

../../../_images/double_counter.png

Порада

За домовленістю, більшість кодів компонентів, шаблонів та CSS повинні мати таку ж назву, що починається з великої літери, як і сам компонент. Наприклад, якщо у нас є компонент TodoList, його код повинен бути у todo_list.js, todo_list.xml та, за необхідності, todo_list.scss

3. Простий компонент Card

Компоненти – це справді найприродніший спосіб розділити складний користувацький інтерфейс на кілька частин, які можна використовувати повторно. Але щоб зробити їх справді корисними, необхідно мати можливість обмінюватися певною інформацією між ними. Давайте розглянемо, як батьківський компонент може надавати інформацію підкомпоненту за допомогою атрибутів (найбільш відомих як властивості).

Мета цієї вправи - створити компонент Card, який приймає два props: title та content. Наприклад, ось як це можна використовувати:

<Card title="'my title'" content="'some content'"/>

У наведеному вище прикладі за допомогою bootstrap має бути створено html-код, який виглядатиме ось так:

<div class="card d-inline-block m-2" style="width: 18rem;">
    <div class="card-body">
        <h5 class="card-title">my title</h5>
        <p class="card-text">
         some content
        </p>
    </div>
</div>
  1. Створіть компонент Card

  2. Імпортуйте його в Playground та відобразіть кілька карток у його шаблоні

../../../_images/simple_card.png

4. Використання markup для відображення html

Якщо ви використовували t-esc у попередній вправі, то, можливо, помітили, що Owl автоматично екранує свій вміст. Наприклад, якщо ви спробуєте відобразити деякий html-код ось так: <Card title="'my title'" content="this.html"/> з this.html = "<div>some content</div>"", отриманий результат просто відобразить html-код як рядок.

У цьому випадку, оскільки компонент Card може бути використаний для відображення будь-якого типу контенту, має сенс дозволити користувачеві відображати деякий html-код. Це робиться за допомогою директиви t-out.

Однак, відображення довільного контенту у форматі html є небезпечним, його можна використовувати для впровадження шкідливого коду, тому за замовчуванням Owl завжди екрануватиме рядок, якщо він не був явно позначений як безпечний за допомогою функції markup.

  1. Оновіть Card, щоб використовувати t-out

  2. Оновіть Playground для імпорту markup та використовуйте його для деяких значень html

  3. Переконайтеся, що звичайні рядки завжди екрануються, на відміну від розмічених рядків.

Примітка

Директиву t-esc все ще можна використовувати в шаблонах Owl. Вона трохи швидша за t-out.

../../../_images/markup.png

5. Перевірка реквізиту

Компонент Card має неявний API. Він очікує отримати два рядки у своїх власстивостях: title та content. Давайте зробимо цей API більш явним. Ми можемо додати визначення властивостей, яке дозволить Owl виконувати крок перевірки в режимі розробки. Ви можете активувати режим розробки в конфігурації програми (але він активується за замовчуванням на ігровому майданчику awesome_owl).

Це гарна практика - проводити перевірку властивостей для кожного компонента.

  1. Додайте перевірка властивостей до компонента Card.

  2. Перейменуйте властивості title на щось інше в шаблоні ігрового майданчика, а потім перевірте вкладку Console інструментів розробника вашого браузера, чи не бачите ви помилку.

6. Сума двох Counter

У попередній вправі ми бачили, що props можна використовувати для передачі інформації від батьківського компонента до дочірнього. Тепер давайте подивимося, як ми можемо передавати інформацію у зворотному напрямку: у цій вправі ми хочемо відобразити два компоненти Counter, а під ними суму їхніх значень. Отже, батьківський компонент (Playground) потрібно інформувати щоразу, коли змінюється одне зі значень Counter.

Це можна зробити за допомогою властивості зворотний виклик властивості: властивості, яка є функцією, призначеною для зворотного виклику. Дочірній компонент може викликати цю функцію з будь-яким аргументом. У нашому випадку ми просто додамо додаткову властивість onChange, яка буде викликатися щоразу, коли компонент Counter збільшується.

  1. Додайте перевірку властивості до компонента Counter: він повинен приймати необов’язкову властивість функції onChange.

  2. Оновіть компонент Counter, щоб він викликав властивість onChange (якщо вона існує) щоразу, коли вона збільшується.

  3. Змініть компонент Playground, щоб він зберігав локальне значення стану (sum), початково встановлене на 2, та відображав його в шаблоні.

  4. Реалізуйте метод incrementSum у Playground

  5. Надайте цей метод як функцію-реквізит для двох (або більше!) підкомпонентів Counter.

../../../_images/sum_counter.png

Важливо

Існує тонкість з властивостями зворотного виклику: зазвичай їх слід визначати з суфіксом .bind. Див. документація.

7. Список справ

Давайте тепер розглянемо різні можливості Owl, створивши список справ. Нам потрібні два компоненти: компонент TodoList, який відображатиме список компонентів TodoItem. Список справ – це стан, який має підтримуватися TodoList.

У цьому посібнику todo – це об’єкт, який містить три значення: id (число), description (рядок) та прапорець isCompleted (логічне значення):

{ id: 3, description: "buy milk", isCompleted: false }
  1. Створіть компоненти TodoList та TodoItem.

  2. Компонент TodoItem повинен отримувати todo як проп та відображати його id та description у div.

  3. Наразі, жорстко закодуйте список завдань:

    // in TodoList
    this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
    
  4. Використовуйте t-foreach для відображення кожного завдання в TodoItem.

  5. Відобразити TodoList на ігровому майданчику.

  6. Додати перевірку властивостей до TodoItem.

../../../_images/todo_list.png

Порада

Оскільки компоненти TodoList та TodoItem настільки тісно пов’язані, має сенс помістити їх в одну папку.

Примітка

Директива t-foreach в Owl не зовсім ідентична реалізації QWeb на Python: вона вимагає унікального значення t-key, щоб Owl міг належним чином узгодити кожен елемент.

8. Використовуйте динамічні атрибути

Наразі компонент TodoItem візуально не показує, чи todo виконано. Зробимо це за допомогою динамічні атрибути.

  1. Додайте класи Bootstrap text-muted та text-decoration-line-through до кореневого елемента TodoItem, якщо він завершений.

  2. Змініть жорстко закодоване значення this.todos, щоб перевірити, чи воно відображається правильно.

Хоча директива має назву t-att (що означає атрибут), її можна використовувати для встановлення значення class (та властивостей html, таких як value вхідних даних).

../../../_images/muted_todo.png

Порада

Owl дозволяє поєднувати статичні значення класу з динамічними значеннями. Наступний приклад працюватиме належним чином:

<div class="a" t-att-class="someExpression"/>

Див. також: Owl: Атрибути динамічного класу

9. Додавання списку справ

Поки що справи в нашому списку жорстко закодовані. Давайте зробимо його кориснішим, дозволивши користувачеві додавати справи до списку.

  1. Видаліть жорстко закодовані значення в компоненті TodoList:

    this.todos = useState([]);
    
  2. Додайте поле введення над списком завдань за допомогою заповнювача Введіть нове завдання.

  3. Додайте обробник подій до події keyup з назвою addTodo.

  4. Реалізуйте addTodo для перевірки, чи було натиснуто Enter (ev.keyCode === 13), і в такому випадку створіть нове завдання з поточним вмістом вхідних даних як описом та очистіть вхідні дані від усього вмісту.

  5. Переконайтеся, що завдання має унікальний id. Це може бути просто лічильник, який збільшується на кожне завдання.

  6. Бонусний момент: не робіть нічого, якщо поле введення порожнє.

../../../_images/create_todo.png

Перегляньте також

Owl: Reactivity

Теорія: Життєвий цикл компонента та перехоплювачі

Досі ми розглянули один приклад функції-перехоплювача: useState. Hook - це спеціальна функція, яка підключається до внутрішніх механізмів компонента. У випадку useState, вона генерує проксі-об’єкт, пов’язаний з поточним компонентом. Ось чому функції-перехоплювачі повинні викликатися в методі setup, і не пізніше!

../../../_images/component_lifecycle.svg

Компонент Owl проходить багато фаз: його можна створювати екземпляри, рендерити, монтувати, оновлювати, від’єднувати, знищувати… Це життєвий цикл компонента. На рисунку вище показано найважливіші події в житті компонента (перехоплювачі показані фіолетовим кольором). Грубо кажучи, компонент створюється, потім оновлюється (можливо, багато разів), а потім знищується.

Owl надає різноманітні вбудовані функції-перехоплювачі. Усі вони мають бути викликані у функції setup. Наприклад, якщо ви хочете виконати якийсь код під час монтування компонента, ви можете використовувати перехоплбвач onMounted:

setup() {
  onMounted(() => {
    // do something here
  });
}

Порада

Усі функції-перехоплювачі починаються з use або on. Наприклад: useState або onMounted.

10. Фокусування вхідних даних

Давайте подивимося, як ми можемо отримати доступ до DOM за допомогою t-ref та useRef. Основна ідея полягає в тому, що вам потрібно позначити цільовий елемент у шаблоні компонента за допомогою t-ref:

<div t-ref="some_name">hello</div>

Тоді ви можете отримати до нього доступ в JS за допомогою перехоплювача useRef. Однак, якщо подумати, є проблема: фактичний елемент html для компонента не існує під час створення компонента. Він існує лише тоді, коли компонент монтується. Але перехоплювачі мають бути викликані в методі setup. Отже, useRef повертає об’єкт, який містить перехоплювач el (для елемента), який визначається лише тоді, коли компонент монтується.

setup() {
   this.myRef = useRef('some_name');
   onMounted(() => {
      console.log(this.myRef.el);
   });
}
  1. Фокусуйте input з попередньої вправи. Це слід зробити з компонента TodoList (зверніть увагу, що для елемента input html є метод focus).

  2. Бонусний момент: витягніть код у спеціалізований перехоплювач useAutofocus у новому файлі awesome_owl/utils.js.

../../../_images/autofocus.png

Порада

Посилання зазвичай мають суфікс Ref, щоб було зрозуміло, що вони є спеціальними об’єктами:

this.inputRef = useRef('input');

11. Перемикання справ

Тепер додамо нову функцію: позначати завдання як виконане. Насправді це складніше, ніж можна подумати. Власник стану - це не той самий компонент, який його відображає. Отже, компонент TodoItem повинен повідомити своєму батьківському компоненту, що стан завдання потрібно перемкнути. Один класичний спосіб зробити це - додати зворотний виклик властивості toggleState.

  1. Додайте вхідні дані з атрибутом type="checkbox" перед id завдання, який потрібно перевірити, якщо стан isCompleted є істинним.

    Порада

    Owl не створює атрибути, обчислені за допомогою директиви t-att, якщо вона обчислюється як хибне значення.

  2. Додайте функцію зворотного виклику toggleState до TodoItem.

  3. Додайте обробник події change на вхід у компоненті TodoItem та переконайтеся, що він викликає функцію toggleState з id справи.

  4. Зробіть так, щоб це спрацювало!

../../../_images/toggle_todo.png

12. Видалення справ

Останній штрих - дозволити користувачеві видалити справу.

  1. Додати новий зворотний виклик властивості removeTodo до TodoItem.

  2. Вставте <span class="fa fa-remove"/> у шаблон компонента TodoItem.

  3. Щоразу, коли користувач натискає на нього, він повинен викликати метод removeTodo.

  4. Зробіть так, щоб це спрацювало!

    Порада

    Якщо ви використовуєте масив для зберігання списку справ, ви можете скористатися функцією JavaScript splice, щоб видалити з нього справу.

// find the index of the element to delete
const index = list.findIndex((elem) => elem.id === elemId);
if (index >= 0) {
      // remove the element at index from list
      list.splice(index, 1);
}
../../../_images/delete_todo.png

13. Загальна Card зі слотами

У попередня вправа ми створили простий компонент Card. Але, чесно кажучи, він досить обмежений. Що робити, якщо ми хочемо відобразити довільний вміст всередині картки, наприклад, підкомпонент? Ну, це не працює, оскільки вміст картки описується рядком. Однак було б дуже зручно, якби ми могли описати вміст як фрагмент шаблону.

Саме для цього й розроблена система слот від Owl: вона дозволяє писати універсальні компоненти.

Давайте модифікуємо компонент Card для використання слотів:

  1. Видаліть властивість content.

  2. Використайте слот за замовчуванням для визначення тіла.

  3. Вставте кілька карток із довільним вмістом, наприклад, компонент Counter.

  4. (бонус) Додати перевірку властивостей.

../../../_images/generic_card.png

14. Мінімізація вмісту картки

Нарешті, давайте додамо функцію до компонента Card, щоб зробити його цікавішим: нам потрібна кнопка для перемикання його вмісту (показати або приховати).

  1. Додайте стан до компонента Card, щоб відстежувати, чи він відкритий (за замовчуванням) чи ні

  2. Додайте t-if до шаблону для умовного відображення контенту

  3. Додайте кнопку в заголовок і змініть код, щоб змінити стан кнопки при натисканні

../../../_images/toggle_card.png