# 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() // } -->