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 aseverity
attribute that can have a number of values and an attributelast-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 thecurrent-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!
Developer advocate / Evangelist. Has been with Fonto since it all began in 2013. He’s currently designing the next steps in Fonto Developer APIs with the input of our valuable partners.
In his spare time, Martin is an avid home brewer.
Receive updates on new Fonto Why & How blog posts by email