I have a react navigation component that is rendered based on some JSON data.
I have sucessfully rendered the top level items, but I'm having trouble rendering the second level sub pages.
This is how I think the code should look I also only want the tag to be output can someone help me with the syntax to achive this?
{headerData.TopLevelPages.map(toplevelPage => (
<li key={toplevelPage.Id}>{toplevelPage.NavLinkText}</li>
<ul>//Only out put UL If SubNavMenuItems as items
{toplevelPage.SubNavMenuItems.map(sublevelPage => (
<li key={sublevelPage.Id}>{sublevelPage.NavLinkText}</li>
))}
<ul>
))}
A simple recursive function can render a nested menu with any depth.
Try like below.
const headerData = { TopLevelPages: [ { NavLinkText: "Text 1", Id: "1", SubNavMenuItems: [ { NavLinkText: "sub Text 1-1", Id: "1-1" }, { NavLinkText: "sub Text 1-2", Id: "1-2", SubNavMenuItems: [ { NavLinkText: "sub-sub Text 1-2-1", Id: "1-2-1" } ] } ] }, { NavLinkText: "Text 2", Id: "2" } ] };
function App() {
const renderNavMenu = (menus) => {
return menus.map(({ NavLinkText, Id, SubNavMenuItems }) => (
<ul>
{/* render current menu item */}
<li key={Id}>{NavLinkText}</li>
{/* render the sub menu items */}
{SubNavMenuItems && <ul>{renderNavMenu(SubNavMenuItems)}</ul>}
</ul>
));
};
return renderNavMenu(headerData.TopLevelPages);
}
ReactDOM.render(<App/>, document.getElementById("root"));
<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>
<div id="root"></div>
{headerData.TopLevelPages.map(toplevelPage => (
<li key={toplevelPage.Id}>{toplevelPage.NavLinkText}</li>
{ toplevelPage.SubNavMenuItems &&
( <ul>//Only out put UL If SubNavMenuItems as items
{toplevelPage.SubNavMenuItems.map(sublevelPage => (
<li key={sublevelPage.Id}>{sublevelPage.NavLinkText}</li>
))}
<ul>)
}
))}
do a conditional rendering once more
Related
I'm using React Hook Form to build a basic page builder application and it's been brilliant so far, I've been using the useFieldArray hook to create lists that contain items, however, I haven't found a way to move items between lists.
I know I can use the move() function to reorder items within the same list, however, since each list has its own nested useFieldArray I can't move the item from one list component to another list component.
If anyone knows of a way around this it would be much appreciated!
Here is a very simplified example of my current setup:
export const App = () => {
const methods = useForm({
defaultValues: {
lists: [
{
list_id: 1,
items: [
{
item_id: 1,
name: 'Apple'
},
{
item_id: 2,
name: 'Orange'
}
]
},
{
list_id: 2,
items: [
{
item_id: 3,
name: 'Banana'
},
{
item_id: 4,
name: 'Lemon'
}
]
}
]
}
});
return (
<FormProvider {...methods}>
<Page/>
</FormProvider>
)
}
export const Page = () => {
const { control } = useFormContext();
const { fields } = useFieldArray({
control,
name: 'lists'
})
return (
<ul>
{fields?.map((field, index) => (
<List listIdx={index} />
))}
</ul>
)
}
export const List = ({ listIdx }) => {
const { control, watch } = useFormContext();
const { fields, move } = useFieldArray({
control,
name: `lists[${sectionIdx}].items`
})
const handleMove = (prevIdx, nextIdx) => {
// this allows me to move within lists but not between them
move(prevIdx, nextIdx);
}
return (
<li>
<p>ID: {watch(lists[${listIdx}].list_id)}</p>
<ul>
{fields?.map((field, index) => (
<Item listIdx={index} itemIdx={index} handleMove={handleMove}/>
))}
</ul>
</li>
)
}
export const Item = ({ listIdx, itemIdx, handleMove }) => {
const { control, register } = useFormContext();
return (
<li>
<p>ID: {watch(lists[${listIdx}].items[${itemIdx}].item_id)}</p>
<label
Name:
<input { ...register('lists[${listIdx}].items[${itemIdx}]) }/>
/>
<button onClick={() => handleMove(itemIdx, itemIdx - 1)}>Up</button>
<button onClick={() => handleMove(itemIdx, itemIdx + 1)}>Down</button>
</div>
)
}
Thanks in advance!
If you'd not like to alter your default values (your data structure), I think the best way to handle this is using update method returning from useFieldArray. You have the data of both inputs that are going to be moved around, knowing their list index and item index, you could easily update their current positions with each other's data.
When the user clicks on a list item, I want it to render the paragraph content for that list item. When the user clicks on another list item, I then want to erase the previous content and register then new paragraph data for the new list item. Currently, when I click the list items, the info data stays for all and never goes away. Does this make sense? If you go to this website https://brittanychiang.com/ and click the "experience" button in the nav, you can see the functionality I am going for.
Questions :
import SingleQuestion from "./SingleQuestion";
import classes from "./Question.module.css";
const questions = [
{
id: 1,
title: "Step 1",
info: "paragraph 1 text",
},
{
id: 2,
title: "Step 2",
info: "paragraph 2 text",
},
{
id: 3,
title: "Step 3",
info: "paragraph 3 text",
},
{
id: 4,
title: "Step 4",
info: "paragraph 4 text",
},
];
const Questions = () => {
return (
<main>
<h1 className="infoTitle">We have answers</h1>
<div className={classes.container}>
{questions.map((question) => {
return <SingleQuestion key={question.id} {...question} />;
})}
</div>
</main>
);
};
export default Questions;
SingleQuestion:
import React, { useState } from "react";
import classes from "./Question.module.css";
const SingleQuestion = ({ title, info }) => {
const [text, setText] = useState("");
const showTextHandler = () => {
setText(info);
};
return (
<section className={classes.elementBin}>
<ul>
<li className={classes.hi} onClick={showTextHandler}>
{title}
</li>
</ul>
<p>{text}</p>
</section>
);
};
export default SingleQuestion;
There are plenty of ways of solving this, I will just show an example that does not differ so much from your current code:
const Questions = () => {
const [selectedId, setSelectedId] = useState();
return (
<main>
<h1 className="infoTitle">We have answers</h1>
<div className={classes.container}>
{questions.map((question) => {
return (
<SingleQuestion
key={question.id}
isSelected={question.id === selectedId}
setSelectedId={setSelectedId}
{...question}
/>
);
})}
</div>
</main>
);
};
const SingleQuestion = ({ id, title, info, isSelected, setSelectedId }) => {
return (
<section className={classes.elementBin}>
<ul>
<li className={classes.hi} onClick={() => setSelectedId(id)}>{title}</li>
</ul>
{isSelected && <p>{info}</p>}
</section>
);
};
As you can see, an upper state was created, holding the selected question id.
The setter for that state is passed to the SingleQuestion component.
Now that component does not hold an internal state anymore, it just update the selected question id, and shows its info just if is the selected one.
I use JSON and useStaticQuery to pass data about links on my website. For example—
simBarItems.json
[
{
"content": "Provia",
"url": "/foto/provia",
"id": 1
},
{
"content": "Velvia",
"url": "/foto/velvia",
"id": 2
},
{
"content": "Astia",
"url": "/foto/astia",
"id": 3
},
{
"content": "Classic Chrome",
"url": "/foto/classic-chrome",
"id": 4
},
{
"content": "Acros",
"url": "/foto/acros",
"id": 5
},
{
"content": "PRO Neg.",
"url": "/foto/pro-neg",
"id": 6
}
]
SimBar.jsx
import React from "react"
import { Link, useStaticQuery, graphql } from "gatsby"
import { container } from "./SimBar.module.scss"
function isActive({ isCurrent }) {
return isCurrent ? { className: "active" } : {}
}
function isPartiallyActive({ isPartiallyCurrent }) {
return isPartiallyCurrent ? { className: "active" } : {}
}
export default function SimBar() {
const data = useStaticQuery(graphql`
query {
allSimBarItemsJson {
nodes {
id
content
url
}
}
}
`)
let items = data.allSimBarItemsJson.nodes
return (
<nav className={container}>
<ul>
<li>
<Link to="/foto" getProps={isActive}>
Alle foto
</Link>
</li>
{items.map(item => (
<li key={item.id}>
<Link to={item.url} getProps={isPartiallyActive}>
{item.content}
</Link>
</li>
))}
</ul>
</nav>
)
}
As you can see, most items should have the same stateProp, so they can go in the same map. However, the first item should have a different stateProp.
My question is: How can I pass the stateProp through JSON so that I can include the item with the isActive link in the map? Is it possible?
Thanks!
If you want to modify your JSON data, you have a few options (at least):
Modify directly the JSON to add the isActive attribute only in the first element
Tweak your data directly in the component
Before the loop:
let items = data.allSimBarItemsJson.nodes
items[0].isActive=true
So now your first item has the attribute active so when you loop you can access to it:
{items.map(item => {
console.log("has isActive attribute: ", item.isActive)
return <li key={item.id}>
<Link to={item.url} getProps={isPartiallyActive}>
{item.content}
</Link>
</li>
})}
Using the index to determine which one is the first, hence isActive, on the fly:
{items.map((item, index) => {
console.log("has active attribute: ", index===0)
return <li key={item.id}>
<Link to={item.url} getProps={isPartiallyActive}>
{item.content}
</Link>
</li>
})}
As you can see in the docs, the map loop as a second argument takes the index so you can access to the first position at any time (index===0).
Each implementation will be more optimal depending on what you want to achieve with the isActive (i.e: if it needs to change between items or not).
Im trying to make a navigation bar for a website and it's giving me the "Warning: Each child in a list should have a unique "key" prop." inside my props.dropList.map
I have two files:
NavigationItems.js -> where I render my navigation bar
const NavigationItems = () => {
const projectDropdown = [
{ id: 0, value: "architecture" },
{ id: 1, value: "land" },
{ id: 2, value: "design" },
{ id: 3, value: "list" },
];
const officeDropdown = [
{ id: 4, value: "contact" },
{ id: 5, value: "team" },
];
return (
<div>
<ul className={styles.NavigationItems}>
<NavigationItem
link={`/projects`}
name="projects"
dropList={projectDropdown}
/>
<NavigationItem link={`/news`} name="news" exact />
<NavigationItem
link={`/office`}
name="office"
dropList={officeDropdown}
/>
</ul>
</div>
);
};
export default NavigationItems;
NavigationItem.js -> where I use the map function
const NavigationItem = (props) => {
let i = 0;
return (
<li className={styles.NavigationItem}>
<NavLink to={props.link} activeClassName={styles.active}>
{props.name}
</NavLink>
{props.dropList && (
<div className={styles.DropdownItems}>
<ul className={styles.DropdownItem}>
{props.dropList.map((drop) => {
console.log("i " + i);
console.log("id " + drop.id);
console.log("value " + drop.value);
i++;
return (
<li key={drop.id}>
<NavLink
exact
to={`${props.link}/${drop.value}`}
activeClassName={styles.active}
>
{drop.value}
</NavLink>
</li>
);
})}
</ul>
</div>
)}
</li>
);
};
export default NavigationItem;
So what happens is that the code loops twice duplicating the key values. It should be looping only once. I don't know why it loops twice, I'm only mapping my values once. For reference
this is what my console shows when I click my links
So your problem doesn't occure in either of the components you provided, but in your "Land" component. (Check the render method of Land)
I am returning data of categories and sub categories the data will come through looking like so:
[
{
"Cid": 1284805663,
"Name": "PARENT CATEGORY",
"ParentCid": 0,
"PicUrl": "",
"SortOrder": 1,
"Type": "manual_type"
},
{
"Cid": 1284805664,
"Name": "CHILD CATEGORY",
"ParentCid": 1284805663,
"PicUrl": "",
"SortOrder": 1,
"Type": "manual_type"
}
]
In the above example the only way I know that these two are connected is because the childs ParentCid matches the parents Cid.
What I am currently doing is mapping out all of the data but instead I would want to add a class to the sub categories and I am not exactly sure how to go about differentiating the two.
Here is my code below that is mapping all of the items.
import React from 'react';
import {Link} from 'react-router';
import {animateScroll as scroll} from 'react-scroll';
class SellerStoreCategories extends React.Component {
componentDidUpdate() {
// scroll.scrollTo(document.documentElement.scrollTop, {
scroll.scrollTo(400, {
duration: 0,
delay: 0,
smooth: false
})
}
render() {
return (
<div className="store__body__menu__accordian">
<ul className="list-unstyled">
{this.props.sellerCats.map((cat) => (
<li>
<Link
to={`/seller/${this.props.storeName}/${cat.Cid}`}
className={`black-link ${this.props.activeCategory == cat.Cid ? 'active' : ''}`}
>
{cat.name_en}
</Link>
</li>)}
</ul>
</div>
);
}
}
export default SellerStoreCategories;
Just to clarify further
I get hundreds of categories and the way to know which one is attached is by ParentCid so if a ParentCid matches a Cid and that Cid's ParentCid = 0 then that is the parent
So basically all of the categories that have "ParentCid": 0, are Parent categories
You need to build a tree from your array. There are several ways to achieve this in javascript. for instance (code not tested)
let childMap = { 0: [] }
let root = { Cid: 0, nodes: childMap[0] }
for (let i = 0; i < sellerCats.length; i++) {
let category = sellerCats[i]
childMap[category.Cid] = childMap[category.Cid] || []
childMap[category.ParentCid] = childMap[category.ParentCid] || []
category.nodes = childMap[category.Cid]
childMap[category.ParentCid].push(category)
}
After this code, the 'root' object should contain the tree representation of your data, then you can build your jsx recusivly with something like that:
buildCategory(cat) {
return (
<li>
<Link
to={`/seller/${this.props.storeName}/${cat.Cid}`}
className={`black-link ${this.props.activeCategory == cat.Cid ? 'active' : ''}`}
>
{cat.name_en}
</Link>
<ul>
{cat.nodes && cat.nodes.length ?
cat.nodes.map(this.buildCategory)
: '' }
</ul>
</li>
)
}
render() {
return (
<div className="store__body__menu__accordian">
<ul className="list-unstyled">
{root.nodes.map(this.buildCategory)}
</ul>
</div>
);
}