Template
在创建一个子组件类时,我们可以为其提供模板。模板中可添加事件处理器、创建数据绑定、访问外部资源和引用其他子组件。
Exact.defineClass({
constructor: function SubComponent() {
Exact.Component.call(this);
},
statics: {
defaults: function() {}, // defaults函数返回默认值
descriptors: {}, // descriptors添加属性验证等辅助描述符
resources: {}, // resources中可注册转换器、子组件类、命名空间等
template: Exact.Skin.query('.subcomp') // DOM元素或字符串,且只有一个根元素
}
});
这里使用了Exact的工具方法defineClass。你依然可以用你喜欢的方式定义一个子组件类,例如使用ES6的语法糖。 重要的是,在子类构造函数中调用了父类的构造函数。
// 在ES6中
class SubComponent extends Exact.Component {
// 默认构造函数会调用父类构造函数
// constructor() {
// super();
// }
static defaults() {}
}
SubComponent.descriptors = {};
SubComponent.resources = {};
SubComponent.template = Exact.Skin.query('.subcomp');
Event Handler
在模板中,可以将一个函数作为事件处理器:
<a x-type="Button" click+="$.onClick">Ok</a>
<div x-type="Ticker" tick+="$.onTick"></div>
对于键盘事件和属性更改通知,还可以指定事件的prop:
<input type="text" keyup.enter+="$.onEnter">
<div x-type="Timer" changed.time+="$.onTimeChanged"></div>
你很可能需要手动地将一个函数绑定到this,像这样:
register: function() { // register在initialize之前被调用
this.onClick = this.onClick.bind(this);
}
也可以使用JavaScript语句作为内联的处理器:
<a click+="$.close()">Close</a>
<input type="text" change+="$.onChange(event.target.value)">
Data Binding
Exact中的绑定是基于事件而实现的。这意味着,即便不使用绑定,你也能完成数据的传递。 当然,在模板中使用绑定自有好处,它以简单直观的方式简化了繁琐重复的赋值操作过程。
<input type="text" value@="$.message" x-ref="msg">
几乎等价于:
var $ = this;
$.on('changed.message', function() {
$.msg.set('value', $.message);
});
在Exact中使用好绑定,需要了解下面几个关键词:
(1)绑定模式:有3种绑定模式可选,即单次、单向和双向。其中,单次绑定是单向的,在首次触发后即刻被移除,因为它使用once方法监听事件。
(2)绑定路径:绑定表达式中出现的属性链即为绑定路径。路径的源头通常是组件本身,但也可以是列表渲染过程中某个已知的局部变量。
(3)绑定对象和绑定对象属性:绑定的目的是要将绑定表达式的运算结果赋给一个对象的属性。 对于单次或单向绑定,这样的对象是任意非null类型的,这样的对象属性也是任意的。 对于双向绑定,则要求绑定对象亦可作为绑定源,绑定对象属性亦可作为绑定源属性,否则双向绑定只能起到单向绑定的作用。
(4)绑定源和绑定源属性:绑定源是绑定路径上数据的直接提供者,通常为Exact中Component或Store的实例对象。绑定源属性的值改变后通常会派发一个属性更改事件,但有时普通属性也可作为绑定源属性,参见(5)。
(5)绑定域和绑定域事件:绑定表达式依托的上下文就是绑定域,通常这样的上下文就是一个组件。没有特别声明的情况下,一个绑定会关心每个绑定路径对应的属性更改事件。 但是,你也可以指定一个绑定域上的事件作为绑定域事件。这样,绑定就只关心指定绑定域事件有没有发生,而无需关心一个或多个属性更改事件。
(6)求值器和转换器:每个绑定都有一个求值器,负责根据一些常量和绑定域上一个或多个绑定路径来求出结果值。 转换器是可选和多选的,负责将求值器求得的值转换成其他形式。
模板中文本插值绑定的基本语法如下:
<!--<绑定模式符>{<求值器表达式>[|转换器表达式] [^绑定域事件]}-->
<!--单次绑定-->
<p>Tip: &{ $.tip }</p>
<!--单向绑定,无参转换器-->
<h1>@{ $.title | upper }</h1>
<!--单向绑定,带常量参数的转换器-->
<span>@{ $.time | format('hh:mm:ss') }</span>
<!--单向绑定,带变量参数的转换器,只当`text`属性改变时才会触发-->
<span>@{ $.text | slice(0, $.length) ^changed.text}...</span>
元素或组件属性绑定的语法,看起来就像是运算符重载:
<!--<绑定对象属性名><绑定模式符>="<求值器表达式>[|转换器表达式] [^绑定域事件]"-->
<!--单次绑定-->
<p style&="$.styleString"></p>
<!--单向绑定,无参转换器-->
<h1 title@="$.title | upper"></h1>
<!--单向绑定,带参转换器-->
<input type="text" value@="$.time | format('hh:mm:ss')">
<!--双向绑定,双向转换器-->
<input type="text" x-type="TextBox" value#="$.color | hex2rgb">
对于双向绑定,绑定对象应当是组件,否则双向绑定表现为单向绑定。另外,特别要求求值器表达式为单个路径,以保持对称关系。 若在双向绑定使用转换器,转换器也必须是双向的。关于求值器和转换器更多的使用细节可参见下文。
如果要将文本插值的结果赋给元素或组件的某个属性,则可以这样书写:
<a href?="http://www.xxx.com/page/@{$.page}/"
style?="color: black; font-size: @{ $.fontSize }px;"
class?="btn-next @{$.page >= $.total ? 'disabled' : ''}">
下一页
</a>
特别地,对于class和style的绑定,Exact提供一种风格一致的扩展语法:
<!--<绑定对象属性名><绑定模式符>:<求值器表达式>[|转换器表达式] [^绑定域事件];-->
<a x-class="btn-next: true; disabled@: $.page >= $.total;"
x-style="color: black; font-size?: @{ $.fontSize }px;">
</a>
即将class和style视作对象而非字符串,然后独立地为其中某个属性添加绑定。
Optional Converters
一些场景下,在视图上呈现数据时需要有一个格式化的过程,比如标题要大写:
{
resources: {
format: function(value, brackets) {
return brackets[0] + value.toUpperCase() + brackets[1];
}
},
template: '<h1>@{$.title | format(\'##\')}</h1>',
descriptors: {title: null}
}
// this.title = 'hello' => <h1>#HELLO#</h1>
可以在全局Exact.RES、本地resources或绑定域上注册转换器,可以使用多个转换器(建议不超过2个),在声明式绑定表达式中以符号 | 连接。 更多示例如下:
// 本地resources或全局Exact.RES中定义了函数(或转换器对象)upper和wrap
@{$.title | upper | wrap('##')}
// 转换器中可以使用变量,当title或upperOrNot改变时都有可能触发绑定
@{$.title | $.format($.upperOrNot, '##')}
// 等价于
@{$.format($.title, $.upperOrNot, '##')}
// 全局Exact.RES上注册双向转换器
Exact.RES.register('num2str', {
exec: String, //正向
back: Number //反向
});
<input type="text" x-type="TextBox" value#="$.price | num2str">
JavaScript Expressions
声明式定义绑定表达式时,除了直接书写单个的绑定路径外,你还可以在其中书写简单的JavaScript表达式,例如:
@{ $.state === 'ok' }
@{ $.firstName + ' ' + $.lastName }
@{ $.amount > 100 ? 'large' : 'normal' }
对于上述含有运算符的表达式,Exact使用new Function(...)将JavaScript表达式转化成函数, 存在一定的限制和问题,主要是无法访问本地resources或全局Exact.RES中注册的内容。 我们建议在声明式定义的绑定表达式中尽量使用函数形式的表达式,例如:
{
descriptors: ['state', 'em'],
resources: {
OK: 'ok',
eq: function(a, b) { return a === b; }
},
template: '<a x-class="active@: eq($.state, OK);" x-style="font-size?: @{ Math.ceil(10 * $.em) }px;"></a>'
}
List Rendering
在模板中,使用x-for指令完成列表渲染。
<!--x-for="迭代标识符 of <求值器表达式> [|转换器表达式] [^绑定域事件]"-->
<ul class="menu-bar">
<li x-for="menu of $.menus">
<a href&="menu.url">&{ menu.label }</a>
</li>
</ul>
var MenuBar = Exact.defineClass({
extend: Exact.Component,
statics: {
descriptors: ['menus'],
template: Exact.Skin.query('.menu-bar')
}
});
Exact.Component.create(MenuBar, {
menus: [
{ label: 'Examples', url: './examples/'},
{ label: 'Downloads', url: './downloads/'},
{ label: 'Documents', url: './documents/'}
]
}).attach(Exact.Skin.query('#menu-bar');
x-for指令的表达式中,of 的左边指定了迭代标识符,右边与普通的数据绑定表达式是一样的。求值器部分一般就是一个可迭代的对象。 可以使用转换器对求值器求出的结果进行再加工,返回一个新的数组:
<div class="page">
<ul class="article-list">
<li x-for="article of $.articels | limit(10)" x-key="aritcle.id">
<a href@="article.url">@{ article.title }</a>
</li>
</ul>
</div>
var Page = Exact.defineClass({
extend: Exact.Component,
statics: {
template: Exact.Skin.query('.page'),
resources: { // 可在本地resources或全局Exact.RES对象上注册转换器
limit: function(items, n) {
return items.slice(0, n);
}
},
defaults: function() {
return {
articles: Exact.Collection.from([])
};
}
}
});
菜单栏一次渲染完成即可,而文章列表可能会刷新,所以这里使用Collection对象存放文章。 有更新的文章出炉时,文章列表就会重新渲染。为了不浪费之前已经渲染过的条目,指定一个x-key可以复用已有的元素。
Condition Rendering
在模板中,使用x-if 指令完成条件渲染。
<!--x-if="<求值器表达式> [|转换器表达式] [^绑定域事件]"-->
<div class="button-bar">
<a>Ok</a>
<a x-if="$.canCancel">Cancel</a>
</div>
当$.canCancel为true时才会添加Cancel按钮,为false又会删除它。如果只想显示或隐藏这个按钮,可以这样:
<div class="button-bar">
<a>Ok</a>
<a x-style="display@: $.canCancel ? 'inline-block' : ''">Cancel</a>
</div>
x-if可以配合x-for命令使用,提供更多的选择:
<ul>
<li x-if="$.isLoaded" x-for="item of $.items">
<img x-if="item.type === 'image'" src@="item.image">
<span x-if="item.type === 'text'">@{ item.text }<span>
</li>
</ul>
注意,上面的模板中,第一个x-if命令优先于x-for执行,因而无法访问到item。如果希望选择性地渲染$.items中的内容,请在x-for指令中添加必要的转换器。
Child Components
在模板中,使用子组件的方法是,在已有HTML标签中添加x-type特性,并在本地resources或全局Exact.RES中注册子组件类或所在命名空间。
<div class="panel">
<a x-type="Button">Ok</a>
<a x-type="EUI.Button">Ok</a>
</div>
var Button = Exact.defineClass(
extend: Exact.Component,
statics: {
template: '<a class="btn"><x-slot></x-slot></a>'
}
);
var Panel = Exact.defineClass({
extend: Exact.Component,
statics: {
template: Exact.Skin.query('.panel'),
resources: {
Button: Button
EUI: EUI // vender namespace
}
}
});
Contents and Slots
组件在内部管理自己的children,在外部接受contents。在模板中,你可以使用x-slot标签来自动收集匹配的contents。 上面展示的代码中,组件Button的模板就使用了一个简单的slot。这个slot是无名的,外部接受到的所有无名contents都会插入到这里。
<div class="panel">
<a x-type="Button">Ok</a>
<a x-type="Button">
<img src="./icons/ok-btn.png">
</a>
<a x-type="Button">
<img src="./icons/ok-btn.png">
<span>Ok</span>
</a>
</div>
上面用到的3个Button分别呈现文字、图标和图文组合。如果需要在模板中的不同位置插入不同的内容,你就可以使用具名slot,同时在父模板中为那些contents指定相应的slot属性。
<!--子组件模板-->
<div class="panel">
<header>
<x-slot name="header"></x-slot>
</header>
<section>
<x-slot></x-slot>
</section>
<footer>
<x-slot name="footer"></x-slot>
</footer>
</div>
<!--父组件模板-->
<div class="app">
...
<div x-type="Panel">
<h2 x-slot="header">title</h2>
<p>(1) ...</p>
<p>(2) ...</p>
<span x-slot="footer">tips</span>
<span x-slot="footer">time</span>
</div>
...
</div>
由此渲染得到的将是:
<div class="app">
...
<div class="panel">
<header>
<h2 x-slot="header">title</h2>
</header>
<section>
<p>(1) ...</p>
<p>(2) ...</p>
</section>
<footer>
<span x-slot="footer">tips</span>
<span x-slot="footer">time</span>
</footer>
</div>
...
</div>
Reference to Part
在模板中,可添加x-ref特性,以便引用对应的部件。一般来说,模板中使用了数据绑定或事件绑定后,就不必添加x-ref了。
<div class="demo">
<input x-ref="input">
</div>
这样,在JavaScript代码中,你就可以用this.input方便地访问到对应的部件,并根据需要手动地添加事件处理器等。
this.input.on('change', this.onInputChange.bind(this));
并且,可方便地为该部件添加CSS:
.demo [x-ref="input"] {
color: #222;
}
camelCase vs kebab-case
HTML特性不区分大小写,在模板中书写组件的属性名时,应由camelCase转变为kebab-case。
var Demo = Exact.defineClass(
extend: Exact.Component,
statics: {
descriptors: { imgSrc: null },
template: '<div><img src@="$.imgSrc"></div>'
}
);
<!--父组件模板-->
<div>
...
<div x-type="Demo" img-src="./demo.png"></div>
...
</div>
有一些DOM元素原生的属性,它们的属性名在HTML与JS之间,存在特别的转换关系,如:
'for' <=> 'htmlFor'
'float' <=> 'cssFloat'
'class' <=> 'className'
'readonly' <=> 'readOnly'
'inner-html' => 'innerHTML'