Skip to content

Commit

Permalink
feat(datepicker): create the md-datepicker component (#3024)
Browse files Browse the repository at this point in the history
* date picker initial commit

* month view

* added month view functionality

* more month view tweaking

* started extracting common stuff to calendar-table.

* base month view on calendar table

* added year view

* add disclaimers

* addressed comments

* fix lint

* fixed aot and added comment

* started on tests

* calendar table tests

* tests for month and year view

* rebase on top of CalendarLocale & SimpleDate

* add some additional functionality to SimpleDate

* add md-datepicker and input[mdDatepicker]

* Add touch UI support

* fix some stuff that got messed up in rebasing

* addressed comments

* move position strategy to separate method

* added tests for error cases
  • Loading branch information
mmalerba committed Apr 14, 2017
1 parent 6d4ece4 commit 4456641
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 3 deletions.
5 changes: 5 additions & 0 deletions src/demo-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ <h1>Work in progress, not ready for use.</h1>

<br>
<div>{{selected?.toNativeDate()}}</div>

<input [mdDatepicker]="dp">
<button (click)="dp.openStandardUi()">open</button>
<button (click)="dp.openTouchUi()">open (mobile)</button>
<md-datepicker #dp></md-datepicker>
23 changes: 23 additions & 0 deletions src/lib/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Directive, ElementRef, Input} from '@angular/core';
import {MdDatepicker} from './datepicker';


/** Directive used to connect an input to a MdDatepicker. */
@Directive({
selector: 'input[mdDatepicker], input[matDatepicker]',
})
export class MdDatepickerInput {
@Input()
set mdDatepicker(value: MdDatepicker) {
if (value) {
this._datepicker = value;
this._datepicker._registerInput(this._elementRef);
}
}
private _datepicker: MdDatepicker;

@Input()
set matDatepicker(value: MdDatepicker) { this.mdDatepicker = value; }

constructor(private _elementRef: ElementRef) {}
}
3 changes: 3 additions & 0 deletions src/lib/datepicker/datepicker.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
I'm a datepicker (touch ui = {{touchUi}}).
</template>
124 changes: 124 additions & 0 deletions src/lib/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import {MdDatepickerModule} from './index';
import {Component, ViewChild} from '@angular/core';
import {MdDatepicker} from './datepicker';

describe('MdDatepicker', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdDatepickerModule],
declarations: [
StandardDatepicker,
MultiInputDatepicker,
NoInputDatepicker,
],
});

TestBed.compileComponents();
}));

describe('standard datepicker', () => {
let fixture: ComponentFixture<StandardDatepicker>;
let testComponent: StandardDatepicker;

beforeEach(() => {
fixture = TestBed.createComponent(StandardDatepicker);
fixture.detectChanges();

testComponent = fixture.componentInstance;
});

it('openStandardUi should open popup', () => {
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();

testComponent.datepicker.openStandardUi();
fixture.detectChanges();

expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
});

it('openTouchUi should open dialog', () => {
expect(document.querySelector('md-dialog-container')).toBeNull();

testComponent.datepicker.openTouchUi();
fixture.detectChanges();

expect(document.querySelector('md-dialog-container')).not.toBeNull();
});

it('close should close popup', () => {
testComponent.datepicker.openStandardUi();
fixture.detectChanges();

let popup = document.querySelector('.cdk-overlay-pane');
expect(popup).not.toBeNull();
expect(parseInt(getComputedStyle(popup).height)).not.toBe(0);

testComponent.datepicker.close();
fixture.detectChanges();

expect(parseInt(getComputedStyle(popup).height)).toBe(0);
});

it('close should close dialog', () => {
testComponent.datepicker.openTouchUi();
fixture.detectChanges();

expect(document.querySelector('md-dialog-container')).not.toBeNull();

testComponent.datepicker.close();
fixture.detectChanges();

expect(document.querySelector('md-dialog-container')).toBeNull();
});
});

describe('datepicker with too many inputs', () => {
it('should throw when multiple inputs registered', () => {
let fixture = TestBed.createComponent(MultiInputDatepicker);
expect(() => fixture.detectChanges()).toThrow();
});
});

describe('datepicker with no inputs', () => {
let fixture: ComponentFixture<NoInputDatepicker>;
let testComponent: NoInputDatepicker;

beforeEach(() => {
fixture = TestBed.createComponent(NoInputDatepicker);
fixture.detectChanges();

testComponent = fixture.componentInstance;
});

it('should throw when opened with no registered inputs', () => {
expect(() => testComponent.datepicker.openStandardUi()).toThrow();
});
});
});


@Component({
template: `<input [mdDatepicker]="d"><md-datepicker #d></md-datepicker>`,
})
class StandardDatepicker {
@ViewChild('d') datepicker: MdDatepicker;
}


@Component({
template: `
<input [mdDatepicker]="d"><input [mdDatepicker]="d"><md-datepicker #d></md-datepicker>
`,
})
class MultiInputDatepicker {
@ViewChild('d') datepicker: MdDatepicker;
}


@Component({
template: `<md-datepicker #d></md-datepicker>`,
})
class NoInputDatepicker {
@ViewChild('d') datepicker: MdDatepicker;
}
159 changes: 159 additions & 0 deletions src/lib/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {
Component,
TemplateRef,
ViewChild,
ViewEncapsulation,
ChangeDetectionStrategy,
ViewContainerRef,
Optional,
ElementRef,
OnDestroy
} from '@angular/core';
import {Overlay} from '../core/overlay/overlay';
import {OverlayRef} from '../core/overlay/overlay-ref';
import {TemplatePortal} from '../core/portal/portal';
import {OverlayState} from '../core/overlay/overlay-state';
import {Dir} from '../core/rtl/dir';
import {MdError} from '../core/errors/error';
import {MdDialog} from '../dialog/dialog';
import {MdDialogRef} from '../dialog/dialog-ref';
import {PositionStrategy} from '../core/overlay/position/position-strategy';
import {
OriginConnectionPosition,
OverlayConnectionPosition
} from '../core/overlay/position/connected-position';


// TODO(mmalerba): Figure out what the real width should be.
const CALENDAR_POPUP_WIDTH = 300;


/** Component responsible for managing the datepicker popup/dialog. */
@Component({
moduleId: module.id,
selector: 'md-datepicker, mat-datepicker',
templateUrl: 'datepicker.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdDatepicker implements OnDestroy {
/**
* Whether the calendar UI is in touch mode. In touch mode the calendar opens in a dialog rather
* than a popup and elements have more padding to allow for bigger touch targets.
*/
touchUi: boolean;

/** The calendar template. */
@ViewChild(TemplateRef) calendarTemplate: TemplateRef<any>;

/** A reference to the overlay when the calendar is opened as a popup. */
private _popupRef: OverlayRef;

/** A reference to the dialog when the calendar is opened as a dialog. */
private _dialogRef: MdDialogRef<any>;

/** A portal containing the calendar for this datepicker. */
private _calendarPortal: TemplatePortal;

/** The input element this datepicker is associated with. */
private _inputElementRef: ElementRef;

constructor(private _dialog: MdDialog, private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef, @Optional() private _dir: Dir) {}

ngOnDestroy() {
this.close();
if (this._popupRef) {
this._popupRef.dispose();
}
}

/**
* Register an input with this datepicker.
* @param inputElementRef An ElementRef for the input.
*/
_registerInput(inputElementRef: ElementRef): void {
if (this._inputElementRef) {
throw new MdError('An MdDatepicker can only be associated with a single input.');
}
this._inputElementRef = inputElementRef;
}

/** Opens the calendar in standard UI mode. */
openStandardUi(): void {
this._open();
}

/** Opens the calendar in touch UI mode. */
openTouchUi(): void {
this._open(true);
}

/**
* Open the calendar.
* @param touchUi Whether to use the touch UI.
*/
private _open(touchUi = false): void {
if (!this._inputElementRef) {
throw new MdError('Attempted to open an MdDatepicker with no associated input.');
}

if (!this._calendarPortal) {
this._calendarPortal = new TemplatePortal(this.calendarTemplate, this._viewContainerRef);
}

this.touchUi = touchUi;
touchUi ? this._openAsDialog() : this._openAsPopup();
}

/** Close the calendar. */
close(): void {
if (this._popupRef && this._popupRef.hasAttached()) {
this._popupRef.detach();
}
if (this._dialogRef) {
this._dialogRef.close();
this._dialogRef = null;
}
if (this._calendarPortal && this._calendarPortal.isAttached) {
this._calendarPortal.detach();
}
}

/** Open the calendar as a dialog. */
private _openAsDialog(): void {
this._dialogRef = this._dialog.open(this.calendarTemplate);
}

/** Open the calendar as a popup. */
private _openAsPopup(): void {
if (!this._popupRef) {
this._createPopup();
}

if (!this._popupRef.hasAttached()) {
this._popupRef.attach(this._calendarPortal);
}

this._popupRef.backdropClick().first().subscribe(() => this.close());
}

/** Create the popup. */
private _createPopup(): void {
const overlayState = new OverlayState();
overlayState.positionStrategy = this._createPopupPositionStrategy();
overlayState.width = CALENDAR_POPUP_WIDTH;
overlayState.hasBackdrop = true;
overlayState.backdropClass = 'md-overlay-transparent-backdrop';
overlayState.direction = this._dir ? this._dir.value : 'ltr';

this._popupRef = this._overlay.create(overlayState);
}

/** Create the popup PositionStrategy. */
private _createPopupPositionStrategy(): PositionStrategy {
let origin = {originX: 'start', originY: 'bottom'} as OriginConnectionPosition;
let overlay = {overlayX: 'start', overlayY: 'top'} as OverlayConnectionPosition;
return this._overlay.position().connectedTo(this._inputElementRef, origin, overlay);
}
}
12 changes: 9 additions & 3 deletions src/lib/datepicker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ import {CommonModule} from '@angular/common';
import {MdCalendarTable} from './calendar-table';
import {MdYearView} from './year-view';
import {DatetimeModule} from '../core/datetime/index';
import {OverlayModule} from '../core/overlay/overlay-directives';
import {MdDatepicker} from './datepicker';
import {MdDatepickerInput} from './datepicker-input';
import {MdDialogModule} from '../dialog/index';


export * from './calendar-table';
export * from './datepicker';
export * from './datepicker-input';
export * from './month-view';
export * from './year-view';


@NgModule({
imports: [CommonModule, DatetimeModule],
exports: [MdCalendarTable, MdMonthView, MdYearView],
declarations: [MdCalendarTable, MdMonthView, MdYearView],
imports: [CommonModule, DatetimeModule, MdDialogModule, OverlayModule],
exports: [MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
declarations: [MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView],
})
export class MdDatepickerModule {}

0 comments on commit 4456641

Please sign in to comment.