>
Odoo
>
Odoo 14前端框架OWL之创建一个待办清单TodoList应用
注意这里把所有代码放到了立即执行的函数中,来避免对全局产生任何影响。
最后应当从Owl仓库下载最新版的
owl.js
(你也可以直接使用
owl.min.js
)。
访问https://github.com/odoo/owl/releases下载最新版本 owl.js
此时,项目准备就绪。在浏览器中加载
index.html
文件,页面内容为空,标题为
Owl Todo App
,在控制台中会打印出
hello owl 1.0.0
这样的消息。
添加第一个组件
Owl应用由
组件
构成,包含一个根组件。下面我们来定义一个
App
组件。使用如下代码替换掉
app.js
中函数的内容。
// Owl Components
class App extends Component {
static template = xml`<div>todo app</div>`;
// Setup code
function setup() {
const app = new App();
app.mount(document.body);
whenReady(setup);
此时重新加载页面会显示一条消息。
代码非常简单,但我们来详细讲解一下最后一行代码。浏览器会尝试尽快执行
app.js
中的JS代码,在有可能在加载
App
组件时DOM尚未准备就绪。为避免这一问题,我们使用
whenReady
帮助函数来将
setup
函数的执行延迟至DOM准备就绪后。
注意1:在更大型的项目中,我们会把代码分割成多个文件,在子文件夹中放置组件,主文件会用于初始化应用。但这里是一个非常小的项目,我们希望保持尽量简单。
注意2:本教程中使用了静态类字段语法。有些浏览器尚不支持。大部分真实项目会对代码进行转译,不会存在问题,但要让本教程的代码在各个浏览器中正常使用,需要将每个
static
关键词转换为类赋值:
注意4:大型应用可能需要对模板进行翻译。使用行内模板会让其变得困难,因为我们需要额外的工具类从代码中提取 xml,再使用翻译值进行替换。
展示任务清单
现在已完成基础工作。是时候考虑任务这块了。为完成所需,我们要使用如下键的对象数组记录任务。
id
: 数字。对于唯一标识任务极其有用。因其标题由用户创建或编辑,无法保证唯一性。因为我们对每个任务生成一个唯一数字
id
。
title
: 字符串,说明任务是关于什么的。
isCompleted
: 布尔值,记录任务的状态。
既然决定好了状态的内部格式,可以对
App
组件添加一些演示数据及模板:
static template = xml/* xml */ `
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<div class="task">
<input type="checkbox" t-att-checked="task.isCompleted"/>
<span><t t-esc="task.title"/></span>
</div>`;
tasks = [
id: 1,
title: "buy milk",
isCompleted: true,
id: 2,
title: "clean house",
isCompleted: false,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class
App
extends
Component
{
static
template
=
xml
/* xml */
`
<
div
class
=
"task-list"
>
<
t
t
-
foreach
=
"tasks"
t
-
as
=
"task"
t
-
key
=
"task.id"
>
<
div
class
=
"task"
>
<
input
type
=
"checkbox"
t
-
att
-
checked
=
"task.isCompleted"
/
>
<
span
>
<
t
t
-
esc
=
"task.title"
/
>
<
/
span
>
<
/
div
>
<
/
t
>
<
/
div
>
`
;
tasks
=
[
{
id
:
1
,
title
:
"buy milk"
,
isCompleted
:
true
,
}
,
{
id
:
2
,
title
:
"clean house"
,
isCompleted
:
false
,
}
,
]
;
}
模板中包含一个
t-foreach
循环对任务进行遍历。它可丰组件中查找
任务
列表,因为组件是渲染上下文。注意我们将每个任务的
id
作为
t-key
,这是常见做法。有两个css类:
task-list
和
task
,在下一节中进行使用。
最后,注意
t-att-checked
属性的使用:在属性名前添加
t-att
来让其成为动态属性。Owl将会运行表达式并设置结果为属性值。
布局:一些基础css
至此,我们的任务清单还不好看。在
app.css
中添加如下样式:
现在很清晰应当有一个
Task
组件用于封装任务的外观和行为。
Task
组件会展示傻,但不能
拥有
任务的状态:一段数据仅能有一个所有者。否则会是自找麻烦。因此
Task
组件会以
prop
获取其数据。这表示数据仍由
App
组件所持有,但可由
Task
组件使用(不进行修改)。
因为我们在动代码,趁机可以对代码进行一些重构:
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
const TASK_TEMPLATE = xml /* xml */`
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
<span><t t-esc="props.task.title"/></span>
</div>`;
class Task extends Component {
static template = TASK_TEMPLATE;
static props = ["task"];
// -------------------------------------------------------------------------
// App Component
// -------------------------------------------------------------------------
const APP_TEMPLATE = xml /* xml */`
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</div>`;
class App extends Component {
static template = APP_TEMPLATE;
static components = { Task };
tasks = [
// -------------------------------------------------------------------------
// Setup code
// -------------------------------------------------------------------------
function setup() {
owl.config.mode = "dev";
const app = new App();
app.mount(document.body);
whenReady(setup);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
const
TASK_TEMPLATE
=
xml
/* xml */
`
<
div
class
=
"task"
t
-
att
-
class
=
"props.task.isCompleted ? 'done' : ''"
>
<
input
type
=
"checkbox"
t
-
att
-
checked
=
"props.task.isCompleted"
/
>
<
span
>
<
t
t
-
esc
=
"props.task.title"
/
>
<
/
span
>
<
/
div
>
`
;
class
Task
extends
Component
{
static
template
=
TASK_TEMPLATE
;
static
props
=
[
"task"
]
;
}
// -------------------------------------------------------------------------
// App Component
// -------------------------------------------------------------------------
const
APP_TEMPLATE
=
xml
/* xml */
`
<
div
class
=
"task-list"
>
<
t
t
-
foreach
=
"tasks"
t
-
as
=
"task"
t
-
key
=
"task.id"
>
<
Task
task
=
"task"
/
>
<
/
t
>
<
/
div
>
`
;
class
App
extends
Component
{
static
template
=
APP_TEMPLATE
;
static
components
=
{
Task
}
;
tasks
=
[
.
.
.
]
;
}
// -------------------------------------------------------------------------
// Setup code
// -------------------------------------------------------------------------
function
setup
(
)
{
owl
.
config
.
mode
=
"dev"
;
const
app
=
new
App
(
)
;
app
.
mount
(
document
.
body
)
;
}
whenReady
(
setup
)
;
首先当前在文件上方定义了一个子组件
Task
,
添加子组件时,需要将其添加到父组件的静态
components
键中,这样Owl可对其进行引用,
模板从组件中进行提取,让“视图/模板”代码与“脚本/行为”代码更易于区分。
Task
组件有一个
props
键:用作校验。它表明每个
Task
仅给定一个属性,名为
task
。若非如此,Owl会抛出一条
错误
。这对于重构组件极为有用。
最后要启用props校验,需要将Owl的
模式
设置为
dev
。通过
setup
函数实现。注意在真实的生产环境应予以删除,因为
dev
模式由于有额外的检查、校验会有一些慢。
添加任务(第一部分)
我们还在使用硬编码的任务。是时候让用户可以自己添加任务了。第一步是对
App
组件添加一个输入框。这个输入框位于任务清单之外,因此需要调整
App
的模板、JS 和 CSS(注意其中对 task-list 样式的修改):
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask"/>
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
const title = ev.target.value.trim();
ev.target.value = "";
console.log('adding task', title);
// todo
现在输入框可以使用了,它会在用户每添加一个任务时在控制台中进行记录。注意在加载页面时,光标不会聚焦到输入框。但添加任务是任务清单的核心功能,我们对输入框进行聚焦来让这一操作更快速。
因为
App
是一个组件,它存在一个
mounted
生命周期方法
可供实现。我们还需要获取对输入框的引用,可通过带有
useRef
钩子的
t-ref
指令。
inputRef
以类字段进行定义,因此和在构造函数中进行定义是一样的。它告知Owl使用对应的
t-ref
关键词来引用元素。然后我们实现了
mounted
生命周期方法,对当前引用的输入框进行聚焦显示。
添加任务(第二部分)
在前一部分中,我们尚未实现任务创建部分的代码。下面进行编写。
需要有一种方式来生成
id
数字。这只需在
App
中添加一个
nextId
数字。同时,我们从
App
中删除演示任务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
addTask
(
ev
)
{
// 13 is keycode for ENTER
if
(
ev
.
keyCode
===
13
)
{
const
title
=
ev
.
target
.
value
.
trim
(
)
;
ev
.
target
.
value
=
""
;
if
(
title
)
{
const
newTask
=
{
id
:
this
.
nextId
++
,
title
:
title
,
isCompleted
:
false
,
}
;
this
.
tasks
.
push
(
newTask
)
;
}
}
}
切换任务状态
如果尝试将任务标记为完成,可能会注意到透明度没有任何变化。这是因为还没有写修改
isCompleted
标记的代码。
这里有一个有趣的状况:任务由
Task
组件显示,但它却不是状态的所有者,因而无法修改状态。我们转而希望通过通讯请求来切换
App
组件任务的状态。因
App
是
Task
的父组件,可以在
Task
中
触发
一个事件并在
App
中监听该事件。
在
Task
中,修改
input
如下:
toggleTask(ev) {
const task = this.tasks.find(t => t.id === ev.detail.id);
task.isCompleted = !task.isCompleted;
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
<span><t t-esc="props.task.title"/></span>
<span class="delete" t-on-click="deleteTask">🗑</span>
<
div
class
=
"task"
t
-
att
-
class
=
"props.task.isCompleted ? 'done' : ''"
>
<
input
type
=
"checkbox"
t
-
att
-
checked
=
"props.task.isCompleted"
t
-
on
-
click
=
"toggleTask"
/
>
<
span
>
<
t
t
-
esc
=
"props.task.title"
/
>
<
/
span
>
<
span
class
=
"delete"
t
-
on
-
click
=
"deleteTask"
>
🗑
<
/
span
>
<
/
div
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.
task
{
font
-
size
:
18px
;
color
:
#111111;
display
:
grid
;
grid
-
template
-
columns
:
30px
auto
30px
;
}
.
task
>
input
{
margin
:
auto
;
}
.
delete
{
opacity
:
0
;
cursor
:
pointer
;
text
-
align
:
center
;
}
.
task
:
hover
.
delete
{
opacity
:
1
;
}
deleteTask(ev) {
const index = this.tasks.findIndex(t => t.id === ev.detail.id);
this.tasks.splice(index, 1);
const { Component, Store } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
const { useRef, useDispatch, useStore } = owl.hooks;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
const actions = {
addTask({ state }, title) {
title = title.trim();
if (title) {
const task = {
id: state.nextId++,
title: title,
isCompleted: false,
state.tasks.push(task);
toggleTask({ state }, id) {
const task = state.tasks.find((t) => t.id === id);
task.isCompleted = !task.isCompleted;
deleteTask({ state }, id) {
const index = state.tasks.findIndex((t) => t.id === id);
state.tasks.splice(index, 1);
const initialState = {
nextId: 1,
tasks: [],
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
const TASK_TEMPLATE = xml/* xml */ `
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-on-click="dispatch('toggleTask', props.task.id)"/>
<span><t t-esc="props.task.title"/></span>
<span class="delete" t-on-click="dispatch('deleteTask', props.task.id)">🗑</span>
</div>`;
class Task extends Component {
static template = TASK_TEMPLATE;
static props = ["task"];
dispatch = useDispatch();
// -------------------------------------------------------------------------
// App Component
// -------------------------------------------------------------------------
const APP_TEMPLATE = xml/* xml */ `
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</div>`;
class App extends Component {
static template = APP_TEMPLATE;
static components = { Task };
inputRef = useRef("add-input");
tasks = useStore((state) => state.tasks);
dispatch = useDispatch();
mounted() {
this.inputRef.el.focus();
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
this.dispatch("addTask", ev.target.value);
ev.target.value = "";
// -------------------------------------------------------------------------
// Setup code
// -------------------------------------------------------------------------
function setup() {
owl.config.mode = "dev";
const store = new Store({ actions, state: initialState });
App.env.store = store;
const app = new App();
app.mount(document.body);
whenReady(setup);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
const
{
Component
,
Store
}
=
owl
;
const
{
xml
}
=
owl
.
tags
;
const
{
whenReady
}
=
owl
.
utils
;
const
{
useRef
,
useDispatch
,
useStore
}
=
owl
.
hooks
;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
const
actions
=
{
addTask
(
{
state
}
,
title
)
{
title
=
title
.
trim
(
)
;
if
(
title
)
{
const
task
=
{
id
:
state
.
nextId
++
,
title
:
title
,
isCompleted
:
false
,
}
;
state
.
tasks
.
push
(
task
)
;
}
}
,
toggleTask
(
{
state
}
,
id
)
{
const
task
=
state
.
tasks
.
find
(
(
t
)
=
>
t
.
id
===
id
)
;
task
.
isCompleted
=
!
task
.
isCompleted
;
}
,
deleteTask
(
{
state
}
,
id
)
{
const
index
=
state
.
tasks
.
findIndex
(
(
t
)
=
>
t
.
id
===
id
)
;
state
.
tasks
.
splice
(
index
,
1
)
;
}
,
}
;
const
initialState
=
{
nextId
:
1
,
tasks
:
[
]
,
}
;
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
const
TASK_TEMPLATE
=
xml
/* xml */
`
<
div
class
=
"task"
t
-
att
-
class
=
"props.task.isCompleted ? 'done' : ''"
>
<
input
type
=
"checkbox"
t
-
att
-
checked
=
"props.task.isCompleted"
t
-
on
-
click
=
"dispatch('toggleTask', props.task.id)"
/
>
<
span
>
<
t
t
-
esc
=
"props.task.title"
/
>
<
/
span
>
<
span
class
=
"delete"
t
-
on
-
click
=
"dispatch('deleteTask', props.task.id)"
>
🗑
<
/
span
>
<
/
div
>
`
;
class
Task
extends
Component
{
static
template
=
TASK_TEMPLATE
;
static
props
=
[
"task"
]
;
dispatch
=
useDispatch
(
)
;
}
// -------------------------------------------------------------------------
// App Component
// -------------------------------------------------------------------------
const
APP_TEMPLATE
=
xml
/* xml */
`
<
div
class
=
"todo-app"
>
<
input
placeholder
=
"Enter a new task"
t
-
on
-
keyup
=
"addTask"
t
-
ref
=
"add-input"
/
>
<
div
class
=
"task-list"
>
<
t
t
-
foreach
=
"tasks"
t
-
as
=
"task"
t
-
key
=
"task.id"
>
<
Task
task
=
"task"
/
>
<
/
t
>
<
/
div
>
<
/
div
>
`
;
class
App
extends
Component
{
static
template
=
APP_TEMPLATE
;
static
components
=
{
Task
}
;
inputRef
=
useRef
(
"add-input"
)
;
tasks
=
useStore
(
(
state
)
=
>
state
.
tasks
)
;
dispatch
=
useDispatch
(
)
;
mounted
(
)
{
this
.
inputRef
.
el
.
focus
(
)
;
}
addTask
(
ev
)
{
// 13 is keycode for ENTER
if
(
ev
.
keyCode
===
13
)
{
this
.
dispatch
(
"addTask"
,
ev
.
target
.
value
)
;
ev
.
target
.
value
=
""
;
}
}
}
// -------------------------------------------------------------------------
// Setup code
// -------------------------------------------------------------------------
function
setup
(
)
{
owl
.
config
.
mode
=
"dev"
;
const
store
=
new
Store
(
{
actions
,
state
:
initialState
}
)
;
App
.
env
.
store
=
store
;
const
app
=
new
App
(
)
;
app
.
mount
(
document
.
body
)
;
}
whenReady
(
setup
)
;
function makeStore() {
const localState = window.localStorage.getItem("todoapp");
const state = localState ? JSON.parse(localState) : initialState;
const store = new Store({ state, actions });
store.on("update", null, () => {
localStorage.setItem("todoapp", JSON.stringify(store.state));
return store;
function setup() {
owl.config.mode = "dev";
App.env.store = makeStore();
const app = new App();
app.mount(document.body);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function
makeStore
(
)
{
const
localState
=
window
.
localStorage
.
getItem
(
"todoapp"
)
;
const
state
=
localState
?
JSON
.
parse
(
localState
)
:
initialState
;
const
store
=
new
Store
(
{
state
,
actions
}
)
;
store
.
on
(
"update"
,
null
,
(
)
=
>
{
localStorage
.
setItem
(
"todoapp"
,
JSON
.
stringify
(
store
.
state
)
)
;
}
)
;
return
store
;
}
function
setup
(
)
{
owl
.
config
.
mode
=
"dev"
;
App
.
env
.
store
=
makeStore
(
)
;
const
app
=
new
App
(
)
;
app
.
mount
(
document
.
body
)
;
}
get displayedTasks() {
switch (this.filter.value) {
case "active": return this.tasks.filter(t => !t.isCompleted);
case "completed": return this.tasks.filter(t => t.isCompleted);
case "all": return this.tasks;
setFilter(filter) {
this.filter.value = filter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在文件顶部,重新添加useState
const
{
useRef
,
useDispatch
,
useState
,
useStore
}
=
owl
.
hooks
;
// in App:
filter
=
useState
(
{
value
:
"all"
}
)
get
displayedTasks
(
)
{
switch
(
this
.
filter
.
value
)
{
case
"active"
:
return
this
.
tasks
.
filter
(
t
=
>
!
t
.
isCompleted
)
;
case
"completed"
:
return
this
.
tasks
.
filter
(
t
=
>
t
.
isCompleted
)
;
case
"all"
:
return
this
.
tasks
;
}
}
setFilter
(
filter
)
{
this
.
filter
.
value
=
filter
;
}
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
<Task task="task"/>
<div class="task-panel" t-if="tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt tasks.length">
/ <t t-esc="tasks.length"/>
task(s)
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="setFilter(f)"
t-esc="f"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<
div
class
=
"todo-app"
>
<
input
placeholder
=
"Enter a new task"
t
-
on
-
keyup
=
"addTask"
t
-
ref
=
"add-input"
/
>
<
div
class
=
"task-list"
>
<
t
t
-
foreach
=
"displayedTasks"
t
-
as
=
"task"
t
-
key
=
"task.id"
>
<
Task
task
=
"task"
/
>
<
/
t
>
<
/
div
>
<
div
class
=
"task-panel"
t
-
if
=
"tasks.length"
>
<
div
class
=
"task-counter"
>
<
t
t
-
esc
=
"displayedTasks.length"
/
>
<
t
t
-
if
=
"displayedTasks.length lt tasks.length"
>
/
<
t
t
-
esc
=
"tasks.length"
/
>
<
/
t
>
task
(
s
)
<
/
div
>
<
div
>
<
span
t
-
foreach
=
"['all', 'active', 'completed']"
t
-
as
=
"f"
t
-
key
=
"f"
t
-
att
-
class
=
"{active: filter.value===f}"
t
-
on
-
click
=
"setFilter(f)"
t
-
esc
=
"f"
/
>
<
/
div
>
<
/
div
>
<
/
div
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.
task
-
panel
{
color
:
#0088ff;
margin
-
top
:
8px
;
font
-
size
:
14px
;
display
:
flex
;
}
.
task
-
panel
.
task
-
counter
{
flex
-
grow
:
1
;
}
.
task
-
panel
span
{
padding
:
5px
;
cursor
:
pointer
;
}
.
task
-
panel
span
.
active
{
font
-
weight
:
bold
;
}
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-att-id="props.task.id"
t-on-click="dispatch('toggleTask', props.task.id)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.title"/></label>
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
<script src="owl.js"></script>
<script src="app.js"></script>
</head>
<body></body>
</html>
const { Component, Store } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
const { useRef, useDispatch, useState, useStore } = owl.hooks;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
const actions = {
addTask({ state }, title) {
title = title.trim();
if (title) {
const task = {
id: state.nextId++,
title: title,
isCompleted: false,
state.tasks.push(task);
toggleTask({ state }, id) {
const task = state.tasks.find((t) => t.id === id);
task.isCompleted = !task.isCompleted;
deleteTask({ state }, id) {
const index = state.tasks.findIndex((t) => t.id === id);
state.tasks.splice(index, 1);
const initialState = {
nextId: 1,
tasks: [],
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
const TASK_TEMPLATE = xml/* xml */ `
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-att-id="props.task.id"
t-on-click="dispatch('toggleTask', props.task.id)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.title"/></label>
<span class="delete" t-on-click="dispatch('deleteTask', props.task.id)">🗑</span>
</div>`;
class Task extends Component {
static template = TASK_TEMPLATE;
static props = ["task"];
dispatch = useDispatch();
// -------------------------------------------------------------------------
// App Component
// -------------------------------------------------------------------------
const APP_TEMPLATE = xml/* xml */ `
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<Task t-foreach="displayedTasks" t-as="task" t-key="task.id" task="task"/>
<div class="task-panel" t-if="tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt tasks.length">
/ <t t-esc="tasks.length"/>
task(s)
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="setFilter(f)"
t-esc="f"/>
</div>`;
class App extends Component {
static template = APP_TEMPLATE;
static components = { Task };
inputRef = useRef("add-input");
tasks = useStore((state) => state.tasks);
filter = useState({ value: "all" });
dispatch = useDispatch();
mounted() {
this.inputRef.el.focus();
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
this.dispatch("addTask", ev.target.value);
ev.target.value = "";
get displayedTasks() {
switch (this.filter.value) {
case "active":
return this.tasks.filter((t) => !t.isCompleted);
case "completed":
return this.tasks.filter((t) => t.isCompleted);
case "all":
return this.tasks;
setFilter(filter) {
this.filter.value = filter;
// -------------------------------------------------------------------------
// Setup code
// -------------------------------------------------------------------------
function makeStore() {
const localState = window.localStorage.getItem("todoapp");
const state = localState ? JSON.parse(localState) : initialState;
const store = new Store({ state, actions });
store.on("update", null, () => {
localStorage.setItem("todoapp", JSON.stringify(store.state));
return store;
function setup() {
owl.config.mode = "dev";
App.env.store = makeStore();
const app = new App();
app.mount(document.body);
whenReady(setup);
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
(
function
(
)
{
const
{
Component
,
Store
}
=
owl
;
const
{
xml
}
=
owl
.
tags
;
const
{
whenReady
}
=
owl
.
utils
;
const
{
useRef
,
useDispatch
,
useState
,
useStore
}
=
owl
.
hooks
;
// -------------------------------------------------------------------------
// Store
// -------------------------------------------------------------------------
const
actions
=
{
addTask
(
{
state
}
,
title
)
{
title
=
title
.
trim
(
)
;
if
(
title
)
{
const
task
=
{
id
:
state
.
nextId
++
,
title
:
title
,
isCompleted
:
false
,
}
;
state
.
tasks
.
push
(
task
)
;
}
}
,
toggleTask
(
{
state
}
,
id
)
{
const
task
=
state
.
tasks
.
find
(
(
t
)
=
>
t
.
id
===
id
)
;
task
.
isCompleted
=
!
task
.
isCompleted
;
}
,
deleteTask
(
{
state
}
,
id
)
{
const
index
=
state
.
tasks
.
findIndex
(
(
t
)
=
>
t
.
id
===
id
)
;
state
.
tasks
.
splice
(
index
,
1
)
;
}
,
}
;
const
initialState
=
{
nextId
:
1
,
tasks
:
[
]
,
}
;
// -------------------------------------------------------------------------
// Task Component
// -------------------------------------------------------------------------
const
TASK_TEMPLATE
=
xml
/* xml */
`
<
div
class
=
"task"
t
-
att
-
class
=
"props.task.isCompleted ? 'done' : ''"
>
<
input
type
=
"checkbox"
t
-
att
-
checked
=
"props.task.isCompleted"
t
-
att
-
id
=
"props.task.id"
t
-
on
-
click
=
"dispatch('toggleTask', props.task.id)"
/
>
<
label
t
-
att
-
for
=
"props.task.id"
>
<
t
t
-
esc
=
"props.task.title"
/
>
<
/
label
>
<
span
class
=
"delete"
t
-
on
-
click
=
"dispatch('deleteTask', props.task.id)"
>
🗑
<
/
span
>
<
/
div
>
`
;
class
Task
extends
Component
{
static
template
=
TASK_TEMPLATE
;
static
props
=
[
"task"
]
;
dispatch
=
useDispatch
(
)
;
}
// -------------------------------------------------------------------------
// App Component
// -------------------------------------------------------------------------
const
APP_TEMPLATE
=
xml
/* xml */
`
<
div
class
=
"todo-app"
>
<
input
placeholder
=
"Enter a new task"
t
-
on
-
keyup
=
"addTask"
t
-
ref
=
"add-input"
/
>
<
div
class
=
"task-list"
>
<
Task
t
-
foreach
=
"displayedTasks"
t
-
as
=
"task"
t
-
key
=
"task.id"
task
=
"task"
/
>
<
/
div
>
<
div
class
=
"task-panel"
t
-
if
=
"tasks.length"
>
<
div
class
=
"task-counter"
>
<
t
t
-
esc
=
"displayedTasks.length"
/
>
<
t
t
-
if
=
"displayedTasks.length lt tasks.length"
>
/
<
t
t
-
esc
=
"tasks.length"
/
>
<
/
t
>
task
(
s
)
<
/
div
>
<
div
>
<
span
t
-
foreach
=
"['all', 'active', 'completed']"
t
-
as
=
"f"
t
-
key
=
"f"
t
-
att
-
class
=
"{active: filter.value===f}"
t
-
on
-
click
=
"setFilter(f)"
t
-
esc
=
"f"
/
>
<
/
div
>
<
/
div
>
<
/
div
>
`
;
class
App
extends
Component
{
static
template
=
APP_TEMPLATE
;
static
components
=
{
Task
}
;
inputRef
=
useRef
(
"add-input"
)
;
tasks
=
useStore
(
(
state
)
=
>
state
.
tasks
)
;
filter
=
useState
(
{
value
:
"all"
}
)
;
dispatch
=
useDispatch
(
)
;
mounted
(
)
{
this
.
inputRef
.
el
.
focus
(
)
;
}
addTask
(
ev
)
{
// 13 is keycode for ENTER
if
(
ev
.
keyCode
===
13
)
{
this
.
dispatch
(
"addTask"
,
ev
.
target
.
value
)
;
ev
.
target
.
value
=
""
;
}
}
get
displayedTasks
(
)
{
switch
(
this
.
filter
.
value
)
{
case
"active"
:
return
this
.
tasks
.
filter
(
(
t
)
=
>
!
t
.
isCompleted
)
;
case
"completed"
:
return
this
.
tasks
.
filter
(
(
t
)
=
>
t
.
isCompleted
)
;
case
"all"
:
return
this
.
tasks
;
}
}
setFilter
(
filter
)
{
this
.
filter
.
value
=
filter
;
}
}
// -------------------------------------------------------------------------
// Setup code
// -------------------------------------------------------------------------
function
makeStore
(
)
{
const
localState
=
window
.
localStorage
.
getItem
(
"todoapp"
)
;
const
state
=
localState
?
JSON
.
parse
(
localState
)
:
initialState
;
const
store
=
new
Store
(
{
state
,
actions
}
)
;
store
.
on
(
"update"
,
null
,
(
)
=
>
{
localStorage
.
setItem
(
"todoapp"
,
JSON
.
stringify
(
store
.
state
)
)
;
}
)
;
return
store
;
}
function
setup
(
)
{
owl
.
config
.
mode
=
"dev"
;
App
.
env
.
store
=
makeStore
(
)
;
const
app
=
new
App
(
)
;
app
.
mount
(
document
.
body
)
;
}
whenReady
(
setup
)
;
}
)
(
)
;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
.
todo
-
app
{
width
:
300px
;
margin
:
50px
auto
;
background
:
aliceblue
;
padding
:
10px
;
}
.
todo
-
app
>
input
{
display
:
block
;
margin
:
auto
;
}
.
task
-
list
{
margin
-
top
:
8px
;
}
.
task
{
font
-
size
:
18px
;
color
:
#111111;
display
:
grid
;
grid
-
template
-
columns
:
30px
auto
30px
;
}
.
task
:
hover
{
background
-
color
:
#def0ff;
}
.
task
>
input
{
margin
:
auto
;
}
.
delete
{
opacity
:
0
;
cursor
:
pointer
;
text
-
align
:
center
;
}
.
task
:
hover
.
delete
{
opacity
:
1
;
}
.
task
.
done
{
opacity
:
0.7
;
}
.
task
.
done
label
{
text
-
decoration
:
line
-
through
;
}
.
task
-
panel
{
color
:
#0088ff;
margin
-
top
:
8px
;
font
-
size
:
14px
;
display
:
flex
;
}
.
task
-
panel
.
task
-
counter
{
flex
-
grow
:
1
;
}
.
task
-
panel
span
{
padding
:
5px
;
cursor
:
pointer
;
}
.
task
-
panel
span
.
active
{
font
-
weight
:
bold
;
}