React table - Dropdown filter - react-table

I have implemented Global Filer using setGlobalFilter
But I want to Set filter column wise like:
Outside the table, I need a dropdown with some values which should filter the react-table based on the opted value from the dropdown.
this dropdown filter should filter out the whole table based on the opted value from the dropdown.
Can anyone give me link of demo or any help regarding this will be appreciated.

Ok, let's do it on react-table v8, yes, it is the new version of react-table.
First, make sure you import the required items from #tanstack/react-table
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
useReactTable,
} from "#tanstack/react-table";
Here, we use simple data.
const defaultData = [
{
firstName: "tanner",
lastName: "linsley",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
},
{
firstName: "tandy",
lastName: "miller",
age: 40,
visits: 40,
status: "Single",
progress: 80,
},
{
firstName: "joe",
lastName: "dirte",
age: 45,
visits: 20,
status: "Complicated",
progress: 10,
},
];
In the new version of react-table, we do not have to memoize the columns. We can define it with the help of columnHelper. You can read the details here.
const columnHelper = createColumnHelper();
const columns = [
columnHelper.accessor("firstName", {
header: "First Name",
}),
columnHelper.accessor("lastName", {
header: "Last Name",
}),
columnHelper.accessor("age", {
header: "Age",
}),
columnHelper.accessor("visits", {
header: "Visits",
}),
columnHelper.accessor("status", {
header: "Status",
}),
columnHelper.accessor("progress", {
header: "Profile Progress",
}),
];
Next, we define required states at our component.
const [data] = useState(() => [...defaultData]);
// to keep the selected column field
const [field, setField] = useState();
// to keep the input search value
const [searchValue, setSearchValue] = useState("");
// required by react-table for filtering purposes
const [columnFilters, setColumnFilters] = useState();
In the previous version, we use useTable hooks to create our table instance, here, in the new version, we use useReactTable instead. We pass these configurations to make our filter run correctly.
const table = useReactTable({
data,
columns,
enableFilters: true,
enableColumnFilters: true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: {
columnFilters,
},
onColumnFiltersChange: setColumnFilters,
});
Next, we create our select option tag where we bind the select option value to field state and the onChange event handler to handleSelectChange. For the input tag, we bind the value to searchValue and and the onChange event handler to handleInputChange method.
Inside the select change handler, we need to reset both columnFilters and searchValue states.
...
const handleSelectChange = (e) => {
setColumnFilters([]);
setSearchValue("");
setField(e.target.value);
};
const handleInputChange = (e) => {
setSearchValue(e.target.value);
};
return (
...
<select value={field} onChange={handleSelectChange}>
<option value="">Select Field</option>
{table.getAllLeafColumns().map((column, index) => {
return (
<option value={column.id} key={index}>
{column.columnDef.header}
</option>
);
})}
</select>
<input
value={searchValue}
onChange={handleInputChange}
className="p-2 font-lg shadow border border-block"
placeholder={
field ? `Search ${field} column...` : "Please select a field"
}
/>
...
)
Here, we got the select options list from table.getAllLeafColumns().
Since columns age, visits, and progress value is number, we need to modify our columns configurations by using custom filterFn options.
const columns = [
...
columnHelper.accessor("age", {
header: "Age",
filterFn: (row, _columnId, value) => {
return row.original.age === parseInt(value);
},
}),
columnHelper.accessor("visits", {
header: "Visits",
filterFn: (row, _columnId, value) => {
return row.original.visits === parseInt(value);
},
}),
...
columnHelper.accessor("progress", {
header: "Profile Progress",
filterFn: (row, _columnId, value) => {
return row.original.progress === parseInt(value);
},
}),
];
And as the documentation said that we need to remember this:
Every filter function receives:
The row to filter
The columnId to use to retrieve the row's value
The filter value
and should return true if the row should be included in the filtered rows, and false if it should be removed.
Finally, it is time to render the table:
return (
<div className="p-2">
...
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
And here is the working code:
I hope it helps.

Related

Filter Apollo Client cache results, tried readQuery but returns null

I have been trying to work with the Apollo Client cache. So, I don't have to make another call to the server. And to help with paging. The stack I am using is Next.js, Apollo Client on the front-end, Keystone.js on the backend.
I am building an e-commerce site. Right now the user can view products by categories. In each category listing, products can be filtered by different attributes. A simple example would be to filter by color and size. Previously I was storing fetched products in state. And I had filtering working pretty well. The main issue I had was paging. When products are filtered on one page, the other pages are not affected. I read up on reading/writing to the cache and thought that would fix the paging issues. But I can't get it to work. Specifically readQuery.
So this is what I have tried and honestly I have not found a good example on using readQuery. It wasn't until I found this question here and read the first answer that I realized you have to use the exact same query that first fetched the results. Or do I?
Here is the parent component and it's first query to fetch products.
\\ Products.jsx
function ProductCategory({ page, category, productType }) {
const [filteredData, setFilteredData] = useState();
const { data, error, loading } = useQuery(ALL_PRODUCTS_FILTERED_QUERY, {
variables: {
skipPage: page * perPage - perPage,
first: perPage,
category,
productType: capitalize(productType),
potency: '0',
},
fetchPolicy: 'cache-first',
});
useEffect(() => {
if (!loading) {
setFilteredData(data?.products);
}
}, [loading]);
if (loading)
return (
<Processing loading={loading.toString()}>
<LoadingIcon tw="animate-spin" />
Loading
</Processing>
);
if (error) return <DisplayError error={error} />;
return (
<>
<Filters
loading={loading}
products={data.products}
setFilteredData={setFilteredData}
productType={productType}
category={category}
page={page}
/>
<ContainerStyles hasBgPrimaryLight20>
<ProductGridStyles>
{filteredData &&
filteredData?.map((product) => (
<Product key={product.id} product={product} />
))}
</ProductGridStyles>
</ContainerStyles>
</>
);
}
ProductCategory.propTypes = {
page: PropTypes.number,
category: PropTypes.string,
productType: PropTypes.string,
};
export default ProductCategory;
My ALL_PRODUCTS_FILTERED_QUERY query:
export const ALL_PRODUCTS_FILTERED_QUERY = gql`
query ALL_PRODUCTS_FILTERED_QUERY(
$skipPage: Int = 0
$first: Int
$category: String
$productType: String
$potency: String
) {
products(
take: $first
skip: $skipPage
orderBy: [{ name: asc }]
where: {
productType: { every: { name: { equals: $productType } } }
category: { slug: { equals: $category } }
flower: { potency: { gte: $potency } }
}
) {
id
name
slug
inventory
price
priceThreshold {
name
price
amount
}
hotDeal
topPick
category {
slug
name
}
photos {
id
image {
publicUrl
}
altText
}
description
status
vendor {
id
name
vendor_ID
}
flower {
label
weight
potency
strain
trimMethod
environment
}
oil {
label
weight
potency
cbd
oilType
solventUsed
}
concentrate {
label
weight
potency
strain
type
}
preRoll {
label
size
potency
strain
type
tube
}
machine {
label
model
modelYear
condition
}
}
}
`;
My Filters.jsx component is what's using the readQuery method to read from the cache and filter results. Or so I hoped. You'll see I am passing the setFilteredData hook from Products.jsx so once products are returned from the cache I am updating the state. Right now I am getting null.
For simplicity I have removed all filters except potency and imports.
\\ Filters.jsx
function Filters({ category, setFilteredData, page, productType }) {
const [potencies, setPotencies] = useState([]);
const [potency, setPotency] = useState();
const { checkboxfilters, setCheckboxFilters } = useFilters([
...strainList,
...environmentList,
...potencyList,
...oilTypeList,
...solventList,
...trimList,
...concentrateTypeList,
...prerollTypeList,
...tubeList,
...priceList,
]);
const client = useApolloClient();
async function fetchProducts(flowerPotency) {
console.log(
page * perPage - perPage,
category,
flowerPotency,
capitalize(productType)
);
try {
const data = await client.readQuery({
query: ALL_PRODUCTS_FILTERED_QUERY,
variables: {
skipPage: page * perPage - perPage,
first: perPage,
category,
productType: capitalize(productType),
potency: flowerPotency,
},
});
setFilteredData(data.products);
} catch (error) {
console.error('Error: ', error);
}
}
const updateCheckboxFilters = (index) => {
setCheckboxFilters(
checkboxfilters.map((filter, currentIndex) =>
currentIndex === index
? {
...filter,
checked: !filter.checked,
}
: filter
)
);
};
const handlePotencyCheck = (e, index) => {
if (e.target.checked) {
setPotency(e.target.value);
fetchProducts(e.target.value);
} else {
setPotency();
}
updateCheckboxFilters(index);
};
return (
<FilterStyles>
<FiltersContainer>
<Popover tw="relative">
<Popover.Button tw="text-sm flex">
Sort{' '}
<ChevronDownIcon
tw="ml-2 h-4 w-4 text-accent"
aria-hidden="true"
/>
</Popover.Button>
<Popover.Panel/>
</Popover>
<div tw="flex space-x-4">
{category === 'flower' ||
category === 'oil' ||
category === 'concentrate' ? (
<Popover tw="relative">
<Popover.Button tw="text-sm flex">
Potency{' '}
<ChevronDownIcon
tw="ml-2 h-4 w-4 text-accent"
aria-hidden="true"
/>
</Popover.Button>
<FilterPopOverPanelStyles>
{potencyList.map((filter) => {
const checkedIndex = checkboxfilters.findIndex(
(check) => check.name === filter.name
);
return (
<Checkbox
key={`potency-${checkedIndex}`}
isChecked={checkboxfilters[checkedIndex].checked}
checkHandler={(e) => handlePotencyCheck(e, checkedIndex)}
label={filter.name.slice(2)}
value={filter.value.slice(2)}
index={checkedIndex}
/>
);
})}
</FilterPopOverPanelStyles>
</Popover>
) : null}
</div>
</FiltersContainer>
<ActiveFilters>
<ActiveFiltersContainer>
<ActiveFiltersHeader>Applied Filters:</ActiveFiltersHeader>
<div tw="flex">
{potencies.map((potency, index) => (
<button
key={index}
type="button"
onClick={() => handleRemoveFilter(potency)}
>
{potency}% <XIcon tw="w-4 h-4 ml-2 text-accent" />
<span tw="sr-only">Click to remove</span>
</button>
))}
</div>
</ActiveFiltersContainer>
</ActiveFilters>
</FilterStyles>
);
}
Filters.propTypes = {
loading: PropTypes.any,
products: PropTypes.any,
setFilteredData: PropTypes.func,
};
export default Filters;
I expected it to return products from the cache based on the potency passed to the query. Instead, I get null. I thought using the exact same query and variables would do the trick. What am I doing wrong? Am I using readQuery correctly? I did try readFragment and got that to successfully work, but it only returns one product. So I know reading from the cache is working.
You're making your life unnecessarily complicated. If you're looking for data that's been previously returned by useQuery or client.readQuery you can query it again with {fetchPolicy: "cache-only"}
Initial query:
const data = await client.readQuery({
query: ALL_PRODUCTS_FILTERED_QUERY,
variables: {
skipPage: page * perPage - perPage,
first: perPage,
category,
productType: capitalize(productType),
potency: flowerPotency,
},
});
Query from cache:
const data = await client.readQuery({
query: ALL_PRODUCTS_FILTERED_QUERY,
variables: {
skipPage: page * perPage - perPage,
first: perPage,
category,
productType: capitalize(productType),
potency: flowerPotency,
},
fetchPolicy: "cache-only", // <-- fetchPolicy!
});
Also you don't need either useState or useEffect to copy the data coming back from your query - it's not adding any value.
After digging back through my code, I realized I had pagination setup incorrectly as it was initially setup for a single product category. Now that I have multiple categories my code needs refactored to take this into account. So conditions were never being met and always fetching from the network. Plus I need to really sit down and truly understand how Apollo client works.
The first issue was as simple as arguments named incorrectly. Initially I had args being passed into my read() function named skip,first. Doing an update last year for Keystone those got renamed to skip,take.
Second issue was my PAGINATION_QUERY was getting the count for all products. But now since they are split into categories, I just need the product count for the current category.
After these two tweaks I can now use readQuery to query the cache.

react table - passing row cell value to a function

I would like to call function and pass json element id value (column ID) to a function onBookAppointment().
I receive error message, id is not defined. I understand error message, but i do not know how to define it.
Data is coming from async function getData().
I would appreciate some guidance here.
PS! Learning react :)
const columns = React.useMemo(
() => [
{
Header: "ID",
accessor: "id", // accessor is the "key" in the data
},
{
Header: "Reserve",
accessor: "reserveButton",
Cell: row => (
<div>
<Button onClick={() => onBookAppointment(id)} variant="dark">{"product.add-appointment"}</Button>
</div>
),
},
],
[]
);

How to hide selected option in Autocomplete using reactjs

I want to hide the selected option in the Dropdown, the option should not appear in the next dropdown. For an example, there are 2 dropdowns, in the first dropdown - i have selected "Hockey" then "hockey" should not be shown in the second dropdown, It should show only "Baseball and badminton".
My JSON data will be appearing in this way:
"details": [
{ "id": "12wer1", "name": "ABC", "age": 15, "game": "badminton" },
{ "id": "78hbg5", "name": "FRE", "age": 21, "game": "Hockey" }
]
Here is the sample Code:
let games = [{ game: "Baseball"}, { game: "Hockey"}, { game: "badminton" }];
class Field extends React.Component {
constructor(props) {
super(props);
this.state = {
details: [{id: '', name: '', age: '', game: ''}]
}
}
...
...
render() {
return (
...
...
{this.state.details.map((y) => (
<Autocomplete
style={{ witdth: 200 }}
options={games}
getOptionLabel={(option) => option.game}
onChange={(value) =>this.onGameChange(value, y.id)}
value={games.filter(i=> i.game=== y.game)}
renderInput={(params) =>
<TextField {...params} label="Games" margin="normal" />
}
/>))}
...
...
)
}
}
onGameChange = (e, id)=> {
let games = this.state.details;
let data = games.find(i => i.id === id);
if (data) {
data.game = value.game;
}
this.setState({ details: games });
}
I have no idea, how to hide the selected option, can anyone help me in this query?
Thanks! in advance.
A possible solution would be to
create an array and store the values in an array when the user selects autocomplete
while passing options, filter the values that have been passed to other autocompletes.
const ary = [111,222,333];
let obj = [{id: 111},{id: 222}];
const i = 1; // this is your index in loop
const ary2 = ary.slice()
ary2.splice(i,1);
console.log(obj.filter((o) => !ary.includes(o.name))); // this should be given to our options list in autocomplete
you can hide this is in CSS easily no need to do anything in ReactJS
autocomplete renders as an unordered list so something like this
.panel > ul > li:first-child {
display:none;
}

How to do pagination based on the document position within a collection? (offset pagination)

I'm trying to do a pagination where the user can see each button's page number in the UI. I'm using Firestore and Buefy for this project.
My problem is that Firestore is returning wrong queries for this case. Sometimes (depending the page that the users clicks on) It works but sometimes don't (It returns the same data of the before page button).
It's really messy I don't understand what's going on. I'll show you the code:
Vue component: (pay attention on the onPageChange method)
<template>
<div>
<b-table
:data="displayData"
:columns="table.columns"
hoverable
scrollable
:loading="isLoading"
paginated
backend-pagination
:total="table.total"
:per-page="table.perPage"
#page-change="onPageChange">
</b-table>
</div>
</template>
<script>
import { fetchBarriosWithLimit, getTotalDocumentBarrios, nextBarrios } from '../../../../firebase/firestore/Barrios/index.js'
import moment from 'moment'
const BARRIOS_PER_PAGE = 5
export default {
data() {
return {
table: {
data: [],
columns: [
{
field: 'name',
label: 'Nombre'
},
{
field: 'dateAddedFormatted',
label: 'Fecha aƱadido'
},
{
field: 'totalStreets',
label: 'Total de calles'
}
],
perPage: BARRIOS_PER_PAGE,
total: 0
},
isLoading: false,
lastPageChange: 1
}
},
methods: {
onPageChange(pageNumber) {
// This is important. this method gets fired each time a user clicks a new page. I page number that the user clicks.
this.isLoading = true
if(pageNumber === 1) {
console.log('show first 5...')
return;
}
const totalPages = Math.ceil(this.table.total / this.table.perPage)
if(pageNumber === totalPages) {
console.log('show last 5...')
return;
}
/* Here a calculate the next starting point */
const startAfter = (pageNumber - 1) * this.table.perPage
nextBarrios(this.table.perPage, startAfter)
.then((querySnap) => {
this.table.data = []
this.buildBarrios(querySnap)
console.log('Start after: ', startAfter)
})
.catch((err) => {
console.err(err)
})
.finally(() => {
this.isLoading = false
})
},
buildBarrios(querySnap) {
querySnap.docs.forEach((docSnap) => {
this.table.data.push({
id: docSnap.id,
...docSnap.data(),
docSnapshot: docSnap
})
});
}
},
computed: {
displayData() {
let data = []
this.table.data.map((barrioBuieldedObj) => {
barrioBuieldedObj.dateAddedFormatted = moment(Number(barrioBuieldedObj.dateAdded)).format("DD/MM/YYYY")
barrioBuieldedObj.totalStreets ? true : barrioBuieldedObj.totalStreets = 0;
data.push(barrioBuieldedObj)
});
return data;
}
},
mounted() {
// obtener primer paginacion y total de documentos.
this.isLoading = true
getTotalDocumentBarrios()
.then((docSnap) => {
if(!docSnap.exists || !docSnap.data().totalBarrios) {
// mostrar mensaje que no hay barrios...
console.log('No hay barrios agregados...')
this.table.total = 0
return;
}
const totalBarrios = docSnap.data().totalBarrios
this.table.total = totalBarrios
if(totalBarrios <= BARRIOS_PER_PAGE) {
return fetchBarriosWithLimit(totalBarrios)
} else {
return fetchBarriosWithLimit(BARRIOS_PER_PAGE)
}
})
.then((querySnap) => {
if(querySnap.empty) {
// ningun doc. mostrar mensaje q no hay barrios agregados...
return;
}
this.buildBarrios(querySnap)
})
.catch((err) => {
console.error(err)
})
.finally(() => {
this.isLoading = false
})
}
}
</script>
<style lang="scss" scoped>
</style>
The nextBarrios function:
function nextBarrios(limitNum, startAtNum) {
const query = db.collection('Barrios')
.orderBy('dateAdded')
.startAfter(startAtNum)
.limit(limitNum)
return query.get()
}
db is the result object of calling firebase.firestore(). Can I tell a query to start at a certain number where number is the index position of the document within a collection? If not, How could I approach this problem?
Thank you!
Firestore doesn't support offset or index based pagination. It's also not possible to tell how many documents the entire query would return without actually reading them all. So, unfortunately, what you're trying to do isn't possible with Firestore.
It seems also that you're misunderstanding how the pagination APIs actually work. startAfter doesn't take an index - it takes either a DocumentSnapshot of the last document in the prior page, or a value of the ordered field that you used to sort the query, again, the last value you saw in the prior page. You are basically going to use the API to tell it where to start in the next page of results based on what you found in the last page. That's what the documentation means when it says you are working with a "query cursor".

How to generate filtered list using reselect redux based on static filtered values?

I am fetching news data from an API, in the app I need to show 3 lists. today news, yesterday news, article news.
I think I should use redux reselect. However, all the examples I am visiting has a dynamic filter value (state filter) while I need data to be fileted statically (no state changes these filters)
my state at the moment is
{news : [] }
How can I generate something like below using reselect
{news: [], todayNews:[], yesterdayNews:[], articleNews: []}
should I use reselect or I should just filter inside a component? I think reselect is memorized so I prefer to use reselect for performance
You can do something like the following:
const { createSelector } = Reselect;
const state = {
news: [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
{ id: 3, name: 'three' },
],
};
const selectNews = (state) => state.news;
const selectOdds = createSelector(selectNews, (news) =>
news.filter(({ id }) => id % 2 !== 0)
);
const selectEvens = createSelector(selectNews, (news) =>
news.filter(({ id }) => id % 2 === 0)
);
const selectFilteredNews = createSelector(
selectNews,
selectEvens,
selectOdds,
(news, even, odd) => ({ news, even, odd })
);
const news = selectFilteredNews(state);
console.log('news:', JSON.stringify(news, undefined, 2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
You use selectors when you need to calculate values based on state such as the total of a list or filtered things from a list. This way you don't need to duplicate the data in your state.

Resources