Skip to content

Persisting Grid State

Jonathan Eiten edited this page Jan 5, 2018 · 8 revisions

This wiki discusses what is meant by grid state and how to "persist" (load and save) it.

What is grid state?

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!

What is a Hypergrid state object?

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 in grid.properties due to JavaScript's prototypal inheritance, the state object is only interested in the grid.properites's "own" properties, those properties actually owned by that layer and not those owned by its ancestors, grid.theme and Hypergrid.defaults, which these properties override.
  • The properties owned by the descendant properties layers The descendant objects of grid.properties (discussed in the Grid Properties wiki), belong to objects instantiated and referenced directly or indirectly by grid, such as the column properties 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.

Where do I find the grid state object?

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.

Dynamic grid properties

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.

Dynamic column properties

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.

column.property.index and column.property.name

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.

column.property.header

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"
        }
    }
}
column.property.calculator

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.

columns

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'
        }
    },
    ...
};

cells

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
    },
    ...
};

rows

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
    },
    ...
};

calculators

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; }'
    },
    ...
};

calculator

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:

  1. create the registry if necessary
  2. insert the function into the registry
  3. reference the function in column.calculator
  4. 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!)

subgrids

var state = {
    ...
    // example needed here
    ...
};

columnIndexes and columnNames

var state = {
    ...
    // example needed here
    ...
};

Resources

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;