Keep checkbox state across pagination vue-bootstrap - checkbox

I am using a bootstrap table using pagination to display users. On every row a checkbox for selecting more than one at a time to perform an action.
I manage to write the functions to select one and select all, keeping the array across pagination.
But now I am not able to control the ui while navigating across pages using pagination. As I said, the array I created for storing the selected users stays across pages, but I 'loose the tick' on the checkboxes... It looks like the v-model binding gets lost while surfing the pages...
Here the inherent parts of the code, hoping someone could help.
In the template:
<b-table
v-if="users && users.length > 0 && !isLoading"
id="table-transition-userList"
:key="users.id"
responsive
:tbody-transition-props="transProps"
:fields="fields"
:items="users"
hover
class="mx-2 py-1 tbl-user-list"
:per-page="perPage"
:filter="filter"
:filterIncludedFields="filterOn"
#filtered="onFiltered"
>
<template
v-slot:head(checkbox)="data"
>
<b-form-checkbox
size="lg"
v-model="isSelectedAll"
#change="selectAll(data)"
/>
</template>
<template
v-slot:cell(checkbox)="data"
>
<b-form-checkbox
v-model="data.item.selected"
#change="select(data.item)"
/>
</template>
In the script data:
fields: [
{ key: 'checkbox', label: ''},
{ key: 'fullName', label: 'Utente' },
{ key: 'user.username', label: 'Username' },
{ key: 'user.email', label: 'Email' },
{ key: 'buttons', label: 'Operazioni' }
],
totalRows: 1,
currentPage: 1,
perPage: 5,
pageOptions: [5, 10, 15, 20, 25],
filter: null,
filterOn: [],
users: [],
selectedUsers: [],
isSelectedAll: false
In the methods:
selectAll() {
this.isSelectedAll = !this.isSelectedAll
this.users.forEach(user => user.selected = this.isSelectedAll)
const notSelected = this.users.filter(user => !user.selected)
const selected = this.users.filter(user => user.selected)
// new selected users array without current page selected users
let selectedUsersCopy = this.selectedUsers.slice().filter(user =>
!notSelected.find(e => e.id === user.id)
)
if(notSelected.length > 0) {
this.isSelectedAll = true
}
else {
this.isSelectedAll = false
}
this.selectedUsers = [
...selectedUsersCopy,
...selected.filter(user => !selectedUsersCopy.find(e => e.id === user.id))
]
},
select(user) {
user.selected = !user.selected
this.isSelectedAll = false
const selected = this.users.filter(user => user.selected)
if(selected.length === this.users.length) {
this.isSelectedAll = true
}
else {
this.isSelectedAll = false
}
let isDouble = false
if(this.selectedUsers.find(v => v.user.id === user.id)) isDouble = true
if(user.selected) {
if(isDouble) {
console.log('double if user selected and isDouble', isDouble)
console.log("object already exists", this.selectedUsers)
return
}
else {
this.selectedUsers.push(user)
return this.selectedUsers
}
}
else {
const index = this.selectedUsers.indexOf(user);
this.selectedUsers.splice(index, 1);
console.log("removed, new array: ", this.selectedUsers)
}
},
async pagination(page) {
const payload = {
limit: this.perPage,
offset: (page - 1) * this.perPage
}
this.isLoading = true
this.isSelectedAll = false
await this.$store.dispatch('user/setUsers', payload)
const response = this.$store.getters.users
this.users = response.results
this.isLoading = false
this.currentPage = page
}
My pagination is calling one api with different limits and offsets.
I think the problem is to be found in the built-in checked property of the form checkbox though...
Thanks to anyone who could give me a hint.
xx

Is your store dispatch event doing an axios call to fetch the page of data? Then the row's checkbox state will be what ever is returned from the call to the remote server (in which case it is being reset).
Once you receive your paged data, you need to iterate over it and set the selected value to true or false, based on the existence in your selectedUsers array, before passing it to <b-table>
async pagination(page) {
const payload = {
limit: this.perPage,
offset: (page - 1) * this.perPage
}
this.isLoading = true
this.isSelectedAll = false
await this.$store.dispatch('user/setUsers', payload)
const response = this.$store.getters.users
// Restore the selected state before passing the array to b-table
this.users = response.results.map(user => {
// if the user is found in selectedUsers array, then set
// set the selected state to `true`, otherwise `false`
// to restore the selected state
user.selected = !!this.selectedUsers.find(u => u.id === user.id)
return user
})
this.isLoading = false
this.currentPage = page
}
I believe you may have an error in this line as well:
if (this.selectedUsers.find(v => v.user.id === user.id)) isDouble = true
I am thinking it should be:
if (this.selectedUsers.find(u => u.id === user.id)) isDouble = true

Related

this.setState isn't making changes in state

I am using functions that change a value in a nested object in the state :
an I am calling those functions in a button , they are executed when I click on that button , but one of those functions doesn't make changes to the state
This is the state :
state = {
data: {
attributesLength: this.props.product.attributes.length,
modalMessage: "",
isOpen: false,
},
};
and these are the functions :
addToCart = (id) => {
let data = { ...this.state.data };
if (Object.keys(this.state).length === 1) {
data.modalMessage = "Please, select product attributes";
this.setState({ data});
return;
}
if (
Object.keys(this.state).length - 1 ===
this.state.data.attributesLength
) {
const attributes = Object.entries(this.state).filter(
([key, value]) => key !== "data"
);
if (this.props.cartProducts.length === 0) {
this.props.addItem({
id: id,
quantity: 1,
attributes: Object.fromEntries(attributes),
});
data.modalMessage = "Added to cart !";
this.setState({ data });
return;
}
const product = this.props.cartProducts.filter((item) => item.id === id);
if (product.length === 0) {
this.props.addItem({
id: id,
quantity: 1,
attributes: Object.fromEntries(attributes),
});
data.modalMessage = "Added to cart !";
this.setState({ data });
return;
}
if (product.length !== 0) {
this.props.changeQuantity({ id: id, case: "increase" });
data.modalMessage = "Quantity increased !";
this.setState({ data });
return;
}
if (this.state.data.attributesLength === 0) {
this.props.addItem({
id: id,
quantity: 1,
attributes: Object.fromEntries(attributes),
});
data.modalMessage = "Added to cart !";
this.setState({ data });
return;
}
} else {
data.modalMessage = 'please, select "ALL" product attributes!';
this.setState({ data });
}
};
changeModalBoolean = () => {
let data = { ...this.state.data };
data.isOpen = !data.isOpen;
this.setState({ data });
};
and this is where I am calling functions :
<button
className={product.inStock ? null : "disabled"}
disabled={product.inStock ? false : true}
onClick={() => {
this.addToCart(product.id);
this.changeModalBoolean();
}}
>
{product.inStock ? "add to cart" : "out of stock"}
</button>
NOTE
changeModalBoolean function works and change state isOpen value,
this.addToCart(product.id);
this.changeModalBoolean();
This code run synchronously one after the other. In every function, you create a copy of previous state let data = { ...this.state.data };
so the this.changeModalBoolean(); just replace state which you set in this.addToCart(product.id); to fix this problem, use this.setState((state) => /*modify state*/)
changeModalBoolean = () => {
this.setState((state) => {
let data = { ...state.data };
data.isOpen = !data.isOpen;
return { data };
})
};
or modify the same object in both functions

Why the table data is not updated immediately after performing an action for the table

I have a table that has an action to delete..like this:
const deleteRow = (row) => {
let indexOfDeleted = -1;
let data = tableData;
data.forEach((item, index) => {
if (item.instrumentId === row.instrumentId) {
indexOfDeleted = index;
}
})
data.splice(indexOfDeleted, 1);
setTableData(data)
};
The data is deleted but I have to refresh it so that it is not displayed in the table.It does not seem to be rerender. What should I do?
for table:
const schema = {
columns: [
{
field: "persianCode",
title: "title",
},
],
operations: [
{
title: "delete",
icon: (
<DeleteIcon
className={clsx(classes.operationsIcon, classes.deleteIcon)}
/>
),
action: (row) => deleteRow(row),
tooltipColor: theme.palette.color.red,
}
],
};
You are mutating the state variable, in your deleteRow function. You should update the state with a copied array:
const deleteRow = (row) => {
setTableData(table => table.filter(data => data.instrumentId !== row.instrumentId))
};
Instead of finding the index and splicing it, you can just use the filter function. Since it returns a new array, we don't worry about mutating the state variable!
you will have to use Spread operator to reflect changes in react dom..
const deleteRow = (row) => {
let indexOfDeleted = -1;
let data = tableData;
data.forEach((item, index) => {
if (item.instrumentId === row.instrumentId) {
indexOfDeleted = index;
}
})
data.splice(indexOfDeleted, 1);
setTableData([...data]) /// like this
};

How to update nested state properties in react using immutable way

I am trying to update children records based on api response in reducer. My current redux state is containing data in following format :
const container = {
data: [
{
id: "5e58d7bc1bbc71000118c0dc",
viewName: "Default",
children: [
{
id: "5e58d7bc1bbc71000118c0dd",
description: "child1",
quantity: 10
},
{
id: "5e58d7bc1bbc71000118c0ff",
description: "child2",
quantity: 20
},
{
id: "5e58d7bc1bbc71000118c0gg",
description: "child3",
quantity: 30
}
]
}
]
}
I am getting child1 data from api in following format :
{
id:"5e58d7bc1bbc71000118c0dd",
description:"child1",
quantity:20 // Quantity is updated
}
How can i update quantity for child1 in correct way ?
I am using immutable package.
https://www.npmjs.com/package/immutable
I honestly see no reason to use immutable.js, if you don't understand spread syntax or find it too verbose then you can use this helper.
const REMOVE = () => REMOVE;
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path) => {
if (path.length === 1) {
return setKey(
current,
path[0],
callback(current[path[0]])
);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1))
);
};
return recur(object, path, callback);
};
const data = {
name: [{ hello: 'world', stay: true }, 4],
list: [1, 2, 3],
};
console.log(
'setting nested value',
set(data, ['name', 0, 'hello'], () => 'hello world')
.name[0].hello
);
console.log(
'doubling nested value',
set(data, ['name', 1], x => x * 2).name[1]
);
console.log(
'removing nested value',
set(data, ['name', 0, 'hello'], REMOVE).name[0]
);
console.log(
'adding to an array',
set(data, ['list'], v => [...v, 4]).list
);
console.log(
'mapping an array',
set(data, ['list'], v => v.map(v => v * 8)).list
);
console.log(
'data is not mutated',
JSON.stringify(data, undefined, 2)
);
You didn't post any code of how you save that data in state, did you use the immutable.js classes for it? If you did then say goodbye to redux dev tools and logging state to the console. Best to just leave it as data objects (serializable with JSON.stringify)

Interupt code and wait for user interaction in a loop - React

I am trying to implement an "add all" button in my react app. to do that, i pass this function to the onClick method of the button :
for (element in elements) {
await uploadfunction(element)
}
const uploadfunction = async (element) => {
if (valid) {
// await performUpload(element)
}
else if (duplicate) {
//show dialog to confirm upload - if confirmed await performUpload(element)
}
else {
// element not valid set state and show failed notification
}
}
const performUpload = async (element) => {
// actual upload
if(successful){
// set state
}else{
// element not successful set state and show failed notification
}
}
the uploadfunction can have three different behaviors :
Add the element to the database and update the state
Fail to add the element and update the state
Prompt the user with the React Dialog component to ask for confirmation to add duplicat element and update the state accordingly
My problem now is since i'm using a for loop and despite using Async/await , i can't seem to wait for user interaction in case of the confirmation.
The behavior i currently have :
The for loop move to the next element no matter what the result
The Dialog will show only for a second and disappear and doesn't wait for user interaction
Wanted behavior:
Wait for user interaction (discard/confirm) the Dialog to perform the next action in the loop.
How can i achieve that with React without Redux ?
Here is an example of a component that might work as an inspiration for you.
You might split it in different components.
class MyComponent extends Component {
state = {
items: [{
// set default values for all booleans. They will be updated when the upload button is clicked
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_2',
}],
performUpload: false,
};
onUploadButtonClick = () => {
this.setState(prevState => ({
...prevState,
items: prevState.items.map((item, index) => ({
isValid: validationFunction(),
isDuplicate: prevState.items.slice(0, index).some(i => i.data === item.data),
shouldUploadDuplicate: false,
data: item.data
})),
performUpload: true,
}), (nextState) => {
this.uploadToApi(nextState.items);
});
};
getPromptElement = () => {
const firstDuplicateItemToPrompt = this.getFirstDuplicateItemToPrompt();
const firstDuplicateItemIndexToPrompt = this.getFirstDuplicateItemIndexToPrompt();
return firstDuplicateItemToPrompt ? (
<MyPrompt
item={item}
index={firstDuplicateItemIndexToPrompt}
onAnswerSelect={this.onPromptAnswered}
/>
) : null;
};
getFirstDuplicateItemToPrompt = this.state.performUpload
&& !!this.state.items
.find(i => i.isDuplicate && !i.shouldUploadDuplicate);
getFirstDuplicateItemIndexToPrompt = this.state.performUpload
&& !!this.state.items
.findIndex(i => i.isDuplicate && !i.shouldUploadDuplicate);
onPromptAnswered = (accepted, item, index) => {
this.setState(prevState => ({
...prevState,
items: prevState.items
.map((i, key) => (index === key ? ({
...item,
shouldUploadDuplicate: accepted,
}) : item)),
performUpload: accepted, // if at last an item was rejected, then the upload won't be executed
}));
};
uploadToApi = (items) => {
if (!this.getFirstDuplicateItemToPrompt()) {
const itemsToUpload = items.filter(i => i.isValid);
uploadDataToApi(itemsToUpload);
}
};
render() {
const { items } = this.stat;
const itemElements = items.map((item, key) => (
<MyItem key={key} {...item} />
));
const promptElement = this.getPromptElement();
return (
<div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{itemElements}
</div>
<Button onClick={this.onUploadButtonClick}>Upload</Button>
{promptElement}
</div>
)
}
}

How to update a state which is a array of objects?

My state is as follows
this.state = {
todos: [{
title: 'asas',
status: 'incomplete',
uuid: 11
}, {
title: 'asas',
status: 'incomplete',
uuid: 12
}, {
title: 'asas',
status: 'complete',
uuid: 13
}],
currentTab: "Show All"
}
and whenever a user clicks on any of the todo items's checkBox i want to update the state status of the checkbox and i have written the following code for it
this.state.todos.map(todo => {
if (todo.uuid === uuid) todo.status = (todo.status === 'complete') ? 'incomplete' : 'complete'
});
this.forceUpdate();
Is Using forceUpdate a good approach here? as i have updating only a single value inside an array of objects. Is there a better solution for this problem?
either of the following will call setState with the updated state without modifying the current state.
https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#inserting-and-removing-items-in-arrays
using the spread operator:
edit.. actually, this is the hard way 8)
see https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#updating-an-item-in-an-array
this.setState(prevState => {
const idx = prevState.todos.findIndex(todo => todo.uuid === uuid);
return {
todos: [
...prevState.todos.slice(0, idx),
{
...prevState.todos[idx],
status: prevState.todos[idx].status === "complete" ? "incomplete" : "complete",
}
...prevState.todos.slice(idx + 1),
]
}
});
or using immer:
import produce from "immer";
this.setState(prevState => {
const idx = prevState.todos.findIndex(todo => todo.uuid === uuid);
return produce(prevState, draft => {
draft.todos[idx].status = prevState.todos[idx].status === "complete" ? "incomplete" : "complete"
});
});

Resources