Fonto Why & How: How do I do async things in a template!?

How do I do async things in a template!?

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 wondering how to do something asynchronous in a template!

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

Hey there!

We are working with an image that we are generating from an endpoint. We have made it so that it serves an image when we request it on an endpoint. We generate the url for the endpoint based on =the values of various descendant elements in the figure element. We then render an <img src="<the endpoint url>"> using the createInnerJsonMl callback in a configureAsObject. This works fine when the image can be set up without issue. However, we want to handle errors in a graceful way. So far I have set up an alt text, and I tried to add an onerror callback, however these are not great to work with. Do you have any advice? I added what we have so far to the support ticket.

A Fonto partner

First of all, that’s a very elegant solution with rendering an <img> element with a generated URL. The code so far looks like this:

 configureAsObject(sxModule, 'self::image[@outputclass = "graphic"]', undefined, {
    createInnerJsonMl: (sourceNode, renderer) => {
      const dataAboutNode = evaluateXPathToFirstNode('ancestor::fig[1]/data-about', sourceNode, readOnlyBlueprint);
      const src = getGraphicsURL(dataAboutNode);

      return [
        'img',
        {
          src
        }
      ];
    }
  });

The partner wants to add some kind of error checking, because right now if the src attribute turns out to get an invalid value, the editor just renders a normal placeholder.

The image gets a default ‘not found’ placeholder if it cannot be loaded for whatever reason.

The error checking feels like an easy problem to fix, just use the fetch API to load an image and render it in place. However, the renderInnerJsonMl callback of configureAsObject can not be asynchronous, so this will not work. What we have to do instead is use an API called addExternalValue. This API adds a way to store ‘some data’ linked to a value that can later be retrieved using an XPath function. If new data is set, the XPath function invalidates, invalidating the template or observed query or anywhere else where dependencies are tracked. The API docs of addExternalValue explain more on how this API works.

The addExternalValue API can be called at a random point in time to set some data, for example a dataUrl of an image. Using a dataUrl has the added benefit that the image will never be retrieved more than once when the image is rendered. To get hold of the dataUrl, we can use the getImageDataFromUrl API.

The getImageDataFromUrl function needs to be executed at some point. A logical place is inside the createInnerJsonMl callback of the configureAsObject. This callback is called once whenever an image element is rendered, and called again if the dependencies of the template change.

The first attempt to fix this looked like this:

// The fallback image if nothing could be loaded
const loadingImg = 'assets/images/graphic-loading-fallback.svg';
const fallbackImg = 'assets/images/graphic-failure-fallback.svg';

const setGraphicUrl = addExternalValue('http://app', 'getGraphicUrl', 'xs:string', 'xs:string', loadingImg);

 configureAsObject(sxModule, 'self::image[@outputclass = "graphic"]', undefined, {
    createInnerJsonMl: (sourceNode, renderer) => {
      const dataAboutNode = evaluateXPathToFirstNode('ancestor::fig[1]/data-about', sourceNode, readOnlyBlueprint);
      const rawUrl = getGraphicsURL(dataAboutNode);

        getImageDataFromUrl(window.document, rawUrl)
          .then(data => setGraphicUrl(rawUrl, data.dataUrl))
          .catch(() => setGraphicUrl(rawUrl, fallbackImg));

      return [
        'img',
        {
          src: evaluateXPathToString(xq`getGraphicUrl(${rawUrl})`, null, readOnlyBlueprint)
        }
      ];
    }
  });

It worked! But with one downside: the image kept re-rendering over and over. That is because the code retrieves an image data url, immediately reads whatever the old version was and renders it. When then the image is downloaded, the external value is updated, causing an update to be scheduled. This update then in turn retrieves a data url, which sets it, requesting a new update, finishing another loop.

To resolve this, we should only set a value once. The easiest way to do this is to create a Set of all the urls that are set. If a URL is already in the set, we can skip the image download.

// The fallback image if nothing could be loaded
const loadingImg = 'assets/images/graphic-loading-fallback.svg';
const fallbackImg = 'assets/images/graphic-failure-fallback.svg';

const setGraphicUrl = addExternalValue('http://app', 'getGraphicUrl', 'xs:string', 'xs:string', loadingImg);

const resolvedUrls = new Set<string>();

configureAsObject(sxModule, 'self::image[@outputclass = "graphic"]', undefined, {
  createInnerJsonMl: (sourceNode, renderer) => {
    const dataAboutNode = evaluateXPathToFirstNode('ancestor::fig[1]/data-about', sourceNode, readOnlyBlueprint);
    const rawUrl = getGraphicsURL(dataAboutNode);

    if (!resolvedUrls.has(rawUrl)) {
      getImageDataFromUrl(window.document, rawUrl)
        .then(data => {
          setGraphicUrl(rawUrl, data.dataUrl);
          resolvedUrls.add(rawUrl);
        })
        .catch(() => {
          setGraphicUrl(rawUrl, fallbackImg);
          resolvedUrls.add(rawUrl);
        });
    }

    return [
      'img',
      {
        src: evaluateXPathToString(xq`getGraphicUrl(${rawUrl})`, null, readOnlyBlueprint)
      }
    ];
  }
});
A rudimentary placeholder for graphics that could no be loaded for any reason.

This was tested with the partner, and it worked to their satisfaction.

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