Configure a custom table definition

This guide will help you configure a custom table definition. If you are not sure whether you need to use a custom table definition, start with the Configure tables guide first. Visit the CALSXHTMLTEI, and Simpletable add-on pages for more information on the available table configurations.

If the table that should be configured does not conform to any of the supported standards, you will need to configure it using a custom table definition.

Create a package

Create a new package by creating a new directory in the packages directory of your application. Create a src directory inside the new package directory.

Keep the name of this package in line with the rest of the packages.

Create a configureSxModule.js file

Create a configureSxModule.js file in the src folder. This file will mark this package as add-on.

configureSxModule.js
export default function configureSxModule(sxModule) {
	sxModule.markAsAddon();
}

Create the table definition

Create the directory table-definition in the src directory. Create a file called MyTableDefinition.js in the newly created directory. Replace the "My" part of the name with the name of the table specification you're implementing.

MyTableDefinition.js
import TableDefinition from 'fontoxml-table-flow/src/TableDefinition.js';

function MyTableDefinition(options) {
	const properties = {};

	TableDefinition.call(this, properties);
}

MyTableDefinition.prototype = Object.create(TableDefinition.prototype);
MyTableDefinition.prototype.constructor = MyTableDefinition;

export default MyTableDefinition;

Create an element configuration file

Create a file called configureAsMyTableElements file in the src folder. Replace the "My" part of the name with the name of the table specification you're implementing.

This file will contain the element configuration for the elements that make up the table. This file should export a single function. This function can be called in any configureSxModule file in the application, just like family configuration.

configureAsMyTableElements.js
import configureAsTableElements from 'fontoxml-table-flow/src/configureAsTableElements.js';
import MyTableDefinition from './TableDefinition/MyTableDefinition.js';

export default function configureAsMyTableElements(sxModule, options) {
	options = options || {};
	const tableDefinition = new MyTableDefinition(options);
	configureAsTableElements(sxModule, options, tableDefinition);
}


You may also want to configure elements that can be found inside a table element, but do not define the structure of the table. An example of such element is the XHTML caption element. This element can be placed inside the table element, but does not influence the structure of the table.

Keep in mind that you are not required to configure elements like these here. These elements can be configured in any configureSxModule file, just like any other element.

Create the insert operation

Create an operations.js file in the src folder. Copy the contents of the example to the operations.js file. Again, replace the "my" in the operation name with the name of the table specification you're implementing.

The operation in the example is based on the generic table-insert operation. This operation is used to generate a table based on the given childNodeStructure. The childNodeStructure should at least contain the table element. It may also contain wrapper elements.

The childNodeStructure is used to determine which kind of table to insert, since Fonto supports the use of multiple table definitions in a single application.


operations.json
{
    "my-table-insert": {
        "label": "Table",
        "description": "Insert a table.",
        "icon": "table",
        "initialData": {
            "childNodeStructure": [
                "table"
            ]
        },
        "steps": [
            {
                "type": "operation/table-insert"
            }
        ]
    }
}


The childNodeStructure should at least contain a table element. The following error will be thrown when the childNodeStructure does not contain a table element:

Found no table elements in the childNodeStructure, please provide exactly one table element. at operation 'my-table-insert', step 1 of 2, type 'command/table-insert'.

You may want to check the namespaces used in the element selectors if you encounter this error. More information on these selectors can be found in the selectorParts property section of this guide.

Add a dependency on the created package

Add the created package to the dependencies of your application. You can find the dependencies in the manifest.json file in the config directory of your application.

manifest.json
...
"my-table-flow": "packages/my-table-flow"
...


Configuring the table definition

The table definition is used to express how a table should be read from XML and how it should be written to XML. A table definition comes down to a collection of XPath queries and strategies which are used in the serialization and deserialization processes.

These XPath queries and strategies will be set as properties on a property variable which can be seen in the example for the MyTableDefinition file. The properties are grouped per type and explained per property. The best way to set up a custom table definition is by reading through all properties and to decide per property if it needs to be set for the table you are implementing.

Configure general settings

shouldRemoveEmptyRows

This property is optional.

The shouldRemoveEmptyRows property determines whether rows which do not contain any cell nodes should be removed. The situation can exists when a row contains cells which all span over the row below, effectively making the row below empty.

This property is set to true by default, when empty rows need to be preserved, set this property to false.

MyTableDefinition.js
...
shouldRemoveEmptyRows: false
...

shouldCreateColumnSpecificationNodes

This property is optional.

The shouldCreateColumnSpecificationNodes property determines whether nodes containing information regarding the columns should be created. This property should be set to true if these nodes should be created.

This property is set to false by default.

MyTableDefinition.js
...
shouldCreateColumnSpecificationNodes: true
...

supportsBorders

This property is optional.

The supportsBorders property determines whether a tables supports borders to be set. If this property is set to false, the table will have all its borders rendered in the content view and the

This property is set to false by default.

...
supportsBorders: true
...

selectorParts

This property is required.

The selectorParts property must contain a map of abstract table element names to XPath selectors. These selectors should include a namespace selector if applicable. The selectorParts are used for configuring the elements and can be used to construct XPath queries.

Some examples of XPath selectors are:

table (: A table element without namespace :)
Q{}table (: A table element without namespace :)
Q{http://some-namespace.com/}table (: A table element in the http://some-namespace.com/ namespace :)
table[@class] (: A table element having a class attribute :)

These abstract table names can be configured:

  • tableFigure - A figure-like element containing the table element. Only use this element when it is a required element. Other possible figure-like elements should be configured separately.
  • table (required) - The table element itself.
  • columnSpecification - An element specifying the properties of a column. Found most commonly as child node of the table element.
  • columnSpecificationGroup - An element used for grouping column specification elements.
  • row (required) - An element defining a table row.
  • headerRow - An element defining a table row which is a header.
  • cell (required) - An element defining a table cell.
  • headerCell - An element defining a table cell which is a header.
  • headerContainer - An element defining a header. These elements contain row elements.
  • bodyContainer - An element defining a body. These elements contain row elements.
  • footerContainer - An element defining a footer. These elements contain row elements.

Below is an example on how to create the mapping for the selectorParts property:

function MyTableDefinition (options) {
	var selectorParts = {
		table: 'table',
		row: 'row',
		cell: 'cell'
	};

	var properties = {
		...
		selectorParts = selectorParts;
		...
	};
}

When a table should be in a different namespace, the URI of that namespace must be passed to the table definition by setting the namespaceURI on the table property of the options object passed to the table definition.

Below is an example on how to create the mapping for the selectorParts property with namespace URIs:

function MyTableDefinition (options) {
	var namespaceURI = options.table.namespaceURI;

	var namespaceSelector = 'Q{' + namespaceURI + '}';
	var selectorParts = {
		table: namespaceSelector + 'table',
		row: namespaceSelector + 'row',
		cell: namespaceSelector + 'cell'
	};

	var properties = {
		...
		selectorParts = selectorParts;
		...
	};
}

widthToHtmlWidthStrategy

This property is optional.

The widthToHtmlWidthStrategy property should contain a function accepting a width (string). The function should then convert the value passed to it to a valid HTML width value (string).

addWidthsStrategy

This property is optional.

The addWidthsStrategy property should contain a function accepting two widths (strings). The function should then add up both values and return the result (string).

divideWidthByTwoStrategy

This property is optional.

The divideWidthByTwoStrategy property should contain a function accepting one width (string). The function should then divide the value given by two and return the result (string).

normalizeColumnWidthsStrategy

This property is optional.

The normalizeColumnWidthStrategy property should contain a function accepting all column widths as a string array as its first and only argument. The function should normalize the given widths.

This property is useful when configuring tables with widths based on percentages. When a column is added to a table, its column specification will be copied from one of its neighbouring columns. This does not pose a problem for proportional values, like the ones used in DITA tables. However, it does pose a problem for percentage based widths, as the total width of all columns added up will exceed 100%.

For example: Imagine a table consisting of five columns. Each column takes up 20% of the table's width (100% divided by 5 columns). When a column is added, its column specification will be copied from another column. This will result in a table with six columns which all, according to the column spec, take up 20% of the table's width. Now there is a 120% total width.

This function can be used to prevent these situations. All column widths are passed to this function after changing the number of columns. This allows you to calculate new widths for each column while keeping the ratio intact.

Example

The following code block contains a part of a table definition that supports widths based on percentages.

// Widths
widthToHtmlWidthStrategy: function(width, _widths) {
	return width + '%';
},
addWidthsStrategy: function(width1, width2) {
	const parsedWidth1 = parseFloat(width1);
	const parsedWidth2 = parseFloat(width2);

	return parsedWidth1 + parsedWidth2 + '';
},
divideByTwoStrategy: function(width) {
	const parsedWidth = parseFloat(width);

	return parsedWidth / 2 + '';
},
widthsToFractionsStrategy: function(widths) {
	return widths.map(width => parseFloat(width) / 100);
},
fractionsToWidthsStrategy: function(fractions) {
	return fractions.map(fraction => fraction * 100 + '');
},
normalizeColumnWidthsStrategy: function(widths) {
	const total = widths.reduce((total, width) => (total += parseFloat(width)), 0);

	return widths.map(width => (width * 100) / total + '');
}

To make this example work, you will also have to configure the appropriate getColumnSpecificationStrategies.

This example assumes that the percentages are saved as only the number. It does not add the '%' character anywhere. If you do need this character to be in the widths, you will need to add it and you also need to consider its presence when parsing the widths. 

widthsToFractionsStrategy

This property is optional. It should be set when the column sizing popover is to be used. See the open-table-column-sizing-popover operation and CALS implementation for more information.

The widthsToFractionsStrategy property should contain a function accepting an array of widths (strings). It should return an array of the same length as the input array which represents the given widths as fractions (number).

fractionsToWidthsStrategy

This property is optional. It should be set when the column sizing popover is to be used. See the open-table-column-sizing-popover operation and the CALS implementation for more information.

The fractionsToWidthsStrategy property should contain a function accepting an array of fractions (numbers). It should return an array of the same length as the input array which represents the given fractions as this tables specific widths (string).

tableDefiningNodeSelector

This property is optional.

The tableDefiningNodeSelector property must contain an XPath selector which returns true when given a table node.

...
tableDefiningNodeSelector: 'self::table'
...

When this property is not set, it will be auto-generated by appending the table property of the selectorParts object to self:: which results in something like self::table.

cellDefiningNodeSelecton

This property is optional.

The cellDefiningNodeSelector property should contain an XPath selector which returns true when given a cell node.

...
cellDefiningNodeSelector: 'self::cell'
...

When this property is not set, it will be auto-generated by appending the table property of the selectorParts object to self:: which results in something like self::cell.

tablePartsNodeSelector

This property is optional.

The tablePartsNodeSelector property should contain an XPath selector which returns true when given any node that defines the structure of a table.

...
tablePartsNodeSelector: 'self::table or self::row or self::cell'
...

When this property is not set, it will be auto-generated by appending every property of the selectorParts object to self:: and then joining the results together with  or , resulting in something like self::table or self::row or self::cell.

Configure finds

The "find" properties are XPath queries used to find one or more specific nodes under a given node. Depending on the structure of the table, not all of these finds have to be configured. For example, when a table does not support container nodes, the finds for these nodes do not have to be set.

When your schema supports nested tables, keep in mind that some XPath queries need to be specific enough to not select nodes from the parent/child table.

Create aliases for all elements to be used in XPath queries to increase readability of those XPath queries. This is especially useful when your table definition should support tables with a namespace different from the namespace of the document.

var table = selectorParts.table;
var row = selectorParts.row;
var cell = selectorParts.cell;

findHeaderRowNodesXPathQuery

This property is optional.

The findHeaderRowNodesXPathQuery should return any row nodes found in the header of the table. This query is executed with the table node as its context node.

...
findHeaderRowNodesXPathQuery: './thead/row'
...

findBodyRowNodesXPathQuery

This property is required.

The findBodyRowNodesXPathQuery should return any row nodes found in the body of the table. These are "regular" rows. This query is executed with the table node as its context node.

...
findBodyRowNodesXPathQuery: './tbody/row'
...

findFooterRowNodesXPathQuery

This property is optional.

The findFooterRowNodesXPathQuery should return any row nodes found in the footer of the table. This query is executed with the table node as its context node.

...
findFooterRowNodesXPathQuery: './tfoot/row'
...

findHeaderContainerNodesXPathQuery

This property is optional.

The findHeaderContainerNodesXPathQuery should return container nodes defining the header of a table. This query is executed with the table node as its context node.

Examples are the thead element found in both CALS and XHTML.

...
findHeaderContainerNodesXPathQuery: './thead'
...

findBodyContainerNodesXPathQuery

This property is optional.

The findBodyContainerNodesXPathQuery should return container nodes defining the body of a table. This query is executed with the table node as its context node.

Examples are the tbody element found in both CALS and XHTML.

...
findBodyContainerNodesXPathQuery: './tbody'
...

findFooterContainerNodesXPathQuery

This property is optional.

The findFooterContainerNodesXPathQuery should return container nodes defining the footer of a table. This query is executed with the table node as its context node.

Examples are the tfoot found in both CALS and XHTML.

...
findFooterContainerNodesXPathQuery: './tfoot'
...

findColumnSpecificationNodesXPathQuery

This property is optional.

The findColumnSpecificationNodesXPathQuery should return column specifying nodes. This query is executed with the table node as its context node.

Examples are the colspec found in CALS and col found in XHTML.

...
findColumnSpecificationNodesXPathQuery: './colspec'
...

findCellNodesXPathQuery

This property is required.

The findCellNodesXPathQuery must return cell nodes found in a given row. This property is required. This query is executed with the current row node as its context node.

...
findCellNodesXPathQuery: './cell'
...

findNonTableNodesPrecedingRowsXPathQuery

This property is optional.

The findNonTableNodesPrecedingRowsXPathQuery should return all nodes that do not define the structure of a table but are located inside the table node, before any structure defining nodes. This query is executed with the table node as its context node.

Examples of these nodes are the caption in XHTML and the head in TEI.

...
var tableNodesSelector = 'self::' + col +
		' or self::' + colGroup +
		' or self::' + tr +
		' or self::' + thead +
		' or self::' + tbody +
		' or self::' + tfoot;
...
var properties = {
	findNonTableNodesPrecedingRowsXPathQuery: './*[(' + tableNodesSelector + ') => not() and following-sibling::*[' + tableNodesSelector + ']]'
...

findNonTableNodesFollowingRowsXPathQuery

This property is optional.

The findNonTableNodesFollowingRowsXPathQuery should return all nodes that do not define the structure of a table but are located inside the table node, after any structure defining nodes. This query is executed with the table node as its context node.

...
findNonTableNodesFollowingRowsXPathQuery: './*[(' + tableNodesSelector + ') => not() and preceding-sibling::*[' + tableNodesSelector + ']]'
...

Configure data XPath queries

The data XPath queries provide the application with information about the table being parsed, such as the number of columns.

getNumberOfColumnsXPathQuery

This property is required.

The getNumberOfColumnsXPathQuery should return the number of columns present in the table. The query is executed with the table node as its context node.

...
getNumberOfColumnsXPathQuery: './row/cell => count()'
...

The given example is a very naive implementation which does not take into account any column spanning cells. For a solution, see the next example.

...
getNumberOfColumnsXPathQuery: '(let $cells := (.//' + row + ')[1]/' + cell + ' return for $cell in $cells return let $colspan := $cell/@colspan => number() return if ($colspan) then $colspan else 1) => sum()'
...

getRowSpanForCellNodeXPathQuery

This property is required.

The getRowSpanForCellNodeXPathQuery should return the row span of a given cell node. This query is executed with the cell node as its context node.

...
getRowSpanForCellNodeXPathQuery: 'let $rowspan := ./@rowspan => number() return if ($rowspan) then $rowspan else 1'
...

This query should always return a number. This means it should always return 1 as a default when a rowspan-like attribute is not present.

getColumnSpanForCellNodeXPathQuery

This property is required.

The getColumnSpanForCellNodeXPathQuery should return the column span of a given cell node. This query is executed with the cell node as its context node.

...
getColumnSpanForCellNodeXPathQuery: 'let $colspan := ./@colspan => number() return if ($colspan) then $colspan else 1'
...

This query should always return a number. This means it should always return 1 as a default when a colspan-like attribute is not present.

Configure normalizations

Normalizations are used for creating, converting and removing elements.

normalizeContainerNodeStrategies

This property is optional.

The normalizeContainerNodeStrategies will normalize the container nodes present in a table.

The following normalization strategies are available for containers:

  • createAddHeaderContainerNodeStrategy - Adds a header container node, only when needed. Moves row nodes to the newly created header container node. Use this strategy when tables without header containers are to be expected and the tables to be created should have header containers.
  • createAddBodyContainerNodeStrategy - Adds a body container node, only when needed. Moves row nodes to the newly created header container node. Use this strategy when tables without body containers are to be expected and the tables to be created should have body containers.
  • createRemoveBodyContainerNodeStrategy - Removes a body container node, when present. Use this strategy when tables with body containers are to be expected and the tables to be created should not have body containers.
  • createRemoveFooterContainerNodeStrategy - Removes a footer container node, when present. Moves rows to the body container node or else to the table node.
  • createRemoveHeaderContainerNodeStrategy - Removes a header container node, when present. Moves rows to the body container node or else to the table node. Use this strategy when tables with header containers are to be expected and the tables to be created should not have header containers.

These strategies are available after including fontoxml-table-flow/normalizeContainerNodeStrategies.

In addition to the normalization that can be set here, during the transformation from XML to the grid model, the header container node will be updated when the number of header rows gets updated. Rows will be moved into the header container when the number of header rows increases and rows will be moved out of the header container when the number of header rows decreases. If the header container remains empty after decreasing the number of header rows, it will remove the header container.

For an example of the usage of these strategies, see the XHTML table definition on Github or the CALS table definition on Github.

normalizeRowNodeStrategies

This property is optional.

The normalizeRowNodeStrategies are used to normalize rows nodes. This, for example, is useful when a table uses row nodes to define its header, like simpletable does.

The following normalization strategies are available for rows:

  • createConvertNormalRowNodeStrategy - This strategy converts a normal row (or body row) to a header row by converting the row element itself. The element to convert to can be configured.
  • createConvertFormerHeaderRowNodeStrategy - This strategy converts a former header row to a normal row by converting the row element itself. The element to convert to can be configured.

These strategies are available after including fontoxml-table-flow/normalizeRowNodeStrategies.

You may almost always want to use the createConvertNormalRowNodeToHeaderRowNodeStrategy and the createConvertFormerHeaderRowNodeToNormalRowNodeStrategy together.

normalizeCellNodeStrategies

This property is optional.

The normalizeCellNodeStrategies are used to normalize cell nodes. This, for example, is useful when a table uses cell nodes to define its header, like XHTML can do.

The following normalization strategies are available for cells:

These strategies are available after including fontoxml-table-flow/normalizeCellNodeStrategies.

...
normalizeCellNodeStrategies: [
	normalizeCellNodeStrategies.createConvertHeaderCellNodeStrategy(namespaceURI, 'th'),
	normalizeCellNodeStrategies.createConvertFormerHeaderCellNodeStrategy(namespaceURI, 'td')
]
...

For a more extensive example of the usage of these strategies, see the XHTML table definition on Github or the CALS table definition on Github.

normalizeColumnSpecificationStrategies

This property is optional.

The normalizeColumnSpecificationStrategies are used to normalize column specifications, not the column specification nodes. This, for example, can be used to re-apply column names.

The following normalization strategies are available for column specifications:

  • createRecreateColumnName - This strategy recreates the column name of the columns. The name is configurable and gets appended with "-0" for the first column.

These strategies are available after including fontoxml-table-flow/normalizeColumnSpecificationStrategies.

For an example of the usage of this strategy, see the CALS table definition on Github.

findNextColumnSpecification

This property is optional.

The findNextColumnSpecification is used to find the column specification for the next column. When no column specification can be found for the next column, false must be returned.

The function has 4 arguments set when it is executed:

  • columnIndex - The index of the column to find the specification for
  • columnSpecification - The current column specification
  • columnSpecificationIndex - The index of the current column specification
  • columnSpecifications - The array containing all column specifications

For an example of the usage of this strategy, see the CALS table definition on Github.

Default specifications

The default specifications can be used to provide some default values to be present on a specification when it is created. Specifications are just plain javascript objects used as a mapping from value name to value.

getDefaultColumnSpecificationStrategy

This property is optional.

The getDefaultColumnSpecificationStrategy should return a default specification for columns. This function will have the context object as its only argument.

When column widths should be updated, the property containing the width must be named "columnWidth".


...
getDefaultColumnSpecificationStrategy: function (context) {
	return {
		columnName: 'column-' + context.columnIndex,
		columnNumber: context.columnIndex + 1,
		columnWidth: '1*',
		rowSeparator: true,
		columnSeparator: true
	};
}
...

getDefaultCellSpecificationStrategy

This property is optional.

The getDefaultCellSpecificationStrategy should return a default specification for cells.

For the rendering of cell borders, the rowSeparator and columnSeparator values will be read from the cell specification.


...
getDefaultCellSpecificationStrategy: function (context) {
	return {
		rowSeparator: true,
		columnSeparator: true
	};
}
...

getDefaultRowSpecificationStrategy

This property is optional.

The getDefaultRowSpecificationStrategy should return a default specification for rows.

...
getDefaultRowSpecificationStrategy: function (context) {
	return {
		someValue: 'value'
	};
}
...

Create strategies

createCellStrategy

This property is required.

This strategy should create a new cell node and return it. This property only accepts one strategy. The platform offers one standard strategt which can be used by including fontoxml-table-flow/createCreateCellStrategy.

...
createCellStrategy: createCreateCellStrategy(namespaceURI, localName)
...

createRowStrategy

This property is required.

This strategy should create a new row node and append it to the appropriate container. This property accepts only one strategy. The platform offers one standard strategy which can be used by including fontoxml-table-flow/createCreateRowStrategy. This strategy creates a new row and appends it to the appropriate node.

...
createRowStrategy: createCreateRowStrategy(namespaceURI, localName)
...

createColumnSpecificationStrategy

This property is optional.

This strategy should create a new column specification node and append it to the table node. This property accepts only one strategy. The platform offers one standard strategy which can be used by including fontoxml-table-flow/createCreateColumnSpecificationNodeStrategy. This strategy creates a new column specification node and inserts it before the row nodes or container nodes of the table.

...
createColumnSpecificationStrategy: createCreateColumnSpecificationNodeStrategy(namespaceURI, localName, insertBeforeXPathQuery)
...

Get specification strategies

Getting values for specifications is done by reading the value of an attribute from a node.

The following strategies are available for getting attribute values from a node:

  • createGetValueAsBooleanStrategy - Reads the value of an attribute and converts it to a boolean using the given XPath selector. The value gets saved under the given name in the current strategy.
  • createGetValueAsStringStrategy - Reads the value of an attribute and converts it to a string using the given XPath selector. The value gets saved under the given name in the current strategy.
  • createGetValueAsNumberStrategy - Reads the value of an attribute and converts it to a number using the given XPath selector. The value gets saved under the given name in the current strategy.

These strategies are available after including fontoxml-table-flow/getSpecificationValueStrategies.

getTableSpecificationStrategies

This property is optional.

The getTableSpecificationStrategies are used to get attributes from the table node.

...
getTableSpecificationStrategies: [
	getSpecificationValueStrategies.createGetValueAsBooleanStrategy('borders', './borders = "all"')
]
...

getColumnSpecificationStrategies

This property is optional.

The getColumnSpecificationStrategies are used to get attributes from a given column specification node.

...
getColumnSpecificationStrategies: [
	getSpecificationValueStrategies.createGetValueAsStringStrategy('columnName', './colname')
]
...

getRowSpecificationStrategies

This property is optional.

The getRowSpecificationStrategies are used to get attributes from a given row node.

getCellSpecificationStrategies

This property is optional.

The getCellSpecificationStrategies are used to get attributes from a given cell node.

Set attribute strategies

The set attribute strategies set, as the name implies, attributes on a specific node. These strategies can also remove attributes from nodes. The set attribute strategies can be found in the setAttributeStrategies file.

The following strategies are available for setting attributes to a node:

These strategies are available after including fontoxml-table-flow/setAttributeStrategies.

setTableNodeAttributeStrategies

This property is optional.

The setTableNodeAttributeStrategies are used to set attributes on the table node.

setColumnSpecificationNodeAttributeStrategies

This property is optional.

The setColumnSpecificationNodeAttributeStrategies are used to set attributes on a given column specification node.

setRowNodeAttributeStrategies

This property is optional.

The setRowNodeAttributeStrategies are used to set attributes on a given row node.

setCellNodeAttributeStrategies

This property is optional.

The setCellNodeAttributeStrategies are used to set attributes on a given cell node.

Examples

If you need examples of already configured tables, please see the Github pages for the CALS, XHTML and TEI table implementations.