React hooks one checkbox checked to uncheck other checkbox - react-hooks

Hi I have a parent component which contains a bunch child components which are checkboxes.
parent component is something like this inside:
const [items, setItems] = useState([
{
id:1,
selected: false,
},
{
id:2,
selected: false,
}
]);
const changeSelected = (id) =>
{
items.forEach((item)=>
{
if (item.id === id)
{
item.selected = !item.selected;
}
else{
item.selected = false;
}
})
}
return(
<div>
{items.map((item)=>{
<Child item={item} changeSelected={changeSelected}/>
})}
</div>
)
and in the child component, it has something like this inside:
return(
<div>
<input type="checkbox" checked={props.item.selected} onChange={()=>{props.changeSelected(props.item.id)}} />
</div>
)
I know partially this isnt working is because useState is async but I dont know what to do to make it work, or if I should try a different approach? Thank you

You can refactor your function to this:
const changeSelected = (id) => {
setItems(prev => ([...prev, {id, selected: !prev.filter(x => x.id === id)[0].selected}]))
}

Obviously I forgot to setItem according to the two answers I received. But I think the right way to do it is to make a deep copy of my existing items, and setItems again. For my future reference, here is what I have now working (an example):
let newItems = [...items];
newItems.forEach((item)=>
{
if (item.id === id)
{
item.selected = !item.selected;
}
else{
item.selected = false;
}
})
setProducts(newItems);

You are not updating the state anywhere. You should make a copy of the array and then change the object you want to:
const changeSelected = (id) =>
{ let newItems = [];
items.forEach((item)=>
{
if (item.id === id)
{
newItems.push({ id : item.id , selected : item.selected });
}
else{
newItems.push({ id : item.id , selected : false });
}
})
setItems(newItems);
}
Call setItems to set it.
Note:
Not related to the question but you should use unique keys when iterating over list.
{items.map((item)=>{
<Child key={item.id} item={item} changeSelected={changeSelected}/>
})}

Related

How to delete data using Vue js?

I'm trying delete data but I'm getting this error:
this.jobPosts.filter is not a function
PostJobIndex.vue file:
deleteJobPost: async function(jobPost) {
if (!window.confirm('Are you sure you want to delete this Job Post?')) {
return;
}
try {
await employerService.deleteJobPost(jobPost.id);
this.jobPosts = this.jobPosts.filter(obj => {
return obj.id != jobPost.id;
});
console.log(this.jobPosts);
this.$toast.success("Job Post deleted Successfully!");
} catch (error) {
console.log(error);
this.$toast.error(error.response.data.message);
}
},
I had this same issue with my Update method and I beleive it was because I was trying to map through an object or something instead of an array. In the end I used Object.keys(this.jobPosts).map for my update method and it worked:
Object.keys(this.jobPosts).map(jobPost => {
if (jobPost.id == response.data.id) {
for (let key in response.data) {
jobPost[key] = response.data[key];
}
}
});
But when I do this for Update it doesn't work:
this.jobPosts = Object.keys(this.jobPosts).filter(obj => {
return obj.id != jobPost.id;
});
UPDATED
Here is the code for loading the job posts:
loadJobPosts: async function() {
try {
const response = await employerService.loadJobPosts();
this.jobPosts = response.data;
console.log(this.jobPosts);
} catch (error) {
this.$toast.error('Some error occurred, please refresh!');
}
},
Im using Vuex for state management and I'm using services, that simply contain the axios http requests. That's where this line comes from employerService.loadJobPosts() loadJobPosts() is a function inside my employerService.js file.
I'm also using Laravel for my back end. Here is my JobPostsController.php file:
public function index()
{
$jobPosts = JobPost::all()->where('user_id', Auth::user()->id);
return response()->json($jobPosts, 200);
}
From what I've understood from your code,
this should work for removing jobPost from jobPosts
this.jobPosts = this.jobPosts.filter(obj => {
return obj.id != jobPost.id;
});
I don't know what you're expecting this to do, but it won't do anything useful and will either error or return false for everything.
this.jobPosts = Object.keys(this.jobPosts).filter(obj => {
return obj.id != jobPost.id;
});
filter exists on array types, so I would check where it's getting set and make sure it's an array.
I've included a small snippet in case it's any help.
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: "#app",
data: () => {
return {
jobPosts: [],
deleteJobId: 1
};
},
methods: {
getJobPosts() {
this.jobPosts = [{
id: 1
}, {
id: 2
}, {
id: 3
}, {
id: 4
}, {
id: 5
}];
},
deleteJob() {
if (!this.deleteJobId)
return;
this.jobPosts = this.jobPosts.filter(x => x.id !== this.deleteJobId);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button type="button" #click="getJobPosts">Get Jobs</button>
<div>
<button type="button" #click="deleteJob">Delete Job #</button>
<input type="number" v-model.number="deleteJobId" />
</div>
<ul>
<li v-for="jobPost in jobPosts">
Job Post #{{jobPost.id}}
</li>
</ul>
</div>
You have already answered your own question:
in my data() object, I have this jobPosts: [], but in the console it says Object
As for your second question:
I don't know how to return the data as an array
There are similiar topics here on SO.
I am not familiar with Laravel but assuming you have an eloquent model with JobPost in your index-function according to the docs you should use the .toArray-method:
$jobPosts = JobPost::all()->where('user_id', Auth::user()->id).toArray();
When working with plain collections the values method should do the trick of returning an array instead of an object:
$collection = collect([
10 => ['product' => 'Desk', 'price' => 200],
11 => ['product' => 'Desk', 'price' => 200]
]);
$values = $collection->values();
$values->all();
UPDATE
I just realized that your result is just a stringified JSON object that needs to be converted into an array. Just parse it before processing (take out the JSON.parse(...) if you are already taking care of it in your service), return the object properties as an array and you are good to go:)
this.jobPosts = Object.values(JSON.parse(this.jobPosts)).filter(obj => {
return obj.id != jobPost.id;
});

UI Flickers when I drag and drop and item

I have a problem getting react-beautiful-dnd to work without flickering. I have followed the example in the egghead course. Here is my code sample.
Item List Container
onDragEnd = (result) => {
if (this.droppedOutsideList(result) || this.droppedOnSamePosition(result)) {
return;
}
this.props.itemStore.reorderItem(result);
}
droppedOnSamePosition = ({ destination, source }) => destination.droppableId
=== source.droppableId && destination.index === source.index;
droppedOutsideList = result => !result.destination;
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<div>
{this.props.categories.map((category, index) => (
<ListCategory
key={index}
category={category}
droppableId={category._id}
/>
))}
</div>
</DragDropContext>
);
}
Item Category
const ListCategory = ({
category, droppableId,
}) => (
<Droppable droppableId={String(droppableId)}>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
<ListTitle
title={category.name}
/>
<ListItems category={category} show={category.items && showIndexes} />
{provided.placeholder}
</div>
)}
</Droppable>
);
List items
<Fragment>
{category.items.map((item, index) => (
<ListItem
key={index}
item={item}
index={index}
/>
))}
</Fragment>
Items
render() {
const {
item, index, categoryIndex, itemStore,
} = this.props;
return (
<Draggable key={index} draggableId={item._id} index={index}>
{(provided, snapshot) => (
<div
role="presentation"
className={cx({
'list-item-container': true,
'selected-list-item': this.isSelectedListItem(item._id),
})}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
onClick={this.handleItemClick}
>
<div className={cx('select-title')}>
<p className={cx('list-item-name')}>{item.title}</p>
</div>
{capitalize(item.importance)}
</div>
</div>
)}
</Draggable>
);
}
Method to reorder Items (I'm using Mobx-State_Tree)
reorderItem: flow(function* reorderItem(result) {
const { source, destination } = result;
const categorySnapshot = getSnapshot(self.itemCategories);
const sourceCatIndex = self.itemCategories
.findIndex(category => category._id === source.droppableId);
const destinationCatIndex = self.itemCategories
.findIndex(category => category._id === destination.droppableId);
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
if (sourceCatIndex === destinationCatIndex) {
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
try {
yield itemService.bulkEditPriorities(prioritizedItems);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
} else {
const destinationCatItems = Array.from(categorySnapshot[destinationCatIndex].items);
destinationCatItems.splice(destination.index, 0, draggedItem);
const prioritizedSourceItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedSourceItems);
const prioritizedDestItems = setItemPriorities(destinationCatItems);
applySnapshot(self.itemCategories[destinationCatIndex].items, prioritizedDestItems);
try {
const sourceCatId = categorySnapshot[sourceCatIndex]._id;
const originalItemId = categorySnapshot[sourceCatIndex].items[source.index]._id;
yield itemService.moveItemToNewCategory(originalItemId, sourceCatId, destinationCatIndex);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
}
}),
Sample data
const itemData = [
{
_id: 'category-1',
title: 'Backlog',
items: [
{ _id: 'item-1', title: 'Here and back again' },
},
{
_id: 'category-2',
title: 'In progress',
items: []
},
{
_id: 'category-3',
title: 'Done',
items: []
}
}
}
Summary
When and item is dragged and dropped, I check to see if the item is dropped in the outside the dnd context or in the same position it was dragged from. If true, i do nothing.
If the item is dropped within the context, i check to see if it was dropped in the same category. if true, i remove the item from its current position, put it in the target position, update my state, and make an API call.
If it was dropped in a different category, i remove the item from the source category, add to the new category, update the state and make an API call.
Am I missing something?
I am using both mst and the react-beautiful-dnd library
I will just paste my onDragEnd action method
onDragEnd(result: DropResult) {
const { source, destination } = result;
// dropped outside the list
if (!destination) {
return;
}
if (source.droppableId === destination.droppableId) {
(self as any).reorder(source.index, destination.index);
}
},
reorder(source: number, destination: number) {
const tempLayout = [...self.layout];
const toMove = tempLayout.splice(source, 1);
const item = toMove.pop();
tempLayout.splice(destination + lockedCount, 0, item);
self.layout = cast(tempLayout);
},
I think in order to avoid the flicker you need to avoid using applySnapshot
You can replace this logic
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
just splice the items tree
const [draggedItem] = categorySnapshot[sourceCatIndex].items.splice(destination.index, 0, draggedItem)
this way you don't need to applySnapshot on the source items after
I believe this issue is caused by multiple dispatches happening at the same time.
There're couple of things going on at the same time. The big category of stuff is going on is the events related to onDragStart, onDragEnd and onDrop. Because that's where an indicator has to show to the user they are dragging and which item they are dragging from and to.
So especially you need to put a timeout to onDragStart.
const invoke = (fn: any) => { setTimeout(fn, 0) }
Because Chrome and other browser will cancel the action if you don't do that. However that is also the key to prevent flickery.
const DndItem = memo(({ children, index, onItemDrop }: DndItemProps) => {
const [dragging, setDragging] = useState(false)
const [dropping, setDropping] = useState(false)
const dragRef = useRef(null)
const lastEnteredEl = useRef(null)
const onDragStart = useCallback((e: DragEvent) => {
const el: HTMLElement = dragRef.current
if (!el || (
document.elementFromPoint(e.clientX, e.clientY) !== el
)) {
e.preventDefault()
return
}
e.dataTransfer.setData("index", `${index}`)
invoke(() => { setDragging(true) })
}, [setDragging])
const onDragEnd = useCallback(() => {
invoke(() => { setDragging(false) })
}, [setDragging])
const onDrop = useCallback((e: any) => {
invoke(() => { setDropping(false) })
const from = parseInt(e.dataTransfer.getData("index"))
onItemDrop && onItemDrop(from, index)
}, [setDropping, onItemDrop])
const onDragEnter = useCallback((e: DragEvent) => {
lastEnteredEl.current = e.target
e.preventDefault()
e.stopPropagation()
setDropping(true)
}, [setDropping])
const onDragLeave = useCallback((e: DragEvent) => {
if (lastEnteredEl.current !== e.target) {
return
}
e.preventDefault()
e.stopPropagation()
setDropping(false)
}, [setDropping])
return (
<DndItemStyle
draggable="true"
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
dragging={dragging}
dropping={dropping}
>
{(index < 100) && (
cloneElement(children as ReactElement<any>, { dragRef })
)}
</DndItemStyle>
)
})
I have to apply two more timeout invoke in the above DndItem, the reason for that is during the drop, there're two many events are competing with each other, to name a few
onDragEnd, to sugar code the indicator
onDrop, to re-order
I need to make sure re-order happens very quickly. Because otherwise you get double render, one with the previous data, and one with the next data. And that's why the flickery is about.
In short, React + Dnd needs to apply setTimeout so that the order of the paint can be adjusted to get the best result.

React Search Filter of Objects not filtering

I'm trying to create a search filter that will filter through facility names that lives in an array of objects.If I hard code an array into the state the filter works, but I need it to drab the info from props. The filtered list is being generated and showing all of the names on the screen but when I type it the textbox to filter nothing happens. What have I overlooked?
class FacilitySearch extends React.Component {
constructor(props) {
super(props);
this.state = {
search: ""
};
}
componentDidMount() {
this.props.dispatch(actions.getFacilitiesList());
}
//The subsr limits the # of characters a user can enter into the seach box
updateSearch = event => {
this.setState({ search: event.target.value.substr(0, 10) });
};
render() {
if (!this.props.facilityList) {
return <div>Loading...</div>
}
let filteredList = this.props.facilityList;
filteredList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
return (
<div>
<input
type="text"
value={this.state.search}
onChange={this.updateSearch.bind(this)}
placeholder="Enter Text Here..."
/>
<ul>
{filteredList.map(facility => {
return <li key={facility.generalIdPk}>{facility.facilityName}</li>;
})}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
facilityList: state.facilityList.facilityList
});
export default connect(mapStateToProps)(FacilitySearch)
The problem is that you are not storing the return value of filter in any variable.
You should do something like:
let filteredList = this.props.facilityList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
From MDN:
The filter() method creates a new array with all elements that pass the test implemented by the provided function.

Redux reducer correct way to check if item already exists in state

I'm a little uncertain of this approach when updating an existing or adding a new object to a redux store but am having trouble getting this to work using the accepted methods i.e. Object.assign, update() or spread operators. I can get it working as follows:
const initialState = {
cart: []
}
export default function cartReducer(state = initialState, action) {
switch (action.type) {
case ADD_TO_CART:
let copy = _.clone(state.cart);
let cartitem = _.find(copy, function (item) {
return item.productId === action.payload.productId;
});
if (cartitem) {
cartitem.qty = action.payload.qty;
} else {
copy.push(action.payload);
}
return {
...state,
cart: copy
}
default:
return state
}
}
Although this works, I'm using Underscore to copy the state and check whether the item already exists in state which seems unnecessary and overkill?
This is the code for the redux. Use the .find function to find if an element is already in the array.
Example Code:
const inCart = state.cart.find((item) =>
item.id === action.payload.id ? true : false
);
return {
...state,
cart: inCart
? state.cart.map((item) =>
item.id === action.payload.id
? { ...item, qty: item.qty + 1 }
: item
)
: [...state.cart, { ...item, qty: 1 }],
};
With Redux-Toolkit you can mutate the state objects so you can likely simplify this a bit.
const basket = createSlice({
name: "cart",
initialState,
reducers: {
addToCart: (state, { payload }) => {
const inCart= state.cart.find((item) => item.id === payload.id);
if (inCart) {
item.qty += payload.qty;
} else {
state.items.push(payload);
}
},
},
});

Rendering an object stored in state

I'm trying to render out my calendar's event summaries, using a .map function. I've stored my calendar events object in state, but can't find a way to .map out the different event summaries. Any suggestions?
export default class Container extends React.Component{
calendarID="xxx"
apiKey="zzz";
state = { events: [] };
setEvents = (a) => {
this.setState(a);
}
componentDidMount() {
ajax.get(`https://www.googleapis.com/calendar/v3/calendars/${this.calendarID}/events?fields=items(summary,id,location,start)&key=${this.apiKey}`)
.end((error, response) => {
if(!error && response ) {
this.setEvents({events: response.body});
console.log("success");
console.log(this.state.events);
} else {
console.log("Errors: ", error);
}
});
}
render(){
let lista = this.state.events;
let arr = Object.keys(lista).map(key => lista[key])
return (
<div class = "container">
{arr.map((event, index) => {
const summary = event.summary;
return (<div key={index}>{summary}</div>);
})}
</div>
);
}
}
EDIT:
Thanks for your answers! This is the data that the ajax call returns when I console log this.state.items:
Object {items: Array[1]}
items: Array[1]
0: Object
id: "cmkgsrcohfebl5isa79034h8a4"
start: Object
summary: "Stuff going down"
If I skip the ajax call and create my own state, the mapping works:
state = { items: [
{ items: { summary: "testing"} },
{ items: { summary: "12"} },
{ items: { summary: "3"} }
]};
To get this working, however, I change my render-function to:
render(){
let lista = this.state.items;
let arr = Object.keys(lista).map(key => lista[key])
return (
<div class = "container">
{arr.map((item, index) => {
const summary = item.items.summary;
return (<div key={index}>{summary}</div>);
})}
</div>
);
}
So maybe it has something to do with the object that this.state.items returns from the ajax call?
Edit2: #Andrea Korinski, you were right! I changed my render function to this, and now it works:
render(){
let list = this.state.items;
const arr = (list.items || []).map((item, index) => {
const summary = item.summary;
return (<div key={index}>{summary}</div>);
});
return (
<div class = "container">
{arr}
</div>
);
}
}
The whole component:
export default class Container extends React.Component{
calendarID="xxx";
apiKey="zzz";
state = {items: []};
setEvents = (a) => {
this.setState(a);
}
componentDidMount() {
ajax.get(`https://www.googleapis.com/calendar/v3/calendars/${this.calendarID}/events?fields=items(summary,id,location,start)&key=${this.apiKey}`)
.end((error, response) => {
if(!error && response ) {
this.setEvents({items: response.body});
console.log("success");
console.log(this.state.items);
} else {
console.log("Errors: ", error);
}
});
}
render(){
let list = this.state.items;
const irr = (list.items || []).map((item, index) => {
const summary = item.summary;
return (<div key={index}>{summary}</div>);
});
return (
<div class = "container">
{irr}
</div>
);
}
}

Resources