美文网首页我爱编程
创建一个看似简单的select下拉框

创建一个看似简单的select下拉框

作者: hux1ao | 来源:发表于2018-04-10 11:53 被阅读0次

这是一道笔试题


需求.png

我们先来了解一下需求是怎样的

首先分析需求

在基本要求中

  • 基本功能同浏览器的下拉框组件(不就select标签吗,实现起来难度好像不是很大 :))
  • 兼容尽量多的浏览器(制作一个vue组件的话能兼容到ie8(还有6,7 好像不是很符合题目 T T),初步决定使用原生js来写吧)
  • 支持直接输入(我参考elementselect组件其中也包含可以直接输入的组件,没有采用原生 html中的 select标签,而是使用input标签来写的,我们这边也仿照elment的写法)
  • 输入时下拉列表的选项自动前缀匹配(嗯,百度了一波前缀匹配,脑海第一印象是正则一把梭)
  • 匹配到的前缀用红色文字显示(好像不是很难...)

在分析完基本要求之后,我决定要这样来完成它

  • 使用原生js + html + css来完成它
  • 使用input标签作为select下拉框的可输入部分
  • 监听输入事件,进行前缀匹配

在扩展要求中

  • 支持异步加载数据(我的理解是异步获取数据之后显示在options中,所以我需要在全局维护一个options数组,在异步获取数组之后,更改options的值,显示在页面上, 同理,也就要构造一个方法,根据传值的不同展示不同的options)
  • 支持大量数据(脑海里首先想到的就是优化匹配方法)
  • 用测试代码测试组件功能(之前学过的 KARMA + MOCHA 总算派上用场了)

那么,动手开始做吧

编写静态页面
页面.png

并且,在js中, 定义我们经常使用到的公共变量

    // 是否显示option
    let optionShow = false
    const body = document.querySelector('body')
    // 输入框
    const input = document.querySelector('.input')
    // 下拉框
    const select = document.querySelector('.select')
    // 下拉框箭头
    const arrow = document.querySelector('.arrow')
    // 选项
    const option = document.querySelector('.option')
    // 等待状态展示
    const loading = document.querySelector('.loading')
    // option为空展示
    const empty = document.querySelector('.empty')
    // 不为空时
    const notEmpty = document.querySelector('.not-empty')
    // 按钮
    const asyncButton = document.querySelector('#async-button')

特别的,我们维护了两个公共状态

  • option框显示状态 optionShow
  • 以及option框内数据options数组

放在全局变量中的目的是唯一的变量对应唯一的状态,减少代码的冗余程度,也方便维护

实现场景1

用户点击下拉框,下拉框展开,输入框旁的小箭头转换方向

function selectClickHandler () {
  optionShow = !optionShow
  // option显隐
  optionDisplay(optionShow)
  // 控制箭头朝向
  arrowDirection()
}
// 是否展示options框
function optionDisplay (optionShow) {
  let show = optionShow ? 'block' : 'none'
  option.style.display = show
}
function arrowDirection () {
  if (arrow.classList.contains('rotate') || arrow.classList.contains('rotate1')) {
    arrow.classList.toggle('rotate') // 新学到的toggle方法
    arrow.classList.toggle('rotate1')
  } else {
    arrow.classList.toggle('rotate')
  }
}
// 监听select点击事件
select.addEventListener('click', selectClickHandler)

到这一步,我们已经能够简单的实现点击input,变弹出下拉框了
但是下拉框此时还没有数据,

我们来为它添加一些默认数据
function initOption (options, pattern) {
  // 如果传进来的options没有内容则显示暂无数据
  if (options.length > 0) {
    empty.style.display = 'none'
    notEmpty.style.display = 'block'
  } else {
    empty.style.display = 'block'
    notEmpty.style.display = 'none'
  }
  // 初始化
  while(notEmpty.hasChildNodes()) {
    notEmpty.removeChild(notEmpty.firstChild);
  }
  // 填充i标签
  options.forEach(item => {
    let li = document.createElement('li')
    li.setAttribute('data-value', item.value)
    li.setAttribute('data-label', item.label)
    let textNode
    if (pattern) {
      textNode = document.createElement('span')
      let redFont = document.createElement('span')
      let text = document.createTextNode(pattern)
      redFont.style.color = 'red'
      redFont.appendChild(text)
      let restChar = item.label.replace(pattern, '')
      let blackFont = document.createTextNode(restChar)
      textNode.appendChild(redFont)
      textNode.appendChild(blackFont)
    } else {
      textNode = document.createTextNode(item.label)
    }
    li.appendChild(textNode)
    notEmpty.appendChild(li)
  })
}
// option选项
let options = [
  {label: '西', value: 1},
  {label: '西瓜', value: 2},
  {label: '西瓜创', value: 3},
  {label: '西瓜创客', value: 4},
  {label: '西西', value: 1},
  {label: '瓜瓜', value: 2},
  {label: '创创', value: 3},
  {label: '客客', value: 4}
]
// 我们在页面初始化时,调用initOption方法,填充对象
initOption(options)

到现在, 页面点击之后已经可以看到下拉框中显示出数据了

实现option点击之后input的value变为选中的值
function handleOptionClick ($event) {
  let element = $event.target
  if (element.nodeName === 'UL') return
  if (element.nodeName !== 'LI') {
    element = element.parentNode
    if (element.nodeName !== 'LI') {
      element = element.parentNode
    }
  }
  let label = element.getAttribute('data-label')
  selectClickHandler()
  window.setTimeout(() => {
    input.value = label
  }, 100)
}
notEmpty.addEventListener('click', handleOptionClick)

我们监听option的点击时间,在初始化li标签的时候,我们已经将数据的值通过自定义标签绑定到li标签上。所以在这里我们可以直接通过getAttribute api获取该值,从而传递到input中

现在

我们来实现前缀匹配呢
function handleValueChange () {
  // 输入框在输入时确保展示option框
  if (!optionShow) {
    optionShow = !optionShow
    optionDisplay(optionShow)
    arrowDirection()
  }
  let value = input.value
  let newOptions
  // 如果数据为空的时候,防止报错,直接初始化
  if (value === '') {
    initOption(options)
    return
  }
  // 我们维护了一个cache对象来存储数据,为了应对数据量大的情况
  if (cache.hasOwnProperty(value.charAt(0))) {
    let re = new RegExp('^' + value)
    newOptions = cache[value.charAt(0)].filter(item => {
      return re.test(item.label)
    })
  } else {
    newOptions = []
  }
  // 过滤已匹配的
  initOption(newOptions, value)
}
// 兼容ie的做法
if (input.onpropertychange) {
  input.addEventListener('propertychange', handleValueChange)
} else {
  input.addEventListener('input', handleValueChange)
}

我们已经实现了基本功能
那么,如何来实现异步操作?

其实在我们构造了一个initOption方法之后,我们只需要将异步操作的结果作为参数传递到函数中,我们的组件就可以根据异步操作结果展示不同的option

那么我们来模拟一下异步操作吧
// 点击获取网络数据之后,页面展示加载中
function showLoadingFlag (loadingFlag) {
  if (loadingFlag) {
    loading.style.display = 'block'
  } else {
    loading.style.display = 'none'
  }
}
// 模拟异步操作
function asyncLoading () {
  isLoading = true
  showLoadingFlag(isLoading)
  setTimeout(() => {
    isLoading = false
    showLoadingFlag(isLoading)
    let value = input.value
    generateRadom(value)
  }, 1000)
}
// 生成随机option
function generateRadom (pattern) {
  let num = Math.ceil((Math.random() * 10))
  let RandomOptions = []
  for (let i = 0; i< num; i++) {
    let value = pattern + Math.random().toString()
    RandomOptions.push({label: value, value})
  }
  initOption(RandomOptions, pattern)
}

那么我们在没有数据的时候,我们构造的函数已经能为我们构造假的数据作为展示,同时也完成了模拟异步操作的效果。

为了适用于数据量大的情况

我们每次在options加载之后对options进行一次处理,我们为options根据首字母构造索引,从而每次匹配时只需要匹配首字母相同的数据,从而减少对数据的操作

function adjustData (options) {
  cache = {}
  options.forEach(item => {
    let firstChar = item.label.charAt(0)
    if (cache.hasOwnProperty(firstChar)) {
      cache[firstChar].push({label: item.label, value: item.value})
    } else {
      cache[firstChar] = [{label: item.label, value: item.value}]
    }
  })
}

总结

我实现了一个可以完成前缀匹配的select下拉框,并且可以实现异步操作的功能。

花费时间: 8小时
可改进的地方:

  • 使用trie数据结构进一步提高效率
  • 因为时间原因没有来得及做单元测试,可以通过KARMA + MOCHA 完成单元测试
  • 增加用户交互特效,提升用户体验

源码在github
https://github.com/hux1ao/-/tree/master/%E7%AC%94%E8%AF%95-%E4%B8%8B%E6%8B%89%E6%A1%86

相关文章

  • 创建一个看似简单的select下拉框

    这是一道笔试题 我们先来了解一下需求是怎样的 首先分析需求 在基本要求中 基本功能同浏览器的下拉框组件(不就sel...

  • JQuery Select2插件用法注意事项

    用法很简单: 如此便可以实现一个简单的select下拉框。如果下拉框无法显示出来,也许是select的z-inde...

  • selenium之定位下拉框(Select)

    1. 定位