I have a react app and I'm trying to use a table component.
This table component has multiple ways of filtering the table, and I want to be able to update the URL with these filters so that when I navigate away and go back, the table can read from the url params and have the table react accordingly.
I am able to get my table to already do something like this, but when working with multiple params, I'm a bit lost. My issue is that using history.replace or history.push` will overwrite the previous params, and then updating them will only just append more params when I want to simply replace them
Here's what I have working already:
const useQueryParams = () => {
const params = new URLSearchParams(useLocation().search);
const startingAfter = params.get('startingAfter');
const endingBefore = params.get('endingBefore');
const status = params.get('status');
return { params, startingAfter, endingBefore, status };
}
export const MyComponent = () => {
const history = useHistory();
const location = useLocation();
const { params, startingAfter, endingBefore, status } = useQueryParams();
const [statusFilter, setStatusFilter] = useState<IStatus | string>(
Status.Pending
);
const [dateFilter, setDateFilter] = useState<{
appointmentStartingAfter?: string;
appointmentEndingBefore?: string;
}>({
appointmentStartingAfter: undefined,
appointmentEndingBefore: undefined,
});
useEffect(() => {
if (startingAfter || endingBefore) {
setDateFilter({
appointmentStartingAfter: moment(startingAfter).toISOString() || undefined,
appointmentEndingBefore: moment(endingBefore).toISOString() || undefined,
});
}
if (status) {
setStatusFilter(status);
}
}, [startingAfter, endingBefore, status]);
// ......
const handleDateChange = (momentDateRange) => {
const startingAfter = momentDateRange[0].subtract(1, 'day');
const endingBefore = momentDateRange[1].add(1, 'day');
history.replace({
search: `startingAfter=${startingAfter.format('YYYY-MM-DD')}&endingBefore=${endingBefore.format('YYYY-MM-DD')}`,
});
setDateFilter({
appointmentStartingAfter: startingAfter.toISOString() || undefined,
appointmentEndingBefore: endingBefore.toISOString() || undefined,
});
};
const handleStatusFilter = (value: string) => {
history.replace({
search: `status=${value || 'ALL_DOCUMENTS'}`,
});
setStatusFilter(value);
};
return (
<>
// ......
<DatePicker.RangePicker defaultValue={initialDates} style={{ marginRight: 20 }} onChange={handleDateChange} />
<CareDocumentStatusFilter value={statusFilter} onChange={handleStatusFilter} />
</>
);
}
I'm able to get the URLs working with history.replace or history.push but it will overwrite each other when I want them to remain persistent.
For example,
history.replace({
search: `startingAfter=${startingAfter.format('YYYY-MM-DD')}&endingBefore=${endingBefore.format('YYYY-MM-DD')}`,
});
// localhost:3000/xyz?startingAfter=2021-08-06&endingBefore=2021-08-19
But then in handleStatusFilter
history.replace({
search: `status=${value || 'ALL_DOCUMENTS'}`,
});
// localhost:3000/xyz?status=CREATED
In the example above, status will overwrite the params when the intended URL should look something like:
localhost:3000/xyz?startingAfter=2021-08-06&endingBefore=2021-08-19&status=CREATED
in my different attempts, when I want to update this, instead of changing the params, the params just seem to append more and end up looking like this:
localhost:3000/xyz?startingAfter=2021-08-06&endingBefore=2021-08-19&status=CREATED?startingAfter=xxxx&endingBefore=yyyy&status=zzzz
I do have react-router-dom available to be used as well.
So I interestingly stumbled upon this answer and it seemed to solve my issue. I believe using URLSearchParams.set was the key here. I don't entirely super understand the inner workings... but it works! Here is my solution:
const handleDateChange = (momentDateRange) => {
const startingAfter = momentDateRange[0].subtract(1, 'day');
const endingBefore = momentDateRange[1].add(1, 'day');
// This was important!
params.set('startingAfter', startingAfter.format('YYYY-MM-DD'));
params.set('endingBefore', endingBefore.format('YYYY-MM-DD'));
history.push({
// and so was this ...?
pathname: location.pathname,
search: params.toString(),
});
setDateFilter({
appointmentStartingAfter: startingAfter.toISOString() || undefined,
appointmentEndingBefore: endingBefore.toISOString() || undefined,
});
};
const handleStatusFilter = (value: string) => {
// This was important!
params.set('status', value || 'ALL_DOCUMENTS');
history.push({
// ... and so was this?
pathname: location.pathname,
search: params.toString(),
})
setStatusFilter(value);
};
Related
I am coding a table with pagination component, and I use multiple v-model and watch on these variables to fetch the data.
When perPage is updated, I want to reset page to 1. So I did it in my watch method but of course, watch is triggered twice (for perPage and then page).
Is it possible to update the variable and disable watch at this moment ?
Here is my current code:
<script setup lang="ts">
const sort = ref(route.query.sort || 'created_at')
const filters = ref(route.query.filters || {})
const page = ref(route.query.page ? parseInt(route.query.page.toString()) : 1)
const perPage = ref(route.query.per_page ? parseInt(route.query.per_page.toString()) : 10)
watch([sort, filters, page, perPage], ([oldSort, oldFilters, oldPage, oldPerPage], [newSort, newFilters, newPage, newPerPage]) => {
if (oldPerPage !== newPerPage)
page.value = 1
fetchItems()
router.push({
query: {
...route.query,
sort: sort.value,
// filters: filters.value,
page: page.value,
per_page: perPage.value,
},
})
})
async function fetchItems() {
items.value = await userApi.list({
filters: toRaw(filters.value),
sort: sort.value,
page: page.value,
perPage: perPage.value,
})
}
</script>
<template>
<CTable
:pagination-enabled="true"
v-model:sort="sort"
v-model:page="page"
v-model:per-page="perPage"
:total-items="items.meta.total"
:total-pages="items.meta.last_page"
/>
</template>
The only workaround I found is to return when I reset page:
watch(..., () => {
if (oldPerPage !== newPerPage) {
page.value = 1
return
}
fetchItems()
...
})
It is working in my case but for some another cases I would like to update without trigger the watch method.
Thanks!
Create another watcher for perPage :
watch([sort, filters, page, perPage], ([oldSort, oldFilters, oldPage, oldPerPage], [newSort, newFilters, newPage, newPerPage]) => {
fetchItems()
router.push({
query: {
...route.query,
sort: sort.value,
// filters: filters.value,
page: page.value,
per_page: perPage.value,
},
})
})
watch(perPage, (newPerPage,oldPerPage ) => {
if (oldPerPage !== newPerPage)
page.value = 1
})
It's recommended to create watch for a single property separately to avoid unnecessarily updates and conflicts. For the first watch try to replace it with watchEffect like since you're not using the old/new value:
watchEffect(() => {
fetchItems()
router.push({
query: {
...route.query,
sort: sort.value,
// filters: filters.value,
page: page.value,
per_page: perPage.value,
},
})
})
Considering that the state is changed through v-model, it needs to be observed with a watcher.
In order to avoid a watcher to be triggered multiple times, it should implement additional conditions like shown in the question, but it also needs to not skip an update when page is already 1, this won't result in additional update:
if (oldPerPage !== newPerPage) {
page.value = 1
if (newPage !== 1)
return
}
Otherwise there need to be multiple watchers, like another answer suggests. In case a watcher that causes asynchronous side effects (fetchItems) shouldn't be triggered more than it needs, and there are other causes for this to happen besides perPage, it can be debounced, e.g.:
watch(perPage, () => {
page.value = 1
});
watch([sort, filters, page, perPage], debounce(() => {
fetchItems()
...
}, 100));
Thank you for all your answers, I finally found what I was looking for using VueUse - watchIgnorable:
const { stop, ignoreUpdates } = watchIgnorable(page, (value) => {
fetchItems()
})
watch(perPage, (newPerPage, oldPerPage) => {
if (newPerPage !== oldPerPage) {
ignoreUpdates(() => {
page.value = 1
})
}
fetchItems()
})
For all React Gurus! The logic is that I make a query to overpass and get some GeoJSON, now I need to pass this GeoJSON object to another Component (which is not its child) so that there I could make some calculations and show it on the screen.
The general structure is like this: There is a Main.js component which has two children MapBox.js and CalculationResults.js. MapBox.js has a child OverpassLayer.js which gives me GeoJSON. This GeoJSON I need to pass to CalculationResults.js. I tried to implement callback function all the way from parent Main.js but it always returns me the GeoJSON from the previous query. Could you please help me with the correct way of passing data between Components.
This is my OverpassLayer.js
const OverpassLayer = (props) => {
const [geojson, setGeojson] = useState();
useEffect(() => {
makeQuery();
}, [props.street, props.houseNumber]);
const makeQuery = () => {
const query = `[out:json];nwr["addr:street"="${props.street}"]["addr:housenumber"="${props.houseNumber}"][building](59.3518076,24.55017,59.5915769,24.9262831);out geom;`;
const options = {
flatProperties: true,
overpassUrl: "https://overpass-api.de/api/interpreter",
};
overpass(query, dataHandler, options);
};
const dataHandler = (error, osmData) => {
if (
!error &&
osmData.features !== undefined &&
osmData.features[0] !== undefined
) {
console.log(osmData.features[0]);
let area = (getArea(osmData.features[0].geometry.coordinates[0]));
console.log(area);
setGeojson(osmData);
}
};
function keyFunction(geojson) {
if (geojson.features.length === 0) {
return "";
} else {
return geojson.features[0].id;
}
}
function getArea(array) {
if (array) {
let arrayPolygon = array;
let polygon = turf.polygon([arrayPolygon]);
let area = turf.area(polygon);
return area;
}
return 0;
}
return geojson ? <GeoJSON key={keyFunction(geojson)} data={geojson} /> : null;
};
the easiest way is to store the data in localStorage. For the example, below setGeojson(osmData) you can write localStorage.setItem("Geojson", JSON.stringify(Geojson)); and in CalculationResults.js you can call it in useEffect() or componentDidMount():
const getGeojson = JSON.parse(localStorage.getItem("Geojson"));
if(getGeojson){
if(getGeojson.length > 0){
setGeojson(getGeojson);
}
}
the more advanced way is to use redux
I have a vue app containing a vue-multiselect and I want to load the multiselect options through ajax. I am using lodash.throttle to throttle the firing of ajax requests as the user types in the search criteria. But whatever I do, I am seeing multiple requests being fired for each character I type in search. What I am doing wrong? Thanks in advance.
<template>
<multiselect :options="allLocations.map(p => p.externalId)"
:searchable="true"
:custom-label="uuid => {const sel = allLocations.filter(s => s.externalId === uuid); return sel.length === 1 ? sel[0].name + ' (' + sel[0].type + ')' : '';}"
class="mx-1 my-1"
style="width:500px;"
v-model="locations"
:clear-on-select="true"
:close-on-select="false"
:show-labels="true"
placeholder="Pick Locations to filter"
:multiple="true"
:loading="locationsLoading"
:internal-search="false"
#search-change="findLocations"
#input="updateLocations"
:allowEmpty="true" />
</template>
<script>
import {throttle} from 'lodash'
export default {
name: 'test-throttle-component',
data() {
allLocations: [],
locationsLoading: false,
locations: [],
},
methods: {
findLocations(search) {
this.$log.debug("Going to find locations for search criteria", search)
const params = {search: search}
this.locationsLoading = true
const self = this
throttle(() => self.$http.get("locations/ddlist", {params}).then(res => {
self.allLocations = res.data.items
self.locationsLoading = false
}), 5000)()
},
updateLocations() {
const self = this
this.$store.dispatch('updateSelectedLocations', this.locations)
.then(() => self.emitRefresh())
},
}
}
</script>
#strelok2010 is nearly right but I think he overlooked the fact that the vue multiselect #search-change handler expects a handler that takes the search argument and so the code won't work as is. Also I think, this won't resolve to the component inside the arrow function and so you may have to use standard JS function. Here is what I think would work.
findLocations: throttle(function(search) {
this.$log.debug("Going to find locations for search criteria", search)
const params = {search: search}
const self = this
this.locationsLoading = true
self.$http.get("locations/ddlist", {params}).then(res => {
self.allLocations = res.data.items
self.locationsLoading = false
}
}, 5000)
Try to wrap findLocations method to throttle function:
findLocations: throttle(() => {
this.$log.debug("Going to find locations for search criteria", search)
const params = {search: search}
const self = this
this.locationsLoading = true
self.$http.get("locations/ddlist", {params}).then(res => {
self.allLocations = res.data.items
self.locationsLoading = false
}
}, 5000)
More info here
I want the same message with different URL, based on a prop. I basically have the below render method, which I call inside my main one.
renderNoBasicMode = () => {
const { securityMode } = this.props;
// Need this, while isFetching securityMode === '',
// Unless, we don't this is rendering on multipel renders.
if (securityMode !== SecurityMode.BASIC && securityMode !== SecurityMode.EMPTY) {
return (
<div className="badge badge-light" data-test="non-basic-mode">
<NoResource
icon="user-o"
title="Non Basic Security Mode"
primaryBtn="New User"
primaryCallback={this.openCreateUserModalPromise}
moreUrl={NonBasicSecurityMode.url}
>
No users available when Lenses is running on {securityMode} security mode.
</NoResource>
</div>
);
}
return null;
};
And I want to display a different url based on the value of the NonBasicSecurityMode, which I have here:
const NonBasicSecurityMode = [
{ securityMode: 'mode1', url: 'https://...' },
{ securityMode: 'mode2', url: 'https://...' },
{ securityMode: 'mode3', url: 'https://...' }
];
The securityMode, is deternment by an API request.
export const securityModeSelector = createSelector(
lensesConfigSelector,
config => (config.&& config['security.mode']) || ''
);
function mapStateToProps(state) {
return {
securityMode: securityModeSelector(state),
};
}
Basically, I tried mapping through them, and a forEach, but I was apparently wrong. Can you help me figure this out? Thanks!!
I have this form in my component :
this.state.customFieldsArray.length > 0 &&
(
<Form
ref="customForm"
type={this.customForm}
options={this.customFormOptions}
/>
)}
I want to add more options to the form (so it render more fields) when i click on a button.
This is the methode that handle the click on the button:
addCustomField = () => {
let newFieldName = this.state.customFieldLabel;
let newField = { newFieldName: "" };
this.customFormOptions.fields[newFieldName] = {
label: newFieldName
};
tempCustomFieldsArray.push(newField);
this.setState({
customFieldsArray: tempCustomFieldsArray
});
};
i have tried this but it didn't work.
this code works for me, create struct form model that call function, and user your condition there
formModel: t.struct({
customFields: this.getCustomFields(),
}),
then you create customFields function like,
getCustomFields = () => {
const customFields = {}
//write your condition here
return t.enums(customFields)
}