The Grid & Forms
Here's the mental model to hold through this entire phase, because it dissolves most of the apparent complexity: the Grid is just a store rendered as rows, and the form is just fields over one record. Nothing more exotic than that.
The Grid (Ext.grid.Panel) is Ext JS's flagship — for a lot of enterprises it's the reason they chose Ext JS in the first place. Sortable, editable, paged, filterable data tables that would take weeks to hand-roll, declared in a config object. But under that power it's doing something simple: it takes the store you built in the data package, walks its records, and paints one row per record. Change the store, the grid updates. The grid is a view of the store.
The form is the same idea aimed at a single record instead of a collection. A grid shows you all the users; a form shows you one user's fields so you can edit them. Both are windows onto the data package — the grid is the list, the form is the detail.
💡 If you remember one thing: a grid is bound to a store (many records), a form is bound to a record (one). Selecting a row in the grid and loading it into the form is just handing one record from the collection over to the detail view. That single move is the spine of nearly every admin screen ever built in Ext JS.
We'll build the users admin from phase 4's shell for real this time: a grid of users on top, a form to edit the selected one below.
The grid: a store with columns
A grid needs two things: a store (where the data lives) and columns (how to show it). The store you already have from phase 5. The columns are new — each one maps to a field on your model via dataIndex.
Ext.;
What just happened: The grid asked usersStore for its records and drew one row per record. Each column's text is the header you see; its dataIndex is the model field it reads from each record — dataIndex: 'name' pulls the name field out of every user and stacks those values down the Name column. flex: 1 lets the Name column grow to fill leftover width (same flex you learned for hbox in phase 4 — columns size the same way), while width: 240 pins Email to a fixed size. Click a header and the grid sorts the store by that field for free.
So far the active column shows raw true/false, which is ugly. That's what renderers are for.
Renderers: formatting a cell
A renderer is a function on a column that turns the raw field value into what the user actually sees. Booleans become "Yes/No", numbers become currency, statuses become badges — all without touching the underlying data.
columns:
What just happened: For every cell in the Active column, the grid called your renderer with three things: value (the raw active field — true or false), meta (cell metadata you can tweak, e.g. meta.tdCls to add a CSS class), and record (the whole user model instance, in case you need another field to decide). You returned a string, and that string is what got painted. The stored value is still a real boolean; only the display changed.
⚠️ Renderers run a lot — once per visible cell, and again on every scroll, sort, filter, and refresh. Keep them cheap. No DOM lookups, no network calls, no heavy formatting inside a renderer. If you find yourself doing real work in there, you're going to feel it on a grid with thousands of rows. Precompute on the model or cache outside the function.
Selection: knowing which row the user picked
The grid tracks selection for you. The selModel (or its shorthand selType) decides how the user selects — 'rowmodel' (the default: click a row to select it) or 'checkboxmodel' (a checkbox column for multi-select).
Ext.;
What just happened: selType: 'rowmodel' means a click selects a whole row. The selectionchange event fires whenever the selection changes, handing you the array of selected records. We grabbed the first one — for single selection that's your picked user. You can also ask the grid directly any time with grid.getSelection() (returns an array). Hold onto this event: it's the trigger we'll use later to load the selected user into the edit form.
Editing in the grid: plugins + editor columns
Grids can be editable in place, but editing isn't built into the base grid — it comes from a plugin. There are two:
Ext.grid.plugin.CellEditing(ptype: 'cellediting') — double-click a single cell to edit just that cell.Ext.grid.plugin.RowEditing(ptype: 'rowediting') — double-click a row to edit the whole row at once, with Update/Cancel buttons.
You add the plugin to the grid, then give each editable column an editor (the field component used while editing).
Ext.;
What just happened: The cellediting plugin made cells editable on double-click (clicksToEdit: 2). Each column with an editor swaps in that field when you edit — Name becomes a textfield that refuses to be blank (allowBlank: false), Email a textfield that validates as an email (vtype: 'email'). The Active column has no editor, so it stays read-only. When you commit an edit, the plugin writes the new value back into the record and the store marks that record dirty — it remembers the field changed.
Here's where this connects straight back to phase 5: editing only changes the record in memory. To push those changes to the server you call sync on the store.
usersStore.;
What just happened: store.sync() looked at every dirty (modified), newly created, and removed record, and sent them to the server through the store's proxy — exactly the proxy/writer machinery from phase 5. Edit a cell, then sync(), and the change persists. No sync, and your edit lives only in the browser until a refresh wipes it. This is the most common "why didn't my change save?" gotcha: editing and persisting are two separate steps.
Paging: don't load 50,000 rows at once
When a store has more records than you want on screen, a paging toolbar (pagingtoolbar) docks at the bottom and walks through pages. It binds to the same store and drives the proxy's paging params.
Ext.;
What just happened: The bbar (bottom toolbar) holds a pagingtoolbar bound to the store. When you click Next, the toolbar asks the store to load the next page; the store's proxy sends page, start, and limit params to the server, and the reader uses totalProperty from the response to know how many records exist in total (so it can compute the page count and enable/disable the arrows). The grid only ever holds one page of rows in memory — that's the whole point.
💡 For genuinely huge datasets (hundreds of thousands of rows) where you want a single scrollbar instead of page buttons, Ext JS offers buffered / infinite grids — the store loads pages on demand as you scroll and discards rows that scroll out of view. Same store/proxy foundation, different rendering strategy. Reach for it only when paging genuinely isn't enough; it's more finicky to set up.
A few more column features you'll bump into in legacy grids, worth recognizing:
- Filtering — the
Ext.grid.filters.Filtersplugin adds per-column filter menus in the headers. - Grouping — the
Ext.grid.feature.Groupingfeature collapses rows into labeled groups by a field. - Locked columns —
locked: trueon a column freezes it on the left while the rest scroll horizontally.
The form: fields over one record
Now the detail half. Ext.form.Panel (xtype 'form') is a container whose children are field components — textfield, numberfield, combobox, datefield, checkbox, and friends. Each field has a fieldLabel (the label shown beside it) and a name (the key it reads/writes under).
Ext.;
What just happened: The form rendered a labeled field per item. fieldLabel is what the user reads; name is the data key — name: 'email' means this field maps to the email value when the form reads or writes data. The combobox is itself bound to a store (rolesStore) — combos are mini-grids really: displayField is what the user sees in the dropdown, valueField is what actually gets stored. So the form, too, leans on the data package. defaults: { anchor: '100%' } applies that config to every child so you don't repeat it.
The form panel exposes its underlying engine as basicForm (reachable via form.getForm()) — that's the object that holds field values, validation state, and the load/submit machinery.
Wiring it together: select → load → edit → save
This is the payoff — connecting the grid and the form into a working editor. The bridge is two methods: form.loadRecord(record) copies a record's values into the form's fields, and form.updateRecord(record) copies the edited field values back into the record.
// when the grid selection changes, load that user into the form
usersGrid.;
// when the user clicks Save on the form
What just happened: Selecting a row fires selectionchange; we take the chosen user and call loadRecord, which matches each field's name to a model field and fills the inputs — the form now mirrors that user. The user edits. On Save we first call isValid() to check every field's validation rules (allowBlank, vtype, custom validators) and bail if anything's wrong. Then updateRecord does the reverse of loadRecord — it copies the field values back onto the record, which marks it dirty. Finally store.sync() ships the change to the server, closing the same loop the grid editing did. Grid and form, two views of the same record, kept in step.
📝 You'll sometimes see older code skip the record entirely and use the form's own proxy:
form.submit()POSTs the field values directly,form.load()GETs them. That classic load/submit path still works and shows up in legacy screens. But the record-based path (loadRecord/updateRecord+store.sync()) is the common modern approach because it keeps the store as the single source of truth — the grid and form never disagree about what a user's data is. When you see both styles in one codebase, that's usually history, not intent.
A couple of validation helpers worth keeping in your pocket:
form.getValues()— returns a plain object of all field values (handy for logging or a custom save).allowBlank: falsemakes a field required;vtype: 'email'(and'url','alpha', etc.) validate format; avalidatorfunction lets you write arbitrary checks. All of these feedisValid().
Recap
- The Grid is a store rendered as rows; the form is fields over one record. Both are views onto the phase-5 data package — the grid shows the collection, the form shows one member of it.
- A grid needs a
storeandcolumns; each column'sdataIndexmaps it to a model field, and arendererformats the displayed value (keep renderers cheap — they run constantly). - Editing comes from a plugin (
celleditingorrowediting) plus aneditoron each editable column. Edits mark records dirty;store.sync()is the separate step that actually persists them. - A
pagingtoolbarbound to the store walks pages using the proxy's paging params and the reader'stotalProperty; buffered grids handle truly huge datasets. - Wire grid to form with
loadRecord(record → fields) andupdateRecord(fields → record), guard withisValid(), thensync()— so the grid and form stay two consistent windows onto the same record.
Quick check
Lock in the two ideas that matter most — what binds to what, and how an edit actually reaches the server:
[
{
"q": "What does a column's dataIndex do?",
"choices": ["Sets the column's pixel width", "Maps the column to a field on the store's model", "Defines the sort order", "Names the CSS class for the cell"],
"answer": 1,
"explain": "dataIndex tells the column which model field to read from each record, so dataIndex: 'name' fills the column with every record's name field."
},
{
"q": "You edit a cell with the cellediting plugin and see the new value in the grid, but after a refresh it's gone. Why?",
"choices": ["The renderer overwrote it", "Editing only changed the record in memory; you never called store.sync()", "The column had no dataIndex", "The grid wasn't bound to a store"],
"answer": 1,
"explain": "Editing marks the record dirty in memory. Persisting is a separate step: store.sync() sends dirty/new/removed records to the server via the proxy."
},
{
"q": "Which pair of methods moves data between a grid's selected record and a form?",
"choices": ["form.load() and form.submit()", "form.loadRecord(record) to fill the form, form.updateRecord(record) to write edits back", "grid.getSelection() and grid.setSelection()", "store.add() and store.remove()"],
"answer": 1,
"explain": "loadRecord copies record values into the form's fields; updateRecord copies edited field values back into the record (which then gets persisted with store.sync())."
}
]
← Phase 5: The Data Package · Guide overview · Phase 7: MVVM: ViewControllers, ViewModels & Binding →
Check your understanding
1. What does a column's dataIndex do?
2. You edit a cell with the cellediting plugin and see the new value in the grid, but after a refresh it's gone. Why?
3. Which pair of methods moves data between a grid's selected record and a form?