Fonto Why and How: How do I know when a document is saved?

In this weekly series, Martin describes a question raised by a Fonto developer, how it was resolved and why Fonto behaved like that in the first place. This week, a partner is wondering how to run some code when a document is saved!

A support ticket came in that went a little like this:

Hello there,

We would like to run some custom code when a document is fully saved. We are using the normal CMS APIs, so no iframe communication and we want to tell the page hosting the editor a save is done, so we can safely close the editor. I did not see a way to inject that into the CMS APIs. How can we do this?

Kind regards, A Fonto partner

Responding to document states comes up every so often. Sometimes it is about responding to when a lock is acquired or lost, sometimes it is about immediately saving a document when it changed, etcetera. In all these cases, it is about a piece of information that we can retrieve from the fonto:remote-document-state function.

Like all XPath functions we offer from the platform, they can be observed. If we take our dita example editor and add the following code, we can already see updates.

import indexManager from 'fontoxml-indices/src/indexManager';
import ReturnTypes from 'fontoxml-selectors/src/ReturnTypes';
import xq from 'fontoxml-selectors/src/xq';

const observer = indexManager.observeQueryResults(
    xq`fonto:remote-document-state("clogs/why.xml")`,
    null,
    {
        expectedResultType: ReturnTypes.MAP
    });
console.log('Initial results', observer.getResults());
const removeCallback = observer.resultsChangedNotifier.addCallback(
    () => {
        console.log('new results', observer.getResults())
    });

This example can be placed inside any install.ts file in the editor. It will show a console log whenever something changes in the remote document state of the ‘why.xml’ document. Try copying it over, adapting the document id to a document that’s actually loaded and see for yourself!

This is a good first step, but we should make it more generic. We should observe all loaded documents, not just one hard-coded one. The set of loaded documents can change over time. Especially when using JIT loading. We can use the documentsCollectionChangedNotifier on the documentsManager class to do this. This is a notifier that fires whenever a document is loaded or unloaded. The user of this API would be responsible to detect the difference between now and the previous time this API was fired. You can do this with a Set, like this:

import documentsManager from 'fontoxml-documents/src/documentsManager';
import type { DocumentId } from 'fontoxml-documents/src/types';
import indexManager from 'fontoxml-indices/src/indexManager';
import ReturnTypes from 'fontoxml-selectors/src/ReturnTypes';
import xq from 'fontoxml-selectors/src/xq';

export default function install(): void {
    const knownDocumentIds = new Set<DocumentId>();
    documentsManager.documentsCollectionChangedNotifier.addCallback(() => {
        for (const documentId of documentsManager.getAllDocumentIds()) {
            if (knownDocumentIds.has(documentId)) {
                return;
            }
            knownDocumentIds.add(documentId);
            console.log(
                `The document ${documentsManager.getRemoteDocumentId(
                    documentId
                )} got loaded!`
            );
        }
        for (const documentId of knownDocumentIds) {
            if (!documentsManager.hasDocument(documentId)) {
                knownDocumentIds.delete(documentId);
                console.log(`The document ${documentId} got unloaded!`);
            }
        }
    });
}

This can also be placed in an install.ts file in your editor.

Combining these two parts will result into something like this:

import documentsManager from 'fontoxml-documents/src/documentsManager';
import type { DocumentId } from 'fontoxml-documents/src/types';
import indexManager from 'fontoxml-indices/src/indexManager';
import type { XPathObserver } from 'fontoxml-indices/src/types';
import ReturnTypes from 'fontoxml-selectors/src/ReturnTypes';
import xq from 'fontoxml-selectors/src/xq';

export default function install(): void {
    const observerByDocumentId = new Map<DocumentId, XPathObserver>();
    documentsManager.documentsCollectionChangedNotifier.addCallback(() => {
        for (const documentId of documentsManager.getAllDocumentIds()) {
            if (observerByDocumentId.has(documentId)) {
                return;
            }
            const documentNode = documentsManager.getDocumentNode(documentId);

            const observer = indexManager.observeQueryResults(
                xq`fonto:remote-document-state(fonto:remote-document-id(.))`,
                documentNode,
                {
                    expectedResultType: ReturnTypes.MAP,
                }
            );

            console.log('Initial results', observer.getResults());

            observer.resultsChangedNotifier.addCallback(() => {
                console.log('new results', observer.getResults());
            });

            observerByDocumentId.set(documentId, observer);
        }

        for (const documentId of observerByDocumentId.keys()) {
            if (!documentsManager.hasDocument(documentId)) {
                const observer = observerByDocumentId.get(documentId);
                observer.stopObservingQueryResults();

                observerByDocumentId.delete(documentId);
                console.log(`The document ${documentId} got unloaded!`);
            }
        }
    });
};

This will now fire any time the remote document state of any document will fire! But that’s not yet what we want to achieve. In this concrete case, we want to listen to a document getting saved. We can listen to this by observing the isSaving property of the remote document state: if that property goes from true to false, a save has happened. Depending on your use case, you might also want to check the hasSaveError field. In this example, let’s go for a simple route. We can rewrite the code around the observer to this:

const observer = indexManager.observeQueryResults(
    xq`fonto:remote-document-state(fonto:remote-document-id(.))?isSaving`,
    documentNode,
    {
        expectedResultType: ReturnTypes.BOOLEAN,
    }
);

let wasSaving = observer.getResults();

observer.resultsChangedNotifier.addCallback(() => {
    const isSaving = observer.getResults();
    if (!isSaving && wasSaving) {
        console.log('A save is done!');
    }
    wasSaving = isSaving;
});

This can of course be adapted to your specific use case. We see partners responding to locks being lost (the lockState field), to saves being rejected (hasUnsaveableContent) or a document going out of sync (the isOutOfSync field.)

XPath is a powerful way to make a lot of Fonto’s application state observable. We are always adding new APIs that expose more API through XPath. Using the addExternalValue API you can also expose the application state to the XPath engine.

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 *

Fonto 8.0 WebinarApril 13, 6.30 pm CET / 11.30 am CST

Providing you with insights from our team and a QA session.

Scroll to Top