Open BUONJG opened 3 years ago
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
Hello,
Is that possible to display weeknumber in datepicker like we have in google calendar?![image](https://user-images.githubusercontent.com/898114/92723631-a09d4b00-f369-11ea-8cc0-8cc765b061fb.png)
Regards,