I am trying to conditionally render part of an object (user comment) onClick of button.
The objects are being pulled from a Firebase Database.
I have multiple objects and want to only render comments for the Result component I click on.
The user comment is stored in the same object as all the other information such as name, date and ratings.
My original approach was to set a boolean value of false to each Result component and try to change this value to false but cannot seem to get it working.
Code and images attached below, any help would be greatly appreciated.
{
accumRating: 3.7
adheranceRating: 4
cleanRating: 2
date: "2020-10-10"
place: "PYGMALIAN"
staffRating: 5
timestamp: t {seconds: 1603315308, nanoseconds: 772000000}
userComment: "Bad"
viewComment: false
}
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button>i</button>
{/* <span>{item.userComment}</span> */}
</div >
)
})
You have to track individual state of each button toggle in that case.
The solution I think of is not the best but you could create a click handler for the button and adding a classname for the span then check if that class exists. If it exists then, just hide the comment.
Just make sure that the next sibling of the button is the target you want to hide/show
const toggleComment = (e) => {
const sibling = e.target.nextElementSibling;
sibling.classList.toggle('is-visible');
if (sibling.classList.contains('is-visible')) {
sibling.style.display = 'none'; // or set visibility to hidden
} else {
sibling.style.display = 'inline-block'; // or set visibility to visible
}
}
<button onClick={toggleComment}>i</button>
<span>{item.userComment}</span>
You can try like this:
const [backendData, setBackendData] = useState([]);
...
const showCommentsHandler = (viewComment, index) => {
let clonedBackendData = [...this.state.backendData];
clonedBackendData[index].viewComment = !viewComment;
setBackendData(clonedBackendData);
}
....
return(
<div>
....
<button onClick={() => showCommentsHandler(item.viewComment, index)}>i</button>
{item.viewComment && item.userComment}
<div>
You can store an array with that places which are clicked, for example:
const [ selectedItems, setSelectedItems] = React.useState([]);
const onClick = (el) => {
if (selectedItems.includes(el.place)) {
setSelectedItems(selectedItems.filter(e => e.place !== el.place));
} else {
setSelectedItems(selectedItems.concat(el));
}
}
and in your render function
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button onClick={() => onClick(item)}>i</button>
{ /* HERE */ }
{ selectedItems.includes(item.place) && <span>{item.userComment}</span> }
</div >
)
})
You need to use useState or your component won't update even if you change the property from false to true.
In order to do so you need an id since you might have more than one post.
(Actually you have a timestamp already, you can use that instead of an id.)
const [posts, setPosts] = useState([
{
id: 1,
accumRating: 3.7,
adheranceRating: 4,
cleanRating: 2,
date: "2020-10-10",
place: "PYGMALIAN",
staffRating: 5,
timestamp: { seconds: 1603315308, nanoseconds: 772000000 },
userComment: "Bad",
viewComment: false
}
]);
Create a function that updates the single property and then updates the state.
const handleClick = (id) => {
const singlePost = posts.findIndex((post) => post.id === id);
const newPosts = [...posts];
newPosts[singlePost] = {
...newPosts[singlePost],
viewComment: !newPosts[singlePost].viewComment
};
setPosts(newPosts);
};
Then you can conditionally render the comment.
return (
<div className="Results" key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={"read-only"}
value={item.accumRating}
style={{
width: "auto",
alignItems: "center"
}}
/>
<button onClick={() => handleClick(item.id)}>i</button>
{item.viewComment && <span>{item.userComment}</span>}
</div>
);
Check this codesandbox to see how it works.
Related
I passed a function to the child to check a checkbox and then to set setDispatch(true), the problem is that when I check the checkbox everything freezes and the website stops until I close and open again.
the function:
const [selectedChkBx, setSelectedChkBx] = useState({ arrayOfOrders: [] });
const onCheckboxBtnClick = (selected) => {
const index = selectedChkBx.arrayOfOrders.indexOf(selected);
if (index < 0) {
selectedChkBx.arrayOfOrders.push(selected);
} else {
selectedChkBx.arrayOfOrders.splice(index, 1);
}
setSelectedChkBx(selectedChkBx)
toggleDispatchButton()
};
const toggleDispatchButton = () => {
if (selectedChkBx.arrayOfOrders.length == 0) {
setDispatchButtonDisplay(false)
}
else {
setDispatchButtonDisplay(true)
}
}
Child Component:
<form style={{ display: 'block' }} >
<Row sm={1} md={2} lg={3}>
{ordersDisplay.map((value, key) => {
return (
<motion.div key={value.id} layout>
<DeliveryQueueComp
selectedChkBx={selectedChkBx}
toggleDispatchButton={toggleDispatchButton}
setDispatchButtonDisplay={setDispatchButtonDisplay}
value={value}
onCheckboxBtnClick={onCheckboxBtnClick}
/>
</motion.div>
)
})
}
</Row> </form>
DeliveryQueueComp Code:
<div
className={styles1.checkBox}
style={{ background: selectedChkBx.arrayOfOrders.includes(value.id) ?
'#f84e5f' : 'transparent' }}
onClick={() => { onCheckboxBtnClick(value.id) }}
>
<FontAwesomeIcon icon={faCheck} style={{ fontSize: '10px', opacity:
selectedChkBx.arrayOfOrders.includes(value.id) ? '1' : '0' }} />
</div>
If I remove toggleDispatchButtonDisplay, it works but then after a while the page freezes again.
Any thoughts about this?
As you didn't provide setDispatch code I don't know what it does, but for the rest I think I know why it's not working.
You're assigning the array and then set it to the state. If you want to do this that way you should only do a forceUpdate instead of a setState (as it has already been mutated by push and splice).
To properly update your state array you can do it like this
const onCheckboxBtnClick = (selected) => {
const index = selectedChkBx.arrayOfOrders.indexOf(selected);
if (index < 0) {
//the spread in array creates a new array thus not editing the state
setSelectedChkBx({
arrayOfOrders: [...selectedChkBx.arrayOfOrders, selected]
});
} else {
// same thing for the filter here
setSelectedChkBx({
arrayOfOrders: selectedChkBx.arrayOfOrders.filter(
(value) => value !== selected
)
});
}
toggleDispatchButton();
};
Here is the sandbox of your code https://codesandbox.io/s/eager-kalam-ntc7n7
What i am trying to do it, when a button is clicked, at the onclick it's variant(material ui button should change from outlined to contained) or simply its background should change. (Please do not suggest for the onFocus property because these is another button in another component, which when clicked focus is lost. So onFocus is not a choice for me here). I am atatching my method here, you can change it (because mine is not working anyhow, it's changing state to true indefinitely)
const [clicked, setClicked] = useState(false);
const categoryChangedHandler = (e) => {
setCategory(e);
};
{categories.map((category, index) => {
console.log("catogoried.map called and categories= " + category);
return <Button className="CategoryButton"
variant={clicked ? "contained" : "outlined"}
color="primary"
value={category}
onClick={() => {
categoryChangedHandler(category);
setClicked(true);
}}
style={{ textAlign: 'center' }}
>
{category}
</Button>
})
}
If you want to show a different color base on it's category, you probably want to change the variant base on the state ( whether it's selected ).
Example
const categories = ['apple', 'banana', 'mango']
const App = () => {
const [ selected, setSelected ] = useState([])
const onClick = (value) => {
//this is a toggle to add/remove from selected array
if (selected.indexOf(value) > -1) {
//if exist, remove
setSelected( prev => prev.filter( item => item !== value )
} else {
//add to the selected array
setSelected( prev => [ ...prev, value ] )
}
}
return <div>
{categories.map((category, index) => {
return <Button className="CategoryButton"
/* if the category had been selected, show contained */
variant={ selected.indexOf(category) > -1 ? "contained" : "outlined"}
color="primary"
value={category}
onClick={() => {
onClick(category);
}}
style={{ textAlign: 'center' }}
>
{category}
</Button>
})
}</div>
}
The above example keeps an array of categories selected. OF course, if you only want to allow ONE to be selected at each click, then instead of an array, you can use setSelected(value) (where value is the category name), then in your button component use
variant={ selected === category ? 'contained' : 'outlined' }
Remember to change your use state to use string instead of array
const [ selected, setSelected ] = useState('') //enter a category name if you want it to be selected by default
I have an intersectionObserver that watches some sections and highlights the corresponding navigation item. But I've only managed to get the "main sections Microsoft, Amazon working, but not the subsections Define, Branding, Design, Deduction. As seen in the gif below:
The reason why I want it structured this way is so that I can highlight the "main" sections if the subsections are in view.
Semi working demo: https://codesandbox.io/s/intersection-with-hooks-fri5jun1344-fe03x
It might seems that I might be able to copy and paste the same functionality with the subsections as well. But I'm having a hard time wrapping my head around how to deal with nested data + useRef + reducer. I was wondering if someone could give me a pointer in the right direction.
Here is an gif of the desired effect. Notice the main title (Loupe, Canon) are still highlighted if one of the subsections are in view:
It all starts with an data array
const data = [
{
title: "Microsoft",
id: "microsoft",
color: "#fcf6f5",
year: "2020",
sections: ["define", "branding", "design", "deduction"]
},
{
title: "Amazon",
id: "amazon",
color: "#FFE2DD",
year: "2018",
sections: ["define", "design", "develop", "deduction"]
},
{
title: "Apple",
id: "apple",
color: "#000",
year: "2020",
sections: ["about", "process", "deduction"]
}
];
App.js padding data object into reduce to create Refs
const refs = data.reduce((refsObj, Case) => {
refsObj[Case.id] = React.createRef();
return refsObj;
}, {});
My components passing in the props
<Navigation
data={data}
handleClick={handleClick}
activeCase={activeCase}
/>
{data.map(item => (
<Case
key={item.id}
activeCase={activeCase}
setActiveCase={setActiveCase}
refs={refs}
data={item}
/>
))}
Case.js
export function Case({ data, refs, activeCase, setActiveCase }) {
const components = {
amazon: Amazon,
apple: Apple,
microsoft: Microsoft
};
class DefaultError extends Component {
render() {
return <div>Error, no page found</div>;
}
}
const Tag = components[data.id] || DefaultError;
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.target.id !== activeCase && entry.isIntersecting) {
setActiveCase(entry.target.id);
}
});
}, observerConfig);
observer.observe(refs[data.id].current);
return () => observer.disconnect(); // Clenaup the observer if unmount
}, [activeCase, setActiveCase, refs, data]);
return (
<React.Fragment>
<section
ref={refs[data.id]}
id={data.id}
className="section"
style={{ marginBottom: 400 }}
>
<Tag data={data} />
</section>
</React.Fragment>
);
}
I've tried mapping the subsections like this but I get stuck at this part:
const subRefs = data.map((refsObj, Case) => {
refsObj[Case] = React.createRef();
return refsObj;
}, {});
Working Example
I've found a solution while trying to keep most of your logic intact. Firstly what you need to do is to store the subrefs (the sections ref) in the same object as your Case ref. So you will need an extra reduce function to create those inside the refs object:
App.js
const refs = data.reduce((refsObj, Case) => { // Put this outside the render
const subRefs = Case.sections.reduce((subrefsObj, Section) => {
subrefsObj[Section] = React.createRef();
return subrefsObj;
}, {});
refsObj[Case.id] = {
self: React.createRef(), // self is the Case ref, like Apple, Microsoft...
subRefs // This is going to be the subrefs
};
return refsObj;
}, {});
Then you add an extra state to handle which sub section is active, like const [activeSection, setActiveSection] = React.useState(); And you put it anywhere you also use the activeCase. You need that because you said that the Case and Sections need to work independently. (Both active at the same time).
Case.js
You will need to pass along the subrefs to the child components, so you do:
<Tag data={data} subRefs={refs[data.id].subRefs} />
And you will also need the intersection observer for each of the subrefs. So your useEffect will look like:
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observerCallback = (entries, isCase) => {
const activeEntry = entries.find(entry => entry.isIntersecting);
if (activeEntry) {
if (isCase) setActiveCase(activeEntry.target.id);
else setActiveSection(activeEntry.target.id);
} else if (isCase) {
setActiveCase(null);
setActiveSection(null);
}
};
const caseObserver = new IntersectionObserver(
entries => observerCallback(entries, true),
observerConfig
);
caseObserver.observe(refs[data.id].self.current);
const sectionObserver = new IntersectionObserver(
entries => observerCallback(entries, false),
observerConfig
);
Object.values(refs[data.id].subRefs).forEach(subRef => {
sectionObserver.observe(subRef.current);
});
return () => {
caseObserver.disconnect();
sectionObserver.disconnect();
}; // Clenaup the observer if unmount
}, [refs, data]);
Then in your amazon/index.js ,microsoft/index.js and apple/index.js files. You pass along the ref again:
<Template
data={this.props.data}
caseSections={caseSections}
subRefs={this.props.subRefs}
/>
Finally, in your template.js file you will have the following so you can assign the right ref:
const Template = props => {
return (
<React.Fragment>
<div
sx={{
background: "#eee",
transition: "background ease 0.5s"
}}
>
{props.data.sections &&
props.data.sections.map(subItem => (
<Container
ref={props.subRefs && props.subRefs[subItem]}
id={`${props.data.id}--${subItem}`}
key={subItem}
className="article"
>
<Section sectionId={subItem} caseSections={props.caseSections} />
</Container>
))}
</div>
</React.Fragment>
);
};
I believe most of it is covered in the post. You can check your forked working repo here
You can simplify your code. You don't really need refs or intersectionObservers for your use case. You can simply scrollIntoView using document.getElementById (you already have ids to your navs.
You can do setActiveCase very well in handleClick.
Working demo
Modify handleClick like this
const handleClick = (subTabId, mainTabName) => {
//console.log("subTabName, mainTabName", subTabId, mainTabName);
setActiveCase({ mainTab: mainTabName, subTab: subTabId.split("--")[1] }); //use this for active tab styling etc
document.getElementById(subTabId).scrollIntoView({
behavior: "smooth",
block: "start"
});
};
Navigation.js Call handleClick like this.
{item.sections &&
item.sections.map(subItem => (
<div
className={`${styles.anchor}`}
key={`#${item.title}--${subItem}`}
sx={{ marginRight: 3, fontSize: 0, color: "text" }}
href={`#${item.title}--${subItem}`}
onClick={e => {
handleClick(`${item.id}--${subItem}`, item.id);
e.stopPropagation();
}}
>
{toTitleCase(subItem)}
</div>
))}
I am working with a set of arrays which are printed to the screen as buttons via an API call.
I am looking to add my bases/frostings buttons (select one) and then add the key of each one selected to a new OrdersArray. I also need to be able to select multiple toppings (multi-select) and add those to a nested array within the OrdersArray.
I would like to also change the colors of each selected button when they are selected.
My Buttons function generates the buttons.
function Buttons({ list }) {
const style = {
display: 'inline-block',
textAlign: 'center',
border: '1px solid black',
padding: '10px',
margin: '10px',
width: '35%'
}
return (
<div>
{list && list.map(item =>
<button key={item.key}
style={style}
>
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
)}
</div>
);
};
My app component renders the buttons.
Class App extends Component {
constructor() {
super();
this.state = {
'basesObject': {},
'frostingsObject': {},
'toppingsObject': {},
selectedButton: null
}
}
componentDidMount() {
this.getBases();
this.getFrostings();
this.getToppings();
}
/* GET DATA FROM SERVER */
getBases() {
fetch('http://localhost:4000/cupcakes/bases')
.then(results => results.json())
.then(results => this.setState({'basesObject': results}))
}
getFrostings() {
fetch('http://localhost:4000/cupcakes/frostings')
.then(results => results.json())
.then(results => this.setState({'frostingsObject': results}))
}
getToppings() {
fetch('http://localhost:4000/cupcakes/toppings')
.then(results => results.json())
.then(results => this.setState({'toppingsObject': results}))
}
render() {
let {basesObject, frostingsObject, toppingsObject} = this.state;
let {bases} = basesObject;
let {frostings} = frostingsObject;
let {toppings} = toppingsObject;
return (
<div>
<h1>Choose a base</h1>
<Buttons on
list={bases}
/>
<h1>Choose a frosting</h1>
<Buttons
list={frostings}
/>
<h1>Choose toppings</h1>
<Buttons
list={toppings}
/>
</div>
);
}
}
I'm new to React, any help would be appreciated! :)
You'll need to pass a function to the buttons that will modify a state value in the parent component when the button is clicked.
In parent:
const addToOrder = item => {
orderArray.push(item);
const newOrder = orderArray.reduce((acc, order) => {
return acc + " " + order.name;
}, "");
setOrder(newOrder);
};
...
<Button addToOrder={addToOrder} />
In Button.js
<button onClick={() => addToOrder(item)} >
Check out the whole thing in this Sandbox
For keeping track of which ones have been clicked you'll need to keep track of button state either in the button component itself or in the parent container if you want to keep the buttons stateless. Then set the button attribute disabled to true or false based on that state.
<button disabled={isButtonDisabled} />
Sorry I didn't have time to flesh the full thing out, but this should get you in the right direction.
So I have a parent component called InventoryView and a child component InventoryBase that basically renders data sent to it by InventoryView. So the problem I keep running into is that I need to test if the Update button in InventoryBase is calling handleUpdate(). HandleUpdate should only be called if input values have actually been changed. I need to test whether or not handleUpdate() is being called if input changes or not.
So the button first called --> handleClick() which looks at the inputs and gathers data and then calls handleUpdate() which propagates to InventoryView. When I test handleUpdate() and make sure if the inputs have changed, it says handleUpdate() has been called zero times. Since I have changed one of the inputs the handleUpdate() and handleClick() both should be called once. But they are being called 0 times. I console.log the prev and cur input values in handleClick() so when I simulate a click on the button those values are being printed meaning handleClick() is being triggered but the click on mock is not being registered..??? I am not sure what is happening here.
InventoryView:
render() {
return(
<InventoryBase
tableHeadings={inventoryViewUpdate.tableHeadings}
tableTitle={inventoryViewUpdate.tableTitle}
tableBody={this.state.tableBody}
tableFooter={this.state.tableFooter}
handleUpdate={this.handleUpdate}
lastUpdated={this.state.lastUpdated}
endOfDay={true} />
);
}
InventoryBase: inventoryBase.updateBtn === 'UPDATE' which is the ID I use to simulate the button.
handleClick = (inputIds, prevVals) => {
let change = false;
let curVals = [];
let update = {
inputUpdate: [],
timeUpdate: this.updateDate()
};
for (let i in inputIds) {
let pkg = {
id: inputIds[i],
value: document.getElementById(inputIds[i]).value
}
curVals[i] = pkg.value;
update.inputUpdate.push(pkg);
}
for (let i in curVals) {
if (curVals[i] != prevVals[i]) change = true;
}
if (change === false) return;
console.log(curVals); <-- prints ['2', '4']
console.log(prevVals); <-- prints ['4', '4']
this.props.handleUpdate(update); <----- call handleUpdate here if inputs have changed
}
render() {
let inputIds = [];
let prevVals = [];
const { classes } = this.props;
return (
<Aux>
<Paper className={classes.root}>
<div style={{ float: 'right', width: "132px" }}>
<div style={{ display: 'flex', align: 'center', justifyContent: 'center' }}>
<Button type="button" id={inventoryBase.updateBtn} variant="contained" onClick={() => this.handleClick(inputIds, prevVals)} className={classes.updateBtn}>
<span>
<Autorenew style={{
color: '#FFFFFF',
marginRight: '8px'
}} />
</span>
<span>
<Typography style={{ marginBottom: '8px' }} className={classes.btnText}>{inventoryBase.updateBtn}</Typography>
</span>
</Button>
</div>
....
..... more code
</Paper>
</Aux>
);
}
InventoryView.test.js:
const STORE = createStore();
describe('InventoryBase', () => {
let wrapper;
let props;
beforeEach(() => {
props = {
inventory: inventory,
packages: packages,
branchLockers: branchLockers,
updateInventory: jest.fn(),
handleUpdate: jest.fn()
}
wrapper = mount(
<Provider store={STORE}>
<InventoryViewComponent {...props}/>
</Provider> , { attachTo: document.body }
);
});
test('Should update last updated time if input values have changed', () => {
document.getElementById('Envelop_1').value = '2'
wrapper.find('#UPDATE').at(0).simulate('click');
wrapper.update();
expect(props.handleUpdate).toHaveBeenCalledTimes(1);
});
});