Get filtred data, react-bootstrap-table2 - reactjs

Is there any global table option that return the filtred rows? Ignore pagination. All rows matching one or several textFilter?
I need a value in the header showin the average value of the filtred data.
I don't find any on https://react-bootstrap-table.github.io/react-bootstrap-table2/docs/table-props.html
There is the onDataSizeChange, but it only gives the prop dataSize (nr of rows), also only available when pagination is not used.
Update to second question in comments:
class App extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
data: [...]
filtredData: null
};
};
const factory = patchFilterFactory(filterFactory, (filteredData) => {
this.setState({filteredData}); // causes maximum update exceeded..
});
render() {
return (
<div>
<BootstrapTable
keyField='id'
striped
hover
bootstrap4
data={anbuds}
filter={factory()}
columns={columns}/>
</div>
);
}
}

Kinda.
One way you could do that is by providing a different implementation of the filter prop, and get the data that you need there.
import BootstrapTable from "react-bootstrap-table-next";
import filterFactory, { textFilter } from "react-bootstrap-table2-filter";
function patchFilterFactory(filterFactory, onFilteredData) {
return (...args) => {
const { createContext, options } = filterFactory(...args)
return {
createContext: (...args) => {
const { Provider: BaseProvider, Consumer } = createContext(...args)
const Provider = class FilterProvider extends BaseProvider {
componentDidUpdate() {
onFilteredData(this.data)
}
}
return { Provider, Consumer }
},
options
}
}
}
patchFilterFactory will just sit in between the original filter provider and your code, allowing you to get the data that you need.
How to use it:
function Table() {
const columns = [
{
dataField: "id",
text: "Product ID"
},
{
dataField: "name",
text: "Product Name",
filter: textFilter({
delay: 0
})
},
{
dataField: "price",
text: "Product Price",
filter: textFilter({
delay: 0
})
}
];
const factory = patchFilterFactory(filterFactory, (filteredData) => {
console.log('on filter data', filteredData)
})
return (
<BootstrapTable
keyField="id"
data={props.products}
columns={columns}
filter={factory()}
/>
);
}
I agree, that's far from ideal, but as far as I was able to assess, it may be the only way at the moment.
If you want to change state in the same component, I would recommend:
const [filteredData, setFilteredData] = React.useState([])
const factory = patchFilterFactory(filterFactory, data => {
setFilteredData(prevData => {
if (JSON.stringify(prevData) !== JSON.stringify(data)) {
return data
}
return prevData
})
})

My 2ยข: after some investigation (and intentionally avoiding implementation of filter factory wrapper suggested by #federkun) I found out I can access current filter context of rendered table.
In order to access table properties, I had to add React Ref:
class MyDataTable extends React.Component {
constructor(props) {
super(props);
this.state = {
data: props.data
};
this.node = React.createRef();
}
...
render() {
return (
<Card>
<CardBody>
<BootstrapTable
ref={n => this.node = n}
keyField="id"
data={this.state.data}
...
/>
<Button name="click-me" onClick={() => this.handleClick()}>Click me!</Button>
</CardBody>
</Card>
)
}
}
Now when it is possible to reference <BootstrapTable> from code using this.node, I can get to all filtered data (without paging):
// member of MyDataTable component
handleClick() {
console.log(this.node.filterContext.data);
}
Please note that if you access data this way, entries won't be sorted as you see it in the table, so if you want to go really crazy, you can get data filtered and sorted this way:
// member of MyDataTable component
handleClick() {
const table = this.node;
const currentDataView =
(table.paginationContext && table.paginationContext.props.data)
|| (table.sortContext && table.sortContext.props.data) // <- .props.data (!)
|| (table.filterContext && table.filterContext.data) // <- .data (!)
|| this.state.data; // <- fallback
console.log(currentDataView);
}
... however this is becoming pretty wild. Important thing here is to start with paginationContext - anything before it is already sliced into pages. You can check how contexts are put together here: react-bootstrap-table2/src/contexts/index.js.
Nevertheless, this approach is hacky - completely avoiding public API, intercepting context pipeline, reading inputs for each layer. Things may change in newer releases, or there may be issue with this approach I haven't discovered yet - just be aware of that.

Related

React Question about promise in a for loop

I am in the process of learning React and making HTTP requests.
Recently Im trying to implement a dropdown for the webpage that Im working on. In my code I had to loop through an array of id, and make a post request for each of the id to extract the metadata. So Im encountering a problem with the dropdown options. The dropdown options are suppose to be the names for the corresponding id.
The array of id is an array of objects that looks like this
[{key: "someidnumber1", count: 5}, {key: "someidnumber2", count: 5}, {key: "someidnumber3", count: 10},....]
So what I did first is to loop through the id array, and make a post request on each of the id as parameter. This is inside my render method.
render() {
return(
<SomeOtherComponent>
{ //Do something to fetch the ids
let promises = [];
let names = [];
let options = [];
ids.map(id => {
promises.push(
axios
.post(TARGET_META_URL, {
filters: [
{
field: "id",
values: [id.key]
}
]
})
.then(response => {
// adding the name from the data into the names array
names.push(response.data[0].name);
})
});
Promise.all(promises).then(() => {
// Wait for the promises to collection all the names
// and pass into a new array
options = [...names];
}
return (
<Dropdown
options={options}
/>
);
}
</SomeOtherComponent>
);
}
My dropdown options after opening it is empty. So I did a couple console log and figured out that the options is declared outside the Promise.all so when the render() method is called, the dropdown takes in an empty array. I need help on how to setup the options for the dropdown so it waits for all the code before it finish running. I tried putting the second return inside the Promise.all() but I get an error method saying that render() doesn't have a return.
Make another component which fetches the data and renders them once the responses have come back. Use Promise.all to wait for all of the Promises to resolve together.
const getName = id => axios
.post(TARGET_META_URL, {
filters: [
{
field: "id",
values: [id.key]
}
]
})
.then(response => response.data[0].name);
const AsyncDropdown = ({ ids }) => {
const [options, setOptions] = useState();
useEffect(() => {
Promise.all(ids.map(getName))
.then(setOptions)
.catch((err) => {
// handle errors
});
}, [ids]);
return options ? <Dropdown options={options} /> : null;
}
And replace your original render method with:
render() {
return(
<SomeOtherComponent>
<AsyncDropdown ids={ids} />
</SomeOtherComponent>
);
}
Maybe this will help -
componentDidMount() {
let promises = [];
let options = [];
ids.map(id => {
promises.push(
axios
.post(TARGET_META_URL, {
filters: [
{
field: "id",
values: [id.key]
}
]
})
});
Promise.all(promises).then((response) => {
// Wait for the promises to collection all the names
// and pass into a new array
options = response.map(res => res.data[0].name);
this.setState({ options })
}
}
render() {
return(
<SomeOtherComponent>
{ this.state.options?.length ? <Dropdown options={this.state.options} /> : null }
</SomeOtherComponent>
);
}

How to make filter and search work together?

I have a component:
export default class Shop extends PureComponent {
state = {
search: "",
filters: [],
items: json
};
onFilterChange = ( event ) => {
const checkboxes = [...event.currentTarget.closest(".filter").getElementsByTagName("input")]
const filters = [];
checkboxes.map(checkbox => {
if (checkbox.checked) {
filters.push(checkbox.name);
}
});
this.setState({ filters }, this.filtredInput);
}
filtredInput() {
let items = json
if (this.state.filters.length !== 0) {
items = items.filter(element => this.state.filters.every(key => element[key]));
}
if (this.state.search.length !== 0) {
items = items.filter(word =>
word.name.toLocaleLowerCase().indexOf(this.state.search.toLocaleLowerCase()) !== -1
)
}
this.setState( {items} )
}
onSearchChange = ( {currentTarget} ) => {
const search = currentTarget.value
this.setState({ search }, this.filtredInput() )
}
render() {
return (
<div>
<div className="navigation">
<Filter
onFilterChange={this.onFilterChange}
/>
<Search
onSearchChange={this.onSearchChange}
/>
</div>
<Filtered
items={this.state.items}
updateShoppingBasket={this.updateShoppingBasket}
/>
</div>
)
}
}
Help to organize the logic so that both the search and the filter work simultaneously. Individually, everything works fine. But in the current version, the search works as if with a delay (apparently, the code works before the state is set), but I'm not sure that there are no other errors. How to organize the logic of the filter + search correctly in React?
You can make this much easier on yourself by thinking about the data differently. If you have it as a requirement that you always store the latest filtered data, then this won't work. I have included a custom code example here (including components that the example depends on): https://codesandbox.io/s/hardcore-cohen-sg263?file=/src/App.js
I like to think about the three pieces of data that we need to store to perform our operation.
The search term
The list of filters
The list of items we want to filter and search within
We can download the list of items from an API if we need to, and this way we can ensure we never lose data by filtering and replacing.
export default class App extends React.Component {
/*
there are three things we store
1. the current search term
2. the current list of filters
3. the entire list of data we want to search and filter through
(this can start as an empty array and then be filled with data from an API)
*/
state = {
term: "",
filters: [],
list: [
{
color: "red",
name: "car"
},
{
color: "blue",
name: "plane"
},
{
color: "red",
name: "boat"
}
]
};
/* This handles toggling filters, when the filter is clicked it will check
the array and add it if it isn't there.
*/
toggleFilter = filter => {
if (this.state.filters.includes(filter)) {
this.setState({
filters: this.state.filters.filter(item => item !== filter)
});
} else {
this.setState({
filters: [...this.state.filters, filter]
});
}
};
updateTerm = term => {
this.setState({
term
});
};
/* selector function to filter a list of items */
applyFilters = list => {
if (this.state.filters.length === 0) {
return list;
}
return list.filter(item => this.state.filters.includes(item.color));
};
/* search function to filter for the search term */
applySearch = list => {
if (this.state.term === "") {
return list;
}
return list.filter(item => item.name.startsWith(this.state.term));
};
render() {
/* we can filter the list and then search through the filtered list */
const filteredItems = this.applyFilters(this.state.list);
const searchedItems = this.applySearch(filteredItems);
/* we pass the filtered items to the list component */
return (
<>
<Filters onChange={this.toggleFilter} />
<Search term={this.state.term} onChange={this.updateTerm} />
<List items={searchedItems} />
</>
);
}
}
Hope this helps build a different mental model for React. I intentionally avoided making the Filters a controlled component, but only to show you the filter in the render function. Always open to discussion. Let me know how it goes.

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

Dealing with ag grid react and rendering a grid of checkboxes

I'm messing with ag-grid, react-apollo, and everything seems to be working fine. The goal here is to click a check box and have a mutation / network request occur modifying some data. The issue i'm having is that it redraws the entire row which can be really slow but im really just trying to update the cell itself so its quick and the user experience is better. One thought i had was to do a optimistic update and just update my cache / utilize my cache. What are some approach you guys have taken.
Both the columns and row data are grabbed via a apollo query.
Heres some code:
CheckboxRenderer
import React, { Component } from "react";
import Checkbox from "#material-ui/core/Checkbox";
import _ from "lodash";
class CheckboxItem extends Component {
constructor(props) {
super(props);
this.state = {
value: false
};
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
}
componentDidMount() {
this.setDefaultState();
}
setDefaultState() {
const { data, colDef, api } = this.props;
const { externalData } = api;
if (externalData && externalData.length > 0) {
if (_.find(data.roles, _.matchesProperty("name", colDef.headerName))) {
this.setState({
value: true
});
}
}
}
updateGridAssociation(checked) {
const { data, colDef } = this.props;
// const { externalData, entitySpec, fieldSpec } = this.props.api;
// console.log(data);
// console.log(colDef);
if (checked) {
this.props.api.assign(data.id, colDef.id);
return;
}
this.props.api.unassign(data.id, colDef.id);
return;
}
handleCheckboxChange(event) {
const checked = !this.state.value;
this.updateGridAssociation(checked);
this.setState({ value: checked });
}
render() {
return (
<Checkbox
checked={this.state.value}
onChange={this.handleCheckboxChange}
/>
);
}
}
export default CheckboxItem;
Grid itself:
import React, { Component } from "react";
import { graphql, compose } from "react-apollo";
import gql from "graphql-tag";
import Grid from "#material-ui/core/Grid";
import _ from "lodash";
import { AgGridReact } from "ag-grid-react";
import { CheckboxItem } from "../Grid";
import "ag-grid/dist/styles/ag-grid.css";
import "ag-grid/dist/styles/ag-theme-material.css";
class UserRole extends Component {
constructor(props) {
super(props);
this.api = null;
}
generateColumns = roles => {
const columns = [];
const initialColumn = {
headerName: "User Email",
editable: false,
field: "email"
};
columns.push(initialColumn);
_.forEach(roles, role => {
const roleColumn = {
headerName: role.name,
editable: false,
cellRendererFramework: CheckboxItem,
id: role.id,
suppressMenu: true,
suppressSorting: true
};
columns.push(roleColumn);
});
if (this.api.setColumnDefs && roles) {
this.api.setColumnDefs(columns);
}
return columns;
};
onGridReady = params => {
this.api = params.api;
this.columnApi = params.columnApi;
this.api.assign = (userId, roleId) => {
this.props.assignRole({
variables: { userId, roleId },
refetchQueries: () => ["allUserRoles", "isAuthenticated"]
});
};
this.api.unassign = (userId, roleId) => {
this.props.unassignRole({
variables: { userId, roleId },
refetchQueries: () => ["allUserRoles", "isAuthenticated"]
});
};
params.api.sizeColumnsToFit();
};
onGridSizeChanged = params => {
const gridWidth = document.getElementById("grid-wrapper").offsetWidth;
const columnsToShow = [];
const columnsToHide = [];
let totalColsWidth = 0;
const allColumns = params.columnApi.getAllColumns();
for (let i = 0; i < allColumns.length; i++) {
const column = allColumns[i];
totalColsWidth += column.getMinWidth();
if (totalColsWidth > gridWidth) {
columnsToHide.push(column.colId);
} else {
columnsToShow.push(column.colId);
}
}
params.columnApi.setColumnsVisible(columnsToShow, true);
params.columnApi.setColumnsVisible(columnsToHide, false);
params.api.sizeColumnsToFit();
};
onCellValueChanged = params => {};
render() {
console.log(this.props);
const { users, roles } = this.props.userRoles;
if (this.api) {
this.api.setColumnDefs(this.generateColumns(roles));
this.api.sizeColumnsToFit();
this.api.externalData = roles;
this.api.setRowData(_.cloneDeep(users));
}
return (
<Grid
item
xs={12}
sm={12}
className="ag-theme-material"
style={{
height: "80vh",
width: "100vh"
}}
>
<AgGridReact
onGridReady={this.onGridReady}
onGridSizeChanged={this.onGridSizeChanged}
columnDefs={[]}
enableSorting
pagination
paginationAutoPageSize
enableFilter
enableCellChangeFlash
rowData={_.cloneDeep(users)}
deltaRowDataMode={true}
getRowNodeId={data => data.id}
onCellValueChanged={this.onCellValueChanged}
/>
</Grid>
);
}
}
const userRolesQuery = gql`
query allUserRoles {
users {
id
email
roles {
id
name
}
}
roles {
id
name
}
}
`;
const unassignRole = gql`
mutation($userId: String!, $roleId: String!) {
unassignUserRole(userId: $userId, roleId: $roleId) {
id
email
roles {
id
name
}
}
}
`;
const assignRole = gql`
mutation($userId: String!, $roleId: String!) {
assignUserRole(userId: $userId, roleId: $roleId) {
id
email
roles {
id
name
}
}
}
`;
export default compose(
graphql(userRolesQuery, {
name: "userRoles",
options: { fetchPolicy: "cache-and-network" }
}),
graphql(unassignRole, {
name: "unassignRole"
}),
graphql(assignRole, {
name: "assignRole"
})
)(UserRole);
I don't know ag-grid but ... in this case making requests results in entire grid (UserRole component) redraw.
This is normal when you pass actions (to childs) affecting entire parent state (new data arrived in props => redraw).
You can avoid this by shouldComponentUpdate() - f.e. redraw only if rows amount changes.
But there is another problem - you're making optimistic changes (change checkbox state) - what if mutation fails? You have to handle apollo error and force redraw of entire grid - change was local (cell). This can be done f.e. by setting flag (using setState) and additional condition in shouldComponentUpdate.
The best way for me to deal with this was to do a shouldComponentUpdate with network statuses in apollo, which took some digging around to see what was happening:
/**
* Important to understand that we use network statuses given to us by apollo to take over, if either are 4 (refetch) we hack around it by not updating
* IF the statuses are also equal it indicates some sort of refetching is trying to take place
* #param {obj} nextProps [Next props passed into react lifecycle]
* #return {[boolean]} [true if should update, else its false to not]
*/
shouldComponentUpdate = nextProps => {
const prevNetStatus = this.props.userRoles.networkStatus;
const netStatus = nextProps.userRoles.networkStatus;
const error = nextProps.userRoles.networkStatus === 8;
if (error) {
return true;
}
return (
prevNetStatus !== netStatus && prevNetStatus !== 4 && netStatus !== 4
);
};
It basically says if there is a error, just rerender to be accurate (and i think this ok assuming that errors wont happen much but you never know) then I check to see if any of the network statuses are not 4 (refetch) if they are I dont want a rerender, let me do what I want without react interfering at that level. (Like updating a child component).
prevNetStatus !== netStatus
This part of the code is just saying I want the initial load only to cause a UI update. I believe it works from loading -> success as a network status and then if you refetch from success -> refetch -> success or something of that nature.
Essentially I just looked in my props for the query and saw what I could work with.

React: Google Places API/ Places Details

I have the following code which retrieves Google Places Reviews based on Google Places API. I have incorporated the logic to work as a React life cycle component. Currently, I am unable to setState and correctly bind the object. I could use some help understanding where my logic is failing.
export default class Reviews extends React.Component{
constructor(props){
super(props);
this.state = {
places: []
}
}
componentDidMount(){
let map = new google.maps.Map(document.getElementById("map"), {
center: {lat:40.7575285, lng: -73.9884469}
});
let service = new google.maps.places.PlacesService(map);
service.getDetails({
placeId: 'ChIJAUKRDWz2wokRxngAavG2TD8'
}, function(place, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
console.log(place.reviews);
// Intended behavior is to set this.setState({places.place.reviews})
}
})
}
render(){
const { places } = this.state;
return(
<div>
<p>
{
places.map((place) => {
return <p>{place.author_name}{place.rating}{place.text}</p>
})
}
</p>
</div>
)
}
}
You can't use this that way in a callback. When the function is called the this in, this.setState({places.place.reviews}) doesn't point to your object. One solution is to use => function notation which will bind this lexically.
service.getDetails({
placeId: 'ChIJAUKRDWz2wokRxngAavG2TD8'
}, (place, status) => {
if (status === google.maps.places.PlacesServiceStatus.OK) {
console.log(place.reviews);
this.setState({places: place.reviews})
}
})
}
Alternatively you can make a new reference to this and us it in the function. Something like
var that = this
...
that({places.place.reviews})
The first option is nicer, but requires an environment where you can use ES6. Since your using let you probably are okay.
With some tweaking -- I got the code to work! Thank you.
render(){
const { places } = this.state;
return(
<div>
<p>
{
places.map((place) => {
if(place.rating >= 4){
return <p key={place.author_name}>{place.author_name}{place.rating}{place.text}</p>
}
})
}
</p>
</div>
)
}

Resources