相关文章推荐
卖萌的烤地瓜  ·  MySQL整库同步Kafka - ...·  10 月前    · 
卖萌的烤地瓜  ·  All Aboard, Part 3: ...·  10 月前    · 
卖萌的烤地瓜  ·  typescript - Vue | ...·  11 月前    · 
聪明的作业本  ·  Advanced query ...·  1小时前    · 
失望的鸡蛋面  ·  "Microsoft Outlook ...·  1小时前    · 
坚强的柿子  ·  mongodb 多表关联处理 : ...·  1小时前    · 
不爱学习的火腿肠  ·  java ...·  3 小时前    · 
旅行中的铁链  ·  错误信息:SSL ShakeHand ...·  3 小时前    · 
憨厚的金鱼  ·  Scanpy数据结构:AnnData - 何帅 ·  3 小时前    · 

Odoo 14前端框架OWL之创建一个待办清单TodoList应用

Odoo Alan 3年前 (2020-08-26) 10418次浏览 6个评论 扫描二维码

完整目录请见 Odoo 14全新前端框架 OWL(Odoo Web Library)官方文档中文版

🦉 OWL教程:待办清单TodoApp 🦉

本教程中,我们将创建一个简单的待办清单应用。该应用需满足以下要求:

  • 让用户可以创建并删除任务
  • 任务可标记为完成
  • 可对任务进行过滤展示活跃/已完成任务
  • 通过本项目有机会发现并学习Owl的一些重要概念,比如组件、存储以及如何组织应用。

  • 添加第一个组件
  • 展示任务清单
  • 布局:一些基础css
  • 将任务提取为子组件
  • 添加任务(第一部分)
  • 添加任务(第二部分)
  • 切换任务状态
  • 在本地存储中保存任务
  • 最后的修饰
  • 本教程中,我们创建一个非常简单的项目,包含静态文件但没有其它工具。第一步是创建如下的文件结构:

    <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>

    注意这里把所有代码放到了立即执行的函数中,来避免对全局产生任何影响。

    最后应当从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);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const { Component } = owl ;
    const { xml } = owl . tags ;
    const { whenReady } = owl . utils ;
    // 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>
    1
    2
    3
    4
    5
    < 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 ;
    }
     
    推荐文章