ListView 踩过的坑

接触mvc不久,突然没有了viewstate和服务端控件处处都觉得不顺手,很多在webform时不必要考虑的问题都出现在眼前,这其中分页时查询条件保持的问题又是最让我头疼的事情,权衡再三,决定用ajax局部刷新列表的方式来解决这个问题。网上基于jquery的grid组件很多,jquerygrid,jqgrid等等,分别试用了一下,功能确实非常强大,但感觉上有点重,配置项太多,用起来依然感觉束手束脚,所以想来想去,还是用最笨的办法自己做了一个组件,很简陋,唯一的好处就是灵活,容易修改和控制。

上节课主要讲解了PHP代码和HTML代码进行配合实现动态数据网页的方法。

select 选择器是个比较复杂的组件了,通过不同的配置可以有多种用法。有必要单独学习学习。

ListView 组件在 react-native 开发中频繁使用到。所以在实际开发时,在实现多选操作时碰到一个坑,虽然使用很奇葩的手段解决了,但并没有从根本上解决遇到的问题。经过别人指点后,终于了解问题产生的原因,记录下这个算得上是基础知识的坑。

没图没真相,先来个截图看看:

这节课开始讲解前端网页如何和后端数据进行交互来实现更复杂的功能。

整体结构

以下是 select 的 template 结构,已去掉了一部分代码便于查看整体结构:

<template>
  <div>
    <!-- 多选 -->
    <div
      v-if="multiple"
      ref="tags">
      <!-- collapse tags 多选时是否将选中值按文字的形式展示 -->

        <el-tag
          type="info"
          disable-transitions>
          {{ selected[0].currentLabel }}
        </el-tag>
        <el-tag
          v-if="selected.length > 1"
          type="info"
          disable-transitions>
            {{ selected.length - 1 }}
        </el-tag>

      <!-- 多选,多个 el-tag 组成 -->
      <transition-group @after-leave="resetInputHeight" v-if="!collapseTags">
        <el-tag
          v-for="item in selected"
          :key="getValueKey(item)"
          type="info"
          disable-transitions>
          {{ item.currentLabel }}
        </el-tag>
      </transition-group>
      <!-- 可输入文本的查询框 -->
      <input
        v-model="query"
        v-if="filterable"
        ref="input">
    </div>
    <!-- 显示结果框 read-only -->
    <el-input
      ref="reference"
      v-model="selectedLabel">
      <!-- 用户显示清空和向下箭头 -->
      <i slot="suffix"></i>
    </el-input>
    <!-- 下拉菜单 -->
    <transition>
      <el-select-menu
        ref="popper"
        v-show="visible && emptyText !== false">
        <el-scrollbar
          tag="ul"
          wrap-class="el-select-dropdown__wrap"
          view-class="el-select-dropdown__list"
          ref="scrollbar"
          v-show="options.length > 0 && !loading">
          <!-- 默认项(创建条目) -->
          <el-option
            :value="query"
            created
            v-if="showNewOption">
          </el-option>
          <!-- 插槽,用于放 option 和 option-group -->
          <slot></slot>
        </el-scrollbar>
        <!-- loading 加载中文本 -->
        <p
          v-if="emptyText &&
            (!allowCreate || loading || (allowCreate && options.length === 0 ))">
          {{ emptyText }}
        </p>
      </el-select-menu>
    </transition>
  </div>
</template>

具体都写在注释中了~从上面内容中可以看到,select 考虑了很多情况,如单选、多选、搜索、下拉框、图标等等。并且使用 slot 插槽来获取开发者传递的 option 和 option-group 组件。
可以发现在 select 中使用了多个外部组件,也就是说 el-select 是由多个组件组装成的一个复杂组件~

  // components
  import ElInput from 'element-ui/packages/input';
  import ElSelectMenu from './select-dropdown.vue';
  import ElOption from './option.vue';
  import ElTag from 'element-ui/packages/tag';
  import ElScrollbar from 'element-ui/packages/scrollbar';

其实只有一个问题,但是这里也将 ListView 常用到的功能点列出来。

图片 1

Form标签的概念和作用

select 要实现的功能

参照官方文档的内容罗列出 select 的一些功能,后面跟上我对功能实现的理解:

  • 单选 —— 点击 select 弹出下拉框,点击 option 完成赋值。
  • 禁用 —— selectoption 都有 disabled 选项用于禁用。
  • 清空 —— 如果 select 中有内容,鼠标悬浮在 input 上显示删除图标,点击执行删除操作。
  • 多选(平铺展示和数字显示数量两种方式) —— 参数 model 变为数组,点击下拉菜单中的选项添加或删除数组中的值。
  • 自定义模板 —— option 中定义了 slot 插槽,默认加了 span 显示内容。可以修改 el-option 标签中内容来自定义模板。
  • 分组 —— 使用 option-group 组件来实现分组效果。
  • 搜索 —— 通过正则匹配搜索项,不符合搜索项的控制 v-show 隐藏
  • 创建条目 —— 在 select 中添加额外 option(一般 option 都是通过 slot 插槽传递的),如允许创建条目,则显示这条 option ,option 的内容显示为查询内容。

渲染数据

'use strict';
import React, {Component} from 'react';
import {
  StyleSheet,
  View,
  ListView,
  Text,
  Image,
  // width and height of screen
  Dimensions
} from 'react-native';
// 获取屏幕宽高
let {height, width} = Dimensions.get('window');
// 从豆瓣api一次获取多少条数据
let pageSize = 5;

export default class ListViewDemo extends Component {
  constructor(props) {
    super(props);
    // 初始化 state
    this.state = {
      loading: true,
      data: [],
      dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 })
    };
  }
  // 
  componentDidMount() {
    // 从豆瓣api获取数据,搜索react相关的书籍
    fetch('https://api.douban.com/v2/book/search?q=react&count=' pageSize)
      .then(res=> {
        if(res.status === 200) {
          // 把得到的字符串转换为对象
          let data = JSON.parse(res._bodyInit).books;
          this.setState({
            loading: false,
            data: data,
            dataSource: this.state.dataSource.cloneWithRows(data)
          });
        }
      })
      .catch(err=> {
        alert(JSON.stringify(err));
      })
  }
  // 单列样式
  _renderRow(row, sectionId, rowId) {
    return (
      <View 
        style = {styles.item}
      >
        <Image
          source = {{uri: row.image}}
          style = {{width: 85, height: 120, marginRight: 20}}
          resizeMode = 'stretch'
        />
        <View style = {{flexDirection: 'column', width: width-150}}>
          <Text style = {{fontSize: 16}}>{row.title}</Text>
          <Text
            numberOfLines = {3}
            style = {{color: '#ccc'}}
          >{row.summary}</Text>
        </View>
      </View>
    )
  }
  render() {
    if(this.state.loading) {
      // 如果正在加载数据,就显示 loading...
      return (
        <View style = {styles.empty}>
          <Text>loading...</Text>
        </View>
      )
    }
    return (
      <View style = {styles.container}>
        <ListView
          dataSource = {this.state.dataSource}
          renderRow = {this._renderRow.bind(this)}
     // 由于屏幕高度一次只够显示4条,所以这里设置为4可以稍微提高性能?
          initialListSize = {4}
          // 隐藏滚动条
          showsVerticalScrollIndicator = {false}
        />
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#eee'
  },
  empty: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  },
  item: {
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderBottomWidth: 1,
    borderStyle: 'solid',
    borderColor: '#ccc',
    flexDirection: 'row'
  }
});

有一点需要注意,由于 _renderRow 是我们自己定义的方法,所以在这个函数里面出现的 this 值需要手动绑定,让 _renderRow 里面的 this 指向我们需要的this。而在 render 函数里面, this 值就是我们需要的,所以this._renderRow.bind(this)就可以了。

如果某个需要绑定 this 值的方法经常用到,可以在构造函数内绑定,这样就不需要每次调用的时候都 .bind(this) 了。

constructor(props){
    //
    this._renderRow = this._renderRow.bind(this);
}

功能:

上节课编写了一个学生分数查询一览页面,需要根据用户输入的条件信息,点击查询按钮后,返回显示不同的数据。

从几个问题去看源码逻辑

增加数据

可以用 onEndReached 属性,值是一个函数,当触底时会执行该函数,在这个函数内获取数据。也可以用
renderFooter 在列表底部渲染一个按钮,点击才加载数据。

这里采用第二种:

// 渲染底部按钮的函数
_renderFooter() {
    if(this.state.loadingMore) {
      return (
        <View
          style = {{paddingVertical: 10, justifyContent: 'center', alignItems: 'center'}}
        >
          <Text>loading...</Text>
        </View>
      )
    }
    return (
      <TouchableOpacity
        onPress = {this.loadMore.bind(this)}
        style = {{paddingVertical: 10, justifyContent: 'center', alignItems: 'center'}}
      >
        <Text>click it load more</Text>
      </TouchableOpacity>
    )
  }

可以看到这里和之前的思路一样,如果正在加载数据,底部按钮就会显示 loading,加载成功后才显示可点击的按钮。

// 获取更多数据的函数
loadMore() {
  // 当点击按钮时,就把底部按钮设置为 loading 状态。
    this.setState({
      loadingMore: true
    });
  // 为了实现分页,**初始化 state 时加上一个属性 index,初始值为2**,因为加载更多的时候就是在加载第二页了
    let start = (this.state.index-1)*pageSize;
    fetch(`https://api.douban.com/v2/book/search?q=react&start=${start}&count=${pageSize}`)
      .then(res=> {
        if(res.status === 200) {
          // parse response
          let response = JSON.parse(res._bodyInit).books;
          if(response.length === 0) {
            alert('no data response');
            return;
          }else {
            let oldAry = [...this.state.data];
            let newAry = [...oldAry, ...response];
            this.setState({
              loading: false,
              loadingMore: false,
              data: newAry,
              dataSource: this.state.dataSource.cloneWithRows(newAry),
       index: this.state.index 1
            });
          }
        }
      })
      .catch(err=> {
        alert(JSON.stringify(err));
      })
  }

_renderRow(row, sectionId, rowId) {
    return (
      <View 
        style = {styles.item}
      >
        <Image
          source = {{uri: row.image}}
          style = {{width: 85, height: 120, marginRight: 20}}
          resizeMode = 'stretch'
        />
        <View style = {{flexDirection: 'column', width: width-150}}>
          <Text style = {{fontSize: 16}}>{row.title}</Text>
          <Text
            numberOfLines = {3}
            style = {{color: '#ccc'}}
          >{row.summary}</Text>
        </View>
      </View>
    )
  }

最后是给 ListView 组件加上 renderFooter 属性

<ListView
          dataSource = {this.state.dataSource}
          renderRow = {this._renderRow.bind(this)}
          initialListSize = {4}
          showsVerticalScrollIndicator = {false}
          renderFooter = {this._renderFooter.bind(this)}
        />

重点是在 loadMore 方法的这部分代码:

    let oldAry = [...this.state.data];
    let newAry = [...oldAry, ...response];

即新获取到的数据,如何放到已有的数组中,this.state.data 是保存这前五条数据的数组这个毫无疑问,接下来因为我们不能直接改变 state(需要使用setState),所以不能用 push、unshift,所以这里是先取到 this.state.data,然后使用 ... 展开,新获取到的数组也展开,一起放到数组中,赋给新变量。

  1. 指定url,支持post或get方式加载数据
  2. 自定义数据查询条件
  3. 列定义时可以对数据项进行简单的转换处理。如:数据源[{ "name": "张三",  "age": 12, "sex": 1 }],渲染时希望把sex的值 1 显示为 男性
  4. 支持多选
  5. 支持分页
  6. 点击行时自动选中,多选时,点击checkbox进行选中和取消选中
  7. 能够返回选中项目的值,多选时返回选中值的数组

先来假设实现一个最简单的功能:假如在姓名检索条件文本输入框里面输入一个"a"字符串,则返回5条数据记录,假如在输入一个"b"字符串,则返回3条数据记录,其它情况则返回1条数据记录。

如何实现基本单选功能?

分析下基本功能:点击 input,显示下拉菜单;鼠标选中一项 option,隐藏下拉菜单;input 中显示选中的结果。
所以这里看下显示内容的 input 都有些什么事件:

      @focus="handleFocus" // 处理 焦点
      @blur="handleBlur" // 处理 焦点 离开
      @keyup.native="debouncedOnInputChange"
      @keydown.native.down.stop.prevent="navigateOptions('next')" // 向下按键,移动到下一个 option
      @keydown.native.up.stop.prevent="navigateOptions('prev')" // 向上按键,移动到上一个 option
      @keydown.native.enter.prevent="selectOption" // 回车按键,选中option
      @keydown.native.esc.stop.prevent="visible = false"  // esc按键,隐藏下拉框
      @keydown.native.tab="visible = false" // tab按键,跳转到下一个文本框,隐藏下拉框
      @paste.native="debouncedOnInputChange" // 
      @mouseenter.native="inputHovering = true" // mouse enter 事件
      @mouseleave.native="inputHovering = false" // mouse leave 事件

从上面的这些事件中可以知道:选中方法为 selectOption(从英文字面意思都能知道~);显示下拉框通过 visible 属性控制;以及其他按键的一些功能。这里主要主要看看 selectOption 方法。

      selectOption() {
        if (!this.visible) {
          this.toggleMenu();
        } else {
          if (this.options[this.hoverIndex]) {
            this.handleOptionSelect(this.options[this.hoverIndex]);
          }
        }
      },

逻辑就是,如果下拉框未显示则执行 toggleMenu 方法触发下拉框,如果已显示下拉框则处理选择 option 的过程。看看这个 toggleMenu 方法:

      toggleMenu() {
        if (!this.selectDisabled) {
          this.visible = !this.visible;
          if (this.visible) {
            (this.$refs.input || this.$refs.reference).focus();
          }
        }
      },

其实就是控制下拉菜单的显示和隐藏。如果显示的时候定焦在 inputreference 上,它们其实就是单选和多选的 input 框(多选 input 定义了 ref="input" 单选 input 定义了 ref="reference")。
至此,下拉菜单的显示与隐藏解决了。然后我们去找 option 点击事件:

      // 处理选项选中事件
      handleOptionSelect(option) {
        if (this.multiple) {
          // 多选
          const value = this.value.slice();
          const optionIndex = this.getValueIndex(value, option.value);
          if (optionIndex > -1) {
            // 已选中,从数组中移除
            value.splice(optionIndex, 1);
          } else if (this.multipleLimit <= 0 || value.length < this.multipleLimit) {
            // 未选中,传入数组
            value.push(option.value);
          }
          this.$emit('input', value);
          this.emitChange(value);
          if (option.created) {
            this.query = '';
            this.handleQueryChange('');
            this.inputLength = 20;
          }
          // 查询
          if (this.filterable) this.$refs.input.focus();
        } else {
          // 单选
          this.$emit('input', option.value);
          this.emitChange(option.value);
          this.visible = false;
        }
        // 渲染完成后
        this.$nextTick(() => {
          this.scrollToOption(option);
          this.setSoftFocus();
        });
      },

处理选中事件考虑了单选和多选两种情况。
如果是多选,检索选中 option 是否在 value 数组中,有则移除、无则添加到 value 数组中。然后 $emit 触发 input 事件,执行 emitChange 方法。如果 option 的 created 为 true,则清空查询内容。
如果是单选,$emit 触发 input 事件将选中值传递给父组件,执行 emitChange 方法,最后隐藏下拉菜单。
最后使用 $nextTick 方法处理下界面。
到这里,选中 option 后下拉菜单消失问题解决,只剩下显示结果到 input 中了。这个显示结果的过程是通过对 visible 属性的监听来完成的(一开始以为在 emitChange 结果发现那只是触发改变事件的)。

      visible(val) {
        // 在下拉菜单隐藏时
        if (!val) {
          // 处理图标
          this.handleIconHide();
          // 广播下拉菜单销毁事件
          this.broadcast('ElSelectDropdown', 'destroyPopper');
          // 取消焦点
          if (this.$refs.input) {
            this.$refs.input.blur();
          }
          // 重置过程
          this.query = '';
          this.previousQuery = null;
          this.selectedLabel = '';
          this.inputLength = 20;
          this.resetHoverIndex();
          this.$nextTick(() => {
            if (this.$refs.input &&
              this.$refs.input.value === '' &&
              this.selected.length === 0) {
              this.currentPlaceholder = this.cachedPlaceHolder;
            }
          });
          // 如果不是多选,进行赋值现在 input 中
          if (!this.multiple) {
            // selected 为当前选中的 option
            if (this.selected) {
              if (this.filterable && this.allowCreate &&
                this.createdSelected && this.createdOption) {
                this.selectedLabel = this.createdLabel;
              } else {
                this.selectedLabel = this.selected.currentLabel;
              }
              // 查询结果
              if (this.filterable) this.query = this.selectedLabel;
            }
          }
        } else {
          // 下拉菜单显示
          // 处理图片显示
          this.handleIconShow();
          // 广播下拉菜单更新事件
          this.broadcast('ElSelectDropdown', 'updatePopper');
          // 处理查询事件
          if (this.filterable) {
            this.query = this.remote ? '' : this.selectedLabel;
            this.handleQueryChange(this.query);
            if (this.multiple) {
              this.$refs.input.focus();
            } else {
              if (!this.remote) {
                this.broadcast('ElOption', 'queryChange', '');
                this.broadcast('ElOptionGroup', 'queryChange');
              }
              this.broadcast('ElInput', 'inputSelect');
            }
          }
        }
        // 触发 visible-change 事件
        this.$emit('visible-change', val);
      },

从 template 中可知,显示结果的 input 绑定的 v-modelselectedLabel,而 select 是通过获取下拉菜单的显示与隐藏事件来执行结果显示部分的功能的。最终 selectedLabel 获得到了选中的 option 的 label 内容。
这样,从 点击-单选-显示 的流程就实现了。还是很简单的。

删除数据

首先是用来删除的方法:

delete(row) {
    let oldAry = [...this.state.data];
    let index = oldAry.indexOf(row);
    oldAry.splice(index, 1);
    let newAry = oldAry;
    this.setState({
      data: newAry,
      dataSource: this.state.dataSource.cloneWithRows(newAry)
    });
  }

传入 row 作为参数,查询到长按的item在数组中的序列,然后删除,把删除后的数组赋给一个新变量。
可能

let newAry = [
    ...oldAry.slice(0, index),
    ...oldAry.slice(index 1)
];

这样会更好?暂时都可以,然后添加上长按事件,给每一行添加长按事件:

_renderRow(row) {
    return (
      <TouchableOpacity 
        style = {styles.item}
        onLongPress = {this.delete.bind(this, row)}
      >
        <Image
          source = {{uri: row.image}}
          style = {{width: 85, height: 120, marginRight: 20}}
          resizeMode = 'stretch'
        />
        <View style = {{flexDirection: 'column', width: width-150}}>
          <Text style = {{fontSize: 16}}>{row.title}</Text>
          <Text
            numberOfLines = {3}
            style = {{color: '#ccc'}}
          >{row.summary}</Text>
        </View>
      </TouchableOpacity>
    )
  }

就可以了,这里要注意的是,需要传入一个参数。

根据个人的习惯和对功能的分解,定义了这样一个setting:

为了实现这个最简单的功能,需要用到Form标签。

如何实现多选,多选选中后 option 右侧的勾以及 input 中的 tag 如何显示?

关于多选,在刚才讲单选的时候提及了一些了。所以有些代码就不贴出浪费篇幅了。具体逻辑如下:
先点击 input 执行 selectOption 方法显示下拉菜单,然后点击下拉菜单中的 option,执行 handleOptionSelect 方法将 option 的值都传给 value 数组。此时 value 数组改变,触发 watch 中的 value 变化监听方法。

      value(val) {
        // 多选
        if (this.multiple) {
          this.resetInputHeight();
          if (val.length > 0 || (this.$refs.input && this.query !== '')) {
            this.currentPlaceholder = '';
          } else {
            this.currentPlaceholder = this.cachedPlaceHolder;
          }
          if (this.filterable && !this.reserveKeyword) {
            this.query = '';
            this.handleQueryChange(this.query);
          }
        }
        this.setSelected();
        // 非多选查询
        if (this.filterable && !this.multiple) {
          this.inputLength = 20;
        }
      },

以上代码关键是执行了 setSelected 方法:

      // 设置选择项
      setSelected() {
        // 单选
        if (!this.multiple) {
          let option = this.getOption(this.value);
          // created 是指创建出来的 option,这里指 allow-create 创建的 option 项
          if (option.created) {
            this.createdLabel = option.currentLabel;
            this.createdSelected = true;
          } else {
            this.createdSelected = false;
          }
          this.selectedLabel = option.currentLabel;
          this.selected = option;
          if (this.filterable) this.query = this.selectedLabel;
          return;
        }
        // 遍历获取 option
        let result = [];
        if (Array.isArray(this.value)) {
          this.value.forEach(value => {
            result.push(this.getOption(value));
          });
        }
        // 赋值
        this.selected = result;
        this.$nextTick(() => {
          // 重置 input 高度
          this.resetInputHeight();
        });
      },

可以看到如果是多选,那么将 value 数组遍历,获取相应的 option 值,传给 selected。而多选界面其实就是对于这个 selected 的 v-for 遍历显示。显示的标签使用的是 element 的另外一个组件 el-tag

        <el-tag
          v-for="item in selected"
          :key="getValueKey(item)">
          {{ item.currentLabel }}
        </el-tag>

这里顺便提一句: option 的 created 参数用于标识是 select 组件中创建的那个用于创建条目的 option。而从 slot 插槽传入的 option 是不用传 created 参数的。

选中状态

上面的实现都还算顺利,在实现选中状态,也就是多选功能时踩到了坑。功能点是:一个列表支持多选,然后可以批量删除。先实现选中功能。

选中功能可以用背景色或者图标来区分选中与未选,有两种思路,

  • 一个全局数组,选中后将row放到这个数组中,判断row是否在这个数组中来表示是否选中。
  • 添加一个字段,这个字段标识是否被选中。
var list = $('#list').GridView({
            'apiUrl': '/Student/List',  // 指定数据请求的URL路径
            'apiType': 'post',  // 请求的方式
            'columns': [    // 要显示的列,title对应表头,column对应数据项的属性名称,width指定列的宽度,func指定绑定时调用的函数名称
                { title: '姓名', column: 'Name', width: 160 },
                { title: '年龄', column: 'Age' },
                { title: '性别', column: 'Sex', width: 100, 'func': 'convertToSex' }
            ],
            'valueColumn': 'StudentId', // data-value 取值的属性名
            'pageSize': 20, // 每页显示的数量
            'isMultiy': false,  // 是否支持多选
            'isTreeView': false,   // 是否支持树形
            'pager': 'pager',   // 指定包含分页的divid,主要是为了能单独控制pager中的一些数据,把pager给拆出来了,后来发现似乎用处不大
            'onRowClick': function(id) { }, // 当数据行被点击时执行的回调,参数是tr中的data-value
            'convertSource': function (data) { return data.body; }, // 使用数据源之前对数据进行转换。因为我的api返回的都是{ code: 200, body: [] }这种类型,需要在这里直接返回body
            'onDataBindComplete': function() {}, // 当数据加载完成,也就是列表渲染完成后的回调。比如说提醒用户加载完成之类的
            'getSearchData': function() { return $('#form1').serialize(); },    // 获取查询参数,这个很重要,想了很多办法,最终采用了这种方案,在查询前运行这个函数,将返回值作为ajax的查询参数
            'listCssClass': 'table',    // 列表table的样式名
            'pagerCssClass': 'pager',   // 分页最外面div的样式名
            'beforeSend': function() { }    // ajax请求之前调用的函数,原本是为了提醒一下加载已开始,请稍后之类的,现在没怎么用到
        });

Form标签元素称之为HTML表单,可以用于收集用户输入信息。

如何实现搜索功能?

从 template 中可知,select 有两个 input,一个用于显示结果,一个则用于查询搜索。我们来看下搜索内容的 input 文本框如何实现搜索功能:
在 input 中有 @input="e => handleQueryChange(e.target.value)"这么一段代码。所以,handleQueryChange 方法就是关键所在了。

      // 处理查询改变
      handleQueryChange(val) {
        if (this.previousQuery === val) return;
        if (
          this.previousQuery === null &&
          (typeof this.filterMethod === 'function' || typeof this.remoteMethod === 'function')
        ) {
          this.previousQuery = val;
          return;
        }
        this.previousQuery = val;
        this.$nextTick(() => {
          if (this.visible) this.broadcast('ElSelectDropdown', 'updatePopper');
        });
        this.hoverIndex = -1;
        if (this.multiple && this.filterable) {
          const length = this.$refs.input.value.length * 15   20;
          this.inputLength = this.collapseTags ? Math.min(50, length) : length;
          this.managePlaceholder();
          this.resetInputHeight();
        }
        if (this.remote && typeof this.remoteMethod === 'function') {
          this.hoverIndex = -1;
          this.remoteMethod(val);
        } else if (typeof this.filterMethod === 'function') {
          this.filterMethod(val);
          this.broadcast('ElOptionGroup', 'queryChange');
        } else {
          this.filteredOptionsCount = this.optionsCount;
          this.broadcast('ElOption', 'queryChange', val);
          this.broadcast('ElOptionGroup', 'queryChange');
        }
        if (this.defaultFirstOption && (this.filterable || this.remote) && this.filteredOptionsCount) {
          this.checkDefaultFirstOption();
        }
      },

其中,remoteMethodfilterMethod 方法是自定义的远程查询和本地过滤方法。如果没有自定义的这两个方法,则会触发广播给 optionoption-group 组件 queryChange 方法。

      // option.vue
      queryChange(query) {
        let parsedQuery = String(query).replace(/(^|(|)|[|]|$|*| |.|?|\|{|}||)/g, '\$1');
        // 匹配字符决定是否显示当前option
        this.visible = new RegExp(parsedQuery, 'i').test(this.currentLabel) || this.created;
        if (!this.visible) {
          this.select.filteredOptionsCount--;
        }
      }

option 中通过正则匹配决定是否隐藏当前 option 组件,而 option-group 通过获取子组件,判断如果有子组件是可见的则显示,否则隐藏。

      // option-group.vue
      queryChange() {
        this.visible = this.$children &&
          Array.isArray(this.$children) &&
          this.$children.some(option => option.visible === true);
      }

所以,其实 option 和 option-group 在搜索的时候只是隐藏掉了不匹配的内容而已。

全局数组

先说明,这种方式行不通,或者说很难实现,下面是一步一步尝试到无法实现。
先给初始化 state 添加一个 selectedAry: [],用来保存选中的 row。然后是 choose 事件:

choose(row) {
    // 
    let oldSeletedAry = [...this.state.seletedAry];
    let index = oldSeletedAry.indexOf(row);
    let newSeletedAry = [];
    if(index > -1) {
      // is exist
      oldSeletedAry.splice(index, 1);
      newSeletedAry = oldSeletedAry;
    }else {
      newSeletedAry = [...oldSeletedAry, row];
    }
    this.setState({
      seletedAry: newSeletedAry
    });
  }

如果点击的这个row已经存在被选择数组中,就移除,思路和长按删除一样;否则就是添加到被选择数组中。
然后给每一行添加上点击事件,就在 onLongPress 下面加上

onPress = {this.choose.bind(this, row)}

然后找个地方放这个数组的长度,这样就能很直观看到选择的数量。
在 ListView 组件上方放一个 Text 组件。

<Text>{this.state.seletedAry.length '/' this.state.data.length}</Text>

看看效果,的确是能够正确显示出数量了,还需要给一个状态来区分出是否被选择。在每一行添加一个 Text 组件,如果没有被选择,就显示 NO,如果被选择了,就显示 YES。

// 先判断在不在
let index = this.state.seletedAry.indexOf(row);
// 然后是显示是否被选中,在之前的Text 组件下面再添加一个组件
// ...
<Text>{index > -1 ? 'YES' : 'NO'}</Text>

OK,页面能正确显示出 NO 了,点击一下,问题来了,被选择数组的长度 1,但是 NO 并没有变成 YES。原因在于,我们做的这些操作,都没有触发到 ListView 重新渲染,setState 的确会触发 render 函数,但是却不会触发 _renderRow 函数啊,所以这里没有变化。OK,问题既然找到了,解决办法就是在 choose 函数内修改 dataSource,以触发 _renderRow 。

choose(row) {
    // 
    let oldSeletedAry = [...this.state.seletedAry];
    let index = oldSeletedAry.indexOf(row);
    let newSeletedAry = [];
    if(index > -1) {
      // is exist
      oldSeletedAry.splice(index, 1);
      newSeletedAry = oldSeletedAry;
    }else {
      newSeletedAry = [...oldSeletedAry, row];
    }
    let oldAry = [...this.state.data];
    let newAry = oldAry;
    this.setState({
      seletedAry: newSeletedAry,
      data: newAry,
      dataSource: this.state.dataSource.cloneWithRows(newAry)
    });
  }

拿到旧数组,赋给新数组,这样OK吗,答案是NO。这个地方我的理解是,虽然有新数组了,但是数组里面的对象还是原来的对象,内存地址并没有变化。
举个例子

let ary = [{
  name: 'ltaoo'
}, {
  name: 'ltooo'
}];

let newAry = [...ary];

newAry[0].name = 'loooo';

console.log(ary);

虽然看起来是修改了 newAry 里面对象的值,实际上原数组 ary 的值也发生了改变。所以我们知道了 ListView 判断数组是否发生了改变,要看里面的元素是否发生了变化,而对象需要内存地址不同,才算发生了变化。

那这里如何解决呢?答案是把对象每个都拷贝:

let oldAry = [...this.state.data];
    let newAry = oldAry.map(item=> {
      return Object.assign({}, item);
    });

虽然的确是触发了 _renderRow 函数(在_renderRow开始alert可以判断是否触发),但是NO 还是 NO,因为 index 的确是 -1 ,经过拷贝后,每一次看到的数组,和上一次都是不同的,即使如果先拷贝,再放到 selectedAry 中去,只能实现只有一个是 YES,点击了另外的,另一个变为 YES,之前为 YES 的变为了 NO。可以仔细思考为什么,这里放出最后挣扎的代码:

choose(row) {
    let oldSeletedAry = [...this.state.seletedAry];
    let newSeletedAry = [];
    // 
    let oldAry = [...this.state.data];
    let newAry = oldAry.map(item=> {
      let newItem = Object.assign({}, item);
      if(item===row) {
        let index = oldSeletedAry.indexOf(item);
        if(index > -1) {
          // 存在原数组中
          oldSeletedAry.splice(index, 1);
          newSeletedAry = oldSeletedAry;
        }else {
          newSeletedAry = [...oldSeletedAry, newItem];
        }
      }
      return newItem;
    });


    this.setState({
      seletedAry: newSeletedAry,
      data: newAry,
      dataSource: this.state.dataSource.cloneWithRows(newAry)
    });
  }

OK,接下来就是这个扩展的具体实现了,代码很多,但思路很简单,主要就是代码的拼接。大家自己看吧!

在3-4节里面已经介绍过:

下拉菜单的显示和隐藏效果是如何实现的?下拉菜单本质是什么东西?

下拉菜单是通过 transition 来实现过渡动画的。
下拉菜单 el-select-menu 本质上就是一个 div 容器而已。

  <div
    class="el-select-dropdown el-popper"
    :class="[{ 'is-multiple': $parent.multiple }, popperClass]"
    :style="{ minWidth: minWidth }">
    <slot></slot>
  </div>

另外,在代码中经常出现的通知下拉菜单显示和隐藏的广播在 el-select-menumounted 方法中接收使用:

    mounted() {
      this.referenceElm = this.$parent.$refs.reference.$el;
      this.$parent.popperElm = this.popperElm = this.$el;
      this.$on('updatePopper', () => {
        if (this.$parent.visible) this.updatePopper();
      });
      this.$on('destroyPopper', this.destroyPopper);
    }

新增字段

在从接口返回数据后,手动添加上一个字段来标识是否被选中。

data = data.map(item=> {
            item.isCheck = false;
            return item;
          });

然后页面会根据这个字段显示 YES 或者 NO

// 点击选中
choose(row) {
    let oldAry = [...this.state.data];
    let index = oldAry.indexOf(row);
    // 对旧数据中的值进行更新
    let newRow = Object.assign({}, row, {
        isCheck: !row.isCheck
    });
    let newAry = [
      ...oldAry.slice(0, index),
      newRow,
      ...oldAry.slice(index 1)
    ];
    this.setState({
      data: newAry,
      dataSource: this.state.dataSource.cloneWithRows(newAry)
    });
  }

判断是否在 selectedAry 数组内可以改成直接判断 row.isCheck

<Text>{row.isCheck ? 'YES' : 'NO'}</Text>

不过显示已选数量还是需要加上,不过不再使用这个数组来做判断。最终的 choose 代码:

choose(row) {
    let oldAry = [...this.state.data];
    let index = oldAry.indexOf(row);
    // 对旧数据中的值进行更新
    let newRow = Object.assign({}, row, {
        isCheck: !row.isCheck
    });
    let newAry = [
      ...oldAry.slice(0, index),
      newRow,
      ...oldAry.slice(index 1)
    ];

    let oldSelectedAry = [...this.state.seletedAry];
    let newSelectedAry = [];
    let seletedIndex = oldSelectedAry.indexOf(row);
    if(seletedIndex > -1) {
      oldSelectedAry.splice(seletedIndex, 1);
      newSelectedAry = oldSelectedAry;
    }else {
      newSelectedAry = [...oldSelectedAry, newRow];
    }
    this.setState({
      data: newAry,
      dataSource: this.state.dataSource.cloneWithRows(newAry),
      seletedAry: newSelectedAry
    });
  }
(function ($) {

    $.fn.GridView = function (settings) {
        // 系统变量
        var self = this;
        self.selected = [];
        var pageindex = 1, pageSize = 20;

        if (settings.pageSize && $.isNumeric(settings.pageSize) && settings.pageSize > 0) {
            pageSize = settings.pageSize;
        }

        var rand = Math.floor(Math.random() * 1000);
        var table = $("<table"   (settings.listCssClass ? " class='"   settings.listCssClass   "'" : "")   " id='myList"   rand   "'></table>");

        var colgroup = colgroup2 = "<colgroup>"
        $.each(settings.columns, function (idx, item) {
            colgroup  = "<col"   item.width ? " style='width:"   item.width   "px;'" : ""   ">";
            colgroup2  = "<col"   item.width ? " style='width:"   item.width   "px;'" : ""   ">";
        });
        colgroup  = "<col style='width:18px;'></col>";
        colgroup  = "</colgroup>";
        colgroup2  = "</colgroup>";
        var table = $("<table"   (settings.listCssClass ? " class='"   settings.listCssClass   "'" : "")   " id='myList"   rand   "'></table>");

        var pagerHtml = '<div'   (!settings.pagerCssClass ? '' : ' class="'   settings.pagerCssClass   '"')   '>';
        //var pagerHtml = '';
        pagerHtml  = '<a class="disabled" id="first'   rand   '"><i class="fa fa-fast-backward"></i></a>';
        pagerHtml  = '<a class="disabled" id="prev'   rand   '"><i class="fa fa-backward"></i></a>';
        pagerHtml  = '<div class="pager-index"><b>第</b><input value="1" type="text" maxlength="4" id="index'   rand   '"><b>页</b></div>';
        pagerHtml  = '<a class="disabled" id="next'   rand   '"><i class="fa fa-forward"></i></a>';
        pagerHtml  = '<a class="disabled" id="last'   rand   '"><i class="fa fa-fast-forward"></i></a>';
        pagerHtml  = '<div class="pager-info2"> 共计 0 条记录,';
        pagerHtml  = '每页显示 '   pageSize   ' 条,';
        pagerHtml  = '共 1 页,';
        pagerHtml  = '用时 0毫秒';
        pagerHtml  = "</div></div>";
        var pagerDom = $(pagerHtml);
        var firstButton, prevButton, nextButton, lastButton, currentSpan, pageSizeSpan, totalCountSpan, totalPagesSpan, loadTimeSpan;

        // 临时变量
        var tbody, pager, cbAll, checkboxes, isPager = false, totalPage = 1;

        // 创建table
        self.append(table);

        // 创建thead
        addChildrenToTable();

        // 为tbody赋值
        tbody = $("#tbody"   rand);

        var colCount = settings.columns.length   2;
        tbody.html("<tr class='empty'><td colspan='"   colCount   "'>等待加载数据...</td></tr>");

        // 创建分页
        if (settings.pager && $("#"   settings.pager)[0]) {
            pager = $("#"   settings.pager);
            isPager = true;
            pager.append(pagerDom);
            setPagerButtonEvent();
        }

        // 创建渲染函数
        self.fun = new Function("data", renderFunString());

        // 渲染第一列
        self.setFirstCol = function (val) {
            if (settings.isMulti) {
                return "<td class='chk'><input type='checkbox' id='cb"   val   "' value='"   val   "'></td>"
            } else {
                return "<td class='no'>"   val   "</td>";
            }
        }

        // 渲染最后一列
        self.setLastCol = function () {
            return "<td></td>";
        }

        // 渲染中间列
        self.setCol = function (content, width, cssClass, level) {
            var html = "<td";
            html  = width ? " style='width:"   width   "px'" : "";
            html  = cssClass && cssClass != 'undefined' ? " class='"   cssClass   "'" : "";
            html  = ">";
            if (settings.isTreeView) {
                html  = level ? ""   (level > 0 ? "|—" : "")   "" : "";
            }
            html  = content && content != "undefined" ? content : "";
            html  = "</td>";
            return html;
        }

        // 显示指定页码的数据
        self.show = function (index, type) {
            self.selected = [];
            if (!settings.apiUrl) {
                return;
            }

            if (!type || type.toLowerCase() != "post") {
                $.get(settings.apiUrl, getAjaxData(), function (data) {
                    var source = [];
                    if (settings.convertSource && $.isFunction(settings.convertSource)) {
                        source = settings.convertSource(data);
                    } else {
                        source = data;
                    }
                    addRowsToTbody(source);
                });
            } else {
                $.post(settings.apiUrl, getAjaxData(), function (data) {
                    var source = [];
                    if (settings.convertSource && $.isFunction(settings.convertSource)) {
                        source = settings.convertSource(data);
                    } else {
                        source = data;
                    }
                    addRowsToTbody(source);
                });
            }
        };

        // 获取选中的ID
        self.getSelectedId = function () {
            if (self.selected.length == 0) {
                return null;
            } else {
                return self.selected[0];
            }
        };

        self.getSelectedIds = function () {
            return self.selected;
        };

        self.clear = function () {
            self.selected = [];
            $("tr", "#myList"   rand).removeClass("selected");
            $(":checkbox", "#myList"   rand).prop("checked", false);
        };

        // 为table内的元素绑定事件
        tbody.on("click", "tr", function () {
            if ($(this).hasClass("empty")) return;
            $("tr", tbody).removeClass("selected").find(":checkbox").prop("checked", false);
            $(this).addClass("selected").find(":checkbox").prop("checked", true);
            $("#cbAll"   rand).prop("checked", false);
            self.selected = [$(this).data("value")];
        });

        // 绑定复选框点击事件
        if (settings.isMulti) {
            cbAll = $("#cbAll"   rand);

            cbAll.on("click", function () {
                if (!checkboxes) checkboxes = $(":checkbox", tbody);

                if ($(this).prop("checked")) {
                    self.selected = [];

                    $.each(checkboxes, function (idx, item) {
                        $(this).prop("checked", true);
                        self.selected.push($(this).val());

                        var tr = $("#tr"   $(item).val());
                        if (!tr.hasClass("selected")) tr.addClass("selected");
                    });
                } else {
                    checkboxes.prop("checked", false);
                    $("tr", tbody).removeClass("selected");
                    self.selected = [];
                }
            });

            tbody.on("click", ":checkbox", function (event) {
                event.stopPropagation();

                if ($(this).prop("checked")) {
                    $("#tr"   $(this).val()).addClass("selected");
                } else {
                    $("#tr"   $(this).val()).removeClass("selected");
                }
                if (!checkboxes) {
                    checkboxes = $(":checkbox", tbody);
                }

                self.selected = [];

                var unCheckedCount = 0;

                $.each(checkboxes, function () {
                    if ($(this).prop("checked")) {
                        self.selected.push($(this).val());
                    } else {
                        unCheckedCount  ;
                    }
                });

                if (unCheckedCount > 0) {
                    cbAll.prop("checked", false);
                } else {
                    cbAll.prop("checked", true);
                }
            });
        }

        // 返回渲染函数的程序体
        function renderFunString() {
            var funString = "var self = this; var html = ''; $.each(data, function(idx, item) { var val = ";
            funString  = (settings.valueColumn ? "item."   settings.valueColumn : "idx");
            funString  = "; html  = '<tr id="tr'   val   '" data-value="'   val   '">'; html  = self.setFirstCol(val);";
            var level = null;
            if (settings.levelColumn) {
                level = settings.levelColumn;
            }
            $.each(settings.columns, function (idx, item) {
                if (item.func) {
                    funString  = " html  = self.setCol( "   item.func   "(item), "   item.width   ", '"   item.cssClass   "'"   (level ? " , item."   level : "")   " );";
                } else {
                    funString  = " html  = self.setCol( item."   item.column   ", "   item.width   ", '"   item.cssClass   "'"   (level ? " , item."   level : "")   " );";
                }
            });
            funString  = " html  = self.setLastCol(); html  = '</tr>'; idx  ; }); return html;";
            return funString;
        }

        // 将数据生成html,并插入到tbody中
        function addRowsToTbody(data) {
            if (data && data.body && data.body.length > 0) {
                var html = self.fun(data.body);
                tbody.html(html);

                if (isPager) {
                    setPagerButton(pageSize, pageindex, data.totalCount, new Date().getTime());
                }
            } else {
                var colCount = settings.columns.length   2;
                tbody.html("<tr class='empty'><td colspan='"   colCount   "'>请求的数据为空</td></tr>");
            }
        }

        // 创建table
        function addChildrenToTable() {
            var body = "<thead>"
            if (settings.columns) {
                if (settings.isMulti) {
                    body  = "<th class='chk'><input type='checkbox' id='cbAll"   rand   "' /></th>";
                } else {
                    body  = "<th class='no'></th>";
                }

                $.each(settings.columns, function (idx, col) {
                    body  = "<th>"   col.title   "</th>";
                });
                body  = "<th></th>";
                body  = "</thead><tbody id='tbody"   rand   "'></tbody>";

                table.append($(body));
            }
        }

        // 绑定分页按钮的点击事件
        function setPagerButtonEvent() {
            firstButton = $("#first"   rand);
            prevButton = $("#prev"   rand);
            nextButton = $("#next"   rand);
            lastButton = $("#last"   rand);
            currentSpan = $("#index"   rand);

            pageSizeSpan = $("#size"   rand);;
            totalCountSpan = $("#total"   rand);;
            totalPagesSpan = $("#page"   rand);;
            loadTimeSpan = $("#time"   rand);;

            firstButton.on("click", function () {
                if (!$(this).hasClass("disabled")) {
                    pageindex = 1;
                    self.show();
                }
            });

            prevButton.on("click", function () {
                if (!$(this).hasClass("disabled")) {
                    pageindex -= 1;
                    pageindex = pageindex <= 0 ? 1 : pageindex;
                    self.show();
                }
            });

            nextButton.on("click", function () {
                if (!$(this).hasClass("disabled")) {
                    pageindex  = 1;
                    self.show();
                }
            });

            lastButton.on("click", function () {
                if (!$(this).hasClass("disabled")) {
                    pageindex = totalPage;
                    self.show();
                }
            });

            currentSpan.on("change", function () {
                var nc = Number($(this).val());
                if (nc && nc <= totalPage && nc > 0) {
                    pageindex = nc;
                    self.show();
                } else {
                    $(this).val(pageindex);
                }
            });
        }

        // 配置 Pager 按钮
        function setPagerButton(size, index, total, start) {
            if (total == 0) {
                pager.hide();
            } else {
                pager.show();
            }

            // 总页数
            var pages = Math.ceil(total / size);
            pages = (pages == 0 ? 1 : pages);
            totalPage = pages;

            if (pages == 1) {
                if (!firstButton.hasClass("disabled")) {
                    firstButton.addClass("disabled");
                }
                if (!prevButton.hasClass("disabled")) {
                    prevButton.addClass("disabled");
                }
                if (!nextButton.hasClass("disabled")) {
                    nextButton.addClass("disabled");
                }
                if (!lastButton.hasClass("disabled")) {
                    lastButton.addClass("disabled");
                }
            } else {
                if (index == 1) {
                    if (!firstButton.hasClass("disabled")) {
                        firstButton.addClass("disabled");
                    }
                    if (!prevButton.hasClass("disabled")) {
                        prevButton.addClass("disabled");
                    }
                } else {
                    if (firstButton.hasClass("disabled")) {
                        firstButton.removeClass("disabled");
                    }
                    if (prevButton.hasClass("disabled")) {
                        prevButton.removeClass("disabled");
                    }
                }
                currentSpan.val(index);
                if (index == pages) {
                    if (!nextButton.hasClass("disabled")) {
                        nextButton.addClass("disabled");
                    }
                    if (!lastButton.hasClass("disabled")) {
                        lastButton.addClass("disabled");
                    }
                } else {
                    if (nextButton.hasClass("disabled")) {
                        nextButton.removeClass("disabled");
                    }
                    if (lastButton.hasClass("disabled")) {
                        lastButton.removeClass("disabled");
                    }
                }
            }
            totalCountSpan.text(total);
            pageSizeSpan.text(this.pageSize);
            totalPagesSpan.text(pages);
            loadTimeSpan.text((new Date().getTime() - start));
        }

        // 获取ajax的查询参数
        function getAjaxData() {
            var param;
            if ($.isFunction(settings.getSearchData)) {
                param = settings.getSearchData();
            }
            if (isPager) {
                if ($.isArray(param)) {                                             // $("form").serializationArray()
                    param.push({ "name": "pageSize", "value": pageSize });
                    param.push({ "name": "pageIndex", "value": pageindex });
                } else if ($.isPlainObject(param)) {                                 // 自定义查询对象
                    $.extend(true, param, { "pageSize": pageSize, "pageIndex": pageindex });
                } else {                                                             // $("form").serialization()
                    param = (param ? param   "&" : "")   "pageSize="   pageSize   "&pageIndex="   pageindex;
                }
            }
            return !!param ? param : {};
        }

        return self;
    }

})(jQuery);

/*
 *使用范例:
 *==========================数据格式============================
 {
  "code": 200,
  "describe": "",
  "totalCount": 3,
  "body": [
    {
      "no": 1,
      "name": "王五",
      "family": {
        "father": "王老五",
        "mother": "陈静蓉"
      }
    },
    {
      "no": 2,
      "name": "张三",
      "family": {
        "father": "张作霖",
        "mother": "李培芳"
      }
    },
    {
      "no": 3,
      "name": "李四",
      "family": {
        "father": "李宗仁",
        "mother": "江少芬"
      }
    }
  ]
 }

 *==========================页面调用============================
    <script src="jquery-1.10.2.js"></script>
    <script src="myGrid.js"></script>
    <script>
        var list = $("#list").myGrid({
            apiUrl: "data.json",
            isMulti: false,
            isTree: true,
            cols: [
                { col: "no", width: 120, title: "编号", cssClass: "chk", level: 0 },
                { col: "name", width: 120, title: "姓名", level: 0 },
                { col: "family.father", width: 120, title: "父亲", level: 1, func: "addFix" }
            ],
            valueCol: "no",
            pager: "pager",
            pageSize: 2,
            cssClass: "default-list-table",
            convertSource: function(data) {
                return data;
            }
        });

        // 这个是用来转换数据的方法
        function addFix(obj) {
            return "000"   obj;
        }
        list.show();
    </script>

一般使用form标签来包含各种输入标签元素,并用于提交给后台程序。

创建条目如何实现?

上文中提到过,就是在 select 中默认藏了一条 option,当创建条目时显示这个 option 并显示创建内容。点击这个 option 就可以把创建的内容添加到显示结果的 input 上了。

总结

数组内如果是对象,要修改对象某个属性的值,一定要使用拷贝,才能够触发_renderRow 函数,重新渲染列表。这个了解了,listView 其实就没什么难点了。这里的选中状态功能,可以拓展出修改列表中的值的功能点。

一个修改的操作:

form标签内部可以使用不同类型的表单元素来输入不同类型的数据。

如何展示远程数据?

通过为 select 设置 remoteremote-method 属性来获取远程数据。remote-method 方法最终将数据赋值给 option 的 v-model 绑定数组数据将结果显示出来即可。

真总结

放上最终的完整代码:

'use strict';
import React, {Component} from 'react';
import {
  StyleSheet,
  View,
  ListView,
  Text,
  Image,
  // width and height of screen
  Dimensions,
  TouchableOpacity
} from 'react-native';

let {height, width} = Dimensions.get('window');
// 从豆瓣api一次获取多少条数据
let pageSize = 5;
export default class ListViewDemo extends Component {
  constructor(props) {
    super(props);
    // 初始化 state
    this.state = {
      loading: true,
      data: [],
      dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }),
      index: 2,
      seletedAry: []
    };
  }

  // 
  componentDidMount() {
    // 从豆瓣api获取数据,搜索react相关的书籍
    fetch('https://api.douban.com/v2/book/search?q=react&count=' pageSize)
      .then(res=> {
        if(res.status === 200) {
          // parse response
          let data = JSON.parse(res._bodyInit).books;
          this.setState({
            loading: false,
            data: data,
            dataSource: this.state.dataSource.cloneWithRows(data)
          });
        }
      })
      .catch(err=> {
        alert(JSON.stringify(err));
      })
  }

  // 单列样式
  _renderRow(row, sectionId, rowId) {
    return (
      <TouchableOpacity 
        style = {styles.item}
        onLongPress = {this.delete.bind(this, row)}
        onPress = {this.choose.bind(this, row)}
      >
        <Image
          source = {{uri: row.image}}
          style = {{width: 85, height: 120, marginRight: 20}}
          resizeMode = 'stretch'
        />
        <View style = {{flexDirection: 'column', width: width-150}}>
          <Text style = {{fontSize: 16}}>{row.title}</Text>
          <Text
            numberOfLines = {3}
            style = {{color: '#ccc'}}
          >{row.summary}</Text>
          <Text>{row.isCheck ? 'YES' : 'NO'}</Text>
        </View>
      </TouchableOpacity>
    )
  }

  loadMore() {
  // 当点击按钮时,就把底部按钮设置为 loading 状态。
    this.setState({
      loadingMore: true
    });

  // 为了实现分页,初始化 state 时加上一个属性 index,初始值为2,因为加载更多的时候就是在加载第二页了
    let start = (this.state.index-1)*pageSize;
    fetch(`https://api.douban.com/v2/book/search?q=react&start=${start}&count=${pageSize}`)
      .then(res=> {
        if(res.status === 200) {
          // parse response
          let response = JSON.parse(res._bodyInit).books;
          if(response.length === 0) {
            alert('no data response');
            return;
          }else {
            let oldAry = [...this.state.data];
            let newAry = [...oldAry, ...response];
            this.setState({
              loading: false,
              loadingMore: false,
              data: newAry,
              dataSource: this.state.dataSource.cloneWithRows(newAry),
              index: this.state.index 1
            });
          }
        }
      })
      .catch(err=> {
        alert(JSON.stringify(err));
      })
  }
  _renderFooter() {
    if(this.state.loadingMore) {
      return (
        <View
          style = {{paddingVertical: 10, justifyContent: 'center', alignItems: 'center'}}
        >
          <Text>loading...</Text>
        </View>
      )
    }
    return (
      <TouchableOpacity
        onPress = {this.loadMore.bind(this)}
        style = {{paddingVertical: 10, justifyContent: 'center', alignItems: 'center'}}
      >
        <Text>click it load more</Text>
      </TouchableOpacity>
    )
  }
  delete(row) {
    let oldAry = [...this.state.data];
    let index = oldAry.indexOf(row);
    oldAry.splice(index, 1);
    let newAry = oldAry;
    this.setState({
      data: newAry,
      dataSource: this.state.dataSource.cloneWithRows(newAry)
    });
  }
  choose(row) {
    let oldAry = [...this.state.data];
    let index = oldAry.indexOf(row);
    // 对旧数据中的值进行更新
    let newRow = Object.assign({}, row, {
        isCheck: !row.isCheck
    });
    let newAry = [
      ...oldAry.slice(0, index),
      newRow,
      ...oldAry.slice(index 1)
    ];
    let oldSelectedAry = [...this.state.seletedAry];
    let newSelectedAry = [];
    let seletedIndex = oldSelectedAry.indexOf(row);
    if(seletedIndex > -1) {
      oldSelectedAry.splice(seletedIndex, 1);
      newSelectedAry = oldSelectedAry;
    }else {
      newSelectedAry = [...oldSelectedAry, newRow];
    }
    this.setState({
      data: newAry,
      dataSource: this.state.dataSource.cloneWithRows(newAry),
      seletedAry: newSelectedAry
    });
  }
  render() {
    if(this.state.loading) {
      // if is loading
      return (
        <View style = {styles.empty}>
          <Text>loading...</Text>
        </View>
      )
    }
    return (
      <View style = {styles.container}>
        <Text>{this.state.seletedAry.length '/' this.state.data.length}</Text>
        <ListView
          dataSource = {this.state.dataSource}
          renderRow = {this._renderRow.bind(this)}
          initialListSize = {4}
          // hidden scroller
          showsVerticalScrollIndicator = {false}
          renderFooter = {this._renderFooter.bind(this)}
        />
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#eee'
  },
  empty: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  },
  item: {
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderBottomWidth: 1,
    borderStyle: 'solid',
    borderColor: '#ccc',
    flexDirection: 'row'
  }
});
  • github 地址

图片 2

例如输入元素、复选元素、单选元素、下拉框选择元素、提交按钮等等。

清空按钮显示和点击事件呢?

在显示结果的 input 文本框中有一个 <i> 标签,用于显示图标。

      <!-- 用户显示清空和向下箭头 -->
      <i slot="suffix"
       :class="['el-select__caret', 'el-input__icon', 'el-icon-'   iconClass]"
       @click="handleIconClick"
      ></i>

最终 input 右侧显示什么图标由 iconClass 决定,其中 circle-close 就是圆形查查,即清空按钮~

      iconClass() {
        let criteria = this.clearable &&
          !this.selectDisabled &&
          this.inputHovering &&
          !this.multiple &&
          this.value !== undefined &&
          this.value !== '';
        return criteria ? 'circle-close is-show-close' : (this.remote && this.filterable ? '' : 'arrow-up');
      },

handleIconClick 方法:

      // 处理图标点击事件(删除按钮)
      handleIconClick(event) {
        if (this.iconClass.indexOf('circle-close') > -1) {
          this.deleteSelected(event);
        }
      },
      // 删除选中
      deleteSelected(event) {
        event.stopPropagation();
        this.$emit('input', '');
        this.emitChange('');
        this.visible = false;
        this.$emit('clear');
      },

最终,清空只是将文本清空掉并且关闭下拉菜单。其实当再次打开 select 的时候,option 还是选中在之前选中的那个位置,即 HoverIndex 没有变为 -1,不知道算不算 bug。

例如这样:

option 的自定义模板是如何实现的?

很简单,使用了 slot 插槽。并且在 slot 中定义了默认显示方式。

    <slot>
      {{ currentLabel }}
    </slot>

<form>

最后

第一次尝试用问题取代主题来写博客,这样看着中心是不是更明确一些?
最后,说下看完 select 组件的感受:

  • element 通过自定义的广播方法进行父子组件间的通信。(好像以前Vue也有这个功能,后来弃用了。)
  • 再复杂的组件都是由一个个基础的组件拼起来的。
  • select 功能还是挺复杂的,加上子组件 1000 行代码了。本文只是讲了基本功能的实现,值得深入学习。
  • 学习了高手写组件的方式和写法~之后在自己写组件的时候可以参考。
  • 方法、参数命名非常规范,一眼就能看懂具体用法。
  • 知道了 Array.some() 方法~

好吧,说好了一天写出来,结果断断续续花了三天才完成。有点高估自己能力啦~
说下之后的Vue实验室博客计划:计划再找两个复杂的 element 组件来学习,最后写一篇总结博客。然后试着自己去创建几个 UI 组件,学以致用。

<input type="text" name="studentname">

</form>

这样就是符合规范定义的,input等标签必须位于form开始和结束标签之内才能被提交给后端程序。

后端程序一般是通过定义form的action属性值来进行定义的。

例如:

<form action="studentlist.php">

<input type="text" name="studentname">

</form>

这个form在提交的时候,会提交给后端程序studentlist.php进行处理。

那么假如这段代码本身就位于studentlist.php内,就是自己提交给自己进行处理,这也是符合规范的。当然了,提交给另外一个文件也是符合规范的。

那么,按下一个按钮之后,通过何种代码方式来触发提交的动作呢?

有2种办法,一种是采用默认的<input type="submit" value="查询"/>,当点击这个按钮的时候,就会自动提交到form对应的action属性的php文件来进行处理。

另一种方法是采用JavaScript来进行提交。由于之前的网页代码编写中,查询按钮是通过a标签来实现了,因此这里我们就来试试看采用JavaScript方法如何提交给后台程序。

JavaScript方式提交Form

首先修改studentlist.php文件,增加form标签,同时修改查询按钮的a标签的onclick方法:

studentlist.php修改如下:

图片 3image

然后创建另外一个文件studentlist_do.php文件:

图片 4image

然后刷新网页:

图片 54-7-3.jpg

在姓名输入框里面输入字符串a,然后点击查询按钮:

图片 6image

可以看到浏览器的地址变成了studentlist_do.php了,并且显示了刚刚输入的字符串a信息,说明后端程序studentlist_do.php已经接收到了用户输入的信息了。

本文由糖果派对电玩城发布于独家专题,转载请注明出处:ListView 踩过的坑

您可能还会对下面的文章感兴趣: