diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index f81afdd13f..dc31321c68 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -585,7 +585,7 @@ export default class Element extends Node { const type = (check_type_attribute() as string); - const valid_date_inputs = new Set(['date', 'datetime-local', 'time', 'month', 'week']); + const valid_date_inputs = new Set(['date', 'time', 'month', 'week']); if (!valid_date_inputs.has(type)) { component.error(binding, { diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index 146324f2a4..e353b456c1 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -162,6 +162,11 @@ export default function(node: Element, renderer: Renderer, options: RenderOption } else if (binding.name === 'value' && node.name === 'textarea') { const snippet = snip(expression); node_contents = '${(' + snippet + ') || ""}'; + } else if (binding.name === 'valueAsDate') { + const snippet = snip(expression); + const type = node.get_static_attribute_value('type'); + const fn = `${type}_input_value`; + opening_tag += '${@add_attribute("value", @' + fn + '(' + snippet + '), 1)}'; } else { const snippet = snip(expression); opening_tag += '${@add_attribute("' + name + '", ' + snippet + ', 1)}'; diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 220775b8b9..1fa09ec919 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -125,22 +125,6 @@ export function to_number(value) { return value === '' ? undefined : +value; } -export function value_as_date(value) { - const valueAsDate = new Date(value); - return isNaN(valueAsDate.getTime()) ? undefined : valueAsDate; -} - -export function date_as_value(date) { - const validDate = Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime()); - if (!validDate) { - return ''; - } - const yyyy = date.getUTCFullYear(); - const mm = date.getUTCMonth() + 1 < 10 ? `0${date.getUTCMonth() + 1}` : date.getUTCMonth() + 1; - const dd = date.getUTCDate() < 10 ? `0${date.getUTCDate()}` : date.getUTCDate(); - return `${yyyy}-${mm}-${dd}`; -} - export function time_ranges_to_array(ranges) { const array = []; for (let i = 0; i < ranges.length; i += 1) { @@ -190,9 +174,7 @@ export function set_data(text, data) { } export function set_input_value(input, value) { - if (Object.prototype.toString.call(value) === '[object Date]') { - input.value = date_as_value(value); - } else if (value != null || input.value) { + if (value != null || input.value) { input.value = value; } } diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index d8fbf15f0a..dfad59098c 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -127,4 +127,68 @@ export function add_attribute(name, value, boolean) { export function add_classes(classes) { return classes ? ` class="${classes}"` : ``; -} \ No newline at end of file +} + +function is_date(value) { + return value instanceof Date; +} + +function pad(n, len = 2) { + n = String(n); + while (n.length < len) n = `0${n}`; + return n; +} + +export function date_input_value(date) { + if (!is_date(date)) return ''; + + const yyyy = date.getUTCFullYear(); + const mm = pad(date.getUTCMonth() + 1); + const dd = pad(date.getUTCDate()); + + return `${yyyy}-${mm}-${dd}`; +} + +export function month_input_value(date) { + if (!is_date(date)) return ''; + + const yyyy = date.getUTCFullYear(); + const mm = pad(date.getUTCMonth() + 1); + + return `${yyyy}-${mm}`; +} + +export function time_input_value(date) { + if (!is_date(date)) return ''; + + const HH = pad(date.getHours()); + const mm = pad(date.getMinutes()); + + let str = `${HH}:${mm}`; + + let s, S; + if (s = date.getSeconds()) str += `:${pad(s)}`; + if (S = date.getMilliseconds()) str += `:${pad(S, 3)}`; + + return str; +} + +const ONE_WEEK = 1000 * 60 * 60 * 24 * 7; + +const to_day = (date, target) => { + const day = date.getUTCDay() || 7; + date.setDate(date.getDate() - (day - target)); +}; + +export const week_input_value = date => { + date = new Date(date); + to_day(date, 4); // pretend it's Thursday to figure out which year we should use + + const year = date.getUTCFullYear(); + const start = new Date(year, 0, 4); // week 1 always contains Jan 4 + to_day(start, 1); // weeks start on Mondays + + const elapsed = Math.floor((date - start.getTime()) / ONE_WEEK); + + return `${year}-W${pad(elapsed + 1)}`; +}; \ No newline at end of file diff --git a/test/runtime/samples/binding-input-date/_config.js b/test/runtime/samples/binding-input-valueasdate-date/_config.js similarity index 100% rename from test/runtime/samples/binding-input-date/_config.js rename to test/runtime/samples/binding-input-valueasdate-date/_config.js diff --git a/test/runtime/samples/binding-input-date/main.svelte b/test/runtime/samples/binding-input-valueasdate-date/main.svelte similarity index 100% rename from test/runtime/samples/binding-input-date/main.svelte rename to test/runtime/samples/binding-input-valueasdate-date/main.svelte diff --git a/test/runtime/samples/binding-input-valueasdate-month/_config.js b/test/runtime/samples/binding-input-valueasdate-month/_config.js new file mode 100644 index 0000000000..709d317db0 --- /dev/null +++ b/test/runtime/samples/binding-input-valueasdate-month/_config.js @@ -0,0 +1,63 @@ +const SEP_2019_INPUT_VALUE = '2019-09'; +const SEP_2019_DATE_VALUE = new Date(SEP_2019_INPUT_VALUE); + +const OCT_2019_INPUT_VALUE = '2019-10'; +const OCT_2019_DATE_VALUE = new Date(OCT_2019_INPUT_VALUE); + +export default { + props: { + month: SEP_2019_DATE_VALUE + }, + + html: ` + +

[object Date] ${SEP_2019_DATE_VALUE}

+ `, + + ssrHtml: ` + +

[object Date] ${SEP_2019_DATE_VALUE}

+ `, + + async test({ assert, component, target, window }) { + const input = target.querySelector('input'); + // https://github.com/jsdom/jsdom/issues/2658 + // assert.equal(input.value, SEP_2019_INPUT_VALUE); + assert.equal(component.month.toString(), SEP_2019_DATE_VALUE.toString()); + + const event = new window.Event('input'); + + // https://github.com/jsdom/jsdom/issues/2658 + // input.value = OCT_2019_INPUT_VALUE; + input.valueAsDate = OCT_2019_DATE_VALUE; + await input.dispatchEvent(event); + + assert.equal(component.month.toString(), OCT_2019_DATE_VALUE.toString()); + assert.htmlEqual(target.innerHTML, ` + +

[object Date] ${OCT_2019_DATE_VALUE}

+ `); + + component.month = SEP_2019_DATE_VALUE; + // https://github.com/jsdom/jsdom/issues/2658 + // assert.equal(input.value, SEP_2019_INPUT_VALUE); + assert.equal(input.valueAsDate.toString(), SEP_2019_DATE_VALUE.toString()); + assert.htmlEqual(target.innerHTML, ` + +

[object Date] ${SEP_2019_DATE_VALUE}

+ `); + + // https://github.com/jsdom/jsdom/issues/2658 + // empty string should be treated as undefined + // input.value = ''; + + input.valueAsDate = null; + await input.dispatchEvent(event); + + assert.equal(component.month, null); + assert.htmlEqual(target.innerHTML, ` + +

[object Null] null

+ `); + }, +}; diff --git a/test/runtime/samples/binding-input-valueasdate-month/main.svelte b/test/runtime/samples/binding-input-valueasdate-month/main.svelte new file mode 100644 index 0000000000..f6013e5068 --- /dev/null +++ b/test/runtime/samples/binding-input-valueasdate-month/main.svelte @@ -0,0 +1,6 @@ + + + +

{Object.prototype.toString.call(month)} {month}

diff --git a/test/runtime/samples/binding-input-valueasdate-time/_config.js b/test/runtime/samples/binding-input-valueasdate-time/_config.js new file mode 100644 index 0000000000..eb35d49188 --- /dev/null +++ b/test/runtime/samples/binding-input-valueasdate-time/_config.js @@ -0,0 +1,48 @@ +const LUNCHTIME = new Date(1970, 0, 1, 12, 30); +const TEATIME = new Date(1970, 0, 1, 12, 30); + +export default { + props: { + time: LUNCHTIME + }, + + html: ` + +

[object Date] ${LUNCHTIME}

+ `, + + ssrHtml: ` + +

[object Date] ${LUNCHTIME}

+ `, + + async test({ assert, component, target, window }) { + const input = target.querySelector('input'); + const event = new window.Event('input'); + + input.valueAsDate = TEATIME; + await input.dispatchEvent(event); + + assert.equal(component.time.toString(), TEATIME.toString()); + assert.htmlEqual(target.innerHTML, ` + +

[object Date] ${TEATIME}

+ `); + + component.time = LUNCHTIME; + assert.equal(input.valueAsDate.toString(), LUNCHTIME.toString()); + assert.htmlEqual(target.innerHTML, ` + +

[object Date] ${LUNCHTIME}

+ `); + + input.valueAsDate = null; + await input.dispatchEvent(event); + + assert.equal(component.time, null); + assert.htmlEqual(target.innerHTML, ` + +

[object Null] null

+ `); + }, +}; diff --git a/test/runtime/samples/binding-input-valueasdate-time/main.svelte b/test/runtime/samples/binding-input-valueasdate-time/main.svelte new file mode 100644 index 0000000000..110c92ba2f --- /dev/null +++ b/test/runtime/samples/binding-input-valueasdate-time/main.svelte @@ -0,0 +1,6 @@ + + + +

{Object.prototype.toString.call(time)} {time}

diff --git a/test/runtime/samples/binding-input-valueasdate-week/_config.js b/test/runtime/samples/binding-input-valueasdate-week/_config.js new file mode 100644 index 0000000000..2a118121be --- /dev/null +++ b/test/runtime/samples/binding-input-valueasdate-week/_config.js @@ -0,0 +1,63 @@ +const SEP_W36_2019_INPUT_VALUE = '2019-W36'; +const SEP_W36_2019_DATE_VALUE = new Date('2019-09-03'); + +const W41_2019_INPUT_VALUE = '2019-10-07'; +const W41_2019_DATE_VALUE = new Date(W41_2019_INPUT_VALUE); + +export default { + props: { + week: SEP_W36_2019_DATE_VALUE + }, + + html: ` + +

[object Date] ${SEP_W36_2019_DATE_VALUE}

+ `, + + ssrHtml: ` + +

[object Date] ${SEP_W36_2019_DATE_VALUE}

+ `, + + async test({ assert, component, target, window }) { + const input = target.querySelector('input'); + // https://github.com/jsdom/jsdom/issues/2658 + // assert.equal(input.value, SEP_W36_2019_INPUT_VALUE); + assert.equal(component.week.toString(), SEP_W36_2019_DATE_VALUE.toString()); + + const event = new window.Event('input'); + + // https://github.com/jsdom/jsdom/issues/2658 + // input.value = W41_2019_INPUT_VALUE; + input.valueAsDate = W41_2019_DATE_VALUE; + await input.dispatchEvent(event); + + assert.equal(component.week.toString(), W41_2019_DATE_VALUE.toString()); + assert.htmlEqual(target.innerHTML, ` + +

[object Date] ${W41_2019_DATE_VALUE}

+ `); + + component.week = SEP_W36_2019_DATE_VALUE; + // https://github.com/jsdom/jsdom/issues/2658 + // assert.equal(input.value, SEP_W36_2019_INPUT_VALUE); + assert.equal(input.valueAsDate.toString(), SEP_W36_2019_DATE_VALUE.toString()); + assert.htmlEqual(target.innerHTML, ` + +

[object Date] ${SEP_W36_2019_DATE_VALUE}

+ `); + + // https://github.com/jsdom/jsdom/issues/2658 + // empty string should be treated as undefined + // input.value = ''; + + input.valueAsDate = null; + await input.dispatchEvent(event); + + assert.equal(component.week, null); + assert.htmlEqual(target.innerHTML, ` + +

[object Null] null

+ `); + }, +}; diff --git a/test/runtime/samples/binding-input-valueasdate-week/main.svelte b/test/runtime/samples/binding-input-valueasdate-week/main.svelte new file mode 100644 index 0000000000..f35a7a8d6c --- /dev/null +++ b/test/runtime/samples/binding-input-valueasdate-week/main.svelte @@ -0,0 +1,6 @@ + + + +

{Object.prototype.toString.call(week)} {week}