作者:Dunizb,原文地址
建议在电脑上阅读此文,全部源代码在文章最后
本文教你如何写一个移动端的 Checklist 组件,使用 vue 单文件形式开发,适合 Vue.js 新手。同时此文非常长,最好跟着文章步骤边看边写。本文说些什么,或者你能收获什么?。
- 一步步从0开始有节奏的编写 Checklist 组件
- 在编码过程中进行功能分析
- 涉及一些移动端适配的问题
-
Vue.js组件方面的一些知识
前置知识阅读此文前您最好有以下知识的基础:
- 对 Vue.js 的.vue单文件和 Vue.js 组件知识有基本的认识
- CSS 的 Flexbox 布局知识。
什么是Checklist
什么是Checklist组件?我们先来看一下市面上已经有的UI框架的Checklist长什么样
weui
Mint-UI
而这种组件的一个典型场景是移动端的购物车列表,打开你的京东、淘宝购物车看看,功能是不是很像呢?
本文写一个什么样的 Checklist 组件?这个组件来自于我司真实项目,刚开始我也是用的 Mint-UI 来做,后来业务升级需求变更 Mint-UI 就不适合了,于是我就自己撸了一个,特整理出来此文,并且我们尽量把他做的通用、灵活一点。我们的 Checklist 如下:
我们的
第零步:分析与准备 0.1 业务需求和功能分析京东购物车列表
在动手撸代码之前,我们先来仔细分析一下业务需求和功能点,这个组件是展示考场和考场地址,考场地址没有就不显示,最多选三个,选了三个后其他的要禁用等,数据是根据考试科目和所在城市动态获取,当列表数据很多我们还得给它一个最大高度让列表可滚动等,从图中得出以下功能点:
- 显示隐藏和过渡动画
- 列表的地址一行可有可无,行高自适应
- 选中状态和禁用状态
- 选择了几个和最多选几个的提示文字(我们叫他操作提示栏吧,为了方便后面都这么叫),为了通用性,最多选几个应该做成可配置选项
- 列表可滚动以及列表最大高度,最大高度也可以做成可配置项
- 头部bar,标题、取消、确定应该也是可有可无,可做成可配置项
- 选中的项的值应该是一个数组
- 选中后点击完成按钮应该把选中的值发送给父组件
- checkbox选框可以在左边也可以在右边
等等….
一个Vue组件的 API 只来自 props、events 和 slots,确定好这 3 部分的命名、规则,剩下的逻辑即使第一版没做好,后续也可以迭代完善。但是 API 如果没有设计好,后续再改对使用者成本就很大了。
0.3 初始化项目再来分析一下 DOM 结构该怎么划分,这样有利于编码时的大局观
画的有点丑,手头没有什么好用的图片标注工具,用的 Mac 原生标注工具
经过上面的的分析,我们就知道这个组件要做些什么了,接下来我们就开始撸代码,首先我们先把组件基本外观和架子做出来。
首先,我们新建一个checklist.vue文件:
<template>
<div class="cl-checklist">
checklist
</div>
</template>
<script>
</script>
<style scoped>
</style>
为了便于开发时测试和观察,我们还得建一个demo.vue文件,在demo.vue中引入我们的checklist.vue组件:
<template>
<div class="cl-div">
<div class="center">checklist demo</div>
<div>
<input type="text" placeholder="请选择考场">
</div>
<checklist></checklist>
</div>
</template>
<script>
import checklist from '@components/checklist/checklist'
export default {
components: {
checklist
}
}
</script>
<style scoped>
.center{
text-align: center;
font-size: 18px;
}
</style>
第一步:实现组件骨架和基础结构
1.1 实现基本骨架
我们先把基本模块写出来,用背景颜色区分一下,写完后再把背景颜色去掉,我日常写页面都是这样,这样可以清晰的看到模块边界在哪里。
checklist.vue
<template>
<div class="cl-checklist">
<div class="topbar"></div>
<div class="desc">您已选中0个,最多可选3个</div>
<div class="list">
</div>
</div>
</template>
<script>
</script>
<style scoped>
.topbar{
height: 30px;
background-color: #d0000e;
}
.desc{
padding: 10px 15px 0 0;
font-size: 14px;
text-align: right;
color: #fff;
background-color: #0d2e44;
}
.list{
height: 300px;
background-color: #00b4ff;
}
</style>
效果如下:
topbar有三个元素,如何选择布局方式呢?可以看出,取消、完成按钮是左右对齐,中间title是居中对齐的。我们可以选择传统的浮动布局,使用三个div,比如叫:
<div class="cancel">取消</div>
<div class="title">选择考场</div>
<div class="confirm">确定</div>
这样需要给div宽度,给取消、确定左右对齐,title居中对齐。NO!NO!NO!太麻烦了!我们使用Flexbox布局,后面我都将使用Flexbox布局。来看看Flexbox如何轻松解决这个布局。
HTML:
<div class="topbar">
<span class="cancel">取消</span>
<span class="title">选择考场</span>
<span class="confirm">完成</span>
</div>
CSS:
.topbar{
display: -webkit-flex;
display: flex;
justify-content: space-between;
align-items: center;
height: 45px;
font-size: 16px;
padding: 0 13px;
border-bottom: 1px solid rgb(217,217,217);
}
.topbar .cancel{
color: rgb(159,159,159);
}
.topbar .confirm{
color: rgb(46,166,242);
}
我们使用justify-content: space-between让他们水平两端对齐,然后align-items: center垂直居中对齐,再给个左右padding即可。效果如下:
我在项目中用的是display:inline-block来布局,做的没现在的好,这种事后用文章的形式来复盘和输出能够让自己更清楚的认识怎么更好的去组织代码,这也是我坚持输出的原因。
这个比较简单,在这个部分直接一起给撸了吧。
HTML
<template>
<div class="cl-checklist">
<div class="topbar">
<span class="cancel">取消</span>
<span class="title">选择考场</span>
<span class="confirm">完成</span>
</div>
<div class="desc">您已选中0个,最多可选3个</div>
<div class="list">
</div>
</div>
</template>
CSS
.desc{
height: 30px;
line-height: 30px;
padding-right: 10px;
font-size: 14px;
text-align: right;
color: rgb(159,159,159);
}
效果如下:
我们先来回顾以下第零部多DOM结构的分析:
可以看到我们把DOM结构总体划分为左右结构,然后左边的又分为上下结构,左边的checkbox水平垂直居中
我们先把结构基本勾勒出来
HTML:
<-- 省略上面的代码 -->
<div class="list">
<div class="line">
<div class="l">
<div class="title">科目二第07考点马路</div>
<div class="address">上海市宝山区保安公路2009号</div>
</div>
<div class="r"></div>
</div>
</div>
<-- 省略下面的代码 -->
CSS:
.list{
height: 300px;
font-size: 14px;
padding: 10px 13px;
background-color: #00b4ff;
}
.list .line {
display: -webkit-flex;
display: flex;
justify-content: center;
align-items: center;
height: 50px;
background-color: #4caf50;
}
.list .line .l{
display: -webkit-flex;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
width: 90%;
background-color: #d0000e;
}
.list .line .r{
width: 20px;
height: 20px;
background-color: #0d2e44;
}
效果如下:
接下来就是完善了,以及右边的checkbox圆圈。注意,我们不能给.line设死高度,这个高度应该由内容撑开,因为我们要考虑没有地址信息的时候的展示。
为了方便展示选中状态,我们复制一行.line,设置这一行为选中状态,也就是加一个.selected的class,然后我们对.selected写选中状态的样式
<div class="list">
<div class="line">
<div class="l">
<div class="title">科目二第07考点马路</div>
<div class="address">上海市宝山区保安公路2009号</div>
</div>
<div class="r"></div>
</div>
<div class="line selected">
<div class="l">
<div class="title">科目二第07考点马路</div>
<div class="address">上海市宝山区保安公路2009号</div>
</div>
<div class="r"></div>
</div>
</div>
我们继续CSS:
.list .line .r{
width: 20px;
height: 20px;
margin: 0 5px;
-webkit-border-radius: 50%;
border-radius: 50%;
border:1px solid #9e9e9e;
background-color: #fff;
position: relative;
z-index: 0;
}
.list .line.selected .l .title{
color: #1799fa;
}
.list .line.selected .r{
border: 1px solid #1799fa;
background-color: #1799fa;
}
.list .line.selected .r::before{
content: ' ';
position: absolute;
top: 4px;
left: 4px;
width: 12px;
height: 12px;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAPCAYAAAALWoRrAAAA90lEQVQ4ja3TMSuFURgH8GeQRGIwy6BkUbIYlHwBu0Umi8VkMVlMJoMvIcNdDAYlJuULWCSESBSLwc9we/P2eu715t6nznKe//Or0zknEF1YS3jAHRa7Aa7iy0/ddAquVUC47ARcT8APLPwX3PC73jGPCPRiGSvoqwFuJuAb5opMoFFqnmCwDbiVgK+YLecCn5XQGYYScDsBXzBTzQb2k/A5hkvBnSTzhOnsRIEBHCdDFxjBbtJ7xFQGFmigH0fJ8HOyd4/JVmAZDc2bP0yQct1ioh1YRYvn1cg0XGP8LzBDC/igAl5hrA7YCg30YE/zl5xitC6I+AYJmBaJbbKurAAAAABJRU5ErkJggg==");
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
z-index: 1;
}
这里为了不依赖图片,我们把勾的图片编码成base64格式,同时我们先把背景去掉,效果如下:
这个小图标最好使用伪元素来实现
.list .line .l .address{
color: rgb(159,159,159);
position: relative;
padding-left: 15px;
}
.list .line .l .address::before{
content: ' ';
display: inline-block;
position: absolute;
width: 15px;
height: 15px;
top: 2px;
left: 0;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAbCAYAAAB836/YAAABu0lEQVRIiaXUzU8TQRjH8U9fpAkXIZqItHghxEgietuD/gP84R420QSDcCDxIq6KaCoJYCuaeJhdu93OlILfpEnnmWd++7zMM63d3V0RuljHQ9xFr7SP8QNfUOBPlmUzB5tsYBtLkb0eHpS/JzjEcd2hs7W1Vf1v4TkeoxMLu0EHa0VRLBdFcTIYDEC75vCsjO6mbGCnWlSCg1uKVTzK83xAqGFHqFmMX3iP7+X6HjbF67ud5/nnLvomXaxziVcY1WxDfMQLLDf8e+i3sZaIbq8hVjHC28SZtTZWIhs/TdKM8S3xsZW2eLrjOWIVMcFeO2Ik1Kc1R6xltoYI1+Y8Yl8Sxi7Funinz9s4SRzaEa/vKp4mzpx0hSHfjGzewUt8EpoA98voUuUoujgT7tdqxKEl3NN+QqDOMMuys6opRwscuI4jJrP8VYjytgxLjanX5uA/BA+qh7YuOBQadFOKLMv+Zde82Ae4uoHYlUZmTcGx8KwvymGWZVNjGhu9DzhdQOy09J0iNct75qd+VfrMkBIcYX+O4L74a5MUJHT8OGI/Nuc2zBOEd7iorS9KW5LrBH/jjZDeCK9LW5K/QatiGcsSFOsAAAAASUVORK5CYII=");
background-repeat: no-repeat;
background-size: contain;
background-position: 0;
}
实现适配移动端1px边框,主要是根据设备的dpr来对边框进行缩放处理,CSS写法如下:
.border-1px{
position: relative;
}
.border-1px::after{
display: block;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
border-bottom: 1px solid rgb(217,217,217);
content: ' ';
}
@media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5) {
.border-1px::after {
-webkit-transform: scaleY(0.7);
transform: scaleY(0.7);
}
}
@media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
.border-1px::after {
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
}
}
然后我们在需要的地方设置.border-1px的样式
<div class="list">
<div class="line border-1px">
<div class="l">
<div class="title">科目二第07考点马路</div>
<div class="address">上海市宝山区保安公路2009号</div>
</div>
<div class="r"></div>
</div>
<div class="line border-1px selected">
<div class="l">
<div class="title">科目二第07考点马路</div>
<div class="address">上海市宝山区保安公路2009号</div>
</div>
<div class="r"></div>
</div>
</div>
我们在每一行的.line元素上添加border-1px,效果如下:
更多移动端1像素边框问题可以参看《移动端1像素边框问题》
实现滚动很简单,只要给父级元素也就是我们代码中的.list元素设置一个高度,在这里我们的数据多少不一定,所以我们最好只设置一个最大高度max-height即可,同时需要给最外层DIV也就是.cl-checklist设置overflow:hidden。我们先复制很多行来进行测试。
CSS:
.cl-checklist{
overflow: hidden;
}
.list{
/*height: 300px;*/
max-height: 300px;
font-size: 14px;
padding: 10px 13px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
/*background-color: #00A2E6;*/
}
注意overflow-scrolling: touch;属性,设置该属性是为了适配在移动端下滚动不平滑的问题,现在的效果如下:
这一步是整个组件的一个核心也是重点难点,这一步写的好、写的巧就会对后面的逻辑交互简化很多。我们借助HTML的原生功能特性来实现:
<label for="xxx"><input id="xxx" type="checkbox" value=""></label>
这个标签组合可以实现点击<label>包裹起来的范围的时候触发checkbox,根据这个原理我们改造一下HTML代码
<div class="desc">您已选中 <span>{{checkboxValue.length}}</span> 个,最多可选<span>3</span>个</div>
<div class="list">
<div class="line-wrapper">
<label for="1" class="line border-1px">
<div class="l">
<div class="title">科目二第07考点马路</div>
<div class="address">上海市宝山区保安公路2009号</div>
</div>
<div class="r"></div>
</label>
<input type="checkbox" id="1" v-model="checkboxValue" value="1">
</div>
<div class="line-wrapper">
<label for="2" class="line border-1px">
<div class="l">
<div class="title">科目二第07考点马路</div>
<div class="address">上海市宝山区保安公路2009号</div>
</div>
<div class="r"></div>
</label>
<input type="checkbox" id="2" v-model="checkboxValue" value="2">
</div>
<div class="line-wrapper">
<label for="3" class="line border-1px">
<div class="l">
<div class="title">科目二第07考点马路</div>
<div class="address">上海市宝山区保安公路2009号</div>
</div>
<div class="r"></div>
</label>
<input type="checkbox" id="3" v-model="checkboxValue" value="3">
</div>
</div>
主要是把div.line的元素变成<label>元素,然后在外面再包裹一个div.line-wrapper,在<label>后面加一个checkbox标签。同时让checkbox不可见我们可以给checkbox设置display:none或者给外围的div.line-wrapper设置overflow:hidden都可以,这里我使用display:none。
Vue.js 提供了 v-model 指令,用于在表单类元素上双向绑定数据,例如在输入框上使用时,输入的内容会实时映射到绑定的数据上。单选按钮在单独使用时不需要 v-model ,直接使用 v-bind 绑定一个布尔类型的值,为真时选中,为否时不选中,如果是组合使用来实现互斥效果时就需要 v-model 配合 value 来使用。
这里给checkbox用 v-model 指令绑定了一个checkboxValue,这个值必须是一个数组,然后Vue.js 会帮我们自动每次变更数组:
export default {
data () {
return {
checkboxValue: []
}
}
}
在操作提示栏里我们给当前选择了几个设置成了checkboxValue的长度,这样之后我们来试试,可以发现每次选择一个都会往数组中push一次,再次点击则会从数组中移除。效果如下:
现在是可以选中了,但是如何给选中的项加上选中的CSS呢,想来想去也是个麻烦事,不过得借助JS来实现了,不知道广大网友有没有牛逼方法。
我们在checkbox标签上绑定一个事件selectedItem:
<div class="line-wrapper">
<label for="1" class="line border-1px">
<div class="l">
<div class="title">科目二第07考点马路</div>
<div class="address">上海市宝山区保安公路2009号</div>
</div>
<div class="r"></div>
</label>
<input type="checkbox" id="1" @click="selectedItem($event)" v-model="checkboxValue" value="1">
</div>
methods: {
selectedItem (event) {
const labelNode = event.target.previousElementSibling
const classList = labelNode.classList
classList.contains('selected') ? classList.remove('selected') : classList.add('selected')
}
}
点击的时候获取Event事件对象,然后通过previousElementSibling找到上一个兄弟节点,给他绑定.selectedclass即可
3.3.1 使用 props 传递数据
用户是可以设置最多选择几项的,于是 Vue.js 的 Props 功能派上用场了。在 Vue.js 组件中,使用选项 Props 来声明需要从父级接收的数据,props 的值可以是两种,一种是字符串,一种是对象,这里我们使用对象,一个Number对象。
先定义 props
props: {
max: {
type: Number,
default: 0
}
}
我们定义了一个max属性,它的类型是Number类型,默认值是 0 。然后我们把max加到操作提示中
<div class="desc">您已选中 <span>{{checkboxValue.length}}</span>
个,最多可选<span>{{max}}</span>个</div>
然后我们就可以给组件传递max属性了,现在转到demo.vue文件:
<template>
<div class="cl-div">
<div class="center">checklist demo</div>
<div>
<input type="text" placeholder="请选择考场">
</div>
<checklist :max="2"></checklist>
</div>
</template>
我们给max设置为2,也就是最多选择2个。
3.3.2 Watch选项、$refs的使用
当我们选择了两个的时候其他的选择项就应该灰掉(禁用),那么就要监控 data 选项里的checkboxValue的长度了,这时候我们需要用到Vue.js的 watch 选项,watch 是一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用$watch(),遍历 watch 对象的每一个属性。
注意,不应该使用箭头函数来定义 watcher 函数 (例如searchQuery: newValue =this.updateAutocomplete(newValue))。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.updateAutocomplete 将是 undefined。
我们通过监听 data 选项里的checkboxValue,来判断它的长度,如果它的长度刚好已经和设置的max属性相等了,就给其他添加.disabled这个class,同事给input checkbox添加disabled属性。
watch: {
checkboxValue (val) {
const listDom = this.$refs['list']
const lines = listDom.querySelectorAll('line-wrapper')
if (val.length === this.max) {
let item = null
for (let i = 0; i < lines.length; i++) {
item =lines[i]
if (val.indexOf(lines[i].dataset.val) === -1) {
item.children[0].classList.add('disabled')
item.querySelector('input[type="checkbox"]').setAttribute('disabled', 'disabled')
}
}
} else {
let item = null
for (let i = 0; i <lines.length; i++) {
item =lines[i]
if (item.children[0].classList.contains('disabled')) {
item.children[0].classList.remove('disabled')
item.querySelector('input[type="checkbox"]').removeAttribute('disabled')
}
}
}
}
}
这个需要配合Vue.js 的 $refs来做,在HTML中的.list节点上设置ref = 'list',也就是为了方便选择这个DOM节点,当然你用传统的document.querySelector来选择.list节点也是没问题的。
<div class="list" ref="list">
上面代码的第9行,判断是否当前DOM是否在选中的数组中,拿的是checkboxValue的数组项和一个自定义属性值比较,这个自定义属性叫data-val,他的值跟input checkbox的 value 值保持一致,这个val自定义属性设置在.list-wrapper节点上是为了方便DOM查找,减少DOM查找层数,不然就需要获取input checkbox的 value 值来比较。
设置.disabled的CSS如下:
.list .line.disabled .l .title{
color: #9e9e9e;
}
.list .line.disabled .r{
border: 1px solid #9e9e9e;
background-color: #9e9e9e;
}
热门评论
点击选择的时候有问题,安卓可以正常操作,ⅰos选择后无法改变样式
讲的很细,谢谢分享!