一步步打造一個支持異步加載數據的移動端選擇器

選擇器在應用中是使用比較頻繁的一個組件。在移動端,不一樣的操做系統默認的選擇器存在各類差別,iOS爲底部滾動的選擇器,不一樣的Android系統,默認的選擇器也是不盡相同的。同時,默認的選擇器在web應用中定製性也比較差。在實際開發中,針對web應用,咱們的需求通常是忽略系統差別,不一樣的操做系統間,同一個組件的表現形式應該是一致的。另外,咱們經常要求選擇器能夠定製,如支持多級聯動,支持標題定製,樣式定製和回調等。所以,web應用中的選擇器大部分狀況須要本身開發組件。目前,關於移動端選擇器組件在各大技術社區和github上都有很多的相關的項目,實現的方法和方式也是各式各樣。就我我的而言,我比較喜歡的選擇器交互方式爲相似京東的地區聯動選擇器,如圖:javascript

緣由有以下幾個(與iOS選擇器作比較):html

  • 實現方法簡單
  • 能夠更好實現真正意義上的無限級聯動
  • UI的定製更加靈活,如單行除了顯示一個項(京東一行顯示一個地址)之外,也能夠全部的項緊密排列在一塊兒,這樣對於比較多的數據時會顯得更清晰,操做更方便;定製樣式也會變得更靈活,如數據描述文字的長短控制(在iOS那種交互的選擇器下,若是文字過長可能須要作一些如顯示省略號的特殊處理
  • 兼容性高,交互的事件更簡單

喜歡歸喜歡,實際上目前爲止,本人接到最多的需求仍是設計成仿照iOS選擇器的樣式,以下:java

本文將基於這種交互方式,實現一個仿iOS的選擇器,最終效果如上圖。相似京東地址選擇器的開發核心思路與此相同,甚至要簡單一些。node

需求

  • 支持扁平的數據和嵌套的數據,如["北京市","上海市","天津市"]
[{
    name: '北京市',
    code:"110000",
    list: [
      {name: '東城區',code: '110101'},
	  {name: '西城區',code: '110102'},
      {name: '朝陽區',code: '110105'},
      {name: '豐臺區',code: '110106'},
      {name: '海淀區',code: "110108"},
      {name: '房山區',code:'110111'}
    ]
 }]

複製代碼
  • 支持多級聯動選擇
  • 支持確認回調,單項選擇回調和取消選擇回調
  • 支持異步加載數據並動態改變選擇器
  • 支持樣式定製

開發思路

(1) 初始化時先建立必須的dom結構git

(2) 遍歷傳入的數據,建立第一個能夠滾動的選擇器,並選中第一個(若是有傳入默認值,則選擇對應的項),選中後若是當前項存在子級列表,則遞歸建立第二個滾動的選擇器,並依次遞歸建立第三個,第四個...直至不存在子級列表爲止github

(3)當滑動選擇器時,改變選擇器的偏移量來選擇對應的項,選擇結束後同樣須要遞歸建立子級滾動器web

輔助數據

因爲傳入的數據能夠是扁平的,也能夠是嵌套的。在實現過程有部分代碼會有不一樣的處理狀況,故如下給出兩組數據輔助理解。數組

//扁平的選擇數據
var fruit = ["蘋果","香蕉","火龍果","牛油果","草莓"];
//城市聯動數據
var city = [
  {
    name: '北京市', //name對應實例化時傳入的textKey
    code:"110000",
    list: [ //list對應傳入的childKey
      {name: '東城區'},
	  {name: '西城區'},
      {name: '朝陽區'},
      {name: '海淀區'}
    ]
  },
  {
    name: '廣東省',
    code: "440000",
    list:[
      {
        name: '廣州市',
        code: "440100",
        list: [
          {name: '海珠區',code:'440105'},
          {name: '天河區',code: "440106",
           list:[
				{name: '黃埔大道'},
				{name: '中山大道'},
				{name: '華夏路'}
			]
		  }
        ]
      },
      {
        name: '深圳市',
        list: [
			{name: '羅湖區'},
			{name: '南山區'},
			{name: '福田區'},
			{name: '龍華新區'},
			{name: '龍崗區'},
			{name: '寶安區'}
		]
      },
      {name: '東莞市'},
    ]
  }
];

複製代碼

實現

html 與 CSS 部分

因爲html結構和CSS自己不難也不是重點,這裏不作詳細的描述和分析,只給出基本的html和在CSS中對應的類名關係說明。詳細的代碼能夠見源碼。緩存

圖中,黑色框部分爲核心的html結構,後面全部的操做都操做這一部分的結構,其它部分實例化再也不改變。每增長一個選擇器就增長一個<div class="mp-list-wrap><ul></ul></div>,經過改變ul的transform屬性來實現滾動效果。app

javascript部分

1. 基本的代碼結構

;(function(global){
  var PICKERCOUNT = 0; //同一個頁面可能有多個選擇器,用於記錄選擇器個數
  var body = document.getElementsByTagName("body")[0]; // body元素
  var coordinate = {start: {y:0},end: {y:0,status:true}, move: {y:0}}; //記錄手指滑動的座標
  function Picker(config){} //構造函數
  
  Picker.prototype = {
   constructor: Picker,
   //核心代碼
   //...
  }

  if (typeof module !== 'undefined' && typeof exports === 'object') {
     module.exports = Picker;
  } else if (typeof define === 'function' && (define.amd || define.cmd)) {
    define(function() { return Picker; });
  } else {
    global.Picker = Picker;
  }
})(this);

複製代碼

2. 工具函數

在實現邏輯時會屢次用到同一個操做或計算,咱們封裝一個Util對象,專門用來處理這部分的邏輯,代碼和解釋以下:

var Util = {
  removeClass: function(el, className) {//刪除el元素指定的類
    var reg = new RegExp('(\\s|^)' + className + '(\\s|$)');
    el.className = el.className.replace(reg, ' ').replace(/^\s\s*/, '').replace(/\s\s*$/, '');
  },
  addClass: function(el, className) { //爲el元素添加一個類
    !Util.hasClass(el,className) && (el.className += (el.className ? ' ' : '') + className);
  },
  hasClass: function(el, className) { //判斷el元素是否有對應的類
    return !!el.className && new RegExp('(^|\\s)' + className + '(\\s|$)').test(el.className);
  },
  loop: function(start,end,handle){ //循環操做
    for(var i = start; i < end; i++){
      Util.isFunc(handle) && handle(i);
    }
  },
  isFunc: function(name){ //判斷是否爲函數
    return typeof name === 'function';
  },
  isArray: function(o) { // 判斷是否爲數組
    return Object.prototype.toString.call(o) === '[object Array]';
  },
  isObject: function(o) { // 判斷是否爲對象
    return typeof o === 'object';
  },
  damping: function (value) {//阻尼運算,用於滾動時的彈性計算
    var steps = [20, 40, 60, 80, 100],rates = [0.5, 0.4, 0.3, 0.2, 0.1];
    var result = value,len = steps.length;
    while (len--) {
      if (value > steps[len]) {
        result = (value - steps[len]) * rates[len];
        for (var i = len; i > 0; i--) {
            result += (steps[i] - steps[i - 1]) * rates[i - 1];
        }
        result += steps[0] * 1;
        break;
      }
    }
    return result;
  },
  //建立元素,參數分別爲父級元素,標籤名,類名,初始的html內容
  createEle: function(parent, tag, className, html) { 
    var ele = document.createElement(tag);
    className && Util.addClass(ele,className);
    html && (ele.innerHTML = html);
    parent && parent.appendChild(ele);
    return ele;
  },
  getEle: function(ctx, selector) { // 獲取元素
    return ctx.querySelector(selector);
  },
  setTransform: function(el,y) { // 設置el元素的translateY
    el.style.transform = 'translate3d(0,'+ y +'px,0)';
  }
}

複製代碼

3. 構造函數

構造函數接收傳入的配置,包括數據,觸發選擇器的元素,事件回調以及初始值,子列表的key等,同時要記錄當前選擇器的索引(經過PICKERCOUNT來計算)。構函數最終的代碼以下:

function Picker(config){
  this.index = ++PICKERCOUNT;//當前選擇器的索引
  this.target = config.target instanceof HTMLElement ? config.target : typeof config.target === "string" ? Util.getEle(document,config.target) : null;//觸發選擇器的dom元素
  this.data  = config.data || [];//須要顯示的數據
  this.value = config.value ? (Util.isArray(config.value) ? config.value : config.value.split(',')) : [];//選擇器默認值
  this.childKey = config.childKey || 'child';//子數據索引名
  this.valueKey = config.valueKey || 'value';//用於索引初始值的key
  this.textKey = config.textKey || 'value';//用於顯示的key
  this.autoFill = !(config.autoFill === false);//選擇肯定後是否填充到目標元素
  this.confirm = config.confirm;//肯定選擇的回調
  this.cancel = config.cancel;//取消回調
  this.initCallback = config.initCallback;//實例化完成的回調
  this.select = config.select;//單個列表選擇後的回調
  this.lock = config.lock === true;//鎖定肯定按鈕,用於異步加載時等待使用
  this.className = config.className || '';//定製的類名
  this.init(); //生成各類結構
}

複製代碼

4. init方法

init方法用於生成基本的dom結構(上圖中非黑色框部分),生成後遞歸建立選擇器列表,並綁定事件。代碼和解析以下:

init: function(){
  this.initResult(); //初始化選擇器結果
  //基本的dom結構
  var html = '<div class="mp-container"><div class="mp-header"><span class="mp-cancel">取消</span><span class="mp-confirm'+(this.lock ? ' disabled' : '')+'">肯定</span></div><div class="mp-content"><div class="mp-shadowup"></div><div class="mp-shadowdown"></div><div class="mp-line"></div><div class="mp-box"></div></div>';
  var container = Util.createEle(body,'div','mp-mask',html);
  // 若是傳入了額外的類名,則在外層容器添加類名
  this.className && Util.addClass(container,this.className);
  container.id = 'mobilePicker'+this.index;
  this.container = container;
  this.box = Util.getEle(container, '.mp-box')//用於包含滾動元素的容器
  this.createScroll(this.data);//核心方法:建立滾動的元素
  this.value = []; // 建立完重置傳入的默認值,後面的選擇器值均爲上一次選擇的值
  this.bindEvent();//綁定事件
  this.finisInit(); //初始化結束後的回調
}
initResult: function(config){
  this.scrollCount = 0;//已渲染的數據層級數
  this.selectIndex = [];//每一個層級選中的索引集合
  this.result = [];//選擇器最終的結果
  this.offset = [];//每一個層級滾動的偏移量集合
}

複製代碼

5. 核心方法

接下來重點看一下如下幾個核心方法的實現思路和具體的代碼。

  • 思路:

整個思路的方法調用關係以下圖:

init方法建立完基本元素後調用createScroll方法,該方法首先建立一個類名爲.mp-list-wrap的元素,用於包裹接下來要建立的滾動結構,同時爲改元素記錄一個遞增的scrollIndex,用於後續定位該元素並更新數據和偏移量等操做;接着調用addList方法,遍歷傳入的data數據,先遍歷第一層數據,每遍歷一個拼接一個li標籤並填充數據,若是有傳入默認值,則比對當前的值是否與默認值相同,若是相同則記錄當前選擇列表選中的index爲當前索引,不然index爲0。拼接完全部的li標籤則添加至滾動器,並初始化當前的滾動器偏移量offset爲0,緊接着調用selectItem方法,傳入index參數,選中當前須要選中的列表項。selectItem根據傳入的index計算出當前選中的結果存儲至結果集,而且計算當前滾動器的偏移量,將滾動器滾動至相應的位置。同時,遍歷傳入的data,根據index判斷當前data[index]是否有子級列表數據,若是有,則遞歸調用createScroll方法繼續建立下一個選擇列表,直至沒有子級列表。同時,每次執行選擇應該將當前列表之後的列表清除掉(removeScroll)或更新(udpateScroll)【見下面場景說明】。滾動的列表已經所有建立完成後,再調用calcWidth計算每個scroll的寬度便可。

init方法調用完createScroll方法後須要調用bindEvent方法綁定事件。事件的處理基本也都是在不一樣的時機調用createScroll,selectItem以及更新單個scroll的方法等。

關於須要更新和刪除scroll的場景說明: 假設當前選擇器爲一個城市聯動選擇器,用戶第一次選擇了廣東省->廣州市->天河區->中山大道。緊接着用戶第二次打開選擇器,將廣東省改成了北京市,此時屬於第一個選擇列表,因此第二個選擇列表(即廣州市所在的列)的數據應該更改成北京市下面的區,經過調用updateList來實現,更新後默認選擇第一個,如東城區。假如此時東城區如下沒有子級的數據,那麼應該調用removeScroll來移除東城區所在列之後的列,即天河區和中山大道所在的列。

  • 詳細代碼和解析
createScroll: function(data){//建立滾動列表
    var scroll = Util.createEle(this.box,'div','mp-list-wrap','<ul></ul>');//建立一個scroll
    scroll.scrollIndex = this.scrollCount++; //爲當前scroll添加索引
    this.addList(Util.getEle(scroll, 'ul'), data); //往scroll添加li元素
 },
 addList: function(parent, data){//添加數據
  var html = '',that = this;
  var index = 0,scrollIndex = parent.parentNode.scrollIndex,text = '';
  Util.loop(0,data.length,function(i){
    text = that.getText(data[i]);//計算要顯示的列表項文字
    html += '<li>'+text+'</li>';
    //初始化時有默認值,應該選中當前值,不然index就會爲0,即選中第一個
    if(that.value.length && that.value[scrollIndex] && (Util.isObject(data[i]) && data[i][that.valueKey] === that.value[scrollIndex][that.valueKey] || data[i] == that.value[scrollIndex])){
     index = i;
    }
  });
  parent.innerHTML = html;
  this.offset.push(0);//每添加一個須要添加一個偏移量的記錄
  this.selectItem(data, index, scrollIndex);//選中並建立下一級選擇器
 },
 updateList: function(index,data){//更新某一列的數據
   var dom = this.box.childNodes[index];
   if(!dom){
     this.createScroll(data);
     return;
   }
   dom = Util.getEle(dom,'ul');
   this.addList(dom, data);
 },
 removeScroll: function(index){//移除某一列
   var that = this;
   var node = this.box.childNodes[index];
   if(node){
     this.box.removeChild(node);
     this.scrollCount--;//移除後當前的列數量要減1
     this.calcWidth(); //從新計算每一列的寬度
   }
 },
 selectItem:function(data, index, scrollIndex){//params: 數據,選中的索引,當前scroll的索引
  var that = this;
  var oldScrollCount = this.scrollCount;//記錄當前的列數
  this.selectIndex.length = this.result.length = scrollIndex + 1; //重置結果集的長度
  this.selectIndex[scrollIndex] = index;//記錄當前列選中的索引
  this.result[scrollIndex] = this.setResult(data[index]); //記錄當前列選中的結果(去除子列表)
  this.setOffset(scrollIndex, index); // 將當前列滾動至選擇的位置
  if(data[index] && data[index][that.childKey] && Util.isArray(data[index][that.childKey]) && data[index][that.childKey].length){//存在子級列表
    if(that.scrollCount < scrollIndex + 2){//若是上一次的列數少於當前須要的列數,則建立一個新的列
      that.createScroll(data[index][that.childKey]);
    }   else { // 目前已有的列數不小於當前須要的列數,則更新對應的列的數據
      that.updateList(scrollIndex + 1, data[index][that.childKey]);
    }
  } else {//說明當前的已有的列數目多於須要的,移除多餘的
    for ( var j = oldScrollCount - 1, len = that.selectIndex.length; j >= len; j-- ) {//刪除多餘的ul
      that.removeScroll(j);
    }
  }
 this.offset.length = this.selectIndex.length;//重置偏移量結果集的長度
 this.calcWidth();//計算每一列的寬度
 Util.isFunc(that.select) && that.select(scrollIndex,this.result,index,data[index] && data[index][that.childKey] && Util.isArray(data[index][that.childKey]) && data[index][that.childKey].length); //執行單列選擇回調,通常用於異步請求數據
},
 bindEvent: function(){//事件綁定
   var that = this;
    that.target.disabled = true;
    ['touchstart','touchend','touchmove'].forEach(function(action){
        that.box.parentNode.addEventListener(action,function(event){
        event = event || window.event;
        event.preventDefault();
        var target  = event.target;
        var index = target.parentNode.scrollIndex;
        var child = target.childNodes;
        var liHeight = child[child.length - 1].offsetHeight;
        var scrollHeight = child[child.length - 2].offsetTop;
        if(target.tagName.toLowerCase() != 'ul') return;
        switch(action) {
          case 'touchstart':
            if(coordinate.end.status){
              coordinate.end.status = !coordinate.end.status;
              coordinate.start.y = event.touches[0].clientY;
              coordinate.start.time = Date.now();
            }
            break;
          case 'touchmove':
            coordinate.move.y = event.touches[0].clientY;
            var distance = coordinate.start.y - coordinate.move.y;
            var os = distance + that.offset[index];
            if(os < 0){//已經滑到最頂部
              Util.setTransform(target, Util.damping(-os));
            } else if(os <= scrollHeight){
              Util.setTransform(target, -os);
            } else {//超過了總體的高度
              Util.setTransform(target, -(scrollHeight + Util.damping(os-scrollHeight)));
            }
            break;
          case 'touchend': //中止滾動後計算應該選擇的項
            coordinate.end.y = event.changedTouches[0].clientY;
            var count = Math.floor((that.offset[index] + (coordinate.start.y - coordinate.end.y))/liHeight + 0.5)
            count = count < 0 ? 0 : Math.min(count, target.childNodes.length - 1);
            var temp = that.offset[index];
            that.offset[index] = count < 0 ? 0 : Math.min(count * liHeight,target.offsetHeight - 5 * liHeight)
            Util.setTransform(target, -that.offset[index]);
            coordinate.end.status = true;
            that.selectIndex.length  = index + 1;
            that.selectIndex[index] = count;
            that.selectItem(that.getData(that.selectIndex),count,index);
            break;
        }
      },false)
    });
    that.target.addEventListener('touchstart',function(event){
      (event || window.event).preventDefault();
	   //記錄舊結果,用於取消恢復
      that.oldResult = that.result.slice(0);
      that.update({//因爲更新整個選擇器
        value: that.result,
        valueKey: that.textKey
      });
      that.show();
    });
    // 用click事件代替touchstart防止點透
    Util.getEle(that.container,'.mp-cancel').addEventListener('click',function(){
      that.hide();
	  //恢復舊的結果,update方法見後面的接口方法
      that.update({
        value: that.oldResult,
        valueKey: that.textKey
      });
      Util.isFunc(that.cancel) && that.cancel();
    },false);
    Util.getEle(that.container,'.mp-confirm').addEventListener('click',function(){
      if(that.lock) return;
      var value = that.fillResult(); //計算最終的選擇結果
      that.hide();
      Util.isFunc(that.confirm) && that.confirm(value, that.result);
    });
  }

複製代碼
  • 其餘輔助代碼
hide: function(){//關閉選擇器
    var that = this;
    Util.getEle(this.container,'.mp-container').style.transform = 'translate3d(0,100%,0)';
    Util.removeClass(body, 'mp-body');
    setTimeout(function(){
      that.container.style.visibility = 'hidden';
    },250)
  },
  show: function(){ //顯示選擇器
    var that = this;
    that.container.style.visibility = 'visible';
    Util.addClass(body, 'mp-body');
    setTimeout(function(){
      Util.getEle(that.container,'.mp-container').style.transform= 'translate3d(0,0,0)';
    },0)
  },
  fillContent: function(content){ //填充最終選擇的數據到觸發的元素
    var tagName  = this.target.tagName.toLowerCase();
    if(['input','select','textarea'].indexOf(tagName) != -1) {
      this.target.value = content;
    } else {
      this.target.innerText = content;
    }
  },
  fillResult: function(){ //計算最終結果
    var value = '';
      for(var i = 0,len = this.result.length; i < len; i++){
        if(Util.isObject(this.result[i])){ //若是是嵌套的數據,則根據textKey來拼接
          this.result[i][this.textKey] && (value += this.result[i][this.textKey]);
        } else { //扁平的數據則直接拼接
          value += this.result[i];
        }
      }
      this.autoFill && this.fillContent(value); //選擇後須要自動填充則填充數據
      return value;
  },
  getText: function(data){ //每一項要顯示的文字,分扁平數據和嵌套數據兩種狀況
    return Util.isObject(data) ? data[this.textKey] : data;
  },
  finisInit: function(){//初始化完成後內部調用的方法
    var value = this.fillResult();
    Util.isFunc(this.initCallback) && this.initCallback(value,this.result);
  },
  setOffset: function(scrollIndex, index){ //設置偏移量
    var scroll = this.box.childNodes[scrollIndex].querySelector('ul');
    var offset = scroll.childNodes[0] ? scroll.childNodes[0].offsetHeight * index : 0;
    Util.setTransform(scroll, -offset)
    this.offset[scrollIndex] = offset;
  },
 setResult: function(data){ //去除子級列表,計算每一列選擇的結果
    if(!Util.isObject(data)) return data;
    var temp = {};
    for(var key in data){
      key != this.childKey && (temp[key] = data[key]);
    }
    return temp;
  },
 getData: function(indexes){//根據一組因此深度遍歷數據,獲取數據當前須要添加的集合
    var arr = [];
    for(var i = 0; i < indexes.length; i++){
      arr = i == 0 ? this.data : arr[indexes[i -1]][this.childKey];
    }
    return arr;
 }

複製代碼

6. 接口方法

因爲選擇器須要支持異步加載數據,則須要在某些時機操做和動態的更新選擇器,所以須要提供一些接口方法供異步加載時使用。其中上面的removeScrolladdList以及updateScroll都可做爲接口方法。除此以外還須要提供如下方法:

/*傳入新的數據,更新整個選擇器 該方法至關於從新初始化選擇器,僅僅只是省去了建立基本dom接口和事件綁定的過程 */
update: function(options){ 
  for(var i in options) {
    this[i] = options[i];
  }
  this.initResult()
  this.box.innerHTML = '';
  this.createScroll(this.data);
  this.value = [];
}, 
/* 更新某一列的數據,通常用於聯動時加載下一級數據 @param: index 要更新的列索引 @param: data 要更新的數據集合 @pramg: value 默認選中的值 @param: callback 更新後的回調函數 */

setScroll: function(index,data,value,callback) {
  value && (this.value[index] = value);
  this.offset.length = this.selectIndex.length = this.result.length = this.selectIndex.length = index;
  if(index == 0){
    this.data = data;
  } else {
    var temp = this.data[this.selectIndex[0]];
    for(var i = 1, len = index; i < len; i++){
      temp = temp[this.childKey][this.selectIndex[i]];
   }
    temp && (temp[this.childKey] = data); //更新data裏對應的數據
  }
  this.updateList(index,data);
  this.value = [];
  Util.isFunc(callback) && callback(index,this.result);
},
 setLock: function(value){ //鎖定或者解鎖肯定按鈕,通常用於請求數據時的等待,以保證數據的完整性
    var confirm = Util.getEle(this.container,'.mp-confirm'),old = this.lock;
    this.lock = value !== false;
    if(old !== this.lock) {
      this.lock ? Util.addClass(confirm,'disabled') : Util.removeClass(confirm, 'disabled');
    }
  },


複製代碼

實例

如下全部實例點擊標題能夠在移動端預覽效果,你能夠在手機端點擊標題或者用手機掃描二維碼預覽。

<div class="container">
 <label for="">水果:</label>
 <input type="text" id="fruit">
</div>

複製代碼
var fruit = ['蘋果','香蕉','火龍果','芒果','百香果'];
new Picker({
  target: '#fruit',
  data: fruit,
  value: '火龍果' //默認值
});

複製代碼

效果以下:

這是一個綜合實例,包含的內容有:嵌套的數據傳默認值,自動填充和自定義填充,一個頁面有多個選擇器,childKey,valueKey,textKey,initCallback,confirm等的使用。

html:

<div class="container">
    <div>
      <label for="">出發地</label>
      <input type="text" id="start">
    </div>
    <div>
      <label for="">目的地</label>
      <input type="text" id="end">
    </div>
    <div class="print">
      <div>選擇結果:</div>
      <div>startValue: <span id="startValue"></span></div>
      <div>startResult: <span id="startResult"></span></div>
      <div style="margin: 10px 0">--------------------</div>
      <div>endValue: <span id="endValue"></span></div>
      <div>endResult: <span id="endResult"></span></div>
    </div>
 </div>

複製代碼

data的基本格式:

var city = [
  {
    name: '北京市',
    code:"110000",
    list: [
      {name: '東城區'},
      {name: '豐臺區'},//...
    ]
  },
  {
    name: '廣東省',
    code: "440000",
    list:[
      {
        name: '廣州市',
        code: "440100",
        list: [
          {name: '海珠區',code:'440105'},
          {name: '天河區',code: "440106",
		   list:[
				{name: '黃埔大道'},
 				{name: '中山大道'},
				{name: '華夏路'}
		  ]}
        ]
      },
      //...
    ]
  }
 //...
 
];

複製代碼

邏輯代碼:

new Picker({
  target: document.getElementById('start'),//直接傳入dom元素
  data: city,
  textKey: 'name', //用於顯示的key
  valueKey: 'name', //用於關聯默認值的key
  childKey: 'list', //子數據列表的key
  value: [{name: '廣東省'},{name: '深圳市'},{name: '南山區'}], //默認值
  confirm: function(value,result){ //肯定選擇的回調
    document.getElementById('startValue').innerText = value;
    document.getElementById('startResult').innerText = JSON.stringify(result);
  },
  initCallback: function(value,result) { //初始化結束後的回調
    document.getElementById('startValue').innerText = value;
    document.getElementById('startResult').innerText = JSON.stringify(result);
  }
});
new Picker({
  target: '#end',
  data: city,
  textKey: 'name',
  autoFill: false, //不自動填充
  childKey: 'list',
  confirm: function(value,result){
    document.getElementById('endValue').innerText = value;
    document.getElementById('endResult').innerText = JSON.stringify(result);
    //結果處理後再顯示
    var text = [];
    for(var i = 0; i < result.length; i++){
      text.push(result[i].name);
    }
    this.target.value = text.join('-');//this.target取得當前的目標元素,並自定義填充
  }
});

複製代碼

效果以下:

其中html結構與上面的城市聯動結構一致,不一樣的是,此次,第三級之後的數據(如天河區等)採用異步加載的方法來獲取。思路是:當選擇一個列後,調用select回調,若是當前沒有下一級數據則須要異步加載,加載時經過setLock方法鎖定肯定按鈕,不讓點擊,以保證數據的完整性。同時,調用setScroll方法預先添加一個空白列,待數據返回後,若是有子級列表,則再調用setScroll方法更新當前列的數據,不然,調用removeScroll方法移除預先添加的列。完成後,再調用setLock方法解鎖肯定按鈕。

function getDistrict(code){
  var data = {
    "code_440330" : [{name: '羅湖區'},{name: '南山區'},{name: '福田區'},{name: '龍華新區'},{name: '龍崗區'},{name: '寶安區'}],
    "code_440100": [{name: '海珠區',code:'440105'},{name: '天河區',code: "440106",list:[{name: '黃埔大道'},{name: '中山大道'},{name: '華夏路'}]}],
    "code_440700":  [{name: '臺山市'},{name: '鶴山市'},{name: '開平市'},{name: '新會區'},{name: '恩平市'}],
    "code_330100": [{name: '桐廬縣'},{name: '江乾區'},{name: '西湖區'},{name: '下城區' }],
    "code_330200": [{name: '江東區'},{name: '江北區'},{name: '高新區'},{name:'海曙區'},{name: '象山區'},{name:' 慈溪市'}]
  }
  return data['code_'+code] || [];
}
var picker = new Picker({
    target: '#area',
    data: city,
    textKey: 'name',
    childKey: 'list',
    confirm: function(value, result){
      var str = [];
      for(var i = 0, len = result.length; i < len; i++){
        str.push(result[i].name);
      }
      this.target.value = str.join('-');
      document.getElementById('value').innerText = value;
      document.getElementById('result').innerText = JSON.stringify(result)
    },
    select: function(scrollIndex,result,index,haschild){
      var city = result[scrollIndex];//獲取結果集中的當前項
      var that = this;
      //當選擇的不是城市級別或者選擇的是直轄市或者當前選擇的城市已有子級列表(沒有的在請求後會被緩存)則不作操做
      if(scrollIndex !== 1 || "11|12|31|50".indexOf(city.code.substring(0, 2)) >= 0 || haschild) return;
      this.setScroll(scrollIndex + 1, []);//建立空白的下一級選擇器,之因此這樣作是防止頁面抖動
      that.setLock(true);//由於是異步請求,數據沒返回以前鎖定選擇器,請留意效果圖中的肯定按鈕
      setTimeout(function(){//這裏模擬一個1秒鐘的異步請求
        var data = getDistrict(city.code);//拿到異步數據
        if(data.length){
          that.setScroll(scrollIndex + 1, data);//更新下一級選擇器的數據
        } else {
          that.removeScroll(scrollIndex + 1);//沒有數據,則移除以前防止抖動的選擇器
        }
        that.setLock(false);//請求完畢,解鎖
      },1000)
    }
 })
複製代碼

效果以下:

時間選擇器

自定義樣式

這兩個例子這裏不放代碼。其中自定義樣式比較簡單,源碼能夠看這裏。而時間選擇器是一種特殊的選擇器,選擇器的數據基本能夠經過計算獲得而不須要額外傳入,同時,配置項也有所差別,因此,時間選擇器更適合寫一個獨立的選擇器,後續我會在github上加上獨立的時間選擇器。當前時間選擇器的例子的源碼比較囉嗦同時還會形成界面抖動,源碼能夠查看這裏查看。兩個例子的效果以下):

總結

不管是實現其餘交互類型的選擇器仍是實現仿iOS的選擇器,實現的方式都各式各樣。若是在實際的應用只是有一處用到了選擇器,那應該沒有必要去專門封裝一個通用的選擇器,直接按需寫一個僅僅知足需求反而會簡單和快捷一些。本文提供一個封裝選擇器的思路,供你們一塊兒學習和探討,歡迎反饋問題。本文全部源碼和使用的文檔均在github上,有興趣的能夠fork下來繼續完善,點擊這裏查看

相關文章
相關標籤/搜索