Building a dynamic form from a JSON schema
Written in May 21, 2021 - 🕒 5 min. readAround 2 months ago, Gatsby v3 was released, and I was already super hyped to upgrade this blog to v3 and start using incremental builds, but “sadly” this blog has way too many customizations, so upgrading it to Gatsby v3 is not an easy feat.
Luckily, my open source project, Resume Builder, didn’t have many customizations, so I was able to upgrade it to Gatsby v3 by simply running ncu -u
, a command by npm-check-updates. But then after upgrading it to Gatsby v3 I kind of got carried away and decided to implement some new features, like the cover letter editor that was on my backlog since 2018.
What is Resume Builder?
Resume Builder is a free open-source project that allows anyone to easily maintain and build any kind of resume using a spreadsheet or a JSON file as a data source. Thanks to this project I was hired to live abroad, as I already explained in another blog post.
One of the biggest flaws of this project was that you must already have a data source for your resume, there was no way to create your own resume from scratch.
Formik
A couple of days ago I was watching a talk by Jared Palmer and I was convinced I should try Formik, and I have the perfect feature for it.
JSON schema
I needed the form to be dynamic, so users could add as much work experience as they wanted it to, and since I’m already using the JSON schema from the json-resume project, why not use it to build the form programmatically for me?
Before coding I decided to use my UML knowledge to make a diagram of how this is going to work:
For this blog post let’s use a simpler version of the JSON schema from the json-resume project.
{
"work": {
"type": "array",
"additionalItems": false,
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "e.g. Facebook"
},
"location": {
"type": "string",
"description": "e.g. Menlo Park, CA"
},
"description": {
"type": "string",
"description": "e.g. Social Media Company"
},
"position": {
"type": "string",
"description": "e.g. Software Engineer"
},
"url": {
"type": "string",
"description": "e.g. http://facebook.example.com",
"format": "uri"
},
"startDate": {
"$ref": "#/definitions/iso8601"
},
"endDate": {
"$ref": "#/definitions/iso8601"
},
"summary": {
"type": "string",
"description": "Give an overview of your responsibilities at the company"
},
"highlights": {
"type": "array",
"description": "Specify multiple accomplishments",
"additionalItems": false,
"items": {
"type": "string",
"description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
}
}
}
}
}
}
Traversing the JSON
There are 3 different data types in the JSON schema that I’m using, object
, array
, and string
, so it should be fairly simple to traverse.
const formik = useFormik({});
const getForm = (jsonSchema) => {
Object.entries(jsonSchema).map(([key, value], index) => {
switch (value.type) {
case 'object': {
return (
<div>
<h1>{key}</h1>
{getForm(value.properties)}
</div>
);
}
case 'array': {
return (
<div>
{getForm({
[key]: value.items,
})}
</div>
);
}
case 'string':
default: {
return (
<div>
<TextField
key={key}
fullWidth
id={key}
name={key}
label={key}
value={formik.values[key]}
onChange={formik.handleChange}
/>
</div>
);
}
}
});
};
const form = getForm(jsonSchema);
That should be enough for most cases when there are not repeating keys in the JSON neither the need for the user to input unlimited data.
Using unique keys
Now things start to get a bit hacky, to make sure every field has a unique key, I will accumulate the keys in the node, for example for the object { foo: { bar: '' } }
, the key for the bar
node would be foo-bar
. I’m not super proud of this, but I can always refactor it later.
const getForm = (jsonSchema, accKey = '') => {
Object.entries(jsonSchema).map(([key, value], index) => {
const newAccKey = `${accKey}-${key}`;
switch (value.type) {
case 'object': {
return (
<div>
<h1>{key}</h1>
{getForm(value.properties, newAccKey)}
</div>
);
}
case 'array': {
return (
<div>
{getForm({
[key]: value.items,
}, newAccKey)}
</div>
);
}
case 'string':
default: {
return (
<div>
<TextField
key={newAccKey}
fullWidth
id={newAccKey}
name={key}
label={key}
value={formik.values[newAccKey]}
onChange={formik.handleChange}
/>
</div>
);
}
}
});
};
Adding more fields dynamically
To add fields dynamically I decided to create a local variable that would hold how many times an input should be displayed based on the input unique key.
const [quantitiesObject, setQuantitiesObject] = useState({});
const getForm = (jsonSchema, accKey = '', quantity = 1) =>
Object.entries(jsonSchema).map(([key, value], index) => {
let newAccKey = key;
if (accKey) {
newAccKey = `${accKey}-${key}`;
}
switch (value.type) {
case 'object': {
return (
<div key={key}>
<h1>{key}</h1>
{(new Array(quantity).fill(null).map(
(v, i) => (
<div key={i}>
{getForm(value.properties, `${newAccKey}-${i}`)}
</div>
)
))}
</div>
);
}
case 'array': {
const currQuantity = quantitiesObject[newAccKey] || 1;
return (
<div key={key}>
{(new Array(quantity).fill(null).map((v, i) => (
<div key={i}>
{getForm({
[key]: value.items,
}, `${newAccKey}-${i}`, currQuantity)}
</div>
)))}
<div>
<Button
onClick={() => {
setQuantitiesObject({
...quantitiesObject,
[newAccKey]: currQuantity + 1,
});
}}
color="primary"
variant="contained"
>
{`+ ${key}`}
</Button>
{currQuantity > 1 && (
<Button
onClick={() => {
setQuantitiesObject({
...quantitiesObject,
[newAccKey]: currQuantity - 1,
});
}}
color="secondary"
variant="contained"
>
{`- ${key}`}
</Button>
)}
</div>
</div>
);
}
case 'string':
default: {
return (
<div key={key}>
{(new Array(quantity).fill(null).map(
(v, i) => {
const newKey = `${newAccKey}-${i}`;
return (
<TextField
key={newKey}
fullWidth
id={newKey}
name={newKey}
label={key}
value={formik.values[newKey]}
onChange={formik.handleChange}
/>
);
}
))}
</div>
);
}
}
});
After wrapping this code into a React component, I can use it like this:
<DynamicForm
schema={jsonSchema}
formik={formik}
/>
The result of this messy code looks like the following:
You can check the actual code for the form on GitHub, and the nice thing is that next time the json-resume schema changes, I don’t have to do anything.
Try the form yourself on the Resume Builder website.
Tags:
Related posts
Post a comment
Comments
No comments yet.