- Blog ➔
- Programação ↴
Criando um formulário dinâmico a partir de um JSON schema
Escrito em 21 de maio de 2021 - 🕒 5 min. de leituraHá cerca de 2 meses, o Gatsby v3 foi lançado, e eu estava super ansioso para atualizar este blog para v3 e começar a usar os incremental builds, mas “infelizmente” este blog tem muita customização, então fazer a atualização para v3 não vai ser fácil 😢.
Porém, o meu projeto open source, Resume Builder, não tinha muitas customizações, então foi tranquilo atualizá-lo para o Gatsby v3 com o comando ncu -u
, usando o package npm-check-updates. Então, depois de atualizá-lo para v3, eu meio que me empolguei e decidi implementar alguns novos recursos do meu backlog desde 2018, como o editor de cover letter.
O que é o Resume Builder?
O Resume Builder é um projeto open source gratuito que permite a qualquer pessoa manter e construir facilmente qualquer tipo de currículo usando planilhas ou um JSON como a fonte de dados. Graças a este projeto eu fui contratado para morar no exterior, como já expliquei em outro post desse blog.
Uma das maiores falhas desse projeto é que você já deve ter o arquivo com os dados para o seu currículo, não havia como criar seu próprio currículo do zero.
Formik
Alguns dias atrás, eu estava assistindo uma palestra do Jared Palmer e me convenci totalmente de que deveria testar o Formik, e eu tenho a feature perfeita para isso.
JSON schema
Eu precisava que o formulário fosse dinâmico, para que os usuários pudessem adicionar quantos dados quisessem, e já estou usando o JSON schema do projeto json-resume, por que não usá-lo para criar o formulário programaticamente para mim?
Antes de começar a programar, eu decidi usar meu conhecimento de UML para fazer um diagrama de como isso vai funcionar:
Para o propósito desse post, vamos usar uma versão mais simples do JSON schema do projeto json-resume.
{
"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"
}
}
}
}
}
}
Percorrendo o JSON
Existem 3 tipos de dados diferentes no JSON schema que estou usando, object
, array
e string
, então ele vai ser bem fácil de percorrer.
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);
Isso deve ser suficiente para a maioria dos casos em que não há keys repetidas no JSON e nem a necessidade do usuário inserir dados ilimitados.
Usando keys únicas
Agora as coisas começam a ficar um pouco gambiarradas, para garantir que cada campo tenha uma key única, vou acumular as keys de todos os nodes, por exemplo para o objeto {foo: {bar: ''}}
, a key do node bar
seria foo-bar
. Não estou muito orgulhoso disso, mas sempre posso refatorá-lo mais tarde.
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>
);
}
}
});
};
Adicionando mais campos dinamicamente
Para adicionar campos dinamicamente, decidi criar uma variável local que controlaria quantas vezes cada input deveria ser exibido baseado na key única de cada input.
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>
);
}
}
});
Depois de colocar esse código em um componente React, eu posso usar-lo assim:
<DynamicForm
schema={jsonSchema}
formik={formik}
/>
O resultado desse código meio confuso fica assim:
Você pode ver o código real do formulário no GitHub, e o bom é que da próxima vez que o schema do json-resume mudar, eu não preciso fazer nada yay.
Acesse o site do Resume Builder e veja como o formulário ficou.
Tags:
Posts relacionados
Publicar um comentário
Comentários
Nenhum comentário.