# Dynamic forms with DRF & React
Connecting REST APIs with frontend components is often a time-consuming task. Developers typically grapple with crafting HTML forms, implementing val``idation (using libraries like Yup, Zod, or Joi), and integrating them with the API. This process can be significantly streamlined by leveraging the power of reusable components and Django DRF's exceptional capabilities.
In this article, we'll explore how to expedite this process and enhance development efficiency. By combining well-structured reusable components with Django DRF's robust features, you can dramatically reduce development time and create a more maintainable codebase.
Discover how to supercharge your frontend development with our [expert Django development services](https://www.manystrategy.com/django-development/).
## Backend setup
We first create a django Model that will be used to store the data. lets use a Books API model.
> Language: python
> Path: books/models.py
```python
from django.db import models
from django.contrib.auth.models import User
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.CharField(max_length=255)
description = models.TextField()
sales = models.IntegerField(default=0)
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
user = models.ForeignKey(User, related_name="books", on_delete=models.CASCADE)
```
We need a serializer to convert the model to JSON.
> Language: python
> Path: books/serializers.py
```python
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = (
"id",
"title",
"author",
"description",
"sales",
"published",
"created_at",
"updated_at",
)
```
The view for this file is located in `books/views.py`
> Language: python
> Path: books/views.py
```python
from rest_framework import generics
from rest_framework.response import Response
class BookList(generics.ListCreateAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
```
the urls for the API are created in the same file.
> Language: python
> Path: books/urls.py
```python
from django.urls import path
from . import views
router = routers.DefaultRouter()
# register the urls for the API
router.register(r'books', views.BookList, basename='books')
urlpatterns = [
path('', include(router.urls)),
]
```
This is the basic setup on the django backend. this should give us a usable API endoint `/api/books/`. (based on how we setup the project)
## Frontend setup
We are now ready to start building our frontend. We will use React and tailwindCSS build our frontend.
reference: https://tailwindcss.com/docs/installation
For forms we will be using the `react-hook-form` library.
Lets start by creating a new form component.
### Standard method
> Language: javascript
> Path: components/books/forms.js
```jsx
import React, { useState } from "react";
import { useForm } from "react-hook-form";
const BookForm = () => {
const {
register,
handleSubmit,
formstate: { errors },
} = useForm();
const onSubmit = async (data) => {
// your submit logic here
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col">
<label htmlFor="title">Title</label>
<input
type="text"
name="title"
className={`${errors.title && "border-red-500"}`}
{...register(title)}
/>
<span className="text-red-500">{errors[title]?.message}</span>
</div>
<div className="flex flex-col">
<label htmlFor="author">Author</label>
<input
type="text"
name="author"
className={`${errors.author && "border-red-500"}`}
{...register(author)}
/>
<span className="text-red-500">{errors[author]?.message}</span>
</div>
<div className="flex flex-col">
<label htmlFor="description">Description</label>
<textarea
name="description"
className={`${errors.description && "border-red-500"}`}
{...register(description)}
/>
<span className="text-red-500">{errors[description]?.message}</span>
</div>
<>...</>
<>...</>
<>Other form fields</>
<>...</>
<>...</>
<div className="flex flex-col">
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
disabled={isSubmitting}
>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
</div>
</form>
);
};
```
## Problems
- the form is very verbose and quite repetitive.
- there are a lot of fields that are not required.
- the form is not connected to the backend directly / for eg if a field in backend is changed, the form will not update.
- developers have to manually update the form when the backend changes.
Lets try and fix these problems.
## Solution
### 1 Reusable components
Lets start by creating input components which play nice with react hook forms and are reusable.
> Language: javascript
> Path: components/forms/inputs.js
```jsx
import React from "react";
const Input = ({ register, label, type, name, errors, ...props }) => {
// a generic input component that can be used for any field
return (
<div className="my-3 flex w-full flex-col items-start justify-between">
<span className="form-title">{label}</span>
<input
type={type}
className={`input-bordered input w-full ${
errors[name] && "input-error"
} bg-gray-50 px-2 py-2`}
{...register(name)}
{...props}
/>
<span className="text-sm text-red-500">{errors[name]?.message}</span>
</div>
);
};
const BooleanInput = ({ register, label, name, errors, ...props }) => {
// a checkbox component that can be used for boolean fields
return (
<div className="my-3 flex w-full flex-col items-start justify-between">
<span className="form-title">{label}</span>
<div className="w-full">
<input
type="checkbox"
className={`input-bordered input toggle-primary toggle toggle-lg ${
errors[name] && "input-error"
} bg-gray-50 px-2 py-2`}
{...register(name)}
{...props}
/>
<span className="text-sm text-red-500">{errors[name]?.message}</span>
</div>
</div>
);
};
const SearchableSelect = ({ label, options, onChange, ...props }) => {
// a searchable select component that can be used for any choice fields
return (
<div className="my-3 w-full">
<span className="form-title">{label}</span>
<Select options={options} onChange={onChange} {...props} />
</div>
);
};
function TextAreaInput({ register, label, type, name, errors, ...props }) {
// a textarea component that can be used for any text fields
return (
<div className="my-3 flex w-full flex-col items-start">
<span className="form-title">{label}</span>
<div className="w-full">
<textarea
rows="3"
className={`textarea-bordered textarea w-full ${
errors[name] && "input-error"
} bg-gray-50 px-2 py-2`}
{...register(name)}
{...props}
/>
<span className="text-sm text-red-500">{errors[name]?.message}</span>
</div>
</div>
);
}
```
### 2 Form component
lets build a form component that will be used to create new dyanamic forms composed of the above components.
> Language: javascript
> Path: components/forms/form.js
```jsx
const Form = ({ fields, hook }) => {
// destructuring the hook object to get the form state and the form methods
const {
register,
setValue,
formState: { errors },
} = hook;
// map over the fields array and create a form component for each field
return fields.map((item) => {
if (item.type == "select")
return (
<div key={item.name}>
<SearchableSelect
placeholder={
item.placeholder ? item.placeholder : `select ${item.label}`
}
onChange={(option) => setValue(item.name, option.value)}
{...item}
/>
<span className="text-sm text-red-500">
{errors[item.name]?.message}
</span>
</div>
);
if (item.type == "textarea")
return (
<TextAreaInput
key={item.name}
register={register}
errors={errors}
{...item}
/>
);
if (item.type == "checkbox")
return (
<BooleanInput
key={item.name}
register={register}
errors={errors}
{...item}
/>
);
return (
<Input key={item?.name} register={register} errors={errors} {...item} />
);
});
};
```
### 3 Dynamic form
Lets build a dynamic form that will be used to create new dyanamic forms composed of the above components.
> Language: javascript
> Path: components/forms/dynamic-form.js
```jsx
export const AutoForm = ({
fields,
formHook,
onSubmit,
submitButton,
...props
}) => {
const { handleSubmit } = formHook;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div {...props}>
<Form fields={fields} hook={formHook} {...props} />
</div>
<div className="mt-8 flex justify-end">
<button type="submit" className="btn flex gap-2">
{submitButton ? submitButton : "submit"}
</button>
</div>
</form>
);
};
```
This component will be used to create dynamic forms. when we pass in a object array of fields, it will create a form with the fields. fields array will typically look like this
```javascript
const fields = [
{
name: "title",
label: "Title of the book",
type: "text",
placeholder: "Enter name",
},
{
name: "author",
label: "Author of the book",
type: "text",
placeholder: "Enter author",
},
{
name: "description",
label: "Description of the book",
type: "textarea",
placeholder: "Enter description",
},
{
name: "published",
label: "Published",
type: "checkbox",
placeholder: "Is the book published?",
},
{
name: "sales",
label: "Sales",
type: "number",
placeholder: "Enter sales",
}
{
name: "created_at",
label: "Created",
type: "date",
placeholder: "Enter date",
},
];
```
### 4 Getting form data
If we send a request using `OPTIONS` method to the api, django will return the expected shape of data it will accept. if we send this to our books api, it will return the following
> Language: javascript
> Path: components/forms/form-data.js
```jsx
const res = await fetch("/api/books/", {
method: "OPTIONS",
headers: {
"Content-Type": "application/json",
},
});
const data = await res.json();
console.log(data);
```
```json
{
"title": {
"required": true,
"type": "string",
"max_length": 100
},
"author": {
"required": true,
"type": "string",
"max_length": 100
},
"description": {
"required": false,
"type": "string",
"max_length": 1000
},
"published": {
"type": "boolean"
},
"sales": {
"required": false,
"type": "number"
},
"created_at": {
"type": "date"
}
}
```
We can use this JSON to create a dynamic form. But first we need transform the JSON to a form array. that we are expecting to see in the form.
### 5 Form array
Here we will do some transformation on the JSON to create a form array.
> Language: javascript
> Path: pages/sample.js
```jsx
const SamplePage = () => {
let url = "/api/books/";
const res = await fetch(url, {
method: "OPTIONS",
headers: {
"Content-Type": "application/json",
},
});
const data = await res.json();
let JSONschema = data.actions.POST;
const { formFields, schema } = buildForm({
JSONschema,
exclude: ["created_at", "created_by"],
});
const formHook = useForm({ resolver: yupResolver(schema) });
const { formstate } = formHook;
console.log(formstate?.errors);
const onSubmit = (data) => {
console.log("submit", data);
// POST data to the api
// and other specific logic
};
return (
<div className="bg-gray-100 px-16 py-8">
<h1 className="mb-8 border-t-2 pt-8">AutoForm</h1>
<AutoForm
fields={formFields}
formHook={formHook}
onSubmit={onSubmit}
className={"grid gap-4 md:grid-cols-2"}
/>
</div>
);
};
export default SamplePage;
```
### 6 Helper functions
lets take a look at `buildForm` function. which does all the heavy lifting.
this function takes in a JSON schema from the django `DRF API` and returns a form array and a schema.
> Language: javascript
> Path: components/forms/build-form.js
```javascript
import { generateYupSchema } from "./yupSchemaGenerator";
export const removeKeys = (schema, excludeFields = []) => {
return Object.keys(schema)
.filter((key) => !schema[key].read_only) // remove read_only fields
.reduce((acc, key) => {
// remove excluded fields
if (!excludeFields.includes(key)) {
// delete the read_only attribute of object, this causes errors in react
delete schema[key].read_only;
acc[key] = schema[key];
}
return acc;
}, {});
};
export const transformInputTypes = (schema) => {
Object.keys(schema).forEach((key) => {
if (
schema[key].type === "integer" ||
schema[key].type === "decimal" ||
schema[key].type === "float" ||
schema[key].type === "number"
) {
schema[key].type = "number";
}
if (
schema[key].type === "file upload" ||
schema[key].type === "image upload"
) {
schema[key].type = "file";
}
if (schema[key].type === "boolean") {
schema[key].type = "checkbox";
}
if (schema[key].type === "nested object") {
schema[key].type = "text";
// deleting the nested object so that it doesn't get rendered in the form
delete schema[key].children;
}
if (schema[key].type === "string") {
schema[key].type = "text";
}
if (schema[key].type === "datetime") {
schema[key].type = "datetime-local";
}
if (schema[key].type === "choice") {
schema[key].type = "select";
schema[key].options = schema[key].choices.map((choice) => ({
value: choice.value,
label: choice.display_name,
}));
}
if (key === "description" || key === "comment") {
schema[key].type = "textarea";
}
});
return schema;
};
export const buildFormShape = (schema) => {
return Object.keys(schema).map((key) => {
return {
name: key,
...schema[key],
};
});
};
export const buildForm = ({ JSONschema, exclude = [] }) => {
let filtered = removeKeys(JSONschema, exclude);
let updatedSchema = transformInputTypes(filtered);
let schema = generateYupSchema(updatedSchema);
let formFields = buildFormShape(updatedSchema);
return { formFields, schema };
};
```
we are using `generateYupSchema` to generate a yup schema from the JSON schema.
> Language: javascript
> Path: components/forms/yupSchemaGenerator.js
```javascript
import * as Yup from "yup";
const getYupType = (schemaObject) => {
switch (schemaObject.type) {
case "text":
return Yup.string();
case "number":
return Yup.number();
case "boolean":
return Yup.boolean();
case "file":
return Yup.mixed();
case "array":
return Yup.array();
case "object":
return Yup.object();
case "date":
return Yup.date();
case "time":
return Yup.time();
case "datetime":
return Yup.datetime();
default:
return Yup.string();
}
};
const checkRequired = (schemaObject, yupObject) => {
if (schemaObject.required) {
return yupObject.required();
}
return yupObject;
};
const addValidation = (schemaObject, yupObject) => {
let obj = yupObject;
if (schemaObject.max_length && schemaObject.type !== "file") {
obj = obj.max(
schemaObject.max_length,
`${schemaObject.title} must be less than ${schemaObject.max_length} characters`
);
}
return obj;
};
export const generateYupSchema = (schema) => {
let shape = {};
Object.keys(schema).forEach((key) => {
let schemaObject = schema[key];
let yupObject = getYupType(schemaObject);
yupObject = checkRequired(schemaObject, yupObject);
yupObject = addValidation(schemaObject, yupObject);
shape = { ...shape, ...{ [key]: yupObject } };
});
let yupSchema = Yup.object().shape(shape);
return yupSchema;
};
```
---
## Conclusion
On the sample page, we have demonstrated how to use the `AutoForm` component.
We have also defined a `url` variable. by changing this `url` we can get form from another API like `/api/authors/`, `/api/users/`, etc. without having to write separate forms for them.
simply change the `url` and we are on our way to create **Dynamic forms** driven by our API.
<!--
let shape = {}
let schema = {
title : {
label:"title",
type: "text",
required: true,
max_length: 100,
},
description : {
label: "Description",
type: "textarea",
required: false,
max_length: 1000,
},
created_at : {
label: "Created at",
type: "date",
required: false,
},
}
//["title","description","created_at",...]
Object.keys(schema).forEach((key) => {
// key = title
// schema[key] = {label:"title", type:"text",...}
let yupObject = getYupType(schemaObject);
// yupObject = Yup.string()
yupObject = checkRequired(schemaObject, yupObject);
// yupObject = Yup.string().required()
yupObject = addValidation(schemaObject, yupObject);
// yupObject = Yup.string().required().max(100,"title can have max of 100 chars")
shape = { ...shape, ...{ [key]: yupObject }}
// shape {title : Yup.string().required().max(100,"title can have max of 100 chars")}
})
let yupSchema = Yup.object().shape(shape);
return yupSchema;
// shape {
// title : Yup.string().required().max(100,"title can have max of 100 chars")
// description : yup.string()
// }
-->