在开发公司的公共组件的时候,遇到了一个切换 tab 的功能:当点击某一 tab 的时候,为其添加
.active
class ,并且它所有的兄弟元素移除掉.active
class。而由于不想操作 dom 便有了这样的笔记,记录自己解决问题的过程。
这样的功能在许多场景都可以用到,包括不限于目前本公司项目组件中的日期选择功能、单选/多选功能、手风琴功能等。它们实现的逻辑是相同的。都是使用某一值对 item(可以是 tabs 中的某个tab,也可以是单选中的某一项等) 的状态进行管理。根据这个提示,再根据下面的具体例子,思考下具体实现:
线上体验
经过一番思考,我们的大致思路就很清楚了,让我们再次来捋一捋
在这些 item 的共同父级,如果在组件中,那就是在这些 item 的共同父组件中,保存了一个类型为基础类型或者是 object 类型的变量,其具体的值是某一 item 的 name 或 id 或能标示 item 唯一性的值,当点击某一 item 的时候,通过事件传递修改这个变量值,然后再将这个变量中的值与 item 中的值进行比较,如果相同那就添加.active
class,不相同则移除.active
class.
到现在,实现逻辑大致应该就是这样的,但是,很明显,上代码才是最实在的事情:
notes:这部分的代码演示基础是基于 ember.js 框架。其后可能会添加 react.js 框架的相关代码。但实现逻辑应该大致相同。
首先来看我们要实现的示例:
每点击一个水果,就为此水果 item 添加 .active
class,在这里的表现就是 border 的颜色从#eee
更改为 lightgreen
.(gif 以及更好看的示例或许会更新)。 那就开始做:
在 ember 项目中,通过 ember-cli 创建名称为 fruit-item
的子组件,它的 component.js
以及 template.hbs
文件为:
/**
* fruit-item/component.js
*/
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
tagName: 'li',
classNames: ['item'],
localClassNames: 'item', // 因为使用了 css-module 插件
classNameBindings: ['active'],
active: computed('choosed',function() {
let {fruit,choosed} = this.getProperties('fruit','choosed');
if(fruit.id === choosed.id) {
return true
}
return false
})
});
{{!-- fruit-item/template.hbs --}}
{{fruit.name}} and count is {{fruit.count}}
通过上面的 hbs
文件我们可以看出此子组件只是简单的展示水果以及数量。而在component.js
文件中,我们使用了 class
为item
的li
标签来进行页面渲染,通过 ember 的 computed来计算当前的 fruit
变量与用户点击的 choosed
变量之间的异同,如果相同,我们返回 true
,通过 ember 的 classNameBindings
在 active
属性值为 true
的时候,为此组件添加名为 active
的类。如果返回 false
,那目前不添加任何其他类。子组件的功能展示目前就是这些了,那我们需要一个共同的父组件来管理这些 item。
继续通过 ember-cli 来生成名为 fruit-list 的组件,它的 component.js
以及 template.hbs
文件为:
/**
* fruit-list/component.js
*/
import Component from '@ember/component';
import {A} from '@ember/array';
import EmberObject from '@ember/object';
export default Component.extend({
choosed: EmberObject.create({name:'orange',value:1,id:'item2',count: 21}),
tagName:'ul',
items: A([
{name:'apple',value:0,id:'item1',count: 3},
{name:'orange',value:1,id:'item2',count: 21},
{name:'banana',value:2,id:'item3',count: 1},
{name:'watermelon',value:3,id:'item4',count: 4},
{name:'cherry tomato',value:4,id:'item5',count: 4},
{name:'nectarine',value:5,id:'item6',count: 9},
])
});
{{!-- fruit-list/template.hbs --}}
{{#each items as |item|}}
{{fruit-item fruit=item choosed=choosed}}
{{/each}}
这里可以看到我们通过 handlebars
的循环语法- eash as
来遍历了我们在 component.js
中 mock
的水果数据,并将每条水果的数据传递给了 fruit-item
组件,同时还有我们预设定的选中水果- choosed
变量。至此我们就可以看到上面我们要实现的样子了。
但是,现在我们点击每个水果,它们的 active
状态并不会改变,我们开始着手添加这个功能。首先修改 fruit-item
的 component.js
文件,问其添加 click
事件,并通过闭包将事件传递给上一层的调用者,也就是这里的 fruit-list
:
/**
* fruit-item/component.js
*/
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
tagName: 'li',
classNames: ['item'],
localClassNames: 'item', // 因为使用了 css-module 插件
classNameBindings: ['active'],
active: computed('choosed',function() {
let {fruit,choosed} = this.getProperties('fruit','choosed');
if(fruit.id === choosed.id) {
return true
}
return false
}),
onClick() { },
click() {
let fruit = this.get('fruit'),
action = this.get('onClick');
action(fruit);
}
});
可以看到在事件传递的过程中我们将当前水果 fruit
的值作为实参进行传递。
而在 fruit-list
中我们接受这个事件,并拿到用户当前点击的水果的值- fruit
:
{{!-- fruit-list/template.hbs --}}
{{#each items as |item|}}
{{fruit-item fruit=item choosed=choosed onClick=(action (mut choosed))}}
{{/each}}
这里可以看到在点击后我们拿到 fruit
的值后将其复制给 choosed
变量。
mut 语法。
这样我们的当前功能就完成了。
以数据驱动 UI 的最基本的操作应该就是这样了,掌握了这个思想,那么其他的组件包括上文提到的 radio/checkbox(array)
也可以按照这样的思想进行设计。
同时本文中应用到的技术大多是与 emberjs 框架相关,其运用到的相关知识点有:
这篇文章到此就结束了,如有疑问,欢迎留言评论。
Written by Frank Wang.