I know Typescript/Javascript pretty well, but am working on my first Angular app. It has a big component (a CAD drafting board) that includes a few canvases. Images are loaded programmatically.
What's a clean, scalable way to ensure images are loaded before ever trying to render with ctx.drawImage()
? Seems like this ought to be a solved problem, but I'm finding only fragments that look like kludges.
Some use Promises, but NgAfterViewInit()
is not async. I've read you can just declare it so anyway and call await
on a Promise that's doing the loading, but this seems ugly and error prone.
Maybe something old school like this?
class ImagesLoader {
private readonly images: Map<string, HTMLImageElement>;
private readonly pendingActions: ((images: Map<string, HTMLImageElement>) => void)[] = [];
public readonly errors = new Set<string>();
private remainingCount: number;
constructor(urls: string[]) {
this.remainingCount = urls.length;
this.images = new Map<string, HTMLImageElement>(
((loader) => {
return urls.map(url => {
const image = new Image();
image.onload = () => {
// On last load, execute pending actions.
if (--loader.remainingCount == 0) {
loader.pendingActions.forEach(action => action(loader.images));
loader.pendingActions.length = 0; // Not necessary, but nice.
}
}
image.onerror = () => loader.errors.add(url);
image.src = url;
return [url, image];
})
})(this));
}
public invokeAfterLoaded(action: (images: Map<string, HTMLImageElement>) => void): void {
if (this.remainingCount == 0) {
action(this.images);
} else {
this.pendingActions.push(action);
}
}
}
// Start loading the images. Put this in the component constructor.
this.loader = new ImagesLoader([
'https://www.jqwidgets.com/wp-content/design/i/logo-jqwidgets.svg',
'https://www.greencastonline.com/assets/img/logo.png']);
// Then we can invoke rendering in NgAfterViewInit, possibly delayed until loaded,
// and re-render as needed with the same loader.
this.loader.invokeAfterLoaded((images) => console.log("first"));
this.loader.invokeAfterLoaded((images) => console.log("second"));
I just don't want to pick a bad pattern that will need to be replaced later when there's much more code.
NB: For the editors: This question is not asking for an opinion. It is asking for a correct, idiomatic, readable way to get the job done in Angular, at which I'm not an expert. This is what "clean" means. How often as engineers have we wasted time on code that went three times around the block to get across the street? Forty lines where there could have been two? This costs time, money, and the good will and respect of fellow engineers. Just hacking up code that sort-of-works is what amateurs do. (This is the level of my proposed solution.) If SO defines "opinion" in this manner, then I'm out of here. I'm convinced half the editors have never worked on significant software in a team environment.
Use signals and observables maybe ?
@Component({
/* ... */
})
export class MyComponent {
$urls = input.required<string[]>();
private images$ = toObservable(this.$urls).pipe(
map((urls) =>
urls.map(
(url) =>
new Promise((res, rej) => {
const image = new Image();
img.onload = () => {
// Do your thing then
res(image);
};
image.onerror = rej;
image.src = url;
}),
),
),
map((promises) => Promise.allSettled(promises)),
switchMap((images) => from(images)),
);
$images = toSignal(this.images$, { initialValue: [] });
constructor() {
effect(() => {
const images = this.$images();
if (!images.length) return;
console.log('Images loaded', images);
});
}
}