Loading indicator while Value Converter computes?

I have a function that takes an array and does a bunch of calculations on it. It’s a pretty beefy calculation, so I moved it off to a web worker.

Previously I would have a rendering flag on the class that I would set to true before the function ran, and then I’d queue a “microTask” for setting it to false. This allowed me show.bind= on a loading spinner components and the array in my view. Once the rendering prop was flipped all the data would be there.

That worked great but now I’ve moved the logic into a Value Converter (with a custom binding behavior to allow the promise from the web worker), but now my trick no longer works… The task fires right after the async function finishes up instead of when it finishes rendering. Is there a way to accomplish what I was doing before?

Value converter:

export class TransformEntriesValueConverter {
    async toView(entries, day) {
        if (!entries) {
            return [];
        }

        return await mapEntries(entries, day);
    }
}

Binding behavior:

export class AsyncBindingBehavior {
    public bind(binding, _scope, callbackFn) {
        binding.originalupdateTarget = binding.updateTarget;

        binding.updateTarget = async a => {
            const d = await a;
            binding.originalupdateTarget(d);

            if (_.isFunction(callbackFn)) {
                callbackFn();
            }
        };
    }

    public unbind(binding) {
        binding.updateTarget = binding.originalupdateTarget;
        binding.originalupdateTarget = null;
    }
}

View:

<template>
  <timeline-container
        repeat.for="day of displayDays"
        if.bind="!day.hidden"
        loading.bind="day.isLoading || day.isRendering"
    >
        <time-entry
            repeat.for="entry of day.entries | transformEntries:day & async:dayLoading(day)"
            item.bind="entry"
            class="timeline-week"
        >
        </time-entry>
    </timeline-container>
</template>

ViewModel:

export class MyClass {
  ...
  public dayLoading(day) {
    this.taskQueue.queueMicroTask(() => {
        day.isRendering = false;
    });
  }
  ...
}
1 Like

This is an interesting question. I don’t have the time for a full-fledge solution now, but if you can make a simple example on https://gist.dumber.app/ I will try to help you debug your example tomorrow.

1 Like

I’m having an issue getting it to work on that. Something about how it’s importing the CSS (from Webpack looks like?).

This is actually for an open source plugin, so maybe it’s easier to view the issue locally?

Here’s the branch I’m working on: https://github.com/bindable-ui/bindable/tree/timeline-columns

If you launch the dev-app, the demo page is at http://localhost:9000/#/components/timeline/demo

If that doesn’t work for you, I can play more with the gist thing to figure it out.

1 Like

Could you instead emit a custom event (either native or event-aggregator) or broadcast a signal from the Binding Behavior which you listen for to toggle the switch? Alternaively the VC could accept a callback as parameter where you’d set your toggle

1 Like

I have a callback in the custom binding behavior. I wouldn’t be able to put on in the VC because the value hasn’t been assigned until it returns.

I think an event would have the same issue as the binding behavior… It just fires in the awkward period between when it is assigned and before it is painted on the DOM.

1 Like

There are a few ways to handle your requirement: connecting a value converter and a custom element instance.

In an Aurelia app, value converters are singleton, which means injecting them will give you the same instance that is used in binding expressions. So this gives us one way to do it: inject the value converter to the App instance, and let me app subscribe to the value converter and get notified whenever it is busying computing. Let’s call this point to point subscription (made up). How to make the value converter subscribe-able: via either making the value converter extend the EventAggregator class, or use:

import { EventAggregator, includeEventsIn } from 'aurelia-event-aggregator';

export interface MyValueConverter extends EventAggregator {}
export class MyValueConverter {
  constructor() {
    this.ea = includeEventsIn(this);
    // this.ea.publish
    // will be the same with this.publish now
  }
}

and in app:

@inject(MyValueConverter)
export class MyApp {
  constructor(myValueConverter) {
    myValueConverter.subscribe('busy', () => {
      this.loading = true;
    });
  }
}

With the above approach, you need to be careful, as you may accidentally introduce some circular dependency issues during large refactoring. It may catch you off guard and waste a good day. Example would be App requires value converter, value converter requires another services, and that services accidentally requires some semi private export from some other module that also requires this value converter (hypothetical scenario).

Another way to do it is to use a simple counter as change notifier within your app. A simple class looks like this:

// just a normal class, not custom element
export class BusyIndicator {
  busy = 0;

  on() {
    this.busy++;
  }

  off() {
    this.busy--;
  }
}

And then you would inject this class into any value converter, or binding behavior like this:

@inject(Busy)
export class AsyncBindingBehavior {
    constructor(busy) {
      this.busy = busy;
    }
}

Then whatever the complex things you do, you can simply solve it by ensuring every this.busy.on() is paired with a this.busy.off() call. That should give you good sleep. As loading indicator will be simply like this:

@inject(BusyIndicator)
export class App {
  constructor(busy) {
    this.busy = busy;
  }
}
<loading-indicator loading.bind="busy.busy > 0">
2 Likes

Thanks for the response!

I think the problem would be that there could be up to 7 of the value converters running at once, so I’m not sure it’d work that way. And it’s not so much the calculating that’s having issues, it’s when it finishes rendering in the DOM.

1 Like

But wouldn’t queuing a macrotask make sure you’re always after rendering is done? I see you’re using a micro task right now perhaps that’s the issue

1 Like

I’ve tried micro and macro and they both did the same thing

1 Like

So it seems something else in there is delaying the rendering. Perhaps some of your codeparts make use of settimeout? On the other hand again that’s weird since you mentioned it used to work before …

1 Like

The only difference is I’m using the value converter now

1 Like

I’m wondering if it’s possible it could be something I changed with the rendering stuff that could make it not work? Something with my show.binds

1 Like

Changed my show.binds around and nothing changes.

Doing the processing before assigning the array works fine. So I need to figure out some other way to doing it if I want to keep the value converter as an option.

1 Like

@m0ngr31 you can easily do a debounce on the loading indicator binding in combination with the counter and it should work fine for you, maybe tried that? :smiley:

@huochunpeng had a small guide here Tip: show spinner only when something is slow

Why not get rid of dayLoading callback, replace it with following?

export class TransformEntriesValueConverter {
    async toView(entries, day) {
       day.isRendering = true;
       let mappedEntries = [];
       try {
         mappedEntries = entries ? await mapEntries(entries, day) : [];
         day.isRendering = false;
       } catch(e) {
         console.error(e);
         day.isRendering = false;
       }
       return mappedEntries;
    }
}

Sure this introduces ugly side effect, but reduces the complexity of your implementation.

Working demo: https://gist.dumber.app/?gist=9f334967c012f8b303f8cfa549eeb6dc&open=src%2Fapp.js&open=src%2Fapp.html

1 Like

I tried reproducing your gist, and it wasn’t consistent enough with the debounce to be reliable.

1 Like

The debounce is decoupled from the value converter. What went wrong?

If you were talking about spinner not showing up every time, that’s debounce ignoring fast change. You can manually adjust the delay debounce:500 to fine tune the timing.

1 Like