Skip to content

Commit

Permalink
feat: setup a dummy form example with validation and keyboard avoidance
Browse files Browse the repository at this point in the history
  • Loading branch information
tsyirvo committed Apr 15, 2024
1 parent 0b982ca commit 0c0b096
Show file tree
Hide file tree
Showing 18 changed files with 307 additions and 96 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"postpack": "pinst --enable"
},
"dependencies": {
"@hookform/resolvers": "3.3.4",
"@react-native-masked-view/masked-view": "0.3.0",
"@react-navigation/native": "6.1.17",
"@react-navigation/native-stack": "6.9.26",
Expand Down Expand Up @@ -106,11 +107,12 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "4.0.13",
"react-hook-form": "7.51.3",
"react-i18next": "14.1.0",
"react-native": "0.73.6",
"react-native-device-info": "10.13.1",
"react-native-gesture-handler": "~2.14.0",
"react-native-keyboard-aware-scroll-view": "0.9.5",
"react-native-keyboard-controller": "1.11.6",
"react-native-mmkv": "2.11.0",
"react-native-reanimated": "~3.6.2",
"react-native-safe-area-context": "4.8.2",
Expand Down
19 changes: 11 additions & 8 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ErrorInfo } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { StatusBar, StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { KeyboardProvider } from 'react-native-keyboard-controller';
import {
SafeAreaProvider,
initialWindowMetrics,
Expand Down Expand Up @@ -63,17 +64,19 @@ const App = () => {
>
<Splashscreen>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<Sandbox>
<>
<RootStack />
<KeyboardProvider>
<Sandbox>
<>
<RootStack />

<Toast config={toastConfig} />
<Toast config={toastConfig} />

<AppUpdateNeeded />
<AppUpdateNeeded />

<MaintenanceMode />
</>
</Sandbox>
<MaintenanceMode />
</>
</Sandbox>
</KeyboardProvider>
</SafeAreaProvider>
</Splashscreen>
</ErrorBoundary>
Expand Down
28 changes: 28 additions & 0 deletions src/core/i18n/resources/en/miscScreens.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,34 @@
"description": "An app update is mandatory to be able to use the application.",
"title": "Update is required"
},
"dummyForm": {
"form": {
"email": {
"label": "Email",
"placeholder": "Enter your email",
"validation": {
"email": "Please enter a valid email"
}
},
"firstName": {
"label": "First name",
"placeholder": "John",
"validation": {
"maxLength": "First name must be at most 20 characters long",
"minLength": "First name must be at least 2 characters long"
}
},
"lastName": {
"label": "Last name",
"placeholder": "Doe",
"validation": {
"maxLength": "Last name must be at most 30 characters long",
"minLength": "Last name must be at least 2 characters long"
}
}
},
"screenTitle": "Dummy form"
},
"errorBoundary": {
"cta": "Relaunch the app",
"description": "An unknown error occured. If the error persist, contact an administrator.",
Expand Down
28 changes: 28 additions & 0 deletions src/core/i18n/resources/fr/miscScreens.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,34 @@
"description": "Une mise à jour de l'application est requise pour fonctionner.",
"title": "Mise à jour requise"
},
"dummyForm": {
"form": {
"email": {
"label": "Email",
"placeholder": "Saisir un email",
"validation": {
"email": "Il faut un email valide"
}
},
"firstName": {
"label": "Prénom",
"placeholder": "Martin",
"validation": {
"maxLength": "Le prénom doit faire au plus 20 caractères",
"minLength": "Le prénom doit faire au moins 2 caractères"
}
},
"lastName": {
"label": "Nom",
"placeholder": "Dupont",
"validation": {
"maxLength": "Le nom doit faire au plus 30 caractères",
"minLength": "Le nom doit faire au moins 2 caractères"
}
}
},
"screenTitle": "Dummy form"
},
"errorBoundary": {
"cta": "Relancer l'app",
"description": "Une erreur est survenue. Si l'erreur persiste, contacter un administrateur.",
Expand Down
2 changes: 1 addition & 1 deletion src/core/monitoring/errorMonitoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class ErrorMonitoringClass {

Sentry.init({
dsn: config.sentryDsn,
debug: config.env === 'development',
debug: false,
tracesSampleRate: sampleRate,
enabled: isEnabled,
environment: config.env,
Expand Down
6 changes: 5 additions & 1 deletion src/core/navigation/RootStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ export const RootStack = () => {

<Stack.Screen component={screens.BlogPost} name="BlogPost" />

<Stack.Screen component={screens.DummyForm} name="DummyForm" />
<Stack.Screen
component={screens.DummyForm}
name="DummyForm"
options={{ title: t('dummyForm.screenTitle', { ns: 'miscScreens' }) }}
/>

{/* inject screens before this */}
</Stack.Navigator>
Expand Down
111 changes: 111 additions & 0 deletions src/features/dummyForm/DummyFormExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/* eslint-disable react/jsx-props-no-spreading */

import { zodResolver } from '@hookform/resolvers/zod';
import { useRef } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import type { TextInput } from 'react-native';

import {
DummyFormSchema,
type DummyFormSchemaType,
} from '$features/dummyForm/utils/dummyForm.schema';
import { Button } from '$shared/uiKit/button';
import { Input } from '$shared/uiKit/input';
import { Box } from '$shared/uiKit/primitives';

export const DummyFormExample = () => {
const firstNameInputRef = useRef<TextInput>(null);
const lastNameInputRef = useRef<TextInput>(null);

const { t } = useTranslation('miscScreens');

const { control, handleSubmit } = useForm<DummyFormSchemaType>({
resolver: zodResolver(DummyFormSchema),
});

const onSubmit: SubmitHandler<DummyFormSchemaType> = (data) => {
// Do your form submission stuff here
console.log('data', data);

Check warning on line 30 in src/features/dummyForm/DummyFormExample.tsx

View workflow job for this annotation

GitHub Actions / quality

Unexpected console statement
};

return (
<>
<Box pb="spacing_16" pt="spacing_32">
<Controller
control={control}
name="email"
render={({ field: { onChange, onBlur, value }, fieldState }) => (
<Input
autoCapitalize="none"
autoComplete="email"
autoCorrect={false}
error={fieldState.error?.message}
keyboardType="email-address"
label={t('dummyForm.form.email.label')}
placeholder={t('dummyForm.form.email.placeholder')}
returnKeyType="next"
value={value}
onBlur={onBlur}
onChangeText={onChange}
onSubmitEditing={() => {
firstNameInputRef.current?.focus();
}}
/>
)}
/>
</Box>

<Box pb="spacing_16">
<Controller
control={control}
name="firstName"
render={({ field: { onChange, onBlur, value }, fieldState }) => (
<Input
ref={firstNameInputRef}
autoComplete="name-given"
error={fieldState.error?.message}
label={t('dummyForm.form.firstName.label')}
placeholder={t('dummyForm.form.firstName.placeholder')}
returnKeyType="next"
value={value}
onBlur={onBlur}
onChangeText={onChange}
onSubmitEditing={() => {
lastNameInputRef.current?.focus();
}}
/>
)}
/>
</Box>

<Box pb="spacing_24">
<Controller
control={control}
name="lastName"
render={({ field: { onChange, onBlur, value }, fieldState }) => (
<Input
ref={lastNameInputRef}
autoComplete="name-family"
error={fieldState.error?.message}
label={t('dummyForm.form.lastName.label')}
placeholder={t('dummyForm.form.lastName.placeholder')}
returnKeyType="done"
value={value}
onBlur={onBlur}
onChangeText={onChange}
onSubmitEditing={handleSubmit(onSubmit)}
/>
)}
/>
</Box>

<Button.Text
onPress={handleSubmit(onSubmit) as (arg: unknown) => Promise<unknown>}
>
Submit
</Button.Text>
</>
);
};
1 change: 1 addition & 0 deletions src/features/dummyForm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DummyFormExample as DummyForm } from './DummyFormExample';
39 changes: 39 additions & 0 deletions src/features/dummyForm/utils/dummyForm.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import i18next from 'i18next';
import { z } from 'zod';

const FIRST_NAME_MIN_LENGTH = 2;
const FIRST_NAME_MAX_LENGTH = 20;
const LAST_NAME_MIN_LENGTH = 2;
const LAST_NAME_MAX_LENGTH = 30;

export const DummyFormSchema = z.object({
email: z.string().email({
message: i18next.t('miscScreens:dummyForm.form.email.validation.email'),
}),
firstName: z
.string()
.min(FIRST_NAME_MIN_LENGTH, {
message: i18next.t(
'miscScreens:dummyForm.form.firstName.validation.minLength',
),
})
.max(FIRST_NAME_MAX_LENGTH, {
message: i18next.t(
'miscScreens:dummyForm.form.firstName.validation.maxLength',
),
}),
lastName: z
.string()
.min(LAST_NAME_MIN_LENGTH, {
message: i18next.t(
'miscScreens:dummyForm.form.lastName.validation.minLength',
),
})
.max(LAST_NAME_MAX_LENGTH, {
message: i18next.t(
'miscScreens:dummyForm.form.lastName.validation.minLength',
),
}),
});

export type DummyFormSchemaType = z.infer<typeof DummyFormSchema>;
23 changes: 15 additions & 8 deletions src/screens/DummyForm.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Box, Text } from '$shared/uiKit/primitives';
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';

import { DummyForm as DummyFormComponent } from '$features/dummyForm';
import { Box } from '$shared/uiKit/primitives';
import { Screen } from '$shared/uiKit/Screen';

export const DummyForm = () => (
<Screen>
<Box p="spacing_16">
<Text variant="large">DummyForm screen</Text>
</Box>
</Screen>
);
export const DummyForm = () => {
return (
<Screen isScrollable={false}>
<KeyboardAwareScrollView bottomOffset={50}>
<Box px="spacing_16">
<DummyFormComponent />
</Box>
</KeyboardAwareScrollView>
</Screen>
);
};
15 changes: 7 additions & 8 deletions src/screens/OtherScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,25 @@ export const OtherScreen = ({ navigation }: OtherScreenProps) => {
return (
<Screen>
<Box borderBottomColor="black" borderBottomWidth={1} p="spacing_16">
<Text testID="otherPage_title" variant="large">
<Text testID="apiExample_title" variant="large">
{t('graphql.title')}
</Text>

<Box alignItems="flex-start" mt="spacing_8">
<Button.Text testID="back_button" onPress={goToBlogPost}>
<Button.Text
testID="apiExample_navigate_button"
onPress={goToBlogPost}
>
{t('graphql.cta')}
</Button.Text>
</Box>
</Box>

<Box pt="spacing_16" px="spacing_16">
<Text testID="otherPage_title" variant="large">
{t('form.title')}
</Text>
<Text variant="large">{t('form.title')}</Text>

<Box alignItems="flex-start" mt="spacing_8">
<Button.Text testID="back_button" onPress={goToDummyForm}>
{t('form.cta')}
</Button.Text>
<Button.Text onPress={goToDummyForm}>{t('form.cta')}</Button.Text>
</Box>
</Box>
</Screen>
Expand Down
13 changes: 0 additions & 13 deletions src/screens/__tests__/BlogPost.test.tsx

This file was deleted.

13 changes: 0 additions & 13 deletions src/screens/__tests__/DummyForm.test.tsx

This file was deleted.

Loading

0 comments on commit 0c0b096

Please sign in to comment.