Element UI used in the background system of the company has hundreds of El-tables, which are directly used for historical reasons. All of a sudden, the product says the table needs to be able to customize columns, give the user control over how columns are visible, fixed, and sorted, and preferably persist. So I have to do a secondary package to fix it, and then I’ll just slightly enhance it.

Demo: element – plus – table – proxy – Demo

Source: aweikalee/element – plus – table – proxy – demo

The company uses Vue 2 + Element UI, but I’m going to use Vue 3 + Element Plus. It’s the same idea, just a little bit different. *

The main idea

For the secondary encapsulation of el-Table, I hope it is:

  1. Does not affect existing tables (it is not possible to change all tables at once in the transition phase).
  2. Preserve as much as possibleel-tableFlexibility in itself.
  3. Enhance the table functionality while moving the original code as little as possible.

For point 1, keep the el-Table component and create a new component, MyTable, where all changes are made.

For the second point, the props(attrs) and slots accepted by MyTable should be the same as those accepted by El-Table, and all should be passed to el-Table.

Therefore, the adjustment plan is as follows:

<! -- Before adjustment -->
<el-table :data="data">
  <el-table-column prop="name" label="Name" />
  <! El-table-column -->
</el-table>

<! -- After adjustment -->
<MyToolbar :table="table" />
<MyTable :data="data" :ref="table">
  <el-table-column prop="name" label="Name" />
  <! El-table-column -->
</MyTable>
Copy the code

The newly wrapped component MyTable does a simple job of reordering, filtering, and modifying the properties of slots to generate a new slot for el-Table to handle.

MyTable gives MyToolbar the interface to expose and modify column data. (You can also wrap MyToolbar in MyTable)

MyTable component implementation

The basic structure

First, the template section (of course you can use Render /JSX instead). Vue passes all unrecognized attributes to the outermost tag by default, so we just need to pass a new slot.

<el-table>
  <children />
</el-table>
Copy the code

Children is the new slot we implemented, which is a child component created inside MyTable. This, like slot.default, is a function that returns VNode.

const slotsOrigin = useSlots()
const children = () = > slotsOrigin.default?.()
Copy the code

Note: Setup syntax is used

At this point, a secondary encapsulation that retains all the functionality of the El-Table is complete. We just need to add a billion more details to perfect it.

Classify the VNode

The VNodes we get from slot will have something more than what we want, so we need to sort them.

The processing of vNodes for el-table-column will be identified by the prop property. Columns without prop properties will not be treated as custom columns.

Vnodes will be divided into three classes:

  1. el-table-columnAnd there arepropProperties of the
  2. el-table-columnBut there is nopropProperty, butfixed="left"
  3. The rest of theel-table-columnOr do not knowVNode

Category 2, you could go into category 3, but I think it’s more practical to divide it into three categories.

const slotsOrigin = useSlots()

/* Classifies slot */
const slots = computed(() = > {
  const main = [] / / first class
  const left = [] / / 2 classes
  const other = [] / / class 3

  slotsOrigin.default?.()?.forEach((vnode) = > {
    if (isElTableColumn(vnode)) {
      // Is the el-table-column component

      const { prop, fixed } = vnode.props ?? {}

      // There is a prop attribute, which belongs to class 1
      if(prop ! = =undefined) return main.push(vnode) 

      // There is no prop property, but fixed="left", belongs to class 2
      if (fixed === 'left') return left.push(vnode)
    }

    // Other items belong to category 3
    other.push(vnode)
  })

  return {
    main,
    left,
    other,
  }
})

/* Determines whether the Vnode is an el-table-column component */
function isElTableColumn(vnode) {
  return (vnode.type asComponent)? .name ==='ElTableColumn'
}

/* The sorted slots are mounted */ in the following order
const children = () = > [slots.value.left, slots.value.main, slots.value.other]
Copy the code

Collect column data

The primary source of column data is slot.main. Therefore, we need to extract the attributes and order we need from VNode.

const columns = reactive({
  slot: computed(() = > 
    slots.value.main.map(({ props }) = > ({
      prop: props.prop, / / logo
      label: props.label, / / column name
      fixed: props.fixed, // Fixed position
      visiable: props.visiable ?? true // Whether it is visible
    })),
    
    storage: []),})Copy the code

El-table-column’s original attributes, except unavailable. Columns. Slot stores only the original column data. The changes to columns need to be stored in another location and will be persisted later, so they are stored in columns.

Provides a method to modify columns. Storage externally.

function updateColumns(value) {
  columns.storage = value
}
Copy the code

Merge column data

Now we have two columns. Slot and columns. Storage. Considering persistent storage, the information of stored columns may be inaccurate (e.g. columns are added/deleted later).

const columns = reactive({
  // Others are omitted as above

  render: computed(() = > {
    const slot = [...columns.slot]
    const storage = [...columns.storage]

    const res = []
    storage.forEach((props) = > {
      const index = slot.findIndex(({ prop }) = > prop === props.prop)
      if (~index) {
        constpropsFromSlot = slot[index] res.push({ ... propsFromSlot,// It is possible to add attributes, so use slot data as a base. props, }) slot.splice(index,1) // Column does not exist in storage
      }
      // Slots that are not found are filtered out}) res.push(... slot)return res
  })
})
Copy the code

Generate a new VNode

Now that you’ve done your homework, you need to create slots for the el-table.

Instead of slots.main, we need to create refactorSlot with the data from Columns. Render.

const refactorSlot = computed(() = > {
  const { main } = slots.value

  const refactorySlot = []

  columns.render.forEach(({ prop, visiable, fixed }) = > {
    // Set invisible to skip (i.e., not render)
    if(! visiable)return

    // Find the VNode corresponding to prop from slot. main
    const vnode = main.find((vnode) = >prop === vnode.props? .prop)if(! vnode)return

    // Clone VNode and modify some properties
    const cloned = cloneVNode(vnode, {
      fixed,
      // You can modify the properties as required, very flexible
    })

    refactorySlot.push(cloned)
  })

  return refactorySlot
})
Copy the code

Finally, all slots are merged to complete the creation of children

const children = () = > [slots.value.left, refactorSlot.value, slots.value.other]
Copy the code

Expose interfaces

<el-table ref="table">
  <children />
</el-table>
Copy the code
const table = ref()
defineExpose({
  // Provide access to el-table
  table,

  // Column data
  columns: computed(() = > readonly(columns.render)),

  // Modify column data (full coverage required)
  updateColumns(value) {
    columns.storage = value
  }
})
Copy the code

At this point, our main structure is complete, the complete code can be viewed at aweikalee/element-plus-table-proxy-demo.

Additional functionality

The next step is to add functionality.

Implementation of the MyToolbar component

MyTable provides columns and updateColumns to display, fix, and sort custom columns as required. I’m not going to go into details because I can implement it either way. Aweikalee /element-plus-table-proxy-demo has a simple implementation for reference.

Column data is stored persistently

Columns. Storage is initialized from localStorage and modified to localStorage.

// Implement a simple version.
const storage = {
  set(key, value) {
    localStorage.setItem(key, JSON.stringify(value))
  },

  get(key) {
    try {
      return JSON.parse(localStorage.getItem(key))
    } catch (error) {
      return}}}Copy the code
const columnsFormStorage = ref(
  storage.get('columns')?? [])const columns = reactive({
  // Others are omitted

  storage: computed({
    get() {
      return columnsFormStorage.value
    },
    set(value) {
      columnsFormStorage.value = value
      storage.set('columns', value)
    }
  })
})
Copy the code

Stroage.get (‘columns’) does not store the columns separately. You can add an attribute name to MyTable, which can be used as an identifier to distinguish between storage and reading.

Of course, columns are set up to be stored on the server, which means that the storage is asynchronous. When the request is read, it will be rendered once before the request is returned, and it will be rendered again after the request is returned, which needs special attention. I chose not to render children until the request was complete and used the loaded state instead. Uploads use anti-shake methods to reduce interaction with the server.

KeepAlive preserves the scroll bar position

Although KeepAlive caches the DOM, it is removed from the document. The DOM leaving the document has no offsetTop, offsetLeft, offsetWidth, offsetHeight, scrollTop, scrollWidth, scrollHeight, clientWidth, ClientHeight, which is also 0.

The most affected in KeepAlive are scrollTop and scrollLeft, which cannot be recovered even if re-added to the document. So we need to save them before we leave the document and re-assign the saved values to the DOM after we add them back to the document.

Here are two methods.

Methods a

Listen for the DOM scroll event, which records the current scroll position. The DOM is then reassigned at onActivated.

<el-table ref="table"></el-table>
Copy the code
const table = ref()
const scrollRef = computed(() = > {
  // The container to scroll in el-table
  returntable.value? .$refs.bodyWrapper }) useKeepScroll(scrollRef)Copy the code
function useKeepScroll(el) { // This is a ref object
  let scrollTop = 0
  let scrollLeft = 0

  /* Save the scrollbar position */
  function save() {
    if(! el.value)return

    scrollTop = el.value.scrollTop
    scrollLeft = el.value.scrollLeft
  }

  /* Restore the scrollbar position */
  function restore() {
    if(! el.value)return

    el.value.scrollTop = scrollTop
    el.value.scrollLeft = scrollLeft
  }

  /* Restore the scrollbar position when the component resumes */
  onActivated(restore)

  /* Add and remove scroll listener */
  let listenedEl = null
  function removeEventListener() { listenedEl? .removeEventListener('scroll', save)
    listenedEl = null
  }
  function addEventListener() {
    if(! el.value)return
    if (listenedEl === el.value) returnremoveEventListener() listenedEl = el.value listenedEl? .addEventListener('scroll', save)
  }

  watch(el, addEventListener)
  onActivated(addEventListener)
  onDeactivated(removeEventListener)
}
Copy the code

Method 2

KeepAlive provides onDeactivated for us, but it is defined as the lifetime of DOM deactivated, so onDeactivated runs when the DOM is removed from the document.

We would probably prefer onBeforeDeactivate, but unfortunately, the RFC is not yet implemented.

The current alternative is a bit of a cheat.

function useKeepScroll(el) {
  let scrollTop = 0
  let scrollLeft = 0

  function save() {
    if(! el.value)return

    scrollTop = el.value.scrollTop
    scrollLeft = el.value.scrollLeft
  }
  function restore() {
    if(! el.value)return

    el.value.scrollTop = scrollTop
    el.value.scrollLeft = scrollLeft
  }

  onActivated(restore) / / recovery
  onDeactivated(save) / / save
}
Copy the code

Now comes the key!

<Transition>
  <KeepAlive>
    <! -->
  </KeepAlive>
</Transition>
Copy the code

Find a place to use KeepAlive and cover it with a Transition component. OnDeactivated is the same as onBeforeDeactivate.

If your project has only one KeepAlive, this is a good solution.

Here’s how it works:

The KeepAlive component’s deactivate method removes the DOM from the document before creating a microtask that calls the component’s onDeactivated. If transition exists on a VNode, removing it will become a macro task, which will then be removed from the document by executing onDeactivated from the microtask.