Dynamic React child components not rendering in UI based on JSON Config - reactjs

I am trying to render React Components based on a JSON response from a CMS system, but I can't seem to render the child item components. Can anyone tell me what I am missing/not understanding here?
Here is my code...
// ComponentRenderer.tsx
export const ComponentsHandler: { [key: string]: any } = {
test: Test,
};
export default function ComponentRenderer(config: IBlock): JSX.Element {
if (typeof ComponentsHandler[config.component] !== 'undefined') {
return React.createElement(
ComponentsHandler[config.component],
{
key: config.id,
...config,
},
config.content &&
(typeof config.content === 'string'
? config.content
: config.content.map((c: IBlock) => ComponentRenderer(c)))
);
}
return React.createElement(() => (
<div>
Error: Unable to load <strong>{config.component}</strong> component.
</div>
));
}
Here is the JSON object I am trying to render:
blocks: [
{
id: '1',
component: BlockType.Test,
content: [],
},
{
id: '2',
component: BlockType.Test,
content: [
{
id: '3',
component: BlockType.Test,
content: [],
},
{
id: '4',
component: BlockType.Test,
content: 'this is a string',
},
],
}
]
export interface IBlock {
id: string;
component: BlockType;
content: IBlock[] | string;
}
export enum BlockType {
Test = 'test',
Hero = 'hero',
Video = 'video',
Menu = 'menu',
Navigation = 'navigation',
Testimonial = 'testimonial',
Card = 'card',
CarTile = 'carTile',
HorizontalScrollSection = 'horizontalScrollSection',
Div = 'div',
Section = 'section',
}
Here is the "Test" component I am looking to render in the JSON response
// TestComponent.tsx
export default function Test(props: IBlock) {
return props.content && <p>{props.id}</p>;
}
// UI HomePage
export default function HomePage() {
return (
mockData &&
mockData.blocks.map((block: IBlock) => (
<ComponentRenderer {...block} key={block.id}></ComponentRenderer>
))
);
}
I would expect the browser to render
1
2
3
4
But instead, I only get the top-level JSON items.
1
2

You created a really cool react recursive structure. A few days ago I tried to do something similar but gave up... I will use yours as an inspiration.
Check if this changes can help you:
IBlock:
export interface IBlock {
id: string;
component: BlockType;
content: IBlock[] | string;
children?: React.ReactNode;
}
TestComponent.tsx
export default function Test(props: IBlock) {
return (
props.content && (
<p>
{props.id} {props.children}
</p>
)
);
}

Related

how to use map in react typescript

Currently using a react in typescript.
make a separate file and manage it.
I want to do map work on the `card component
parameter on the map wrong?
definition wrong?
Can I get some advice...
Card.tsx
import { LIST_DATA, ListData } from './data';
const Card = () => {
return(
{LIST_DATA.map(({ id, title }: ListData[]) => {
return <S.InfoTitle key={id}>{title}</S.InfoTitle>;
})}
)
}
data.tsx
export interface ListData {
id: number;
title: string;
}
export const LIST_DATA: ListData[] = [
{
id: 0,
title: 'drawing',
},
{
id: 1,
title: 'total',
},
{
id: 2,
title: 'method',
},
{
id: 3,
title: 'material',
},
];
When you map, each item inside the map as argument is only one item from the original array. In your case you have an array of ListData but when you map each argument is only one of this ListData item type, therefore you have to change ListData[] to ListData as such:
import { LIST_DATA, ListData } from './data';
const Card = () => {
return(
{LIST_DATA.map(({ id, title }: ListData) => {
return <S.InfoTitle key={id}>{title}</S.InfoTitle>;
})}
)
}
Otherwise to answer the main question: map in typescript is the same as map in javascript, you just have to ensure you input the right type in order to "secure" your app.

How to refactor a prop in React with Typescript?

I am trying to refactor a component in my App.tsx file to it's own file (InvoiceList.ts).
I have moved the component to another file and imported it within App.tsx, but I am getting an error of Cannot find name 'InvoiceDataType'
When I bring the component back into the App.tsx, file, I get no errors. Why am I getting that error and how can I fix it.
Note: This is my first time using Typescript with React and so I am still learning how to fuse the two together.
// App.tsx
import { InvoiceList } from "./components/InvoiceList";
const App = () => {
const data: InvoiceDataType = {
customerName: "Google",
invoices: [
{ id: 123, name: "Dev work", total: "20.00", paymentStatus: "paid" },
{
id: 456,
name: "More Dev work",
total: "50.00",
paymentStatus: "pending",
},
{
id: 789,
name: "Something different",
total: "100.00",
paymentStatus: "paid",
},
],
};
return (
<div>
<InvoiceList invoiceData={data} />
</div>
);
};
export default App;
// InvoiceList.tsx
interface IInvoiceListProps {
invoiceData: InvoiceDataType;
logo?: string;
}
type InvoiceDataType = {
customerName: string;
invoices: InvoiceType[];
};
type PaymentStatusType = "paid" | "pending" | "late";
type InvoiceType = {
id: number;
name: string;
total: string;
paymentStatus: PaymentStatusType;
};
export const InvoiceList = (props: IInvoiceListProps) => {
const { customerName, invoices } = props.invoiceData;
const itemStyleContainer = {
display: "flex",
justifyContent: "space-between",
};
return (
<div>
<h1>{customerName}</h1>
<hr />
<div>
{invoices.map((invoice) => (
<div key={invoice.id} style={itemStyleContainer}>
<div>{invoice.name}</div>
<div>
{invoice.total} - {invoice.paymentStatus}
</div>
</div>
))}
</div>
</div>
);
};
It appears the problem is occuring because InvoiceDataType is no longer defined inside of App.tsx after your refactoring.
To make it available for use inside of App.tsx, you should export it from InvoiceList.ts and then import it inside of App.tsx for use.
For example:
// App.tsx
import { InvoiceList, InvoiceDataType } from "./components/InvoiceList";
...
// InvoicList.tsx
...
export type InvoiceDataType = {
customerName: string;
invoices: InvoiceType[];
};
...

How does the method menuDataRender() get its input for menuList?

Looking at the following snips from the Ant Design Pro work work, how does the method menuDataRender get its parameters? The reason I ask this, is because I want to modify the signature, and given the current calling method, there does not appear to be any parameters being passed.
The method:
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
menuList.map(item => {
const localItem = {
...item,
children: item.children ? menuDataRender(item.children) : [],
};
return Authorized.check(item.authority, localItem, null) as MenuDataItem;
});
The caller:
//
// ... code removed for brevity ...
//
return (
<>
<ProLayout
logo={logo}
menuHeaderRender={(logoDom, titleDom) => (
<Link to="/">
{logoDom}
{titleDom}
</Link>
)}
onCollapse={handleMenuCollapse}
menuItemRender={(menuItemProps, defaultDom) => {
if (menuItemProps.isUrl || menuItemProps.children) {
return defaultDom;
}
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
breadcrumbRender={(routers = []) => [
{
path: '/',
breadcrumbName: formatMessage({
id: 'menu.home',
defaultMessage: 'Home',
}),
},
...routers,
]}
itemRender={(route, params, routes, paths) => {
const first = routes.indexOf(route) === 0;
return first ? (
<Link to={paths.join('/')}>{route.breadcrumbName}</Link>
) : (
<span>{route.breadcrumbName}</span>
);
}}
footerRender={footerRender}
menuDataRender={menuDataRender} // <--- called here!
formatMessage={formatMessage}
rightContentRender={rightProps => <RightContent {...rightProps} />}
{...props}
{...settings}
>
<Authorized authority={authorized!.authority} noMatch={noMatch}>
{children}
</Authorized>
</ProLayout>
<SettingDrawer
settings={settings}
onSettingChange={config =>
dispatch({
type: 'settings/changeSetting',
payload: config,
})
}
/>
</>
);
};
The ProLayout is the BasicLayout component that comes from the #ant-design/pro-layout package and this component can receive a route prop, which defaults to (GitHub):
route = {
routes: [],
}
The value of the routes key (an empty array by default) is used to call the menuDataRender function (GitHub):
const { routes = [] } = route;
if (menuDataRender) {
renderMenuInfoData = getMenuData(
routes,
menu,
formatMessage,
menuDataRender,
);
}
The expected schema for the elements in the routes array is an array of Route type (GitHub):
export interface MenuDataItem {
authority?: string[] | string;
children?: MenuDataItem[];
hideChildrenInMenu?: boolean;
hideInMenu?: boolean;
icon?: string;
locale?: string;
name?: string;
key?: string;
path?: string;
[key: string]: any;
parentKeys?: string[];
}
export interface Route extends MenuDataItem {
routes?: Route[];
}
Example:
// routes.ts
import { MenuDataItem } from "#ant-design/pro-layout/lib/typings";
const routes : MenuDataItem[] = [
{
path: "/",
name: "Home",
authority: []
},
{
path: "/users",
name: "Users",
authority: ["admin"]
}
];
export default routes;
// app.ts
import React from "react";
import ProLayout, {
MenuDataItem,
BasicLayoutProps as ProLayoutProps,
} from "#ant-design/pro-layout";
import routeList from "./routes";
const Application: React.FC<ProLayoutProps> = (props) => {
const { children } = props;
// or get your route list from where you have it (ex. from a store ...)
// const routeList = useStoreState(state => state.app.routeList);
const route = { routes: routeList };
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
menuList.map(item => {
const localItem = {
...item,
children: item.children ? menuDataRender(item.children) : [],
};
return Authorized.check(item.authority, localItem, null) as MenuDataItem;
});
return (
<ProLayout
// more props
route={route}
menuDataRender={menuDataRender}
>
{ children }
</ProLayout>
);
}

Map an object within an object - Reactjs

Hi! I'm trying to render a dynamic settings items.
const settingsItemData = [
{
settingsCategory: 'Exam',
settingsCategoryItems: [
{
image: LabExamRequestIcon,
link: '/settings/exam-request',
label: 'Exam Items',
col: '4',
// offset: 4
},
{
image: LabExamRequestIcon,
link: '/settings/lab-exam-request',
label: 'Exam Request',
col: '4',
}
]
},
{
settingsCategory: 'Others',
settingsCategoryItems: [
{
image: LabExamRequestIcon,
link: '../settings/panel-exam',
label: 'Panel Exam',
col: '4'
},
{
image: UserMaintenanceIcon,
link: '/settings/user-maintenance',
label: 'User Maintenance',
col: '4'
}
]
}
]
What I want to do is display the title which is Exam on the left side. And display to the right side everything inside the settingsCategoryItems. I've mapped out the settingsCategory. But for the settingsCategoryItems, I can't seem to get render anything.
I've tried this:
const Items = settingsItemData.map((item) => (
<SettingsCard
image={item.settingsCategoryItems.image}
link={item.settingsCategoryItems.link}
label={item.settingsCategoryItems.label}
/>
))
But none of it renders. How do you usually do this in reactjs? Thank you.
Try this. you gotta also perform second map via settingsCategoryItems to make it work
const Items = settingsItemData.map((item) => (
item.settingsCategoryItems.map(el => (
<SettingsCard
image={el.image}
link={el.link}
label={el.label}
/>
))
))
you're in the right track, but you also need to map the settingsCategoryItems array in order to access the values and paint the UI that you want to show (As you can see in the following code snippet):
import React, { Component } from 'react';
import { render } from 'react-dom';
import './style.css';
class Component1 extends Component {
state = {};
render() {
const settingsItemData = [
{
settingsCategory: 'Exam',
settingsCategoryItems: [
{
image: 'LabExamRequestIcon',
link: '/settings/exam-request',
label: 'Exam Items',
col: '4',
// offset: 4
},
{
image: 'LabExamRequestIcon',
link: '/settings/lab-exam-request',
label: 'Exam Request',
col: '4',
}
]
},
{
settingsCategory: 'Others',
settingsCategoryItems: [
{
image: 'LabExamRequestIcon',
link: '../settings/panel-exam',
label: 'Panel Exam',
col: '4'
},
{
image: 'UserMaintenanceIcon',
link: '/settings/user-maintenance',
label: 'User Maintenance',
col: '4'
}
]
}
];
const rowItems = settingsItemData.map((item) => {
return <div className={`row__${item.settingsCategory}`}>
<h1>{item.settingsCategory}</h1>
{item.settingsCategoryItems.map(item2 => {
return <button>{item2.label}</button>;
})}
</div>;
});
return (
<div className="container">
<h1>Settings</h1>
{rowItems}
</div>
);
}
}
This way you could render all the data that you want (Then you could refactor this in order to use the SettingsCard component).
item.item.settingsCategoryItems is an array, not an object, so as a minimum you need to iterate the settingsCategoryItems as well.
And your arrow functions looks a bit weird, it should be
(item) => { ... }
and not
(item) => (....)
This
const Items = settingsItemData.map((item) => {
<SettingsCard
image={item.settingsCategoryItems.image}
link={item.settingsCategoryItems.link}
label={item.settingsCategoryItems.label}
/>
})
could/should become some thing like
const Items = settingsItemData.map((outerItem) => {
outerItem.map((item) => {
<SettingsCard
image={item.image}
link={item.link}
label={item.label}
/>
})
})
I can't test it, but hope you get the general idea..

ToggleCollapsed on imported sidebar reactjs component

Using a sidebar component from [office-ui-fabric-react][1].
I want to sidebar to start in the Collapsed state, but I am having difficulty setting the state of the ISidebar interface to do this.
How can I toggleCollapsed on this imported component?
export interface ISidebar {
/**
* Toggles the sidebar state to put the sidebar in or out of collapsed mode
* #type {(boolean) => void}
*/
toggleCollapsed: () => void;
}
export class SidebarExample extends React.Component {
public render(): JSX.Element {
this.state = {
active: true
};
return (
<Sidebar
id={'sidebar-collapsed'}
collapsible={true}
theme={getTheme()}
/>
)
Public methods (e.g. toggleCollapsed) of Sidebar component are accessible through componentRef, for example:
<Sidebar componentRef={initSidebar} />
const initSidebar = (sideBar: ISidebar) => {
sideBar.toggleCollapsed();
};
Example
The initial collapsed state could be set like this:
const initSidebar = (ref: ISidebar) => {
ref.setCollapsed(true);
};
const SidebarBasicExample: React.SFC<{}> = props => {
return (
<Sidebar
componentRef={initSidebar}
collapsible={true}
id={"1"}
theme={getTheme()}
items={[
{
key: "basic-example-item1",
name: "Item 1",
iconProps: { iconName: "BuildQueue" },
active: false
},
{
key: "basic-example-item2",
name: "Item 2",
iconProps: { iconName: "Bullseye" },
active: true
}
]}
/>
);
};
With Vadim's help, my answer.
import { getTheme } from 'office-ui-fabric-react';
import * as React from 'react';
import { Sidebar, ISidebar } from '#uifabric/experiments/lib/Sidebar';
const initSidebar = (sideBar: ISidebar) => {
sideBar.toggleCollapsed();
};
export class SidebarCollapsibleExample extends React.Component {
public render(): JSX.Element {
this.state = {
active: true
};
return (
<Sidebar
id={'sidebar-collapsed'}
collapsible={true}
theme={getTheme()}
collapseButtonAriaLabel={'sitemap'}
componentRef={initSidebar}
items={[
{
key: 'collapsible-example-item1',
name: 'Item 1',
iconProps: { iconName: 'BuildQueue' },
active: false
} ]}
/>
);
}
}

Resources