ZLComboBox自定义控件开发详解

【引言】距离上一回写博客已经有一些时日了,之前的爱莲iLinkIT系列主要是讲解了如何用NodeJS来实现一个简单的“文件传送”软件,属于JavaScript中在服务器端的应用。

今天,我们就回归到JavaScript的主战场 ---- 前端设计,一起来聊聊如何开发一个“自定义”的Web控件(基于jQuery),属于基础级别,高手请轻拍,还望不吝赐教,先谢过。

 

我们先假设这样一个场景,BOSS下达了命令,要求开发一款“幼儿早教”的应用,方便小朋友进行选择操作,而你则负责设计一个“下拉框”的Web控件,方便团队中其他弟兄使用,设计师的MM已经把效果图设计好了,如下:

总的来说,这是一个类似“下拉框”的Web控件:

a. 用户单击控件之后,显示下拉列表。

b. 选中下拉列表中的某项(例如:桃子),会将选中项的图片显示出来。

 

接下来,就让我们一步一步来实现这样的功能,我们先对整个讲解过程做一个整体的规划,以便我们心中有谱:

1. 实现控件的外观部分:包括HTML代码的组织,CSS样式设计。

2. 实现控件的基本框架:用JavaScript实现控件的交互设计。

3. 优化控件的可扩展性:让控件可以接受JavaScript回调函数。

4. 优化控件的可用性:可以动态传入选项的数据,达到自定义选项列表的目的。

最后,我们来做一个简单的回顾和总结。单击此处下载演示包,解压缩后打开index.html页面体验。

实现控件的外观部分

我们先来分析一下整个控件的几个组成部分,然后组织我们的HTML代码,为了后面设计CSS样式表的方便,在组织HTML代码时,就应该遵照相关规范(例如:公司的样式表命名规范),完成class的名称设计。

控件的组成

控件的最外层是一个大的容器,包含控件的各个组成部分,其中的组件包括:
1. 结果显示框

可以将选中的选项显示在结果显示框中。

2. 下拉框按钮

控制下拉框的显示与隐藏,单击按钮,显示下拉列表,再次单击时,隐藏下拉框,不同的状态下,箭头的方向不一样。

3. 下拉框

下拉框中包含多个可供用户选择的“项”。

4. 下拉框中的项

选项包含图片和文字两个部分,当前被选中的选项,应该呈现不同的外观。 

HTML代码

根据前面的组成分析,我们可以实现如下的HTML代码(index.html):

 1 <div class="zl_combobox">
 2 <div class="cb_input"><img src="images/f_apple.png" /><span>苹果</span></div>
 3 <span class="cb_btn"></span>
 4         <ul class="item_list">
 5                 <li><img src="images/f_apple.png" /><span>苹果</span></li>
 6                 <li><img src="images/f_lemon.png" /><span>柠檬</span></li>
 7                 <li><img src="images/f_peach.png" /><span>桃子</span></li>
 8                 <li><img src="images/f_watermelon.png" /><span>西瓜</span></li>
 9         </ul>
10 </div>

其中:

  • zl_combobox:代表整个Web控件。
  • cb_input:结果显示框。
  • cb_btn: 下拉框按钮。
  • item_list:下拉框。下拉框中的“项”用<li>元素来表示。

CSS样式表设计

接下来我们要根据控件最终的效果图和前面设计的HTML代码,来完成样式表的设计。在设计样式表时,一般分为两个部分:

  • 基础外观:包括各个子部件的部件、关联关系、呈现外观等。
  • 状态样式:要考虑到控件在交互过程中的状态变化,通过‘开关’来实现不同状态下的外观,方便后续的JavaScript的控制。

基础外观的相关CSS代码(zlbox.css)如下:

 1 .zl_combobox{
 2     position:relative;
 3     width:180px;
 4     font-size:1.5em;
 5     margin-left:5px;
 6     color:#323232;
 7 }
 8 .zl_combobox .cb_input{
 9     width:170px;
10     height:60px;
11     padding:0px 5px;
12     line-height:60px;
13     border:1px #A9A9A9 solid;
14     background-color:#F2F2F2;
15     cursor:pointer;
16 }
17 .zl_combobox .cb_input:after,
18 .zl_combobox .item_list li:after{
19     content:'';
20     clear:both;
21     display:table;
22 }
23 .zl_combobox .cb_input span,
24 .zl_combobox .item_list li span{
25     display:block;
26     padding-left:10px;
27     height:60px;
28     line-height:60px;
29 }
30 .zl_combobox .cb_input img,
31 .zl_combobox .item_list li img{
32     display:block;
33     width:50px;
34     height:50px;
35 }
36 .zl_combobox .cb_input>img,
37 .zl_combobox .cb_input>span,
38 .zl_combobox .item_list li>img,
39 .zl_combobox .item_list li>span
40 {
41     float:left;
42 }
43 .zl_combobox .cb_btn{
44     position:absolute;
45     right:0px;
46     top:1px;
47     width:60px;
48     height:60px;
49     background:url('img/cb_btn_down.png') no-repeat center center;
50     cursor:pointer;
51 }
52 .zl_combobox .item_list{
53     display:none;
54     position:absolute;
55     right:0px;
56     top:62px;
57     width:180px;
58     line-height:60px;
59     background-color:#FEFEFE;
60     z-index:999;
61 }
62 .zl_combobox .item_list li{
63     padding-left:10px;
64     border-bottom:2px #CCCCCC dotted;
65     cursor:pointer;
66 }

因为本文重点在于介绍Web控件的设计过程,关于CSS设计的细节,就不展开细讲,如有疑问,咱们在评论中交流讨论。

那么,整个控件在交互过程中,会有哪些状态需要考虑呢?

1. 默认的状态:不显示下拉框,仅仅显示“结果显示框”和“下拉框按钮”。

2. 下拉框展开状态:显示下拉框,并且下拉框按钮的箭头变为向上。

3. 选项的鼠标悬停状态:当鼠标悬停到选项时,改变不同的样式。

(当然,如果你确定你的控件只是在手机/Pad上使用,可以不考虑鼠标悬停)。

4. 被选中的选项的状态:这个可以根据需要设计。

状态相关的样式的CSS代码(zlbox.css)如下:

1 .zl_combobox.selected .item_list{
2     display:block;
3 }
4 .zl_combobox.selected .cb_btn{
5     background:url('img/cb_btn_up.png') no-repeat center center;
6 }
7 .zl_combobox .item_list li:hover{
8     color:#5281F8;
9 }

注意到,我们通过判断控件主体.zl_combobox是否有selected的class来控制下拉框的显示状态。

至此,控件的外观部分设计已经完成,我们可以看到,Web控件的外观设计有以下几个特点:

a. 布局属性:Web控件可能在各种场合中使用,所以,我们一般不对控件主体的布局属性进行设置(举例中我们仅仅设置了margin-left:5px,约定控件在使用过程中,距离它前面的控件为5px)。

b. 基础属性:像字体大小、字体颜色这样基础属性,在控件主体中尽量设置一下,确保控件的风格可以得到保证。比如:常规情况下,背景色是白色的,而字体的颜色是黑色,这时候,黑色的字体在白色背景的下拉框中可以显示出来。假设你没有设置控件主体的字体颜色,而在某个使用环境中,背景变成了黑色,字体颜色设置为白色,由于样式具有继承的特性,这时候就会变成在白色下拉框中的字体颜色是白色的。所以,在控件主体中设置基础属性,可以有效避免外部环境的“入侵”,让控件的风格自成一体。

实现控件的基本框架

工程文件组织

尽管前面我们已经讲解了控件的外观设计,也写了相关的HTML代码和CSS代码,但其实有一个本来应该先做的事情没有完成,就是工程文件组织,如下所示:

控件主文件夹的说明如下:

  • css:保存项目用到的CSS相关的文件。
  • images:保存需要在项目中直接引用的图片资源。
  • js:保存项目用到的JavaScript相关的文件。
  • index.html:HTML主体文件。

CSS文件夹下的内容:

  • img:保存在css样式表中要引用的背景图片资源。
  • index.css:与工程相关的样式表,比如:页面布局等。
  • reset.css:一个重置样式表,将HTML中元素的标准特性重置。
  • zlbox.css:与控件相关的样式表。

JS文件夹下的内容:

  • jquery.zlbox.js:与控件相关的JavaScript文件,因为我们用到了jQuery库,所以以jquery为前缀。
  • jquery.1.7.1.js:jQuery库文件。

本来应该还有一个index.js,用于保存工程相关的js代码,考虑到我们的演示demo的内容比较简单,工程相关的js代码调用就直接放到index.html文件中。

关于工程文件的组织和命名,各个公司应该有各自的规范标准,实际使用过程中,请遵照公司的规范。本文的论述将按以上的组织来进行。

JavaScript代码设计

总算到了我们Web控件设计的核心环节,我们的控件是基于jQuery库来设计的。jQuery插件的设计方法有很多种,我们依据我们的业务特点,选用了下面的基本框架模式,核心代码如下:

 1 $.fn.czl_combobox = function( options )
 2     {
 3        this.each( function()  
 4        {
 5           var instance = $.data( this , 'czl_combobox' );
 6           if( !instance )
 7           {
 8             $.data( this, 'czl_combobox' , new $.ZLComboBox( options , this ) );
 9           }
10           
11        });//end of each
12        
13        return this;  
14 };
15 $.ZLComboBox = function( options , element )
16 {    
17     this.$el= $( element ); 
18     this._init( options );
19 };
20 $.ZLComboBox.defaults = {
21     
22 };
23 $.ZLComboBox.prototype = {   
24 _init : function( options ) {
25    //初始化函数
26 },
27 _loadEvents:function( ){
28   //控件相关的事件注册
29 }
30   };

我们先来看一下调用这个Web控件的代码,然后我们对照起来分析:

$('.zl_combobox').czl_combobox( {} );

让前端的HTML元素和后端的JavaScript代码关联起来,czl_combobox 这个函数是关键,所以,我们就从$.fn.czl_combobox这个函数入手,来看看它到底是如何关联的,先看代码:

 1 $.fn.czl_combobox = function( options )
 2  {
 3        this.each( function()  
 4        {
 5           var instance = $.data( this , 'czl_combobox' );
 6           if( !instance )
 7           {
 8             $.data( this, 'czl_combobox' , new $.ZLComboBox( options , this ) );
 9           }
10           
11        });//end of each
12        
13        return this;  
14 };

1. czl_combobox是$.fn的成员,意味着凡是jQuery对象都可以调用它。

2. 通过$.data()获取jQuery对象(对应一个HTML元素)的名称为czl_combobox的数据对象。

   如果不存在,那么,就通过$.data()新建一个ZLComboBox 的对象。

   如果已存在,那么,就不重复创建对象。

3. 创建对象时,把当前jQuery对象和参数options传入。

经过分析,我们发现$.fn.czl_combobox也仅仅是桥梁,还不是控件的核心,真正的核心是ZLComboBox对象,这个对象可以通过对应的HTML元素访问到。

现在,我们依据控件的业务特征,先来预估一下ZLComboBox对象能做什么:

1. 既然将HTML元素对应的DOM对象作为参数传给ZLComboBox对象,那么,ZLComboBox对象就能对相关的HTML元素以及它的子元素进行操作。

2. 同时将参数options传递给ZLComboBox对象,意味着,可以根据业务需要对ZLComboBox对象进行一些”定制”操作,包括:属性和行为。

 

下面我们再来看一下ZLComboBox的核心代码:

1. 在它的构造函数中,将传入的HTML DOM对象保存到自己的一个属性成员中,并且将参数options传递给自己的一个初始化方法(_init())。

2. 在它的原型中,定义了_init() 和 _loadEvents()两个方法。

 

我们的目的是要让控件能够响应’单击’事件,并且对控件的各个子部件进行状态的改变,所以我们可以这样操作:

1. 在_init中,通过控件主体的 DOM对象,获得它的各个子部件的jQuery对象。

2. 在_loadEvents中,对子部件的jQuery对象绑定响应事件。

3. 增加一个属性selected_index,记录当前选中的项的序号。

4. 定义一个方法_show_itemlist,用来操作下拉框的显示和隐藏。

完整的JS代码(zlbox.js)如下,可对照注释进行理解:

 1 (function($){
 2 $.fn.czl_combobox = function( options )
 3     {
 4        this.each( function()  
 5        {
 6           var instance = $.data( this , 'czl_combobox' );
 7           if( !instance )
 8           {
 9             //主体功能通过一个对象实现 
10             $.data( this, 'czl_combobox' , new $.ZLComboBox( options , this ) );           
11  }
12         });//end of each
13        
14        return this;  
15     };
16     $.ZLComboBox = function( options , element )
17     {    
18         this.$el= $( element ); 
19         this._init( options );
20     };
21      
22     $.ZLComboBox.prototype = {   
23           _init : function( options )
24           {
25             //相关的控件
26             this.comboBox = this.$el ;
27             
28             //相关的HTML控件
29             this.cb_btn = this.comboBox.children( '.cb_btn' ).eq(0);
30             this.cb_input = this.comboBox.children( '.cb_input' ).eq(0);
31             this.cb_item = this.comboBox.find( 'li' );
32 
33             //控件的状态,标记是否显示下拉列表
34             this.cb_showitem_status = false ;     
35             
36             //初始化默认的值,默认选中最后一个选项
37             this.selected_index = this.cb_item.length-1 ;                       
38             this.cb_input.html( this.cb_item.eq(this.selected_index).html() );  
39 
40             //注册响应事件
41             this._loadEvents();
42           },
43          _show_itemlist:function(){
44                 if( this.cb_showitem_status === false ){
45                     this.comboBox.addClass( 'selected' );
46                     this.cb_showitem_status = true;
47                 }
48                 else{
49                     this.comboBox.removeClass( 'selected' );
50                     this.cb_showitem_status = false;
51                 }
52                 return ;
53          },
54          _loadEvents:function(){
55             var _self = this;   
56             //1_单击下拉箭头
57             this.cb_btn.on( 'click' , function( event ){ 
58                 _self._show_itemlist( );
59                 return ;
60             });
61             //2_单击编辑框,也同样进行下拉框的状态切换
62             this.cb_input.on( 'click' , function( event ){ 
63                 _self._show_itemlist( );
64                 return ;
65             });
66             //3_单击选项
67             this.cb_item.on( 'click' , function( event ){ 
68                 var index = $(this).index();
69                 if( _self.selected_index !== index ){
70                     //设置选项的值
71                     _self.cb_input.html( $(this).html() );
72                     
73                     //设置当前选中的项
74                     _self.selected_index = index ;
75                     
76                     //隐藏下拉框
77                     _self._show_itemlist();
78                 }
79                 return ;
80             });
81          }
82 };
83 }(jQuery));

 

插件验证:

在index.html中,增加对插件调用的代码:

<script type="text/javascript">
$('.zl_combobox').czl_combobox( {} );
</script>

打开index.html网页,我们发现自定义的控件已经可以响应用户的单击事件,效果如下:

优化控件的可扩展性

有了前面的基础,接下来的理解就会比较简单。前面我们在分析控件的调用过程中,有一个参数options传到ZLComboBox对象中,但是,在最后的_init和_loadEvents方法中都没有用到过,那么,这个options的意义在哪里呢?

我们还是从业务的需求出发,然后再来考虑我们如何实现:

  • 如果将控件用在“幼儿教学”的应用中,用户选中一种水果之后,可以播放与这种水果相关的介绍视频。
  • 如果将控件用在“卖水果”的应用中,用户选中一种水果,可以显示这种水果的价格、产地等信息。

这就意味着,在不同的应用场景中,用户做出“选择水果”的操作时,触发的后续动作是不一样的,我们的控件应该支持这种操作才对。

如果在初始化控件的时候,传入一个“回调函数”,当事件触发时,调用一下这个传入的回调函数,那不就达到我们的目的了吗?这样,不同的业务场景,我们只要传入不同的回调函数即可。

回调函数是JavaScript的最拿手的,现在,我们就来优化我们的控件,让它支持回调函数。

这里也先假设一个场景:

当用户选中一个选项之后,我们就将选项中水果的图片显示出来,这时候,调用这个控件的方式变成这样:

1 $('#fruit_box').czl_combobox( {
2             selectItemEvent:function( index ){
3                 var html_content = $( '.item_list li img' ).get( index ).outerHTML;
4                 $('#result_box').html( html_content );
5             }
6 } );

注意到两点:

1. #fruit_box 为控件. zl_combobox对应的id,在同一个应用中可能存在多个zl_combobox控件,当要传入回调函数时,一般就用id去引用对应的控件元素,因为相同的控件,在不同的应用场景,行为是不同的,传入的回调函数也应该不同,所以不建议通过$(‘. zl_combobox’)一次性对所有的控件进行初始化。

2.传入的回调函数的名称是selectItemEvent,带有一个index 的参数。

 

现在,我们来优化控件的核心对象ZLComboBox中的内容:

1. 新增一个options成员,用来保存传入的参数对象:

_init:function( options ){
     //
     this. options = options ;
}

2. 在下拉列表框中的选项单击事件中,增加对回调函数的调用:

       //….
       //隐藏下拉框
    _self._show_itemlist();
    //调用回调函数
    _self.options.selectItemEvent( index );

然后,我们再来验证一下效果,打开index.html,从下拉框中选中一个选项,效果如下:

 

至此,我们最初设定的目的是已经达成了。为了完整性,再补充一下控件“扩展性”的另外一个特征:默认值设定。

还是以之前的业务场景为例:如果用户没有传入回调函数,我们希望控件就把选中项的序号通过提示框显示出来。如果有传入会调用函数,就调用用户传入的回调函数。也就是说,给控件增加默认的行为,当调用时没有传入指定参数,就调用默认的值

现在我们就来实现控件的默认值设定:

1. 给ZLComboBox对象增加defaults成员,代码如下:

1 //默认值设置
2     $.ZLComboBox.defaults = {
3         selectItemEvent:function( index ){
4             alert( index );
5             return ;
6         }
7     };

2. 在_init方法中,保存传入的options对象采用如下的方式:

this.options = $.extend( true , {} , $.ZLComboBox.defaults , options );

这样,如果options中没有指定相关的成员,就调用defaults中的成员。这是jQuery插件处理传入参数的一种方式,$.extend为jQuery库定义的方法。 

优化控件的可用性

从理解语言特性来看,这部分内容与上一部分没有什么差异,我们只是从业务角度来看,对实现方式做一些区分。

回到我们控件的场景,本文实现的控件我们取名为ZLComboBox,ZL为前缀,显然,它的行为特征与标准的组合框控件是类似的,组合框控件最常见的使用方式就是在表单(Form)中,那么,我们的控件也应该支持在表单(Form)中使用。

如果ZLComboBox是作为表单的一个组件,那么,就需要在用户提交表单数据时,能够获取到用户到底选了哪个选项。从目前来看,似乎只能判断.cb_input容器中的内容,而这个内容是这个样子:

<img src="images/f_apple.png" /><span>苹果</span>

显然,用户处理起来非常不方便。

也许你已经想到了,可以通过传入回调函数的方式,当用户选中一个选项之后,就将这个序号值(index)更新到某个隐藏的<input>元素中,表单提交数据时,提交这个隐藏的<input>中的值即可,这当然是一种处理方式。

其实,我们可以给控件的核心对象ZLComboBox增加一个方法getSelectedIndex,用来获取当前用户选中的项的序号,代码如下:

1 _loadEvents:function(){
2             //
3          },
4          getSelectedIndex:function(){
5                 return this.selected_index;
6          }

这时候,控件的调用方式变成:

1 $('#fruit_box').czl_combobox( {
2             selectItemEvent:function( index ){
3                 var html_content = $( '.item_list li img' ).get( index ).outerHTML;
4                 $('#result_box').html( html_content );
5             }
6 } );
7 
8 //在表单提交前的数据处理中,取得用户选中项的序号
9 var index = $('#fruit_box').data( ‘czl_combobox’ ). getSelectedIndex() ;

重点在第9行。

当然,你也许会觉得,这种方式还不如之前动态更新隐藏<input>的方式自然,这里的主要目的是给大家一个特性解释,具体情况当然依据业务需要来确定。

另外,也许你已经发现了,每次调用我们自定义的控件,在HTML代码中都要写入一大堆内容,能不能在使用时HTML代码中仅仅定义<div class=” zl_combobox”></div>?然后将选项通过参数options传入,在ZLComboBox的_init方法中动态生产子部件呢?答案是:当然可以,就留作练习吧。

总结

通过前面的内容介绍,我们大致理解了自定义控件的意义,以及开发一个Web自定义控件的大致过程,希望能给大家带来一些启发。当然,我们的举例,实现方式都仅仅是为了讲解整个过程,在实际的开发过程中,除了业务诉求之外,还需要考虑其他方面的要求,比如性能方面:将整个控件库中CSS样式用到的背景图片优化成"雪碧"图,减少加载时间。或者安全性的优化:将某些插件的框架由'伪类'(new)调整为'闭包'模式,增强控件的安全性。

完整的示例代码,请单击此处下载。

感谢诸位捧场^_^~~

posted @ 2016-03-15 22:41  阿来之家  阅读(712)  评论(0编辑  收藏  举报