How to create custom XPath/XQuery invalidation

XPath and XQuery queries in the Fonto Editor invalidate based on changes in the DOM. There are cases in which you want them to invalidate on another, external, source. This guide will help you set up a custom XPath function which can track external dependencies.

This guide will help you set up a custom XPath function which can track external dependencies. Unlike normal custom XPath functions, which are registered with the registerCustomXPathFunction function, dependency tracked functions are registered using the addExternalValue function.

Creating a dependency on a manager

This first example will show you how to create a custom XPath function which invalidates when the number of loaded documents in the editor changes. The number of loaded documents will be shown in a widget on the paragraph element.

The first thing to do is call the addExternalValue function. It should be called in a configureSxModule.js file. Import the addExternalValue function from the fontoxml-selectors package like this:

JavaScript

import addExternalValue from 'fontoxml-selectors/src/addExternalValue.js';

Then, call the addExternalValue function in the function body of the exported function in configureSxModule.js:

JavaScript

const setDocumentsData = addExternalValue(
	'http://app',
	'getDocumentData',
	'xs:string',
	'xs:integer',
	0
);

Save its return value in a variable, we'll need it later on. The addExternalValue function requires five arguments to be passed to it. The first argument is a namespace for the custom XPath function. The second argument is a name for the custom XPath function. The third argument is the data type of the custom XPath function's argument. The fourth argument is the data type of the custom XPath function's return type. And the fifth, and final, argument is a default value to return.

We now need to subscribe to a notifier on the documentsHierarchy, called hierarchyChangedNotifier. This notifier fires when something in the documents hierarchy changes, this also happens when a document gets loaded, which is what we are currently interested in. Add the following bit of code to, still the same, configureSxModule.js file:

JavaScript

documentsHierarchy.hierarchyChangedNotifier.addCallback(() => {
	// Find all loaded documents in the hierarchy
	const loaded = documentsHierarchy.findAll(hierarchyNode =>
		hierarchyNode.documentReference.isLoaded()
	);

	// Fire and forget, this returns a promise.
	setDocumentsData('loaded-documents', loaded.length);
});

Note that we here call the function that is returned from calling the addExternalData function.

Now that we have registered a custom XPath function which invalidates on an external data source, we can use it anywhere we can write XPath/XQuery. Again, in a relevant configureSxModule.js file:

JavaScript

// p
//     A paragraph element (<p>) is a block of text containing a single main idea. Category: Body elements
configureAsBlock(sxModule, 'self::p', t('paragraph'));

configureProperties(sxModule, 'self::p', {
	blockHeaderRight: [
		createLabelQueryWidget(
			'"Number of loaded documents: " || Q{http://app}getDocumentData("loaded-documents")'
		)
	]
});

This bit of element configuration was taken from the DITA example editor. The new thing is a label query widget on the paragraph element. This widget shows the number of loaded documents. The XPath used here should now invalidate every time a document gets loaded.

All code together, in a single configureSxModule.js file:

JavaScript

import addExternalValue from 'fontoxml-selectors/src/addExternalValue.js';
import configureAsBlock from 'fontoxml-families/src/configureAsBlock.js';
import configureProperties from 'fontoxml-families/src/configureProperties.js';
import createLabelQueryWidget from 'fontoxml-families/src/createLabelQueryWidget.js';
import documentsHierarchy from 'fontoxml-documents/src/documentsHierarchy.js';

export default function configureSxModule(sxModule) {
	const setDocumentsData = addExternalValue(
		'http://app',
		'getDocumentData',
		'xs:string',
		'xs:integer',
		0
	);

	documentsHierarchy.hierarchyChangedNotifier.addCallback(() => {
		// Find all loaded documents in the hierarchy
		const loaded = documentsHierarchy.findAll(hierarchyNode =>
			hierarchyNode.documentReference.isLoaded()
		);

		// Fire and forget, this returns a promise.
		setDocumentsData('loaded-documents', loaded.length);
	});

	// p
	//     A paragraph element (<p>) is a block of text containing a single main idea. Category: Body elements
	configureAsBlock(sxModule, 'self::p', t('paragraph'));

	configureProperties(sxModule, 'self::p', {
		blockHeaderRight: [
			createLabelQueryWidget(
				'"Number of loaded documents: " || Q{http://app}getDocumentData("loaded-documents")'
			)
		]
	});
}