Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

date #172

Merged
merged 7 commits into from
Sep 19, 2021
Merged

date #172

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Observable Inputs provides basic inputs:
* [Range](#Range) - choose a numeric value in a range (slider)
* [Select](#Select) - choose one or many from a set (drop-down menu)
* [Text](#Text) - freeform single-line text input
* [Date](#Date) - date input
* [Textarea](#Textarea) - freeform multi-line text input

Observable Inputs provides fancy inputs for tabular data:
Expand Down Expand Up @@ -318,6 +319,29 @@ The available *options* are:

If *validate* is not defined, [*text*.checkValidity](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-checkvalidity) is used. While the input is not considered valid, changes to the input will not be reported.

<a name="Date" href="#Date">#</a> Inputs.<b>date</b>(<i>options</i>) · [Source](./src/date.js), [Examples](https://observablehq.com/@observablehq/input-date)

```js
viewof start = Inputs.date({label: "Start date", value: "1982-03-06"})
```

A Date allows a [calendar-based input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date). By default, a Date will report its value immediately on input. If more deliberate behavior is desired, say if the input will trigger an expensive computation or remote API, the *submit* option can be set to true to wait until a button is clicked or the Enter key is pressed.

The available *options* are:

* *label* - a label; either a string or an HTML element.
* *value* - the initial value, as a JavaScript Date or formatted as an ISO string (yyyy-mm-dd); defaults to null.
* *min* - [minimum value](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/min) attribute.
* *max* - [maximum value](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/max) attribute.
* *required* - if true, the input must be a valid date.
* *validate* - a function to check whether the text input is valid.
* *width* - the width of the input (not including the label).
* *submit* - whether to require explicit submission before updating; defaults to false.
* *readonly* - whether input is readonly; defaults to false.
* *disabled* - whether input is disabled; defaults to false.

Note that the displayed date format is formatted [based on the browser’s locale](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date).

<a name="Textarea" href="#Textarea">#</a> Inputs.<b>textarea</b>(<i>options</i>) · [Source](./src/textarea.js)

<img src="./img/textarea.png" alt="A Textarea asking for your biography" width="660">
Expand Down
45 changes: 45 additions & 0 deletions src/date.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {html} from "htl";
import {maybeWidth} from "./css.js";
import {maybeLabel} from "./label.js";
import {createText} from "./text.js";

const isoformat = /^(?:[-+]\d{2})?\d{4}(?:-\d{2}(?:-\d{2})?)?(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{3})?)?(?:Z|[-+]\d{2}:?\d{2})?)?$/;

export function date({
label,
min,
max,
required,
readonly,
disabled,
width,
value,
...options
} = {}) {
const input = html`<input type=date name=date readonly=${readonly} disabled=${disabled} required=${required} min=${format(min)} max=${format(max)}>`;
const form = html`<form class=__ns__ style=${maybeWidth(width)}>
${maybeLabel(label, input)}<div class=__ns__-input>
${input}
</div>
</form>`;
return createText(form, input, {
...options,
value: coerce(value),
get: (input) => input.valueAsDate,
set: (input, value) => input.value = format(value),
same: (input, value) => +input.valueAsDate === +value
});
}

function coerce(value) {
return value instanceof Date && !isNaN(value) ? value
: typeof value === "string" ? (isoformat.test(value) ? new Date(value) : null)
: value == null || isNaN(value = +value) ? null
: new Date(+value);
}

function format(value) {
return (value = coerce(value))
? value.toISOString().slice(0, 10)
: value;
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {button} from "./button.js";
export {checkbox, radio, toggle} from "./checkbox.js";
export {date} from "./date.js";
export {range} from "./range.js";
export {search, searchFilter} from "./search.js";
export {select} from "./select.js";
Expand Down
5 changes: 5 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ form.__ns__-toggle .__ns__-input {
margin-left: var(--length2);
}

/* Tweak the height so that date inputs match text (and others). */
.__ns__-input > input[type=date] {
height: 22px;
}

/* Checkboxes and radios aren’t constrained by their input width; */
form.__ns__-checkbox {
width: auto;
Expand Down
19 changes: 13 additions & 6 deletions src/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ import {checkValidity, dispatchInput, preventDefault} from "./event.js";
import {stringify} from "./format.js";
import {maybeLabel} from "./label.js";

export function createText(form, input, {value = "", submit, validate = checkValidity} = {}) {
export function createText(form, input, {
value = "",
submit,
get = (input) => input.value,
set = (input, value) => input.value = stringify(value),
same = (input, value) => input.value === value,
validate = checkValidity
} = {}) {
submit = submit === true ? "Submit" : submit || null;
const button = submit ? html`<button type=submit disabled>${submit}` : null;
if (submit) input.after(button);
input.value = stringify(value);
value = validate(input) ? input.value : undefined;
set(input, value);
value = validate(input) ? get(input) : undefined;
form.onsubmit = onsubmit;
input.oninput = oninput;
function update() {
if (validate(input)) {
value = input.value;
value = get(input);
return true;
}
}
Expand All @@ -32,7 +39,7 @@ export function createText(form, input, {value = "", submit, validate = checkVal
}
function oninput(event) {
if (submit) {
button.disabled = value === input.value;
button.disabled = same(input, value);
event.stopPropagation();
} else if (!update()) {
event.stopPropagation();
Expand All @@ -43,7 +50,7 @@ export function createText(form, input, {value = "", submit, validate = checkVal
return value;
},
set(v) {
input.value = stringify(v);
set(input, v);
update();
}
});
Expand Down
50 changes: 50 additions & 0 deletions test/date-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as Inputs from "@observablehq/inputs";
import assert from "assert";
import it from "./jsdom.js";

it("Inputs.date() sets the initial value to null", () => {
const input = Inputs.date();
assert.strictEqual(input.value, null);
assert.strictEqual(input.elements.date.type, "date");
});

it("Inputs.date() sets the initial value and label", () => {
const input = Inputs.date({label: "Date", value: new Date("1970-01-01")});
assert.deepStrictEqual(input.value, new Date("1970-01-01"));
assert.strictEqual(input.textContent.trim(), "Date");
});

it("Inputs.date() coerces strings to dates", () => {
const input = Inputs.date({label: "Date", value: "1970-01-01"});
assert.deepStrictEqual(input.value, new Date("1970-01-01"));
input.value = "2021-01-01";
assert.deepStrictEqual(input.value, new Date("2021-01-01"));
});

it("Inputs.date() coerces numbers to dates", () => {
const input = Inputs.date({label: "Date", value: Date.UTC(1970, 0, 1)});
assert.deepStrictEqual(input.value, new Date("1970-01-01"));
input.value = Date.UTC(2021, 0, 1);
assert.deepStrictEqual(input.value, new Date("2021-01-01"));
});

it("Inputs.date() coerces non-midnights to midnight", () => {
const input = Inputs.date({label: "Date", value: new Date("2021-01-05T12:34:45Z")});
assert.deepStrictEqual(input.value, new Date("2021-01-05"));
input.value = Date.UTC(2021, 0, 1, 12, 34, 45);
assert.deepStrictEqual(input.value, new Date("2021-01-01"));
});

it("Inputs.date() sets the initial value, min, and max", () => {
const input = Inputs.date({type: "date", value: "1970-01-01", min: "1970-01-01", max: "2021-07-11"});
assert.deepStrictEqual(input.value, new Date("1970-01-01"));
assert.strictEqual(input.elements.date.min, "1970-01-01");
assert.strictEqual(input.elements.date.max, "2021-07-11");
input.value = "2015-01-01";
assert.deepStrictEqual(input.value, new Date("2015-01-01"));
// We should not be able to set the date to a value outside of the [min, max] range
input.value = "1969-01-01";
assert.notDeepStrictEqual(input.value, new Date("1969-01-01"));
// verify that trying to set an invalid date does not change the existing value
assert.deepStrictEqual(input.value, new Date("2015-01-01"));
});
4 changes: 4 additions & 0 deletions test/jsdom.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ jsdomit.skip = (description, run) => {
return it.skip(description, withJsdom(run));
};

jsdomit.only = (description, run) => {
return it.only(description, withJsdom(run));
};

function withJsdom(run) {
return async () => {
const jsdom = new JSDOM("");
Expand Down
58 changes: 28 additions & 30 deletions test/text-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,41 @@ import * as Inputs from "@observablehq/inputs";
import assert from "assert";
import it from "./jsdom.js";

it("Inputs.text() test initial value is set to ''", () => {
const t = Inputs.text();
assert.strictEqual(t.value, "");
it("Inputs.text() sets the initial value is the empty string", () => {
const input = Inputs.text();
assert.strictEqual(input.value, "");
});

it("Inputs.text() test setting various text properties ", () => {
const t = Inputs.text({type: "password", label: "Name", placeholder: "Enter your name", value: "Anonymous"});
assert.strictEqual(t.value, "Anonymous");
assert.strictEqual(t.textContent.trim(), "Name");
assert.strictEqual(t.elements.text.placeholder, "Enter your name");
assert.strictEqual(t.elements.text.type, "password");
it("Inputs.text() sets the initial value, label, placeholder, and type", () => {
const input = Inputs.text({type: "password", label: "Name", placeholder: "Enter your name", value: "Anonymous"});
assert.strictEqual(input.value, "Anonymous");
assert.strictEqual(input.textContent.trim(), "Name");
assert.strictEqual(input.elements.text.placeholder, "Enter your name");
assert.strictEqual(input.elements.text.type, "password");
});

it("Inputs.text() test type=date settings ", () => {
const t = Inputs.text({type: "date", label: "Date", value: "1970-01-01"});
assert.strictEqual(t.value, "1970-01-01");
assert.strictEqual(t.textContent.trim(), "Date");
assert.strictEqual(t.elements.text.type, "date");
it("Inputs.text() supports type=date", () => {
const input = Inputs.text({type: "date"});
assert.strictEqual(input.value, "");
assert.strictEqual(input.elements.text.type, "date");
});

it("Inputs.text() test type=date setting initial value ", () => {
const t = Inputs.text({type: "date", label: "Date", value: "1970-01-01", min: "1970-01-01", max: "2021-07-11"});
assert.strictEqual(t.value, "1970-01-01");
assert.strictEqual(t.textContent.trim(), "Date");
assert.strictEqual(t.elements.text.type, "date");
assert.strictEqual(t.elements.text.min, "1970-01-01");
assert.strictEqual(t.elements.text.max, "2021-07-11");
it("Inputs.text() supports type=date with initial value", () => {
const input = Inputs.text({type: "date", value: "1970-01-01"});
assert.strictEqual(input.value, "1970-01-01");
assert.strictEqual(input.elements.text.type, "date");
});

it("Inputs.text() test type=date settings for min and max", () => {
const t = Inputs.text({type: "date", label: "Date", value: "2010-01-01", min: "2000-01-01", max: "2021-07-11"});
assert.strictEqual(t.value, "2010-01-01");
t.value = "2015-01-01";
assert.strictEqual(t.value, "2015-01-01");
t.value = "1999-01-01";
// We should not be able to the date to a value outside of the [min, max] range
assert.notStrictEqual(t.value, "1999-01-01");
it("Inputs.text() supports type=date with min and max", () => {
const input = Inputs.text({type: "date", value: "2010-01-01", min: "2000-01-01", max: "2021-07-11"});
assert.strictEqual(input.elements.text.min, "2000-01-01");
assert.strictEqual(input.elements.text.max, "2021-07-11");
assert.strictEqual(input.value, "2010-01-01");
input.value = "2015-01-01";
assert.strictEqual(input.value, "2015-01-01");
input.value = "1999-01-01";
// We should not be able to set the date to a value outside of the [min, max] range
assert.notStrictEqual(input.value, "1999-01-01");
// verify that trying to set an invalid date does not change the existing value
assert.strictEqual(t.value, "2015-01-01");
assert.strictEqual(input.value, "2015-01-01");
});
12 changes: 6 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"@babel/highlight" "^7.14.5"

"@babel/helper-validator-identifier@^7.14.5":
version "7.14.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48"
integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==
version "7.15.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==

"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5":
version "7.14.5"
Expand Down Expand Up @@ -2115,9 +2115,9 @@ ini@^1.3.4:
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==

"internmap@1 - 2":
version "2.0.1"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.1.tgz#33d0fa016185397549fb1a14ea3dbe5a2949d1cd"
integrity sha512-Ujwccrj9FkGqjbY3iVoxD1VV+KdZZeENx0rphrtzmRXbFvkFO88L80BL/zeSIguX/7T+y8k04xqtgWgS5vxwxw==
version "2.0.2"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.2.tgz#3efa1165209cc56133df1400df9c34a73e0dad93"
integrity sha512-6O4dJQZN4+83kg9agi21fbasiAn7V2JRvLv29/YT1Kz8f+ngakB1hMG+AP0mYquLOtjWhNO8CvKhhXT/7Tla/g==

ip@^1.1.5:
version "1.1.5"
Expand Down