OWL教程0 创建一个todoList App

OWL教程0 创建一个todoList App

原文地址:https://github.com/odoo/owl/blob/master/README.md#documentation

教程: 创建一个todoList App

在这篇教程里,我们将创建一个非常简单的TodoList 应用,该app应该满足下面的需求:

  • 让用户添加和移除任务
  • 任务可以标记为已完成
  • 任务可以根据状态(活跃/已完成)来过滤显示

这个工程师探索和学习一些Owl重要概念的非常好的机会,比如组件,存储以及怎么组织一个应用.

1.设置工程

这篇教程,我们将创建一个非常简单的工程,只有一些静态文件没有额外的工具. 第一步创建下列文件结构:

todoapp/
    index.html
    app.css
    app.js
    owl.js

这个应用的入口是index.html, 它包含下面的内容:

DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>OWL Todo Apptitle>
    <link rel="stylesheet" href="app.css" />
  head>
  <body>
    <script src="owl.js">script>
    <script src="app.js">script>
  body>
html>

然后,app.css现在可以空着,它将在后面用于定制我们应用的样式. app.js 是我们需要写代码的地方,现在,让我们写上如下代码:

(function () {
  console.log("hello owl", owl.__info__.version);
})();

注意: 我们将所有代码都放在一个立即执行函数里, 这样可以避免污染全局作用域.

最后,owl.js应该是从owl仓库下载的最新版本,当然你也可以使用owl.min.js , 注意,你还应该下载owl.iife.js 因为这些文件是用来在浏览器直接运行的,将它重命名为owl.js.(其他的文件比如owl.cjs.js捆绑了其他工具,可能比较大一些)

现在,工程已经准备好了,在浏览器里加载index.html会显示一个空的页面, 标题是"Owl Todo App",它应该在控制台显示一个信息,比如 hello owl 2.x.y

2.添加第一个组件

一个Owl程序由多个组件组成,只有一个单独的根组件.让我们从定义根组件开始. 用下列代码代替app.js的内容

const {Component,mout,xml} = owl;

// Owl Component
class Root extends Component{
	static template=xml`
todo app
`
} mount(Root,document.body)

现在,在浏览器中刷新页面会显示一条信息.

代码相当简单,我们通过内联模板定义了一个组件,然后将它挂载到document body.

要点1

一个大的工程,我们将会把代码分别写在多个不同的文件里,组件在不同的子目录中,一个main文件初始化整个应用, 然而,这是一个非常小的项目,我们让它尽可能简单.

要点2

这篇教程使用了静态的类属性语法,并不是左右的浏览器都支持这种写法. 大多数真实的工程都会对代码进行编译,所以这不是一个问题. 但是对于这篇教程来说,你如果想让代码运行在任一浏览器上,你需要将用static关键字进行的赋值改成这样:

class App extends Compents {}
App.template=xml`
todo app
`
;

要点3

使用xml助手写内联模板是好的,但是没有语法高亮, 这很容易出现语法错误的xml, 一些编辑器支持语法高亮,比如 vscode有一个插件 “Comment tagged template”, 如果安装了它,它会正正确的显示标记模板.

要点4

对于大型应用程序来说,使用内联模板会稍微增加一些困难,因为我们需要额外的工具来提取代码中的xml,并将其替换为翻译后的值。

3.显示任务列表

现在基础工作已经做好了,是时候考虑任务了.为了实现我们的需求,我们将通过一个对象数组来记录任务,它包含下列关键字:

  • id: 一个数字,任务的唯一标识.
  • text: 一个字符串, 用于描述任务
  • isCompleted: 布尔类型,记录任务状态

现在我们已经确定了数据格式,让我们给app组件添加一些演示数据和模板.

class Root extends Component{
	static template = xml`
        
`
; tasks=[{ id:1, text: "buy milk", isCompleted:true, },{ id:2, text: "clean house", isCompleted:false, }] }

模板中包含了t-foreach 循环来遍历任务,它可以从组件中发现任务列表,因为在渲染的上下文中包含了组件的属性,注意我们使用id作为t-key, 这很普遍, 这里有两个css类, task-list和task, 我们将在下一小节使用他们.

4 布局: 基本的CSS

到目前为止,我们的任务列表看上去相当难看, 让我们在app.css中增加下面的代码

.task-list {
  width: 300px;
  margin: 50px auto;
  background: aliceblue;
  padding: 10px;
}

.task {
  font-size: 18px;
  color: #111111;
}

这样好多了,现在,我们来增加额外的特性. 已完成的任务风格让他稍微不同, 让它看上去没有那么重要,要做到这一点,我们需要给每条任务增加一个动态的css class:

<div class="task" t-att-class="task.isCompleted? 'done': ''">
.task.done {
	opacity:0.7
}

注意: 这里我们看到了动态属性的另外一种用法.

5.将Task提取为子组件

现在很清楚了,我们需要一个task组件来描述一条任务的外观和行为.

Task组件用来显示一条任务,但是它不拥有任务的状态: 一组数组只有一个拥有者. 否则就是自找麻烦.所以, Task组件通过prop属性来获取它的数据. 这意味着,数据存储在App组件中,但是可以被Task组件使用(不能修改)

由于我们再移动代码,这是重构代码的好机会.

// -----------------------------------------------------------------
// Task Component
// -----------------------------------------------------------------
class Task extends Component {
	static template = xml`
		
`
; static props=['task'] } // ----------------------------------------------------------------- // Root Component // ----------------------------------------------------------------- class Root extends Component{ static template=xml`
`
; static component={Task}; tasks=[{ id:1, text: "buy milk", isCompleted:true, },{ id:2, text: "clean house", isCompleted:false, }] } // ----------------------------------------------------------------- // Setup // ----------------------------------------------------------------- mount(Root, document.body);

这里发生了很多事情:

第一,我们有了一个子组件Task, 在文件的顶部被定义.

第二 无论什么时候我们定义子组件,都需要将它添加到静态属性components中

第三 Task组件有一个props属性, 这只是出于验证的目的,它表明每一Task组件都要给一个名字叫task的属性值,否则,Owl会报错, 这在重构组件的时候会很有用.

最后, 为了激活属性验证,我们需要将Owl的模式设置为"dev", 这是在mount函数的最后一个参数完成的, 注意,在生产环境下应该移除它,因为dev模式会稍微慢一点,因为它要做一些额外的检测和校验.

6 增加任务(part1)

我们依然在使用一个硬编码的任务列表,真的是时候让用户自己来添加任务了. 第一步是添加一个input到Root组件,但是这个input要在task list外面,所以我们需要调整Root的模板,js以及css.

addTask(ev) {
    // 13 is keycode for ENTER
    if (ev.keyCode === 13) {
        const text = ev.target.value.trim();
        ev.target.value = "";
        console.log('adding task', text);
        // todo
    }
}
.todo-app {
  width: 300px;
  margin: 50px auto;
  background: aliceblue;
  padding: 10px;
}

.todo-app > input {
  display: block;
  margin: auto;
}

.task-list {
  margin-top: 8px;
}

我们现在有了一个工作的input框,当我们增加一条任务的时候会在控制台打印出来. 注意,当我们加载页面, input框没有获取焦点, 但是添加任务是任务列表的一个核心特性. 所以,让我们尽可能快的让input框获取焦点.

我们需要在Root组件准备好的时候(mounted)执行一些代码, 让我们使用onMounted钩子,我们也需要引用这个input框, 可以通过useRef钩子使用 t-ref指令.


// on top of file:
const { Component, mount, xml, useRef, onMounted } = owl;
// in App
setup() {
    const inputRef = useRef("add-input");
    onMounted(() => inputRef.el.focus());
}

这是非常常见的场景: 无论什么时候我们需要执行一些动作依赖于组件的生命周期循环,我们需要在setup方法中使用生命周期钩子, 这里,我们第一步获取到inputRef, 然后再onMounted钩子中,我们简单的让html元素获得焦点.

7 添加任务(part 2)

前一章节,我们做了所有事情除了真的添加任务. 现在让我们实现它.

我们需要一个方法来生成唯一的id, 我们在App中增加一个nextId, 同时移除演示数据tasks

nextId = 1;
tasks = [];

现在,addTask方法可以这样实现:

addTask(ev) {
    // 13 is keycode for ENTER
    if (ev.keyCode === 13) {
        const text = ev.target.value.trim();
        ev.target.value = "";
        if (text) {
            const newTask = {
                id: this.nextId++,
                text: text,
                isCompleted: false,
            };
            this.tasks.push(newTask);
        }
    }
}

这几乎就工作了,但是如果你测试它,你会注意到,你按回车后,新的任务并没有显示出来.但是你添加debugger或者console.log语句, 你会看到,代码确实如期望的运行了. 问题在于Owl没办法知道它需要重新渲染用户界面. 我们可以通过让tasks reactive来解决这个问题,使用useState钩子.

// on top of the file
const { Component, mount, xml, useRef, onMounted, useState } = owl;

// replace the task definition in App with the following:
tasks = useState([]);

现在它可以如预期工作了.

8 任务切换

如果你尝试标记一条任务为已完成,你会注意到任务内容的透明度并没有发生变化,这是因为没有代码去修改isCompleted 标志.

现在,这是有趣的解决方案: 任务是通过Task组件显示的,单它却不是数据的拥有者.所以理想情况下,它不应该改变它. 然而,现在,这就是我们要做的(后面会改进它), 在Task组件中,修改input标签:

<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>

增加 toggleTask 方法: 注意: 一定要添加this关键字

toggleTask() {
  this.props.task.isCompleted = !this.props.task.isCompleted;
}

9 删除任务

让我们现在增加删除任务的功能. 这根之前的功能是不同的: 删除任务必须在任务自身做,但是实际的操作需要在任务列表. 所以,我们需要跟Root组件通信, 这通常通过提供一个callback函数来实现.

首先,让我们更新Task组件的模板,css和js

.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;
}
static props = ["task", "onDelete"];

deleteTask() {
    this.props.onDelete(this.props.task);
}

现在我们需要在Root组件中为每一条任务提供 onDelete的回调方法.

  
deleteTask(task) {
    const index = this.tasks.findIndex(t => t.id === task.id);
    this.tasks.splice(index, 1);
}

注意: onDelete 属性的定义有一个后缀.bind, 这是一个特殊的后缀用来确保回调函数跟组件做了绑定.

通过测试发现: 如果不加这个后缀,在回调函数里,this是没用的.

另外还要注意,我们有两个函数名字都叫deleteTask, task组件只是将工作委托给Root组件.

10 使用存储

看一下代码,很明显,所有处理任务的代码都分散在应用程序的各个地方。此外,它还混合了UI代码和业务逻辑代码。Owl没有提供任何高级抽象来管理业务逻辑,但是使用基本的响应性原语(useState和reactive)很容易做到这一点。

让我们在程序中使用它来实现中央存储,这是相当大的重构,(对我们的程序而言),因为它实现了将所有任务相关的代码从组件中抽取出来. 这里是app.js文件的最新内容:

const { Component, mount, xml,useRef,onMounted,useState,reactive,useEnv } = owl;

// --------------------------------------------------------
// Store
// --------------------------------------------------------
function useStore(){
  const env= useEnv();
  return useState(env.store)
}

// --------------------------------------------------------
// tasklist
// --------------------------------------------------------
class TaskList{
  nextId = 1;
  tasks=[];

  addTask(text){
    if(text){
      const task={
        id:this.nextId++,
        text: text,
        isCompleted:false
      }
      this.tasks.push(task);
    }
  }

  toggleTask(task){
    task.isCompleted = !task.isCompleted
  }
  deleteTask(task){
    const index= this.tasks.findIndex(t => t.id ===task.id)
    this.tasks.splice(index,1)
  }
}

function createTaskStore(){
  return reactive(new TaskList())
}
// --------------------------------------------------------
// Task Components
// --------------------------------------------------------

class Task extends Component{
  static template = xml /* xml */`
              
`
; setup(){ this.store=useStore() } static props=["task"]; } // -------------------------------------------------------- // Root Components // -------------------------------------------------------- class Root extends Component { static template = xml/* xml */ `
`
; static components={Task}; setup(){ const useref= useRef("add-todo"); onMounted(()=>useref.el.focus()) this.store = useStore(); } addTask(ev){ if(ev.keyCode == 13){ this.store.addTask(ev.target.value); ev.target.value = ""; } } } // -------------------------------------------------------- // Setup // -------------------------------------------------------- const env={ store:createTaskStore(), } mount(Root, document.body,{dev:true,env});

重构后的代码: 将数据相关的逻辑从组件中抽取出来.

11.在本地存储中保存任务

现在,我们的todoApp可以很好的工作, 除了用户关闭或者刷新浏览器! 数据只保存在内存中是相当不方便的,为了解决这个问题,我们将利用本地存储来保存数据, 对于我们的代码来说,改变很简单,我们需要将任务保存在本地存储中并且监听任何改变.

class TaskList {
  constructor(tasks) {
    this.tasks = tasks || [];
    const taskIds = this.tasks.map((t) => t.id);
    this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
  }
  // ...
}

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

关键点是reactive方法, 它有一个回调函数,每当观测值发生变化的时候,回调函数都会执行.

注意: 我们需要调用saveTasks方法来初始化确保我们能观测到现在所有的值.

12 过滤任务

我们几乎完成了,我们可以增加,更新,删除任务,唯一漏掉的特性是根据任务状态来显示任务.

我们需要在Root中保存过滤器的状态,然后根据它的值来显示任务.

class Root extends Component {
  static template = xml /* xml */`
    
/ task(s)
`
; setup() { ... this.filter = useState({ value: "all" }); } get displayedTasks() { const tasks = this.store.tasks; switch (this.filter.value) { case "active": return tasks.filter(t => !t.isCompleted); case "completed": return tasks.filter(t => t.isCompleted); case "all": return tasks; } } setFilter(filter) { this.filter.value = filter; } }

注意: 这里我们设置过滤器动态的css类使用的是对象语法

t-att-class="{active: filter.value===f}"

13. 最后一击(The Final Touch)

我们的任务列表的所有特性都完成了,不过我们依然可以增加额外的一些细节来提高用户体验.

1 当用户鼠标滑过任务时,增加一个视觉反馈

.task:hover {
  background-color: #def0ff;
}
  1. 让任务的文本可以点击,切换它的复选框


3 . 改变完成任务的文本的风格

.task.done label {
  text-decoration: line-through;
}

最后的话

我的的程序现在完成了,它能很好的工作, UI代码能和商业逻辑代码很好的分离,它可以测试,总共不到150行代码

这里是最后的代码:

index.html

DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>OWL Todo Apptitle>
    <link rel="stylesheet" href="app.css" />
  head>
  <body>
    <script src="owl.js">script>
    <script src="app.js">script>
  body>
html>

app.js

(function () {
  const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;

  // -------------------------------------------------------------------------
  // Store
  // -------------------------------------------------------------------------
  function useStore() {
    const env = useEnv();
    return useState(env.store);
  }

  // -------------------------------------------------------------------------
  // TaskList
  // -------------------------------------------------------------------------
  class TaskList {
    constructor(tasks) {
      this.tasks = tasks || [];
      const taskIds = this.tasks.map((t) => t.id);
      this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
    }

    addTask(text) {
      text = text.trim();
      if (text) {
        const task = {
          id: this.nextId++,
          text: text,
          isCompleted: false,
        };
        this.tasks.push(task);
      }
    }

    toggleTask(task) {
      task.isCompleted = !task.isCompleted;
    }

    deleteTask(task) {
      const index = this.tasks.findIndex((t) => t.id === task.id);
      this.tasks.splice(index, 1);
    }
  }

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

  // -------------------------------------------------------------------------
  // Task Component
  // -------------------------------------------------------------------------
  class Task extends Component {
    static template = xml/* xml */ `
      
`
; static props = ["task"]; setup() { this.store = useStore(); } } // ------------------------------------------------------------------------- // Root Component // ------------------------------------------------------------------------- class Root extends Component { static template = xml/* xml */ `
/ task(s)
`
; static components = { Task }; setup() { const inputRef = useRef("add-input"); onMounted(() => inputRef.el.focus()); this.store = useStore(); this.filter = useState({ value: "all" }); } addTask(ev) { // 13 is keycode for ENTER if (ev.keyCode === 13) { this.store.addTask(ev.target.value); ev.target.value = ""; } } get displayedTasks() { const tasks = this.store.tasks; switch (this.filter.value) { case "active": return tasks.filter((t) => !t.isCompleted); case "completed": return tasks.filter((t) => t.isCompleted); case "all": return tasks; } } setFilter(filter) { this.filter.value = filter; } } // ------------------------------------------------------------------------- // Setup // ------------------------------------------------------------------------- const env = { store: createTaskStore() }; mount(Root, document.body, { dev: true, env }); })();

app.css

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

你可能感兴趣的:(odoo16官方文档翻译计划,odoo,owl)