I am creating a React application. I want to display the last added note on top.
Notes is an array of objects. As you can see I've tried notes.reverse() but somehow it's not working. Elements are still adding at the bottom.
<div>
{notes.reverse().map((note)=> (
<NotesCard
note={note}
key={note.id}
setInputText={setInputText}
setInputTitle={setInputTitle}
setNotes={setNotes}
notes={notes}
setId={setId}
/>
))
}
</div>
I'm using submitHandler function that triggers on clicking the submit button. The function looks like this:
const submitHandler = () => {
if (inputText || inputTitle) {
setNotes([
...notes,
{title:inputTitle , text: inputText, id: Math.random()*1000}
]);
} else {
alert("Notes are Empty. Type something in textarea.");
}
setInputText("");
setInputTitle("");
setId("");
};
I'm also using an updateHandler function to update the existing note. Which triggers on clicking the Update button. Whenever I use splice() or unshift() here to set the updated note on the top chrome stops responding and everything hangs.
const updateHandler = () => {
for (var i=0; i<notes.length; i++) {
if (id===notes[i].id) {
if (inputText || inputTitle) {
notes.push(
{title:inputTitle , text: inputText, id: Math.random()*1000}
);
setNotes(notes.filter((el) => el.id !== id));
} else {
alert("Notes are Empty. Type something in textarea.");
}
}
};
setInputText("");
setInputTitle("");
setId("");
}
Remove the reverse() and add the newNote to the top of notesArray
FIRST
instead of {notes.reverse().map((note)=> ( //code ))}
use {notes.map((note)=> ( //code ))}
SECOND
instead of
setNotes([
...notes,
{title:inputTitle , text: inputText, id: Math.random()*1000}
]);
use
setNotes([
{title:inputTitle , text: inputText, id: Math.random()*1000},
...notes
]);
I think you should add your new objects at first position like below.
const submitHandler = () => {
if (inputText || inputTitle) {
setNotes([{
title: inputTitle,
text: inputText,
id: Math.random() * 1000
}, ...notes]);
} else {
alert('Notes are Empty. Type something in textarea.');
}
setInputText('');
setInputTitle('');
setId('');
};
const updateHandler = () => {
if (inputText || inputTitle) {
const newCopyNotes = notes.filter(el => el.id !== id);
setNotes([
{ title: inputTitle, text: inputText, id: Math.random() * 1000 },
...newCopyNotes
]);
} else {
alert('Notes are Empty. Type something in textarea.');
}
setInputText('');
setInputTitle('');
setId('');
};
Remove reverse and map normally
<div>
{notes.map((note)=> (
<NotesCard
note={note}
key={note.id}
setInputText={setInputText}
setInputTitle={setInputTitle}
setNotes={setNotes}
notes={notes}
setId={setId}
/>
))
}
</div>
Related
I have an editable AgGrid in my functional component as below.
On the last column, I have buttons to Add/Remove rows.
Now I want the Add row to be displayed only for the last row. I am using cellRenderer for the last column.
With the below change, I get the Add button for the last row (i.e. on 2nd row in my case) on initial render. But if I click on Add for this 2nd row, while I get the Add button for the new 3rd row, but it does not get removed for the 2nd row. not sure if I am implementing this in the wrong way.
const MyCmp = (props) => {
const getData = () => {
return [{
id: 0,
firstName: 'F1',
lastName: 'L1'
}, {
id: 1,
firstName: 'F2',
lastName: 'L2',
}];
}
const [myCols, setMyCols] = useState(null);
const [gridData, setGridData] = useState(getData());
const [gridApi, setGridApi] = useState('');
let cellRules = {
'rag-red': params => {
if (params.data.lastName === 'INVALID_VAL') {
return true;
}
}
};
const handleGridReady = (params) => {
setGridApi(params.api);
setMyCols([{
headerName: 'F Name',
field: 'firstName',
editable: true
}, {
headerName: 'L Name',
field: 'lastName',
cellClassRules: cellRules,
editable: true
}, {
headerName: '',
field: 'buttonCol',
cellRenderer: 'customColRenderer',
cellRendererParams: {
addItems: addItems
}
}]
);
};
const createNewRowData = () => {
const newData = {
id: newCount,
firstName: '',
lastName: ''
};
newCount++;
return newData;
}
let newCount = getData().length;
const addItems = (addIndex, props) => {
const newItems = [createNewRowData()];
const res = props.api.applyTransaction({
add: newItems,
addIndex: addIndex,
});
setGridData(...gridData, res.add[0].data); // IS THIS CORRECT ?
if (props.api.getDisplayedRowCount() > props.api.paginationGetPageSize()) {
props.api.paginationGoToPage(parseInt((props.api.getDisplayedRowCount() / props.api.paginationGetPageSize())) + 1);
}
}
const onCellClicked = (e) => {
}
const frameworkComponents = () => {
return {customColRenderer: customColRenderer}
}
return (
<>
<MyAgGrid
id="myGrid"
columnDefs={myCols}
rowData={gridData}
frameworkComponents={{customColRenderer: customColRenderer}}
{...props}
/>
</>
)
}
My customColRenderer is as below;
export default (props) => {
let isLastRow = (props.rowIndex === (props.api.getDisplayedRowCount() -1)) ? true: false;
const addItems = (addIndex) => {
props.addItems(addIndex, props);
}
return (
<span>
{isLastRow ? <button onClick={() => addItems()}>Add</button> : null}
<span><button onClick={() => props.api.applyTransaction({remove: props.api.getSelectedRows()})}>Remove</button>
</span>
);
};
Within the AgGrid React internals a transaction is generated automatically when your rowData is updated, as such you can choose to apply the transaction either through the api, or by updating your state - you shouldn't need to do both (as you're currently doing). Generally with React I'd suggest updating the state to keep your state true to the data displayed in the grid, however that can depend on use case.
As for the further issue of your cell not updating - that'll be due to the fact AgGrid has detected no change in the 'value' of the cell, as it attempts to reduce the amount of unnecessary cell rendering done.
You could attempt to call:
api.refreshCells({ force: true });
After your api call applying the transaction (I'm not sure where this would need to happen using the setState approach).
Hello I've got a problem with refreshing component, it doesn't work, in console log shows properly data. I want after click on div to change clicked element on true and add to this element changed class name
below is jsx
{tabObjects.map((item) => (
<div
key={item.key}
id={item.key}
className={item.isChecked ? checked : notChecked}
onClick={() => selectItem(item.key)}
>
<i className={item.icon}></i>
<p className="hide-sm">{item.pText}</p>
</div>
))}
after clicking selectItem I want to change class name to checked and rest of them set checked as false so:
const selectItem = (e) => {
tabObjects.map((item) => {
item.isChecked = false;
if (e === item.key) {
item.isChecked = true;
}
});
setTabObjects(tabObjects);
};
and sample data json
const [tabObjects, setTabObjects] = useState([
{
key: "sample1",
isChecked: true,
icon: "sample1i",
pText: "Test text",
},
{
key: "sample2",
isChecked: false,
icon: "sample2i",
pText: "Test text",
},
]);
let checked = "sampleClass checked";
let notChecked = "sampleClass";
What Am I doing wrong? Clicking on any div with console log working fine
Missing return statement is the reason.
const selectItem = (e) => {
const objects = tabObjects.map((item) => {
item.isChecked = false;
if (e === item.key) {
item.isChecked = true;
}
return item;
});
setTabObjects(objects);
};
I have this dynamic checkbox, that I want to update the state with the selected options only ,I tried to add some checks to filter the state on change , but it seems I am not seeing what went wrong!
const checkBoxesOptions = [
{ id: 1, title: 'serviceOne' },
{ id: 2, title: 'serviceTwo' },
{ id: 3, title: 'serviceThree' },
];
const [selectedCheckBoxes, setSelectedCheckBoxes] = React.useState([]);
{checkBoxesOptions.map((checkBox, i) => (
<CheckBox
key={i}
label={checkBox.title}
value={1}
checked={false}
onChange={value => {
let p = {
title: checkBox.title,
isTrue: value,
};
if (p.isTrue) {
const tempstate = selectedCheckBoxes.filter(
checkbox => checkbox !== checkBox.title
);
console.log('temp state', tempstate);
setSelectedCheckBoxes([...selectedCheckBoxes, p.title]);
}
console.log(p);
}}
/>
))}
The value parameter is the event object.
(event) => {
const value = event.target.checked
<... Rest of the code ...>
}
I have list items represent names, when clicking any name it turns red then take one second to return black again, but clicking two identical names consecutively make them keep red color, not turning black again
you can imagine it as a memory game, but i tried to make a simple example here of what i am trying to achieve in the original project
This is my code and my wrong trial:
const App = () => {
const { useState } = React;
const items = [
{
name: 'mark',
id: 1,
red: false
},
{
name: 'peter',
id: 2,
red: false
},
{
name: 'john',
id: 3,
red: false
},
{
name: 'mark',
id: 4,
red: false,
},
{
name: 'peter',
id: 5,
red: false
},
{
name: 'john',
id: 6,
red: false
}
];
const [names, setNames] = useState(items);
const [firstName, setFirstName] = useState(null);
const [secondName, setSecondName] = useState(null)
const handleItemClick = (item) => {
setNames(prev => prev.map(i => i.id === item.id ? { ...i, red: true } : i));
//the problem is here
setTimeout(() => {
setNames(prev => prev.map(n => {
if (secondName && (secondName.name === firstName.name) && n.name === firstName.name) {
return { ...n,red: true }
}
return { ...n, red: false };
}))
}, 1000)
if (!firstName) setFirstName(item);
else if (firstName && !secondName) setSecondName(item)
else if (firstName && secondName) {
setFirstName(item);
setSecondName(null)
}
}
return (
<div class="app">
<ul class="items">
{
names.map(i => {
return (
<Item
item={i}
handleItemClick={handleItemClick}
/>
)
})
}
</ul>
</div>
)
}
const Item = ({ item, ...props }) => {
const { id, name, red } = item;
const { handleItemClick } = props;
return (
<li
className={`${red ? 'red' : ''}`}
onClick={() => handleItemClick(item)}
>
{name}
</li>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
But this code doesn't work correctly, when clicking two identical names consecutively they don't keep red color and turning black again
To me it seems the issue is overloading the event handler and violating the Single Responsibility Principle.
The handler should be responsible for handling the click event and nothing else. In this case, when the element is clicked you want to add the id to the state of selected/picked names, and toggle the red state value of item with matching id. Factor the timeout effect into (strangely enough) an useEffect hook, with the picks as dependencies. This inverts the logic of the timeout to clearing/resetting the state versus setting what is "red" or not. You can/should also move any logic of determining matches into this same effect (since it already has the dependencies anyway).
useEffect(() => {
... logic to determine matches
const timer = setTimeout(() => {
// time expired, reset only if two names selected
if (firstName && secondName) {
setFirstName(null);
setSecondName(null);
setNames(names => names.map(name => ({ ...name, red: false })));
}
}, 1000);
// clean up old timeout when state updates, i.e. new selected
return () => clearTimeout(timer);
}, [firstName, secondName]);
This will allow you to simplify your name setting logic to
if (!firstName) {
setFirstName(item);
} else {
setSecondName(item);
}
Note: I believe you need another data structure to hold/track/store existing matches made by the user.
How this works:
Starting from clean state, no names are chosen
When first name is picked, firstName is null and updated, red state updated
Timeout is set (but won't clear state yet)
When second name is picked, firstName is defined, so secondName is updated, red state updated
If match, add match to state (to keep red)
Timeout expire and reset state (go back to step 1)
The following is how I'd try to simplify state a bit more, using an array of selected ids that only update if the selected id isn't already chosen and 2 picks haven't been chosen yet.
const App = () => {
const [names, setNames] = useState(items);
const [picks, setPicks] = useState([]);
const [matched, setMatched] = useState({});
/**
* On click event, add id to `picks` array, allow only two picks
*/
const onClickHandler = id => () =>
picks.length !== 2 &&
!picks.includes(id) &&
setPicks(picks => [...picks, id]);
/**
* Effect to toggle red state if id is included in current picks
*/
useEffect(() => {
setNames(names =>
names.map(name => ({
...name,
red: picks.includes(name.id)
}))
);
}, [picks]);
/**
* Effect checks for name match, if a match is found it is added to the
* `matched` array.
*/
useEffect(() => {
// matches example: { mark: 1, peter: 0, john: 0 }
const matches = names.reduce((matches, { name, red }) => {
if (!matches[name]) matches[name] = 0;
red && matches[name]++;
return matches;
}, {});
const match = Object.entries(matches).find(([_, count]) => count === 2);
if (match) {
const [matchedName] = match;
setMatched(matched => ({ ...matched, [matchedName]: matchedName }));
}
const timer = setTimeout(() => {
if (picks.length === 2) {
setPicks([]);
setNames(names => names.map(name => ({ ...name, red: false })));
}
}, 1000);
return () => clearTimeout(timer);
}, [names, picks]);
return (
<div className="App">
<ul>
{names.map(item => (
<Item
key={item.id}
item={item}
matches={matched}
onClick={onClickHandler(item.id)}
/>
))}
</ul>
</div>
);
};
const Item = ({ item, matches, ...props }) => {
const { name, red } = item;
return (
<li
className={classnames({
red: red || matches[name], // for red text color
matched: matches[name] // any other style to make matches stand out
})}
{...props}
>
{name}
</li>
);
};
Here i have three filters on selection of which i need to filter data in a table.
I am using if else statement to check and filter the data , hence i want to modify the code in some modular way to achieve the same can any one suggest me , should i go with switch case ?
if (mapFilter === 'Mapped') {
if (listFilter) {
const result = fullData.filter(
data =>
data.partner_mapping_classification.length > 0 &&
data.account === listFilter,
);
setFinalData(result);
} else {
const result = fullData.filter(
data => data.partner_mapping_classification.length > 0,
);
setFinalData(result);
}
} else if (mapFilter === 'Not Mapped') {
if (listFilter) {
const result = fullData.filter(
data =>
data.partner_mapping_classification === '' &&
data.account === listFilter,
);
setFinalData(result);
} else {
const result = fullData.filter(
data => data.partner_mapping_classification === '',
);
setFinalData(result);
}
} else if (mapFilter === 'All') {
if (listFilter) {
const result = fullData.filter(
data => data.account === listFilter,
);
setFinalData(result);
} else {
const result = fullData.filter(
data => data.partner_mapping_classification.length > 0,
);
setFinalData(result);
}
} else if (mapFilter === '' && listFilter !== '') {
const result = fullData.filter(
data => data.account === listFilter,
);
setFinalData(result);
} else if (mapFilter === '' && listFilter === '') {
setFinalData([]);
} else {
setFinalData([]);
}
};
Easy to scale method (followed by live-demo)
Using switch statements or multiple chained if( statements (or, even, multiple conditions within same if( statement) doesn't seem to be a good idea, as scaling and maintaining such code will become way too difficult.
As the opposite to above mentioned hardcoding techniques, I would suggest to have an object within your table component's state that will bind object properties (you wish your table entries to get filtered by) to keywords (attached to your inputs).
Assuming (based on your screenshot) you use MaterialUI for styling your components, following example would demonstrate above approach:
const { useState } = React,
{ render } = ReactDOM,
{ Container, TextField, TableContainer, Table, TableHead, TableBody, TableRow, TableCell } = MaterialUI,
rootNode = document.getElementById('root')
const sampleData = [
{id: 0, name: 'apple', category: 'fruit', color: 'green'},
{id: 1, name: 'pear', category: 'fruit', color: 'green'},
{id: 2, name: 'banana', category: 'fruit', color: 'yellow'},
{id: 3, name: 'carrot', category: 'vegie', color: 'red'},
{id: 4, name: 'strawberry', category: 'berry', color: 'red'}
],
sampleColumns = [
{id: 0, property: 'name', columnLabel: 'Item Name'},
{id: 1, property: 'category', columnLabel: 'Category'},
{id: 2, property: 'color', columnLabel: 'Item Color'}
]
const MyFilter = ({filterProperties, onFilter}) => (
<Container>
{
filterProperties.map(({property,id}) => (
<TextField
key={id}
label={property}
name={property}
onKeyUp={onFilter}
/>
))
}
</Container>
)
const MyTable = ({tableData, tableColumns}) => (
<TableContainer>
<Table>
<TableHead>
<TableRow>
{
tableColumns.map(({id, columnLabel}) => (
<TableCell key={id}>
{columnLabel}
</TableCell>
))
}
</TableRow>
</TableHead>
<TableBody>
{
tableData.map(row => (
<TableRow key={row.id}>
{
tableColumns.map(({id, property}) => (
<TableCell key={id}>
{row[property]}
</TableCell>
))
}
</TableRow>
))
}
</TableBody>
</Table>
</TableContainer>
)
const App = () => {
const [state, setState] = useState({
data: sampleData,
columns: sampleColumns,
filterObj: sampleColumns.reduce((r,{property}) => (r[property]='', r), {})
}),
onFilterApply = ({target:{name,value}}) => {
const newFilterObj = {...state.filterObj, [name]: value}
setState({
...state,
filterObj: newFilterObj,
data: sampleData.filter(props =>
Object
.entries(newFilterObj)
.every(([key,val]) =>
!val.length ||
props[key].toLowerCase().includes(val.toLowerCase()))
)
})
}
return (
<Container>
<MyFilter
filterProperties={state.columns}
onFilter={onFilterApply}
/>
<MyTable
tableData={state.data}
tableColumns={state.columns}
/>
</Container>
)
}
render (
<App />,
rootNode
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><script src="https://unpkg.com/#material-ui/core#latest/umd/material-ui.development.js"></script><div id="root"></div>
As Sudhanshu pointed out, you should create event listeners for all these select dropdowns and then update state based on that.
I created a small sample of how I would do it, but just be warned that this isn't tested and I just wrote it without actually running the code or anything. So it is buggy for sure in some regard.
const fullData = ['first', 'second', 'third'];
const BigFilter = () => {
const [activeFilters, setActiveFilters] = useState([]);
const [filteredValues, setFilteredValues] = useState([]);
const handleFilterChange = (event) => {
const { target } = event;
const isInFilter = activeFilters.some((element) => element.name === target.name);
if (!isInFilter) {
setActiveFilters((currentState) => {
return [...currentState, { name: target.name, value: target.value }];
});
} else {
setActiveFilters((currentState) => {
return [...currentState.filter((x) => x.name !== target.name), { name: target.name, value: target.value }];
});
}
};
useEffect(() => {
// Just set full data as filtered values if no filter is active
if (activeFilters.length === 0) {
setFilteredValues([...fullData]);
return;
};
let finalData = [...fullData];
// Returns undefined if it cannot find the element with .name === 'list' in array, otherwise it will return that element
const listData = activeFilters.find((element) => (element.name = 'list'));
if (listData) {
// Do some filtering for first select/dropdown
const { value } = listData;
// value is the value of your select dropdown that was selected
finalData = finalData.filter((x) => x.something > 0);
}
// Returns undefined if it cannot find the element with .name === 'list' in array, otherwise it will return that element
const statusData = activeFilters.find((element) => (element.name = 'status'));
if (statusData) {
// Do some filtering for second select/dropdown
const { value } = statusData;
// value is the value of your select dropdown that was selected
finalData = finalData.filter((x) => x.something > 0);
}
// Returns undefined if it cannot find the element with .name === 'list' in array, otherwise it will return that element
const amountData = activeFilters.find((element) => (element.name = 'amount'));
if (amountData) {
// Do some filtering for third select/dropdown
const { value } = amountData;
// value is the value of your select dropdown that was selected
finalData = finalData.filter((x) => x.something > 0);
}
setFilteredValues(finalData);
// You can go with multiple if statements to filter everything step by step
}, [activeFilters]);
return (
<>
<select name="list" onChange={handleFilterChange}>
<option>List Option 1</option>
</select>
<select name="status" onChange={handleFilterChange}>
<option>Status Option 1</option>
</select>
<select name="amount" onChange={handleFilterChange}>
<option>Amount Option 1</option>
</select>
<div>
{/* Render filtered values */}
{filteredValues.map((singleValue) => singleValue.name)}
</div>
</>
);
};
The basic idea here is that all your <select> elements react to the same event listener, making it easier to coordinate.
You got two basic arrays as state (activeFilters and filteredValues). When onChange handler is triggered, you check the name of the filter and check if that filter is already present in your activeFilters state. If it isn't, you add its name and value to that state. That's why I used name="some name" on each <select> in order to identify it somehow. In other case, if the filter is already present in that state, we remove it and just add its entry again with the new value. (This can probably be written way better, but it's just to give you an idea.)
Both of these cases set new state for active filters with setActiveFilter. Then we have the useEffect hook below which filters all the data based on active filters. As you can see it has that dependency array as a second argument and I added activeFilters variable to it so that every time activeFilters updates it will trigger all the logic in useEffect and it will change your filteredValues.
The logic in useEffect will go step by step and check if each filter is active and filter data for each of them if they are active step by step. If the first filter is active it will filter data that's needed and store it again in finalData and then it will go to the second if statement and if the filter for that is active it will perform another filter, but now on already filtered data. In the end, you should get data that passes through all active filters. I'm sure there's a better way of doing this, but it's a start.
Btw, usually I wouldn't do this
finalData = finalData.filter((x) => x.something > 0);
Re-assigning the same variable with filtered data from it, but I would say it's ok in this case since that finalData variable was created in that useEffect scope and it cannot be mutated from outside the scope. So it's easy to track what it is doing.
I'm sorry if this doesn't work, but it might guide you to your solution.
You can add a filter to the fullData array and provide the value of each of the dropdowns to the filter function
fullData.filter(element => {
return element.account == first && element.account == second && element.account == third;
});
You can also put in checks for the filters, like if the value is just '' then return false i.e return the whole array else return the filtered list