Skip to content

Commit

Permalink
Merge pull request #51 from linked-planet/feature/form-wrapper
Browse files Browse the repository at this point in the history
adding initial form-wrapper to ui-kit-ts
  • Loading branch information
marcus-wishes authored Sep 30, 2024
2 parents ea77f7e + 281aa5a commit 4b0018d
Show file tree
Hide file tree
Showing 17 changed files with 1,399 additions and 417 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
/build

# misc
.idea
.DS_Store
.env.local
.env.development.local
Expand Down
120 changes: 120 additions & 0 deletions library/src/components/form/DynamicForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
type Path,
useForm,
type Control,
type DefaultValues,
type FieldValues,
type RegisterOptions,
type UseFormRegisterReturn,
type UseFormWatch,
} from "react-hook-form"
import { twMerge } from "tailwind-merge"
import { Button } from "../Button"

export interface FormProps<T extends FieldValues> {
control: Control<T>
watch: UseFormWatch<T>
register: (
name: Path<T>,
options?: RegisterOptions<T>,
) => UseFormRegisterReturn
readonly?: boolean
}

export interface FormField<T extends FieldValues> {
name: Path<T>
title: string
description?: string
required?: boolean
formProps: FormProps<T>
}

export interface DynamicFormProps<T extends FieldValues>
extends Omit<
React.FormHTMLAttributes<HTMLFormElement>,
"children" | "onSubmit"
> {
obj: DefaultValues<T>
children: (
formProps: FormProps<T>,
reset: (newDefaultValue?: T) => void,
) => React.JSX.Element
onSubmit: (obj: T) => void
readonly?: boolean
vertical?: boolean
hideReset?: boolean
hideSave?: boolean
}

export function DynamicForm<T extends FieldValues>({
obj,
children,
onSubmit,
readonly,
vertical,
className,
hideSave,
hideReset,
...props
}: DynamicFormProps<T>) {
const {
register,
handleSubmit,
formState: { isDirty, isValid },
control,
reset,
watch,
} = useForm<T>({
defaultValues: obj,
})

const onReset = (e: React.FormEvent) => {
e.preventDefault()
reset()
}

return (
<form
{...props}
className={twMerge(
`flex ${vertical ? "flex-row" : "flex-col"} gap-4`,
className,
)}
onSubmit={handleSubmit(onSubmit)}
onReset={onReset}
>
{children(
{
control: control,
watch: watch,
register: register,
readonly: readonly,
},
reset,
)}

<hr className="border border-border" />

<div className="flex flex-row items-end w-full justify-end mt-4">
{!hideReset && (
<Button
type="reset"
appearance="subtle"
disabled={!isDirty}
>
Reset
</Button>
)}
{!hideSave && (
<Button
appearance="primary"
type="submit"
disabled={!isValid}
>
Save
</Button>
)}
</div>
</form>
)
}
50 changes: 50 additions & 0 deletions library/src/components/form/elements/CheckboxFormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useRef } from "react"
import type { FieldValues } from "react-hook-form"
import type { FormField } from "../DynamicForm"
import { Label } from "../../inputs"
import { Checkbox } from "../../Checkbox"

export interface CheckboxFormField<T extends FieldValues> extends FormField<T> {
onChange?: (value: string) => void
}

export function CheckboxFormField<T extends FieldValues>({
name,
onChange,
formProps,
required,
description,
title,
}: CheckboxFormField<T>) {
const fieldValue = formProps.watch(name)
const onChangeCB = useRef(onChange)
if (onChangeCB.current !== onChange) {
onChangeCB.current = onChange
}

useEffect(() => {
onChangeCB.current?.(fieldValue)
}, [fieldValue])

const inputProps = formProps.register(name)

return (
<div className="flex flex-1 flex-col">
<Label htmlFor={name} required={required}>
{title}
</Label>
{description && (
<p className="mt-0 pb-2">
<small>{description}</small>
</p>
)}
<Checkbox
className="mt-2"
label={"Aktivieren"}
disabled={formProps.readonly}
id={inputProps?.name}
{...inputProps}
/>
</div>
)
}
50 changes: 50 additions & 0 deletions library/src/components/form/elements/InputFormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useRef } from "react"
import type { FieldValues } from "react-hook-form"
import type { FormField } from "../DynamicForm"
import { Input, Label } from "../../inputs"

export interface InputFormField<T extends FieldValues> extends FormField<T> {
onChange?: (value: string) => void
placeholder?: string
}

export function InputFormField<T extends FieldValues>({
onChange,
formProps,
name,
required,
description,
title,
placeholder,
}: InputFormField<T>) {
const fieldValue = formProps.watch(name)
const onChangeCB = useRef(onChange)
if (onChangeCB.current !== onChange) {
onChangeCB.current = onChange
}

useEffect(() => {
onChangeCB.current?.(fieldValue)
}, [fieldValue])

const inputProps = formProps.register(name)

return (
<div className="flex flex-1 flex-col min-w-max">
<Label htmlFor={name} required={required}>
{title}
</Label>
{description && (
<p className="mt-0 pb-2">
<small>{description}</small>
</p>
)}
<Input
id={name}
{...inputProps}
disabled={formProps?.readonly}
placeholder={placeholder}
/>
</div>
)
}
62 changes: 62 additions & 0 deletions library/src/components/form/elements/SelectMultiFormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useEffect, useRef } from "react"
import type { FieldValues } from "react-hook-form"
import type { FormField } from "../DynamicForm"
import { Label, Select } from "../../inputs"

export interface SelectMultiFormField<
T extends FieldValues,
A extends string | number,
> extends FormField<T> {
options: Array<{ label: string; value: A }>
onChange?: (values: Array<string>) => void
placeholder?: string
}

export function SelectMultiFormField<
T extends FieldValues,
A extends string | number,
>({
name,
onChange,
formProps,
required,
description,
title,
options,
placeholder,
}: SelectMultiFormField<T, A>) {
const fieldValue = formProps.watch(name)
const onChangeCB = useRef(onChange)
if (onChangeCB.current !== onChange) {
onChangeCB.current = onChange
}

useEffect(() => {
onChangeCB.current?.(fieldValue)
}, [fieldValue])

const inputProps = formProps.register(name)

return (
<div className="flex flex-1 flex-col min-w-max">
<Label htmlFor={inputProps?.name} required={required}>
{title}
</Label>
{description && (
<p className="mt-0 pb-2">
<small>{description}</small>
</p>
)}
<Select<T, A, true>
isMulti
id={name}
name={name}
control={formProps.control}
options={options}
required={required}
disabled={formProps.readonly}
placeholder={placeholder}
/>
</div>
)
}
61 changes: 61 additions & 0 deletions library/src/components/form/elements/SelectSingleFormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect, useRef } from "react"
import type { FieldValues, Path } from "react-hook-form"
import type { FormField } from "../DynamicForm"
import { Label, Select } from "../../inputs"

export interface SelectSingleFormField<
T extends FieldValues,
A extends string | number,
> extends FormField<T> {
options: Array<{ label: string; value: A }>
onChange?: (value: string) => void
placeholder?: string
}

export function SelectSingleFormField<
T extends FieldValues,
A extends string | number,
>({
name,
description,
title,
formProps,
onChange,
required,
options,
placeholder,
}: SelectSingleFormField<T, A>) {
const fieldValue = formProps.watch(name)
const onChangeCB = useRef(onChange)
if (onChangeCB.current !== onChange) {
onChangeCB.current = onChange
}

useEffect(() => {
onChangeCB.current?.(fieldValue)
}, [fieldValue])

const inputProps = formProps.register(name)

return (
<div className="flex flex-1 flex-col min-w-max">
<Label htmlFor={inputProps?.name} required={required}>
{title}
</Label>
{description && (
<p className="mt-0 pb-2">
<small>{description}</small>
</p>
)}
<Select<T, A, false>
id={inputProps?.name}
name={inputProps?.name as Path<T>}
control={formProps.control}
options={options}
required={required}
disabled={formProps.readonly}
placeholder={placeholder}
/>
</div>
)
}
15 changes: 14 additions & 1 deletion library/src/components/timetable/TimeTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { type MutableRefObject, useCallback, useEffect, useRef } from "react"
import useResizeObserver from "use-resize-observer"
import { InlineMessage } from "../InlineMessage"
import type { TimeTableItemProps } from "./ItemWrapper"
import { LPTimeTableHeader, headerText } from "./TimeTableHeader"
import {
type CustomHeaderRowHeaderProps,
type CustomHeaderRowTimeSlotProps,
LPTimeTableHeader,
headerText,
} from "./TimeTableHeader"
import {
PlaceHolderItemPlaceHolder,
type TimeTablePlaceholderItemProps,
Expand Down Expand Up @@ -181,6 +186,12 @@ export interface LPTimeTableProps<

className?: string
style?: React.CSSProperties

/** custom header row */
customHeaderRow?: {
timeSlot: (props: CustomHeaderRowTimeSlotProps) => JSX.Element
header: (props: CustomHeaderRowHeaderProps) => JSX.Element
}
}

const nowbarUpdateIntervall = 1000 * 60 // 1 minute
Expand Down Expand Up @@ -238,6 +249,7 @@ const LPTimeTableImpl = <G extends TimeTableGroup, I extends TimeSlotBooking>({
disableMessages = false,
className,
style,
customHeaderRow,
}: LPTimeTableProps<G, I>) => {
// if we have viewType of days, we need to round the start and end date to the start and end of the day
const { setMessage, translatedMessage } = useTimeTableMessage(
Expand Down Expand Up @@ -495,6 +507,7 @@ const LPTimeTableImpl = <G extends TimeTableGroup, I extends TimeSlotBooking>({
dateHeaderTextFormat={dateHeaderTextFormat}
weekStartsOnSunday={weekStartsOnSunday}
locale={locale}
customHeaderRow={customHeaderRow}
ref={tableHeaderRef}
/>
<tbody ref={tableBodyRef} className="table-fixed">
Expand Down
Loading

0 comments on commit 4b0018d

Please sign in to comment.