diff --git a/docs/playground/playground.js b/docs/playground/playground.js index 26692e7e0..cb6bb14f9 100644 --- a/docs/playground/playground.js +++ b/docs/playground/playground.js @@ -81,7 +81,7 @@ const SAMPLES = [ code: ["js", "xml", "css"], }, { - description: "Form Input Bindings", + description: "t-model", folder: "form", code: ["js", "xml"], }, @@ -169,7 +169,7 @@ class TabbedEditor extends Component { this.editor = this.editor || ace.edit(this.editorNode()); this.editor.setValue(this.props[this.state.currentTab], -1); - this.editor.setFontSize("12px"); + this.editor.setFontSize("14px"); this.editor.setTheme("ace/theme/monokai"); this.editor.setSession(this.sessions[this.state.currentTab]); const tabSize = this.state.currentTab === "xml" ? 2 : 4; diff --git a/docs/playground/samples/components/components.css b/docs/playground/samples/components/components.css index 70eee24db..37a22e0f9 100644 --- a/docs/playground/samples/components/components.css +++ b/docs/playground/samples/components/components.css @@ -1,4 +1,4 @@ -.greeter { +.counter { font-size: 20px; width: 300px; height: 100px; @@ -7,4 +7,4 @@ line-height: 100px; background-color: #eeeeee; user-select: none; -} +} \ No newline at end of file diff --git a/docs/playground/samples/components/components.js b/docs/playground/samples/components/components.js index 0a1f34785..581ca3bb8 100644 --- a/docs/playground/samples/components/components.js +++ b/docs/playground/samples/components/components.js @@ -1,26 +1,19 @@ // In this example, we show how components can be defined and created. -import { Component, useState, mount } from "@odoo/owl"; +import { Component, signal, mount } from "@odoo/owl"; -class Greeter extends Component { - static template = "Greeter"; +class Counter extends Component { + static template = "Counter"; - setup() { - this.state = useState({ word: 'Hello' }); - } - - toggle() { - this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi'; + count = signal(0); + + increment() { + this.count.update(val => val + 1); } } -// Main root component class Root extends Component { - static components = { Greeter }; + static components = { Counter }; static template = "Root" - - setup() { - this.state = useState({ name: 'World'}); - } } mount(Root, document.body, { templates: TEMPLATES, dev: true }); diff --git a/docs/playground/samples/components/components.xml b/docs/playground/samples/components/components.xml index 1caaf12a8..722cfcddc 100644 --- a/docs/playground/samples/components/components.xml +++ b/docs/playground/samples/components/components.xml @@ -1,9 +1,10 @@ -
- , +
+ Count:
- + + diff --git a/docs/playground/samples/form/form.js b/docs/playground/samples/form/form.js index 345c118ba..66ca04747 100644 --- a/docs/playground/samples/form/form.js +++ b/docs/playground/samples/form/form.js @@ -2,20 +2,16 @@ // data between html inputs (and select/textareas) and the state of a component. // Note that there are two controls with t-model="color": they are totally // synchronized. -import { Component, useState, mount } from "@odoo/owl"; +import { Component, signal, mount } from "@odoo/owl"; class Form extends Component { static template = "Form"; - setup() { - this.state = useState({ - text: "", - othertext: "", - number: 11, - color: "", - bool: false - }); - } + text = signal(""); + othertext = signal(""); + number = signal(11); + color = signal(""); + bool = signal(false); } // Application setup diff --git a/docs/playground/samples/form/form.xml b/docs/playground/samples/form/form.xml index 1693eb9e9..55e99600a 100644 --- a/docs/playground/samples/form/form.xml +++ b/docs/playground/samples/form/form.xml @@ -2,20 +2,20 @@

Form

- Text (immediate): + Text (immediate):
- Other text (lazy): + Other text (lazy):
- Number: + Number:
- Boolean: + Boolean:
Color, with a select: - @@ -23,15 +23,15 @@
Color, with radio buttons: - - + +

State

-
Text:
-
Other Text:
-
Number:
-
Boolean: TrueFalse
-
Color:
+
Text:
+
Other Text:
+
Number:
+
Boolean: TrueFalse
+
Color:
diff --git a/docs/playground/samples/todo_app/todo_app.js b/docs/playground/samples/todo_app/todo_app.js index b63cbbc5f..8bed42d40 100644 --- a/docs/playground/samples/todo_app/todo_app.js +++ b/docs/playground/samples/todo_app/todo_app.js @@ -1,121 +1,161 @@ // This example is an implementation of the TodoList application, from the // www.todomvc.com project. This is a non trivial application with some // interesting user interactions. It uses the local storage for persistence. -// -// In this implementation, we use the owl reactivity mechanism. -import { Component, useState, mount, useRef, reactive, useEnv, useEffect } from "@odoo/owl"; +import { + Component, + derived, + effect, + mount, + plugin, + Plugin, + PluginManager, + props, + signal, + useEffect, + onWillDestroy, +} from "@odoo/owl"; -//------------------------------------------------------------------------------ -// Constants, helpers -//------------------------------------------------------------------------------ const ENTER_KEY = 13; const ESC_KEY = 27; -function useAutofocus(name) { - let ref = useRef(name); - useEffect(el => el && el.focus(), () => [ref.el]); -} +class TodoItem { + #list; -function useStore() { - const env = useEnv(); - return useState(env.store); + constructor(list, { id, text, isCompleted }) { + this.#list = list; + this.id = id; + this.text = signal(text); + this.isCompleted = signal(isCompleted); + } + + delete() { + this.#list.delete(this); + } + + toggle() { + this.isCompleted.update((value) => !value); + } } -//------------------------------------------------------------------------------ -// Task store -//------------------------------------------------------------------------------ -class TaskList { - constructor(tasks) { - this.tasks = tasks || []; - const taskIds = this.tasks.map((t) => t.id); - this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1; +class LocalStoragePlugin extends Plugin { + static id = "local_storage"; + + cleanups = []; + + setup() { + onWillDestroy(() => { + for (let cleanup of cleanups) { + cleanup(); + } + }); + } + + open({ key, encode, decode }) { + const str = localStorage.getItem(key); + const result = signal(decode(str)); + this.cleanups.push( + effect(() => { + const str = encode(result()); + localStorage.setItem(key, str); + }) + ); + return result; } +} - addTask(text) { +class TodoListPlugin extends Plugin { + static id = "todo_list"; + todos = signal([]); + + localStorage = plugin(LocalStoragePlugin); + todos = this.localStorage.open({ + key: "todoapp", + encode: (data) => { + const json = data.map((todo) => ({ + id: todo.id, + text: todo.text(), + isCompleted: todo.isCompleted(), + })); + return JSON.stringify(json); + }, + decode: (str) => { + const data = JSON.parse(str || "[]"); + return data.map((todo) => new TodoItem(this, todo)); + }, + }); + + generateId() { + return Math.random().toString(36).substring(2, 10); + } + + isEmpty = derived(() => !this.todos().length); + + add(text) { text = text.trim(); if (text) { - const task = { - id: this.nextId++, - text: text, - isCompleted: false, - }; - this.tasks.push(task); + const data = { id: this.generateId(), text, isComplete: false }; + const todo = new TodoItem(this, data); + this.todos().push(todo); + this.todos.update(); } } - toggleTask(id) { - const task = this.tasks.find(t => t.id === id); - task.isCompleted = !task.isCompleted; + delete(todo) { + const result = this.todos().filter((t) => t !== todo); + this.todos.set(result); } toggleAll(value) { - for (let task of this.tasks) { - task.isCompleted = value; + for (let todo of this.todos()) { + todo.isCompleted.set(value); } } - + clearCompleted() { - const tasks = this.tasks.filter(t => t.isCompleted); - for (let task of tasks) { - this.deleteTask(task.id); - } - } - - deleteTask(id) { - const index = this.tasks.findIndex((t) => t.id === id); - this.tasks.splice(index, 1); - } - - updateTask(id, text) { - const value = text.trim(); - if (!value) { - this.deleteTask(id); - } else { - const task = this.tasks.find(t => t.id === id); - task.text = value; + const todos = this.todos().filter((t) => t.isCompleted()); + for (let todo of todos) { + this.delete(todo); } } } -function createTaskStore() { - const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks)); - const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]"); - const taskStore = reactive(new TaskList(initialTasks), saveTasks); - saveTasks(); - return taskStore; -} - //------------------------------------------------------------------------------ // Todo //------------------------------------------------------------------------------ class Todo extends Component { static template = "Todo"; - + + props = props({ todo: TodoItem }); + todo = this.props.todo; + isEditing = signal(false); + input = signal(null); + text = signal(this.todo.text()); + setup() { - useAutofocus("input"); - this.store = useStore(); - this.state = useState({ - isEditing: false - }); + useEffect( + (el) => el && el.focus(), + () => [this.input()] + ); + } + + stopEditing() { + this.isEditing.set(false); } handleKeyup(ev) { if (ev.keyCode === ENTER_KEY) { - this.updateText(ev.target.value); + this.todo.text.set(this.text()); + this.stopEditing(); } if (ev.keyCode === ESC_KEY) { - ev.target.value = this.props.text; - this.state.isEditing = false; + this.text.set(this.todo.text()); + this.stopEditing(); } } handleBlur(ev) { - this.updateText(ev.target.value); - } - - updateText(text) { - this.store.updateTask(this.props.id, text); - this.state.isEditing = false; + this.todo.text.set(this.text()); + this.stopEditing(); } } @@ -125,54 +165,48 @@ class Todo extends Component { class TodoList extends Component { static template = "TodoList"; static components = { Todo }; - - setup() { - this.store = useStore(); - this.state = useState({ filter: "all" }); - } - get displayedTasks() { - const tasks = this.store.tasks; - switch (this.state.filter) { - case "active": - return tasks.filter((t) => !t.isCompleted); - case "completed": - return tasks.filter((t) => t.isCompleted); - case "all": - return tasks; - } - } - - get allChecked() { - return this.store.tasks.every(todo => todo.isCompleted); - } + todoList = plugin(TodoListPlugin); + filter = signal("all"); + + visibleTodos = derived(() => { + const todos = this.todoList.todos(); + switch (this.filter()) { + case "active": + return todos.filter((t) => !t.isCompleted()); + case "completed": + return todos.filter((t) => t.isCompleted()); + case "all": + return todos; + } + }); - get remaining() { - return this.store.tasks.filter(todo => !todo.isCompleted).length; - } + remaining = derived(() => { + const todos = this.todoList.todos(); + return todos.filter((todo) => !todo.isCompleted()).length; + }); - get remainingText() { - const items = this.remaining < 2 ? "item" : "items"; - return ` ${items} left`; - } + remainingText = derived(() => { + return ` ${this.remaining() < 2 ? "item" : "items"} left`; + }); + + allChecked = derived(() => this.remaining() === 0); addTodo(ev) { if (ev.keyCode === ENTER_KEY) { const text = ev.target.value; if (text.trim()) { - this.store.addTask(text); + this.todoList.add(text); } ev.target.value = ""; } } - - setFilter(filter) { - this.state.filter = filter; - } } //------------------------------------------------------------------------------ // App Initialization //------------------------------------------------------------------------------ -const env = { store: createTaskStore() }; -mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true }); +const pluginManager = new PluginManager(); +pluginManager.startPlugins([TodoListPlugin]); + +mount(TodoList, document.body, { templates: TEMPLATES, pluginManager, dev: true }); diff --git a/docs/playground/samples/todo_app/todo_app.xml b/docs/playground/samples/todo_app/todo_app.xml index 1ed4d9f72..98655ef13 100644 --- a/docs/playground/samples/todo_app/todo_app.xml +++ b/docs/playground/samples/todo_app/todo_app.xml @@ -2,49 +2,49 @@

todos

- +
-
- +
+
    - - + +
-
-
  • +
  • - -
    - +
  • diff --git a/docs/playground/templates.xml b/docs/playground/templates.xml index 174ea0f0d..bd339dc66 100644 --- a/docs/playground/templates.xml +++ b/docs/playground/templates.xml @@ -1,49 +1,49 @@ -
    -
    +
    + -
    +
    -
    +
    - + js="this.state.js" + css="!this.state.splitLayout and this.state.css" + xml="!this.state.splitLayout and this.state.xml" + style="this.topEditorStyle" + updateCode="this.updateCode"/> +
    + updatePanelHeight.bind="this.updatePanelHeight" + updateCode="this.updateCode"/>
    -
    +
    -
    +
    🦉 Odoo Web Library 🦉
    v
    @@ -53,7 +53,7 @@

    -
    +
    diff --git a/tests/components/basics.test.ts b/tests/components/basics.test.ts index 3f15b536d..a407dad69 100644 --- a/tests/components/basics.test.ts +++ b/tests/components/basics.test.ts @@ -1081,9 +1081,7 @@ describe("t-out in components", () => { } await mount(Test, fixture); - expect(fixture.innerHTML).toBe( - "
    onetwotree
    " - ); + expect(fixture.innerHTML).toBe("
    onetwotree
    "); }); test("can switch the contents of two t-out repeatedly", async () => {