如何調整自動完成/選擇欄位以與伺服器端過濾和分頁一起使用
介紹
在前端開發中,有大量的組件框架可供選擇,可以為大多數類型的問題提供簡單的解決方案。不過,您經常會遇到需要客製化的問題。有些框架比其他框架在更大程度上允許這樣做,但並非所有框架都同樣容易自訂。 Vuetify 是功能最豐富的框架之一,擁有非常詳細的文件。但在實踐中,研究一些看似微不足道的功能並提出優化的解決方案仍然需要大量時間。
辨識挑戰
Vuetify 的自動完成元件非常棒。在自訂方面,它為用戶提供了多種視覺和功能選項。雖然某些模式可以透過單一屬性觸發,但其他模式則需要付出更多努力,而且解決方法並不總是那麼簡單。在本文中,我將介紹利用無限滾動概念實現伺服器端過濾和分頁的解決方案。此外,這裡討論的技術也可以應用於 v-select 元件。
解決方案:伺服器端增強
在本章中,我們將概述使用伺服器端邏輯增強 v-autocomplete 的解決方案。首先,我們將其包裝到我們自己的自訂元件中,該元件將用於進行進一步的調整。使用內建的附加項插槽與 Vuetify 的 v-intersect 指令結合,我們將實現所謂的無限滾動。這意味著我們一開始只會載入少量記錄。透過上述組合,我們將偵測何時到達清單底部。此時,我們會自動發送後續請求來載入下一頁記錄,直到最終到達底部。
之後,我們將透過調整 v-autocomplete 的屬性、停用前端過濾、添加足夠的指示器和處理滾動位置來擴展我們的解決方案以包括過濾,以確保最終用戶獲得流暢直觀的體驗。我們最後會得到這樣的結果:
設定事情
技術實作將透過 Vue(我日常工作的首選框架)以及 Vuetify(Vue 生態系統中常用的非常強大且高度可自訂的元件框架)進行演示。請注意,此處使用的概念可以使用流行 JavaScript 技術的其他組合來應用。
根據 Vue 和 Vuetify 版本的不同,解決方案會略有不同。由於兩者的 3.x 版本已經發布相當長一段時間並且現在已成為行業標準,因此我將使用它們。不過,我將為 Vue 2/Vuetify 2 留下重要註釋,因為許多活躍專案仍在使用它們。差異通常很小,除了存取內部 Vuetify 元素時(這在 Vue 3 中更難做到,因為不支援 $refs)。
首先,我們將建立一個新的空白項目。如果您希望將解決方案新增至現有專案中,則可以跳過本段。使用節點套件管理器(NPM),我們將使用以下命令建立專案:npm create vue@latest。預設設定適合我們的目的,但如果您願意,可以更改它們。我啟用了 ESLint 和 Prettier 選項。還有其他方式來啟動 Vue 項目,但我更喜歡這種方式,因為它預設使用 Vite 作為開發伺服器。
接下來,我們需要新增 Vuetify 以及其中未包含的基本相依性。除非您選擇其他圖示字體或更喜歡其他 CSS 選項,否則可以執行以下命令:npm install vuetify @mdi/font sass。依照官方文檔,您可以在 main.js 文件中設定 Vuetify。如果您像我一樣使用 MDI 圖標,請不要忘記字體行。
// file: main.js import './assets/main.css'; import { createApp } from 'vue'; import App from './App.vue'; import '@mdi/font/css/materialdesignicons.css'; import 'vuetify/styles'; import { createVuetify } from 'vuetify'; import { VAutocomplete } from 'vuetify/components'; import { Intersect } from 'vuetify/directives'; const vuetify = createVuetify({ components: { VAutocomplete }, directives: { Intersect } }); createApp(App).use(vuetify).mount('#app');
對於我們的後端,我選擇使用帶有虛假資料的免費 API 服務,稱為 JSON Placeholder。雖然它不是您在生產應用程式中使用的東西,但它是一項簡單且免費的服務,只需最少的調整即可為我們提供所需的一切。
現在,讓我們深入了解實際的編碼過程。在元件目錄中建立一個新的 Vue 檔案。根據您的喜好命名 - 我選擇了 PaginatedAutocomplete.vue。新增包含單一 v-autocomplete 元素的範本部分。為了用資料填充此元素,我們將定義一個將傳遞給元件的記錄屬性。
For some minor styling adjustments, consider adding classes or props to limit the width of the autocomplete field and its dropdown menu to around 300px, preventing it from stretching across the entire window width.
// file: PaginatedAutocomplete.vue <template> <v-autocomplete :items="items" :menu-props="{ maxWidth: 300 }" class="autocomplete"> <!-- --> </v-autocomplete> </template> <script setup> defineProps({ items: { type: Array, required: true } }); </script> <style lang="scss" scoped> .autocomplete { width: 300px; } </style>
In the App.vue file, we can delete or comment out the header and Welcome components and import our newly created PaginatedAutocomplete.vue. Add the data ref that will be used for it: records, and set its default value to an empty array.
// file: App.vue <script setup> import { ref } from 'vue'; import PaginatedAutocomplete from './components/PaginatedAutocomplete.vue'; const records = ref([]); </script> <template> <main> <PaginatedAutocomplete :items="records" /> </main> </template>
Adjust global styles if you prefer. I changed the color scheme from dark to light in base.css and added some centering CSS to main.css.
That completes the initial setup. So far, we only have a basic autocomplete component with empty data.
Controlling Data Flow with Infinite Scroll
Moving forward, we need to load the data from the server. As previously mentioned, we will be utilizing JSON Placeholder, specifically its /posts endpoint. To facilitate data retrieval, we will install Axios with npm install axios.
In the App.vue file, we can now create a new method to fetch those records. It’s a simple GET request, which we follow up by saving the response data into our records data property. We can call the function inside the onMounted hook, to load the data immediately. Our script section will now contain this:
// file: App.vue <script setup> import { ref, onMounted } from 'vue'; import axios from 'axios'; import PaginatedAutocomplete from './components/PaginatedAutocomplete.vue'; const records = ref([]); function loadRecords() { axios .get('https://jsonplaceholder.typicode.com/posts') .then((response) => { records.value = response.data; }) .catch((error) => { console.log(error); }); } onMounted(() => { loadRecords(); }); </script>
To improve the visual user experience, we can add another data prop called loading. We set it to true before sending the request, and then revert it to false after the response is received. The prop can be forwarded to our PaginatedAutocomplete.vue component, where it can be tied to the built-in v-autocomplete loading prop. Additionally, we can incorporate the clearable prop. That produces the following code:
// file: Paginated Autocomplete.vue <template> <v-autocomplete :items="items" :loading="loading" :menu-props="{ maxWidth: 300 }" class="autocomplete" clearable > <!-- --> </v-autocomplete> </template> <script setup> defineProps({ items: { type: Array, required: true }, loading: { type: Boolean, required: false } }); </script>
// file: App.vue // ... const loading = ref(false); function loadRecords() { loading.value = true; axios .get('https://jsonplaceholder.typicode.com/posts') .then((response) => { records.value = response.data; }) .catch((error) => { console.log(error); }) .finally(() => { loading.value = false; }); } // ...
<!-- file: App.vue --> <!-- ... --> <PaginatedAutocomplete :items="records" :loading="loading" /> <!-- ... -->
At this point, we have a basic list of a hundred records, but it’s not paginated and it doesn’t support searching. If you’re using Vuetify 2, the records won’t show up correctly - you will need to set the item-text prop to title. This is already the default value in Vuetify 3. Next, we will adjust the request parameters to attain the desired behavior. In a real project, the back-end would typically provide you with parameters such as page and search/query. Here, we have to get a little creative. We can define a pagination object on our end with page: 1, itemsPerPage: 10 and total: 100 as the default values. In a realistic scenario, you likely wouldn’t need to supply the first two for the initial request, and the third would only be received from the response. JSON Placeholder employs different parameters called _start and _limit. We can reshape our local data to fit this.
// file: App.vue // ... const pagination = ref({ page: 1, perPage: 10, total: 100 }); function loadRecords() { loading.value = true; const params = { _start: (pagination.value.page - 1) * pagination.value.perPage, _limit: pagination.value.perPage }; axios .get('https://jsonplaceholder.typicode.com/posts', { params }) .then((response) => { records.value = response.data; pagination.value.total = response.headers['x-total-count']; }) .catch((error) => { console.log(error); }) .finally(() => { loading.value = false; }); } // ...
Up to this point, you might not have encountered any new concepts. Now we get to the fun part - detecting the end of the current list and triggering the request for the next page of records. Vuetify has a directive called v-intersect, which can inform you when a component you attached it to enters or leaves the visible area in your browser. Our interest lies in its isIntersecting return argument. The detailed description of what it does can be found in MDN Web Docs. In our case, it will allow us to detect when we’ve reached the bottom of the dropdown list. To implement this, we will attach the directive to our v-autocomplete‘s append-item slot.
To ensure we don’t send multiple requests simultaneously, we display the element only when there’s an intersection, more records are available, and no requests are ongoing. Additionally, we add the indicator to show that a request is currently in progress. This isn’t required, but it improves the user experience. Vuetify’s autocomplete already has a loading bar, but it might not be easily noticeable if your eyes are focused on the bottom of the list. We also need to update the response handler to concatenate records instead of replacing them, in case a page other than the first one was requested.
To handle the intersection, we check for the first (in Vuetify 2, the third) parameter (isIntersecting) and emit an event to the parent component. In the latter, we follow this up by sending a new request. We already have a method for loading records, but before calling it, we need to update the pagination object first. We can do this in a new method that encapsulates the old one. Once the last page is reached, we shouldn’t send any more requests, so a condition check for that should be added as well. With that implemented, we now have a functioning infinite scroll.
// file: PaginatedAutocomplete.vue <template> <v-autocomplete :items="items" :loading="loading" :menu-props="{ maxWidth: 300 }" class="autocomplete" clearable > <template #append-item> <template v-if="!!items.length"> <div v-if="!loading" v-intersect="handleIntersection" /> <div v-else class="px-4 py-3 text-primary">Loading more...</div> </template> </template> </v-autocomplete> </template> <script setup> defineProps({ items: { type: Array, required: true }, loading: { type: Boolean, required: false } }); const emit = defineEmits(['intersect']); function handleIntersection(isIntersecting) { if (isIntersecting) { emit('intersect'); } } </script>
// file: App.vue // ... function loadRecords() { loading.value = true; const params = { _start: (pagination.value.page - 1) * pagination.value.perPage, _limit: pagination.value.perPage }; axios .get('https://jsonplaceholder.typicode.com/posts', { params }) .then((response) => { if (pagination.value.page === 1) { records.value = response.data; pagination.value.total = response.headers['x-total-count']; } else { records.value = [...records.value, ...response.data]; } }) .catch((error) => { console.log(error); }) .finally(() => { loading.value = false; }); } function loadNextPage() { if (pagination.value.page * pagination.value.perPage >= pagination.value.total) { return; } pagination.value.page++; loadRecords(); } // ...
Efficiency Meets Precision: Moving Search to the Back-end
To implement server-side searching, we begin by disabling from-end filtering within the v-autocomplete by adjusting the appropriate prop value (no-filter). Then, we introduce a new property to manage the search string, and then bind it to v-model:search-input (search-input.sync in Vuetify 2). This differentiates it from the regular input. In the parent component, we capture the event, define a query property, update it when appropriate, and reset the pagination to its default value, since we will be requesting page one again. We also have to update our request parameters by adding q (as recognized by JSON Placeholder).
// file: PaginatedAutocomplete.vue <template> <v-autocomplete :items="items" :loading="loading" :menu-props="{ maxWidth: 300 }" class="autocomplete" clearable no-filter v-model:search-input="search" @update:search="emitSearch" > <template #append-item> <template v-if="!!items.length"> <div v-if="!loading" v-intersect="handleIntersection" /> <div v-else class="px-4 py-3 text-primary">Loading more...</div> </template> </template> </v-autocomplete> </template> <script setup> import { ref } from 'vue'; defineProps({ items: { type: Array, required: true }, loading: { type: Boolean, required: false } }); const emit = defineEmits(['intersect', 'update:search-input']); function handleIntersection(isIntersecting) { if (isIntersecting) { emit('intersect'); } } const search = ref(null); function emitSearch(value) { emit('update:search-input', value); } </script>
// file: App.vue <script> // ... const query = ref(null); function handleSearchInput(value) { query.value = value; pagination.value = Object.assign({}, { page: 1, perPage: 10, total: 100 }); loadRecords(); } onMounted(() => { loadRecords(); }); </script> <template> <main> <PaginatedAutocomplete :items="records" :loading="loading" @intersect="loadNextPage" @update:search-input="handleSearchInput" /> </main> </template>
// file: App.vue // ... function loadRecords() { loading.value = true; const params = { _start: (pagination.value.page - 1) * pagination.value.perPage, _limit: pagination.value.perPage, q: query.value }; // ...
If you try the search now and pay attention to the network tab in developer tools, you will notice that a new request is fired off with each keystroke. While our current dataset is small and loads quickly, this behavior is not suitable for real-world applications. Larger datasets can lead to slow loading times, and with multiple users performing searches simultaneously, the server could become overloaded. Fortunately, we have a solution in the Lodash library, which contains various useful JavaScript utilities. One of them is debouncing, which allows us to delay function calls by leaving us some time to call the same function again. That way, only the latest call within a specified time period will be triggered. A commonly used delay for this kind of functionality is 500 milliseconds. We can install Lodash by running the command npm install lodash. In the import, we only reference the part that we need instead of taking the whole library.
// file: PaginatedAutocomplete.vue // ... import debounce from 'lodash/debounce'; // ...
// file: PaginatedAutocomplete.vue // ... const debouncedEmit = debounce((value) => { emit('update:search-input', value); }, 500); function emitSearch(value) { debouncedEmit(value); } // ...
Now that’s much better! However, if you experiment with various searches and examine the results, you will find another issue - when the server performs the search, it takes into account not only post titles, but also their bodies and IDs. We don’t have options to change this through parameters, and we don’t have access to the back-end code to adjust that there either. Therefore, once again, we need to do some tweaking of our own code by filtering the response data. Note that in a real project, you would discuss this with your back-end colleagues. Loading unused data isn’t something you would ever want!
// file: App.vue // ... .then((response) => { const recordsToAdd = response.data.filter((post) => post.title.includes(params.q || '')); if (pagination.value.page === 1) { records.value = recordsToAdd; pagination.value.total = response.headers['x-total-count']; } else { records.value = [...records.value, ...recordsToAdd]; } }) // ...
To wrap up all the fundamental functionalities, we need to add record selection. This should already be familiar to you if you’ve worked with Vuetify before. The property selectedRecord is bound to model-value (or just value in Vuetify 2). We also need to emit an event on selection change, @update:model-value, (Vuetify 2: @input) to propagate the value to the parent component. This configuration allows us to utilize v-model for our custom component.
Because of how Vuetify’s autocomplete component works, both record selection and input events are triggered when a record is selected. Usually, this allows more customization options, but in our case it’s detrimental, as it sends an unnecessary request and replaces our list with a single record. We can solve this by checking for selected record and search query equality.
// file: App.vue // ... function handleSearchInput(value) { if (selectedRecord.value === value) { return; } query.value = value; pagination.value = Object.assign({}, { page: 1, perPage: 10, total: 100 }); loadRecords(); } const selectedRecord = ref(null); // ...
<!-- file: App.vue --> <template> <main> <PaginatedAutocomplete v-model="selectedRecord" :items="records" :loading="loading" @intersect="loadNextPage" @update:search-input="handleSearchInput" /> </main> </template>
<!-- file: PaginatedAutocomplete.vue --> <!-- ... --> <v-autocomplete :items="items" :loading="loading" :menu-props="{ maxWidth: 300 }" :model-value="selectedItem" class="autocomplete" clearable no-filter v-model:search-input="search" @update:model-value="emitSelection" @update:search="emitSearch" > <!-- ... -->
// file: PaginatedAutocomplete.vue // ... const emit = defineEmits(['intersect', 'update:model-value', 'update:search-input']); function handleIntersection(isIntersecting) { if (isIntersecting) { emit('intersect'); } } const selectedItem = ref(null); function emitSelection(value) { selectedItem.value = value; emit('update:model-value', value); } // ...
Almost done, but if you are thorough with your testing, you will notice an annoying glitch - when you do a search, scroll down, then do another search, the dropdown scroll will remain in the same place, possibly causing a chain of new requests in quick succession. To solve this, we can reset the scroll position to the top whenever a new search input is entered. In Vuetify 2, we could do this by referencing the internal v-menu of v-autocomplete, but since that’s no longer the case in Vuetify 3, we need to get creative. Applying a unique class name to the menu allows us to select it through pure JavaScript and then follow up with necessary adjustments.
<!-- file: PaginatedAutocomplete.vue --> <!-- ... --> <v-autocomplete ... :menu-props="{ maxWidth: 300, class: `dropdown-${uid}` }" ... > <!-- ... -->
// file: PaginatedAutocomplete.vue // ... const debouncedEmit = debounce((value) => { emit('update:search-input', value); resetDropdownScroll(); }, 500); function emitSearch(value) { debouncedEmit(value); } const uid = Math.round(Math.random() * 10e4); function resetDropdownScroll() { const menuWrapper = document.getElementsByClassName(`dropdown-${uid}`)[0]; const menuList = menuWrapper?.firstElementChild?.firstElementChild; if (menuList) { menuList.scrollTop = 0; } } // ...
There we have it, our custom autocomplete component with server side filtering and pagination is now complete! It was rather simple in the end, but I’m sure you would agree that the way to the solution was anything but with all these little tweaks and combinations we had to make.
If you need to compare anything with your work, you can access the source files through a GitHub repository here.
概述與結論
旅程不必在這裡結束。如果您需要進一步定制,可以搜尋 Vuetify 文件以獲取想法。有無數的可能性等待探索。例如,您可以嘗試一次使用多個值。 Vuetify 已經支援這一點,但可能需要進行額外的調整才能與我們的解決方案結合。儘管如此,這在許多項目中仍然是有用的。或者,您可以嘗試模板自訂。您可以重新定義選擇範本、清單範本等的外觀和風格。這為製作與您的專案設計和品牌完美契合的使用者介面打開了大門。
除此之外還有很多其他選擇。事實上,可用的自訂深度保證了創建額外的文章來全面涵蓋這些高級主題。最後,Vue + Vuetify 堆疊並不是唯一支援此類功能的堆疊。如果您使用其他框架,我鼓勵您嘗試自行開發與此等效的框架。
總之,我們將一個基本元件轉變為適合我們需求的專業解決方案。您現在已經為自己配備了一個可應用於各種專案的多功能工具。每當您發現自己正在處理大量記錄清單時,伺服器端分頁和過濾解決方案就會成為您的首選策略。它不僅從伺服器的角度優化了效能,還確保為使用者提供更流暢的渲染體驗。透過一些調整,我們解決了一些常見問題,並為進一步調整開闢了新的可能性。
以上是如何調整自動完成/選擇欄位以與伺服器端過濾和分頁一起使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。
