DEV Community

Cover image for Dynamic Form using Schema-Based UI
Avas77
Avas77

Posted on

Dynamic Form using Schema-Based UI

If your application needs to work with many forms and data collection flows, then writing forms in JSX(React) can be very repetitive. Even though we might try to extract the standard components, the core problem lies in displaying the right set of inputs according to the type of form.

For Example, let's say you are building a car insurance registration form where the form needs to be dynamic according to the response of the user. In the first form, the user needs to enter his basic details and details about his automobile, and based on this info, different other forms need to be rendered to get the best insurance plan. Such as there is an input field for Is this vehicle owned, leased or financed? , and if the user answers saying the car is owned by the user then in the next form questions need to be asked about Is this vehicle registered to the insured or a spouse? . If the user responds as the car is leased then in the next form. questions about the leasing company could be asked. And, similarly, another input field may occur if the user answers as financed.

Now, one way to do this would be by conditionally rendering the above-mentioned fields according to the response of the user and for this simple case, it may be reasonable as well. But, let us imagine that the form grows in size and number and different fields need to be rendered according to the different responses of the user. We would have to keep track of all of the responses and render fields accordingly, the code becomes convoluted and unmanageable as it further grows. Not to mention adding validation rules, post-validation, and sending the complete data to the server according to the correct fields becomes super complex. So, how can we be able to tackle this simply? The answer is Schema Based UI. Let's have a look at it next.

First Step: Creating a Next js project

Let's start by creating a simple Next.js project using either npm or yarn. In this tutorial, I will be using npm. Run this command in your terminal.

npx create-next-app@latest

After the installation is complete, run npm run dev to start the development server.

Create a simple API route in Next.js:

After the creation of our project, the first step we need to do is to create a simple Next js API. For that, we can create a folder inside the pages directory of the Next.js project called API and in there create a file called input.js.

Folder Structure

Inside the file, let's start by creating a simple array, that we will pass as JSON schema upon the response of the API



schema = [
  { id: 1, inputType: "TEXT", label: "What is your Name?" },
  { id: 2, inputType: "NUMBER", label: "What is your Age?" },
  { id: 3, inputType: "CHECKBOX", label: "Are you a driver?" },
];


Enter fullscreen mode Exit fullscreen mode

In this Array, we have three objects and each object contains properties like id, inputType, and label. Now, let's create an API route by passing the above array as a JSON response with a status code of 200.



export default function handler(req, res) {
  res.status(200).json(schema);
}


Enter fullscreen mode Exit fullscreen mode

Now, that we have created our API endpoint we simply have to invoke the API on our pages.

Creating the Form Builder Component

Now, we move to the index.js file and remove all the preexisting code there and write a component from scratch.



import React from "react";

const Index = () => {
  return <div>Index</div>;
};

export default Index;



Enter fullscreen mode Exit fullscreen mode

The first thing we have to do is invoke the API that we created using fetch API call and store the response in a state variable called Fields.



import React, { useState } from "react";

const index = () => {
  const [fields, setFields] = useState();

  useEffect(() => {
    fetch("/api/input")
      .then((res) => res.json())
      .then((data) => {
        setFields(data);
      });
  }, []);

  return (
      <div>Hello World</div>
  );
};

export default index;


Enter fullscreen mode Exit fullscreen mode

Now, our next objective is to create a mapper component to map the input type values present on the fields state to the respective inputs. For this project, I will be adding the library, Mantine UI for the inputs. You can install Mantine by installing the dependencies.

npm install @mantine/core @mantine/hooks @mantine/next @emotion/server @emotion/react

Okay so now we will create a new file called FormBuilder which takes the fields state as props and maps the element of the fields onto a new Component Called Dynamic Framework which will be the actual component responsible for showing the input UI fields.



import React from "react";
import DynamicFramework from "./DynamicFramework";

const FormBuilder = ({ fields }) => {
    return fields?.map((field) => (
      <DynamicFramework fields={field} key={field.id} />
    ));
};

export default FormBuilder;


Enter fullscreen mode Exit fullscreen mode

Before we move on to create the Dynamic Framework component, we will need to have an Object mapper with its keys being the name of the inputs and the value containing the actual components. So we will create an Object Mapper named FieldFactory.



import CheckboxInput from "./CheckboxInput";
import FloatingInput from "./NumberInput";
import SelectInput from "./SelectInput";
import Input from "./TextInput";

export const FieldFactory = {
  TEXT: Input,
  NUMBER: FloatingInput,
  CHECKBOX: CheckboxInput,
  SELECT: SelectInput,
};


Enter fullscreen mode Exit fullscreen mode

The input component files simply contain Mantine UI imports and return the Mantine components:



import React from "react";
import { Box, Text, TextInput } from "@mantine/core";

const Input = () => {
  return (
    <Box>
      <Text>Label</Text>
      <TextInput />
    </Box>
  );
};

export default Input;


Enter fullscreen mode Exit fullscreen mode

Now, that our Object Mapper is done, we are ready to create the Dynamic Framework component. So, let's a create file called DynamicFramework.js. Inside the file, we will create a variable called component which will hold the mapped component. We will map the input type from the field to the Field Factory Object that we just created to get the desired input.



import React from "react";
import { FieldFactory } from "./FieldFactory";

const DynamicFramework = ({ field }) => {
  const component = FieldFactory[field?.inputType] || Input;
  return (
    <div>Dynamic Framework</div>
  );
};

export default DynamicFramework;


Enter fullscreen mode Exit fullscreen mode

Now, that we have got the input that we want to render in the Ui, we will need to pass some extra properties that may be present on the field value like labels, options, etc, to the input components. We will pass these properties as props so we will use React's createElement.



import { Box, Input } from "@mantine/core";
import React, { createElement } from "react";
import { FieldFactory } from "./FieldFactory";

const DynamicFramework = ({ field }) => {
  const component = FieldFactory[field?.inputType] || Input;
  return (
    <Box w={500} m="auto" my={50}>
      {createElement(component, {
        meta: {
          label: field?.label,
          options: field?.options,
        },
      })}
    </Box>
  );
};

export default DynamicFramework;


Enter fullscreen mode Exit fullscreen mode

React's createElement() will create an element of the type whatever input value is mapped and pass the field's properties like labels and options as props called meta. With this, we will be able to get the values in our input components and use them accordingly.



import { Box, Text, TextInput } from "@mantine/core";
import React from "react";

const Input = ({ meta }) => {
  return (
    <Box>
      <Text>{meta?.label}</Text>
      <TextInput />
    </Box>
  );
};

export default Input;


Enter fullscreen mode Exit fullscreen mode

Now, that all of this is done our FormBuilder component is ready and we can simply render it in the UI and see the output on the screen.



import React, { useEffect, useState } from "react";
import { Box } from "@mantine/core";
import FormBuilder from "./FormBuilder";

const index = () => {
  const [fields, setFields] = useState();

  useEffect(() => {
    fetch("/api/input")
      .then((res) => res.json())
      .then((data) => {
        setFields(data);
      });
  }, []);

  return (
    <Box>
        <FormBuilder fields={fields} />
    </Box>
  );
};

export default index;


Enter fullscreen mode Exit fullscreen mode

Output:

Dynamic Form UI

So, now what's great about this output is that everything that you see is dynamic. If we want to add another input then we can simply add another field-type object in the schema in the API input.js file like this and it will be rendered automatically on the screen.



const schema = [
  { id: 1, inputType: "TEXT", label: "What is your Name?" },
  { id: 2, inputType: "NUMBER", label: "What is your Age?" },
  { id: 3, inputType: "CHECKBOX", label: "Are you a driver?" },
  { id: 4, inputType: "TEXT", label: "At what age did you get your license?" },
];



Enter fullscreen mode Exit fullscreen mode

Output:

Dynamic Form UI with newly added field

We can also add other inputs like TextArea, FileInput, etc, simply by adding the input components in FieldFactory and mapping through the API response just as did earlier. With this approach, the code looks super clean and manageable. This is fairly a simple example but the possibilities of this approach can be endless as we can add validation regex to the API schema and use that schema to validate the input fields for frontend validation.

Top comments (10)

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
alwaz profile image
Alwaz Qazi

If we create custom input components how can we handle change in it? I am using this approach with Shadcn but this is only working when I map Shadcn Input components handle change is not working for custom components.

Collapse
 
jwp profile image
John Peters

A logical architecture in getting to low code solutions. This pattern is the future. Great job.

Collapse
 
avas77 profile image
Avas77

Thanks for the comment

Collapse
 
codeofrelevancy profile image
Code of Relevancy

Great article. Thanks for sharing..

Collapse
 
avas77 profile image
Avas77

Thanks for the read

Collapse
 
alwaz profile image
Alwaz Qazi

This is really helpful. Thank you for writing!!

Collapse
 
naucode profile image
Al - Naucode

Great article, you got my follow, keep writing!

Collapse
 
avas77 profile image
Avas77

Thanks for the supportπŸ€—

Collapse
 
idman profile image
Idan Damari • Edited

This is polymorphic input rather than dynamic UI.
The intro would suggest a visibility dependency questions between the driver and license.