Dynamic layout of React component from JSON - javascript

I am writing a react application that is a thin client UI for a financial trading application. A core requirement is that the application be completely dynamic and configurable, including forms. Specifically, I have trade entry forms that need to be defined on the server side and stored in the database to be rendered dynamically on the client but the layout is important and needs to be able to support multiple different formats. I have seen a few libraries that take JSON form schemas and create static forms from them but none of them seem to support the kind of layout flexibility I need. For example, I need to support tabs, columns, and rows of components. My question is - can anyone suggest a ReactJs library that can do what I'm looking for? If not, how might I go about implementing it myself?
Here's a more concrete example; Suppose I have a schema fetched from the server via a REST call that looks something like this:
{
title: "Securities Finance Trade Entry",
children: [
{
containerType: "tabs",
children: [
{
title: "Common",
children: [
{
containerType: "row",
children: [
{
input: "ComboBox",
label: "Trade Type",
options: ["Repo", "Buy/Sell", "FeeBased"]
},
{
input: "ComboBox",
label: "Direction",
options: ["Loan", "Borrow"]
}
]
},
{
containerType: "row",
children: [
{
containerType: "column",
children: [
{
containerType: "row",
children: [
{
input: "text",
label: "Book"
},
{
input: "text",
label: "Counterparty"
}
]
},
{
containerType: "row",
children: [
{
input: "date",
label: "StartDate"
},
{
input: "date",
label: "EndDate"
}
]
},
{
containerType: "row",
children: [
{
input: "text",
label: "Security"
},
{
input: "numeric",
label: "Quantity"
}
]
}
]
}
]
}
]
}
]
}
]
}
Which I expect to render something like:
Basically in that schema only one tab would be shown, but there could be multiple tabs, each containing multiple children in rows and columns and potentially nested containers of tabs too. If I were to render this myself in react I would think about using .map to iterate through the json and a number of if statements to insert tags where appropriate. However, items need to be nested so I don't know how to render it such that the tag chosen is dynamic and it can have children... For example I could write:
{ if (container.containerType === "column") { () } but then I'd somehow need to embed the rest of the controls inside that tag, I don't think I could just emit a () at the end...
Another option I've considered is translating the above json to JSX on the server side and sending that. It would be fairly easy to write a parser on the Java server side that converts the above json to a JSX document and return that to the client, but then how would I render it? Is there any way I could do something like:
onComponentMount() {
fetch(webserviceUrl + 'getJsxForForm/' + props.formName)
.then(result => {this.setState({form : result});
}
render() {
return ({this.state.form});
}
But again, I don't think this will work. If I fetch the document from the server it would render it just as plain text and not actually convert it to valid html, right?
So, what are my options? I'm looking for suggestions of an existing library that would do this, or suggestions on either of the other two approaches that I'm mentioned (would they work?, how could I do it?), or alternative ideas.
Thanks,
Troy

I love the concept of dynamically rendering pages from some sort of JSON configuration.
The key will be defining Components to match containerTypes and inputs and then traversing your JSON configuration through a recursive function. In your JSON configuration, I suggest using component naming conventions wherever you want a component to be rendered. Hence, capitalizing Tabs, Row, Column, etc
Here is one example of that function. Note, in each of the containerType Components there is a call to this function with children being passed in.
See this pen: https://codepen.io/wesleylhandy/pen/oQaExK/
Example Component:
const Container = props => {
return (
<div className="container">
<h1>{props.title}</h1>
{renderChildren(props.children)}
</div>
)
}
Example Recursive-ish rendering of children
const renderChildren = children => {
return children ? children.map((child, ind) => {
const newChildren = child.children ? [...child.children] : [];
const {containerType, title, input, label, options} = child
let key;
if (newChildren.length) {
key = `${containerType}-${ind}`;
switch (containerType) {
case "Tabs":
return <Tabs
key={key}
title={title}
children={newChildren}
/>
case "Column":
return <Column
key={key}
title={title}
children={newChildren}
/>
case "Row":
return <Row
key={key}
title={title}
children={newChildren}
/>
default:
return <Common
key={key}
title={title}
children={newChildren}
/>
}
} else {
key=`${input}-${ind}`
switch (input) {
case "ComboBox":
return <SelectGroup
key={key}
label={label}
options={options}
/>
case "Text":
case "Date":
case "Numeric":
return <InputGroup
key={key}
label={label}
type={input}
/>
}
}
}) : null
}

Related

Attribute is not saved in Gutenberg

I am trying to create a custom block for Wordpress Gutenberg.
I have the following attributes:
"icon": {
"type": "object",
"source": "html",
"selector": ".jig_defbox-icon"
},
"color": {
"type": "string",
"default": "#919191"
}
In my EditJS I try to store values from a color-picker and a radiogroup into these attributes.
const [ toggled, setToggled ] = useState( );
return(
<InspectorControls>
<ColorPalette
colors={[
{name: 'lightgray', color: '#d8d8d8'},
{name: 'darkgray', color: '#919191'},
{name: 'red', color: '#dc1a22'}
]}
disableCustomColors={true}
onChange={(value) => {props.setAttributes({color: value});}}
value={props.attributes.color}
clearable={false}
/>
<RadioGroup
onChange={ (value) => {setToggled(value);props.setAttributes({icon: value});} }
checked={props.attributes.icon}
>
{
str_array.map(
item => ( <Radio value={item}><Icon icon={icon_getter(item);}/></Radio>)
)
}
</RadioGroup>
</InspectorControls>
)
In my SaveJS I try to render my markup according to these attributes:
const {attributes} = props
<div style={{"fill": attributes.color}}>
<Icon icon={icon_getter(attributes.icon)}/>
</div>
The goal is to render an svg-icon according to the selection on my radiogroup.
Issue 1: Every new edit-session in backend, the selection of my radiogroup is gone, even with useState (tried without useState first)
Issue 2: Every new edit-session, a console error is logged, that says that the post-body markup does not match the markup returned by the save function, because the save-markup does not contain the icon attribute content
As far as I was able to enclose the problem, the icon attribute is not correctly saved. I tried to save the "key" for the icon as a string and object. If I log the value in the save function, it is empty, while the selected color works as expected, both in frontend and backend.
I was able to fix it via rethinking my concept of fetching the icon.
Apparently, Gutenberg tried to store the code of the icon into the database, but not the key. When loading the backend editor, the icon_getter function received a null value, therefore, the difference between post body and save function code.
I changed my editJS:
<RadioGroup
onChange={ (value) => {props.setAttributes({icon: value});} }
checked={props.attributes.icon}
>
{
my_icons_as_list.map(
item => ( <Radio value={item}><Icon icon={my_icons[props.attributes.icon];}/></Radio>)
)
}
</RadioGroup>
and my saveJS:
<Icon icon={ my_icons[ attributes.icon ] } />

Return the full array that include that specific element

I'm trying to create a glossary component in React, that allows me to filter only the glossary terms that I need for a specific unit learning. I do have two different datas array, one for the glossary terms, and one for the units. This is my glossary data file:
{
id: 1,
order: 1,
term: "Mario",
content: "It's a meeee"
},
{
id: 2,
order: 2,
term: "Luigi",
content: "Okie dokie!"
},
{
id: 3,
order: 3,
term: "Peach",
content: "Thank you, Mario!"
},
{
id: 4,
order: 4,
term: "Bowser",
content: "No one asks for a trap faster than a plumber!"
},
{
id: 5,
order: 5,
term: "Ganondorf",
content: "How dare you attempt to wound the Demon King!"
}
This is my unit data file:
const units = [
{
order: 1,
glossaryTermsRequired: [1,2,3]
},
{
order: 2,
glossaryTermsRequired: [4,5]
}
]
Now this is my glossary component:
const Glossary = ({ glossaryId, glossaryTerm }) => {
const [isOpen,setIsOpen]=useState(false)
useEffect(()=>{
insertGlossaryTerm()
})
return (
<>
<span onClick={()=>setIsOpen(true)} className="glossaryTerm">{glossaryTerm}</span>
<Sidebar closeSidebar={()=>setIsOpen(false)} isOpen={isOpen}/>
</>
);
The glossary component take two props, the id and the term string. My aim is to click on the glossary and show those required terms in the sidebar that will open on click. Now for example, if I click on the Term Bowser, how can i get to show all the terms for that required unit?(4,5 unit 2)
THank you guys
const Glossary = ({ glossaryId, glossaryTerm }) => {
const [isOpen,setIsOpen]=useState(false);
const [sidebarTerms, setSidebarTerms] = useState([]);
useEffect(()=>{
if (isOpen)
insertGlossaryTerm()
}, [isOpen])
const insertGlossaryTerm = () => {
let unit = units.find(u => u.glossaryTermsRequired.indexOf(4)>=0);
if (unit) {
let termArray = unit.glossaryTermsRequired.map(id => elements[id].term);
setSidebarTerms(termArray);
}
}
return (
<>
<span onClick={()=>setIsOpen(true)} className="glossaryTerm">{glossaryTerm}</span>
<Sidebar closeSidebar={()=>setIsOpen(false)} isOpen={isOpen} terms={sidebarTerms}/>
</>
);
Explanation:
a state to manage related terms has been created. It's a list of strings
when span is clicked, the sidebar is opened
useEffect is triggered as the isOpen state changes. Searches related terms and stores them in the related state
state is connected with Sidebar so it received via props the related terms, ready to be displayed
To make it work you need to create a global object in my example called elements, that is a key-value JSON object where the key is the id of the element, and the value is the element itself.
Example:
{
'1': {
id: 1,
order: 1,
term: "Mario",
content: "It's a meeee"
},
'2': {
id: 2,
order: 2,
term: "Luigi",
content: "Okie dokie!"
},
...
}
This avoids your application to loop through the whole list of elements to find those with the right id.
Then, you need to have units object visible too.
Improvements:
you can trigger before the insertGlossaryTerm() than the isOpen() function
if a terms can be in more than one unit, then you need to substitute .find function with a .filter

How do i dynamically generate vue components from wordpress custom fields

I am building a website using vue with nuxt, loading data from a wordress site through the rest api.
I would like to give the client the ability to modify page templates using custom fields, so I need to dynamically create my vue templates with vue components that are generated depending on the custom fields placed in the wordpress page editor.
This is simplified, but for example, if the clients builds a page with three custom fields:
[custom-field type='hero']
[custom-field type='slider']
[custom-field type='testimonial']
I can get the field information via the rest api in a json object, like this:
page: {
acf: [
{field 1: {
{type: 'hero'},
{content: '...'}
},
{field 2: {
{type:'slider'},
{content: '...'}
},
{field 3: {
{type:'testimonial'},
{content: '...'}
}
}
}
I'll bring this into my vue app, but then i would the template to generate dynamically from a list of possible components mapped to the custom field types. the above would output:
<template>
<Hero ... />
<Slider ... />
<Testimonial ... />
</template>
Would this be done using the v-is directive (https://v2.vuejs.org/v2/guide/components-dynamic-async.html) like:
<component v-for="field in custom-fields" v-is="field.type" :data="field.data"/>?
Is this possible? Any help would be greatly appreciated.
You can dynamically register and display components in Nuxt but there are some caveats.
Method 1 - SSR & SSG Support
This method will register your components dynamically and maintain the integrity of Server Side Rendering/Static Site Generation.
The only trade-off here is that you will need to list the names and file locations of all potential imported components. This shouldn't be too cumbersome for your use-case (I don't imagine you have too many ACF fields) but it could be a lengthy job if you're intending to build an entire component library from it.
<template>
<div>
<div v-for="field in page.acf" :key="field.uniqueId">
<component :is="field.type" :my-prop="field.content" />
</div>
</div>
</template>
<script>
export default {
components: {
hero: () => import('~/components/hero.vue'),
slider: () => import('~/components/slider.vue'),
testimonial: () => import('~/components/testimonial.vue')
},
data() {
return {
page: {
acf: [
{
uniqueId: 1,
type: 'hero',
content: '...'
},
{
uniqueId: 2,
type: 'slider',
content: '...'
},
{
uniqueId: 3,
type: 'testimonial',
content: '...'
}
]
}
}
}
}
</script>
Method 2 - Client Side Only Rendering
This method allows you to programmatically register the component's name and file location. This saves you from writing out each component one by one, but comes at the cost of no SSR or SSG support. However, this may be preferable if you're going the SPA route.
<template>
<div>
<div v-for="field in page.acf" :key="field.uniqueId">
<no-ssr>
<component :is="field.type" :my-prop="field.content" />
</no-ssr>
</div>
</div>
</template>
<script>
import Vue from 'vue'
export default {
data() {
return {
page: {
acf: [
{
uniqueId: 1,
type: 'hero',
content: '...'
},
{
uniqueId: 2,
type: 'slider',
content: '...'
},
{
uniqueId: 3,
type: 'testimonial',
content: '...'
}
]
}
}
},
mounted() {
const sections = this.page.acf
for (let i = 0; i < sections.length; i++) {
Vue.component(sections[i].type, () =>
import(`~/components/${sections[i].type}.vue`)
)
}
}
}
</script>
Be aware that the <no-ssr> tag is being deprecated and if you are using Nuxt above v2.9.0, you should use <client-only> instead.
Notes On Your Question
I understand you've tried to simplify your data architecture but your JSON object would be rather difficult to loop through since the key changes on each object in array. You've also got unnecessary objects in the data structure. I've simplified the structure in the data method so you can understand the concept.
To get the v-for loop working, you need to nest the component inside a HTML tag that has a v-for attribute (as demonstrated above).
You may want to sanitise the data you get from the WordPress API by ensuring that WordPress doesn't supply you with a module that doesn't correspond to a component. If the API supplies a type that doesn't correspond to a component, the whole build will fail.
Hope this helps!
P.S If anyone knows of a method that allows you to programatically set the component name and component file location while maintaining SSG - please let me know!

How to create a nested menu in JavaScript?

So I want to achieve the image below but with content sent from server.
We can set menu items
const items = [
{ key: 'editorials', active: true, name: 'Editorials' },
{ key: 'review', name: 'Reviews' },
{ key: 'events', name: 'Upcoming Events' },
]
//...
<Menu vertical color='black' items={items}>
</Menu>
However, I do not see how to nest them. Just setting item 'content' to some XML.
How do I create a menu with multiple nested sections in ReactJS\Semantic-UI in Javacript?
I would create the following components:
<MenuContainer /> -> our root component
<Menu></Menu> -> can render either itself (<Menu />) or an <Item /> component
<Item></Item> -> can render only a <li /> or smth
And suppose we have the following JSON coming from our server:
{
label: 'Some menu',
children: [{ label: 'Sub menu', children: [...] }, ...],
}
Let assume that when we find an array in our JSON, that means that we have to render a menu. If we have an object, we render a simple child. A rough algorithm would be:
const MenuContainer = ({items}) => ({
{items.map(item => item.items ? <Menu items={items} /> : <Item data={item} /> }
});
Is this something you are looking for?

How to design a generic filter like ecommerce website have using ReactJs?

i am planning to build a generic filter like Gbif Have.
My question is how to approach this problem.
I like to use ReactJs for this project.
What other technology i need to look into along with React and redux in order to design such a generic filter.
I try to design this filter using React and redux only.
In my approach, i try to maintain the query parameter inside the state variable of the get_data method, in which i am fetching the data from the server. As somebody click on any filter button, then i pass custom event from that filter component along with query parameter and handle this event in get_data method. In get_data method again i am saving this value in get_data state parameter and again getting the new filtered data.
Now the Problem with above approach is that as the number of parameter increases it become very difficult to maintain.
my get_data constructor look like this.
constructor(props){
super(props);
this.state={
params:{
max:10,
offset:0,
taxon:[],
sGroup:[],
classification:undefined,
userGroupList:[],
isFlagged:undefined,
speciesName:undefined,
isMediaFilter:undefined,
sort:"lastRevised",
webaddress:""
},
title:[],
groupName:[],
userGroupName:[],
view:1
}
this.props.fetchObservations(this.state.params)
this.loadMore=this.loadMore.bind(this);
};
The way i am getting data from filter component is something like this.
this is my handleInput method which fire onSelect method from one of the filter.
handleInput(value,groupName){
this.setState({
active:true
})
this.props.ClearObservationPage();
var event = new CustomEvent("sGroup-filter",{ "detail":{
sGroup:value,
groupName:groupName
}
});
document.dispatchEvent(event);
};
the way i am handling this event in my get_data component is look something like this.
sGroupFilterEventListner(e){
const params=this.state.params;
if(!params.sGroup){
params.sGroup=[];
}
console.log("params.sGroup",params.taxon)
params.sGroup.push(e.detail.sGroup)
params.sGroup=_.uniqBy(params.sGroup)
const groupName=this.state.groupName;
var titleobject={};
titleobject.sGroup=e.detail.sGroup;
titleobject.groupName=e.detail.groupName;
groupName.push(titleobject);
let newgroupname=_.uniqBy(groupName,"sGroup")
params.classification=params.classification;
let isFlagged=params.isFlagged;
let speciesName=params.speciesName;
let MediaFilter=params.isMediaFilter;
let taxonparams=params.taxon;
taxonparams= taxonparams.join(",");
let sGroupParams=params.sGroup;
sGroupParams=sGroupParams.join(",");
let userGroupParams=params.userGroupList;
userGroupParams=userGroupParams.join(",");
let newparams={
max:10,
sGroup:sGroupParams,
classification:params.classification,
offset:0,
taxon:taxonparams,
userGroupList:userGroupParams,
isFlagged:isFlagged,
speciesName:speciesName,
isMediaFilter:MediaFilter,
sort:params.sort
}
this.props.fetchObservations(newparams);
this.setState({
params:{
max:10,
sGroup:params.sGroup,
classification:params.classification,
offset:0,
taxon:params.taxon,
userGroupList:params.userGroupList,
isFlagged:isFlagged,
speciesName:speciesName,
isMediaFilter:MediaFilter,
sort:params.sort
},
groupName:newgroupname
})
}
I registered and unRegistered the sGroupFilterEventListner in my componentDidMount and componentunmount method.
Presently i am also not considering the case where if somebody type in url bar, the filter panel change automatically.
Please consider all the above scenario and suggest me a generic way to do the same. thanks.
My Current Filter Panle look like this
Here's a quick example (React only, no Redux) I whipped up with a dynamic number of filters (defined in the filters array, but naturally you can acquire that from wherever).
const filters = [
{ id: "name", title: "Name", type: "string" },
{
id: "color",
title: "Color",
type: "choice",
choices: ["blue", "orange"],
},
{
id: "height",
title: "Height",
type: "choice",
choices: ["tiny", "small", "big", "huge"],
},
{
id: "width",
title: "Width",
type: "choice",
choices: ["tiny", "small", "big", "huge"],
},
];
const filterComponents = {
string: ({ filter, onChange, value }) => (
<input
value={value || ""}
onInput={e => onChange(filter.id, e.target.value)}
/>
),
choice: ({ filter, onChange, value }) => (
<select
value={value || ""}
onInput={e => onChange(filter.id, e.target.value)}
size={1 + filter.choices.length}
>
<option value="">(none)</option>
{filter.choices.map(c => (
<option value={c} key={c}>
{c}
</option>
))}
</select>
),
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = { filters: {} };
this.onChangeFilter = this.onChangeFilter.bind(this);
}
onChangeFilter(filterId, value) {
const newFilterState = Object.assign({}, this.state.filters, {
[filterId]: value || undefined,
});
this.setState({ filters: newFilterState });
}
renderFilter(f) {
const Component = filterComponents[f.type];
return (
<div key={f.id}>
<b>{f.title}</b>
<Component
filter={f}
value={this.state.filters[f.id]}
onChange={this.onChangeFilter}
/>
</div>
);
}
render() {
return (
<table>
<tbody>
<tr>
<td>{filters.map(f => this.renderFilter(f))}</td>
<td>Filters: {JSON.stringify(this.state.filters)}</td>
</tr>
</tbody>
</table>
);
}
}
ReactDOM.render(<App />, document.querySelector("main"));
body {
font: 12pt sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main/>
(originally on https://codepen.io/akx/pen/JyemQQ?editors=0010)
Hope this helps you along.

Categories

Resources