components
components copied to clipboard
Display week number in datepicker component
Hello,
Is that possible to display weeknumber in datepicker like we have in google calendar?

Regards,
We don't support this at the moment, but it sounds like a reasonable feature to have.
Many thanks for your reply. Do you have an idea when it could be available?
Do you have any news on this topic @crisbeto ? This is a feature we also need for our project.
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 ;)

.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;
}
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.
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 ;)
.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
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.
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 :)
My SCSS: week-hack.scss.zip
The solution by @micha5strings is almost good. But there is a bug today of all days. 😄 date() === 1 is causing issues, and week numbers are all over the place. Here is the fixed code:
dateClass: MatCalendarCellClassFunction<Date> = (cellDate, view) => {
if (view === 'month') {
const day = dayjs(cellDate).day();
const isoWeekNumber = dayjs(cellDate).isoWeek();
const date = dayjs(cellDate).date();
// Find the first day of the month
const startOfMonth = dayjs(cellDate).startOf('month');
const startDayOfWeek = startOfMonth.day();
let prefix = '';
// Add the week number class for the first visible Monday or start of the month
if (day === 1) {
prefix = 'show-iso-week iso-week-' + isoWeekNumber + ' ';
}
// Add the week number class for the midweeks and date indent class
if (date <= 7 && day === startDayOfWeek) {
prefix = 'show-iso-week iso-week-' + isoWeekNumber + ' date-indent-' + day + ' ';
}
// Add specific classes for Saturdays and Sundays
if (day === 6) return prefix + 'date-saturday';
if (day === 0) return prefix + 'date-sunday';
return prefix;
}
return '';
};
For @micha5strings and @paradox37 answer. They are working well thanks for the answers. If you don't want to re-write every month and using scss you can use something like this in it:
@for $i from 1 through 53 {
.iso-week-#{$i} {
::before {
@if($i< 10) {
content: "0#{$i}";
}
@else {
content: "#{$i}";
}
}
}
}
Here I changed the code a little bit. With this, you can adjust the firstDayOfWeek if you don't want to use Monday.
// Add the week number class for the first visible Monday or start of the month
if (day === firstDayOfWeek) {
prefix = 'show-iso-week iso-week-' + isoWeekNumber + ' ';
}
if (date <= 7 && day === startDayOfWeek) {
// Add the week number class for the midweeks and date indent class
let indentValue = (day + (7 - firstDayOfWeek)) % 7;
prefix = 'show-iso-week iso-week-' + isoWeekNumber + ' date-indent-' + indent value + ' ';
}
This is now available as part of Angular Material extensions !! No hacks needed !!
You can style it similar to google calendar like this:
/* example style, obviously don't use red */
.mtx-calendar-body-week-number {
background-color: red;
}
tr:last-child .mtx-calendar-body-week-number {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
tr:first-child .mtx-calendar-body-week-number {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
https://github.com/ng-matero/extensions/issues/393
Enjoy and send thanks to @nzbin because it wouldn't be possible without his work. Together we solved this 9 year old issue in just a few hours and I'm not even angular guy. Insane.