diff --git a/src/demo-app/datepicker/datepicker-demo.html b/src/demo-app/datepicker/datepicker-demo.html
index 331b77e28c2d..70a7cc1e6556 100644
--- a/src/demo-app/datepicker/datepicker-demo.html
+++ b/src/demo-app/datepicker/datepicker-demo.html
@@ -1,7 +1,6 @@
Work in progress, not ready for use.
-
-
+
{{selected?.toNativeDate()}}
diff --git a/src/demo-app/datepicker/datepicker-demo.scss b/src/demo-app/datepicker/datepicker-demo.scss
new file mode 100644
index 000000000000..c98cba687f8c
--- /dev/null
+++ b/src/demo-app/datepicker/datepicker-demo.scss
@@ -0,0 +1,3 @@
+md-calendar {
+ width: 300px;
+}
diff --git a/src/demo-app/datepicker/datepicker-demo.ts b/src/demo-app/datepicker/datepicker-demo.ts
index 38c4e8247c3c..240e721a5ef9 100644
--- a/src/demo-app/datepicker/datepicker-demo.ts
+++ b/src/demo-app/datepicker/datepicker-demo.ts
@@ -5,9 +5,11 @@ import {SimpleDate} from '@angular/material';
@Component({
moduleId: module.id,
selector: 'datepicker-demo',
- templateUrl: 'datepicker-demo.html'
+ templateUrl: 'datepicker-demo.html',
+ styleUrls: ['datepicker-demo.css'],
})
export class DatepickerDemo {
+ startAt = new SimpleDate(2017, 0, 1);
date = SimpleDate.today();
selected: SimpleDate;
}
diff --git a/src/lib/core/datetime/simple-date.ts b/src/lib/core/datetime/simple-date.ts
index 75a498425d57..abb904fbab7a 100644
--- a/src/lib/core/datetime/simple-date.ts
+++ b/src/lib/core/datetime/simple-date.ts
@@ -52,11 +52,21 @@ export class SimpleDate {
* Adds an amount of time (in days, months, and years) to the date.
* @param amount The amount of time to add.
*/
- add(amount: {days: number, months: number, years: number}): SimpleDate {
+ add(amount: {days?: number, months?: number, years?: number}): SimpleDate {
return new SimpleDate(
- this.year + amount.years || 0,
- this.month + amount.months || 0,
- this.date + amount.days || 0);
+ this.year + (amount.years || 0),
+ this.month + (amount.months || 0),
+ this.date + (amount.days || 0));
+ }
+
+ /**
+ * Compares this SimpleDate with another SimpleDate.
+ * @param other The other SimpleDate
+ * @returns 0 if the dates are equal, a number less than 0 if this date is earlier,
+ * a number greater than 0 if this date is greater.
+ */
+ compare(other: SimpleDate): number {
+ return this.year - other.year || this.month - other.month || this.date - other.date;
}
/** Converts the SimpleDate to a native JS Date object. */
diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html
new file mode 100644
index 000000000000..4c30a6bd9037
--- /dev/null
+++ b/src/lib/datepicker/calendar.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/datepicker/calendar.scss b/src/lib/datepicker/calendar.scss
new file mode 100644
index 000000000000..8192360fad9b
--- /dev/null
+++ b/src/lib/datepicker/calendar.scss
@@ -0,0 +1,36 @@
+$mat-calendar-arrow-size: 5px !default;
+
+.mat-calendar {
+ display: block;
+}
+
+.mat-calendar-header {
+ display: flex;
+}
+
+.mat-calendar-spacer {
+ flex: 1 1 auto;
+}
+
+.mat-calendar-button {
+ background: transparent;
+ padding: 0;
+ margin: 0;
+ border: none;
+ outline: none;
+}
+
+.mat-calendar-button > svg {
+ vertical-align: middle;
+}
+
+.mat-calendar-arrow {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ border-left: $mat-calendar-arrow-size solid transparent;
+ border-right: $mat-calendar-arrow-size solid transparent;
+ border-top: $mat-calendar-arrow-size solid;
+ margin: 0 $mat-calendar-arrow-size;
+ vertical-align: middle;
+}
diff --git a/src/lib/datepicker/calendar.spec.ts b/src/lib/datepicker/calendar.spec.ts
new file mode 100644
index 000000000000..27608c2b655b
--- /dev/null
+++ b/src/lib/datepicker/calendar.spec.ts
@@ -0,0 +1,128 @@
+import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import {MdDatepickerModule} from './index';
+import {Component} from '@angular/core';
+import {SimpleDate} from '../core/datetime/simple-date';
+import {MdCalendar} from './calendar';
+import {By} from '@angular/platform-browser';
+
+
+describe('MdCalendar', () => {
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdDatepickerModule],
+ declarations: [
+ StandardCalendar,
+ ],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ describe('standard calendar', () => {
+ let fixture: ComponentFixture;
+ let testComponent: StandardCalendar;
+ let calendarElement: HTMLElement;
+ let periodButton: HTMLElement;
+ let prevButton: HTMLElement;
+ let nextButton: HTMLElement;
+ let calendarInstance: MdCalendar;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StandardCalendar);
+ fixture.detectChanges();
+
+ let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar));
+ calendarElement = calendarDebugElement.nativeElement;
+ periodButton = calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
+ prevButton = calendarElement.querySelector('.mat-calendar-previous-button') as HTMLElement;
+ nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLElement;
+
+ calendarInstance = calendarDebugElement.componentInstance;
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should be in month view with specified month visible', () => {
+ expect(calendarInstance._monthView).toBe(true, 'should be in month view');
+ expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));
+ });
+
+ it('should toggle view when period clicked', () => {
+ expect(calendarInstance._monthView).toBe(true, 'should be in month view');
+
+ periodButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._monthView).toBe(false, 'should be in year view');
+
+ periodButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._monthView).toBe(true, 'should be in month view');
+ });
+
+ it('should go to next and previous month', () => {
+ expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));
+
+ nextButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 1, 1));
+
+ prevButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));
+ });
+
+ it('should go to previous and next year', () => {
+ periodButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._monthView).toBe(false, 'should be in year view');
+ expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));
+
+ nextButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1));
+
+ prevButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));
+ });
+
+ it('should go back to month view after selecting month in year view', () => {
+ periodButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._monthView).toBe(false, 'should be in year view');
+ expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1));
+
+ let monthCells = calendarElement.querySelectorAll('.mat-calendar-table-cell');
+ (monthCells[monthCells.length - 1] as HTMLElement).click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._monthView).toBe(true, 'should be in month view');
+ expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 11, 1));
+ expect(testComponent.selected).toBeFalsy('no date should be selected yet');
+ });
+
+ it('should select date in month view', () => {
+ let monthCells = calendarElement.querySelectorAll('.mat-calendar-table-cell');
+ (monthCells[monthCells.length - 1] as HTMLElement).click();
+ fixture.detectChanges();
+
+ expect(calendarInstance._monthView).toBe(true, 'should be in month view');
+ expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 31));
+ });
+ });
+});
+
+
+@Component({
+ template: ``
+})
+class StandardCalendar {
+ selected: SimpleDate;
+}
diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts
new file mode 100644
index 000000000000..cd840713e05f
--- /dev/null
+++ b/src/lib/datepicker/calendar.ts
@@ -0,0 +1,108 @@
+import {
+ ChangeDetectionStrategy,
+ ViewEncapsulation,
+ Component,
+ Input,
+ AfterContentInit, Output, EventEmitter
+} from '@angular/core';
+import {SimpleDate} from '../core/datetime/simple-date';
+import {CalendarLocale} from '../core/datetime/calendar-locale';
+
+
+/**
+ * A calendar that is used as part of the datepicker.
+ * @docs-private
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'md-calendar',
+ templateUrl: 'calendar.html',
+ styleUrls: ['calendar.css'],
+ host: {
+ '[class.mat-calendar]': 'true',
+ },
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class MdCalendar implements AfterContentInit {
+ /** A date representing the period (month or year) to start the calendar in. */
+ @Input()
+ get startAt() {return this._startAt; }
+ set startAt(value: any) { this._startAt = this._locale.parseDate(value); }
+ private _startAt: SimpleDate;
+
+ /** Whether the calendar should be started in month or year view. */
+ @Input() startView: 'month' | 'year' = 'month';
+
+ /** The currently selected date. */
+ @Input()
+ get selected() { return this._selected; }
+ set selected(value: any) { this._selected = this._locale.parseDate(value); }
+ private _selected: SimpleDate;
+
+ /** Emits when the currently selected date changes. */
+ @Output() selectedChange = new EventEmitter();
+
+ /**
+ * A date representing the current period shown in the calendar. The current period is always
+ * normalized to the 1st of a month, this prevents date overflow issues (e.g. adding a month to
+ * January 31st and overflowing into March).
+ */
+ get _currentPeriod() { return this._normalizedCurrentPeriod; }
+ set _currentPeriod(value: SimpleDate) {
+ this._normalizedCurrentPeriod = new SimpleDate(value.year, value.month, 1);
+ }
+ private _normalizedCurrentPeriod: SimpleDate;
+
+ /** Whether the calendar is in month view. */
+ _monthView: boolean;
+
+ /** The names of the weekdays. */
+ _weekdays: string[];
+
+ /** The label for the current calendar view. */
+ get _label(): string {
+ return this._monthView ? this._locale.getCalendarMonthHeaderLabel(this._currentPeriod) :
+ this._locale.getCalendarYearHeaderLabel(this._currentPeriod);
+ }
+
+ constructor(private _locale: CalendarLocale) {
+ this._weekdays = this._locale.narrowDays.slice(this._locale.firstDayOfWeek)
+ .concat(this._locale.narrowDays.slice(0, this._locale.firstDayOfWeek));
+ }
+
+ ngAfterContentInit() {
+ this._currentPeriod = this.startAt || SimpleDate.today();
+ this._monthView = this.startView != 'year';
+ }
+
+ /** Handles date selection in the month view. */
+ _dateSelected(date: SimpleDate) {
+ if ((!date || !this.selected) && date != this.selected || date.compare(this.selected)) {
+ this.selectedChange.emit(date);
+ }
+ }
+
+ /** Handles month selection in the year view. */
+ _monthSelected(month: SimpleDate) {
+ this._currentPeriod = month;
+ this._monthView = true;
+ }
+
+ /** Handles user clicks on the period label. */
+ _currentPeriodClicked() {
+ this._monthView = !this._monthView;
+ }
+
+ /** Handles user clicks on the previous button. */
+ _previousClicked() {
+ let amount = this._monthView ? {months: -1} : {years: -1};
+ this._currentPeriod = this._currentPeriod.add(amount);
+ }
+
+ /** Handles user clicks on the next button. */
+ _nextClicked() {
+ let amount = this._monthView ? {months: 1} : {years: 1};
+ this._currentPeriod = this._currentPeriod.add(amount);
+ }
+}
diff --git a/src/lib/datepicker/index.ts b/src/lib/datepicker/index.ts
index 9bea0410509a..5f6c2f471738 100644
--- a/src/lib/datepicker/index.ts
+++ b/src/lib/datepicker/index.ts
@@ -8,8 +8,10 @@ import {OverlayModule} from '../core/overlay/overlay-directives';
import {MdDatepicker} from './datepicker';
import {MdDatepickerInput} from './datepicker-input';
import {MdDialogModule} from '../dialog/index';
+import {MdCalendar} from './calendar';
+export * from './calendar';
export * from './calendar-table';
export * from './datepicker';
export * from './datepicker-input';
@@ -19,7 +21,7 @@ export * from './year-view';
@NgModule({
imports: [CommonModule, DatetimeModule, MdDialogModule, OverlayModule],
- exports: [MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
- declarations: [MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
+ exports: [MdCalendar, MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
+ declarations: [MdCalendar, MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
})
export class MdDatepickerModule {}