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. This week, a partner is confused because the callback for their MutationHook is called a lot!
This one came in from the support board. Summarized, a partner is writing a MutationHook that should get triggered whenever an author is typing in an element. The hook is implemented using the JavaScript callback and the set-up looks roughly like this:
addMutationHook({ selector: 'self::description', valueQuery: 'normalize-space(.)', onEvent: callbackFunction, expectedResultType: ResultTypes.STRING, });
The partner is signaling the following:
- The hook is triggered even if the content of my
<description />
element is unchanged
Even if the author just places their cursor in the observed element, the hook is immediately triggered. I expect the hook to only be triggered when the user actually does something! - The OldValue and the NewValue are inconsistent!
It looks like when there is a selection in the element, the OldValue reflects the ‘current’ state, and the NewValue has the whole selection removed!
Let’s start by coming up with a solution. The MutationHook API is not the API we are looking for here! The partner is interested in getting a signal when the actual textual content of the element changes! MutationHooks are meant to keep several pieces of XML DOM in sync, such as a word-count attribute, or making sure that references are kept intact when attempting to remove an element.
Because of the similarities in the APIs, we managed to convert the codebase to use ObserveQueryResults quite easily. The partner could resolve their requirement and everything is well.
But why?!
But why did the hook trigger that often, and why do we have two APIs that are so similar? To explain this, we should go into how Fonto’s transaction framework works and how we compute whether an author can press on a button.
Let’s first go over the transaction framework. It has a number of phases. Let’s take the operation that inserts an image over the selection as an example. This operation contains a customMutation
that does the following:
- Empty out the current selection
- Attempt to insert an image at the place of the selection
- If this fails, split the element (likely a paragraph) where the selection was at and go to step 2
- If this succeeds, we are done
This is executed to determine whether a button is enabled. Every time a user changes the selection. While most selections certainly allow inserting an image, there are many that actually do not. Take a selection that wraps over an element that can not be removed for example! Or a selection that is in element that does not allow images, and can not be split, like a title! To be on the safe side, we invalidate and recompute whether an operation is enabled for any visible button!
But! We are not actually changing the XML whenever the user changes the selection, that would be silly. To be sure we are not changing the XML document, we invented an overlay over the DOM, which we call Blueprints.
Blueprints are a term that should sound familiar to any Fonto developer. We actually wrote a paper on it that we presented at XML Prague: read the paper, or watch the video here! In short, Blueprints represent a possible future of a DOM. They can be realized, or they can be discarded, depending on the validity of the DOM and whether we are actually executing a transaction, or just discovering whether it is enabled. This follows the following steps:
- Perform the custom mutation
- See which nodes are changed and validate the document. If the document is not valid according to the schema, report the custom mutation (and therefore the operation) to be disabled
- See which MutationHooks are invalidated. Evaluate them one by one, checking the schema validity after each hook.
- After all hooks are done with their run, check again which hooks are invalidated and call their callbacks. Do this until all hooks are stable
- We are now at a schema-valid situation. If we were just checking if the custom mutation is enabled, we stop, otherwise we continue with the next step.
- Apply the Blueprint
- Collect the observed queries that are invalidated and call their callbacks
- Collect what in the rendered view of the editor is invalidated, and rerender that.
- Collect what operations are now invalidated, and start computing whether they are enabled
- We are now done with executing the whole transaction!
This is why Mutation Hooks are called that often, they are called whenever a user changes anything, including their selection. Furthermore, the mutation hooks are used to determine whether an operation is enabled.
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!
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