• Home
  • Calendar component with date range select built with Vue.js

Calendar component with date range select built with Vue.js

Hello again in my new demonstration series. Today I will show you advanced calendar component which I build and use in one of my previous projects and of course here I will show you simplified version with core functionalities and not full one.  Why is that? Because full working calendar component is included in one of my bigger project and has other dependencies and also frontend part of this application is connected  with server-side part. This raw componet that I will show you here was my starting point in developing final component.  Full source code for this calendar is available on my Github.

So let me explain how this component works and their capabilities:

  • Calendar is rendered as table for given year (default is current year). Columns represents days of week and rows are months.
  • In head of component we can easily change years
  • From view you can easily spot on which day of week starts and end some month
  • With click on cells you can select first day of range and last day of range
  • You can also select only one day with double click on day cell

What calendar returns:

When you select some date range you will get array of selected dates as strings in “YYYY-MM-DD” format. Dates are shown bellow component after select. You can notice that array have all dates in specific order… first index(key) in array represents first selected date, second index(key) in array represents last date in range selected and all dates between first an last date selected are shown after second index in array.

Also one important thing is that order of selecting is not important, even if you select from bigger date to smaller in table you will get array of dates sorted from smaller to bigger date and first two keys in array will represents begin date and end date.

„As you can see on Github, the code of component is not optimized and doesn’t follow the best practice simply because it’s raw starting point version. Final code that I have build is fully optimized, there is no repeated code (DRY principle) and it’s fully commented for easy maintaining. Also everything is build with Vue tools and bundlers and not as standalone app. My goal here is to show you how things could be done and if you need something like this I can build it for you“

So how I build this:

I use Vue.js, some Bootstrap and simple custom CSS that also produce nice view of callendar on mobile screens.

Here is main container code that holds whole calendar table.

<div id="calapp">{{ message }}	
<div id="no-more-tables">
  <table class="col-sm-12 table-bordered cf">
    <thead class="text-center">
      <tr>
        <th rowspan="3" colspan="3" class="text-center"></th>
        <th colspan="15" class="text-center" @click="godinaMinus()" style="background:#fcfcfc; cursor:pointer;"><span><strong style="font-weight:900;"> <<< </strong></span></th>
        <th colspan="8" class="text-center" style="background:#fcfcfc;"><h5>Godina {{ godina }}</h5></th>
        <th colspan="15" class="text-center" @click="godinaPlus()" style="background:#fcfcfc; cursor:pointer;"><span><strong style="font-weight:900;"> >>> </strong></span></th>
      </tr>
      <tr>
        <!--<th colspan="3"></th>-->
        <th colspan="42"></th>
        <!--<th colspan="7" class="text-center" :data-title="tjedan1" style="background: lightgray;">{{ tjedan1 }}</th>
        <th colspan="7" class="text-center" :data-title="tjedan2" style="background: lightgray;">{{ tjedan2 }}</th>
        <th colspan="7" class="text-center" :data-title="tjedan3" style="background: lightgray;">{{ tjedan3 }}</th>
        <th colspan="7" class="text-center" :data-title="tjedan4" style="background: lightgray;">{{ tjedan4 }}</th>
        <th colspan="7" class="text-center" :data-title="tjedan5" style="background: lightgray;">{{ tjedan5 }}</th>
        <th colspan="7" class="text-center" :data-title="tjedan6" style="background: lightgray;">{{ tjedan6 }}</th>-->
      </tr>
      <tr>
        <!--<th colspan="3"></th>-->
        <th style="display:none;">|</th>
        <th v-for="(dayname, index) in weekDays" :data-title="dayname[index]">{{ dayname[index] }}</th>
        <th style="display:none;">|</th>
        <th v-for="(dayname, index) in weekDays" :data-title="dayname[index]">{{ dayname[index] }}</th>
        <th style="display:none;">|</th>
        <th v-for="(dayname, index) in weekDays" :data-title="dayname[index]">{{ dayname[index] }}</th>
        <th style="display:none;">|</th>
        <th v-for="(dayname, index) in weekDays" :data-title="dayname[index]">{{ dayname[index] }}</th>
        <th style="display:none;">|</th>
        <th v-for="(dayname, index) in weekDays" :data-title="dayname[index]">{{ dayname[index] }}</th>
        <th style="display:none;">|</th>
        <th v-for="(dayname, index) in weekDays" :data-title="dayname[index]" v-if="index < 3">{{ dayname[index] }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(month, index) in monthNames">
        <td colspan="3">{{ month[index + 1] }}</td>
        <td v-for="n in 44" :id="retid(index, n)" :data-tf="cellDays[index]['sel_' + n]" :data-date="cellDays[index]['day_' + n]" @click="clickedCell(index, n)" :style="cellDays[index]['class_' + n]" ><div :data-item="cellDays[index]['raz_' + n]" class="text-center">{{ cellDays[index][n] }}</div>
        </td>
      </tr>    
    </tbody>
  </table>
  <span v-for="dstr in selectedDays">
{{ dstr.dateStr }},&nbsp;
</span>
</div><!--calapp-->

In above code you see main structure of table with props and other Vue stuff that I set so calendar can be dynamically rearanged.

Now here is interesting part. Vue template script with methods, and data props.

var app = new Vue({
  el: '#calapp',
  data: {
    message: 'Created by Kristijan Klepač for private use! If you wanna use it please contact me for permission.',
    isLoading: true,
    color: '#41B883',
    firstCellClicked: 0,
    secondCellClicked: 1,
    firstVal: '',
    secVal: '',
    prevFirstVal: '',
    prevSecVal: '',
    selectedDays: [],
    disabledX: 'lightgray',
    disabledI: 'lightgray',
    gradFirstLast: 'repeating-linear-gradient(45deg,#41B883,#41B883 3px,red 10px,red 6px)',
    selectedRangeColor: '#41B883',
    colorFontSelected: '#fff',
    colorFontStandard: '#333',
    standardBackground: '#fff',
    grayBackground: 'gray',
    godina: new Date().getUTCFullYear(),
    tjedan1: '',
    tjedan2: '',
    tjedan3: '',
    tjedan4: '',
    tjedan5: '',
    tjedan6: '',
    monthNames: [
    { 1: 'SIJ' },
    { 2: 'VELJ' },
    { 3: 'OŽU' },
    { 4: 'TRA' },
    { 5: 'SVI' },
    { 6: 'LIP' },
    { 7: 'SRP' },
    { 8: 'KOL' },
    { 9: 'RUJ' },
    { 10: 'LIS' },
    { 11: 'STU' },
    { 12: 'DEC' }
    ],
    weekDays: [
      { 0: 'P' },
      { 1: 'U' },
      { 2: 'S' },
      { 3: 'Č' },
      { 4: 'P' },
      { 5: 'S' },
      { 6: 'N' }
    ],
    cellDays: [
        { 1: {} },
        { 2: {} },
        { 3: {} },
        { 4: {} },
        { 5: {} },
        { 6: {} },
        { 7: {} },
        { 8: {} },
        { 9: {} },
        { 10: {} },
        { 11: {} },
        { 12: {} }
    ]
  },

In above code we set main data props for this component like some dynamic CSS and arrays which holds dayname, monthNames etc…

I will try to explain here what some methods do:

  • Method to render initial calendar ( and for rerendering after select or any other change)
renderCal () {
  	/* get month first day */ /* 0-pon, ... 6-ned */
    for (var miy = 0; miy <= 11; miy++) { // za svaki mjesec prvi dan pada na dan u tjednu
    	var mFD = new Date(this.godina, miy, 1);
        var dayInWeek = mFD.getUTCDay();
        var howManyDaysInMonth = this.getDaysInMonth(miy,this.godina);
        for (var x=1; x <= 48; x++) { // 40 polja za dane u mjesecu ( max 31 punih ... ostalo prazno zavisno od mjeseca )
        	if(x === 1 || x === 9 || x === 17 || x === 25 || x === 33 || x === 41) { // razdjelnici kalendara |
               this.createCellDays(miy, x, "", "razdjelnik");
        	} else {
        	  if( x < 9 && ((x - 2) === dayInWeek) ) { // prvi tjedan u kojem trazimo pocetni dan (pon, ut, sri ...)
        	  	 var prviDanZadnjiDan = 1;
                 this.createCellDays(miy, x, prviDanZadnjiDan, "firstweek");
        	     var pocetniIndex = dayInWeek + 2;
        	     prviDanZadnjiDan++;
        	  }
        	  else {
        	  	 if(prviDanZadnjiDan && (prviDanZadnjiDan <= howManyDaysInMonth)) {
        	  	 	this.createCellDays(miy, x, prviDanZadnjiDan, "ostalidani");
        	        prviDanZadnjiDan++;
        	  	 } 
                 else if(prviDanZadnjiDan && (prviDanZadnjiDan > howManyDaysInMonth)) {
        	  	 	this.createCellDays(miy, x, "", "lastweek");
                    prviDanZadnjiDan = null;
        	  	 } 
        	  	 else {
        	  	 	this.createCellDays(miy, x, "", "");
        	  	 	
        	  	 }
        	  }
        	  
        	}
   		
    	}
    	
    }
  	},
  • Method to create days (cells data)
createCellDays(miy, x, danNum, variant) { // variant ( razdjelnik, firstweek, lastweek, ostalidani )
       switch(variant) {
        case 'razdjelnik':
            this.cellDays[miy][x] = "|";
                this.cellDays[miy]['raz_' + x] = "razdjelnik";
                this.cellDays[miy]['sel_'+ x]= false;
                this.cellDays[miy]['class_' + x] = "background:gray;padding:5px;color:white;display:none;";
                this.cellDays[miy]['day_'+x] = "x";
            break;
        case 'firstweek':
            this.cellDays[miy][x] = danNum;
        	    this.cellDays[miy]['raz_' + x] = "fdm";
        	    this.cellDays[miy]['sel_'+x]= false;
        	    this.cellDays[miy]['class_' + x] = "padding:5px;cursor:pointer;";
        	    if(danNum < 10) { var day = "0"+danNum; } else { var day = danNum; }
        	    if(miy < 10) { var mt = "0"+(miy+1); } else { var mt = miy+1; }
        	    this.cellDays[miy]['day_'+x] = this.godina+"-"+mt+"-"+day;
            break;
        case 'ostalidani':
            this.cellDays[miy][x] = danNum;
        	    this.cellDays[miy]['raz_' + x] = "x";
        	    this.cellDays[miy]['sel_'+x]= false;
        	    this.cellDays[miy]['class_' + x] = "padding:5px;cursor:pointer;";
        	    if(danNum < 10) { var day = "0"+danNum; } else { var day = danNum; }
        	    if(miy < 10) { var mt = "0"+(miy+1); } else { var mt = miy+1; }
        	    this.cellDays[miy]['day_'+x] = this.godina+"-"+mt+"-"+day;
            break;
            case 'lastweek':
            this.cellDays[miy][x] = " x ";
        	    this.cellDays[miy]['raz_' + x] = "x";
        	    this.cellDays[miy]['sel_'+x]= false;
        	    this.cellDays[miy]['class_' + x] = "background:gray;padding:5px;color:white;";
        	    this.cellDays[miy]['day_'+x] = "x";
                break;
        default:
            this.cellDays[miy][x] = " x ";
        	    this.cellDays[miy]['raz_' + x] = "x";
        	    this.cellDays[miy]['sel_'+x]= false;
        	    this.cellDays[miy]['class_' + x] = "background:gray;padding:5px;color:white;";
        	    this.cellDays[miy]['day_'+x] = "x";
    }
  	},
  • Methods to change year in header of calendar
godinaPlus() {
    this.godina++;
    this.loopThrough(this.selectedDays);
    this.selectedDays = [];
  	},
godinaMinus() {
    this.godina--;
    this.loopThrough(this.selectedDays);
    this.selectedDays = [];
    },
  • One of important method  is retid() method bellow which set IDs for table cells. With that we can easily see what is selected
retid(ind, n) { // jedinstveni id za svaki td
    //console.log("tt"+this.cellDays[ind][n]);
    var Numberx = this.cellDays[ind][n];
        //console.log(this.isNumInt(Numberx));
    if(this.isNumInt(Numberx)) {
      return "ind_"+ind+"n_"+n;
    } else {
      return "ind_"+ind+"n_"+n+"_disabled";
    }
  },
  • This method here is responsible for click events on some cells. In this method we determine which cell is clicked and also first click and last click
clickedCell(ind, n) {
    this.cellDays[ind]['sel_'+ n] = !this.cellDays[ind]['sel_'+ n]; // true to false and back
    
      if(!document.getElementById("ind_"+ind+"n_"+n+"_disabled")) { 
          if(this.firstCellClicked == 0 && this.secondCellClicked == 1) { // prvi klik na neki cell
          	this.loopThrough(this.selectedDays);
            this.selectedDays = [];
          	this.firstVal = '';
          	this.secVal = '';
          this.firstCellClicked = 1;
          this.secondCellClicked = 2;
                this.firstVal = "ind_"+ind+"n_"+n;
                this.cellDays[ind]['sel_'+ n] = true;
                if(this.prevFirstVal && this.prevFirstVal != '' && this.prevFirstVal != this.firstVal) {
                    //console.log(this.retSelIndex(this.prevFirstVal));
                    this.prevFirstSecCheck(this.prevFirstVal, "one");
                    this.prevFirstVal = '';
                }
                if(this.prevSecVal && this.prevSecVal != '' && this.prevSecVal != this.secVal) {
                	//console.log(this.retSelIndex(this.prevSecVal));
                	this.prevFirstSecCheck(this.prevSecVal, "two");
                    this.prevSecVal = '';
                }
        }
        else if(this.firstCellClicked == 1 && this.secondCellClicked == 2) { // drugi klik na neki cell
          this.firstCellClicked = 0;
          this.secondCellClicked = 1;
          if(this.prevFirstVal && this.prevFirstVal != '' && this.prevFirstVal != this.firstVal) {
          	//console.log(this.retSelIndex(this.prevFirstVal));
          	this.prevFirstSecCheck(this.prevFirstVal, "one");
                	this.prevFirstVal = '';
                }
                if(this.prevSecVal && this.prevSecVal != '' && this.prevSecVal != this.secVal) {
                	//console.log(this.retSelIndex(this.prevSecVal));
                	this.prevFirstSecCheck(this.prevSecVal, "two");
                	this.prevSecVal = '';
                }
          if(this.firstVal == '') {
                    this.firstVal = "ind_"+ind+"n_"+n;
                    this.secVal = '';
              } else {
              	this.firstVal = this.firstVal;
              	this.secVal = "ind_"+ind+"n_"+n;
              	//console.log('?imamo oba');
              	/* resetiraj all light green ako ima */
              	/* obojaj sve izmedju ta dva datuma */
              	this.obojiCelije( this.firstVal,this.secVal );
              }
              if(this.prevFirstVal == '') {
                     
                     this.prevFirstVal = this.firstVal;
          }
          if(this.prevSecVal == '') {
          	this.prevSecVal = this.secVal;
          	
          }
        }
    }
        //console.log(this.firstCellClicked, this.secondCellClicked, this.firstVal, this.secVal);
    if(!this.cellDays[ind]['sel_'+ n] && this.cellDays[ind]['sel_'+ n] === false) {
      if(!document.getElementById("ind_"+ind+"n_"+n+"_disabled")) {
      var tdx = document.getElementById("ind_"+ind+"n_"+n);
            tdx.style.background = this.selectedRangeColor;
            tdx.style.color = this.colorFontSelected;
            } 
    } 
    else if(this.cellDays[ind]['sel_'+ n] && this.cellDays[ind]['sel_'+ n] === true) {
      if(!document.getElementById("ind_"+ind+"n_"+n+"_disabled")) {
      var tdx = document.getElementById("ind_"+ind+"n_"+n);
            tdx.style.background = this.gradFirstLast; 
            }
    }
    else {
            
            var tdx = document.getElementById("ind_"+ind+"n_"+n+"_disabled");
            tdx.style.background = this.grayBackground; 
    }
  },

 

So this is brief example of everything … you can check DEMO HERE … and if you like it and maybe need something like that call me…

*Please note: I’m not a native english speaker – so forgive me grammar or other mistakes in text here 🙂 – is that ok? 

Tags: , ,

Copyright by Kristijan Klepač 2018