Fonto: Why & How: How can I react to changes in the XML?!

How can I react to changes in the XML?

In this weekly series, Martin describes a question that was raised by a Fonto developer, how it was resolved and why Fonto behaves like that in the first place. This week, a support question came in asking us how to react to changes in the XML.

The question came in through our support board, and it went a bit like this:

Hey there! We have a question. We would like to know how we can react to changes to XML elements, specifically their attributes. We want to do this because we want to update an attribute on the element, documenting when it was last updated. Something akin to MutationObservers would be ideal! Do you offer such an API?

Love, a Fonto developer working with a partner

We actually offer two APIs that can react to changes in XML documents. The first is usually referred to as observeQueryResults. It accepts a query and a node and it provides a notifier which will trigger its registered callbacks when the results of the query change. This is also the API that backs the useXPath React Hook.

The downside of this API is that the callbacks are not evaluated in the same transaction as where the XML changes happen. The callbacks should not make any XML changes because those changes will then generate their own undo steps. This is a bit dangerous: if a user would use the undo operation to undo only those automatic changes, but not the changes they applied themselves, the document may get into an unexpected state. Also, if the automatically applied changes would make the document invalid, those changes will not be applied while the manual changes are.

The other API is referred to as MutationHooks. They accept a filter that matches nodes that should be looked at and a query whose results are going to be tracked. They can mutate XML because they run inside the same transaction as the one where the change is made. The changes they make are validated, and if the document ends up invalid to the schema, the whole transaction is rejected. They are even evaluated when changing XML in a sandbox, when computing the state of an operation. The caveat of this API is that the callback should not have any side-effects: it should not change any state outside of the (sandboxed) XML. The ObserveQueryResults API is invented for those usecases.

In our support question, the partner wants to make changes to the XML, so we should use the MutationHook API. To learn how we can use it, we should take a look at what our partner wants to do exactly.

I have an element that looks like this: <note severity="something" last-changed-severity="2022-03-10Z">. It has a severity attribute that can have a number of values and an attribute last-changed-severity that should be set to the date of the last change. Currently my authors need to update that attribute to the current date whenever they change it, but I would like to make their lives easier. I have thought about computing a difference at the CMS whenever an author saves the document and applying the changes there, but Fonto would mark those documents as out-of-sync in the browser. We primarily use Fonto to edit our documents, so we would like to handle this in Fonto!

In summary, the listener should look like this:

  • Watch for all elements matching the self::node XPath
  • For these elements, look at the result the query ./attribute::severity.
  • Whenever the result of that query changes, set value of the last-changed-severity to the result of the current-date() function. Or insert that attribute if it did not already exist.

The JavaScript part of the code should look like this:

// In any 'install.ts' file:

addMutationHook({
    selector: xq`self::node`,
    valueQuery: xq`./attribute::severity`,
    onEvent: {
        functionLocalName: 'on-severity-change',
        namespaceURI: 'http://www.example.fontoxml.com/hooks'
    }
});

This references an XQuery function that also needs to be declared. It is usually the easiest to fine-tune the selector and valueQuery before we write the actual hook:

xquery version "3.1";

module namespace hooks="http://www.example.fontoxml.com/hooks";

declare %public %updating function hooks:on-severity-change (
    $event-type as xs:string,
    $node as node(),
    $previous-value as xs:string?,
    $current-value as xs:string?
) {
    (: Use a trace to test whether the mutation hook triggers nicely :)
    trace((), 'Called with ' || $event-type || ' previous: ' || $previous-value || ' current: ' || $current-value)
};

This works! We are getting the log whenever we (in the editor provided by the partner) change the value of the severity attribute. Now to apply the attributes. We find the xpath playground useful for this. I prepared a simple playground example. In this example I changed the function to the following:

declare %public %updating function hooks:on-severity-change (
    $event-type as xs:string,
    $node as node(),
    $previous-value as xs:string?,
    $current-value as xs:string?
) {
    replace value of node $node/@last-changed-severity with current-date()
};

After testing this, the partner was content with the solution, but they saw an error with existing <note/> elements that had no last-changed-severity attribute set: an error with the code XUDY002 was thrown: XUDY0027: The target for an insert, replace, or rename expression expression should not be empty. We can reproduce this in the playground by removing the attribute from the xml.

The quickest fix for this error is to test whether the attribute is present before trying to set it: if ($node/@last-changed-severity) then replace value of node $node/@last-changed-severity with current-date() else insert node attribute last-changed-severity {current-date()} into $node.

With this bug out of the way, the partner was fully satisfied. Because they expect to repeat this same pattern of attributes that can be absent in multiple places, they chose to extract the logic to a separate function that they could reuse in multiple places.

I hope this explained how Fonto works and why it works like that. During the years we have built quite the product and we are aware some parts work in unexpected ways for those who have not been with it from the start. If you have any points of Fonto you would like some focus on, we are always ready and willing to share! Reach out on Twitter to Martin Middel or file a support issue!

Stay up-to-dateFonto Why & How posts direct in your inbox

Receive updates on new Fonto Why & How blog posts by email

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top