Component
组件系统是Exact的重中之重,其他模块和类几乎都是在为组件的实现做铺垫。组件是对一般标签元素的扩展,支持模板、属性更改通知、属性验证等高级功能。
Component, Element and Text
组件、元素和文本共同组成了Shadow层。文本是最简单的,元素则可以设置样式、插入或移除子节点等。 一般来说,使用组件和模板后,你不需要手动创建文本、元素或子组件。下面的代码展示了相关的api,移步这里可以看到结果。
var text = Exact.Text.create('');
text.set('data', "It's a rect");
var span = Exact.Element.create('span');
span.set('title', '...');
span.style.set('color', 'white');
// span.attrs.set('data-time', '12');
span.classes.set('dark-bg', true); // 第二个参数表示是否包含该类名
span.children.insert(text);
var rect = Exact.Element.create('rect', 'svg'); //第二个参数为命名空间
rect.save({width: 100, height: 100});
rect.style.save({fill: 'rgb(0, 120, 240)'});
rect.attrs.save({transform: 'translate(10, 10)'});
var svg = Exact.Element.create('svg', 'svg');
svg.children.insert(rect);
var Panel = Exact.defineClass({
extend: Exact.Component,
statics: {
$template: '<div class="panel" x-style="border?: solid 2px @{$.color};">' +
'<h3 class="head" x-style="background-color@: $.color;">@{ $.title }</h3>' +
'<div class="body"><slot></slot></div>' +
'</div>',
descriptors: { title: null, color: null }
}
});
var panel = Exact.Component.create(Panel);
panel.save({title: 'SVG', color: '#eee'}); // 设置DOM属性或自定义属性
panel.set('contents', [svg, span]); // 模板中slot处会被替换为svg和span
// 同样可以手动设置style、classes,一般不会手动更新组件的attrs和children
panel.attach(document.getElementById('panel'));
需要特别说明的是,Text和Element作为final class都是不能被继承的,而Component具有很好的可扩展性。
Props, Attrs, Style and Classes
无论是组件、元素还是文本,都可使用set方法更新它的DOM属性值(save只是多次调用set)。 某个DOM属性的值发生更改时,这个DOM属性就会被标记为脏,并触发失效机制,等待渲染。异步渲染后,脏标记被自动清除。
对于组件和元素,它们的成员对象style、attrs和classes,都有脏标记的功能,并与失效机制挂钩。因为我们可以在模板使用数据绑定,所以一般不需要手动更新style、attrs和classes的属性值。
Defaults and Descriptors
对于组件,可以有自定义属性,可自定义属性的默认值,还可为自定义属性添加一些描述。如果在类的静态函数`defaults`或静态属性`descriptors`中声明了某个属性,在创建这一组件的首个实例时,就会为这个自定义属性创建一对getter和setter。 对于这样自定义的属性,当它的值发生改变时,会派发属性更改通知,触发失效机制。
var Demo = Exact.defineClass({
extend: Exact.Component,
statics: {
descriptors: {active: null, label: null},// or descriptors: ['active', 'label']
defaults: function() {
return {active: true, amount: 1000};
}
}
});
var demo = new Demo();
demo.label = 'abcd'; // 等效于demo.set('label', 'abcd'),派发事件`changed.label`;
demo.active = false; // 等效于demo.set('active', false),派发事件`changed.active`;
demo.amount = 20000; // 等效于demo.set('amount', 20000),派发事件`changed.amount`;
demo.undeclared = 1; // 不同于demo.set('undeclared', 1);
注意,`defaults`或`descriptors`中自定义的属性,默认是没有脏标记功能的,不会被渲染到真实DOM层。如果希望原生DOM属性更改时派发通知,或者希望自定以属性能渲染到真实DOM上,可以在`descriptors`中进行特别声明。 下面创建的组件TextBox,它的属性`value`是支持双向绑定的。
var TextBox = Exact.defineClass({
extend: Exact.Component,
statics: {
descriptors: {
value: {
native: true // 指示该属性为原生DOM属性,方可渲染到真实DOM
}
},
template: '<input type="text" change+="$.onChange(event)">'
},
onChange: function(event) {
this.value = event.target.value;
}
});
Property Validation
一些场景下,在更新属性值时,我们可能需要验证一下值的类型或其他因素。 为此,我们可以在子组件类的静态属性`descriptors`中这样进行定义:
descriptors: {
// 无需验证
x: {},
y: null,
// 对于boolean、number、string类型
label: 'string',
amount: 'number',
active: 'boolean',
// 对于Object类型
tool: Knife, // Knife是类的构造函数
items: Array,
// 以上是简单的类型验证,亦可允许多类型,模式匹配或自定义一个验证函数
date: {
type: ['number', 'string', Date]
},
name: {
validator: /^[a-zA-Z0-9]{1,32}$/,
},
score: {
type: 'number',
validator: function(value) {
if (value < 0 || value > 100) {
return new Error('out of the range 0~100');
}
}
}
}
于是,`date`属性可接收的值包括123456、"2016/11/18"或者Date对象;`name`属性接收的值与模式不匹配时,不会将该值赋给`name`;`score`的数字范围需在区间[0,100]上。 验证后会派发带有keyName的 `validated` 的事件,并附带参数error(为null 时,表示验证通过)。
Property Coercion
属性值经过验证之后,我们可能需要对属性值做一些强制变换。比如,opacity范围在0至1之间,超出之后就应修整。
var Demo = Exact.defineClass({
extend: Exact.Component,
statics: {
descriptors: {
opacity: {
type: 'number'
coerce: function(value) { // 验证过后,强制裁剪
return value < 0 ? 0 : (value > 1 ? 1 : value);
}
}
}
}
});
var demo = new Demo();
demo.opacity = 1.25;
console.log(demo.opacity); // 输出:1
Property Interception
Exact允许你在`descriptors`中自定义`get`或`set`方法,来拦截原有的取值、赋值的操作。 你可以在自定义的`get`方法中覆盖默认的返回值,可以在自定义的`set`方法中追加一些行为,等等。
{
template: '<h2>@{ $.fullName }</h2>',
descriptors: [{
fullName: {
depends: ['firstName', 'lastName'],
get: function() {
return this.firstName + ' ' + this.lastName;
}
//,set: function(value) {
// var names = value.split(' ');
// this.save({firstName: names[0], lastName: names[1]});
//}
}
}]
}
Property Dependency
组件模板中支持的绑定表达式是丰富的,但是Exact建议不要书写冗长而难以阅读的绑定表达式。 如果一个绑定表达式含有多个绑定路径或者逻辑过于复杂,你可以考虑在descriptors中定义一个依赖于其他绑定路径的属性。 就比如上面代码展示的,`depends`指明属性`fullName`依赖于`fistName`和`lastName`。只要`firstName`或`lastName`的值发生改变,就会重新将`fullName`渲染到页面。
Children and Contents
同样,元素和组件都有children,它是Collection对象,继承自Array,添加了方法insert、remove和replace,对应DOM操作中的insertBefore、removeChild和replaceChild。 通过监听changed事件,children内部的变化也与失效机制实现了挂钩。
组件在内部管理自己的children,在外部接受contents。在模板中,你可以使用slot标签来自动收集匹配的contents,就像第一节那样。 具体用法参见Contents and Slots。或者不使用slot,你也可以选择在何处手动插入来自外部的contents。
// ...
var Panel = Exact.defineClass({
extend: Exact.Component,
statics: {
template: '<div class="panel" x-style="border?: solid 2px @{ $.color };">' +
'<h3 class="head" x-style="background-color@: $.color;">@{ $.title }</h3>' +
'<div class="body" x-ref="body"></div>' +
'</div>',
descriptors: { title: null, color: null }
},
ready: function() {
this.once('changed.contents', (function() {
var children = this.body.children;
children.push.apply(children, this.contents);
}).bind(this));
}
});
var panel = Exact.Component.create(Panel, {title: 'SVG', color: '#eee'});
panel.set('contents', [svg, span]);
// ...