Dans ce tutoriel nous allons mettre en pratique VueJS à travers la création d'un sélecteur de périodes. Le principe est de permettre à un utilisateur de sélectionner les disponibilitées d'un bien sur une année.

Gestion des dates

Pour la création de ce rangepicker nous allons avoir besoin de travailler avec les dates et nous n'utiliseront pas de librairies tiers (comme momentJS) afin d'avoir le code le plus simple possible. De la même manière on essaiera tant que possible de séparer la logique de notre application dans des classes génériques pour rendre le code plus portable et testable.

Nous allons créer une classe qui permettra de représenter un mois sur le calendrier.

  • getDays(), permettra d'obtenir un tableau contenant tous les jours à afficher sur un mois (avec les jours du mois précédent / suivant à afficher en semi transparent).
  • getName(), retourne le nom du mois traduit via la méthode toLocaleString.
  • contains(date), retourne un booléen pour savoir si la date est incluse dans le mois. Cette méthode servira à appliquer une classe différente aux dates correspondant au mois sélectionné.
  • clone(date), clone une date afin d'éviter les effets de bords lors des mutations (setDate, setMonth...).
  • ::createMonthsForYear(year), génère un tableau contenant les 12 mois d'une années.
export default class Month {
  constructor (year, month) {
    this.year = year
    this.month = month
    this.start = new Date(this.year, this.month)
  }

  getDays () {
    let days = []
    let dayOfWeek = this.start.getDay() - 1
    if (dayOfWeek === -1) { dayOfWeek = 6 }
    if (dayOfWeek > 0) {
      for (let i = dayOfWeek; i > 0; i--) {
        let date = this.clone(this.start)
        date.setDate(i * -1 + 1)
        days.push(date)
      }
    }
    let end = this.clone(this.start)
    end.setMonth(end.getMonth() + 1)
    end.setDate(0)
    for (let i = 0; i < end.getDate(); ++i) {
      let date = this.clone(this.start)
      date.setDate(i + 1)
      days.push(date)
    }
    dayOfWeek = end.getDay() - 1
    if (dayOfWeek === -1) { dayOfWeek = 6 }
    if (dayOfWeek < 6) {
      for (let i = 0; i < (6 - dayOfWeek); i++) {
        let date = this.clone(end)
        date.setDate(end.getDate() + i + 1)
        days.push(date)
      }
    }
    return days
  }

  getName () {
    return this.start.toLocaleString('fr-fr', {month: 'long'})
  }

  contains (date) {
    return date.getMonth() === this.month
  }

  clone (date) {
    return new Date(date.getTime())
  }

  static createMonthsForYear (year) {
    let months = []
    for (let i = 0; i < 12; i++) {
      months.push(new Month(year, i))
    }
    return months
  }
}

Nous allons aussi avoir besoin de représenter les périodes qui seront crées par l'utilisateur. Une période sera caractérisée par sa date de démarrage et sa date de fin.

export default class Range {
  constructor (start, end) {
    this.start = start
    this.end = end
  }

  getStart () {
    return this.start
  }

  setStart (date) {
    if (date.getTime() > this.end.getTime()) {
      throw new Error('start > end')
    }
    this.start = date
  }

  getEnd () {
    return this.end
  }

  setEnd (date) {
    if (date.getTime() < this.start.getTime()) {
      throw new Error('end < start')
    }
    this.end = date
  }

  contains (range) {
    if (range instanceof Date) {
      let date = range
      return date.getTime() >= this.start.getTime() && date.getTime() <= this.end.getTime()
    } else if (range instanceof Range) {
      return range.getStart().getTime() > this.start.getTime() && range.getEnd().getTime() < this.end.getTime()
    } else {
      throw new Error('Type inconnu')
    }
  }

  intersect (range) {
    return this.contains(range.getStart()) ||
      this.contains(range.getEnd()) ||
      (range.getStart().getTime() < this.start.getTime() && range.getEnd().getTime() > this.end.getTime())
  }

  merge (range) {
    if (range.getStart().getTime() < this.start.getTime()) {
      this.setStart(range.getStart())
    }
    if (range.getEnd().getTime() > this.end.getTime()) {
      this.setEnd(range.getEnd())
    }
  }

  isStart (date) {
    return date.toDateString() === this.start.toDateString()
  }

  isEnd (date) {
    return date.toDateString() === this.end.toDateString()
  }
}

On créera aussi une classe Ranges qui permettra de représenter toutes les périodes. Cette classe nous permettra de savoir si une date donnée est dans une des période ou non.

Une fois que l'on a ces classes il est possible des les utiliser dans notres composants pour accélérer le développement. Par exemple pour obtenir les classes associées à un jour du mois :

{
  classForDay (day, month, newRange) {
    let classes = []
    let range = this.ranges.contains(day)
    if (range !== null) {
      classes.push('rangepicker_range')
      if (range.isStart(day)) { classes.push('rangepicker_range-start') }
      if (range.isEnd(day)) { classes.push('rangepicker_range-end') }
    }
    if (newRange !== null) {
      if (newRange.contains(day)) {
        classes = ['rangepicker_newrange']
        if (newRange.isStart(day)) { classes.push('rangepicker_range-start') }
        if (newRange.isEnd(day)) { classes.push('rangepicker_range-end') }
      }
    }
    if (!month.contains(day)) {
      classes.push('rangepicker_out')
    }
    return classes
  }
}