我爱编程

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

2018-04-10  本文已影响0人  hux1ao

这是一道笔试题


需求.png

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

首先分析需求

在基本要求中

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

在扩展要求中

那么,动手开始做吧

编写静态页面
页面.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')

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

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

实现场景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小时
可改进的地方:

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

上一篇 下一篇

猜你喜欢

热点阅读