I want to programmatically build a dynamic data object based on a context object, like in the example below.
const context = {
...other props...,
groups: [
{
heading: 'basic',
canHaveMultiple: false, // data.basic = {username: { type: 'text', value: '' }, password: { type: 'password', value: ''}}
inputs: [
{
name: 'username',
type: 'text',
placeholder: 'username',
},
{
name: 'password',
type: 'password',
placeholder: 'password',
},
],
},
{
heading: 'about',
canHaveMultiple: false, // data.about = {about: { type: 'textarea', value: '' }}
inputs: [
{
name: 'about',
type: 'textarea',
placeholder: 'about',
canHaveMultiple: false,
},
],
},
{
heading: 'hobbies',
canHaveMultiple: true, // data.hobbies = { model: { title: {type: 'text', value: ''}, description: {type: 'textarea', value: ''} }, values: [ { title: {type: 'text', value: ''}, description: {type: 'textarea', value: ''} }]
inputs: [
{
name: 'title',
type: 'text',
placeholder: 'about',
canHaveMultiple: false,
},
{
name: 'description',
type: 'description',
placeholder: null,
canHaveMultiple: false,
},
],
},
{
heading: 'friends',
canHaveMultiple: true, // data.friends = { model: {title: {type: 'text', value: '' }, description: { type: 'textarea', value: '' }} }, values: [{ name: {type: 'text', value: ''},hobbies: [{ title: {type: 'text', value: ''}, description: {type: 'textarea', value: ''}} }] }
inputs: [
{
name: 'name',
type: 'text',
placeholder: 'this is fine',
canHaveMultiple: false
},
{
name: 'hobbies',
type: 'nested',
canHaveMultiple: true,
inputs: [
{
name: 'title',
type: 'textarea',
placeholder: 'about',
canHaveMultiple: false,
},
{
name: 'description',
type: 'textarea',
placeholder: 'about',
canHaveMultiple: false,
},
]
}
],
},
],
}
The output data should be something like so:
data: {
basic: {
username: {
type: 'text',
value: '',
},
password: {
type: 'password',
value: ''
}
},
about: {
about: {
type: 'textarea',
value: '',
}
},
hobbies: {
model: {
title: {
type: 'text',
value: '',
},
description: {
type: 'textarea',
value: '',
}
},
values: [
{
title: {
type: 'text',
value: '',
},
description: {
type: 'textarea',
value: '',
}
}
]
},
friends: {
model: {
name: {
type: 'text',
value: '',
},
hobbies: {
title: {
type: 'text',
value: '',
},
description: {
type: 'textarea',
value: '',
}
}
},
values: [
]
},
}
In essence,
- groups[int].heading becomes the top level property of the data object, and
- each group along with each of the child from inputs has a canHaveMultiple property, which distinguishes whether the resulting object structure will be either:
canHaveMultiple == false
group.input.name: {
group.type,
value: ''
}
OR
canHaveMultiple == true
{
model: {
group.input.name: {
type: group.input.type,
value: ''
},
... etc
},
values: [{
group.input[0].name: {
type: group.input[0].type,
value: '',
},
group.input[1].name: {
type: group.input[1].type,
value: '',
}
}]
}
The model is there so that I can easily push a new copy of that object into the values array.
So here is my question:
Is there a way to recursively do this so that the program will create the data object from the context object, and keep looking down the context object chain for any 'nested' type (also within the inputs array) until there is none left? Is this do-able and efficient or am I thinking and going about this the wrong way?
*PS: I have been trying real hard and wrecking my head for a few days now on this but I cannot seem to get it working for Nested Objects beyond 3 levels because I am a newbie in recursion. Please help me :(
CodePudding user response:
This function first group by heading (using reduce) then goes recursively over the inputs
fields. That is if an input has inputs
we loop that too.
const context={groups:[{heading:"basic",canHaveMultiple:!1,inputs:[{name:"username",type:"text",placeholder:"username"},{name:"password",type:"password",placeholder:"password"},]},{heading:"about",canHaveMultiple:!1,inputs:[{name:"about",type:"textarea",placeholder:"about",canHaveMultiple:!1},]},{heading:"hobbies",canHaveMultiple:!0,inputs:[{name:"title",type:"text",placeholder:"about",canHaveMultiple:!1},{name:"description",type:"textarea",placeholder:null,canHaveMultiple:!1},]},{heading:"friends",canHaveMultiple:!0,inputs:[{name:"name",type:"text",placeholder:"this is fine",canHaveMultiple:!1},{name:"hobbies",type:"nested",canHaveMultiple:!0,inputs:[{name:"title",type:"textarea",placeholder:"about",canHaveMultiple:!1},{name:"description",type:"textarea",placeholder:"about",canHaveMultiple:!1},]}]},]}
function transform(arr) {
var result = arr.reduce(function(agg, item) {
var heading = item.heading
var canHaveMultiple = item.canHaveMultiple
var parent;
agg[heading] = {}
if (canHaveMultiple === false) {
parent = agg[heading]
}
if (canHaveMultiple === true) {
agg[heading] = {
model: {},
values: []
}
parent = agg[heading]['model']
}
function do_inputs(parent, inputs) {
inputs.forEach(function(input) {
if (!input.inputs) {
parent[input.name] = {
type: input.type,
value: ''
// todo: placeholder and other properties
}
} else {
// nested
parent[input.name] = {}
do_inputs(parent[input.name], input.inputs)
}
})
}
do_inputs(parent, item.inputs)
return agg;
}, {})
return result;
}
console.log(transform(context.groups));
.as-console-wrapper {
max-height: 100% !important;
}
CodePudding user response:
I have two snippets which might get you most of the way there.
The first one is perhaps too simple, ignoring your model/value
part. But it should be easy to understand:
const convert = (xs) => Object .fromEntries (
xs .map (({name, type, inputs = []}) =>
[name, inputs .length ? convert (inputs) : {type, value: ''}]
)
)
const restructure = (context) => ({
data: Object .fromEntries (context .groups .map (
({heading, inputs}) => [heading, convert (inputs)]
))
})
const context = {other: "props", groups: [{heading: "basic", canHaveMultiple: !1, inputs: [{name: "username", type: "text", placeholder: "username"}, {name: "password", type: "password", placeholder: "password"}]}, {heading: "about", canHaveMultiple: !1, inputs: [{name: "about", type: "textarea", placeholder: "about", canHaveMultiple: !1}]}, {heading: "hobbies", canHaveMultiple: !0, inputs: [{name: "title", type: "text", placeholder: "about", canHaveMultiple: !1}, {name: "description", type: "description", placeholder: null, canHaveMultiple: !1}]}, {heading: "friends", canHaveMultiple: !0, inputs: [{name: "name", type: "text", placeholder: "this is fine", canHaveMultiple: !1}, {name: "hobbies", type: "nested", canHaveMultiple: !0, inputs: [{name: "title", type: "textarea", placeholder: "about", canHaveMultiple: !1}, {name: "description", type: "textarea", placeholder: "about", canHaveMultiple: !1}]}]}]}
console .log (restructure (context))
.as-console-wrapper {max-height: 100% !important; top: 0}
The second one does add the model
/value
part at the expense of some additional complexity. And it's not clear to me if it's entirely correct, although I think it's close:
const convert = (xs) => Object .fromEntries (
xs .map (({name, type, inputs = [], canHaveMultiple}) => [
name,
canHaveMultiple
? {model: convert (inputs), values: [convert (inputs)]}
: inputs.length ? convert (inputs) : {type, value: ''}
])
)
const restructure = (context) => ({
data: Object .fromEntries (context .groups .map (
({heading, inputs, canHaveMultiple}) => [
heading,
canHaveMultiple
? {model: convert (inputs), values: [convert (inputs)]}
: convert (inputs)
]
))
})
const context = {other: "props", groups: [{heading: "basic", canHaveMultiple: !1, inputs: [{name: "username", type: "text", placeholder: "username"}, {name: "password", type: "password", placeholder: "password"}]}, {heading: "about", canHaveMultiple: !1, inputs: [{name: "about", type: "textarea", placeholder: "about", canHaveMultiple: !1}]}, {heading: "hobbies", canHaveMultiple: !0, inputs: [{name: "title", type: "text", placeholder: "about", canHaveMultiple: !1}, {name: "description", type: "description", placeholder: null, canHaveMultiple: !1}]}, {heading: "friends", canHaveMultiple: !0, inputs: [{name: "name", type: "text", placeholder: "this is fine", canHaveMultiple: !1}, {name: "hobbies", type: "nested", canHaveMultiple: !0, inputs: [{name: "title", type: "textarea", placeholder: "about", canHaveMultiple: !1}, {name: "description", type: "textarea", placeholder: "about", canHaveMultiple: !1}]}]}]}
console .log (restructure (context))
.as-console-wrapper {max-height: 100% !important; top: 0}
In both of them, we could simplify a lot if you had a consistent interface. That is, if groups
was called inputs
and heading
was called name
, we could consolidate the two repetitive functions into a single one.