As an example, we have a simple application that scrolls through levels in the style of the game Super Mario. Each level consists of tiles that are available in four different styles: overworld, underground, underwater, and castle. In our implementation, users can switch freely between these styles. Figure 1 shows the first level in overworld style, while Figure 2 shows the same level in underground style.
Figure 1: Level 1 in overworld style
Figure 2: Level 1 in the underground style
A LevelComponent in the example application takes care of loading level files (JSON) and tiles for drawing the levels using an httpResource. To render and animate the levels, the example relies on a very simple engine that is included with the source code but is treated as a black box here in the article.
HttpClient in the substructure enables the use of interceptors
At its core, the new httpResource currently uses the good old HttpClient. Therefore, the application has to provide this service, which is usually done by calling provideHttpClient during bootstrapping. As a consequence, the httpResource also automatically picks up the registered HttpInterceptors.
However, the HttpClient is just an implementation detail that Angular may eventually replace with a different implementation.
iJS Newsletter
Keep up with JavaScript’s latest news!
Level files
The different levels are described by our example JSON files, which define which tiles are to be displayed at which coordinates (Listing 1).
Listing 1:
{
"levelId": 1,
"backgroundColor": "#9494ff",
"items": [
{ "tileKey": "floor", "col": 0, "row": 13, [...] },
{ "tileKey": "cloud", "col": 12, "row": 1, [...] },
[...]
]
}
These coordinates define positions within a matrix of blocks measuring 16×16 pixels. An overview.json file is provided with these level files, which provides information about the names of the available levels.
A LevelLoader takes care of loading these files. To do this, it uses the new httpResource (Listing 2).
Listing 2:
@Injectable({ providedIn: 'root' })
export class LevelLoader {
getLevelOverviewResource(): HttpResourceRef<LevelOverview> {
return httpResource<LevelOverview>('/levels/overview.json', {
defaultValue: initLevelOverview,
});
}
getLevelResource(levelKey: () => string | undefined): HttpResourceRef<Level> {
return httpResource<Level>(() => !levelKey() ? undefined : `/levels/${levelKey()}.json`, {
defaultValue: initLevel,
});
}
[...]
}
The first parameter passed to httpResource represents the respective URL. The second optional parameter accepts an object with further options. This object allows the definition of a default value that is used before the resource has been loaded.
The getLevelResource method expects a signal with a levelKey, from which the service derives the name of the desired level file. This read-only signal is an abstraction of the type () => string | undefined.
The URL passed from getLevelResource to httpResource is a lambda expression that the resource automatically reevaluates when the levelKey signal changes. In the background, httpResource uses it to generate a calculated signal that acts as a trigger: every time this trigger changes, the resource loads the URL.
To prevent the httpResource from being triggered, this lambda expression must return the value undefined. This way, the loading can be delayed until the levelKey is available.
Further options with HttpResourceRequest
To get more control over the outgoing HTTP request, the caller can pass an HttpResourceRequest instead of a URL (Listing 3).
Listing 3:
getLevelResource(levelKey: () => string) {
return httpResource<Level>(
() => ({
url: `/levels/${levelKey()}.json`,
method: "GET",
headers: {
accept: "application/json",
},
params: {
levelId: levelKey(),
},
reportProgress: false,
body: null,
transferCache: false,
withCredentials: false,
}),
{ defaultValue: initLevel }
);
}
This HttpResourceRequest can also be represented by a lambda expression, which the httpResource uses to construct a calculated signal internally.
It is important to note that although the httpResource offers the option to specify HTTP methods (HTTP verbs) beyond GET and a body that is transferred as a payload, it is only intended for retrieving data. These options allow you to integrate web APIs that do not adhere to the semantics of HTTP verbs. By default, the httpResource converts the passed body to JSON.
With the reportProgress option, the caller can request information about the progress of the current operation. This is useful when downloading large files. I will discuss this in more detail below.
Analyzing and validating the received data
By default, the httpResource expects data in the form of JSON that matches the specified type parameter. In addition, a type assertion is used to ensure that TypeScript assumes the presence of correct types. However, it is possible to intervene in this process to provide custom logic for validating the received raw value and converting it to the desired type. To do this, the caller defines a function using the map property in the options object (Listing 4).
Listing 4:
getLevelResourceAlternative(levelKey: () => string) {
return httpResource<Level>(() => `/levels/${levelKey()}.json`, {
defaultValue: initLevel,
map: (raw) => {
return toLevel(raw);
},
});
}
The httpResource converts the received JSON into an object of type unknown and passes it to map. In our example, a simple self-written function toLevel is used. In addition, map also allows the integration of libraries such as Zod, which performs schema validation.
Loading data other than JSON
By default, httpResource expects a JSON document, which it converts into a JavaScript object. However, it also offers other methods that provide other forms of representation:
- httpResource.text returns text
- httpResource.blob returns the retrieved data as a blob
- httpResource.arrayBuffer returns the retrieved data as an ArrayBuffer
To demonstrate the use of these possibilities, the example discussed here requests an image with all possible tiles as a blob. From this blob, it derives the tiles required for the selected level style. Figure 3 shows a section of this tilemap and illustrates that the application can switch between the individual styles by choosing a horizontal or vertical offset.
Figure 3: Section of the tilemap used in the example (Source)
A TilesMapLoader delegates to httpResource.blob to load the tilemap (Listing 5).
Listing 5:
@Injectable({ providedIn: "root" })
export class TilesMapLoader {
getTilesMapResource(): HttpResourceRef<Blob | undefined> {
return httpResource.blob({
url: "/tiles.png",
reportProgress: true,
});
}
}
This resource also requests progress information and uses the example to display the progress information to the left of the drop-down fields.
Putting it all together: reactive flow
The httpResources described in the last sections can now be combined into the reactive graph of the application (Figure 4).
Figure 4: Reactive flow of ngMario
The signals levelKey, style, and animation represent the user input. The first two correspond to the drop-down fields at the top of the application. The animation signal contains a Boolean that indicates whether the animation was started by clicking the Toggle Animation button (see screenshots above).
The tilesResource is a classic resource that derives the individual tiles for the selected style from the tilemap. To do this, it essentially delegates to a function of the game engine, which is treated as a black box here.
The rendering is triggered by an effect, especially since we cannot draw the level directly using data binding. It draws or animates the level on a canvas, which the application retrieves as a signal-based viewChild. Angular then calls the effect whenever the level (provided by the levelResource), the style, the animation flag, or the canvas changes.
A tilesMapProgress signal uses the progress information provided by tilesMapResource to indicate how much of the tilesmap has already been downloaded. To load the list of available levels, the example uses a levelOverviewResource that is not directly connected to the reactive graph discussed so far.
Listing 6 shows the implementation of this reactive flow in the form of fields of the LevelComponent.
Listing 6:
export class LevelComponent implements OnDestroy {
private tilesMapLoader = inject(TilesMapLoader);
private levelLoader = inject(LevelLoader);
canvas = viewChild<ElementRef<HTMLCanvasElement>>("canvas");
levelKey = linkedSignal<string | undefined>(() => this.getFirstLevelKey());
style = signal<Style>("overworld");
animation = signal(false);
tilesMapResource = this.tilesMapLoader.getTilesMapResource();
levelResource = this.levelLoader.getLevelResource(this.levelKey);
levelOverviewResource = this.levelLoader.getLevelOverviewResource();
tilesResource = createTilesResource(this.tilesMapResource, this.style);
tilesMapProgress = computed(() =>
calcProgress(this.tilesMapResource.progress())
);
constructor() {
[...]
effect(() => {
this.render();
});
}
reload() {
this.tilesMapResource.reload();
this.levelResource.reload();
}
private getFirstLevelKey(): string | undefined {
return this.levelOverviewResource.value()?.levels?.[0]?.levelKey;
}
[...]
}
Using a linkedSignal for the levelKey allows us to use the first level as the default value as soon as the list of levels has been loaded. The getFirstLevelKey helper returns this from the levelOverviewResource.
The effect retrieves the named values from the respective signal and passes them to the engine’s animateLevel or rederLevel function (Listing 7).
Listing 7:
private render() {
const tiles = this.tilesResource.value();
const level = this.levelResource.value();
const canvas = this.canvas()?.nativeElement;
const animation = this.animation();
if (!tiles || !canvas) {
return;
}
if (animation) {
animateLevel({
canvas,
level,
tiles,
});
} else {
renderLevel({
canvas,
level,
tiles,
});
}
}
Resources and missing parameters
The tilesResource shown in the diagram discussed is simply delegated to the asynchronous extractTiles function, which the engine also provides (Listing 8).
Listing 8:
function createTilesResource(
tilesMapResource: HttpResourceRef<Blob | undefined>,
style: () => Style
) {
const tilesMap = tilesMapResource.value();
// undefined prevents the resource from beeing triggered
const request = computed(() =>
!tilesMap
? undefined
: {
tilesMap: tilesMap,
style: style(),
}
);
return resource({
request,
loader: (params) => {
const { tilesMap, style } = params.request!;
return extractTiles(tilesMap, style);
},
});
}
This simple resource contains an interesting detail: before the tilemap is loaded, the tilesMapResource has the value undefined. However, we cannot call extractTiles without a tilesMap. The request signal takes this into account: it returns undefined if no tilesMap is available yet, so the resource does not trigger its loader.
iJS Newsletter
Keep up with JavaScript’s latest news!
Displaying Progress
The tilesMapResource was configured above to provide information about the download progress via its progress signal. A calculated signal in the LevelComponent projects it into a string for display (Listing 9).
Listing 9:
function calcProgress(progress: HttpProgressEvent | undefined): string {
if (!progress) {
return "-";
}
if (progress.total) {
const percent = Math.round((progress.loaded / progress.total) * 100);
return percent + "%";
}
const kb = Math.round(progress.loaded / 1024);
return kb + " KB";
}
If the server reports the file size, this function calculates a percentage for the portion already downloaded. Otherwise, it just returns the number of kilobytes already downloaded. There is no progress information before the download starts. In this case, only a hyphen is used.
To test this function, it makes sense to throttle the browser’s network connection in the developer console and press the reload button in the application to instruct the resources to reload the data.
Status, header, error, and more
In case the application needs the status code or the headers of the HTTP response, the httpResource provides the corresponding signals:
console.log(this.levelOverviewResource.status());
console.log(this.levelOverviewResource.statusCode());
console.log(this.levelOverviewResource.headers()?.keys());
In addition, the httpResource provides everything that is also known from ordinary resources, including an error signal that provides information about any errors that may have occurred, as well as the option to update the value that is available as a local working copy.
Conclusion
The new httpResource is another building block that complements Angular’s new signal story. It allows data to be loaded within the reactive graph. Currently, it uses the HttpClient as an implementation detail, which may eventually be replaced by another solution at a later date.
While the HTTP resource also allows data to be retrieved using HTTP verbs other than GET, it is not designed to write data back to the server. This task still needs to be done in the conventional way.