原生JS实现一个日期选择器(DatePicker)组件

原生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">&lt;</a>
            <a href="#" class="ui-datepicker-btn ui-datepicker-next-btn">&gt;</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">&lt;</a>'+
            '<a href="#" class="ui-datepicker-btn ui-datepicker-next-btn">&gt;</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">&lt;</a>' +
                        '<a href="#" class="ui-datepicker-btn ui-datepicker-next-btn">&gt;</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());