-
Notifications
You must be signed in to change notification settings - Fork 143
Persisting Grid State
This wiki discusses what is meant by grid state and how to "persist" (load and save) it.
Collectively, all the properties
objects discussed in the Grid Properties wiki describe the state of the grid.
Advisory: The reader is urged to give the Grid Properties wiki a thorough read before proceeding!
A Hypergrid state object is a single object that contains all the own properties from the various objects that make up a Hypergrid instance:
-
The properties owned by the
grid.properties
layer of the hierarchy Although the full set of grid properties are visible ingrid.properties
due to JavaScript's prototypal inheritance, the state object is only interested in thegrid.properites
's "own" properties, those properties actually owned by that layer and not those owned by its ancestors,grid.theme
andHypergrid.defaults
, which these properties override. -
The properties owned by the descendant
properties
layers The descendant objects ofgrid.properties
(discussed in the Grid Properties wiki), belong to objects instantiated and referenced directly or indirectly bygrid
, such as the columnproperties
objects (grid.behavior.getColumns().map(column => column.properties)
).
grid.saveState()
returns a serialized version of such a state object can serve as a JSON datagram, useful for persisting state on a remote server, and consumable by grid.loadState()
. It is up to the application developer to actually send (PUT) the resulting JSON to the data store.
The grid.properties
object has been engineered to serve as the grid's state object, by inclusion of a number of specialized dynamic grid properties that reference the descendant properties
objects in a hierarchy that describes their relationship to the grid. For example, the columns
dynamic grid property produces (getter) and consumes (setter) an object whose keys are the column names and whose values are the repective column properties objects.
The following dynamic grid properties can be thought of as "pseudo-properties" because although they are members of grid.properties
, they do not in fact alter or refer to any actual grid properties. Instead they serve as "conduits" to the descendant properties
objects, packaging them all together in one persistable grid state object.
These conduit properties are:
-
columns
- a collection of column properties objects, keyed by column name -
cells
- a collection of cell properties objects, keyed by subgrid, row index, and column name -
rows
— a collection of row properties objects, keyed by sugrid and row index -
calculators
— a registry of computed column functions, keyed by calculator name -
subgrids
— ordered list of subgrids (with options), keyed by subgrid name or type -
features
[v2.1.0] — ordered list of features, keyed by feature name -
columnIndexes
— ordered list of column indexes -
columnNames
— ordered list of column names
All of the above properties are implemented as getters/setters ("dynamic properties") on the grid properties object. Use them sparingly because they are algorithms and not performant. These properties are typically set once, after the grid has been created and has data. Although they can be reset later, this is not recommended when performance is a factor.
Examples of the shape of each of above properties as they appear in a state object can be found following the next section.
Besides the dynamic grid properties discussed above, there are also several dynamic column properties:
column.property.index
column.property.name
column.property.type
column.property.header
column.property.calculator
Some of these get special treatment by loadState
and saveState
, as described in the following subsections.
These two properties, index
and name
, are read-only. They are not settable and should not be included in the state object given to loadState
. They are also marked as non-enumerable, which means that will never be included in the output of saveState
.
The header
dynamic column property provides property access to the text to be displayed in the column header. (If not set, the column name is displayed.)
If your column headers have been automatically generated from the column names, however, it is preferable that not appear in the output of saveState
. To prevent this, you can provide a reference to the function you used to generate the headers as an option to saveState
, which will then compare the current headers to the function-generated headers, and thereby will know when to include header
properties in the output (i.e., only when the actual header in use differs from the generated header).
In practical terms, headers are typically generated by supplying a column schema on grid instantiation generated by fields.getSchema(data)
(see Resources, below). In this case, the "headerifying" function is accessible in fields.titleize
. (The default titleize
function, used by getSchema
, separates camel-case or hyphenated or underscore-separated column names into words with initial capitals. You may override this function if your specs differ.)
The following example uses jQuery's deferreds to fetch the data and the state just by way of example (not an endorsement):
jQuery.when([getMyData(), getMyState()]).then(function(data, state) {
var schema = fields.getSchema(data); // generates headers from names
var options = {
data: data,
schema: schema
};
var grid = new Hypergrid(options);
grid.loadState(state);
var jsonWithAllHeaders = grid.saveState();
options = {
headerify: fields.titleize
};
var jsonWithCustomHeadersOnly = grid.saveState(options);
putMyState(jsonWithCustomHeadersOnly);
});
In the above, getMyData
, getMyState
, and putMyState
are presumably RESTful calls to your data store.
getMyState
returns the following JSON:
{
"columns": {
"memberBMI": {
"header": "Body Mass Index"
}
}
}
Calculators are JavaScript functions used to implement computed columns.
The dynamic column property column.property.calculator
is a very special case: It is a column-only dynamic property that works in conjunction with the grid.property.calculators
registry.
For performance reasons and because the data model does not have access to the registry, column calculators are resolved ahead of time, when the property is set, rather than at runtime when the calculator is needed. That is, the column calculator (column.calculator
), is always a function reference.
The column.property.calculator
property setter is overloaded, accepting any of the following:
-
A calculator name — The setter looks up the function in the registry and assigns it to
column.calculator
. For this to work, the registry must be already available. - A calculator function reference — The setter inserts the function into the registry using the function name as the key.
- A calculator function serialized in a string (as it might arrive via JSON) — The setter instantiates the function, and then inserts it into the registry as above.
In any case, the column.property.calculator
property getter always returns the function name (or, in the case of anonymous functions, the serialized function).
Which overload you use depends on how you prefer to persist your calculators. Calculators are JavaScript code. If you are uncomfortable mixing code right into your data, you might want to "bring your own" registry. That is, load your grid.properties.calculators
registry first, separately from your data. The registry is simply a hash, where each key is the calculator name and each value is a serialized anonymous function. loadState
will instantiate the functions for you as it encounters them. Your column.property.calculator
properties simply reference the calculators by name (registry key).
// example needed here
Alternatively, you can load each column.property.calculator
as a serialized function, (even when multiple columns use identical function code). loadState
will build the registry for you, again instantiating the functions as it inserts them. loadState
will then replace the calculator
values with references to these newly registered function. This results in a single copy of each unique function.
// example needed here
A nice compromise is to include calculators
in your root state object, keeping all the code in that one object, referencing the calculators by name in the column properties objects (just as you would have, had you loaded the registry separately). This has the advantage of persisting the calculators with the rest of the state (one GET instead of separate gets for state and calculators), without messily mixing code into columns' calculator
properties themselves.
// example needed here
If you choose to mix code in calculator
properties, be advised that although anonymous functions are supported (for legacy reasons), they use the serialized form of the function as the registry key, and for that reason are not recommended; named functions here is preferred because it will produce clearer/cleaner/shorter output from saveState
.
Advisory: Individual grid cells can also be calculator functions rather than primitive values. These will override column calculators, when present. See the Cell Values wiki for more information.
This example describes properties for the three columns with names memberHeight
, memberWeight
, and memberBMI
.
var state = {
...
columns: {
memberHeight: {
halign: 'right',
format: 'foot'
},
memberWeight: {
halign: 'right',
strikeThrough: true,
format: 'stone'
},
memberBMI: {
halign: 'right',
calculator: 'bmiuk'
}
},
...
};
This example describes properties on the cell on the 17th row (16
) in the height
column.
var state = {
...
cells: {
data: {
16: {
height: {
font: '10pt Tahoma',
color: 'lightblue',
backgroundColor: 'red',
halign: 'left',
reapplyCellProperties: true
},
... // addition columns in the 17th data row would go here
},
... // additional rows in the data subgrid would go here
},
... // additional subgrids would go here
},
...
};
This example sets the 1st row (0
) of the header
subgrid to have a height of 40 pixels.
Note: height
is the only row property currently implemented.
var state = {
...
rows: {
header: {
0: {
height: 40
},
... // additional rows in the data subgrid would go here
},
... // additional subgrids would go here
},
...
};
This example defines a single calculator called Add10
which simply adds 10
to the underlying column value.
Note: If you don't supply a calculators
registry, one will be built for you from function strings given directly in column calculator
properties. (See next section.)
var state = {
...
calculators: {
Add10: 'function(dataRow, columnName) { return dataRow[columnName] + 10; }'
},
...
};
The following example references the function defined in the calculators
registry defined in the previous section.
var state = {
...
columns: {
vehicles: {
halign: 'right',
format: 'number',
calculator: 'Add10',
color: 'green'
}
}
...
};
If no calculators
property had been defined (or if it lacked this calculator), the following would:
- create the registry if necessary
- insert the function into the registry
- reference the function in
column.calculator
- reference the function name in
column.properties.calculator
(which can be subsequently set to another function name)
var state = {
...
columns: {
vehicles: {
halign: 'right',
format: 'number',
calculator: 'function Add10(dataRow, columnName) { return dataRow[columnName] + 10; }',
color: 'green'
}
}
...
};
The name in the function string above, Add10
, is utilized as the registry key. Without naming it, the entire )(anonymous) function string would have to serve as the key. (Not recommeneded!)
var state = {
...
// example needed here
...
};
var state = {
...
// example needed here
...
};
Hypergrid resources can be "required" if you are using commonJS-like modules with something like Browserify; or referenced from the global fin
variable if you are including the the build file via <script src="fin-hypergrid.js"></script>
or <script src="fin-hypergrid.min.js"></script>
.
var dataModels = require('fin-hypergrid/src/dataModels');
var dataModels = fin.Hypergrid.dataModels;
var fields = require('fin-hypergrid/src/lib/fields');
var fields = fin.Hypergrid.lib.fields;