原生JS实现一个日期选择器(DatePicker)组件
前言: 最近看到慕课网上有一个实现日期选择器组件的课程,空闲时间就看了一下,觉得还是有一些借鉴之处,在参考了一下其他文章,写下了这篇文章。通过自己的理解将整个实现过程以最简单的语言描述出来,争取让小白也能看懂。文章中可能会出现许多拓展知识,千万不要错过哦!
文章的知识点: 通过原生HTML/CSS/JavaScript完成一个日期选择器(datepicker)组件的开发。主要包括datepicker静态结构的编写、日历数据的计划获取、组件的渲染以及组件事件的处理。
其它参考文章:
实现效果:
实现效果:
日期选择器(datepicker) https://www.zhihu.com/video/1081204916437139456一.什么是日期选择器(datepicker)?
日期选择器在我们的网站或者其他应用上是经常碰到的,它具有让你快速选择日期的功能。日期选择器的类型也是各种各样的,但是总体上就和下图差不多:
我们这次要做的就和上图差不多,记住最重要的一句话:实现过程才是最重要的,理解实现思路以及方法才是这篇文章的重点。
二.组件化开发思想
随着前端技术的发展,组件化开发的思想越来越深入人心,组件化并不是前端所特有的,一些其他的语言或者桌面程序等,都具有组件化的先例。其实总的来说,只要涉及到UI层面的开发,基本都会涉及到组件化开发思想。一个组件就是一个独立的个体,在网站开发中,一个页面可以由多个组件组成,比如说一个按钮可以是一个组件,一个侧边栏可以是一个组件,总之,随着前端技术的发展,组件化开发的思想是不可或缺的。
更多关于组件化可以阅读以下:
三.编写页面结构和样式
(一)HTML结构
我们可以看到HTML结构是很简单的,总共就分为了两大部分,一个head部分,一个body部分。这里值得注意的是我们给元素取的类名都比较长,比较特殊,这是因为我们这里是组件化开,所谓组件就是可以重复使用的,使用的场景有很多,所以我们就要尽可能的让我们的类名特殊,这样避免使用的使用重名。
<body>
<div class="ui-datepicker-wrapper">
<!-- 头部区,选择上或者下 -->
<div class="ui-datepicker-header">
<a href="#" class="ui-datepicker-btn ui-datepicker-prev-btn"><</a>
<a href="#" class="ui-datepicker-btn ui-datepicker-next-btn">></a>
<!-- 当前月份 -->
<span class="ui-datepicker-curr-month">2019-2</span>
</div>
<!-- body区,显示日历 -->
<div class="ui-datepicker-body">
<table>
<!-- 日历顶端显示星期几 -->
<thead>
<th>一</th>
<th>二</th>
<th>三</th>
<th>四</th>
<th>五</th>
<th>六</th>
<th>日</th>
</thead>
<!-- 日历主体部分,每一行显示七天 -->
<tbody>
<td>29</td>
<td>30</td>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>5</td>
<td>6</td>
<td>7</td>
<td>8</td>
<td>9</td>
<td>10</td>
<td>12</td>
<td>29</td>
<td>30</td>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>29</td>
<td>30</td>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>29</td>
<td>30</td>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
</tbody>
</table>
</div>
</div>
</body>
此时没有样式的页面黑很丑,但是也能看出大体结构:
更过关于表格结构的知识点请移步:
(二)添加样式
新增style.css文件
/* 最外层区域 */
.ui-datepicker-wrapper {
width: 240px;
font-size: 16px;
color: #666666;
box-shadow: 2px 2px 8px 2px rgba(128, 128, 128, 0.3);
/* 头部区域 */
.ui-datepicker-wrapper .ui-datepicker-header {
padding: 0 20px;
height: 50px;
line-height: 50px;
text-align: center;
background: #f0f0f0;
border-bottom: 1px solid #cccccc;
font-weight: bold;
/* 设置两个按钮 */
.ui-datepicker-wrapper .ui-datepicker-btn {
font-family: serif;
font-size: 20px;
width: 20px;
height: 50px;
line-height: 50px;
color: #1abc9c;
text-align: center;
cursor: pointer;
text-decoration: none;
.ui-datepicker-wrapper .ui-datepicker-prev-btn {
float: left;
.ui-datepicker-wrapper .ui-datepicker-next-btn {
float: right;
/* body区域 */
.ui-datepicker-wrapper .ui-datepicker-body table {
width: 100%;
border-collapse: collapse;
/* 表头和正文 */
.ui-datepicker-wrapper .ui-datepicker-body th,
.ui-datepicker-wrapper .ui-datepicker-body td {
height: 30px;
text-align: center;
.ui-datepicker-wrapper .ui-datepicker-body th {
font-size: 12px;
height: 40px;
line-height: 40px;
/* 表格部分 */
.ui-datepicker-wrapper .ui-datepicker-body td {
border: 1px solid #f0f0f0;
font-size: 10px;
width: 14%;
cursor: pointer;
}
样式部分没有什么可说的,都是用的很基本的样式属性,主要就是设置表头和表格的样式。补充一个我比较少接触到的样式属性:
此时的日期选择器的基本结构已经完成:
四.日历中的核心数据
所谓核心数据就是日历中显示每一天的数据,如下所示:
这些数据的作用:
- 渲染当月日历表格
- 用户点击时获取日期值
五.需要事先了解的知识点
(一)日期对象——Data
W3C上的部分说明:
具体详情请移步:
这里我们主要是使用以下方式进行传值,因为当用户选择日期的时候,实际上是将点击的值传入Date对象里面,然后获取值:
new Date(year,month-1,date)//注意月份需要-1
注意:日期对象有一个“越界自动进(退)位”的特性。
(二)其他API——getFullYear()/getMonth()/getDate()/getDay()
W3C上的解释:
1.getFullYear()
详情请移步:
2.getMonth()
W3C上的解释:
具体详情请移步:
3.getDate()
W3C上的解释:
详情请移步:
4.getDay()
W3C上的解释:
详情请移步:
(三)日期对象获取天数
这里为什么会把这个单独拿出来讲一下呢,那肯定是有它令人疑惑的地方:
- 获取当月第一天:
new Date(year,month-1,1)
- 获取当月最后一天
new Date(year,month,0)
- 星期一-星期天
[1,2,3,4,5,6,0]//注意周天不是7而是0
这里我们可以看到获取当月最后一天的时候我们的月份并没有-1,那么就是默认获取的下一个月,然后我们天那里传的0,这里就解释了前面所说的“越界自动进(退)位”。
注意:我们传入的月份的范围:0~11
为什么会有这么奇怪的定义呢?打个比方,我们要获取某年2月份的最后一天,这时候很多人就可能会这样写:
new Date(year,1,28/29)//注意,因为month要-1,所以月份就要填1,才表示获取的2月份
这里大家应该就看得出来了,2月份有多少天是不固定的,所以我们传入值的时候就有可能不知道了,但是我们有了上面的规则就不一样了,我们可以这样写:
new Date(year,2,0)
利用“越界自动进(退)位”的特性,让系统自己去获取最后一天,这样是不是就不用我们瞎操心了,所以,任何事物存在必有它的道理的。
六.编写我们的JavaScript
(一)获取日历数据
我们新建一个data.js文件
(function(){
var datepicker = {};
datepicker.getMonthDate = function (year, month) {
var ret = [];
if(!year || !month){
var today = new Date();
year = today.getFullYear();
month = today.getMonth() + 1;
var firstDay = new Date(year, month-1, 1);//获取当月第一天
var firstDayWeekDay = firstDay.getDay();//获取星期几,才好判断排在第几列
if(firstDayWeekDay === 0){//周日
firstDayWeekDay = 7;
year = firstDay.getFullYear();
month = firstDay.getMonth() + 1;
var lastDayOfLastMonth = new Date(year, month-1, 0);//获取最后一天
var lastDateOfLastMonth = lastDayOfLastMonth.getDate();
var preMonthDayCount = firstDayWeekDay - 1;
var lastDay = new Date(year, month, 0);
var lastDate = lastDay.getDate();
for(var i=0; i<7*6; i++){
var date = i + 1 - preMonthDayCount;
var showDate = date;
var thisMonth = month;
//上一月
if(date <= 0){
thisMonth = month - 1;
showDate = lastDateOfLastMonth + date;
}else if(date > lastDate){
//下一月
thisMonth = month + 1;
showDate = showDate -lastDate;
if(thisMonth === 0){
thisMonth = 12;
if(thisMonth === 13){
thisMonth = 1;
ret.push({
month: thisMonth,
date: date,
showDate: showDate
return {
year: year,
month:month,
days: ret
window.datepicker = datepicker;//该函数唯一暴露的对象
})();
index.html页面添加如下代码:
<script src="./data.js"></script>
<script>
var monthDate = datepicker.getMonthDate(2019, 2);
console.log(monthDate);
</script>
此时我们看一下打印台上打印的什么:
很明显,这里我们已经打印出来了2月份所有的天数,至于为什么会答应出来这么多天留给大家思考一下。
(二)数据渲染
获取到了数据那么重要的u是渲染到我们的日历当中了.
新建一个main.js
(function () {
var datepicker = window.datepicker;
datepicker.buildUi = function (year, month){
var monthData = datepicker.getMonthDate(year, month);//获取一个月的数据
//由于没有使用第三方插件,所以采用拼接字符串的方式
var html = '<div class="ui-datepicker-header">'+
'<a href="#" class="ui-datepicker-btn ui-datepicker-prev-btn"><</a>'+
'<a href="#" class="ui-datepicker-btn ui-datepicker-next-btn">></a>'+
'<span class="ui-datepicker-curr-month">'+monthData.year+'-'+monthData.month+'</span>'+
'</div>'+
'<div class="ui-datepicker-body">'+
'<table>'+
'<thead>'+
'<tr>'+
'<th>一</th>'+
'<th>二</th>'+
'<th>三</th>'+
'<th>四</th>'+
'<th>五</th>'+
'<th>六</th>'+
'<th>日</th>'+
'</tr>'+
'</thead>'+
'<tbody>';
for(var i=0; i<monthData.days.length; i++){
var date = monthData.days[i];
if(i%7 === 0){
html += '<tr>';
html += '<td>' + date.showDate + '</td>'
if(i%7 === 6){
html += '</tr>'
'<tr>'+
'<td>29</td>'+
'<td>30</td>'+
'<td>1</td>'+
'<td>2</td>'+
'<td>3</td>'+
'<td>4</td>'+
'<td>5</td>'+
'</tr>'
html += '</tbody>'+
'</table>'+
'</div>';
return html;
datepicker.init = function ($dom) {
var html = datepicker.buildUi();
$dom.innerHTML = html;
})();
index.html页面加上:
<script>
var monthDate = datepicker.getMonthDate(2019, 2);
console.log(monthDate);
datepicker.init(document.querySelector('.ui-datepicker-wrapper'))
</script>
可以看到我们的数据已经能够正常渲染出来了。值得注意的是我们在js里面进行了渲染,那么index.html里面的部分代码就可以不要了,变成这样:
<body>
<div class="ui-datepicker-wrapper">
</body>
<script src="./data.js"></script>
<script src="./main.js"></script>
<script>
var monthDate = datepicker.getMonthDate(2019, 2);
console.log(monthDate);
datepicker.init(document.querySelector('.ui-datepicker-wrapper'))
</script>
注意 :我们这里没有引用第三方插件或库,所以我们渲染的时候用的是字符串拼接,但是实践中通常采用的是第三方插件或库,更多知识可以参考:
(三)细节修改
我们都知道很多日期选择器的样式都是一个选择框,点击选择框然后才弹出日历,这里我们还没有实现,所以我们现在来改一下:
此时的index.html变成这样:
<html lang="en">
<link rel="stylesheet" href="style.css">
<style>
.datepicker {
border: 1px solid #ccc;
border-radius: 4px;
width: 230px;
padding: 5px;
line-height: 24px;
.datepicker:focus {
outline: none;
border: 1px solid #1abc9c;
</style>
</head>
<input type="text" class="datepicker">
</body>
<script src="./data.js"></script>
<script src="./main.js"></script>
<script>
// var monthDate = datepicker.getMonthDate(2019, 2);
// console.log(monthDate);
// datepicker.init(document.querySelector('.ui-datepicker-wrapper'))
datepicker.init('.datepicker');
</script>
</html>
然后修改一下main.js,动态的来创建我们的div:
datepicker.init = function (input) {
var html = datepicker.buildUi();
// document.body.innerHTML = html;
var $wrappper = document.createElement('div');
$wrappper.className = 'ui-datepicker-wrapper';
$wrappper.innerHTML = html;
document.body.appendChild($wrappper);
}
此时页面上多了一个文本框,但是此时我们的页面上已经没有了div了,整个包含日历的div元素由我们的js代码来创建。
(四)日历的展开收起
我们可以看到一进页面的时候日历就已经存在了,一般情况下是需要点击input框的时候日历才会显示元素,而且我们需要采用定位的方式来对日历进行限定,因为页面上有其他元素,如果不采用定位的 话就会影响到其他元素。
在style.css里面添加一个类用来控制显示或者隐藏:
.ui-datepicker-wrapper {
........
display: none;//添加默认隐藏
position: absolute;//添加绝对定位
.ui-datepicker-wrapper-show {
display: block;
}
此时我们在main.js里面的init函数里面设置显示或者隐藏,并且根据input框的位置动态的给日历添加top和left值,这样可以适用于多种场景,此时init函数变为:
datepicker.init = function (input) {
var html = datepicker.buildUi();
// document.body.innerHTML = html;
var $wrappper = document.createElement('div');
$wrappper.className = 'ui-datepicker-wrapper';
$wrappper.innerHTML = html;
document.body.appendChild($wrappper);
//控制显示或者隐藏
var $input = document.querySelector(input);
var isOpen = false;
$input.addEventListener('click',function(){
if(isOpen){
$wrappper.classList.remove('ui-datepicker-wrapper-show');
isOpen = false;
}else {
$wrappper.classList.add('ui-datepicker-wrapper-show');
//获取input的位置,设置日历的位置
var left = $input.offsetTop;
var top = $input.offsetTop;
var height = $input.offsetHeight;
$wrappper.style.top = top + height + 2 + 'px';
$wrappper.style.left = left + 'px';
isOpen =true;
},false)
}
这样编写之后,我们便能通过点击输入框实现日历的显示或者隐藏了,而且也能通过定位的方式来确定日历的位置。
点击之后:
(五)月份切换和日期选择
我们将实现月份切换的逻辑也放在init函数里面:
这里值得注意的是:我们的init函数只执行了一次,如果我们直接把点击事件绑定在btn上面,那么事件就只有在渲染页面的时候才会初始化一次,意味着只绑定了一次,但是在我们渲染之后,我们的按钮每一次都是根据html字符串重新渲染出来的,也就是我们的按钮会不断的销毁和重建,所以我们绑定的事件是无法生效的。所以我们这里采用将时间绑定在不变的外层元素wrapper上。
这里修改的地方较多,最终的main.js代码如下:
(function () {
var datepicker = window.datepicker;
var monthData;
var $wrapper;
//渲染函数,由于没有使用第三方插件或库,所以使用的是模板拼接的方法
datepicker.buildUi = function (year, month) {
monthData = datepicker.getMonthDate(year, month);
var html = '<div class="ui-datepicker-header">' +
'<a href="#" class="ui-datepicker-btn ui-datepicker-prev-btn"><</a>' +
'<a href="#" class="ui-datepicker-btn ui-datepicker-next-btn">></a>' +
'<span class="datepicker-curr-month">'+monthData.year+'-'+monthData.month+'</span>' +
'</div>' +
'<div class="ui-datepicker-body">' +
'<table>' +
'<thead>' +
'<tr>' +
'<th>一</th>' +
'<th>二</th>' +
'<th>三</th>' +
'<th>四</th>' +
'<th>五</th>' +
'<th>六</th>' +
'<th>日</th>' +
'</tr>' +
'</thead>' +
'<tbody>';
for (var i = 0; i < monthData.days.length; i++) {
var date = monthData.days[i];
if (i % 7 === 0) {
html += '<tr>';
html += '<td data-date="'+date.date+'">' + date.showDate + '</td>';
if (i % 7 === 6) {
html += '</tr>';
html+='</tbody>' +
'</table>'+
'</div>';
return html;
//日历渲染函数
datepicker.render = function (direction) {
var year, month;
if (monthData) {
year = monthData.year;
month = monthData.month;
if (direction === 'prev') month--;
if (direction === 'next') month++;
var html = datepicker.buildUi(year,month);
$wrapper=document.querySelector('.ui-datepicker-wrapper');
if(!$wrapper){
$wrapper = document.createElement('div');
$wrapper.className = 'ui-datepicker-wrapper';
$wrapper.innerHTML = html;
document.body.appendChild($wrapper);
//初始换函数
datepicker.init = function (input) {
datepicker.render();
var $input=document.querySelector(input);
var isOpen=false;
//给input框赋予点击事件
$input.addEventListener('click',function(){
if(isOpen){
$wrapper.classList.remove('ui-datepicker-wrapper-show');
isOpen=false;
}else{
$wrapper.classList.add('ui-datepicker-wrapper-show');
var left=$input.offsetLeft;
var top=$input.offsetTop;
var height=$input.offsetHeight;
$wrapper.style.top=top+height+2+'px';
$wrapper.style.left=left+'px';
isOpen=true;
},false);
//给按钮添加点击事件
$wrapper.addEventListener('click',function(e){
var $target=e.target;
if(!$target.classList.contains('ui-datepicker-btn')){
return false;
//上一月,下一月
if($target.classList.contains('ui-datepicker-prev-btn')){
datepicker.render('prev');
}else if($target.classList.contains('ui-datepicker-next-btn')){
datepicker.render('next');
},false);
$wrapper.addEventListener('click',function(e){
var $target= e.target;
if($target.tagName.toLocaleLowerCase()!=='td'){
return false;
var date=new Date(monthData.year,monthData.month-1,$target.dataset.date);
$input.value=format(date);
$wrapper.classList.remove('ui-datepicker-wrapper-show');
isOpen=false;
},false);
//格式化数据
function format(date){
var ret='';
var padding=function(num){
if(num<=9){
return '0'+num;
return num;
ret+=date.getFullYear()+'-';
ret+=padding(date.getMonth()+1)+'-';
ret+=padding(date.getDate());