Skip to content

Commit e5c342f

Browse files
committed
fix test and clean up the types a bit
1 parent 4b2ed28 commit e5c342f

File tree

2 files changed

+148
-123
lines changed

2 files changed

+148
-123
lines changed

src/material/core/datetime/date-selection-model.ts

Lines changed: 136 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -6,177 +6,206 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {FactoryProvider, Injectable, OnDestroy, Optional, SkipSelf} from '@angular/core';
910
import {DateAdapter} from '@angular/material/core';
10-
import {FactoryProvider, Optional, SkipSelf, Injectable} from '@angular/core';
11-
import {Subject, Observable} from 'rxjs';
11+
import {Observable, Subject} from 'rxjs';
1212

13+
/** A class representing a range of dates. */
1314
export class DateRange<D> {
14-
// DateRange should be a class with a private member.
15-
// Otherwise any object with a `start` and `end` property might be considered a
16-
// `DateRange`
17-
private _disableStructuralEquivalency: never;
15+
/**
16+
* Ensures that objects with a `start` and `end` property can't be assigned to a variable that
17+
* expects a `DateRange`
18+
*/
19+
private _disableStructuralEquivalency: never;
1820

19-
start: D | null;
20-
end: D | null;
21+
/** The start date of the range. */
22+
readonly start: D | null;
2123

22-
constructor(range?: { start?: D | null, end?: D | null } | null) {
23-
this.start = range && range.start || null;
24-
this.end = range && range.end || null;
25-
}
24+
/** The end date of the range. */
25+
readonly end: D | null;
26+
27+
constructor(range?: {start?: D | null, end?: D | null} | null) {
28+
this.start = range && range.start || null;
29+
this.end = range && range.end || null;
30+
}
2631
}
2732

28-
type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : T;
33+
type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : NonNullable<T>;
2934

30-
export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<S>> {
31-
protected _valueChangesSubject = new Subject<void>();
35+
/** A selection model containing a date selection. */
36+
export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<S>>
37+
implements OnDestroy {
38+
/** Subject used to emit value change events. */
39+
private _valueChangesSubject = new Subject<void>();
40+
41+
/** Observable of value change events. */
3242
valueChanges: Observable<void> = this._valueChangesSubject.asObservable();
3343

34-
constructor(protected readonly adapter: DateAdapter<D>, protected _selection: S |
35-
null = null) {}
44+
/** The current selection. */
45+
get selection (): S { return this._selection; }
46+
set selection(s: S) {
47+
this._selection = s;
48+
this._valueChangesSubject.next();
49+
}
50+
51+
protected constructor(protected readonly adapter: DateAdapter<D>, private _selection: S) {}
3652

37-
abstract get selection(): S | null;
38-
abstract set selection(selection: S | null);
39-
abstract add(date: D): void;
53+
ngOnDestroy() {
54+
this._valueChangesSubject.complete();
55+
}
56+
57+
/** Adds a date to the current selection. */
58+
abstract add(date: D | null): void;
59+
60+
/** Checks whether the current selection is complete. */
4061
abstract isComplete(): boolean;
62+
63+
/**
64+
* Checks whether the current selection is the same as the selection in the given selection model.
65+
*/
66+
abstract isSame(other: MatDateSelectionModel<D>): boolean;
67+
68+
/** Checks whether the current selection is valid. */
4169
abstract isValid(): boolean;
42-
abstract isSame(other: MatDateSelectionModel<any, any>):boolean;
70+
71+
/** Checks whether the current selection overlaps with the given range. */
4372
abstract overlaps(range: DateRange<D>): boolean;
4473
}
4574

46-
/**
47-
* Concrete implementation of a MatDateSelectionModel that holds a single date.
48-
*/
75+
/** A selection model that contains a single date. */
4976
@Injectable()
50-
export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D, D> {
51-
77+
export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | null, D> {
5278
constructor(adapter: DateAdapter<D>, date?: D | null) {
53-
super(adapter, date);
54-
}
55-
56-
get selection(): D | null {
57-
return this._selection;
58-
}
59-
60-
set selection(selection: D | null) {
61-
this._selection = selection;
62-
}
63-
64-
add(date: D) {
65-
this._selection = date;
66-
this._valueChangesSubject.next();
79+
super(adapter, date || null);
6780
}
6881

69-
isComplete(): boolean {
70-
return this._selection != null;
82+
/**
83+
* Adds a date to the current selection. In the case of a single date selection, the added date
84+
* simply overwrites the previous selection
85+
*/
86+
add(date: D | null) {
87+
this.selection = date;
7188
}
7289

73-
isValid(): boolean {
74-
return this._selection != null && this.adapter.isDateInstance(this._selection) &&
75-
this.adapter.isValid(this._selection);
76-
}
90+
/**
91+
* Checks whether the current selection is complete. In the case of a single date selection, this
92+
* is true if the current selection is not null.
93+
*/
94+
isComplete() { return this.selection != null; }
7795

78-
isSame(other: MatDateSelectionModel<D>): boolean {
96+
/**
97+
* Checks whether the current selection is the same as the selection in the given selection model.
98+
*/
99+
isSame(other: MatDateSelectionModel<any>): boolean {
79100
return other instanceof MatSingleDateSelectionModel &&
80-
this._selection === other.selection;
101+
this.adapter.sameDate(other.selection, this.selection);
81102
}
82103

83104
/**
84-
* Determines if the single date is within a given date range. Retuns false if either dates of
85-
* the range is null or if the selection is undefined.
105+
* Checks whether the current selection is valid. In the case of a single date selection, this
106+
* means that the current selection is not null and is a valid date.
86107
*/
108+
isValid(): boolean {
109+
return this.selection != null && this.adapter.isDateInstance(this.selection) &&
110+
this.adapter.isValid(this.selection);
111+
}
112+
113+
/** Checks whether the current selection overlaps with the given range. */
87114
overlaps(range: DateRange<D>): boolean {
88-
return !!(this._selection && range.start && range.end &&
89-
this.adapter.compareDate(range.start, this._selection) <= 0 &&
90-
this.adapter.compareDate(this._selection, range.end) <= 0);
115+
return !!(this.selection && range.start && range.end &&
116+
this.adapter.compareDate(range.start, this.selection) <= 0 &&
117+
this.adapter.compareDate(this.selection, range.end) <= 0);
91118
}
92119
}
93120

94-
/**
95-
* Concrete implementation of a MatDateSelectionModel that holds a date range, represented by
96-
* a start date and an end date.
97-
*/
121+
/** A selection model that contains a date range. */
98122
@Injectable()
99-
export class MatRangeDateSelectionModel<D> extends
100-
MatDateSelectionModel<DateRange<D>, D> {
101-
protected _selection: DateRange<D>;
102-
103-
constructor(adapter: DateAdapter<D>, range?: DateRange<D> | null) {
104-
super(adapter, range || new DateRange<D>());
105-
}
106-
107-
get selection(): DateRange<D> | null {
108-
return new DateRange(this._selection);
109-
}
110-
111-
set selection(selection: DateRange<D> | null) {
112-
this._selection = new DateRange(selection);
123+
export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRange<D>, D> {
124+
constructor(adapter: DateAdapter<D>, range?: {start?: D | null, end?: D | null} | null) {
125+
super(adapter, new DateRange(range));
113126
}
114127

115128
/**
116-
* Adds an additional date to the range. If no date is set thus far, it will set it to the
117-
* beginning. If the beginning is set, it will set it to the end.
118-
* If add is called on a complete selection, it will empty the selection and set it as the start.
129+
* Adds a date to the current selection. In the case of a date range selection, the added date
130+
* fills in the next `null` value in the range. If both the start and the end already have a date,
131+
* the selection is reset so that the given date is the new `start` and the `end` is null.
119132
*/
120133
add(date: D | null): void {
121-
if (this._selection.start == null) {
122-
this._selection.start = date;
123-
} else if (this._selection.end == null) {
124-
this._selection.end = date;
134+
let {start, end} = this.selection;
135+
136+
if (start == null) {
137+
start = date;
138+
} else if (end == null) {
139+
end = date;
125140
} else {
126-
this._selection.start = date;
127-
this._selection.end = null;
141+
start = date;
142+
end = null;
128143
}
129144

130-
this._valueChangesSubject.next();
131-
}
132-
133-
setRange(start: D | null, end: D | null) {
134-
this._selection.start = start;
135-
this._selection.end = end;
145+
this.selection = new DateRange<D>({start, end});
136146
}
137147

148+
/**
149+
* Checks whether the current selection is complete. In the case of a date range selection, this
150+
* is true if the current selection has a non-null `start` and `end`.
151+
*/
138152
isComplete(): boolean {
139-
return this._selection && this._selection.start != null && this._selection.end != null;
140-
}
141-
142-
isValid(): boolean {
143-
return this._selection.start != null && this._selection.end != null &&
144-
this.adapter.isValid(this._selection.start!) &&
145-
this.adapter.isValid(this._selection.end!);
153+
return this.selection.start != null && this.selection.end != null;
146154
}
147155

148-
isSame(other: MatDateSelectionModel<D>): boolean {
156+
/**
157+
* Checks whether the current selection is the same as the selection in the given selection model.
158+
*/
159+
isSame(other: MatDateSelectionModel<any>): boolean {
149160
if (other instanceof MatRangeDateSelectionModel) {
150-
return this._selection.start === other._selection.start &&
151-
this._selection.end === other._selection.end;
161+
return this.adapter.sameDate(this.selection.start, other.selection.start) &&
162+
this.adapter.sameDate(this.selection.end, other.selection.end);
152163
}
153164
return false;
154165
}
155166

167+
/**
168+
* Checks whether the current selection is valid. In the case of a date range selection, this
169+
* means that the current selection has a `start` and `end` that are both non-null and valid
170+
* dates.
171+
*/
172+
isValid(): boolean {
173+
return this.selection.start != null && this.selection.end != null &&
174+
this.adapter.isValid(this.selection.start!) && this.adapter.isValid(this.selection.end!);
175+
}
176+
156177
/**
157178
* Returns true if the given range and the selection overlap in any way. False if otherwise, that
158179
* includes incomplete selections or ranges.
159180
*/
160181
overlaps(range: DateRange<D>): boolean {
161-
const selectionStart = this._selection.start;
162-
const selectionEnd = this._selection.end;
163-
const rangeStart = range.start;
164-
const rangeEnd = range.end;
165-
if (!(selectionStart && selectionEnd && rangeStart && rangeEnd)) {
182+
if (!(this.selection.start && this.selection.end && range.start && range.end)) {
166183
return false;
167184
}
168185

169186
return (
170-
this._isBetween(rangeStart, selectionStart, selectionEnd) ||
171-
this._isBetween(rangeEnd, selectionStart, selectionEnd) ||
172-
(
173-
this.adapter.compareDate(rangeStart, selectionStart) <= 0 &&
174-
this.adapter.compareDate(selectionEnd, rangeEnd) <= 0
175-
)
187+
this._isBetween(range.start, this.selection.start, this.selection.end) ||
188+
this._isBetween(range.end, this.selection.start, this.selection.end) ||
189+
(
190+
this.adapter.compareDate(range.start, this.selection.start) <= 0 &&
191+
this.adapter.compareDate(this.selection.end, range.end) <= 0
192+
)
176193
);
177194
}
178195

179196
private _isBetween(value: D, from: D, to: D): boolean {
180197
return this.adapter.compareDate(from, value) <= 0 && this.adapter.compareDate(value, to) <= 0;
181198
}
182199
}
200+
201+
export function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY<D>(parent:
202+
MatSingleDateSelectionModel<D>,
203+
adapter: DateAdapter<D>) {
204+
return parent || new MatSingleDateSelectionModel(adapter);
205+
}
206+
207+
export const MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
208+
provide: MatDateSelectionModel,
209+
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
210+
useFactory: MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY,
211+
};

src/material/datepicker/datepicker-input.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import {
3232
import {
3333
DateAdapter,
3434
MAT_DATE_FORMATS,
35-
MatSingleDateSelectionModel,
3635
MatDateFormats,
36+
MatSingleDateSelectionModel,
3737
ThemePalette,
3838
} from '@angular/material/core';
3939
import {MatFormField} from '@angular/material/form-field';
@@ -67,10 +67,10 @@ export class MatDatepickerInputEvent<D> {
6767
value: D | null;
6868

6969
constructor(
70-
/** Reference to the datepicker input component that emitted the event. */
71-
public target: MatDatepickerInput<D>,
72-
/** Reference to the native input element associated with the datepicker input. */
73-
public targetElement: HTMLElement) {
70+
/** Reference to the datepicker input component that emitted the event. */
71+
public target: MatDatepickerInput<D>,
72+
/** Reference to the native input element associated with the datepicker input. */
73+
public targetElement: HTMLElement) {
7474
this.value = this.target.value;
7575
}
7676
}
@@ -131,17 +131,15 @@ export class MatDatepickerInput<D> implements ControlValueAccessor, OnDestroy, V
131131
@Input()
132132
get value(): D | null { return this._selectionModel.selection; }
133133
set value(value: D | null) {
134-
value = this._dateAdapter.deserialize(value);
134+
value = this._dateAdapter.deserialize(value);
135+
this._lastValueValid = !value || this._dateAdapter.isValid(value);
136+
value = this._getValidDateOrNull(value);
135137
const oldDate = this._selectionModel.selection;
136-
const isDifferent = !this._dateAdapter.sameDate(oldDate, value);
137138
this._selectionModel.selection = value;
139+
this._formatValue(value);
138140

139-
this._lastValueValid = this._selectionModel.isValid();
140-
141-
this._formatValue(this._selectionModel.selection);
142-
143-
if (isDifferent) {
144-
this._valueChange.emit(this.value);
141+
if (!this._dateAdapter.sameDate(oldDate, value)) {
142+
this._valueChange.emit(value);
145143
}
146144
}
147145
private _selectionModel: MatSingleDateSelectionModel<D>;
@@ -335,9 +333,7 @@ export class MatDatepickerInput<D> implements ControlValueAccessor, OnDestroy, V
335333
date = this._getValidDateOrNull(date);
336334

337335
if (!this._dateAdapter.sameDate(date, this._selectionModel.selection)) {
338-
if (date) {
339-
this._selectionModel.add(date);
340-
}
336+
this._selectionModel.selection = date;
341337
this._cvaOnChange(date);
342338
this._valueChange.emit(date);
343339
this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));

0 commit comments

Comments
 (0)