A custom template based on NextJS in Typescript.
It includes the following libraries/frameworks:
- Chakra UI (UI Framework)
- React Hook Form (Form Validation)
- Joi (Validation Schema)
- React Query (Data Handling)
The following are also setup:
- Added
useJoiForm
that assists in usinguseForm
with ajoiResolver
. This uses a custom classFormObject
that would also assist you in creating the default values and schemas of your form. - Addded
<QFormControl />
which can help createFormControl
s already integrated withreact-hook-form
. To use this:- you must wrap them in a
<FormProvider />
. More info here. - For nested or complex inputs (e.g, using
<InputGroup />
), use the control prop to set what component should be registered.
- you must wrap them in a
- Custom Layout component with Sidebar
- ESLint setup using recommended rules
- Custom prettier rules that includes automatic sorting of imports
- Pre-commit check that validates if there are no prettier, ESLint and Typescript errors
- Added a post-install script that generates Chakra UI theme typings that takes into account the extended theme set on
/theme.ts
interface UserProfile {
firstName: string;
lastName: string;
age: number;
gender: GenderDemographicsEnum;
yearsInCurrRole: number;
}
export default new FormObject<UserProfile>(
{
firstName: '',
lastName: '',
age: null,
gender: null,
yearsInCurrRole: null,
},
{
firstName: Joi.string()
.required()
.messages(createErrorMessages('First Name', ['emptyString'])),
lastName: Joi.string()
.required()
.messages(createErrorMessages('Last Name', ['emptyString'])),
age: Joi.number()
.min(MIN_AGE)
.required()
.messages(
createErrorMessages('Age', [
'notNumber',
{ type: 'lessThanMin', min: MIN_AGE },
])
),
gender: Joi.string()
.valid(...Object.values(GenderDemographicsEnum))
.messages(createErrorMessages('Gender', ['emptyString', 'notOption'])),
yearsInCurrRole: Joi.number()
.min(MIN_YEARS_IN_CURR_ROLE)
.required()
.messages(
createErrorMessages('Field', [
'notNumber',
{ type: 'lessThanMin', min: MIN_YEARS_IN_CURR_ROLE },
])
),
}
);
-
The form must be surrounded by
<FormProvider />
. -
A
<FormControl />
wrapper named<QFormControl />
was created to do the following:- Handle passing the
register(name)
props to the child input component. This means that you would only need to pass thename
prop to link the input component to a field inreact-hook-form
. - Handles showing the
react-hook-form
error messages. This can be disabled by passing thehideErrorMessage
prop to<QFormControl />
.
- Handle passing the
const EditProfile: React.FC = () => {
const methods = useJoiForm(profileFormObject);
return (
<FormProvider {...methods}>
<QFormControl
name="firstName"
label="First Name"
isRequired
>
<Input variant="flushed" />
</QFormControl>
<QFormControl
name="lastName"
label="Last Name"
isRequired
>
<Input variant="flushed" />
</QFormControl>
/* ... */
</FormProvider>
)
}
This should also work for other Chakra UI input components like<Select />
, <Textarea />
, and <Checkbox />
.
<QFormControl name="confirmed" isRequired mt={8} hideErrorMessage>
<Checkbox>
I agree to be contacted when new organisations are added.
</Checkbox>
</QFormControl>
<QFormControl name="country" label="Country" isRequired flex={1}>
<Select variant="flushed">
{Object.values(countries).map((country) => (
<option value={country.code} key={country.code}>
{country.name}
</option>
))}
</Select>
</QFormControl>
If you need to use nested (e.g, using <InputGroup />
) or complex (e.g, handling array of inputs)input components , using react-hook-form
's <Controller />
is the way to go.
const EditProfile: React.FC = () => {
const methods = useJoiForm(profileFormObject);
const surveyFormData = [
{
name: 'serviceQuality',
label: 'Quality of Service',
img: PaternityImage,
},
// ...
]
return (
<Layout>
{surveyPageData.map((pdata) => (
<Box
key={`slider-${pdata.name}`}
display={pdata.name === pageData.name ? 'inherit' : 'none'}
>
<Controller
control={methods.control}
name={pdata.name}
render={({ field: { onChange, value, name } }) => (
<AnimatedSlider
onChange={onChange}
value={value}
name={name}
/>
)}
/>
</Box>
))}
</Layout>
)
}
Use the createApiHandler
to easily create your api routes.
// /pages/api/users/[id].ts
const UserHandler = createApiHandler().get(async (req, res) => {
const { id } = req.query;
if (isInvalid(id)) {
throw new Error('ID invalid');
}
const userData = getData(id);
return res.json(userData);
}).post(async (req, res) => {
// ...
}).put(async (req, res) => {
// ...
})
//...
Extend the APIError
class at /src/lib/errors/APIError
when creating custom errors.
export class ListingNotFoundError extends ApiError {
constructor(id: string) {
super(404, `Listing of ${id} was not found.`);
}
}