Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 22, 2023 09:18 am GMT

table: make columns resizable

In this series, we will make our table columns resizable. This time I won't show everything step by step. You can take this as an challenge to do on your own, and watch my solution if you get stuck. Maybe you'll come up with better solution!

The code before and after

How to add a feature

We've been making column swappable. Now, we want to add another feature "resizable". Can they work together? Do we need to change other code to adopt this feature? We probably don't have straight answers until really implement it. But asking these questions to ourselves can help us make better decisions.

To resize a column, we need to add a handle for user to interact with. So besides column name, there should be a resize handler. This means the structure of the column will be different than createTableHead which only render column name. we will need to make a new function to create this structure.

function createResizableTableHead(columns, options = {}) {  const {    // settings for individual column's min width    columnsMinWidth,    // settings for individual column's label     // e.g. the column key from database is firstName    // but we won't show that directly to end user    // so we make a mapping like 'firstName' -> 'first name'    columnsLabel = {}  } = options  const thead = document.createElement('thead')  const tr = document.createElement('tr')  columns.forEach(columnKey => {    const th = document.createElement('th')    // save column name on the element,    // so we know which column a element represents    th.dataset.columnKey = columnKey    // set display: flex; on `th` would break    // the table layout algorithm    // so we need a wrapper `div` to do that    const wrapper = document.createElement('div')    wrapper.classList.add('resizable-column-wrapper')    th.appendChild(wrapper)    const content = document.createElement('div')    const resizeHandle = document.createElement('div')    content.classList.add('resizable-column-content')    resizeHandle.classList.add('resizable-column-handle')    // here we simply add text to a div    // but we can also make a column formatter    // similar to what we did for `createTableRow`    // if we need more complex markup    content.textContent = columnsLabel[columnKey] || columnKey    wrapper.append(content, resizeHandle)    tr.appendChild(th)  });  makeColumnsResizable(tr, { columnsMinWidth })  thead.appendChild(tr)  return thead}

The overall structure of makeColumnsResizable is very similar to makeColumnsSwappable. We would want them to work together. We would also want to make the logic consistent.

function makeColumnsResizable(columnsContainer, options = {}) {  const {    // elements to resize together with the target column    elementsToPatch = [],    // setting of each column min width    columnsMinWidth = {},    // default value of each column min width    DEFAULT_MIN_COLUMN_WIDTH = 100  } = options  columnsContainer.classList.add('resizable-columns-container')  const _elementsToPatch = [columnsContainer, ...elementsToPatch]  const columnElements = [...columnsContainer.children]  columnElements.forEach((column) => {    column.classList.add('resizable-column')    const minWidthSetting = columnsMinWidth[column.dataset.columnKey]    if (minWidthSetting) {      // set width does not work on table because      // it has built-in layout algorithm      column.style.minWidth = minWidthSetting + 'px'      // we are still setting width because      // `makeColumnsResizable` is not made specifically for table      column.style.width = minWidthSetting + 'px'    }  })  columnsContainer.addEventListener('pointerdown', e => {    // because we use event delegation pattern,    // `e.target` could be other irrelevant elements    // so we need to make sure that the event    // is triggered by a resize handle    const resizeHandle = e.target.closest('.resizable-column-handle')    if (!resizeHandle)      return    // stop event propagation so we don't trigger resize    // and swap at the same time. this is used with    // { capture: true } to make sure this event handler has     // higher priority and don't propagate to others.    // it is also possible to use e.stopImmediatePropagation()    // in this case because this event listener of 'pointerdown'    // is added before the one from `makeColumnsSwappable`    e.stopPropagation()    const column = e.target.closest('.resizable-column')    const indexOfColumn = [...columnsContainer.children].indexOf(column)    const minColumnWidth = columnsMinWidth[column.dataset.columnKey] || DEFAULT_MIN_COLUMN_WIDTH    // prevent text selection when moving columns    document.addEventListener('selectstart', preventDefault)    const initialColumnWidth = parseFloat(getComputedStyle(column).width)    const initialCursorX = e.clientX    // elements that are in the same column    const elementsToResize = _elementsToPatch.map((columnsContainer) => {      return columnsContainer.children[indexOfColumn]    })    // calculate how much to resize    function handleMove(e) {      const newCursorX = e.clientX      const moveDistance = newCursorX - initialCursorX      let newColumnWidth = initialColumnWidth + moveDistance      // we don't want to resize column width below its      // minimal value so if `newColumnWidth` is lower than      // `minColumnWidth` we want to use `minColumnWidth`, which       // value would be the "bigger" one of Math.max()      newColumnWidth = Math.max(newColumnWidth, minColumnWidth)      // if we need to frequently update UI, use      // `requestAnimationFrame` to make it optimal      requestAnimationFrame(() => {        elementsToResize.forEach((element) => {          element.style.minWidth = newColumnWidth + 'px'          element.style.width = newColumnWidth + 'px'        })      })    }    document.addEventListener('pointermove', handleMove)    // clean up event listeners    document.addEventListener('pointerup', e => {      document.removeEventListener('pointermove', handleMove)      document.removeEventListener('selectstart', preventDefault)      // this clean up listener only needs to run once      // after 'pointerdown'    }, { once: true })    // capture of 'pointerdown' is used with e.preventDefault()    // as mentioned above  }, { capture: true })}

If you don't know what is event propagation and event delegation, and what e.stopPropagation() and { capture: true } do. Check out my addEventListener tutorial may help.

Some CSS for resizable column.

.resizable-column-wrapper {  /* make its content layout horizontally */  display: flex;}.resizable-column-content {  /* makes this element can grow and shrink      within the free space of flexbox */  flex: 1;  text-align: left;  padding: 8px;}.resizable-column-handle {  width: 20px;  background: rgba(255, 0, 0, 0.154);  cursor: col-resize;  /* makes this element not to shrink */  flex-shrink: 0;}

Update createTable to be able to create a table of resizable and swappable columns through options.

import { makeColumnsResizable } from "./makeColumnsResizable.js"import { makeColumnsSwappable } from "./makeColumnsSwappable.js"export function createTable(columns, dataList, options = {}) {  const {    columnFormatter = {},    resizeOptions = {},    swapOptions = {},  } = options  const table = document.createElement('table')  // create resizable structure if resize option is enable  const thead = resizeOptions.enable    ? createResizableTableHead(columns, resizeOptions)    : createTableHead(columns)  const tbody = createTableBody(columns, dataList, columnFormatter)  table.append(thead, tbody)  // make columns swappable if swap option is enable  if (swapOptions.enable) {    const columnsContainer = table.querySelector('thead').firstElementChild    const elementsToPatch = table.querySelectorAll('tbody > tr')    makeColumnsSwappable(columnsContainer, elementsToPatch)  }  return table}

We can create a table like this.

// const users = [...]// const nameOfDefaultColumns = [...]// const columnFormatter = [...]createTable(nameOfDefaultColumns, users, {  columnFormatter,  resizeOptions: {    enable: true,    columnsMinWidth: {      id: 50    }  },  swapOptions: {    enable: true,  },})

There are more to consider

The code above should already work, but it doesn't work well. We can resize columns. What happens if total columns width is larger than the container. We need to make some changes to makeColumnsSwappable.

We would want ghost to stay within the container.

function getGhostBoundary() {...}

What about scroll? We need a way to scroll the container.

function getScrollContainerFunc() {...}

And decide when to scroll. moveGhost handles the logic that ghost touches the edges, so we can also make it start the scroll.

function moveGhost() {...}

Then, how to swap after auto scrolling? Some code from handleMove is doing that job. We need to extract it out to reuse it.

function handleSwap() {...}

Do we want to pass handleSwap all the way down to getScrollContainerFunc from createGhostColumn? Or there is other way to do it? I think handleSwap is more reasonable to be called directly inside makeColumnsSwappable. So I use a custom event to notify the universe that I just auto scroll.

// inside `startScroll` const event = new CustomEvent('custom:autoscroll', {  detail: {    direction,    predicateGhostEdgeX  }})ghost.dispatchEvent(event)

We can addEventListener to ghost on that custom event inside makeColumnsSwappable to handleSwap. This flow is more clear to me.

// inside `makeColumnsSwappable`ghost.element.addEventListener('custom:autoscroll', (e) => {  const { direction, predicateGhostEdgeX } = e.detail  handleSwap({    isMoveToLeft: direction === 'left',    isMoveToRight: direction === 'right',    ghostLeft: predicateGhostEdgeX.left,    ghostRight: predicateGhostEdgeX.right  })})

There are other pieces to consider, but these are the big picture.

The result of my solution. The ghost should stay within the content box area of the container. Auto scroll should swap as well.

as description

Note that we don't need to pass elementsToPatch to makeColumnsResizable in this case, because browser will adjust columns width with its table algorithm. Another thing to note is that the table algorithm prevent the content to overflow. Resizing to a width that is too small won't take effect. To make makeColumnsResizable also work on other tags e.g. a bunch of div, The content of columns should be aware of potential overflow and handled with line clamp.

I have added as much as possible comments to the source code for this section. If you are still confused, feel free to leave a comment below.


Original Link: https://dev.to/gohomewho/table-make-columns-resizable-2l3h

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To