Grid sorting misbehaves for columns using value instead of / together with field
Version: cx 26.7.5 (reproduced on master)
Repro: litmus/bugs/grid-sorting-without-field.js (see PR #1313) — three grids, one per issue below.
Columns whose displayed content comes from a value selector (tpl/expr/computable) interact
badly with the sorting machinery, which mostly assumes a sort is identified by a field name.
Issue 1 — Sort indicator lights up on every field-less column
Setup: plain header-click sorting, no sortField/sortDirection/sorters bindings.
Two sortable columns defined with only a value (no field).
Actual: sorting itself works (the comparer correctly falls back to the value selector),
but after clicking one value-only column, both value-only headers get the sorted style and
icon (cxs-sorted-asc/desc).
Why: renderHeader decides "is this the sorted column" via
data.sorters[0].field == (hdwidget.sortField || hdwidget.field) (Grid.tsx ~line 1310).
For a value-only sorter both sides are undefined, so the check passes for every
field-less sortable column.
Issue 2 — With sortField/sortDirection bindings, value-only columns can't be sorted at all (and can crash the grid)
Setup: same columns, but the grid defines sortField-bind / sortDirection-bind.
Actual:
- If the bound values are initially
undefined, the grid crashes on first render with
TypeError: Cannot read properties of undefined (reading 'toLowerCase'). prepareData
fabricates a sorter { field: undefined, direction: undefined }, the header check from
Issue 1 matches it against any field-less column (undefined == undefined), and
direction.toLowerCase() throws.
- With initialized bindings, clicking a value-only header never sorts. Instead it clears
the sortField binding and merely toggles the direction of the previously active sort.
Why:
prepareData (Grid.tsx ~lines 920–952) rebuilds data.sorters from data.sortField alone,
discarding the value selector captured on header click. The fix-up loop that re-attaches
value/comparer/sortOptions can only find a column by matching c.field == sortField —
impossible when the column has no field. The resulting sorter has neither field nor value,
so the comparer selector is () => null and the order never changes.
- In
onHeaderClick (Grid.tsx ~line 1492), the current-sorter check falls back to
data.sortField when the column has no field, so a click on a value-only column always
"matches" the currently sorted field and toggles its direction instead of starting a new sort.
- Two smaller defects in the same fix-up loop: it iterates
line1–line9 (skipping line0,
which createRowTemplate supports), and matches only c.field, never c.sortField.
Issue 3 — value silently overrides field and even explicit sortField; docs say the opposite
Setup: records with a numeric priority field (1, 2, 10), displayed via
value: { tpl: "P{$record.priority}" }. One column with field: "priority" + value,
another with sortField: "priority" + value.
Actual: both columns sort as strings by the displayed label — P1, P10, P2 — instead of
numerically by the field (1, 2, 10). The explicit sortField has no effect.
Why: onHeaderClick (Grid.tsx ~lines 1483–1484) puts both field = sortField || field and
value = sortValue || value on the sorter, and getComparer (data/comparer.ts) always prefers
value when it is defined. So the effective precedence today is:
sortValue > value > sortField > field
This contradicts the documentation, which states (docs, Grid page, "Columns" section):
Use value for computed display values while keeping field for sorting
The docs example (field: "active" with a Yes/No expr) happens to produce the same order
either way, which likely hid the discrepancy. Additionally, the sortValue column property is
not documented at all.
Question for maintainers: what is the intended precedence? Intuitively sortField and
sortValue exist specifically to override the sorting behavior, which suggests something like
sortValue > sortField > value > field — but the current code and the current docs each imply
a different rule, and all three disagree. Whatever the intended rule is, it would be great to
have it implemented consistently and documented explicitly (including sortValue) on the Grid
docs page.
Grid sorting misbehaves for columns using
valueinstead of / together withfieldVersion: cx 26.7.5 (reproduced on
master)Repro:
litmus/bugs/grid-sorting-without-field.js(see PR #1313) — three grids, one per issue below.Columns whose displayed content comes from a
valueselector (tpl/expr/computable) interactbadly with the sorting machinery, which mostly assumes a sort is identified by a field name.
Issue 1 — Sort indicator lights up on every field-less column
Setup: plain header-click sorting, no
sortField/sortDirection/sortersbindings.Two sortable columns defined with only a
value(nofield).Actual: sorting itself works (the comparer correctly falls back to the
valueselector),but after clicking one value-only column, both value-only headers get the sorted style and
icon (
cxs-sorted-asc/desc).Why:
renderHeaderdecides "is this the sorted column" viadata.sorters[0].field == (hdwidget.sortField || hdwidget.field)(Grid.tsx ~line 1310).For a value-only sorter both sides are
undefined, so the check passes for everyfield-less sortable column.
Issue 2 — With
sortField/sortDirectionbindings, value-only columns can't be sorted at all (and can crash the grid)Setup: same columns, but the grid defines
sortField-bind/sortDirection-bind.Actual:
undefined, the grid crashes on first render withTypeError: Cannot read properties of undefined (reading 'toLowerCase').prepareDatafabricates a sorter
{ field: undefined, direction: undefined }, the header check fromIssue 1 matches it against any field-less column (
undefined == undefined), anddirection.toLowerCase()throws.the
sortFieldbinding and merely toggles the direction of the previously active sort.Why:
prepareData(Grid.tsx ~lines 920–952) rebuildsdata.sortersfromdata.sortFieldalone,discarding the
valueselector captured on header click. The fix-up loop that re-attachesvalue/comparer/sortOptionscan only find a column by matchingc.field == sortField—impossible when the column has no field. The resulting sorter has neither
fieldnorvalue,so the comparer selector is
() => nulland the order never changes.onHeaderClick(Grid.tsx ~line 1492), the current-sorter check falls back todata.sortFieldwhen the column has no field, so a click on a value-only column always"matches" the currently sorted field and toggles its direction instead of starting a new sort.
line1–line9(skippingline0,which
createRowTemplatesupports), and matches onlyc.field, neverc.sortField.Issue 3 —
valuesilently overridesfieldand even explicitsortField; docs say the oppositeSetup: records with a numeric
priorityfield (1, 2, 10), displayed viavalue: { tpl: "P{$record.priority}" }. One column withfield: "priority"+value,another with
sortField: "priority"+value.Actual: both columns sort as strings by the displayed label —
P1, P10, P2— instead ofnumerically by the field (
1, 2, 10). The explicitsortFieldhas no effect.Why:
onHeaderClick(Grid.tsx ~lines 1483–1484) puts bothfield = sortField || fieldandvalue = sortValue || valueon the sorter, andgetComparer(data/comparer.ts) always prefersvaluewhen it is defined. So the effective precedence today is:This contradicts the documentation, which states (docs, Grid page, "Columns" section):
The docs example (
field: "active"with a Yes/No expr) happens to produce the same ordereither way, which likely hid the discrepancy. Additionally, the
sortValuecolumn property isnot documented at all.
Question for maintainers: what is the intended precedence? Intuitively
sortFieldandsortValueexist specifically to override the sorting behavior, which suggests something likesortValue > sortField > value > field— but the current code and the current docs each implya different rule, and all three disagree. Whatever the intended rule is, it would be great to
have it implemented consistently and documented explicitly (including
sortValue) on the Griddocs page.