>

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]);
// ...

Lifecycle and Hooks