angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.22k stars 6.69k forks source link

Display week number in datepicker component #20530

Open BUONJG opened 3 years ago

BUONJG commented 3 years ago

Hello,

Is that possible to display weeknumber in datepicker like we have in google calendar? image

Regards,

crisbeto commented 3 years ago

We don't support this at the moment, but it sounds like a reasonable feature to have.

BUONJG commented 3 years ago

Many thanks for your reply. Do you have an idea when it could be available?

cluen commented 3 years ago

Do you have any news on this topic @crisbeto ? This is a feature we also need for our project.

jesben commented 2 years ago

I needed week numbers so I made this quick and DIRTY solution for a week picker. Sorry for not sharing clean code, but maybe it will inspire others. Looking forward to see week numbers and timepicker becoming part of Angular Material ;)

WeekPickerAnimation

.ts

import {
  Injectable, Component, OnInit, AfterViewInit, Input, ChangeDetectionStrategy,
  ChangeDetectorRef, Inject, OnDestroy
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import { MomentDateAdapter, MAT_MOMENT_DATE_ADAPTER_OPTIONS, MAT_MOMENT_DATE_FORMATS } from '@angular/material-moment-adapter';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatDateFormats } from '@angular/material/core';
import {
  MatDateRangeSelectionStrategy,
  DateRange,
  MAT_DATE_RANGE_SELECTION_STRATEGY,
} from '@angular/material/datepicker';
import { MatCalendar } from '@angular/material/datepicker';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import * as moment from 'moment';
import { extendMoment } from 'moment-range';

declare var jQuery: any;

@Injectable()
export class WeekSelectionStrategy<D> implements MatDateRangeSelectionStrategy<D> {
  constructor(private _dateAdapter: DateAdapter<D>) { }

  selectionFinished(date: D | null): DateRange<D> {
    return this._createWeekRange(date);
  }

  createPreview(activeDate: D | null): DateRange<D> {
    return this._createWeekRange(activeDate);
  }

  private _createWeekRange(date: D | null): DateRange<D> {
    if (date) {
      // Week
      //const startDays = moment(date).diff(moment(date).startOf('week'), 'days');
      //const endDays = moment(date).diff(moment(date).endOf('week'), 'days');
      // ISO week
      const startDays = moment(date).diff(moment(date).startOf('isoWeek'), 'days');
      const endDays = moment(date).diff(moment(date).endOf('isoWeek'), 'days');

      const start = this._dateAdapter.addCalendarDays(date, -Math.abs(startDays));
      const end = this._dateAdapter.addCalendarDays(date, Math.abs(endDays));

      return new DateRange<D>(start, end);
    }

    return new DateRange<D>(null, null);
  }
}

/** Custom header component for datepicker. */
@Component({
  selector: 'custom-calendar-header',
  styles: [`
    .custom-calendar-header {
      display: flex;
      align-items: center;
      padding: 0.5em;
      background-color: #ffffff;
    }

    .custom-calendar-header-label {
      flex: 1;
      height: 1em;
      font-weight: 500;
      text-align: center;
    }

    .example-double-arrow .mat-icon {
      margin: -22%;
    }
  `],
  template: `
    <div class="custom-calendar-header">
      <button mat-icon-button class="example-double-arrow" (click)="previousClicked('year')">
        <mat-icon>keyboard_arrow_left</mat-icon>
        <mat-icon>keyboard_arrow_left</mat-icon>
      </button>
      <button mat-icon-button (click)="previousClicked('month')">
        <mat-icon>keyboard_arrow_left</mat-icon>
      </button>
      <span class="custom-calendar-header-label">{{periodLabel}}</span>
      <button mat-icon-button (click)="nextClicked('month')">
        <mat-icon>keyboard_arrow_right</mat-icon>
      </button>
      <button mat-icon-button class="example-double-arrow" (click)="nextClicked('year')">
        <mat-icon>keyboard_arrow_right</mat-icon>
        <mat-icon>keyboard_arrow_right</mat-icon>
      </button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomCalendarHeaderComponent<D> implements OnDestroy, AfterViewInit {
  private _destroyed = new Subject<void>();

  constructor(
    private _calendar: MatCalendar<D>, private _dateAdapter: DateAdapter<D>,
    @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, cdr: ChangeDetectorRef) {
    _calendar.stateChanges
      .pipe(takeUntil(this._destroyed))
      .subscribe(() => cdr.markForCheck());
  }

  ngAfterViewInit() {
    setTimeout(() => {
      this.appendWeekNumbers();
    }, 0);
  }

  ngOnDestroy() {
    this._destroyed.next();
    this._destroyed.complete();
  }

  get periodLabel() {
    return this._dateAdapter
      .format(this._calendar.activeDate, this._dateFormats.display.monthYearLabel)
      .toLocaleUpperCase();
  }

  previousClicked(mode: 'month' | 'year') {
    this._calendar.activeDate = mode === 'month' ?
      this._dateAdapter.addCalendarMonths(this._calendar.activeDate, -1) :
      this._dateAdapter.addCalendarYears(this._calendar.activeDate, -1);

    setTimeout(() => {
      this.appendWeekNumbers();
    }, 0);
  }

  nextClicked(mode: 'month' | 'year') {
    this._calendar.activeDate = mode === 'month' ?
      this._dateAdapter.addCalendarMonths(this._calendar.activeDate, 1) :
      this._dateAdapter.addCalendarYears(this._calendar.activeDate, 1);

    setTimeout(() => {
      this.appendWeekNumbers();
    }, 0);
  }

  appendWeekNumbers() {
    const { range } = extendMoment(moment);
    const firstDay = moment(this._calendar.activeDate).startOf('month')
    const endDay = moment(this._calendar.activeDate).endOf('month')
    const monthRange = range(firstDay, endDay);
    const weeks = [];
    const days = Array.from(monthRange.by('day'));
    days.forEach(day => {
      // Week
      //if (!weeks.includes(day.week())) {
      //  weeks.push(day.week());
      //}
      // ISO week
      if (!weeks.includes(day.isoWeek())) {
        weeks.push(day.isoWeek());
      }
    })
    //console.log(weeks);

    if (!document.getElementById('weekNumberWrapper')) {
      jQuery(".mat-datepicker-content").css("box-shadow", 'none');
      //jQuery(".custom-calendar-header").css("margin-left", '-40px');

      // .mat-datepicker-content | mat-calendar-content
      jQuery(".mat-datepicker-content").wrap('<div id="weekNumberWrapper" class="d-flex flex-row" style="background-color: #ffffff; box-shadow: 0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);"><div></div></div>');
      jQuery("#weekNumberWrapper").prepend('<div id="weekNumberContent" style="padding-top: 92px; border-right: solid 1px #eeeeee; color: rgba(0, 0, 0, 0.87); background-color: #eeeeee;"><table id="weekNumberTable"></table></div>');
    }

    if (document.getElementById('weekNumberTable')) {
      const rows = [];

      const widthAndheight = jQuery('.mat-calendar-body tr').eq(0).outerHeight();

      if (jQuery('.mat-calendar-body tr').eq(0).children('td').length === 1) {
        rows.push(`<tr><td style="width: ${widthAndheight}px; height: ${widthAndheight}px;"></td></tr>`);
      }

      for (let i = 0; i < weeks.length; i++) {
        rows.push(`<tr><td style="width: ${widthAndheight}px; height: ${widthAndheight}px; text-align: center; font-weight: 500;">${weeks[i]}</td></tr>`);
      }

      jQuery("#weekNumberTable").html(rows.join(''));
    }
  }
}

export const DATEPICKER_FORMATS = {
  parse: {
    dateInput: 'DD-MM-YYYY',
  },
  display: {
    dateInput: 'DD-MM-YYYY',
    monthYearLabel: 'MMM YYYY',
    dateA11yLabel: 'LL',
    monthYearA11yLabel: 'MMMM YYYY',
  },
};

@Component({
  selector: 'app-custom-datepicker',
  templateUrl: './custom-datepicker.component.html',
  styleUrls: ['./custom-datepicker.component.scss'],
  providers: [
    { provide: MAT_DATE_LOCALE, useValue: 'da' },
    {
      provide: DateAdapter,
      useClass: MomentDateAdapter,
      deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS]
    },
    //{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
    { provide: MAT_DATE_FORMATS, useValue: DATEPICKER_FORMATS },
    {
      provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
      useClass: WeekSelectionStrategy
    }
  ],
})
export class CustomDatepickerComponent implements OnInit {

  customCalendarHeaderComponent = CustomCalendarHeaderComponent;

  @Input() field: any;

  isTouchDevice = false;

  ngOnInit() {
    this.isTouchDevice = this.checkIfTouchDevice();
  }

  checkIfTouchDevice() {
    return (('ontouchstart' in window) ||
      (navigator.maxTouchPoints > 0) ||
      (navigator.msMaxTouchPoints > 0));
  }

  getWeekNumber(date: any) {
    // Week
    //return date ? moment(date).week() : '';
    // ISO week
    return date ? moment(date).isoWeek() : '';
  }

}

.html

<mat-form-field *ngSwitchCase="'week'" class="{{field?.cssClasses ? field?.cssClasses : ''}}">
    <mat-label>Uge {{getWeekNumber(field.controlStart.value)}}
    </mat-label>
    <mat-date-range-input [rangePicker]="picker">
        <input matStartDate [formControl]="field.controlStart" (focus)="picker.open()" (click)="picker.open()">
        <input matEndDate [formControl]="field.controlEnd" (focus)="picker.open()" (click)="picker.open()">
    </mat-date-range-input>
    <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
    <mat-date-range-picker #picker [touchUi]="isTouchDevice"
        [calendarHeaderComponent]="customCalendarHeaderComponent">
    </mat-date-range-picker>
</mat-form-field>

.scss:

// Date range preview
::ng-deep .mat-calendar-body-in-preview {
    color: #69d01b !important;
    background-color: #69d01b !important;
}
angular-robot[bot] commented 2 years ago

Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends.

Find more details about Angular's feature request process in our documentation.

umargd commented 1 year ago

I needed week numbers so I made this quick and DIRTY solution for a week picker. Sorry for not sharing clean code, but maybe it will inspire others. Looking forward to see week numbers and timepicker becoming part of Angular Material ;)

WeekPickerAnimation WeekPickerAnimation

.ts

import {
  Injectable, Component, OnInit, AfterViewInit, Input, ChangeDetectionStrategy,
  ChangeDetectorRef, Inject, OnDestroy
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import { MomentDateAdapter, MAT_MOMENT_DATE_ADAPTER_OPTIONS, MAT_MOMENT_DATE_FORMATS } from '@angular/material-moment-adapter';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatDateFormats } from '@angular/material/core';
import {
  MatDateRangeSelectionStrategy,
  DateRange,
  MAT_DATE_RANGE_SELECTION_STRATEGY,
} from '@angular/material/datepicker';
import { MatCalendar } from '@angular/material/datepicker';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import * as moment from 'moment';
import { extendMoment } from 'moment-range';

declare var jQuery: any;

@Injectable()
export class WeekSelectionStrategy<D> implements MatDateRangeSelectionStrategy<D> {
  constructor(private _dateAdapter: DateAdapter<D>) { }

  selectionFinished(date: D | null): DateRange<D> {
    return this._createWeekRange(date);
  }

  createPreview(activeDate: D | null): DateRange<D> {
    return this._createWeekRange(activeDate);
  }

  private _createWeekRange(date: D | null): DateRange<D> {
    if (date) {
      // Week
      //const startDays = moment(date).diff(moment(date).startOf('week'), 'days');
      //const endDays = moment(date).diff(moment(date).endOf('week'), 'days');
      // ISO week
      const startDays = moment(date).diff(moment(date).startOf('isoWeek'), 'days');
      const endDays = moment(date).diff(moment(date).endOf('isoWeek'), 'days');

      const start = this._dateAdapter.addCalendarDays(date, -Math.abs(startDays));
      const end = this._dateAdapter.addCalendarDays(date, Math.abs(endDays));

      return new DateRange<D>(start, end);
    }

    return new DateRange<D>(null, null);
  }
}

/** Custom header component for datepicker. */
@Component({
  selector: 'custom-calendar-header',
  styles: [`
    .custom-calendar-header {
      display: flex;
      align-items: center;
      padding: 0.5em;
      background-color: #ffffff;
    }

    .custom-calendar-header-label {
      flex: 1;
      height: 1em;
      font-weight: 500;
      text-align: center;
    }

    .example-double-arrow .mat-icon {
      margin: -22%;
    }
  `],
  template: `
    <div class="custom-calendar-header">
      <button mat-icon-button class="example-double-arrow" (click)="previousClicked('year')">
        <mat-icon>keyboard_arrow_left</mat-icon>
        <mat-icon>keyboard_arrow_left</mat-icon>
      </button>
      <button mat-icon-button (click)="previousClicked('month')">
        <mat-icon>keyboard_arrow_left</mat-icon>
      </button>
      <span class="custom-calendar-header-label">{{periodLabel}}</span>
      <button mat-icon-button (click)="nextClicked('month')">
        <mat-icon>keyboard_arrow_right</mat-icon>
      </button>
      <button mat-icon-button class="example-double-arrow" (click)="nextClicked('year')">
        <mat-icon>keyboard_arrow_right</mat-icon>
        <mat-icon>keyboard_arrow_right</mat-icon>
      </button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomCalendarHeaderComponent<D> implements OnDestroy, AfterViewInit {
  private _destroyed = new Subject<void>();

  constructor(
    private _calendar: MatCalendar<D>, private _dateAdapter: DateAdapter<D>,
    @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, cdr: ChangeDetectorRef) {
    _calendar.stateChanges
      .pipe(takeUntil(this._destroyed))
      .subscribe(() => cdr.markForCheck());
  }

  ngAfterViewInit() {
    setTimeout(() => {
      this.appendWeekNumbers();
    }, 0);
  }

  ngOnDestroy() {
    this._destroyed.next();
    this._destroyed.complete();
  }

  get periodLabel() {
    return this._dateAdapter
      .format(this._calendar.activeDate, this._dateFormats.display.monthYearLabel)
      .toLocaleUpperCase();
  }

  previousClicked(mode: 'month' | 'year') {
    this._calendar.activeDate = mode === 'month' ?
      this._dateAdapter.addCalendarMonths(this._calendar.activeDate, -1) :
      this._dateAdapter.addCalendarYears(this._calendar.activeDate, -1);

    setTimeout(() => {
      this.appendWeekNumbers();
    }, 0);
  }

  nextClicked(mode: 'month' | 'year') {
    this._calendar.activeDate = mode === 'month' ?
      this._dateAdapter.addCalendarMonths(this._calendar.activeDate, 1) :
      this._dateAdapter.addCalendarYears(this._calendar.activeDate, 1);

    setTimeout(() => {
      this.appendWeekNumbers();
    }, 0);
  }

  appendWeekNumbers() {
    const { range } = extendMoment(moment);
    const firstDay = moment(this._calendar.activeDate).startOf('month')
    const endDay = moment(this._calendar.activeDate).endOf('month')
    const monthRange = range(firstDay, endDay);
    const weeks = [];
    const days = Array.from(monthRange.by('day'));
    days.forEach(day => {
      // Week
      //if (!weeks.includes(day.week())) {
      //  weeks.push(day.week());
      //}
      // ISO week
      if (!weeks.includes(day.isoWeek())) {
        weeks.push(day.isoWeek());
      }
    })
    //console.log(weeks);

    if (!document.getElementById('weekNumberWrapper')) {
      jQuery(".mat-datepicker-content").css("box-shadow", 'none');
      //jQuery(".custom-calendar-header").css("margin-left", '-40px');

      // .mat-datepicker-content | mat-calendar-content
      jQuery(".mat-datepicker-content").wrap('<div id="weekNumberWrapper" class="d-flex flex-row" style="background-color: #ffffff; box-shadow: 0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);"><div></div></div>');
      jQuery("#weekNumberWrapper").prepend('<div id="weekNumberContent" style="padding-top: 92px; border-right: solid 1px #eeeeee; color: rgba(0, 0, 0, 0.87); background-color: #eeeeee;"><table id="weekNumberTable"></table></div>');
    }

    if (document.getElementById('weekNumberTable')) {
      const rows = [];

      const widthAndheight = jQuery('.mat-calendar-body tr').eq(0).outerHeight();

      if (jQuery('.mat-calendar-body tr').eq(0).children('td').length === 1) {
        rows.push(`<tr><td style="width: ${widthAndheight}px; height: ${widthAndheight}px;"></td></tr>`);
      }

      for (let i = 0; i < weeks.length; i++) {
        rows.push(`<tr><td style="width: ${widthAndheight}px; height: ${widthAndheight}px; text-align: center; font-weight: 500;">${weeks[i]}</td></tr>`);
      }

      jQuery("#weekNumberTable").html(rows.join(''));
    }
  }
}

export const DATEPICKER_FORMATS = {
  parse: {
    dateInput: 'DD-MM-YYYY',
  },
  display: {
    dateInput: 'DD-MM-YYYY',
    monthYearLabel: 'MMM YYYY',
    dateA11yLabel: 'LL',
    monthYearA11yLabel: 'MMMM YYYY',
  },
};

@Component({
  selector: 'app-custom-datepicker',
  templateUrl: './custom-datepicker.component.html',
  styleUrls: ['./custom-datepicker.component.scss'],
  providers: [
    { provide: MAT_DATE_LOCALE, useValue: 'da' },
    {
      provide: DateAdapter,
      useClass: MomentDateAdapter,
      deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS]
    },
    //{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
    { provide: MAT_DATE_FORMATS, useValue: DATEPICKER_FORMATS },
    {
      provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
      useClass: WeekSelectionStrategy
    }
  ],
})
export class CustomDatepickerComponent implements OnInit {

  customCalendarHeaderComponent = CustomCalendarHeaderComponent;

  @Input() field: any;

  isTouchDevice = false;

  ngOnInit() {
    this.isTouchDevice = this.checkIfTouchDevice();
  }

  checkIfTouchDevice() {
    return (('ontouchstart' in window) ||
      (navigator.maxTouchPoints > 0) ||
      (navigator.msMaxTouchPoints > 0));
  }

  getWeekNumber(date: any) {
    // Week
    //return date ? moment(date).week() : '';
    // ISO week
    return date ? moment(date).isoWeek() : '';
  }

}

.html

<mat-form-field *ngSwitchCase="'week'" class="{{field?.cssClasses ? field?.cssClasses : ''}}">
    <mat-label>Uge {{getWeekNumber(field.controlStart.value)}}
    </mat-label>
    <mat-date-range-input [rangePicker]="picker">
        <input matStartDate [formControl]="field.controlStart" (focus)="picker.open()" (click)="picker.open()">
        <input matEndDate [formControl]="field.controlEnd" (focus)="picker.open()" (click)="picker.open()">
    </mat-date-range-input>
    <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
    <mat-date-range-picker #picker [touchUi]="isTouchDevice"
        [calendarHeaderComponent]="customCalendarHeaderComponent">
    </mat-date-range-picker>
</mat-form-field>

.scss:

// Date range preview
::ng-deep .mat-calendar-body-in-preview {
    color: #69d01b !important;
    background-color: #69d01b !important;
}

Please can you send Clean and file wise code , Appreciated in advance. Thanks

daninaydenow commented 1 year ago

Hello, in 2023 this feature is still much needed. https://github.com/angular/components/issues/22910 this issue was closed without any implementation on the topic. Hopefully the date-picker will get an update with that feature.

micha5strings commented 12 months ago

Another way to show iso weeks without jQuery: Make use of the dateClass.

dateClass: MatCalendarCellClassFunction<Date> = (cellDate: any, view) => {
        if (view === 'month') {
            const day = cellDate.day();
            let prefix = '';
            if (day === 1 || cellDate.date() === 1) {
                if (day !== 1 && cellDate.date() === 1) {
                    prefix = 'date-indent-' + cellDate.day() + ' ';
                }
                prefix += 'show-iso-week iso-week-' + cellDate.isoWeek() + ' ';
            }
            if (day === 6) return prefix + 'date-saturday';
            if (day === 0) return prefix + 'date-sunday';
            if (this.holidayService.isHoliday(cellDate)) return prefix + 'date-holiday';
            return prefix;
        }
        return '';
    };

this.holidayService is in no way mandatory for this to work. It's just a service to determine if the given date is a holiday and can be commented out.

Add some CSS and you have week numbers :)

Bildschirmfoto 2023-07-10 um 16 34 04

My SCSS: week-hack.scss.zip