Side Navigation (Shopify Polaris Library) active item selection - javascript

I'm trying to show the active side navigation items in Shopify Polaris Navigation components.
Here I have tried in few ways, problem is when I'm clicking nav items 2 times then showing the currently active nav link!
Can you suggest me better efficient code to solve this problem? If any more dynamic solutions available please suggest!
import React, { Fragment, useState } from "react";
import { Frame, Navigation, Page } from "#shopify/polaris";
import { GiftCardMinor } from "#shopify/polaris-icons";
import { useNavigate } from "react-router-dom";
const LeftNavigation = ({ pageTitle, loading, children }: any) => {
const [selected, setSelected] = useState([false, false]);
const navigate = useNavigate();
const handleSelect = (index) => {
// const newFilter = [...selected];
// newFilter.fill(false, 0, newFilter.length);
// newFilter[index] = true;
// setSelected(newFilter);
const newArray = selected.map((item, itemIndex) => {
return itemIndex === index
? (selected[index] = true)
: (selected[itemIndex] = false);
});
setSelected(newArray);
};
return (
<Fragment>
<Frame
navigation={
<div style={{ marginTop: "4rem" }}>
<Navigation location="/">
<Navigation.Section
title="nav items"
items={[
{
label: "one",
icon: icon1,
selected: selected[0],
onClick: () => {
handleSelect(0);
navigate("/one");
},
},
{
label: "two",
icon: icon2,
selected: selected[1],
onClick: () => {
handleSelect(1);
navigate("/two");
},
},
]}
/>
</Navigation>
</div>
}
></Frame>
</Fragment>
);
};
export default LeftNavigation;

In order to make the "active" state more dynamic it should probably only store the id of the active menu item. When mapping/rendering the menu items check explicitly against the current state value. The onClick handler should pass the currently clicked-on item's "id" value to the callback handler.
Example using the "target path" as the id value*:
const LeftNavigation = ({ pageTitle, loading, children }: any) => {
const [selected, setSelected] = useState<string | null>(null);
const navigate = useNavigate();
const handleSelect = (selectedId) => {
setSelected(selected => selected === selectedId ? null : selectedId);
};
return (
<Fragment>
<Frame
navigation={
<div style={{ marginTop: "4rem" }}>
<Navigation location="/">
<Navigation.Section
title="nav items"
items={[
{
label: "one",
icon: icon1,
selected: selected === "/one",
onClick: () => {
handleSelect("/one");
navigate("/one");
},
},
{
label: "two",
icon: icon2,
selected: selected === "/two",
onClick: () => {
handleSelect("/two");
navigate("/two");
},
},
]}
/>
</Navigation>
</div>
}
></Frame>
</Fragment>
);
};
*Note: the id value can be anything, but you should ensure it's unique per menu item if you want up to only one active at-a-time.

Related

How to build a react button that stores the selection in an array

I am trying to create a list of buttons with values that are stored in a state and user is only allowed to use 1 item (I dont want to use radio input because I want to have more control over styling it).
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState([]);
const clickTask = (item) => {
setTask([...task, item.id]);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
const chosenTask = (item) => {
if (task.find((v) => v.id === item.id)) {
return true;
}
return false;
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={chosenTask(item) ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
https://codesandbox.io/s/react-fiddle-forked-cvhivt?file=/src/App.js
I am trying to only allow 1 item to be added to the state at all the time, but I dont know how to do this?
Example output is to have Easy and Fast in task state and is selected. If user click on Easy and Cheap, select that one and store in task state and remove Easy and Fast. Only 1 item can be in the task state.
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState();
const clickTask = (item) => {
setTask(item);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={item.id === task?.id ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
Is this what you wanted to do?
Think of your array as a configuration structure. If you add in active props initialised to false, and then pass that into the component you can initialise state with it.
For each task (button) you pass down the id, and active state, along with the text and the handler, and then let the handler in the parent extract the id from the clicked button, and update your state: as you map over the previous state set each task's active prop to true/false depending on whether its id matches the clicked button's id.
For each button you can style it based on whether the active prop is true or false.
If you then need to find the active task use find to locate it in the state tasks array.
const { useState } = React;
function Tasks({ config }) {
const [ tasks, setTasks ] = useState(config);
function handleClick(e) {
const { id } = e.target.dataset;
setTasks(prev => {
// task.id === +id will return either true or false
return prev.map(task => {
return { ...task, active: task.id === +id };
});
});
}
// Find the active task, and return its text
function findSelectedItem() {
const found = tasks.find(task => task.active)
if (found) return found.text;
return 'No active task';
}
return (
<section>
{tasks.map(task => {
return (
<Task
key={task.id}
taskid={task.id}
active={task.active}
text={task.text}
handleClick={handleClick}
/>
);
})};
<p>Selected task is: {findSelectedItem()}</p>
</section>
);
}
function Task(props) {
const {
text,
taskid,
active,
handleClick
} = props;
// Create a style string using a joined array
// to be used by the button
const buttonStyle = [
'taskButton',
active && 'active'
].join(' ');
return (
<button
data-id={taskid}
className={buttonStyle}
type="button"
onClick={handleClick}
>{text}
</button>
);
}
const taskConfig = [
{ id: 1, text: 'Easy and Fast', active: false },
{ id: 2, text: 'Easy and Cheap', active: false },
{ id: 3, text: 'Cheap and Fast', active: false }
];
ReactDOM.render(
<Tasks config={taskConfig} />,
document.getElementById('react')
);
.taskButton { background-color: palegreen; padding: 0.25em 0.4em; }
.taskButton:not(:first-child) { margin-left: 0.25em; }
.taskButton:hover { background-color: lightgreen; cursor: pointer; }
.taskButton.active { background-color: skyblue; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

How to avoid rerendering all list items

On every click react rerender every item. How to avoid it? I want react to only render items that have changed. I tried using react memo and usecallback but it didn't help. I can't understand what is the reason. What are the ways to solve this problem? Thank you.
App.ts
import React, { useState } from "react";
import "./styles.css";
import ListItem from "./ListItem";
const items = [
{ id: 1, text: "items1" },
{ id: 2, text: "items2" },
{ id: 3, text: "items3" },
{ id: 4, text: "items4" },
{ id: 5, text: "items5" },
{ id: 6, text: "items6" },
{ id: 7, text: "items7" }
];
export default function App() {
const [activeIndex, setActiveIndex] = useState(1);
const onClick = (newActiveIndex: number) => {
setActiveIndex(newActiveIndex);
};
return (
<div className="App">
{items.map(({ id, text }) => (
<ListItem
key={id}
id={id}
text={text}
activeId={activeIndex}
onClick={onClick}
/>
))}
</div>
);
}
Item.ts
import React from "react";
interface ListItemProps {
id: number;
activeId: number;
text: string;
onClick: (newActiveIndex: number) => void;
}
const ListItem: React.FC<ListItemProps> = ({ id, activeId, text, onClick }) => {
const isActive = activeId === id;
const style = {
marginRight: "42px",
marginTop: "24px",
color: isActive ? "green" : "red"
};
console.log(`update id: ${id}`);
return (
<div>
<span style={style}>id: {id}</span>
<span style={style}>is active: {isActive ? "active" : "inactive"}</span>
<span style={style}>text: {text}</span>
<button onClick={() => onClick(id)}>setActive</button>
</div>
);
};
export default ListItem;
activeIndex changes with each click, so all of the components have new prop values and will need to re-render.
Instead of passing the activeIndex to every component every time:
const ListItem: React.FC<ListItemProps> = ({ id, activeId, text, onClick }) => {
const isActive = activeId === id;
Pass an isActive to each component:
const ListItem: React.FC<ListItemProps> = ({ id, isActive, text, onClick }) => {
Effectively moving the calculation of the bool from the component to the consuming code:
<ListItem
key={id}
id={id}
text={text}
isActive={activeIndex === id}
onClick={onClick}
/>
Then memoization (coupled with useCallback for the onClick handler) should work because most components (the ones which aren't changing their "active" state) won't receive new props and won't need to re-render.
Even with memoization it won't work, because your onClick function changes your components state, and your children components depend on activeIndex value, so every time you click you change the components state, and when state changes, the component re-renders itself and it's children, if your children didn't depend on your state, they wouldn't re-render (if you use memoization).
Plus to David's answer i would return ListItem in React.memo React.memo(ListItem)

How to manage props of a list of components in React?

I am trying to create an ObjectList component, which would contain a list of Children.
const MyList = ({childObjects}) => {
[objects, setObjects] = useState(childObjects)
...
return (
<div>
{childObjects.map((obj, idx) => (
<ListChild
obj={obj}
key={idx}
collapsed={false}
/>
))}
</div>
)
}
export default MyList
Each Child has a collapsed property, which toggles its visibility. I am trying to have a Collapse All button on a parent level which will toggle the collapsed property of all of its children. However, it must only change their prop once, without binding them all to the same state. I was thinking of having a list of refs, one for each child and to enumerate over it, but not sure if it is a sound idea from design perspective.
How can I reference a dynamic list of child components and manage their state?
Alternatively, is there a better approach to my problem?
I am new to react, probably there is a better way, but the code below does what you explained, I used only 1 state to control all the objects and another state to control if all are collapsed.
Index.jsx
import MyList from "./MyList";
function Index() {
const objList = [
{ data: "Obj 1", id: 1, collapsed: false },
{ data: "Obj 2", id: 2, collapsed: false },
{ data: "Obj 3", id: 3, collapsed: false },
{ data: "Obj 4", id: 4, collapsed: false },
{ data: "Obj 5", id: 5, collapsed: false },
{ data: "Obj 6", id: 6, collapsed: false },
];
return <MyList childObjects={objList}></MyList>;
}
export default Index;
MyList.jsx
import { useState } from "react";
import ListChild from "./ListChild";
const MyList = ({ childObjects }) => {
const [objects, setObjects] = useState(childObjects);
const [allCollapsed, setallCollapsed] = useState(false);
const handleCollapseAll = () => {
allCollapsed = !allCollapsed;
for (const obj of objects) {
obj.collapsed = allCollapsed;
}
setallCollapsed(allCollapsed);
setObjects([...objects]);
};
return (
<div>
<button onClick={handleCollapseAll}>Collapse All</button>
<br />
<br />
{objects.map((obj) => {
return (
<ListChild
obj={obj.data}
id={obj.id}
key={obj.id}
collapsed={obj.collapsed}
state={objects}
setState={setObjects}
/>
);
})}
</div>
);
};
export default MyList;
ListChild.jsx
function ListChild(props) {
const { obj, id, collapsed, state, setState } = props;
const handleCollapse = (id) => {
console.log("ID", id);
for (const obj of state) {
if (obj.id == id) {
obj.collapsed = !obj.collapsed;
}
}
setState([...state]);
};
return (
<div>
{obj} {collapsed ? "COLLAPSED!" : ""}
<button
onClick={() => {
handleCollapse(id);
}}
>
Collapse This
</button>
</div>
);
}
export default ListChild;

Disabling dragndrop from baseweb DnD list

I actually have a drag and drop list from BaseWeb https://baseweb.design/components/dnd-list/,
And instead of havings strings as the exemple shows, i'm having components for a blog, like a text section, some inputs, etc... I use this list to reorder my components easily, but i got a problem, if i want to click to go in my text input, I drag my component, and don't get in.
I'm using React-Quill for the text editor
Here's my code for the list:
initialState={{
items: componentsArray
}}
onChange={({oldIndex, newIndex}) =>
setComponentsArray(newIndex === -1 ?
arrayRemove(componentsArray, oldIndex) :
arrayMove(componentsArray, oldIndex, newIndex))
}
className=""
overrides={{
DragHandle: <FontAwesomeIcon icon={Icons.faLeftRight} />,
}}
/>
Try cancel onmousedown BaseWeb's handler for elements you need to free of drag.
import React, { useState, useEffect, useRef } from 'react';
import { List, arrayMove } from 'baseui/dnd-list';
import { StatefulSelect } from 'baseui/select';
import { v4 as uuidv4 } from 'uuid';
const RulesTab = () => {
const [items, setItems] = useState([{ id: uuidv4() }, { id: uuidv4() }]);
const dndRootRef = useRef(null);
useEffect(() => {
// override base-web's mousedown event handler
dndRootRef.current.addEventListener('mousedown', (el) => {
let isFreeOfDrag = false;
let currentEl = el.target;
// check if the element you clicked is inside the "free-of-drag block"
do {
if (currentEl.getAttribute('test-id') === 'free-of-drag') {
isFreeOfDrag = true;
break;
} else {
currentEl = currentEl.parentElement;
}
} while (currentEl);
// invoke el.stopPropagation(); if the element you clicked is inside the "free-of-drag block"
if (isFreeOfDrag) {
el.stopPropagation();
}
});
}, []);
return (
<List
items={items.map(({ id }, index) => (
<div style={{ display: 'flex' }} test-id="free-of-drag" key={id}>
<StatefulSelect
options={[
{ label: 'AliceBlue', id: '#F0F8FF' },
{ label: 'AntiqueWhite', id: '#FAEBD7' },
]}
placeholder="Select color"
/>
</div>
))}
onChange={({ oldIndex, newIndex }, el) => setItems(arrayMove(items, oldIndex, newIndex))}
overrides={{
Root: {
props: {
ref: dndRootRef,
},
},
}}
/>
);
};

My react child callback returns the previous data (navbar)

I am learning React and I have a problem with my childCallback, for return data from child to parent.
I make a navbar and when I click on the navButton, the parent receive the name of the precedent button, I have to click two times for display the good data.
Here is my code :
//PARENT COMPONENT
export const Profil = () => {
const [navActive, setNavActive] = useState("infos");
const handleCallback = (childData, e) => {
setNavActive(childData);
};
// View
return (
<div>
<Navbar callback={handleCallback} />
<Infos />
<Gpg />
{navActive}
</div>
);
};
//CHILD COMPONENT
export const Navbar = ({ callback }) => {
// Nav Items
let nav = [
{ key: 1, name: "infos" },
{ key: 2, name: "gpg" },
{ key: 3, name: "compte" },
{ key: 4, name: "otp" },
{ key: 5, name: "journal" },
];
// State
const [activeTitle, setActiveTitle] = useState(nav[0].name);
// Change the selected nav
function selectNav(e) {
let selected = e.target.innerText;
setActiveTitle(selected);
}
const onTrigger = () => {
callback(activeTitle);
};
// View
return (
<Nav tabs>
{nav.map((item) => (
<NavItem key={item.key} onClick={onTrigger}>
<NavLink
style={{ cursor: "pointer" }}
onClick={selectNav}
active={activeTitle === item.name}
>
{item.name}
</NavLink>
</NavItem>
))}
</Nav>
);
};
I don't know how to correct that.
Try to remove onclick from NavItem and call your callback function from selectNav function directly.
The problem come from the setActiveTitle(selected) in Navbar, with console.log I test and the value is good before this.

Categories

Resources