Розділ 1: Компоненти Owl¶
У цьому розділі представлено фреймворк Owl, спеціально розроблену систему компонентів для Odoo. Основними структурними блоками OWL є компоненти та шаблони.
В Owl кожна частина користувацького інтерфейсу керується компонентом: вони зберігають логіку та визначають шаблони, що використовуються для відображення користувацького інтерфейсу. На практиці компонент представлений невеликим класом JavaScript, що є підкласом класу Component.
Щоб розпочати, вам знадобиться запущений сервер Odoo та налаштоване середовище розробки. Перш ніж розпочати вправи, переконайтеся, що ви виконали всі кроки, описані в цьому вступ до посібника.
Порада
Якщо ви використовуєте Chrome як веббраузер, ви можете встановити розширення Owl Devtools. Це розширення надає багато функцій, які допоможуть вам зрозуміти та створити профіль будь-якої програми Owl.
У цьому розділі ми використовуємо додаток 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. Відображення лічильника¶
Як першу вправу, давайте змінимо компонент Playground, розташований у awesome_owl/static/src/, щоб перетворити його на лічильник. Щоб побачити результат, ви можете перейти за маршрутом /awesome_owl у вашому браузері.
Змініть
playground.jsтак, щоб він діяв як лічильник, як у наведеному вище прикладі. ЗалиштеPlaygroundяк назву класу. Вам потрібно буде використовувати хук useState, щоб компонент повторно відображався щоразу, коли змінюється будь-яка частина об’єкта стану, який був прочитаний цим компонентом.У тому ж компоненті створіть метод
increment.Змініть шаблон у
playground.xmlтак, щоб він відображав вашу змінну лічильника. Використовуйте t-esc для виведення даних.Додайте кнопку в шаблон і вкажіть атрибут t-on-click в кнопці, щоб запускати метод
incrementщоразу, коли на кнопці натискається кнопка.
Важливо
Не забудьте /** @odoo-module **/ у ваших JavaScript-файлах. Більше інформації про це можна знайти тут.
Порада
JavaScript-файли Odoo, завантажені браузером, мініфіковані. Для цілей налагодження простіше, коли файли не мініфіковані. Перейдіть у режим налагодження з активами, щоб файли не мініфікувалися.
Ця вправа демонструє важливу особливість Owl: система реактивності. Функція useState обгортає значення в проксі, щоб Owl міг відстежувати, який компонент потребує яку частину стану, і таким чином його можна оновлювати щоразу, коли значення змінюється. Спробуйте видалити функцію useState і подивіться, що станеться.
2. Вилучити Counter у підкомпоненті¶
Наразі у нас є логіка лічильника в компоненті Playground, але він не може бути використаний повторно. Давайте подивимося, як створити з нього підкомпонент:
Витягніть код лічильника з компонента
Playgroundу новий компонентCounter.Спочатку ви можете зробити це в тому ж файлі, але після цього оновіть свій код, щоб перемістити
Counterв окрему папку та файл. Імпортуйте його відносно з./counter/counter. Переконайтеся, що шаблон знаходиться в окремому файлі з тою самою назвою.Використайте
<Counter/>у шаблоні компонентаPlayground, щоб додати два лічильники на ваш ігровий майданчик.
Порада
За домовленістю, більшість кодів компонентів, шаблонів та 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>
Створіть компонент
CardІмпортуйте його в
Playgroundта відобразіть кілька карток у його шаблоні
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.
Оновіть
Card, щоб використовуватиt-outОновіть
Playgroundдля імпортуmarkupта використовуйте його для деяких значень htmlПереконайтеся, що звичайні рядки завжди екрануються, на відміну від розмічених рядків.
Примітка
Директиву t-esc все ще можна використовувати в шаблонах Owl. Вона трохи швидша за t-out.
5. Перевірка реквізиту¶
Компонент Card має неявний API. Він очікує отримати два рядки у своїх власстивостях: title та content. Давайте зробимо цей API більш явним. Ми можемо додати визначення властивостей, яке дозволить Owl виконувати крок перевірки в режимі розробки. Ви можете активувати режим розробки в конфігурації програми (але він активується за замовчуванням на ігровому майданчику awesome_owl).
Це гарна практика - проводити перевірку властивостей для кожного компонента.
Додайте перевірка властивостей до компонента
Card.Перейменуйте властивості
titleна щось інше в шаблоні ігрового майданчика, а потім перевірте вкладку Console інструментів розробника вашого браузера, чи не бачите ви помилку.
6. Сума двох Counter¶
У попередній вправі ми бачили, що props можна використовувати для передачі інформації від батьківського компонента до дочірнього. Тепер давайте подивимося, як ми можемо передавати інформацію у зворотному напрямку: у цій вправі ми хочемо відобразити два компоненти Counter, а під ними суму їхніх значень. Отже, батьківський компонент (Playground) потрібно інформувати щоразу, коли змінюється одне зі значень Counter.
Це можна зробити за допомогою властивості зворотний виклик властивості: властивості, яка є функцією, призначеною для зворотного виклику. Дочірній компонент може викликати цю функцію з будь-яким аргументом. У нашому випадку ми просто додамо додаткову властивість onChange, яка буде викликатися щоразу, коли компонент Counter збільшується.
Додайте перевірку властивості до компонента
Counter: він повинен приймати необов’язкову властивість функціїonChange.Оновіть компонент
Counter, щоб він викликав властивістьonChange(якщо вона існує) щоразу, коли вона збільшується.Змініть компонент
Playground, щоб він зберігав локальне значення стану (sum), початково встановлене на 2, та відображав його в шаблоні.Реалізуйте метод
incrementSumуPlaygroundНадайте цей метод як функцію-реквізит для двох (або більше!) підкомпонентів
Counter.
Важливо
Існує тонкість з властивостями зворотного виклику: зазвичай їх слід визначати з суфіксом .bind. Див. документація.
7. Список справ¶
Давайте тепер розглянемо різні можливості Owl, створивши список справ. Нам потрібні два компоненти: компонент TodoList, який відображатиме список компонентів TodoItem. Список справ – це стан, який має підтримуватися TodoList.
У цьому посібнику todo – це об’єкт, який містить три значення: id (число), description (рядок) та прапорець isCompleted (логічне значення):
{ id: 3, description: "buy milk", isCompleted: false }
Створіть компоненти
TodoListтаTodoItem.Компонент
TodoItemповинен отримуватиtodoяк проп та відображати йогоidтаdescriptionуdiv.Наразі, жорстко закодуйте список завдань:
// in TodoList this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
Використовуйте t-foreach для відображення кожного завдання в
TodoItem.Відобразити
TodoListна ігровому майданчику.Додати перевірку властивостей до
TodoItem.
Порада
Оскільки компоненти TodoList та TodoItem настільки тісно пов’язані, має сенс помістити їх в одну папку.
Примітка
Директива t-foreach в Owl не зовсім ідентична реалізації QWeb на Python: вона вимагає унікального значення t-key, щоб Owl міг належним чином узгодити кожен елемент.
8. Використовуйте динамічні атрибути¶
Наразі компонент TodoItem візуально не показує, чи todo виконано. Зробимо це за допомогою динамічні атрибути.
Додайте класи Bootstrap
text-mutedтаtext-decoration-line-throughдо кореневого елементаTodoItem, якщо він завершений.Змініть жорстко закодоване значення
this.todos, щоб перевірити, чи воно відображається правильно.
Хоча директива має назву t-att (що означає атрибут), її можна використовувати для встановлення значення class (та властивостей html, таких як value вхідних даних).
Порада
Owl дозволяє поєднувати статичні значення класу з динамічними значеннями. Наступний приклад працюватиме належним чином:
<div class="a" t-att-class="someExpression"/>
Див. також: Owl: Атрибути динамічного класу
9. Додавання списку справ¶
Поки що справи в нашому списку жорстко закодовані. Давайте зробимо його кориснішим, дозволивши користувачеві додавати справи до списку.
Видаліть жорстко закодовані значення в компоненті
TodoList:this.todos = useState([]);
Додайте поле введення над списком завдань за допомогою заповнювача Введіть нове завдання.
Додайте обробник подій до події
keyupз назвоюaddTodo.Реалізуйте
addTodoдля перевірки, чи було натиснуто Enter (ev.keyCode === 13), і в такому випадку створіть нове завдання з поточним вмістом вхідних даних як описом та очистіть вхідні дані від усього вмісту.Переконайтеся, що завдання має унікальний id. Це може бути просто лічильник, який збільшується на кожне завдання.
Бонусний момент: не робіть нічого, якщо поле введення порожнє.
Перегляньте також
Теорія: Життєвий цикл компонента та перехоплювачі¶
Досі ми розглянули один приклад функції-перехоплювача: useState. Hook - це спеціальна функція, яка підключається до внутрішніх механізмів компонента. У випадку useState, вона генерує проксі-об’єкт, пов’язаний з поточним компонентом. Ось чому функції-перехоплювачі повинні викликатися в методі setup, і не пізніше!
Компонент 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);
});
}
Фокусуйте
inputз попередньої вправи. Це слід зробити з компонентаTodoList(зверніть увагу, що для елемента input html є методfocus).Бонусний момент: витягніть код у спеціалізований перехоплювач
useAutofocusу новому файліawesome_owl/utils.js.
Порада
Посилання зазвичай мають суфікс Ref, щоб було зрозуміло, що вони є спеціальними об’єктами:
this.inputRef = useRef('input');
11. Перемикання справ¶
Тепер додамо нову функцію: позначати завдання як виконане. Насправді це складніше, ніж можна подумати. Власник стану - це не той самий компонент, який його відображає. Отже, компонент TodoItem повинен повідомити своєму батьківському компоненту, що стан завдання потрібно перемкнути. Один класичний спосіб зробити це - додати зворотний виклик властивості toggleState.
Додайте вхідні дані з атрибутом
type="checkbox"перед id завдання, який потрібно перевірити, якщо станisCompletedє істинним.Порада
Owl не створює атрибути, обчислені за допомогою директиви
t-att, якщо вона обчислюється як хибне значення.Додайте функцію зворотного виклику
toggleStateдоTodoItem.Додайте обробник події
changeна вхід у компонентіTodoItemта переконайтеся, що він викликає функціюtoggleStateз id справи.Зробіть так, щоб це спрацювало!
12. Видалення справ¶
Останній штрих - дозволити користувачеві видалити справу.
Додати новий зворотний виклик властивості
removeTodoдоTodoItem.Вставте
<span class="fa fa-remove"/>у шаблон компонентаTodoItem.Щоразу, коли користувач натискає на нього, він повинен викликати метод
removeTodo.Зробіть так, щоб це спрацювало!
Порада
Якщо ви використовуєте масив для зберігання списку справ, ви можете скористатися функцією 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);
}
13. Загальна Card зі слотами¶
У попередня вправа ми створили простий компонент Card. Але, чесно кажучи, він досить обмежений. Що робити, якщо ми хочемо відобразити довільний вміст всередині картки, наприклад, підкомпонент? Ну, це не працює, оскільки вміст картки описується рядком. Однак було б дуже зручно, якби ми могли описати вміст як фрагмент шаблону.
Саме для цього й розроблена система слот від Owl: вона дозволяє писати універсальні компоненти.
Давайте модифікуємо компонент Card для використання слотів:
Видаліть властивість
content.Використайте слот за замовчуванням для визначення тіла.
Вставте кілька карток із довільним вмістом, наприклад, компонент
Counter.(бонус) Додати перевірку властивостей.
Перегляньте також
14. Мінімізація вмісту картки¶
Нарешті, давайте додамо функцію до компонента Card, щоб зробити його цікавішим: нам потрібна кнопка для перемикання його вмісту (показати або приховати).
Додайте стан до компонента
Card, щоб відстежувати, чи він відкритий (за замовчуванням) чи ніДодайте
t-ifдо шаблону для умовного відображення контентуДодайте кнопку в заголовок і змініть код, щоб змінити стан кнопки при натисканні