Create a mutation hook

This guide explains how and when to use mutation hooks to react on changes to a document.

Sometimes it is necessary to 'react' on changes made to a document. For example, an element needs to be updated with the value of another element, or an attribute needs to be changed whenever an author makes a change to a document. This can be done by using MutationHooks. This guide will provide examples and tips on how to write performant hooks. For more information on Mutation hooks in general and examples, refer to the concept page on mutation hooks.

In this guide we will go through the steps to create a mutation hook. The example hook will update the content of a paragraph when it is added, you might have to change some selectors to match your schema.

Step 1: Create the necessary files

Mutation hooks need at least two files containing code: an install.js that holds the call to addMutationHook and either an XQuery module file (commonly called hooks.xqm, though the name does not matter) or a JavaScript file that holds the onEvent callback. These files should be created in the package that relates the most to the hook.

Step 2: Configure the triggers for the addMutationHook

Mutation hooks have three variable parts: execute a callback when for some node there is a change to some related value.

Step 2.1: Stub the callback in the XQuery module

In the XQuery module, add the following code. This will register a callback that will log all of its parameters to the console using the fn:trace#2 function. This is a great start to see whether the hook is executed at the correct moment.

xquery version "3.0";

(: The namespace URI that is used in other places of the editor :)
module namespace app-hooks="";

declare %public %updating function app-hooks:on-paragraph-change ($event-type as xs:string, $node as node(), $previous-value as item()*, $current-value as item()*) {
	trace((), "Executing hook! " || $event-type || " name: " || name($node) || " prev: " || $previous-value || " cur: " || $current-value)

Step 2.2: Wire the callback to the hook

In the install.js, add the following code in order to import the mutation hook:

import addMutationHook from 'fontoxml-blueprints/src/addMutationHook.js';

And add the following code in the body of the install callback:

    selector: 'self::simpara',
    onEvent: {
        functionLocalName: 'on-paragraph-change',
        namespaceURI: ''

Step 3: Test the triggers for the hook

Open the editor and perform the action that should execute the hook. This should cause the console to display the result of the trace that was defined in step 2.1. It might be that the console logs occur more than expected, this is because whenever an operation triggers a hook during the state computation, it will execute the callback. If you changed the value query, it might be wrong. Add a trace call to that to determine whether it results in the correct value.

Step 4: Implement the callback

Now we can implement the callback with the actual implementation in which we will update the title.

xquery version "3.0";

(: The namespace URI that is used in other places of the editor :)
module namespace app-hooks="";

declare %public %updating function app-hooks:on-paragraph-change ($event-type as xs:string, $node as
node(), $previous-value as item()*, $current-value as item()*) {
    if(trace($event-type) = 'add') then
        replace value of node $node with "Paragraph was added on " || current-dateTime()
    else (: The paragraph is not added, no action needed :) ()

For your final implementation refer to the XQuery update facility  guide for more information on how to compose the action. To make a hook callback disable the operation that triggers it, import the namespace and return the return value of the not-allowed function. Returning the empty sequence from the callback is handled the same as returning the result of the ok function.

To set the selection in XQuery, you can use selection PIs.

Step 4.1 XQuery versus the JavaScript API

While we recommend to write the callback in XQuery, it is sometimes required to write it in JavaScript. The callback needs to be written in JavaScript if the callback needs to do one of the following things:

  1. Use a base-flow primitive, such as insertNodeHorizontal. These are not yet available when using XQuery
  2. Move nodes around in the dom. Moves in XQuery are currently implemented as a remove followed by an insert of a clone of the element. This may cause issues when using add-ons like fontoxml-track-changesfontoxml-annotations or fontoxml-feedback.

Future versions of Fonto will support these use cases and will start deprecating the JavaScript API once the XQuery Update Facility APIs get up-to-par with the JavaScript API. Writing a hook in XQUF will make it more future-proof.

The JavaScript API works like writing a custom mutation. Check the API documentation for examples.