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

remove formik in favor of react-hook-form #239

Merged
merged 2 commits into from
Sep 22, 2024
Merged
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
4 changes: 2 additions & 2 deletions frontend/apps/mobile/src/components/DateTimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DateTimePickerAndroid, DateTimePickerEvent } from "@react-native-commun
import React, { useEffect, useState } from "react";
import { HelperText, TextInput } from "react-native-paper";

interface Props
export interface DateTimeInputProps
extends Omit<React.ComponentProps<typeof TextInput>, "onChange" | "value" | "disabled" | "editable" | "mode"> {
value: Date | null;
onChange: (newValue: Date) => void;
Expand All @@ -12,7 +12,7 @@ interface Props
editable?: boolean;
}

export const DateTimeInput: React.FC<Props> = ({
export const DateTimeInput: React.FC<DateTimeInputProps> = ({
value,
onChange,
mode = "date",
Expand Down
27 changes: 27 additions & 0 deletions frontend/apps/mobile/src/components/FormCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from "react";
import { Control, Controller } from "react-hook-form";
import { Checkbox, HelperText, CheckboxItemProps } from "react-native-paper";

export type FormCheckboxProps = Omit<CheckboxItemProps, "onPress" | "status"> & {
name: string;
control: Control<any, any>;
};

export const FormCheckbox: React.FC<FormCheckboxProps> = ({ name, control, ...props }) => {
return (
<Controller
name={name}
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<>
<Checkbox.Item
status={value ? "checked" : "unchecked"}
onPress={() => onChange(!value)}
{...props}
/>
{error && <HelperText type="error">{error.message}</HelperText>}
</>
)}
/>
);
};
30 changes: 30 additions & 0 deletions frontend/apps/mobile/src/components/FormDateTimeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from "react";
import { Control, Controller } from "react-hook-form";
import { HelperText } from "react-native-paper";
import DateTimeInput, { DateTimeInputProps } from "./DateTimeInput";
import { fromISOStringNullable, toISODateStringNullable } from "@abrechnung/utils";

export type FormDateTimeInputProps = Omit<DateTimeInputProps, "onChange" | "value" | "error"> & {
name: string;
control: Control<any, any>;
};

export const FormDateTimeInput: React.FC<FormDateTimeInputProps> = ({ name, control, ...props }) => {
return (
<Controller
control={control}
name={name}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<>
<DateTimeInput
value={fromISOStringNullable(value)}
onChange={(val) => onChange(toISODateStringNullable(val))}
error={!!error}
{...props}
/>
{error && <HelperText type="error">{error.message}</HelperText>}
</>
)}
/>
);
};
24 changes: 24 additions & 0 deletions frontend/apps/mobile/src/components/FormNumericInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react";
import { Control, Controller } from "react-hook-form";
import { HelperText } from "react-native-paper";
import { NumericInput, NumericInputProps } from "./NumericInput";

export type FormNumericInput = Omit<NumericInputProps, "onChange" | "value" | "error"> & {
name: string;
control: Control<any, any>;
};

export const FormNumericInput: React.FC<FormNumericInput> = ({ name, control, ...props }) => {
return (
<Controller
name={name}
control={control}
render={({ field: { onChange, onBlur, value }, fieldState: { error } }) => (
<>
<NumericInput error={!!error} onChange={onChange} onBlur={onBlur} value={value} {...props} />
{error && <HelperText type="error">{error.message}</HelperText>}
</>
)}
/>
);
};
24 changes: 24 additions & 0 deletions frontend/apps/mobile/src/components/FormTagSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react";
import { Control, Controller } from "react-hook-form";
import { HelperText } from "react-native-paper";
import { TagSelect, TagSelectProps } from "./tag-select";

export type FormTagSelect = Omit<TagSelectProps, "value" | "onChange"> & {
name: string;
control: Control<any, any>;
};

export const FormTagSelect: React.FC<FormTagSelect> = ({ name, control, ...props }) => {
return (
<Controller
control={control}
name={name}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<>
<TagSelect value={value} onChange={onChange} {...props} />
{error && <HelperText type="error">{error.message}</HelperText>}
</>
)}
/>
);
};
23 changes: 23 additions & 0 deletions frontend/apps/mobile/src/components/FormTextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from "react";
import { Control, Controller } from "react-hook-form";
import { HelperText, TextInput, TextInputProps } from "react-native-paper";

export type FormTextInputProps = Omit<TextInputProps, "onChange" | "value" | "error"> & {
name: string;
control: Control<any, any>;
};

export const FormTextInput: React.FC<FormTextInputProps> = ({ name, control, ...props }) => {
return (
<Controller
name={name}
control={control}
render={({ field: { onChange, onBlur, value }, fieldState: { error } }) => (
<>
<TextInput error={!!error} onChange={onChange} onBlur={onBlur} value={value} {...props} />
{error && <HelperText type="error">{error.message}</HelperText>}
</>
)}
/>
);
};
24 changes: 24 additions & 0 deletions frontend/apps/mobile/src/components/FormTransactionShareInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react";
import { Control, Controller } from "react-hook-form";
import { HelperText } from "react-native-paper";
import { TransactionShareInput, TransactionShareInputProps } from "./transaction-shares/TransactionShareInput";

export type FormTransactionShareInputProps = Omit<TransactionShareInputProps, "value" | "onChange"> & {
name: string;
control: Control<any, any>;
};

export const FormTransactionShareInput: React.FC<FormTransactionShareInputProps> = ({ name, control, ...props }) => {
return (
<Controller
control={control}
name={name}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<>
<TransactionShareInput value={value} onChange={onChange} error={!!error} {...props} />
{error && <HelperText type="error">{error.message}</HelperText>}
</>
)}
/>
);
};
4 changes: 2 additions & 2 deletions frontend/apps/mobile/src/components/NumericInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from "react";
import { TextInput } from "react-native-paper";
import { parseAbrechnungFloat } from "@abrechnung/utils";

type Props = Omit<React.ComponentProps<typeof TextInput>, "onChange" | "value"> & {
export type NumericInputProps = Omit<React.ComponentProps<typeof TextInput>, "onChange" | "value"> & {
value: number;
onChange: (newValue: number) => void;
};

export const NumericInput: React.FC<Props> = ({ value, onChange, ...props }) => {
export const NumericInput: React.FC<NumericInputProps> = ({ value, onChange, ...props }) => {
const [internalValue, setInternalValue] = React.useState("");
const { editable } = props;

Expand Down
6 changes: 6 additions & 0 deletions frontend/apps/mobile/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ export * from "./NumericInput";
export * from "./DateTimeInput";
export * from "./CurrencySelect";
export * from "./tag-select";
export * from "./FormTextInput";
export * from "./FormCheckbox";
export * from "./FormTransactionShareInput";
export * from "./FormTagSelect";
export * from "./FormNumericInput";
export * from "./FormDateTimeInput";
4 changes: 2 additions & 2 deletions frontend/apps/mobile/src/components/tag-select/TagSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { TouchableHighlight, View } from "react-native";
import { Portal, Text, useTheme } from "react-native-paper";
import { TagSelectDialog } from "./TagSelectDialog";

interface Props {
export interface TagSelectProps {
groupId: number;
label: string;
value: string[];
disabled: boolean;
onChange: (newValue: string[]) => void;
}

export const TagSelect: React.FC<Props> = ({ groupId, label, value, onChange, disabled }) => {
export const TagSelect: React.FC<TagSelectProps> = ({ groupId, label, value, onChange, disabled }) => {
const theme = useTheme();
const [showDialog, setShowDialog] = useState(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { TransactionShare } from "@abrechnung/types";
import { useAppSelector } from "../../store";
import { selectGroupAccounts } from "@abrechnung/redux";

interface Props {
export interface TransactionShareInputProps {
groupId: number;
title: string;
multiSelect: boolean;
Expand All @@ -19,7 +19,7 @@ interface Props {
excludedAccounts?: number[];
}

export const TransactionShareInput: React.FC<Props> = ({
export const TransactionShareInput: React.FC<TransactionShareInputProps> = ({
groupId,
title,
multiSelect,
Expand Down Expand Up @@ -103,5 +103,3 @@ export const TransactionShareInput: React.FC<Props> = ({
</>
);
};

export default TransactionShareInput;
101 changes: 40 additions & 61 deletions frontend/apps/mobile/src/screens/AddGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,57 @@
import { components } from "@abrechnung/api";
import { createGroup } from "@abrechnung/redux";
import { toFormikValidationSchema } from "@abrechnung/utils";
import { useFormik } from "formik";
import React from "react";
import { StyleSheet, View } from "react-native";
import { Button, Checkbox, HelperText, ProgressBar, TextInput, useTheme } from "react-native-paper";
import { Button, HelperText, useTheme } from "react-native-paper";
import { CurrencySelect } from "../components/CurrencySelect";
import { useApi } from "../core/ApiProvider";
import { GroupStackScreenProps } from "../navigation/types";
import { useAppDispatch } from "../store";
import { StackNavigationOptions } from "@react-navigation/stack";
import { z } from "zod";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { FormCheckbox, FormTextInput } from "../components";
import { notify } from "../notifications";

type FormSchema = z.infer<typeof components.schemas.GroupPayload>;

export const AddGroup: React.FC<GroupStackScreenProps<"AddGroup">> = ({ navigation }) => {
const theme = useTheme();
const dispatch = useAppDispatch();
const { api } = useApi();

const formik = useFormik({
initialValues: {
const {
control,
handleSubmit,
reset: resetForm,
} = useForm<FormSchema>({
resolver: zodResolver(components.schemas.GroupPayload),
defaultValues: {
name: "",
description: "",
currency_symbol: "€",
terms: "",
add_user_account_on_join: false,
},
validationSchema: toFormikValidationSchema(components.schemas.GroupPayload),
onSubmit: (values, { setSubmitting }) => {
setSubmitting(true);
});
const onSubmit = React.useCallback(
(values: FormSchema) => {
dispatch(createGroup({ api, group: values }))
.unwrap()
.then(() => {
setSubmitting(false);
navigation.goBack();
})
.catch(() => {
setSubmitting(false);
notify({ text: "An error occured during group creation" });
});
},
});
[dispatch, navigation, api]
);

const cancel = React.useCallback(() => {
formik.resetForm();
resetForm();
navigation.goBack();
}, [formik, navigation]);
}, [resetForm, navigation]);

React.useLayoutEffect(() => {
navigation.setOptions({
Expand All @@ -53,63 +62,33 @@ export const AddGroup: React.FC<GroupStackScreenProps<"AddGroup">> = ({ navigati
<Button onPress={cancel} textColor={theme.colors.error}>
Cancel
</Button>
<Button onPress={() => formik.handleSubmit()}>Save</Button>
<Button onPress={() => handleSubmit(onSubmit)()}>Save</Button>
</>
);
},
} as any);
}, [theme, navigation, formik, cancel]);
}, [theme, navigation, handleSubmit, onSubmit, cancel]);

return (
<View style={styles.container}>
{formik.isSubmitting ? <ProgressBar indeterminate /> : null}
<TextInput
label="Name"
value={formik.values.name}
style={styles.input}
onChangeText={(val) => formik.setFieldValue("name", val)}
error={formik.touched.name && !!formik.errors.name}
/>
{formik.touched.name && !!formik.errors.name ? (
<HelperText type="error">{formik.errors.name}</HelperText>
) : null}
<TextInput
label="Description"
value={formik.values.description}
style={styles.input}
onChangeText={(val) => formik.setFieldValue("description", val)}
error={formik.touched.description && !!formik.errors.description}
/>
{formik.touched.description && !!formik.errors.description ? (
<HelperText type="error">{formik.errors.description}</HelperText>
) : null}
<TextInput
label="Terms"
value={formik.values.terms}
style={styles.input}
multiline={true}
onChangeText={(val) => formik.setFieldValue("terms", val)}
error={formik.touched.terms && !!formik.errors.terms}
/>
{formik.touched.terms && !!formik.errors.terms ? (
<HelperText type="error">{formik.errors.terms}</HelperText>
) : null}
<CurrencySelect
label="Currency"
value={formik.values.currency_symbol}
onChange={(val) => formik.setFieldValue("currency_symbol", val)}
// error={formik.touched.description && !!formik.errors.currency_symbol}
<FormTextInput label="Name" name="name" control={control} style={styles.input} />
<FormTextInput label="Description" name="description" control={control} style={styles.input} />
<FormTextInput label="Terms" name="terms" control={control} style={styles.input} multiline={true} />
<Controller
name="currency_symbol"
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<>
<CurrencySelect label="Currency" value={value} onChange={onChange} />
{error && <HelperText type="error">{error.message}</HelperText>}
</>
)}
/>
{formik.touched.currency_symbol && !!formik.errors.description ? (
<HelperText type="error">{formik.errors.currency_symbol}</HelperText>
) : null}
<Checkbox.Item
<FormCheckbox
label="Add user accounts on join"
status={formik.values.add_user_account_on_join ? "checked" : "unchecked"}
name="add_user_account_on_join"
control={control}
style={styles.input}
onPress={() =>
formik.setFieldValue("add_user_account_on_join", !formik.values.add_user_account_on_join)
}
/>
</View>
);
Expand Down
Loading
Loading