>

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>

特别地,对于classstyle的绑定,Exact提供一种风格一致的扩展语法:

<!--<绑定对象属性名><绑定模式符>:<求值器表达式>[|转换器表达式] [^绑定域事件];-->
<a x-class="btn-next: true; disabled@: $.page >= $.total;"
   x-style="color: black; font-size?: @{ $.fontSize }px;">
</a>  

即将classstyle视作对象而非字符串,然后独立地为其中某个属性添加绑定。

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>

$.canCanceltrue时才会添加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'