Rendering a nested navigation in React - javascript

I have the following data structure for my website’s navigation. This is just a JSON object:
[{
"path": "/",
"name": "Home"
}, {
"path": "/products",
"name": "Products",
"subnav": [{
"path": "/sharing-economy",
"name": "Sharing Economy"
}, {
"path": "/pre-employment-screening",
"name": "Pre-Employment Screening"
}, {
"path": "/kyc-and-aml",
"name": "KYC & AML"
}]
}, {
"path": "/checks",
"name": "Checks"
}, {
"path": "/company",
"name": "Company"
}]
What I’d like to do is to render the following from it, having a nested list inside of the Products list item when the subnav key is present:
<ul>
<li>Home</li>
<li>Products
<ul>
<li>Sharing Economy</li>
<li>Pre-Employment Screening</li>
<li>KYC & AML</li>
</ul>
</li>
<li>Checks</li>
<li>Company</li>
</ul>
Currently, my React code looks like this:
// This is the data structure from above
import navigation from '../data/navigation.json'
const SubNavigation = (props) => {
// Here I’m trying to return if the props are not present
if(!props.subnav) return
props.items.map((item, index) => {
return <Link key={index} to={item.path}>{item.name}</Link>
})
}
class Header extends React.Component {
render() {
return (
<header className='header'>
{navigation.map((item, index) => {
return(
<li key={index}>
<Link to={item.path}>{item.name}</Link>
<SubNavigation items={item.subnav}/>
</li>
)
})}
</header>
)
}
}
export default Header
I’m using a functional stateless component to render the SubNavigation, however am running into trouble when item.subnav is undefined.
How would I adapt this code so that I conditionally render the SubNavigation based on the item.subnav key being present/undefined.

Could you try this:
<header className='header'>
{navigation.map((item, index) => {
return(
<li key={index}>
<Link to={item.path}>{item.name}</Link>
{ item.subnav && <SubNavigation items={item.subnav}/> }
</li>
)
})}
</header>

Change your code to:
const SubNavigation = (props) => {
// Here I’m trying to return if the props are not present
if(!props.items) return null;
return (<ul>
{props.items.map((item, index) => {
return <Link key={index} to={item.path}>{item.name}</Link>
})}
</ul>
);
}

Try the following:
import _ from 'underscore';
class Link extends React.Component
{
static defaultProps = {
to: '/hello'
};
static propTypes = {
to: React.PropTypes.string
};
render() {
var props = _.omit(this.props, 'to');
return (
<a href={this.props.to} {... this.props} />
);
}
}
class Header extends React.Component
{
static defaultProps = {
nav: [{"path":"/","name":"Home"},{"path":"/products","name":"Products","subnav":[{"path":"/sharing-economy","name":"Sharing Economy"},{"path":"/pre-employment-screening","name":"Pre-Employment Screening"},{"path":"/kyc-and-aml","name":"KYC & AML"}]},{"path":"/checks","name":"Checks"},{"path":"/company","name":"Company"}]
};
static propTypes = {
nav: React.PropTypes.array
};
render() {
var props = _.omit(this.props, 'nav');
return (
<header className="header" {... props}>
{this.renderNav(this.props.nav)}
</header>
)
}
renderNav(items, props = {}) {
return (
<ul {... props}>
{items.map((v, k) => this.renderNavItem(v, {key: k}))}
</ul>
);
}
renderNavItem(item, props = {}) {
return (
<li {... props}>
<Link to={item.path}>
{item.name}
</Link>
{item.subnav ? this.renderNav(item.subnav) : null}
</li>
);
}
}

Related

How to create algolia autocomplete custom renderer using react class component

I am tickling with Algolia autocomplete, and I am trying to replicate their custom renderer in react using the class component. This is the sandbox of the minimal demo of custom renderer using functional component,
and here is my attempt to convert it into a class component.
import { createAutocomplete } from "#algolia/autocomplete-core";
import { getAlgoliaResults } from "#algolia/autocomplete-preset-algolia";
import algoliasearch from "algoliasearch/lite";
import React from "react";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
);
// let autocomplete;
class AutocompleteClass extends React.PureComponent {
constructor(props) {
super(props);
this.inputRef = React.createRef();
this.autocomplete = null;
this.state = {
autocompleteState: {},
};
}
componentDidMount() {
if (!this.inputRef.current) {
return undefined;
}
this.autocomplete = createAutocomplete({
onStateChange({ state }) {
// (2) Synchronize the Autocomplete state with the React state.
this.setState({ autocompleteState: state });
},
getSources() {
return [
{
sourceId: "products",
getItems({ query }) {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: "instant_search",
query,
params: {
hitsPerPage: 5,
highlightPreTag: "<mark>",
highlightPostTag: "</mark>",
},
},
],
});
},
getItemUrl({ item }) {
return item.url;
},
},
];
},
});
}
render() {
const { autocompleteState } = this.state;
return (
<div className="aa-Autocomplete" {...this.autocomplete?.getRootProps({})}>
<form
className="aa-Form"
{...this.autocomplete?.getFormProps({
inputElement: this.inputRef.current,
})}
>
<div className="aa-InputWrapperPrefix">
<label
className="aa-Label"
{...this.autocomplete?.getLabelProps({})}
>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
ref={this.inputRef}
{...this.autocomplete?.getInputProps({})}
/>componentDidUpdate()
</div>
</form>
<div className="aa-Panel" {...this.autocomplete?.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source">
{items.length > 0 && (
<ul
className="aa-List"
{...this.autocomplete?.getListProps()}
>
{items.map((item) => (
<li
key={item.objectID}
className="aa-Item"
{...this.autocomplete?.getItemProps({
item,
source,
})}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
}
export default AutocompleteClass;
and the sandbox of the same version, I also tried using componentDidUpdate() but no luck, any lead where I did wrong would be much appreciated thank you :)
Ok, dont know why you need it made into class component but here you go:
import { createAutocomplete } from "#algolia/autocomplete-core";
import { getAlgoliaResults } from "#algolia/autocomplete-preset-algolia";
import algoliasearch from "algoliasearch/lite";
import React from "react";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
);
// let autocomplete;
class AutocompleteClass extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
autocompleteState: {},
query: '',
};
this.autocomplete = createAutocomplete({
onStateChange: this.onChange,
getSources() {
return [
{
sourceId: "products",
getItems({ query }) {
console.log('getting query', query)
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: "instant_search",
query,
params: {
hitsPerPage: 5,
highlightPreTag: "<mark>",
highlightPostTag: "</mark>"
}
}
]
});
},
getItemUrl({ item }) {
return item.url;
}
}
];
}
});
}
onChange = ({ state }) => {
console.log(state)
this.setState({ autocompleteState: state, query: state.query });
}
render() {
const { autocompleteState } = this.state;
return (
<div className="aa-Autocomplete" {...this.autocomplete?.getRootProps({})}>
<form
className="aa-Form"
{...this.autocomplete?.getFormProps({
inputElement: this.state.query
})}
>
<div className="aa-InputWrapperPrefix">
<label
className="aa-Label"
{...this.autocomplete?.getLabelProps({})}
>
Search
</label>
</div>
<div className="aa-InputWrapper">
<input
className="aa-Input"
value={this.state.query}
{...this.autocomplete?.getInputProps({})}
/>
</div>
</form>
<div className="aa-Panel" {...this.autocomplete?.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source">
{items.length > 0 && (
<ul
className="aa-List"
{...this.autocomplete?.getListProps()}
>
{items.map((item) => (
<li
key={item.objectID}
className="aa-Item"
{...this.autocomplete?.getItemProps({
item,
source
})}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
}
export default AutocompleteClass;
Anyway the componentDidMount is called only once, and because of ref object is undefined it just returned from it.
Also messing with this in class components is quite a bad idea (that is why func components are recommended)

Updating state from recursively rendered component in React.js

I am in need of updating deeply nested object in a React state from a recursively rendered component. The items look like this and can be nested dynamically:
const items = [
{
id: "1",
name: "Item 1",
isChecked: true,
children: []
},
{
id: "3",
name: "Item 3",
isChecked: false,
children: [
{
id: "3.1",
name: "Child 1",
isChecked: false,
children: [
{
id: "3.1.1",
name: "Grandchild 1",
isChecked: true,
children: []
},
{
id: "3.1.2",
name: "Grandchild 2",
isChecked: true,
children: []
}
]
},
{
id: "3.2",
name: "Child 2",
isChecked: false,
children: []
}
]
}
]
I have a problem figuring out how to update the top level state from within a deeply nested component, because it only "knows" about itself, not the entire data structure.
class App extends React.Component {
state = {
items
};
recursivelyRenderItems(items) {
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<input type="checkbox" checked={item.isChecked} onChange={(event) => {
// TODO: Update the item's checked status
}} />
{this.recursivelyRenderItems(item.children)}
</li>
))}
</ul>
)
}
render() {
return (
<div>
{this.recursivelyRenderItems(this.state.items)}
</div>
);
}
}
How can I achieve this, please?
This works in your fiddle you posted.
Basically, each component needs to know about its item.isChecked and its parent's isChecked. So, create a component that takes 2 props, item and parentChecked where the latter is a boolean from the parent and the former becomes an mutable state variable in the constructor.
Then, the component simply updates its checked state in the event handler and it all flows down in the render method:
import React from "react";
import { render } from "react-dom";
import items from "./items";
class LiComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
item: props.item
}
}
render() {
var t = this;
return (
<ul>
<li key={t.state.item.id}>
{t.state.item.name}
<input
type="checkbox"
checked={t.state.item.isChecked || t.props.parentChecked}
onChange={event => {
t.setState({item: {...t.state.item, isChecked: !t.state.item.isChecked}})
}}
/>
</li>
{t.state.item.children.map(item => (
<LiComponent item={item} parentChecked={t.state.item.isChecked}/>
))}
</ul>
);
}
}
class App extends React.Component {
state = {
items
};
render() {
return (
<div>
{this.state.items.map(item => (
<LiComponent item={item} parentChecked={false} />
))}
</div>
);
}
}
render(<App />, document.getElementById("root"));
https://codesandbox.io/s/treelist-component-ywebq
How about this:
Create an updateState function that takes two args: id and newState. Since your ids already tell you what item you are updating: 3, 3.1, 3.1.1, you can from any of the recursive items call your update function with the id of the item you want to modify. Split the id by dots and go throw your items recursively to find out the right one.
For example from the recursive rendered item 3.1.1, can call updateState like this: updateState('3.1.1', newState).
import React from "react";
import { render } from "react-dom";
import items from "./items";
class App extends React.Component {
state = {
items
};
updateState = item => {
const idArray = item.id.split(".");
const { items } = this.state;
const findItem = (index, children) => {
const id = idArray.slice(0, index).join(".");
const foundItem = children.find(item => item.id === id);
if (index === idArray.length) {
foundItem.isChecked = !item.isChecked;
this.setState(items);
return;
} else {
findItem(index + 1, foundItem.children);
}
};
findItem(1, items);
};
recursivelyRenderItems(items) {
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<input
type="checkbox"
checked={item.isChecked}
onChange={event => this.updateState(item)}
/>
{this.recursivelyRenderItems(item.children)}
</li>
))}
</ul>
);
}
render() {
return <div>{this.recursivelyRenderItems(this.state.items)}</div>;
}
}
render(<App />, document.getElementById("root"));
Working example.

I can not show elements of my json - React

I need to retrieve all the information from other fields, not just "Padre" and "Hijo" in the json. I also want to get information from fields like "Id" or "url". I think I can not get them back for the reduced function in componentWillMount, what if I need for "Padre" and "Hijo".
This is an example of my json.
{
"Id": "114",
"Description": "SALUD NORMAL",
"Padre": "CRM",
"Hijo": "Argumentarios",
"URL": "www.test.com",
"Closable": "1",
"Autoload": "0",
"Visible": "1"
}
Nav would be the parent element and content his child, Menu. Menu is the one that creates a button for each of the "Padre" (this.props.menu) and "Hijo" (this.props.submenu) of the json.
This is my code.
class Nav extends Component{
constructor(props){
super(props)
this.state = {
menuSubmenu:[],
abrirSubmenu: false,
}
this.submenuClick = this.submenuClick.bind(this);
}
submenuClick() {
this.setState(state => ({
abrirSubmenu: !state.abrirSubmenu
}));
//alert('Click!')
}
componentWillMount(){
fetch('fake.php')
.then(response => response.json())
.then(menuSubmenu =>{
const PadreMap = menuSubmenu.reduce((acc, obj) => {
if (!acc[obj.Padre]) {
acc[obj.Padre] = {
...obj,
Hijo: [obj.Hijo],
Description: [obj.Description]
};
} else {
!acc[obj.Padre].Hijo.includes(obj.Hijo) && acc[obj.Padre].Hijo.push(obj.Hijo);
//!acc[obj.Padre].Hijo.includes(obj.Hijo) && acc[obj.Padre].Hijo.push(obj.Description)
}
return acc;
}, {});
this.setState({
menuSubmenu: Object.keys(PadreMap).map((padre) => ({
menu: padre,
submenu: PadreMap[padre].Hijo,
id: PadreMap.Id,
descripcion: PadreMap[padre].Description,
url: PadreMap[padre].URL
}))
})
console.log(PadreMap);
})
}
render() {
if (this.state.menuSubmenu.length > 0) {
return(
<nav className="nav">
<div className="menu">
<ul className="list">
{this.state.menuSubmenu.map(datos => <Menu key={datos.id} menu={datos.menu} submenu={datos.submenu} descripcion={datos.descripcion} submenuClick={this.submenuClick} abrirSubmenu={this.state.abrirSubmenu}/>)}
</ul>
<div className="content-bnt">
<button id="desplegar" className='btn btn--rounded'>
<Icon icon="flecha" className='ico-flecha'/>
</button>
</div>
</div>
</nav>
);
}
return (<p>Cargando usuarios...</p>);
}
}
class Menu extends Component{
render(){
return (
<li key={this.props.id} className="list__item">
<button title={this.props.menu} id={"mn-" + this.props.menu} className="desplegable" onClick={this.props.submenuClick}><Icon icon="auriculares" className='ico-auriculares'/>{this.props.menu}</button>
{
this.props.abrirSubmenu
? (
<div id="sb-crm" className="submenu">
{this.props.submenu.map(hijo => <h3 className="nav--title"><Icon icon="descargar" className='ico-descargar'/>{hijo}</h3>)}
<ul className="list">
<li className="list__item">
{this.props.descripcion.map(tercerNivel => <a href={this.props.url} title={this.props.descripcion}>{tercerNivel}</a>)}
</li>
</ul>
</div>
)
: (
null
)
}
</li>
)
}
}
export default Nav;
I need to be able to use all the information of the json, not just "Padre" and "Hijo". I also need the information to be grouped by his "Padre"
This snippet should be suitable:
this.setState(
{
menuSubmenu: Object.keys(PadreMap).map((padre) => {
const {Padre, Hijo, ...rest} = PadreMap[padre];
return {
menu: padre,
submenu: Hijo,
...rest
}
})
}
)

2 Props passed to Child Component with a Map Function

Hi how can I pass the Path into the map function! Have tried as nested array but can't get it
Navbar (parent component):
class Navbar extends Component {
constructor(props) {
super(props);
this.state = {
nav: [{
topic: 'Home',
path: '/',
},
{
topic: 'Bogen',
path: '/Bogen'
},
],
topics: ['Home', 'Bogen', 'Projekt', 'Nutzer'],
path: ['/', '/Bogen', '/Projekt', '/Nutzer'],
}
}
render() {
return (
<div className='navbar'>
<NavbarTopics topics={this.state.topics}
/>
</div>
)
}
}
NavbarTopics (child component):
const NavbarTopics = (props) => (
<ul className='ul_Ntopics'>
{props.topics.map((topic, index) => <NavTopic topic={topic} key={index} />)}
</ul>
)
NavTopic (child component):
const NavTopic = (props) => <li className='li_Ntopic'><Link className='Link_Ntopic' to=''>{props.topic}</Link></li>;
NavTopic.propTypes = {
topic: PropTypes.string.isRequired,
}
export default NavTopic;
How can I pass the state path to the map-function, so that I can pass it as prop to NavTopic?
Instead of passing topic as a prop, pass nav.
//Navbar
<NavbarTopics nav={this.state.nav}
Then iterate through nav array
// NavbarTopics
const NavbarTopics = (props) => (
<ul className='ul_Ntopics'>
{props.nav.map((nav, index) => <NavTopic nav={nav} key={index} />)}
</ul>
)
In NavTopic,
const NavTopic = (props) => <li className='li_Ntopic'>
<Link className='Link_Ntopic' to={props.nav.path}>{props.nav.topic}</Link>
</li>;

Using props to toggle nav list, with nested array/object

I have two components that are loading some data. What I want is for the links to output like this:
group1
linka linkb
But it's doing this
group1
linka
group1
linkb
I can understand it is to do with the use of my maps and how i am returning the data but I cant figure out how to fix it and keep the click handler working for the groups.
const navList = [
{
"GroupName": "group1",
"links": [
{"name": "link0a","id": "434"},
{"name": "link0b","id": "342"}
]
},
{
"GroupName": "group2",
"links": [
{"name": "link1a","id": "345"},
{"name": "link1b","id": "908"}
]
}
]
class Nav extends Component {
constructor(props) {
super(props);
this.state = {
openItem: null
}
this.toggleClick = this.toggleClick.bind(this);
}
toggleClick(item, index, event) {
event.preventDefault()
if (index === this.state.openItem) {
this.setState({openItem: null})
} else {
this.setState({openItem: index})
}
}
render() {
return (
<ul>
{navList.map((section, i) => {
const links = section.links.map((item, index) => {
const isOpen = this.state && this.state.openItem === index
return <NavItem title={section.GroupName} children={item.name} onClick={this.toggleClick.bind(null, item, index)} open={isOpen} />
})
return links
})}
</ul>
)
}
}
class NavItem extends Component {
render() {
const toggleClass = this.props.open ? 'is-open' : ''
return (
<li>
<h2 onClick={this.props.onClick}>{this.props.title}</h2>
<ul className={toggleClass}>
<li>{this.props.children}</li>
</ul>
</li>
)
}
}
export default NavItem
Remove {this.props.title} from NavLink and move it in your render() function that you posted above. Otherwise, every time you render a NavLink, the title would be shown. Don't forget to move your onClick handler also.
Here is a working example based on your jsfiddle: https://jsfiddle.net/lustoykov/n21Lspm3/1/

Categories

Resources