Skip to content

Commit 4b0018d

Browse files
Merge pull request #51 from linked-planet/feature/form-wrapper
adding initial form-wrapper to ui-kit-ts
2 parents ea77f7e + 281aa5a commit 4b0018d

File tree

17 files changed

+1399
-417
lines changed

17 files changed

+1399
-417
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
/build
1919

2020
# misc
21+
.idea
2122
.DS_Store
2223
.env.local
2324
.env.development.local
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
type Path,
3+
useForm,
4+
type Control,
5+
type DefaultValues,
6+
type FieldValues,
7+
type RegisterOptions,
8+
type UseFormRegisterReturn,
9+
type UseFormWatch,
10+
} from "react-hook-form"
11+
import { twMerge } from "tailwind-merge"
12+
import { Button } from "../Button"
13+
14+
export interface FormProps<T extends FieldValues> {
15+
control: Control<T>
16+
watch: UseFormWatch<T>
17+
register: (
18+
name: Path<T>,
19+
options?: RegisterOptions<T>,
20+
) => UseFormRegisterReturn
21+
readonly?: boolean
22+
}
23+
24+
export interface FormField<T extends FieldValues> {
25+
name: Path<T>
26+
title: string
27+
description?: string
28+
required?: boolean
29+
formProps: FormProps<T>
30+
}
31+
32+
export interface DynamicFormProps<T extends FieldValues>
33+
extends Omit<
34+
React.FormHTMLAttributes<HTMLFormElement>,
35+
"children" | "onSubmit"
36+
> {
37+
obj: DefaultValues<T>
38+
children: (
39+
formProps: FormProps<T>,
40+
reset: (newDefaultValue?: T) => void,
41+
) => React.JSX.Element
42+
onSubmit: (obj: T) => void
43+
readonly?: boolean
44+
vertical?: boolean
45+
hideReset?: boolean
46+
hideSave?: boolean
47+
}
48+
49+
export function DynamicForm<T extends FieldValues>({
50+
obj,
51+
children,
52+
onSubmit,
53+
readonly,
54+
vertical,
55+
className,
56+
hideSave,
57+
hideReset,
58+
...props
59+
}: DynamicFormProps<T>) {
60+
const {
61+
register,
62+
handleSubmit,
63+
formState: { isDirty, isValid },
64+
control,
65+
reset,
66+
watch,
67+
} = useForm<T>({
68+
defaultValues: obj,
69+
})
70+
71+
const onReset = (e: React.FormEvent) => {
72+
e.preventDefault()
73+
reset()
74+
}
75+
76+
return (
77+
<form
78+
{...props}
79+
className={twMerge(
80+
`flex ${vertical ? "flex-row" : "flex-col"} gap-4`,
81+
className,
82+
)}
83+
onSubmit={handleSubmit(onSubmit)}
84+
onReset={onReset}
85+
>
86+
{children(
87+
{
88+
control: control,
89+
watch: watch,
90+
register: register,
91+
readonly: readonly,
92+
},
93+
reset,
94+
)}
95+
96+
<hr className="border border-border" />
97+
98+
<div className="flex flex-row items-end w-full justify-end mt-4">
99+
{!hideReset && (
100+
<Button
101+
type="reset"
102+
appearance="subtle"
103+
disabled={!isDirty}
104+
>
105+
Reset
106+
</Button>
107+
)}
108+
{!hideSave && (
109+
<Button
110+
appearance="primary"
111+
type="submit"
112+
disabled={!isValid}
113+
>
114+
Save
115+
</Button>
116+
)}
117+
</div>
118+
</form>
119+
)
120+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useEffect, useRef } from "react"
2+
import type { FieldValues } from "react-hook-form"
3+
import type { FormField } from "../DynamicForm"
4+
import { Label } from "../../inputs"
5+
import { Checkbox } from "../../Checkbox"
6+
7+
export interface CheckboxFormField<T extends FieldValues> extends FormField<T> {
8+
onChange?: (value: string) => void
9+
}
10+
11+
export function CheckboxFormField<T extends FieldValues>({
12+
name,
13+
onChange,
14+
formProps,
15+
required,
16+
description,
17+
title,
18+
}: CheckboxFormField<T>) {
19+
const fieldValue = formProps.watch(name)
20+
const onChangeCB = useRef(onChange)
21+
if (onChangeCB.current !== onChange) {
22+
onChangeCB.current = onChange
23+
}
24+
25+
useEffect(() => {
26+
onChangeCB.current?.(fieldValue)
27+
}, [fieldValue])
28+
29+
const inputProps = formProps.register(name)
30+
31+
return (
32+
<div className="flex flex-1 flex-col">
33+
<Label htmlFor={name} required={required}>
34+
{title}
35+
</Label>
36+
{description && (
37+
<p className="mt-0 pb-2">
38+
<small>{description}</small>
39+
</p>
40+
)}
41+
<Checkbox
42+
className="mt-2"
43+
label={"Aktivieren"}
44+
disabled={formProps.readonly}
45+
id={inputProps?.name}
46+
{...inputProps}
47+
/>
48+
</div>
49+
)
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useEffect, useRef } from "react"
2+
import type { FieldValues } from "react-hook-form"
3+
import type { FormField } from "../DynamicForm"
4+
import { Input, Label } from "../../inputs"
5+
6+
export interface InputFormField<T extends FieldValues> extends FormField<T> {
7+
onChange?: (value: string) => void
8+
placeholder?: string
9+
}
10+
11+
export function InputFormField<T extends FieldValues>({
12+
onChange,
13+
formProps,
14+
name,
15+
required,
16+
description,
17+
title,
18+
placeholder,
19+
}: InputFormField<T>) {
20+
const fieldValue = formProps.watch(name)
21+
const onChangeCB = useRef(onChange)
22+
if (onChangeCB.current !== onChange) {
23+
onChangeCB.current = onChange
24+
}
25+
26+
useEffect(() => {
27+
onChangeCB.current?.(fieldValue)
28+
}, [fieldValue])
29+
30+
const inputProps = formProps.register(name)
31+
32+
return (
33+
<div className="flex flex-1 flex-col min-w-max">
34+
<Label htmlFor={name} required={required}>
35+
{title}
36+
</Label>
37+
{description && (
38+
<p className="mt-0 pb-2">
39+
<small>{description}</small>
40+
</p>
41+
)}
42+
<Input
43+
id={name}
44+
{...inputProps}
45+
disabled={formProps?.readonly}
46+
placeholder={placeholder}
47+
/>
48+
</div>
49+
)
50+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useEffect, useRef } from "react"
2+
import type { FieldValues } from "react-hook-form"
3+
import type { FormField } from "../DynamicForm"
4+
import { Label, Select } from "../../inputs"
5+
6+
export interface SelectMultiFormField<
7+
T extends FieldValues,
8+
A extends string | number,
9+
> extends FormField<T> {
10+
options: Array<{ label: string; value: A }>
11+
onChange?: (values: Array<string>) => void
12+
placeholder?: string
13+
}
14+
15+
export function SelectMultiFormField<
16+
T extends FieldValues,
17+
A extends string | number,
18+
>({
19+
name,
20+
onChange,
21+
formProps,
22+
required,
23+
description,
24+
title,
25+
options,
26+
placeholder,
27+
}: SelectMultiFormField<T, A>) {
28+
const fieldValue = formProps.watch(name)
29+
const onChangeCB = useRef(onChange)
30+
if (onChangeCB.current !== onChange) {
31+
onChangeCB.current = onChange
32+
}
33+
34+
useEffect(() => {
35+
onChangeCB.current?.(fieldValue)
36+
}, [fieldValue])
37+
38+
const inputProps = formProps.register(name)
39+
40+
return (
41+
<div className="flex flex-1 flex-col min-w-max">
42+
<Label htmlFor={inputProps?.name} required={required}>
43+
{title}
44+
</Label>
45+
{description && (
46+
<p className="mt-0 pb-2">
47+
<small>{description}</small>
48+
</p>
49+
)}
50+
<Select<T, A, true>
51+
isMulti
52+
id={name}
53+
name={name}
54+
control={formProps.control}
55+
options={options}
56+
required={required}
57+
disabled={formProps.readonly}
58+
placeholder={placeholder}
59+
/>
60+
</div>
61+
)
62+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useEffect, useRef } from "react"
2+
import type { FieldValues, Path } from "react-hook-form"
3+
import type { FormField } from "../DynamicForm"
4+
import { Label, Select } from "../../inputs"
5+
6+
export interface SelectSingleFormField<
7+
T extends FieldValues,
8+
A extends string | number,
9+
> extends FormField<T> {
10+
options: Array<{ label: string; value: A }>
11+
onChange?: (value: string) => void
12+
placeholder?: string
13+
}
14+
15+
export function SelectSingleFormField<
16+
T extends FieldValues,
17+
A extends string | number,
18+
>({
19+
name,
20+
description,
21+
title,
22+
formProps,
23+
onChange,
24+
required,
25+
options,
26+
placeholder,
27+
}: SelectSingleFormField<T, A>) {
28+
const fieldValue = formProps.watch(name)
29+
const onChangeCB = useRef(onChange)
30+
if (onChangeCB.current !== onChange) {
31+
onChangeCB.current = onChange
32+
}
33+
34+
useEffect(() => {
35+
onChangeCB.current?.(fieldValue)
36+
}, [fieldValue])
37+
38+
const inputProps = formProps.register(name)
39+
40+
return (
41+
<div className="flex flex-1 flex-col min-w-max">
42+
<Label htmlFor={inputProps?.name} required={required}>
43+
{title}
44+
</Label>
45+
{description && (
46+
<p className="mt-0 pb-2">
47+
<small>{description}</small>
48+
</p>
49+
)}
50+
<Select<T, A, false>
51+
id={inputProps?.name}
52+
name={inputProps?.name as Path<T>}
53+
control={formProps.control}
54+
options={options}
55+
required={required}
56+
disabled={formProps.readonly}
57+
placeholder={placeholder}
58+
/>
59+
</div>
60+
)
61+
}

library/src/components/timetable/TimeTable.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { type MutableRefObject, useCallback, useEffect, useRef } from "react"
55
import useResizeObserver from "use-resize-observer"
66
import { InlineMessage } from "../InlineMessage"
77
import type { TimeTableItemProps } from "./ItemWrapper"
8-
import { LPTimeTableHeader, headerText } from "./TimeTableHeader"
8+
import {
9+
type CustomHeaderRowHeaderProps,
10+
type CustomHeaderRowTimeSlotProps,
11+
LPTimeTableHeader,
12+
headerText,
13+
} from "./TimeTableHeader"
914
import {
1015
PlaceHolderItemPlaceHolder,
1116
type TimeTablePlaceholderItemProps,
@@ -181,6 +186,12 @@ export interface LPTimeTableProps<
181186

182187
className?: string
183188
style?: React.CSSProperties
189+
190+
/** custom header row */
191+
customHeaderRow?: {
192+
timeSlot: (props: CustomHeaderRowTimeSlotProps) => JSX.Element
193+
header: (props: CustomHeaderRowHeaderProps) => JSX.Element
194+
}
184195
}
185196

186197
const nowbarUpdateIntervall = 1000 * 60 // 1 minute
@@ -238,6 +249,7 @@ const LPTimeTableImpl = <G extends TimeTableGroup, I extends TimeSlotBooking>({
238249
disableMessages = false,
239250
className,
240251
style,
252+
customHeaderRow,
241253
}: LPTimeTableProps<G, I>) => {
242254
// if we have viewType of days, we need to round the start and end date to the start and end of the day
243255
const { setMessage, translatedMessage } = useTimeTableMessage(
@@ -495,6 +507,7 @@ const LPTimeTableImpl = <G extends TimeTableGroup, I extends TimeSlotBooking>({
495507
dateHeaderTextFormat={dateHeaderTextFormat}
496508
weekStartsOnSunday={weekStartsOnSunday}
497509
locale={locale}
510+
customHeaderRow={customHeaderRow}
498511
ref={tableHeaderRef}
499512
/>
500513
<tbody ref={tableBodyRef} className="table-fixed">

0 commit comments

Comments
 (0)