te')); return $arr; } /* 遍历用户所有主题 * @param $uid 用户ID * @param int $page 页数 * @param int $pagesize 每页记录条数 * @param bool $desc 排序方式 TRUE降序 FALSE升序 * @param string $key 返回的数组用那一列的值作为 key * @param array $col 查询哪些列 */ function thread_tid_find_by_uid($uid, $page = 1, $pagesize = 1000, $desc = TRUE, $key = 'tid', $col = array()) { if (empty($uid)) return array(); $orderby = TRUE == $desc ? -1 : 1; $arr = thread_tid__find($cond = array('uid' => $uid), array('tid' => $orderby), $page, $pagesize, $key, $col); return $arr; } // 遍历栏目下tid 支持数组 $fid = array(1,2,3) function thread_tid_find_by_fid($fid, $page = 1, $pagesize = 1000, $desc = TRUE) { if (empty($fid)) return array(); $orderby = TRUE == $desc ? -1 : 1; $arr = thread_tid__find($cond = array('fid' => $fid), array('tid' => $orderby), $page, $pagesize, 'tid', array('tid', 'verify_date')); return $arr; } function thread_tid_delete($tid) { if (empty($tid)) return FALSE; $r = thread_tid__delete(array('tid' => $tid)); return $r; } function thread_tid_count() { $n = thread_tid__count(); return $n; } // 统计用户主题数 大数量下严谨使用非主键统计 function thread_uid_count($uid) { $n = thread_tid__count(array('uid' => $uid)); return $n; } // 统计栏目主题数 大数量下严谨使用非主键统计 function thread_fid_count($fid) { $n = thread_tid__count(array('fid' => $fid)); return $n; } ?>Javascript Sorting Nulls Wrong In HTML Table - Stack Overflow
最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Javascript Sorting Nulls Wrong In HTML Table - Stack Overflow

programmeradmin3浏览0评论

I'm using the below JavaScript code to sort my tables alphabetically and numerical. However, it puts rows with null values at the top instead of the bottom.

In the below image, taken from this URL I am working on, when sorting the table from biggest to smallest in the Rank Change column, nulls are at the top instead of the bottom.

*In this table, Null values are the cells with the NEW tag or a dash. *The problem applies to all columns/rows

Shouldn't Nulls be classed as less than 1 and sorted as such? What am I doing wrong?

Any help is really appreciated.

const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;

const parer = (idx, asc) => (a, b) => ((v1, v2) => 
    v1 !=='' && v2 !=='' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
    )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
    const table = th.closest('table');
    Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
        .sort(parer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
        .forEach(tr => table.appendChild(tr) );
})));

I'm using the below JavaScript code to sort my tables alphabetically and numerical. However, it puts rows with null values at the top instead of the bottom.

In the below image, taken from this URL I am working on, when sorting the table from biggest to smallest in the Rank Change column, nulls are at the top instead of the bottom.

*In this table, Null values are the cells with the NEW tag or a dash. *The problem applies to all columns/rows

Shouldn't Nulls be classed as less than 1 and sorted as such? What am I doing wrong?

Any help is really appreciated.

const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;

const parer = (idx, asc) => (a, b) => ((v1, v2) => 
    v1 !=='' && v2 !=='' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
    )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
    const table = th.closest('table');
    Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
        .sort(parer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
        .forEach(tr => table.appendChild(tr) );
})));
Share Improve this question edited Mar 23, 2020 at 10:20 Electron asked Mar 16, 2020 at 1:24 ElectronElectron 1,1091 gold badge9 silver badges23 bronze badges 4
  • Maybe I first need to convert all nulls to 0 first? – Electron Commented Mar 16, 2020 at 11:26
  • 1 or perhaps -1, and reserve 0 for 0 – Seabizkit Commented Mar 18, 2020 at 5:17
  • nulls are not a value. they are an object. – John Lord Commented Mar 21, 2020 at 1:02
  • Is there a reason you are appending child and not replacing them? – grodzi Commented Mar 23, 2020 at 7:29
Add a ment  | 

6 Answers 6

Reset to default 8 +50

You could take a check for null values first and the check both values for finiteness, like numbers or strings who are coercible to number and then take either the delta of the numbers or sort by string.

Examples:

 v1   v2  (v1 === null) - (v2 === null) isFinite(v1) && isFinite(v2)             result
---- ---- ----------------------------- ---------------------------------------- ------
null null       true -  true ->  0       true -> v1 - v2                             0
null abc        true - false ->  1                                                   1
null  2         true - false ->  1                                                   1
abc  null      false -  true -> -1                                                  -1
 2   null      false -  true -> -1                                                  -1
abc  abc       false - false ->  0      false -> v1.toString().localeCompare(v2)     0
abc   2        false - false ->  0      false -> v1.toString().localeCompare(v2)     1
 2   abc       false - false ->  0      false -> v1.toString().localeCompare(v2)    -1
 2    2        false - false ->  0       true -> v1 - v2                             0

Code:

const parer = (idx, asc) => (a, b) => ((v1, v2) =>
    (v1 === null) - (v2 === null) ||
    (isFinite(v1) && isFinite(v2)
        ? v1 - v2
        : v1.toString().localeCompare(v2)
    )
)(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

Working Example:

var array = [null, 2, 1, 20, 11, 'b', 'aaa', 'a', null];

array.sort((v1, v2) => 
    (v1 === null) - (v2 === null) ||
    (isFinite(v1) && isFinite(v2)
        ? v1 - v2
        : v1.toString().localeCompare(v2)
    )
);

console.log(...array);

Considering null as -Infinity should fix the sorting. I can suggest to use the asc property on the th element so you can avoid using 'this'.

// don't know if theres any other listeners on the th element so i clear them before use my code ( just for testing )
document.querySelectorAll('th').forEach((th, idx) => th.removeEventListener('click', () => {}, true));
document.querySelectorAll('th').forEach((th, idx) => th.addEventListener('click', (() => {
    const table = th.closest('table');
    th.asc = !th.asc;
    [...table.querySelectorAll('tr:nth-child(n+2)')]
        .sort((a, b) => +((th.asc ? a : b).children[idx].innerText || -Infinity) - +((th.asc ? b : a).children[idx].innerText || -Infinity))
        .forEach(tr => table.appendChild(tr));
})));

If it is NaN you should set the change to 0 Or +-Infinity depending where you want it top bottom or at 0 change. (the value change is applied only when paring)

I did the code expanded for easy understanding.

Check this parerNew and modify it to your needs.


const parerNew = (idx, asc) => (a, b) => ((v1, v2) => {      
    if (isNaN(v1)) {
      v1=0; // or Infinity 
    }
    if (isNaN(v2)) {
      v2=0; // or Infinity 
    }
    return v1 !=='' && v2 !=='' ? v1 - v2 : v1.toString().localeCompare(v2)
}

   )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
    const table = th.closest('table');
    Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
        .sort(parerNew(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
        .forEach(tr => table.appendChild(tr) );
  console.log("sorded");
})));

Here is an example which does not coerce null to -1 nor rely on tricking to infty which still allows you to put the null values to the end.

  • the loadData payload should reflect your json (I copied from it)
  • the sort handler arguably stores the last sort (asc/decs) on the th itself. You may obviously consider an array as an alternative should it pleases you

Straightfoward way

  • We store the rows in memory
  • We sort the rows (in memory)
  • We replot the table by recreating all tr/tds

function loadData () {
  return fetch('https://brendonbrewer./lbrynomics/subscriber_counts.json').then(r => r.json())
  return Promise.resolve({
    rank_change: [null, null, 5,    6, 8, 2],
    views_change: [null, 5,    null, 6, 7, 2],
  })
}
let rows = []
const myTable = document.querySelector('table')

function render(myTable, rows) {
  myTable.tBodies[0].innerHTML = ''
  rows.forEach(row => {
    const tr = document.createElement('tr')
    row.forEach(f => {
      const td = document.createElement('td')
      td.innerText = f == null ? '--' : f
      tr.appendChild(td)
    })
    myTable.tBodies[0].appendChild(tr)
  })
}

loadData().then(iRows => {
  // store the fields in order of your DOM nodes
  rows = iRows.views_change.map((_,i) => [iRows.views_change[i], iRows.rank_change[i]])
  render(myTable, rows)
})

// sort the rows
document.querySelector('table thead').onclick = e => {
  const th = e.target
  if (th.nodeName !== 'TH') { return }
  const order = th.getAttribute('data-sort') === '-1' ? -1 : 1
  th.setAttribute('data-sort', order * -1)
  const fieldIndex = [...th.parentNode.children].findIndex(other => other === th)
  const cmp = (r, s) => {
    const a = r[fieldIndex]
    const b = s[fieldIndex]
    return a === null && b === null ? 0 :
      a === null && b !== null ? 1 :
      b === null && a !== null ? -1 :
      a - b
  }
  rows.sort(order === 1 ? cmp : (a,b) => -1 * cmp(a,b))
  console.time('render')
  render(myTable, rows)
  console.timeEnd('render')
}
<table id="sub-stats">
  <thead> <!-- first fix your th. They go to thead. -->
    <tr>
      <th class="views-change">Views Change</th>  
      <th class="rank-change">Rank Change</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

Same but we reuse the existing rows

We basically map each row in memory to its DOM node (a tr that is). See rowToNode in code below

function loadData () {
  return fetch('https://brendonbrewer./lbrynomics/subscriber_counts.json').then(r => r.json())
  return Promise.resolve({
    rank_change: [null, null, 5,    6, 8, 2],
    views_change: [null, 5,    null, 6, 7, 2],
  })
}
let rows = []
let rowToNode = new Map()
const myTable = document.querySelector('table')

function init(myTable, rows) {
  const rowToNode = new Map()
  rows.forEach(row => {
    const tr = document.createElement('tr')
    row.forEach(f => {
      const td = document.createElement('td')
      td.innerText = f == null ? '--' : f
      tr.appendChild(td)
    })
    myTable.tBodies[0].appendChild(tr)
    rowToNode.set(row, tr)
  })
  return rowToNode
}

loadData().then(iRows => {
  // store the fields in order of your DOM nodes
  rows = iRows.views_change.map((_,i) => [iRows.views_change[i], iRows.rank_change[i]])
  rowToNode = init(myTable, rows)
})

// sort the rows
document.querySelector('table thead').onclick = e => {
  const th = e.target
  if (th.nodeName !== 'TH') { return }
  const order = th.getAttribute('data-sort') === '-1' ? -1 : 1
  th.setAttribute('data-sort', order * -1)
  const fieldIndex = [...th.parentNode.children].findIndex(other => other === th)
  const cmp = (r, s) => {
    const a = r[fieldIndex]
    const b = s[fieldIndex]
    return a === null && b === null ? 0 :
      a === null && b !== null ? 1 :
      b === null && a !== null ? -1 :
      a - b
  }
  rows.sort(order === 1 ? cmp : (a,b) => -1 * cmp(a,b))
  console.time('render')
  myTable.tBodies[0].innerHTML = ''
  const tpl = new DocumentFragment()
  rows.forEach(r => {
    tpl.appendChild(rowToNode.get(r))
  })
  myTable.tBodies[0].appendChild(tpl)
  console.timeEnd('render')
}
<!DOCTYPE html>
<html>
<body>
<table id="sub-stats">
  <thead> <!-- first fix your th. They go to thead. -->
    <tr>
      <th class="views-change">Views Change</th>  
      <th class="rank-change">Rank Change</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
</body>
</html>

A bit of refactoring

Global variables are not the best, the pesky variables rows and its associated rowToNode are not the more glamorous and get a scope bigger than they need.

A possible way is to make a Component which scopes internally those variables, and eventually put it in its own file. Being a bit funky we could write a WebComponent

class MyTable extends HTMLTableElement {
  constructor () {
    super ()
    this._shadowRoot = this.attachShadow({ 'mode': 'open' });
    this._rows = []
    this.rowToNode = new Map()
    const template = document.createElement('template')
    template.innerHTML = `
      <thead>
        <tr>
          <th class="views-change">Views Change</th>  
          <th class="rank-change">Rank Change</th>
        </tr>
      </thead>
      <tbody>
      </tbody>
      <style>th{cursor:pointer;}</style>
    `
    this._shadowRoot.appendChild(template.content)
    this.tbody = this._shadowRoot.querySelector('tbody')
    this.render();
    this._shadowRoot.querySelector('thead').onclick = this.handleSort.bind(this)
  }
  loadRows (rows) {
    this.rowToNode = new Map()
    this._rows = rows
    rows.forEach(row => {
      const tr = document.createElement('tr')
      row.forEach(f => {
        const td = document.createElement('td')
        td.innerText = f == null ? '--' : f
        tr.appendChild(td)
      })
      this.tbody.appendChild(tr)
      this.rowToNode.set(row, tr)
    })
  }
  render () {
    this.tbody.innerHTML = ''
    const tpl = new DocumentFragment()
    this._rows.forEach(r => {
      tpl.appendChild(this.rowToNode.get(r))
    })
    this._shadowRoot.querySelector('tbody').appendChild(tpl)
  }
  handleSort (e) {
    const th = e.target
    if (th.nodeName !== 'TH') { return }
    const order = th.getAttribute('data-sort') === '-1' ? -1 : 1
    th.setAttribute('data-sort', order * -1)
    const fieldIndex = [...th.parentNode.children].findIndex(other => other === th)
    const cmp = (r, s) => {
      const a = r[fieldIndex]
      const b = s[fieldIndex]
      return a === null && b === null ? 0 :
        a === null && b !== null ? 1 :
        b === null && a !== null ? -1 :
        a - b
    }
    this._rows.sort(order === 1 ? cmp : (a,b) => -1 * cmp(a,b))
    console.time('render')
    this.render()
    console.timeEnd('render')
  }
}
customElements.define('my-table', MyTable, { extends: 'table' })



function loadData () {
  return fetch('https://brendonbrewer./lbrynomics/subscriber_counts.json').then(r => r.json())
  return Promise.resolve({
    rank_change: [null, null, 5,    6, 8, 2],
    views_change: [null, 5,    null, 6, 7, 2],
  })
}
loadData().then(iRows => {
  // store the fields in order of your DOM nodes
  rows = iRows.views_change.map((_,i) => [iRows.views_change[i], iRows.rank_change[i]])
  document.querySelector('my-table').loadRows(rows)
})
<my-table id="sub-stats"></my-table>

Alternative to webponents

Finally if we don't want webponents but a crude object, we can make a crude object. Make some class which takes a (table) node as argument and operate on it the same way we did.

Below a code closer (DOM wise) to the live production.

class MyTable {
  constructor (node) {
    this.node = node
    this._rows = []
    this.rowToNode = new Map()
    const template = document.createElement('template')
    template.innerHTML = `
      <thead>
        <tr>
          <th class="ranks">Channel rank</th>
          <th class="vanity_names">Top 200 LBRY Channels</th>
          <th class="subscribers">followers</th>
          <th class="views">Content view</th>
          <th class="views_change">Views Change</th>  
          <th class="rank_change">Rank Change</th>
        </tr>
      </thead>
      <tbody>
      </tbody>
    `
    this.node.appendChild(template.content)
    this.tbody = this.node.querySelector('tbody')
    this.render();
    this.node.querySelector('thead').onclick = this.handleSort.bind(this)
  }
  loadRows (rows) {
    this.rowToNode = new Map()
    this._rows = rows
    rows.forEach(row => {
      const tr = document.createElement('tr')
      row.forEach(f => {
        const td = document.createElement('td')
        td.innerText = f == null ? '--' : f
        tr.appendChild(td)
      })
      this.tbody.appendChild(tr)
      this.rowToNode.set(row, tr)
    })
  }
  render () {
    this.tbody.innerHTML = ''
    const tpl = new DocumentFragment()
    this._rows.forEach(r => {
      tpl.appendChild(this.rowToNode.get(r))
    })
    this.tbody.appendChild(tpl)
  }
  handleSort (e) {
    const th = e.target
    if (th.nodeName !== 'TH') { return }
    const order = th.getAttribute('data-sort') === '-1' ? -1 : 1
    th.setAttribute('data-sort', order * -1)
    const fieldIndex = [...th.parentNode.children].findIndex(other => other === th)
    const cmp = (r, s) => {
      const a = r[fieldIndex]
      const b = s[fieldIndex]
      return a === null && b === null ? 0 :
        a === null && b !== null ? 1 :
        b === null && a !== null ? -1 :
        a - b
    }
    this._rows.sort(order === 1 ? cmp : (a,b) => -1 * cmp(a,b))
    console.time('render')
    this.render()
    console.timeEnd('render')
  }
}

function loadData () {
  return fetch('https://brendonbrewer./lbrynomics/subscriber_counts.json').then(r => r.json())
  return Promise.resolve({
    rank_change: [null, null, 5,    6, 8, 2],
    views_change: [null, 5,    null, 6, 7, 2],
  })
}
console.log('go')

const table = new MyTable(document.querySelector('table'))
table.render()
loadData().then(iRows => {
  // store the fields in order of your DOM nodes
  const fields = ['ranks', 'vanity_names', 'subscribers', 'views', 'views_change', 'rank_change']
  rows = iRows.views_change.map((_,i) => fields.map(f => iRows[f][i]))
  console.log('go')
  table.loadRows(rows)
})
<link href="https://lbrynomics./wp-content/themes/Divi/style.dev.css" rel="stylesheet"/>

<body class="page-template-default page page-id-12177 custom-background et-tb-has-template et-tb-has-footer et_pb_button_helper_class et_fullwidth_nav et_fixed_nav et_show_nav et_primary_nav_dropdown_animation_fade et_secondary_nav_dropdown_animation_fade et_header_style_left et_cover_background et_pb_gutter windows et_pb_gutters3 et_pb_pagebuilder_layout et_smooth_scroll et_no_sidebar et_divi_theme et-db gecko">
<div id="et-main-area">
<div id="main-content">
<div class="entry-content">
<div class="et-l et-l--post">
<div class="et_builder_inner_content et_pb_gutters3">
<div class="et_pb_section et_pb_section_0 et_pb_with_background et_section_regular">
<div class="et_pb_row et_pb_row_0">
<div class="et_pb_column et_pb_column_4_4 et_pb_column_0  et_pb_css_mix_blend_mode_passthrough et-last-child">
<div class="et_pb_module et_pb_code et_pb_code_0">
<div class="et_pb_module et_pb_tabs et_pb_tabs_0 et_slide_transition_to_1">
<div class="et_pb_tab et_pb_tab_1 clearfix et-pb-active-slide">
<div class="et_pb_tab_content">
<div class="et_pb_section et_pb_section_4 et_pb_with_background et_section_regular">
<div class="et_pb_row et_pb_row_3">
<div class="et_pb_column et_pb_column_4_4 et_pb_column_3  et_pb_css_mix_blend_mode_passthrough et-last-child">
<div class="et_pb_module et_pb_code et_pb_code_1">
<div class="et_pb_code_inner">


<table id="sub-stats"></table>









<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>

</body>

Your code looks unfriendly. It's hard to read. I make a little refactor and also fix your problem:

function getCellValue (tr, idx) {
  const val = tr.children[idx].innerText.trim() || tr.children[idx].textContent.trim()

  if (!val || val === '-' || val.toLowerCase() === 'new') {
    return null
  }

  return val
}

function parer (idx, asc) {
  asc = asc ? 1 : -1

  return function (a, b) {
    a = getCellValue(a, idx)
    b = getCellValue(b, idx)

    if (b === null) {
      return asc
    }

    if (a === null) {
      return -asc
    }

    if (isFinite(Number(a)) && isFinite(Number(b))) {
      return asc * (parseInt(a, 10) - parseInt(b, 10))
    }


    return asc * a.toString().localeCompare(b)
  }
}

I checked your site, and I think your rank changes are number. So getCellValue should return number only and pare with number. Please check the following code.

function getCellValue (tr, idx) {
    const val = tr.children[idx].innerText.trim() || tr.children[idx].textContent.trim()
    return !isNaN(val)?val:0;
}

function parer (idx, asc) {
    asc = asc ? 1 : -1

    return function (a, b) {
        a = getCellValue(a, idx)
        b = getCellValue(b, idx)
        return asc * ( a - b);
    }
}
发布评论

评论列表(0)

  1. 暂无评论