Search list tools component

This article is based on the Vue technology stack.

background

The business function of each search list page is essentially the same.

Functions to be implemented:

  1. Search using search form data, click search button according to the search form data, reset the page number to 1 for data query;
  2. Click the reset button to restore the form to the default value and reset the page number to 1 for data query;
  3. Page flipper provides different page numbers to jump.

However, during the development process, it is common to find some bad interactive experiences on the history page:

  1. When you enter the details page from the list page and return to the search list page, the search criteria and page number are lost.
  2. After modifying an element in the search list in the form of a pop-up list, the page is refreshed, and the search criteria and page number are lost.
  3. The content of the search form was modified, but the page was turned without searching. The new data in the search form was used when the page was turned, resulting in empty data after the page was turned.

I do not know whether you have experienced these kinds of bad experience. The following part of this paper will first put forward solutions to these adverse experiences; Design a common component to implement the common functionality of the search list page and a way to resolve the bad experience.

Resolve bad experiences

When you enter the details page from the list page and return to the search list page, the search criteria and page number are lost.

Most of you, like me before, use the Vue native component keepAlive to cache instances of search list pages. Go to the details page from the list page and re-render the keepAlive cached search list instance when you return. In fact, however, not all scenarios use cached search list pages. For example: enter an edit page from the search list page and modify the field displayed on the owning list page. When the hold returns, the search list page needs to search again according to the previous search criteria and page number.

To solve the above problem, many online solutions combine the dynamic values of keepAlive include and exclude with routing. I have tried, but this way makes me uncomfortable. I wonder if there are any students who have the same feelings with me. Another slightly hacky solution is to find the keepAlive cache instance object and destory it when the cache is no longer needed.

There is a common problem with both keepAlive uses. In a scenario where you go from the search list page to the edit page, save the edit, and return to the search list page, a good user experience is to keep the search criteria and page numbers and other states, but update the modified data in the list. Whether the keepAlive include and exclude dynamic values are used to delete the cache instance or the keepAlive cache object is directly deleted, the search criteria and page numbers will be lost.

To this end, I compromised the age-old solution of hardening search terms and page numbers into the browser’s access path. Is part of search as a browser link. So that when the page comes back, you can do a new search based on the data on the link.

After modifying an element in the search list in the form of a pop-up list, the page is refreshed, and the search criteria and page number are lost.

In the old days, after the changes were successful, the data in the list was modified accordingly. But if the search terms and page numbers are already fixed to browsing, a data re-query is a good solution.

The content of the search form was modified, but the page was turned without searching. The new data in the search form was used when the page was turned, resulting in empty data after the page was turned.

This solution is also simple, separating the bidirectional binding data of the form elements from the search criteria, that is, caching the read form data in a variable when the user clicks search. The cached data is used when the search list page is page-turning, and the cached data is resynchronized to the form to ensure that the search conditions are consistent with the data displayed in the form.

Implementation of common components

There is so much work to be done on a search list page that it would be crazy to do it on every search list page. A common solution must be extracted to ensure that each search list page behaves consistently.

In the early days, I used mixin’s scheme, but it was a demon that seriously affected code readability.

In the end, I chose to use a common component where I put the common business logic for all the search list pages. The ideas come from the React business container component and the Element-UI tabel component.

The implementation code is as follows:

<template>
  <div class="list-page">
    <slot
      :formData="searchFormData"
      :listData="listData"
      :search="search"
      :refresh="refresh"
      :resetForm="resetForm"
      :loading="loading"
    >
      <div>Query form</div>
      <div>form</div>
    </slot>
    <el-pagination
      v-if="totalSize > 0"
      :page-size="10"
      :total="totalSize"
      :current-page.sync="currentPage"
      @current-change="pageJump"
    />
  </div>
</template>
<script>
const cloneDeep = require('clone-deep')
function removeUrlQuery(url, key) {
  const urlObj = new URL(url)
  const reg = new RegExp(`? [&]${key}* `? = [^ & $].'g')
  let search = urlObj.search.replace(reg, ' ')
  if(search && ! search.startsWith('? ')) {
    search = '? ' + search
  }
  urlObj.search = search
  return urlObj.href
}
function cureDataToUrl(key, data) {
  let url = window.location.href
  url = removeUrlQuery(url, key)
  if(! data) {return
  }
  const urlObj = new URL(url)
  const query = `${key}=The ${encodeURIComponent(JSON.stringify(data))}`
  urlObj.search += (urlObj.search.startsWith('? ')?'&' : '? ') + query
  url = urlObj.href
  history.pushState({ url: url }, ' ', url)
}
export default {
  name: 'SearchListTool'.props: {
    ifGetDataImmediate: {
      type: Boolean.default: true
    },
    requestListMethod: {
      type: Function.required: true
    },
    defaultFormData: {
      type: Object.default() {
        return{}}}},data() {
    const {
      formData = this._getDefaultFormData(),
      currentPage = 1
    } = this._getSearchDataCacheFromUrl() || {}
    return {
      searchFormData: formData,
      currentPage: currentPage,
      listData: null.totalSize: 0.loading: false}},created() {
    if (this.ifGetDataImmediate) {
      this.getData()
    }
  },
  methods: {
    _cacheSearchData() {
      this._searchDataCache = {
        formData: cloneDeep(this.searchFormData),
        currentPage: this.currentPage
      }
    },
    _cureSearchDataToUrl() {
      cureDataToUrl('s'.this._searchDataCache)
    },
    _syncSearchFormData() {
      this.searchFormData = this._searchDataCache.formData
    },
    _getSearchDataCacheFromUrl() {
      let { s: searchData } = this.$route.query
      if (searchData) {
        try {
          searchData = JSON.parse(searchData)
        } catch (error) {
          console.log(error)
        }
      }
      return searchData
    },
    search() {
      this.currentPage = 1
      this.getData()
    },
    pageJump() {
      this._syncSearchFormData()
      this.getData()
    },
    refresh() {
      this.pageJump()
    },
    resetForm() {
      this.searchFormData = this._getDefaultFormData()
      this.currentPage = 1
      this.totalSize = 0
      this.loading = false
      this.getData()
    },
    async getData() {
      this._cacheSearchData()
      this._cureSearchDataToUrl()
      const { formData = {}, currentPage = 1 } = this._searchDataCache || {}
      this.loading = true
      const request = this.requestListMethod(formData, currentPage)
      if (Object.prototype.toString.call(request) ! = ='[object Promise]') {
        throw new Error('request-list-method must return Promise instance, please')}try {
        const { listData, totalSize } = await request

        if (listData.length === 0 && currentPage > 1) {
          this.currentPage = currentPage - 1
          this.getData()
          return
        }

        this.listData = listData
        this.totalSize = totalSize
      } catch (error) {
        this.listData = []
        console.log(error)
      }
      this.loading = false
    },
    _getDefaultFormData() {
      return cloneDeep(this.defaultFormData)
    }
  }
}
</script>


Copy the code

use

<template>
  <search-list-tool 
    :requestListMethod="requestListMethod"
    ref="searchListTool"
    v-slot="{formData, search, listData, loading, resetForm}">
    <el-form>
      <el-form-item label="Element 1:">
        <el-input v-model="formData.input1"/>
      </el-form-item>
      <el-form-item label=Element 2: "">
        <el-input v-model="formData.input2"/>
      </el-form-item>
      <el-form-item label="Three elements:">
        <el-input v-model="formData.input3"/>
      </el-form-item>
      <el-button @click="search">The query</el-button>
      <el-button @click="resetForm">reset</el-button>
    </el-form>
    <el-table :data="listData" v-loading="loading">.</el-table>
  </search-list-tool>
</template>
<script>
import SearchListTool from '@/components/SearchListTool.vue'
export default {
  components: {
    SearchListTool
  },
  methods: {
    demoMethod() {
      this.$refs['searchListTool'].refresh()
    },
    requestListMethod(formData, pageNum) {
      // Assemble data
      // Initiate a request
      // return [Promise.resolve({listData, totalSize})]}... }... }</script>
Copy the code