.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-if与t-slot限制,比如<t t-if="env.isSmall"><t t-set-slot="xxx">...</t></t>
- 通用组件没有使用
t-slot定义xxx插槽 - 当前环境非移动端环境
slots中还会出现{xxx: {...} ...} - 参考源码
插入内容的渲染上下文
插槽插入的内容,是在父组件的上下文中进行取值,下面代码摘自ImageField组件的模板。第二个button插入到FileUploader组件默认插槽中,button的t-if指令对应的表达式从ImageField组件中取值。由于在父组件的上下文中取值,因此t-on-click对应的方法无需使用.bind绑定this。onUploaded方法是在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>
点击后,侧边栏组件挂载到body
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>