Skip to content

Custom DataGrid columns cannot read the row value at their field ref without casting the row accessor #129

Description

@matej21

Summary

A column's cell render gets the row as EntityAccessor<object>, and there is no typed way to read the value at the column's own field ref from it. Every custom column that reads a relation or a second field of the row therefore casts the accessor to a hand-written shape and indexes it by name. bindx does the same internally (accessField in packages/bindx-dataview/src/columnTypes.ts returns unknown).

Environment

  • @contember/bindx-dataview@0.1.52
  • contember/bindx@main as of 78c1712

Reproduction

A custom column that shows whether a has-one is connected, declared inside a grid as <IsConnectedColumn field={it.author} />:

export const IsConnectedColumn = Object.assign(
	(_props: { field: unknown }): null => null,
	{
		staticRender: ({ field }: { field: unknown }): ReactElement => {
			const fieldName = extractFieldName(field) ?? ''
			return (
				<ColumnLeaf
					fieldName={null}
					fieldRef={null}
					sortingField={null}
					filterName={null}
					filterHandler={undefined}
					isTextSearchable={false}
					renderCell={row =>
						// row: EntityAccessor<object> — nothing links it to `field`
						(row as unknown as Record<string, { $isConnected?: boolean } | undefined>)[fieldName]?.$isConnected
							? 'yes'
							: 'no'}
				/>
			)
		},
	},
)

The same happens for:

  • a column that reads the items of a has-many (row[fieldName].items);
  • a column that reads the value of a scalar other than the one it binds, e.g. an enum column whose cell also shows a date from the same row: the grid's children closure has the typed ref (it.state.publishedAt), but the cell only gets the erased row.

Expected behavior

The code that holds a typed ref (it.author, it.state.publishedAt) can read that ref's value on a given row without a cast, with the type the ref already carries.

Proposal

A resolver typed by the ref, exported from @contember/bindx-dataview (or @contember/bindx):

/** The accessor at `ref`'s path on `row`, typed as the ref. */
function resolveOnRow<TRef extends FieldRef<unknown> | HasOneRef<any> | HasManyRef<any>>(
	row: EntityAccessor<object>,
	ref: TRef,
): TRef

It walks FIELD_REF_META.fullPath (the same path extractFieldName and accessField use) through the row's proxy. Since the collector refs and the runtime accessors already share one type (FieldRef<T> & FieldAccessor<T>), returning TRef needs no new types. accessField and the built-in column types could then use it internally.

A wider alternative is to make ColumnLeafProps.renderCell generic over the grid's entity so the row itself is typed; that touches every column factory and the analyzer, so the resolver looks like the smaller step.

Open question for the maintainers: whether the resolver should throw or return null when the path is not in the row's selection (today a missing field raises UnfetchedFieldError on read, which seems right to keep).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions