第1章:フクロウコンポーネント

This chapter introduces the Owl framework, a tailor-made component system for Odoo. The main building blocks of OWL are components and templates.

フクロウ中 ユーザーインターフェイスのすべての部分はコンポーネントによって管理されています。ロジックを保持し、ユーザーインターフェイスをレンダリングするために使用されるテンプレートを定義します。 実際には、コンポーネントは Component クラスをサブクラス化した小さなJavaScriptクラスによって表現されます。

始めるには、実行中の Odoo サーバーと開発環境のセットアップが必要です。 練習問題に入る前に、この :ref:`tutorial <tutorials/discover_js_framework/setup> `で説明されているすべてのステップに従っていることを確認してください。

ちなみに

Chromeをウェブブラウザとして使用している場合は、`Owl Devtools`拡張機能をインストールできます。 この拡張機能は Owl アプリケーションを理解し、プロファイルするのに役立つ多くの機能を提供します。

Video: How to use the DevTools

この章では、awesome_owl addonを使います。これは、フクロウと他のいくつかのファイルしか含まれていない簡素化された環境を提供します。 目標は、Odoo Webクライアントコードに頼らずに、Owl自体を学ぶことです。

章の各演習の解決策は、 official Odoo tutorials repository にホストされています。 解を見ずに最初に解いてみることをお勧めします!

例: Counter コンポーネント

まず、簡単な例を見てみましょう。 Counter コンポーネントは内部番号の値を保持するコンポーネントです。 が表示され、ユーザーがボタンをクリックするたびに更新されます。

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を表すテンプレートの名前を指定します。これは、QWeb言語を使用してXMLで書かれます。

<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

最初の課題として、 awesome_owl/static/src/`にある`Playground`コンポーネントを変更してカウンタにしましょう。 結果を確認するには、ブラウザで `/awesome_owl route (ルート)に移動します。

  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. 同じコンポーネントで increment メソッドを作成します。

  3. playground.xml 内のテンプレートを変更してカウンター変数を表示します。データを出力するには、t-esc を使用します。

  4. テンプレートにボタンを追加し、ボタンをクリックするたびに`increment`メソッドをトリガーするために、ボタンに`t-on-click <https://github.com/odoo/owl/blob/master/doc/reference/event_handling.md#event-handling>`_ 属性を指定します。

ちなみに

ブラウザーがダウンロードした Odoo JavaScript ファイルはミニファイされています。デバッグ目的では、ファイルがミニファイされていない場合の方が簡単です。 assets のデバッグモードに切り替えます。

この課題では、反応システム という Owl の重要な機能を示します。 useState 関数はプロキシに値をラップするため、Owl は state のどの部分が必要かを追跡できます。 値を変えるたびに更新することができます useState 関数を削除して、何が起こるか見てみましょう。

2. サブコンポーネントに Counter を展開する

今のところ、 Playground コンポーネントにカウンターのロジックがありますが、それは再利用できません。 サブコンポーネント を作成する方法を見てみましょう。

  1. `Playground`コンポーネントからカウンターコードを新しい`Counter`コンポーネントに抽出します。

  2. 同じファイルで最初に実行できますが、一度実行されるとします。 Counter`を自分のフォルダとファイルに移動させるには、コードを更新してください。 `./counter/counter から比較的インポートします。テンプレートが同じ名前のファイルにあることを確認します。

  3. `Playground`コンポーネントのテンプレートに`<Counter/>`を使用して、プレイグラウンドに2つのカウンターを追加します。

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

ちなみに

通常、ほとんどのコンポーネントコード、テンプレート、cssはコンポーネントと同じヘビケースの名前を持つ必要があります。 例えば、TodoList`コンポーネントがある場合、コードは`todo_list.jstodo_list.xml、必要に応じて`todo_list.scss`の中にある必要があります。

3. シンプルな「カード」コンポーネント

コンポーネントは、複雑なユーザーインターフェイスを複数の再利用可能なピースに分割する最も自然な方法です。 しかし、本当に役に立つようにするためには、それら間の情報を伝える必要があります。 親コンポーネントがどのように属性を使用してサブコンポーネントに情報を提供できるかを見てみましょう (最も一般的には props として知られています)。

この課題の目的は、 Card コンポーネントを作成することです。これには titlecontent という 2 つのプロパティがあります。 例えば、以下のように使用できます。

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

上記の例では、次のようなブートストラップを使用して 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. html を表示するために markup を使用する

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.

この場合、Card`コンポーネントはあらゆる種類のコンテンツを表示するために使用されることがあります。 ユーザーにhtmlを表示させるのは理にかなっています これは `t-out ディレクティブ で行います。

ただし、任意のコンテンツを html として表示することは危険です。悪意のあるコードを挿入するために使用することができます。 `markup`関数で明示的に安全とマークされていない限り、フクロウは常に文字列をエスケープします。

  1. `t-out`を使用するにはカードを更新してください

  2. Playground を更新して markup をインポートし、html の値で使用します

  3. マークアップされた文字列とは異なり、通常の文字列が常にエスケープされていることを確認してください。

注釈

t-esc ディレクティブは Owl テンプレートでも使用できます。t-out よりも若干速くなります。

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

5. プロパティの検証

The Card component has an implicit API. It expects to receive two strings in its props: the title and the content. Let us make that API more explicit. We can add a props definition that will let Owl perform a validation step in dev mode. You can activate the dev mode in the App configuration (but it is activated by default on the awesome_owl playground).

すべてのコンポーネントの props の検証を行うことをお勧めします。

  1. props バリデーションCard コンポーネントに追加します。

  2. title プロパティを別のプレイグラウンドテンプレートに変更します。 次に、ブラウザの開発ツールの Console タブでエラーが表示されます。

6. 2つの「カウンター」の合計

前回の課題では、props は親コンポーネントから子コンポーネントに情報を提供するために使用できることがわかりました。 では、どのようにして逆方向に情報を伝えることができるのか見てみましょう。 2つの`Counter`コンポーネントを表示し、その下にその値の合計を表示します。 そのため、親コンポーネント(Playground)は、いずれかの値が変更されたときに通知される必要があります。

これは callback prop: 呼び出される関数である props を使用することで行うことができます。 子コンポーネントは、任意の引数でその関数を呼び出すことを選択できます。 ここでは、オプションの onChange プロパティを追加するだけで、Counter コンポーネントがインクリメントされるたびに呼び出されます。

  1. Counter コンポーネントにプロパティ検証を追加します。オプションの onChange 関数プロパティを受け付ける必要があります。

  2. Counter コンポーネントを更新して、インクリメントされるたびに`onChange` プロパティを呼び出すようにします。

  3. Playground`コンポーネントを変更してローカルの状態値(`sum)を維持し、最初に2に設定し、テンプレートに表示します。

  4. PlaygroundincrementSum メソッドを実装

  5. このメソッドを props として 2 つ(またはそれ以上!)サブの Counter コンポーネントに渡します。

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

重要

callback propsを持つsubtletyがあります: 通常は`.bind`サフィックスで定義する必要があります。documentation を参照してください。

7. ToDoリスト

ToDoリストを作成することで、フクロウのさまざまな機能を発見しましょう。 2つのコンポーネントが必要です: TodoItem コンポーネントのリストを表示する TodoList コンポーネント。 todosのリストは、 TodoList によって維持されるべき状態です。

このチュートリアルでは、todo`は、`id (数値)、description (文字列)、isCompleted (真偽値)の3つの値を含むオブジェクトです。

{ id: 3, description: "buy milk", isCompleted: false }
  1. TodoListTodoItem コンポーネントを作成します。

  2. `TodoItem`コンポーネントは`todo`をプロパティとして受け取り、`div`に`id`と`description`を表示します。

  3. 今のところ、todosのリストをハードコードします:

    // in TodoList
    this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
    
  4. t-foreach を使用すると、TodoItem に各タスクを表示できます。

  5. プレイグラウンドに TodoList を表示します。

  6. TodoItem にプロパティ検証を追加します。

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

ちなみに

TodoListTodoItem コンポーネントは非常に密接に結合されているため、同じフォルダに配置するのは理にかなっています。

注釈

t-foreach ディレクティブは QWeb python 実装の Owl と同じではありません: t-key 固有の値が必要です。 フクロウがそれぞれの要素を適切に調整できるようにしました

8. 動的な属性を使用

今のところ、 TodoItem コンポーネントは、 todo が完了した場合、視覚的には表示されません。dynamic attributes を使用してみましょう。

  1. `TodoItem`ルート要素に`text-muted`と`text-decoration-line-through `Bootstrapクラスを追加します。

  2. ハードコードされた this.todos 値を変更して、正しく表示されていることを確認します。

ディレクティブ名は t-att (属性用) ですが、 これは class の値を設定するために使うことができます (そして入力の value のような html プロパティ)。

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

ちなみに

フクロウでは静的なクラス値と動的な値を組み合わせます。次の例は期待通りに動作します:

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

参照: Owl: Dynamic class attributes

9. タスクを追加する

これまでのところ、私たちのリストのタスクはハードコードされています。 ユーザーがTodoをリストに追加できるようにすることで、より便利になります。

  1. TodoList コンポーネント内のハードコードされた値を削除します。

    this.todos = useState([]);
    
  2. タスクリストの上にプレースホルダ付きの入力を追加します。 新しいタスクを入力

  3. addTodo という名前の keyup イベントに event handler を追加します。

  4. 入力が押されたかどうかを確認するために addTodo を実装します(ev. eyCode === 13), そしてその場合は 入力の現在のコンテンツを説明として新しいTodoを作成し、すべてのコンテンツの入力をクリアします。

  5. todo に固有の id があることを確認してください。todo ごとに増分されるカウンターにすることができます。

  6. ボーナスポイント: 入力が空の場合は何もしないでください。

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

関連項目

Owl: Reactivity

理論: コンポーネントのライフサイクルとフック

これまでにフック関数の例として`useState`があります。 hook は、コンポーネントの内部を*フックする*特別な関数です。 useState の場合、現在のコンポーネントにリンクされているプロキシオブジェクトを生成します。 このため、フック関数は setup メソッドで呼び出される必要があります。

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

フクロウコンポーネントは、インスタンス化、レンダリング、マウント、更新、取り外し、破棄など、多くの段階を経ています。 をクリックします。これは component lifycle です。 上の図は、コンポーネントの寿命における最も重要な出来事を示しています(フックは紫色で表示されています)。 大まかに言えば、コンポーネントが作成され、更新された後(潜在的に何度も)、破棄されます。

フクロウは様々な組み込みの`フック関数 <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md>`_ を提供します。それらは全て`setup`関数で呼び出されなければなりません。 たとえば、コンポーネントがマウントされたときにコードを実行したい場合は、onMounted フックを使用できます。

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

ちなみに

フック関数はすべて`use`または`on`で始まります。例えば、`useState`または`onMounted`です。

10. 入力にフォーカスする

t-refuseRef を使って DOM にアクセスする方法を見てみましょう。 主な考え方は、コンポーネントテンプレート内のターゲット要素を `t-ref`でマークする必要があることです。

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

次に、useRefフック を使ってJSでアクセスできます。 しかし、考えてみると問題があります: コンポーネントの実際の html 要素は、コンポーネントの作成時には存在しません。 コンポーネントがマウントされている場合にのみ存在しますが、フックは setup メソッドで呼び出す必要があります。 ですから、 useRef はコンポーネントがマウントされたときにのみ定義される、 el (要素用) キーを含むオブジェクトを返します。

setup() {
   this.myRef = useRef('some_name');
   onMounted(() => {
      console.log(this.myRef.el);
   });
}
  1. 前の課題の input に焦点を合わせます。 これは TodoList コンポーネントから行う必要があります(入力の html 要素に focus メソッドがあります)。

  2. Bonus point: extract the code into a specialized hook useAutofocus in a new awesome_owl/utils.js file.

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

ちなみに

refは、特別なオブジェクトであることを明らかにするために、 Ref でサフィックスされます。

this.inputRef = useRef('input');

11. Toggling todos

では、新しい機能を追加しましょう。タスクを完了としてマークします。これは実際に考えられるよりも難しいです。 state の所有者はそれを表示するコンポーネントとは異なります。 そのため、TodoItem`コンポーネントは親に、todoの状態を切り替える必要があることを伝える必要があります。 これを行うには、`callback prop toggleState を追加します。

  1. タスクのIDの前に type="checkbox"という属性を持つ入力を追加します。これは `isCompleted の状態がtrueの場合にチェックする必要があります。

    ちなみに

    Owl は t-att ディレクティブで値が false の場合に計算された属性を生成しません。

  2. `toggleState`コールバックプロパティを`TodoItem`に追加します。

  3. TodoItem コンポーネント内の入力に change イベントハンドラを追加し、todo id で toggleState 関数を呼び出すようにします。

  4. うまくいくようにしよう!

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

12. タスクを削除しています

最後のタッチは、ユーザーがtodoを削除できるようにすることです。

  1. `TodoItem`に新しいコールバックプロパティ`removeTodo`を追加します。

  2. TodoItem コンポーネントのテンプレートに <span class="fa fa-remove"/> を挿入します。

  3. ユーザーがクリックするたびに、`removeTodo`メソッドを呼び出します。

  4. うまくいくようにしよう!

    ちなみに

    配列を使用して todo リストを保存する場合は、JavaScript の splice 関数を使用して todo を削除することができます。

// 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.スロット付きの一般的な`カード`

:ref:`前の練習問題 <tutorials/discover_js_framework/simple_card>`では、シンプルな`カード`コンポーネントを作りました。しかし、正直、それはかなり限られています。 サブコンポーネントなど、カード内に任意のコンテンツを表示したい場合はどうすればよいでしょうか? カードの内容は文字列で記述されているので、まあ、それは動作しません。 しかし、コンテンツをテンプレートとして表現することができれば非常に便利です。

これはまさに、Owl の slot システムのために設計されているものです。

スロットを使用するために、 Card コンポーネントを修正しましょう。

  1. content プロパティを削除します。

  2. デフォルトのスロットを使用して本文を定義します。

  3. Counter コンポーネントのような任意のコンテンツを含むカードをいくつか挿入します。

  4. (ボーナス) プロパティ検証を追加します。

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

14. カードコンテンツを最小化する

最後に、`Card`コンポーネントに機能を追加しましょう。 より興味深いものにするには、ボタンでそのコンテンツを切り替える(表示または非表示)

  1. `Card`コンポーネントに状態を追加して、開いているかどうかを追跡します (デフォルト)。

  2. テンプレートに t-if を追加して、条件付きでコンテンツをレンダリングします。

  3. ヘッダーにボタンを追加し、ボタンがクリックされたときに状態を反転させるコードを変更します

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