1. fejezet: Owl komponensek

Ez a fejezet bemutatja az Owl keretrendszert, amely egy Odoo-ra szabott komponensrendszer. Az OWL fő építőelemei a komponensek és a sablonok.

Az Owl-ban a felhasználói felület minden részét egy komponens kezeli: ezek tartalmazzák a logikát és meghatározzák a sablonokat, amelyeket a felhasználói felület megjelenítésére használnak. Gyakorlatilag egy komponenst egy kis JavaScript osztály képvisel, amely a Component osztályból származik.

A kezdéshez szüksége van egy futó Odoo szerverre és egy fejlesztői környezet beállítására. Mielőtt belekezdene a gyakorlatokba, győződjön meg róla, hogy követte az összes lépést, amelyeket ebben a tutorial introduction leírtunk.

Javaslat

Ha a Chrome-ot használja webböngészőként, telepítheti az Owl Devtools kiterjesztést. Ez a kiterjesztés számos funkciót biztosít, amelyek segítenek megérteni és profilozni bármely Owl alkalmazást.

Videó: Hogyan használjuk a DevTools-t

In this chapter, we use the awesome_owl addon, which provides a simplified environment that only contains Owl and a few other files. The goal is to learn Owl itself, without relying on Odoo web client code.

The solutions for each exercise of the chapter are hosted on the official Odoo tutorials repository. It is recommended to try to solve them first without looking at the solution!

Példa: egy Counter komponens

Először nézzünk meg egy egyszerű példát. Az alábbi Counter komponens egy olyan komponens, amely egy belső számértéket tart fenn, megjeleníti azt, és frissíti, amikor a felhasználó a gombra kattint.

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++;
    }
}

A Counter komponens megadja annak a sablonnak a nevét, amely a html-jét képviseli. Ez XML-ben van írva a QWeb nyelv használatával:

<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. Egy számláló megjelenítése

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

Első gyakorlatként módosítsuk az awesome_owl/static/src/ helyen található Playground komponenst, hogy számlálóvá alakítsuk. Az eredmény megtekintéséhez a böngészőjével elérheti a /awesome_owl útvonalat.

  1. Modify playground.js so that it acts as a counter like in the example above. Keep Playground for the class name. You will need to use the useState hook so that the component is re-rendered whenever any part of the state object that has been read by this component is modified.

  2. Ugyanebben a komponensben hozzon létre egy increment metódust.

  3. Módosítsa a playground.xml sablont úgy, hogy megjelenítse a számláló változót. Használja a t-esc funkciót az adatok kiíratásához.

  4. Adjon hozzá egy gombot a sablonhoz, és adjon meg egy t-on-click attribútumot a gombban, hogy az increment metódust hívja meg, amikor a gombra kattintanak.

Javaslat

The Odoo JavaScript files downloaded by the browser are minified. For debugging purpose, it’s easier when the files are not minified. Switch to debug mode with assets so that the files are not minified.

Ez a gyakorlat bemutatja az Owl egyik fontos funkcióját: a reactivity system-et. A useState függvény egy értéket proxyba csomagol, így az Owl nyomon tudja követni, melyik komponensnek melyik állapotrészre van szüksége, így frissíthető, amikor egy érték megváltozik. Próbálja meg eltávolítani a useState függvényt, és nézze meg, mi történik.

2. Counter kivonása egy alkomponensbe

Jelenleg a számláló logikája a Playground komponensben van, de nem újrafelhasználható. Nézzük meg, hogyan lehet belőle sub-component létrehozni:

  1. Vonja ki a számláló kódját a Playground komponensből egy új Counter komponensbe.

  2. Először megteheti ugyanabban a fájlban, de ha kész, frissítse a kódját, hogy a Counter saját mappájába és fájljába kerüljön. Importálja relatívan a ./counter/counter útvonalról. Győződjön meg róla, hogy a sablon saját fájlban van, ugyanazzal a névvel.

  3. Use <Counter/> in the template of the Playground component to add two counters in your playground.

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

Javaslat

Konvenció szerint a legtöbb komponens kódja, sablonja és css-e ugyanazzal a snake-case névvel kell rendelkezzen, mint a komponens. Például, ha van egy TodoList komponensünk, akkor a kódjának a todo_list.js, todo_list.xml és szükség esetén a todo_list.scss fájlokban kell lennie.

3. Egy egyszerű Card komponens

A komponensek valóban a legtermészetesebb módja annak, hogy egy bonyolult felhasználói felületet több újrahasznosítható darabra osszunk. Ahhoz azonban, hogy valóban hasznosak legyenek, szükséges, hogy képesek legyünk információt közvetíteni közöttük. Nézzük meg, hogyan tud egy szülő komponens információt biztosítani egy alkomponens számára attribútumok (leggyakrabban props néven ismertek) használatával.

Ennek a gyakorlatnak a célja egy Card komponens létrehozása, amely két prop-ot vesz fel: title és content. Például, így lehetne használni:

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

A fenti példa némi html-t kell, hogy előállítson bootstrap használatával, amely így néz ki:

<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. Hozz létre egy Card komponenst

  2. Importáld a Playground-ba és jeleníts meg néhány kártyát a sablonjában

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

4. markup használata html megjelenítéséhez

If you used t-esc in the previous exercise, then you may have noticed that Owl automatically escapes its content. For example, if you try to display some html like this: <Card title="'my title'" content="this.html"/> with this.html = "<div>some content</div>"", the resulting output will simply display the html as a string.

Ebben az esetben, mivel a Card komponens bármilyen tartalom megjelenítésére használható, ésszerű lehetővé tenni a felhasználó számára, hogy némi HTML-t jelenítsen meg. Ezt a t-out direktívával lehet megtenni <https://github.com/odoo/owl/blob/master/doc/reference/templates.md#outputting-data>`_.

Azonban tetszőleges tartalom HTML-ként való megjelenítése veszélyes, mivel rosszindulatú kód befecskendezésére használható, ezért alapértelmezés szerint az Owl mindig escape-eli a karakterláncot, hacsak nem jelölték meg kifejezetten biztonságosként a markup függvénnyel.

  1. Frissítse a Card-ot a t-out használatára

  2. Frissítse a Playground-ot a markup importálására, és használja azt néhány HTML értéken

  3. Győződjön meg arról, hogy látja, hogy a normál karakterláncok mindig escape-eltek, ellentétben a markupolt karakterláncokkal.

Megjegyzés

A t-esc direktíva továbbra is használható az Owl sablonokban. Ez kissé gyorsabb, mint a t-out.

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

5. Props validáció

A Card komponensnek implicit API-ja van. Két karakterláncot vár a props-ban: a title és a content. Tegyük ezt az API-t kifejezettebbé. Hozzáadhatunk egy props definíciót, amely lehetővé teszi az Owl számára, hogy validációs lépést hajtson végre dev módban. A dev módot aktiválhatja az App konfigurációban (de alapértelmezés szerint aktiválva van az awesome_owl játszótéren).

Jó gyakorlat minden komponensnél props validációt végezni.

  1. Adjon hozzá props validációt a Card komponenshez.

  2. Nevezze át a title tulajdonságokat valami másra a játszótér sablonban, majd ellenőrizze a böngésző fejlesztői eszközeinek Konzol fülén, hogy látható-e hiba.

6. Két Counter összege

Egy korábbi gyakorlatban láttuk, hogy a props használható információk átadására egy szülő komponensből egy gyermek komponensbe. Most nézzük meg, hogyan tudunk információt közvetíteni az ellenkező irányba: ebben a gyakorlatban két Counter komponenst szeretnénk megjeleníteni, és alattuk az értékeik összegét. Tehát a szülő komponensnek (Playground) értesülnie kell, amikor az egyik Counter értéke megváltozik.

Ezt úgy lehet megtenni, hogy egy callback prop használatával: egy tulajdonság, amely egy visszahívásra szánt függvény. A gyermek komponens dönthet úgy, hogy bármilyen argumentummal meghívja ezt a függvényt. Esetünkben egyszerűen hozzáadunk egy opcionális onChange tulajdonságot, amelyet akkor hívunk meg, amikor a Counter komponens növekszik.

  1. Adjon hozzá tulajdonság-ellenőrzést a Counter komponenshez: fogadnia kell egy opcionális onChange függvény tulajdonságot.

  2. Frissítse a Counter komponenst, hogy hívja meg az onChange tulajdonságot (ha létezik), amikor növekszik.

  3. Módosítsa a Playground komponenst, hogy fenntartson egy helyi állapotértéket (sum), amely kezdetben 2-re van állítva, és jelenítse meg a sablonjában.

  4. Valósítson meg egy incrementSum metódust a Playground-ban.

  5. Adja meg ezt a metódust tulajdonságként két (vagy több!) al Counter komponensnek.

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

Fontos

There is a subtlety with callback props: they usually should be defined with the .bind suffix. See the documentation.

7. Egy teendőlista

Most fedezzük fel az Owl különböző funkcióit egy teendőlista létrehozásával. Két komponensre van szükségünk: egy TodoList komponensre, amely megjeleníti a TodoItem komponensek listáját. A teendők listája egy állapot, amelyet a TodoList-nek kell fenntartania.

Ebben az útmutatóban egy todo egy objektum, amely három értéket tartalmaz: egy id (szám), egy description (sztring) és egy isCompleted (logikai) jelzőt:

{ id: 3, description: "buy milk", isCompleted: false }
  1. Create a TodoList and a TodoItem components.

  2. A TodoItem komponensnek egy todo-t kell kapnia propként, és meg kell jelenítenie annak id-ját és description-ját egy div-ben.

  3. Egyelőre kódolja be a teendők listáját:

    // in TodoList
    this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
    
  4. Use t-foreach to display each todo in a TodoItem.

  5. Display a TodoList in the playground.

  6. Add props validation to TodoItem.

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

Javaslat

Since the TodoList and TodoItem components are so tightly coupled, it makes sense to put them in the same folder.

Megjegyzés

The t-foreach directive is not exactly the same in Owl as the QWeb python implementation: it requires a t-key unique value, so that Owl can properly reconcile each element.

8. Használjon dinamikus attribútumokat

Jelenleg a TodoItem komponens nem mutatja vizuálisan, ha a todo be van fejezve. Tegyük ezt meg dinamikus attribútumok <https://github.com/odoo/owl/blob/master/doc/reference/templates.md#dynamic-attributes> használatával.

  1. Adja hozzá a Bootstrap text-muted és text-decoration-line-through osztályokat a TodoItem gyökérelemhez, ha az be van fejezve.

  2. Change the hardcoded this.todos value to check that it is properly displayed.

Bár az utasítás neve t-att (attribútumra), használható class érték beállítására (és HTML tulajdonságokra, mint például egy input value értéke).

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

Javaslat

Az Owl lehetővé teszi statikus osztályértékek kombinálását dinamikus értékekkel. A következő példa a vártnak megfelelően fog működni:

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

Lásd még: Owl: Dinamikus osztály attribútumok

9. Feladat hozzáadása

Eddig a listánkban lévő feladatok kódoltak. Tegyük hasznosabbá azáltal, hogy lehetővé tesszük a felhasználó számára, hogy feladatot adjon a listához.

  1. Remove the hardcoded values in the TodoList component:

    this.todos = useState([]);
    
  2. Adjon hozzá egy bemeneti mezőt a feladatlista fölé a következő helyőrzővel: Adjon meg egy új feladatot.

  3. Adjon hozzá egy eseménykezelőt a keyup eseményhez, amelynek neve addTodo.

  4. Valósítsa meg az addTodo-t, hogy ellenőrizze, ha az enter billentyűt lenyomták (ev.keyCode === 13), és ebben az esetben hozzon létre egy új teendőt a bemeneti mező aktuális tartalmával leírásként, majd törölje a bemeneti mező összes tartalmát.

  5. Győződjön meg róla, hogy a teendőnek egyedi azonosítója van. Ez lehet egy számláló, amely minden teendőnél növekszik.

  6. Bónusz pont: ne tegyen semmit, ha a bemeneti mező üres.

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

Lásd még

Owl: Reaktivitás

Elmélet: Komponens életciklus és horgok

Eddig egy példa horgos függvényre: useState. A horog egy speciális függvény, amely bekapcsolódik a komponens belső működésébe. A useState esetében egy proxy objektumot generál, amely a jelenlegi komponenshez kapcsolódik. Ezért kell a horgos függvényeket a setup metódusban meghívni, és nem később!

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

An Owl component goes through a lot of phases: it can be instantiated, rendered, mounted, updated, detached, destroyed… This is the component lifecycle. The figure above show the most important events in the life of a component (hooks are shown in purple). Roughly speaking, a component is created, then updated (potentially many times), then is destroyed.

Az Owl számos beépített hooks függvényt biztosít. Mindegyiket a setup függvényben kell meghívni. Például, ha kódot szeretne futtatni, amikor a komponens fel van szerelve, használhatja az onMounted hookot:

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

Javaslat

Minden hook függvény use vagy on előtaggal kezdődik. Például: useState vagy onMounted.

10. Az input fókuszálása

Nézzük meg, hogyan érhetjük el a DOM-ot t-ref és useRef segítségével. A fő ötlet az, hogy meg kell jelölni a cél elemet a komponens sablonjában egy t-ref-el:

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

Ezután a JS-ben elérheti a useRef hook segítségével. Azonban van egy probléma, ha belegondol: a komponens tényleges html eleme nem létezik, amikor a komponens létrejön. Csak akkor létezik, amikor a komponens fel van szerelve. De a hookokat a setup metódusban kell meghívni. Tehát a useRef egy olyan objektumot ad vissza, amely tartalmaz egy el (az elemhez) kulcsot, amely csak akkor van definiálva, amikor a komponens fel van szerelve.

setup() {
   this.myRef = useRef('some_name');
   onMounted(() => {
      console.log(this.myRef.el);
   });
}
  1. Fókuszálja az előző feladat input-ját. Ezt a TodoList komponensből kell megtenni (vegye figyelembe, hogy van egy focus metódus az input html elemen).

  2. Bónusz pont: vonja ki a kódot egy specializált hook useAutofocus-ba egy új awesome_owl/utils.js fájlban.

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

Javaslat

A Ref-eket általában Ref utótaggal látják el, hogy egyértelmű legyen, hogy ezek speciális objektumok:

this.inputRef = useRef('input');

11. Teendők váltása

Now, let’s add a new feature: mark a todo as completed. This is actually trickier than one might think. The owner of the state is not the same as the component that displays it. So, the TodoItem component needs to communicate to its parent that the todo state needs to be toggled. One classic way to do this is by adding a callback prop toggleState.

  1. Add an input with the attribute type="checkbox" before the id of the task, which must be checked if the state isCompleted is true.

    Javaslat

    Az Owl nem hoz létre t-att direktívával számított attribútumokat, ha azok hamis értékre értékelődnek ki.

  2. Adjon hozzá egy toggleState visszahívási tulajdonságot a TodoItem-hez.

  3. Add a change event handler on the input in the TodoItem component and make sure it calls the toggleState function with the todo id.

  4. Működjön!

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

12. Teendők törlése

Az utolsó simítás az, hogy a felhasználó törölhessen egy teendőt.

  1. Adjon hozzá egy új removeTodo visszahívási tulajdonságot a TodoItem-hez.

  2. Insert <span class="fa fa-remove"/> in the template of the TodoItem component.

  3. Amikor a felhasználó rákattint, meg kell hívnia a removeTodo metódust.

  4. Működjön!

    Javaslat

    Ha tömböt használ a teendőlista tárolására, a JavaScript splice függvényével eltávolíthat egy teendőt belőle.

// 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. Általános Card slotokkal

In a previous exercise, we built a simple Card component. But it is honestly quite limited. What if we want to display some arbitrary content inside a card, such as a sub-component? Well, it does not work, since the content of the card is described by a string. It would however be very convenient if we could describe the content as a piece of template.

This is exactly what Owl’s slot system is designed for: allowing to write generic components.

Módosítsuk a Card komponenst, hogy slotokat használjon:

  1. Remove the content prop.

  2. Use the default slot to define the body.

  3. Insert a few cards with arbitrary content, such as a Counter component.

  4. (bonus) Add prop validation.

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

14. A kártya tartalmának minimalizálása

Végül adjunk hozzá egy funkciót a Card komponenshez, hogy érdekesebbé tegyük: szeretnénk egy gombot, amellyel a tartalmát váltogathatjuk (megmutatjuk vagy elrejtjük)

  1. Adjon hozzá egy állapotot a Card komponenshez, hogy nyomon kövesse, nyitva van-e (alapértelmezett) vagy sem

  2. Adjon hozzá egy t-if-et a sablonhoz, hogy feltételesen jelenítse meg a tartalmat

  3. Adjon hozzá egy gombot a fejlécben, és módosítsa a kódot, hogy a gomb megnyomásakor megváltozzon az állapot

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