Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular: detect changes inside expensive synchronous function

I have a function which executes an expensive synchronous task. In my case it's a client-side pdf generation through pdfkit, but let's just emulate it with a while-loop sleep.

I'd like to display a "loading" spinner before running the task, and hide it when it's done.

If everything is run synchronously, Angular won't have a chance to run the change detection loop before everything ends, so I thought I just had to find a way to run it asynchronously.

I tried wrapping it in a promise, so let's say I have:

sleep(ms) {
  return new Promise((resolve, reject) => {
    const expire = (new Date()).getTime() + ms;
    while ((new Date()).getTime() < expire);
    resolve();
  });
}

But whether I run it with a .then() call:

run() {
  this.running = true;
  this.sleep(2000).then(() => {
    this.running = false;
  });
}

or with async/await:

async run() {
  this.running = true;
  await this.sleep(2000);
  this.running = false;
}

the change is not detected until the function is over, and nothing gets displayed.

I guess the problem is that Javascript is still single-threaded and the promise is still run immediately when created, so everything basically still runs synchronously.

But even forcing change detection with ChangeDetectorRef.detectChanges() doesn't help.

The only solution I found so far is running it inside a setTimeout hack:

setTimeoutRun() {
  this.running = true;
  setTimeout(() => {
    this.sleep(2000);
    this.running = false;
  }, 100);
}

but it doesn't look like the right formal solution.

Is setTimeout really the only way?

Plunker: https://embed.plnkr.co/ywsIhulPMqjqwzaMxVjn/

like image 397
Tukler Avatar asked Nov 08 '22 08:11

Tukler


1 Answers

If your job is synchronous, then your loading logic needs to be synchronous as well. There is no other way (as far as I can tell) other than leveraging the event loop with setTimeout.

In other words, you can't do something like this.loading = true, because that will have to wait for change detection to run. You have to explicitly start the loading logic (manually add loader element to DOM so it's visible immediately, etc).

Otherwise, it by definition will have to wait until your long synchronous job is finished before it will start loading because the loader logic itself would be asynchronous and thus will only be called once the current execution (i.e. your synchronous job) is finished.

For example:

@Component({...})
export class MyComponent implements OnInit {
    constructor(private loadingService: LoadingService) {}

    ngOnInit() {
        // Start synchronous loader...
        this.loadingService.start();

        // By the time code reaches here, loader should be visible.

        // Do expensive synchronous task...
        this.expensiveSynchronousTask().then(() => {

            // Stop synchronous loader...
            this.loadingService.stop();

        });
    }
}
like image 82
Lansana Camara Avatar answered Nov 11 '22 13:11

Lansana Camara