Table of Contents generated with DocToc
- Ember初步--Component
$ ember g component my-component-name
component名称里至少要有一个-
,因此blog-post
或者my-blog-component
都是ok的,但是blog
不行。
一个简单的component可能长这样:
<!-- app/templates/components/blog-post.hbs -->
<article class="blog-post">
<h1>{{title}}</h1>
<p>{{yield}}</p>
<p>Edit title: {{input type="text" value=title}}</p>
</article>
我们可以在模板里引用组件:
<!-- app/templates/index.hbs -->
{{#each model as |post|}}
{{#blog-post title=post.title}}
{{post.body}}
{{/blog-post}}
{{/each}}
在默认情况下,引用的component在渲染的时候被<div>
所包裹。
而数据来源自模板对应的route-handler:
// app/routes/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('post');
}
});
当component需要自己的逻辑的时候,可以在app/components/
里添加一个和component同名的js文件,来处理component里的逻辑。
// app/components/my-component.js
import Ember from 'ember';
export default Ember.Component.extend({
});
当component被render/re-render/remove的时候,Ember都提供了对应生成周期的回调函数。
- Initial
- init
- didReceiveAttrs
- willRender
- didInsertElement
- didRender
- Re-Render
- didUpdateAttrs
- didReceiveAttrs
- willUpdate
- willRender
- didUpdate
- didRender
- Component Destroy
- willDestroyElement
- willClearRender
- didDestroyElement
didUpdateAttrs
方法会在属性改变之后,Re-Render之前触发
<!-- /app/templates/components/profile-editor.hbs -->
<ul class="errors">
{{#each errors as |error|}}
<li>{{error.message}}</li>
{{/each}}
</ul>
<fieldset>
{{input name="user.name" value=name change=(action "required")}}
{{input name="user.department" value=department change=(action "required")}}
{{input name="user.email" value=email change=(action "required")}}
</fieldset>
// /app/components/profile-editor.js
import Ember from 'ember';
export default Ember.Component.extend({
init() {
this._super(...arguments);
this.errors = [];
},
didUpdateAttrs() {
this._super(...arguments);
this.set('errors', []);
},
actions: {
required(event) {
if (!event.target.value) {
this.get('errors').pushObject({ message: `${event.target.name} is required`});
}
}
}
});
didReceiveAttrs
在init
调用之后被调用,而且在之后的Re-Render以后也会被调用。可以借此在接受到参数之后来统一他们的格式。
例如,某个component可能接受string或者json作为参数,你可以在didReceiveAttrs
里把他们统一成为json的格式:
import Ember from 'ember';
export default Ember.Component.extend({
didReceiveAttrs() {
this._super(...arguments);
const profile = this.get('data');
if (typeof profile === 'string') {
this.set('profile', JSON.parse(profile));
} else {
this.set('profile', profile);
}
}
});
假设你想把一个第三方日期选择插件运用在你的Ember项目里。通常来说,这种第三方组件需要绑定一个DOM。那么什么时候是调用第三方组件的最佳时机呢?
当组件成功的渲染了HTML元素之后,就会触发didInsertElement
回调。有两种情况:
- 组件的元素都生成完毕并插入到了DOM里
- 组件的元素可以通过
$()
方法被取到
组件的$()
方法返回jQuery对象,以此来拿到组件的DOM元素,并且对它进行操作:
didInsertElement() {
this._super(...arguments);
this.$().attr('contenteditable', true);
}
$()
会返回组件的根元素。还可以设定目标元素,跟jQuery中一样:
didInsertElement() {
this._super(...arguments);
this.$('div p button').addClass('enabled');
}
回到例子。日期选择器通常要绑定在<input>
元素上,因此可以:
didInsertElement() {
this._super(...arguments);
this.$('input.date').initialDatePicker();
}
didInsertElement
也是个进行事件监听的好地方:
didInsertElement() {
this._super(...arguments);
this.$().on('animationend', () => {
$(this).removeClass('.sliding-anim');
});
}
除此以外,关于didInsertElement
你还需要知道:
- 它只在组件渲染之后调用一次
- 子组件总会在父组件调用该方法之前,调用
didInsertElement
- 不要在
didInsertElement
里设置组件的属性,会导致re-render
在render和re-render之后都会触发didRender
方法,你可以在这里对更新过后的DOM进行操作。
例如,有一个组件组成的列表,需要你在渲染完成之后滚动到某一个被选择的组件处。因此我们就要确认在触发滚动事件之前,所有的组件已经渲染完毕。
{{selected-item-list items=items selectedItem=selection}}
<!-- /app/templates/components/selected-item-list.hbs -->
{{#each items as |item|}}
<div class="list-item {{if item.isSelected 'selected-item'}}">{{item.label}}</div>
{{/each}}
// /app/components/selected-item-list.js
import Ember from 'ember';
export default Ember.Component.extend({
className: 'item-list',
didReceiveAttrs() {
this._super(...arguments);
this.set('items', this.get('items').map((item) => {
if (item.id === this.get('selectedItem.id')) {
item.isSelected = true;
}
return item;
}));
},
didRender() {
this._super(...arguments);
this.$('.item-list').scrollTop(this.$('.selected-item').position.top);
}
});
当一个组件不再被渲染时,会触发willDestroyElement
事件,例如:
<!-- 当falseBool是false的时候卸载组件 -->
{{#if falseBool}}
{{my-component}}
{{/if}}
我们可以在这个方法里进行清理工作:
willDestroyElement() {
this._super(...arguments);
this.$().off('animationend');
this.$('input.date').myDatepickerLib().destroy();
}
组件和他周围的环境所孤立,因此组件需要的属性要从外部传入
例如,有一个blog-post
组件来展现blog:
<!-- app/templates/components/blog-post.hbs -->
<article class="blog-post">
<h1>{{title}}</h1>
<p>{{body}}</p>
</article>
假设我们有如下的模板和route:
// app/routes/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('post');
}
});
在模板里这样使用组件:
<!-- app/templates/index.hbs -->
{{#each model as |post|}}
{{blog-post title=post.title body=post.body}}
{{/each}}
就能够正确的渲染组件:
<article class="blog-post">
<h1>XX</h1>
<p>XXXX</p>
</article>
当传入的属性更新时,组件DOM也会跟着更新。
上面的例子{{blog-post title=post.title body=post.body}}
展示了组件的命名参数:传入的都是键值对。除此以外,还有组件的位置参数,需要按照顺序传入组件:
<!-- app/templates/index.hbs -->
{{#each model as |post|}}
{{blog-post post.title post.body}}
{{/each}}
但只是这样还不行,还需要在组件的class中处理positionalParams
:
// app/components/blog-post.js
import Ember from 'ember';
const BlogPostComponent = Ember.Component.extend({});
BlogPostComponent.reopenClass({
// 按照参数传入的顺序组成列表
positionalParams: ['title', 'body']
});
export default BlogPostComponent;
之后就能想传入命名参数一样正常渲染。
在不知道传入参数多少的情况下,还可以把positionalParams
设置成为string,例如:positionalParams: 'params'
,然后像下面这样使用它:
<!-- app/components/blog-post.js -->
import Ember from 'ember';
const BlogPostComponent = Ember.Component.extend({
title: Ember.computed('params.[]', function(){
return this.get('params')[0];
}),
body: Ember.computed('params.[]', function(){
return this.get('params')[1];
})
});
BlogPostComponent.reopenClass({
positionalParams: 'params'
});
export default BlogPostComponent;
通常情况下,想这样{{blog-post title=title body=body}}
的调用,我们只能给组件传入正常的值。但是如果想要用组件包裹其他DOM元素,即把DOM元素作为参数传入的时候,需要使用{{yield}}
关键字:
<!-- app/templates/components/blog-post.hbs -->
<h1>{{title}}</h1>
<div class="body">{{yield}}</div>
然后把组件的调用改为:
<!-- app/templates/index.hbs -->
{{#blog-post title=title}}
<p class="author">by {{author}}</p>
{{body}}
{{/blog-post}}
需要说明的是,这种方式传入组件后,组件内的作用域和外面相同。也就是说,如果一个属性(例如author)在组件外可以被拿到,那么它在组件内也是如此。
在默认情况下,渲染的组件会被<div>
标签所包裹。如果有需要我们可以自定义它的包裹标签。
// app/components/navigation-bar.js
import Ember from 'ember';
export default Ember.Component.extend({
// 使用<nav>标签来包裹
tagName: 'nav'
});
// app/components/navigation-bar.js
import Ember from 'ember';
export default Ember.Component.extend({
// 会使用<nav class="primary">来包裹
tagName: 'nav',
classNames: ['primary']
});
- 如果你希望包裹组件的标签的class名称可以收到组件属性的控制,则要通过
classNameBindings
:
// app/components/todo-item.js
import Ember from 'ember';
export default Ember.Component.extend({
classNameBindings: ['isUrgent'],
isUrgent: true
});
在默认情况下isUrgent
为true,渲染出来的组建为:
<div class="ember-view is-urgent"></div>
而当isUrgent
为false的时候则不会有is-urgent
类名。
- 还可以把属性名和class名做成一个对应关系:
// app/components/todo-item.js
import Ember from 'ember';
export default Ember.Component.extend({
classNameBindings: ['isUrgent:urgent'],
isUrgent: true
});
这样的话会渲染:
<div class="ember-view urgent">
- 还可以有一个表达式
// app/components/todo-item.js
import Ember from 'ember';
export default Ember.Component.extend({
classNameBindings: ['isEnabled:enabled:disabled'],
isEnabled: false
});
当isEnabled
为true的时候使用class名enabled
,否则使用disabled
<!-- 上面的代码在默认情况下渲染出 -->
<div class="ember-view disabled"></div>
- 也可以在指定条件不满足的情况下使用指定的class名:
// app/components/todo-item.js
import Ember from 'ember';
export default Ember.Component.extend({
// 在不是isEnabled的时候使用class名disabled
classNameBindings: ['isEnabled::disabled'],
isEnabled: false
});
这会渲染出:
<div class="ember-view disabled">
<!-- 当isEnabled为true时则是 -->
<div class="ember-view">
- 如果组件的属性为String,则会直接把这个属性对应的值作为class渲染
// app/components/todo-item.js
import Ember from 'ember';
export default Ember.Component.extend({
classNameBindings: ['priority'],
priority: 'highestPriority'
});
<div class="ember-view highestPriority">
上面说了在组件的js里,根据组件的属性来绑定组件的class。然后再来了解一下绑定元素的其他属性。
使用attributeBindings
来指定绑定的其他属性:
绑定<a>
标签的href:
// app/components/link-item.js
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'a',
attributeBindings: ['href'],
href: 'http://emberjs.com'
});
或者可以这样:
// app/components/link-item.js
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'a',
attributeBindings: ['customHref:href'],
customHref: 'http://emberjs.com'
});
当属性为null
的时候,则不会被render出来:
// app/components/link-item.js
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'span',
title: null,
attributeBindings: ['title'],
});
这时会渲染:
<span class="ember-view">
而如果把title赋值为,例如Ember JS
,则渲染出来的是:
<span class="ember-view" title="Ember JS">
组件不仅可以传入参数然后渲染在DOM里,也可以作为返回值用在模板的块状表达式中。
<!-- app/templates/index.hbs -->
{{{blog-post post=model}}}
组件内通过yield
来返回值
<!-- app/templates/components/blog-post.hbs -->
{{yield post.title post.body post.author}}
组件可以处理例如单击、双击、hover、key-press这样的事件
新建一个组件:
$ ember g component event-example
为了演示,组件的hbs模板可以不做改动。
修改组件的js文件:
// app/components/enevt-example.js
import Ember from 'ember';
export default Ember.Component.extend({
click() {
alert('click');
},
mouseEnter() {
Ember.Logger.info("mouseEnter");
}
});
然后在模板里使用它:
<!-- app/templates/index.hbs -->
{{#event-example}}
hover or click here
{{/event-example}}
接着上面的栗子。假设index.hbs
所属的controllers/index.js
中有如下事件:
// app/controllers/index.js
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
handleClickAction(value) {
alert(value);
}
}
});
我们可以在index.hbs
中把该事件传递给组件:
<!-- app/templates/index.hbs -->
{{#event-example handleClic=(action "handleClickAction")}}
hover or click here
{{/event-example}}
而在组件js文件中:
// app/components/enevt-example.js
import Ember from 'ember';
export default Ember.Component.extend({
click() {
// 通过sendAction来进行回调
this.sendAction('handleClic', 'click');
},
mouseEnter() {
Ember.Logger.info("mouseEnter");
}
});
最终效果跟之前一样,在点击组件内元素的时候弹窗,内容为"click"。
参见:可处理的事件名称
触摸事件:
touchStart
touchMove
touchEnd
touchCancel
键盘事件:
keyDown
keyUp
keyPress
鼠标事件:
mouseDown
mouseUp
mouseMove
mouseEnter
mouseLeave
contextMenu
click
doubleClick
focusIn
focusOut
表单事件:
submit
change
focusIn
focusOut
input
HTML5拖拽事件:
drag
dragStart
dragEnter
dragLeave
dragOver
dragEnd
drop
组件类似于一个独立的黑箱。目前为止,我们已经接触了从父组件把属性传递给子组件的过程,但如果是反响的数据流呢?在Ember中,可以通过触发action来进行数据的反馈。举个栗子。
$ ember g component button-with-confirm
我们想要能够这样:
<!-- app/templates/components/user-profile.hbs -->
{{button-with-confirm text="点击OK将删除账户"}}
或者这样
<!-- app/templates/components/send-message.hbs -->
{{button-with-confirm text="点击OK将发送信息"}}
- 在父组件里,决定事件的处理方式。比如删除账户或者发送消息
- 在子组件里,决定什么时候事件被处罚。除此以外,我们还想在外层事件处理完成之后进行追踪
在Ember中,每个组件都可以有一个叫作actions
的属性,应该把你的事件定义在这里面。
先来看下父组件的js文件。假设我们有一个叫做user-profile
的父组件:
// app/components/user-profile.js
import Ember from 'ember';
export default Ember.Component.extend({
login: Ember.inject.service(),
actions: {
userDidDeleteAccount() {
this.get('login').deleteUser();
}
}
});
// app/components/button-with-confirmation.js
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
launchConfirmDialog() {
this.set('confirmShown', true);
},
submitConfirm() {
// trigger action on parent component
this.set('confirmShown', false);
},
cancelConfirm() {
this.set('confirmShown', false);
}
}
});
并且在组件的模板中这样调用方法:
<!-- app/templates/components/button-with-confirmation.hbs -->
<button {{action "launchConfirmDialog"}}>{{text}}</button>
{{#if confirmShown}}
<div class="confirm-dialog">
<button class="confirm-submit" {{action "submitConfirm"}}>OK</button>
<button class="confirm-cancel" {{action "cancelConfirm"}}>Cancel</button>
</div>
{{/if}}
现在,我们要做的就是在触发子组件的submitConfirm
方法时,能够触发外层传递给子组件的方法。
<!-- app/templates/components/user-profile.hbs -->
{{button-with-confirmation text="Click here to delete your account." onConfirm=(action "userDidDeleteAccount")}}
<!-- app/templates/components/send-message.hbs -->
{{button-with-confirmation text="Click to send your message." onConfirm=(action "sendMessage")}}
然后修改子组件的事件处理:
// app/components/button-with-confirmation.js
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
launchConfirmDialog() {
this.set('confirmShown', true);
},
submitConfirm() {
//call the onConfirm property to invoke the passed in action
this.get('onConfirm')();
},
cancelConfirm() {
this.set('confirmShown', false);
}
}
});
让父组件的异步事件返回Promise,则子组件可以在父组件事件完成之后再触发一些方法:
// app/components/button-with-confirmation.js
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
launchConfirmDialog() {
this.set('confirmShown', true);
},
submitConfirm() {
//call onConfirm with the value of the input field as an argument
const promise = this.get('onConfirm')();
promise.then(() => {
this.set('confirmShown', false);
});
},
cancelConfirm() {
this.set('confirmShown', false);
}
}
});
sendMessage方法期待接受一个参数:
<!-- app/templates/components/send-message.hbs -->
{{button-with-confirmation text="Click to send your message." onConfirm=(action "sendMessage" "info")}}
为了能够实现参数的传递:
<!-- app/templates/components/button-with-confirmation.hbs -->
<button {{action "launchConfirmDialog"}}>{{text}}</button>
{{#if confirmShown}}
<div class="confirm-dialog">
{{yield confirmValue}}
<button class="confirm-submit" {{action "submitConfirm"}}>OK</button>
<button class="confirm-cancel" {{action "cancelConfirm"}}>Cancel</button>
</div>
{{/if}}
<!-- app/templates/components/send-message.hbs -->
{{#button-with-confirmation
text="Click to send your message."
onConfirm=(action "sendMessage" "info")
as |confirmValue|}}
{{input value=confirmValue}}
{{/button-with-confirmation}}
// app/components/button-with-confirmation.js
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
launchConfirmDialog() {
this.set("confirmShown", true);
},
submitConfirm() {
//call onConfirm with the value of the input field as an argument
const promise = this.get('onConfirm')(this.get('confirmValue'));
promise.then(() => {
this.set('confirmShown', false);
});
},
cancelConfirm() {
this.set('confirmShown', false);
}
}
});
// app/components/send-message.js
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
sendMessage(messageType, messageText) {
//send message here and return a promise
}
}
});
我们已经在button-with-confirmation.hbs
组件中处理了点击事件,然后向上传递给了user-profile.hbs
组件。现在假设删除账户的事件还要继续向上传递,交给system-preferences-editor.hbs
组件进行处理。
<!-- app/templates/components/user-profile.hbs -->
{{button-with-confirmation onConfirm=(action deleteCurrentUser)
text="Click OK to delete your account."}}
<!-- app/templates/components/system-preferences-editor.hbs -->
{{user-profile deleteCurrentUser=(action 'deleteUser' login.currentUser.id)}}
// app/components/system-preferences-editor.js
import Ember from 'ember';
export default Ember.Component.extend({
login: Ember.inject.service(),
actions: {
deleteUser(idStr) {
return this.get('login').deleteUserAccount(idStr);
}
}
});