{
"type": "Container",
"data": {
"id": "4400936b-6158-4943-9dc8-a04c57e1af46",
"items": [
{
"type": "Card",
"data": {
"id": "26b3f355-2f65-4aae-b9fd-609779f24fdd",
"title": "A card example",
"subtitle": "A subtitle",
"items": [
{
"type": "Button",
"data": {
"id": "4400936b-6158-4943-9dc8-a04c57e1af46",
"title": "Button text",
"className": "btn-primary",
"action": {
"type": "call",
"url": "https://pokeapi.co/api/v2/"
},
}
},
]
}
},
{
"type": "Divider",
"data": {
"id": "4400936b-6158-4943-9dc8-a04c57e1af46",
"marginX": 3,
}
},
{
"type": "Card",
"data": {
"id": "4400936b-6158-4943-9dc8-a04c57e1af46",
"title" : "Title",
"headline" : "Month ## - Month ##, ####",
"copy": "A really long text....",
"image" : {
"url" : "https://i.stack.imgur.com/y9DpT.jpg"
},
}
},
{
"type": "Container",
"data": {
"id": "d76e3a5f-01ad-46f6-a45d-3ad9699ecf99",
"fluid": true,
"embeddedView": {
"type": "Input",
"data": {
"id": "26b3f355-2f65-4aae-b9fd-609779f24fdd",
"label": "Input",
"type": "password",
"placeholder": "Password",
"isRequired": false,
"minCharactersAllowed": 1,
"maxCharactersAllowed": 100,
"validations": [
{
"regexType": "eightOrMoreCharacters",
"regexErrorCopy": "Use 8 or more characters"
},
]
}
}
}
}
]
}
}
// dynamic-rendering.interfaces.ts
// Here is a type to map all the components names.
type ComponentList =
| 'Button'
| 'Card'
| 'Container'
| 'Divider'
| 'Input';
export interface IComponent {
type: ComponentList;
data: {
id: string;
embeddedView?: IComponent;
items?: Array<IComponent>;
[key: string]: unknown;
};
}
// dynamic-rendering.constants.ts
// All the component imports
export const Components = {
Button,
Card,
Container,
Divider,
Input,
};
// dynamic-rendering.service.ts
import React from 'react';
import { IComponent } from './dynamic-rendering.interfaces';
import { Components } from './dynamic-rendering.constants';
export function createPage(data?: IComponent): React.ReactNode {
// Don't render anything if the payload is falsey.
if (!data) return null;
function createComponent(item: IComponent): React.ReactNode {
const { data, type } = item;
const { items, embeddedView, id, ...rest } = data;
return React.createElement(
// TODO: This can be improved
Components[type] as any,
{
// Pass all the props coming from the data object.
...rest,
id,
// Make each react key unique
key: id,
} as any,
// Map if there are items, if not try to render the embedded view as children
Array.isArray(items)
? items.map(renderer)
: renderer(embeddedView ?? null),
);
}
// Don't render anything if the payload is falsey.
function renderer(
config: IComponent | null,
): React.ReactNode {
if (!config) return null;
return createComponent(config);
}
return renderer(data);
}
// components/DynamicComponentLoader.js
import React, { Suspense } from 'react';
// Define a map (registry) to map component names to their corresponding dynamic imports
const componentRegistry = {
Input: () => import('your-ui-library/Input'),
Button: () => import('your-ui-library/Button'),
Grid: () => import('your-ui-library/Grid'),
// Add more components as needed
};
// Dynamic component loader that handles 'element' type differently
const DynamicComponentLoader = ({ type, componentName, componentProps, content }) => {
// If the type is "element", render it as a raw HTML element
if (type === 'element') {
const Element = componentName;
return <Element {...componentProps}>{content}</Element>;
}
// Look up the component in the registry
const componentLoader = componentRegistry[componentName];
if (!componentLoader) {
console.error(`Component "${componentName}" is not registered.`);
return null;
}
// Lazily load the component
const LazyComp = React.lazy(componentLoader);
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComp {...componentProps}>{content}</LazyComp>
</Suspense>
);
};
export default DynamicComponentLoader;
// App.js
import React from 'react';
import DynamicComponentLoader from './components/DynamicComponentLoader';
const jsonData = [
{
"id": "mobilePhone",
"type": "field",
"component": "Input",
"componentProps": {
"path": "cellNumber",
"readOnly": "true",
"label": {
"id": "gw-pe-components-platform-react.PhoneDetails.Mobile Phone",
"defaultMessage": "meta.Mobile Phone"
},
"contentContainerClassName": "mobilePhoneInputContainer"
}
},
{
"id": "button",
"component": "Button",
"type": "action",
"componentProps": {
"size": "large",
"icon": "save",
"type": "secondary",
"iconPosition": "right"
},
"content": "Button"
},
{
"id": "codelessForm",
"title": "Codeless form",
"subtitle": "A form generated from json metadata",
"type": "page",
"dataSource": "ConstructionDetails",
"componentProps": {},
"componentLayout": {
"component": "Grid",
"componentProps": {
"gap": "xlarge"
}
},
"content": [
{
"id": "test15161788",
"type": "action",
"component": "Button",
"content": "Click Me",
"componentProps": {
"size": "large",
"icon": "twitter"
}
},
{
"id": "heading1",
"type": "element",
"component": "h1",
"componentProps": {
"className": "heading-class"
},
"content": "Dynamic Heading"
}
]
}
];
function App() {
return (
<div className="App">
{jsonData.map(item => (
<DynamicComponentLoader
key={item.id}
type={item.type} // Pass the type field from the JSON
componentName={item.component}
componentProps={item.componentProps}
content={item.content}
/>
))}
</div>
);
}
export default App;
[
{
"id": "mobilePhone",
"type": "field",
"component": "Input",
"componentProps": {
"path": "cellNumber",
"readOnly": "true",
"label": {
"id": "gw-pe-components-platform-react.PhoneDetails.Mobile Phone",
"defaultMessage": "meta.Mobile Phone"
},
"contentContainerClassName": "mobilePhoneInputContainer"
}
},
{
"id": "button",
"component": "Button",
"type": "action",
"componentProps": {
"size": "large",
"icon": "save",
"type": "secondary",
"iconPosition": "right"
},
"content": "Button"
},
{
"id": "codelessForm",
"title": "Codeless form",
"subtitle": "A form generated from json metadata",
"type": "page",
"dataSource": "ConstructionDetails",
"componentProps": {},
"componentLayout": {
"component": "Grid",
"componentProps": {
"gap": "xlarge"
}
},
"content": [
{
"id": "test15161788",
"type": "action",
"component": "Button",
"content": "Click Me",
"componentProps": {
"size": "large",
"icon": "twitter"
}
},
{
"id": "heading1",
"type": "element",
"component": "h1",
"componentProps": {
"className": "heading-class"
},
"content": "Dynamic Heading"
}
]
}
]
Idea: The application structure which means the render layout to be completely controlled by server. Every action taken by the user such as button click or input change should push an event to the server and rerenders the component on the response. For that the JSON will have "actionEvents" array which will have the event type such as onclick, onchange, and the name of event to push to server. For this the element will be rendered in a wrapper which will pass these onClick, onChange handlers which will push event to server.
import React, { Suspense, lazy } from 'react';
// Predefined component mapping with dynamic imports
const componentRegistry = {
Button: lazy(() => import('./components/Button')),
};
// Helper to resolve components from registry or fallback to HTML tag
const resolveComponent = (type) =>
componentRegistry[type] ||
((props) => React.createElement(type, props, props.children || null));
// Event dispatcher function
const dispatchEvent = async (eventName, payload) => {
try {
await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: eventName, data: payload }),
});
} catch (error) {
console.error('Failed to dispatch event:', error);
}
};
// Sanitize event arguments to remove circular references
const sanitizeEventArgs = (args) => {
if (!args || args.length === 0) return null;
const seen = new WeakSet();
const sanitize = (obj) => {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return '[Circular]'; // Prevent circular references
seen.add(obj);
if (obj instanceof Event) {
// Extract relevant event properties
return {
type: obj.type,
target: obj.target && {
tagName: obj.target.tagName,
id: obj.target.id,
className: obj.target.className,
value: obj.target.value,
},
currentTarget: obj.currentTarget && {
tagName: obj.currentTarget.tagName,
id: obj.currentTarget.id,
className: obj.currentTarget.className,
},
timeStamp: obj.timeStamp,
};
}
if (Array.isArray(obj)) {
return obj.map(sanitize); // Recursively sanitize array elements
}
// Handle general objects
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, sanitize(value)])
);
};
return args.map(sanitize);
};
// Bind events with event dispatcher
const bindEventHandlers = (props) => {
const boundProps = { ...props };
Object.keys(props).forEach((key) => {
if (key.startsWith('on') && typeof props[key] === 'string') {
const eventName = props[key];
boundProps[key] = (...args) => {
const sanitizedArgs = sanitizeEventArgs(args);
dispatchEvent(eventName, sanitizedArgs);
};
}
});
return boundProps;
};
// Render components based on JSON layout
const renderComponent = (componentConfig) => {
const { type, props, children } = componentConfig;
const Component = resolveComponent(type);
const boundProps = bindEventHandlers(props);
return (
<Suspense fallback={<div>Loading {type}...</div>}>
<Component {...boundProps}>
{children &&
children.map((childConfig, index) => (
<React.Fragment key={index}>
{renderComponent(childConfig)}
</React.Fragment>
))}
</Component>
</Suspense>
);
};
// Example JSON layout
const jsonLayout = {
type: 'div',
props: {
title: 'Event-Driven UI',
content: 'This card uses event-driven development.',
},
children: [
{
type: 'Button',
props: {
label: 'Click Me',
onClick: 'buttonClicked', // Event name
},
},
{
type: 'div',
props: { style: { color: 'blue' }, children: 'This is a fallback div.' },
},
],
};
export function App() {
const [layout, setLayout] = React.useState(jsonLayout);
// Fetch JSON from API
React.useEffect(() => {
const fetchLayout = async () => {
const response = await fetch('/api/layout');
const data = await response.json();
setLayout(data);
};
fetchLayout();
}, []);
return <div className="App">{renderComponent(layout)}</div>;
}