Skip to content

Commit 37f452a

Browse files
committed
ui: add ControlDate
1 parent dd4dba7 commit 37f452a

File tree

7 files changed

+430
-128
lines changed

7 files changed

+430
-128
lines changed

apps/playground/src/components/Home.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ const Home = () => {
77
const form = useForm({
88
schema: t.partial(
99
t.object({
10-
firstName: t.text(),
10+
firstName: t.text({
11+
$control: {
12+
select: {},
13+
},
14+
}),
1115
lastName: t.text(),
1216
email: t.email(),
1317
password: t.text(),
1418
birthday: t.date(),
19+
status: t.enum(["active", "inactive", "pending", "banned", "deleted"]),
20+
tags: t.array(t.string({ enum: ["tech", "news", "blog"] })),
21+
keywords: t.array(t.string()),
1522
}),
1623
),
1724
handler: (values, args) => {
@@ -26,7 +33,7 @@ const Home = () => {
2633
return (
2734
<Flex p={"lg"} direction={"column"}>
2835
<Flex>
29-
<DarkModeButton mode={"segmented"} />
36+
<DarkModeButton mode={"minimal"} />
3037
</Flex>
3138
<Flex p={"lg"}>
3239
<TypeForm form={form} />

packages/ui/src/components/Control.tsx

Lines changed: 46 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
type PasswordInputProps,
1010
SegmentedControl,
1111
type SegmentedControlProps,
12-
Select,
1312
type SelectProps,
1413
Switch,
1514
type SwitchProps,
@@ -18,15 +17,14 @@ import {
1817
TextInput,
1918
type TextInputProps,
2019
} from "@mantine/core";
21-
import {
22-
DateInput,
23-
type DateInputProps,
24-
DateTimePicker,
25-
type DateTimePickerProps,
26-
TimeInput,
27-
type TimeInputProps,
20+
import type {
21+
DateInputProps,
22+
DateTimePickerProps,
23+
TimeInputProps,
2824
} from "@mantine/dates";
2925
import type { ComponentType, ReactNode } from "react";
26+
import ControlDate from "./ControlDate";
27+
import ControlSelect from "./ControlSelect";
3028

3129
export interface ControlProps {
3230
input: InputField;
@@ -173,33 +171,27 @@ const Control = (props: ControlProps) => {
173171
}
174172
// endregion
175173

176-
// region <Select/>
177-
if (
178-
(props.input.schema &&
179-
"enum" in props.input.schema &&
180-
props.input.schema.enum) ||
181-
props.select
182-
) {
183-
const data =
184-
props.input.schema &&
185-
"enum" in props.input.schema &&
186-
Array.isArray(props.input.schema.enum)
187-
? props.input.schema.enum?.map((value: string) => ({
188-
value,
189-
label: value,
190-
}))
191-
: [];
192-
193-
const selectProps = typeof props.select === "object" ? props.select : {};
174+
// region <ControlSelect/>
175+
// Handle: single enum, array of enum, array of strings, or explicit select/multi/tags props
176+
const isEnum =
177+
props.input.schema &&
178+
"enum" in props.input.schema &&
179+
props.input.schema.enum;
180+
const isArray =
181+
props.input.schema &&
182+
"type" in props.input.schema &&
183+
props.input.schema.type === "array";
184+
const hasArrayItems =
185+
isArray && "items" in props.input.schema && props.input.schema.items;
194186

187+
if (isEnum || isArray || props.select) {
195188
return (
196-
<Select
197-
{...inputProps}
198-
id={id}
199-
leftSection={icon}
200-
data={data}
201-
{...props.input.props}
202-
{...selectProps}
189+
<ControlSelect
190+
input={props.input}
191+
title={props.title}
192+
description={props.description}
193+
icon={icon}
194+
select={props.select}
203195
/>
204196
);
205197
}
@@ -259,91 +251,36 @@ const Control = (props: ControlProps) => {
259251
}
260252
//endregion
261253

262-
// region <DateTimePicker/>
263-
if (
264-
props.datetime ||
265-
(props.input.schema &&
266-
"type" in props.input.schema &&
267-
props.input.schema.type === "string" &&
268-
"format" in props.input.schema &&
269-
props.input.schema.format === "date-time")
270-
) {
271-
const dateTimePickerProps =
272-
typeof props.datetime === "object" ? props.datetime : {};
273-
return (
274-
<DateTimePicker
275-
{...inputProps}
276-
id={id}
277-
leftSection={icon}
278-
defaultValue={
279-
props.input.props.defaultValue
280-
? new Date(props.input.props.defaultValue)
281-
: undefined
282-
}
283-
onChange={(value) => {
284-
props.input.set(value ? new Date(value).toISOString() : undefined);
285-
}}
286-
{...dateTimePickerProps}
287-
/>
288-
);
289-
}
290-
//endregion
254+
// region <ControlDate/>
255+
// Handle: date, date-time, and time formats
256+
const format =
257+
props.input.schema &&
258+
"format" in props.input.schema &&
259+
typeof props.input.schema.format === "string"
260+
? props.input.schema.format
261+
: undefined;
291262

292-
// region <DateInput/>
293263
if (
294264
props.date ||
295-
(props.input.schema &&
296-
"type" in props.input.schema &&
297-
props.input.schema.type === "string" &&
298-
"format" in props.input.schema &&
299-
props.input.schema.format === "date")
300-
) {
301-
const dateInputProps = typeof props.date === "object" ? props.date : {};
302-
return (
303-
<DateInput
304-
{...inputProps}
305-
id={id}
306-
leftSection={icon}
307-
defaultValue={
308-
props.input.props.defaultValue
309-
? new Date(props.input.props.defaultValue)
310-
: undefined
311-
}
312-
onChange={(value) => {
313-
props.input.set(
314-
value ? new Date(value).toISOString().slice(0, 10) : undefined,
315-
);
316-
}}
317-
{...dateInputProps}
318-
/>
319-
);
320-
}
321-
//endregion
322-
323-
// region <TimeInput/>
324-
if (
265+
props.datetime ||
325266
props.time ||
326-
(props.input.schema &&
327-
"type" in props.input.schema &&
328-
props.input.schema.type === "string" &&
329-
"format" in props.input.schema &&
330-
props.input.schema.format === "time")
267+
format === "date" ||
268+
format === "date-time" ||
269+
format === "time"
331270
) {
332-
const timeInputProps = typeof props.time === "object" ? props.time : {};
333271
return (
334-
<TimeInput
335-
{...inputProps}
336-
id={id}
337-
leftSection={icon}
338-
defaultValue={props.input.props.defaultValue}
339-
onChange={(event) => {
340-
props.input.set(event.currentTarget.value);
341-
}}
342-
{...timeInputProps}
272+
<ControlDate
273+
input={props.input}
274+
title={props.title}
275+
description={props.description}
276+
icon={icon}
277+
date={props.date}
278+
datetime={props.datetime}
279+
time={props.time}
343280
/>
344281
);
345282
}
346-
//endregion
283+
// endregion
347284

348285
// region <TextInput/>
349286
const textInputProps = typeof props.text === "object" ? props.text : {};
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { TypeBoxError } from "@alepha/core";
2+
import type { InputField } from "@alepha/react-form";
3+
import { useFormState } from "@alepha/react-form";
4+
import {
5+
DateInput,
6+
type DateInputProps,
7+
DateTimePicker,
8+
type DateTimePickerProps,
9+
TimeInput,
10+
type TimeInputProps,
11+
} from "@mantine/dates";
12+
import type { ReactNode } from "react";
13+
14+
export interface ControlDateProps {
15+
input: InputField;
16+
17+
title?: string;
18+
description?: string;
19+
icon?: ReactNode;
20+
21+
date?: boolean | DateInputProps;
22+
datetime?: boolean | DateTimePickerProps;
23+
time?: boolean | TimeInputProps;
24+
}
25+
26+
/**
27+
* ControlDate component for handling date, datetime, and time inputs.
28+
*
29+
* Features:
30+
* - DateInput for date format
31+
* - DateTimePicker for date-time format
32+
* - TimeInput for time format
33+
*
34+
* Automatically detects date formats from schema and renders appropriate picker.
35+
*/
36+
const ControlDate = (props: ControlDateProps) => {
37+
const form = useFormState(props.input);
38+
39+
if (!props.input?.props) {
40+
return null;
41+
}
42+
43+
const disabled = false; // form.loading;
44+
const id = props.input.props.id;
45+
46+
// Extract label from props or schema
47+
const label =
48+
props.title ??
49+
("title" in props.input.schema &&
50+
typeof props.input.schema.title === "string"
51+
? props.input.schema.title
52+
: undefined) ??
53+
prettyName(props.input.path);
54+
55+
// Extract description from props or schema
56+
const description =
57+
props.description ??
58+
("description" in props.input.schema &&
59+
typeof props.input.schema.description === "string"
60+
? props.input.schema.description
61+
: undefined);
62+
63+
// Extract error message
64+
const error =
65+
form.error && form.error instanceof TypeBoxError
66+
? form.error.value.message
67+
: undefined;
68+
69+
const icon = props.icon;
70+
const required = props.input.required;
71+
72+
const inputProps = {
73+
label,
74+
description,
75+
error,
76+
required,
77+
disabled,
78+
};
79+
80+
// Detect format from schema
81+
const format =
82+
props.input.schema &&
83+
"format" in props.input.schema &&
84+
typeof props.input.schema.format === "string"
85+
? props.input.schema.format
86+
: undefined;
87+
88+
// region <DateTimePicker/>
89+
if (props.datetime || format === "date-time") {
90+
const dateTimePickerProps =
91+
typeof props.datetime === "object" ? props.datetime : {};
92+
return (
93+
<DateTimePicker
94+
{...inputProps}
95+
id={id}
96+
leftSection={icon}
97+
defaultValue={
98+
props.input.props.defaultValue
99+
? new Date(props.input.props.defaultValue)
100+
: undefined
101+
}
102+
onChange={(value) => {
103+
props.input.set(value ? new Date(value).toISOString() : undefined);
104+
}}
105+
{...dateTimePickerProps}
106+
/>
107+
);
108+
}
109+
//endregion
110+
111+
// region <DateInput/>
112+
if (props.date || format === "date") {
113+
const dateInputProps = typeof props.date === "object" ? props.date : {};
114+
return (
115+
<DateInput
116+
{...inputProps}
117+
id={id}
118+
leftSection={icon}
119+
defaultValue={
120+
props.input.props.defaultValue
121+
? new Date(props.input.props.defaultValue)
122+
: undefined
123+
}
124+
onChange={(value) => {
125+
props.input.set(
126+
value ? new Date(value).toISOString().slice(0, 10) : undefined,
127+
);
128+
}}
129+
{...dateInputProps}
130+
/>
131+
);
132+
}
133+
//endregion
134+
135+
// region <TimeInput/>
136+
if (props.time || format === "time") {
137+
const timeInputProps = typeof props.time === "object" ? props.time : {};
138+
return (
139+
<TimeInput
140+
{...inputProps}
141+
id={id}
142+
leftSection={icon}
143+
defaultValue={props.input.props.defaultValue}
144+
onChange={(event) => {
145+
props.input.set(event.currentTarget.value);
146+
}}
147+
{...timeInputProps}
148+
/>
149+
);
150+
}
151+
//endregion
152+
153+
// Fallback - shouldn't happen
154+
return null;
155+
};
156+
157+
export default ControlDate;
158+
159+
const prettyName = (name: string) => {
160+
return capitalize(name.replaceAll("/", ""));
161+
};
162+
163+
const capitalize = (str: string) => {
164+
return str.charAt(0).toUpperCase() + str.slice(1);
165+
};

0 commit comments

Comments
 (0)