Fonto Why & How: How do I change two attributes at once?!

Fonto Why & How: How do I change two attributes at once?!

In this weekly series, Martin describes a question that was raised by a Fonto developer, how it was resolved and why Fonto behaved like that in the first place. One of our own developers was puzzled: how do I change two attributes on different elements at once?

One of our own developers reached out directly to me with a question. They wanted to change two different attributes at once. They knew that the set-attributes operation would not work because it only allows working with a single element, and they have two. They also knew that repeating that operation twice would not work, because it causes two undo steps. They used XQuery Update Facility to change the element, but they had a strange error. What is happening?

There was a link to the XPath playground with a reproducable error: xpath.playground.fontoxml.com. This playground contained the following code:

for $role in contrib/role return
	replace value of node $role/@attr1 with 'changes',
	replace value of node $role/reason/@value with 'more changes'

The error caused here is simple to fix. The comma (,) operator binds strongly, breaking off the for expression. What is actually written here is (for $role in contrib/role return (...)), replace value of node .... This can be fixed by using a few round braces:

for $role in contrib/role return (
	replace value of node $role/@attr1 with 'changes',
	replace value of node $role/attr2 with 'more changes'
)

The developer refactored this to use the values they wanted to use and added this to the operation. He confirmed it worked, but it was not very readable. An earlier version of the operation only replaced the value in a single attribute, making the script nice and short. With this extra case added, it was a lot larger; running out of the screen. Including whitespace and indentation in the JSON file, we exceeded 300 characters. Furthermore, the whole script is just a string. With an IDE that does some basic linting (like VSCode with its XML tools extension installed), the error about variables would have been uncovered earlier.

Fonto knows the concept of XQuery modules. By declaring one in the src folder of any package, it is available in any operations.json file. We made a module with the following contents:

xquery version "3.0";
module namespace app = "namespaceuri";

declare %public %updating function app:update-contrib-existing-data($contextNode as node()?, $creditTerm as xs:string?, $creditIdentifier as xs:string?) {
    for $role in $contextNode/role return (
        if($role/@vocab-term) then
            replace value of node $role/@vocab-term with $creditTerm
        else (),
        if($role/@vocab-term-identifier) then
            replace value of node $role/@vocab-term-identifier with $creditIdentifier
        else ()
    )
};

This module is then used like this in the operation:

{
  "type": "operation/execute-update-script",
  "data": {
    "expression": "import module namespace app = 'namespaceuri'; app:update-contrib-existing-data($data?contextNode, $data?creditTerm, $data?creditIdentifier)"
  }
}

But Why?

We used an XQuery Update Facility script to edit two places at once. But why did we go for XQuery Update Facility here? Why not for a custom mutation, or a pre-made operation? Or use a mutation hook to synchronize the two places? And why do we not need something like this when editing inclusions? That’s also editing two places at once?

That is a common question. To make these choices, we ask ourselves these questions:

Is (one of the) places that need to be synchronized text that can be edited using the CVK (as in, is it visualized in the editor as a normal editable text)? In that case, we should use a mutation hook to synchronize the two places. A mutation hook can react on any change and update another piece of XML without the author being involved.

Is the operation simple, and do we have a pre-made operation? Just use it! Less code is always better code!

Is the operation going to just touch attributes, or elements where it is OK that the cursor is moved around? In that case, XQuery Update Facility might be the shortest way to write the operation.

If the cursor position is important, or we are going to split elements, merge them, or do other complex things with them, a custom mutation is the only way forward. XQuery Update Facility is perfect for simple operations, but especially merging, or splitting structures might get tedious: a custom mutation written in TypeScript might be the best way forward.

I hope this explained how Fonto works and why it works like that. During the years we 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