Skip to content

Commit 876723c

Browse files
Allow value to be null
2 parents 4caab6f + 616b7fa commit 876723c

File tree

6 files changed

+104
-66
lines changed

6 files changed

+104
-66
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
/.svelte-kit
33
/package
44
/coverage
5+
/node_modules

src/lib/DateInput.svelte

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@
66
import { parse, createFormat } from './parse'
77
import type { FormatToken } from './parse'
88
import DateTimePicker from './DatePicker.svelte'
9-
import { writable } from 'svelte/store'
9+
import { Writable, writable } from 'svelte/store'
1010
import { createEventDispatcher } from 'svelte'
1111
1212
const dispatch = createEventDispatcher<{ select: undefined }>()
1313
14+
/** Default date to display in picker before value is assigned */
1415
const defaultDate = new Date()
1516
1617
// inner date value store for preventing value updates (and also
1718
// text updates as a result) when date is unchanged
18-
const innerStore = writable(defaultDate)
19+
const innerStore: Writable<Date | null> = writable(null)
1920
const store = (() => {
2021
return {
2122
subscribe: innerStore.subscribe,
22-
set: (d: Date) => {
23-
if (d.getTime() !== $innerStore.getTime()) {
23+
set: (d: Date | null) => {
24+
if (d === null) {
25+
innerStore.set(null)
26+
value = d
27+
} else if (d.getTime() !== $innerStore?.getTime()) {
2428
innerStore.set(d)
2529
value = d
2630
}
@@ -29,8 +33,9 @@
2933
})()
3034
3135
/** Date value */
32-
export let value = defaultDate
36+
export let value: Date | null = null
3337
$: store.set(value)
38+
3439
/** The earliest value the user can select */
3540
export let min = new Date(defaultDate.getFullYear() - 20, 0, 1)
3641
/** The latest value the user can select */
@@ -48,7 +53,7 @@
4853
/** Locale object for internationalization */
4954
export let locale: Locale = {}
5055
51-
function valueUpdate(value: Date, formatTokens: FormatToken[]) {
56+
function valueUpdate(value: Date | null, formatTokens: FormatToken[]) {
5257
text = toText(value, formatTokens)
5358
}
5459
$: valueUpdate($store, formatTokens)
@@ -58,12 +63,21 @@
5863
$: textHistory = [textHistory[1], text]
5964
6065
function textUpdate(text: string, formatTokens: FormatToken[]) {
61-
const result = parse(text, formatTokens, $store)
62-
if (result.date !== null) {
63-
valid = true
64-
store.set(result.date)
66+
if (text.length) {
67+
const result = parse(text, formatTokens, $store)
68+
if (result.date !== null) {
69+
valid = true
70+
store.set(result.date)
71+
} else {
72+
valid = false
73+
}
6574
} else {
66-
valid = false
75+
valid = true // <-- empty string is always valid
76+
// value resets to null if you clear the field
77+
if (value) {
78+
value = null
79+
store.set(null)
80+
}
6781
}
6882
}
6983
$: textUpdate(text, formatTokens)

src/lib/DatePicker.svelte

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,37 @@
77
88
const dispatch = createEventDispatcher<{ select: undefined }>()
99
10-
/** Date value */
11-
export let value = new Date()
10+
/** Date value. It's `null` if no date is selected */
11+
export let value: Date | null = null
1212
function setValue(d: Date) {
13-
if (d.getTime() !== value.getTime()) value = d
13+
if (d.getTime() !== value?.getTime()) {
14+
value = d
15+
}
1416
}
1517
function updateValue(updater: (date: Date) => Date) {
16-
let d = updater(new Date(value.getTime()))
17-
setValue(d)
18+
const newValue = updater(new Date(shownDate.getTime()))
19+
setValue(newValue)
1820
}
21+
22+
/** Default Date to use */
23+
const defaultDate = new Date()
24+
25+
/** The date shown in the popup, for when `value` is null */
26+
let shownDate = value ?? defaultDate
27+
$: if (value) shownDate = value
28+
29+
/** Update the shownDate. The date is only selected if a date is already selected */
30+
function updateShownDate(updater: (date: Date) => Date) {
31+
shownDate = updater(new Date(shownDate.getTime()))
32+
if (value && shownDate.getTime() !== value.getTime()) {
33+
setValue(shownDate)
34+
}
35+
}
36+
1937
/** The earliest year the user can select */
20-
export let min = new Date(new Date().getFullYear() - 20, 0, 1)
38+
export let min = new Date(defaultDate.getFullYear() - 20, 0, 1)
2139
/** The latest year the user can select */
22-
export let max = new Date(new Date().getFullYear(), 11, 31, 23, 59, 59, 999)
40+
export let max = new Date(defaultDate.getFullYear(), 11, 31, 23, 59, 59, 999)
2341
let years = getYears(min, max)
2442
$: years = getYears(min, max)
2543
function getYears(min: Date, max: Date) {
@@ -29,29 +47,30 @@
2947
}
3048
return years
3149
}
32-
$: if (value > max) {
33-
setValue(max)
34-
} else if (value < min) {
35-
setValue(min)
50+
51+
$: if (shownDate > max) {
52+
updateShownDate(() => max)
53+
} else if (shownDate < min) {
54+
updateShownDate(() => min)
3655
}
3756
3857
/** Locale object for internationalization */
3958
export let locale: Locale = {}
4059
$: iLocale = getInnerLocale(locale)
4160
42-
let year = value.getFullYear()
43-
const getYear = (value: Date) => (year = value.getFullYear())
61+
let year = shownDate.getFullYear()
62+
const getYear = (tmpPickerDate: Date) => (year = tmpPickerDate.getFullYear())
4463
function setYear(year: number) {
45-
updateValue((value) => {
46-
value.setFullYear(year)
47-
return value
64+
updateShownDate((tmpPickerDate) => {
65+
tmpPickerDate.setFullYear(year)
66+
return tmpPickerDate
4867
})
4968
}
50-
$: getYear(value)
69+
$: getYear(shownDate)
5170
$: setYear(year)
5271
53-
let month = value.getMonth()
54-
const getMonth = (value: Date) => (month = value.getMonth())
72+
let month = shownDate.getMonth()
73+
const getMonth = (tmpPickerDate: Date) => (month = tmpPickerDate.getMonth())
5574
function setMonth(month: number) {
5675
let newYear = year
5776
let newMonth = month
@@ -64,26 +83,26 @@
6483
}
6584
6685
const maxDate = getMonthLength(newYear, newMonth)
67-
const newDate = Math.min(value.getDate(), maxDate)
68-
setValue(
69-
new Date(
86+
const newDate = Math.min(shownDate.getDate(), maxDate)
87+
updateShownDate((date) => {
88+
return new Date(
7089
newYear,
7190
newMonth,
7291
newDate,
73-
value.getHours(),
74-
value.getMinutes(),
75-
value.getSeconds(),
76-
value.getMilliseconds()
92+
date.getHours(),
93+
date.getMinutes(),
94+
date.getSeconds(),
95+
date.getMilliseconds()
7796
)
78-
)
97+
})
7998
}
80-
$: getMonth(value)
99+
$: getMonth(shownDate)
81100
$: setMonth(month)
82101
83-
let dayOfMonth = value.getDate()
84-
$: dayOfMonth = value.getDate()
102+
let dayOfMonth = value?.getDate() || null
103+
$: dayOfMonth = value?.getDate() || null
85104
86-
$: calendarDays = getCalendarDays(value, iLocale.weekStartsOn)
105+
$: calendarDays = getCalendarDays(shownDate, iLocale.weekStartsOn)
87106
88107
function setDay(calendarDay: CalendarDay) {
89108
if (dayIsInRange(calendarDay, min, max)) {

src/lib/date-utils.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ export function getMonthLength(year: number, month: number): number {
99
return monthLenghts[month]
1010
}
1111

12-
export function toText(date: Date, formatTokens: FormatToken[]): string {
12+
export function toText(date: Date | null, formatTokens: FormatToken[]): string {
1313
let text = ''
14-
for (const token of formatTokens) {
15-
if (typeof token === 'string') {
16-
text += token
17-
} else {
18-
text += token.toString(date)
14+
if (date) {
15+
for (const token of formatTokens) {
16+
if (typeof token === 'string') {
17+
text += token
18+
} else {
19+
text += token.toString(date)
20+
}
1921
}
2022
}
2123
return text

src/lib/parse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type ParseResult = {
1212
missingPunctuation: string
1313
}
1414
/** Parse a string according to the supplied format tokens. Returns a date if successful, and the missing punctuation if there is any that should be after the string */
15-
export function parse(str: string, tokens: FormatToken[], baseDate?: Date): ParseResult {
15+
export function parse(str: string, tokens: FormatToken[], baseDate: Date | null): ParseResult {
1616
let missingPunctuation = ''
1717
let valid = true
1818

src/routes/docs.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,21 @@ npm install -D date-picker-svelte
2525
## DateInput
2626

2727
Component with an input field that shows the DatePicker component on focus.
28+
The component will not assign a date value until a specific date is selected in the picker or entered into the field.
2829

2930
### Props
3031

31-
| Prop | Type | Description |
32-
| :----------------- | :----- | :------------------------------------------- |
33-
| `value` | Date | Date value |
34-
| `min` | Date | The earliest value the user can select |
35-
| `max` | Date | The latest value the user can select |
36-
| `placeholder` | string | Placeholder |
37-
| `valid` | bool | Whether the text is valid |
38-
| `format` | string | Format string |
39-
| `visible` | bool | Whether the date popup is visible |
40-
| `closeOnSelection` | bool | Close the date popup when a date is selected |
41-
| `locale` | Locale | Locale object for internationalization |
32+
| Prop | Type | Description |
33+
| :----------------- | :----------- | :------------------------------------------- |
34+
| `value` | Date \| null | Date value |
35+
| `min` | Date | The earliest value the user can select |
36+
| `max` | Date | The latest value the user can select |
37+
| `placeholder` | string | Placeholder used when date value is null |
38+
| `valid` | bool | Whether the text is valid |
39+
| `format` | string | Format string |
40+
| `visible` | bool | Whether the date popup is visible |
41+
| `closeOnSelection` | bool | Close the date popup when a date is selected |
42+
| `locale` | Locale | Locale object for internationalization |
4243

4344
#### Format string
4445

@@ -56,15 +57,16 @@ Example format string: `yyyy-MM-dd HH:mm:ss`
5657
## DatePicker
5758

5859
Component with a calendar for choosing a date.
60+
The component will not assign a date value until a specific date is selected in the picker.
5961

6062
### Props
6163

62-
| Prop | Type | Description |
63-
| :------- | :----- | :------------------------------------- |
64-
| `value` | Date | Date value |
65-
| `min` | Date | The earliest year the user can select |
66-
| `max` | Date | The latest year the user can select |
67-
| `locale` | Locale | Locale object for internationalization |
64+
| Prop | Type | Description |
65+
| :------- | :----------- | :------------------------------------- |
66+
| `value` | Date \| null | Date value |
67+
| `min` | Date | The earliest year the user can select |
68+
| `max` | Date | The latest year the user can select |
69+
| `locale` | Locale | Locale object for internationalization |
6870

6971
## Internationalization
7072

0 commit comments

Comments
 (0)