MemberJunction / MemberJunction/MJ
mj-entity-data-grid renders ZERO columns when a stale cross-entity grid state matches no fields — rows load, count is correct, nothing displays
- Dominant language
- TSQL
- Stars
- 29
- Forks
- 6
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 323
Description
## Summary
`mj-entity-data-grid` accepts a **zero-column result** from a stale, cross-entity grid state and
renders it — producing a grid that loads its rows, reports the correct row count, and displays
**no columns and therefore nothing at all**. There is no error and no console warning.
The correctly-built column list is sitting in the very next branch of the same `if`, unused.
## Repro
1. Host **one** `` and rebind `[EntityName]` between two entities that share **no**
`DefaultInView` field names. (We used `MJ: Animals` → `MJ: Care Logs`.)
2. Load entity A and let its grid render — this populates the grid state's `columnSettings`.
3. Rebind to entity B.
4. Entity B's toolbar shows its **correct row count**; the grid shows **no header row and no rows**.
## Measured, live in the browser
Read off the component with `ng.getComponent()` on the failing page:
```
entity: "MJ: Care Logs"
agRowsInDom: 21 ← the rows ARE loaded and in the DOM
columnsBuilt: 6 ← _columns was built correctly from metadata
agColumnDefs: 0 ← …and then discarded
agHeaderCells: 0
defaultInViewTrue: 6 ← metadata is correct
truthyButNotTrue: 0 ← and they are real booleans, not 1/truthy
gridStateColumnNames: ["Name","Species","IntakeDate","Status","Breed","Housing"] ← ANIMAL's columns
ownFieldNames: ["ID","AnimalID","CareDate","CareType","Description","PerformedBy",
"IsComplete","FollowUpDate","Notes","__mj_CreatedAt","__mj_UpdatedAt","Animal"]
```
The server side was fully eliminated first: rows returned, entity permissions, `DefaultInView`
metadata, and the **serialized `GetDatasetByName('MJ_Metadata')` payload** the browser actually
receives (six `DefaultInView: true`, `typeof boolean`). All correct.
## Root cause — two spots, both in `packages/Angular/Generic/entity-viewer/src/lib/entity-data-grid/entity-data-grid.component.ts`
**1. `buildAgColumnDefsFromGridState()` (~line 2526)** drops any setting whose field the current
entity does not have, with no floor when *every* setting is dropped:
```ts
for (const colConfig of sortedColumns) {
const field = this._entityInfo.Fields.find(f =>
f.Name.toLowerCase() === colConfig.Name.toLowerCase()
);
if (!field) continue; // all 6 of Animal's settings miss → cols === []
...
}
```
**2. `buildAgColumnDefs()` (~line 2390)** takes that empty array as the answer:
```ts
if (this._gridState?.columnSettings?.length && this._entityInfo) {
this.agColumnDefs = this.buildAgColumnDefsFromGridState(this._gridState.columnSettings);
} else if (this._columns.length > 0) { // ← the correctly-built 6, never reached
this.agColumnDefs = this._columns.map(col => this.mapColumnConfigToColDef(col));
} else if (this._entityInfo) {
this.agColumnDefs = this.generateAgColumnDefs(this._entityInfo);
} else {
this.agColumnDefs = [];
}
```
## Suggested fix
Treat an empty result from the grid-state branch as **no usable state** and fall through:
```ts
const fromState = (this._gridState?.columnSettings?.length && this._entityInfo)
? this.buildAgColumnDefsFromGridState(this._gridState.columnSettings)
: [];
if (fromState.length) this.agColumnDefs = fromState;
else if (this._columns.length > 0) this.agColumnDefs = this._columns.map(c => this.mapColumnConfigToColDef(c));
else if (this._entityInfo) this.agColumnDefs = this.generateAgColumnDefs(this._entityInfo);
else this.agColumnDefs = [];
```
**This is consistent with code already in the same file.** `generateAgColumnDefs()` (~line 2620)
has exactly this kind of floor — *"Fallback: if no DefaultInView fields are defined, show first 10
non-system fields"*. The grid-state branch is the one path missing one.
## Two reasons this is worth more than the empty grid we hit
**1. The partial case is worse than the total one, because nobody notices.** The failure degrades by
field-name **overlap**, not by erroring. In the same app, Animals → **Breeds** looked fine because
those two share `Name` and `Species`, so two of four columns survived — a silently truncated grid
that reads as normal. Only Animals → **Care Logs**, which shares nothing, collapsed to zero and
became visible. Every partial-overlap pair in every app is currently showing a subset of its columns
with no indication.
**2. Rebinding `[EntityName]` is a supported pattern, not misuse.** `EntityName` is a rebindable
`@Input`, and the `Entity` setter already has explicit `entityChanged` handling that clears
`viewTypeConfigById`, resets `InternalSortState`, and nulls `dynamicRendererRef` — with a comment
naming this exact symptom:
> *"Keeping the old entity's config applies its columnSettings to the new entity, so only fields
> common to both survive (e.g. just Name/Description) — the 'no/too-few columns' symptom."*
So the intent to handle entity switching is clearly there and was implemented in several places; the
child grid's `_gridState` just isn't covered by it. A secondary question for whoever picks this up:
should `EntityViewerComponent` also be clearing the child grid's `_gridState` on entity change, in
addition to the grid defending itself?
## Workaround for anyone hitting this now
Force the viewer to be destroyed and recreated when the entity changes, so no state crosses
entities — e.g. in Angular 17+ control flow, key the block on the page identity:
```html
@for (page of activePageAsList; track page.id) {
}
```
That prevents state crossing entities but does not address the underlying acceptance of an empty
column set, which still bites any host that legitimately reuses one viewer.
## Environment
MJ **v6.1.0-edge.4** (commit `43217fa139`). Found while building MJ Academy (a MemberJunction
teaching course) — a left-rail category whose sub-pages are separate entity grids sharing one viewer.
Contributor guide
Research direction
Start in packages/Angular/Generic/entity-viewer/src/lib/entity-data-grid/entity-data-grid.component.ts, reading buildAgColumnDefs() and buildAgColumnDefsFromGridState() around the mentioned lines. Verify that stale state with no matching fields falls through to the existing _columns or entity fallback, then confirm rebinding between disjoint entities renders headers and rows instead of an empty grid.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- angular, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100