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
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()
})
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);
};
So, i'm getting the selected items from the resource picker and trying to use it for initialSelectionIds.
However i'm getting this error when using it as in the beginning it is empty: I'm also not sure how to get selectedValue into the correct format.
error:
appBridgeError: APP::ERROR::INVALID_OPTIONS: `type_error_expected_object` thrown for path: ['initialSelectionIds']['0'] and value: `[]
const handleSelection = (resources) => {
const idsFromResources = resources.selection.map(v => "id:'" + v.id +'\'');
const namesFromResources = resources.selection.map(p => p.title);
selectedValue = JSON.parse(idsFromResources); <Page
title='Allergen Selector'
primaryAction={{
content: 'Select Allergens',
onAction: () => opend(),
}}>
<ResourcePicker
resourceType="Product"
showVariants={true}
open={active}
onSelection={(resources) => handleSelection(resources)}
onCancel={handleToggle}
initialSelectionIds={[selectedValue ] }
/>
</Page>
But it works when i use this:
const productWithAllVariantsSelected = {
id: 'gid://shopify/Product/6337793261759',
};
<ResourcePicker
resourceType="Product"
showVariants={true}
open={active}
onSelection={(resources) => handleSelection(resources)}
onCancel={handleToggle}
initialSelectionIds={[productWithAllVariantsSelected] }
/>
I'm using Vue js to display and edit details of a person. The person being edited has a list of favourite colours that I want to display as a list of checkboxes. When I change the colours selected, and click the 'Update' button, the person object should be updated accordingly, so I can pass back to the api to update.
I've got as far as displaying the Person object's colours correctly against their respective checkboxes. But I'm struggling with passing the changes to the colour selection, back to the Person object. Below is my checkbox list and details of how I've tried to implement this. Is there a better way of doing this?
I've tried using 'b-form-checkbox-group'. Below is my code.
Please note - The list of available colours is dynamic, but I've temporarily hardcoded a list of colours ('colourData') till I get this working.
Also, in the 'UpdatePerson' method, I've commented out my attempts to get the selected colours mapped back to the Person object.
<template>
<form #submit.prevent="updatePerson">
<b-form-group label="Favourite colours:">
<b-form-checkbox-group id="favColours"
v-model="colourSelection"
:options="colourOptions"
value-field="item"
text-field="name">
</b-form-checkbox-group>
</b-form-group>
<div class="container-fluid">
<b-btn type="submit" variant="success">Save Record</b-btn>
</div>
</form>
</template>
<script>
import service from '#/api-services/colours.service'
export default {
name: 'EditPersonData',
data() {
return {
personData: {
personId: '',
firstName: '',
lastName: '',
colours:[]
},
colourData: [
{ colourId: '1', isEnabled: '1', name: 'Red' },
{ colourId: '2', isEnabled: '1', name: 'Green' },
{ colourId: '3', isEnabled: '1', name: 'Blue' },
],
selectedColours: [],
colourSelection: []
};
},
computed: {
colourOptions: function () {
return this.colourData.map(v => {
let options = {};
options.item = v.name;
options.name = v.name;
return options;
})
}
},
created() {
service.getById(this.$route.params.id).then((response) => {
this.personData = response.data;
this.colourSelection = response.data.colours.map(function (v) { return v.name; });
this.selectedColours = response.data.colours;
}).catch((error) => {
console.log(error.response.data);
});
},
methods: {
async updatePerson() {
//const cs = this.colourSelection;
//const cd = this.colourData.filter(function (elem) {
// if (cs.indexOf(elem.name) != -1) { return elem;}
//});
//this.personData.colours = [];
//this.personData.colours = cd;
service.update(this.$route.params.id, this.personData).then(() => {
this.personData = {};
}).catch((error) => {
console.log(error.response.data);
});
},
}
}
</script>
Any help wold be much appreciated.
Thanks
I got this working by making the below changes to the commented part in the 'updatePerson()' method:
methods: {
async updatePerson() {
const cs = this.colourSelection;
const cd = this.colourData.filter(function (elem) {
if (cs.some(item => item === elem.name)) { return elem; }
});
this.personData.colours = [];
this.personData.colours = cd;
service.update(this.$route.params.id, this.personData).then(() => {
this.personData = {};
}).catch((error) => {
console.log(error.response.data);
});
}
}
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!!