Odoo模板:常用指令解析

763 阅读5分钟

.bind指令

父组件将回调函数作为prop传递到子组件是一种很常见的操作。由于OWL组件是类组件,回调函数在运行时通常需要绑定父组件的运行环境,使用.bind指令可以方便的实现该操作。

  • 类组件:类组件内部有this保存组件的状态,回调函数如果不绑定父组件的this,会通过子组件的this调用回调函数
  • 函数式组件:基于函数式编程的思想,组件在两次外部输入相同的情况下,必定有一致的输出效果。函数式组件内部没有this指向模糊性的问题。OWL不支持函数式组件。React框架主推函数式组件。

不使用.bind指令的写法,回调函数绑定父组件this后,再进行传递

class SomeComponent extends Component {
  static template = xml`
    <div>
      <Child callback="doSomething"/>
    </div>`;

  setup() {
    this.doSomething = this.doSomething.bind(this);
  }

  doSomething() {
    // ...
  }
}

使用OWL提供的.bind指令,就可以直接在模板中完成绑定,简化了代码

class SomeComponent extends Component {
  static template = xml`
    <div>
      <Child callback.bind="doSomething"/>
    </div>`;

  doSomething() {
    // ...
  }
}

t-slot指令

OWL可以定义一些通用组件,这些通用组件可以减少组件的重复定义,例如导航栏是一个通用组件,开发人员只需要根据需要向导航栏插入特定内容即可,而不需要定义一个完整的导航栏组件。在通用组件中使用t-slot指令设置内容插入位置,使用t-set-slot向通用组件中插入特定内容。

t-set-slot指令的参数

有些情况下使用t-set-slot向通用组件插入内容时,需要附加一些额外的信息,可以通过下列方式实现:

<div class="info-box" t-name="InfoBox">
  <div class="info-box-title">
    <t t-slot="title"/>
    <span class="info-box-close-button" t-on-click="close">X</span>
  </div>
</div>

<!-- 使用通用组件,插入特定内容,同时传递额外信息 -->
<InfoBox>
  <t t-set-slot="title" subtitle="subtitle">
    Specific Title. It could be html also.
  </t>
</InfoBox>
  • 在通用组件中使用this.props.slots['title'].subtitle就可以获取传递的参数。
  • slot参数与props的使用方式类似,可以使用.bind指令对回调函数进行绑定。
  • 只能在<t>标签中使用t-set-slot指令

t-slot-scope指令

插入内容需要依靠通用组件内部的信息来完成渲染,此时可以使用t-slot-scope定义一个变量,来访问通用组件内部的信息。比如下面的例子,对话框是一个通用组件,用户可以在对话框的底部插入特定内容,但是底部关闭按钮的功能依赖于对话框内部的实现

<t t-name="web.Dialog" owl="1">
    <footer t-if="props.footer">
        <!-- 定义close -->
        <t t-slot="footer" close="data.close">
            <button class="btn btn-primary o-default-button" t-on-click="data.close">
                <t>Ok</t>
            </button>
        </t>
    </footer>
</t>

<Dialog>
   ...
    <t t-set-slot="footer" t-slot-scope="dialog">
            <!-- 使用变量dialog访问close -->
        <button class="btn btn-secondary" t-on-click="dialog.close">
            Discard
        </button>
    </t>
</Dialog>

当向默认插槽插入内容时,可以不使用t-set-slot指令,此时如果需要使用t-slot-scope指令,可以这样:

    <Transition visible="state.show" name="'o-fade'" t-slot-scope="transition" >
        <span class="o_loading_indicator" t-att-class="transition.className">
            Loading 
        </span>
    </Transition>

使用slots插入内容

组件内部使用props.slots来保存插入的内容。除了使用t-set-slot指令,还可以直接使用slots属性来插入内容

<t t-name="web.Layout" owl="1">
    ...
    <t t-component="components.ControlPanel" slots="controlPanelSlots" t-if="display.controlPanel" display="display.controlPanel"/>
    ...
</t>
export class Layout extends Component {
    ...
    // controlPanelSlots属性对应的getter
    get controlPanelSlots() {
        // Layout组件接收的slots插入到components.ControlPanel对应的组件
        // 例如:FormControlPanel
        const slots = { ...this.props.slots };
        slots["control-panel-bottom-left-buttons"] = slots["layout-buttons"];
        delete slots["layout-buttons"];
        delete slots.default;
        return slots;
    }
    ...
}

t-set-slot指令在模板编译阶段会被放到slots中,无视t-ift-slot限制,比如<t t-if="env.isSmall"><t t-set-slot="xxx">...</t></t>

  • 通用组件没有使用t-slot定义xxx插槽
  • 当前环境非移动端环境 slots中还会出现{xxx: {...} ...}
  • 参考源码

插入内容的渲染上下文

插槽插入的内容,是在父组件的上下文中进行取值,下面代码摘自ImageField组件的模板。第二个button插入到FileUploader组件默认插槽中,buttont-if指令对应的表达式从ImageField组件中取值。由于在父组件的上下文中取值,因此t-on-click对应的方法无需使用.bind绑定thisonUploaded方法是在FileUploader上下文中取值,因此onFileUploaded方法需要绑定this

<FileUploader
    acceptedFileExtensions="props.acceptedFileExtensions"
    t-key="props.record.resId"
    onUploaded.bind="onFileUploaded"
    type="'image'"
>
    <t t-set-slot="toggler">
        <button
            class="o_select_file_button btn btn-light border-0 rounded-circle m-1 p-1"
            data-tooltip="Edit"
            aria-label="Edit">
            <i class="fa fa-pencil fa-fw"/>
        </button>
    </t>
    <button
        t-if="props.value and state.isValid"
        class="o_clear_file_button btn btn-light border-0 rounded-circle m-1 p-1"
        data-tooltip="Clear"
        aria-label="Clear"
        t-on-click="onFileRemove">
        <i class="fa fa-trash-o fa-fw"/>
    </button>
</FileUploader>

插入内容的渲染context来自Parent组件

t-portal指令

有时候需要在组件之外呈现一些内容,这种情况下可以使用t-portal指令。

class SomeComponent extends Component {
  static template = xml`
      <div>this is inside the component</div>
      <div t-portal="'body'">and this is outside</div>
    `;
}

t-portal指令的参数是一个css选择器

// 源码来源于owl.js
class VPortal extends VText {
    ...
    mount(parent, anchor) {
        super.mount(parent, anchor);
        // this.selector就是t-portal指令接收的参数
        this.target = document.querySelector(this.selector);
        if (this.target) {
            // this.content ComponentNode
            this.content.mount(this.target, null);
        }
        else {
            this.content.mount(parent, anchor);
        }
    }
    ...
  }

Odoo移动端的侧边栏菜单就是使用了t-portal指令来实现的

<div t-name="web.BurgerMenu" owl="1">
    <!-- 这个按钮控制侧边栏的显示与隐藏 -->
    <button
        class="o_mobile_menu_toggle o-no-caret d-md-none border-0"
        title="Toggle menu" aria-label="Toggle menu"
        t-on-click="_openBurger">
        <i class="oi oi-panel-right"/>
    </button>
    <!-- 将侧边栏组件挂载到body节点 -->
    <t t-portal="'body'">
      <Transition name="'burgerslide'" visible="state.isBurgerOpened" leaveDuration="200" t-slot-scope="transition">
        ...
      </Transition>
    </t>
    <t t-portal="'body'">
        <div t-if="state.isBurgerOpened" class="o_burger_menu_backdrop modal-backdrop show d-block d-md-none" t-on-click.stop="_closeBurger" t-on-touchstart.stop="_onSwipeStart" t-on-touchend.stop="_onSwipeEnd" />
    </t>
</div>
image.png

点击后,侧边栏组件挂载到body image.png

t-call指令

在OWL模板中可以通过t-call指令来调用子模板,从而实现模板的复用。子模板渲染的时候使用的是父模板的上下文。虽然子模板是内联到父模板中的,但是子模板有自己的作用域,子模板中定义的变量不会暴露到父模板中。

向子模板传递信息

<em>content</em>会被插入到t-raw="0"所在的位置。

<t t-name="other-template">
    This template was called with content:
    <t t-raw="0"/>
</t>

<div t-name="main-template">
    <t t-call="other-template">
        <em>content</em>
    </t>
</div>

<!-- 效果 -->

<div>
    This template was called with content:
    <em>content</em>
</div>

还可以通过这种方式在父模板中为子模板定义变量

<t t-call="other-template">
    <t t-set="var" t-value="1"/>
</t>
<!-- 变量var在父模板中不可见 -->

t-call-context指令

子模板默认使用父模板的上下文进行渲染。可以使用t-call-context指令为子模板指定特定的渲染上下文

<t t-call="other-template" t-call-context="obj"/>

t-inherit指令

该指令用于继承修改模板,修改模板有两种方式,通过指令t-inherit-mode指定

  • t-inherit-mode="primary": 使用被继承模块创建一个新模板(默认继承方式,不影响已有组件)

    <t t-name="child.template" t-inherit="base.template" t-inherit-mode="primary">
        <xpath expr="//ul" position="inside">
            <li>new element</li>
        </xpath>
    </t>
    
  • t-inherit-mode="extension": 修改被继承的模版(会改变已有组件)

    <t t-inherit="base.template" t-inherit-mode="extension">
        <xpath expr="//tr[1]" position="after">
            <tr><td>new cell</td></tr>
        </xpath>
    </t>